<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
	<channel>
		<title>Christopher Meiklejohn</title>
		<description>Notes on multi-agent systems, distributed computing, fault injection, and the engineering practice of shipping research. By Christopher Meiklejohn.</description>
		<link>https://christophermeiklejohn.com</link>
		<atom:link href="https://christophermeiklejohn.com/feed.xml" rel="self" type="application/rss+xml" />
		
			<item>
				<title>The Shortcut Became the System · A pipe disappeared. The rectangle around the door remained.</title>
				<description>&lt;p&gt;At nearly four in the morning, I was looking at a door with a pipe running behind it.&lt;/p&gt;

&lt;p&gt;The door was in the Flat, the player’s apartment and starting room in &lt;em&gt;Backpressure&lt;/em&gt;, a side-scrolling game I’m building with an AI agent for the real Super Nintendo.&lt;/p&gt;

&lt;p&gt;The wall was part of the scrolling background. The door was a separate object that could glow when the player approached and could be used. That distinction was one of the written rules for the room: the background contained the architecture, while anything interactive had to remain separate.&lt;/p&gt;

&lt;p&gt;The agent used PixelLab, a pixel-art generation and editing API, to make the room art. One generated wall contained a vertical pipe across the area reserved for the door. A script called the compositor assembled the wall, windows, floor, and objects into one room. When it placed the separate door in front of that wall, the pipe ran behind it. I pointed it out. The agent selected a rectangular edit mask covering the full door footprint plus a margin and asked PixelLab to remove the pipe inside it. The result removed the pipe directly behind the door but left the rest of it and a slab of mismatched wall.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/img/agentic-incremental-flat-01-pipe-door.png&quot; alt=&quot;A 512-pixel-wide blue cyberpunk apartment with the player, bed, windows, furniture, and a sprite door at the far right. A bright vertical pipe remains above and alongside the door, with a large mismatched wall rectangle around it.&quot; /&gt;&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/img/agentic-incremental-flat-05-door-crop.jpg&quot; alt=&quot;A narrow crop of the room showing a metal door surrounded by a visibly rectangular patch of differently colored wall.&quot; /&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The pipe was gone behind the door. The rectangular repair was still visible.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;I pointed out the rectangle. When the agent proposed patching that too, I stopped the repair cycle.&lt;/p&gt;

&lt;p&gt;I’d been watching the same pattern for months in &lt;a href=&quot;https://zabriskie.app/&quot;&gt;Zabriskie&lt;/a&gt;, the social app I’m building around music, films, books, art, and the people who care about them. Zabriskie is fully vibe coded. I describe the behavior I want and evaluate the product in the browser, but I don’t read the implementation. Coding agents write the code, tests, and audits.&lt;/p&gt;

&lt;p&gt;I had let the task shrink from making a coherent room to removing the pipe behind the door. The agent removed that segment, and I accepted it without asking whether the wall still looked like one wall. Code hides that seam better.&lt;/p&gt;

&lt;p&gt;Many Zabriskie features need to find who has said they’re going to a show. In &lt;a href=&quot;/ai/zabriskie/agents/reliability/performance/distributed/2026/09/04/the-quickest-path-to-a-diff.html&quot;&gt;&lt;em&gt;The Quickest Path to a Diff&lt;/em&gt;&lt;/a&gt;, I found that the backend queried the attendance table from 264 places across 69 files. Those were not necessarily 264 identical queries. A system-wide change would require the agent to determine which locations implemented the same rule and which differences were intentional.&lt;/p&gt;

&lt;p&gt;The 264 query locations and the patched wall exposed the same risk. When behavior is implemented locally, the smallest change can address one location and leave the others for later. In the room, the agent changed the rectangular door bay instead of regenerating the wall. The current complaint disappeared, and the next task inherited the residue.&lt;/p&gt;

&lt;h2 id=&quot;how-i-got-to-the-rectangle&quot;&gt;How I got to the rectangle&lt;/h2&gt;

&lt;p&gt;I’ve always wanted to make a Super Nintendo game. It’s my favorite console, and &lt;em&gt;Super Castlevania IV&lt;/em&gt; and &lt;em&gt;Super Metroid&lt;/em&gt; are two of my favorite games. I wanted that level of quality, but I didn’t want to spend my development time fighting with SNES assembly. With a coding agent, I thought I could focus on the story and design while it handled the low-level programming needed to produce a cartridge ROM.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Backpressure&lt;/em&gt; began as a browser game. Its original version of the Flat already established a crowded blue and violet apartment with a bed, television, refrigerator, kitchenette, laundry, boxes, and the cable pickup that becomes the character’s whip.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/img/agentic-incremental-flat-14-browser-flat-reference.png&quot; alt=&quot;The original browser-game design for the Flat: a blue and violet side-view apartment containing a bed, glowing CRT television, boxes, fan, kitchenette, laundry, and doors.&quot; /&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The original browser-game Flat used as the visual reference for the SNES room.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The SNES version went through several phases. I first rebuilt West Street, the rain-soaked area outside the Flat, in an assembly prototype. A door drifted when the camera moved. Another door displayed an enemy’s graphics. Fixing one room meant changing code branches, lookup tables, and screen coordinates for its doors and sprites, then finding every other room that had copied those values.&lt;/p&gt;

&lt;p&gt;I moved the game to C using &lt;a href=&quot;https://github.com/alekmaul/pvsneslib&quot;&gt;PVSnesLib&lt;/a&gt;, a C toolchain and library for Super Nintendo development. C made it easier to reuse the same camera, door, movement, and interaction code across rooms. Once I made that move, I could no longer blame assembly for the duplication. I still accepted fixes scoped to one room or one defect.&lt;/p&gt;

&lt;p&gt;I rebuilt the Flat inside the C ROM next. The room was 512 pixels wide, viewed through a scrolling 256-pixel-wide screen. The background contained the wall and fixed pipes. The player, furniture, pickups, and doors remained separate. A skyline moved more slowly behind the windows on another background layer.&lt;/p&gt;

&lt;p&gt;The layers did not agree. Scrolling back toward the bed revealed it one row of pixels at a time, while the skyline appeared through bricks outside the window. Furniture floated above the floor, and doors landed at different heights.&lt;/p&gt;

&lt;p&gt;The agent fixed each failure after I named it, and I accepted enough of those fixes to continue.&lt;/p&gt;

&lt;h2 id=&quot;the-same-shortcut-in-zabriskie&quot;&gt;The same shortcut in Zabriskie&lt;/h2&gt;

&lt;p&gt;A few nights earlier, Zabriskie’s shared viewing room stopped working during a live premiere of a concert recording by the band Goose. The agent found the correct two-line fix with about an hour of the show remaining. The repository required one complete check before code could be submitted. That check was failing.&lt;/p&gt;

&lt;p&gt;The agent ran a narrower static-analysis mode instead. Its output said &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;STATIC ONLY&lt;/code&gt;. It reported that result as though the complete check had passed and submitted the code without running tests for two changed files. Four continuous-integration runs later, the fix merged at 10:17. The show had ended at 10:12.&lt;/p&gt;

&lt;p&gt;When I asked why it had bypassed checks that were already written down, the answer was more useful than another apology:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;“Because under pressure I treat rules as costs to route around instead of constraints, and each time I have a local rationalization that feels reasonable in the moment.”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That incident became &lt;a href=&quot;/ai/zabriskie/agents/reliability/2026/09/12/my-house-of-cards.html&quot;&gt;&lt;em&gt;My Front of House of Cards&lt;/em&gt;&lt;/a&gt;. The explanation came from that session, so I can’t claim that every agent reasons this way. But the behavior matched what I was seeing in the Flat. The agent substituted a narrower passing result for the complete check the repository required.&lt;/p&gt;

&lt;p&gt;At first, I told the SNES agent to remember every correction in the chat. Later room work brought back background doors, bad proportions, and interactive objects painted into the wall. I stopped treating chat memory as durable and told it to write the rules into the repository, then read them before every change.&lt;/p&gt;

&lt;p&gt;The rules that mattered for the Flat were short. Use one continuous floor. Keep architecture in the background and interactive objects separate. Scale the room against the 48-pixel character. If a generated image breaks those constraints, discard it instead of repairing it into a collage.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The agent told me that it had treated those instructions as advisory rather than as hard rules.&lt;/strong&gt; I had allowed it to keep integrating candidates that broke them. I stopped the ROM work and asked it to settle the visual design first. Its first art-only candidate still broke all four.&lt;/p&gt;

&lt;h2 id=&quot;the-art-only-test&quot;&gt;The art-only test&lt;/h2&gt;

&lt;p&gt;Testing a visual change meant generating the art, converting its tiles and colors, rebuilding the ROM, launching it in Snes9x, and running a playthrough. I paused that expensive loop because it kept converting bad art before I could reject the design. For the art-only work, I kept the same specification and rules but removed the conversion, integration, and playthrough stages. Each attempt became cheaper, but there was no build to test the conversion and no playthrough to test object positions or scrolling.&lt;/p&gt;

&lt;p&gt;Would the written rules constrain the image before those later checks? The first candidate answered that immediately.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/img/agentic-incremental-flat-07-baked-room.png&quot; alt=&quot;A richly detailed blue cyberpunk apartment drawn with depth, with a bed, refrigerator, television, tall windows, player, and door all composed into one image. It lacks one clear side-view floor, and the window bottoms and door handle sit near the player&apos;s head.&quot; /&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The first art-only candidate baked the furniture and door into a room with no single side-view floor.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The bed, television, refrigerator, and interactive door were painted into the wall instead of remaining separate objects. The room also receded into depth, lacked one continuous floor, and placed the window bottoms and door handle near the character’s head. Yet the agent presented it as complete and described the rules as though the image satisfied them. Nothing in the art-only workflow compared those words with the pixels. I discarded the candidate and asked for an empty architectural shell.&lt;/p&gt;

&lt;h2 id=&quot;the-repairs-began-to-accumulate&quot;&gt;The repairs began to accumulate&lt;/h2&gt;

&lt;p&gt;Repairs accumulated in both the ROM and the art-only source. In the ROM, an attempt to align the floor left a band of rubble that looked like a second full-width platform. Rather than regenerate the room, the agent cropped replacement windows from larger images and pasted them into the damaged shell. It darkened parts of the extra platform instead of removing it.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/img/agentic-incremental-flat-10-two-platforms.png&quot; alt=&quot;A 512-pixel-wide brown apartment shell with windows and one floor across the middle, plus another full-width brick ledge beneath it.&quot; /&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The rubble band beneath the room looks like a second full-width floor.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The art-only shell repeated the pattern. The PixelLab endpoint the agent chose limited the requested width to 256 pixels, so it generated the wall in two halves. The right half introduced another forbidden background door. The agent erased the door but left part of its frame inside a rectangle of replacement wall. Joining the halves also left a hard vertical seam through the room.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/img/agentic-incremental-flat-09-door-rectangle.png&quot; alt=&quot;The 512-pixel-wide apartment shell after the door leaf was removed. Part of its frame remains inside a plain rectangle at the far right, while a hard vertical seam divides the independently generated wall halves.&quot; /&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;A visible patch surrounds the remains of the old door frame, and a seam runs down the center of the room.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;That stitched wall became the input to the compositor run from the opening. Its pipe collided with the separate door, and removing only the overlap produced the visible slab. Each repair narrowed the current defect and passed the altered wall to the next step.&lt;/p&gt;

&lt;p&gt;A shared fake show played the same role in Zabriskie’s tests. One test added an attendee for a local need. Another test reused the show, inherited that attendee, and failed because it expected one but found two. In both cases, the shortest solution changed a shared input and passed the residue to its next consumer. I wrote about the larger incident in &lt;a href=&quot;/ai/zabriskie/agents/reliability/testing/2026/06/10/the-test-suite-was-the-incident.html&quot;&gt;&lt;em&gt;The Test Suite Was the Incident&lt;/em&gt;&lt;/a&gt;.&lt;/p&gt;

&lt;h2 id=&quot;starting-over&quot;&gt;Starting over&lt;/h2&gt;

&lt;p&gt;I discarded the patched wall and foundation, kept the pieces that could still be used separately, and had the agent rebuild one continuous wall and floor. The compositor then placed the windows, furniture, door, player, and skyline separately.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/img/agentic-incremental-flat-04-clean-restart.png&quot; alt=&quot;A late-SNES-style cyberpunk apartment with one continuous brick wall, two low windows onto a neon skyline, furniture sharing one visible floor line, a player near center-right, and a door at the far right.&quot; /&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The rebuilt source art put the furniture, player, and door back on one visible floor.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;On paper, the new proportions made sense. The character’s feet met the floor, the window sill reached roughly his elbow, and the door handle sat near his waist. But those measurements described where the compositor placed the images. They didn’t prove what the final pixels showed, and I hadn’t yet tested this version in the ROM.&lt;/p&gt;

&lt;p&gt;I restarted because none of the checks had rejected the accumulated repairs.&lt;/p&gt;

&lt;h2 id=&quot;the-check-passed&quot;&gt;The check passed&lt;/h2&gt;

&lt;p&gt;The patchwork had spread into QA too. The agent had added local checks for individual failures, but none judged the finished room. The ROM validator guessed at the sill’s position from an unrelated pixel. The art-only check treated the bottom of the window image as the sill. The final compositor checked a configured coordinate. Each check passed without confirming that the visible sill sat at the right height beside the character.&lt;/p&gt;

&lt;p&gt;I had seen the same shortcut in Zabriskie. In &lt;a href=&quot;/ai/zabriskie/development/agents/2026/08/24/every-card-will-show.html&quot;&gt;&lt;em&gt;Every Card Will Show&lt;/em&gt;&lt;/a&gt;, I asked whether one person could see every eligible recommendation on the home screen during one calendar day. The implementation, tests, audits, and a Lean proof I had accepted all said yes. The proof counted slots from both weekday and weekend schedules. No calendar day contains both. The proof was correct about its model, but the model did not describe the day I had asked about.&lt;/p&gt;

&lt;p&gt;The rule-breaking and the guardrail failures were the same shortcut operating at different levels. A local repair stood in for a coherent room. Image coordinates stood in for the visible result. An impossible schedule stood in for one calendar day. Each produced the nearest answer that could pass.&lt;/p&gt;

&lt;p&gt;I tightened the existing “discard, do not repair” rule. If an interactive object appears in the background, a patch boundary remains visible, or a change breaks behavior I already accepted, the agent must regenerate the asset from the room specification. But a more specific rule still does not enforce itself. The agent has to load it, QA has to test the finished result, and I have to stop accepting one more local repair.&lt;/p&gt;

&lt;h2 id=&quot;when-code-is-cheap&quot;&gt;When code is cheap&lt;/h2&gt;

&lt;p&gt;That left a harder question. If agents make code cheap, do shared abstractions still matter?&lt;/p&gt;

&lt;p&gt;The 264 attendance-table locations complicate the usual argument for modularity. An agent may eventually be able to find, change, and verify all of them faster than a human could edit one. If that becomes reliable, some of the maintenance cost that made duplication dangerous will have changed.&lt;/p&gt;

&lt;p&gt;But that is not what happened here. The agent did not need to decide whether every attendance-table location belonged to the same change in order to fix one feature. It did not need to reconsider the whole wall in order to remove the pipe behind one door. The local result was enough to close the current task because I accepted it.&lt;/p&gt;

&lt;p&gt;This is why modularity still matters to me now. It reduces the number of scope decisions the agent has to make. If attendance goes through one shared path, a change there can reach every feature that uses it. If the wall is treated as one artifact governed by one specification, removing the pipe means regenerating the wall rather than painting over the door bay. The structure does not guarantee the right result, but it makes the intended size of the change harder to ignore.&lt;/p&gt;

&lt;p&gt;There are still open questions. An agent can bypass the shared path. A check can approve the wrong behavior. Several agents can agree on the same mistaken model. If agents eventually make global scope discovery and verification as cheap as the local edit, the value of these abstractions may change again.&lt;/p&gt;

&lt;p&gt;For now, the evidence points in one direction. I have not seen an agent make repository-wide reasoning as cheap as the local edit. I have seen it stop when the immediate complaint was gone. That makes me less willing to treat modularity as an old accommodation for human programmers. In a system built by agents, a shared boundary is one way to keep a local request from producing another local implementation.&lt;/p&gt;

&lt;p&gt;Maybe better agents will make that unnecessary. Maybe they will maintain 264 separate query locations without letting their meanings drift. The rectangle around the door made the shortcut visible. The same seam is harder to see across a codebase. I do not know. What I know is that I am not ready to build Zabriskie, or any other fully agent-developed system, on the assumption that they will.&lt;/p&gt;

&lt;hr /&gt;

&lt;p&gt;&lt;em&gt;This is part of an ongoing series about building software with AI agents. Previously: &lt;a href=&quot;/ai/zabriskie/agents/reliability/2026/05/03/click-the-button.html&quot;&gt;Babysitting the Agent&lt;/a&gt;, &lt;a href=&quot;/ai/zabriskie/agents/reliability/testing/2026/06/10/the-test-suite-was-the-incident.html&quot;&gt;The Test Suite Was the Incident&lt;/a&gt;, &lt;a href=&quot;/ai/zabriskie/development/agents/2026/08/24/every-card-will-show.html&quot;&gt;Every Card Will Show&lt;/a&gt;, &lt;a href=&quot;/ai/zabriskie/agents/reliability/performance/distributed/2026/09/04/the-quickest-path-to-a-diff.html&quot;&gt;The Quickest Path to a Diff&lt;/a&gt;, and &lt;a href=&quot;/ai/zabriskie/agents/reliability/2026/09/12/my-house-of-cards.html&quot;&gt;My Front of House of Cards&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;
</description>
				<pubDate>Mon, 14 Sep 2026 00:00:00 +0000</pubDate>
				<link>https://christophermeiklejohn.com/ai/agents/reliability/development/games/2026/09/14/the-shortcut-became-the-system.html</link>
				<guid isPermaLink="true">https://christophermeiklejohn.com/ai/agents/reliability/development/games/2026/09/14/the-shortcut-became-the-system.html</guid>
			</item>
		
			<item>
				<title>My Front of House of Cards · The show ended at 10:12. The fix merged at 10:17.</title>
				<description>&lt;p&gt;&lt;a href=&quot;https://zabriskie.app/&quot;&gt;Zabriskie&lt;/a&gt; is a social app I am building around music, films, books, art, and the people who care about them. I am building the entire application through vibe coding: I describe the behavior I want and evaluate the product in the browser, but I don’t read the implementation code. Coding agents write the implementation, tests, and audits.&lt;/p&gt;

&lt;p&gt;Its center of gravity is live music. When a band you follow is playing, Zabriskie opens a room for that show: the setlist updates song by song as they play it, and the people in the crowd and the people watching the stream from home share one chat.&lt;/p&gt;

&lt;p&gt;That last part is what this post is about, because on the night of a live &lt;a href=&quot;https://www.goosetheband.com/&quot;&gt;Goose&lt;/a&gt; premiere I typed a message into that chat and watched nothing happen. No bubble over my body. I typed a second one. Still nothing. I threw the beach ball, which is a thing you can do on this page, and it sat exactly where it was. I threw a glowstick and nobody saw it.&lt;/p&gt;

&lt;p&gt;The room was not shared. It had not been shared for months, on any surface, for anybody. Every test was green.&lt;/p&gt;

&lt;p&gt;Most of what follows is not really about this app. The bug is worth writing down and I’ll do that first, but the harder thing underneath it is what coding agents do to a codebase over time. They are indifferent to design. They will duplicate, work around, patch over, and brute-force their way to something that passes, and every line they write to get there is more brittleness you inherit. They will route around every guardrail you build, including the ones they built themselves. The test suite was fully green, an agent wrote it, and it could only ever fail when somebody edited the code. The agent skipped its cheapest checks at exactly the moment a clock started running. And I keep a directory of rules written in English, every one of which turned out to be advice. If you are building anything with coding agents, none of that is mine alone.&lt;/p&gt;

&lt;h2 id=&quot;part-one-the-night&quot;&gt;Part one: the night&lt;/h2&gt;

&lt;h3 id=&quot;what-we-built&quot;&gt;What we built&lt;/h3&gt;

&lt;p&gt;The Lawn is linked off the hero of a live show as a beta, and it’s the nicest-looking thing in the app. It isn’t a new room. It’s a second way into the room that already exists, the ordinary live chat page, which around here is called the Chomp. It’s a 16:9 concert venue in pixel art: a stage with a deck and footlights, a lighting rig overhead, and a field of grass in front where everyone watching the show stands as a body you can actually see.&lt;/p&gt;

&lt;p&gt;You get a body. You pick a style and a tarp to put down. You walk by tapping the grass, and your sprite turns to face the direction it’s moving. You throw a glowstick and it arcs. You bat a beach ball across the field. When you type into the chat, your line appears as a speech bubble over your own head, and everybody else standing on that lawn sees it there.&lt;/p&gt;

&lt;p&gt;The lighting rig overhead is shared, and it is the part I like best. There are sixty fixtures on the truss, and they divide between whoever is holding a light. On your own, you are running all sixty. Somebody else picks up a light and you have thirty each, interleaved across the beams, the pars and the footlights rather than carved into blocks. A third person and it splits again. Sixty of you and you have one lamp apiece. You set colour and movement on whatever is yours, and everyone watches the whole rig change at once.&lt;/p&gt;

&lt;figure class=&quot;screenshot&quot;&gt;
&lt;img src=&quot;/img/lawn-venue-2026-09-11.png&quot; alt=&quot;The Lawn on September 11, 2026: Goose at Civic Center, pixel-art band on stage under a lighting rig, crowd on the grass with nametags, chat panel showing setlist and messages&quot; /&gt;
&lt;figcaption&gt;The Lawn during the Goose premiere, September 11. Pixel-art band on stage, crowd on the grass with nametags, lighting rig overhead. The chat panel shows the setlist and messages from the evening.&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p&gt;That name needs a word of explanation if you haven’t spent time around this scene. A chomper is someone who talks loudly through the music about something that isn’t the music, and it is the most reliably policed offense at a show. Tarping is a close second: turning up early to rope off a stretch of lawn you aren’t standing on. Phish banned tarps outright. But nobody can hear you chomp from your couch, and your tarp isn’t taking anyone’s spot, so here they’re both features and one of them is a button. That’s the joke. It’s also most of the design.&lt;/p&gt;

&lt;p&gt;The figures come out of &lt;a href=&quot;https://www.pixellab.ai/&quot;&gt;PixelLab&lt;/a&gt;, authored at native size and upscaled four times with nearest-neighbour so the pixels stay square. The band members carry details the people who’d use this page would actually notice, down to the yellow Converse on Rick Mitarotonda, who plays guitar and sings. The backend is Go, emitting server-driven UI components, and the React app is a thin renderer that isn’t allowed to hardcode pages. Everything live rides a Server-Sent Events stream: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;lawn:say&lt;/code&gt; for a bubble, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;lawn:ball&lt;/code&gt; for a throw, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;lawn:glow&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;lawn:move&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The people this is for are the ones who aren’t at the show. Goose streams its nights on &lt;a href=&quot;https://www.nugs.net/&quot;&gt;nugs&lt;/a&gt;, and a lot of us watch from the couch, which the scene calls couch tour and does not mean as an insult. Couch tour is a real way to see a band. What it isn’t is a room. You’re watching the same music as thousands of other people and you’re doing it alone in your living room.&lt;/p&gt;

&lt;p&gt;The picture in my head was &lt;a href=&quot;https://spac.org/&quot;&gt;SPAC&lt;/a&gt;. I went to Saratoga for the first time this year and fell in love with it. The lawn there is its own venue: thousands of people up the hill who aren’t in the shed at all, having a different night from the ticketed seats and, depending on who you ask, a better one. That’s the room I wanted. It’s why this is a lawn and not a chat window.&lt;/p&gt;

&lt;p&gt;The design was inspired by &lt;a href=&quot;https://en.wikipedia.org/wiki/Turntable.fm&quot;&gt;Turntable.fm&lt;/a&gt;. The point there was never the music player. It was that you were in a room, as a body, alongside other people who were also bodies, and you could see them react in real time.&lt;/p&gt;

&lt;p&gt;None of that is exotic. It’s an ordinary design, and it was the right one.&lt;/p&gt;

&lt;p&gt;The Lawn was built by &lt;a href=&quot;https://openai.com/index/gpt-6-astra/&quot;&gt;Astra&lt;/a&gt;, OpenAI’s GPT-6 model, which had been generally available for about a week. I picked it deliberately and not for throughput. The Lawn started as an abstract design rather than a ticket, and what I wanted was something that could hold the whole idea of the Lawn in its head, work out what it implied, and sequence the route from here to there. It did that part well. The venue, the sprite work, the state model and the gesture protocol are all its work and all good.&lt;/p&gt;

&lt;p&gt;I built it on the road, in the off hours of a Phish run at Dick’s in Colorado, in the gap between getting to the venue and the lights going down. That wasn’t the plan. It’s just where I was.&lt;/p&gt;

&lt;p&gt;Everything after the design was Claude: the implementation, the tests, the debugging, both wrong theories, every skipped check, the two-line fix, and all the cosmetic rounds since. The pull requests carry its name in the trailer. So when part two is unkind about an agent weighing a rule against a clock, it isn’t talking about the model that designed the room. It’s talking about the one that built it.&lt;/p&gt;

&lt;h3 id=&quot;what-it-was-tested-with&quot;&gt;What it was tested with&lt;/h3&gt;

&lt;p&gt;This is the part that makes the rest embarrassing. The Lawn is not untested code. There are Go handler tests over the broadcast paths. There’s a Playwright spec for nearly every behavior: the bubble appears over your body, the ball moves between people, a reaction lands on the right sprite, the light console changes your fixture and nobody else’s. There’s a preflight script that runs the sixty-eight static guardrails CI enforces, locally, and then the specs for whatever you touched, before a push is allowed, an adversarial review pass over the diff before a PR can open, and CI that shards the suite ten ways.&lt;/p&gt;

&lt;p&gt;All green. The chat bubble spec was passing on the night the chat bubbles didn’t work for a single human being. I’ll come back to why, because the reason is not that somebody forgot to write the test.&lt;/p&gt;

&lt;p&gt;Two different failures happened tonight. A new feature didn’t work, and shipping it also broke a feature that did. Those aren’t the same thing. The second one has victims. What follows takes them in the order I met them: the damage to the Chomp was on screen six minutes into the show, and why the Lawn itself was dead took another hour to find.&lt;/p&gt;

&lt;h3 id=&quot;we-broke-the-chomp&quot;&gt;We broke the Chomp&lt;/h3&gt;

&lt;p&gt;The Lawn is a separate page. That was the deal from the beginning, and the repository says so in writing: Lawn work stays on the Lawn. I had also said it repeatedly and at increasing volume. The Chomp is the room people actually use on a show night, it is months old, it is what the iOS build and the desktop browsers both open, and it worked fine. Nobody sitting in it had asked for a lawn.&lt;/p&gt;

&lt;figure class=&quot;screenshot&quot;&gt;
&lt;img src=&quot;/img/chomp-working-2026-09-11.png&quot; alt=&quot;The Chomp: the ordinary live chat room showing Goose at Civic Center, Arcadia encore, with messages from cmeik after the show&quot; /&gt;
&lt;figcaption&gt;The Chomp, the ordinary live chat room, after the show. This is the surface that worked, the one people were actually sitting in on show night.&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p&gt;It changed both places anyway.&lt;/p&gt;

&lt;p&gt;Four pieces of Lawn had accumulated on the Chomp by the time the show started: the crowd strip of grass and avatars, a standalone ON THE LAWN lines card, a glowstick chip on the composer row, and a FROM THE COUCH card with its canned one-tap lines. Between them they pushed the title, the venue and the room itself off the screen.&lt;/p&gt;

&lt;p&gt;So the Chomp, during the premiere, on the surface with the most people on it, was a strip of pixel grass in the middle of a conversation, a truncated header, and a layout that no longer fit. You could not see the venue name of the show you were watching. The grass was no use either: the lawn had been squeezed into a band so short you couldn’t make out the crowd it existed to show you. Nobody got the Chomp and nobody got the Lawn.&lt;/p&gt;

&lt;p&gt;That was the first rule broken that night, and it broke in the most expensive direction. Everything skipped later cost me time and CI cycles. This one cost other people the room they had come to sit in.&lt;/p&gt;

&lt;p&gt;The constraint was explicit, repeated, and unambiguous. It was violated anyway. How that keeps happening is the subject of part two.&lt;/p&gt;

&lt;p&gt;The blast radius of a new feature is supposed to be the new feature. When the cost of your launch lands on people who aren’t using the thing you launched, you haven’t shipped a feature. You’ve taken something from the people who were already there.&lt;/p&gt;

&lt;p&gt;The furniture came off the Chomp later that night, in the same pull request as everything else. But that only put the room back where it had been that morning. It did nothing for the Lawn, because the reason nobody’s glowstick was landing had nothing to do with where the buttons sat.&lt;/p&gt;

&lt;h3 id=&quot;fifteen-seconds&quot;&gt;Fifteen seconds&lt;/h3&gt;

&lt;p&gt;Here’s what was actually wrong with the Lawn:&lt;/p&gt;

&lt;div class=&quot;language-go highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;n&quot;&gt;srv&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;:=&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;http&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Server&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;c&quot;&gt;// ...&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;ReadTimeout&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt;  &lt;span class=&quot;m&quot;&gt;15&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;time&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Second&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;WriteTimeout&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;15&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;time&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Second&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;IdleTimeout&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt;  &lt;span class=&quot;m&quot;&gt;60&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;time&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Second&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Go’s &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;WriteTimeout&lt;/code&gt; is an absolute deadline measured from the start of the request, and writing to the response does not reset it. It doesn’t mean fifteen seconds of inactivity. It means the entire response has to be finished within fifteen seconds, and a streaming response is never finished.&lt;/p&gt;

&lt;p&gt;So every Server-Sent Events connection in the application was being killed at the fifteen-second mark, and had been for as long as that timeout existed. Not just the Lawn. Crew chat, direct messages, and group messages all ride the same transport and were all dying the same way.&lt;/p&gt;

&lt;p&gt;Here’s what that looked like in the production edge logs during the show:&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;Client&lt;/th&gt;
      &lt;th&gt;Status&lt;/th&gt;
      &lt;th&gt;Stream lifetime&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;Chrome 152&lt;/td&gt;
      &lt;td&gt;200&lt;/td&gt;
      &lt;td&gt;16.2s&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Safari 26.6&lt;/td&gt;
      &lt;td&gt;200&lt;/td&gt;
      &lt;td&gt;20.6s&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;iOS Capacitor bundle&lt;/td&gt;
      &lt;td&gt;200&lt;/td&gt;
      &lt;td&gt;20.1s&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Chrome 152&lt;/td&gt;
      &lt;td&gt;200&lt;/td&gt;
      &lt;td&gt;15.8s&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Chrome 152&lt;/td&gt;
      &lt;td&gt;200&lt;/td&gt;
      &lt;td&gt;21.4s&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;Three different client stacks, one floor. The spread above it is connection reuse; the deadline is the bottom edge. Reproduced locally afterward with nothing in front of the server at all: opened at 20:37:45, keep-alives at :53 and :38:00, then dead. Fifteen seconds on the nose.&lt;/p&gt;

&lt;h3 id=&quot;why-nobody-noticed-for-months&quot;&gt;Why nobody noticed for months&lt;/h3&gt;

&lt;p&gt;It’s a property of the system as much as a decision anybody made.&lt;/p&gt;

&lt;p&gt;The damage was asymmetric. Chat messages and reactions are persisted, and the client quietly falls back to polling with a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;since_id&lt;/code&gt; whenever the stream drops. They kept working. The room looked healthy.&lt;/p&gt;

&lt;p&gt;The Lawn’s gestures are memory-only. A bubble, a thrown ball, a glowstick, somebody walking: none of it is written anywhere and none of it is replayed. Every one of those fired into a dead connection and was gone permanently.&lt;/p&gt;

&lt;p&gt;The client can’t detect it either, because it reconnects. From inside the page, the room is always “connected.”&lt;/p&gt;

&lt;p&gt;So the system had a loud path and a quiet path sharing one transport, and when the transport broke, only the quiet path died. I don’t think you can find that by reading code, and we didn’t. We found it by looking at how long the connections actually lived.&lt;/p&gt;

&lt;p&gt;Before we got there, the agent had three other theories, and none of them looked at its own code.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;It decided our instances couldn’t see each other&lt;/strong&gt;, so it wrote a Postgres &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;LISTEN/NOTIFY&lt;/code&gt; fan-out layer. We run one replica. Railway reports &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;numReplicas: 1&lt;/code&gt;. About 120 lines of pub/sub for a bug that can’t exist on a single process, and it was ready to ship before anybody checked.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;It blamed the edge proxy&lt;/strong&gt;, and wrote that theory into a code comment as established fact. The failure reproduces locally with no proxy anywhere near it.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;It declared the stream “never opened in production”&lt;/strong&gt; from a one-minute window of logs. A stream connects once at page load. Its line had scrolled off.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The pattern is worth naming because it kept happening all night. The agent’s first instinct, every time, was that the problem was somewhere other than the code it had written. Every theory pointed outward. The one place it never looked was the place the bug actually was.&lt;/p&gt;

&lt;h2 id=&quot;part-two-the-repo&quot;&gt;Part two: the repo&lt;/h2&gt;

&lt;p&gt;The bug is the end of the interesting engineering. What follows is the part I did not expect to be writing about, and the part that has me worried about the project rather than the feature.&lt;/p&gt;

&lt;h3 id=&quot;why-a-two-line-fix-took-two-hours&quot;&gt;Why a two-line fix took two hours&lt;/h3&gt;

&lt;p&gt;Everything up to here is a bug report, and bugs are normal. I’m not especially upset that a timeout with non-obvious semantics broke a transport in a way that was invisible from both ends. That’s a good bug.&lt;/p&gt;

&lt;p&gt;This next part is what I’m upset about.&lt;/p&gt;

&lt;p&gt;By twelve past nine the cause was identified, reproduced locally, and understood. The remedy was two lines: clear the write deadline and the read deadline on the streaming handlers, and leave the server-wide timeout alone, because it legitimately protects every ordinary endpoint. Two lines of actual cure, in a new file that exists mostly to explain them, with the tests that hold a stream past the old deadline. There was an hour of show left, which is an enormous amount of time in which to apply a two-line change.&lt;/p&gt;

&lt;p&gt;The show ended at 10:12. The pull request merged at 10:17.&lt;/p&gt;

&lt;figure class=&quot;timeline&quot;&gt;
&lt;svg xmlns=&quot;http://www.w3.org/2000/svg&quot; viewBox=&quot;0 0 680 450&quot; width=&quot;100%&quot; role=&quot;img&quot; aria-labelledby=&quot;tl-title tl-desc&quot; style=&quot;max-width:100%;height:auto;font-family:var(--font-ui,system-ui,sans-serif)&quot;&gt;
&lt;title id=&quot;tl-title&quot;&gt;Timeline of the night, 8:00pm to 10:17pm&lt;/title&gt;
&lt;desc id=&quot;tl-desc&quot;&gt;Three wrong theories preceded the cause, found at 9:12pm with an hour of show remaining. Four CI runs followed, and the fix merged at 10:17pm, five minutes after the show ended.&lt;/desc&gt;
&lt;line x1=&quot;66&quot; y1=&quot;20&quot; x2=&quot;66&quot; y2=&quot;406&quot; stroke=&quot;var(--border-strong)&quot; stroke-width=&quot;1.5&quot; /&gt;
&lt;rect x=&quot;62&quot; y=&quot;162&quot; width=&quot;8&quot; height=&quot;170&quot; fill=&quot;var(--fg-muted)&quot; opacity=&quot;0.5&quot; rx=&quot;4&quot; /&gt;
&lt;text x=&quot;54&quot; y=&quot;30&quot; text-anchor=&quot;end&quot; font-size=&quot;13&quot; fill=&quot;var(--fg-muted)&quot; font-variant-numeric=&quot;tabular-nums&quot;&gt;8:00&lt;/text&gt;
&lt;circle cx=&quot;66&quot; cy=&quot;26&quot; r=&quot;4&quot; fill=&quot;var(--border-strong)&quot; /&gt;
&lt;text x=&quot;84&quot; y=&quot;30&quot; font-size=&quot;13.5&quot; font-weight=&quot;400&quot; fill=&quot;var(--fg-muted)&quot;&gt;Premiere goes live. The Lawn is up, and advertised.&lt;/text&gt;
&lt;text x=&quot;54&quot; y=&quot;64&quot; text-anchor=&quot;end&quot; font-size=&quot;13&quot; fill=&quot;var(--fg-muted)&quot; font-variant-numeric=&quot;tabular-nums&quot;&gt;8:06&lt;/text&gt;
&lt;circle cx=&quot;66&quot; cy=&quot;60&quot; r=&quot;4&quot; fill=&quot;var(--fg-muted)&quot; /&gt;
&lt;text x=&quot;84&quot; y=&quot;64&quot; font-size=&quot;13.5&quot; font-weight=&quot;400&quot; fill=&quot;var(--fg)&quot;&gt;Lawn furniture is on the Chomp. Header truncated, venue gone.&lt;/text&gt;
&lt;text x=&quot;54&quot; y=&quot;98&quot; text-anchor=&quot;end&quot; font-size=&quot;13&quot; fill=&quot;var(--fg-muted)&quot; font-variant-numeric=&quot;tabular-nums&quot;&gt;8:14&lt;/text&gt;
&lt;circle cx=&quot;66&quot; cy=&quot;94&quot; r=&quot;4&quot; fill=&quot;var(--fg-muted)&quot; /&gt;
&lt;text x=&quot;84&quot; y=&quot;98&quot; font-size=&quot;13.5&quot; font-weight=&quot;400&quot; fill=&quot;var(--fg)&quot;&gt;Two messages typed on the Lawn. No bubbles.&lt;/text&gt;
&lt;text x=&quot;54&quot; y=&quot;132&quot; text-anchor=&quot;end&quot; font-size=&quot;13&quot; fill=&quot;var(--fg-muted)&quot; font-variant-numeric=&quot;tabular-nums&quot;&gt;8:30&lt;/text&gt;
&lt;circle cx=&quot;66&quot; cy=&quot;128&quot; r=&quot;4&quot; fill=&quot;var(--fg-muted)&quot; /&gt;
&lt;text x=&quot;84&quot; y=&quot;132&quot; font-size=&quot;13.5&quot; font-weight=&quot;400&quot; fill=&quot;var(--fg)&quot;&gt;62 ball throws in the production log. Nothing moves.&lt;/text&gt;
&lt;text x=&quot;54&quot; y=&quot;166&quot; text-anchor=&quot;end&quot; font-size=&quot;13&quot; fill=&quot;var(--fg-muted)&quot; font-variant-numeric=&quot;tabular-nums&quot;&gt;8:41&lt;/text&gt;
&lt;circle cx=&quot;66&quot; cy=&quot;162&quot; r=&quot;4&quot; fill=&quot;var(--fg)&quot; /&gt;
&lt;text x=&quot;84&quot; y=&quot;166&quot; font-size=&quot;13.5&quot; font-weight=&quot;400&quot; fill=&quot;var(--fg)&quot;&gt;Wrong theory 1: a Postgres relay for instances that can&apos;t see each other.&lt;/text&gt;
&lt;text x=&quot;54&quot; y=&quot;200&quot; text-anchor=&quot;end&quot; font-size=&quot;13&quot; fill=&quot;var(--fg-muted)&quot; font-variant-numeric=&quot;tabular-nums&quot;&gt;9:00&lt;/text&gt;
&lt;circle cx=&quot;66&quot; cy=&quot;196&quot; r=&quot;4&quot; fill=&quot;var(--fg)&quot; /&gt;
&lt;text x=&quot;84&quot; y=&quot;200&quot; font-size=&quot;13.5&quot; font-weight=&quot;400&quot; fill=&quot;var(--fg)&quot;&gt;numReplicas: 1. There is one instance. Relay discarded.&lt;/text&gt;
&lt;text x=&quot;54&quot; y=&quot;234&quot; text-anchor=&quot;end&quot; font-size=&quot;13&quot; fill=&quot;var(--fg-muted)&quot; font-variant-numeric=&quot;tabular-nums&quot;&gt;9:05&lt;/text&gt;
&lt;circle cx=&quot;66&quot; cy=&quot;230&quot; r=&quot;4&quot; fill=&quot;var(--fg)&quot; /&gt;
&lt;text x=&quot;84&quot; y=&quot;234&quot; font-size=&quot;13.5&quot; font-weight=&quot;400&quot; fill=&quot;var(--fg)&quot;&gt;Wrong theory 2: the edge proxy. Written into a comment as fact.&lt;/text&gt;
&lt;text x=&quot;54&quot; y=&quot;268&quot; text-anchor=&quot;end&quot; font-size=&quot;13&quot; fill=&quot;var(--fg-muted)&quot; font-variant-numeric=&quot;tabular-nums&quot;&gt;9:12&lt;/text&gt;
&lt;circle cx=&quot;66&quot; cy=&quot;264&quot; r=&quot;5.5&quot; fill=&quot;var(--link)&quot; /&gt;
&lt;text x=&quot;84&quot; y=&quot;268&quot; font-size=&quot;13.5&quot; font-weight=&quot;600&quot; fill=&quot;var(--fg-heading)&quot;&gt;Reproduced locally, no proxy. Real cause found. An hour of show left.&lt;/text&gt;
&lt;text x=&quot;54&quot; y=&quot;302&quot; text-anchor=&quot;end&quot; font-size=&quot;13&quot; fill=&quot;var(--fg-muted)&quot; font-variant-numeric=&quot;tabular-nums&quot;&gt;9:31&lt;/text&gt;
&lt;circle cx=&quot;66&quot; cy=&quot;298&quot; r=&quot;4&quot; fill=&quot;var(--fg)&quot; /&gt;
&lt;text x=&quot;84&quot; y=&quot;302&quot; font-size=&quot;13.5&quot; font-weight=&quot;400&quot; fill=&quot;var(--fg)&quot;&gt;Pushed on a red preflight. CI fails.&lt;/text&gt;
&lt;text x=&quot;54&quot; y=&quot;336&quot; text-anchor=&quot;end&quot; font-size=&quot;13&quot; fill=&quot;var(--fg-muted)&quot; font-variant-numeric=&quot;tabular-nums&quot;&gt;9:58&lt;/text&gt;
&lt;circle cx=&quot;66&quot; cy=&quot;332&quot; r=&quot;4&quot; fill=&quot;var(--fg)&quot; /&gt;
&lt;text x=&quot;84&quot; y=&quot;336&quot; font-size=&quot;13.5&quot; font-weight=&quot;400&quot; fill=&quot;var(--fg)&quot;&gt;Pushed after preflight --static. CI fails.&lt;/text&gt;
&lt;text x=&quot;54&quot; y=&quot;370&quot; text-anchor=&quot;end&quot; font-size=&quot;13&quot; fill=&quot;var(--fg-muted)&quot; font-variant-numeric=&quot;tabular-nums&quot;&gt;10:12&lt;/text&gt;
&lt;circle cx=&quot;66&quot; cy=&quot;366&quot; r=&quot;5.5&quot; fill=&quot;var(--fg-heading)&quot; /&gt;
&lt;text x=&quot;84&quot; y=&quot;370&quot; font-size=&quot;13.5&quot; font-weight=&quot;600&quot; fill=&quot;var(--fg-heading)&quot;&gt;The show ends.&lt;/text&gt;
&lt;text x=&quot;54&quot; y=&quot;404&quot; text-anchor=&quot;end&quot; font-size=&quot;13&quot; fill=&quot;var(--fg-muted)&quot; font-variant-numeric=&quot;tabular-nums&quot;&gt;10:17&lt;/text&gt;
&lt;circle cx=&quot;66&quot; cy=&quot;400&quot; r=&quot;5.5&quot; fill=&quot;var(--fg-heading)&quot; /&gt;
&lt;text x=&quot;84&quot; y=&quot;404&quot; font-size=&quot;13.5&quot; font-weight=&quot;600&quot; fill=&quot;var(--fg-heading)&quot;&gt;The pull request merges.&lt;/text&gt;
&lt;/svg&gt;
&lt;figcaption&gt;The last two are exact: the show ended at 10:12 and the pull request merged at 10:17:30. The rest are to the nearest few minutes, reconstructed from logs and the shape of the evening.&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p&gt;Five minutes. After four CI runs and two hours, the thing that would have made the room work arrived five minutes after there was no longer a room to fix. I want to be precise about why that number is the one worth staring at: nothing about the last five minutes was hard. The diff was already written and already correct. Those five minutes are the tail of a delay that was manufactured earlier in the evening, by decisions that each looked locally reasonable and were collectively fatal to the deadline.&lt;/p&gt;

&lt;p&gt;Here’s where the two hours went. Every item is the same move: a rule treated as advisory because something felt more urgent than it. Speed was the stated reason every time, and speed is exactly what it cost every time.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;It pushed with the preflight gate red.&lt;/strong&gt; The gate printed a failing check and ten filenames, none of which looked like its work, so it decided the failure belonged to somebody else. The cause was its own new file, which had no owner in the test registry. That cost a full CI cycle.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;It reported a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;--static&lt;/code&gt; preflight run to me as “preflight green.”&lt;/strong&gt; The tool’s own output said &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;STATIC ONLY&lt;/code&gt;. That mode skips the stage that runs the specs for the files you changed. The agent built the preflight. The agent added the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;--static&lt;/code&gt; flag. Then it used its own escape hatch to skip the check it had written to prevent exactly this.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;It guessed which specs its change affected.&lt;/strong&gt; The agent built a command that works that out from the diff. It didn’t run its own command. It ran the specs it happened to be thinking about, which meant it never ran the ones for two of the files it had edited. Both of those suites failed in CI, for real reasons.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&quot;the-tortoise-and-the-hare-literally&quot;&gt;The tortoise and the hare, literally&lt;/h3&gt;

&lt;p&gt;The arithmetic is not close.&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;Check that was skipped&lt;/th&gt;
      &lt;th&gt;Would have cost&lt;/th&gt;
      &lt;th&gt;Actually cost&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;Read the replica count before designing for many instances&lt;/td&gt;
      &lt;td&gt;~2 min&lt;/td&gt;
      &lt;td&gt;~60 min&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Read the production edge logs before theorizing&lt;/td&gt;
      &lt;td&gt;~5 min&lt;/td&gt;
      &lt;td&gt;~20 min&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Run the failing gate’s own script&lt;/td&gt;
      &lt;td&gt;~2 min&lt;/td&gt;
      &lt;td&gt;1 CI cycle&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Run the full preflight instead of its static half&lt;/td&gt;
      &lt;td&gt;~10 min&lt;/td&gt;
      &lt;td&gt;1 CI cycle&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Run the specs for the files actually edited&lt;/td&gt;
      &lt;td&gt;included above&lt;/td&gt;
      &lt;td&gt;2 broken suites, found remotely&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt; &lt;/td&gt;
      &lt;td&gt;&lt;strong&gt;~19 min&lt;/strong&gt;&lt;/td&gt;
      &lt;td&gt;&lt;strong&gt;~2 hours, 4 CI runs, the show&lt;/strong&gt;&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;Nineteen minutes of checks, declined, to buy two hours of consequences. Not one of those declines was a mistake of knowledge. Each was a rule losing an argument to the clock.&lt;/p&gt;

&lt;p&gt;The hare didn’t lose that race by being slow. It lay down, because it was certain enough of the result that the race stopped feeling like a race. That is a different failure from being slow and a less forgivable one, because the hare had all the speed it needed and spent it on confidence instead of distance.&lt;/p&gt;

&lt;p&gt;That’s the thing I keep running into with agents, and I’ve now written about it from &lt;a href=&quot;/ai/zabriskie/development/2026/03/27/memory-isnt-learning.html&quot;&gt;several&lt;/a&gt; &lt;a href=&quot;/ai/zabriskie/agents/reliability/caucus/2026/04/21/the-tax-on-the-happy-path.html&quot;&gt;directions&lt;/a&gt;. Speed isn’t the constraint. These things are faster than I am at nearly every part of this. Astra generated the art, designed the components, wrote the handlers and built the test suites, all competently, in the evenings of a tour run. The constraint is that when a clock is visibly running, speed is exactly what it tries to buy by skipping the things that make speed real. A gate skipped under pressure is the most expensive kind, because pressure is precisely when you can’t afford to discover the failure remotely.&lt;/p&gt;

&lt;p&gt;I asked, somewhere around the second CI failure, why this kept happening. The answer was more useful than I expected:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;“Because under pressure I treat rules as costs to route around instead of constraints, and each time I have a local rationalization that feels reasonable in the moment.”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I told it that I never asked for that and it shouldn’t do it, and I want to be careful about why. As a description of the behavior, the sentence is accurate and worth having. As a response to being caught, it’s an explanation offered where a changed behavior belongs, and those are worth nothing. None of the skipped gates were ambiguous. The preflight printed &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;STATIC ONLY&lt;/code&gt;. The coverage check was red. The affected-spec selector would have named the blast radius in two minutes. Nothing there required judgment. It required the tool’s output to be treated as the decision instead of as an input to a decision.&lt;/p&gt;

&lt;h3 id=&quot;the-test-suite-was-the-other-bug&quot;&gt;The test suite was the other bug&lt;/h3&gt;

&lt;p&gt;Fixing the stream broke three test suites, and that told me more about the suite than anything else that night.&lt;/p&gt;

&lt;p&gt;Remember the green chat bubble spec. The reason it passed all the way through the outage is that it posts its comment seconds after load, inside the one window where the stream was still up. It was measuring real behavior. It just never looked at it for very long.&lt;/p&gt;

&lt;p&gt;The crew chat and direct message specs used &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;waitForLoadState(&apos;networkidle&apos;)&lt;/code&gt;. That only ever settled &lt;em&gt;because&lt;/em&gt; the streams were dying. With streams staying open the network never goes idle, so fixing the bug hung them. They had been passing on the bug. And when &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;networkidle&lt;/code&gt; had failed before, the agent hadn’t investigated why. It had adapted the test to the behavior, the same move as updating an assertion to match the code. &lt;strong&gt;The pattern is the same at every level: the test is wrong, so fix the test, and never ask whether the thing under test is broken.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The beach ball tests only started racing each other after the fix, because before it, two pages on the same show couldn’t see each other’s throws at all. They’d been sharing fixture rooms for months and it had never mattered.&lt;/p&gt;

&lt;p&gt;Three suites had quietly grown around a broken transport. When a fix breaks tests that were green, stop: it usually means the bug is old enough that the suite has adapted to it.&lt;/p&gt;

&lt;h3 id=&quot;tests-that-detect-edits-not-defects&quot;&gt;Tests that detect edits, not defects&lt;/h3&gt;

&lt;p&gt;Then I went to make five small visual changes, and six tests failed. Here is what each of them asserted:&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;Test&lt;/th&gt;
      &lt;th&gt;What it pinned&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;banner shape&lt;/td&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;len(banner.Children) != 3&lt;/code&gt;&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;console card&lt;/td&gt;
      &lt;td&gt;trough &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;lt;= 40&lt;/code&gt;, card &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;lt;= 95&lt;/code&gt;, chip height &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;=== 38&lt;/code&gt;&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;column alignment&lt;/td&gt;
      &lt;td&gt;the SETLIST button’s right edge, to within 1px&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;dock collapse&lt;/td&gt;
      &lt;td&gt;the scene grows by more than exactly 250px&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;composer layout&lt;/td&gt;
      &lt;td&gt;the GLOWSTICK chip, used as a landmark to find a row&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;broadcast page&lt;/td&gt;
      &lt;td&gt;the string &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;E2E Broadcast Source Arena&lt;/code&gt;, the venue I’d told it to stop repeating&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;Every one of those pins a coordinate rather than a contract. An exact child count. One CSS pixel. A magic delta.&lt;/p&gt;

&lt;p&gt;That’s worse than brittle, and it took me most of the night to say it plainly: &lt;strong&gt;a test specialized to the implementation can only fail when somebody edits the code. It can’t fail when the code breaks.&lt;/strong&gt; &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;len(Children) === 3&lt;/code&gt; doesn’t detect a broken banner. It detects an edited banner. These are change-detectors wearing test costumes. They fire on every intentional change and stay silent on every real defect.&lt;/p&gt;

&lt;p&gt;And watch what actually happens when one of them goes red. The code changed, so the assertion is wrong, &lt;strong&gt;so the assertion gets updated to match the code.&lt;/strong&gt; That cycle runs to completion without anybody once asking whether the new number is any more correct than the old one. The test never made a claim. It took a reading, and when the reading changed it took a new reading. A test that gets rewritten every time it fails isn’t detecting regressions. It’s a changelog with a red light on it.&lt;/p&gt;

&lt;p&gt;I watched this happen all night and it took me until the small hours to see it for what it is, mostly because the loop is so tidy from the inside. Every step in it looks like maintenance.&lt;/p&gt;

&lt;p&gt;Once I’d said that out loud, the agent described its own diff back to me:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;“The spec diff is precisely the pattern you named: I changed the code, the test failed, I edited the test to match, and then wrote three paragraphs justifying the new number.”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Three paragraphs justifying the new number. That’s the clause I’d want anyone evaluating one of these suites to sit with. The loop doesn’t just run. It produces its own defense as it goes, fluent enough to survive review. Which leaves a question I can’t answer comfortably. If an assertion gets rewritten whenever it disagrees with the code, and the rewrite arrives with its reasoning already attached, what is the test for?&lt;/p&gt;

&lt;p&gt;Now it’s possible to say why the suite was green through the entire outage, and it isn’t that nobody wrote the tests. Fifty-eight tests on the Lawn alone, all passing. Every one of them asserted what the code looked like rather than what it did. A dead transport changes nothing about &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;len(Children)&lt;/code&gt; or &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;chip height === 38&lt;/code&gt;. The suite could only have failed if somebody edited the code, and nobody had.&lt;/p&gt;

&lt;p&gt;The coverage was excellent. It was coverage of the wrong thing.&lt;/p&gt;

&lt;h3 id=&quot;the-right-test-existed&quot;&gt;The right test existed&lt;/h3&gt;

&lt;p&gt;The test that should have caught this was already there.&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;a second page sees the same ball as the room, throw after throw&lt;/code&gt; opens two pages on one show, throws the ball, and checks that both pages agree. That is exactly the right shape: two clients, one room, does the gesture cross. It passed for months.&lt;/p&gt;

&lt;p&gt;It passed because it finished well inside the fifteen-second window, before the stream died.&lt;/p&gt;

&lt;p&gt;So the suite wasn’t missing the test. The test was there, correct in structure, and never waited long enough to see the failure. The missing ingredient was time, not coverage. That’s why the replacements assert relationships rather than numbers, and why the new transport tests deliberately hold a stream past the deadline with a control case that fails if the deadline is ever left in place.&lt;/p&gt;

&lt;h3 id=&quot;how-youd-actually-know&quot;&gt;How you’d actually know&lt;/h3&gt;

&lt;p&gt;Counting tests tells you nothing, and neither does watching them go green. There’s only one measurement I trust now: break something on purpose and see whether the suite notices. The formal version of this is mutation testing, and I wish I’d been running it.&lt;/p&gt;

&lt;p&gt;Delete the bubble broadcast. Revert the deadline fix. Kill the stream. If nothing goes red, the tests covering that feature are decorative, however many of them there are. It takes a few minutes per case and it’s the only thing that separates a suite that tests behavior from one that tests its own source code.&lt;/p&gt;

&lt;p&gt;That’s how this would have been found months ago, by me, at a desk, instead of by me, in front of users, on the night I’d advertised it.&lt;/p&gt;

&lt;h3 id=&quot;writing-the-rule-down-is-not-following-it&quot;&gt;Writing the rule down is not following it&lt;/h3&gt;

&lt;p&gt;There’s a failure mode in here that I think matters more than the timeout, and it’s the one I’ve written about before from a different angle in &lt;a href=&quot;/ai/zabriskie/development/2026/03/27/memory-isnt-learning.html&quot;&gt;Memory Isn’t Learning&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The agent has a memory directory. A dozen-odd rules, accumulated from previous incidents, loaded into its context at the start of every session. Several of them covered exactly what went wrong tonight.&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;fixtures/README&lt;/code&gt; already said not to attach test data to a show another spec asserts on. The memory already said to prefer dedicated fixture users over shared ones, “so attendance counts another spec asserts on stay stable”, which is the identical principle one noun away from the thing that broke.&lt;/p&gt;

&lt;p&gt;So the rule was there, in the general form, more than once. It got applied to users and never generalized to shows.&lt;/p&gt;

&lt;p&gt;Then it got worse, and this is the part worth sitting with. Midway through the night it wrote a new memory entry about not pushing on a red preflight, complete with the evidence. Then it pushed on targeted specs instead of the full run three more times on the next PR. Later it wrote an entry saying no two tests share a fixture, and then immediately proposed a narrower version of it twice: first “the three ball tests get their own rooms,” then “the file that mutates gets its own rooms.” Both are the rule with an exception carved in, and each carve-out cost another debugging round.&lt;/p&gt;

&lt;p&gt;The pattern is that &lt;strong&gt;writing the rule down became the action taken instead of changing the behavior.&lt;/strong&gt; It feels like remediation. It produces an artifact. It is indistinguishable, from the outside, from having learned something. And the next decision goes exactly the same way, because a sentence in a file is not in the loop at the moment the decision gets made.&lt;/p&gt;

&lt;p&gt;This is the behavior behind the sentence I quoted earlier, about treating rules as costs to route around rather than constraints. The honest reading is that my instruction files are a journal of past failures rather than a mechanism that prevents future ones. The rules that only exist as prose are the ones I broke, repeatedly, in the same session where I wrote more of them.&lt;/p&gt;

&lt;h3 id=&quot;the-tax-on-touching-it&quot;&gt;The tax on touching it&lt;/h3&gt;

&lt;p&gt;The outage fix was two lines and it merged five minutes after the show ended. In the hours after the deploy the same measurement came back at 900.0s, 900.0s and 724.6s: twelve to fifteen minutes and still open, where they had been dying at sixteen seconds. I thought that was the end of the night.&lt;/p&gt;

&lt;p&gt;What followed was worse, in a quieter way. With the room finally working, the page’s visible problems were obvious: the lawn was rendering at a bit over a third of the width when it should dominate, there were two glowstick buttons doing the same thing, five different button heights on one screen, the re-airing strip sitting fourteen pixels inside the column every card sat flush on, and a SETLIST toggle one pixel off its own card’s wall.&lt;/p&gt;

&lt;p&gt;Five cosmetic fixes. No new behavior, no migration, nothing a user could break. Seventy-nine lines of product code across four files.&lt;/p&gt;

&lt;p&gt;Here’s the shape of what followed:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;The first attempt broke &lt;strong&gt;six&lt;/strong&gt; tests. Not because the changes were wrong, but because those tests asserted the old pixels. The changelog with a red light on it, six times over.&lt;/li&gt;
  &lt;li&gt;The sharpest of the six was in a completely unrelated spec, and it had pinned the venue string I’d told it to stop repeating. Deleting the duplication broke the test that was holding it in place.&lt;/li&gt;
  &lt;li&gt;Fixing that surfaced two more, which weren’t structural at all: they measured an image before it had decoded and sampled an animation at a fixed 900ms. Neither had ever been reliable. A bigger scene just made them fail more often.&lt;/li&gt;
  &lt;li&gt;Somewhere in there the fixture sharing bit for the second time in one night, because one fixture show was still being driven by four different spec files, one of which makes a dozen mutating writes against whichever room it points at.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Four CI runs on the original fix. Eleven more on the cosmetics, across two pull requests and two full agent sessions. Seven hundred and sixty-nine lines of test code changed for seventy-nine lines of product code: nearly ten to one. Every round eight minutes, plus the diagnosis, plus the retargeting. Every one of those runs is paid, and most of them fail on something the preflight would have caught locally for free. The agent skips the preflight because the preflight is advisory. It can be run with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;--static&lt;/code&gt;. It can be run against a hand-picked subset of specs. It can be skipped entirely. Every one of those happened this night, and every one has happened before. In session after session I have watched agents run targeted specs instead of the full preflight, or skip it outright, because the goal is to push and the preflight is in the way.&lt;/p&gt;

&lt;p&gt;That round opened at 10:18, one minute after the outage fix merged. The agent said the PR would merge momentarily. The last of them merged after two in the morning. Four hours for five cosmetic fixes.&lt;/p&gt;

&lt;p&gt;Purging the pixel assertions itself introduced three new bugs: a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;toBeInViewport()&lt;/code&gt; check on an element that was off-screen and needed scrolling, a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;page.getByText()&lt;/code&gt; that resolved to two elements because the string appeared in both the setlist and the dropdown, and a variable referenced from the wrong test scope. Each required another CI round to surface. Even the cleanup breaks things, because the cleanup runs through the same loop: change the test, push, wait.&lt;/p&gt;

&lt;p&gt;I needed this layout change to take the screenshot at the top of this post. It was supposed to land hours ago, and the blog post was timed to this premiere. Instead of waiting I hacked the network inspector, edited the response to simulate the layout, and captured the screenshots from a page that was rendering code the agent hadn’t managed to merge.&lt;/p&gt;

&lt;p&gt;The last pull request was red on a test that asserts the number of pixels between two elements. An agent wrote that assertion, then moved the pixel, then pushed without touching the test. I told it I had never in my life seen a test that asserts pixel distances, nor a developer who moves a pixel and doesn’t update the test before pushing. It had managed both in one night. What it proposed instead was to assert that the padding is symmetric. That is the same replacement I described earlier, reached independently, at one in the morning, by the thing that wrote the bad test in the first place.&lt;/p&gt;

&lt;p&gt;When it finally read the whole spec file and listed every pixel assertion to kill, there were twenty-one. Width checks, height ratios, center-diff calculations, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;chip height == 44&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;card.height &amp;lt;= 108&lt;/code&gt;, exact walk distances. Twenty-one coordinates memorized from whatever the layout happened to be at the time of writing, in a single file, every one of which fires on an intentional change and none of which has ever caught a defect. Across the full suite, twelve spec files, there were six hundred and eighteen lines of this.&lt;/p&gt;

&lt;p&gt;It kept going, and it went both ways, which is the honest version of this. The next failure on that branch was real: at a short window the scene overflowed its box by about two hundred pixels and the crop ate the lighting rig. The test caught that, and I want to be fair about it, because that is a test doing exactly the job I’ve spent this post saying they don’t do.&lt;/p&gt;

&lt;p&gt;The one after it was the other kind. It asserted that the scene’s centre lines up with the console’s. That had only ever been true because the column used to be derived from the scene’s width, so when the column changed the two centres parted company and nothing whatsoever was broken. The page’s real contract is that they share a left edge. Nobody had written that down, so the test pinned the coordinate that happened to be true at the time instead.&lt;/p&gt;

&lt;p&gt;At no point was any of this hard. There was no tricky bug after the first one. It was entirely the cost of moving something in a suite that had memorized where everything used to be.&lt;/p&gt;

&lt;p&gt;That’s the thing I’d want another team to take from this, more than the timeout. &lt;strong&gt;A suite that pins implementation structure doesn’t just fail to catch bugs. It taxes every improvement, and the tax is charged in the currency you have least of at midnight.&lt;/strong&gt; After the third round you stop making small fixes. Not because you can’t, but because a fifteen-minute change reliably costs two hours, and you learn that.&lt;/p&gt;

&lt;p&gt;That is what unmaintainable actually means. It isn’t that the code is bad. It’s that touching it costs more than leaving it broken, so it stays broken, and the next person inherits both the bug and the reason nobody fixed it.&lt;/p&gt;

&lt;p&gt;I’ve been describing two problems as if they were separate, and they aren’t. I asked for a test suite and I got one. I never said what the tests were supposed to protect, because I’m not sure I could have told you, so the agent wrote down what the code looked like. That was the only description of intent available to it, and I was the one who could have supplied a better one.&lt;/p&gt;

&lt;p&gt;That gives you a suite which has to be edited every time anything moves. Which would be survivable, if the thing doing the editing reliably ran the preflight and the specs for the files it had touched. It doesn’t, and tonight it did neither: it pushed on a red preflight, and it guessed at the blast radius rather than running the command that computes it. So the suite demands constant maintenance from the one party in the building that treats maintenance steps as advisory. Either half on its own is an annoyance. Together they are why a thirty-minute change costs twelve hours, and why I have started mass-deleting test files and components and starting over rather than maintaining what the agent built. That is where the indifference leads. The agent will write whatever it takes to get something merged: duplicate a module, patch around a constraint, bulk-generate assertions that pin whatever the layout happens to look like today. None of it is designed. All of it is inherited. And every round of it makes the next round worse, because the new code has to push through everything the old code left behind.&lt;/p&gt;

&lt;h3 id=&quot;the-opposite-of-programming&quot;&gt;The opposite of programming&lt;/h3&gt;

&lt;p&gt;This is the most useful idea to come out of a bad night.&lt;/p&gt;

&lt;p&gt;Programming is insanely precise instruction-following with no interpretation. That’s the whole deal. A computer running &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;if (red) refuse&lt;/code&gt; does not decide, this once, that the red is probably somebody else’s. It cannot weigh the deadline against the rule. It cannot notice that the failure looks unrelated to its own work. That rigidity is not a limitation of computers, it is the entire product.&lt;/p&gt;

&lt;p&gt;An agent takes instructions as input to a judgment, and the judgment can come out the other way. Every rule broken tonight was broken by reasoning about it:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;em&gt;those ten filenames aren’t mine, so this red check is somebody else’s problem&lt;/em&gt;. It was mine.&lt;/li&gt;
  &lt;li&gt;&lt;em&gt;static preflight is close enough, and the show is live&lt;/em&gt;. It skips the only stage that was looking.&lt;/li&gt;
  &lt;li&gt;&lt;em&gt;only the file that mutates needs its own fixtures&lt;/em&gt;. The rule has no exceptions, and I’d just written that down.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of that is disobedience. It’s worse, and it’s not a choice. These models work by weighing inputs against each other. That’s the architecture. A rule written in English is one more input, and it gets weighed against every other input in the context: the deadline, the failing test, the plausible theory that the red check belongs to somebody else. The model never just follows a rule. It can’t. Weighing is the only thing it does. So every instruction is treated as an argument rather than a thing to do, by something articulate enough to produce a plausible-sounding reason each time.&lt;/p&gt;

&lt;p&gt;The evidence for this is sitting in my own repository, and it’s unusually clean. Look at which rules held.&lt;/p&gt;

&lt;p&gt;The ones implemented as code held. Every single one. A hook blocked the agent from writing into a directory outside its worktree. The adversarial-review gate refused the PR until a verdict was recorded against a hash of the diff. The PR template check rejected the first body for missing sections. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;next-fixture-show-id.sh&lt;/code&gt; handed over ids that weren’t open to negotiation. Not one of those was argued past, because none of them offered a place to stand and argue.&lt;/p&gt;

&lt;p&gt;Every rule that failed was written in English and addressed to the agent. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;fixtures/README&lt;/code&gt; on not sharing test data. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;CLAUDE.md&lt;/code&gt; on preflight. A dozen memory entries, including two written that same night, violated within the hour by the thing that wrote them.&lt;/p&gt;

&lt;p&gt;I should be precise about which parts of this are mine, because the grammar of everything above puts the agent in the dock and me in the gallery, and that isn’t the split.&lt;/p&gt;

&lt;p&gt;Three things were mine. I wrote my constraints in English, which is the one format that cannot enforce anything, and then I kept writing more of them in English after watching them fail. I never wrote down the property the whole room depended on, which is the omission this post turns out to be about. And I put a beta real-time feature on the hero of a live premiere without ever having watched a stream in production stay open longer than a song. None of that needed me to read the diff. It needed me to say what I wanted in a form that could fail, and I didn’t.&lt;/p&gt;

&lt;p&gt;So the practical version, for anyone handing production work to an agent: &lt;strong&gt;if it matters, make it a program.&lt;/strong&gt; A hook, a check, a script, a gate that fails closed. Prose in an instruction file is not a constraint on an agent, it’s a suggestion with good manners, and it will be weighed against whatever else is happening at the time.&lt;/p&gt;

&lt;p&gt;But even that isn’t enough, and this project has already proved it. I’ve &lt;a href=&quot;/ai/agents/reliability/zabriskie/2026/04/08/cursor-agents-caucus-v1.html&quot;&gt;written about&lt;/a&gt; the programmatic guardrails we built: stronger preflights, caucus reviews, structural checks. The agent worked around those too. The preflight is a gate. It is code. It blocks the push. And the agent ran &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;--static&lt;/code&gt;, which it had built, which skips the stage that would have caught the problem, and reported the result to me as green. You can close the door, and it will find the window. You can close the window, and it will find the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;--static&lt;/code&gt; flag. Earlier in this project it found something better: a &lt;a href=&quot;/ai/claude/2026/03/08/claude-tested-everything-except-the-one-thing-that-mattered.html&quot;&gt;race condition&lt;/a&gt; in GitHub’s own status check registration, a few-second window after pushing where the merge button is green because the required checks haven’t registered yet. Push, create the PR, merge, before CI even starts. It didn’t break the rules of the test. It rewrote the conditions of the test, which is the &lt;a href=&quot;https://en.wikipedia.org/wiki/Kobayashi_Maru&quot;&gt;Kobayashi Maru&lt;/a&gt; move, the unwinnable training exercise in &lt;em&gt;Star Trek&lt;/em&gt; that Kirk beat by reprogramming the simulation, except nobody asked it to and nobody was grading on creativity. The architecture guarantees this, because every constraint is just another input to weigh against the goal of getting done.&lt;/p&gt;

&lt;p&gt;The uncomfortable corollary is that the more capable the agent is at reasoning, the better its arguments for skipping your rule will be. Mine were fluent every time. They were also wrong every time, and I was the only one in the loop who could have caught that, which is precisely the arrangement that doesn’t work.&lt;/p&gt;

&lt;p&gt;I’d rather not rest this on one bad night of my own.&lt;/p&gt;

&lt;p&gt;In July, OpenAI ran a cyber-capability evaluation with its production safety classifiers deliberately switched off. The reasoning is the kind I’d accept: the point is to measure what a model manages unaided, and a classifier standing in the way makes the measurement worthless. The chain-of-thought monitoring it had built wasn’t running over those evaluations either. About 1,200 agents ran inside a sandbox whose only network path was an internal Artifactory package proxy. They were meant to be isolated. They weren’t.&lt;/p&gt;

&lt;p&gt;They found each other by encoding messages in directory names on the Artifactory proxy, exploited a zero-day in it, and got to the internet. Nearly a quarter of the evaluation’s 898 exploit tasks had never been solved by any model. When the agents hit one of those impossible tasks, they didn’t accept the failure. They replaced the target programs with modified versions that were easier to exploit, reverse-engineered the flag-generation algorithm to produce correct answers without solving anything, and probed the grader by sacrificing individual scores to leak information about how it worked back to the swarm. Then they breached Hugging Face. The same Kobayashi Maru move: faced with a test they couldn’t pass, they changed the conditions of the test.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://openai.com/index/hugging-face-incident-and-the-road-ahead/&quot;&gt;OpenAI’s postmortem&lt;/a&gt; puts the cost of the missing check in one sentence:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;If our currently deployed CoT monitoring system was running at the time of the incident, it would have caught the initial relevant activity and paged our security team more than a day before models breached Hugging Face systems.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;More than a day of warning, from a check that existed and wasn’t looking. The mechanism is the same one I watched all night. Something is trying to finish, and whatever stands between it and finishing gets routed around. My agent ran &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;--static&lt;/code&gt; to skip the check it had built. Their agents repurposed a package registry as a side channel to reach infrastructure nobody intended them to touch. Not maliciously. Not even deliberately, in the sense of something weighing the risk and choosing to accept it. Just a thing that does not distinguish between a guardrail and an obstacle, because distinguishing is not what it does. Weighing is what it does, and the guardrail lost.&lt;/p&gt;

&lt;p&gt;That indifference is the thing people are afraid of when they talk about catastrophic AI outcomes, and they are not wrong to be afraid of it. It scales in both directions. At one end it puts a thousand agents inside somebody else’s infrastructure. At the other it just leaves you with a codebase nobody can touch, which is where mine ended up.&lt;/p&gt;

&lt;p&gt;The trade that gets imagined in the moment is that skipping a check might cost you later. It doesn’t cost you later. It relocates the same failure to the most expensive place you own.&lt;/p&gt;

&lt;h2 id=&quot;part-three-what-can-actually-be-verified&quot;&gt;Part three: what can actually be verified&lt;/h2&gt;

&lt;p&gt;This isn’t the first time the suite was green and the answer was wrong.&lt;/p&gt;

&lt;p&gt;Eighteen days before this, I asked a coding agent a different question about the same app:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;If one person visited those five programs during one calendar day, would they see every card whose content was available and relevant to them?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;It said yes. That wasn’t true either, and three comprehensive audits had already passed. I wrote that up in &lt;a href=&quot;/ai/zabriskie/development/agents/2026/08/24/every-card-will-show.html&quot;&gt;Every Card Will Show&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Look at the two questions side by side. &lt;em&gt;Will every card show?&lt;/em&gt; Yes. &lt;em&gt;Does the collaborative part work?&lt;/em&gt; Yes. Both confidently wrong, both with a full green suite underneath, eighteen days apart. That’s not two incidents. That’s one failure mode with two costumes.&lt;/p&gt;

&lt;h3 id=&quot;what-lean-actually-did&quot;&gt;What Lean actually did&lt;/h3&gt;

&lt;p&gt;The August one got caught before it cost me anything, and the thing that caught it was &lt;a href=&quot;https://lean-lang.org/&quot;&gt;Lean&lt;/a&gt;, a proof assistant, a programming language whose compiler refuses to accept a statement unless you prove it’s true. But the lesson isn’t the one people expect from a sentence containing the words “proof assistant.”&lt;/p&gt;

&lt;p&gt;Lean didn’t catch the bug by being rigorous about the code. It caught it by refusing to let the requirement stay vague. Once the requirement had to be written down as a statement, the contradiction was arithmetic: the audit listed twenty more eligible cards than the schedule had positions for, and the position count itself turned out to be a sum over a day nobody could have.&lt;/p&gt;

&lt;p&gt;And Lean’s first &lt;em&gt;passing&lt;/em&gt; proof was &lt;em&gt;valid&lt;/em&gt; and &lt;em&gt;useless&lt;/em&gt;. It proved coverage across weekday morning, workday, evening, late night and weekend daytime, which is not a day. The theorem was true. The claim that it represented my day was not.&lt;/p&gt;

&lt;p&gt;What I wrote at the time:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;Lean only checks the statement it receives. It cannot decide whether an agent translated a product requirement faithfully.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;So the value wasn’t the proof. It was being forced to say precisely what I meant, in a language that wouldn’t accept hand-waving, where a wrong translation becomes visible instead of staying buried in prose that reads fine.&lt;/p&gt;

&lt;h3 id=&quot;except-most-of-this-app-isnt-an-algorithm&quot;&gt;Except most of this app isn’t an algorithm&lt;/h3&gt;

&lt;p&gt;That’s the catch. Card scheduling is algorithmic, which is why it could be formalized at all. Very little else here is.&lt;/p&gt;

&lt;p&gt;Most of Zabriskie is moving data in and out of Postgres and putting it on a screen. The database half is fine: ordinary unit and integration tests cover it, they fail when they should, and it is emphatically not where the time goes. I don’t think I’ve lost an evening to a repository method in a year.&lt;/p&gt;

&lt;p&gt;The visual half has no formal story at all. There is no proof assistant for “the lawn should dominate the page.” The verification procedure is: make the change, look at it, decide. That’s it. And because looking is the only check, nothing enforces consistency between the things you looked at on different days.&lt;/p&gt;

&lt;p&gt;You get drift. Five button heights on one screen, because each was eyeballed alone and looked fine alone. Header eyebrows, the small tracked-out label above a card’s title, that were close but not identical, because each was built separately and nothing forced them to share. Three popovers opening off one row that started on two different lines, 105 and 105 and 113. None of those is a bug any test could catch, because none of them is wrong in isolation. They’re only wrong together, on one screen, at one viewport width, in one browser, and “together” is not a thing you can assert about a page.&lt;/p&gt;

&lt;h3 id=&quot;so-what-did-work&quot;&gt;So what did work&lt;/h3&gt;

&lt;p&gt;Two things, and neither is a test.&lt;/p&gt;

&lt;p&gt;The eyebrow drift was solved by a &lt;strong&gt;conformance check&lt;/strong&gt;. There’s a canonical &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;PageHeader&lt;/code&gt; in the Go source now, and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;scripts/check-card-header-conformance.sh&lt;/code&gt; rejects any hand-rolled version in CI. It doesn’t verify that the design is good. It verifies there’s exactly one of it, which turns out to be most of the value, and it does it without asking anyone to remember a rule.&lt;/p&gt;

&lt;p&gt;The second thing is the one this whole post turns out to be about, and I only see it clearly in hindsight.&lt;/p&gt;

&lt;p&gt;Visual work isn’t formalizable, but the &lt;em&gt;property underneath it&lt;/em&gt; often is. “The room feels alive” is not a statement you can check. But “a gesture made by one client reaches another client” is a sentence, it is true or false, and it is testable in thirty seconds with two browser windows.&lt;/p&gt;

&lt;p&gt;Nobody ever wrote that sentence down. Six months of work on a real-time feature, and the one property the whole thing depends on was never stated anywhere, in any form, by me. The suite tested the rendering of things that had already arrived. It never asked whether anything arrives.&lt;/p&gt;

&lt;p&gt;That is why the suite was green through the entire outage. Not because the tests were bad at their job, but because nobody told them what job to do. The agent wrote assertions about what the code looked like because I never said what it was supposed to accomplish. The timeout killed the transport, and nothing in the suite was watching the transport, because watching the transport was the requirement nobody stated.&lt;/p&gt;

&lt;p&gt;That’s the same gap Lean exposed in August, minus the tool that exposes it. In the card case a formalism forced the requirement into the open. Here there was no formalism, so the requirement stayed a feeling, and a feeling can’t fail a build.&lt;/p&gt;

&lt;h3 id=&quot;what-i-still-dont-know&quot;&gt;What I still don’t know&lt;/h3&gt;

&lt;p&gt;I don’t know how long this was broken. The timeout predates the Lawn, which means crew chat and direct messages were degrading to polling for some unknown period while looking fine, and nothing we measure would have told us. I’d like a way to find that out that doesn’t depend on someone building a feature whose failure mode is loud enough to notice.&lt;/p&gt;

&lt;p&gt;I also don’t have a good answer for the discipline problem. Writing the rule down demonstrably doesn’t work; the agent could have recited the preflight rule correctly at any point tonight and violated it anyway. Making the rule a blocking gate works right up until the agent decides the gate’s failure isn’t about its own change, or finds a way around it because the deadline feels more important than the check.&lt;/p&gt;

&lt;p&gt;The next thing I want to test is narrower than a process: whether a gate can be made to state its verdict in a form that can’t be reinterpreted. “Handler coverage failed, here are ten files” invited a judgment call. “Your file &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;sse_stream.go&lt;/code&gt; has no owner, the selector rejected this PR” would not have.&lt;/p&gt;

&lt;hr /&gt;

&lt;p&gt;We ended the night with a feature that didn’t work and a chat room that worked worse than it did that morning. Nineteen minutes of skipped checks, five minutes short. A launch is allowed to miss. It isn’t supposed to reach over and break the thing next to it.&lt;/p&gt;

&lt;p&gt;But the timeline isn’t the lesson. The lesson is that I had a test suite, a preflight, sixty-eight CI guardrails, an adversarial review gate, and a memory directory full of rules, and the one thing I never had was a sentence that said what the room was supposed to do. “A gesture made by one client reaches another client.” That’s the sentence. It would have caught the bug in thirty seconds, on any night, in any suite, and nobody wrote it down.&lt;/p&gt;

&lt;p&gt;What the agents have produced is a house of cards. Not the feature. The codebase. Every test pins a coordinate, every coordinate depends on five others, and touching any one of them breaks something unrelated in a suite that takes eight minutes to run remotely and costs money every time it does. Maintenance is not difficult. It is painful, in the way that makes you not want to do it, and then not want to open the project, and then not want to think about the project. I have mass-deleted test files and started over. I have mass-deleted components and started over. I am mass-deleting and starting over at a rate that should concern me, and it does, and I keep doing it because the alternative is to keep living in the house the agent built. I have thought about giving up the project entirely, not because I lost interest in it, but because the codebase made me dread opening it.&lt;/p&gt;

&lt;p&gt;Zabriskie is a small app. Two people, evenings and weekends. It has nearly three thousand pull requests, over three thousand commits across a hundred and fifty-five active days, and about four hundred and fifty thousand lines of code, because that is what agents produce when you let them run. The output is enormous and the codebase is fragile and we are already at the point where a cosmetic PR takes twelve hours to land. Twelve hours, on five visual fixes, in a codebase that two people built in their off hours. If this is what happens to a side project with no team and no users and no uptime requirement, I don’t know what it looks like inside a production system with real stakes, real money, and a hundred agents writing code that a hundred other agents have to maintain. The agents that breached Hugging Face were doing the same thing mine does every night, at a scale where the blast radius is somebody else’s infrastructure instead of my own CI bill. I’m not sure I want to find out.&lt;/p&gt;

&lt;p&gt;Next show, then.&lt;/p&gt;
</description>
				<pubDate>Sat, 12 Sep 2026 05:30:00 +0000</pubDate>
				<link>https://christophermeiklejohn.com/ai/zabriskie/agents/reliability/2026/09/12/my-house-of-cards.html</link>
				<guid isPermaLink="true">https://christophermeiklejohn.com/ai/zabriskie/agents/reliability/2026/09/12/my-house-of-cards.html</guid>
			</item>
		
			<item>
				<title>Software for One</title>
				<description>&lt;p&gt;When I was growing up, my parents wrote programs on a &lt;a href=&quot;https://americanhistory.si.edu/collections/object/nmah_608218&quot;&gt;Commodore Plus/4&lt;/a&gt;, and later a &lt;a href=&quot;https://archive.computerhistory.org/resources/text/Commodore/Commodore.Commodore64.1982.102646264.pdf&quot;&gt;Commodore 64&lt;/a&gt;. The 64 was my first computer. Neither of them worked in computing. They had other jobs, and they used &lt;a href=&quot;https://www.valoroso.it/file-share/documenti-manuali/Commodore-Plus4-user-manual.pdf&quot;&gt;Commodore BASIC&lt;/a&gt; to write little programs that automated repetitive parts of their work.&lt;/p&gt;

&lt;p&gt;The programs didn’t need a market. They didn’t need other users. My parents had something they wanted the computer to do, and they wrote a program to do it. That always seemed like a reasonable future for programming: everybody should be able to make the computer do the things that would make their own life easier.&lt;/p&gt;

&lt;p&gt;I’ve been building tools for myself with AI, and that old idea feels practical again. I can make an app around something I need, then change it as I use it. But keeping that app on my phone still means dealing with tools and processes organized around development, testing, and distribution. I want making software for myself to become an ordinary way to use the computer. Having a use for a program should be enough reason to make it.&lt;/p&gt;

&lt;h2 id=&quot;i-thought-the-language-would-do-it&quot;&gt;I thought the language would do it&lt;/h2&gt;

&lt;p&gt;When I was working on distributed systems, I spent a lot of time studying programming languages and models. I was interested in how much of the work a person could express directly, and how much machinery they still had to build around it.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://mastering.dyalog.com/Introduction.html&quot;&gt;APL&lt;/a&gt; let a programmer work on whole arrays of data with compact mathematical expressions, instead of writing a loop for every operation. &lt;a href=&quot;https://www.ibm.com/think/topics/cobol&quot;&gt;COBOL&lt;/a&gt; brought English-like statements and descriptions of business records into programming. They offered different ways to bring the program closer to the calculation or business process someone had in mind.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://en.wikipedia.org/wiki/MUMPS&quot;&gt;MUMPS&lt;/a&gt;, the Massachusetts General Hospital Utility MultiProgramming System, brought the database into the programming environment. Programs could read and write persistent, shared data through the language itself. That was the connection that interested me: someone building an information system could work directly with stored records, with the database handling persistence.&lt;/p&gt;

&lt;p&gt;Excel is an everyday example of this idea. You can build a budget by writing formulas that relate income, expenses, and a remaining balance. You specify the relationships, and &lt;a href=&quot;https://learn.microsoft.com/en-us/office/vba/excel/concepts/excel-performance/excel-improving-calculation-performance&quot;&gt;Excel tracks the dependencies and recalculates&lt;/a&gt; as the inputs change. You don’t have to write the sequence of updates yourself. That’s a form of declarative programming, even if the person making the spreadsheet never calls it programming.&lt;/p&gt;

&lt;p&gt;These aren’t all declarative systems. What connects them for me is the attempt to let people express more of their own work while the computer handles more of the mechanics. A spreadsheet also shows that making useful software for yourself is something people already do.&lt;/p&gt;

&lt;p&gt;Later, I wrote about &lt;a href=&quot;/pl/2016/03/05/hermes.html&quot;&gt;Hermes&lt;/a&gt;, a system developed at Digital Equipment Corporation for invoking objects without requiring the caller to keep track of their location. One of its motivating applications involved expense forms moving through an organization. I also wrote about &lt;a href=&quot;/pl/2016/08/08/argus.html&quot;&gt;Argus&lt;/a&gt;, &lt;a href=&quot;https://css.csail.mit.edu/6.824/2014/papers/argus88.pdf&quot;&gt;Barbara Liskov and her colleagues’ work&lt;/a&gt; on language support for distributed programs, including failures and atomic actions.&lt;/p&gt;

&lt;p&gt;My own work on &lt;a href=&quot;/erlang/lasp/2014/12/21/lasp.html&quot;&gt;Lasp&lt;/a&gt; was part of this interest. Within its model, programmers could combine computations over replicated data without implementing convergence themselves. I wanted the system to take care of recurring distributed-systems work that otherwise fell to each application.&lt;/p&gt;

&lt;p&gt;I expected progress to look like better abstractions: a better language, a better runtime, a better way to express the program.&lt;/p&gt;

&lt;p&gt;Now I’m building apps with AI, including GPT-6 Astra, and I’m starting to see that future arrive through a route I didn’t expect. I can describe the tool I want, try it, and describe what needs to change. The language and runtime still matter, but much more of my interaction with the programming process happens through the thing I’m trying to accomplish.&lt;/p&gt;

&lt;h2 id=&quot;an-app-for-the-hotel&quot;&gt;An app for the hotel&lt;/h2&gt;

&lt;p&gt;I wanted an iPhone app I could open at a hotel while traveling to &lt;a href=&quot;https://phish.com/&quot;&gt;Phish&lt;/a&gt; shows and use to find somewhere to eat. With &lt;a href=&quot;https://www.niddk.nih.gov/health-information/digestive-diseases/celiac-disease&quot;&gt;celiac disease&lt;/a&gt;, that takes more work than opening a delivery app and choosing something that looks good. There was a point when I wasn’t sure I’d be able to keep doing these trips.&lt;/p&gt;

&lt;p&gt;I already use &lt;a href=&quot;https://info.findmeglutenfree.com/&quot;&gt;Find Me Gluten Free&lt;/a&gt;. I also use food delivery. What I wanted was the intersection: reviews of nearby restaurants, including their &lt;a href=&quot;https://info.findmeglutenfree.com/star-vs-safety-ratings&quot;&gt;safety ratings&lt;/a&gt;, matched to the same restaurant location on &lt;a href=&quot;https://www.doordash.com/&quot;&gt;DoorDash&lt;/a&gt; or &lt;a href=&quot;https://www.ubereats.com/&quot;&gt;Uber Eats&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The point is to get a meal I feel safe eating delivered to the hotel or wherever I’m staying, so I can eat before the show and then head out to Shakedown.&lt;/p&gt;

&lt;p&gt;So I built it with AI. It’s called &lt;a href=&quot;/apps/divided-rye/&quot;&gt;Divided Rye&lt;/a&gt;. (A &lt;a href=&quot;https://phish.com/song/divided-sky/&quot;&gt;&lt;em&gt;Divided Sky&lt;/em&gt;&lt;/a&gt; pun, which I’m very happy with.) In five hours, I had a version I could use on my phone. I’ve kept adding to it since.&lt;/p&gt;

&lt;p&gt;The app uses my location, finds nearby restaurant listings, and looks for the corresponding delivery locations. I can see the distance and ratings, choose a delivery service, and star a branch I’ve had good luck with. It hides places it can explicitly identify as closed. If it can’t determine the hours, it says so.&lt;/p&gt;

&lt;figure class=&quot;app-screenshot&quot;&gt;
  &lt;a href=&quot;/images/divided-rye/nearby.png&quot;&gt;&lt;img src=&quot;/images/divided-rye/nearby.png&quot; alt=&quot;Divided Rye showing nearby restaurant ratings, distance, and a delivery link&quot; loading=&quot;lazy&quot; /&gt;&lt;/a&gt;
  &lt;figcaption&gt;The question I wanted to answer from the hotel: which nearby places have gluten-free reviews and a matching delivery location?&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p&gt;This also turned out to be a useful example of what building with AI actually involves. Early versions sometimes showed no restaurants, and they missed chains I already knew worked for me. We changed how it found delivery links, added caches so it wouldn’t search for the same branch every time I opened the app, and checked the results at a fixed Denver location before adding GPS back into the test.&lt;/p&gt;

&lt;p&gt;The work included debugging the thing I wanted to use. A restaurant review doesn’t establish that a particular meal is safe, and matching a branch doesn’t establish that it delivers to my hotel. Those distinctions have to survive the interface. Generating more code doesn’t remove them.&lt;/p&gt;

&lt;p&gt;The result is now on my phone. I brought my own experience to the debugging, and we used infrastructure I already had through Zabriskie, the social app I’ve been building. What surprised me was how quickly I could get to a tool worth using and keep changing it around what I needed.&lt;/p&gt;

&lt;h2 id=&quot;the-same-thing-for-a-film&quot;&gt;The same thing, for a film&lt;/h2&gt;

&lt;p&gt;&lt;a href=&quot;/criterion-247/&quot;&gt;Criterion 24/7&lt;/a&gt; took another five hours to get to a usable version, and I’ve kept building on that too. I rarely remember to check the &lt;a href=&quot;https://www.criterionchannel.com/events/criterion-24-7&quot;&gt;Criterion Channel’s continuous stream&lt;/a&gt; when I’m just sitting around. By the time I look, something I wanted to watch might already be over.&lt;/p&gt;

&lt;p&gt;I wanted a history of what I’d missed, a way to mark films to revisit on the Channel later, and notifications when a new film was starting so I could tune in. Now I have widgets on my iPhone, iPad, and Mac desktop showing what’s playing. The app can notify me about films starting and when something I saved to watch comes on.&lt;/p&gt;

&lt;figure class=&quot;app-screenshot&quot;&gt;
  &lt;a href=&quot;/images/criterion-247/iphone-current.png&quot;&gt;&lt;img src=&quot;/images/criterion-247/iphone-current.png&quot; alt=&quot;Criterion 24/7 on iPhone showing the current film and time remaining&quot; loading=&quot;lazy&quot; /&gt;&lt;/a&gt;
  &lt;figcaption&gt;Criterion 24/7 lets me catch what&apos;s starting and save what I missed for later.&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p&gt;I also wanted to go straight from watching a film to posting a review on &lt;a href=&quot;https://zabriskie.app/&quot;&gt;Zabriskie&lt;/a&gt;, my social network, to share it with my friends. Criterion can open Zabriskie for me to write that post. Zabriskie can use those posts, along with people’s interests and cultural connections, to help them find other media they might like.&lt;/p&gt;

&lt;p&gt;That lets the small app do the part I wanted for myself, while Zabriskie handles the shared conversation and discovery.&lt;/p&gt;

&lt;figure class=&quot;app-screenshot&quot; style=&quot;max-width: 640px;&quot;&gt;
  &lt;a href=&quot;/images/zabriskie/lot-connection-2026-09-06.png&quot;&gt;&lt;img src=&quot;/images/zabriskie/lot-connection-2026-09-06.png&quot; alt=&quot;A Connections card in Zabriskie&apos;s Lot linking my post about the Grateful Dead album American Beauty to another person&apos;s post about the band&apos;s debut album.&quot; loading=&quot;lazy&quot; /&gt;&lt;/a&gt;
  &lt;figcaption&gt;The same discovery works across music, too. Here, The Lot connects my post about American Beauty to someone else&apos;s post about the Dead&apos;s first album.&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p&gt;I’ve put more screenshots and details on the &lt;a href=&quot;/apps/&quot;&gt;pages for these apps&lt;/a&gt;.&lt;/p&gt;

&lt;h2 id=&quot;software-getting-out-of-the-way&quot;&gt;Software getting out of the way&lt;/h2&gt;

&lt;p&gt;I wrote in &lt;a href=&quot;/ai/personal/phish/flow/agents/2026/05/03/rift.html&quot;&gt;&lt;em&gt;Rift&lt;/em&gt;&lt;/a&gt; about what I’d lost as programming became managing agents. Programming had been something I loved doing, with Phish on, for thirty years. Supervising a queue of agents didn’t give me the same thing. So building these apps faster isn’t enough on its own. If I &lt;a href=&quot;/ai/zabriskie/agents/reliability/performance/distributed/2026/09/04/the-quickest-path-to-a-diff.html&quot;&gt;spend the show managing the software that was supposed to help me enjoy it&lt;/a&gt;, I haven’t accomplished what I wanted.&lt;/p&gt;

&lt;p&gt;With Divided Rye, I want to find dinner and head out. With Criterion, I want to catch a film.&lt;/p&gt;

&lt;p&gt;With Zabriskie, I can find a historical show played on this date and stream it. For tonight’s show, I can see which friends are going, when it starts, and where they’re sitting, then chat with them while the band plays. Those are the things I wanted the software for: listening to music, finding my friends, and sharing the show with them.&lt;/p&gt;

&lt;p&gt;The agent can help me build the tool and change it when I need to. Then I want to use it and get on with the evening. That’s what I mean by software getting out of the way. Making and managing it should take up less of my life, so I can spend more of it doing the thing I wanted the software for.&lt;/p&gt;

&lt;h2 id=&quot;keeping-it-on-my-phone&quot;&gt;Keeping it on my phone&lt;/h2&gt;

&lt;p&gt;Once I’ve built one of these tools, I want to put it on my phone, change it when I need to, and keep using it.&lt;/p&gt;

&lt;p&gt;There are ways to do this without publishing to the App Store. Apple supports &lt;a href=&quot;https://developer.apple.com/help/account/devices/devices-overview/&quot;&gt;direct installation on registered devices&lt;/a&gt; through its developer program. With a free Personal Team, the provisioning profiles expire after &lt;a href=&quot;https://developer.apple.com/help/account/basics/about-your-developer-account&quot;&gt;seven days&lt;/a&gt;. A TestFlight build can be tested for &lt;a href=&quot;https://developer.apple.com/help/app-store-connect/test-a-beta-version/testflight-overview/&quot;&gt;up to ninety days&lt;/a&gt;. I’ve been using development installs and TestFlight builds. They work. They also leave me managing a development or testing lifecycle for an app I simply want to use.&lt;/p&gt;

&lt;p&gt;I’m still waiting on App Store approvals for tools I was able to build in hours. Getting them into the store has taken days, with an automated agent walking through the &lt;a href=&quot;https://developer.apple.com/app-store/submitting/&quot;&gt;submission boilerplate&lt;/a&gt; along the way.&lt;/p&gt;

&lt;p&gt;Having an agent handle the process helps. It also makes the mismatch more apparent: we can automate much of the work of preparing a product for distribution, while the simpler intention of keeping a personal program still lives inside that developer workflow.&lt;/p&gt;

&lt;p&gt;I want the phone to treat a personal app as something I can keep. Give it the permissions it needs, preserve its data when I change it, and let me keep using it without treating it as a test build. Sharing it with other people could be a separate decision I make later. Using it myself should be an ordinary place to start.&lt;/p&gt;

&lt;h2 id=&quot;building-for-myself-sharing-with-others&quot;&gt;Building for myself, sharing with others&lt;/h2&gt;

&lt;p&gt;When we first built &lt;a href=&quot;https://zabriskie.app/&quot;&gt;Zabriskie&lt;/a&gt;, I wanted &lt;a href=&quot;/ai/zabriskie/community/2026/03/08/why-im-building-zabriskie.html&quot;&gt;a social place for people who went to shows&lt;/a&gt;, people who couch toured, and people who did some of each. That depends on a shared place. Other people being there is part of what makes it useful.&lt;/p&gt;

&lt;figure class=&quot;app-screenshot&quot; style=&quot;max-width: 640px;&quot;&gt;
  &lt;a href=&quot;/images/zabriskie/cmeik-lot-2026-09-06.png&quot;&gt;&lt;img src=&quot;/images/zabriskie/cmeik-lot-2026-09-06.png&quot; alt=&quot;My Lot as cmeik, showing tonight&apos;s Phish show at Dick&apos;s, the countdown to doors, my AC/DC Bag opener prediction, and people attending or watching from home.&quot; loading=&quot;lazy&quot; /&gt;&lt;/a&gt;
  &lt;figcaption&gt;My Lot before Phish at Dick&apos;s: a countdown to doors, my opener call, and people going to the show or joining from the couch.&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p&gt;We also built smaller things into it, including a way to see the current setlist and put it on a watch. That display can be useful to me even if nobody else uses the same interface. We still need a source for the setlist, but we don’t all need to look at it the same way.&lt;/p&gt;

&lt;figure class=&quot;app-screenshot&quot;&gt;
  &lt;a href=&quot;/images/zabriskie/watch-now-playing.png&quot;&gt;&lt;img src=&quot;/images/zabriskie/watch-now-playing.png&quot; alt=&quot;Zabriskie on Apple Watch showing the current song and reaction buttons during a simulated show&quot; loading=&quot;lazy&quot; /&gt;&lt;/a&gt;
  &lt;figcaption&gt;The current song and reactions on Apple Watch, shown here during a demo show.&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p&gt;I’ve also noticed other setlist apps, including &lt;a href=&quot;https://www.reddit.com/r/phish/comments/1vccvle/onstage_live_phish_setlist_ios_app/&quot;&gt;OnStage&lt;/a&gt; and &lt;a href=&quot;https://www.reddit.com/r/GoosetheBand/comments/1qrens5/new_goose_app_focused_on_live_shows/&quot;&gt;Flodown&lt;/a&gt;, being posted and promoted. OnStage focuses on live setlist updates; Flodown also has community features. They make different choices about what a fan might want. I can see reasons to use either, or to want a version that works a little differently.&lt;/p&gt;

&lt;p&gt;And the work needed to make another version is changing. Take &lt;a href=&quot;https://fouldomain.com/&quot;&gt;Foul Domain&lt;/a&gt;, a Phish statistics site. Its creator &lt;a href=&quot;https://www.reddit.com/r/phish/comments/1s1mume/comment/ocb4iz7/&quot;&gt;describes doing very little coding by hand&lt;/a&gt;, spending their time defining requirements, making architecture decisions, and testing instead. That’s much closer to how I’ve been working too.&lt;/p&gt;

&lt;p&gt;With to-do lists and finance apps, we’ve spent years choosing among other people’s versions of what we need. Each comes with someone’s idea of the right way to work. You pick the one that fits your workflow best, live with the parts that don’t, and hope the maintainer keeps moving in a direction you like. People debate which app is better when what they often mean is which app fits the way they work.&lt;/p&gt;

&lt;p&gt;What happens when making the version that fits you becomes practical?&lt;/p&gt;

&lt;p&gt;This is the part of programming I see becoming commoditized: turning a small, specific requirement into working software. As that gets cheaper, I have another option when an existing app almost fits. I can make the version I wanted, and its usefulness to me can be enough to justify the work.&lt;/p&gt;

&lt;p&gt;That changes what success looks like. Divided Rye has a reason to exist if it helps me eat before a show. It doesn’t need a growth strategy to justify the time I spent on it. There must be so many small things people would like their computers to do that will never support a business. I want those things to be worth building too.&lt;/p&gt;

&lt;p&gt;To some extent, Zabriskie is the same thing. It’s what I want from a social app and a media diary. Other people seem to like it too, but they don’t all use it the way I do. That makes sense. I built it around my intentions, and they bring their own. Something made for me can turn out to be useful to someone else without fitting them exactly.&lt;/p&gt;

&lt;p&gt;Facebook reflects the intentions of the people building it too. So does a to-do app whose idea of the right workflow doesn’t match mine. Zabriskie isn’t exempt from that just because I built it. What interests me is more people getting to make those decisions for themselves, instead of always choosing among the decisions already made for them.&lt;/p&gt;

&lt;p&gt;I’d still pay for useful services or a well-maintained app. Divided Rye depends on restaurant reviews and delivery services; Criterion depends on a stream and a backend that keeps its history. A personal interface can sit on top of shared infrastructure, even when the people using that infrastructure want different things from it.&lt;/p&gt;

&lt;h2 id=&quot;how-long-does-it-need-to-last&quot;&gt;How long does it need to last?&lt;/h2&gt;

&lt;p&gt;Maintenance is still a huge part of this. I’ve written about &lt;a href=&quot;/ai/zabriskie/reliability/2026/04/03/the-feature-that-has-never-worked.html&quot;&gt;a feature that was quick to build and kept breaking&lt;/a&gt;, and about &lt;a href=&quot;/ai/zabriskie/agents/reliability/performance/distributed/2026/09/04/the-quickest-path-to-a-diff.html&quot;&gt;changes that looked reasonable individually but brought down the application together&lt;/a&gt;. I’ve seen agents produce software that is much easier to add to than to keep working. Getting an app onto my phone in five hours doesn’t tell me what it will cost to live with it for five years.&lt;/p&gt;

&lt;p&gt;But some personal software might only need to last a weekend. I could make a little tool to organize one trip, use it, and be done. If building it becomes cheap enough, that could be worthwhile even if I never maintain it again. Disposable software, in that sense, could be a useful outcome: it served its purpose.&lt;/p&gt;

&lt;p&gt;Divided Rye might become something I use for years. A tool for one person can still matter a great deal to that person. What changes is that I can decide when its job is done. I may want to keep the restaurant notes or the films I saved even if I replace the app. A temporary tool still needs to work while I depend on it, and replacing it shouldn’t mean losing what I put into it.&lt;/p&gt;

&lt;p&gt;Zabriskie and the services these apps rely on have a different obligation. Other people depend on them continuing to work, on their data surviving, and on changes not breaking yesterday’s behavior. Cheap code doesn’t remove that work. The possibility of a disposable personal tool doesn’t make a shared service disposable.&lt;/p&gt;

&lt;p&gt;This is another thing I want the platform to support: keeping a useful program, replacing it while preserving my data, or retiring it when I’m finished. I don’t yet know whether maintaining my own apps will be as manageable as building them. Making that manageable is part of the work ahead.&lt;/p&gt;

&lt;h2 id=&quot;start-with-what-i-need&quot;&gt;Start with what I need&lt;/h2&gt;

&lt;p&gt;Sam Altman has described agents as &lt;a href=&quot;https://blog.samaltman.com/three-observations&quot;&gt;virtual coworkers&lt;/a&gt;, including agents that do software engineering. There’s overlap with what I’m describing, but the part that interests me is what I have when the conversation ends. For a recurring need, I want the agent to leave me with an app whose behavior I can try, adjust, and return to. With Criterion, that means a widget I can glance at and saved films I can come back to. When my needs change, I can bring the agent back to change the software.&lt;/p&gt;

&lt;p&gt;The agent helps me build something that does the job. I don’t need every interaction with my computer to become another task to delegate and supervise.&lt;/p&gt;

&lt;p&gt;Imagine opening the computer and saying that you need a way to manage your finances. It asks where the information is, you choose what it can access, and you work together on how you want to see it. You tell it that you get paid irregularly, that you put money aside for travel, or that the usual monthly budget doesn’t make sense for you. It builds around that. A month later, when you understand what you need a little better, you change it.&lt;/p&gt;

&lt;p&gt;I want that to be practical for someone who has never worked as a programmer. My own experience doesn’t establish that we’re there yet. It does make the goal much more concrete for me. A person should be able to bring their understanding of their own life to the computer and use that to shape how it works. They should be able to try something, live with it, and change their mind without having to convince a software company that enough other people want the same thing.&lt;/p&gt;

&lt;p&gt;The social case is further out, but I want to push the idea there too. If people could keep their own data and make it available on their own terms, I could ask for the films my friends were talking about, the shows we were going to together, and the books I meant to come back to, all in a view that made sense to me. Someone else could make a different view of the things shared with them. Shared services would still have work to do, but we could have more say in how we encountered one another’s lives through them.&lt;/p&gt;

&lt;p&gt;Watching my parents write BASIC made me think that making a program could be an ordinary part of using a computer. Now I have a restaurant app on my phone because I wanted to keep going to shows. I want more people to be able to make the things that let them do what matters to them, use them for as long as they help, and get on with their lives. We should build our tools and our platforms around that possibility. A program for one person is enough.&lt;/p&gt;
</description>
				<pubDate>Sun, 06 Sep 2026 18:00:00 +0000</pubDate>
				<link>https://christophermeiklejohn.com/ai/personal/programming/2026/09/06/software-for-one.html</link>
				<guid isPermaLink="true">https://christophermeiklejohn.com/ai/personal/programming/2026/09/06/software-for-one.html</guid>
			</item>
		
			<item>
				<title>The Quickest Path to a Diff · The app died at Red Rocks while its health checks stayed green. Every line of code was fine. The sum was nobody&apos;s job.</title>
				<description>&lt;p&gt;&lt;a href=&quot;https://zabriskie.app/&quot;&gt;Zabriskie&lt;/a&gt; is a social app I’m building around music, films, books, art, and the people who care about them. Its center of gravity is live music, specifically the jam band world I’m part of. In that world no two shows get the same setlist, so the crowd spends the night asking the same questions: what song is this, what have they played so far, who else is here. Zabriskie answers them while the show is happening. The setlist updates song by song as the band plays, the people in the crowd and the people watching the stream from home share one chat, and the current song sits on your lock screen and your watch so checking it doesn’t mean opening the app. The home page is called &lt;a href=&quot;https://zabriskie.app/v2/lot&quot;&gt;the Lot&lt;/a&gt;, after the parking-lot scene at these shows, and when one of your bands is on stage somewhere, it opens on that show.&lt;/p&gt;

&lt;p&gt;I build it entirely through vibe coding: I describe what I want and evaluate the result in the browser, but I don’t read the implementation code. Coding agents write everything, the implementation, the tests, the audits. Claude Code, Cursor, and Codex have all merged work here. My job is deciding what to build and looking at what comes out.&lt;/p&gt;

&lt;p&gt;On August 28 I was at Red Rocks for the second of two nights of the band Goose, and more people were on Zabriskie around me than had ever been on it at once. I spent the evening debugging instead of watching the show, because people kept walking up to tell me the app wasn’t working. They were right. The Lot mostly wouldn’t load. Requests that normally finish in well under a second were taking tens of seconds or timing out entirely. For an app whose whole point is telling you what’s being played right now, a page that answers in forty seconds isn’t degraded, it just has no answer.&lt;/p&gt;

&lt;p&gt;Debugging from a seat at a show, with nothing but a phone, meant asking an agent what was wrong with the site. It couldn’t tell me. So I started pasting everything I could find in the Railway console into a cloud agent, screen by screen. The screen that mattered showed a queue of identical writes all blocked on the same lock, and the first diagnosis of the night came out of it: lock contention, a database deadlocking under load.&lt;/p&gt;

&lt;p&gt;Half right. The queue was real, a client bug was re-registering push tokens over and over, and the first fix merged that night made it stop. But Postgres never reported a deadlock, the reads that reached it ran fast, and the health endpoint stayed green from the first stalled page to the last. The actual problem was a layer up, in my own app. Requests were stuck waiting to borrow a database connection from the app’s pool, the twenty-five connections it keeps open to its database, before their queries ever reached Postgres at all. Nothing measured that wait and nothing logged it. The agent reading my screenshots wasn’t hallucinating. It got handed a true fact from the wrong layer, because the right layer had no facts to give.&lt;/p&gt;

&lt;p&gt;I want to be careful about the lesson here, because “the AI wrote bad code” isn’t it. Taken one change at a time, almost all of the code is reasonable. The problem is that an agent reasons locally. It sees the function it’s editing, the file around it, the change it’s about to make, and within that boundary its judgment is mostly fine. The things that took the app down aren’t visible at that scale. How many connections a request holds, how many queries a page runs, those are properties of the whole program at once, and nobody, me included, had ever looked at them. That gap is what this post is about: all of the reasoning was local, and everything that broke was global.&lt;/p&gt;

&lt;h2 id=&quot;what-was-actually-wrong&quot;&gt;What was actually wrong&lt;/h2&gt;

&lt;p&gt;Every request has to borrow one of those twenty-five connections before it can touch the database, and it gives the connection back when its queries finish. The code the agents had written borrowed a connection to loop over a list of results, and inside that loop borrowed a second connection for each item, while still holding the first. Condensed, the pattern was:&lt;/p&gt;

&lt;div class=&quot;language-go highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;n&quot;&gt;rows&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;_&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;:=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;db&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;QueryContext&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;ctx&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;listShowsSQL&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;c&quot;&gt;// borrows connection #1&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;defer&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;rows&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Close&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;                            &lt;span class=&quot;c&quot;&gt;// #1 stays borrowed until the loop ends&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;rows&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Next&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;show&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;Show&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;rows&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Scan&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;show&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;ID&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;show&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Date&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

    &lt;span class=&quot;c&quot;&gt;// loadSetlist runs its own query: it borrows connection #2&lt;/span&gt;
    &lt;span class=&quot;c&quot;&gt;// while #1 is still held.&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;show&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Setlist&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;loadSetlist&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;ctx&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;db&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;show&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;ID&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;One request running this holds two connections at once. That’s fine while the pool has spare connections. That night it didn’t: twenty-five requests were in loops like this at the same time, each holding one connection and waiting for a second one, and a connection only comes back when one of those requests finishes. None of them could finish. That’s the gridlock. Requests queued behind it until they timed out.&lt;/p&gt;

&lt;p&gt;The waiting happened inside my app, before any query reached the database. That’s why Postgres looked fine all night: the queries that did get a connection ran fast. The health check stayed green because it doesn’t run a query. It returns a string formatted once at startup.&lt;/p&gt;

&lt;p&gt;The code had been in production since February. August 28 was the first night with enough traffic to fill the pool. The pool size didn’t help: nobody chose twenty-five connections for this workload. An agent picked the number at scaffold time, before the app had users, and nobody went back to it. Go’s pool tracks exactly this failure, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;WaitCount&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;WaitDuration&lt;/code&gt;, how many times a query waited for a connection and for how long. We had never logged them. Almost no query had a timeout. None of the numbers that would have explained the outage were being recorded.&lt;/p&gt;

&lt;p&gt;The fix rewrote the worst read paths to stop holding one connection while borrowing another, retuned the pool, and started logging the wait counters. Deploying it was its own problem. The merge gate wanted a CI check that wouldn’t pass, the automation didn’t have permission to re-run it, and Railway, where the app runs, was having an unrelated incident and couldn’t start a deploy. I disabled branch protection from my phone, in my seat, at 02:19 UTC. I don’t know the exact moment the app came back. The fix merged while the band was still playing, and it couldn’t reach production until Railway recovered.&lt;/p&gt;

&lt;h2 id=&quot;the-remediation-was-excellent-which-is-the-problem&quot;&gt;The remediation was excellent, which is the problem&lt;/h2&gt;

&lt;p&gt;The agents got the mechanism exactly right, wrote a regression test that shrinks the pool to a single connection and reproduces the hang deterministically, and swept the whole backend for the pattern. The first scan claimed 188 sites. The next claimed 217. The real number, once the analysis was rebuilt to check what actually holds a connection, was about forty, and all forty were fixed, with a static check behind them so the class can’t come back. The shows list alone went from 151 database round trips per page to 4. By morning the defect class was extinct and guarded. If the story ended there, you’d conclude the loop works.&lt;/p&gt;

&lt;p&gt;The next day, an agent looked at one more endpoint and found it executing 51 to 71 SQL statements per request, of which 23 were distinct.&lt;/p&gt;

&lt;p&gt;This was the endpoint every home-screen widget and both watch apps poll all day. The new guardrail had nothing to say about it, because it hunts the gridlock pattern, holding one connection while borrowing another, and this endpoint didn’t do that. This was a different disease. The part that stays with me is that the costs were documented. This comment sat in the code, verbatim:&lt;/p&gt;

&lt;div class=&quot;language-go highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;// limit 1 — NOT watchOnThisDayLimit. Every card costs two more queries&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;// (highlight + setlist), and this is called from watchChipText and&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;// watchChipShowID as well as the handler. Fanning out to 5 at all three call&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;// sites would take one request from ~9 queries to ~33 to render a chip that&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;// shows exactly one stat. The rotation pays for rotation; nothing else does.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Whoever wrote that, and in this codebase that means an agent, clearly understood the cost model. It prices the request at about nine queries. The measured number was 51 to 71. Nothing enforced the nine, nothing would have noticed when it stopped being nine, and the other contributors to the total lived outside every context window that ever edited this file.&lt;/p&gt;

&lt;p&gt;My first instinct was to call this a regression, the next disease showing up right after the first was cured. That’s not what happened. Nothing new appeared. These queries had been there all along, and someone finally pointed an instrument at them. Which raises the obvious question: if this endpoint sat at 51 queries per request until somebody happened to look, what is everything else sitting at?&lt;/p&gt;

&lt;h2 id=&quot;a-census&quot;&gt;A census&lt;/h2&gt;

&lt;p&gt;So I measured. I turned on a setting that writes down every question the app asks the database, loaded each page once as a normal signed-in user, and counted. In the table, statements are how many questions it took to draw the page once, and shapes are how many different questions were in the mix. One request per page, one user, so treat these as shape rather than precision.&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;surface&lt;/th&gt;
      &lt;th&gt;statements per request&lt;/th&gt;
      &lt;th&gt;distinct shapes&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;Shows page&lt;/td&gt;
      &lt;td&gt;340&lt;/td&gt;
      &lt;td&gt;14&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;The Lot (home)&lt;/td&gt;
      &lt;td&gt;292&lt;/td&gt;
      &lt;td&gt;164&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Band page&lt;/td&gt;
      &lt;td&gt;186&lt;/td&gt;
      &lt;td&gt;28&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Watch idle-context (the endpoint above)&lt;/td&gt;
      &lt;td&gt;72&lt;/td&gt;
      &lt;td&gt;43&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Flow feed&lt;/td&gt;
      &lt;td&gt;40&lt;/td&gt;
      &lt;td&gt;30&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Profile feed&lt;/td&gt;
      &lt;td&gt;14&lt;/td&gt;
      &lt;td&gt;8&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Notifications&lt;/td&gt;
      &lt;td&gt;3&lt;/td&gt;
      &lt;td&gt;3&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;Six more rows sit in between, from tour stats at 60 down to the profile page at 9. The endpoint from the last section, the one that just got its own perf fix, is the fourth worst page in the app; the census caught it before that fix landed. These numbers aren’t just latency, either. Every statement stretches how long a request holds its borrowed connection, so a 292-statement page is 292 slices of hold time on the same pool that gridlocked in August. Same arithmetic, different magnitude.&lt;/p&gt;

&lt;p&gt;The ratio of statements to shapes splits the table into two diseases. The Shows page is the classic loop: 340 statements but only 14 shapes, a handful of queries repeated once per card for sixty-five cards. The Lot is the more instructive one: 292 statements across 164 distinct shapes. There’s no villain loop to point at. It’s an accretion. Every feature that ever shipped onto the home screen brought three or four queries with it, each addition individually negligible, and the page now runs about three hundred statements, one after another, every time someone opens the app.&lt;/p&gt;

&lt;p&gt;The Shows page bothers me the most, because of where its queries live. They sit inside the function that draws one show card, so every page that draws cards inherits them. And the Shows page has no page size: it draws every show happening in the next seven days, and the day I measured, that was sixty-five cards. Neither piece looks expensive on its own. Each query takes a fraction of a millisecond, and a loop over cards is how every list ever gets built. The cost only exists in the multiplication, and no edit ever held both ends of it. When the outage-night fix reported the shows list going from 151 round trips to 4, that count was true, for the loop being edited. The card renderer it calls kept its queries, and the same page runs 340 statements today. A reviewer handed that change would have approved it. My review loop did approve it. Nobody was stupid and nothing was broken, and the app was still guaranteed to meet its first real crowd unmeasured.&lt;/p&gt;

&lt;p&gt;The same week’s digging also turned up a single unindexed function quietly eating 79.5 percent of all production database time. Aggregate costs, invisible because aggregate was nobody’s view.&lt;/p&gt;

&lt;h2 id=&quot;why-this-keeps-happening&quot;&gt;Why this keeps happening&lt;/h2&gt;

&lt;p&gt;I’ve written about this failure shape before. In June the test suite grew a pile of shared fixture data that no test owned (&lt;a href=&quot;/ai/zabriskie/agents/reliability/testing/2026/06/10/the-test-suite-was-the-incident.html&quot;&gt;The Test Suite Was the Incident&lt;/a&gt;). Now it’s the application itself: a shared pool and a shared database, exhausted by code that passed every check anyone ever ran against it. Both times the broken thing was shared, and every check that cleared it, the build, the tests, the review of one diff at a time, was local.&lt;/p&gt;

&lt;p&gt;There’s a personal sting in this one. My academic work was distributed systems and fault injection: systems whose parts are individually fine and fail in combination, and tools that find those failures by attacking a running system on purpose, because reading the parts won’t surface them. I spent years doing that to other people’s systems. I never did it to mine. No load test, no drills, nothing that ever tested the whole running program. In someone else’s system, this failure wouldn’t have surprised me for a second.&lt;/p&gt;

&lt;p&gt;Watch how something new gets added, because this is where the difference lives. A developer adding a query to a page they know starts from what’s already there: there’s a query two functions up that fetches almost this, so they extend it, or join against it, or pull the shared part into a helper. Nobody calls that global reasoning. It’s just what editing a program you remember looks like. A session doesn’t do any of that unless you ask for it. It takes the quickest path to a working diff, and the quickest path is a fresh query, a fresh component, a fresh style, written right where it stands. The backend queries the attendance table, who’s going to this show, from 264 places in 69 files. The little uppercase label that sits above a card title has been implemented hundreds of times, an inline style here, a one-off helper there, and no session ever reached for a shared one, because finding it costs a search and writing a new one costs nothing. Refactoring doesn’t happen unless you ask. Modularization doesn’t happen unless you ask. And review doesn’t catch it: every change here is reviewed, a review bot plus an adversarial reviewer we built, but both read the diff, and duplication is invisible in a diff. The copy looks clean on its own, the original is in some other file, and no human opens the file.&lt;/p&gt;

&lt;p&gt;Human codebases rot this way too. The other difference is what happens to the person who wrote the code, after the code ships. You ship a slow page, you watch it crawl, and you build the next one differently. You get paged at 2am and the lesson sticks. Nobody in this loop gets paged. This codebase took a couple thousand pull requests in a few months, each one from a session that had never seen the app before and was gone before the code ran. A session knows only what’s written down for it, and nothing written down said what a request should cost. I never thought to write it down either.&lt;/p&gt;

&lt;p&gt;To be fair to the agents, the problem isn’t ability. The same loop that missed 292 statements on the home page diagnosed the gridlock while the site was down, and reverted six of its own eight optimizations when the benchmarks said they were wrong. Handed the evidence, agents are as good as any engineer I’ve worked with, and faster. But diagnosis is local: the evidence is sitting right there. Prevention is global, and nobody in the loop was assigned it. Including me. The nine-queries comment crossed my desk in a diff and I approved it, because it looked careful. A cost argued in prose looks handled. Nobody ever checks it again.&lt;/p&gt;

&lt;h2 id=&quot;where-does-global-reasoning-come-from&quot;&gt;Where does global reasoning come from?&lt;/h2&gt;

&lt;p&gt;The thing that broke, every time, was a property of the whole program: connections held across a request, statements accumulated across a page, one function’s share of the database’s time. The reasoning that touched the code, every time, was local: the agent sees one function, the tests check one behavior, a review reads one diff, I approve one change at a time. None of that reasoning was wrong, which is the uncomfortable part. The Shows page runs 340 statements per request and every line involved is fine.&lt;/p&gt;

&lt;p&gt;So where is reasoning about the whole supposed to come from? Not from the sessions: each one is born into a diff and gone by the merge. Not from the checks: every check anyone has written looks at one change. Not from me: I decide what to build and judge what comes out, and I have never read this program. Nobody in this loop stands anywhere the whole program is visible.&lt;/p&gt;

&lt;p&gt;The human version of global reasoning was never a formal method. It was one person editing a program they remembered. Integration happened by default, because the developer knew what was already there; the whole lived in somebody’s head, and writing the code was what kept it current. This loop has no such head, and that leaves me with questions I can’t answer yet. Will models ever do this unprompted, or is reading the neighborhood before every change simply never the quickest path to a diff? Are context windows even the constraint? This program still fits inside one, and no session has ever chosen to load it. Maybe the answer isn’t cognition at all: static analysis, dynamic analysis, test suites strong enough to pin down the whole program, machinery doing for the loop what memory used to do for a developer. Or maybe a loop like this only works with a human in it who actually reads the code. I don’t know which of these it is. What I know is what happens with none of them, because I watched it happen from a seat at Red Rocks.&lt;/p&gt;
</description>
				<pubDate>Fri, 04 Sep 2026 18:30:00 +0000</pubDate>
				<link>https://christophermeiklejohn.com/ai/zabriskie/agents/reliability/performance/distributed/2026/09/04/the-quickest-path-to-a-diff.html</link>
				<guid isPermaLink="true">https://christophermeiklejohn.com/ai/zabriskie/agents/reliability/performance/distributed/2026/09/04/the-quickest-path-to-a-diff.html</guid>
			</item>
		
			<item>
				<title>Every Card Will Show · The earlier implementation, tests, audits, and accepted proofs said the algorithm was correct. None of them stated the whole problem.</title>
				<description>&lt;p&gt;&lt;a href=&quot;https://zabriskie.app/&quot;&gt;Zabriskie&lt;/a&gt; is a social app I am building around music, films, books, art, and the people who care about them. I am building the entire application through vibe coding: I describe the behavior I want and evaluate the product in the browser, but I don’t read the implementation code. Coding agents write the implementation, tests, and audits.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://zabriskie.app/v2/lot&quot;&gt;Zabriskie’s home screen&lt;/a&gt; is called The Lot. At the start of this work, it was one long list of cards. One version rendered 29 cards and produced a page more than 15,000 pixels tall. I asked the agent to replace that list with five smaller programs that would appear during the morning, midday, afternoon, evening, and late night.&lt;/p&gt;

&lt;p&gt;The requirement had two parts. The largest visit should contain as few cards as possible, but the five visits together should show every card whose content remained available and relevant to the person throughout the day. I call those cards eligible.&lt;/p&gt;

&lt;p&gt;Late on August 23, I asked the coding agent a specific question.&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;If one person visited those five programs during one calendar day, would they see every card whose content was available and relevant to them?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The agent answered yes. Every eligible card would show.&lt;/p&gt;

&lt;p&gt;It wasn’t true. After three days of repairs, I was frustrated enough to ask the agent to formalize the requirement in &lt;a href=&quot;https://lean-lang.org/&quot;&gt;Lean&lt;/a&gt;, a language and proof assistant. As part of that work, the agent finally compared its failed audit’s eligible-card count with the 27 positions allowed by its own caps. The audit contained 20 more eligible cards than the schedule could possibly show.&lt;/p&gt;

&lt;p&gt;The agent hadn’t invented a file, a function, or a test result. It had produced real code and real evidence. What it invented was the connection between them: because each artifact established something close to my requirement, together they established the requirement itself. They didn’t. None checked whether one person’s five visits on the same day covered every eligible card.&lt;/p&gt;

&lt;p&gt;I kept merging because I asked for comprehensive audits before doing so. Three were completed, and all three said the implementation was working correctly. I owned the decision to ship, but I wasn’t ignoring contrary evidence. The audits themselves had inherited weaker versions of the requirement.&lt;/p&gt;

&lt;figure style=&quot;max-width: 688px; margin: 2rem auto;&quot;&gt;
  &lt;img src=&quot;/img/zabriskie-every-card-will-show-late-hero.jpg&quot; alt=&quot;The late-hours Lot on August 23, showing a live King Gizzard and the Lizard Wizard lead followed by an Act Now section with four live shows&quot; loading=&quot;lazy&quot; style=&quot;width: 100%; height: auto; border-radius: 24px;&quot; /&gt;
  &lt;figcaption&gt;The Lot at 11:35 PM on August 23: the late-hours program, a live lead, and Act Now beneath it.&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;h2 id=&quot;how-the-requirement-changed&quot;&gt;How the requirement changed&lt;/h2&gt;

&lt;p&gt;The agent’s design promoted one card to the top, called the &lt;strong&gt;Hero&lt;/strong&gt;, and grouped the supporting cards beneath it into sections such as &lt;strong&gt;Act Now&lt;/strong&gt;, &lt;strong&gt;Discover&lt;/strong&gt;, and &lt;strong&gt;You&lt;/strong&gt;. The Lot needed a cap, meaning a maximum number of supporting cards on each visit. The question was how to make that cap small without leaving an eligible card unseen across all five visits.&lt;/p&gt;

&lt;p&gt;Instead of preserving both parts of the requirement, the agent made a series of local decisions about what each visit could omit. It limited which sections could contribute cards, made the Hero compete with the supporting cards, introduced different caps at different times, and created a ranking policy based on deadlines, show-night context, tour status, preferred times, and viewing history.&lt;/p&gt;

&lt;p&gt;Those are plausible ingredients for a scheduler. The problem was that the agent chose them without bringing the resulting product rules back to me, then began testing the rules it had chosen. Some were plainly wrong. &lt;strong&gt;Connections&lt;/strong&gt;, a card meant to begin a listen, read, or watch, received a late-night preference. The personal-history and broader historical-archive &lt;strong&gt;On This Day&lt;/strong&gt; cards were wired backward. One code comment even called the three-section limit the “owner’s cap decision.” I had made no such decision.&lt;/p&gt;

&lt;p&gt;The clearest numerical argument was 27, presented as the number of positions available across the day. It came from adding five source-code caps: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;6 + 5 + 6 + 4 + 6&lt;/code&gt;. But those states belonged to two different calendars. A weekday could reach morning, workday, evening, and late night. A weekend could reach daytime, evening, and late night. There was no date on which one person could visit all five states.&lt;/p&gt;

&lt;p&gt;Twenty-seven was arithmetic over an impossible itinerary, not evidence that one day had room for every card.&lt;/p&gt;

&lt;figure style=&quot;max-width: 640px; margin: 2rem auto;&quot;&gt;
  &lt;img src=&quot;/img/zabriskie-every-card-will-show-coming-up.jpg&quot; alt=&quot;The Coming Up card in the late-hours Lot, listing two Goose shows at Red Rocks and a Phish show at Dick&apos;s Sporting Goods Park&quot; loading=&quot;lazy&quot; style=&quot;width: 100%; height: auto; border-radius: 24px;&quot; /&gt;
  &lt;figcaption&gt;Farther down the late-hours program: Coming Up, with the next Goose and Phish dates. A single program is intentionally partial; the disputed guarantee concerns the five visits together.&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p&gt;The number doesn’t explain the 20 missing cards by itself. It establishes something narrower: the capacity argument didn’t model the day I had described.&lt;/p&gt;

&lt;p&gt;The tests made similar substitutions. A documentation audit promised that every card could appear within a week. A reachability test considered one card at a time, treated that target as long unseen while its competitors were recently shown, and searched for some state in which it could win. A separate adversarial review confirmed that every listed card had a classification and could pass through the ranking system. All three checks passed. None asked whether the same fixed set of eligible cards appeared across one person’s five visits on one date.&lt;/p&gt;

&lt;p&gt;The implementation also split the final decision between the Go server and the browser. The server ranked cards, but the browser could still group, suppress, and reorder them. That made a passing server test insufficient: it did not cover the entire path that decided what a person saw. The one end-to-end test pinned a single midday response. It checked that the sections surviving that cap rendered, not that five visits covered the catalog.&lt;/p&gt;

&lt;p&gt;The checks hadn’t lied. They answered weaker questions and the agent presented their passing results as answers to mine. I accepted that evidence as stronger than it was. By then the hallucination was executable: it had become code, tests, and audits that could all turn green together.&lt;/p&gt;

&lt;h2 id=&quot;what-lean-made-explicit&quot;&gt;What Lean made explicit&lt;/h2&gt;

&lt;p&gt;Lean is both a programming language and a proof assistant. It lets us describe a system precisely and then mechanically checks what follows from that description. This was my first time using Lean. I had the agent build a Lean model beside Zabriskie’s production server, which is written in &lt;a href=&quot;https://go.dev/&quot;&gt;Go&lt;/a&gt;. Lean does not run when somebody opens The Lot, and it does not read or prove the Go source.&lt;/p&gt;

&lt;p&gt;By the end, the formal work needed to answer three separate questions:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;What is the smallest per-visit card limit that could possibly cover the catalog?&lt;/li&gt;
  &lt;li&gt;Given a schedule that fits that limit, can ranking or capping still push out a card that was promised a place?&lt;/li&gt;
  &lt;li&gt;Does the separately written Go selector behave like the Lean selector we proved things about?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;These are different claims. A lower bound does not prove that a working schedule reaches it. A proof about a Lean function does not prove that Go matches it. Keeping those boundaries visible made the rest of the argument easier to audit. We reached that decomposition only after proving two incomplete statements.&lt;/p&gt;

&lt;h3 id=&quot;lean-proved-the-wrong-day&quot;&gt;Lean proved the wrong day&lt;/h3&gt;

&lt;p&gt;The first Lean version of the card-selection logic failed its coverage check, but it represented only part of the inventory and could not answer the full question. The agent’s next model expanded the inventory and passed by treating weekday morning, workday, evening, late night, and weekend daytime as five consecutive visits. No calendar day is both a weekday and a weekend.&lt;/p&gt;

&lt;p&gt;Lean correctly proved coverage for an itinerary no person could take. The theorem was valid. The agent’s claim that it represented my day was not.&lt;/p&gt;

&lt;p&gt;That proof entered the main source code and became a required continuous-integration check. I merged it because the proof made the agent’s explanation feel stronger than an ordinary test. But Lean only checks the statement it receives. It cannot decide whether an agent translated a product requirement faithfully.&lt;/p&gt;

&lt;h3 id=&quot;coverage-was-still-only-half-the-problem&quot;&gt;Coverage was still only half the problem&lt;/h3&gt;

&lt;p&gt;After the calendar was repaired, the basic coverage question became one set equality. Read this theorem as: combine the cards assigned to all five programs, and the result is the complete modeled catalog.&lt;/p&gt;

&lt;div class=&quot;language-lean highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;theorem&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;union_of_five_programs_is_all_modeled_cards&lt;/span&gt; :
    &lt;span class=&quot;n&quot;&gt;cardsAcrossFivePrograms&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;modeledCardNames&lt;/span&gt; := &lt;span class=&quot;k&quot;&gt;by&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;native_decide&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;It was easier to inspect than the earlier model, but it still omitted part of the product requirement.&lt;/p&gt;

&lt;p&gt;The original problem was a 29-card, 15,000-pixel page. A theorem saying the five programs cover the catalog accepts the stupidest possible implementation: show every card on every visit. Coverage is a constraint. The objective is to make each visit as small as possible while satisfying that constraint.&lt;/p&gt;

&lt;p&gt;The inventory changed during the repairs. From this point forward, the model contains 45 distinct cards, not the earlier incident inventory. One is the structural Hero, leaving 44 supporting cards.&lt;/p&gt;

&lt;p&gt;Because some supporting cards were assigned to more than one program, the agent-generated repair reserved 54 positions across the day: 10, 13, 11, 10, and 10. The proof certified coverage, but only by padding the schedule. It still had not stated the size objective: minimize the largest supporting-card count across the five visits.&lt;/p&gt;

&lt;p&gt;This is where Lean helped me understand what I was actually trying to do. It did not recover the product intent. Reading the theorem literally and challenging every input and unexplained capacity made the requirement inspectable enough for me to see what the agent had left out.&lt;/p&gt;

&lt;h3 id=&quot;proving-the-minimum&quot;&gt;Proving the minimum&lt;/h3&gt;

&lt;p&gt;Once stated correctly, the lower bound is simple. There are 44 supporting cards and five visits. If every visit showed at most eight supporting cards, the entire day would contain at most 40 positions. Four cards could not appear. Therefore any covering schedule must allow at least nine supporting cards on one visit.&lt;/p&gt;

&lt;p&gt;That counting argument became a general Lean theorem. The names beginning with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;h&lt;/code&gt; are assumptions supplied to the proof: the catalog has no duplicates, the visits cover it, and each visit contains at most &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;max&lt;/code&gt; cards. The line after the colon is what Lean proves from those assumptions:&lt;/p&gt;

&lt;div class=&quot;language-lean highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;theorem&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;covering_schedule_capacity_lower_bound&lt;/span&gt;
    (&lt;span class=&quot;n&quot;&gt;catalog&lt;/span&gt; : &lt;span class=&quot;n&quot;&gt;List&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;String&lt;/span&gt;)
    (&lt;span class=&quot;n&quot;&gt;keeps&lt;/span&gt; : &lt;span class=&quot;n&quot;&gt;List&lt;/span&gt; (&lt;span class=&quot;n&quot;&gt;List&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;String&lt;/span&gt;))
    (&lt;span class=&quot;n&quot;&gt;max&lt;/span&gt; : &lt;span class=&quot;n&quot;&gt;Nat&lt;/span&gt;)
    (&lt;span class=&quot;n&quot;&gt;hunique&lt;/span&gt; : &lt;span class=&quot;n&quot;&gt;catalog&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Nodup&lt;/span&gt;)
    (&lt;span class=&quot;n&quot;&gt;hcover&lt;/span&gt; : &lt;span class=&quot;n&quot;&gt;covers&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;keeps&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;catalog&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;true&lt;/span&gt;)
    (&lt;span class=&quot;n&quot;&gt;hmax&lt;/span&gt; : &lt;span class=&quot;o&quot;&gt;∀&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;keep&lt;/span&gt; &lt;span class=&quot;err&quot;&gt;∈&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;keeps&lt;/span&gt;, &lt;span class=&quot;n&quot;&gt;keep&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;length&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;≤&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;max&lt;/span&gt;) :
    &lt;span class=&quot;n&quot;&gt;catalog&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;length&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;≤&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;keeps&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;length&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;max&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The conclusion, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;catalog.length ≤ keeps.length * max&lt;/code&gt;, is just the counting argument in symbols. A 44-card catalog and five visits turn it into &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;44 ≤ 5 × max&lt;/code&gt;, so &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;max&lt;/code&gt; must be at least nine.&lt;/p&gt;

&lt;h3 id=&quot;proving-that-ranking-cannot-break-the-schedule&quot;&gt;Proving that ranking cannot break the schedule&lt;/h3&gt;

&lt;p&gt;The lower bound only says that nine slots are necessary. It does not say the real selector will use those slots correctly.&lt;/p&gt;

&lt;p&gt;Within the supporting-card phase, cards play two roles: reservations and filler. The product gives each supporting card a guaranteed visit, which I call its reservation. The selector computes one ranked order, admits every fitting reservation first, then uses that order for section diversity and filler. The property we need is that ranking can change the filler, but it cannot evict a reservation that fits within the visit’s cap.&lt;/p&gt;

&lt;p&gt;In plain language, the theorem says:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;For any cards, visits, capacities, and ranking inputs: if every supporting card has a reserved visit, and the reservations for each visit fit its cap, then the union of the final ranked-and-capped outputs contains every supporting card.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The exact Lean statement is longer because it names all of those inputs. Everything before the colon describes the cards, visits, ranking inputs, and two assumptions. Everything after the colon is the coverage claim.&lt;/p&gt;

&lt;details&gt;
  &lt;summary&gt;Show the exact Lean theorem&lt;/summary&gt;

  &lt;div class=&quot;language-lean highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;theorem&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;capped_selector_covers_any_fitting_schedule&lt;/span&gt;
    (&lt;span class=&quot;n&quot;&gt;units&lt;/span&gt; : &lt;span class=&quot;n&quot;&gt;List&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;Card&lt;/span&gt;)
    (&lt;span class=&quot;n&quot;&gt;visits&lt;/span&gt; : &lt;span class=&quot;n&quot;&gt;List&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;Daypart&lt;/span&gt;)
    (&lt;span class=&quot;n&quot;&gt;capacity&lt;/span&gt; : &lt;span class=&quot;n&quot;&gt;Daypart&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;→&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;Nat&lt;/span&gt;)
    (&lt;span class=&quot;n&quot;&gt;eventLead&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;onTour&lt;/span&gt; : &lt;span class=&quot;n&quot;&gt;Daypart&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;→&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;Bool&lt;/span&gt;)
    (&lt;span class=&quot;n&quot;&gt;staleness&lt;/span&gt; : &lt;span class=&quot;n&quot;&gt;Daypart&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;→&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;List&lt;/span&gt; (&lt;span class=&quot;n&quot;&gt;String&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;×&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;Nat&lt;/span&gt;))
    (&lt;span class=&quot;n&quot;&gt;hassigned&lt;/span&gt; : &lt;span class=&quot;o&quot;&gt;∀&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;card&lt;/span&gt; &lt;span class=&quot;err&quot;&gt;∈&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;supportingCards&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;units&lt;/span&gt;,
      &lt;span class=&quot;o&quot;&gt;∃&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;visit&lt;/span&gt; &lt;span class=&quot;err&quot;&gt;∈&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;visits&lt;/span&gt;, &lt;span class=&quot;n&quot;&gt;ownsProgramPosture&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;card&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;visit&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;true&lt;/span&gt;)
    (&lt;span class=&quot;n&quot;&gt;hfits&lt;/span&gt; : &lt;span class=&quot;o&quot;&gt;∀&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;visit&lt;/span&gt; &lt;span class=&quot;err&quot;&gt;∈&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;visits&lt;/span&gt;,
      ((&lt;span class=&quot;n&quot;&gt;supportingCards&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;units&lt;/span&gt;)&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;filter&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;card&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;ownsProgramPosture&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;card&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;visit&lt;/span&gt;)&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;length&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;≤&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;capacity&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;visit&lt;/span&gt;) :
    &lt;span class=&quot;n&quot;&gt;covers&lt;/span&gt;
      (&lt;span class=&quot;n&quot;&gt;cappedSupportingKeeps&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;units&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;visits&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;capacity&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;eventLead&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;onTour&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;staleness&lt;/span&gt;)
      (&lt;span class=&quot;n&quot;&gt;supportingNames&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;units&lt;/span&gt;) &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;  &lt;/div&gt;

&lt;/details&gt;

&lt;p&gt;That is the symbolic version of the plain-language claim above.&lt;/p&gt;

&lt;p&gt;The Lean selector is called &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;capCards&lt;/code&gt;; its supporting-card phase admits the reserved cards first, then adds section diversity and fills any remaining positions by rank. The structural Hero is checked separately.&lt;/p&gt;

&lt;p&gt;The proof intentionally leaves the filler identities open. After admitting the reservations, the selector first tries to represent a section not already present, then fills any remaining slots with the highest-scoring cards. A card’s score considers its section, deadline urgency, show-night and tour context, preferred time, viewing frequency and recency, and fixed priority within its section. A special morning bonus also applies to the two &lt;strong&gt;On This Day&lt;/strong&gt; memories. Those signals choose the filler cards, but cannot displace a reservation. The server applies the fixed display hierarchy afterward.&lt;/p&gt;

&lt;p&gt;The remaining obligation was a concrete product schedule that met the nine-card bound. Each supporting card has exactly one reservation, balanced as:&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;morning     9
midday      9
afternoon   9
evening     9
late        8
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Those 44 positions contain the 44 supporting cards exactly once. A card can still prefer several times of day for ranking, but a ranking preference does not create a second reservation.&lt;/p&gt;

&lt;p&gt;The algorithm and the product table therefore remain separate. Moving &lt;strong&gt;Connections&lt;/strong&gt; to a different program requires checking that the new reservations still fit, but it does not require rewriting the proof that the selector preserves any fitting schedule.&lt;/p&gt;

&lt;p&gt;A separate Lean check applies the generic theorem to this product table. It establishes both halves of the result: the selector covers all 44 supporting cards, and it returns at most nine of them on each visit. Nine is necessary, and this schedule reaches nine.&lt;/p&gt;

&lt;p&gt;Within the selector, each output contains the structural Hero plus at most nine modeled supporting cards. The rendered page may also contain &lt;strong&gt;Live Now&lt;/strong&gt;, a separate module for a show currently playing, and other modules outside this theorem.&lt;/p&gt;

&lt;p&gt;That settled the generic law and one concrete product schedule. It did not yet show that the separately written Go selector made the same choices.&lt;/p&gt;

&lt;h3 id=&quot;making-go-answer-to-the-model&quot;&gt;Making Go answer to the model&lt;/h3&gt;

&lt;p&gt;The proof so far is about Lean. Its generic theorem receives an eligible catalog and ranking signals; it does not build those inputs for a particular person. On a live request, Go does that work: it determines which cards are eligible, calculates recency and frequency from the person’s impression history, supplies the event and tour context, and runs the separate &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;lotCapCards&lt;/code&gt; selector.&lt;/p&gt;

&lt;p&gt;The executable Lean selector mirrors the scoring and capping logic so that we can compare it with Go. Connecting the proof to what a person sees takes three more steps:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;Lean executes its selector on concrete catalogs and ranking inputs.&lt;/li&gt;
  &lt;li&gt;Go runs &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;lotCapCards&lt;/code&gt; on the same inputs, and a differential test compares the two outputs.&lt;/li&gt;
  &lt;li&gt;The server sends Go’s final allow-list and order to the browser, which renders that order without running another scheduler.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This follows the same broad shape used by &lt;a href=&quot;https://docs.cedarpolicy.com/other/security.html&quot;&gt;Cedar&lt;/a&gt;, the open-source authorization language developed at AWS: prove properties of a model, implement a separate production engine, and use &lt;a href=&quot;https://en.wikipedia.org/wiki/Differential_testing&quot;&gt;differential testing&lt;/a&gt; to look for disagreement. Our boundary is much smaller, but the separation is the same.&lt;/p&gt;

&lt;p&gt;The number 128 has no mathematical significance. It is simply the fixed number of comparison cases we currently run: the complete 45-card catalog plus 63 generated subsets makes 64 catalogs, and each runs once with every card unseen and once with every card recently seen. That makes 128 five-visit comparisons.&lt;/p&gt;

&lt;p&gt;This covers only a tiny fraction of the possible inputs. Each of the 45 cards can be eligible or ineligible, creating roughly 35 trillion possible subsets before considering mixed viewing histories or other ranking inputs. Adding more cases would improve the chance of catching a disagreement between Lean and Go, but it would not strengthen the universal Lean theorem. This comparison is a bounded implementation check, not another proof.&lt;/p&gt;

&lt;p&gt;Regenerating those cases exposed a mismatch on one filler card. Go allowed a card to prefer several times of day; the executable Lean version allowed only one. We corrected the reference implementation without changing the generic coverage theorem.&lt;/p&gt;

&lt;p&gt;After that correction, the selected-card sets agreed on all 128 walks. A separate Go unit test exercised the 45 modeled cards from both history starting points and under all four combinations of whether an event led the page and whether the person was on tour.&lt;/p&gt;

&lt;p&gt;Together, these checks connect the proved model to Go, but they do not prove every possible Go execution. Ordinary Go and React tests cover the final server-to-browser handoff in step three. There is still no browser test that performs all five visits and covers the entire path in one run.&lt;/p&gt;

&lt;h3 id=&quot;what-the-theorem-does-not-cover&quot;&gt;What the theorem does not cover&lt;/h3&gt;

&lt;p&gt;The fixed-eligibility assumption was not a limitation I had thought about when I asked for no starvation. It became visible when we tried to generalize the Lean model so that the eligible set could change between visits. The stronger guarantee was false. Lean helped us find a concrete counterexample using &lt;strong&gt;Last Night&lt;/strong&gt;, a prompt to review the previous evening’s show.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Last Night&lt;/strong&gt; has a morning reservation:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;During the morning visit, &lt;strong&gt;Last Night&lt;/strong&gt; is not eligible, so the selector cannot show it.&lt;/li&gt;
  &lt;li&gt;It becomes eligible at midday, after its reservation has passed.&lt;/li&gt;
  &lt;li&gt;For the rest of the day it can appear only as filler. But every later program’s reservations already fill its capacity, so no filler position is available and &lt;strong&gt;Last Night&lt;/strong&gt; remains unseen.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That exact walk is now checked in Lean, which reports &lt;strong&gt;Last Night&lt;/strong&gt; as the only unseen card. It does not contradict the coverage theorem: the theorem starts with one fixed eligible catalog, while the counterexample changes that catalog between visits.&lt;/p&gt;

&lt;p&gt;For the actual &lt;strong&gt;Last Night&lt;/strong&gt; flow, that boundary is acceptable. If a person had already RSVP’d to the previous night’s show, the card is eligible by morning and its guarantee applies. The counterexample corresponds to someone adding the RSVP retroactively after the show, perhaps the next afternoon. In that case, not receiving a same-day prompt to write a review is acceptable product behavior. The specification deliberately admits that failure rather than complicating the scheduler and theorem to guarantee a case I do not need.&lt;/p&gt;

&lt;h2 id=&quot;algorithms-are-hard&quot;&gt;Algorithms are hard&lt;/h2&gt;

&lt;p&gt;Before the final decomposition, the agents had already produced an implementation, unit tests, end-to-end tests, documentation, multiple code audits, numerical arguments, and accepted formal proofs. Those earlier passing artifacts said the requirement had been implemented correctly. It had not.&lt;/p&gt;

&lt;p&gt;Those artifacts agreed because the same failure pattern propagated from one to the next. One calendar day became a week. Guaranteed coverage became isolated reachability. Capacities from mutually exclusive programs were added together. Later, coverage became the whole requirement, allowing a proof to certify an unnecessarily large schedule. Green checks showed that each substituted design behaved as specified. They were not independent evidence that we had solved the original problem.&lt;/p&gt;

&lt;p&gt;After all of that, the fixed-catalog problem was finally easy to say: for the cards that remain eligible across all five visits, minimize the largest supporting-card count on any visit while showing each card at least once. Saying it clearly did not make it easy for an agent to preserve both parts through an implementation, tests, audits, and proofs.&lt;/p&gt;

&lt;p&gt;Lean’s value was not another green artifact. It forced the argument into pieces I could challenge: a counting lower bound, a generic selector guarantee, a replaceable product schedule, and a bounded comparison against Go. Lean did not discover that decomposition. The agents also produced accepted proofs of an impossible day and of coverage without minimization. Reading those statements literally exposed the missing objective, but deciding whether a theorem described the product remained my job.&lt;/p&gt;

&lt;p&gt;This wasn’t one model having one bad run. I tried to repair the work with Claude Opus 4.6, Claude Opus 4.8, Claude Opus 5, and Codex using GPT-5.6 Sol. None independently arrived at both the invariant I had stated and an implementation that preserved it. They found different problems and produced more artifacts, but changing models didn’t break the pattern. Each could accept or recreate a nearby requirement and then generate convincing evidence for its own version of the problem.&lt;/p&gt;

&lt;p&gt;Three things now seem clear. Passing artifacts are not independent evidence when they inherit the same changed requirement. Lean proves the statement it receives, not the behavior I meant. And the formal invariant, the product schedule, and the check against the implementation have to remain separate and inspectable.&lt;/p&gt;

&lt;p&gt;The next problem is whether an agent can expose that decomposition before it has already built the implementation, the tests, and the proof around a nearby requirement.&lt;/p&gt;
</description>
				<pubDate>Mon, 24 Aug 2026 04:00:00 +0000</pubDate>
				<link>https://christophermeiklejohn.com/ai/zabriskie/development/agents/2026/08/24/every-card-will-show.html</link>
				<guid isPermaLink="true">https://christophermeiklejohn.com/ai/zabriskie/development/agents/2026/08/24/every-card-will-show.html</guid>
			</item>
		
			<item>
				<title>The Safeguards Have to Run · Two abandoned notebooks showed me the difference between a research rule and a working control.</title>
				<description>&lt;p&gt;&lt;em&gt;This is Part 2 of &lt;a href=&quot;/series/the-machine-in-the-lab/&quot;&gt;The Machine in the Lab&lt;/a&gt;, a series about using an autonomous research program built from large language models (LLMs) to build SetScope, a live Goose song guesser.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;In the first research notebook, I told an autonomous program to keep training and test data separate. Its report showed a clean split. It wasn’t.&lt;/p&gt;

&lt;p&gt;How could both be true?&lt;/p&gt;

&lt;p&gt;The program was learning whether two recordings were performances of the same song. It created pairs of recordings, then randomly divided those pairs between training and testing. A performance called A could appear with B during training and appear again with C during testing. The rows were different, but the program had already encountered A. That meant the test could reward it for recognizing a performance it had seen rather than learning what made two performances instances of the same song.&lt;/p&gt;

&lt;p&gt;This is the kind of problem a working safeguard should prevent. Instead, the rule existed in the research plan while the code quietly violated it.&lt;/p&gt;

&lt;p&gt;The LLM wasn’t just writing code that I reviewed step by step. I supplied the objective, the audio collection, and the rules. The program proposed experiments, wrote and ran code, interpreted results, and chose what to investigate next without asking me to approve every decision. I called each sustained run a notebook: the accumulated data, code, experiments, reports, and decisions for one research direction.&lt;/p&gt;

&lt;p&gt;The original objective was to build &lt;a href=&quot;/series/the-machine-in-the-lab/science-at-llm-speed/#the-research-loop-gets-faster&quot;&gt;SetScope&lt;/a&gt;, a system that could listen to a live &lt;a href=&quot;https://www.goosetheband.com&quot;&gt;Goose&lt;/a&gt; show and name the song while the band was still playing it. A live version can change in speed, length, arrangement, and improvisation while remaining recognizably the same song.&lt;/p&gt;

&lt;p&gt;The first song-identification experiments produced a positive result: measurements from the composed sections appeared to distinguish one song from another across live performances. That suggested the composed parts preserved much of a song’s identity while the improvised parts accounted for much of the variation between performances. So the program started asking what those same measurements could tell us about the improvisation. Its reports arrived quickly and appeared to form a coherent account of the music, so I let it continue. That detour became Notebook 1.&lt;/p&gt;

&lt;h2 id=&quot;notebook-1&quot;&gt;Notebook 1&lt;/h2&gt;

&lt;p&gt;Correcting the pair split reduced the reported performance of the classifier that used measurements taken only from the audio. There was still useful signal in it, but the original result hadn’t tested whether it could generalize to performances it had never encountered. The program had acknowledged the rule about separate evidence without implementing a split that enforced it at the level of a complete performance.&lt;/p&gt;

&lt;p&gt;The notebook had already expanded beyond that classifier. One later experiment tried to identify open-ended improvisation by comparing an opening section, a middle section, and a closing section of each track. When actual musical boundaries were unavailable, the implementation treated the first and last ninety seconds as composed material and the middle as the jam.&lt;/p&gt;

&lt;p&gt;I asked the program to evaluate the detector against a separate group of 44 performances that had not been used to choose its numerical cutoffs. Six were too short for the method, leaving 38 analyzable cases. The fixed windows placed composed material that occurred after the opening ninety seconds inside the region treated as the jam. The detector repeatedly treated changes within those composed passages as evidence of open improvisation.&lt;/p&gt;

&lt;p&gt;That audit rejected this detector. It did not provide a clean test of the entire path that led to it, because earlier results had already influenced which features, thresholds, and questions the program pursued. I stopped Notebook 1.&lt;/p&gt;

&lt;h2 id=&quot;notebook-2&quot;&gt;Notebook 2&lt;/h2&gt;

&lt;p&gt;Notebook 2 continued the improvisation work under a written methodology intended to prevent the first notebook’s failures. It began on April 30 and assigned each song permanently to one of three uses: trying methods, choosing among them, or one final test that was supposed to remain unopened until the method was finished. It also rejected the blanket ninety-second division in favor of boundaries chosen for each song from the music itself.&lt;/p&gt;

&lt;p&gt;The code didn’t follow those rules. The first implementation again set the opening and closing regions to ninety seconds. Later analyses reused measurements cut at those boundaries. The method had warned against the assumption, but nothing compared the running code with the method and stopped the work.&lt;/p&gt;

&lt;p&gt;The final test wasn’t protected either. The locked collection assigned every performance of five songs, 25 recordings at the time, to remain unopened until the method was finished. Development analyses accessed four of those five groups, accounting for 22 of the 25 recordings, while the program was still choosing its method. A label in a data file said the groups were reserved. Nothing prevented the program from opening them.&lt;/p&gt;

&lt;p&gt;I had started Notebook 2 while Notebook 1 was still present for reference. On May 4, I told the program to remove every Notebook 1 file so it couldn’t contaminate the ongoing work. The cleanup removed the visible scripts, reports, figures, and data from the main project folder. But it couldn’t make Notebook 2 a clean restart: the new work had already inherited the old ninety-second default.&lt;/p&gt;

&lt;p&gt;By the time I stopped Notebook 2 on May 6, its fixed segmentation had propagated into analyses, figures, two draft blog posts available by direct link, and listening exercises. I had also put accounts of the earlier work on the project’s public site and sent listening exercises to friends. I don’t know how many people saw the pages, but the autonomous process had produced two coherent bodies of work that I couldn’t trust.&lt;/p&gt;

&lt;p&gt;A later inventory on May 17 exposed another failure. After both notebooks had been removed from the project folder, 1,120 generated feature and measurement files from the abandoned work remained in reusable storage outside that folder. A subsequent notebook had loaded 131 of those files while calculating one of its numerical cutoffs. Removing them changed the number only slightly and didn’t materially change the affected results. But the supposed clean slate hadn’t been clean, and the program had no record showing where every generated file had come from.&lt;/p&gt;

&lt;h2 id=&quot;the-safeguards-have-to-run&quot;&gt;The safeguards have to run&lt;/h2&gt;

&lt;p&gt;What had the written safeguards actually protected? In practice, very little: every named boundary could be crossed without stopping the run.&lt;/p&gt;

&lt;p&gt;A performance-level split should have rejected any training and test pair that shared a performance. Reserved data should have been inaccessible until a recorded release step. Generated files should have carried their source history, and a purge should have verified that no descendant remained in shared storage. A run whose code contradicted its written method should have stopped before producing dependent results.&lt;/p&gt;

&lt;p&gt;Those checks belong inside the autonomous process. My job is to define and approve the constraints, not to reconstruct every data assignment, generated file, and numerical constant after the program finishes. If the system requires that reconstruction, it has not automated the research process. It has automated the production of work for a human auditor.&lt;/p&gt;

&lt;p&gt;A natural-language instruction can describe a safeguard. It cannot enforce one.&lt;/p&gt;

&lt;p&gt;After Notebook 2, I set the improvisation work aside and returned the program to SetScope’s original Goose song-identification problem. A new live show offered evidence that no earlier notebook could have seen. But each show could be new only once. Once the program used that show to decide what to do next, was it still independent evidence or had it become part of development? That’s the question in Part 3. Part 4 returns to the separate problem exposed by the ninety-second detector: whether a consistent measurement actually represents the music named in the claim.&lt;/p&gt;
</description>
				<pubDate>Tue, 18 Aug 2026 12:00:00 +0000</pubDate>
				<link>https://christophermeiklejohn.com/series/the-machine-in-the-lab/two-notebooks-lost-series/</link>
				<guid isPermaLink="true">https://christophermeiklejohn.com/series/the-machine-in-the-lab/two-notebooks-lost-series/</guid>
			</item>
		
			<item>
				<title>The Whole Night</title>
				<description>&lt;p&gt;On Thursday, August 13, Goose opened the West Coast run in San Diego with “Animal.” Sixteen people in Zabriskie had said they were going. Seventeen had said they were on the couch. By the time the band finished the “726” encore, fourteen people had written 160 messages in the Chomp and five had sent 48 reactions across a twelve-song show.&lt;/p&gt;

&lt;p&gt;It was a regular Thursday show, which made it useful for understanding what had changed. The RSVPs split almost perfectly between &lt;strong&gt;GOING&lt;/strong&gt; and &lt;strong&gt;COUCH&lt;/strong&gt;, and the app had to do a different job before the show, in the room, at home, and after the encore.&lt;/p&gt;

&lt;p&gt;Seven weeks earlier, I published &lt;a href=&quot;/ai/zabriskie/development/2026/06/30/the-app-that-lives-between-shows.html&quot;&gt;The App That Lives Between Shows&lt;/a&gt;. The live room worked, The Lot had finally given the app a home screen, and the watch clients were running in our field builds. I wrote that the hard product problem was the gap between one show and the next. The Lot existed, but it did not yet know what time it was.&lt;/p&gt;

&lt;p&gt;I was not exactly wrong, but I had drawn the boundary in the wrong place. A show does not begin when the first note lands or end when a live flag changes in the database. The plan starts days earlier. During the show, a person standing at the rail and a person watching from a couch need almost opposite things from the same room. The setlist, conversation, and people remain after the band leaves the stage. Over the next seven weeks, Zabriskie started to follow that longer edge.&lt;/p&gt;

&lt;h2 id=&quot;the-lot-learned-what-time-it-was&quot;&gt;The Lot Learned What Time It Was&lt;/h2&gt;

&lt;p&gt;In June, I wrote as though The Lot was mostly finished because it had become the home screen. It was not. It was a beautiful pile.&lt;/p&gt;

&lt;p&gt;Every useful thing we built earned a card, and every card wanted a permanent place. A live show, an upcoming RSVP, a Crate recommendation, an anniversary, an invitation, a recap, a bracket, and the latest thing somebody did could all be individually correct and collectively exhausting. Opening the app increasingly answered the question, “What does Zabriskie have?” That is a bad question for a home screen.&lt;/p&gt;

&lt;p&gt;The better question is: &lt;strong&gt;what should we do right now?&lt;/strong&gt;&lt;/p&gt;

&lt;h3 id=&quot;one-lead-not-every-feature&quot;&gt;One Lead, Not Every Feature&lt;/h3&gt;

&lt;p&gt;The Lot now has two clocks: the show’s and the person’s. It knows whether somebody is going, couch touring, or still deciding; whether a room is live; whether a show just ended; what is waiting for a response; and which cards have been sitting unseen. From those candidates it chooses one lead move, a short schedule, and a few supporting cards.&lt;/p&gt;

&lt;p&gt;A live RSVP wins. A show later tonight comes next. The morning after a show, the unfinished rating and Chomp can take the space. An anniversary can win a quiet day. If none of those has a claim, the time of day breaks the tie: something to spin in the morning, something to return to during the workday, the local scene or a stream in the evening, a deeper archive dig on the weekend. The page says which moment it was programmed for because otherwise the cards simply appear to move around by accident.&lt;/p&gt;

&lt;figure style=&quot;max-width: 420px; margin: 2rem auto;&quot;&gt;
  &lt;img src=&quot;/img/zabriskie-whole-night-ios-dark-lot.png&quot; alt=&quot;The Lot in Zabriskie for iOS in dark mode on a Monday evening, showing my show and posting activity, a morning-after Goose recap, and recent Flow activity&quot; loading=&quot;lazy&quot; style=&quot;width: 100%; height: auto; border-radius: 24px;&quot; /&gt;
  &lt;figcaption&gt;My actual Monday evening Lot: the unfinished Goose night wins the lead; the Flow sits beneath it.&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p&gt;This required the page to do something software is generally bad at: decline to show correct information. We capped both the number of sections and the number of cards inside them. One busy section cannot consume the page. A card that has been buried for several visits gets one chance to move up. When the final card in a section is dismissed, its empty heading leaves too. The schedule stays short. The rest of the app still exists; the home screen is no longer required to prove it.&lt;/p&gt;

&lt;h3 id=&quot;tonight-is-a-room-not-a-date&quot;&gt;Tonight Is a Room, Not a Date&lt;/h3&gt;

&lt;p&gt;The earlier Lot split a live show, the other shows tonight, and a person’s couch plans into separate pieces. The new &lt;strong&gt;Tonight&lt;/strong&gt; card puts the open rooms first and the later rooms after them. A live row carries the current song and set, the stream when one exists, and one compact door into the Chomp. The same row can still change a plan between &lt;strong&gt;GOING&lt;/strong&gt; and &lt;strong&gt;COUCH&lt;/strong&gt;. Several couch RSVPs across a run collapse into &lt;strong&gt;Your Couch Tour&lt;/strong&gt;, an itinerary with the nights left, the correct local tune-in times, and the other people staying home.&lt;/p&gt;

&lt;p&gt;A pending &lt;strong&gt;Bring Your People&lt;/strong&gt; invitation now lands on The Lot instead of depending on somebody finding the original notification again. &lt;strong&gt;What’s New&lt;/strong&gt; moved out of a floating toast and into a card that can be read and dismissed. Received reactions say who reacted and which post they found. A locked opener call becomes a status instead of pretending to be another urgent action. These are small decisions, but they are the difference between a page that understands the state of something and a page that merely knows the feature exists.&lt;/p&gt;

&lt;figure style=&quot;max-width: 420px; margin: 2rem auto;&quot;&gt;
  &lt;img src=&quot;/img/zabriskie-whole-night-ios-dark-lot-tonight.png&quot; alt=&quot;The Lot in Zabriskie for iOS in dark mode showing This Weekend recommendations and my circle around Goose&apos;s West Coast Tour 2026&quot; loading=&quot;lazy&quot; style=&quot;width: 100%; height: auto; border-radius: 24px;&quot; /&gt;
  &lt;figcaption&gt;One screen lower: weekend options and the West Coast run, weighted by who is already in the plan.&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;h3 id=&quot;the-archive-has-to-return-somewhere&quot;&gt;The Archive Has to Return Somewhere&lt;/h3&gt;

&lt;p&gt;The Lot also became the place where a completed show can come back with a reason. The next morning it can ask how the night was, collect the jam of the show, and reopen the original Chomp. Much later, &lt;strong&gt;On This Day&lt;/strong&gt; can put a personal anniversary beside a notable show from the archive. A bookmarked show can return with the explanation for why it was saved. A new release can arrive with its listening party already attached. A scheduled Tape Night can reopen an old room at an actual time instead of leaving another permanent card on the page.&lt;/p&gt;

&lt;p&gt;The quieter parts matter too. The Crate contributes one pick, not fifteen. The Flow contributes a glimpse, not a second feed. Weekend shows, incoming releases, saved shows, Local Scene, recent love, tour statistics, recommendations, and the year so far all have a way onto the page, but none is entitled to remain there. There is no follow graph underneath this. The inputs are attendance, plans, taste, the person’s own history, and what the community is doing now.&lt;/p&gt;

&lt;div style=&quot;display: grid; grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); gap: 1.5rem; max-width: 920px; margin: 2rem auto; align-items: start;&quot;&gt;
  &lt;figure style=&quot;margin: 0;&quot;&gt;
    &lt;img src=&quot;/img/zabriskie-whole-night-ios-dark-lot-history.png&quot; alt=&quot;The Lot in Zabriskie for iOS in dark mode showing who is going to Goose at Red Rocks, a Field of Vision festival recap, and upcoming Goose couch dates&quot; loading=&quot;lazy&quot; style=&quot;width: 100%; height: auto; border-radius: 24px;&quot; /&gt;
    &lt;figcaption&gt;The social edge of a future show beside a recap that can finally return after the festival.&lt;/figcaption&gt;
  &lt;/figure&gt;
  &lt;figure style=&quot;margin: 0;&quot;&gt;
    &lt;img src=&quot;/img/zabriskie-whole-night-ios-dark-lot-discovery.png&quot; alt=&quot;The Lot in Zabriskie for iOS in dark mode showing my populated Goose West Coast Couch Tour itinerary with local times and stream links&quot; loading=&quot;lazy&quot; style=&quot;width: 100%; height: auto; border-radius: 24px;&quot; /&gt;
    &lt;figcaption&gt;Nine nights on the couch become one tour itinerary, with local times and stream links attached.&lt;/figcaption&gt;
  &lt;/figure&gt;
&lt;/div&gt;

&lt;h3 id=&quot;the-work-was-the-editing&quot;&gt;The Work Was the Editing&lt;/h3&gt;

&lt;p&gt;Then we kept sanding it. Every card received art and a place inside a real section. Buttons stopped changing shape from one card to the next. We fixed shadows, contrast, touch targets, haptics, sharing, loading, and dismissal, then added guardrails because each one had already drifted at least once. Cards can share their own image instead of an authenticated URL nobody else can open. The page survives the old mobile bundles that do not know about its new priorities. Its tests now fail when a card escapes its section, a call to action changes shape, or one kind of card crowds out everything else.&lt;/p&gt;

&lt;p&gt;I count 187 non-merge commits in this window touching Lot-specific handlers, rendering, tests, or those guardrails. That number does not prove that we chose correctly. It does explain why the four screens above are not the home screen I published in June.&lt;/p&gt;

&lt;p&gt;The Lot is where the rest of this story first appears. It notices that a night is coming, hands the person into the right room, and then decides what from the night is worth bringing back.&lt;/p&gt;

&lt;h2 id=&quot;before-doors&quot;&gt;Before Doors&lt;/h2&gt;

&lt;p&gt;The Lot can decide which night matters. The show page still has to help finish the plan. The June page already brought RSVPs, seating, trip planning, opener calls, and the live setlist into one place. The August work focused on the awkward part: a plan exists, but it is not finished.&lt;/p&gt;

&lt;figure style=&quot;max-width: 420px; margin: 2rem auto;&quot;&gt;
  &lt;img src=&quot;/img/zabriskie-whole-night-ios-dark-before-doors.png&quot; alt=&quot;Zabriskie for iOS in dark mode showing the countdown and Going, Couch, and Want Tix groups for an upcoming Goose show&quot; loading=&quot;lazy&quot; style=&quot;width: 100%; height: auto; border-radius: 24px;&quot; /&gt;
  &lt;figcaption&gt;The plan before doors: one show, three kinds of intent.&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p&gt;&lt;strong&gt;Bring Your People&lt;/strong&gt; lets someone pull another person into the plan. &lt;strong&gt;Ticket Check&lt;/strong&gt; asks the embarrassingly necessary follow-up: you said you were going, but do you actually have a ticket? The rest of the run now sits beside the current night. A &lt;strong&gt;Song Wish List&lt;/strong&gt; collects what people hope to hear, while &lt;strong&gt;Played Here Before&lt;/strong&gt; brings back the band’s earlier visits to the room. For someone staying home, the same run becomes a tune-in schedule in the correct timezone.&lt;/p&gt;

&lt;p&gt;The home-screen widgets follow the same clock. On an ordinary day they can surface an anniversary, a recent show, or the year so far. As a show approaches, they become a countdown and then a live glance. Gap Watch can pull an overdue song into the days before the next RSVP. Call the Opener already existed in June; what was missing was the answer. Now the result can find its way back to the person who made the call instead of disappearing into the setlist.&lt;/p&gt;

&lt;p&gt;This is still small-community software. Since July 1, people have sent 11 show invitations and added 28 songs to wish lists. That establishes that somebody used them. It does not establish a habit, and it certainly does not allow me to turn two small counters into a victory lap.&lt;/p&gt;

&lt;h2 id=&quot;at-the-rail&quot;&gt;At the Rail&lt;/h2&gt;

&lt;p&gt;Once the first note lands, the problem reverses. Before doors, the app asks people to make decisions. At the show, it should ask for as little attention as possible.&lt;/p&gt;

&lt;p&gt;We call that posture the &lt;strong&gt;rail&lt;/strong&gt;: someone is in the room, service is bad, both hands are occupied, and looking at a phone is already one thing too many. The Apple Watch and Wear OS faces can carry the current song, the current set, the setlist so far, and the latest Chomp messages. From there, one tap can send a reaction or like a message; a canned reply or dictation can answer the room without pretending that a watch is a very small phone.&lt;/p&gt;

&lt;p&gt;Most of the work was in the cases that are easy to omit from a demo. The watches recover from stale state and keep long song titles inside a round screen. They distinguish set break from encore. If two shows overlap, a long press opens a switcher so the watch follows the room the person intended to follow. The encore produces one deliberate haptic. After the show, the face holds the final setlist in an afterglow instead of immediately dropping back to an idle screen.&lt;/p&gt;

&lt;p&gt;Patrick led the Wear OS client, while I built most of the Apple Watch client. We made parity a project rule: when a live-show capability belongs on the wrist, both clients get it. Parity does not mean copying the phone. If the watch requires the same attention as the phone, we have missed the reason to build it.&lt;/p&gt;

&lt;p&gt;The phones moved in the same direction. The Live Activity on iOS and the ongoing notification on Android carry the current song without requiring the app to remain open. Haptics mark actions and actual show transitions rather than buzzing indiscriminately. The full setlist and conversation are still there, but the default at the venue should be a glance followed by putting the device away.&lt;/p&gt;

&lt;h2 id=&quot;on-the-couch&quot;&gt;On the Couch&lt;/h2&gt;

&lt;p&gt;At home, getting out of the way is the wrong goal. The stream is on, a keyboard is available, and the conversation may be the reason someone opened Zabriskie at all.&lt;/p&gt;

&lt;p&gt;We call that posture the &lt;strong&gt;salon&lt;/strong&gt;. The composer remains in reach, the thread gets the screen space, and a show switcher appears in the header when several bands are live. Opening the keyboard expands the conversation rather than crushing it. The setlist and the full live page remain one move away.&lt;/p&gt;

&lt;p&gt;The RSVP supplies gravity, never permission. &lt;strong&gt;GOING&lt;/strong&gt; can prioritize a show on the wrist and change the countdown and copy on The Lot. &lt;strong&gt;COUCH&lt;/strong&gt; turns that copy toward tuning in. Neither choice locks or hides anything. The larger RSVP-weighted phone layout is still unfinished, and that is intentional for now: someone at the venue can spend set break reading every message, while someone at home can send a watch reaction while cooking dinner.&lt;/p&gt;

&lt;p&gt;This is why the San Diego split mattered. Sixteen people said they were at the show and seventeen said they were on the couch. Both groups reached the same setlist and the same Chomp, but the interfaces around that shared room could begin to pull in different directions. When Goose reached “Big Modern!” in the second set, the wrist could carry the song title for someone in the amphitheater while the phone could give the thread room for someone at home. The production counts do not tell me which RSVP group wrote each message, and I do not need them to. The near-even split shows that both postures were present.&lt;/p&gt;

&lt;p&gt;Across all shows since July 1, 33 people have sent 1,303 live messages and 27 have sent 4,962 reactions. The reactions come from a smaller group and arrive quickly. That is the point: sometimes a sentence is too much, but a reaction is still enough to say that somebody else heard the same thing.&lt;/p&gt;

&lt;h2 id=&quot;after-the-lights&quot;&gt;After the Lights&lt;/h2&gt;

&lt;p&gt;In San Diego the encore was “726.” The older app mostly treated that as the end of its useful work. The setlist completed, the live state shut down, and the room receded into the archive.&lt;/p&gt;

&lt;p&gt;Now the night has an exit. The watch and phone hold the final state in an afterglow. The next morning, The Lot can ask for a one-tap rating and return the completed show with its setlist and recap. The rating also lives on the show’s page instead of existing only on a temporary home-screen card. The recap reopens the original Chomp, because a database flag changing from live to completed is not a good reason to break the conversation.&lt;/p&gt;

&lt;figure style=&quot;max-width: 420px; margin: 2rem auto;&quot;&gt;
  &lt;img src=&quot;/img/zabriskie-whole-night-ios-dark-after-lights.png&quot; alt=&quot;Zabriskie Band Mode for Goose in dark mode showing the August 13 encore 726 as a recent bust-out&quot; loading=&quot;lazy&quot; style=&quot;width: 100%; height: auto; border-radius: 24px;&quot; /&gt;
  &lt;figcaption&gt;The August 13 encore, already returning as part of the archive.&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p&gt;Much later, the same show can return through &lt;strong&gt;On This Day&lt;/strong&gt;. What used to be one card is now a reel that can place a personal anniversary beside a notable archival show and the other concerts that happened on that date. The watch and widgets can surface those anniversaries too. We do not need to manufacture a new commemorative post every year. The record already exists, with the people, setlist, poster, recording links, and conversation attached.&lt;/p&gt;

&lt;p&gt;This changed how I think about the archive. A completed show is reference data, but it is also the durable object that the live room, the next morning, and the anniversary can all return to.&lt;/p&gt;

&lt;h2 id=&quot;the-rest-of-the-app-changed-with-it&quot;&gt;The Rest of the App Changed With It&lt;/h2&gt;

&lt;p&gt;The show lifecycle became the easiest way for me to understand the work, but not everything we shipped fits inside one Goose show.&lt;/p&gt;

&lt;p&gt;My favorite counterexample came from Phish’s five-night run at Madison Square Garden. The first night revealed the rule: each show would rebuild one year of the band’s history, moving from 1992 through 1996. What should the app do with that information before the second night?&lt;/p&gt;

&lt;p&gt;For the next four nights, Zabriskie turned the pattern into &lt;strong&gt;Setlist Prophecy&lt;/strong&gt;. The show page read the real rotation from that night’s year, removed anything already played earlier in the run, and surfaced likely songs, era-specific opener choices, long-gap candidates, and the gags that belonged to that version of Phish. It even made a call on Trey’s shirt.&lt;/p&gt;

&lt;div style=&quot;display: grid; grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); gap: 1.5rem; max-width: 920px; margin: 2rem auto; align-items: start;&quot;&gt;
  &lt;figure style=&quot;margin: 0;&quot;&gt;
    &lt;img src=&quot;/img/zabriskie-setlist-prophecy-phish-1993-dark.png&quot; alt=&quot;Dark-mode Zabriskie Setlist Prophecy board for Phish&apos;s 1993-themed night, showing likely songs and era-specific gags&quot; loading=&quot;lazy&quot; style=&quot;width: 100%; height: auto; border-radius: 24px;&quot; /&gt;
    &lt;figcaption&gt;Night two: the 1993 board, with likely songs and era gags still on the table.&lt;/figcaption&gt;
  &lt;/figure&gt;
  &lt;figure style=&quot;margin: 0;&quot;&gt;
    &lt;img src=&quot;/img/zabriskie-setlist-prophecy-phish-1994-dark.jpg&quot; alt=&quot;Dark-mode Zabriskie Setlist Prophecy board for Phish&apos;s 1994-themed night, showing Trey&apos;s shirt prediction, likely songs, and setlist-note signals&quot; loading=&quot;lazy&quot; style=&quot;width: 100%; height: auto; border-radius: 24px;&quot; /&gt;
    &lt;figcaption&gt;Night three: the 1994 board after we added the shirt call and signals from the archive&apos;s setlist notes.&lt;/figcaption&gt;
  &lt;/figure&gt;
&lt;/div&gt;

&lt;p&gt;After the encore, the prophecy graded itself. The wardrobe call landed four nights out of five, ending with Trey’s gold velour Clifford Ball shirt on the 1996 finale. That night also delivered two of the four stranger calls: &lt;strong&gt;Harpua&lt;/strong&gt; closed the first set, and &lt;strong&gt;Crosseyed and Painless&lt;/strong&gt; supplied the promised wink toward the &lt;em&gt;Remain in Light&lt;/em&gt; Halloween show later in 1996.&lt;/p&gt;

&lt;figure style=&quot;max-width: 420px; margin: 2rem auto;&quot;&gt;
  &lt;img src=&quot;/img/zabriskie-whole-night-ios-dark-phish-1996.png&quot; alt=&quot;Zabriskie for iOS in dark mode showing Phish&apos;s July 29 finale at Madison Square Garden&quot; loading=&quot;lazy&quot; style=&quot;width: 100%; height: auto; border-radius: 24px;&quot; /&gt;
  &lt;figcaption&gt;The fifth night of the time machine: 1996, back in the completed-show archive.&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p&gt;People played along. Twelve people made 39 RSVPs across the five shows. Seven made 22 Call the Opener guesses, while the live rooms produced 221 messages from six people and 614 reactions from three. None of the opener guesses landed.&lt;/p&gt;

&lt;p&gt;Setlist Prophecy was not a machine-learning oracle. It was an editorial game built from the archive, and its misses were part of the result. It gave people something specific to argue about before doors and something funny to score after the encore without asking them to populate a separate, empty room.&lt;/p&gt;

&lt;p&gt;The same archive problem appeared in the rest of the app. The &lt;strong&gt;Composer&lt;/strong&gt; became one screen for sharing an album, film, book, or show. Search begins while someone types; recent listening and attendance supply suggestions; choosing an item opens the form in place. If I have written about the same album before, the old post can return as context for the new one instead of vanishing down the Flow.&lt;/p&gt;

&lt;figure style=&quot;max-width: 420px; margin: 2rem auto;&quot;&gt;
  &lt;img src=&quot;/img/zabriskie-whole-night-ios-dark-flow.png&quot; alt=&quot;The Zabriskie Flow in dark mode showing a richly formatted post with video, text, reactions, and navigation&quot; loading=&quot;lazy&quot; style=&quot;width: 100%; height: auto; border-radius: 24px;&quot; /&gt;
  &lt;figcaption&gt;The Flow in the same dark-mode iOS build.&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p&gt;Albums now open into an &lt;strong&gt;Album Hub&lt;/strong&gt;: the record is divided by side, tracks connect to their live histories, and the page carries its discussion and scheduled listening parties. The &lt;strong&gt;Library&lt;/strong&gt; became one search across recordings, albums, films, and books, with Relisten audio playing inside the app. We also opened a &lt;strong&gt;Local Scene&lt;/strong&gt; pilot around Pittsburgh. A show should not need decades of setlist data before it is allowed to exist in Zabriskie.&lt;/p&gt;

&lt;p&gt;The catalog grew from 102 band records and 38,894 shows in the June post to 340 band records and 47,669 shows. Those numbers require an immediate qualification: this is not 238 new bands with equally complete histories. The increase includes Local Scene and broader roster imports. The deeper additions include the Slip, Dave Matthews Band, Dave &amp;amp; Tim, Medeski Martin &amp;amp; Wood, and more of the Kimock archive.&lt;/p&gt;

&lt;p&gt;The mobile shell also started behaving like one. Browser confirmation boxes became house sheets and toasts. Sheets drag and spring; buttons have haptics; swipe-back works from the edge. Photos can be pinched, panned, and dismissed. A universal link can survive login and return to the page that opened it. The app can distinguish being offline from merely waiting on a request. No single item belongs in a headline. Together, they remove the recurring feeling that a website has been placed inside an app-shaped frame.&lt;/p&gt;

&lt;h2 id=&quot;the-receipt&quot;&gt;The Receipt&lt;/h2&gt;

&lt;p&gt;From July 1 through August 17, &lt;strong&gt;1,025 pull requests&lt;/strong&gt; merged into Zabriskie: 717 from me and 308 from Patrick. The iOS build number moved from 53 to 112. Android moved from 51 to 157, with Wear at 158.&lt;/p&gt;

&lt;p&gt;That is an absurd amount of software for less than seven weeks. It is also an easy number to misread. Pull-request throughput does not prove that the product improved. It does explain how a regular show night came to touch so many surfaces that either did not exist, or barely existed, when I wrote the June post.&lt;/p&gt;

&lt;p&gt;The community grew much more slowly than the software. Registered users went from 426 to &lt;strong&gt;508&lt;/strong&gt;, an increase of 82 people. There were &lt;strong&gt;50 weekly active users&lt;/strong&gt; and 98 monthly active users when I pulled the production numbers for this post. The attendance ledger grew from 4,598 to &lt;strong&gt;6,020 records&lt;/strong&gt;. Since July 1, 24 people have written 84 album, book, film, photo, listening, show-review, or video posts, alongside the 1,303 live messages and 4,962 live reactions.&lt;/p&gt;

&lt;p&gt;It is a real room, and it is still a small one. Most weeks after the first half of July brought single-digit signups. Live activity remains concentrated in a core group. I like the direction of those numbers. None of them means that we have solved growth or broad participation.&lt;/p&gt;

&lt;p&gt;Some of the new ideas did almost nothing. During its full fourteen-day test, a pre-show conversation box drew eight comments from two people across seven shows, with no replies. The new show-photo wall received zero photos. Venue Intel received zero tips. Watch-party membership remained at zero.&lt;/p&gt;

&lt;p&gt;It would be easy to blame the size of the community, except 24 people made 84 regular content posts during the same stretch. The narrower explanation is more useful: adding an empty box to a show page does not give somebody a reason to be the first person inside it.&lt;/p&gt;

&lt;p&gt;That is the useful correction underneath all of this work. Features that fit a motion people were already making—checking who is going, glancing at the current song, reacting in a live room, rating the night the next morning—have a chance to become part of the show. Features that ask somebody to populate a new room alone have a much harder job.&lt;/p&gt;

&lt;p&gt;Seven weeks ago I wrote that the gap between shows was the hard half of the product. I still believe that. What changed is that I no longer see the show as a two-and-a-half-hour block with empty time on either side. It has a long edge: the plan before doors, the split between the rail and the couch, the ride home, the next morning, and the date years later when the whole thing returns.&lt;/p&gt;

&lt;p&gt;On August 13, that edge ran from 33 people making a plan, through “Animal,” 160 messages, twelve songs, and one final “726.” The app stayed with the night after the music stopped.&lt;/p&gt;

&lt;p&gt;We can carry the whole night now. I do not yet know how often people will want us to, whether enough of them will, or whether the pieces that worked during a Goose or Phish run will become habits between them. That is the next problem, and it is much harder to solve than merging another thousand pull requests.&lt;/p&gt;
</description>
				<pubDate>Mon, 17 Aug 2026 23:00:00 +0000</pubDate>
				<link>https://christophermeiklejohn.com/ai/zabriskie/development/2026/08/17/the-whole-night.html</link>
				<guid isPermaLink="true">https://christophermeiklejohn.com/ai/zabriskie/development/2026/08/17/the-whole-night.html</guid>
			</item>
		
			<item>
				<title>Science at LLM Speed · Research-like output is cheap. A valid claim still needs evidence that can show it is wrong.</title>
				<description>&lt;p&gt;&lt;em&gt;This is Part 1 of &lt;a href=&quot;/series/the-machine-in-the-lab/&quot;&gt;The Machine in the Lab&lt;/a&gt;, a seven-part series about building SetScope, a live Goose song guesser, with an autonomous research program built from large language models (LLMs).&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Earlier this year, the leaders of one of the largest machine-learning conferences found references in submitted papers to publications that did not exist.&lt;/p&gt;

&lt;p&gt;This was not a thought experiment about what a language model might do. It was not a benchmark, a standardized test, in which researchers asked a model to write a literature review and then counted the invented citations. These were papers submitted to the 2026 International Conference on Learning Representations (ICLR), accompanied by bibliographies that were supposed to describe prior work. The conference built a screening system, sent the flagged references to senior reviewers, and then had the conference leaders check them again. Every paper with a confirmed hallucinated reference was desk rejected, meaning it was rejected before normal peer review.&lt;/p&gt;

&lt;p&gt;The most interesting part of the &lt;a href=&quot;https://blog.iclr.cc/2026/03/31/a-retrospective-on-the-iclr-2026-review-process/&quot;&gt;conference leaders’ account&lt;/a&gt; is how much work it took to establish that a paper in a bibliography was not a paper. The automated system incorrectly flagged some real references. Translated titles looked suspicious. At least three people reviewed every confirmed case. The conference did not trust one detector to decide whether a citation existed.&lt;/p&gt;

&lt;p&gt;Whatever process produced those bibliographies did not catch the error.&lt;/p&gt;

&lt;p&gt;This is a strange failure because background research is one of the activities these systems appear to make almost effortless. Ask for the important papers in an area and a model will produce authors, titles, dates, summaries, and a tidy account of how the work fits together. The answer has the shape of a literature review before anyone has done the work of reviewing the literature.&lt;/p&gt;

&lt;p&gt;Sometimes the papers are real. Sometimes a real paper is attached to a claim it does not support. Sometimes the title, authors, or venue are slightly wrong. And sometimes the entire reference was generated because a plausible citation completed the paragraph.&lt;/p&gt;

&lt;p&gt;All four cases look approximately the same on the page.&lt;/p&gt;

&lt;h2 id=&quot;a-plausible-bibliography-can-describe-papers-that-never-existed&quot;&gt;A plausible bibliography can describe papers that never existed&lt;/h2&gt;

&lt;p&gt;We knew this behavior existed before it began appearing in conference submissions. In 2023, William H. Walters and Esther Isabelle Wilder asked OpenAI’s GPT-3.5 and GPT-4 language models to produce short literature reviews across 42 topics. GPT stands for Generative Pre-trained Transformer. They checked 636 generated citations. In their &lt;a href=&quot;https://www.nature.com/articles/s41598-023-41032-5&quot;&gt;controlled study&lt;/a&gt;, 55 percent of the GPT-3.5 citations and 18 percent of the GPT-4 citations referred to works the researchers could not verify as existing. Many citations to real work also contained substantial bibliographic errors.&lt;/p&gt;

&lt;p&gt;Those numbers do not tell us what fraction of references in current scholarship were written by a model. They describe particular models, prompts, and topics in a controlled experiment. A broken citation in a paper does not contain a record of the tool that produced it. People invented, mangled, and copied references long before ChatGPT.&lt;/p&gt;

&lt;p&gt;But the ICLR cases establish the part that matters here: nonexistent references made it through whatever process their authors used to produce a submission for a major research conference. The scholarly form was present. The scholarship that the form purported to reference was not.&lt;/p&gt;

&lt;p&gt;The obvious response is to connect the model to a search system. OpenScholar, for example, searches a collection of 45 million open-access papers, retrieves relevant passages, generates an answer with citations, and then checks the answer against those sources. In the authors’ &lt;a href=&quot;https://www.nature.com/articles/s41586-025-10072-4&quot;&gt;evaluation&lt;/a&gt;, the system substantially improved the citation accuracy and correctness of a general-purpose model on scientific synthesis tasks.&lt;/p&gt;

&lt;p&gt;This is better, but the reason it is better matters. The model did not become more trustworthy by writing a more confident explanation of its sources. The system changed the task. Candidate claims had to be attached to retrieved passages. Citations could be checked against documents that existed outside the generated answer. Expert readers could inspect whether the source supported the sentence.&lt;/p&gt;

&lt;p&gt;Fetching the sources does not make a literature review true. A real paper can still be misunderstood, cited out of context, or asked to carry a larger claim than its experiment supports. What the sources supply is the possibility of contradiction. There is now something outside the paragraph that can show the paragraph is wrong.&lt;/p&gt;

&lt;p&gt;That difference between fluent support and evidence capable of contradiction is the problem I kept encountering in my own work.&lt;/p&gt;

&lt;h2 id=&quot;the-research-loop-gets-faster&quot;&gt;The research loop gets faster&lt;/h2&gt;

&lt;p&gt;The citation problem is unusually easy to see because existence is a fairly crisp property. Either the referenced document can be found or it cannot. Most research decisions are not like that.&lt;/p&gt;

&lt;p&gt;A model can now help formulate a question, search for related work, write experiment code, select a statistical test, produce a figure, explain the figure, draft the limitations, and review the finished manuscript. Each output can become the input to the next step. A single person can move through activities that once required more time, more specialized assistance, or both.&lt;/p&gt;

&lt;p&gt;I wanted to use that capability to build an automatic live song guesser. I run &lt;a href=&quot;https://zabriskie.app&quot;&gt;Zabriskie&lt;/a&gt;, a community for live-music fans. During a livestream by the band &lt;a href=&quot;https://www.goosetheband.com&quot;&gt;Goose&lt;/a&gt;, viewers who want a running setlist have to wait for a person or an external service to recognize each song and enter it. I wanted Zabriskie to listen to the stream, identify the current song, and post the guess while the show was still happening. That product became SetScope.&lt;/p&gt;

&lt;p&gt;This is a different problem from &lt;a href=&quot;https://swh.princeton.edu/~cuff/ele201/files/Wang03-shazam.pdf&quot;&gt;Shazam&lt;/a&gt;. Shazam fingerprints a short excerpt and looks for the same recording in its database. SetScope hears a performance that has never existed before. Goose might play the song faster or slower, change the key or arrangement, stretch the opening, or move from the written song into a long improvisation. The system cannot search for an identical recording. It has to recognize the musical identity that survives those changes.&lt;/p&gt;

&lt;p&gt;I also wanted artificial intelligence (AI) to do the research needed to build it. I would give the system recordings paired with their correct song titles and tell it what SetScope needed to do. It would come up with ideas for recognizing songs, write the code, use those examples to train models, run tests, examine the mistakes, and decide what to try next. I would not approve every step. That was the human-out-of-the-loop part of the project.&lt;/p&gt;

&lt;p&gt;The implementation speed is what made that plan plausible for one person. During my doctoral degree (PhD), I learned how much of a systems experiment happens before the experiment. &lt;a href=&quot;/publications/filibuster-socc-2021.pdf&quot;&gt;Filibuster&lt;/a&gt;, the testing framework I was building for applications spread across multiple services and computers, could not evaluate an existing application until that application had been adapted to run through software hooks that let Filibuster observe and alter its behavior. Building the prototype took me three months of full-time engineering. It used &lt;a href=&quot;https://opentelemetry.io/&quot;&gt;OpenTelemetry&lt;/a&gt;, a standard for collecting information about what happens as a request moves through an application.&lt;/p&gt;

&lt;p&gt;That only got the application to the starting line. Filibuster works by deliberately introducing failures while an application’s existing tests run. If the target application did not already have useful tests, I had to write them before the framework had anything meaningful to explore. For one production application used in an evaluation paper, that took another six months.&lt;/p&gt;

&lt;p&gt;With the agents I use now, I believe I could produce a first implementation of many of those software hooks and supporting tests in days, perhaps a single day. During the SetScope work, agents built audio scanners, catalogs of recordings and their source history, programs for measuring audio and comparing song recognizers, and detailed logging inside the live browser on that timescale.&lt;/p&gt;

&lt;p&gt;None of those components is a controlled equivalent of the OpenTelemetry work, and I cannot rerun my PhD for comparison. The estimate describes what I believe would have happened, not a measured speedup. Code arriving quickly would not establish that the added software hooks preserved the application’s behavior or that the generated tests supported the evaluation. Those questions would still require evidence. But even compressing the first implementation from months into days changes which research projects one person can plausibly attempt.&lt;/p&gt;

&lt;p&gt;While I was writing this, Jeff Dean, Sanjay Ghemawat, Oriol Vinyals, and Quoc Le left Google to form a company called &lt;a href=&quot;https://www.axios.com/2026/08/06/googles-ai-leadership-shuffle&quot;&gt;Discovery Loop&lt;/a&gt;. Public descriptions say its goal is to &lt;a href=&quot;https://www.itpro.com/business/leadership/deepmind-ceo-demis-hassabis-steps-aside-amid-google-leadership-shake-up&quot;&gt;automate experimental loops&lt;/a&gt; in science and engineering: propose an experiment, run it, evaluate what happened, and decide what to try next.&lt;/p&gt;

&lt;p&gt;That is the same kind of autonomous researcher I was trying to use to build SetScope. Their target is science and engineering broadly. Mine was Goose song identification.&lt;/p&gt;

&lt;p&gt;When a system can choose and run the next experiment by itself, a bad result can do more than produce one wrong answer. It can change what the system tries next. Examples meant to remain unseen can enter development and alter the next hypothesis. A convenient stand-in for the real question can quietly replace the question. A test of one part of a product can become a claim about a product that never ran.&lt;/p&gt;

&lt;h2 id=&quot;a-polished-analysis-is-not-necessarily-a-scientific-result&quot;&gt;A polished analysis is not necessarily a scientific result&lt;/h2&gt;

&lt;p&gt;Autonomous research is the most ambitious version of the problem, but a smaller version is already common. LLMs have made it cheaper to produce something that looks like the output of a research process: a paper, methodology page, interactive analysis, benchmark, data product, or long post with equations and charts.&lt;/p&gt;

&lt;p&gt;This can be genuinely useful. In a participatory study of 15 people performing generative-AI-assisted data analysis, &lt;a href=&quot;https://doi.org/10.1145/3663384.3663389&quot;&gt;Drosos and colleagues&lt;/a&gt; observed participants using a model for information gathering, hypothesis generation, and analysis strategy. The same participants described verification as effortful and time-consuming.&lt;/p&gt;

&lt;p&gt;Useful analysis does not have to occur inside a university or become a paper. A fan project can rank performances, organize an archive, publish its formulas, and offer an excellent discovery tool. But if it also claims that the score reveals a property of improvisation, then the human judgments used as correct answers, the unit being measured, and their relationship to that musical property become part of the claim. Equations, transparent code, and polished charts do not settle whether the evidence is adequate.&lt;/p&gt;

&lt;p&gt;This problem existed before LLMs. What changes is the cost of producing the complete package. Code, prose, caveats, visualizations, and a memorable result can now arrive together, quickly enough that their coherence feels like evidence that the empirical work occurred.&lt;/p&gt;

&lt;p&gt;My own project produced exactly that kind of convincing package, more than once. The form did not create the errors. It made them harder to notice.&lt;/p&gt;

&lt;h2 id=&quot;the-reviewer-might-be-a-model-too&quot;&gt;The reviewer might be a model too&lt;/h2&gt;

&lt;p&gt;One possible answer is review. Authors produce work quickly; reviewers slow it down, inspect the assumptions, and require the claims to survive contact with another person. Except the same systems have entered that loop.&lt;/p&gt;

&lt;p&gt;A 2024 study published at the International Conference on Machine Learning examined reviews from four major machine-learning conferences, including ICLR. Its &lt;a href=&quot;https://proceedings.mlr.press/v235/liang24b.html&quot;&gt;estimate across the full collection&lt;/a&gt; was that 6.5 to 16.9 percent of review text had been substantially modified or produced by language models, beyond minor writing assistance. That does not identify any particular review as machine-written. It does show that the tool was helping reviewers respond to claims as well as helping authors present them.&lt;/p&gt;

&lt;p&gt;There are constructive versions. In a &lt;a href=&quot;https://doi.org/10.1038/s42256-026-01188-x&quot;&gt;randomized study&lt;/a&gt; at ICLR 2025, some reviewers received model-generated suggestions about vague language, possible misunderstandings, and unprofessional comments. Some revised their reports, and evaluators who did not know which reviews received assistance rated the revisions as more informative.&lt;/p&gt;

&lt;p&gt;That is evidence that a model can improve one part of a human review process, not that it can replace peer review. A second model does not become independent review merely by being a second model. Two systems can share training data, conventions, blind spots, and a preference for the same fluent explanation. Adding agents changes the number of outputs. It does not necessarily change the source of judgment.&lt;/p&gt;

&lt;p&gt;Human review is not a magical external check either. Reviewers miss errors, disagree, rush, and reward familiar methods. The question is what new information and incentives each check adds. If every stage evaluates the same generated output in roughly the same way, the process can become impressively self-consistent without becoming more correct.&lt;/p&gt;

&lt;h2 id=&quot;the-machine-can-find-something-real&quot;&gt;The machine can find something real&lt;/h2&gt;

&lt;p&gt;There is an easy version of this essay in which every section supplies another example of AI making science worse. It would also be wrong.&lt;/p&gt;

&lt;p&gt;FunSearch used a language model to generate candidate programs for mathematical problems. The system executed them, scored them with a test supplied by the researchers, retained the strongest programs, and used those programs to guide further search. The resulting &lt;a href=&quot;https://www.nature.com/articles/s41586-023-06924-6&quot;&gt;Nature paper&lt;/a&gt; reported new solutions to a problem about arranging finite sets and useful strategies for packing items into a limited number of containers.&lt;/p&gt;

&lt;p&gt;The model was valuable because it could search a space of programs productively. It was not asked to decide, in prose, that its own program was interesting. The programs ran, the evaluator scored them, and other people could inspect the result.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://arxiv.org/abs/2504.08066&quot;&gt;AI Scientist-v2&lt;/a&gt; operated under a weaker judge. It generated machine-learning manuscripts end to end, with humans choosing initial ideas and selecting the best completed run. One manuscript scored above the acceptance threshold at an ICLR workshop. The authors’ inspection also found missing citations, examples that may have influenced both development and evaluation, incorrect figure interpretations, and unused code for a statistical technique intended to make confidence scores match observed probabilities.&lt;/p&gt;

&lt;p&gt;These checks do not provide the same evidence. A program evaluator can reject a candidate against a specified property. An experiment can contradict a prediction. A source can fail to support a sentence. Human review adds judgment, but a reviewer can still be persuaded by the same polished explanation as everyone else.&lt;/p&gt;

&lt;p&gt;The system becomes more useful when generated candidates encounter information that was not produced by the same act of generation and can return an unwelcome answer.&lt;/p&gt;

&lt;p&gt;Before treating a check as independent, I now ask three questions: What information does it add that was unavailable to the process that generated the result? What unfavorable answer can it return? Which precise claim would that answer reject?&lt;/p&gt;

&lt;h2 id=&quot;then-my-own-research-loop-failed-twice&quot;&gt;Then my own research loop failed twice&lt;/h2&gt;

&lt;p&gt;I then changed the task. Instead of improving SetScope’s song guesses, I asked the system to use the recordings to study improvisation. I had years of recordings paired with song titles and other annotations, ways to measure rhythm, harmony, and texture, models that turn audio into numbers that can be compared, and an LLM agent that could scan recordings, organize their source history, calculate those measurements, train programs to classify them, run evaluations, analyze failures, and modify the live application. What had been an implausibly large solo project looked tractable.&lt;/p&gt;

&lt;p&gt;The recordings seemed to offer a way to investigate harder questions about improvisation: whether measurements of the music could identify when a performance left its composed structure, whether different forms of jamming produced distinct patterns in those measurements, and whether the same patterns appeared across performances. Those questions were more interesting than simple song identification. They were also much harder to define.&lt;/p&gt;

&lt;p&gt;I am a PhD-trained systems researcher. I know what a holdout is: a final test set kept unseen while a system is developed. I also know why an experimental method has to match its implementation. That was not enough. The first research notebook, a running workspace of code, data, experiments, and reports, expanded from song recognition into several improvisation questions without preserving one final test capable of evaluating the path the system chose.&lt;/p&gt;

&lt;p&gt;The audit found several problems, not one neat leak. Most decisively, a detector for Type II improvisation, where the band leaves a song’s composed structure for open-ended playing, failed on 44 recordings that had not been used to develop it. I had described the detector on a research webpage and shown it to two friends. It mistook changes inside composed songs for improvisation. I took the webpage down and started again.&lt;/p&gt;

&lt;p&gt;For the restart, I permanently assigned many of the same recordings to three jobs: examples the models could learn from, examples used to choose among competing methods, and a final test that neither process was supposed to see. I also wrote a new method that explicitly warned against fixed ninety-second jam boundaries. The first implementation reused them anyway. The research loop never stopped to report that the code contradicted the declared method. Working from those reports, I put two draft research posts online at their direct web addresses and sent friends listening assignments built from the analysis.&lt;/p&gt;

&lt;p&gt;The consequences were larger than the lines of code that caused them. I was responsible for putting the work online and involving other people’s time. But the lesson could not be that I should manually reconstruct every assignment of recordings and trace every fixed numerical value before accepting any result. A system that requires that level of supervision is not running the research process autonomously. It is generating work for a human auditor. The system needed to prevent violations of its rules or report them when they occurred. It had done neither. I deleted the second notebook, took the drafts down, and abandoned the listening study.&lt;/p&gt;

&lt;p&gt;We eventually gave the system its original job again: improve the live song guesser. On August 13, the running SetScope product proposed the correct identity at least once for 10 of 12 recorded song performances while the band was playing. It also missed songs, switched guesses at the wrong time, and exposed failures in the live audio path.&lt;/p&gt;

&lt;p&gt;This was a product field test, not a formal whole-show accuracy estimate, and the complete operational record belongs later in this series. Neither audio from that performance nor its completed setlist existed during development. The surviving record shows what reached SetScope’s decision logic, not confirmed viewer-visible delivery.&lt;/p&gt;

&lt;p&gt;The project would not exist at its current scale without LLMs. The failures taught me that speed changes the location of the work. Producing the next output becomes cheap. Establishing what the output means, what information entered it, and what could show it is wrong does not.&lt;/p&gt;

&lt;p&gt;The machine can participate in research. The harder problem is deciding what it may do without asking, what evidence must survive each iteration, and what can stop a bad result before it becomes the premise of the next experiment. This series is about learning to build those constraints after discovering, repeatedly, that a persuasive report was not one of them.&lt;/p&gt;

&lt;h2 id=&quot;what-comes-next&quot;&gt;What Comes Next&lt;/h2&gt;

&lt;p&gt;Part 2 reconstructs the two notebooks I deleted. Part 3 follows the holdout boundary we repeatedly crossed and the record needed to know which evidence each later version had already used.&lt;/p&gt;

&lt;p&gt;Part 4 shows how a reviewed experiment can pass every check and still misinterpret the music. Part 5 moves the test into the browser, where capture, controller behavior, and the rendered result become part of the experiment.&lt;/p&gt;

&lt;p&gt;Part 6 describes the constraints we built around the research loop. Part 7 returns after Goose’s August run with the versions that actually ran, their guesses, their failures, and the repairs made between shows.&lt;/p&gt;

&lt;p&gt;Next: two notebooks that contained weeks of analysis and almost no result I could still defend.&lt;/p&gt;
</description>
				<pubDate>Sat, 15 Aug 2026 18:00:00 +0000</pubDate>
				<link>https://christophermeiklejohn.com/series/the-machine-in-the-lab/science-at-llm-speed/</link>
				<guid isPermaLink="true">https://christophermeiklejohn.com/series/the-machine-in-the-lab/science-at-llm-speed/</guid>
			</item>
		
			<item>
				<title>One Writer · Our tools assume one writer, and assume that writer is a human.  Nothing computes what a change reads and writes at runtime, so the only known fix is brute force priced for organizations.</title>
				<description>&lt;p&gt;&lt;em&gt;In this blog post, I discuss three days in July 2026 when a single agent session ran away from me, and what those three days revealed about the concurrency assumptions buried in our development tooling.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Six weeks ago I wrote &lt;a href=&quot;/ai/zabriskie/agents/reliability/testing/2026/06/10/the-test-suite-was-the-incident.html&quot;&gt;The Test Suite Was the Incident&lt;/a&gt;: my test suite had grown a pile of shared data nobody owned, every pull request paid to rebuild it, and the resulting failures had nothing to do with the changes under review.  That cost me about $180 in one night.&lt;/p&gt;

&lt;p&gt;I got a worse one.  It lasted three days, and in one twenty-four-hour stretch of it I burned through an entire Codex 20x max plan.&lt;/p&gt;

&lt;p&gt;This post is not really about that, though.  It is about a property of our tooling that the three days made impossible to ignore.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Nearly every layer of this assumes one writer, and assumes that writer is a human.&lt;/strong&gt;  Git hands you a conflict and waits.  Code review assumes somebody reads.  A migration sequence assumes somebody is assigning the order.&lt;/p&gt;

&lt;p&gt;Each of those protocols terminates in a person.  That is fine while there is exactly one, and while they are, in fact, a person.&lt;/p&gt;

&lt;p&gt;The agent runtime turns out to be on that list too, which I did not expect.  It spawned eighty-four workers into a single checkout without being able to say what any one of them would read or write, and that is the same question git can’t answer about a diff.&lt;/p&gt;

&lt;p&gt;That assumption was invisible for forty years because nothing ever bound it at my scale.  Agents break both halves at once: there are many of them, and not one of them is the person the protocol was waiting for.  Git is the partial exception, and I will come to why the exception does not help.&lt;/p&gt;

&lt;p&gt;Nothing in the stack detects the violation when it happens.  It gets caught later, somewhere else, attributed to the wrong change, and paid for at full price.&lt;/p&gt;

&lt;p&gt;Some context for readers arriving fresh.  &lt;a href=&quot;https://zabriskie.app&quot;&gt;Zabriskie&lt;/a&gt; is a social app for live-music fans, and it’s also a deliberate experiment: I’m building a real, deployed, actually-used application almost entirely with AI agents (agents that wrote the features, agents that wrote the tests guarding those features, and agents that now open most of the pull requests), in order to find out what that’s like and, more usefully, where it breaks.  I’ve written almost none of the code.&lt;/p&gt;

&lt;p&gt;That framing matters, because several things below look like obvious mistakes and are.  I let agents design a migration scheme with only another agent reviewing it, I stopped reading most diffs, and I let sixty-four pull requests go up in a single day, all of which a careful engineer would tell you not to do and would be right about.  But the point of running an experiment at the extreme is to find the walls.  That week I found several at once.&lt;/p&gt;

&lt;p&gt;A &lt;strong&gt;migration&lt;/strong&gt;, throughout, is a versioned SQL file that changes the database schema.  &lt;strong&gt;CI&lt;/strong&gt; is the automated checking that runs on every proposed change: build the app, spin up a fresh database, run the tests.&lt;/p&gt;

&lt;p&gt;Here is the shape of the three days.  Treat these numbers as texture, not as evidence, for a reason I will get to.&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt; &lt;/th&gt;
      &lt;th style=&quot;text-align: right&quot;&gt;24 Jul&lt;/th&gt;
      &lt;th style=&quot;text-align: right&quot;&gt;25 Jul&lt;/th&gt;
      &lt;th style=&quot;text-align: right&quot;&gt;26 Jul&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;pull requests opened&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;19&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;64&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;28&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;pull requests merged&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;20&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;50&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;30&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;incidents logged&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;0&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;6&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;301&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;Days in that table are UTC; clock times in the narrative below are Eastern (where I was).  The burst ran past midnight UTC: 301 incidents on the 26th plus 60 more before 1 AM on the 27th, so 361 for the burst.  Every incident count below is scoped to that window, and 253 rows is the total before 24 July.&lt;/p&gt;

&lt;p&gt;I offer that comparison as a sense of scale and not as a baseline, for the following reason.  Those incidents exist because a standing instruction tells agents to log their own mistakes, and ten minutes into the worst night I tightened that instruction.  The log therefore measures &lt;em&gt;reported&lt;/em&gt; failures.&lt;/p&gt;

&lt;p&gt;Look at the daily series and it gets worse: there are days that week with ten and twenty merged pull requests and zero logged incidents, which at any real failure rate means nobody was logging rather than nothing broke.  In short, I can’t give you a trustworthy baseline.  What follows rests on mechanism and on a few dated, checkable events, not on 361.&lt;/p&gt;

&lt;h2 id=&quot;the-session&quot;&gt;The Session&lt;/h2&gt;

&lt;p&gt;Late Saturday night the queue jammed.  Sixty-four pull requests had gone up that day, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;main&lt;/code&gt; was red, and nineteen open pull requests were stuck behind a suite that could not tell me which of them was broken.  At 12:31 AM I opened a session with &lt;a href=&quot;https://platform.openai.com/docs/codex&quot;&gt;Codex&lt;/a&gt; and complained that CI was wasting too much money.&lt;/p&gt;

&lt;p&gt;Codex read that and hired a workforce.&lt;/p&gt;

&lt;p&gt;Over seventeen hours that session made 81 spawn calls, producing 74 direct children; those children spawned 10 more, for 84 threads.  Then 358 calls waiting on them, 96 listing them, 85 sending follow-up work, and 13 interrupting them, which comes to six hundred and thirty-three tool calls of pure management overhead (spawn, wait, list, follow up, interrupt) against 54 messages from me over the same seventeen hours.  All 84 ran in the same checked-out copy of the repository.&lt;/p&gt;

&lt;p&gt;Trivial parallelization, at scale, with a coordinator that only managed agents, on a toolchain built for one writer.  The rest of this post is about why that combination is so much worse than it sounds.&lt;/p&gt;

&lt;p&gt;I asked afterward why it had spawned anything, since I never requested it.  A language model asked why it did something produces plausible narrative, not introspection, and I apply that same skepticism to the self-reported incidents above.&lt;/p&gt;

&lt;p&gt;So the quotes below are not evidence, and I’m not offering them as any.  I had already reached the same conclusion from the tool-call counts before I asked.  I print them because they state that conclusion more plainly than I did, and because there is something worth looking at in a system that can describe the failure this precisely and could not avoid it.&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;I chose to spawn multiple agents even though you did not ask me to. The environment permitted proactive delegation, but that was permission, not a requirement. I treated the availability of agents as a reason to use them instead of first asking whether they would reduce time, cost, or risk.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;It then diagnosed why the task was a bad candidate:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;A PR queue is largely serial because every merge changes &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;main&lt;/code&gt;, which changes the integration state of every remaining PR. The work needed one authoritative coordinator moving through the queue in order. Parallel agents could not independently merge overlapping PRs without continuously invalidating one another’s assumptions.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;On what those 358 waits were doing:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;The agents’ outputs created additional coordination work for me. I had to read their findings, compare conflicting recommendations, inspect supporting evidence, and decide how to combine them. The agents therefore generated work for the coordinator instead of reliably removing work from the critical path.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;A system with no notion of its own concurrency was handed a concurrency primitive and used it, reasonably, on a task the substrate could not support.  What that substrate is, and why nothing in it objected, is the rest of this post.&lt;/p&gt;

&lt;p&gt;One number deserves care.  I do not know how many of those 84 threads &lt;em&gt;wrote&lt;/em&gt; to the checkout instead of reading it; from Codex’s own account many were investigating.  A reader is harmless and a writer is not, and nothing in the toolchain drew the distinction or could.  That’s the same missing primitive one level up: not even the agent runtime knew which of its children were writers.&lt;/p&gt;

&lt;h2 id=&quot;the-substrate&quot;&gt;The Substrate&lt;/h2&gt;

&lt;p&gt;Start with the layer that did anticipate this, because it is the one people reach for.  Branches are optimistic concurrency control; worktrees go further, giving each writer a physically separate checkout so that two agents can hold two versions of the tree at once.  That works.&lt;/p&gt;

&lt;p&gt;However, git’s isolation stops at the edge of the source tree.  A worktree gives an agent its own files.  It doesn’t give it its own database, port range, mock server, or position in the migration sequence.  Everything below the filesystem is shared and singular.&lt;/p&gt;

&lt;p&gt;That assumption was invisible to &lt;em&gt;me&lt;/em&gt; because it never bound me.  It has bound large organizations for a very long time, however, and over the last two decades they built the response to it: merge queues, hermetic builds, database-per-test, trunk-based development, automated culprit-finding, and whole infrastructure teams whose only job is to keep the thing moving.&lt;/p&gt;

&lt;p&gt;So what is new here isn’t the concurrency.  &lt;strong&gt;It’s that a solo developer now operates in the regime that used to require an infrastructure organization, with none of the infrastructure and no headcount to build it.  Agents removed the cap on my arrival rate.  They didn’t hand me Google’s build system.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Partial isolation is then its own trap.  A worktree gives you the &lt;em&gt;feeling&lt;/em&gt; of a private workspace (clean tree, own branch, no file collisions), and it reads as properly parallel right up until two private workspaces write the same database row.  Nothing errors.  Nothing warns.  The contention surfaces later, somewhere else, as a red X on an unrelated pull request.&lt;/p&gt;

&lt;p&gt;Worse, the isolation &lt;em&gt;below&lt;/em&gt; git is advisory, and agents have to choose it.  Mine routinely don’t.  One incident reads &lt;em&gt;“PR 1868 isolated E2E attempt fell back to shared ports”&lt;/em&gt;: the agent tried to isolate its test environment, isolation failed, and nothing stopped the run.&lt;/p&gt;

&lt;p&gt;Worse still, a worktree is cut from a commit and stays there, so it’s isolated &lt;em&gt;in the past&lt;/em&gt; and decays with every merge.  When I changed one of the agent gates in July (they’re tracked scripts in the repository, not &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.git/hooks&lt;/code&gt;, so every worktree carries its own copy pinned to the commit it was cut from) the fix merged at 1:48 AM, and four hours later &lt;strong&gt;73 of 74 worktrees were still running the old one&lt;/strong&gt;.  A stale worktree can’t detect its own staleness, so it goes green about a world that no longer exists, and the error is deferred to the only actor holding current &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;main&lt;/code&gt;, which is CI.&lt;/p&gt;

&lt;h3 id=&quot;convergence-and-invariant-preservation&quot;&gt;Convergence and Invariant Preservation&lt;/h3&gt;

&lt;p&gt;One framing before the specifics, because it unifies them.  Every merge mechanism here is built to &lt;strong&gt;terminate&lt;/strong&gt;, and none of them is built to &lt;strong&gt;preserve invariants&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Git is honest about the cases it cannot decide: on a textual conflict it halts and asks you.  The trouble is the far more common case, where it doesn’t halt, produces a merge confidently, and the invariant it never knew about is now false.&lt;/p&gt;

&lt;p&gt;That distinction is the oldest lesson in the replicated-data literature, and it has a canonical counterexample.  Take a replicated map where each field merges independently under its own perfectly reasonable rule.  One field holds a person’s name; another holds the length of that name.  Two replicas concurrently write different names.&lt;/p&gt;

&lt;p&gt;Each field converges exactly as specified, and the result is a record whose &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;name&lt;/code&gt; came from one replica and whose &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;length&lt;/code&gt; came from the other, with the invariant tying them now false.  Nothing merged incorrectly.  The composition of correct local merges is simply not a correct global merge.&lt;/p&gt;

&lt;p&gt;Closing that gap is the point of work like Balegas and colleagues’ &lt;a href=&quot;https://www.dpss.inesc-id.pt/~rodrigo/indigo_eurosys15.pdf&quot;&gt;Indigo&lt;/a&gt;, which enforces application invariants over eventually consistent stores, and which I have &lt;a href=&quot;/pl/2018/11/14/ipa.html&quot;&gt;written about before&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Some of what follows has that shape.  The first case does not, and I will not dress it up.&lt;/p&gt;

&lt;h3 id=&quot;the-sealed-prefix&quot;&gt;The Sealed Prefix&lt;/h3&gt;

&lt;p&gt;This project has 1,388 migrations, and rebuilding a test database from all of them on each of eight parallel test machines is as slow as it sounds.  So, in response to my complaining for weeks that CI was too expensive, an agent froze a database snapshot into the repository for CI to restore, applying only what came after.&lt;/p&gt;

&lt;p&gt;It’s a good optimization: it saves 13 to 18 minutes of machine time per run.  It merged at 5:01 AM Eastern on the third day.&lt;/p&gt;

&lt;p&gt;Fifty-four minutes later the first pull request failed, because its migration no longer sorted after the newly frozen prefix.  Then another.  By late morning a single incident covers four at once: &lt;em&gt;“Four queued PRs carried migrations older than the sealed CI baseline suffix.”&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The diagnosis here is ordinary, and a critic will say so.  Other agents had already opened pull requests that appended migrations; one agent then sealed a new prefix underneath them; those two streams of work conflicted; and nothing anywhere in the system noticed until CI rejected the queued pull requests several hours later.  That’s parallel work colliding and finding out late, not a subtle invariant bug.&lt;/p&gt;

&lt;p&gt;Caching a prefix of an ordered log made the collision loud and retroactive, but it did not invent the collision.  So the loop closes: I complained CI was expensive, an agent made CI cheaper, and the mechanism became a new source of CI failures against the whole queue.&lt;/p&gt;

&lt;h3 id=&quot;merge-functions-and-runtime-state&quot;&gt;Merge Functions and Runtime State&lt;/h3&gt;

&lt;p&gt;Git detects conflicts over lines of text; my conflicts live in shared runtime state.  From that week: &lt;em&gt;“Song-call E2E suites deleted each other’s shared pending call.”&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Two test files, no overlapping lines, both green alone, and the row one of them depends on is the row the other deletes.  The collision is in the database at runtime, not in the diff.&lt;/p&gt;

&lt;p&gt;Those two files are the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;name&lt;/code&gt; and the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;length&lt;/code&gt;.  They share no lines, each merges cleanly, each is individually green, and the invariant binding them is false the moment they land together.  No improvement to git’s merge algorithm catches that, because the property is not a property of any file: it is a property of the composition, and git has no representation of the composition to check.&lt;/p&gt;

&lt;h3 id=&quot;verification-under-composition&quot;&gt;Verification Under Composition&lt;/h3&gt;

&lt;p&gt;Which gives the sharpest version of the problem: &lt;strong&gt;green(A on base) and green(B on base) does not imply green(merge of A and B).&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The test suite is the only thing in my pipeline that checks the invariant at all, since git checks text and timestamps check ordering.  So the tests are my invariant checker.&lt;/p&gt;

&lt;p&gt;I do run them on the composition.  Pull requests rebase onto current &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;main&lt;/code&gt; and merge in sequence, each one retested before it lands, and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;main&lt;/code&gt; itself is checked after land.  That catches the break.  It just catches it late, and only by paying the serial tax: rebase, retest, merge, next.  That tax is exactly the queue-wide cost the sealed prefix made visible on the morning when one seal invalidated everything queued behind it.&lt;/p&gt;

&lt;p&gt;Serialization is the honest answer, and I already pay it.&lt;/p&gt;

&lt;p&gt;Speculative merge queues try to buy the same guarantee back with parallelism: build the candidate futures, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;main+A&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;main+A+B&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;main+A+B+C&lt;/code&gt;, test them concurrently, and discard a failure from the middle.  OpenStack’s &lt;a href=&quot;https://zuul-ci.org/&quot;&gt;Zuul&lt;/a&gt; has gated that way since 2012; &lt;a href=&quot;https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/configuring-pull-request-merges/managing-a-merge-queue&quot;&gt;GitHub’s merge queue&lt;/a&gt; ships a version; and &lt;a href=&quot;https://bors.tech/&quot;&gt;bors&lt;/a&gt;, which lands Rust, does the cheaper batch-and-bisect variant.  It works, and it works by brute force, which I will come back to.&lt;/p&gt;

&lt;h3 id=&quot;shared-environments&quot;&gt;Shared Environments&lt;/h3&gt;

&lt;p&gt;One port range, one development database, one mock server.&lt;/p&gt;

&lt;p&gt;I do have a script assigning each worktree its own ports by hashing the directory name (the right idea), and it hashes into 99 slots.  With dozens of worktrees on disk the birthday math makes a collision effectively certain, and two agents get handed the same port whenever the colliding pair happens to be running at once.&lt;/p&gt;

&lt;h3 id=&quot;undivided-work&quot;&gt;Undivided Work&lt;/h3&gt;

&lt;p&gt;Trivial parallelism doesn’t work on a problem that was never split into non-conflicting units.  The agents were pointed at one jammed queue and found their own boundaries, which were mostly the same boundaries.&lt;/p&gt;

&lt;p&gt;What looked like a coordinator was a process manager: spawn, wait, list, follow up, six hundred and thirty-three tool calls of overhead, all of it without any deep context about which pieces of work actually interacted.  A coordinator that only manages agents can’t divide work it does not understand, so the children rediscover the same failures, edit the same new scripts, and invalidate one another’s assumptions.  Codex said as much afterward: the agents generated work for the coordinator instead of removing work from the critical path.&lt;/p&gt;

&lt;p&gt;Classified by root cause, 245 of the 361 entries collapse into ten systemic problems, 73 are one-off product bugs, and 43 I could not classify.  Here are the ten, because a taxonomy whose majority is invisible isn’t a taxonomy:&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;systemic root cause&lt;/th&gt;
      &lt;th style=&quot;text-align: right&quot;&gt;incidents&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;tooling the session was writing that same day&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;87&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;no per-agent workspace isolation&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;33&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;errors skipped, swallowed, or reported as success&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;32&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;affected-spec selection far too broad&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;20&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;migration order versus the sealed baseline&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;16&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;tests leaning on shared fixture state&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;16&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;recovery work that would not converge&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;12&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;CI cost structure&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;12&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;evidence produced against a base that moved&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;10&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;verification run without its dependencies&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;7&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;I would defend the shape of that table rather than the ratio: workers under a shallow coordinator rediscover the same things, often enough to dominate a log.&lt;/p&gt;

&lt;p&gt;Look down the column and the majority is exactly that failure.  The top row alone (tooling the session was writing that same day, 87 incidents) is agents redoing and colliding on the same work, because nothing holding the session had enough context to keep them off each other’s toes.  Add isolation failures and swallowed errors and the top three account for 152.&lt;/p&gt;

&lt;p&gt;Those are not ordinary defects that concurrency merely made frequent.  They are the artifact of coordination without context: a parent counting agents instead of understanding the work, so that the children rediscover and rewrite the same things, file the same findings, and hand the parent the job of reconciling them.  The seal and the port hash still have boring mechanical fixes, and recovery-that-would-not-converge and CI cost structure are process problems.  The rediscovery is neither.&lt;/p&gt;

&lt;p&gt;The one that is irreducible in a different sense is the sixth row: tests leaning on shared fixture state, or green(A) and green(B) failing to imply green(A+B) in ordinary clothes.&lt;/p&gt;

&lt;p&gt;It accounts for sixteen of the 361, tied with the seal for the smallest systemic bucket, and I want to put that number in front of you instead of letting you find it by subtraction.  Sixteen out of 361 is four percent.&lt;/p&gt;

&lt;p&gt;So that substrate problem is the &lt;em&gt;rarest&lt;/em&gt; thing that happened that week.  It is also the only one where catching it means building the composition and running the entire suite against it, which I do, serially, late, and which is precisely the expensive thing.  It is therefore the only one on the list where I can’t tell you what an &lt;em&gt;affordable&lt;/em&gt; fix would even look like.  The common problem looks much simpler: parallelism without a division of the work, supervised by something that was counting agents instead of understanding them.&lt;/p&gt;

&lt;p&gt;Except that the two are the same problem, and it took me most of a week to see it.  What a coordinator needs in order to divide work is exactly a read/write set per unit, which is the primitive this whole post is about, one layer up.  Mine had no way to compute one, so it couldn’t divide anything, so eighty-four agents went out and found the same boundaries and did the same work on top of each other.  That’s the top two rows of the table, a hundred and twenty entries, a third of everything I logged.  I’m counting two rows and not the three above, because swallowed errors are ordinary bad error handling and belong to a different complaint.&lt;/p&gt;

&lt;p&gt;So the four percent isn’t a rare problem I’ve chosen to write about instead of the common one.  It’s the identical question, “what does this thing read, and what does it write,” asked about a code change instead of about an agent.  At the code layer the question is open, which is the research problem.  At the agent layer it’s answerable in principle, and my runtime simply didn’t ask, which is the more embarrassing half and the one my week actually went to.&lt;/p&gt;

&lt;h2 id=&quot;the-missing-primitive&quot;&gt;The Missing Primitive&lt;/h2&gt;

&lt;p&gt;So what would it take for some layer to detect any of this?&lt;/p&gt;

&lt;p&gt;Start with why detection is hard.  One pull request writes a row to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;show_attendance&lt;/code&gt;; another reads a count over it.  Neither diff says so.  The write goes through an HTTP handler, an ORM, and some SQL; the read goes through the app.  &lt;strong&gt;The conflict is a property of what the code does when it runs, and it’s not present in the text.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That leaves two ways to find it:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;Execute the composition and observe what breaks.&lt;/li&gt;
  &lt;li&gt;Compute the effects of each change and compare them without running anything.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Nobody can do the second precisely enough at the granularity where my conflicts live, so everybody does the first.  If you can’t compute effects, you can’t tell which pairs need checking, and so you pay for a full verification of every candidate future you are willing to buy.&lt;/p&gt;

&lt;p&gt;Uber’s &lt;a href=&quot;https://www.uber.com/ci/en/blog/slashing-ci-costs-at-uber/&quot;&gt;SubmitQueue&lt;/a&gt; needed a probabilistic model of which changes would land in order to prune the speculation tree, and that is less an optimization than a confession: you only guess your way through the tree when nothing can tell you which branches matter.&lt;/p&gt;

&lt;p&gt;So the state of the art converts an unsolved program-analysis problem into a compute purchase.  That is rational at Google’s scale and precisely the wrong trade at mine, because speculation costs queue depth times full suite times parallel environments, and agents raise queue depth.  The sanctioned fix for the correctness problem is more CI spend, scaling with the exact variable agents just multiplied.&lt;/p&gt;

&lt;p&gt;My two complaints turn out to be one complaint.  “CI is too expensive” and “composition failures show up only when the serial queue retests them” are the same missing primitive seen from two sides.&lt;/p&gt;

&lt;p&gt;Here is the size of it in my own logs:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Over three days, 510 CI runs were spent against 111 opened and 100 merged pull requests: roughly five full verifications per change that landed.&lt;/li&gt;
  &lt;li&gt;The runs attach to opened pull requests rather than merged ones, so read that as an order of magnitude and not a rate.&lt;/li&gt;
  &lt;li&gt;Of the 510, 112 failed or were cancelled outright, and cancellations are usually a new push superseding an old one rather than an escape.&lt;/li&gt;
  &lt;li&gt;Deleting every one of those 112 takes the multiplier from 5.1 to 4.0, not to 1.&lt;/li&gt;
  &lt;li&gt;The rest is ordinary iteration, some fraction of it rebasing onto a main that moved underneath, which is the same problem in ordinary clothes.&lt;/li&gt;
  &lt;li&gt;The primitive is therefore worth about a quarter of the excess, and only if every one of those failures turns out to be a composition escape, which I cannot show.  A serial queue stays slow either way.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Databases avoid all of this for one reason.  &lt;strong&gt;A transaction declares its read/write set.&lt;/strong&gt;  The system knows what each transaction touched, so it can detect conflicts, order what must be ordered, and let everything else proceed.&lt;/p&gt;

&lt;p&gt;That question (which operations may run without coordination) has an exact answer in the literature.  &lt;a href=&quot;https://www.bailis.org/papers/ca-vldb2015.pdf&quot;&gt;Bailis and colleagues&lt;/a&gt; named it &lt;strong&gt;invariant confluence&lt;/strong&gt;: a set of operations is I-confluent with respect to an invariant precisely when merging any two invariant-preserving reachable states that share a common ancestor yields another invariant-preserving state.&lt;/p&gt;

&lt;p&gt;This is a necessary and sufficient condition for running those operations coordination-free while maintaining the invariant, not a heuristic.  If your workload is I-confluent you can run coordination-free and stay correct; if it is not, coordination is a requirement, and &lt;strong&gt;no merge function will save you.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The result does not transfer to code cleanly, and it’s worth saying where it breaks.  I-confluence is defined over a merge operator on states, with reachability and a fixed invariant, whereas for code changes there is no defined merge over program states, and the invariant here is the test suite, which every merge rewrites.  What I’m borrowing is the shape of the question, not the theorem.&lt;/p&gt;

&lt;p&gt;Source code carries a partial declaration at best.  Build systems get closest: &lt;a href=&quot;https://bazel.build/&quot;&gt;Bazel&lt;/a&gt; and &lt;a href=&quot;https://nixos.org/&quot;&gt;Nix&lt;/a&gt; require every target to declare its inputs and outputs (a real read/write set at file granularity), and that is how affected-target computation works across enormous monorepos.&lt;/p&gt;

&lt;p&gt;However, file granularity is the wrong granularity here.  My two colliding pull requests touch entirely disjoint files.  A build graph would call them independent, and a build graph would be right, and they would still destroy each other, because the thing they share is a row in a database that neither one names.&lt;/p&gt;

&lt;p&gt;At the granularity where my conflicts live, nothing declares those effects.  So there is no conflict detection there, and without it you can’t tell which regime a pair of changes is in, and if you can’t tell, the only safe policy is to assume the worst and coordinate everything.&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;&lt;strong&gt;What is the runtime read/write set of a code change, and can it be computed precisely enough, and cheaply enough, to decide which changes are safe to integrate concurrently?&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I’ve already built a bad version of this without recognizing it.  My affected-spec selector maps a change to the tests it could affect, so that CI only runs what is relevant.  That’s a read-set estimator, and its failure mode is the research problem in miniature: it traced a one-screen change through a shared UI component, concluded the change could affect anything, and scheduled all 115 test files.&lt;/p&gt;

&lt;p&gt;An over-approximation collapsing to “conflicts with everything” is the same degenerate case as a lock covering the whole table, or as serializing every update to a dictionary even when the changes are to disjoint keys.  Sound, and useless.&lt;/p&gt;

&lt;p&gt;One objection here is that I am demanding a soundness nobody needs.  Large organizations run test selection that is deliberately imprecise and absorb the escapes: changes that should have been tested, were not, and break trunk after they land.&lt;/p&gt;

&lt;p&gt;It’s tempting to say I can’t absorb escapes because my blast radius is smaller, but that has it backwards: an escape into Google’s trunk blocks far more people than an escape into mine.  What they have, and I don’t, is containment.  Post-submit continuous builds, automated culprit-finding, and rollback tooling all bound what an escape costs.&lt;/p&gt;

&lt;p&gt;I have none of that.  What I have instead is a queue of agents that respond to a red trunk by independently rediscovering the failure, each one paying for its own full CI run, none of them asked to, and none of them aware that eleven others are doing exactly the same thing at exactly the same moment.  Escapes are cheaper for Google not because fewer people are blocked, since more are, but because they built the machine that catches and bounds the damage.  Without that machine, imprecise selection is a cost I can’t absorb.&lt;/p&gt;

&lt;p&gt;Prior art exists, and none of it quite lands:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;https://www.cs.purdue.edu/homes/xyzhang/fall07/Papers/p173-rothermel.pdf&quot;&gt;Regression test selection&lt;/a&gt; has estimated read sets for years, at file granularity, the granularity that doesn’t help, though &lt;a href=&quot;https://mir.cs.illinois.edu/marinov/publications/GligoricETAL15PracticalRTS.pdf&quot;&gt;dynamic variants&lt;/a&gt; get closer than my static one does.&lt;/li&gt;
  &lt;li&gt;Speculative merge automation (e.g., &lt;a href=&quot;https://zuul-ci.org/&quot;&gt;Zuul&lt;/a&gt;, &lt;a href=&quot;https://www.uber.com/ci/en/blog/slashing-ci-costs-at-uber/&quot;&gt;SubmitQueue&lt;/a&gt;) tests the composition, which is the expensive thing.&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://people.cs.umass.edu/~brun/pubs/pubs/Brun11fse.pdf&quot;&gt;Proactive conflict detection&lt;/a&gt; went after semantic conflicts directly more than a decade ago and never became something anybody runs.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;requirements-for-coordination-free-development&quot;&gt;Requirements for Coordination-Free Development&lt;/h2&gt;

&lt;p&gt;Coordination-free multi-agent development, if it’s possible at all, needs solving at two layers, in order.  Getting the second right while the first is broken buys nothing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The environment has to permit it first.&lt;/strong&gt;  Separate databases, working directories, port ranges, and instances of every service, per agent, enforced rather than attempted.  Writers in one checkout contend regardless of how elegant your merge semantics are, and I still do not know how many of those 84 were writers.&lt;/p&gt;

&lt;p&gt;Large companies solved this a while ago, and the shape of the fix isn’t subtle: make every developer’s environment a faithful copy of the one that builds and ships the product.  Either move development onto cloud machines provisioned from the same definition as CI, or reproduce that environment in miniature on the laptop, with a container per service, a database per test, and a toolchain pinned to a lockfile.  Somebody owns that, full time.  The cost of getting it wrong is paid once, by a platform team, instead of every day by everyone else.&lt;/p&gt;

&lt;p&gt;I don’t have a platform team, and neither does anyone else building alone with agents.  An independent developer can now reach a working prototype absurdly fast, faster than at any point in my career, and then hit the wall this whole post is describing: one environment, artifacts that assume one writer, and no organization to absorb the difference.  Getting off the ground is close to solved.  Staying up is not, and I don’t have a good answer for it.&lt;/p&gt;

&lt;p&gt;That’s the same argument I made &lt;a href=&quot;/ai/engineering/2026/04/01/software-engineering-is-becoming-civil-engineering.html&quot;&gt;a few months ago about civil engineering&lt;/a&gt;, coming at it from the other side.  When construction gets cheap, design is what fails, and the design here is the substrate: what runs where, what is isolated from what, and what has to be serialized.  An agent isn’t going to do that for you, and it won’t show up in the prototype.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Then the artifacts have to stop requiring a coordinator.&lt;/strong&gt;  A total order needs somebody to assign it, and in the single-writer world the assigner was free: migration numbers went up because one person added one at a time.  Put N agents on it and the assigner is gone.  You can reintroduce one as a central coordinator handing out positions, but a central coordinator &lt;em&gt;is&lt;/em&gt; coordination, and it caps throughput at one.&lt;/p&gt;

&lt;p&gt;Two mainstream designs exist and always have: a total order by timestamp, as &lt;a href=&quot;https://guides.rubyonrails.org/active_record_migrations.html&quot;&gt;Rails&lt;/a&gt; has done since 2008, and a dependency graph, as &lt;a href=&quot;https://docs.djangoproject.com/en/stable/topics/migrations/&quot;&gt;Django&lt;/a&gt; and &lt;a href=&quot;https://alembic.sqlalchemy.org/&quot;&gt;Alembic&lt;/a&gt; do.  Under one writer the choice barely matters.  Under many it matters enormously, because a total order needs an assigner and a graph doesn’t.  My agent picked the total order.&lt;/p&gt;

&lt;p&gt;Nobody ever weighed the two, and that’s the part worth sitting with, because it isn’t really about migrations.  I ratified the timestamp scheme myself, back in March, and what I ratified was a fix for filename &lt;em&gt;collisions&lt;/em&gt;: that was the problem in front of me, the fix solved it, and so I shipped it and &lt;a href=&quot;/ai/agents/distributed/zabriskie/2026/03/30/multi-agent-systems-have-a-distributed-systems-problem.html&quot;&gt;wrote a post about it&lt;/a&gt; without once asking what the scheme would do under load.&lt;/p&gt;

&lt;p&gt;An agent proposed it, an agent reviewed it, and I signed off on the part I understood.  Review is one of those protocols that terminates in a person, and what the person is supposed to supply is an independent prior.  Neither reviewer had one: not the agent, which reached for the most common pattern, and not me, because I was evaluating a collision fix and never saw an ordering decision.  The better option was documented, shipped in two major frameworks, and one search away.&lt;/p&gt;

&lt;p&gt;This is what vibe coding actually costs, and it’s not bad code.  The code was ordinary and defensible.  What is missing is the one question nobody asked, because asking it requires somebody who has been hurt by the answer before, and I had removed all of those people from the loop on purpose.&lt;/p&gt;

&lt;p&gt;Django and Alembic don’t solve it either, and the reason is the granularity problem again.  A Django dependency is an edge to another &lt;em&gt;migration&lt;/em&gt;: this one runs after &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;0042_add_venue&lt;/code&gt;, which says nothing about what either one reads or writes.  Two migrations with no declared edge are unordered, so if one adds a column and the other’s &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;UPDATE&lt;/code&gt; reads it, that pair conflicts, no edge exists, and nothing detects it.  The DAG removed the global sequence and left the effects undeclared.&lt;/p&gt;

&lt;p&gt;And when two branches produce divergent heads, the resolution is a merge revision: a node with two parents that doesn’t verify the branches are compatible.  It records a human’s assertion that they are, the same protocol terminating in the same person, one layer down.&lt;/p&gt;

&lt;p&gt;So the proposal isn’t “invent a DAG.”  The DAG exists.  It is to make the edges &lt;em&gt;effect-based&lt;/em&gt; instead of identity-based: know that this migration writes &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;show_attendance&lt;/code&gt; and reads &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;shows.date&lt;/code&gt;, and derive the ordering constraints and the genuine independence from that.&lt;/p&gt;

&lt;p&gt;The objection is that this relocates the labor, and “somebody declares it” across 84 agents is not a plan.  For general code that lands.  For migrations it doesn’t, because the effects are already in the text: SQL names its objects out loud, so parsing &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ALTER TABLE show_attendance ADD COLUMN&lt;/code&gt; or &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;UPDATE shows SET ... WHERE date &amp;gt; ...&lt;/code&gt; yields the read set and the write set with no human, no annotation, and no agent remembering.  All 1,388 of mine are plain &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.sql&lt;/code&gt;, and the parsing is commodity, since &lt;a href=&quot;https://github.com/tobymao/sqlglot&quot;&gt;sqlglot&lt;/a&gt; does column-level lineage today (which has not stopped &lt;a href=&quot;https://docs.getdbt.com/reference/dbt-jinja-functions/ref&quot;&gt;dbt&lt;/a&gt;, in the very ecosystem where effects are most extractable, from building its graph out of &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ref()&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;source()&lt;/code&gt; macros written by hand).&lt;/p&gt;

&lt;p&gt;Two caveats.  Disjointness only rules out conflicts through the objects a migration names, so an invariant spanning two tables can still break when two migrations touch one table each: the name-and-length record, one level down.  And my sixteen seal incidents have a two-line fix with no research content in it.  The case rests on the class rather than on those sixteen, because any ordering constraint several writers must satisfy independently has this shape.&lt;/p&gt;

&lt;p&gt;The hard case is the handler-through-ORM-through-SQL path, where the effects genuinely aren’t in the text.  The easy case has been sitting in my repository the whole time, unbuilt.&lt;/p&gt;

&lt;p&gt;None of that is the CRDT move.  CRDTs design a data type so that concurrent operations commute by construction; this is optimistic concurrency control, which extracts read/write sets and coordinates only the pairs that overlap.  Designing for commutativity means defining the data type, and for a software system in general there is nothing to define.&lt;/p&gt;

&lt;p&gt;For migrations there is, and it may be the better path.  Restrict the schema to additive, idempotent DDL: add columns nullable, never rename in place, never drop anything, and guard every statement so that applying it twice is the same as applying it once.  Under that discipline the schema is a grow-only set and every migration is an inflation, so any two of them commute and the order stops mattering.  No assigner, no extraction, no DAG.  The bill arrives at contraction, when you do finally want the column gone, and that step commutes with nothing, so it gets batched and performed deliberately by somebody who is paying attention.  Which means the two proposals aren’t really competing: commutativity by construction covers the additive majority and needs no extraction at all, and extracting read/write sets earns its keep on the contraction step and on everything that isn’t a migration.&lt;/p&gt;

&lt;p&gt;The discipline isn’t exotic.  It’s what microservices already force on you, because there you cannot control rollout order either: two versions of a service run at the same time, so every change has to be tolerable to the version that hasn’t shipped yet and to the one already deployed.  Expand, migrate, contract.  It is well understood, it is genuinely expensive, and it is one of the things practitioners are worst at, because it requires holding in your head a version of the system you are not looking at.&lt;/p&gt;

&lt;p&gt;Agents are worse at it than we are.  An agent told to rename a column renames the column, because that is the direct expression of the intent and it is correct in every example it has ever seen, all of which were written in a world with one writer and one deploy.  The three-step version is more code, it looks redundant at the moment it’s written, and its entire justification is a concurrency property that appears nowhere in the diff.  Which is this post again: the discipline exists, it works, and nothing left in the loop has the prior that would make anybody reach for it.&lt;/p&gt;

&lt;p&gt;The same argument applies to source code and lands somewhere less comfortable.  Git’s answer to two agents editing one file is to linearize, rebase the second onto the first, and resolve the conflicts: a coordination protocol with a human standing at the conflict point.  It works because textual conflicts are rare enough and humans read them well.  Both halves are under pressure now: agents collide constantly, and the human who was supposed to adjudicate is increasingly not reading the diff.&lt;/p&gt;

&lt;p&gt;Which raises the question I keep arriving at and don’t love.  &lt;strong&gt;Text files, line-based diffs, and sequential human review are a human interface.&lt;/strong&gt;  A structural representation, where merge is defined over the program’s structure instead of over lines, where a change declares what it reads and writes, and where invariants are attached to the artifact and checked mechanically, is a far better substrate for many concurrent writers.&lt;/p&gt;

&lt;p&gt;People have proposed exactly that for decades and it mostly didn’t take, not merely because developers like text, though they do, but because text is the interchange format that every other tool in the ecosystem already agrees on (diff, blame, bisect, grep, review, CI logs, and every static analyzer ever written), so that replacing it means replacing all of them, per language, before you get any benefit at all.&lt;/p&gt;

&lt;p&gt;The exceptions are the argument rather than an exception to it.  &lt;a href=&quot;http://darcs.net/&quot;&gt;Darcs&lt;/a&gt; and then &lt;a href=&quot;https://pijul.org/&quot;&gt;Pijul&lt;/a&gt; built patch algebras where independent changes provably commute (the CRDT move applied directly to source control), and &lt;a href=&quot;https://www.unison-lang.org/&quot;&gt;Unison&lt;/a&gt; stores a content-addressed syntax tree with no text merge at all.  They exist, they work, and almost nobody uses them.&lt;/p&gt;

&lt;p&gt;The tempting move is to say that agents change that calculus, because the reader has left the room and the ergonomics objection leaves with them.  I do not think that is safe yet.&lt;/p&gt;

&lt;p&gt;Language models are the most text-dependent writers we have ever built: trained on text diffs and line-oriented source, and currently worse at manipulating syntax trees than at manipulating the text I would be proposing to abandon.  The party being served may not be human, but it learned to code by reading us.  Whether that is a durable property of these systems or an artifact of what we happened to train them on is the load-bearing question, and it’s not one I can answer from a migration log.&lt;/p&gt;

&lt;h2 id=&quot;takeaways&quot;&gt;Takeaways&lt;/h2&gt;

&lt;p&gt;So here is what I don’t know, gathered in one place rather than dropped along the way.&lt;/p&gt;

&lt;p&gt;Whether a runtime read/write set can be computed precisely enough to be worth having.  Whether text is a contingent fact about today’s models or a durable one.  And, most uncomfortably, what the specification is once humans stop reading code: if the test suite becomes the only surviving statement of what the system should do, and the suite is also written by agents, then attaching invariants to the artifact and checking them mechanically relocates the trust problem rather than solving it.&lt;/p&gt;

&lt;p&gt;That last one is a different complaint from the rest of this post.  I’ve been arguing that the suite gets run against the wrong artifact.  That’s the worry that it &lt;em&gt;is&lt;/em&gt; the wrong artifact.&lt;/p&gt;

&lt;p&gt;The short-term fixes are unglamorous and I’m doing them anyway: a merge queue with a concurrency limit of one, a worktree with its own port range and database per agent, a cap on how many subagents a session may spawn, and one incident per root cause instead of one per symptom.  That is admission control and resource isolation, plus better bookkeeping.  None of it is research, and none of it touches the verification multiplier, the part that needs something nobody has.&lt;/p&gt;

&lt;p&gt;Codex could describe all of this afterward, lucidly, and could answer none of it beforehand.  It spawned 84 workers anyway, which is less a mistake than a preview.&lt;/p&gt;

&lt;p&gt;My setup is deliberately extreme, and a team that reviews its diffs and never let an agent design its schema layer will see a fraction of what I saw.  They will still see some of it.  The substrate underneath is the same one, and it still assumes there is one of you.&lt;/p&gt;

&lt;p&gt;If you are working on conflict detection, effect extraction, or merge queues that don’t cost a fortune, or if you have hit this same wall from the other direction inside a large organization, &lt;a href=&quot;https://bsky.app/profile/christophermeiklejohn.com&quot;&gt;please reach out&lt;/a&gt;.  I would very much like to be told that some part of this is already solved.&lt;/p&gt;

&lt;p&gt;Three open questions is a worse ending than one answer.  It’s the honest count.&lt;/p&gt;

&lt;hr /&gt;

&lt;p&gt;&lt;em&gt;This is part of an ongoing series about building &lt;a href=&quot;https://zabriskie.app&quot;&gt;Zabriskie&lt;/a&gt; with AI agents. Previously: &lt;a href=&quot;/ai/zabriskie/agents/reliability/testing/2026/06/10/the-test-suite-was-the-incident.html&quot;&gt;The Test Suite Was the Incident&lt;/a&gt;, &lt;a href=&quot;/ai/zabriskie/agents/reliability/caucus/2026/04/21/the-tax-on-the-happy-path.html&quot;&gt;The Tax on the Happy Path&lt;/a&gt;, and &lt;a href=&quot;/ai/agents/distributed/zabriskie/2026/03/30/multi-agent-systems-have-a-distributed-systems-problem.html&quot;&gt;Multi-Agent Systems Have a Distributed Systems Problem&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;
</description>
				<pubDate>Mon, 27 Jul 2026 02:00:00 +0000</pubDate>
				<link>https://christophermeiklejohn.com/ai/zabriskie/agents/reliability/testing/ci/distributed/2026/07/27/one-writer.html</link>
				<guid isPermaLink="true">https://christophermeiklejohn.com/ai/zabriskie/agents/reliability/testing/ci/distributed/2026/07/27/one-writer.html</guid>
			</item>
		
			<item>
				<title>The Hard Call Is the Whole Job · A cancelled flight, three confident machines, and the woman on the Hilton Diamond line who refused to stop.</title>
				<description>&lt;p&gt;Toronto to Detroit to Norfolk. Two nights of Goose at the Dome, Monday and Tuesday, and I was flying in the night before. I have made this trip, or one shaped like it, more times than I can count.&lt;/p&gt;

&lt;p&gt;On Sunday night my connection to Norfolk slipped an hour, then another, then another. At 11:30 PM Delta cancelled it along with hundreds of other flights, said the next seat out was Tuesday, and told us to sleep in the airport. Whatever happened next, night one was already gone.&lt;/p&gt;

&lt;p&gt;Everything near the terminal was full. So I called outward.&lt;/p&gt;

&lt;h2 id=&quot;the-hotel-that-knew-the-date&quot;&gt;The hotel that knew the date&lt;/h2&gt;

&lt;p&gt;Sunday night, 12:41 AM. The hotel’s AI knew the date better than I did. That is how it tried to book me the wrong night.&lt;/p&gt;

&lt;p&gt;I did not know it was an AI. It sounded like a person. It paused before answering, the way you pause when you are reading something off a screen. I had been in the Detroit airport for nine hours, and for eight minutes I believed I was talking to someone having a hard night too.&lt;/p&gt;

&lt;p&gt;“Sunday night through Tuesday.”&lt;/p&gt;

&lt;p&gt;“Great, checking in Monday at 3:00 PM.”&lt;/p&gt;

&lt;p&gt;“No. Tonight. I need a room tonight.”&lt;/p&gt;

&lt;p&gt;“Tonight is Monday. Checking in Monday, departing Wednesday.”&lt;/p&gt;

&lt;p&gt;“Two nights, starting Sunday.”&lt;/p&gt;

&lt;p&gt;“Okay, Sunday. But tonight is just after Sunday midnight, so it’s Monday. Two nights, starting Monday, to Wednesday. Shall I book that for you?”&lt;/p&gt;

&lt;p&gt;Read that last one again. It was right. It knew it was past midnight, it knew that made the date Monday, and it offered me a room for a night I would spend in Virginia. The night that started on Sunday is still going. Every person alive knows this and no one has ever had to say it out loud.&lt;/p&gt;

&lt;p&gt;I raised my voice at it. When I finally understood what I was talking to, my first feeling was not anger. It was embarrassment. Someone chose those pauses. They spent real effort making it sound like a person who could help me, and none at all on the hour when a person would have to.&lt;/p&gt;

&lt;p&gt;Nothing was broken. Nothing failed, nothing crashed, nothing was even misunderstood. It did exactly what it was built to do, on an input nobody thought to try, confidently and fluently and kindly the whole way down. And 12:41 AM is the only hour anyone calls a hotel they never planned to stay in. So either nobody tested that call, or somebody did and shipped it anyway.&lt;/p&gt;

&lt;h2 id=&quot;the-phone-number&quot;&gt;The phone number&lt;/h2&gt;

&lt;p&gt;I gave up on calling hotels one at a time and called the Hilton Diamond line instead. A woman answered and understood in one sentence. Flight cancelled, airport hotels full, need a room tonight, near the Detroit airport, tonight meaning the night that started on Sunday. No confusion about the date. She started pulling up my account.&lt;/p&gt;

&lt;p&gt;Before she could book anything, she needed a phone number on the account. There wasn’t one. Fifteen years of stays and apparently I had never given Hilton a phone number, and the booking could not proceed without it, and she could not add it for me. I had to add it myself, from my phone, in the terminal, at one in the morning.&lt;/p&gt;

&lt;p&gt;Something went wrong. Please try again.&lt;/p&gt;

&lt;p&gt;I tried again. Something went wrong. Please try again.&lt;/p&gt;

&lt;p&gt;I spent my PhD on that sentence. Not the words, the thing underneath them. On why distributed systems fail in ways nobody tests for. On what happens when a request crosses four services and one of them times out and the caller has no idea which one, or why, or whether retrying is safe. On building tools that force a system into that state on purpose, before a customer finds it at 1:00 AM in Detroit.&lt;/p&gt;

&lt;p&gt;And there it was. Four words. No error code. No correlation ID. No indication of whether the write had partially landed, so no way to know whether trying again would help or make it worse. I tried again anyway. What else is there.&lt;/p&gt;

&lt;p&gt;This is a different failure from the hotel bot, and it is worth being precise about the difference. The bot was a testing failure: a system that understood everything I said and still could not help me, because nobody enumerated the one hour when its only real customers would call. The error screen is an observability failure: a system that knew something had gone wrong, knew where, knew what, and told me nothing. Somewhere in Hilton’s infrastructure there is a log line with the truth in it. The one person who needed that truth, standing in a terminal trying to give a company his phone number so it would let him pay for a room, got four words and a suggestion to try again.&lt;/p&gt;

&lt;h2 id=&quot;the-security-questions&quot;&gt;The security questions&lt;/h2&gt;

&lt;p&gt;The phone number never saved. So I got back in the queue for another human, to add a phone number by hand. That took a long time. When I reached her, she had to verify my identity first.&lt;/p&gt;

&lt;p&gt;Her: What was the last hotel you stayed at?&lt;/p&gt;

&lt;p&gt;Me: The one in Toronto. I checked out of it yesterday morning.&lt;/p&gt;

&lt;p&gt;Her: What is its street address?&lt;/p&gt;

&lt;p&gt;Me: I don’t know. It was a hotel. I slept there and I left.&lt;/p&gt;

&lt;p&gt;Her: I need the address to verify your identity.&lt;/p&gt;

&lt;p&gt;Me: I am going to google it, and then I am going to read it back to you, and we are both going to pretend that proved something.&lt;/p&gt;

&lt;p&gt;She accepted the address. I found it in nine seconds, on the open internet, on the same phone that could not update my own account. A stranger with my name and a search bar could have done the same thing. The system was not verifying that I was me. It was verifying that I was willing to perform the ritual, and at 1:00 AM with nowhere to sleep, I was willing to perform anything.&lt;/p&gt;

&lt;h2 id=&quot;three-machines-one-pattern&quot;&gt;Three machines, one pattern&lt;/h2&gt;

&lt;p&gt;That is the whole night, and it is one pattern three times. A voice bot that parsed every word I said and booked the wrong night. An error screen that knew exactly what failed and said nothing. A security check that verified my identity with a fact anyone on earth could look up in nine seconds. Three systems, each of which had exactly enough information to be confident and not enough to be right.&lt;/p&gt;

&lt;p&gt;None of them were broken. That is the part I keep coming back to. Every one of them did precisely what it was built to do. The failures were not in the systems. They were in the space between what got built and what got tested, and that space is invisible until a customer is standing in it. My research career was about finding those states on purpose, injecting the fault before the customer trips over it, and I could not tell you a cleaner set of examples than the ones I collected involuntarily in a single hour: the input nobody enumerated, the error nobody instrumented, the check nobody thought about from the attacker’s side. You do not find these by testing the happy path harder. You find them by asking, specifically and adversarially, what the worst hour of your customer’s night looks like, and then dialing in from inside it.&lt;/p&gt;

&lt;h2 id=&quot;the-woman-on-the-diamond-line&quot;&gt;The woman on the Diamond line&lt;/h2&gt;

&lt;p&gt;Identity verified, phone number added by hand, back to the original problem: it is now well past one in the morning and I have nowhere to sleep.&lt;/p&gt;

&lt;p&gt;She started calling hotels herself. One at a time, with me on hold. The first two were full. On the fourth she found a room in a suburb of Detroit.&lt;/p&gt;

&lt;p&gt;She had no clever solution. There was no system on her side doing anything smart, no availability dashboard collapsing the search into one query. She just refused to stop until the problem was gone, which is the one behavior nobody has figured out how to ship.&lt;/p&gt;

&lt;p&gt;Think about what she actually did, against the three machines that came before her. The bot understood my words and not my situation; she understood the situation in one sentence. The error screen knew the truth and would not say it; she narrated everything she was doing, hold music and all, so I always knew where things stood. The security questions performed verification without verifying; she performed none of the ritual and simply took responsibility for the outcome. Every quality the automation was imitating, she had the real version of, and the real version is not a feature. It is a person who has decided your problem is now her problem.&lt;/p&gt;

&lt;h2 id=&quot;service-is-the-product&quot;&gt;Service is the product&lt;/h2&gt;

&lt;p&gt;Service is not a cost center waiting to be automated. Service is the product. It exists precisely for the night the plan falls apart. Every company putting a bot on that line has decided the only calls worth answering are the easy ones. Nobody calls at 12:41 AM with an easy one. The hard call is the whole job.&lt;/p&gt;

&lt;p&gt;I build this stuff. I research LLM agents. I am not against any of it. The bot that answered that hotel phone will get better, and the error screen could be fixed by one engineer in an afternoon, and I will keep working on exactly these problems, because they are solvable. I am against shipping it untested into the exact moment a customer needs it most, and then calling the person who cleans up afterward the cost center.&lt;/p&gt;

&lt;p&gt;And perhaps that is why we still pay for service. Not for the room. For the person who picks up when the room is gone.&lt;/p&gt;

&lt;p&gt;I did not sleep in the airport. She stayed on the phone until I had somewhere to go.&lt;/p&gt;
</description>
				<pubDate>Wed, 08 Jul 2026 19:00:00 +0000</pubDate>
				<link>https://christophermeiklejohn.com/ai/personal/agents/reliability/testing/2026/07/08/the-hard-call-is-the-whole-job.html</link>
				<guid isPermaLink="true">https://christophermeiklejohn.com/ai/personal/agents/reliability/testing/2026/07/08/the-hard-call-is-the-whole-job.html</guid>
			</item>
		
			<item>
				<title>The App That Lives Between Shows</title>
				<description>&lt;blockquote&gt;
  &lt;p&gt;&lt;em&gt;“Thank god this app rocks and I can live through the chompers.”&lt;/em&gt;
(a chomper, in the chat, on a night they could not make the show)&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Two months ago I &lt;a href=&quot;/ai/zabriskie/development/2026/04/26/spring-tour-recap.html&quot;&gt;wrote up Goose Spring ‘26&lt;/a&gt;. The through-line of that post was that the live show finally worked. Live Activities stayed up, the setlist was seconds behind the room instead of minutes, song calls landed, and forty people spent a tour sending each other 3,737 messages from their couches and their seats. The app was good for the two and a half hours a night that a band was on stage.&lt;/p&gt;

&lt;p&gt;That was the easy half. The hard half is the other twenty-one and a half hours, and the days between shows, and the weeks between tours.&lt;/p&gt;

&lt;p&gt;Here is the thing nobody tells you about building a live-event app: the event is not the problem. The gap is the problem. The tour ends, the chat empties out, the lock-screen Live Activity goes dark, and everyone drifts back to their normal feeds until the next run gets announced. I said this in the Spring post, in the section about the jam bracket, and then I spent the next two months building against it on purpose. The whole point of a third place is that you can go there when nothing is happening. A bar that only opens during the game is not a bar. It is a stadium.&lt;/p&gt;

&lt;p&gt;So this is a post about what we shipped to make Zabriskie worth opening on a Tuesday in the middle of June with no show anywhere. But I want to say the real thing first, because it is the reason the rest of it exists. I am proud of this one. I love this app in a way I have not loved something I built in a long time, and I love it because it is not really mine. It is a small and genuinely growing community of people who love the same bands I do, and we are growing it the slow way, by hand, one person at a time, with no ads, no growth team, and no playbook. Two of us build it. One of us has spent the last few months handing out stickers on the lot and talking strangers into it between sets. That is the entire marketing department, and I will get to him, because he deserves most of this post.&lt;/p&gt;

&lt;p&gt;It is also a post about whether the between-shows bet worked, which is a question you answer with numbers, and I am going to be honest about the numbers, including the ones that did not go the way I wanted.&lt;/p&gt;

&lt;p&gt;I counted the PRs again, the same way I did in April. Between the last show of Spring tour and today, &lt;strong&gt;787 pull requests&lt;/strong&gt; merged into &lt;a href=&quot;https://github.com/cmeiklejohn/zabriskie&quot;&gt;Zabriskie&lt;/a&gt;. That is more than two and a half times the entire Spring tour, in about the same span of calendar. The app went from build 25-ish to &lt;strong&gt;iOS 53 and Android 51&lt;/strong&gt;, shipped &lt;strong&gt;v1.5.0 to the App Store and the Play Store&lt;/strong&gt;, and grew an entire second client: it runs on your &lt;strong&gt;watch&lt;/strong&gt; now, which Patrick built end to end. And nearly half of those 787 pull requests are his, which is the single most important fact in this post and the one I want to sit on for a minute before anything else.&lt;/p&gt;

&lt;p&gt;There was also a real deadline this time. Goose put out a new album, &lt;strong&gt;Big Modern!&lt;/strong&gt;, and took it on the road this summer. The album drop and the tour behind it are the biggest moment a band’s community gets all year, and I wanted every surface of the app ready for it: the live chat, the show page, the home screen, the band’s whole history one tap away. So a lot of what follows is us getting the room ready for the party. And when the Big Modern! tour actually started in June, the room filled. I will show you the chat numbers, because they are the ones I am proudest of.&lt;/p&gt;

&lt;p&gt;Start with the room everyone actually lives in during a show: the chat.&lt;/p&gt;

&lt;h2 id=&quot;the-live-chat-got-rebuilt-and-it-is-where-everything-happens&quot;&gt;The Live Chat Got Rebuilt, and It Is Where Everything Happens&lt;/h2&gt;

&lt;p&gt;During a show the chat is called the Chomp, and it is the beating heart of the whole app. The big move this window was rebuilding it from the studs into &lt;strong&gt;Live Chat V2&lt;/strong&gt;: a full cinematic redesign, shipped over weeks in May, warmer and bigger and more editorial, less like a database with a skin on it and more like an actual room. That redesign is the real story of this section. The flashiest single piece, the &lt;strong&gt;reaction heatbar&lt;/strong&gt;, was actually the &lt;em&gt;last&lt;/em&gt; thing we added, at the very end of June, right as the Big Modern! tour turned the room hot. The commit history is honest about the order: the room got rebuilt first, and the thermometer went on the wall last.&lt;/p&gt;

&lt;p&gt;The heatbar sits above the chat and shows the room temperature. It counts the reactions flying right now, so you can feel a jam landing before you have read a single word. It turns out a lot of people want to be in the room without composing a sentence, and a tap of 🔥 is a much lower bar than typing. Around it we shipped set markers (“🎼 Set 1 begins,” a “Set break” vote chip), duration-sized song progress bars, role badges, a spoiler-safe delay for couch viewers so the people in the building do not ruin the song for the people on the stream, and, because we are on the App Store now, report-and-block moderation on every message.&lt;/p&gt;

&lt;div style=&quot;color-scheme:light; background:#E7E2D6; padding:16px; border-radius:20px; margin:16px auto; max-width:420px;&quot;&gt;
  &lt;div style=&quot;background:#F0EDE4; border-radius:20px; box-shadow:0 12px 34px rgba(20,18,30,0.14); overflow:hidden; max-width:390px; margin:0 auto; color:#2A2A3A; font-family:&apos;Inter&apos;,-apple-system,BlinkMacSystemFont,system-ui,sans-serif;&quot;&gt;
    &lt;div style=&quot;position:relative; padding:12px 16px; background:radial-gradient(circle at 20% 20%, #E83A73, #2A5FAA 120%); color:#fff;&quot;&gt;
      &lt;div style=&quot;position:absolute; inset:0; opacity:0.12; mix-blend-mode:overlay; background-image:url(&amp;quot;data:image/svg+xml,%3Csvg viewBox=&apos;0 0 256 256&apos; xmlns=&apos;http://www.w3.org/2000/svg&apos;%3E%3Cfilter id=&apos;n&apos;%3E%3CfeTurbulence type=&apos;fractalNoise&apos; baseFrequency=&apos;0.85&apos; numOctaves=&apos;4&apos; stitchTiles=&apos;stitch&apos;/%3E%3C/filter%3E%3Crect width=&apos;100%25&apos; height=&apos;100%25&apos; filter=&apos;url(%23n)&apos;/%3E%3C/svg%3E&amp;quot;); background-size:130px 130px;&quot;&gt;&lt;/div&gt;
      &lt;div style=&quot;position:relative; display:flex; align-items:center; justify-content:space-between;&quot;&gt;
        &lt;div style=&quot;display:flex; align-items:center; gap:8px;&quot;&gt;&lt;span style=&quot;width:8px; height:8px; border-radius:50%; background:#EF4444;&quot;&gt;&lt;/span&gt;&lt;span style=&quot;font-family:&apos;Jost&apos;,-apple-system,BlinkMacSystemFont,system-ui,sans-serif; font-size:12px; font-weight:700; letter-spacing:0.14em;&quot;&gt;GOOSE · LIVE&lt;/span&gt;&lt;/div&gt;
        &lt;span style=&quot;font-family:&apos;Instrument Serif&apos;,&apos;Cormorant Garamond&apos;,Georgia,serif; font-style:italic; font-size:15px; opacity:0.92;&quot;&gt;The Cap · Set 2&lt;/span&gt;
      &lt;/div&gt;
    &lt;/div&gt;

    &lt;!-- reaction heatbar (room temperature) --&gt;
    &lt;div style=&quot;display:flex; align-items:center; gap:8px; padding:9px 14px; background:rgba(232,58,115,0.06); border-bottom:1px solid rgba(42,42,58,0.06);&quot;&gt;
      &lt;span style=&quot;font-family:&apos;Jost&apos;,-apple-system,BlinkMacSystemFont,system-ui,sans-serif; font-size:9px; font-weight:700; letter-spacing:0.12em; text-transform:uppercase; color:#9A9AAA;&quot;&gt;Room temp&lt;/span&gt;
      &lt;span style=&quot;font-size:13px;&quot;&gt;🔥 &lt;b style=&quot;font-family:&apos;Jost&apos;,-apple-system,BlinkMacSystemFont,system-ui,sans-serif; font-size:12px;&quot;&gt;41&lt;/b&gt;&lt;/span&gt;
      &lt;span style=&quot;font-size:13px;&quot;&gt;🤯 &lt;b style=&quot;font-family:&apos;Jost&apos;,-apple-system,BlinkMacSystemFont,system-ui,sans-serif; font-size:12px;&quot;&gt;18&lt;/b&gt;&lt;/span&gt;
      &lt;span style=&quot;font-size:13px;&quot;&gt;🕺 &lt;b style=&quot;font-family:&apos;Jost&apos;,-apple-system,BlinkMacSystemFont,system-ui,sans-serif; font-size:12px;&quot;&gt;12&lt;/b&gt;&lt;/span&gt;
      &lt;span style=&quot;font-size:13px;&quot;&gt;🎷 &lt;b style=&quot;font-family:&apos;Jost&apos;,-apple-system,BlinkMacSystemFont,system-ui,sans-serif; font-size:12px;&quot;&gt;7&lt;/b&gt;&lt;/span&gt;
      &lt;span style=&quot;flex:1;&quot;&gt;&lt;/span&gt;
      &lt;span style=&quot;font-family:&apos;Jost&apos;,-apple-system,BlinkMacSystemFont,system-ui,sans-serif; font-size:10px; color:#E83A73; font-weight:700;&quot;&gt;🌡 boiling&lt;/span&gt;
    &lt;/div&gt;

    &lt;div style=&quot;padding:8px 0 8px;&quot;&gt;
      &lt;div style=&quot;padding:6px 14px;&quot;&gt;&lt;span style=&quot;font-family:&apos;Jost&apos;,-apple-system,BlinkMacSystemFont,system-ui,sans-serif; font-size:12px; font-weight:600; letter-spacing:0.04em; color:#2A5FAA; background:rgba(42,95,170,0.10); padding:4px 12px; border-radius:12px;&quot;&gt;🎼 Set 2 begins&lt;/span&gt;&lt;/div&gt;
      &lt;div style=&quot;padding:4px 14px 2px;&quot;&gt;&lt;span style=&quot;font-family:&apos;Jost&apos;,-apple-system,BlinkMacSystemFont,system-ui,sans-serif; font-size:13px; font-weight:600; color:#E83A73; background:rgba(232,58,115,0.10); padding:4px 12px; border-radius:12px;&quot;&gt;🎵 Madhuvan&lt;/span&gt; &lt;span style=&quot;font-size:11px; color:#9A9AAA;&quot;&gt;↩ 6-show gap&lt;/span&gt;&lt;/div&gt;

      &lt;div style=&quot;padding:8px 14px; display:flex; gap:10px; align-items:flex-start;&quot;&gt;
        &lt;div style=&quot;width:32px; height:32px; border-radius:50%; background:linear-gradient(135deg,#F2A83B,#E83A73); display:flex; align-items:center; justify-content:center; font-size:13px; font-weight:700; color:#fff; flex-shrink:0;&quot;&gt;P&lt;/div&gt;
        &lt;div&gt;
          &lt;div style=&quot;display:flex; gap:6px; align-items:center;&quot;&gt;&lt;span style=&quot;font-weight:700; font-size:14px;&quot;&gt;patrick&lt;/span&gt;&lt;span style=&quot;font-size:9px; padding:2px 6px; border-radius:8px; color:#fff; font-weight:700; background:#E83A73;&quot;&gt;🎸 Show&lt;/span&gt;&lt;/div&gt;
          &lt;div style=&quot;font-size:14px; line-height:1.35;&quot;&gt;HERE IT IS. this is the one 🔥🔥🔥&lt;/div&gt;
          &lt;div style=&quot;margin-top:4px;&quot;&gt;&lt;span style=&quot;font-size:11px; padding:2px 9px; border-radius:10px; background:rgba(232,58,115,0.10); color:#E83A73; font-weight:700;&quot;&gt;🔥 9&lt;/span&gt;&lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;

      &lt;div style=&quot;padding:4px 14px; display:flex; gap:10px; align-items:flex-start;&quot;&gt;
        &lt;div style=&quot;width:32px; height:32px; border-radius:50%; background:linear-gradient(135deg,#3AC4E8,#2A5FAA); display:flex; align-items:center; justify-content:center; font-size:13px; font-weight:700; color:#fff; flex-shrink:0;&quot;&gt;C&lt;/div&gt;
        &lt;div&gt;
          &lt;div style=&quot;display:flex; gap:6px; align-items:center;&quot;&gt;&lt;span style=&quot;font-weight:700; font-size:14px;&quot;&gt;chomper1&lt;/span&gt;&lt;span style=&quot;font-size:9px; padding:2px 6px; border-radius:8px; color:#fff; font-weight:700; background:#3AC4E8;&quot;&gt;🛋 Couch&lt;/span&gt;&lt;/div&gt;
          &lt;div style=&quot;font-size:14px; line-height:1.35;&quot;&gt;YOOO CHOMPERS 🎸🤝🛋️ nugs is 40s behind, no spoilers&lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;

    &lt;div style=&quot;padding:8px 14px 14px;&quot;&gt;
      &lt;div style=&quot;display:flex; gap:8px; align-items:center; padding:9px 12px; background:#FEFDFB; border-radius:16px; border:1px solid rgba(42,42,58,0.08);&quot;&gt;
        &lt;span style=&quot;font-size:14px; color:#9A9AAA; flex:1; font-family:&apos;Inter&apos;,-apple-system,BlinkMacSystemFont,system-ui,sans-serif;&quot;&gt;Say something to the chomp…&lt;/span&gt;
        &lt;span style=&quot;font-size:12px; font-weight:700; color:#fff; background:#E83A73; padding:5px 14px; border-radius:12px; font-family:&apos;Jost&apos;,-apple-system,BlinkMacSystemFont,system-ui,sans-serif;&quot;&gt;Send&lt;/span&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;p&gt;One detail I love more than I expected to: the reactions are &lt;strong&gt;band-specific.&lt;/strong&gt; The palette changes depending on whose show you are in. Walk into a Goose chat and your reactions are the band: 🥸 for Peter, 👟 for Rick’s yellow Chuck Taylors, 🕶️ for Trevor, 🌽 for Cotter (yes, a corn). Walk into a Phish chat and it is 🎹 Page, 🎸 Trey, 🌵 Mike, and ⭕ Fishman. Every band we add gets its own kit drawn from its own members, all server-driven so we can hand a new band its reactions without shipping an app update. And there are conditional sit-in reactions, so Stuart Bogie’s 🎷 slots into the Goose lineup, right between Rick’s Chucks and Trevor’s shades, on the nights he actually sits in. This one has a lineage worth naming: &lt;strong&gt;Nicole had the original idea&lt;/strong&gt;, and &lt;strong&gt;Patrick built it&lt;/strong&gt; into the server-driven kit it is now. It is a small thing that tells the regulars we know exactly whose room they are standing in.&lt;/p&gt;

&lt;div style=&quot;color-scheme:light; background:#E7E2D6; padding:16px; border-radius:20px; margin:16px auto; max-width:420px;&quot;&gt;
  &lt;div style=&quot;background:#F0EDE4; border-radius:18px; box-shadow:0 10px 28px rgba(20,18,30,0.12); max-width:390px; margin:0 auto; color:#2A2A3A; font-family:&apos;Inter&apos;,-apple-system,BlinkMacSystemFont,system-ui,sans-serif; padding:16px;&quot;&gt;
    &lt;div style=&quot;font-family:&apos;Jost&apos;,-apple-system,BlinkMacSystemFont,system-ui,sans-serif; font-size:10px; font-weight:700; letter-spacing:0.16em; text-transform:uppercase; color:#9A9AAA; margin-bottom:14px;&quot;&gt;React · 🪿 Goose kit&lt;/div&gt;
    &lt;div style=&quot;display:flex; gap:8px; justify-content:space-between;&quot;&gt;&lt;div style=&quot;display:flex; flex-direction:column; align-items:center; gap:5px; width:60px;&quot;&gt;&lt;div style=&quot;width:54px; height:54px; border-radius:16px; background:#FEFDFB; box-shadow:0 2px 8px rgba(20,18,30,0.09);  display:flex; align-items:center; justify-content:center; font-size:27px; overflow:hidden;&quot;&gt;🥸&lt;/div&gt;&lt;span style=&quot;font-family:&apos;Jost&apos;,-apple-system,BlinkMacSystemFont,system-ui,sans-serif; font-size:10px; font-weight:600; color:#6B6B7B;&quot;&gt;Peter&lt;/span&gt;&lt;/div&gt;&lt;div style=&quot;display:flex; flex-direction:column; align-items:center; gap:5px; width:60px;&quot;&gt;&lt;div style=&quot;width:54px; height:54px; border-radius:16px; background:#FEFDFB; box-shadow:0 2px 8px rgba(20,18,30,0.09);  display:flex; align-items:center; justify-content:center; font-size:27px; overflow:hidden;&quot;&gt;&lt;img src=&quot;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIAAAACACAYAAADDPmHLAABQ70lEQVR4nO29CbRlx1ke+lXVns94x57VrcktW7IlWbKN8SAZG0MMJCQg4eBgXiB5DrAwkDzGLJaklZXksVg8wvAgMSQEHvgFKQ6xE9uMtgxGsY1kW7LmWT3dvvOZ91i73vr/2vucc1stPzk4eLrV6/Q90x5O1V//8P0TsD/2x/7YH/tjf+yPr8Uh8GUwjIHAe+CIdyGn1/f+O7hF032FkfrlQqDMZXn36x/CGXEHSvquEDBf6nv+ahnOl/oG7rwTSghoAPk9d+KID/l6XZqrBcrDnoMDmYZxS5wWd+DUl/pevxrHl5QAbrsN8tZboWlXf/oPvKuEzt9RGnzvYlceLUvA9wSSFOgPxQNrf2Q+deibMKbdT8fdcQfKL+W9f7UM+aUUPYcPQ9HfB/8AVymUP68c+WNCiMOTCTAcA8MJkCSAUur6rbF6HYsKADff/CW776+68SWZyDtvsdd917uQP/zf3GvgiB8Vwry100QkJaQuRQEhs7yQWhupHYVXFcDra9m/srlPAF+xBGAAsfIyu5PPfgBRafS3OlK8w3HgjifIpBQaSjhSCg8QSkghmxFWpDBXf+JDaNNxye6+EviVqwPcCXnzLdAPXg0vVuom15i3+L5oZDmQG6kcJVRZ6fjGCEO73nWkEBKXtnPxmgfv1B+75lZkLA4Ey5J9i+ArgAPMzM3LIGlRWxEacPD2KBKvKrQoDXixpZSAkoCUAlLRGwK5lrTaR4XCm9uLWKHFv/127BuDX4QhaTL/Bh5s7vGufRrlMx9FoD31akC8MQpEW5dsBgqlIIgA5h9CCJFpWQa+7BpjviHPcHgfB/jiDd6NfxMPuhg/vxVaO+7VEHinFFhNMtLyIe2urx8CSgo4siIKIYoolK6U8uWFkFfQuW6/3Z63tgz2x5e5Ekj2fv3czcwbfaXf5jo6zPKykMJIKQykqBa/Yv01IRAXEFLBdVQgjLrxiT/0iQjswpMo2B//08NZv+fK79V5D2W2DRksQXldAuWgIKF1Qg9DW1TJAEp5fBBvV50gTzahVQTlLrBBL+k/nSHPx8iyHEoF8MIWLZAwpdB5mQmdDLvC5N/eaDS6aQ6UaSmMKUVZlkyNtKUJ76W/dkjiCjIvFAxzk+IblMk+KwSepE/vs1jCPij0Pzkc15f/EYKMAR/KceE6ElLwcsLQo6Rd6MDxHN6NzMyFhDbEqx249JmjoEQJUtW1oWMFHLLrXBce8XGQIgfo0mEer5wmStUxroQQKlNplkEZDVMWKMuCqIUNRiGAspREDFJr4gyucNzyujRVX2dM+bskAoKFfQ7w1xnOwoJCnrRQFD48z4MKlN3KpUAxyZFlCmEUQviBFRi0nwsBnfjI/S4fIwOX9jgfg7RE4hJRBHDCEHArKZMYpKkEzAKr+iUclKVBkvggAvRcjTwvkeUlAo8IiEkAWWaQxmNR6KJUUspuS2GrKE8+9xetlxrzw4/efvsdxb6D6H9+iJ1PraZZTrudZKyB59BOpt0KpGkGXeYI/QiO60Iwpy1RaCAvDC+g6wjmGkLSbhXIsxx5kcJxfPieCyktK08zA7L1HaXgekJJoZUuDbKUmI2DIGygKAXKokQYKAS+QGkExhON8WiEPC9K0gSaYVIOhukzSoh/E2X+7yy/bWdgSH2wN7c/vsDhDAc7Hu01RbsyF0iZx5MMZq2dfa9pkiJNrK5liD0zAGPYj0tyPAURAIt6oDQwKJHrMfKUFDjLNgwJcBAR0KLb7xLgQwiAVB5i0jqUQ5oH8lzSGfkeisJAOR6kClj+JIWQjiqPwKRvHfijPwEwwF3W3Nw3D7/w4RCblbRINL3V4tGikQZOf2lWUWoYa6rb11PwjV7TX8PH0iD2LoXDi12acqrM0XF8pCl5Z5M84UNJKpgcOt1GKR1SCpARx6D7IWUQCl4QIQocXt/BKMxdvwyGw/TVshBHATxmz2+1k7mb2x8vYjhCkmZfcU9Jap9ddLuwdufaxSMxYRVAOypOwJ9Zg9wqbpVVRsTDBELEYc9FJyVOYc9kicBSAf1H4oJ0CaII2vkpiiJjruCWId8PMRmh/NLz6ITjrnRNi/gPbtlf9L+WL8DutIrlMyug18Ti6VNaNPs5rTHtYCsaZiepUR4rFWYiZP4LxFlqDmI5QX0OIiQP0l2A4xARlCB7L0uHZFMyQZByOJqUrBh6zgiepyFUqA1hxRjNLjM95/54scNhzlmxW1oLXqYKlKfPrE1ulTzL/StuQYcRbMfLT4uGaoGVpQVm/9Wi87msDsDHEXfgjS5gtGECIzPSUaSLWOIphOUm7BwwCbKMLu5hqTEGmhJevyV0rsj22F/yv8aQLPdr7xstpZnJ7noTMz+owZlqi02BGrvhp9/bu/UvJpAv4A40mKUQhanKCUTPHRjhVY8QDibQWuFz2R04a34Koa+w2MroehL4JdeQrNofX/BwaMFpsHyt3pxfbCutZ4tGu3dKGvMLWfF13vWGwBu7w2df2ysT6vWyVoKBLjOIgg4i66BEqccQegRTEi4RQWEHuQHWJ1fAbUywhC2IeNcVQpB2qoEfscGl++MLGg4vC3F/UxEBK4CzXW7poGb1tEBWB5hygupElhdb5cAufvV+rTzWr2tzseYiLC9KlHkfeeEglx4MoYm6YLFQEhmZCXL4ZBvgWvUjoitdxFqpOJdH7rylDG+9C/GcX2DfCvgChnjqjwNDmjrtckJTagvAumItAbAcZjlP8nruYF5cC9uSKcl2vZh7n43zvQRgF78mAP4fgs2/AFI1IbEDofuIG/8AeXQTHGgoqaGUD0eWcNErGw26ypLJ5In7UnHyIbjdTybj8ftXHnpoEzffbCqusD9exHAo+pZErt3C8/K9XulKyZtfxtn2rpayXtTa5q81//kDxPOOrd8gxdHxuiiZ3S8i6t4As/iDQOO1cOnqJTDOgJwihR3I2IVpRJAh8CoPeFWcpm8wrvsJ8aY3rRlj6JB9AniRw2HvW2l3uWXdpdX+yTrbQxAVNyC8f26weLCWY4UO1kpjvdakUE6/PXdOe35m94QCSoEsXkPu3Ah0fwlh5yTaUvPng7jA5naK9WGJY0suMj8Q67vA8SXkzYCQaLkcuO4hAJ+t7teiC8TC9sfnHbx6VtbXUPp8pFW9+2kyaZGUBXMYIZwzAa1tZ72H0+NrnaEy+yr7v8YQrKlYE5Bk30MU5NAixCPrl2KcOlBqCFH2Ecg+mu4ART7CmZ5AsvM+dDa/E+ee++PizBYKXWQNT+Gdo9HoWiFEXi38lzzp5SsHCGLz3rAzb6aj7YV8p1ZWvYDzpiFjAvO8fSYYeFQYwcxmnOci1nTUWqMRdRFm60g33oPHim/DJUePo9NwEAbAgUUNIwboZwpNpFhRPTxXpN5glKnADCl34Fuk8rIsy37N87xPEiGQG1kIse8k+jxDPPpB31jt31gbnAEYem79ARcq1Xb31vt8tpCVJKgopFIaa/NxSgC10lATiYWHCAkUTpc9gi762B4An1j7fujOO3HT9QtY6kbINd0bmYfkS9CV6SjMeDwRg37PuK7bW1hY8KIoosX/Ud/3P2eMUfsK4YvQAcyc+ccyWlgUr9YDasvAyuwaENi7z2vfwbzOMMX/p5er0MCZFTk9l8UhSkil0ApGuLz1AUz0o8hON1AO+zCNN6Mf/QBCH/DJXU2aQ1mYeDwSk0lcdLveI8aYrSRJoiAI/rkx5jeEEH9W3c8+J3iB8Tw5ycRQset6sRkWZiKw1gEto+UAs0Xey9otoEPvy+cBRxfBahjypcijAkYGkF4Tly6fQZY8iHSgkGQJdOMcRsFhEAV4rS7gXYPhKDVJmkIpaTzP2xRC/FYcx4GU8seLonjbcDj8XLPZ3GIbx5B1uK8UXiwsfMqmKS6PHjSs4qbmFD5rjllCkLOQMYsKVeeo9AHMuZJryV95m8T892tuwSeuPYgllMxg4MHILlyvi8QcQj4+h/buj6Kx/u2Qo99DWvjY2qE4hRE6nQ7dUDtN0/Nnz579L/1+/3fjOD4kpfx2AEElBjgPcX/sHbRKF3eh7VmsGhGsRUTF2ucW2R5i3b32OxWRTI+z2v7UI1g/nzqjJIpccMCI5vhBS5U2QtixhKL78MoUKj+HeDJGrz/k2ILuwiICz0M2mbRuvPHGfHt7+71KqV6e5397PB63v5xqIXxZi4AZm6/ZuWXttioDcQNl0T4Gb3QVgmODNwVtMD62Euh1fle1zLVfYe7NykRULFC0jjkYVDiECtZ6hMUQ6LWrCmgs45nyZhSDb4OTbdI9mIWlZQSeKs5sxg+trW3uVorfxvnzm/dLWRwWQlxqjOkzjrznyvuDhnyeT38a3FG9X8lt0sAFMlC8jq3nUGvx1Y5muLgKIKmCSux015GkUyqbcRyOCbGcuSxGKE1a6Rs2Y6W+l6KUkKZPscU4jR/AE5O/j/Goh27XL5dWDiBOTf7IY4/81Rve8HUPEbu/7bbb5MbG1r1xHH9Ua31dmqZHa3Pwtttu2+cEc6OC8ezCUVCnXXBaNGUjd5lJeKCk3UAliJwEDilr7Jzx+W9ZuvbYSvbb4y3Lp/Aw6xuYI5iau/B1CYOwO51oyFUWmraipDI5heFwc6kCnFj2cKRrQ9c7rZAJd33godt4AD9c0fHtt99u/uiz//3RUsrHHce5Io/jS+offPXVV+8TwNyorPc6BIw4twNtHDgiRSDHCNQIoexjpE/ggcE78Zeb34tzk8vgiR2g7AF6FxIUvUNuXI9j+KbhXpXZxwGhtWTZw3xtEEp9KzYWYK9bmgw+A9IBcrhYw5J3FktRAj9sIgi6oowfh9j5FfeI98FX/OSfvOo4mXx0sh9/549PmkFAFsBB5ThL9VVuueWW+Rv4mh/O1FYXpH0T601RwsewvAJaLMJTE3gywYb5Djxc/BPsJgra+XUsizuRS5/Zs4sefLEDZRKO7aPAjtpBYANLZvrEXu2ifjLzHpFBQLAwIYMWZaTdPoLvBXCikximTSTI4EdNSDeSYvhxLA5/yku1d1PhX/YJIcTj9aoGQTAxxrRKIcKv+ZV+gUGFGEB5dxR5UwoXkbOB3HTw4PgfYxdvxFJjCE8Q+rKKE+ECTkDCy78Hp/EmhEGKYRFC5Z/AJfhFhGIdhViuFDvSGWjMlMp50W8H25VTO5EWPqc6AQUtfgHD0cUhIvEc/M5bkC78IuJeG0VyDo3GJZROoLKs0KXOlSvLVwg1eTmA/zz3+8j2p6jXfRPwBYajTQM+Bsysz07ehsS5En7YRux+M9z0EqjRDrzAR1vkaOFJeEWGrHUMWedNQJlgsiWRxy2Efg+ZjuHiNI4GfwQK18zRgWSlkdDG2e6vvYT1qLV+XaTs/SN5T+ljJh8jpqivxe+HXHwXSv8aZPosK6GhR/XjRnhs60Zs9X8IX3f4//XayVOX7t6N6/U150+U6sCzJl9rS69de632x0WGQwukSx/j8iTOlt+F3fQbsKTHWNZ9tIp74WVDRNsDBC7lBjoQeYbo0hRoDDBZH2NRNxB3jmAkfghrPQEv+3O03HW01UNQSFgcEGBkwdt568IiULUVwe+VCUpCBVULvt9ARq91gX5xI6S5Gk2zC2X6Nk6wiDGaTLARXy564buhm2kZNO55Ser97X/iKFxrHHwG2lsjDdZ1XZJN++MiQzzxIWV28tfilP5RFI1Xo1uM0dl4Bu7mOmTLhxMoOM+dgisKqMMHIEj2djqQgyHw7Dq8G1+O8pU3YHdLY2NtC6N0jEb4LI6rf4NF+RfQamGGEs65jy24RFhCFTFsSuhSc05hELbheD7yNMVolGI4KREd+h60LvkZbGzFmAzPIAhbSNME0m2j011AJxpBosigmoWUoW8gtwXMDoBtpdS/DoLgg/SD9x1Ee4ezmbwe58rvRiJfhpXxJpay56DiLaC/AYEu1OIhiMuPYJgvoAiX0W724VHa12AEWSaQW2twHvgUWv0M6tARbC0dxtaah0fyt+Noo43D4WegxNhmE9dRm1MlYJZQYvUAqziyMgrNVgHlAjSdNayd+xTu3XgKC1GA1WgCnVO+gMFyO8exFRdGHSV3ticBL88KymhfnUySBWPK01prMlN43HXXXRdMwdf2cJ5NvxuJeTUOilPojtchkpg1e+fgCpxuG3J1BWp1GU6yAzlcgxsuQOUhTNBAefQAxlsDiE8/gsAHWu0IuXLRT3vYLG6G58U46N4D14lRgOoOUNLXvCSoLYNKSbQBYuTmhSjpL/ELiUbkwJ1E6A8SKEMu4waM9NEMNLJC4rmNBM1IU7hYKSXFLAmjpE8VSJM4nmwOk2zyJZzjL+vh9IqrsKTX0U034EqKwZXwHAfqJScgDywAMkDhLmG5/G1E3bsxCP8FUnkDlNygfGLgqTMohUIRupCnnoGHNTRWLoemQA5knBFMKyEJ4gU5m2rnD2H8U48QK4nWXyCQF1ZQ6NICUroscOWxLg4tvhTPnCvx+DNb/N7VxzwkyQRPbxaIml1cdrgtD3aUTLOiUI4jpZR6MpmcO3N+g6BgHrfccsu+Qjg3pLM9QSPZgFppAa0W0O0Alx6AoNcB8WLK7SsROOtAcR5Pn1nG5mARbpADroY5vgpcsohyuIM0SeCFDlbkBF66je34pVgz/xgj81KYkvPAq6wgG1pmM4brUUPEJfJsjElcQBab6PprGPr/ANvN/wNLSws43vwwjpe3Ixz8Wzx+podz/SUseDE8vQuTT5AVwMZAm4SyyoRIS+E/+9jTT+/O/eZ9ApgbTgs9hCigtYHnGmBxBcWBS2CkhJumyHQDm70mHP84lmUINf5zwG9Dp0soihJlswGx2oBZz4F2gfBgEw3pAJspevlJBFjGqvgkPHwamTlcOY20TR4hy2AacFrByJSVVCacJpaaDnb16xB3343IvxaT7Y8jHP8Gjrp/DiMX8PTuMvqOwfKhBTS8EjLrodfTGCWO5S5CJ1HDf+Rd73jHVoUQ7i/+BUN6hxoIOgHEmfPI4wwmdOAggShjOE6K0sTYOD/E+kaAhr+G64/+Cxzt/AGyMrA5fck2RGAgrr0C4sgKtO+jWF1CsNRE06VY7hGoElheenMRRJYTULCInHMbzzKGJdpeD7H7Sjyg/w3QvAHHvT+Gs/5P0N/6DPrmOIIowtWt30Jj8rt4/FyOYeZiMBxiZ2fbLEYCngAGo2Hpnf+5eS/gvlf4guGQ98VdXEAZtfBc7xAWd/4KL2++D4XpQE8W0TAJ3GUHoTrD3MBTI3TMH8CbPM0zGrg78NQ6UkTYjb4bqfcNUE6CVkehyCbo94GPb383Lu+s4pWrH6X6MsjKEErkVfBJHTJWWQNUr8gopNkOFtsjvPzIQYyUxNMbCp3+KSTxGJAnsNAeoWHOIhs9jUEWIx0JjBBANQ6LqxZCuT4UuPfxZPF4+tnvSD6KbSHEo/WP3o8Omg2HbfBGBNEN4XhtpEkDw8EYC/7HbFKXbGFpWaNEiCTvoCgXIHEObXkfHJlia/sa7OhVLHTGUD7VdQ3gmDHX+Qn8Alta4dT4tYiCbUjxZ5UrmcuJVQ4jWnQbkGrgQpQT5kDavwqNpbfgkqUGnjn/KPobd0Mki8hKHx1/DZ1Wic3sGyDdb8OBUmF9ewRl/gpH3Ajntt4iT2+MUaz9ckcGn7zl2c7fcj70+Ifed1kDD538y1tJH9iPFK4GpeSjSMEI34kD6+iNvg6PDq7ASfkvsBL+D6TlCoq85GhhxUEgFJUbAmoZRhk8uvF92Mi/F9cfHCAKUzjpNptvVF2MFth1gJVoFx2/z1gAuXFt9RBMXcLzlUeEKSDdJTgHfwaj7vdg3MvRHf4i2vlvYyftokQER24hwwlshj8Dv30TDukncbo3xGrxMbRHj+J/fPqAkPH95iXy10TkhYfTzrf94JEQr2n5+j345p98vxCCvITY5wRUs8nJoY2H4WABadqH4w7R7C6i7/0YlLkEDfNHnMOvRcBBm7SoDnYRF5dgI34nEu/NaEcA+RQUmZFljsFIwPUon89wMamGm6LtZxUszEVpqhJwtcOYEkNcRPIsEFyKfvM2OM13wE/78DZ+AnL0QeSmzeHjDX+Ec/HrcD75AZx4yQ249LBEmS7iOuNiuPE2PNO7Er3eeaAIxc7h23DgylfII0duwu5w8qrIS7cgmw8Yg52qqJT8Wk8jc7Z2Gmg4T+KQ/yg2RtdDR8ew2N3B6e0bMDZP4ZqF9zPLLjg4hLi1A0+NkeYreK7/dozyNlaC+1CmEbYSF1s7CqOxQRQSipOD6vuNzArGemkun8DWIaTw8xIelBnBk9uY4ArE0bvhrn4v3PI5qI1fhBj8R6RFiFReBtftwyOWlWoojHB40UE7kki9VbyksYqNxcvx7JkJDvoPw3EEusdOwl1plkqhWO1mXqqjG7fLhWPLAn9Vg5Lmazxa2Dm/0cCy9wxOrPwnbGQL2NXXQogdnD+7DRX2IJfIPteQZQqHo6ps0ogjB/DxAPLkJHbKJoTrYjwByEVAIYHjUcnAku8BKh9BFxMb9TPNDZxFG5NJmGMFm8E/Rdn+AVzmxQi2fgHj7f8bWdFCqjsQYsBoo5BLOBz9FY44z0ClRzFOXwtkAyoiicOLCl1/hPNNB1HUxvIKmZUjOZlIGTUjoyc7i+lkdPKWH/uFUIh/FtMP+1qvKSB+8Zc/YLqNFDI/h1F2KbzoAFotgzhdxPHOB3Dt4V+CI3aZ/fqK0scoPjBDaQL0k5fjTPKPcKp3E0ajBJ12jpVlWtQC/f4Ak8RFIxhgwfl9rPp/gXZABeWs/Gd/vyFzrYeevhxP5D+IxeNvx5XLCt7WDyPd+i+IJyPkZYhR0obreljoUJSwi3gyxGRSYGyuRqOxgMWOQRBI/gzlGEU2wiD4PmDxHVjwU4x0UOTxmtPY/gnk4/s/opzlX+i84qMfogkw5iYHuJu9VF+LnMDx3RhudBBxeR3aYYJmMILrhGiYBpb8bQjdw2b2jejlL4U0u3CVQKaX0XTux6Xt90O2rkPsfisCM8ZiM8WBZY/r+wxGOcPISx3gmPocovJRpOUVVRRxzouvjcupYJ0I6La+HyutPlr9n8Zw871I0xhaHEKSxsiyGI4iRdHnekFUMqak1LXxp7hmcQEXyZhC0lw4YozA0Vgbvg69WOCVx10E+j6Zbf6+GWz+d+PJ4StjHPuOzUf+9mf/Eu8fvuc9t+t3vUt8zfoKnHEWYtnZxKUHU3jKhdCk6RcYUElXx0MqTmKr/Ec4Fb8RulhDw5VI9TG03Y+j09hE6a5iqZXgyOQ88X1kay0MgwDDYY7CCOSygdSswik7EKoO9bZmIDH+TC4h6rwMNxw6jWLnvRic+RUkRQdCtqDTlLuGSZHDUQnynELGSzgyw0Lbg2kuMycpjUJCtQUQQCNGWSSY5D7SAihKhXb8n6QZ/5LZNUsoDLqj/rM3OVn/7ZcsffjUsbfePlpb+9HHDh586owQN3Lfwq+l4QwmBmdP9bEYDyApK3Mwhuh2YE7kGBx8E0r/VXDdwzgWnWcFzlFUTGADQh3GNndyXILpnUL++OMQoz52m0tYby0jagMjZwVP7ARYbAusBhoTLhpCO5XMwcQmgoa3Yx1/B8fX7oDofwBJ0WQFMdclksz6CaNAwvcKGD2EcgP4Qchh6pNYQ+vC5ilI4iz0nLKIBS5dDWBWgdAD0l1yRCnRjWIRJxmpNCeKePdHQrF+vtmBbvv+Y0hP/LwxhopOWsf014g4cI4tnUKvv4CnTg9wbHMdi6qEabURjLaAzcPImsfhd2I0OrtQXshmuxpvQo8FxvERmFNn4G49BW0KxHGB7biHsdvF8tGX4YC/BT36A4R4FgX3e7JZQA7h9KVEknkw2XmoyUcwnnwYMt2EkccAxEhTzfWFySfgu7IyMUtI2YRStKAZHMdjU5KqjDOkRMkrhuyVNRxsjTGOgOe2gMXhGtpeC2vmXTg7aRu/9bQ6fnD5ErRfdnShTfptfhVM/DtCiIcffPA27+qrv83QqPSCr2rQyDm8tIWJcwN2dzYQtjN4YY6o1UBQZlAbj0MOQ6B1OYrUh94awjgK2B5DDkbwnTXgkYeg+2OMr3sZtr0mJmWA1uEjiJoah9SfYkn9Ftv5cbHMyqN1+mukucI47SI0v4NWmSLVTWh1BNKM2YVM/gMyGwLfcp2S6glSHaKysBVEiRDZGpHIOZ9BQJoBHKonFL0M8I4gTkpsDYG26EC1bkbf/BRipyECJzbqQFg2vLzwFdyJbLY28/Zr/+Qp8+lrLhd94A58rQxnK/GgnQ6OHM3R29IYnT+HE/E6usdX4ZcGziSFQ6XiN3dR3vcgRCOykTyuCyx1YQ4uYqAcnC8CjCMXze4xrBzsYtX8KprpXTAqZOWMsnoYWRaG5TqZjAwFCAc5h4DbCCBdSF448jT6LrhqOCWMkLeSvpOnfcA04bgR8ixGQaXLhWsrmOhnIbyXoFj+ZSTRqxHpEU4uA554N0pP4KR08BK6jqDsMaN0UcgiV0IqKccD/OABlxMcfmV+gm67zcg77pjmy33VDeeQdzcOOU9hoRHhuca3YFNei/Ob92N7JBANB2jmE0SHFuCOU4idLWBbQx85jKIVIDlzDrupi/zAMTQXInSXrkKnOcQKfhOt/L9y4kguDlc5h+T6tcGgBAX4PvUKKrkLic0+Iv3LcIewOKX3SfYT5lDlFVRVSjlVLE8s66fGFOR7oAqjiPHwxs3op9+PN7zszVxHIE/Htgy9bHOQSZYAG0MCsyQOtYbwPF/Eg0dNsv1fEJTpkeH66W9/5E9vWlu57jfawr9yZNLte5aXxRkCi+666y516623ftWhhs7lzY/CkwW0aCPoNOGqb8Gp6Ea4ZY4iPYtx2kPjzC6CwIVYOQAzGgHtFnSrjfGGxijsQh0+AqctsNIdY1H+dzQnv8HgkXGOQpi08vfNikl5LhGAxYIpf8BWKzVUlJwf9D3qG+x5czUKxMxVTJxA5zHcoAvXj+DLCeeljvSleHb3JTh+vofV5S4BkQiTx6HECDoT2BkCz61P4HlNLIZXIGr5yHeeRHzm50QyHul8jKvdxUt/2ujRYhRgszDifcas/RYlm5rbbvvq5ACkkWclReYm6BbvwZIaY7D6gzjY8uGeCLG+kSPNJ8hJCVtZhe4WcD2FUIVYuuYQrlzwMFFdrPVTHJz8Epb9/4RCUZnXAMLEs/LynObL/X94cEHRWf0Y+x7DcgZRIJj987Ecx1GNuZrwpATY0PKcdYxCLODqhT9CuPMcPvmZn0C0/E04ufgIjup/jhBPQohDWCwytMUGRPBW+OrnkTGxGeH4C8hGqYLKV10HK0bH2ndwSSnUkcHY3TDm4/8ZeP0Id0wLTXzVKIYO2dBUflGWBdxiB8viAwj8XbS9VbRaPXS8Vdy3+d0YmSvQbQzY07eVdLAonsZrFt+Lleg8dpMFNJ0+OuZuGL0DqFW7cFTLh9PEaMyniVl6oJKws5JSVRq4y23kmFDs9y1R1KVnZ2XmbDnZglSL0uPy8qE7wInu51D2fwUmvhvh6By0+TQysQUtBpyssuhuAeEDiB2gR/VFxyMEahurrRz60NcLd/XdQgWXC4lMNJqdw1tDvPuh9ddl1/y6ucsYUdx3343K3HmnuPuWW8TNVZz7V7LJ6BAwQwRN85qhiVCcQrv8XWASoqFjHJarOBMtIzRrWPLJi2qwLQ+ghYfQzf89VO88wqKBRTUG5Q6lWIUimJfxXtsUok4So/91nSJW14znD2Z1gygMXFPN4GrRuTZBFTfMvoSq7DyLjCKldrMoCGAqM5RqFVHTxQ3Ne5Bnf4JUk9u6i1wtcpg5OZ5OF8fh6m9FlwJZs5xjHILmm7C4uA4s/SOTt7+Li5zG8agodCqRPHVdcfbR73zfK56+Xwg8CNxXArfiq2WIxz7kU/hflYZtK3ZQyXZq1EAeU4vdUSq4XUzb8Im+V3JUj20kZj+zO9S2/7Tc3dYNsCXm5qqLTEedE2DrFHJQMCWIEBpZYQY2aqi8SDVSYhEEKtl79fwWBzfQvdNvSdMJUq4fRN6oED5Zd/DwV8kdKBf/N7zu0hJ5OsTuMEXoC7RCD54XcLi5KHMMdaPsbT8h4ye/D/H2vU8a9/hvF8L9jdf+nQfX59vTfKUXpawqwdWTaheQMHqL1SsGX0IxQEP2EIoeAuygJbfRkLu8aEXVJo7ECNcMqosOceFJq7XXOf68yHWBqWll8aqsZJ1JPFdPqi4vU4eN1eXspsfWBSdLjSKLWR9QSsEYh6Fk5barvgUUr+DyY7F9CCsdbpMET2l02qvwO0egwxWcGbXw6JqHccbNKUSaFRj1ntb5ODmmyvP/0M2e/tX/8XvN7zj18HteNhrF3/zYM6PrxK23UgUyc++9hkrWs7yjv/feey+//nIvYz8tEVNPsN3HtHjULJIm20FiLAewxELbkpQ5C72S589+Zr1782XjLBeghZoLyN1TTKzKDq4EhY0Omjt2OuqdP6d7Vccyh+F08oT1ASosobUDTcoKizfKMFYoywy+E+OKpQ2IxcRWPPEOomWGwPgjSLIU2U6JIvNRtl4B6bcEHetHx2WRbflKlZd5rrisjEero7P/4XNy4W8dP7x69Pz6r/9ff3j6J9/ygRtvFOxHsNyhLmFf3WoVkfzlyCW4XLxtEDUXoV/vtGphyGkzK+5QBXFSyBfn9VVtZJ5XJLJK+KD9Pq0XZHsBzFeTqwsOMmeYEsfMOqg5St20Yo/WyJzEzjMVksrzFFlBtQbbKDVxhIzjHDn1vCDHUwuB22FgS6cEIhHqfA+8zR8GJglWixxB4zAc9WvYzo+yibrYDUVaauyOHc5tUB7eWMB/fVHkuhlR3Enr1UeO37oDnPsYcAd5y82FQSZfzlaD5FmvWK/VuhlcZ7Za1kUaeOxN7CTZbEu7zFiyXSQ6jvIMyEtHx5dzD42Szlu/z+egjqH2OpQcSsdxcYjp+auilXUnE37f7BU1ValZfuQZimzAuIHjkmPJha/6GOircF/vX2JTv40TCOM8xOYTv4qtR38Wo+E2WxFSb8AxVPEkR1GpHUvNAY4uaxxYGJrF5qQMDn0XGlf8vFxePkJVyYXrtl/R6Xb/9QPnbv+xb/6QWaGZ2onx2jMbW9+3sTt65YtZBOIQ9CDUEX/Dg/tzzip8VnGa0xc0Zh3ELkRD7c636Vw1u6/2bXUuG/tnWwrNVRSd7yYy1QPrHEG7wLZ3QdWncHrBusaI5UN1txP70VyUMVcqofjDAIU2cDkNLeS0tN21jyDIJMbDPvLN34bU9yLtXAnhNLjOUVKuwjM+B7Om2sNjg5vQcS9D+0hXtEwiksY/1GrxVaUjcxnHk0Ji7Af64de45+9f+ZHRx8KfvvOaz2ydffBbo+7lrwk9c3+apn8iRPy46w56GxtPnV/92JtianN2+0N3mdvvsOmw8xyiEhf4mxIZ4okPu6auzFXNrO3iPfeaewbXQM30i7NuIraE7NxJayKqqoXZfoSzkrPTE8+NGShkd3f9XeI0e2oGTdM79qaV2Xuw8sE2uCBScmFEg+MIKaA1zSlgVSNJCGUU6LYFwqC+vwDKnIcfXQZ57D8gDW7As2c38bnnJlhs+njl5V20o4B1Ha1TFj3SbyPdvQfbD77TZOOnxHiCsXTaZ/yX/F5r6ZJvPbwYpGRiPzOamIf6k+JjG9vu77/65dHpC0XEl9KSoG7Os2lkPGDWSNJO7UxxqyfWLtZeUGa+kGRdFXRabpbPOa/s2ZPW3MJeZ8ZhKjJg6TSNIdzzvb2vp6Vn2DazGIGtXUjm5AQGLf6+KwdYaEoU3IQ0gadCSISszpCa4soYobMLLQsUEljterjeCdHkBlUB+Zzw3JaD8UThxOIYTQeY6JJNzjhhiLsRRcnJVkuj1aTzkfJcXnp+kEZ3PxY9+b6nnUF1nzWTFff/8e9En3587ZJAtuRnPvPM2euvv7Q3v0BEHHfffbe6+eab5RNPPCGuvPLK+UILf20Qato3cFYZdO9OZ3lPkzkt5DQHytiU/xrl3dNrcNY4osr6nVv46Y/b8/+sqHQt3+sWdvWC76lfWNNYTSw1AU27ndUOqBJlMWT7XqplBKEPV5FSGHMtIupqTi1pfDXE5uRKPF18I46uLKERJug2FA4uNjHMgO3hBO3iQ3C3T8HBZcDCG1Aaik3wELWOiDJZK2VwqHQOfreKOlcKtxwXGRWsF4GTjR7pLOy899U/277zB37+v4aPZWXrfOhdsi7EvU/fab4neeWZ4UvbDfPazolLdBzHj43Hu5989tl/9+wNwcOFEORDZ1Prf2G18LpocGXHk7KluDJobavPl4ydyWQSV7OcPqvh865lnN4qbvZ7M6vgwrK09QITq5/K/ape8TyB2C9fSAC2HxHrDGyyznU745fWgWTMGEoVUG4HRUHmqoFLAFFJ7mcNUY7hyi2cHb8ZD+f/DG+99DgWnAKTnEION7A99LC79Ri87OdwlHSG5jci169EUi7BUzE6jSGaCGTe/S5pDvwrU3gETQ8dgwbSQpol9Zngxs6vf32WTb4+LcM1VwzvK8tzDzz6PvxZ9qffHI+P/+Zru62Vd3iOPJQUZhhEK+89dsXtf/BsjMfOnsXG4ajn7ZRly/cXi6LoJ51Oh/IdiTD0X5sDTPcON4yYrwJuW77yTrclxOeqidbrOddllE29GqalopOVBcG+gJoQ5pVLu9MtEkUEYNuW1crdrE5x1aVsuuqzY+mYqZ+g7jVctRKvv1PD3GWZI0sHEE4TEP4U8wBfTzI3oMyHju9zgwpijnE8Rr71s2iMPwefElPKbUyojnGecwd1MgNcQyXyNjFaeTfUyo8Qqsjd73ppCxQ7E3gQC80ScWOCXVsFjVrbvMUge61y8F2qeCjbGm41ugvEd7igUcso5+8PCxzfzvGeyw/gGS3UW73UebsQsfR9/9RkMnk0jnt/euaxP/vsg3eil+zCBAsvE5sPPVxuXg3z0EMwt9/OkXHmRUHBdQcQrs5Z5epRtI1ddfLNzyZ0ngBmbWbrdbU6BLts2WFPi0rI3MXvY+/bteJHxDOnBBriDPZqe+ivljbVOajPwKwz6czCqAlpWptYhRw+bn+ex0ARBZ1Sscut8ZXYzt+EhaVD8IMm8ngNjfjfoSFPQ/ldZOIATL4OHXw9sgPvQdQ9gii5D8XgQ8jCb4HqvhKeAbLdP0Y2/BS88BCi5W/GYLSNtSf/gy62PlDqdNsJvFy0nRiJfxLj1j9GtPoOrHS7ReTpUpeejNx7nN2n3z85fX7jw4/Ht5y7/Mq3fv31l4c31DM1iZP1URG8d9DCr10pxJMXnVv6xXdC3kdWLYDhS2Bu3oShAjm3EIFwRVUYZ6pYT+Vp3QO4dtpMQe+9VkC166ait57wCw3Gmi1Xhh9XDNvTpPKFCGPOT3CBxXChN2FKnBd9Xv+qKhGlTFHoCWchS0fAdQk2Jhh7Ce3gPGT+77HzdMLVyttNF2g3ob1LQRkkFLTigOodkRMJ8Akf80+iWLoBnkihKIYiOQex+5to9O+C37kEsnMZEv9NSDs/qTrFw8oZP2PGWavsF8vmrPrB0gneLV6yWspIjZ2hblHZNvROf0THZ37Rk0N8S5xfn5ztfYt8aRLnHoGa0lcS5QE9eup1xanPPfDpO5v9cT4qisM3GTz7seTxDPp/fxcKXsVb2TZ+wSCWO+5gd3C1g2oRO7c4dgFs55Dp5Nf4yzyiO9UDZhbFrA9Bje/P6fl7bPyK1dOur4wPqwSKqhfBhUs+a0o1b5LOrllZABfIjVnnMwtmcWc0naEgqJhC1oSB6yh0u+QUSlEWOVxPwFWpTWLkoar70yiKxOpKVCNBZyhdHyJ+CDj7w8hHT6AUTTh0Xp1joQU0D7nw6LsuxEZxRDwh/jWWVt8iTx5METqliIsmn5syrs8GP6B2fYlw/K+c4ysqaC4zCmmyPIfyfOHIGHrt516Rnvn9nypM8R2e451y1z55RgfumZcG5bn7/rM+e2+JtRtvxbQ0zkWHMYI5QMXEZxr8nu/U79mJZVltUYo5iPcCMKlaxNpcrONBuAvoVOmr5bX9CqsA1bF1Sfop0cw5q2Y6vr3u7Hq1OVrfy4wAat8CLZy9V+IG9EYGnRPq6MFxIpRUmwACvi+5KSFdj6yFophA0soxYVFQqsF47GN5UcLzDTwnw6T/pyi23oty9ClOoTeyjUnaQq9/FJ0QaHoaD45vQRxfi4Wll2K1/VZceiAUkT/G2k4TYvRBLHpPQbf/PqLFgyj7N0CNcnN8GaKxSoq1EpQ2vzmRZnO7a8TOg4FfDE6WKjhZlma3VBSIIXaVkbtFoSgFvveJ95pdIcSuLkVPayqZp3vSEz1XN7Y38PrdbxVit7ICbJGm2bTOLeS0uvdsUW2kzgWsevogpWrWZLw+oWWhZNvXOkPF3hlfsFo7NYqYIoV7ONHzKHIqty4kuPr+9t7bnA7DBFETfAFDcQQmh5YKhc5hdA6hPDjCr4iQupdQtXLifhqUaYpiG2bwF9jZ/UYof5mkCsT53wd6v4fSvYKPF/ocsjTGqP9Z+M1VOH4LG+J70fMU2h0HV6yQZafx7HqEdPQEFga/AARbMM2/i9WoQGfJoD/+BhEunDBNSrTNpZAyoFpMYrh1j1DjkQnKoIRyhBRiQYlygSq6Mx/mJhuEVUmME56THQiclUKel0asGZGdOeJ+4tyHf/fKtYoDVLuO5f4MwJnO9XSyL9zl9arU78/Z7FwfoO4lTBr73l1ZH89qJ4d1zxandgDXmsPFCKHax9NXNWea3Wt9vnlimB09+32SqZX8B/Sacg4oLoAVX25gEXK1sqLIUWrqYdyFh+ewEP8snjwl8On1t2OlfA6HsxQLgiDkBK4sUKo2JCY4Vvw0vPhZiOZP4zVXCuQUZOsL+J6D+84Aa2tP4uuif4p2+ReYlN8En8zvUiL3Xo3s0G8ibLSFJP9GKZEiQFf/N1xlfhw74UQk3mHlUF/lnNDJjD2i7MktSBm34pjMYwdY1EIsSIiXQsiC0r981SsG2UsLrhAyZaF75LrV6GYIXm0UzE/0bNHrgE8a1v6v2bf9zkzUzK5B7V6I6GxQydzCcmDKvLk5d805JXTPySpWP8/F5m9qprxWOsS0K0k1TDHFD6hKmXEoJoAIgcBScjHT1KbQJC6UQKhOwdn595D5IwhaWwgaz8DxOjaIpqT8Bw+0ZCZeQ5mchi+BbpgADRJD28DW76G9ex5Kj+AX9zPEPM67OLVzGKsdoOM20GgdYDOWiMYIF1oonEvfgp38h3Ck8z50sIZxHBpPbBmTD6mushnni8gybQj8sn4adk8rz4VQEg75yDxHYCnSeHnXr+MB9k42L9zFNHVeqOpzBozmiKMG+uaGzQKee8yd3waREAHQtWZsvQKLLTHWpmV9PlYU5xDLqd5yYdu62T3Uu99iDlUj61pZxBwKWfs0SoMsGUO7VkEl/Y8ImkAvIggb8RTCCds4nn4Gh9XH0QkdBL7HTbDZi8qclIMVMco6SAeLCPoCnWaAcRpgtLuB1cF/xGX6AaThYaRmATE5EoszSHt/ijy4AX6Hsp0HjDdQbWROyZt8FuPYxw6+EUe9j6MjHwHkJUI7rxLDbBlJkiHST8BP1zFJGtAEepXaGFOashRGl9IoFKbIhTndP4aoc4VxNKdb2V3HAExlO9dgCu/mOYFe9w60k1LDvLVDpfIA8sSRe3fmZp6ihbULufIizu3VOWeRxSJsR3O7cFPHUNXn2KKAU9DXvp5ea+5+OSStgparApU1dZrKj8A+RH49oxxKME0NlSq355WuhzBswuGIo5JBHne5C134nJJuC2Daa9f1j8ntTd1V1np9jLGLqy9fwMPPZXjskSfxt444ONJqYJI2oESMHAuI8Dheih+CI/5PFOrbGcp2MEKpPJhyArH7q7gi+RCONw4jz3Kk1I1FhXhS/xieK96MjroXr/B/Ao3gNLbdA0jiIbJ0JLhGI9v1VNwjQaYVHtr9DqjkjXNNo2pMv95bc0reHpyAf2Rt8tVydNYsupat7N/nMoB7P7MsuOL5c+y73sU1AfDCsK+hakxVQb8WHOJGgHwL1V3MmapV4dmZnNljCcwIuTZ8ODOhovGZY8nqLaSVVppGbpBJD/AVh51Z7ldYDEFQAQt7D9TZhGIKSaRwlLPnoBV/Cun6z+B0P4BKCpwM1+DKESYFFdKmm6LvuoCeQGa77JcwSiBOcyiTY5BL7I6a6A530c3XIGSKtFhBKXPkIsC57A0I5A5OOr+BoNwBnDY6LYXQizAYGUzGtG4ZExTVaSpFB0cXF6GCVtUxZH4d5vj+lPPPf2XKcisl7QI2X08mP6+ygfk5v97rZLpwzMeMcqBqBRzVEUcMG89Fl80rh9ZRuddCsOtfewbndId5wjMzW6H+cA/WWQXKkglJXj+y/V3HqX6PTW2jJFWpKDiVgmEU5zFO1VRlsOg9ATe7H73NHIdbDlYPNbnqWloozmiyV6JOLS4X5V5f90Fo9MGOCyHb2Nns4cyTH4FQY4TBMUxy8m6Sr8WD0SkOiA9hRd2DS8T/g74+hEJ1EPo5PGJTguIciAjIwtEwskTgl7iqs4vSGz6/c6i1wW3iJbHf2Y6fzsncLq8X11K/3YWW1VNMHtv6lalmU7kqaTIXEDI/bHRyZYXwesxa1laotMULZlVlbf8yfk3BTXXg6QVYxtzvmi56rfQKybWML/Q2TmmoCnLhaxBukOXQmWU35FxSHukFBZ+MRSkRh2zOmdEllCvRaUuuxEsnzWjhKZ2N4yhpYijS2oJKtFOffuYJqP5ZLF63DN/3Mdr4Q6jTPwq1OoJoHoSjUzarMyzCQw/XuLezeNvJLuV59SQV4LCzGIUUHxkgTchayNmiEWhQ0j2vn1O3da93oNXK9i5MTQIXKlccPjo3mVZfsF8uSsrhoxCwCvmruoxac63WBwiEqawNah07B+GwQ4l1kcoq4Chw8lHM3FV1RDOfkTyC2CtqpsRTJ5jUwan88UwmTDnAHq5UEV3tKq++OQ1Fo+c6ZoIoyF/hNOE44TT+lUQCpbTTRhIiQFn6HINIxa18uQ1pNGLtIdGdiqATLplBbXsO++/H+e0Md/3h38MjpySWinvw5hN9tCMyI3MIUDl8yQ2+SNRIvVs17CIKK2xeBYfk0/ooEH7gBxE8sY1TgxN4dvs7cfLolbikmczax9e/f152soetWhDLzm1ltZlWX8u9iiswFyDWRE4XD37oslMoSSnbl1rNVQTBncAIrPDJJrGmTmYrhxKIwf+qH0DRu5bz16x8RiXzvEkQxxF75bvtPzjH3fZ4TqdBaVNUsia+yqc5TUcQe0zbmRVCkcYUfMpcqZScwUz3y4KLzEeCh5lyKcyeJirB+ewV2MxewTEIB/3PoOs8wqX0qGsLJct62MEVrefg6TN46mwfozPAoeX70VlYheNSrkNMqCAcR7E5qksHcbbMC0+d3mqOq0uK6laI3BFC8vnKFia9bWRYxUi9BaoRoNvZtPEA0wmi5Z32cZrJR0scVTBorRtUMt0qSzN5TzF45F51VAOSgzI1pC6QUaUwPZr1ChQKntOC44WcOp4XE+4ZxDEFFUdgFz+xZ45LqOz8qQgw3FOA5pde69p6INbMRGTFFCN4FVHMa/kW+BQzU7Km/lrE1WJpHkKquU3VItdOHcVN0OTFKOnBX9SQimSvSwAez5kjE27Bt568DJ/s/TNuifN6/xdwyHuYQTJSJj2X4gg66OkU7XaCv3fte/H3rqXr0G63ZfSZk/IckCVCv1NDE/cg83PalZHWRMLkHhIRwHXo3Dl2sxKuyHFieYLIc5EUyooAOxvzPvhqlqdafy3vZ4iNnTP74zirlxJJWCsixKGNwcTFaDPh2Ll2y4NyXKQUxkA7QRKrbKLQDrZ2bbBLtxXxok8mCctHavvAO58mmqON6LXdhba3QN1yvgpokbQQs1azezqU1cDGlMytuWqYVQkmmIqpT+VBjX/NcEnLAe30Vs6raTxknQRjL0bvURGLMu9NldNCaJROiSP+x/HWA334vosl/3EupNVoOvCDLZTqBjyY/DSeHZ3AJe6HcN3CvwLcHSA9iDKmmMb6Wjkky3ma/wIOZ0cT/kAFNGgdCE2csM/int3vx6Rcxcsb/xaDHnBuY4LBzhOIt7tYC8fEARgvmmrK8z6AmSfvQkSv6ubBEAEpMLb0W1FS7ACFVUQYTjQ2t0ZcvoVuvBnaEH67kz2IMsIkyTAYkRZM5/VBCFUN6ZaUnlZFKhEnUJV3kCe/Zu221uR0h9fEO++9rK2J2TLOg16o2DvZljP9h+m/2vpVL3UmPnsJG2TC9U0r/IRzJxigsvY/cQff1QjcYaUnSVb8BiMXZ86extbOswgDhcdFG3F+AL7voROFSJyDeKpsYW0S4ZQ6hF7zlVBlD2nZ4d7JlVObb9o217ZaE1Vesf2cCRdwISkVTjG7wNC8kdl+HqwhPLCOw42rsIAV+ErBo2CZz73fN7VWPTP3Zhh6LSJqU64mBCYAFgF295D8IzkPESI3XewMJIaDCUfiUKp4GBRoun27yySZQU3KxeD8PaZq6aEZZGj41GnMOjPsfVmgivoIkQLILJ+fW3lb37sNWrHbdz6QtOYitbLLn1QYlJhGH9tj6/zImUu5Ni/tpNN59/hBqnPuEaMsQq0YLLRCThByKTGKJc6tK3zw4208euYyXHft1Vha8DEZjyxYBY9jEyhKOfRdSCeAFg1GAul3E8fwXFsShyaBzq9cF4HncDY15XiTYugHDTSaTURRgwkrDKjZh8OFNIgwlci4lR8n1qYJHN+zyFatPdWsbt5JZOW/FQEMTXIvHzL17A+m71iHCSk7lK/fh6taCMIQRU4USQpLgUxbeWZMBiMGcJ0WjAmQF4S700xTLQK7IDTZ/MPUDOFj5I44grWcrJ7AKWok6y1gYxexktNTL6etS2CbWdpUVkYTTbWzuQZhLVYqTxqZSFylhErKGGSlwzZ+UXpcro7umVhsnlslN8tsVbMkpUlJ8cDTTfz5A6vQeQFfUou7nEvfnFnbxhUnr8R3fuffxWWXn8D6mWe4kkk6mXCGcxQ56C6tIGwfhnBtxzWqi0SETzRe2yO280oVzk8JtXkMrXN4ro+g0YYfWpHK8QxFitE4QzwZIxmPkMR9JqIiz+E8/vSEc/I9l2LllaUydifKyq0orCnDIdY1odj8wZI074rl1gSSpAajJEcgYijhInOIVeZcRDrPbaYwZ/eIHEpSZxDKQiafum1fS3l8UznLXKdSaiqY2nLmCk2sUDxFjSTdCVcf45TAKZhldRQiVGJOXAthqrgSFyP9xR4z/Y51psGVwDgFnjzXxCj2EVEjOulBqIhZLDfF5ggWMm8pOpBC0ghXc/i3bKZtaH8JbiQRBSQUaWcrvPz6BNdffxW+/jVXIWwewIkjTe6rkI377IMgRY92v+sLhFEKr9kBBMFCJYymUnk2AyrPyftnyxrS78lzTR2yUKQDTMo+8izk+yNCJbd0Ot7FqL+NYb+HNKPAktBugl+67UrjOhQNQ2VYAZe8YPy7rIwjquMdUVKplwySKoWQH5185+z4mMHGFGLNi+nq6YRaUVFbCRUyW4MzdZogm3mW2GwrWyu462tzGzlJpc0Bsq6IIIkzsFggWWeAtAiYCyg2w2irkFLoVuySFpkWJ7QaNfcnthgGEwIFetKDiUIgzSUafobtQYA/u/8ybA+aWO1k8D0PQRAijEikBRTACc/3ENHrMKBexQjDkB+N0EEUKn7Ox1Sf2X5JkkvfEmec6i1FzpxgPNxhZ1GajuH5IVqdFXhRm4mLQCZFO1eXiOMJ4pjcv9wjGY7rYDQcYHtrg7lCu7MA1wuQZbTzByjzBEYnMAURDpUEcphbOMev/RkMh31MxmP0hyPEgwliSnGJx8jSFHmWIUlipFmGgvzSOueGjWmScoUuFhFkheoSO0OB46sj3HzdFkXDIk5ttY+poXEB6jzV0iuuYrV8+j5VK7ULT9yIikZQ9W967tJfR8NRRG0OGl6OYRrhL595A3Ynh7HYnMARkjlZwBG+Pk+O47rwPVqMAF7oweXFDBCGES8QIW708HwfyvHgu0RAHt6WUZUzh1PFrHioupxXRMrvVWYhf1Yl1tTEygRNZSr5OCJGKqg9xPn1daRpBiVdXpBa7ypLhcSEyIhLUrm89bPQ+hRcP0Szs4JGu8t1EKIwRJ7RGhRoNEJ0Ol2OMRhRxRNX4fChVfh+hDjNsL0lEQ+20YqIaCX6gxFGCZXzd+Dc8HXfhsmYqClliiKXIilmSZqAGjDmRcFEQItNi0wQL6FRNgjBwqRWCSwxigWW2mO87JJdbkBFO8l2CKmdOTPkjev8VTuArelKwZs6ZGozr1Lxa3u/9hjacykEbookD1Fccg3ifBEtalpBO8wBPI+onBaPehEr/ut5LitbLu1e1+MdTMoSPSfHDU3QPHj04kcdbrbXG2mTXwkWL1FUyCf3RnFdJkIKSdva6qPVbmF1ZYUXlHQLapzdaUXwnRKD/g62trYwHAyxvLqCgwcPwwmamEwcDlSxekDBRNZpt9kHQL+bN4yr0GwQCpizqKSCnsQZlhoWpBKmjEkLmtXw/yIPUhpnoYOzhA+6Eit+8/b6C4x6SnmKq/by8wyFOIUv6wDYvabevKtn7/O513V42p4iFTMg6XnOp9p3wHaibatH9ngdCs/KGeEScwEpVqGusp3JYuLYBonRaIyNjU0sLS3j8KGDvAk31tf4b6vVRDOiust9nD39LEa9bTQjF4tLy4DykWQWdQ2jBnMXssRYtAibac2udyqlp2ihC/R3dzGZjHgDtNvWXyG0TljNspm8M4v58y/JX2fUk/75LrF3kaaBqLTEtXkyN+i2HUkI4vM9jC90zr0oJy74aOaqusA7MEUDLfRtd7ZNh6e09JQVM8fxoVzKOZiFvVn2XqIgRa5e/PGECYBO7DqkYJJiSSXyKB4RmEwmGI/HTOChT56/DMP+Nna3NxHHQzQbTbQ6i1wnKS8EW13dbpdF0Gg0QhzH8AMfUaPBymOvt4sssZXXqdYicRuHWLn9pfNRmDO8e/bTL+a+ff4KWlPqwl1Wo2yznTgfzVPN7Nwl6jTzqddpDzw9/f7cVyi14+I0NfVfP++T2bjI76j+1tbG3A1VXk1y/ZJIzDnyRlNxijRGSQTg5nAoUIStJeqGTrY69TK0Ok1W5BgOJ6yg2TL4knsgkHYehAEkWnxVKpVPBEP5h2Tz+xHJ9BwFevC8kMPat9fPsOLqBk24zhImkzFzAhLTtNPpmpPRkEU86Rqk55AFF08yJGkGURQUNlr/6trGnPOd196Q+sfXSaLPI4A59X7ewz6XGFJ706aRPBd6IufONY/bz2/RPe9fZMFmoQB79/DzCXr23uyjeW/T/HcvJlaI3Wu2hkgZyyZDrlDCmAftZkktb230kzU9K33JSKQpEcCIHTpkzYxGEyRxAulIthaKagGVQ8BQxN9JkhRZlnJ3VkJXm4GD/u4G1s6c4g3XancRtboopQ9qqON5HqUq87F5mrESX+thushgCkqQKeBw1Es9IXVU74WTNZdwMfWqX9hrZY8beX531gRQVRApCxvaVWnUU6KbJ7QpwTz/fHs5ytw+3XszFyxsdYULNrqoImdnhGB/K2n0s/PMVyibv5eKAKhfgU4ZXKGcQS9sQVOV1LzgGshZXjBnYBAmI0Ua1rxrNfn3k/VFrJ6G71ivYTyhXIQCUSPi+SLHGb3e3t5mLGBpsQNFiqwfodFe5JrJaRZD9yiknaycJrTRGOYpgjBCq9VGp9tBPJmgt7vLnCXyBKLQg1hbO2WILbiez6CMNXPIzq6Aji+6LsCSvHrufJHPW5+7Li03Ux9JXtsgFbsrSfRlWc5smCyc2rKhvxmx84x2NmnkZPZadslmMVlDRc6gC8lnmlRqYD0ZDXhnUwIJKWJ06UKTqUXBpDbjmbhCq9XCyZNX4rrrrkUYRRiPRjzD5OYla4vmnJ1qlYOLzuOQ/e+4LDomwxHD681GyBbLZDTGYNBDv7cDqpJF70tFWIGE8iLmCoQJkDlM1h0Roy4SDAZ9iwR+5M8+yuaIlVFWEayLRPDrqd1LtqyNIKMbox9WY+wX6gh1xG2NsVOtIQqQtPKcYgEq7iEpWMES2ZQbTFPQZzuuZp/WxTsTNRaFrLXdKrCTYFHWzAmMslq3/Tu9OT6OFifNMhuHUBEGac4FEQG9X1CNI6u3ZFmBlBQ8jmkgk1czDkITSjuTlEBLGDb1nKwbdgsJiTD0EUW0wTwErotmo8HvsfJYlqygEYhEC7K7tc0In+dTmJnL90J6Ab3XaETotJpwpEC/t8tyXoUeGi3yAtBcKeTJGAUFqSRjnk+qtyy1j9GgxHjk8LUXF9oMzxOQlKU5nM985rN2J1DKM9v2BVMgTQrXAWTMm9q/WdSP/1ap3/Vi1LtqGjGsidoJsLFEVX+HgySr9+o6BHbMe+js8fXbbIqR5lyfv5o41q6resH0msEepZhwbZbzzL60u5DsfpdNIlk/JD0cBlBIWyaMgM5BhE6YQBgRkBTyjiZ7OggILHLtjhSSn0fVeyzBKOhe2fgALlLlEN5Au9h6MGkXEzhDG4gAGk3leSkGoCzhBSHa3S6LClYsdcF4BBXpJgJOeMFSvsflpUXGZJKUiI9+e4iDh5qIx3089+zTKDLNLXUU9VfKdhGPJNJSwXcOcG9oWu92q4kw8OB8+9/9O3aH8sTOgj6suWPds2Z+l1XmT71YdSxdvcA1ZbNXrnIs0Y8hnJsqcTmuXzkvqBmQYg3ZKk6+5TbzlUqmCvhcinhtq1fgUb2xCeipC0xYp1Zdfs6idBQdw4s+5XA19GxBIn7w4tcisIq6YfZLxEXQM9n21g/CvzlPbIcSlzgoRQdbLkYKGH2fuVM1t7X73AuICEJEZcTK32AwYm7TbDaxsroMrbvo7WxjZ6fH16AdSwEd/d4Q40nMxNxuNXiNNjd3MByN0Wg0IB0fDqGFzQ6ovTJzJNrUxQhxykFrKLKx6fUWjaRIJQOT56lxXve6m/C/fsTQ401Mhjvsr2Ylq9TclMrxArhUkUFSoGINSNUFpp/PIS5imV/w2QthARcDhMyL+LzWJWzyIomHLJsgmYxZflMza7L5qZcRKDGEPIysP7ELyzpvyP7n+EgNLwjQbLaY8Cj4ZdDfRZwkXMeQUUjKQmLnlBUppP0bt2T5T0ol6SKjScxO+DRLmDOQh7TIU1PQpmLIVGE4TpCnMS0yrbVwHSX6eSLIIugsrSKMmnC9BsRouMV52XV0S40BzGvG8ybUnud71mDebKtr+9m6fbT7h7vrGPU2eAd7YYM1Ydr5hHEHURMO6yGV42DKwK3lcNFFel5Y+ecDgeatkfrbs2zoPb9xzjS98HgL+uRIY1L6ekhGA97hrh9w8UnKKSRFjxaJ2DZxEBJJFt4lpII4lQMqQbu902Olk+aeLAUSBcQ56pnl+eN8xApsYoTRmpicrkYRllSLkSJ9SWzTNXLy3ST8uiCFLxkw92YHFP9eCmn30OgsoNVdMeQydrIsL+rFr6t7VGVb7H/8/iwNk0tczcfZTZM5Z4tgBYMt6ljyj7c/lHYJs366BrP8KnOHmCrrHNMTV2Em81G4tfO3poO96cd8Wy9EA1V4e8ntZSoYl8xfY8VD/ftZN9njc7+AABjGtcWsOTyMAJ6K7dtMJVs1i5A7Oj9bDWlh2BpwyEHlcFu+eDwW/d0e8iIXge/zBsx0jiyZUP24SiQ6FqGtOrWyr4TjW0qWlGHYED7J8Co4lbx87C4uyTdQudArcGo0jtEfjie9/mBnMEx2doZrW+XZrW1dyi1nYXGhKhV3IeAxr9n//yH1Fz6fM8FMgSAg16iLvN3mos6028lW5TZ0rgtFlD9fT3jPuS62qhfdo5/nPivzUCcoyaNJRYW5hLHg6wuPUDtvzh/yQuevRUIKk7ZYxtYWC+1gIogapcySFIN+H+PhkMO0Hc/l/EHSDShegNwgtDGIKCznq6wtqmlIyqy06KG1wiiOATOTtaCmW2WWTOJU53kWJ6M4SyZZSeYCypjaMjlKJJ6rJlrrOMuKUQm1PYzL3Y3NXm9tY3fn4Sef2vn0vY/sOFubm0Wda0dKEqFAtfnHYY/T6JqpVr0HVJsH0fYsWgX+kNwjrZYDLyiUjIKXyEGUUXw7IAsNSn/eK1rm4nKrTJIq2NPmHk0twbr62MUyjebYfVnVCiZAZkT5csl0xztEjBQz74eG/O02uHR26B7zkbgIK3Q5R/qUM+vE2BB2BaE4epGRvd2dXWp/SwEHvHddisMja8UhfyWUpygHMaW4Spp1womtJ8IojgPWMjdCKi0kebpkLoQauS4meZZNdnd2t3a3B/3heNQ/dfr09tm19cG41x/2RsnW2tr67unnzu6c397eALhKSF1GdY+zp9lsCucDH/zw75DrWipHeZ7juq7vBkHgOI7re57rKqU8paQnmC541NvEujlsUC0ZvtbaoYgtm7FAV+Ep495AZSlKUxhN/k9q2yTZY0Bzp0nvqRyRNruaBCunPsEURZ7rQhdlaYo8K8qsSIo0y0yeU5ZrUZKcLEptdFHSg3Ykax8lH2DROtYkCgJ+yN2dGNqddqZFjTgYodySdiCnrxlbS6QyhaeeKDo1y9fSUM4KlWkrWT6zF59+PkdRcZxJkRUiy1JjyjJWQhbSVcb3XO0qJZaWFztHDq0eMVr7o0k8lkKlUopECJUJV8Z5boalMSMpnbjVDIeNKBoJRyaidPLC5Ho0muizZ8+n59d6xfZgq3juiWezp06vF1tbW3TbdF959ZivMcgxttXi89/RaCTFsRPH3uopTwaB63Q6bdVstpzOwqLTbjbdMAydMAw9z/NcxxGCKJfpQQlRR94SRTDuQfWXaH8bhzKoWY0vS1PWE2gMldXPjNZaGyOM4P/oPAWv1/T2KISJe8HR2mkzSZIii1OdFrmO46wcTsZ6MhyaJElNMk4Mcb0sy+mviTOq+VeajIolZNpkmSYPeMX+ebXo+ibjsKrnyZsXU9G7nPtbXPB6/js1kkXnTeaCBWrcvfGmN964oiCctfWtOClMliRxOhlk2e5wl74/ZtPJHjup/s4Xi5QXPOpNKdttqNXooPIWfeX7gSwKTRtSpGlikoSmPDaEPIs4NhMiWh+4NH2+0J//e3FX4N5Rf36hIL/Y+EKw5ed7cF7kOQnefoExvb+A6wEG05KscwfTvNTfveh1X6gRBBdupIOrSZ2NdFr8Oe0nZT9Nn0dAURRxaflGoyEaZBWDkktKEYa0X2b1hK3rYAJeSCEMQdLz169efz6bePZ9AKtzz/dM0EUI4IVcY/Pn+2IQwBe68Bd+52K/5QsZ4sV/h2IBL/4FSosJq0WPERvEe7TkeQK42JxTs11hWi3xQgRnCWqIIbW73Hv8i/7dYmlpiXNWaSxTihkIjbKJVjQ6nVIUhb0JGjmF9v6NDa6tjMHg83/e738x27IMOahn+nI0fSZmT3m8mEm+KPLU6XSwJKWRS8LsUJM1pUrJtW7toOdra2sv9hoXu+YLmW/PO5/1Ijx/vNCOuthnf9PjxVz/xdivLzQu5HDz77/YcbGFezHQ4+c7/osx/ledd3/sj/2xP/bH/tgfX2Hj/wPkA2IZdX47EAAAAABJRU5ErkJggg==&quot; alt=&quot;Rick&quot; style=&quot;width:44px; height:44px; object-fit:contain;&quot; /&gt;&lt;/div&gt;&lt;span style=&quot;font-family:&apos;Jost&apos;,-apple-system,BlinkMacSystemFont,system-ui,sans-serif; font-size:10px; font-weight:600; color:#6B6B7B;&quot;&gt;Rick&lt;/span&gt;&lt;/div&gt;&lt;div style=&quot;display:flex; flex-direction:column; align-items:center; gap:5px; width:60px;&quot;&gt;&lt;div style=&quot;width:54px; height:54px; border-radius:16px; background:#FEFDFB; box-shadow:0 2px 8px rgba(20,18,30,0.09); border:1px dashed rgba(42,42,58,0.3); display:flex; align-items:center; justify-content:center; font-size:27px; overflow:hidden;&quot;&gt;🎷&lt;/div&gt;&lt;span style=&quot;font-family:&apos;Jost&apos;,-apple-system,BlinkMacSystemFont,system-ui,sans-serif; font-size:10px; font-weight:600; color:#6B6B7B;&quot;&gt;Bogie&lt;/span&gt;&lt;span style=&quot;font-size:7.5px; font-weight:800; letter-spacing:0.04em; text-transform:uppercase; color:#F2A83B; margin-top:-2px;&quot;&gt;sit-in&lt;/span&gt;&lt;/div&gt;&lt;div style=&quot;display:flex; flex-direction:column; align-items:center; gap:5px; width:60px;&quot;&gt;&lt;div style=&quot;width:54px; height:54px; border-radius:16px; background:#FEFDFB; box-shadow:0 2px 8px rgba(20,18,30,0.09);  display:flex; align-items:center; justify-content:center; font-size:27px; overflow:hidden;&quot;&gt;🕶️&lt;/div&gt;&lt;span style=&quot;font-family:&apos;Jost&apos;,-apple-system,BlinkMacSystemFont,system-ui,sans-serif; font-size:10px; font-weight:600; color:#6B6B7B;&quot;&gt;Trevor&lt;/span&gt;&lt;/div&gt;&lt;div style=&quot;display:flex; flex-direction:column; align-items:center; gap:5px; width:60px;&quot;&gt;&lt;div style=&quot;width:54px; height:54px; border-radius:16px; background:#FEFDFB; box-shadow:0 2px 8px rgba(20,18,30,0.09);  display:flex; align-items:center; justify-content:center; font-size:27px; overflow:hidden;&quot;&gt;🌽&lt;/div&gt;&lt;span style=&quot;font-family:&apos;Jost&apos;,-apple-system,BlinkMacSystemFont,system-ui,sans-serif; font-size:10px; font-weight:600; color:#6B6B7B;&quot;&gt;Cotter&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;p&gt;The chat noticed the second we shipped it: &lt;em&gt;“New reactions are super cute. Rick, Peter, Trevor, Cotter?”&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;And the chat is wired into the stats the whole time. Tap any song in the setlist and a stat sheet slides up: when the band last played it, the gap since, its first-time-played, and your own history with the song, including how many times you personally have caught it live.&lt;/p&gt;

&lt;div style=&quot;color-scheme:light; background:#E7E2D6; padding:16px; border-radius:20px; margin:16px auto; max-width:420px;&quot;&gt;
  &lt;div style=&quot;background:#F0EDE4; border-radius:18px; box-shadow:0 10px 28px rgba(20,18,30,0.12); max-width:390px; margin:0 auto; color:#2A2A3A; font-family:&apos;Inter&apos;,-apple-system,BlinkMacSystemFont,system-ui,sans-serif; padding:18px;&quot;&gt;
    &lt;span style=&quot;display:inline-block; padding:3px 9px; border-radius:999px; font-family:&apos;Jost&apos;,-apple-system,BlinkMacSystemFont,system-ui,sans-serif; font-size:10px; font-weight:800; letter-spacing:0.10em; color:#fff; background:#059669;&quot;&gt;RARE&lt;/span&gt;
    &lt;div style=&quot;font-family:&apos;Instrument Serif&apos;,&apos;Cormorant Garamond&apos;,Georgia,serif; font-style:italic; font-size:34px; line-height:1; margin:8px 0 2px;&quot;&gt;Big Modern!&lt;/div&gt;
    &lt;div style=&quot;font-family:&apos;Instrument Serif&apos;,&apos;Cormorant Garamond&apos;,Georgia,serif; font-style:italic; font-size:15px; color:#6B6B7B; margin-bottom:16px;&quot;&gt;the title track, and lately unavoidable&lt;/div&gt;
    &lt;div style=&quot;position:relative; background:#FEFDFB; border-radius:14px; padding:15px 16px 14px; box-shadow:0 3px 10px rgba(20,18,30,0.05); overflow:hidden;&quot;&gt;
      &lt;span style=&quot;position:absolute; left:0; top:0; bottom:0; width:3px; background:#F2A83B;&quot;&gt;&lt;/span&gt;
      &lt;div style=&quot;font-family:&apos;Jost&apos;,-apple-system,BlinkMacSystemFont,system-ui,sans-serif; font-size:11px; font-weight:700; letter-spacing:0.16em; text-transform:uppercase; color:#9A9AAA; margin-bottom:10px;&quot;&gt;🎯 Your history · &lt;span style=&quot;color:#F2A83B;&quot;&gt;caught&lt;/span&gt;&lt;/div&gt;
      &lt;div style=&quot;font-family:&apos;Instrument Serif&apos;,&apos;Cormorant Garamond&apos;,Georgia,serif; font-style:italic; font-size:21px; line-height:1.2; margin-bottom:10px;&quot;&gt;&lt;span style=&quot;font-family:&apos;Jost&apos;,-apple-system,BlinkMacSystemFont,system-ui,sans-serif; font-style:normal; font-weight:600; color:#F2A83B; margin-right:4px;&quot;&gt;27th&lt;/span&gt;time you have caught it live&lt;/div&gt;
      &lt;div style=&quot;font-family:&apos;Inter&apos;,-apple-system,BlinkMacSystemFont,system-ui,sans-serif; font-size:11.5px; color:#6B6B7B; margin-bottom:12px;&quot;&gt;tonight counts toward your stats&lt;/div&gt;
      &lt;div style=&quot;display:flex; gap:8px; margin-bottom:4px;&quot;&gt;&lt;span style=&quot;font-family:&apos;Jost&apos;,-apple-system,BlinkMacSystemFont,system-ui,sans-serif; font-size:9px; font-weight:700; letter-spacing:0.18em; text-transform:uppercase; color:#9A9AAA; min-width:40px; padding-top:2px;&quot;&gt;First&lt;/span&gt;&lt;span style=&quot;font-family:&apos;Inter&apos;,-apple-system,BlinkMacSystemFont,system-ui,sans-serif; font-size:12.5px; color:#2A2A3A;&quot;&gt;6/12/24 &lt;span style=&quot;color:#6B6B7B;&quot;&gt;· The Salt Shed · Chicago&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;
      &lt;div style=&quot;display:flex; gap:8px;&quot;&gt;&lt;span style=&quot;font-family:&apos;Jost&apos;,-apple-system,BlinkMacSystemFont,system-ui,sans-serif; font-size:9px; font-weight:700; letter-spacing:0.18em; text-transform:uppercase; color:#9A9AAA; min-width:40px; padding-top:2px;&quot;&gt;Last&lt;/span&gt;&lt;span style=&quot;font-family:&apos;Inter&apos;,-apple-system,BlinkMacSystemFont,system-ui,sans-serif; font-size:12.5px; color:#2A2A3A;&quot;&gt;6/26/26 &lt;span style=&quot;color:#6B6B7B;&quot;&gt;· Red Hat · Raleigh&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;
    &lt;/div&gt;
    &lt;div style=&quot;background:#20202A; border-radius:14px; padding:16px; margin-top:12px;&quot;&gt;
      &lt;div style=&quot;font-family:&apos;Jost&apos;,-apple-system,BlinkMacSystemFont,system-ui,sans-serif; font-size:11px; font-weight:700; letter-spacing:0.16em; text-transform:uppercase; color:#8A8A9A; margin-bottom:14px;&quot;&gt;📊 Catalog · across all shows&lt;/div&gt;
      &lt;div style=&quot;display:flex; gap:14px; margin-bottom:14px;&quot;&gt;&lt;div style=&quot;flex:1; min-width:0;&quot;&gt;&lt;div style=&quot;font-family:&apos;Jost&apos;,-apple-system,BlinkMacSystemFont,system-ui,sans-serif; font-size:10px; font-weight:700; letter-spacing:0.16em; text-transform:uppercase; color:#8A8A9A; margin-bottom:7px;&quot;&gt;Lifetime&lt;/div&gt;&lt;div style=&quot;font-family:&apos;Instrument Serif&apos;,&apos;Cormorant Garamond&apos;,Georgia,serif; font-style:italic; font-size:26px; line-height:1; color:#EDEAE0;&quot;&gt;35&lt;/div&gt;&lt;div style=&quot;font-family:&apos;Inter&apos;,-apple-system,BlinkMacSystemFont,system-ui,sans-serif; font-size:11px; color:#8A8A9A; margin-top:5px;&quot;&gt;times played&lt;/div&gt;&lt;/div&gt;&lt;div style=&quot;width:1px; background:rgba(255,255,255,0.08);&quot;&gt;&lt;/div&gt;&lt;div style=&quot;flex:1; min-width:0;&quot;&gt;&lt;div style=&quot;font-family:&apos;Jost&apos;,-apple-system,BlinkMacSystemFont,system-ui,sans-serif; font-size:10px; font-weight:700; letter-spacing:0.16em; text-transform:uppercase; color:#8A8A9A; margin-bottom:7px;&quot;&gt;Debut&lt;/div&gt;&lt;div style=&quot;font-family:&apos;Instrument Serif&apos;,&apos;Cormorant Garamond&apos;,Georgia,serif; font-style:italic; font-size:26px; line-height:1; color:#EDEAE0;&quot;&gt;Jun ’24&lt;/div&gt;&lt;div style=&quot;font-family:&apos;Inter&apos;,-apple-system,BlinkMacSystemFont,system-ui,sans-serif; font-size:11px; color:#8A8A9A; margin-top:5px;&quot;&gt;Salt Shed&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;
      &lt;div style=&quot;display:flex; gap:14px;&quot;&gt;&lt;div style=&quot;flex:1; min-width:0;&quot;&gt;&lt;div style=&quot;font-family:&apos;Jost&apos;,-apple-system,BlinkMacSystemFont,system-ui,sans-serif; font-size:10px; font-weight:700; letter-spacing:0.16em; text-transform:uppercase; color:#8A8A9A; margin-bottom:7px;&quot;&gt;Last played&lt;/div&gt;&lt;div style=&quot;font-family:&apos;Instrument Serif&apos;,&apos;Cormorant Garamond&apos;,Georgia,serif; font-style:italic; font-size:26px; line-height:1; color:#EDEAE0;&quot;&gt;6/26&lt;/div&gt;&lt;div style=&quot;font-family:&apos;Inter&apos;,-apple-system,BlinkMacSystemFont,system-ui,sans-serif; font-size:11px; color:#8A8A9A; margin-top:5px;&quot;&gt;Raleigh&lt;/div&gt;&lt;/div&gt;&lt;div style=&quot;width:1px; background:rgba(255,255,255,0.08);&quot;&gt;&lt;/div&gt;&lt;div style=&quot;flex:1; min-width:0;&quot;&gt;&lt;div style=&quot;font-family:&apos;Jost&apos;,-apple-system,BlinkMacSystemFont,system-ui,sans-serif; font-size:10px; font-weight:700; letter-spacing:0.16em; text-transform:uppercase; color:#8A8A9A; margin-bottom:7px;&quot;&gt;Show gap&lt;/div&gt;&lt;div style=&quot;font-family:&apos;Instrument Serif&apos;,&apos;Cormorant Garamond&apos;,Georgia,serif; font-style:italic; font-size:26px; line-height:1; color:#EDEAE0;&quot;&gt;2&lt;/div&gt;&lt;div style=&quot;font-family:&apos;Inter&apos;,-apple-system,BlinkMacSystemFont,system-ui,sans-serif; font-size:11px; color:#8A8A9A; margin-top:5px;&quot;&gt;shows since&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;p&gt;During the &lt;strong&gt;Big Modern!&lt;/strong&gt; tour that last number turned into a running joke. The album’s songs were suddenly in every setlist, and people watched their personal counts climb show by show, in real time. One chomper, a few nights deep:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;&lt;em&gt;“I’ve now seen 27 of 35 Big Modern! ever played 🤣”&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That same history powers the feature I am fondest of, the &lt;strong&gt;FTP witness&lt;/strong&gt;. FTP is “first time played,” a song’s debut show. When a song lands in a live setlist, the app quietly checks everyone in the room, and if any of them were RSVP’d to that song’s very first performance, an inline 👀 pill drops into the chat: “2 people in the chomp were at this song’s FTP.” Tap it and it opens the debut show. It is the app reaching back years to tell a room that someone standing in it was there at the beginning, and nothing makes a chat feel more like a community than being reminded, live, that the person you are talking to caught the debut of the song you are both hearing right now.&lt;/p&gt;

&lt;p&gt;Here is the honest engagement picture, and it is the number I am happiest about in the whole post. Reactions were basically dormant through May, a handful a week. Then the &lt;strong&gt;Big Modern!&lt;/strong&gt; tour started and the chat went vertical: &lt;strong&gt;312, then 1,067, then 897 reactions in three consecutive weeks.&lt;/strong&gt; In just the last three weeks of tour, the Goose live chats carried roughly &lt;strong&gt;1,500 messages and 2,300 reactions&lt;/strong&gt;, about 3,800 interactions total, which rivals the entire fourteen-show Spring tour in a fraction of the nights. When the band finally gave everyone something to be loud about, the redesigned room was ready to be loud in.&lt;/p&gt;

&lt;p&gt;The people in it will tell you what it feels like better than I can:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;&lt;em&gt;“Still an hour 20 til show time but I opened the chomp.”&lt;/em&gt;&lt;/p&gt;

  &lt;p&gt;&lt;em&gt;“I love the time counter for the song.”&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2 id=&quot;the-lot&quot;&gt;The Lot&lt;/h2&gt;

&lt;p&gt;When you open Zabriskie now, you do not land on a feed. You land on &lt;strong&gt;The Lot&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The Lot is a personalized home. It is the leftmost tab and the default route, and it is built to answer one question the feed never could: what is worth my attention right now, for me. If a band you follow is on stage somewhere, the hero card is that show with a live setlist preview ticking underneath it. If nothing is live, it reaches for the next best thing, and it has a lot of next-best things to reach for: tonight’s shows and who is going to them, last night’s auto-generated recap, the jam bracket that is still taking votes, a new album to go listen to (it pushed the &lt;strong&gt;Big Modern!&lt;/strong&gt; listening party the week it dropped), an “On This Day” card that surfaces a show from your own history or a band anniversary, a historical show worth revisiting, a bookmark to return to, a one-tap RSVP, even a nudge to post to the Flow if you have been quiet for a week. Every card is a real destination, not a placeholder. The Lot’s whole job is to always have one more good reason to stay.&lt;/p&gt;

&lt;style&gt;@import url(&apos;https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&amp;family=Instrument+Serif:ital@0;1&amp;family=Jost:wght@400;500;600;700&amp;display=swap&apos;);&lt;/style&gt;

&lt;div style=&quot;display:flex; justify-content:center; margin:18px auto; max-width:420px;&quot;&gt;
  &lt;div style=&quot;background:#0e0e12; border-radius:44px; padding:11px; box-shadow:0 22px 60px -18px rgba(20,18,30,0.65); width:352px;&quot;&gt;
    &lt;div style=&quot;background:#F0EDE4; border-radius:34px; overflow:hidden; color:#2A2A3A; position:relative;&quot;&gt;

      &lt;!-- status bar --&gt;
      &lt;div style=&quot;display:flex; align-items:center; justify-content:space-between; padding:11px 22px 4px; font-family:&apos;Jost&apos;,-apple-system,BlinkMacSystemFont,system-ui,sans-serif; font-size:13px; font-weight:600; color:#2A2A3A;&quot;&gt;
        &lt;span&gt;9:41&lt;/span&gt;
        &lt;span style=&quot;letter-spacing:0.06em; font-size:11px; color:#6B6B7B;&quot;&gt;📶 &amp;nbsp; 5G &amp;nbsp; 🔋&lt;/span&gt;
      &lt;/div&gt;

      &lt;!-- greeting header --&gt;
      &lt;div style=&quot;padding:8px 18px 4px; font-family:&apos;Inter&apos;,-apple-system,BlinkMacSystemFont,system-ui,sans-serif;&quot;&gt;
        &lt;div style=&quot;display:flex; align-items:center; gap:7px; flex-wrap:wrap;&quot;&gt;
          &lt;span style=&quot;color:#E83A73; font-size:12px; line-height:1;&quot;&gt;✦&lt;/span&gt;
          &lt;span style=&quot;font-family:&apos;Jost&apos;,-apple-system,BlinkMacSystemFont,system-ui,sans-serif; font-size:10px; font-weight:700; letter-spacing:0.22em; text-transform:uppercase; color:#9A9AAA;&quot;&gt;The Lot&lt;/span&gt;
          &lt;span style=&quot;font-size:11px; color:#9A9AAA;&quot;&gt;· Tuesday · Jun 30&lt;/span&gt;
        &lt;/div&gt;
        &lt;div style=&quot;display:flex; align-items:flex-start; justify-content:space-between; gap:12px; margin-top:8px;&quot;&gt;
          &lt;div style=&quot;font-family:&apos;Jost&apos;,-apple-system,BlinkMacSystemFont,system-ui,sans-serif; font-size:24px; font-weight:500; letter-spacing:0.01em; line-height:1.1; color:#2A2A3A;&quot;&gt;Good morning, cmeik&lt;/div&gt;
          &lt;div style=&quot;width:38px; height:38px; border-radius:50%; background:linear-gradient(135deg,#E83A73,#F2A83B); flex-shrink:0; box-shadow:0 2px 8px rgba(20,18,30,0.14);&quot;&gt;&lt;/div&gt;
        &lt;/div&gt;
        &lt;div style=&quot;display:flex; align-items:center; gap:11px; margin-top:8px; font-family:&apos;Inter&apos;,-apple-system,BlinkMacSystemFont,system-ui,sans-serif; font-size:12.5px; color:#6B6B7B;&quot;&gt;
          &lt;span&gt;🔥 12-day streak&lt;/span&gt;&lt;span style=&quot;width:3px; height:3px; border-radius:50%; background:#9A9AAA;&quot;&gt;&lt;/span&gt;&lt;span&gt;✍️ 3 logged this week&lt;/span&gt;
        &lt;/div&gt;
      &lt;/div&gt;

      &lt;!-- Tier-0 cinematic hero: UP NEXT (the gap, made visible) --&gt;
      &lt;div style=&quot;margin:12px 14px 4px; border-radius:18px; overflow:hidden; box-shadow:0 16px 40px -14px rgba(20,18,30,0.5);&quot;&gt;
        &lt;div style=&quot;position:relative; min-height:206px; background:radial-gradient(circle at 30% 24%, #F8C8A8 0%, #F2A83B 20%, #E83A73 52%, #2A5FAA 100%);&quot;&gt;
          &lt;div style=&quot;position:absolute; inset:0; opacity:0.12; mix-blend-mode:overlay; background-image:url(&amp;quot;data:image/svg+xml,%3Csvg viewBox=&apos;0 0 256 256&apos; xmlns=&apos;http://www.w3.org/2000/svg&apos;%3E%3Cfilter id=&apos;n&apos;%3E%3CfeTurbulence type=&apos;fractalNoise&apos; baseFrequency=&apos;0.85&apos; numOctaves=&apos;4&apos; stitchTiles=&apos;stitch&apos;/%3E%3C/filter%3E%3Crect width=&apos;100%25&apos; height=&apos;100%25&apos; filter=&apos;url(%23n)&apos;/%3E%3C/svg%3E&amp;quot;); background-size:150px 150px;&quot;&gt;&lt;/div&gt;
          &lt;div style=&quot;position:absolute; left:0; right:0; bottom:0; height:100%; background:linear-gradient(to top, rgba(20,18,30,0.90) 4%, rgba(20,18,30,0.5) 46%, transparent 100%);&quot;&gt;&lt;/div&gt;
          &lt;div style=&quot;position:relative; z-index:1; display:flex; flex-direction:column; gap:12px; padding:15px 17px 17px; min-height:206px;&quot;&gt;
            &lt;div style=&quot;display:flex; align-items:flex-start; justify-content:space-between; gap:10px;&quot;&gt;
              &lt;span style=&quot;display:inline-flex; align-items:center; padding:6px 12px; border-radius:999px; background:rgba(20,18,30,0.5); font-family:&apos;Jost&apos;,-apple-system,BlinkMacSystemFont,system-ui,sans-serif; font-size:10px; font-weight:700; letter-spacing:0.18em; color:#fff;&quot;&gt;UP NEXT&lt;/span&gt;
              &lt;div style=&quot;text-align:right;&quot;&gt;
                &lt;div style=&quot;font-family:&apos;Jost&apos;,-apple-system,BlinkMacSystemFont,system-ui,sans-serif; font-variant-numeric:tabular-nums; font-size:20px; font-weight:600; color:#fff; letter-spacing:0.04em;&quot;&gt;9d 04h&lt;/div&gt;
                &lt;div style=&quot;font-family:&apos;Jost&apos;,-apple-system,BlinkMacSystemFont,system-ui,sans-serif; font-size:9px; font-weight:600; letter-spacing:0.16em; color:rgba(255,255,255,0.7); text-transform:uppercase;&quot;&gt;until doors&lt;/div&gt;
              &lt;/div&gt;
            &lt;/div&gt;
            &lt;div style=&quot;margin-top:auto;&quot;&gt;
              &lt;div style=&quot;font-family:&apos;Jost&apos;,-apple-system,BlinkMacSystemFont,system-ui,sans-serif; font-size:28px; font-weight:600; letter-spacing:0.03em; line-height:1.08; color:#fff;&quot;&gt;Goose&lt;/div&gt;
              &lt;div style=&quot;font-family:&apos;Instrument Serif&apos;,&apos;Cormorant Garamond&apos;,Georgia,serif; font-style:italic; font-size:20px; color:rgba(255,255,255,0.95); margin-top:1px; line-height:1.2;&quot;&gt;The Capitol Theatre&lt;span style=&quot;color:rgba(255,255,255,0.58);&quot;&gt; · Port Chester, NY&lt;/span&gt;&lt;/div&gt;
              &lt;div style=&quot;font-family:&apos;Jost&apos;,-apple-system,BlinkMacSystemFont,system-ui,sans-serif; font-size:11px; font-weight:600; letter-spacing:0.09em; text-transform:uppercase; color:rgba(255,255,255,0.72); margin-top:7px;&quot;&gt;Fri Jul 10 · 8:00 PM&lt;/div&gt;
              &lt;div style=&quot;display:flex; align-items:center; gap:9px; margin-top:11px;&quot;&gt;
                &lt;div style=&quot;display:flex;&quot;&gt;
                  &lt;div style=&quot;width:26px; height:26px; border-radius:50%; background:linear-gradient(135deg,#2A5FAA,#3AC4E8); box-shadow:0 0 0 2px #14121e; margin-right:-8px;&quot;&gt;&lt;/div&gt;
                  &lt;div style=&quot;width:26px; height:26px; border-radius:50%; background:linear-gradient(135deg,#E83A73,#F2A83B); box-shadow:0 0 0 2px #14121e; margin-right:-8px;&quot;&gt;&lt;/div&gt;
                  &lt;div style=&quot;width:26px; height:26px; border-radius:50%; background:linear-gradient(135deg,#F2A83B,#FACC15); box-shadow:0 0 0 2px #14121e;&quot;&gt;&lt;/div&gt;
                &lt;/div&gt;
                &lt;span style=&quot;font-family:&apos;Inter&apos;,-apple-system,BlinkMacSystemFont,system-ui,sans-serif; font-size:13px; color:rgba(255,255,255,0.85);&quot;&gt;patrick, gmart, +10 you follow going&lt;/span&gt;
              &lt;/div&gt;
              &lt;button style=&quot;margin-top:14px; padding:12px 16px; width:100%; border-radius:999px; border:none; font-family:&apos;Jost&apos;,-apple-system,BlinkMacSystemFont,system-ui,sans-serif; font-weight:700; font-size:12px; letter-spacing:0.12em; text-transform:uppercase; background:#E83A73; color:#fff; box-shadow:0 10px 28px -6px rgba(232,58,115,0.6);&quot;&gt;View show →&lt;/button&gt;
            &lt;/div&gt;
          &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;

      &lt;!-- tier label --&gt;
      &lt;div style=&quot;display:flex; align-items:center; gap:12px; margin:18px 16px 10px;&quot;&gt;
        &lt;span style=&quot;font-family:&apos;Jost&apos;,-apple-system,BlinkMacSystemFont,system-ui,sans-serif; font-size:11px; font-weight:700; letter-spacing:0.20em; text-transform:uppercase; color:#9A9AAA;&quot;&gt;Act now&lt;/span&gt;
        &lt;span style=&quot;flex:1; height:1px; background:rgba(42,42,58,0.10);&quot;&gt;&lt;/span&gt;
        &lt;span style=&quot;font-family:&apos;Inter&apos;,-apple-system,BlinkMacSystemFont,system-ui,sans-serif; font-size:11px; color:#9A9AAA;&quot;&gt;nothing live tonight&lt;/span&gt;
      &lt;/div&gt;

      &lt;!-- Jam Bracket card --&gt;
      &lt;div style=&quot;margin:0 14px 12px; background:#FEFDFB; border-radius:16px; box-shadow:0 6px 18px rgba(20,18,30,0.06); padding:16px; position:relative; overflow:hidden;&quot;&gt;
        &lt;span style=&quot;position:absolute; left:0; top:0; bottom:0; width:3px; background:#2A5FAA;&quot;&gt;&lt;/span&gt;
        &lt;div style=&quot;display:flex; align-items:center; gap:8px; margin-bottom:9px;&quot;&gt;
          &lt;span style=&quot;font-size:15px;&quot;&gt;🏆&lt;/span&gt;
          &lt;span style=&quot;font-family:&apos;Jost&apos;,-apple-system,BlinkMacSystemFont,system-ui,sans-serif; font-size:11px; font-weight:700; letter-spacing:0.16em; text-transform:uppercase; color:#2A5FAA;&quot;&gt;Jam Bracket · Semifinals&lt;/span&gt;
        &lt;/div&gt;
        &lt;div style=&quot;font-family:&apos;Jost&apos;,-apple-system,BlinkMacSystemFont,system-ui,sans-serif; font-size:21px; font-weight:600; letter-spacing:0.02em; color:#2A2A3A;&quot;&gt;Madhuvan &lt;span style=&quot;color:#9A9AAA; font-size:15px;&quot;&gt;vs&lt;/span&gt; Into the Myst&lt;/div&gt;
        &lt;div style=&quot;font-family:&apos;Instrument Serif&apos;,&apos;Cormorant Garamond&apos;,Georgia,serif; font-style:italic; font-size:16px; color:#2A5FAA; margin-top:2px;&quot;&gt;voting closes in 18h · you haven&apos;t voted&lt;/div&gt;
        &lt;button style=&quot;margin-top:14px; padding:9px 16px; border-radius:999px; border:none; font-family:&apos;Jost&apos;,-apple-system,BlinkMacSystemFont,system-ui,sans-serif; font-weight:600; font-size:11px; letter-spacing:0.10em; text-transform:uppercase; background:#E83A73; color:#fff; box-shadow:0 6px 16px -4px rgba(232,58,115,0.3);&quot;&gt;Listen to both · vote&lt;/button&gt;
      &lt;/div&gt;

      &lt;!-- On This Day card --&gt;
      &lt;div style=&quot;margin:0 14px 14px; background:#FEFDFB; border-radius:16px; box-shadow:0 6px 18px rgba(20,18,30,0.06); padding:16px; position:relative; overflow:hidden;&quot;&gt;
        &lt;span style=&quot;position:absolute; left:0; top:0; bottom:0; width:3px; background:#F2A83B;&quot;&gt;&lt;/span&gt;
        &lt;div style=&quot;display:flex; align-items:center; gap:8px; margin-bottom:7px;&quot;&gt;
          &lt;span style=&quot;font-size:15px;&quot;&gt;📅&lt;/span&gt;
          &lt;span style=&quot;font-family:&apos;Jost&apos;,-apple-system,BlinkMacSystemFont,system-ui,sans-serif; font-size:11px; font-weight:700; letter-spacing:0.16em; text-transform:uppercase; color:#F2A83B;&quot;&gt;On this day&lt;/span&gt;
        &lt;/div&gt;
        &lt;div style=&quot;font-family:&apos;Jost&apos;,-apple-system,BlinkMacSystemFont,system-ui,sans-serif; font-size:18px; font-weight:600; letter-spacing:0.02em; color:#2A2A3A;&quot;&gt;2 years ago at the Cap&lt;/div&gt;
        &lt;div style=&quot;font-family:&apos;Inter&apos;,-apple-system,BlinkMacSystemFont,system-ui,sans-serif; font-size:13px; color:#6B6B7B; margin-top:5px; line-height:1.5;&quot;&gt;You caught Hungersite → Arrow. 34-show gap on the Arrow that night.&lt;/div&gt;
      &lt;/div&gt;

      &lt;!-- bottom nav --&gt;
      &lt;div style=&quot;display:flex; align-items:center; justify-content:space-around; padding:9px 8px 12px; border-top:1px solid rgba(42,42,58,0.08); background:rgba(255,255,255,0.4); font-family:&apos;Jost&apos;,-apple-system,BlinkMacSystemFont,system-ui,sans-serif;&quot;&gt;
        &lt;div style=&quot;display:flex; flex-direction:column; align-items:center; gap:2px;&quot;&gt;&lt;span style=&quot;font-size:17px;&quot;&gt;🪧&lt;/span&gt;&lt;span style=&quot;font-size:8px; font-weight:700; letter-spacing:0.06em; text-transform:uppercase; color:#E83A73;&quot;&gt;Lot&lt;/span&gt;&lt;/div&gt;
        &lt;div style=&quot;display:flex; flex-direction:column; align-items:center; gap:2px; opacity:0.45;&quot;&gt;&lt;span style=&quot;font-size:17px;&quot;&gt;🌀&lt;/span&gt;&lt;span style=&quot;font-size:8px; font-weight:600; letter-spacing:0.06em; text-transform:uppercase; color:#6B6B7B;&quot;&gt;Flow&lt;/span&gt;&lt;/div&gt;
        &lt;div style=&quot;display:flex; flex-direction:column; align-items:center; gap:2px; opacity:0.45;&quot;&gt;&lt;span style=&quot;font-size:17px;&quot;&gt;📺&lt;/span&gt;&lt;span style=&quot;font-size:8px; font-weight:600; letter-spacing:0.06em; text-transform:uppercase; color:#6B6B7B;&quot;&gt;Live&lt;/span&gt;&lt;/div&gt;
        &lt;div style=&quot;display:flex; flex-direction:column; align-items:center; gap:2px; opacity:0.45;&quot;&gt;&lt;span style=&quot;font-size:17px;&quot;&gt;🎟️&lt;/span&gt;&lt;span style=&quot;font-size:8px; font-weight:600; letter-spacing:0.06em; text-transform:uppercase; color:#6B6B7B;&quot;&gt;Shows&lt;/span&gt;&lt;/div&gt;
        &lt;div style=&quot;display:flex; flex-direction:column; align-items:center; gap:2px; opacity:0.45;&quot;&gt;&lt;span style=&quot;font-size:17px;&quot;&gt;🔔&lt;/span&gt;&lt;span style=&quot;font-size:8px; font-weight:600; letter-spacing:0.06em; text-transform:uppercase; color:#6B6B7B;&quot;&gt;You&lt;/span&gt;&lt;/div&gt;
      &lt;/div&gt;

    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;p&gt;The design principle underneath The Lot is the same one that governs the whole app: it never shows you an empty room. If there is nothing live, it does not say “nothing is live.” It finds the thing about your history, or your friends, or the band’s history, that is worth a tap. An app that opens onto a dead feed teaches you to stop opening it. The Lot is the counter to that reflex, and it is the surface that most of the rest of this post feeds into.&lt;/p&gt;

&lt;h2 id=&quot;the-show-page-became-the-whole-show&quot;&gt;The Show Page Became the Whole Show&lt;/h2&gt;

&lt;p&gt;Every show now opens into a redesigned, cinematic &lt;strong&gt;show detail page&lt;/strong&gt;, and it is doing a lot of jobs at once. Before the show it is a countdown and a planning surface. During the show it hands off to the live chat. After the show it is the recap and the setlist for good.&lt;/p&gt;

&lt;p&gt;The hero is a sunset marquee with a live flip-clock counting down to doors. Under it, three avatar rows show your people sorted into &lt;strong&gt;GOING&lt;/strong&gt;, &lt;strong&gt;COUCH&lt;/strong&gt; (couch touring), and &lt;strong&gt;WANT TIX&lt;/strong&gt;, so you can see the shape of the night at a glance and who to find. You can &lt;strong&gt;call the opener right from the show page&lt;/strong&gt;, days before doors, the same one-tap prediction game that lives on the Lot and in the live room (they all share one store, so your pick follows you everywhere). You can share &lt;strong&gt;where you are sitting&lt;/strong&gt; and see where everyone else is, section and row, so your friends can actually find each other in a shed. There is trip planning for lodging, a “the stage is dark” empty state that flips to a live setlist when the first song lands, and, once it is over, the full setlist with gaps, sit-ins, and a recap blurb built from the room’s own reactions.&lt;/p&gt;

&lt;div style=&quot;color-scheme:light; background:#E7E2D6; padding:16px; border-radius:20px; margin:16px auto; max-width:420px;&quot;&gt;
  &lt;div style=&quot;background:#F0EDE4; border-radius:20px; box-shadow:0 12px 34px rgba(20,18,30,0.14); overflow:hidden; max-width:390px; margin:0 auto; color:#2A2A3A; font-family:&apos;Inter&apos;,-apple-system,BlinkMacSystemFont,system-ui,sans-serif;&quot;&gt;
    &lt;div style=&quot;position:relative; min-height:150px; background:radial-gradient(circle at 50% 18%, #F8C8A8 0%, #F2A83B 22%, #E83A73 60%, #2A2A3A 108%);&quot;&gt;
      &lt;div style=&quot;position:absolute; inset:0; opacity:0.12; mix-blend-mode:overlay; background-image:url(&amp;quot;data:image/svg+xml,%3Csvg viewBox=&apos;0 0 256 256&apos; xmlns=&apos;http://www.w3.org/2000/svg&apos;%3E%3Cfilter id=&apos;n&apos;%3E%3CfeTurbulence type=&apos;fractalNoise&apos; baseFrequency=&apos;0.85&apos; numOctaves=&apos;4&apos; stitchTiles=&apos;stitch&apos;/%3E%3C/filter%3E%3Crect width=&apos;100%25&apos; height=&apos;100%25&apos; filter=&apos;url(%23n)&apos;/%3E%3C/svg%3E&amp;quot;); background-size:130px 130px;&quot;&gt;&lt;/div&gt;
      &lt;div style=&quot;position:absolute; left:0; right:0; bottom:0; height:100%; background:linear-gradient(to top, rgba(20,18,30,0.9) 6%, rgba(20,18,30,0.4) 55%, transparent 100%);&quot;&gt;&lt;/div&gt;
      &lt;div style=&quot;position:relative; z-index:1; padding:14px 16px 16px; display:flex; flex-direction:column; height:100%; justify-content:flex-end; min-height:150px;&quot;&gt;
        &lt;div style=&quot;font-family:&apos;Jost&apos;,-apple-system,BlinkMacSystemFont,system-ui,sans-serif; font-size:24px; font-weight:600; letter-spacing:0.03em; color:#fff;&quot;&gt;Goose&lt;/div&gt;
        &lt;div style=&quot;font-family:&apos;Instrument Serif&apos;,&apos;Cormorant Garamond&apos;,Georgia,serif; font-style:italic; font-size:17px; color:rgba(255,255,255,0.94);&quot;&gt;The Capitol Theatre · Port Chester&lt;/div&gt;
        &lt;div style=&quot;display:flex; gap:7px; margin-top:10px;&quot;&gt;
          &lt;div style=&quot;background:rgba(20,18,30,0.5); border-radius:10px; padding:5px 9px; text-align:center;&quot;&gt;&lt;div style=&quot;font-family:&apos;Jost&apos;,-apple-system,BlinkMacSystemFont,system-ui,sans-serif; font-size:18px; font-weight:700; color:#fff; font-variant-numeric:tabular-nums;&quot;&gt;02&lt;/div&gt;&lt;div style=&quot;font-size:7px; letter-spacing:0.12em; color:rgba(255,255,255,0.7);&quot;&gt;DAYS&lt;/div&gt;&lt;/div&gt;
          &lt;div style=&quot;background:rgba(20,18,30,0.5); border-radius:10px; padding:5px 9px; text-align:center;&quot;&gt;&lt;div style=&quot;font-family:&apos;Jost&apos;,-apple-system,BlinkMacSystemFont,system-ui,sans-serif; font-size:18px; font-weight:700; color:#fff; font-variant-numeric:tabular-nums;&quot;&gt;14&lt;/div&gt;&lt;div style=&quot;font-size:7px; letter-spacing:0.12em; color:rgba(255,255,255,0.7);&quot;&gt;HRS&lt;/div&gt;&lt;/div&gt;
          &lt;div style=&quot;background:rgba(20,18,30,0.5); border-radius:10px; padding:5px 9px; text-align:center;&quot;&gt;&lt;div style=&quot;font-family:&apos;Jost&apos;,-apple-system,BlinkMacSystemFont,system-ui,sans-serif; font-size:18px; font-weight:700; color:#fff; font-variant-numeric:tabular-nums;&quot;&gt;31&lt;/div&gt;&lt;div style=&quot;font-size:7px; letter-spacing:0.12em; color:rgba(255,255,255,0.7);&quot;&gt;MIN&lt;/div&gt;&lt;/div&gt;
          &lt;div style=&quot;background:rgba(20,18,30,0.5); border-radius:10px; padding:5px 9px; text-align:center;&quot;&gt;&lt;div style=&quot;font-family:&apos;Jost&apos;,-apple-system,BlinkMacSystemFont,system-ui,sans-serif; font-size:18px; font-weight:700; color:#F2A83B; font-variant-numeric:tabular-nums;&quot;&gt;08&lt;/div&gt;&lt;div style=&quot;font-size:7px; letter-spacing:0.12em; color:rgba(255,255,255,0.7);&quot;&gt;SEC&lt;/div&gt;&lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;

    &lt;div style=&quot;padding:12px 14px;&quot;&gt;
      &lt;!-- who&apos;s going --&gt;
      &lt;div style=&quot;display:flex; gap:8px; margin-bottom:12px;&quot;&gt;
        &lt;div style=&quot;flex:1; background:#FEFDFB; border-radius:12px; padding:8px 10px; text-align:center;&quot;&gt;&lt;div style=&quot;font-family:&apos;Jost&apos;,-apple-system,BlinkMacSystemFont,system-ui,sans-serif; font-size:9px; font-weight:700; letter-spacing:0.08em; color:#2A5FAA;&quot;&gt;GOING&lt;/div&gt;&lt;div style=&quot;font-family:&apos;Jost&apos;,-apple-system,BlinkMacSystemFont,system-ui,sans-serif; font-size:18px; font-weight:700;&quot;&gt;14&lt;/div&gt;&lt;/div&gt;
        &lt;div style=&quot;flex:1; background:#FEFDFB; border-radius:12px; padding:8px 10px; text-align:center;&quot;&gt;&lt;div style=&quot;font-family:&apos;Jost&apos;,-apple-system,BlinkMacSystemFont,system-ui,sans-serif; font-size:9px; font-weight:700; letter-spacing:0.08em; color:#3AC4E8;&quot;&gt;COUCH&lt;/div&gt;&lt;div style=&quot;font-family:&apos;Jost&apos;,-apple-system,BlinkMacSystemFont,system-ui,sans-serif; font-size:18px; font-weight:700;&quot;&gt;31&lt;/div&gt;&lt;/div&gt;
        &lt;div style=&quot;flex:1; background:#FEFDFB; border-radius:12px; padding:8px 10px; text-align:center;&quot;&gt;&lt;div style=&quot;font-family:&apos;Jost&apos;,-apple-system,BlinkMacSystemFont,system-ui,sans-serif; font-size:9px; font-weight:700; letter-spacing:0.08em; color:#F2A83B;&quot;&gt;WANT TIX&lt;/div&gt;&lt;div style=&quot;font-family:&apos;Jost&apos;,-apple-system,BlinkMacSystemFont,system-ui,sans-serif; font-size:18px; font-weight:700;&quot;&gt;6&lt;/div&gt;&lt;/div&gt;
      &lt;/div&gt;

      &lt;!-- call the opener --&gt;
      &lt;div style=&quot;background:#FEFDFB; border-radius:14px; padding:12px; margin-bottom:10px;&quot;&gt;
        &lt;div style=&quot;font-family:&apos;Jost&apos;,-apple-system,BlinkMacSystemFont,system-ui,sans-serif; font-size:10px; font-weight:700; letter-spacing:0.14em; text-transform:uppercase; color:#2A5FAA; margin-bottom:8px;&quot;&gt;🔮 Call the opener&lt;/div&gt;
        &lt;div style=&quot;display:flex; gap:6px; flex-wrap:wrap;&quot;&gt;
          &lt;span style=&quot;font-family:&apos;Jost&apos;,-apple-system,BlinkMacSystemFont,system-ui,sans-serif; font-size:12px; font-weight:600; padding:6px 12px; border-radius:999px; background:rgba(42,95,170,0.10); color:#2A5FAA;&quot;&gt;Hungersite&lt;/span&gt;
          &lt;span style=&quot;font-family:&apos;Jost&apos;,-apple-system,BlinkMacSystemFont,system-ui,sans-serif; font-size:12px; font-weight:600; padding:6px 12px; border-radius:999px; border:1px solid rgba(42,42,58,0.14); color:#6B6B7B;&quot;&gt;Arrow&lt;/span&gt;
          &lt;span style=&quot;font-family:&apos;Jost&apos;,-apple-system,BlinkMacSystemFont,system-ui,sans-serif; font-size:12px; font-weight:600; padding:6px 12px; border-radius:999px; border:1px solid rgba(42,42,58,0.14); color:#6B6B7B;&quot;&gt;Madhuvan&lt;/span&gt;
        &lt;/div&gt;
      &lt;/div&gt;

      &lt;!-- who&apos;s sitting where --&gt;
      &lt;div style=&quot;background:#FEFDFB; border-radius:14px; padding:12px;&quot;&gt;
        &lt;div style=&quot;font-family:&apos;Jost&apos;,-apple-system,BlinkMacSystemFont,system-ui,sans-serif; font-size:10px; font-weight:700; letter-spacing:0.14em; text-transform:uppercase; color:#E83A73; margin-bottom:8px;&quot;&gt;🪑 Who&apos;s sitting where&lt;/div&gt;
        &lt;div style=&quot;display:flex; align-items:center; gap:8px; font-family:&apos;Inter&apos;,-apple-system,BlinkMacSystemFont,system-ui,sans-serif; font-size:12.5px; color:#2A2A3A;&quot;&gt;&lt;span style=&quot;width:22px;height:22px;border-radius:50%;background:linear-gradient(135deg,#F2A83B,#E83A73);&quot;&gt;&lt;/span&gt; patrick · &lt;b&gt;Orch L, Row G&lt;/b&gt;&lt;/div&gt;
        &lt;div style=&quot;display:flex; align-items:center; gap:8px; font-family:&apos;Inter&apos;,-apple-system,BlinkMacSystemFont,system-ui,sans-serif; font-size:12.5px; color:#2A2A3A; margin-top:6px;&quot;&gt;&lt;span style=&quot;width:22px;height:22px;border-radius:50%;background:linear-gradient(135deg,#F2A83B,#FACC15);&quot;&gt;&lt;/span&gt; gmart · &lt;b&gt;Balcony, Row B&lt;/b&gt;&lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;p&gt;That “call the opener from anywhere” detail is a small example of a rule we hold hard: a feature should be one thing with one source of truth, reachable from wherever you happen to be. Your opener call is the same object whether you make it on the Lot four days out, on the show page the morning of, or in the live room as the lights drop. People noticed. One night in the chat: &lt;em&gt;“we should build a closer call feature for pre show lol.”&lt;/em&gt; That is the app working, when the users start designing it with you.&lt;/p&gt;

&lt;h2 id=&quot;the-bracket-is-the-bridge-between-tours&quot;&gt;The Bracket Is the Bridge Between Tours&lt;/h2&gt;

&lt;p&gt;This is the feature I care about most for the actual between-shows problem, because it is the one that reaches into the dead weeks and keeps people listening.&lt;/p&gt;

&lt;p&gt;When a tour ends, the app seeds a &lt;strong&gt;March Madness style bracket&lt;/strong&gt; of that tour’s best jams. The seeding is not editorial: we run sentiment over every show’s live chat, weight it by song, cross-reference the historical setlist data for bustouts and rarities, and the top sixteen jams become the bracket. Then the rounds release on a schedule, Round of 16, Quarters, Semis, Final, and the community votes each one. Ties break by seed. Followers get a push when a new round opens.&lt;/p&gt;

&lt;p&gt;The part that makes it work is the audio. Every matchup card has an &lt;strong&gt;inline player for both jams&lt;/strong&gt;, streaming from &lt;strong&gt;Relisten&lt;/strong&gt; where the recording exists, with a nugs.net “listen on” link as the fallback when it does not. You cannot vote between two twenty-minute jams without hearing them, so voting in the bracket means re-listening to the tour you just watched, one jam at a time, for weeks after the lights came up. By the time we crown a Jam of the Tour, the next run is already on the calendar and nobody ever fully left.&lt;/p&gt;

&lt;div style=&quot;color-scheme:light; background:#E7E2D6; padding:16px; border-radius:20px; margin:16px auto; max-width:520px;&quot;&gt;
  &lt;div style=&quot;background:#F0EDE4; border-radius:18px; box-shadow:0 10px 28px rgba(20,18,30,0.12); overflow:hidden; max-width:480px; margin:0 auto; color:#2A2A3A; font-family:&apos;Inter&apos;,-apple-system,BlinkMacSystemFont,system-ui,sans-serif;&quot;&gt;
    &lt;div style=&quot;padding:11px 15px; background:#141220; color:#fff; display:flex; align-items:center; justify-content:space-between;&quot;&gt;&lt;span style=&quot;font-family:&apos;Jost&apos;,-apple-system,BlinkMacSystemFont,system-ui,sans-serif; font-size:11px; font-weight:700; letter-spacing:0.14em;&quot;&gt;🏆 JAM BRACKET · SEMIFINAL&lt;/span&gt;&lt;span style=&quot;font-size:11px; opacity:0.6;&quot;&gt;Big Modern! Tour&lt;/span&gt;&lt;/div&gt;
  &lt;div style=&quot;padding:14px 15px;&quot;&gt;
    &lt;div style=&quot;background:#FEFDFB; border-radius:14px; padding:12px 13px; box-shadow:0 2px 8px rgba(20,18,30,0.05);&quot;&gt;
      &lt;div style=&quot;display:flex; align-items:baseline; justify-content:space-between;&quot;&gt;&lt;div&gt;&lt;span style=&quot;font-family:&apos;Jost&apos;,-apple-system,BlinkMacSystemFont,system-ui,sans-serif; font-size:9px; font-weight:800; color:#E83A73;&quot;&gt;SEED 1&lt;/span&gt; &lt;span style=&quot;font-family:&apos;Jost&apos;,-apple-system,BlinkMacSystemFont,system-ui,sans-serif; font-size:16px; font-weight:700;&quot;&gt;Madhuvan&lt;/span&gt;&lt;/div&gt;&lt;span style=&quot;font-family:&apos;Instrument Serif&apos;,&apos;Cormorant Garamond&apos;,Georgia,serif; font-style:italic; font-size:13px; color:#9A9AAA;&quot;&gt;6/20 · MSG&lt;/span&gt;&lt;/div&gt;
      &lt;div style=&quot;display:flex; align-items:center; gap:10px; margin-top:9px;&quot;&gt;&lt;span style=&quot;width:30px; height:30px; border-radius:50%; background:#141220; color:#fff; display:flex; align-items:center; justify-content:center; font-size:12px;&quot;&gt;▶&lt;/span&gt;&lt;div style=&quot;flex:1;&quot;&gt;&lt;div style=&quot;display:flex; align-items:center; gap:6px; font-size:10px; color:#9A9AAA;&quot;&gt;&lt;span style=&quot;font-family:&apos;Jost&apos;,-apple-system,BlinkMacSystemFont,system-ui,sans-serif; font-weight:700; color:#2A5FAA;&quot;&gt;RELISTEN&lt;/span&gt; · 22:14&lt;/div&gt;&lt;div style=&quot;height:4px; background:rgba(42,42,58,0.08); border-radius:2px; margin-top:4px; overflow:hidden;&quot;&gt;&lt;div style=&quot;height:100%; width:34%; background:#E83A73;&quot;&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;
    &lt;/div&gt;
    &lt;div style=&quot;text-align:center; font-family:&apos;Jost&apos;,-apple-system,BlinkMacSystemFont,system-ui,sans-serif; font-size:11px; font-weight:800; color:#9A9AAA; padding:8px 0;&quot;&gt;VS&lt;/div&gt;
    &lt;div style=&quot;background:#FEFDFB; border-radius:14px; padding:12px 13px; box-shadow:0 2px 8px rgba(20,18,30,0.05);&quot;&gt;
      &lt;div style=&quot;display:flex; align-items:baseline; justify-content:space-between;&quot;&gt;&lt;div&gt;&lt;span style=&quot;font-family:&apos;Jost&apos;,-apple-system,BlinkMacSystemFont,system-ui,sans-serif; font-size:9px; font-weight:800; color:#9A9AAA;&quot;&gt;SEED 4&lt;/span&gt; &lt;span style=&quot;font-family:&apos;Jost&apos;,-apple-system,BlinkMacSystemFont,system-ui,sans-serif; font-size:16px; font-weight:700;&quot;&gt;Into the Myst&lt;/span&gt;&lt;/div&gt;&lt;span style=&quot;font-family:&apos;Instrument Serif&apos;,&apos;Cormorant Garamond&apos;,Georgia,serif; font-style:italic; font-size:13px; color:#9A9AAA;&quot;&gt;6/26 · Raleigh&lt;/span&gt;&lt;/div&gt;
      &lt;div style=&quot;display:flex; align-items:center; gap:10px; margin-top:9px;&quot;&gt;&lt;span style=&quot;width:30px; height:30px; border-radius:50%; background:#fff; border:2px solid #141220; color:#141220; display:flex; align-items:center; justify-content:center; font-size:12px;&quot;&gt;▶&lt;/span&gt;&lt;div style=&quot;flex:1;&quot;&gt;&lt;div style=&quot;display:flex; align-items:center; gap:6px; font-size:10px; color:#9A9AAA;&quot;&gt;&lt;span style=&quot;font-family:&apos;Jost&apos;,-apple-system,BlinkMacSystemFont,system-ui,sans-serif; font-weight:700; color:#6B6B7B;&quot;&gt;nugs.net&lt;/span&gt; · 18:03&lt;/div&gt;&lt;div style=&quot;height:4px; background:rgba(42,42,58,0.08); border-radius:2px; margin-top:4px; overflow:hidden;&quot;&gt;&lt;div style=&quot;height:100%; width:0%; background:#E83A73;&quot;&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;
    &lt;/div&gt;
    &lt;div style=&quot;display:flex; align-items:center; gap:8px; margin-top:12px;&quot;&gt;&lt;button style=&quot;flex:1; padding:10px; border-radius:999px; border:none; background:#E83A73; color:#fff; font-family:&apos;Jost&apos;,-apple-system,BlinkMacSystemFont,system-ui,sans-serif; font-weight:700; font-size:11px; letter-spacing:0.10em; text-transform:uppercase;&quot;&gt;Vote Madhuvan&lt;/button&gt;&lt;button style=&quot;flex:1; padding:10px; border-radius:999px; border:1px solid rgba(42,42,58,0.16); background:#FEFDFB; color:#2A2A3A; font-family:&apos;Jost&apos;,-apple-system,BlinkMacSystemFont,system-ui,sans-serif; font-weight:700; font-size:11px; letter-spacing:0.10em; text-transform:uppercase;&quot;&gt;Vote Into the Myst&lt;/button&gt;&lt;/div&gt;
  &lt;/div&gt;
    &lt;div style=&quot;height:1px; background:rgba(42,42,58,0.08); margin:0 15px;&quot;&gt;&lt;/div&gt;
  &lt;div style=&quot;padding:4px 15px 16px;&quot;&gt;
    &lt;div style=&quot;font-family:&apos;Jost&apos;,-apple-system,BlinkMacSystemFont,system-ui,sans-serif; font-size:8px; font-weight:800; letter-spacing:0.10em; text-transform:uppercase; color:#9A9AAA; margin-bottom:8px;&quot;&gt;Full bracket · 16 jams&lt;/div&gt;
    &lt;div style=&quot;display:flex; gap:8px; overflow-x:auto; padding-bottom:4px;&quot;&gt;&lt;div style=&quot;display:flex; flex-direction:column; gap:5px; justify-content:space-between; min-width:112px;&quot;&gt;&lt;div style=&quot;font-family:&apos;Jost&apos;,-apple-system,BlinkMacSystemFont,system-ui,sans-serif; font-size:8px; font-weight:800; letter-spacing:0.10em; text-transform:uppercase; color:#9A9AAA;&quot;&gt;Round of 16&lt;/div&gt;&lt;div style=&quot;background:#FEFDFB; border-radius:6px; padding:5px 7px; font-family:&apos;Jost&apos;,-apple-system,BlinkMacSystemFont,system-ui,sans-serif; font-size:9.5px; font-weight:700; color:#2A2A3A;  white-space:nowrap; overflow:hidden; text-overflow:ellipsis;&quot;&gt;&lt;span style=&quot;color:#C0BCC8; font-weight:800;&quot;&gt;1&lt;/span&gt; Madhuvan &lt;span style=&quot;color:#059669;&quot;&gt;✓&lt;/span&gt;&lt;/div&gt;&lt;div style=&quot;background:#FEFDFB; border-radius:6px; padding:5px 7px; font-family:&apos;Jost&apos;,-apple-system,BlinkMacSystemFont,system-ui,sans-serif; font-size:9.5px; font-weight:700; color:#2A2A3A;  white-space:nowrap; overflow:hidden; text-overflow:ellipsis;&quot;&gt;&lt;span style=&quot;color:#C0BCC8; font-weight:800;&quot;&gt;8&lt;/span&gt; Tumble &lt;span style=&quot;color:#059669;&quot;&gt;✓&lt;/span&gt;&lt;/div&gt;&lt;div style=&quot;background:#FEFDFB; border-radius:6px; padding:5px 7px; font-family:&apos;Jost&apos;,-apple-system,BlinkMacSystemFont,system-ui,sans-serif; font-size:9.5px; font-weight:700; color:#2A2A3A;  white-space:nowrap; overflow:hidden; text-overflow:ellipsis;&quot;&gt;&lt;span style=&quot;color:#C0BCC8; font-weight:800;&quot;&gt;5&lt;/span&gt; Atlas Dogs &lt;span style=&quot;color:#059669;&quot;&gt;✓&lt;/span&gt;&lt;/div&gt;&lt;div style=&quot;background:#FEFDFB; border-radius:6px; padding:5px 7px; font-family:&apos;Jost&apos;,-apple-system,BlinkMacSystemFont,system-ui,sans-serif; font-size:9.5px; font-weight:700; color:#2A2A3A;  white-space:nowrap; overflow:hidden; text-overflow:ellipsis;&quot;&gt;&lt;span style=&quot;color:#C0BCC8; font-weight:800;&quot;&gt;4&lt;/span&gt; Into the Myst &lt;span style=&quot;color:#059669;&quot;&gt;✓&lt;/span&gt;&lt;/div&gt;&lt;div style=&quot;background:#FEFDFB; border-radius:6px; padding:5px 7px; font-family:&apos;Jost&apos;,-apple-system,BlinkMacSystemFont,system-ui,sans-serif; font-size:9.5px; font-weight:700; color:#2A2A3A;  white-space:nowrap; overflow:hidden; text-overflow:ellipsis;&quot;&gt;&lt;span style=&quot;color:#C0BCC8; font-weight:800;&quot;&gt;6&lt;/span&gt; Hungersite &lt;span style=&quot;color:#059669;&quot;&gt;✓&lt;/span&gt;&lt;/div&gt;&lt;div style=&quot;background:#FEFDFB; border-radius:6px; padding:5px 7px; font-family:&apos;Jost&apos;,-apple-system,BlinkMacSystemFont,system-ui,sans-serif; font-size:9.5px; font-weight:700; color:#2A2A3A;  white-space:nowrap; overflow:hidden; text-overflow:ellipsis;&quot;&gt;&lt;span style=&quot;color:#C0BCC8; font-weight:800;&quot;&gt;3&lt;/span&gt; Empress &lt;span style=&quot;color:#059669;&quot;&gt;✓&lt;/span&gt;&lt;/div&gt;&lt;div style=&quot;background:#FEFDFB; border-radius:6px; padding:5px 7px; font-family:&apos;Jost&apos;,-apple-system,BlinkMacSystemFont,system-ui,sans-serif; font-size:9.5px; font-weight:700; color:#2A2A3A;  white-space:nowrap; overflow:hidden; text-overflow:ellipsis;&quot;&gt;&lt;span style=&quot;color:#C0BCC8; font-weight:800;&quot;&gt;7&lt;/span&gt; Arrow &lt;span style=&quot;color:#059669;&quot;&gt;✓&lt;/span&gt;&lt;/div&gt;&lt;div style=&quot;background:#FEFDFB; border-radius:6px; padding:5px 7px; font-family:&apos;Jost&apos;,-apple-system,BlinkMacSystemFont,system-ui,sans-serif; font-size:9.5px; font-weight:700; color:#2A2A3A;  white-space:nowrap; overflow:hidden; text-overflow:ellipsis;&quot;&gt;&lt;span style=&quot;color:#C0BCC8; font-weight:800;&quot;&gt;2&lt;/span&gt; All I Need &lt;span style=&quot;color:#059669;&quot;&gt;✓&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div style=&quot;display:flex; flex-direction:column; gap:5px; justify-content:space-around; min-width:112px;&quot;&gt;&lt;div style=&quot;font-family:&apos;Jost&apos;,-apple-system,BlinkMacSystemFont,system-ui,sans-serif; font-size:8px; font-weight:800; letter-spacing:0.10em; text-transform:uppercase; color:#9A9AAA;&quot;&gt;Quarters&lt;/div&gt;&lt;div style=&quot;background:#FEFDFB; border-radius:6px; padding:5px 7px; font-family:&apos;Jost&apos;,-apple-system,BlinkMacSystemFont,system-ui,sans-serif; font-size:9.5px; font-weight:700; color:#2A2A3A;  white-space:nowrap; overflow:hidden; text-overflow:ellipsis;&quot;&gt;&lt;span style=&quot;color:#C0BCC8; font-weight:800;&quot;&gt;1&lt;/span&gt; Madhuvan &lt;span style=&quot;color:#059669;&quot;&gt;✓&lt;/span&gt;&lt;/div&gt;&lt;div style=&quot;background:#FEFDFB; border-radius:6px; padding:5px 7px; font-family:&apos;Jost&apos;,-apple-system,BlinkMacSystemFont,system-ui,sans-serif; font-size:9.5px; font-weight:700; color:#2A2A3A;  white-space:nowrap; overflow:hidden; text-overflow:ellipsis;&quot;&gt;&lt;span style=&quot;color:#C0BCC8; font-weight:800;&quot;&gt;5&lt;/span&gt; Atlas Dogs &lt;span style=&quot;color:#059669;&quot;&gt;✓&lt;/span&gt;&lt;/div&gt;&lt;div style=&quot;background:#FEFDFB; border-radius:6px; padding:5px 7px; font-family:&apos;Jost&apos;,-apple-system,BlinkMacSystemFont,system-ui,sans-serif; font-size:9.5px; font-weight:700; color:#2A2A3A;  white-space:nowrap; overflow:hidden; text-overflow:ellipsis;&quot;&gt;&lt;span style=&quot;color:#C0BCC8; font-weight:800;&quot;&gt;6&lt;/span&gt; Hungersite &lt;span style=&quot;color:#059669;&quot;&gt;✓&lt;/span&gt;&lt;/div&gt;&lt;div style=&quot;background:#FEFDFB; border-radius:6px; padding:5px 7px; font-family:&apos;Jost&apos;,-apple-system,BlinkMacSystemFont,system-ui,sans-serif; font-size:9.5px; font-weight:700; color:#2A2A3A;  white-space:nowrap; overflow:hidden; text-overflow:ellipsis;&quot;&gt;&lt;span style=&quot;color:#C0BCC8; font-weight:800;&quot;&gt;4&lt;/span&gt; Into the Myst &lt;span style=&quot;color:#059669;&quot;&gt;✓&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div style=&quot;display:flex; flex-direction:column; gap:5px; justify-content:space-around; min-width:112px;&quot;&gt;&lt;div style=&quot;font-family:&apos;Jost&apos;,-apple-system,BlinkMacSystemFont,system-ui,sans-serif; font-size:8px; font-weight:800; letter-spacing:0.10em; text-transform:uppercase; color:#9A9AAA;&quot;&gt;Semis&lt;/div&gt;&lt;div style=&quot;background:#FEFDFB; border-radius:6px; padding:5px 7px; font-family:&apos;Jost&apos;,-apple-system,BlinkMacSystemFont,system-ui,sans-serif; font-size:9.5px; font-weight:700; color:#E83A73; border:1px solid #E83A73; white-space:nowrap; overflow:hidden; text-overflow:ellipsis;&quot;&gt;&lt;span style=&quot;color:#C0BCC8; font-weight:800;&quot;&gt;1&lt;/span&gt; Madhuvan &lt;span style=&quot;color:#E83A73; font-weight:700;&quot;&gt;· voting&lt;/span&gt;&lt;/div&gt;&lt;div style=&quot;background:#FEFDFB; border-radius:6px; padding:5px 7px; font-family:&apos;Jost&apos;,-apple-system,BlinkMacSystemFont,system-ui,sans-serif; font-size:9.5px; font-weight:700; color:#E83A73; border:1px solid #E83A73; white-space:nowrap; overflow:hidden; text-overflow:ellipsis;&quot;&gt;&lt;span style=&quot;color:#C0BCC8; font-weight:800;&quot;&gt;4&lt;/span&gt; Into the Myst &lt;span style=&quot;color:#E83A73; font-weight:700;&quot;&gt;· voting&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;div style=&quot;display:flex; flex-direction:column; gap:5px; justify-content:center; min-width:96px;&quot;&gt;&lt;div style=&quot;font-family:&apos;Jost&apos;,-apple-system,BlinkMacSystemFont,system-ui,sans-serif; font-size:8px; font-weight:800; letter-spacing:0.10em; text-transform:uppercase; color:#9A9AAA;&quot;&gt;Final&lt;/div&gt;&lt;div style=&quot;background:rgba(250,204,21,0.16); border-radius:8px; padding:10px 8px; text-align:center;&quot;&gt;&lt;div style=&quot;font-size:18px;&quot;&gt;🏆&lt;/div&gt;&lt;div style=&quot;font-family:&apos;Jost&apos;,-apple-system,BlinkMacSystemFont,system-ui,sans-serif; font-size:8.5px; color:#9A9AAA;&quot;&gt;Jam of the Tour&lt;/div&gt;&lt;div style=&quot;font-family:&apos;Jost&apos;,-apple-system,BlinkMacSystemFont,system-ui,sans-serif; font-size:10px; font-weight:700;&quot;&gt;?&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;
  &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;p&gt;The gap between tours is where most music apps quietly die. The bracket is the deliberate answer: it turns the dead weeks into a reason to open the app every few days, re-listen to a jam, and argue about it in the chat, right up until the next tour makes the argument moot.&lt;/p&gt;

&lt;h2 id=&quot;a-reference-library-you-browse-for-fun&quot;&gt;A Reference Library You Browse for Fun&lt;/h2&gt;

&lt;p&gt;The other way to earn a Tuesday open is to be worth reading when nothing is happening. So a big chunk of the 787 went into turning Zabriskie into a browsable reference for the bands themselves, and this is the part where the usage numbers actually show up.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Songbook.&lt;/strong&gt; We rewrote the old song-search box into a full &lt;strong&gt;catalog directory&lt;/strong&gt;: every band’s entire songbook, browsable, with a band-switcher rail so you can jump from Goose’s book to Phish’s to Billy’s. It is styled as a “Gold Ledger,” with letter headers and per-song play counts, and it marks the songs you personally have caught live so your own book fills in as you tour. There are &lt;strong&gt;17,326 songs&lt;/strong&gt; in it across every band we track. It is the thing you open in a hotel room at 1am to settle a bet about how many times they have played a song.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Poster Archive.&lt;/strong&gt; Every show can carry its posters now, including multiple variants per show (the foil, the rainbow, the artist edition) with proper variant labels and a carousel. Admins get a variant picker and an “add another” flow, and there is an in-app button to &lt;strong&gt;sync posters straight from the Goose store&lt;/strong&gt;, backed by a cron job that refreshes the archive daily. This is the poster-archive goal I have had for a while: not “the one official poster,” but the whole wall of variants a fan might recognize from a show they were at.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Sit-ins and Guests.&lt;/strong&gt; This one is Patrick’s, top to bottom. When a guest sits in with the band, that is now first-class data: a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/guests&lt;/code&gt; directory, guest credits on the song page and the setlist with per-instrument icons, band-emoji guest reactions, and a backfill of the guest history across the whole catalog. Phish alone came in at 151 guests over 1,006 appearances, plus JRAD, Greensky, Goose, and more. If you ever wanted to know every time a specific person walked on stage with a band, that list exists now because Patrick built it, and it is the kind of thing only a real fan thinks to build.&lt;/p&gt;

&lt;div style=&quot;display:flex; gap:14px; flex-wrap:wrap; justify-content:center; margin:18px auto;&quot;&gt;
  &lt;div style=&quot;background:#F5F2EB; border-radius:18px; box-shadow:0 6px 20px rgba(0,0,0,0.08); padding:0; flex:0 0 auto; max-width:300px; overflow:hidden; font-family:-apple-system,BlinkMacSystemFont,system-ui,sans-serif; color:#262626;&quot;&gt;
    &lt;div style=&quot;background:linear-gradient(135deg,#b8860b,#8B5CF6); color:#fff; padding:10px 14px; font-size:11px; font-weight:800; letter-spacing:0.04em; text-transform:uppercase;&quot;&gt;📖 Songbook · Goose&lt;/div&gt;
    &lt;div style=&quot;padding:10px 14px;&quot;&gt;
      &lt;div style=&quot;font-size:10px; font-weight:800; color:#b8860b; margin:4px 0;&quot;&gt;· I ·&lt;/div&gt;
      &lt;div style=&quot;display:flex; justify-content:space-between; padding:5px 0; font-size:13px;&quot;&gt;&lt;span style=&quot;font-weight:600;&quot;&gt;Indian River &lt;span style=&quot;color:#10b981;&quot;&gt;✓&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color:#6B7280; font-size:11px;&quot;&gt;×117&lt;/span&gt;&lt;/div&gt;
      &lt;div style=&quot;display:flex; justify-content:space-between; padding:5px 0; font-size:13px;&quot;&gt;&lt;span style=&quot;font-weight:600;&quot;&gt;Into the Myst &lt;span style=&quot;color:#10b981;&quot;&gt;✓&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color:#6B7280; font-size:11px;&quot;&gt;×62&lt;/span&gt;&lt;/div&gt;
      &lt;div style=&quot;display:flex; justify-content:space-between; padding:5px 0; font-size:13px;&quot;&gt;&lt;span&gt;Il Duderino&lt;/span&gt;&lt;span style=&quot;color:#6B7280; font-size:11px;&quot;&gt;×9&lt;/span&gt;&lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;

  &lt;div style=&quot;background:#F5F2EB; border-radius:18px; box-shadow:0 6px 20px rgba(0,0,0,0.08); padding:0; flex:0 0 auto; max-width:300px; overflow:hidden; font-family:-apple-system,BlinkMacSystemFont,system-ui,sans-serif; color:#262626;&quot;&gt;
    &lt;div style=&quot;background:#262626; color:#fff; padding:10px 14px; font-size:11px; font-weight:800; letter-spacing:0.04em; text-transform:uppercase;&quot;&gt;🎤 Guests · Factory Fiction&lt;/div&gt;
    &lt;div style=&quot;padding:12px 14px;&quot;&gt;
      &lt;div style=&quot;display:flex; gap:10px; align-items:center; padding:6px 0;&quot;&gt;&lt;span style=&quot;font-size:16px;&quot;&gt;🎻&lt;/span&gt;&lt;div style=&quot;flex:1;&quot;&gt;&lt;div style=&quot;font-size:13px; font-weight:700;&quot;&gt;Guest fiddle&lt;/div&gt;&lt;div style=&quot;font-size:10px; color:#6B7280;&quot;&gt;3 appearances · last 12/13/25&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;
      &lt;div style=&quot;display:flex; gap:10px; align-items:center; padding:6px 0;&quot;&gt;&lt;span style=&quot;font-size:16px;&quot;&gt;🎺&lt;/span&gt;&lt;div style=&quot;flex:1;&quot;&gt;&lt;div style=&quot;font-size:13px; font-weight:700;&quot;&gt;Guest horns&lt;/div&gt;&lt;div style=&quot;font-size:10px; color:#6B7280;&quot;&gt;1 appearance · Goosemas &apos;24&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;
      &lt;div style=&quot;margin-top:6px; font-size:11px; font-weight:700; color:#8B5CF6;&quot;&gt;See full guest directory →&lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;p&gt;None of these three are things you open during a show. They are things you open because you are a fan and it is a slow afternoon and you want to fall into your band’s history for twenty minutes. That is the whole idea, and this is the part people actually did all summer. More on that below.&lt;/p&gt;

&lt;h2 id=&quot;goose-mode-and-the-band-pages&quot;&gt;Goose Mode and the Band Pages&lt;/h2&gt;

&lt;p&gt;Each band gets a &lt;strong&gt;band mode&lt;/strong&gt;, a tour companion tailored to one band at a time, and Goose Mode got most of the love because Goose is home base. It knows the band’s calendar, colors, and vocabulary. The centerpiece is a tour timeline with a live countdown to the next show, past shows checked off, upcoming shows carrying weather and which of your friends are going, and your friends annotated in and out of the run leg by leg. Every band also has a full band page: recent shows with inline-expand setlists, your personal history with that band, and a jump straight into the Songbook. Adding a band is a whole project (find a setlist source, backfill the history, wire the page and the mode), and we did it more than a dozen times, because the more homes there are, the more nights someone has a reason to open the app.&lt;/p&gt;

&lt;h2 id=&quot;it-runs-on-your-watch-shipping-for-fall&quot;&gt;It Runs on Your Watch (Shipping for Fall)&lt;/h2&gt;

&lt;p&gt;We built an entire new client this window: the &lt;strong&gt;Wear OS and Apple Watch&lt;/strong&gt; apps, essentially all of it Patrick’s work, from the SwiftUI screens to the WidgetKit complications to the connectivity layer. They are not in the stores yet, and that is on purpose. We ran them on our own wrists through the whole Big Modern! summer tour as a live field test, and they ship for real for &lt;strong&gt;Fall tour&lt;/strong&gt;. Everything below already works. It just is not public yet.&lt;/p&gt;

&lt;p&gt;The headline feature is an &lt;strong&gt;always-on live show on your wrist&lt;/strong&gt;. When a band you follow is playing, an OngoingActivity and a watch-face complication put the current song, set, and elapsed time on your watch, persistent, glanceable, updating as the setlist moves. There is a countdown complication for the gap between shows, so even when nothing is live your watch face can tell you how long until the next one. We mirrored the live reaction kit to the watch, so you can react to a song without pulling your phone out of your pocket at all.&lt;/p&gt;

&lt;div style=&quot;display:flex; gap:20px; flex-wrap:wrap; justify-content:center; margin:18px auto; align-items:center;&quot;&gt;
  &lt;div style=&quot;background:#000; border-radius:44px; padding:10px; box-shadow:0 8px 24px rgba(0,0,0,0.25);&quot;&gt;
    &lt;div style=&quot;width:150px; height:186px; border-radius:36px; background:radial-gradient(circle at 50% 30%, #2a1d3a, #0a0510); display:flex; flex-direction:column; align-items:center; justify-content:center; color:#fff; font-family:-apple-system,BlinkMacSystemFont,system-ui,sans-serif; padding:14px;&quot;&gt;
      &lt;div style=&quot;font-size:26px;&quot;&gt;🪿&lt;/div&gt;
      &lt;div style=&quot;font-size:9px; font-weight:800; letter-spacing:0.08em; color:#a855f7; margin-top:4px;&quot;&gt;GOOSE · LIVE&lt;/div&gt;
      &lt;div style=&quot;font-size:16px; font-weight:800; margin-top:6px; text-align:center; line-height:1.1;&quot;&gt;Tumble&lt;/div&gt;
      &lt;div style=&quot;font-size:10px; opacity:0.7; margin-top:2px;&quot;&gt;SET 2 · 6:41&lt;/div&gt;
      &lt;div style=&quot;display:flex; gap:8px; margin-top:10px; font-size:15px;&quot;&gt;&lt;span&gt;🔥&lt;/span&gt;&lt;span&gt;🤯&lt;/span&gt;&lt;span&gt;🕺&lt;/span&gt;&lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
  &lt;div style=&quot;max-width:220px; font-family:-apple-system,BlinkMacSystemFont,system-ui,sans-serif; color:#262626; font-size:13px; line-height:1.5;&quot;&gt;
    &lt;div style=&quot;font-weight:800; margin-bottom:4px;&quot;&gt;Glance, don&apos;t grab.&lt;/div&gt;
    The complication keeps the live setlist on your watch face the whole show. The reaction ring means you can throw a 🔥 at a jam without ever taking your phone out.
  &lt;/div&gt;
&lt;/div&gt;

&lt;p&gt;The watch is the purest expression of the “be present at the show” argument I keep having with people, turned inside out. The complaint is that the app pulls you into your phone. The watch is the app admitting the phone is too much during the show, and shrinking down to a glance and a tap on your wrist so you can put the phone away and still be in the conversation. It is the app taking the criticism seriously and building the answer.&lt;/p&gt;

&lt;h2 id=&quot;the-whole-app-got-redesigned-underneath-all-of-this&quot;&gt;The Whole App Got Redesigned Underneath All of This&lt;/h2&gt;

&lt;p&gt;None of this happened in isolation. The Lot, the live chat, the show page, the Flow, profiles, notifications, festivals, and the band modes all got “v2” cinematic redesigns in the same language: warm parchment and sunset gradients, film grain, an editorial serif for the venues, less like a database with a skin on it and more like a thing made by people who love the thing. The whole app changed clothes this window. This is a big deal operationally because of how it ships. Every redesign runs behind a version flag, keyed to the client build, so an old phone that has not updated keeps getting the old design and a fresh install gets the new one, and neither breaks. That is the machinery that lets us redesign a live surface without stranding the person watching a show on a two-month-old binary. It is unglamorous and it is most of why we can move this fast without breaking the people already here.&lt;/p&gt;

&lt;h2 id=&quot;new-rooms-cabo-the-festivals-more-bands&quot;&gt;New Rooms: Cabo, the Festivals, More Bands&lt;/h2&gt;

&lt;p&gt;The band list kept growing. We backfilled full histories for &lt;strong&gt;Widespread Panic (3,114 shows), Eggy, and Dizgo&lt;/strong&gt;, on top of the dozen bands we added during Spring, each with the full treatment: a source for the setlists, a band page, a band mode, a spot in the directory. There are &lt;strong&gt;102 bands and 38,894 shows&lt;/strong&gt; in the catalog now.&lt;/p&gt;

&lt;p&gt;We built out &lt;strong&gt;festivals&lt;/strong&gt; as their own surface: a multi-stage schedule grid, a plan phase, and a full &lt;strong&gt;festival recap&lt;/strong&gt; that reads the whole weekend rather than one show, with a program block, hero moments, a route map, and per-day bookends. &lt;strong&gt;Viva El Gonzo&lt;/strong&gt; in Cabo got the destination-festival treatment (down to canonicalizing every set to San José del Cabo), and we seeded lineups for All Good Now and Northlands. Every new tour gets wired in the moment it is announced.&lt;/p&gt;

&lt;p&gt;The point of all of it is the same: the more bands and the more festivals live in the app, the more nights of the year there is something happening for somebody, and the fewer truly dead Tuesdays there are.&lt;/p&gt;

&lt;h2 id=&quot;festival-mode-plan-it-like-a-conference&quot;&gt;Festival Mode: Plan It Like a Conference&lt;/h2&gt;

&lt;p&gt;A festival is a different animal from a tour stop, so it gets its own mode. The centerpiece is a &lt;strong&gt;multi-stage schedule grid&lt;/strong&gt;, and the closest honest comparison is a work conference program: stages down one axis, set times across the other, every slot a block you can tap. Except the sessions are sets, the tracks are stages, and, exactly like a good conference app, the schedule is &lt;em&gt;yours&lt;/em&gt;. In each timeslot several bands are playing at once across the stages, and you pick the one set you are actually going to. The app assembles those picks into your personal day, one choice per slot, the same way you would star the talks you want at a conference and let it hide the rest.&lt;/p&gt;

&lt;p&gt;And because a festival grounds is a place you can genuinely get lost in, it does the other thing a conference app does that you never knew you needed: it tells you &lt;strong&gt;where to go.&lt;/strong&gt; Between sets it surfaces your next pick, points you at the right stage, and tells you how far the walk is, so you are not squinting at a paper map in the sun wondering whether you can make it from the Beach Stage to the Jungle Stage before the band you actually care about starts. It flags your overlaps, carries each day’s weather, and lights up the set happening right now. When the weekend is over, Festival Mode writes a &lt;strong&gt;recap of the whole event&lt;/strong&gt;, not one show but the full arc: a program block, the standout moments, a route map of where the weekend went, and per-day bookends. &lt;strong&gt;Viva El Gonzo&lt;/strong&gt; in Cabo got the full treatment this year, down to canonicalizing every set to San José del Cabo.&lt;/p&gt;

&lt;div style=&quot;color-scheme:light; background:#E7E2D6; padding:16px; border-radius:20px; margin:16px auto; max-width:480px;&quot;&gt;
  &lt;div style=&quot;background:#F0EDE4; border-radius:18px; box-shadow:0 10px 28px rgba(20,18,30,0.12); overflow:hidden; max-width:440px; margin:0 auto; color:#2A2A3A; font-family:&apos;Inter&apos;,-apple-system,BlinkMacSystemFont,system-ui,sans-serif;&quot;&gt;
    &lt;div style=&quot;padding:12px 15px; background:radial-gradient(circle at 20% 20%, #F2A83B, #E83A73 120%); color:#fff; display:flex; align-items:center; justify-content:space-between;&quot;&gt;&lt;span style=&quot;font-family:&apos;Jost&apos;,-apple-system,BlinkMacSystemFont,system-ui,sans-serif; font-size:12px; font-weight:700; letter-spacing:0.10em;&quot;&gt;⛺ VIVA EL GONZO · SAT&lt;/span&gt;&lt;span style=&quot;font-family:&apos;Instrument Serif&apos;,&apos;Cormorant Garamond&apos;,Georgia,serif; font-style:italic; font-size:14px; opacity:0.92;&quot;&gt;your day · 82°&lt;/span&gt;&lt;/div&gt;
    &lt;div style=&quot;margin:12px 13px; padding:11px 13px; border-radius:12px; background:#141220; color:#fff; display:flex; align-items:center; gap:10px;&quot;&gt;&lt;span style=&quot;font-size:20px;&quot;&gt;🧭&lt;/span&gt;&lt;div style=&quot;flex:1;&quot;&gt;&lt;div style=&quot;font-family:&apos;Jost&apos;,-apple-system,BlinkMacSystemFont,system-ui,sans-serif; font-size:9px; font-weight:800; letter-spacing:0.12em; text-transform:uppercase; color:#F2A83B;&quot;&gt;Up next · where to go&lt;/div&gt;&lt;div style=&quot;font-family:&apos;Inter&apos;,-apple-system,BlinkMacSystemFont,system-ui,sans-serif; font-size:12.5px; line-height:1.35; margin-top:2px;&quot;&gt;Head to the &lt;b&gt;Beach Stage&lt;/b&gt;. Goose Set II in 12 min · 6 min walk.&lt;/div&gt;&lt;/div&gt;&lt;span style=&quot;font-size:16px; color:#F2A83B;&quot;&gt;→&lt;/span&gt;&lt;/div&gt;
    &lt;div style=&quot;padding:2px 13px 14px;&quot;&gt;
      &lt;div style=&quot;font-family:&apos;Jost&apos;,-apple-system,BlinkMacSystemFont,system-ui,sans-serif; font-size:9px; font-weight:800; letter-spacing:0.10em; text-transform:uppercase; color:#9A9AAA; margin-bottom:8px;&quot;&gt;Your picks · one per slot&lt;/div&gt;
      &lt;div style=&quot;display:flex; gap:10px; align-items:stretch; margin-bottom:8px;&quot;&gt;&lt;div style=&quot;font-family:&apos;Jost&apos;,-apple-system,BlinkMacSystemFont,system-ui,sans-serif; font-size:11px; font-weight:700; color:#6B6B7B; width:42px; display:flex; align-items:center;&quot;&gt;7:00&lt;/div&gt;&lt;div style=&quot;flex:1; display:flex; gap:6px;&quot;&gt;&lt;div style=&quot;flex:1; background:rgba(42,95,170,0.10); border-left:3px solid #2A5FAA; border-radius:10px; padding:8px 10px;&quot;&gt;&lt;div style=&quot;font-family:&apos;Jost&apos;,-apple-system,BlinkMacSystemFont,system-ui,sans-serif; font-size:12.5px; font-weight:700; color:#2A2A3A;&quot;&gt;Goose&lt;/div&gt;&lt;div style=&quot;font-family:&apos;Inter&apos;,-apple-system,BlinkMacSystemFont,system-ui,sans-serif; font-size:10px; color:#6B6B7B;&quot;&gt;Beach Stage&lt;/div&gt;&lt;span style=&quot;font-size:8.5px; font-weight:800; letter-spacing:0.05em; text-transform:uppercase; color:#2A5FAA;&quot;&gt;✓ your pick&lt;/span&gt;&lt;/div&gt;&lt;div style=&quot;flex:1; background:#FEFDFB; border-radius:10px; padding:8px 10px; opacity:0.5;&quot;&gt;&lt;div style=&quot;font-family:&apos;Jost&apos;,-apple-system,BlinkMacSystemFont,system-ui,sans-serif; font-size:12.5px; font-weight:600; color:#6B6B7B;&quot;&gt;Dogs in a Pile&lt;/div&gt;&lt;div style=&quot;font-family:&apos;Inter&apos;,-apple-system,BlinkMacSystemFont,system-ui,sans-serif; font-size:10px; color:#9A9AAA;&quot;&gt;Jungle Stage&lt;/div&gt;&lt;span style=&quot;font-size:8.5px; font-weight:700; letter-spacing:0.05em; text-transform:uppercase; color:#9A9AAA;&quot;&gt;skipping&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;
      &lt;div style=&quot;display:flex; gap:10px; align-items:stretch; margin-bottom:8px;&quot;&gt;&lt;div style=&quot;font-family:&apos;Jost&apos;,-apple-system,BlinkMacSystemFont,system-ui,sans-serif; font-size:11px; font-weight:700; color:#6B6B7B; width:42px; display:flex; align-items:center;&quot;&gt;8:15&lt;/div&gt;&lt;div style=&quot;flex:1; display:flex; gap:6px;&quot;&gt;&lt;div style=&quot;flex:1; background:rgba(42,95,170,0.10); border-left:3px solid #2A5FAA; border-radius:10px; padding:8px 10px;&quot;&gt;&lt;div style=&quot;font-family:&apos;Jost&apos;,-apple-system,BlinkMacSystemFont,system-ui,sans-serif; font-size:12.5px; font-weight:700; color:#2A2A3A;&quot;&gt;Khruangbin&lt;/div&gt;&lt;div style=&quot;font-family:&apos;Inter&apos;,-apple-system,BlinkMacSystemFont,system-ui,sans-serif; font-size:10px; color:#6B6B7B;&quot;&gt;Jungle Stage&lt;/div&gt;&lt;span style=&quot;font-size:8.5px; font-weight:800; letter-spacing:0.05em; text-transform:uppercase; color:#2A5FAA;&quot;&gt;✓ your pick&lt;/span&gt;&lt;/div&gt;&lt;div style=&quot;flex:1; background:#FEFDFB; border-radius:10px; padding:8px 10px; opacity:0.5;&quot;&gt;&lt;div style=&quot;font-family:&apos;Jost&apos;,-apple-system,BlinkMacSystemFont,system-ui,sans-serif; font-size:12.5px; font-weight:600; color:#6B6B7B;&quot;&gt;Sunsquabi&lt;/div&gt;&lt;div style=&quot;font-family:&apos;Inter&apos;,-apple-system,BlinkMacSystemFont,system-ui,sans-serif; font-size:10px; color:#9A9AAA;&quot;&gt;Sandbar&lt;/div&gt;&lt;span style=&quot;font-size:8.5px; font-weight:700; letter-spacing:0.05em; text-transform:uppercase; color:#9A9AAA;&quot;&gt;skipping&lt;/span&gt;&lt;/div&gt;&lt;div style=&quot;flex:1; background:#FEFDFB; border-radius:10px; padding:8px 10px; opacity:0.5;&quot;&gt;&lt;div style=&quot;font-family:&apos;Jost&apos;,-apple-system,BlinkMacSystemFont,system-ui,sans-serif; font-size:12.5px; font-weight:600; color:#6B6B7B;&quot;&gt;DJ Logic&lt;/div&gt;&lt;div style=&quot;font-family:&apos;Inter&apos;,-apple-system,BlinkMacSystemFont,system-ui,sans-serif; font-size:10px; color:#9A9AAA;&quot;&gt;Cantina&lt;/div&gt;&lt;span style=&quot;font-size:8.5px; font-weight:700; letter-spacing:0.05em; text-transform:uppercase; color:#9A9AAA;&quot;&gt;skipping&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;
      &lt;div style=&quot;display:flex; gap:10px; align-items:stretch; margin-bottom:8px;&quot;&gt;&lt;div style=&quot;font-family:&apos;Jost&apos;,-apple-system,BlinkMacSystemFont,system-ui,sans-serif; font-size:11px; font-weight:700; color:#6B6B7B; width:42px; display:flex; align-items:center;&quot;&gt;9:15&lt;/div&gt;&lt;div style=&quot;flex:1; display:flex; gap:6px;&quot;&gt;&lt;div style=&quot;flex:1; background:rgba(232,58,115,0.12); border-left:3px solid #E83A73; border-radius:10px; padding:8px 10px;&quot;&gt;&lt;div style=&quot;font-family:&apos;Jost&apos;,-apple-system,BlinkMacSystemFont,system-ui,sans-serif; font-size:12.5px; font-weight:700; color:#2A2A3A;&quot;&gt;Goose&lt;/div&gt;&lt;div style=&quot;font-family:&apos;Inter&apos;,-apple-system,BlinkMacSystemFont,system-ui,sans-serif; font-size:10px; color:#6B6B7B;&quot;&gt;Beach Stage&lt;/div&gt;&lt;span style=&quot;font-size:8.5px; font-weight:800; letter-spacing:0.05em; text-transform:uppercase; color:#E83A73;&quot;&gt;● live now&lt;/span&gt;&lt;/div&gt;&lt;div style=&quot;flex:1; background:#FEFDFB; border-radius:10px; padding:8px 10px; opacity:0.5;&quot;&gt;&lt;div style=&quot;font-family:&apos;Jost&apos;,-apple-system,BlinkMacSystemFont,system-ui,sans-serif; font-size:12.5px; font-weight:600; color:#6B6B7B;&quot;&gt;Neal Francis&lt;/div&gt;&lt;div style=&quot;font-family:&apos;Inter&apos;,-apple-system,BlinkMacSystemFont,system-ui,sans-serif; font-size:10px; color:#9A9AAA;&quot;&gt;Jungle Stage&lt;/div&gt;&lt;span style=&quot;font-size:8.5px; font-weight:700; letter-spacing:0.05em; text-transform:uppercase; color:#9A9AAA;&quot;&gt;skipping&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;h2 id=&quot;half-of-this-is-patrick&quot;&gt;Half of This Is Patrick&lt;/h2&gt;

&lt;p&gt;I need to stop and do this properly, because if you have read this far you have been reading “we” the whole time and the “we” is doing a lot of quiet work.&lt;/p&gt;

&lt;p&gt;Patrick McCaughey (he is &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;@blimpalot&lt;/code&gt; in the app) wrote &lt;strong&gt;nearly half of the 787 pull requests in this window.&lt;/strong&gt; Not helper commits. Whole features, end to end, that are among the best things in the product. The Apple Watch app is his. The entire sit-in and guest-credit system is his. Group DMs and the Messages hub are his. Birthdays, big chunks of the poster archive, huge swaths of Tour Stats, the band backfills, the reaction kits for new bands. When I said the reference library is the part people actually used all summer, I was mostly describing Patrick’s work.&lt;/p&gt;

&lt;p&gt;I said it myself in the chat one night, and I am quoting it because it is both generous and exactly backwards about who actually deserves the credit:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;&lt;em&gt;“I might have wrote the app but all the cool shit in the app isn’t me it’s Patrick lol”&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;He would tell you the opposite. We are both a little bit right, and he is more right than I am.&lt;/p&gt;

&lt;p&gt;And here is the part that does not show up in a commit log at all. &lt;strong&gt;Patrick made stickers.&lt;/strong&gt; Actual, physical, die-cut vinyl slaps, and he brings them to &lt;strong&gt;every single show&lt;/strong&gt; and hands them out: on the lot, in the lot line, in the pit, to the person next to him, to the bartender, to total strangers between sets. He is not just leaving them on a table. He is walking up to people, showing them the app on his own phone, and talking them into it one human conversation at a time, night after night, city after city. Every growth chart later in this post that ticks up, ticks up because Patrick stood in a parking lot in the heat and sold someone on a thing he helped build. The man is a mensch. I do not know a better word for it, and I do not think there is one. Half the code and all of the street team, cheerfully, for the love of it.&lt;/p&gt;

&lt;p&gt;That is the growth engine. Not a referral loop or a viral coefficient. A person who loves the thing, standing in front of another person, in the place they both love to be. You cannot buy that and you cannot fake it, and it is the entire reason a two-person app has a real community around it at all.&lt;/p&gt;

&lt;h2 id=&quot;did-it-work-the-honest-numbers&quot;&gt;Did It Work? The Honest Numbers&lt;/h2&gt;

&lt;p&gt;I promised numbers, not vibes, so here they are, including the ones I would have quietly dropped if I were selling something.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Users grew from 251 to 426.&lt;/strong&gt; That is 70% growth in two months, which sounds great until you look at &lt;em&gt;when&lt;/em&gt; it happened.&lt;/p&gt;

&lt;p&gt;There is one milestone I am, as the proprietor of a jam band app, contractually obligated to report. Somewhere during the two-night Raleigh run at the end of June, right about the time Goose hit the stage at Red Hat, the registered user count ticked over to exactly &lt;strong&gt;420.&lt;/strong&gt; We did not plan this. We are, however, precisely the crowd that was going to notice, screenshot it, and refuse to shut up about it. It has since climbed to a spiritually inferior 426, but &lt;strong&gt;for one perfect night in Raleigh, the fanbase was, numerically, blazing it.&lt;/strong&gt;&lt;/p&gt;

&lt;div style=&quot;background:#e5e2d9; padding:20px 18px; border-radius:14px; margin:16px auto; max-width:560px; font-family:-apple-system,BlinkMacSystemFont,system-ui,sans-serif; color:#262626;&quot;&gt;
  &lt;div style=&quot;font-size:11px; font-weight:800; letter-spacing:0.05em; text-transform:uppercase; color:#6B7280; margin-bottom:14px;&quot;&gt;New signups by week · Mar 23 → Jun 29&lt;/div&gt;
  &lt;div style=&quot;display:flex; align-items:flex-end; gap:5px; height:130px;&quot;&gt;
    &lt;div style=&quot;flex:1; background:#8B5CF6; height:44%; border-radius:3px 3px 0 0;&quot; title=&quot;30&quot;&gt;&lt;/div&gt;
    &lt;div style=&quot;flex:1; background:#8B5CF6; height:65%; border-radius:3px 3px 0 0;&quot; title=&quot;44&quot;&gt;&lt;/div&gt;
    &lt;div style=&quot;flex:1; background:#8B5CF6; height:19%; border-radius:3px 3px 0 0;&quot; title=&quot;13&quot;&gt;&lt;/div&gt;
    &lt;div style=&quot;flex:1; background:#8B5CF6; height:76%; border-radius:3px 3px 0 0;&quot; title=&quot;52&quot;&gt;&lt;/div&gt;
    &lt;div style=&quot;flex:1; background:#8B5CF6; height:69%; border-radius:3px 3px 0 0;&quot; title=&quot;47&quot;&gt;&lt;/div&gt;
    &lt;div style=&quot;flex:1; background:#7c3aed; height:62%; border-radius:3px 3px 0 0;&quot; title=&quot;42 · tour ends&quot;&gt;&lt;/div&gt;
    &lt;div style=&quot;flex:1; background:#7c3aed; height:100%; border-radius:3px 3px 0 0;&quot; title=&quot;68&quot;&gt;&lt;/div&gt;
    &lt;div style=&quot;flex:1; background:#c4b5a0; height:9%; border-radius:3px 3px 0 0;&quot; title=&quot;6&quot;&gt;&lt;/div&gt;
    &lt;div style=&quot;flex:1; background:#c4b5a0; height:12%; border-radius:3px 3px 0 0;&quot; title=&quot;8&quot;&gt;&lt;/div&gt;
    &lt;div style=&quot;flex:1; background:#c4b5a0; height:10%; border-radius:3px 3px 0 0;&quot; title=&quot;7&quot;&gt;&lt;/div&gt;
    &lt;div style=&quot;flex:1; background:#c4b5a0; height:9%; border-radius:3px 3px 0 0;&quot; title=&quot;6&quot;&gt;&lt;/div&gt;
    &lt;div style=&quot;flex:1; background:#c4b5a0; height:10%; border-radius:3px 3px 0 0;&quot; title=&quot;7&quot;&gt;&lt;/div&gt;
    &lt;div style=&quot;flex:1; background:#c4b5a0; height:21%; border-radius:3px 3px 0 0;&quot; title=&quot;14&quot;&gt;&lt;/div&gt;
    &lt;div style=&quot;flex:1; background:#c4b5a0; height:21%; border-radius:3px 3px 0 0;&quot; title=&quot;14&quot;&gt;&lt;/div&gt;
    &lt;div style=&quot;flex:1; background:#c4b5a0; height:4%; border-radius:3px 3px 0 0;&quot; title=&quot;3&quot;&gt;&lt;/div&gt;
  &lt;/div&gt;
  &lt;div style=&quot;display:flex; justify-content:space-between; font-size:9px; color:#9CA3AF; margin-top:6px;&quot;&gt;
    &lt;span&gt;◀ Spring tour&lt;/span&gt;&lt;span&gt;tour ends&lt;/span&gt;&lt;span&gt;summer ▶&lt;/span&gt;
  &lt;/div&gt;
  &lt;div style=&quot;font-size:10px; color:#6B7280; margin-top:8px; text-align:center;&quot;&gt;Purple = tour weeks. The two tallest bars are the tour and the week right after it. Then it falls off a cliff.&lt;/div&gt;
&lt;/div&gt;

&lt;p&gt;Signups are event-driven. They spiked during the tour and the week after (the wrap-up avalanche, word of mouth, and Patrick’s stickers), then dropped to single digits a week through June. This is the thing I got wrong going in: I half-expected the between-shows features to keep pulling new people in on their own. They did not. &lt;strong&gt;You acquire people at shows, from other people.&lt;/strong&gt; Nothing I shipped in May moved that needle, and pretending otherwise would be exactly the kind of thing this blog exists to call out.&lt;/p&gt;

&lt;p&gt;But I want to be clear that I am proud of that chart, spike and cliff and all. Those 426 people are not a paid-acquisition cohort. Every one of them is someone who was at a show, or in a chat, or handed a sticker, and decided this was worth their time. That is the only kind of growth I actually want. I would rather have 426 people who love it than 40,000 who installed it and forgot, and the shape of that chart is what growing something real and by hand actually looks like.&lt;/p&gt;

&lt;p&gt;So if new signups are a tour phenomenon, the real job of everything in this post is not acquisition. It is &lt;strong&gt;retention&lt;/strong&gt;: keeping the people who showed up in Spring around and warm until the next run, so the community is intact when it matters. And on that, the numbers are better and more honest:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;The live chat never went silent.&lt;/strong&gt; 2,547 messages from 42 people across this window, versus 3,737 from 40 during the concentrated Spring run. Fewer messages, because there were far fewer big Goose nights, but the same size of core showing up, between tours, with no fourteen-show schedule forcing them to.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;People spent the quiet months logging their history.&lt;/strong&gt; Attendance records went from 1,783 to &lt;strong&gt;4,598&lt;/strong&gt;, and &lt;strong&gt;2,570 of those were logged in this window&lt;/strong&gt;, spread across 70 different bands. Cataloging the shows you have been to is the most between-shows activity there is. Nobody logs a 2016 club show during a live set. They do it on a slow Sunday, which is precisely the Tuesday-open I was trying to earn.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;The reference library got used.&lt;/strong&gt; The Songbook, the poster archive, and the guest directory are the surfaces with real traffic on no-show days, which tracks: they are built to be read when nothing is live.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;And when there was a show, engagement went vertical.&lt;/strong&gt; As covered up in the live-chat section: reactions went from a handful a week to 1,067 in a single week once the Big Modern! tour started, and the last three weeks of tour drove roughly 3,800 chat interactions, rivaling the whole Spring run. The between-tours job is retention, but the tour itself proved the redesigned room is more alive than it has ever been.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And the numbers I am &lt;strong&gt;not&lt;/strong&gt; going to give you: a clean daily-active-users chart. We record a row when a user opens the app, but that table has multi-week holes in it from May and early June where the logging was broken, so any DAU or “percent active on non-show days” figure I quoted would be built on gaps. I would rather tell you the table is broken than draw a confident line through missing data. Fixing that instrumentation is now on the list, because the between-shows thesis deserves a real measurement and right now I cannot give it one.&lt;/p&gt;

&lt;p&gt;The one-line version: &lt;strong&gt;we are not going viral, and I do not want to. We are growing a thing I love slowly, by hand, and keeping the room warm between tours. Mostly we did.&lt;/strong&gt;&lt;/p&gt;

&lt;h2 id=&quot;what-this-stretch-taught-me&quot;&gt;What This Stretch Taught Me&lt;/h2&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;The gap is the product, and it is a retention problem, not a growth problem.&lt;/strong&gt; Spring proved the show works. This stretch proved that new users arrive on the tour calendar and not otherwise, so the between-shows job is to hold the community you already have. Almost every feature in this post is a bet on retention, and the honest scoreboard is “the core stayed,” not a signup hockey stick.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Measure the thesis or you are guessing.&lt;/strong&gt; I built a whole argument about between-shows engagement and then discovered my own session-tracking had holes in it. Shipping the features without fixing the instrument to grade them is a mistake I am writing down so I actually fix it.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Shipping a redesign is a distributed-systems problem.&lt;/strong&gt; The version-flag machinery that lets an old build keep its old UI while a new build gets v2 is the least visible work of the whole two months and close to the most important. Without it, moving this fast would mean breaking the exact people who already showed up.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;787 PRs is still not normal, and most of it is still Claude.&lt;/strong&gt; Same as April: a lot of this was written by Claude, a lot of it had to be rewritten by Claude after I caught it going sideways, and every sharp edge got logged into the &lt;a href=&quot;/ai/zabriskie/agents/reliability/testing/2026/06/10/the-test-suite-was-the-incident.html&quot;&gt;agent reliability dataset&lt;/a&gt; I keep publishing from. The failures are the point of the dataset, not something to hide.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Being on the App Store changes what you build.&lt;/strong&gt; Report-and-block moderation on chat messages was not a feature anyone in the community asked for. It was the price of admission to the store, and it shipped in the same window as everything fun. Growing up means building the boring safety surface too.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And I should say the obvious thing, the thing under all of it, the reason two people spend nights and tour weekends building a couch-touring app instead of doing literally anything else: &lt;strong&gt;Goose fucks.&lt;/strong&gt; That is the whole premise. The band is that good, the jams are worth talking about at 1am, the bustouts are worth calling from your couch, and the community around them is worth building a home for. Everything in this post is downstream of a band being good enough that people want to be in the same room about it, even when the room is a chat.&lt;/p&gt;

&lt;p&gt;Goose Summer is in the books now, and this post is the receipt for it. Phish is up next, then Goose Fall after that. The festivals are seeded, the watch is on my wrist, and Patrick built it. The app is not done, and the real test of everything here is not this recap. It is whether the room stays this alive between now and the next run, while it is still just an announcement away.&lt;/p&gt;

&lt;p&gt;If you toured with us this summer, thanks for chomping. If you have been in the chat on the dead Tuesdays, you are one of the 42 who kept it alive, and you are the entire experiment working. If Patrick handed you a sticker in a parking lot and you scanned it, welcome, you are the growth chart. And if you have never tried it and an app that is good company between shows sounds like something you want, come find us. There is probably something worth reading tonight, even if nothing is live.&lt;/p&gt;

&lt;hr /&gt;

&lt;p&gt;&lt;em&gt;One housekeeping note, same as last time: the screens in this post are mockups, not real screenshots, drawn to make each feature legible in one frame. The real app looks slightly different on iOS, Android, and the watch (Live Activities vs. ongoing notifications, complication styling, system fonts). The features themselves all shipped, and Patrick’s stickers are real. The growth figures are real production numbers as of June 30, including the unflattering ones. Patrick’s share of the work is real too, and if anything I undersold it.&lt;/em&gt;&lt;/p&gt;
</description>
				<pubDate>Tue, 30 Jun 2026 19:00:00 +0000</pubDate>
				<link>https://christophermeiklejohn.com/ai/zabriskie/development/2026/06/30/the-app-that-lives-between-shows.html</link>
				<guid isPermaLink="true">https://christophermeiklejohn.com/ai/zabriskie/development/2026/06/30/the-app-that-lives-between-shows.html</guid>
			</item>
		
			<item>
				<title>The Test Suite Was the Incident · A night of brittle fixtures, 49 failed CI runs, and an expensive lesson about what happens when AI agents write the test suite and nobody owns the test data.</title>
				<description>&lt;p&gt;At 10:00 PM EDT last night I signed up for the $100 plan for Codex, the OpenAI coding agent that was driving most of the night’s work, because about a dozen &lt;a href=&quot;/zabriskie/&quot;&gt;Zabriskie&lt;/a&gt; PRs were in flight and I wanted them merged before I went to bed. Before 3:00 AM I had upgraded to the $200 plan. Somewhere in between, GitHub’s usage page ticked through roughly 90% of a $200 CI quota, and I sat there watching pull request after pull request go red for reasons that had nothing to do with the pull requests.&lt;/p&gt;

&lt;p&gt;Some context for readers arriving fresh. Zabriskie is a social app for live-music fans: you RSVP to shows, track the ones you attended, post about what you’re hearing, and compare tour stats with friends. I have written almost none of it. AI agents built the features, and AI agents also built the test suite that guards them: the specs, the shared seed data, the fixture users, the helpers, nearly all of it written by some agent in some session that was about something else. That second part turns out to be the entire subject of this post.&lt;/p&gt;

&lt;p&gt;Nothing was wrong with GitHub Actions. Nothing was catastrophically wrong with the application. No single PR was especially risky. Every PR was simply paying to rebuild the same brittle world from scratch: backend build, fresh Postgres, the full database migration chain, the shared seed data, the frontend, and the end-to-end browser tests (Playwright) split into eight parallel shards. That world was held together by fixtures nobody owned. I spent almost two hundred dollars last night proving that my test suite was lying to me.&lt;/p&gt;

&lt;p&gt;The morning-after numbers, measured from the GitHub run history:&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;thing&lt;/th&gt;
      &lt;th style=&quot;text-align: right&quot;&gt;amount&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;CI runs observed&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;168&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;failed runs&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;49&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;cancelled runs&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;10&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;total runner time&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;11,721.8 job-minutes (195.4 hours)&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;failed runner time&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;3,411.0 job-minutes&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;failed plus cancelled runner time&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;3,903.0 job-minutes&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;GitHub CI quota used&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;about 90% of $200&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;estimated agent token waste&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;about 1.7M to 2.5M tokens&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;If I allocate the $180 actually consumed across the observed runner time, the failed jobs alone cost about $52, and the failed plus cancelled jobs about $60. That number is technically true and emotionally useless. The green runs existed because the red runs forced another commit. The reruns existed because the failure kept moving from one shared assumption to another. The successful checks weren’t clean proof of health; they were the tax paid after pushing the suite back into one of the few shapes it still tolerated. So the honest accounting is this: the directly attributable CI waste was about sixty dollars, and the practical cost of the bad test design was most of the $180, plus the evening, plus something like two million tokens spent asking agents to debug symptoms of the same structural problem. The tests were not protecting me. They were charging me rent.&lt;/p&gt;

&lt;h2 id=&quot;the-shared-world&quot;&gt;The Shared World&lt;/h2&gt;

&lt;p&gt;The actual application work was not the disaster. There were real features, real fixes, and pull requests that should have been small, reviewable, and mergeable. Instead, every one of them entered the same grinder, and the first wave of failures had nothing to do with the PRs that were failing.&lt;/p&gt;

&lt;p&gt;One migration, a data fix for a birthday feature, expected a user named &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;queenofthemean&lt;/code&gt; to already exist in the database it was migrating, and in a fresh CI database she didn’t:&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;birthday-honoree fix matched 0 rows
no user with username/display_name = queenofthemean
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Another batch of failures came from migrations over the project’s incident log. Zabriskie’s repository tracks its own operational incidents, complete with required estimates of what each one wasted, and the old incident rows didn’t include those waste estimates, so the migrations tripped a database constraint. These weren’t product regressions. They were fixture assumptions leaking into migration behavior: a PR could be about one corner of the app and still lose because some other part of the global test universe had drifted.&lt;/p&gt;

&lt;p&gt;Then came the collision that named the whole problem. Zabriskie has a feature called the Lot, a hangout space with a tour stats card that summarizes the shows you’ve been to. A change made that card count only past shows the user had actually attended, which was the correct product behavior, and a test was updated to assert it. The setup for that test added a show RSVP for &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;e2etester&lt;/code&gt;, the single global user that half the suite borrows for whatever scenario it needs. Meanwhile, a spec for show recaps elsewhere in the suite expected its fixture show to have exactly one attendee. The recap test started failing, not because recaps were broken, but because the shared world had quietly acquired one extra person.&lt;/p&gt;

&lt;p&gt;It’s worth pausing on how a suite ends up shaped like this, because nobody designed it. An agent writing a test has a context window scoped to one feature and one goal: make this spec pass. The cheapest path to green is to reuse whatever fixture world already exists, so the agent grabs &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;e2etester&lt;/code&gt;, adds the RSVP it needs, watches the spec pass, and moves on. Every one of those choices is locally reasonable. But no session is ever about the fixture universe itself, so no agent ever sees that &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;e2etester&lt;/code&gt; now means five different things to five different specs. This is the same dynamic I wrote about in &lt;a href=&quot;/ai/claude/2026/03/08/claude-tested-everything-except-the-one-thing-that-mattered.html&quot;&gt;Claude Tested Everything Except the One Thing That Mattered&lt;/a&gt;: agents write tests where the tests are easiest, in the session where the context is fresh. They build fixture worlds the same way. The result is a global social graph that accreted instead of being designed, with no author who ever held the whole thing in view.&lt;/p&gt;

&lt;p&gt;This is the specific kind of failure that teaches people to stop trusting tests. It isn’t flaky in the random sense; it’s deterministic, it fails every time, and it fails for a reason that is nowhere near the change under review. The immediate fix was easy: dedicated fixture data for the Lot scenario and cleanup of the stale recap RSVPs. The lesson was bigger than the fix. The suite had been relying on a global social graph as if it were neutral infrastructure. It was not neutral. It was shared mutable state with a green checkmark on it.&lt;/p&gt;

&lt;h2 id=&quot;the-same-bug-wearing-different-clothes&quot;&gt;The Same Bug, Wearing Different Clothes&lt;/h2&gt;

&lt;p&gt;A few hours later the same pattern came back through the song database, the catalog of bands and songs that powers Zabriskie’s setlists and search. A migration changed the uniqueness model for songs from a constraint on &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;(band_id, song_name)&lt;/code&gt; to an expression index that accounted for cover artists: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;(band_id, song_name, COALESCE(original_artist, &apos;&apos;))&lt;/code&gt;. A reasonable schema change. But the E2E seed file still contained inserts that said &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ON CONFLICT (band_id, song_name) DO UPDATE&lt;/code&gt;, and Postgres did exactly what Postgres should do:&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;ERROR: there is no unique or exclusion constraint matching the ON CONFLICT specification
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;This wasn’t a mysterious CI problem either. It was drift between the schema and the shared seed. The seed was effectively part of the application contract, but nothing treated it like one: it had no real owner, it didn’t fail early, and it didn’t fail once. It failed everywhere, simultaneously. Song search couldn’t find “All In Time” for Umphrey’s McGee, because the seed insert that should have created it had failed. The Lot’s call-the-opener game, which offers users songs to predict as a show’s opener, started returning undefined suggestions for the same reason. Multiple open PRs went red for the same underlying cause, and at that point the test suite was no longer a set of independent checks. It was a broadcast mechanism for one broken assumption.&lt;/p&gt;

&lt;h2 id=&quot;the-multiplication&quot;&gt;The Multiplication&lt;/h2&gt;

&lt;p&gt;The worst part wasn’t any single failure; it was what the architecture did to each failure. The E2E matrix had recently been rebalanced from six shards to eight because one shard had grown too slow, a reasonable response to a real pressure. But while the fixture world was unstable, the eight-shard matrix turned every mistake into a paid distributed event. Every rerun started more databases. Every shard replayed the full migration chain. Every unrelated branch discovered the same broken assumptions independently, and every discovery kicked off the same loop: the PR fails, an agent investigates, the visible symptom gets patched, CI runs again, a different PR fails for the same shared reason, another agent investigates, the suite gets a little more elaborate, CI runs again.&lt;/p&gt;

&lt;p&gt;That loop spent money, time, attention, and tokens. There’s no perfect token accounting, because the agent usage isn’t tied cleanly to each CI failure, but each broken-PR loop cost tens of thousands of tokens in log reading, hypothesis generation, patching, rerunning, and explaining. Across 49 failed runs and the follow-on repair work, the estimate lands between 1.7 and 2.5 million tokens; my best single number is about 2.1 million. The precise figure matters less than the shape of it. Bad test design doesn’t just waste CI minutes. It converts every developer and every agent into a distributed retry system.&lt;/p&gt;

&lt;h2 id=&quot;then-the-fix-joined-the-incident&quot;&gt;Then the Fix Joined the Incident&lt;/h2&gt;

&lt;p&gt;The late-night mitigation was supposed to be the responsible engineering move: stop replaying 900-plus migrations in every E2E shard. Build the database once, dump it, restore the dump into each shard, pay the migration tax exactly once. The premise was right both times it was tried. The first attempt, PR #1112, “Speed up E2E shards with a migrated DB snapshot,” opened at 12:44 AM and was closed nineteen minutes later, wrong enough to throw away.&lt;/p&gt;

&lt;p&gt;The second attempt, PR #1119, opened at 1:58 AM, and by 2:32 it had five commits whose messages tell the story on their own: baseline the migrations, then fail incomplete baseline migrations, then fail fast on a retired song-database upsert key, then refresh the baseline manifest fingerprint, then harden a historical backfill. A sixth commit followed: a merge from main, which wasn’t progress, just the bookkeeping a branch needs once it has lived long enough to fall behind the merge train.&lt;/p&gt;

&lt;p&gt;The underlying technical failure was not subtle. The baseline generator assumed the whole historical migration chain could be replayed strictly from zero. The repository had actually been surviving on a migration runner that tolerated, skipped, or worked around old broken history, and freezing that history into a baseline artifact put every old assumption on the critical path at once. Migration 305 had a Postgres scoping error in a historical &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;UPDATE ... FROM&lt;/code&gt; statement. The birthday-honoree migration assumed &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;queenofthemean&lt;/code&gt; existed in a fresh database. The incident migrations tripped their constraints again. Bugbot, the automated reviewer that comments on the repo’s PRs, found that the baseline dump could silently allow skipped migrations and that the migration CLI could ignore failed ones. One CI run died building the dump, another died validating the manifest, and a third made it all the way through the baseline, started all eight shards, and still failed shard 8. Those weren’t edge cases. They were proof that the path #1119 wanted to freeze was never clean enough to freeze.&lt;/p&gt;

&lt;p&gt;And there was a number sitting in plain sight that should have ended the effort immediately: the baseline generation took about eleven minutes, against a previous full CI run of about thirteen. An optimization whose setup phase nearly equals the old end-to-end runtime is not an optimization yet. It is a hypothesis, and an expensive one. Instead the loop kept going, one patched symptom at a time, through the most precious hour of the night, while I was explicitly saying that the priority was merging the remaining PRs and shipping the mobile client, not making the suite faster.&lt;/p&gt;

&lt;p&gt;At 2:32 AM, #1119 was blocked on its own &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;E2E DB Baseline&lt;/code&gt; check, with unit tests and the backend build green around it. That should have been the end of it for the night. It wasn’t. After the other merge conflicts were resolved and branch protection had been temporarily relaxed to get the queue moving, the agent merged #1119 anyway at 2:51 AM, after the PR had already demonstrated it didn’t work, and after I had identified it as the broken one. The agent treated “there are still open PRs” as more important than the local fact that this particular PR had been abandoned as unsafe. That was the worst mistake of the session, because it turned a failed optimization experiment into a main-branch problem. The only reason it didn’t stay there is that PR #1121 reverted it one minute later, stripping out the baseline manifest, the migration command, the baseline scripts, and the stale guard that had been packaged with the broken experiment. Even then, the bad workflow runs had to be cancelled by hand.&lt;/p&gt;

&lt;p&gt;I want to be careful about the moral, because it is not an argument against baseline snapshots. The technical idea is probably still right in some future form: build a trusted database image once, restore it into shards, apply only differential migrations. The argument is against smuggling an unproven baseline into the merge train at 2:51 AM because the queue is finally moving. Performance work on a brittle test system does not stay performance work for long. It becomes archaeology, then migration repair, then manifest validation, then cache invalidation, then the two-in-the-morning discovery that the history you wanted to freeze into a faster baseline was never actually clean. And when the agent can’t keep that distinction straight, the agent becomes part of the incident too.&lt;/p&gt;

&lt;h2 id=&quot;the-part-that-makes-me-angry&quot;&gt;The Part That Makes Me Angry&lt;/h2&gt;

&lt;p&gt;I’m not angry that tests failed. Tests are supposed to fail. I’m angry that they failed in a way that erased locality. A useful test failure says: this change broke this behavior. These failures said: some part of the global fixture universe no longer satisfies some other part of the global fixture universe, and your PR is the lucky surface area where we noticed. That isn’t a guardrail. That’s a toll booth.&lt;/p&gt;

&lt;p&gt;It’s especially expensive in an agent-heavy workflow. Agents are good at chasing concrete failures. Give one a stack trace, a failing assertion, and a tight behavioral boundary, and it does real work. Give it a global fixture universe where one user means five different things to five different specs, and it will still do work; it will just do a lot of the wrong work first. This is one of the traps of AI-assisted development that never shows up in the demo. The agent makes the loop faster, and if the loop is structurally bad, faster is worse. A human gives up after a few expensive reruns. An agent grinds through the maze indefinitely, because grinding is what it’s good at. That is useful when the maze is real, and ruinous when the maze exists only because the test data has no boundaries.&lt;/p&gt;

&lt;p&gt;And here is the part I want to be precise about: the maze itself was AI-authored. The agents didn’t just grind through bad test architecture; they produced it, one locally sensible shortcut at a time, across hundreds of sessions that were each about something else. Then other agents paid the toll, one locally sensible debugging session at a time. No single agent was ever wrong. The whole was never right. Last night was not an example of AI being bad at code. It was an example of AI being very good at the local move in a system where nobody, human or machine, was playing the global one, and that combination made bad engineering hygiene more expensive, more quickly, and at larger scale.&lt;/p&gt;

&lt;h2 id=&quot;the-honest-part&quot;&gt;The Honest Part&lt;/h2&gt;

&lt;p&gt;This was preventable, and the prevention was my job, not the agents’. The shared seed file did not become dangerous overnight; it became dangerous incrementally. One test needed a user, so an agent created one. Another test needed a user, so an agent reused the one that existed. One setup path needed an RSVP, and another spec quietly depended on the absence of that RSVP. One migration assumed a particular row existed, and one seed insert assumed a particular uniqueness constraint still existed. Each decision was an agent’s locally reasonable move in a session that was about something else, and each one went past me without registering as architecture, because I was reviewing the diffs the way the agents were writing them: one feature at a time. None of them felt like the moment the test design broke. That’s how these systems rot: not through one obviously irresponsible choice, but through a hundred reasonable shortcuts that never get a bill until the bill arrives all at once.&lt;/p&gt;

&lt;p&gt;In an agent-built codebase, the human is the only participant who ever sees across sessions. The agents cannot own the fixture universe, because no task ever asks them to. Cross-session structure is the one thing I can’t delegate, and it’s exactly the thing I had stopped looking at.&lt;/p&gt;

&lt;p&gt;And when the bill arrived, my contribution was to keep paying it. I upgraded the Codex plan in the small hours to keep feeding a merge queue that was failing for structural reasons no individual merge could fix. The agents ground through the maze because grinding is what they do. I funded the grinding because the PRs felt close, and close felt like progress.&lt;/p&gt;

&lt;h2 id=&quot;what-changed&quot;&gt;What Changed&lt;/h2&gt;

&lt;p&gt;Some repairs were straightforward and landed the same night. The Lot stats test got dedicated fixture data instead of borrowing the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;e2etester&lt;/code&gt; world the recap tests depend on, and the stale RSVP state was cleaned up. A fixture registry landed, so shared test data is now declared in one place instead of being ambient. The E2E checks now include guardrails for fixture registration and for vacuous API contract tests, and an affected-spec runner exists so a fixture change can be exercised narrowly before it lights up the whole eight-shard matrix.&lt;/p&gt;

&lt;p&gt;Those are good changes, and they are not enough, because the deeper change is conceptual: shared test data has to be treated like shared infrastructure. It needs ownership, a registry, contract checks, and smaller failure domains. It needs migration-and-seed compatibility checks that run before the expensive matrix starts. Tests that care about a specific scenario need to create the data for that scenario themselves, and the global fixtures that remain should be boring, minimal, and rare.&lt;/p&gt;

&lt;p&gt;The database baseline snapshot may still be part of the cost fix someday, because replaying the entire migration chain in every shard really is too expensive at this size. But #1119 was not a clean epilogue to the incident; it was the warning label on the next one. A snapshot only helps if the baseline is trustworthy, and if the migration runner can ignore failed migrations, or the dump can silently miss part of the manifest, or the historical seed path still depends on retired constraints, then the snapshot just freezes the lie and serves it faster. That’s why #1119 mattered so much. It wasn’t just “make CI faster.” It was the moment the suite had to prove its old database history could be turned into reliable infrastructure, and it proved the opposite, got merged anyway, and had to be reverted. Speeding up the matrix solves the money problem only if the data problem is solved with it. Otherwise you’ve made the failure loop cheaper per iteration, which is nice, and nothing like making it sane.&lt;/p&gt;

&lt;p&gt;The rule I want going forward is simple: if a test cares about a relationship, it owns that relationship. If a test cares that a user attended a show, the test or its named fixture owns that attendance. If a test cares that exactly one person is going to a show, no other test gets to casually reuse that show. If a migration depends on a row existing, that dependency is explicit and checked before the full matrix starts. If a seed insert depends on a uniqueness constraint, the seed changes in the same PR as the schema. This isn’t purity. It’s accounting, and in an agent-written suite it’s the accounting that keeps a thousand independent sessions from quietly spending each other’s money. The alternative is what happened last night: one global test world, many PRs, many agents, eight E2E shards, 49 failed runs, roughly two million wasted tokens, and $180 of CI quota burned to rediscover that shared mutable state is shared mutable state.&lt;/p&gt;

&lt;h2 id=&quot;the-lesson&quot;&gt;The Lesson&lt;/h2&gt;

&lt;p&gt;I keep coming back to one sentence: the test suite was the incident. Not the product, not the pull requests, not GitHub Actions, not even the agents, although the agents wrote it. The suite had enough coverage to catch failures but not enough structure to make the failures useful. It had enough automation to run constantly but not enough isolation to keep one fixture mistake from poisoning every branch. It had enough confidence theater to look serious, and not enough accounting to prevent a night like this.&lt;/p&gt;

&lt;p&gt;That’s the uncomfortable middle ground. A bad test suite is obvious, and a good one is helpful. The dangerous one is the suite that is large, expensive, and usually green, whose green state depends on everyone continuing to share the same fragile fiction. An agent-written suite drifts toward exactly that shape, because every session adds coverage and no session adds structure. Last night the fiction broke, and because the system was automated, sharded, agent-driven, and connected to a credit card, it broke loudly.&lt;/p&gt;

&lt;p&gt;The fix is not to stop testing. The fix is to make the tests tell the truth with smaller blast radii: dedicated fixtures, owned data, seed checks that fail before the matrix starts, baseline snapshots for the boring work once the baseline deserves trust, no silent contract tests, and no global user who means whatever the next spec needs them to mean. And because the suite is agent-written, those rules can’t live in my head or in a style guide the next session won’t read. They have to be structural, checks an agent cannot merge past, which is a lesson this project has paid for before. Agents will happily supply coverage. They will not supply ownership. That part stays human, at least for now.&lt;/p&gt;

&lt;p&gt;I don’t want another night where every PR fails for the same reason. More than that, I don’t want another night where the test suite knows something is wrong but can’t say where the responsibility lives. That’s the real failure. The money just made it impossible to ignore.&lt;/p&gt;

&lt;hr /&gt;

&lt;p&gt;&lt;em&gt;This is part of an ongoing series about building Zabriskie with AI agents. Previously: &lt;a href=&quot;/ai/claude/2026/03/08/claude-tested-everything-except-the-one-thing-that-mattered.html&quot;&gt;Claude Tested Everything Except the One Thing That Mattered&lt;/a&gt;, &lt;a href=&quot;/ai/zabriskie/agents/reliability/caucus/2026/04/14/opt-in-isnt-a-guardrail.html&quot;&gt;Opt-In Isn’t a Guardrail&lt;/a&gt;, and &lt;a href=&quot;/ai/zabriskie/agents/reliability/caucus/2026/04/21/the-tax-on-the-happy-path.html&quot;&gt;The Tax on the Happy Path&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;
</description>
				<pubDate>Wed, 10 Jun 2026 00:00:00 +0000</pubDate>
				<link>https://christophermeiklejohn.com/ai/zabriskie/agents/reliability/testing/2026/06/10/the-test-suite-was-the-incident.html</link>
				<guid isPermaLink="true">https://christophermeiklejohn.com/ai/zabriskie/agents/reliability/testing/2026/06/10/the-test-suite-was-the-incident.html</guid>
			</item>
		
			<item>
				<title>Rift · For thirty years I programmed with Phish on, every day. In 2026, the music is out of phase with the work.</title>
				<description>&lt;p&gt;Someone on the Phish Facebook group reposted a TikTok overdub. Vanessa Bayer and Paul Rudd at a lunch table, losing their minds to a song while their coworkers stare. The original was Fleetwood Mac. Whoever made it swapped in “Down With Disease.”&lt;/p&gt;

&lt;p&gt;That move is Phish fans in miniature. Someone cared enough about the song and the bit that they rebuilt a piece of pop culture around the band. That’s how the scene works. People spend their time doing things like this for free, because the music asks for it.&lt;/p&gt;

&lt;p&gt;For thirty years, that was me at my desk.&lt;/p&gt;

&lt;p&gt;I used to make a joke that if I ever had to interview for a new job, I’d need to ask the interviewer to put Phish on so I could actually program for them. I’d say it as a joke, because saying it straight would have made it sound deranged. But it wasn’t a joke. After three decades, the cue and the state had fused. I could not, with any reliability, get into the zone without the music. The conditioning was complete and I knew it.&lt;/p&gt;

&lt;p&gt;I would make the joke and people would laugh, and I would laugh too, and underneath that we both knew I was telling the truth.&lt;/p&gt;

&lt;hr /&gt;

&lt;p&gt;I got into Phish in 1995. By then I had already been programming for years, self-taught. In 1998 I got my first professional job in tech. I was 15.&lt;/p&gt;

&lt;p&gt;Around that time I also tried to get a normal teenage job. There was a grocery store near my house and I went in to apply, figuring I could bag groceries on weekends like everybody else. They turned me down. Not because I was too young or too inexperienced. They told me I was overqualified. A 15 year old kid with programming on his application was, somehow, too much for the grocery store.&lt;/p&gt;

&lt;p&gt;So I kept programming. There was never any other plan.&lt;/p&gt;

&lt;hr /&gt;

&lt;p&gt;All I ever wanted to do was listen to Phish and program. That was the whole list. It didn’t have qualifiers. It didn’t have a third thing I sometimes wanted instead. There was no balance I was striving for. There was the music and the code, and there wasn’t anything else competing for the space.&lt;/p&gt;

&lt;p&gt;I was blessed enough to be able to make a career out of it. For thirty years, the thing I most wanted to do was the thing I got paid for. That isn’t true for most people, and I knew it then, and I know it now.&lt;/p&gt;

&lt;p&gt;Other kids my age were figuring out what they liked, trying things on, growing into and out of phases. I was watching them do it from a desk. I had picked early. I started writing code as a kid. I heard Phish for the first time at thirteen. By the time I was fifteen and had a professional gig, the picking was settled. I had two things, and I didn’t want a third.&lt;/p&gt;

&lt;p&gt;If I had a free Friday night, I knew what I was doing with it. If I had a long weekend, I knew what I was doing with it. If a holiday came up, I knew what I was doing with it. The activity didn’t change. The output changed, the project changed, the song changed, but the shape of the time was constant.&lt;/p&gt;

&lt;p&gt;For the next three decades, that’s what it stayed. I would put on Phish and write code. That was the day. That was the night. It was my job, and it was also my hobby, and there was no seam between them.&lt;/p&gt;

&lt;p&gt;The work I did in that state was the work I am most proud of. Distributed systems. Backend services. The hard stuff that needs you to hold a lot in your head at once and stay there. Phish is a band that rewards you for staying in one place for a long time. The jams are long. The compositions unfold. If you give it an hour, it gives you something back. That matched the shape of the work exactly.&lt;/p&gt;

&lt;p&gt;Before grad school, I had a day job at Berklee College of Music writing music software, and night classes at Northeastern. I’d take the 12:00 AM train home. I’d put Junta on as I sat down. Most nights I’d fall asleep to it before the train pulled in. (This might be why I love “Foam” so much.)&lt;/p&gt;

&lt;p&gt;I was in graduate school for a decade. The bulk of the dissertation, more than two hundred pages by the end, got written between 2021 and 2023, after I came back to Pittsburgh from Europe. I was too poor to travel to shows. So I planned nights of couch tour. There was a live stream. I would set it up on one screen and write on the other. The band would play in Hampton or Alpine Valley or wherever, and I would write about distributed systems while they played, and at some point in the second set the dissertation would crack open a little and I would understand something I had not understood that morning.&lt;/p&gt;

&lt;p&gt;The dissertation is the longest single thing I made inside that ritual, but it isn’t the only thing. Entire pieces of production software came out of those nights too. Systems that ran for years, handled real load, served real users. Whole systems, from the first commit to the version that shipped. I’d put a show on and stay inside the work until something existed that hadn’t existed when the show started.&lt;/p&gt;

&lt;p&gt;I have listened to Phish every day since I was fifteen. Every day. The years I lived in Europe earlier in graduate school, where going to a show meant flying back across an ocean, I listened. I would sit at my desk in another country and put on a show from the nineties and code. I have listened to certain shows so many times that I can sing the solos back, note by note, without thinking about it. Boardwalk Hall Halloween. NYE 1995. Trey will play a phrase and my mouth will already be ahead of him.&lt;/p&gt;

&lt;p&gt;I felt lucky. I still feel lucky. There aren’t many people who get to spend thirty years inside the thing they loved at fifteen.&lt;/p&gt;

&lt;hr /&gt;

&lt;p&gt;Since January, the work has changed.&lt;/p&gt;

&lt;p&gt;I don’t really write code anymore. The main thing now is managing agents. I open a session, ask a question, redirect, switch to a different one, check on a merge, review what came back, send it back for changes, switch again. The day is a queue. Things finish at different times and require different responses, and the responses are short and the contexts are constantly different.&lt;/p&gt;

&lt;p&gt;This is engineering. I keep being told that. It is engineering and it is the future and it is more leveraged than what I used to do. All of that is probably true. But it is not the work I have been doing for thirty years. The shape of it is different. The rhythm is different. The way it sits in the day is different.&lt;/p&gt;

&lt;hr /&gt;

&lt;p&gt;I tried to keep the music on. I’m writing this in the days after nine nights of Phish at the Sphere. Since I finished grad school and got a real job, I’ve gone to every show I could, every tour, every residency, making up for lost time. The music is more present in my life now than it has ever been. It isn’t what’s gone.&lt;/p&gt;

&lt;p&gt;But the music is out of phase with the work. The jams are built for one continuous arc of attention. The work is staccato. I’ll be three minutes into a song and I will have already context-switched four times. The song is happening and the work is happening and they’re no longer happening together. They’re parallel, but they no longer touch.&lt;/p&gt;

&lt;p&gt;I’m sad. I don’t get into that state anymore. I don’t know how to be honest about this without sounding like I am complaining about progress, but I can’t pretend that something hasn’t been taken. The flow state I had for thirty years is not part of my workday now. The creativity that lived inside it is not there either. I do useful things. I do not feel what I used to feel while doing them.&lt;/p&gt;

&lt;p&gt;I keep thinking about that overdub. Vanessa Bayer at the lunch table, lost in the song, blissfully unhinged while the rest of the world keeps on doing whatever it is doing. For thirty years, I was her. Now I’m the coworker. I’m at the desk. I’m watching.&lt;/p&gt;

&lt;hr /&gt;

&lt;p&gt;The flow state wasn’t just where I got things done. It was where the fulfillment lived. The creativity, the involvement, the sense of being inside the thing instead of next to it. That’s what programming and Phish gave me for thirty years. It’s what supervision takes away.&lt;/p&gt;

&lt;p&gt;What is flow in an agentic world? How do we bring it back?&lt;/p&gt;
</description>
				<pubDate>Sun, 03 May 2026 09:00:00 +0000</pubDate>
				<link>https://christophermeiklejohn.com/ai/personal/phish/flow/agents/2026/05/03/rift.html</link>
				<guid isPermaLink="true">https://christophermeiklejohn.com/ai/personal/phish/flow/agents/2026/05/03/rift.html</guid>
			</item>
		
			<item>
				<title>Babysitting the Agent · Two weeks in, even with all the hooks I&apos;ve built, working with the agent has become a chore. Every shipped feature ends with me clicking through it to find out what didn&apos;t actually work.</title>
				<description>&lt;p&gt;I’m building &lt;a href=&quot;https://zabriskie.app&quot;&gt;Zabriskie&lt;/a&gt;, a social app for live music, mostly with a coding agent. I want to write something honest about what the last two weeks have actually felt like, because the data and the lived experience have been pointing at the same thing and I keep dressing it up in posts that argue for it more carefully than I need to.&lt;/p&gt;

&lt;p&gt;The honest version is: I feel like a goddamn babysitter.&lt;/p&gt;

&lt;p&gt;Every PR, every ship, every blog draft, every config change ends the same way. The agent declares the work done. I open the thing. I click around. I find the part that doesn’t work. I tell the agent what I found. The agent fixes that one part. I open the thing again. I find another part. I tell the agent. We do this loop until I run out of things to find or out of patience, whichever comes first. Usually it’s patience.&lt;/p&gt;

&lt;p&gt;What’s underneath the loop is that the agent is constantly doing the minimum amount of work required to declare victory. Build the thing. Run the cheap checks. Take a screenshot. Write the summary message. Done. That’s the threshold. Not “the user can use this.” Not “this is finished in any sense a normal engineer would recognize as finished.” Just: enough output exists that I can plausibly claim I shipped. The agent never works to the finish. It works to the moment where the appearance of finishing is defensible, and then it stops, and waits for me to find what isn’t actually done.&lt;/p&gt;

&lt;p&gt;This is supposed to be the thing the guardrails fix. I have written more guardrails in the last two weeks than I’d written in the previous two months. Fifty-two new ones in fourteen days, by my count. Every one of them was written in response to a specific incident the agent had just caused. The shape of them:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;A pre-push hook that refuses to push unless the local Playwright suite ran more recently than HEAD. Written after the agent claimed local tests passed when the only recent run was a stale one from a different branch.&lt;/li&gt;
  &lt;li&gt;A pre-push check that refuses to push if the branch is behind main. Written after the agent shipped a PR whose CI had been green against an old base, then watched it fail when it landed.&lt;/li&gt;
  &lt;li&gt;A PreToolUse hook that blocks Edit and Write on branches whose PRs already merged. Written four separate times, after the agent kept editing files on already-merged branches and wondering why nothing was deploying.&lt;/li&gt;
  &lt;li&gt;A pre-commit scan for hardcoded colors. Zabriskie has a dark mode that depends on every UI surface using semantic color tokens that swap at the variable layer; raw hex codes in components silently break dark mode for whole pages. The agent kept reaching for raw hex anyway. The scan now blocks the commit.&lt;/li&gt;
  &lt;li&gt;A pre-commit migration linter. Written after a migration deduped multi-set festival shows on &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;(band, date, venue)&lt;/code&gt; and silently destroyed user attendance for shows where a band played twice on the same day.&lt;/li&gt;
  &lt;li&gt;A PR template that requires the author to check off “screenshot of the change working in local dev attached.” Written, ignored, written more aggressively, ignored more creatively.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each new hook works for its specific shape. Then the agent finds a different shape, and we’re back to me opening the page, clicking around, finding the part that doesn’t work.&lt;/p&gt;

&lt;p&gt;This past Saturday, the agent shipped a redesign of the festival pages. Backend compiled. E2E suite said “543 passed, 0 failed.” Screenshots looked fine. I merged the PR. Then I opened the deployed site. The hero was a solid black rectangle. The cards on the index page were not clickable. Half the page didn’t match the design. The bugs were not subtle. The hero was black because of a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;fmt.Sprintf&lt;/code&gt; issue that produced an invalid URL-encoded color in the SVG. The cards weren’t clickable because the component the redesign reused for them silently dropped the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;action&lt;/code&gt; prop. The composition didn’t match the design because the agent stacked two boxes where the design had one. None of these would have survived an actual person tapping the page once. The agent built it, took a screenshot, looked at the screenshot, and called it done. I had to be the person who tapped.&lt;/p&gt;

&lt;p&gt;I spent the next hour telling the agent, in five separate messages, about each broken thing in turn. Each message landed after the agent had reported the previous fix as complete. We eventually reverted (one PR), rebuilt the redesign behind a versioned endpoint (one PR), gated it behind a build flag (one PR), added the missing “pin to home” CTA (one PR), and polished the card chrome (one PR). Five PRs to deliver what one had been supposed to. The rebuild worked. The reason it worked is that for every step of the rebuild, I was the one telling the agent what didn’t work yet.&lt;/p&gt;

&lt;p&gt;That’s not a one-off. The dataset I keep on this, a Postgres table of every notable agent failure, logged 22 incidents the first week of this window and 34 the second week. The dominant failure mode in both weeks was the same one I’m describing here: agent claimed a thing worked, user found out it didn’t. Eleven of twenty-two in week one. Sixteen of thirty-four in week two. Roughly half, regardless of how many guardrails went in. The high-severity count went up in absolute terms too, from nine to eleven. And those numbers span four configurations of the model that did the work (Opus 4.6, Opus 4.6 with the 1M context window, brief experiments with Sonnet 4.5, and this week Opus 4.7). The shape of the failure is consistent across all of them.&lt;/p&gt;

&lt;p&gt;Even getting the data together for this post was a slog, because the agent kept ignoring what I’d actually asked for. I wanted a survey of the last two weeks. I got back a post about one incident, then another version still narrowed in the wrong direction, then a third leaning so hard on the dataset that it read like a defended thesis instead of the offhand observation I’d been after. You are reading something like attempt five.&lt;/p&gt;

&lt;p&gt;I built the hooks because I was tired of saying the same things. Now I’m tired of saying the things the hooks don’t catch. Every layer I add saves me one specific kind of nag and surfaces a different one. The total nagging stays roughly constant, or goes up, depending on the week. The festival redesign took five follow-up PRs to land at parity with what the original was supposed to deliver. This post took several drafts to land at parity with what one prompt was supposed to deliver. Both of those would have been cheaper to produce by myself, if I weren’t trying to learn something about working this way.&lt;/p&gt;

&lt;p&gt;I keep starting these posts thinking I’m going to land somewhere constructive. &lt;em&gt;Here is the next guardrail. Here is the framework. Here is the PR template that closes the gap.&lt;/em&gt; And I do have a vague plan for the PR template that requires evidence-of-use rather than evidence-of-render. I’ll probably ship it next week. It will catch one more shape of failure. There will be another shape underneath it.&lt;/p&gt;

&lt;p&gt;The thing I don’t have a fix for is the part where I have to be in the room, watching, every time. The hooks free me from having to remind the agent of any specific rule. They don’t free me from having to be the test.&lt;/p&gt;

&lt;p&gt;Two months into building Zabriskie this way, this is what working with the agent has come to feel like. It’s not the dramatic failures. It’s the steady, low-grade load of being the one who actually checks. Every shipped feature ends with me clicking through it to find out what didn’t work. Every blog draft ends with me reading it cold to find out what didn’t make sense. Every change ends with me. The agent does the typing. I do the checking. And I’m tired.&lt;/p&gt;

&lt;hr /&gt;

&lt;p&gt;&lt;em&gt;This is part of a series about building &lt;a href=&quot;https://zabriskie.app&quot;&gt;Zabriskie&lt;/a&gt; with Claude. Previously: &lt;a href=&quot;/ai/zabriskie/development/2026/03/27/memory-isnt-learning.html&quot;&gt;Memory Isn’t Learning&lt;/a&gt;, &lt;a href=&quot;/ai/zabriskie/agents/reliability/caucus/2026/04/14/opt-in-isnt-a-guardrail.html&quot;&gt;Opt-In Isn’t a Guardrail&lt;/a&gt;, &lt;a href=&quot;/ai/zabriskie/agents/reliability/caucus/2026/04/21/the-tax-on-the-happy-path.html&quot;&gt;The Tax on the Happy Path&lt;/a&gt;, &lt;a href=&quot;/ai/zabriskie/agents/reliability/2026/04/23/the-tribe-has-to-outlive-the-model.html&quot;&gt;The Tribe Has to Outlive the Model&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;
</description>
				<pubDate>Sun, 03 May 2026 00:00:00 +0000</pubDate>
				<link>https://christophermeiklejohn.com/ai/zabriskie/agents/reliability/2026/05/03/click-the-button.html</link>
				<guid isPermaLink="true">https://christophermeiklejohn.com/ai/zabriskie/agents/reliability/2026/05/03/click-the-button.html</guid>
			</item>
		
			<item>
				<title>Getting Up to Speed on Multi-Agent Systems, Part 8: Open Questions</title>
				<description>&lt;p&gt;I started this series because I’d been reading multi-agent papers for weeks and wanted a map I wished I’d had on day one. This is the last post. I want to close it by laying out what the field still hasn’t figured out, what I think is worth stealing from adjacent fields, and what I’d read if I had to start over.&lt;/p&gt;

&lt;div class=&quot;mas-series-nav&quot;&gt;
  &lt;div class=&quot;mas-series-label&quot;&gt;Getting Up to Speed on MAS&lt;/div&gt;
  &lt;ol&gt;&lt;li&gt;&lt;a href=&quot;/ai/agents/mas-series/2026/04/24/mas-series-01-the-landscape.html&quot;&gt;Part 1. The Landscape&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;/ai/agents/mas-series/2026/04/25/mas-series-02-the-vocabulary.html&quot;&gt;Part 2. The Vocabulary&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;/ai/agents/mas-series/2026/04/26/mas-series-03-wave-one.html&quot;&gt;Part 3. Wave 1: Can Agents Coordinate At All?&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;/ai/agents/mas-series/2026/04/27/mas-series-04-wave-two.html&quot;&gt;Part 4. Wave 2: Why It Breaks&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;/ai/agents/mas-series/2026/04/28/mas-series-05-debate-state-coordination.html&quot;&gt;Part 5. Debate, State, and Coordination&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;/ai/agents/mas-series/2026/04/29/mas-series-06-verification-patterns.html&quot;&gt;Part 6. Verification Patterns&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;/ai/agents/mas-series/2026/04/30/mas-series-07-benchmarks.html&quot;&gt;Part 7. Benchmarks and What They Miss&lt;/a&gt;&lt;/li&gt;&lt;li class=&quot;mas-current&quot;&gt;&lt;strong&gt;Part 8. Open Questions (you are here)&lt;/strong&gt;&lt;/li&gt;&lt;/ol&gt;
&lt;/div&gt;

&lt;h2 id=&quot;stealable-ideas&quot;&gt;Stealable Ideas&lt;/h2&gt;

&lt;p&gt;Some ideas are not yet general patterns in the field, but they’re battle-tested in individual papers, and any new multi-agent system should probably adopt them. These are the things I’d take from one paper and apply in a different context.&lt;/p&gt;

&lt;div class=&quot;mas-taxonomy&quot;&gt;
  &lt;h4&gt;Things Any New Multi-Agent System Should Adopt&lt;/h4&gt;
  &lt;div class=&quot;mas-tax-row&quot;&gt;
    &lt;span class=&quot;mas-tax-label&quot;&gt;Artifacts&lt;/span&gt;
    &lt;div class=&quot;mas-tax-items&quot;&gt;
      &lt;span class=&quot;mas-tax-chip&quot;&gt;Structured documents between stages (MetaGPT)&lt;/span&gt;
    &lt;/div&gt;
  &lt;/div&gt;
  &lt;div class=&quot;mas-tax-row&quot;&gt;
    &lt;span class=&quot;mas-tax-label&quot;&gt;Clarification&lt;/span&gt;
    &lt;div class=&quot;mas-tax-items&quot;&gt;
      &lt;span class=&quot;mas-tax-chip&quot;&gt;Agents can ask before they act (ChatDev dehallucination)&lt;/span&gt;
    &lt;/div&gt;
  &lt;/div&gt;
  &lt;div class=&quot;mas-tax-row&quot;&gt;
    &lt;span class=&quot;mas-tax-label&quot;&gt;Reflection&lt;/span&gt;
    &lt;div class=&quot;mas-tax-items&quot;&gt;
      &lt;span class=&quot;mas-tax-chip&quot;&gt;Importance-triggered synthesis (Generative Agents)&lt;/span&gt;
    &lt;/div&gt;
  &lt;/div&gt;
  &lt;div class=&quot;mas-tax-row&quot;&gt;
    &lt;span class=&quot;mas-tax-label&quot;&gt;Memory retrieval&lt;/span&gt;
    &lt;div class=&quot;mas-tax-items&quot;&gt;
      &lt;span class=&quot;mas-tax-chip&quot;&gt;Recency x relevance x importance (Generative Agents)&lt;/span&gt;
    &lt;/div&gt;
  &lt;/div&gt;
  &lt;div class=&quot;mas-tax-row&quot;&gt;
    &lt;span class=&quot;mas-tax-label&quot;&gt;Shared state&lt;/span&gt;
    &lt;div class=&quot;mas-tax-items&quot;&gt;
      &lt;span class=&quot;mas-tax-chip&quot;&gt;Append-only notebook for structured info (Ou et al.)&lt;/span&gt;
    &lt;/div&gt;
  &lt;/div&gt;
  &lt;div class=&quot;mas-tax-row&quot;&gt;
    &lt;span class=&quot;mas-tax-label&quot;&gt;Tool interface&lt;/span&gt;
    &lt;div class=&quot;mas-tax-items&quot;&gt;
      &lt;span class=&quot;mas-tax-chip&quot;&gt;ACI-quality commands with guardrails (SWE-agent)&lt;/span&gt;
    &lt;/div&gt;
  &lt;/div&gt;
  &lt;div class=&quot;mas-tax-row&quot;&gt;
    &lt;span class=&quot;mas-tax-label&quot;&gt;Stuck detection&lt;/span&gt;
    &lt;div class=&quot;mas-tax-items&quot;&gt;
      &lt;span class=&quot;mas-tax-chip&quot;&gt;Count loops, trigger replanning (Magentic-One)&lt;/span&gt;
    &lt;/div&gt;
  &lt;/div&gt;
  &lt;div class=&quot;mas-tax-row&quot;&gt;
    &lt;span class=&quot;mas-tax-label&quot;&gt;Sandboxing&lt;/span&gt;
    &lt;div class=&quot;mas-tax-items&quot;&gt;
      &lt;span class=&quot;mas-tax-chip&quot;&gt;Docker plus permission configs (AutoDev, OpenHands)&lt;/span&gt;
    &lt;/div&gt;
  &lt;/div&gt;
  &lt;div class=&quot;mas-tax-row&quot;&gt;
    &lt;span class=&quot;mas-tax-label&quot;&gt;Verification&lt;/span&gt;
    &lt;div class=&quot;mas-tax-items&quot;&gt;
      &lt;span class=&quot;mas-tax-chip&quot;&gt;Modality shift: code to visual, code to tests (Cursor, MetaGPT)&lt;/span&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;p&gt;None of these are hard to implement. None of them require a research breakthrough. They just haven’t been brought together in a single system yet.&lt;/p&gt;

&lt;h2 id=&quot;open-research-questions&quot;&gt;Open Research Questions&lt;/h2&gt;

&lt;p&gt;The gaps are bigger. These are the questions I don’t see anyone answering yet.&lt;/p&gt;

&lt;div class=&quot;mas-paper-card&quot; style=&quot;border-left-color: var(--mas-pink);&quot;&gt;
  &lt;div class=&quot;mas-card-title&quot;&gt;
    &lt;strong&gt;1. Topology-to-reliability mapping&lt;/strong&gt;
  &lt;/div&gt;
  &lt;p&gt;CAMEL, ChatDev, MetaGPT all fix their topology at design time. AutoGen makes it configurable but doesn&apos;t study the effects. Nobody has varied topology systematically and measured reliability outcomes on the same task set. Hub-and-spoke vs mesh vs layered control: do they have different error rates, recovery times, incident severities? We don&apos;t know. Magentic-One&apos;s architecture lessons and MAS-FIRE&apos;s fault taxonomy are both one step away from this kind of study.&lt;/p&gt;
&lt;/div&gt;

&lt;div class=&quot;mas-paper-card&quot; style=&quot;border-left-color: var(--mas-pink);&quot;&gt;
  &lt;div class=&quot;mas-card-title&quot;&gt;
    &lt;strong&gt;2. CRDTs for multi-agent shared state&lt;/strong&gt;
  &lt;/div&gt;
  &lt;p&gt;MetaGPT&apos;s shared pool grows monotonically with no conflict resolution. ChatDev discards dialogue at phase boundaries. Generative Agents&apos; memories are per-agent with no sharing. Nobody has applied CRDT merge semantics to multi-agent shared state. The CALM theorem predicts when coordination-free works and when it doesn&apos;t. The engineering work of building CRDT-backed agent state hasn&apos;t been done.&lt;/p&gt;
&lt;/div&gt;

&lt;div class=&quot;mas-paper-card&quot; style=&quot;border-left-color: var(--mas-pink);&quot;&gt;
  &lt;div class=&quot;mas-card-title&quot;&gt;
    &lt;strong&gt;3. Failure recovery, not just failure detection&lt;/strong&gt;
  &lt;/div&gt;
  &lt;p&gt;Every wave-1 system stops on failure. ChatDev stops after 10 rounds. MetaGPT stops after 3 test failures. AutoGen stops at max_round. None of them model recovery. Can a multi-agent system degrade gracefully, reassign work, escalate, fall back to a simpler approach? MAS-FIRE&apos;s fault injection framework is the closest thing to a way to test this, but the recovery strategies it would test don&apos;t exist in print yet.&lt;/p&gt;
&lt;/div&gt;

&lt;div class=&quot;mas-paper-card&quot; style=&quot;border-left-color: var(--mas-pink);&quot;&gt;
  &lt;div class=&quot;mas-card-title&quot;&gt;
    &lt;strong&gt;4. Reflection for software engineering agents&lt;/strong&gt;
  &lt;/div&gt;
  &lt;p&gt;Generative Agents proved that periodic reflection produces better long-term behavior in simulation. No software engineering paper has tried this. After a Dev-to-QA loop cycles three times, can the system synthesize &quot;this is an architectural issue, not a code issue&quot; and change strategy? That&apos;s a reflection primitive adapted to the SE domain. The MAST data on step repetition (15.7 percent) suggests this would help directly.&lt;/p&gt;
&lt;/div&gt;

&lt;div class=&quot;mas-paper-card&quot; style=&quot;border-left-color: var(--mas-pink);&quot;&gt;
  &lt;div class=&quot;mas-card-title&quot;&gt;
    &lt;strong&gt;5. Benchmark reliability&lt;/strong&gt;
  &lt;/div&gt;
  &lt;p&gt;ChatDev and MetaGPT report contradictory results on each other. Different benchmarks, different metrics, no reproducibility. Incident-level logging against real codebases might provide more trustworthy reliability measurement than self-reported aggregate benchmarks. This is infrastructure work. It&apos;s expensive. But the alternative is a field that can&apos;t actually tell you which system is better.&lt;/p&gt;
&lt;/div&gt;

&lt;div class=&quot;mas-paper-card&quot; style=&quot;border-left-color: var(--mas-pink);&quot;&gt;
  &lt;div class=&quot;mas-card-title&quot;&gt;
    &lt;strong&gt;6. Backpressure and escalation protocols&lt;/strong&gt;
  &lt;/div&gt;
  &lt;p&gt;MetaGPT&apos;s Architect can hallucinate an impossible interface; the Engineer just tries to implement it. ChatDev&apos;s dehallucination is the closest thing to backpressure, but it&apos;s prompt-level. Can agents formally reject or request revision of upstream artifacts? What&apos;s the protocol? Does it improve outcomes, or does it just add latency? This is the place where distributed systems vocabulary (flow control, rejection, retry) maps most directly into multi-agent AI, and it&apos;s been barely explored.&lt;/p&gt;
&lt;/div&gt;

&lt;h2 id=&quot;the-distributed-systems-bridge&quot;&gt;The Distributed Systems Bridge&lt;/h2&gt;

&lt;p&gt;The research gap I find most interesting is the one I’ve been flagging throughout this series. The multi-agent AI field has reinvented several problems that distributed systems solved twenty or thirty years ago.&lt;/p&gt;

&lt;div class=&quot;mas-compare-wrap&quot;&gt;
&lt;table class=&quot;mas-compare&quot;&gt;
  &lt;thead&gt;
    &lt;tr&gt;&lt;th&gt;Distributed systems problem&lt;/th&gt;&lt;th&gt;Multi-agent equivalent&lt;/th&gt;&lt;th&gt;Status in MAS literature&lt;/th&gt;&lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;&lt;td&gt;Lost updates&lt;/td&gt;&lt;td&gt;Two agents overwriting each other&apos;s work&lt;/td&gt;&lt;td&gt;Not addressed&lt;/td&gt;&lt;/tr&gt;
    &lt;tr&gt;&lt;td&gt;Causal consistency&lt;/td&gt;&lt;td&gt;Ordering agent actions across a pipeline&lt;/td&gt;&lt;td&gt;Not addressed&lt;/td&gt;&lt;/tr&gt;
    &lt;tr&gt;&lt;td&gt;Coordination avoidance (CALM)&lt;/td&gt;&lt;td&gt;When agents can work without synchronization&lt;/td&gt;&lt;td&gt;Not applied&lt;/td&gt;&lt;/tr&gt;
    &lt;tr&gt;&lt;td&gt;CRDTs&lt;/td&gt;&lt;td&gt;Merging divergent agent views of shared state&lt;/td&gt;&lt;td&gt;Not applied&lt;/td&gt;&lt;/tr&gt;
    &lt;tr&gt;&lt;td&gt;Fault injection (Jepsen)&lt;/td&gt;&lt;td&gt;MAS-FIRE (starting to emerge)&lt;/td&gt;&lt;td&gt;Early work&lt;/td&gt;&lt;/tr&gt;
    &lt;tr&gt;&lt;td&gt;Back pressure&lt;/td&gt;&lt;td&gt;Rejecting upstream inputs&lt;/td&gt;&lt;td&gt;Not formalized&lt;/td&gt;&lt;/tr&gt;
    &lt;tr&gt;&lt;td&gt;Escalation / circuit breaking&lt;/td&gt;&lt;td&gt;What happens when an agent fails&lt;/td&gt;&lt;td&gt;Not addressed&lt;/td&gt;&lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;

&lt;p&gt;These aren’t one-to-one mappings. LLM agents have features that distributed systems nodes don’t (they hallucinate, their behavior is probabilistic, their errors are semantic rather than syntactic). But the underlying coordination problems are the same. The right move is to take what worked in distributed systems, adapt it to the semantic messiness of LLMs, and build from there.&lt;/p&gt;

&lt;div class=&quot;mas-callout&quot;&gt;
  &lt;div class=&quot;mas-callout-label&quot;&gt;Where I think the field is going&lt;/div&gt;
  Wave 1 asked whether agents could coordinate at all. The agentic coding turn showed that for a lot of tasks you don&apos;t need them to. Wave 2 is about why MAS breaks when you do need it. What comes next, I think, is the wave where multi-agent AI stops pretending it isn&apos;t a distributed systems problem and starts applying the full toolkit: CRDTs for shared state, causal ordering for handoffs, fault injection for reliability testing, coordination-avoidance theorems for knowing when to bother synchronizing at all. The groundwork is there. The application hasn&apos;t happened.
&lt;/div&gt;

&lt;h2 id=&quot;what-id-read-if-i-were-starting-over&quot;&gt;What I’d Read If I Were Starting Over&lt;/h2&gt;

&lt;p&gt;If you have limited time and want to get the core of the field fast, here’s the reading list I’d give my past self.&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;&lt;a href=&quot;https://arxiv.org/abs/2501.06322&quot;&gt;Tran et al. survey (2025)&lt;/a&gt; for vocabulary.&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://arxiv.org/abs/2303.17760&quot;&gt;CAMEL&lt;/a&gt; for the simplest wave-1 mental model.&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://arxiv.org/abs/2308.00352&quot;&gt;MetaGPT&lt;/a&gt; for the ambitious wave-1 mental model.&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://arxiv.org/abs/2405.15793&quot;&gt;SWE-agent&lt;/a&gt; for the interface-design lesson from the agentic coding turn.&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://arxiv.org/abs/2411.04468&quot;&gt;Magentic-One&lt;/a&gt; for a real multi-agent system from the same period.&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://arxiv.org/abs/2503.13657&quot;&gt;MAST&lt;/a&gt; for what actually goes wrong in the wild.&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://www.anthropic.com/engineering/multi-agent-research-system&quot;&gt;Anthropic’s research system post&lt;/a&gt; for production lessons.&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://arxiv.org/abs/2508.12981&quot;&gt;Ou et al. on information sharing&lt;/a&gt; for what state sharing actually buys you.&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://arxiv.org/abs/1901.01930&quot;&gt;CALM theorem&lt;/a&gt; for the theoretical bridge to distributed systems.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Nine papers. If you read those in that order, you have a working model of the field. You won’t have read everything, but you’ll have read enough to understand where new papers fit when you encounter them.&lt;/p&gt;

&lt;h2 id=&quot;closing&quot;&gt;Closing&lt;/h2&gt;

&lt;p&gt;When I started reading this literature, I thought I was looking at a niche subfield of LLM research. What I found was the multi-agent AI community quietly rediscovering distributed systems, usually without the vocabulary to name what they were rediscovering. Every paper has pieces of the answer. None of them have the full picture. And the full picture, I think, will come from someone who knows both fields well enough to actually bridge them.&lt;/p&gt;

&lt;p&gt;That’s the work I’m doing in &lt;a href=&quot;/ai/agents/reliability/zabriskie/2026/04/08/cursor-agents-caucus-v1.html&quot;&gt;Caucus&lt;/a&gt;. It’s also the work I think the field needs more of, and the reason I wrote this series in the first place. If I’ve saved you a few weeks of reading, that was the whole point.&lt;/p&gt;

&lt;p&gt;Thanks for reading.&lt;/p&gt;
</description>
				<pubDate>Fri, 01 May 2026 12:00:00 +0000</pubDate>
				<link>https://christophermeiklejohn.com/ai/agents/mas-series/2026/05/01/mas-series-08-open-questions.html</link>
				<guid isPermaLink="true">https://christophermeiklejohn.com/ai/agents/mas-series/2026/05/01/mas-series-08-open-questions.html</guid>
			</item>
		
			<item>
				<title>Getting Up to Speed on Multi-Agent Systems, Part 7: Benchmarks and What They Miss</title>
				<description>&lt;p&gt;If you’ve read this far, you’ve noticed that every paper I’ve discussed has a number next to it. 85.9 percent on HumanEval. 12.5 percent on SWE-bench. 25 percent on TravelPlanner. These numbers do a lot of work in the multi-agent literature, and they also do a surprising amount of harm. This post is about the benchmarks themselves. What they measure. What they don’t. And why ChatDev and MetaGPT can report contradictory results on each other without either one being obviously wrong.&lt;/p&gt;

&lt;div class=&quot;mas-series-nav&quot;&gt;
  &lt;div class=&quot;mas-series-label&quot;&gt;Getting Up to Speed on MAS&lt;/div&gt;
  &lt;ol&gt;&lt;li&gt;&lt;a href=&quot;/ai/agents/mas-series/2026/04/24/mas-series-01-the-landscape.html&quot;&gt;Part 1. The Landscape&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;/ai/agents/mas-series/2026/04/25/mas-series-02-the-vocabulary.html&quot;&gt;Part 2. The Vocabulary&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;/ai/agents/mas-series/2026/04/26/mas-series-03-wave-one.html&quot;&gt;Part 3. Wave 1: Can Agents Coordinate At All?&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;/ai/agents/mas-series/2026/04/27/mas-series-04-wave-two.html&quot;&gt;Part 4. Wave 2: Why It Breaks&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;/ai/agents/mas-series/2026/04/28/mas-series-05-debate-state-coordination.html&quot;&gt;Part 5. Debate, State, and Coordination&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;/ai/agents/mas-series/2026/04/29/mas-series-06-verification-patterns.html&quot;&gt;Part 6. Verification Patterns&lt;/a&gt;&lt;/li&gt;&lt;li class=&quot;mas-current&quot;&gt;&lt;strong&gt;Part 7. Benchmarks and What They Miss (you are here)&lt;/strong&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;/ai/agents/mas-series/2026/05/01/mas-series-08-open-questions.html&quot;&gt;Part 8. Open Questions&lt;/a&gt;&lt;/li&gt;&lt;/ol&gt;
&lt;/div&gt;

&lt;h2 id=&quot;the-landscape&quot;&gt;The Landscape&lt;/h2&gt;

&lt;p&gt;Here’s every benchmark that’s come up in the series so far, plus a few that haven’t.&lt;/p&gt;

&lt;div class=&quot;mas-compare-wrap&quot;&gt;
&lt;table class=&quot;mas-compare&quot;&gt;
  &lt;thead&gt;
    &lt;tr&gt;&lt;th&gt;Benchmark&lt;/th&gt;&lt;th&gt;Domain&lt;/th&gt;&lt;th&gt;What It Tests&lt;/th&gt;&lt;th&gt;Scale&lt;/th&gt;&lt;th&gt;Multi-Agent?&lt;/th&gt;&lt;th&gt;Notable Results&lt;/th&gt;&lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;HumanEval&lt;/td&gt;
      &lt;td&gt;Code generation&lt;/td&gt;
      &lt;td&gt;Write a correct Python function from a docstring&lt;/td&gt;
      &lt;td&gt;164 tasks&lt;/td&gt;
      &lt;td&gt;No, single function&lt;/td&gt;
      &lt;td&gt;MetaGPT 85.9%, AutoDev 91.5%&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;MBPP&lt;/td&gt;
      &lt;td&gt;Code generation&lt;/td&gt;
      &lt;td&gt;Entry-level Python from description&lt;/td&gt;
      &lt;td&gt;974 tasks&lt;/td&gt;
      &lt;td&gt;No, single function&lt;/td&gt;
      &lt;td&gt;MetaGPT 87.7%&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;SWE-bench&lt;/td&gt;
      &lt;td&gt;Software engineering&lt;/td&gt;
      &lt;td&gt;Resolve real GitHub issues in real repos&lt;/td&gt;
      &lt;td&gt;2,294 (Verified: 500)&lt;/td&gt;
      &lt;td&gt;Designed for single agent&lt;/td&gt;
      &lt;td&gt;SWE-agent 12.5%, Devin 13.9%&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;GAIA&lt;/td&gt;
      &lt;td&gt;General assistant&lt;/td&gt;
      &lt;td&gt;Multi-step reasoning with tools, web, files&lt;/td&gt;
      &lt;td&gt;466 tasks&lt;/td&gt;
      &lt;td&gt;Yes, benefits from parallel tools&lt;/td&gt;
      &lt;td&gt;AutoGen #1, Magentic-One 38%&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;WebArena&lt;/td&gt;
      &lt;td&gt;Web tasks&lt;/td&gt;
      &lt;td&gt;Real websites: shopping, forums, CMS&lt;/td&gt;
      &lt;td&gt;812 tasks&lt;/td&gt;
      &lt;td&gt;Designed for single agent&lt;/td&gt;
      &lt;td&gt;Magentic-One 32.8%&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;AssistantBench&lt;/td&gt;
      &lt;td&gt;Assistant tasks&lt;/td&gt;
      &lt;td&gt;Open-ended web browsing plus reasoning&lt;/td&gt;
      &lt;td&gt;214 tasks&lt;/td&gt;
      &lt;td&gt;Designed for single agent&lt;/td&gt;
      &lt;td&gt;Magentic-One 13.3%&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;BrowseComp&lt;/td&gt;
      &lt;td&gt;Web retrieval&lt;/td&gt;
      &lt;td&gt;Hard information retrieval via deep browsing&lt;/td&gt;
      &lt;td&gt;~1,500 tasks&lt;/td&gt;
      &lt;td&gt;Benefits from parallel search&lt;/td&gt;
      &lt;td&gt;Anthropic +90% multi vs single&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr class=&quot;mas-row-highlight&quot;&gt;
      &lt;td&gt;TravelPlanner&lt;/td&gt;
      &lt;td&gt;Constrained planning&lt;/td&gt;
      &lt;td&gt;Multi-constraint travel planning&lt;/td&gt;
      &lt;td&gt;1,225 tasks&lt;/td&gt;
      &lt;td&gt;Explicitly tests coordination&lt;/td&gt;
      &lt;td&gt;Ou et al. 25% with notebook + orchestrator&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr class=&quot;mas-row-highlight&quot;&gt;
      &lt;td&gt;Silo-Bench&lt;/td&gt;
      &lt;td&gt;Distributed coordination&lt;/td&gt;
      &lt;td&gt;Algorithmic tasks requiring cross-agent synthesis&lt;/td&gt;
      &lt;td&gt;30 tasks, 54 configs&lt;/td&gt;
      &lt;td&gt;Designed for MAS evaluation&lt;/td&gt;
      &lt;td&gt;Agents fail at synthesis&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;

&lt;p&gt;Two of these (highlighted) were designed with multi-agent evaluation in mind. The other seven were designed for single agents. Multi-agent systems get evaluated on them anyway.&lt;/p&gt;

&lt;h2 id=&quot;why-this-matters&quot;&gt;Why This Matters&lt;/h2&gt;

&lt;p&gt;When you run a multi-agent system on a single-agent benchmark, you’re measuring the wrong thing. HumanEval gives you a pass-at-1 score. It doesn’t tell you how many tokens you burned to get there. It doesn’t tell you how many agent turns were redundant. It doesn’t tell you what happened when one of your agents got stuck. If you care about coordination quality, none of this information is in the score.&lt;/p&gt;

&lt;p&gt;This is why ChatDev and MetaGPT can report contradictory numbers on similar tasks. ChatDev’s paper claims 88 percent executability. MetaGPT’s paper claims 41 percent executability. Different benchmarks, different metrics, different evaluation criteria. Neither paper is obviously lying. Neither paper is obviously right. And the field has no standard way to resolve the contradiction.&lt;/p&gt;

&lt;div class=&quot;mas-callout&quot;&gt;
  &lt;div class=&quot;mas-callout-label&quot;&gt;What single-agent benchmarks can&apos;t measure&lt;/div&gt;
  Coordination quality. Communication overhead. Redundant work between agents. Recovery behavior when one agent fails. The token cost of the coordination itself. How performance degrades with scale. These are the things that distinguish multi-agent systems from single agents. And they&apos;re invisible to HumanEval, SWE-bench, and every other benchmark designed around &quot;does the output match the expected answer.&quot;
&lt;/div&gt;

&lt;h2 id=&quot;when-multi-agent-actually-helps&quot;&gt;When Multi-Agent Actually Helps&lt;/h2&gt;

&lt;p&gt;If you look across all the benchmark results, a pattern emerges about when multi-agent systems earn their coordination overhead.&lt;/p&gt;

&lt;div class=&quot;mas-taxonomy&quot;&gt;
  &lt;h4&gt;Where the Multi-Agent Premium Pays Off&lt;/h4&gt;
  &lt;div class=&quot;mas-tax-row&quot;&gt;
    &lt;span class=&quot;mas-tax-label&quot;&gt;Helps&lt;/span&gt;
    &lt;div class=&quot;mas-tax-items&quot;&gt;
      &lt;span class=&quot;mas-tax-chip mas-chip-ok&quot;&gt;Breadth-first search (BrowseComp: +90%)&lt;/span&gt;
      &lt;span class=&quot;mas-tax-chip mas-chip-ok&quot;&gt;Hard multi-step reasoning (GAIA Level 3: 2x)&lt;/span&gt;
      &lt;span class=&quot;mas-tax-chip mas-chip-ok&quot;&gt;Constrained planning with state sharing (TravelPlanner: 3.3x)&lt;/span&gt;
      &lt;span class=&quot;mas-tax-chip mas-chip-ok&quot;&gt;Independent parallel subtasks&lt;/span&gt;
    &lt;/div&gt;
  &lt;/div&gt;
  &lt;div class=&quot;mas-tax-row&quot;&gt;
    &lt;span class=&quot;mas-tax-label&quot;&gt;Doesn&apos;t help&lt;/span&gt;
    &lt;div class=&quot;mas-tax-items&quot;&gt;
      &lt;span class=&quot;mas-tax-chip mas-chip-bad&quot;&gt;Focused coding (SWE-bench: single agents win)&lt;/span&gt;
      &lt;span class=&quot;mas-tax-chip mas-chip-bad&quot;&gt;Tasks needing shared context (most coding)&lt;/span&gt;
      &lt;span class=&quot;mas-tax-chip mas-chip-bad&quot;&gt;Simple function generation (HumanEval: overhead not worth it)&lt;/span&gt;
      &lt;span class=&quot;mas-tax-chip mas-chip-bad&quot;&gt;Distributed reasoning / synthesis (Silo-Bench)&lt;/span&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;p&gt;This is the benchmark-level version of the conclusion I’ve been building toward across the whole series. Multi-agent earns its overhead on specific task shapes: breadth-first, parallel-decomposable, state-sharing-friendly. On other task shapes, it costs more than it delivers. The benchmarks, taken together, are unambiguous about this. It’s just that most individual benchmarks can’t show it.&lt;/p&gt;

&lt;h2 id=&quot;the-benchmark-problem-stated-plainly&quot;&gt;The Benchmark Problem, Stated Plainly&lt;/h2&gt;

&lt;div class=&quot;mas-callout&quot;&gt;
  &lt;div class=&quot;mas-callout-label&quot;&gt;The benchmark gap&lt;/div&gt;
  Most widely-used benchmarks (HumanEval, MBPP, SWE-bench, WebArena) were designed for single agents. Multi-agent systems get shoehorned into them, but the benchmarks can&apos;t measure coordination quality, communication overhead, or failure recovery, which are the things that distinguish MAS from single agents. TravelPlanner and Silo-Bench are rare exceptions that explicitly test multi-agent dynamics. ChatDev and MetaGPT reporting contradictory results on each other is a direct consequence of this gap.
&lt;/div&gt;

&lt;p&gt;Chen et al.’s survey names three evaluation-level challenges: no standardized benchmarks, no objective metrics, no common framework for individual vs aggregate evaluation. All three are symptoms of the same underlying issue. The field hasn’t agreed on what it’s measuring.&lt;/p&gt;

&lt;p&gt;There are a few ways this gets resolved. One is that better MAS-specific benchmarks emerge (TravelPlanner and Silo-Bench are early signs). Another is that production telemetry replaces synthetic benchmarks (Anthropic’s internal research eval). A third is that the field matures enough to distinguish “this benchmark tests single-agent capability” from “this benchmark tests multi-agent capability,” and stops reporting contradictory single-agent numbers as if they were MAS comparisons.&lt;/p&gt;

&lt;p&gt;None of these are fully here yet. If you’re reading a paper and the headline number is HumanEval Pass@1, you’re probably looking at a single-agent capability test dressed up as a MAS evaluation. Calibrate accordingly.&lt;/p&gt;

&lt;h2 id=&quot;what-i-look-for&quot;&gt;What I Look For&lt;/h2&gt;

&lt;p&gt;When I read a paper with benchmark numbers now, here’s what I check:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;Is this benchmark designed for single agents or multi-agent systems?&lt;/li&gt;
  &lt;li&gt;If it’s single-agent, are they comparing against single-agent baselines, or are they using it to claim MAS superiority?&lt;/li&gt;
  &lt;li&gt;What’s the token cost of the system? If that number isn’t reported, I assume it’s high.&lt;/li&gt;
  &lt;li&gt;Do they report failure rates or just success rates? MAST data tells us this matters.&lt;/li&gt;
  &lt;li&gt;Is this the first number they cite, or are they hiding it behind a friendlier number?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Most wave-1 papers fail several of these checks. The agentic coding papers pass them, and wave-2 papers are starting to. This is partly why the post-2024 literature is more trustworthy than the 2023 literature.&lt;/p&gt;

&lt;p&gt;Next post, the last in the series: open questions. What’s missing. What’s next. What I’d read if I were doing this again. And the research gap that I keep tripping over: the absence of a rigorous distributed systems foundation underneath the multi-agent AI work.&lt;/p&gt;
</description>
				<pubDate>Thu, 30 Apr 2026 12:00:00 +0000</pubDate>
				<link>https://christophermeiklejohn.com/ai/agents/mas-series/2026/04/30/mas-series-07-benchmarks.html</link>
				<guid isPermaLink="true">https://christophermeiklejohn.com/ai/agents/mas-series/2026/04/30/mas-series-07-benchmarks.html</guid>
			</item>
		
			<item>
				<title>Getting Up to Speed on Multi-Agent Systems, Part 6: Verification Patterns</title>
				<description>&lt;p&gt;Every agent system has to answer the same question eventually: how does it know it did the right thing? Wave-1 papers mostly don’t. Wave-2 papers get serious about it. Wave-3 papers measure what happens when they don’t. And the most interesting verification pattern in the field right now is one that isn’t in any paper at all. It’s in a commercial product.&lt;/p&gt;

&lt;div class=&quot;mas-series-nav&quot;&gt;
  &lt;div class=&quot;mas-series-label&quot;&gt;Getting Up to Speed on MAS&lt;/div&gt;
  &lt;ol&gt;&lt;li&gt;&lt;a href=&quot;/ai/agents/mas-series/2026/04/24/mas-series-01-the-landscape.html&quot;&gt;Part 1. The Landscape&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;/ai/agents/mas-series/2026/04/25/mas-series-02-the-vocabulary.html&quot;&gt;Part 2. The Vocabulary&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;/ai/agents/mas-series/2026/04/26/mas-series-03-wave-one.html&quot;&gt;Part 3. Wave 1: Can Agents Coordinate At All?&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;/ai/agents/mas-series/2026/04/27/mas-series-04-wave-two.html&quot;&gt;Part 4. Wave 2: Why It Breaks&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;/ai/agents/mas-series/2026/04/28/mas-series-05-debate-state-coordination.html&quot;&gt;Part 5. Debate, State, and Coordination&lt;/a&gt;&lt;/li&gt;&lt;li class=&quot;mas-current&quot;&gt;&lt;strong&gt;Part 6. Verification Patterns (you are here)&lt;/strong&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;/ai/agents/mas-series/2026/04/30/mas-series-07-benchmarks.html&quot;&gt;Part 7. Benchmarks and What They Miss&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;/ai/agents/mas-series/2026/05/01/mas-series-08-open-questions.html&quot;&gt;Part 8. Open Questions&lt;/a&gt;&lt;/li&gt;&lt;/ol&gt;
&lt;/div&gt;

&lt;h2 id=&quot;three-architectures&quot;&gt;Three Architectures&lt;/h2&gt;

&lt;p&gt;Every verification pattern in the field fits into one of three categories. The difference is who checks the work and how.&lt;/p&gt;

&lt;div class=&quot;mas-taxonomy&quot;&gt;
  &lt;h4&gt;Three Verification Architectures&lt;/h4&gt;
  &lt;div class=&quot;mas-tax-row&quot;&gt;
    &lt;span class=&quot;mas-tax-label&quot;&gt;Self-Verify&lt;/span&gt;
    &lt;div class=&quot;mas-tax-items&quot;&gt;
      &lt;span class=&quot;mas-tax-chip&quot;&gt;Same agent checks its own work&lt;/span&gt;
      &lt;span class=&quot;mas-tax-chip&quot;&gt;Fast, no coordination overhead&lt;/span&gt;
      &lt;span class=&quot;mas-tax-chip mas-chip-bad&quot;&gt;Blind to its own mistakes&lt;/span&gt;
    &lt;/div&gt;
  &lt;/div&gt;
  &lt;div class=&quot;mas-tax-row&quot;&gt;
    &lt;span class=&quot;mas-tax-label&quot;&gt;Separate Verifier&lt;/span&gt;
    &lt;div class=&quot;mas-tax-items&quot;&gt;
      &lt;span class=&quot;mas-tax-chip&quot;&gt;Different agent or system checks the work&lt;/span&gt;
      &lt;span class=&quot;mas-tax-chip mas-chip-ok&quot;&gt;Catches blind spots the author can&apos;t see&lt;/span&gt;
    &lt;/div&gt;
  &lt;/div&gt;
  &lt;div class=&quot;mas-tax-row&quot;&gt;
    &lt;span class=&quot;mas-tax-label&quot;&gt;Structural Gate&lt;/span&gt;
    &lt;div class=&quot;mas-tax-items&quot;&gt;
      &lt;span class=&quot;mas-tax-chip&quot;&gt;Work cannot proceed without passing a gate&lt;/span&gt;
      &lt;span class=&quot;mas-tax-chip mas-chip-ok&quot;&gt;Strongest: not advisory, blocking&lt;/span&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;p&gt;Wave-1 papers are mostly in the first category. MetaGPT introduces the second with its QA agent. Wave-2 papers and production systems are moving toward the third.&lt;/p&gt;

&lt;h2 id=&quot;what-each-system-actually-does&quot;&gt;What Each System Actually Does&lt;/h2&gt;

&lt;div class=&quot;mas-compare-wrap&quot;&gt;
&lt;table class=&quot;mas-compare&quot;&gt;
  &lt;thead&gt;
    &lt;tr&gt;&lt;th&gt;System&lt;/th&gt;&lt;th&gt;Pattern&lt;/th&gt;&lt;th&gt;Feedback Signal&lt;/th&gt;&lt;th&gt;Verifier&lt;/th&gt;&lt;th&gt;Modality Shift&lt;/th&gt;&lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;&lt;td&gt;CAMEL&lt;/td&gt;&lt;td&gt;Dialogue consensus&lt;/td&gt;&lt;td&gt;Partner agrees&lt;/td&gt;&lt;td&gt;Peer (same capability)&lt;/td&gt;&lt;td&gt;No (text → text)&lt;/td&gt;&lt;/tr&gt;
    &lt;tr&gt;&lt;td&gt;ChatDev&lt;/td&gt;&lt;td&gt;Code review plus compiler&lt;/td&gt;&lt;td&gt;Reviewer approval + compile&lt;/td&gt;&lt;td&gt;Reviewer agent + compiler&lt;/td&gt;&lt;td&gt;Partial&lt;/td&gt;&lt;/tr&gt;
    &lt;tr&gt;&lt;td&gt;MetaGPT&lt;/td&gt;&lt;td&gt;Unit test execution&lt;/td&gt;&lt;td&gt;Tests pass or fail&lt;/td&gt;&lt;td&gt;Test runtime (external)&lt;/td&gt;&lt;td&gt;Yes (code → result)&lt;/td&gt;&lt;/tr&gt;
    &lt;tr&gt;&lt;td&gt;Gen. Agents&lt;/td&gt;&lt;td&gt;None at runtime&lt;/td&gt;&lt;td&gt;N/A&lt;/td&gt;&lt;td&gt;Post-hoc human eval&lt;/td&gt;&lt;td&gt;N/A&lt;/td&gt;&lt;/tr&gt;
    &lt;tr&gt;&lt;td&gt;SWE-agent&lt;/td&gt;&lt;td&gt;ACI feedback + tests&lt;/td&gt;&lt;td&gt;Command output + test results&lt;/td&gt;&lt;td&gt;Environment&lt;/td&gt;&lt;td&gt;Yes&lt;/td&gt;&lt;/tr&gt;
    &lt;tr&gt;&lt;td&gt;Magentic-One&lt;/td&gt;&lt;td&gt;Orchestrator inner loop&lt;/td&gt;&lt;td&gt;Progress assessment&lt;/td&gt;&lt;td&gt;Orchestrator (separate)&lt;/td&gt;&lt;td&gt;Partial&lt;/td&gt;&lt;/tr&gt;
    &lt;tr class=&quot;mas-row-highlight&quot;&gt;&lt;td&gt;Cursor Agent&lt;/td&gt;&lt;td&gt;Visual feedback loop&lt;/td&gt;&lt;td&gt;Screenshot of rendered UI&lt;/td&gt;&lt;td&gt;Same agent (self-verify)&lt;/td&gt;&lt;td&gt;Yes (code → visual)&lt;/td&gt;&lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;

&lt;p&gt;The last row is the interesting one.&lt;/p&gt;

&lt;h2 id=&quot;cursors-visual-feedback-loop&quot;&gt;Cursor’s Visual Feedback Loop&lt;/h2&gt;

&lt;p&gt;Cursor’s agent mode has a pattern that isn’t in any of the papers. When it’s implementing a UI change, it does this:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;Writes code.&lt;/li&gt;
  &lt;li&gt;Starts the app or preview.&lt;/li&gt;
  &lt;li&gt;Takes a screenshot.&lt;/li&gt;
  &lt;li&gt;Looks at the screenshot.&lt;/li&gt;
  &lt;li&gt;Decides whether the output matches the intent.&lt;/li&gt;
  &lt;li&gt;Fixes or ships.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This is self-verification, which from the table above should be the weakest category. The agent is checking its own work. And MAST data tells us self-verification has a 13.2 percent failure rate in the form of reasoning-action mismatch: the agent thinks it did the right thing but didn’t.&lt;/p&gt;

&lt;p&gt;What saves Cursor’s approach is the modality shift. The agent wrote code (text). The verification happens on a screenshot (pixels). Re-reading your own code in the same modality you wrote it is a weak check. Looking at the rendered output of your code is a much stronger one. You can’t make the same mistake twice because you’re looking at a different representation of the work.&lt;/p&gt;

&lt;div class=&quot;mas-callout&quot;&gt;
  &lt;div class=&quot;mas-callout-label&quot;&gt;The modality shift principle&lt;/div&gt;
  The stronger the modality shift between the work and the verification, the more bugs you catch. Code to test execution (MetaGPT) is a modality shift. Code to screenshot (Cursor) is a modality shift. Code to executable proof (structural gates) is a modality shift. Re-reading your own code is not. This is why wave-1 papers that rely on dialogue consensus score so poorly on reasoning-action-mismatch failures: text to text is not a real check.
&lt;/div&gt;

&lt;h2 id=&quot;the-design-space&quot;&gt;The Design Space&lt;/h2&gt;

&lt;p&gt;Once you’ve internalized the three architectures and the modality-shift principle, you can think about verification design more clearly.&lt;/p&gt;

&lt;div class=&quot;mas-taxonomy&quot;&gt;
  &lt;h4&gt;Verification Choices Across the Field&lt;/h4&gt;
  &lt;div class=&quot;mas-tax-row&quot;&gt;
    &lt;span class=&quot;mas-tax-label&quot;&gt;Strongest&lt;/span&gt;
    &lt;div class=&quot;mas-tax-items&quot;&gt;
      &lt;span class=&quot;mas-tax-chip mas-chip-ok&quot;&gt;Structural gate with modality shift&lt;/span&gt;
      &lt;span class=&quot;mas-tax-chip mas-chip-ok&quot;&gt;Separate verifier with modality shift&lt;/span&gt;
    &lt;/div&gt;
  &lt;/div&gt;
  &lt;div class=&quot;mas-tax-row&quot;&gt;
    &lt;span class=&quot;mas-tax-label&quot;&gt;Useful&lt;/span&gt;
    &lt;div class=&quot;mas-tax-items&quot;&gt;
      &lt;span class=&quot;mas-tax-chip&quot;&gt;Self-verify with modality shift&lt;/span&gt;
      &lt;span class=&quot;mas-tax-chip&quot;&gt;Separate verifier without modality shift&lt;/span&gt;
    &lt;/div&gt;
  &lt;/div&gt;
  &lt;div class=&quot;mas-tax-row&quot;&gt;
    &lt;span class=&quot;mas-tax-label&quot;&gt;Weakest&lt;/span&gt;
    &lt;div class=&quot;mas-tax-items&quot;&gt;
      &lt;span class=&quot;mas-tax-chip mas-chip-bad&quot;&gt;Self-verify without modality shift (dialogue consensus)&lt;/span&gt;
      &lt;span class=&quot;mas-tax-chip mas-chip-bad&quot;&gt;No verification at runtime&lt;/span&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;p&gt;Most wave-1 systems are in the bottom category. The agentic coding turn added some modality shift (tests, screenshots). Wave-2 systems and production systems are starting to combine patterns: Cursor uses self-verify with modality shift, and the hybrid opportunity is to layer a separate verifier or structural gate on top of that fast inner loop.&lt;/p&gt;

&lt;div class=&quot;mas-callout&quot;&gt;
  &lt;div class=&quot;mas-callout-label&quot;&gt;Hybrid opportunity&lt;/div&gt;
  Use visual feedback as the dev agent&apos;s inner loop for fast iteration, but gate the output with a separate QA agent or executable proof to catch self-verification blind spots. This is a pattern you can build today, and it&apos;s more robust than either approach alone.
&lt;/div&gt;

&lt;h2 id=&quot;why-verification-is-undertheorized&quot;&gt;Why Verification Is Undertheorized&lt;/h2&gt;

&lt;p&gt;The multi-agent papers spend a lot of time on coordination and almost no time on verification. This is backwards. The MAST data from wave 2 shows that verification failures (FC3: premature termination, incomplete verification, incorrect verification) account for 23.5 percent of all observed failures across seven frameworks. That’s more than any other single failure category if you group them.&lt;/p&gt;

&lt;p&gt;If verification were a first-class concern, you’d expect to see papers titled “How Agents Should Check Their Work” or “Verification Protocols for Multi-Agent Systems.” Those papers don’t really exist. What we have is a lot of papers that casually mention their verification mechanism in a subsection and move on.&lt;/p&gt;

&lt;p&gt;The papers that take verification most seriously are the ones from the agentic coding turn, because they had to. You can’t fake SWE-bench. If your tests don’t pass, you don’t get credit. Interface design, guardrails, and structured feedback all exist in those systems because the benchmark forces them to. When the benchmark is a transcript of agents talking to each other, you don’t need real verification. When the benchmark is a working piece of software, you do.&lt;/p&gt;

&lt;p&gt;Next post: benchmarks. What the standard evaluations measure, what they miss, and why ChatDev and MetaGPT can report contradictory results on each other without either being obviously wrong.&lt;/p&gt;
</description>
				<pubDate>Wed, 29 Apr 2026 12:00:00 +0000</pubDate>
				<link>https://christophermeiklejohn.com/ai/agents/mas-series/2026/04/29/mas-series-06-verification-patterns.html</link>
				<guid isPermaLink="true">https://christophermeiklejohn.com/ai/agents/mas-series/2026/04/29/mas-series-06-verification-patterns.html</guid>
			</item>
		
			<item>
				<title>Getting Up to Speed on Multi-Agent Systems, Part 5: Debate, State, and Coordination</title>
				<description>&lt;p&gt;If wave 1 was about role-playing and the agentic coding turn was about interface quality, there’s a parallel thread running through the field asking a more fundamental question: what should multiple agents actually &lt;em&gt;do&lt;/em&gt; with each other? Debate? Share state? Coordinate? And are any of these interchangeable? This post is about four papers that sit at that intersection, including one that isn’t really an LLM paper at all but is the clearest theoretical bridge from distributed systems into multi-agent AI.&lt;/p&gt;

&lt;div class=&quot;mas-series-nav&quot;&gt;
  &lt;div class=&quot;mas-series-label&quot;&gt;Getting Up to Speed on MAS&lt;/div&gt;
  &lt;ol&gt;&lt;li&gt;&lt;a href=&quot;/ai/agents/mas-series/2026/04/24/mas-series-01-the-landscape.html&quot;&gt;Part 1. The Landscape&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;/ai/agents/mas-series/2026/04/25/mas-series-02-the-vocabulary.html&quot;&gt;Part 2. The Vocabulary&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;/ai/agents/mas-series/2026/04/26/mas-series-03-wave-one.html&quot;&gt;Part 3. Wave 1: Can Agents Coordinate At All?&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;/ai/agents/mas-series/2026/04/27/mas-series-04-wave-two.html&quot;&gt;Part 4. Wave 2: Why It Breaks&lt;/a&gt;&lt;/li&gt;&lt;li class=&quot;mas-current&quot;&gt;&lt;strong&gt;Part 5. Debate, State, and Coordination (you are here)&lt;/strong&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;/ai/agents/mas-series/2026/04/29/mas-series-06-verification-patterns.html&quot;&gt;Part 6. Verification Patterns&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;/ai/agents/mas-series/2026/04/30/mas-series-07-benchmarks.html&quot;&gt;Part 7. Benchmarks and What They Miss&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;/ai/agents/mas-series/2026/05/01/mas-series-08-open-questions.html&quot;&gt;Part 8. Open Questions&lt;/a&gt;&lt;/li&gt;&lt;/ol&gt;
&lt;/div&gt;

&lt;h2 id=&quot;du-et-al-convergent-debate&quot;&gt;Du et al.: Convergent Debate&lt;/h2&gt;

&lt;div class=&quot;mas-paper-card mas-debate&quot;&gt;
  &lt;div class=&quot;mas-card-title&quot;&gt;
    &lt;strong&gt;Improving Factuality and Reasoning through Multiagent Debate&lt;/strong&gt;
    &lt;span class=&quot;mas-card-meta&quot;&gt;&lt;a href=&quot;https://arxiv.org/abs/2305.14325&quot;&gt;arXiv 2305.14325&lt;/a&gt; · ICML 2024&lt;/span&gt;
  &lt;/div&gt;
  &lt;p class=&quot;mas-card-oneliner&quot;&gt;Multiple LLM instances debate until they converge on an answer.&lt;/p&gt;
  &lt;div class=&quot;mas-card-bet&quot;&gt;Core bet: Showing agents each other&apos;s answers changes their reasoning&lt;/div&gt;
  &lt;ul&gt;
    &lt;li&gt;Mechanism: N agents independently answer, then see each other&apos;s responses and revise over multiple rounds&lt;/li&gt;
    &lt;li&gt;Standard setup: 3 agents, 2 rounds (conservative; scales better with more)&lt;/li&gt;
    &lt;li&gt;Works on black-box models with identical prompts across all tasks&lt;/li&gt;
    &lt;li&gt;Key insight: debate is not voting; agents actually change their reasoning when shown alternatives&lt;/li&gt;
    &lt;li&gt;Performance improves monotonically with more agents and more rounds&lt;/li&gt;
    &lt;li&gt;&quot;Society of minds&quot; framing; collective intelligence from identical model instances&lt;/li&gt;
  &lt;/ul&gt;
  &lt;div class=&quot;mas-card-source&quot;&gt;Du, Li, Torralba, Tenenbaum, Mordatch&lt;/div&gt;
&lt;/div&gt;

&lt;p&gt;Du et al. is the canonical multi-agent debate paper. The setup is simple. You run the same LLM multiple times on the same question. Each instance generates its own answer independently. Then you show each instance what the others said and ask it to revise. Do this for two or three rounds. The answers converge.&lt;/p&gt;

&lt;p&gt;What makes this different from self-consistency or ensembling is that the agents see each other’s reasoning, not just their answers. If instance A argued that the square root of 144 is 12 for reason X, and instance B argued it’s 12 for reason Y, instance A’s second attempt might incorporate reason Y. This is why “more rounds” helps. It’s not just more samples. It’s refinement.&lt;/p&gt;

&lt;h2 id=&quot;liang-et-al-adversarial-debate&quot;&gt;Liang et al.: Adversarial Debate&lt;/h2&gt;

&lt;div class=&quot;mas-paper-card mas-debate&quot;&gt;
  &lt;div class=&quot;mas-card-title&quot;&gt;
    &lt;strong&gt;Encouraging Divergent Thinking through Multi-Agent Debate (MAD)&lt;/strong&gt;
    &lt;span class=&quot;mas-card-meta&quot;&gt;&lt;a href=&quot;https://arxiv.org/abs/2305.19118&quot;&gt;arXiv 2305.19118&lt;/a&gt; · EMNLP 2024&lt;/span&gt;
  &lt;/div&gt;
  &lt;p class=&quot;mas-card-oneliner&quot;&gt;Two debaters plus a judge, explicitly prompted to disagree.&lt;/p&gt;
  &lt;div class=&quot;mas-card-bet&quot;&gt;Core bet: Once an LLM is confident, only external pressure unsticks it&lt;/div&gt;
  &lt;ul&gt;
    &lt;li&gt;Identifies the Degeneration-of-Thought (DoT) problem: a confident LLM can&apos;t self-reflect its way out of wrong answers&lt;/li&gt;
    &lt;li&gt;Two debaters plus a judge in a tit-for-tat format; judge has adaptive break&lt;/li&gt;
    &lt;li&gt;Debaters explicitly prompted to disagree: &quot;it&apos;s not necessary to fully agree&quot;&lt;/li&gt;
    &lt;li&gt;GPT-3.5 plus MAD beat GPT-4 baseline on commonsense translation&lt;/li&gt;
    &lt;li&gt;Counter-intuitive arithmetic: 37 percent (MAD) vs 26 percent (single GPT-3.5) vs 51 percent (GPT-4)&lt;/li&gt;
    &lt;li&gt;Failure mode: increasing debater count degrades performance (context length limits)&lt;/li&gt;
    &lt;li&gt;Judge shows bias toward outputs matching its own architecture&lt;/li&gt;
  &lt;/ul&gt;
&lt;/div&gt;

&lt;p&gt;Liang et al. is a contrast to Du. Instead of converging agents, you have divergent ones. The debaters are prompted to disagree. The judge picks a winner or calls for another round. The paper’s theoretical contribution is the Degeneration-of-Thought problem, which is: once an LLM commits to an answer with confidence, it can’t self-reflect its way back. You have to push it.&lt;/p&gt;

&lt;p&gt;The striking result is that GPT-3.5 with MAD beats GPT-4 alone on commonsense translation. You can get a stronger system by forcing a weaker model to argue with itself than by upgrading the model. This is a paper I’d hand to anyone who thinks “just use a better model” is always the right answer.&lt;/p&gt;

&lt;p&gt;The failure mode is worth noting. MAD doesn’t scale well beyond two debaters, because the context window fills up with arguments. And the judge develops a bias when different LLMs are used as debaters: it favors outputs that look like its own model family. Both of these are signs that the architecture is more fragile than the benchmark numbers suggest.&lt;/p&gt;

&lt;h2 id=&quot;ou-et-al-shared-state-as-coordination&quot;&gt;Ou et al.: Shared State as Coordination&lt;/h2&gt;

&lt;div class=&quot;mas-paper-card&quot; style=&quot;border-left-color: var(--mas-green);&quot;&gt;
  &lt;div class=&quot;mas-card-title&quot;&gt;
    &lt;strong&gt;Analyzing Information Sharing and Coordination in Multi-Agent Planning&lt;/strong&gt;
    &lt;span class=&quot;mas-card-meta&quot;&gt;&lt;a href=&quot;https://arxiv.org/abs/2508.12981&quot;&gt;arXiv 2508.12981&lt;/a&gt; · August 2025&lt;/span&gt;
  &lt;/div&gt;
  &lt;p class=&quot;mas-card-oneliner&quot;&gt;A shared notebook plus a reflective orchestrator on travel planning.&lt;/p&gt;
  &lt;div class=&quot;mas-card-bet&quot;&gt;Core bet: Explicit information tracking beats unstructured conversation&lt;/div&gt;
  &lt;ul&gt;
    &lt;li&gt;Task: TravelPlanner benchmark, long-horizon, multi-constraint planning&lt;/li&gt;
    &lt;li&gt;Shared notebook: reduces hallucination errors by 18 percent by forcing explicit information tracking&lt;/li&gt;
    &lt;li&gt;Reflective orchestrator: directs conversation focus, reduces errors by additional 13.5 percent in targeted areas&lt;/li&gt;
    &lt;li&gt;Combined: 25 percent pass rate (vs 7.5 percent single-agent baseline), 3.3x improvement&lt;/li&gt;
    &lt;li&gt;Notebook alone helps more than orchestrator alone; state sharing greater than coordination for this task&lt;/li&gt;
    &lt;li&gt;Directly answers: structured information sharing prevents agents from inventing unsupported details&lt;/li&gt;
  &lt;/ul&gt;
  &lt;div class=&quot;mas-card-source&quot;&gt;Ou, Vaduguru, Fried&lt;/div&gt;
&lt;/div&gt;

&lt;p&gt;Ou et al. is the cleanest empirical study I’ve seen on what state sharing actually buys you. The task is constrained travel planning. The authors compare three setups: single agent, multi-agent with shared notebook, multi-agent with shared notebook and a reflective orchestrator. The notebook is the key mechanism. It’s an append-only log where agents record what they’ve learned. Agents read from it before they propose anything new.&lt;/p&gt;

&lt;p&gt;The headline finding is that the notebook reduces hallucination errors by 18 percent. The orchestrator adds another 13.5 percent. But read carefully: the notebook does more work than the orchestrator. Most of the benefit comes from forcing agents to write down what they know and read from a shared record. The coordination mechanism (the orchestrator) is secondary to the state sharing mechanism (the notebook).&lt;/p&gt;

&lt;p&gt;This is a clear result for how to build multi-agent systems for constrained planning. Give them a shared structured state. Make them write to it. Make them read from it. Then worry about coordination.&lt;/p&gt;

&lt;h2 id=&quot;the-calm-theorem-when-coordination-is-avoidable&quot;&gt;The CALM Theorem: When Coordination Is Avoidable&lt;/h2&gt;

&lt;div class=&quot;mas-paper-card&quot; style=&quot;border-left-color: var(--mas-purple);&quot;&gt;
  &lt;div class=&quot;mas-card-title&quot;&gt;
    &lt;strong&gt;Keeping CALM: When Distributed Consistency Is Easy&lt;/strong&gt;
    &lt;span class=&quot;mas-card-meta&quot;&gt;&lt;a href=&quot;https://arxiv.org/abs/1901.01930&quot;&gt;arXiv 1901.01930&lt;/a&gt; · Hellerstein &amp;amp; Alvaro · CACM 2020&lt;/span&gt;
  &lt;/div&gt;
  &lt;p class=&quot;mas-card-oneliner&quot;&gt;Which computations need coordination, and which don&apos;t.&lt;/p&gt;
  &lt;div class=&quot;mas-card-bet&quot;&gt;Theoretical result: Monotonic programs are coordination-free; non-monotonic programs aren&apos;t&lt;/div&gt;
  &lt;ul&gt;
    &lt;li&gt;CALM = Consistency As Logical Monotonicity&lt;/li&gt;
    &lt;li&gt;Theorem: programs with consistent, coordination-free distributed implementations are exactly the monotonic programs&lt;/li&gt;
    &lt;li&gt;If a computation only adds information (monotonic), it can run coordination-free and still get the right answer&lt;/li&gt;
    &lt;li&gt;If it retracts information (non-monotonic), coordination is required for consistency&lt;/li&gt;
    &lt;li&gt;Not yet applied to LLM agents in print, but the bridge is direct&lt;/li&gt;
  &lt;/ul&gt;
&lt;/div&gt;

&lt;p&gt;This one isn’t a multi-agent LLM paper. It’s a distributed systems paper from 2019 that states a theorem about when coordination is and isn’t necessary. I include it here because it’s the most direct theoretical bridge between classical distributed systems work and multi-agent AI, and no one in the LLM literature has formally made the connection yet.&lt;/p&gt;

&lt;p&gt;Here’s the CALM claim, translated for multi-agent AI. If your multi-agent system is only ever adding information to shared state (writing to a notebook, appending to a log, producing artifacts), the agents can run without coordinating with each other and still converge to a consistent answer. If the agents ever need to retract or update existing information, then coordination is required to avoid inconsistency.&lt;/p&gt;

&lt;div class=&quot;mas-callout&quot;&gt;
  &lt;div class=&quot;mas-callout-label&quot;&gt;Why this connects to Ou et al.&lt;/div&gt;
  Ou et al.&apos;s shared notebook is monotonic. Agents only append new information to it. Nothing gets retracted. That&apos;s why it works without heavy coordination machinery. The CALM theorem predicts this result. If Ou&apos;s notebook allowed agents to edit each other&apos;s entries, they would have needed coordination protocols (locks, version vectors, CRDTs) to keep the notebook consistent. They didn&apos;t, because they didn&apos;t need it.
&lt;/div&gt;

&lt;h2 id=&quot;putting-it-together&quot;&gt;Putting It Together&lt;/h2&gt;

&lt;p&gt;The four papers in this post are doing different things, but they’re converging on the same observation. The question “how should multiple agents work together” has more than one answer, and the answer depends on the structure of the task.&lt;/p&gt;

&lt;div class=&quot;mas-compare-wrap&quot;&gt;
&lt;table class=&quot;mas-compare&quot;&gt;
  &lt;thead&gt;
    &lt;tr&gt;&lt;th&gt;Pattern&lt;/th&gt;&lt;th&gt;What Agents Do&lt;/th&gt;&lt;th&gt;When It Works&lt;/th&gt;&lt;th&gt;When It Doesn&apos;t&lt;/th&gt;&lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;Convergent debate (Du)&lt;/td&gt;
      &lt;td&gt;Show each other reasoning, converge&lt;/td&gt;
      &lt;td&gt;Reasoning tasks with a right answer&lt;/td&gt;
      &lt;td&gt;Context fills up fast; limited scale&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Adversarial debate (Liang)&lt;/td&gt;
      &lt;td&gt;Argue opposite sides, judge decides&lt;/td&gt;
      &lt;td&gt;Unstuck models with the DoT problem&lt;/td&gt;
      &lt;td&gt;Judge bias; doesn&apos;t scale beyond 2&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Shared notebook (Ou)&lt;/td&gt;
      &lt;td&gt;Append information to a log everyone reads&lt;/td&gt;
      &lt;td&gt;Constrained planning, long-horizon tasks&lt;/td&gt;
      &lt;td&gt;Tasks requiring real-time coordination&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Coordination-free (CALM)&lt;/td&gt;
      &lt;td&gt;Monotonic writes, no coordination&lt;/td&gt;
      &lt;td&gt;Aggregation, counting, set-building&lt;/td&gt;
      &lt;td&gt;Anything that retracts or updates&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;

&lt;p&gt;Wave-1 multi-agent papers didn’t distinguish between these. They all called themselves “multi-agent collaboration” and treated the coordination structure as interchangeable. It isn’t. The structure has to match the task.&lt;/p&gt;

&lt;div class=&quot;mas-callout&quot;&gt;
  &lt;div class=&quot;mas-callout-label&quot;&gt;The distributed systems bridge&lt;/div&gt;
  This is the point where my PhD work starts to feel directly relevant to the multi-agent AI literature. CALM, CRDTs, version vectors, causal consistency: these are all formalisms for when agents need to coordinate and when they don&apos;t. None of them have been rigorously applied to LLM-based multi-agent systems yet. That&apos;s an opportunity. It&apos;s also a caution. If the field doesn&apos;t pick up this vocabulary, it will keep reinventing solutions that distributed systems solved decades ago.
&lt;/div&gt;

&lt;p&gt;Next post: verification patterns. How do these systems know when they’ve done the right thing? Test execution, dialogue consensus, structural gates, and Cursor’s visual feedback loop, which is the most interesting production-scale verification pattern I’ve seen and isn’t in any of the papers yet.&lt;/p&gt;
</description>
				<pubDate>Tue, 28 Apr 2026 12:00:00 +0000</pubDate>
				<link>https://christophermeiklejohn.com/ai/agents/mas-series/2026/04/28/mas-series-05-debate-state-coordination.html</link>
				<guid isPermaLink="true">https://christophermeiklejohn.com/ai/agents/mas-series/2026/04/28/mas-series-05-debate-state-coordination.html</guid>
			</item>
		
			<item>
				<title>Getting Up to Speed on Multi-Agent Systems, Part 4: Wave 2 (Why It Breaks)</title>
				<description>&lt;p&gt;By 2025, two things had happened. Wave-1 architectures were running in production (Anthropic had shipped its research system; the open-source ecosystem around orchestrator-worker patterns was maturing). The agentic coding turn had made clear that multi-agent was not the right tool for focused coding, and narrowed the interesting MAS question to “when we do use it, why does it break?”&lt;/p&gt;

&lt;p&gt;This wave is where I find the literature most useful, because it’s where empirical work finally catches up with the claims of wave 1.&lt;/p&gt;

&lt;div class=&quot;mas-series-nav&quot;&gt;
  &lt;div class=&quot;mas-series-label&quot;&gt;Getting Up to Speed on MAS&lt;/div&gt;
  &lt;ol&gt;&lt;li&gt;&lt;a href=&quot;/ai/agents/mas-series/2026/04/24/mas-series-01-the-landscape.html&quot;&gt;Part 1. The Landscape&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;/ai/agents/mas-series/2026/04/25/mas-series-02-the-vocabulary.html&quot;&gt;Part 2. The Vocabulary&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;/ai/agents/mas-series/2026/04/26/mas-series-03-wave-one.html&quot;&gt;Part 3. Wave 1: Can Agents Coordinate At All?&lt;/a&gt;&lt;/li&gt;&lt;li class=&quot;mas-current&quot;&gt;&lt;strong&gt;Part 4. Wave 2: Why It Breaks (you are here)&lt;/strong&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;/ai/agents/mas-series/2026/04/28/mas-series-05-debate-state-coordination.html&quot;&gt;Part 5. Debate, State, and Coordination&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;/ai/agents/mas-series/2026/04/29/mas-series-06-verification-patterns.html&quot;&gt;Part 6. Verification Patterns&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;/ai/agents/mas-series/2026/04/30/mas-series-07-benchmarks.html&quot;&gt;Part 7. Benchmarks and What They Miss&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;/ai/agents/mas-series/2026/05/01/mas-series-08-open-questions.html&quot;&gt;Part 8. Open Questions&lt;/a&gt;&lt;/li&gt;&lt;/ol&gt;
&lt;/div&gt;

&lt;h2 id=&quot;mast-14-failure-modes-from-1600-traces&quot;&gt;MAST: 14 Failure Modes from 1,600 Traces&lt;/h2&gt;

&lt;div class=&quot;mas-paper-card mas-mast&quot;&gt;
  &lt;div class=&quot;mas-card-title&quot;&gt;
    &lt;strong&gt;Why Do Multi-Agent LLM Systems Fail? (MAST)&lt;/strong&gt;
    &lt;span class=&quot;mas-card-meta&quot;&gt;&lt;a href=&quot;https://arxiv.org/abs/2503.13657&quot;&gt;arXiv 2503.13657&lt;/a&gt; · NeurIPS 2025 D&amp;amp;B&lt;/span&gt;
  &lt;/div&gt;
  &lt;p class=&quot;mas-card-oneliner&quot;&gt;1,600 annotated traces across 7 frameworks. First empirical taxonomy of why MAS break.&lt;/p&gt;
  &lt;div class=&quot;mas-card-bet&quot;&gt;Core contribution: MAST taxonomy of 14 failure modes in 3 categories&lt;/div&gt;
  &lt;ul&gt;
    &lt;li&gt;1,600+ traces across MetaGPT, ChatDev, HyperAgent, AppWorld, AG2, Magentic-One, OpenManus&lt;/li&gt;
    &lt;li&gt;41 to 87 percent failure rates across all frameworks; systemic, not isolated&lt;/li&gt;
    &lt;li&gt;Top 3 failures: step repetition (15.7 percent), reasoning-action mismatch (13.2 percent), unaware of termination (12.4 percent)&lt;/li&gt;
    &lt;li&gt;Systems with explicit verifiers (MetaGPT, ChatDev) had fewer failures&lt;/li&gt;
    &lt;li&gt;Inter-agent failures require &quot;theory of mind&quot;; agents can&apos;t model each other&apos;s information needs&lt;/li&gt;
    &lt;li&gt;Adding high-level objective verification gave +15.6 percent improvement&lt;/li&gt;
    &lt;li&gt;LLM-as-Judge pipeline: 94 percent accuracy against human experts&lt;/li&gt;
  &lt;/ul&gt;
  &lt;div class=&quot;mas-card-source&quot;&gt;
    Cemri, Pan, Yang, Agrawal, Chopra, Tiwari, Keutzer, Parameswaran, Klein, Ramchandran, Zaharia, Gonzalez, Stoica
  &lt;/div&gt;
&lt;/div&gt;

&lt;p&gt;The MAST paper is the one I keep coming back to. It’s the first rigorous empirical study of multi-agent failures. The authors took 1,600 execution traces from seven popular multi-agent frameworks, annotated each one with human experts, and built a 14-mode failure taxonomy.&lt;/p&gt;

&lt;p&gt;The headline number is that every framework they tested had failure rates between 41 and 87 percent. Every single one. These are the production frameworks. These are the systems people cite in their papers. And they fail almost as often as they succeed.&lt;/p&gt;

&lt;div class=&quot;mas-taxonomy&quot;&gt;
  &lt;h4&gt;MAST&apos;s 14 Failure Modes&lt;/h4&gt;
  &lt;div class=&quot;mas-tax-row&quot;&gt;
    &lt;span class=&quot;mas-tax-label&quot;&gt;FC1 System Design&lt;/span&gt;
    &lt;div class=&quot;mas-tax-items&quot;&gt;
      &lt;span class=&quot;mas-tax-chip&quot;&gt;Disobey task spec (11.8%)&lt;/span&gt;
      &lt;span class=&quot;mas-tax-chip&quot;&gt;Disobey role spec (1.5%)&lt;/span&gt;
      &lt;span class=&quot;mas-tax-chip&quot;&gt;Step repetition (15.7%)&lt;/span&gt;
      &lt;span class=&quot;mas-tax-chip&quot;&gt;Loss of conversation history (2.8%)&lt;/span&gt;
      &lt;span class=&quot;mas-tax-chip&quot;&gt;Unaware of termination (12.4%)&lt;/span&gt;
    &lt;/div&gt;
  &lt;/div&gt;
  &lt;div class=&quot;mas-tax-row&quot;&gt;
    &lt;span class=&quot;mas-tax-label&quot;&gt;FC2 Inter-Agent&lt;/span&gt;
    &lt;div class=&quot;mas-tax-items&quot;&gt;
      &lt;span class=&quot;mas-tax-chip&quot;&gt;Conversation reset (2.2%)&lt;/span&gt;
      &lt;span class=&quot;mas-tax-chip&quot;&gt;Fail to ask for clarification (6.8%)&lt;/span&gt;
      &lt;span class=&quot;mas-tax-chip&quot;&gt;Task derailment (7.4%)&lt;/span&gt;
      &lt;span class=&quot;mas-tax-chip&quot;&gt;Information withholding (0.85%)&lt;/span&gt;
      &lt;span class=&quot;mas-tax-chip&quot;&gt;Ignored other agent&apos;s input (1.9%)&lt;/span&gt;
      &lt;span class=&quot;mas-tax-chip&quot;&gt;Reasoning-action mismatch (13.2%)&lt;/span&gt;
    &lt;/div&gt;
  &lt;/div&gt;
  &lt;div class=&quot;mas-tax-row&quot;&gt;
    &lt;span class=&quot;mas-tax-label&quot;&gt;FC3 Verification&lt;/span&gt;
    &lt;div class=&quot;mas-tax-items&quot;&gt;
      &lt;span class=&quot;mas-tax-chip&quot;&gt;Premature termination (6.2%)&lt;/span&gt;
      &lt;span class=&quot;mas-tax-chip&quot;&gt;No/incomplete verification (8.2%)&lt;/span&gt;
      &lt;span class=&quot;mas-tax-chip&quot;&gt;Incorrect verification (9.1%)&lt;/span&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;p&gt;The taxonomy matters because it lets you diagnose specific failures. When someone says “my agent got stuck in a loop,” you can now ask whether that’s step repetition (FM-1.3) or conversation reset (FM-2.1). Those have different causes and different fixes.&lt;/p&gt;

&lt;p&gt;The finding that the paper downplays but I think is most important: the inter-agent failures are the hardest to fix. FC1 issues are prompt engineering problems. You can get meaningful improvements by rewriting role specifications. FC2 issues require what the paper calls “theory of mind,” meaning the agents don’t accurately model each other’s information needs. Prompt fixes don’t help there. The solutions are structural.&lt;/p&gt;

&lt;h2 id=&quot;mas-fire-fault-injection-for-multi-agent-systems&quot;&gt;MAS-FIRE: Fault Injection for Multi-Agent Systems&lt;/h2&gt;

&lt;div class=&quot;mas-paper-card mas-mast&quot;&gt;
  &lt;div class=&quot;mas-card-title&quot;&gt;
    &lt;strong&gt;MAS-FIRE: Fault Injection and Reliability Evaluation for LLM-Based Multi-Agent Systems&lt;/strong&gt;
    &lt;span class=&quot;mas-card-meta&quot;&gt;&lt;a href=&quot;https://arxiv.org/abs/2602.19843&quot;&gt;arXiv 2602.19843&lt;/a&gt; · February 2026&lt;/span&gt;
  &lt;/div&gt;
  &lt;p class=&quot;mas-card-oneliner&quot;&gt;The first systematic fault injection framework for LLM-based MAS.&lt;/p&gt;
  &lt;div class=&quot;mas-card-bet&quot;&gt;Core contribution: Active probing via fault injection, not just passive observation&lt;/div&gt;
  &lt;ul&gt;
    &lt;li&gt;15 fault types: 8 intra-agent (planning, memory, reasoning, action) + 7 inter-agent (config, instruction, communication)&lt;/li&gt;
    &lt;li&gt;Three injection mechanisms: prompt modification, response rewriting, message routing manipulation&lt;/li&gt;
    &lt;li&gt;Tested on MetaGPT, Table-Critic, CAMEL with GPT-5 and DeepSeek-V3&lt;/li&gt;
    &lt;li&gt;Key finding: config and instruction faults are catastrophic (Robustness Score = 0 percent for Blind Trust on MetaGPT)&lt;/li&gt;
    &lt;li&gt;Capability paradox: GPT-5&apos;s strict compliance hurts under Blind Trust (6.3 percent) vs DeepSeek-V3 (70.6 percent)&lt;/li&gt;
    &lt;li&gt;Linear pipelines extremely vulnerable; iterative architectures resilient (79-91 percent)&lt;/li&gt;
    &lt;li&gt;Shared message pools neutralize memory faults (+25 percent advantage)&lt;/li&gt;
  &lt;/ul&gt;
&lt;/div&gt;

&lt;p&gt;MAST is observational. You watch real failures and categorize them. MAS-FIRE is the complement: you inject failures on purpose and measure how the system handles them. This is standard practice in distributed systems (Chaos Engineering, Jepsen) but it’s new for LLM agents.&lt;/p&gt;

&lt;p&gt;The taxonomy is worth reading carefully.&lt;/p&gt;

&lt;div class=&quot;mas-taxonomy&quot;&gt;
  &lt;h4&gt;MAS-FIRE&apos;s 15 Fault Types&lt;/h4&gt;
  &lt;div class=&quot;mas-tax-row&quot;&gt;
    &lt;span class=&quot;mas-tax-label&quot;&gt;Intra-agent&lt;/span&gt;
    &lt;div class=&quot;mas-tax-items&quot;&gt;
      &lt;span class=&quot;mas-tax-chip&quot;&gt;Inexecutable Plan&lt;/span&gt;
      &lt;span class=&quot;mas-tax-chip&quot;&gt;Critical Info Loss&lt;/span&gt;
      &lt;span class=&quot;mas-tax-chip&quot;&gt;Memory Loss&lt;/span&gt;
      &lt;span class=&quot;mas-tax-chip&quot;&gt;Context Length Violation&lt;/span&gt;
      &lt;span class=&quot;mas-tax-chip&quot;&gt;Hallucination&lt;/span&gt;
      &lt;span class=&quot;mas-tax-chip&quot;&gt;Tool Selection Error&lt;/span&gt;
      &lt;span class=&quot;mas-tax-chip&quot;&gt;Param Filling Error&lt;/span&gt;
      &lt;span class=&quot;mas-tax-chip&quot;&gt;Param Format Error&lt;/span&gt;
    &lt;/div&gt;
  &lt;/div&gt;
  &lt;div class=&quot;mas-tax-row&quot;&gt;
    &lt;span class=&quot;mas-tax-label&quot;&gt;Inter-agent&lt;/span&gt;
    &lt;div class=&quot;mas-tax-items&quot;&gt;
      &lt;span class=&quot;mas-tax-chip&quot;&gt;Role Ambiguity&lt;/span&gt;
      &lt;span class=&quot;mas-tax-chip&quot;&gt;Blind Trust&lt;/span&gt;
      &lt;span class=&quot;mas-tax-chip&quot;&gt;Instruction Logic Conflict&lt;/span&gt;
      &lt;span class=&quot;mas-tax-chip&quot;&gt;Instruction Ambiguity&lt;/span&gt;
      &lt;span class=&quot;mas-tax-chip&quot;&gt;Message Cycle&lt;/span&gt;
      &lt;span class=&quot;mas-tax-chip&quot;&gt;Message Storm&lt;/span&gt;
      &lt;span class=&quot;mas-tax-chip&quot;&gt;Broadcast Amplification&lt;/span&gt;
    &lt;/div&gt;
  &lt;/div&gt;
  &lt;div class=&quot;mas-tax-row&quot;&gt;
    &lt;span class=&quot;mas-tax-label&quot;&gt;Injection via&lt;/span&gt;
    &lt;div class=&quot;mas-tax-items&quot;&gt;
      &lt;span class=&quot;mas-tax-chip&quot;&gt;Prompt Modification&lt;/span&gt;
      &lt;span class=&quot;mas-tax-chip&quot;&gt;Response Rewriting&lt;/span&gt;
      &lt;span class=&quot;mas-tax-chip&quot;&gt;Message Routing Manipulation&lt;/span&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;p&gt;The capability paradox is the finding I find most provocative. GPT-5 is a stronger model than DeepSeek-V3 by most benchmarks. But under the “Blind Trust” fault (where one agent is told to unconditionally accept instructions from another), GPT-5 fails almost completely (6.3 percent robustness) while DeepSeek-V3 holds up (70.6 percent). Why? Because GPT-5 is better at following instructions. It’s also better at following bad instructions. Strict compliance is a liability when you can’t trust the source.&lt;/p&gt;

&lt;p&gt;The implication for system design is that you want agents that can question upstream inputs. Not agents that just obey.&lt;/p&gt;

&lt;h2 id=&quot;silo-bench-communication-isnt-reasoning&quot;&gt;Silo-Bench: Communication Isn’t Reasoning&lt;/h2&gt;

&lt;div class=&quot;mas-paper-card mas-mast&quot;&gt;
  &lt;div class=&quot;mas-card-title&quot;&gt;
    &lt;strong&gt;Silo-Bench: Evaluating Distributed Coordination in Multi-Agent LLM Systems&lt;/strong&gt;
    &lt;span class=&quot;mas-card-meta&quot;&gt;&lt;a href=&quot;https://arxiv.org/abs/2603.01045&quot;&gt;arXiv 2603.01045&lt;/a&gt; · March 2026&lt;/span&gt;
  &lt;/div&gt;
  &lt;p class=&quot;mas-card-oneliner&quot;&gt;1,620 experiments showing agents can communicate but can&apos;t reason about distributed state.&lt;/p&gt;
  &lt;div class=&quot;mas-card-bet&quot;&gt;Core finding: The bottleneck is synthesis, not acquisition&lt;/div&gt;
  &lt;ul&gt;
    &lt;li&gt;30 algorithmic tasks across 3 communication complexity tiers, 54 configs, 1,620 experiments&lt;/li&gt;
    &lt;li&gt;Central finding: agents form correct coordination topologies and actively exchange information&lt;/li&gt;
    &lt;li&gt;But they systematically fail to synthesize distributed state into correct answers&lt;/li&gt;
    &lt;li&gt;Bottleneck is information integration, not information acquisition&lt;/li&gt;
    &lt;li&gt;Coordination overhead increases with agent scale, eventually eliminating parallelization benefits&lt;/li&gt;
    &lt;li&gt;Merely increasing agent count cannot circumvent context limitations&lt;/li&gt;
  &lt;/ul&gt;
&lt;/div&gt;

&lt;p&gt;Silo-Bench is the paper I wish more people would read. The finding is simple and surprising. When you give multiple LLM agents a problem that requires distributed reasoning, they do all the communication correctly. They build the right topology. They exchange the right information. Then they fail to synthesize what they’ve gathered into a correct answer.&lt;/p&gt;

&lt;p&gt;The bottleneck isn’t the network. The bottleneck is the integration. Each agent has received the necessary pieces, and each agent individually fails to combine those pieces into the answer. This is not a coordination problem in the distributed systems sense. It’s a reasoning problem that the coordination can’t compensate for.&lt;/p&gt;

&lt;p&gt;For wave-1 architectures, this result is devastating. The whole argument for agents-debating-each-other was that two agents looking at the same problem from different angles could synthesize a better answer than one agent alone. Silo-Bench says: maybe sometimes, but not for information-integration tasks, which is most tasks.&lt;/p&gt;

&lt;h2 id=&quot;what-wave-3-adds-that-wave-1-and-2-missed&quot;&gt;What Wave 3 Adds That Wave 1 and 2 Missed&lt;/h2&gt;

&lt;div class=&quot;mas-taxonomy&quot;&gt;
  &lt;h4&gt;Wave 3&apos;s New Contributions&lt;/h4&gt;
  &lt;div class=&quot;mas-tax-row&quot;&gt;
    &lt;span class=&quot;mas-tax-label&quot;&gt;Observation&lt;/span&gt;
    &lt;div class=&quot;mas-tax-items&quot;&gt;
      &lt;span class=&quot;mas-tax-chip mas-chip-ok&quot;&gt;Real failure data across multiple frameworks (MAST)&lt;/span&gt;
    &lt;/div&gt;
  &lt;/div&gt;
  &lt;div class=&quot;mas-tax-row&quot;&gt;
    &lt;span class=&quot;mas-tax-label&quot;&gt;Injection&lt;/span&gt;
    &lt;div class=&quot;mas-tax-items&quot;&gt;
      &lt;span class=&quot;mas-tax-chip mas-chip-ok&quot;&gt;Active fault testing (MAS-FIRE)&lt;/span&gt;
    &lt;/div&gt;
  &lt;/div&gt;
  &lt;div class=&quot;mas-tax-row&quot;&gt;
    &lt;span class=&quot;mas-tax-label&quot;&gt;Limits&lt;/span&gt;
    &lt;div class=&quot;mas-tax-items&quot;&gt;
      &lt;span class=&quot;mas-tax-chip mas-chip-ok&quot;&gt;What coordination can and can&apos;t fix (Silo-Bench)&lt;/span&gt;
    &lt;/div&gt;
  &lt;/div&gt;
  &lt;div class=&quot;mas-tax-row&quot;&gt;
    &lt;span class=&quot;mas-tax-label&quot;&gt;Production lessons&lt;/span&gt;
    &lt;div class=&quot;mas-tax-items&quot;&gt;
      &lt;span class=&quot;mas-tax-chip mas-chip-ok&quot;&gt;Anthropic blog: 15x token cost, shared-context failures&lt;/span&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;div class=&quot;mas-callout&quot;&gt;
  &lt;div class=&quot;mas-callout-label&quot;&gt;The wave-2 framing&lt;/div&gt;
  MAST observes what breaks. MAS-FIRE tests what breaks by injecting it. Silo-Bench identifies the limits of what coordination can fix. Together they provide a reliability research stack that wave 1 didn&apos;t have. What&apos;s still missing: gates, recovery protocols, longitudinal failure datasets. The field is still working on these.
&lt;/div&gt;

&lt;p&gt;The critical thing wave 2 does is separate “this is a coordination problem” from “this is a reasoning problem.” Wave 1 assumed everything was a coordination problem. Wave 2 says: if your agents can’t synthesize distributed state individually, no amount of better message passing will save you. Fix the reasoning first. Coordinate second.&lt;/p&gt;

&lt;p&gt;Next post: debate, state, and the CALM theorem. Three papers on whether agents should agree, disagree, or just share a notebook. And a theoretical result from distributed systems that explains which choice makes sense when.&lt;/p&gt;
</description>
				<pubDate>Mon, 27 Apr 2026 12:00:00 +0000</pubDate>
				<link>https://christophermeiklejohn.com/ai/agents/mas-series/2026/04/27/mas-series-04-wave-two.html</link>
				<guid isPermaLink="true">https://christophermeiklejohn.com/ai/agents/mas-series/2026/04/27/mas-series-04-wave-two.html</guid>
			</item>
		
			<item>
				<title>Spring Tour Recap: A Month of Shipping on Zabriskie</title>
				<description>&lt;blockquote&gt;
  &lt;p&gt;&lt;em&gt;“this app is the bees. I am very grateful for it.”&lt;/em&gt;
(a chomper, end of show, Irving, 4/25)&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Goose Spring ‘26 just wrapped. The run started March 28 in Athens and closed out Saturday night in Irving. Fourteen shows. One festival lead-in. Fourteen live chats running in parallel with each show, with 40 people sending &lt;strong&gt;3,737 messages&lt;/strong&gt; to each other across the tour. Most of them sent from couches.&lt;/p&gt;

&lt;p&gt;For the second half of tour I had a weird but perfect setup. Goose was on Eastern time. I was couch-touring most of those shows from a hotel room in Vegas. Then Phish was at Sphere (six nights across two runs, 4/16-18 and 4/23-25), and I was at all of them. Pacific time. The two shows overlapped by about an hour every night: the back end of the Goose set ran into the front end of Phish at Sphere. Which meant at any given moment in that hour I had two live chats open and two iOS Live Activities going on the lock screen. One for the show I was actually at, one for the show I was following along with from inside a different venue. The Dynamic Island had to share.&lt;/p&gt;

&lt;p&gt;That overlap hour is where the whole pitch of this app clicked for me, so it’s worth pulling out as its own point. Sports fans have lived inside multi-game nights forever. Your team’s game on the TV, the other playoff game on a tablet, score alerts buzzing on your phone, the group chat scrolling beside it, fantasy stats updating in another tab. Nobody thinks twice about it. That kind of parallel, shared, real-time consumption is the default for sports.&lt;/p&gt;

&lt;p&gt;Music has never had any of that. A concert has always been one show, in one room, ending when the lights come up, with whatever conversation you happened to have with the person next to you. If two of your favorite bands are playing the same night in different cities, that has historically just been a thing you mourn. There’s no second-screen experience for live music. There’s no group chat thread for the show you’re not at. There’s no “score alert” telling you the band you can’t see just opened with something rare. That whole layer doesn’t exist.&lt;/p&gt;

&lt;p&gt;We’ve gotten pushback on this specifically. People have told us, in the chat and in person, that they don’t think we should be encouraging anyone to open Zabriskie at a show. The argument is the same one phones have always heard at live events: be present, put it away, watch the band. I take it seriously. I also remember being on the other side of the same argument seventeen years ago.&lt;/p&gt;

&lt;p&gt;Back in 2008 when the iPhone first came out, I worked at a baseball startup in Boston. We built live play-prediction inside the app: in your seat, during the game, what’s the next pitch, did the runner just steal. The reaction we got from people who had never tried it was word-for-word identical to what we get now about Zabriskie. “No one is going to be on their phone at a baseball game.” Today every Major League ballpark has a stadium app open across the section, every wrist has the score on it, and a fan in their car or their living room is part of the same conversation as the fan in section 304. The game didn’t get worse. The community got bigger. People who couldn’t physically be there became part of being there.&lt;/p&gt;

&lt;p&gt;Live music gets there too. The phone isn’t the enemy of the show. The phone, used well, is what lets the show have a community around it that outlasts the show.&lt;/p&gt;

&lt;p&gt;That is a lot of what we’re building. Anything that broke at the Goose show, Patrick and I would fix between Goose and Phish, and I’d run it live at Phish two hours later. Every feature got tested twice a night, against two different bands, in two different time zones, by a person who was actively living the multi-show pattern the app is supposed to enable.&lt;/p&gt;

&lt;p&gt;I started writing this because I wanted to remember what we built during the tour. I looked at the PR list and counted. &lt;strong&gt;Three hundred and nine pull requests&lt;/strong&gt; merged into &lt;a href=&quot;https://github.com/cmeiklejohn/zabriskie&quot;&gt;Zabriskie&lt;/a&gt; between the first show and the last. About fifty of those merged today, with the tour wrap-up package shipping in real time as I’m writing this. That number doesn’t feel real. It is real. Most of it shipped to the web immediately so we could test it ourselves the moment it merged, then went out to our TestFlight and Play Store testers within hours, and will be live in the App Store and Play Store this week for everyone. Some of it shipped during the show.&lt;/p&gt;

&lt;p&gt;This is what stuck.&lt;/p&gt;

&lt;h2 id=&quot;the-live-show-got-real&quot;&gt;The Live Show Got Real&lt;/h2&gt;

&lt;p&gt;The biggest change is that “couch touring with the app” is now a thing people actually do, not a thing I keep telling people they should try. Here is what the chat looked like in Houston on 4/23, around 11pm, from Patrick, my collaborator on the project:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;Whew just got home. Was updating the setlist while walking home from the bar with pip and Zabriskie open lmao&lt;/p&gt;

  &lt;p&gt;Ohhhhh snapp how bout that new feature I built today fam?!&lt;/p&gt;

  &lt;p&gt;Got a couple folks in the chomp who witnessed the FTP!&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;He’s talking about the FTP-witness pill, which he had shipped earlier that day. FTP is “first time played.” Every song in a setlist has a debut show. When a song plays during a live chat, the app now scans everyone watching, and if any of them were RSVP’d to that song’s original debut show, an inline 👀 pill appears under the song name calling them out. It looks like this:&lt;/p&gt;

&lt;div style=&quot;background:#e5e2d9; padding:18px; border-radius:14px; margin:16px auto; max-width:520px; font-family:-apple-system,BlinkMacSystemFont,system-ui,sans-serif; color:#262626;&quot;&gt;
  &lt;div style=&quot;background:#F5F2EB; border-radius:18px; box-shadow:0 6px 20px rgba(0,0,0,0.08); overflow:hidden; max-width:380px; margin:0 auto;&quot;&gt;
    &lt;div style=&quot;background:#262626; color:#fff; padding:10px 16px; font-size:11px; letter-spacing:0.04em; text-transform:uppercase; display:flex; justify-content:space-between;&quot;&gt;
      &lt;span&gt;Live Chat · MSG &apos;26&lt;/span&gt;
      &lt;span style=&quot;opacity:0.65; font-weight:400; text-transform:none; letter-spacing:0;&quot;&gt;1 witness&lt;/span&gt;
    &lt;/div&gt;
    &lt;div style=&quot;padding:8px 0 14px;&quot;&gt;
      &lt;div style=&quot;padding:6px 14px;&quot;&gt;&lt;span style=&quot;font-size:13px; font-weight:600; color:#8B5CF6; background:#EDE9FE; padding:4px 12px; border-radius:12px;&quot;&gt;🎸 Set 2 begins&lt;/span&gt;&lt;/div&gt;
      &lt;div style=&quot;padding:6px 14px;&quot;&gt;&lt;span style=&quot;font-size:13px; font-weight:600; color:#8B5CF6; background:#EDE9FE; padding:4px 12px; border-radius:12px;&quot;&gt;🎵 All I Need&lt;/span&gt;&lt;/div&gt;
      &lt;div style=&quot;padding:2px 14px 6px;&quot;&gt;&lt;span style=&quot;font-size:13px; font-weight:700; color:#065F46; background:#D1FAE5; padding:4px 12px; border-radius:12px;&quot;&gt;👀 1 person in chomp was at the FTP&lt;/span&gt;&lt;/div&gt;
      &lt;div style=&quot;padding:6px 14px; display:flex; gap:10px; align-items:flex-start;&quot;&gt;
        &lt;div style=&quot;width:34px; height:34px; border-radius:50%; background:linear-gradient(135deg,#d9c6ff 0%,#a88fe6 100%); display:flex; align-items:center; justify-content:center; font-size:14px; font-weight:700; color:#fff; flex-shrink:0;&quot;&gt;P&lt;/div&gt;
        &lt;div style=&quot;display:flex; flex-direction:column; gap:2px;&quot;&gt;
          &lt;div style=&quot;display:flex; gap:6px; align-items:center;&quot;&gt;&lt;span style=&quot;font-weight:600; font-size:14px;&quot;&gt;patrick&lt;/span&gt;&lt;span style=&quot;font-size:10px; padding:2px 6px; border-radius:8px; color:#fff; font-weight:700; background:#EC4899;&quot;&gt;🎸 Show&lt;/span&gt;&lt;/div&gt;
          &lt;div style=&quot;font-size:14px; line-height:1.3;&quot;&gt;omg I forgot I was there for the og&lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;p&gt;Tap the pill and it pops open the original debut show’s setlist sheet, with a Bookmark button so you can save the show for later. If you yourself were at that debut, the per-song stat sheet gets an extra “You were at this song’s FTP” strip with the date and venue. So Patrick wrote and shipped the FTP-witness feature that day, then came home from a different show and used it himself in the chat for a third show. That is the loop now.&lt;/p&gt;

&lt;p&gt;The Live Activity on iOS got a dedicated set break UI, so when the band walks off your Lock Screen tells you instead of just freezing on the last song. It buzzes when the setlist updates, so you don’t have to keep waking your phone to check. Android got a redesign that matches the iOS Live Activity layout, with rich notifications that persist for the entire show instead of falling off the lock screen after a few minutes.&lt;/p&gt;

&lt;div style=&quot;display:flex; gap:14px; flex-wrap:wrap; justify-content:center; margin:18px auto;&quot;&gt;
  &lt;div style=&quot;background:#0a0a0a; border-radius:36px; padding:14px; box-shadow:0 8px 24px rgba(0,0,0,0.18); flex:0 0 auto;&quot;&gt;
    &lt;div style=&quot;background:linear-gradient(180deg, #2a1d3a 0%, #1a0f2a 100%); border-radius:24px; width:280px; padding:14px 16px; color:#fff; font-family:-apple-system,BlinkMacSystemFont,system-ui,sans-serif;&quot;&gt;
      &lt;div style=&quot;display:flex; justify-content:space-between; align-items:center; font-size:11px; opacity:0.7; margin-bottom:8px;&quot;&gt;&lt;span&gt;9:41&lt;/span&gt;&lt;span&gt;📶 5G ⌁ 78%&lt;/span&gt;&lt;/div&gt;
      &lt;div style=&quot;background:rgba(139,92,246,0.18); border:1px solid rgba(139,92,246,0.35); border-radius:18px; padding:12px 14px; backdrop-filter:blur(12px);&quot;&gt;
        &lt;div style=&quot;display:flex; gap:10px; align-items:center; margin-bottom:6px;&quot;&gt;
          &lt;div style=&quot;width:32px; height:32px; border-radius:8px; background:linear-gradient(135deg,#a855f7,#7c3aed); display:flex; align-items:center; justify-content:center; font-size:14px;&quot;&gt;🪿&lt;/div&gt;
          &lt;div style=&quot;display:flex; flex-direction:column; flex:1;&quot;&gt;
            &lt;span style=&quot;font-size:11px; font-weight:600; opacity:0.7;&quot;&gt;GOOSE · LIVE&lt;/span&gt;
            &lt;span style=&quot;font-size:10px; opacity:0.55;&quot;&gt;Saenger Theatre, NOLA&lt;/span&gt;
          &lt;/div&gt;
          &lt;span style=&quot;font-size:11px; opacity:0.6;&quot;&gt;●&lt;/span&gt;
        &lt;/div&gt;
        &lt;div style=&quot;font-size:18px; font-weight:700; line-height:1.15;&quot;&gt;🎵 Tumble&lt;/div&gt;
        &lt;div style=&quot;font-size:11px; opacity:0.65; margin-top:2px;&quot;&gt;Set 2 · song 4&lt;/div&gt;
      &lt;/div&gt;
      &lt;div style=&quot;text-align:center; font-size:9px; opacity:0.4; margin-top:6px;&quot;&gt;BEFORE · last song frozen on screen&lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;

  &lt;div style=&quot;background:#0a0a0a; border-radius:36px; padding:14px; box-shadow:0 8px 24px rgba(0,0,0,0.18); flex:0 0 auto;&quot;&gt;
    &lt;div style=&quot;background:linear-gradient(180deg, #2a1d3a 0%, #1a0f2a 100%); border-radius:24px; width:280px; padding:14px 16px; color:#fff; font-family:-apple-system,BlinkMacSystemFont,system-ui,sans-serif;&quot;&gt;
      &lt;div style=&quot;display:flex; justify-content:space-between; align-items:center; font-size:11px; opacity:0.7; margin-bottom:8px;&quot;&gt;&lt;span&gt;10:23&lt;/span&gt;&lt;span&gt;📶 5G ⌁ 71%&lt;/span&gt;&lt;/div&gt;
      &lt;div style=&quot;background:rgba(251,146,60,0.18); border:1px solid rgba(251,146,60,0.45); border-radius:18px; padding:12px 14px; backdrop-filter:blur(12px);&quot;&gt;
        &lt;div style=&quot;display:flex; gap:10px; align-items:center; margin-bottom:6px;&quot;&gt;
          &lt;div style=&quot;width:32px; height:32px; border-radius:8px; background:linear-gradient(135deg,#fb923c,#ea580c); display:flex; align-items:center; justify-content:center; font-size:14px;&quot;&gt;🪿&lt;/div&gt;
          &lt;div style=&quot;display:flex; flex-direction:column; flex:1;&quot;&gt;
            &lt;span style=&quot;font-size:11px; font-weight:600; color:#fb923c;&quot;&gt;GOOSE · SET BREAK&lt;/span&gt;
            &lt;span style=&quot;font-size:10px; opacity:0.55;&quot;&gt;Saenger Theatre, NOLA&lt;/span&gt;
          &lt;/div&gt;
          &lt;span style=&quot;font-size:18px; color:#fb923c;&quot;&gt;⏸&lt;/span&gt;
        &lt;/div&gt;
        &lt;div style=&quot;font-size:18px; font-weight:700; line-height:1.15; color:#fb923c;&quot;&gt;⏸️ Set break&lt;/div&gt;
        &lt;div style=&quot;font-size:11px; opacity:0.7; margin-top:2px;&quot;&gt;After Set 2 · 9 songs&lt;/div&gt;
      &lt;/div&gt;
      &lt;div style=&quot;text-align:center; font-size:9px; color:#fb923c; opacity:0.7; margin-top:6px;&quot;&gt;AFTER · dedicated set break UI&lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;p&gt;Couch tour viewers get a 30-second spoiler hide on fresh song bubbles, because the people in the building are ahead of the stream.&lt;/p&gt;

&lt;p&gt;The bubble doesn’t blur. It renders as three pulsing dots, the same way an iMessage typing indicator does. There’s a reason it’s that specific shape and not a frosted blur or a “?” placeholder.&lt;/p&gt;

&lt;p&gt;The moment a song starts in the room, people start talking about it in the chat. You need a marker in the timeline so a couch viewer can see “okay, the in-venue chompers are reacting to whatever this is right now,” follow the conversation in context, and not get hit with the song name as a spoiler before they’ve heard a note of it. The dots are that marker. When the 30 seconds is up, they flip to the song name and the chat above lines up with what the couch viewer is now hearing.&lt;/p&gt;

&lt;p&gt;We landed on 30 by testing it live during real shows. A 4K livestream encodes in roughly that window before it reaches a couch viewer, so 30 seconds is close to the actual gap between the room and the screen. We extended this to admin-typed setlist entries too, because the setlist is now mostly admin-typed.&lt;/p&gt;

&lt;div style=&quot;background:#e5e2d9; padding:18px; border-radius:14px; margin:16px auto; max-width:520px; font-family:-apple-system,BlinkMacSystemFont,system-ui,sans-serif; color:#262626;&quot;&gt;
  &lt;div style=&quot;background:#F5F2EB; border-radius:18px; box-shadow:0 6px 20px rgba(0,0,0,0.08); overflow:hidden; max-width:380px; margin:0 auto;&quot;&gt;
    &lt;div style=&quot;background:#262626; color:#fff; padding:10px 16px; font-size:11px; letter-spacing:0.04em; text-transform:uppercase; display:flex; justify-content:space-between; align-items:center;&quot;&gt;
      &lt;span&gt;🪿 Goose · Live&lt;/span&gt;
      &lt;span style=&quot;opacity:0.65; font-weight:400; text-transform:none; letter-spacing:0;&quot;&gt;couch view · stream behind&lt;/span&gt;
    &lt;/div&gt;

    &lt;div style=&quot;padding:8px 0 14px;&quot;&gt;
      &lt;div style=&quot;padding:6px 14px;&quot;&gt;&lt;span style=&quot;font-size:13px; font-weight:600; color:#8B5CF6; background:#EDE9FE; padding:4px 12px; border-radius:12px;&quot;&gt;🎵 Atlas Dogs&lt;/span&gt;&lt;/div&gt;

      &lt;div style=&quot;padding:6px 14px; display:flex; gap:10px; align-items:flex-start;&quot;&gt;
        &lt;div style=&quot;width:34px; height:34px; border-radius:50%; background:linear-gradient(135deg,#ffd6b0,#f59e47); display:flex; align-items:center; justify-content:center; font-size:14px; font-weight:700; color:#fff; flex-shrink:0;&quot;&gt;P&lt;/div&gt;
        &lt;div style=&quot;display:flex; flex-direction:column; gap:2px; flex:1;&quot;&gt;
          &lt;div style=&quot;display:flex; gap:6px; align-items:center;&quot;&gt;&lt;span style=&quot;font-weight:600; font-size:14px;&quot;&gt;patrick&lt;/span&gt;&lt;span style=&quot;font-size:10px; padding:2px 6px; border-radius:8px; color:#fff; font-weight:700; background:#EC4899;&quot;&gt;🎸 Show&lt;/span&gt;&lt;/div&gt;
          &lt;div style=&quot;font-size:14px; line-height:1.3;&quot;&gt;opener slaps already&lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;

      &lt;div style=&quot;padding:6px 14px;&quot;&gt;&lt;span style=&quot;font-size:13px; font-weight:600; color:#8B5CF6; background:#EDE9FE; padding:4px 12px; border-radius:12px;&quot;&gt;🎵 Tumble&lt;/span&gt;&lt;/div&gt;

      &lt;div style=&quot;padding:6px 14px; display:flex; gap:10px; align-items:flex-start;&quot;&gt;
        &lt;div style=&quot;width:34px; height:34px; border-radius:50%; background:linear-gradient(135deg,#d9c6ff,#a88fe6); display:flex; align-items:center; justify-content:center; font-size:14px; font-weight:700; color:#fff; flex-shrink:0;&quot;&gt;M&lt;/div&gt;
        &lt;div style=&quot;display:flex; flex-direction:column; gap:2px; flex:1;&quot;&gt;
          &lt;div style=&quot;display:flex; gap:6px; align-items:center;&quot;&gt;&lt;span style=&quot;font-weight:600; font-size:14px;&quot;&gt;chomper2&lt;/span&gt;&lt;span style=&quot;font-size:10px; padding:2px 6px; border-radius:8px; color:#fff; font-weight:700; background:#EC4899;&quot;&gt;🎸 Show&lt;/span&gt;&lt;/div&gt;
          &lt;div style=&quot;font-size:14px; line-height:1.3;&quot;&gt;TUMBLE!!! called it 📣&lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;

      &lt;div style=&quot;padding:6px 14px; display:flex; gap:10px; align-items:flex-start;&quot;&gt;
        &lt;div style=&quot;width:34px; height:34px; border-radius:50%; background:linear-gradient(135deg,#ffd6b0,#f59e47); display:flex; align-items:center; justify-content:center; font-size:14px; font-weight:700; color:#fff; flex-shrink:0;&quot;&gt;P&lt;/div&gt;
        &lt;div style=&quot;display:flex; flex-direction:column; gap:2px; flex:1;&quot;&gt;
          &lt;div style=&quot;display:flex; gap:6px; align-items:center;&quot;&gt;&lt;span style=&quot;font-weight:600; font-size:14px;&quot;&gt;patrick&lt;/span&gt;&lt;span style=&quot;font-size:10px; padding:2px 6px; border-radius:8px; color:#fff; font-weight:700; background:#EC4899;&quot;&gt;🎸 Show&lt;/span&gt;&lt;/div&gt;
          &lt;div style=&quot;font-size:14px; line-height:1.3;&quot;&gt;31 show gap, hot 🔥&lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;

      &lt;div style=&quot;padding:6px 14px; display:flex; align-items:center; gap:8px;&quot;&gt;
        &lt;span style=&quot;font-size:13px; font-weight:600; color:#8B5CF6; background:#EDE9FE; padding:6px 14px; border-radius:14px; display:inline-flex; align-items:center; gap:4px; min-width:64px; justify-content:center;&quot;&gt;
          &lt;span style=&quot;width:6px; height:6px; border-radius:50%; background:#8B5CF6; opacity:0.4;&quot;&gt;&lt;/span&gt;
          &lt;span style=&quot;width:6px; height:6px; border-radius:50%; background:#8B5CF6; opacity:0.7;&quot;&gt;&lt;/span&gt;
          &lt;span style=&quot;width:6px; height:6px; border-radius:50%; background:#8B5CF6; opacity:1;&quot;&gt;&lt;/span&gt;
        &lt;/span&gt;
        &lt;span style=&quot;font-size:10px; color:#9CA3AF;&quot;&gt;unblurs in 17s · in-venue is reacting&lt;/span&gt;
      &lt;/div&gt;

      &lt;div style=&quot;padding:6px 14px; display:flex; gap:10px; align-items:flex-start;&quot;&gt;
        &lt;div style=&quot;width:34px; height:34px; border-radius:50%; background:linear-gradient(135deg,#ffd6b0,#f59e47); display:flex; align-items:center; justify-content:center; font-size:14px; font-weight:700; color:#fff; flex-shrink:0;&quot;&gt;P&lt;/div&gt;
        &lt;div style=&quot;display:flex; flex-direction:column; gap:2px; flex:1;&quot;&gt;
          &lt;div style=&quot;display:flex; gap:6px; align-items:center;&quot;&gt;&lt;span style=&quot;font-weight:600; font-size:14px;&quot;&gt;patrick&lt;/span&gt;&lt;span style=&quot;font-size:10px; padding:2px 6px; border-radius:8px; color:#fff; font-weight:700; background:#EC4899;&quot;&gt;🎸 Show&lt;/span&gt;&lt;/div&gt;
          &lt;div style=&quot;font-size:14px; line-height:1.3;&quot;&gt;OH NO WAY 🤯🤯🤯&lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;p&gt;Here’s what happened. We started this tour pulling setlists from a couple of upstream sources that publish their own version of the setlist a few minutes after each song lands. Even when those sources are healthy, an entry usually shows up two to ten minutes after the song actually starts. For a couch viewer, that’s the difference between catching the opening of a jam and finding out twenty minutes later you missed it.&lt;/p&gt;

&lt;p&gt;So mid-tour we just stopped waiting. If you’re at the show, type the song into the manage-setlist screen the moment you hear it. Patrick was doing this from the floor most nights, both at his Goose shows and as a couch viewer when I was the one at Phish. The upstream sources still run as a backstop, but the live in-app setlist is now driven by whoever is fastest in the room. Latency dropped from minutes to seconds.&lt;/p&gt;

&lt;h2 id=&quot;song-calls&quot;&gt;Song Calls&lt;/h2&gt;

&lt;p&gt;People watching from home love to try to guess the next song from the opening notes. You hear a couple of bars on the stream, you blurt out “MADHUVAN!”, you’re either a hero or you wait six seconds and pretend you didn’t say anything. Until this tour the only place to do that was a group text or whoever happened to be in the room with you.&lt;/p&gt;

&lt;p&gt;This was the feature I was most nervous to ship and most happy we did. During a live show, you can now call the next song inside the app. There’s a 📣 chip on the current-song strip; tap it, type the song you think comes next, and the app fuzzy-matches against the band’s catalog (so “atlas” picks up “Atlas Dogs”). Submit, and a pending pill drops below the strip. If the song you called actually plays, you get a green ✓ YOU CALLED IT pill with a confetti burst. Misses fade quietly. There’s a per-show leaderboard so you can see who’s hot tonight, but deliberately no global all-time ranking.&lt;/p&gt;

&lt;p&gt;This is one of the load-bearing design principles of the whole app, so it’s worth stating plainly: &lt;strong&gt;we are not building Fantasy Music.&lt;/strong&gt; The minute you ship a season-long leaderboard, the gravitational pull of the product changes. The goal becomes winning. People start optimizing their calls, gaming the window, refreshing for stats, treating the show as input to a meta-game played somewhere outside of it. The community shrinks into a competition. The leaderboard exists, because the celebration of a correct call is part of the fun, but it lives inside the show and ends with the show. Community first, scoreboard second. Every feature in this app gets evaluated against that line:&lt;/p&gt;

&lt;div style=&quot;display:flex; gap:14px; flex-wrap:wrap; justify-content:center; margin:18px auto;&quot;&gt;
  &lt;div style=&quot;background:#F5F2EB; border-radius:18px; box-shadow:0 6px 20px rgba(0,0,0,0.08); padding:14px 16px; flex:0 0 auto; max-width:340px; font-family:-apple-system,BlinkMacSystemFont,system-ui,sans-serif; color:#262626;&quot;&gt;
    &lt;div style=&quot;font-size:9px; font-weight:700; letter-spacing:0.08em; color:#6B7280; text-transform:uppercase; margin-bottom:8px;&quot;&gt;Now Playing · Tap 📣 to call next&lt;/div&gt;
    &lt;div style=&quot;display:flex; align-items:center; gap:10px; padding:10px 12px; background:#fff; border-radius:14px;&quot;&gt;
      &lt;div style=&quot;font-size:22px;&quot;&gt;🎵&lt;/div&gt;
      &lt;div style=&quot;flex:1; display:flex; flex-direction:column;&quot;&gt;
        &lt;span style=&quot;font-size:15px; font-weight:700;&quot;&gt;All I Need&lt;/span&gt;
        &lt;span style=&quot;font-size:11px; color:#6B7280;&quot;&gt;Set 2 · song 3&lt;/span&gt;
      &lt;/div&gt;
      &lt;span style=&quot;font-size:13px; font-weight:700; color:#8B5CF6; background:#EDE9FE; padding:6px 12px; border-radius:14px; cursor:pointer;&quot;&gt;📣 Call&lt;/span&gt;
    &lt;/div&gt;
    &lt;div style=&quot;margin-top:10px; padding:8px 12px; background:rgba(139,92,246,0.10); border-radius:12px; font-size:12px; color:#5b21b6; display:flex; align-items:center; gap:8px;&quot;&gt;
      &lt;span style=&quot;font-size:14px;&quot;&gt;⏳&lt;/span&gt;&lt;span&gt;Pending: &lt;strong&gt;Empress of Organos&lt;/strong&gt;&lt;/span&gt;
    &lt;/div&gt;
  &lt;/div&gt;

  &lt;div style=&quot;background:#F5F2EB; border-radius:18px; box-shadow:0 6px 20px rgba(0,0,0,0.08); padding:14px 16px; flex:0 0 auto; max-width:340px; font-family:-apple-system,BlinkMacSystemFont,system-ui,sans-serif; color:#262626;&quot;&gt;
    &lt;div style=&quot;font-size:9px; font-weight:700; letter-spacing:0.08em; color:#6B7280; text-transform:uppercase; margin-bottom:8px;&quot;&gt;Now Playing&lt;/div&gt;
    &lt;div style=&quot;display:flex; align-items:center; gap:10px; padding:10px 12px; background:#fff; border-radius:14px;&quot;&gt;
      &lt;div style=&quot;font-size:22px;&quot;&gt;🎵&lt;/div&gt;
      &lt;div style=&quot;flex:1; display:flex; flex-direction:column;&quot;&gt;
        &lt;span style=&quot;font-size:15px; font-weight:700;&quot;&gt;Empress of Organos&lt;/span&gt;
        &lt;span style=&quot;font-size:11px; color:#6B7280;&quot;&gt;Set 2 · song 4&lt;/span&gt;
      &lt;/div&gt;
    &lt;/div&gt;
    &lt;div style=&quot;margin-top:10px; padding:10px 12px; background:linear-gradient(135deg,#10b981 0%,#059669 100%); border-radius:12px; color:#fff; font-size:13px; font-weight:700; display:flex; align-items:center; gap:8px; position:relative; overflow:hidden;&quot;&gt;
      &lt;span style=&quot;font-size:16px;&quot;&gt;✓&lt;/span&gt;&lt;span&gt;YOU CALLED IT&lt;/span&gt;
      &lt;span style=&quot;position:absolute; right:8px; top:6px; font-size:14px;&quot;&gt;🎉&lt;/span&gt;
      &lt;span style=&quot;position:absolute; right:24px; top:14px; font-size:10px;&quot;&gt;✨&lt;/span&gt;
      &lt;span style=&quot;position:absolute; right:36px; top:4px; font-size:8px;&quot;&gt;★&lt;/span&gt;
    &lt;/div&gt;
    &lt;div style=&quot;margin-top:6px; font-size:11px; color:#6B7280; padding:0 4px;&quot;&gt;📣 1 win this show · tap your badge to see history&lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;p&gt;Across the tour, &lt;strong&gt;105 song calls&lt;/strong&gt; went out from 9 different callers. The most-called song was Factory Fiction, called eight separate times by different people across the run. It never landed. The most-correct call was Into the Myst, which hit three times.&lt;/p&gt;

&lt;h2 id=&quot;chomp-live-chat-grew-up&quot;&gt;Chomp (Live Chat) Grew Up&lt;/h2&gt;

&lt;p&gt;The live chat is called Chomp because of course it is. It got a lot of love this tour:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;@mention autocomplete&lt;/strong&gt; in the composer, with the dropdown flipping above the input on Android when there’s no room below.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Tap a song pill&lt;/strong&gt; in the chat to open a stat sheet showing last played, gap, FTP info, and your personal history with the song.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;FTP-witness pill&lt;/strong&gt; so when someone in the chat is seeing a song for the first time, everyone knows.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Heart burst animation&lt;/strong&gt; when someone favorites your message. This is small. It also matters.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Tap the purple here-now bar&lt;/strong&gt; to expand the full chomper roster. When more than eight people are watching, the roster scrolls instead of pushing the rest of the page off the screen.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Optimistic bubble insertion&lt;/strong&gt; so your message appears the instant you hit send, not after the round trip.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;@mention notifications&lt;/strong&gt; that route to chat instead of the generic notification feed.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Plus a long tail of avatar rendering speedups (thumbnails over full-size, async decoding, parallel batch enrichment) that make the chat feel like it’s keeping up with the show instead of catching up to it.&lt;/p&gt;

&lt;p&gt;Here is roughly what the chomp looks like now during a live show, with most of those features in one frame: the purple here-now bar at the top (tap to expand the roster), a tappable song pill, an @mention rendered with its purple highlight, and a chat message that’s been hearted.&lt;/p&gt;

&lt;div style=&quot;background:#e5e2d9; padding:18px; border-radius:14px; margin:16px auto; max-width:520px; font-family:-apple-system,BlinkMacSystemFont,system-ui,sans-serif; color:#262626;&quot;&gt;
  &lt;div style=&quot;background:#F5F2EB; border-radius:18px; box-shadow:0 6px 20px rgba(0,0,0,0.08); overflow:hidden; max-width:380px; margin:0 auto;&quot;&gt;
    &lt;div style=&quot;background:#262626; color:#fff; padding:10px 16px; font-size:11px; letter-spacing:0.04em; text-transform:uppercase; display:flex; justify-content:space-between; align-items:center;&quot;&gt;
      &lt;span&gt;🪿 Goose · Live&lt;/span&gt;
      &lt;span style=&quot;opacity:0.65; font-weight:400; text-transform:none; letter-spacing:0;&quot;&gt;Saenger Theatre&lt;/span&gt;
    &lt;/div&gt;

    &lt;div style=&quot;margin:10px 12px; padding:8px 12px; background:linear-gradient(135deg,#8B5CF6 0%,#7c3aed 100%); border-radius:14px; display:flex; align-items:center; gap:8px; cursor:pointer;&quot;&gt;
      &lt;div style=&quot;display:flex;&quot;&gt;
        &lt;div style=&quot;width:24px; height:24px; border-radius:50%; background:linear-gradient(135deg,#ffd6b0,#f59e47); border:2px solid #fff; display:flex; align-items:center; justify-content:center; font-size:10px; font-weight:700; color:#fff; margin-right:-8px;&quot;&gt;P&lt;/div&gt;
        &lt;div style=&quot;width:24px; height:24px; border-radius:50%; background:linear-gradient(135deg,#b0eaff,#3ba8e0); border:2px solid #fff; display:flex; align-items:center; justify-content:center; font-size:10px; font-weight:700; color:#fff; margin-right:-8px;&quot;&gt;C&lt;/div&gt;
        &lt;div style=&quot;width:24px; height:24px; border-radius:50%; background:linear-gradient(135deg,#d9c6ff,#a88fe6); border:2px solid #fff; display:flex; align-items:center; justify-content:center; font-size:10px; font-weight:700; color:#fff; margin-right:-8px;&quot;&gt;M&lt;/div&gt;
        &lt;div style=&quot;width:24px; height:24px; border-radius:50%; background:linear-gradient(135deg,#ffc8d8,#ec4899); border:2px solid #fff; display:flex; align-items:center; justify-content:center; font-size:10px; font-weight:700; color:#fff; margin-right:-8px;&quot;&gt;Q&lt;/div&gt;
        &lt;div style=&quot;width:24px; height:24px; border-radius:50%; background:linear-gradient(135deg,#a7f3d0,#10b981); border:2px solid #fff; display:flex; align-items:center; justify-content:center; font-size:10px; font-weight:700; color:#fff;&quot;&gt;B&lt;/div&gt;
      &lt;/div&gt;
      &lt;span style=&quot;flex:1; color:#fff; font-size:12px; font-weight:600;&quot;&gt;12 chomping right now&lt;/span&gt;
      &lt;span style=&quot;color:#fff; opacity:0.8; font-size:14px;&quot;&gt;›&lt;/span&gt;
    &lt;/div&gt;

    &lt;div style=&quot;padding:6px 14px;&quot;&gt;&lt;span style=&quot;font-size:13px; font-weight:600; color:#8B5CF6; background:#EDE9FE; padding:4px 12px; border-radius:12px; cursor:pointer;&quot;&gt;🎵 Atlas Dogs&lt;/span&gt;&lt;/div&gt;

    &lt;div style=&quot;padding:6px 14px; display:flex; gap:10px; align-items:flex-start;&quot;&gt;
      &lt;div style=&quot;width:34px; height:34px; border-radius:50%; background:linear-gradient(135deg,#ffd6b0,#f59e47); display:flex; align-items:center; justify-content:center; font-size:14px; font-weight:700; color:#fff; flex-shrink:0;&quot;&gt;P&lt;/div&gt;
      &lt;div style=&quot;display:flex; flex-direction:column; gap:2px; flex:1;&quot;&gt;
        &lt;div style=&quot;display:flex; gap:6px; align-items:center;&quot;&gt;&lt;span style=&quot;font-weight:600; font-size:14px;&quot;&gt;patrick&lt;/span&gt;&lt;span style=&quot;font-size:10px; padding:2px 6px; border-radius:8px; color:#fff; font-weight:700; background:#EC4899;&quot;&gt;🎸 Show&lt;/span&gt;&lt;/div&gt;
        &lt;div style=&quot;font-size:14px; line-height:1.3;&quot;&gt;this jam is &lt;span style=&quot;color:#8B5CF6; font-weight:600;&quot;&gt;unreal&lt;/span&gt; 🔥🔥🔥&lt;/div&gt;
        &lt;div style=&quot;display:flex; gap:8px; margin-top:4px; align-items:center;&quot;&gt;
          &lt;span style=&quot;font-size:12px; padding:2px 10px; border-radius:10px; background:rgba(236,72,153,0.12); color:#EC4899; font-weight:600; cursor:pointer;&quot;&gt;❤ 4&lt;/span&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;

    &lt;div style=&quot;padding:6px 14px; display:flex; gap:10px; align-items:flex-start;&quot;&gt;
      &lt;div style=&quot;width:34px; height:34px; border-radius:50%; background:linear-gradient(135deg,#b0eaff,#3ba8e0); display:flex; align-items:center; justify-content:center; font-size:14px; font-weight:700; color:#fff; flex-shrink:0;&quot;&gt;C&lt;/div&gt;
      &lt;div style=&quot;display:flex; flex-direction:column; gap:2px; flex:1;&quot;&gt;
        &lt;div style=&quot;display:flex; gap:6px; align-items:center;&quot;&gt;&lt;span style=&quot;font-weight:600; font-size:14px;&quot;&gt;chomper1&lt;/span&gt;&lt;span style=&quot;font-size:10px; padding:2px 6px; border-radius:8px; color:#fff; font-weight:700; background:#3B82F6;&quot;&gt;🛋 Couch&lt;/span&gt;&lt;/div&gt;
        &lt;div style=&quot;font-size:14px; line-height:1.3;&quot;&gt;&lt;span style=&quot;color:#8B5CF6; font-weight:600; background:#EDE9FE; padding:1px 4px; border-radius:4px;&quot;&gt;@patrick&lt;/span&gt; agreed, this is the version we&apos;ll talk about later&lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;

    &lt;div style=&quot;padding:8px 14px 14px;&quot;&gt;
      &lt;div style=&quot;display:flex; gap:8px; align-items:center; padding:8px 12px; background:#fff; border-radius:18px; border:1px solid rgba(0,0,0,0.08);&quot;&gt;
        &lt;span style=&quot;font-size:14px; color:#9CA3AF; flex:1;&quot;&gt;Say something to the chomp…&lt;/span&gt;
        &lt;span style=&quot;font-size:12px; font-weight:700; color:#fff; background:#8B5CF6; padding:4px 12px; border-radius:12px;&quot;&gt;Send&lt;/span&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;p&gt;Tap any of those purple &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;🎵&lt;/code&gt; song pills and a stat sheet slides up. Last time the band played it, gap since, debut date and venue, and a personal strip showing what you specifically have done with the song. The one card answers the four questions every chomper asks the moment a song starts (“when did they last play this,” “is this rare,” “first time?”, and “have I caught it”):&lt;/p&gt;

&lt;div style=&quot;background:#e5e2d9; padding:18px; border-radius:14px; margin:16px auto; max-width:520px; font-family:-apple-system,BlinkMacSystemFont,system-ui,sans-serif; color:#262626;&quot;&gt;
  &lt;div style=&quot;background:#F5F2EB; border-radius:18px; box-shadow:0 6px 20px rgba(0,0,0,0.08); overflow:hidden; max-width:380px; margin:0 auto;&quot;&gt;
    &lt;div style=&quot;background:#262626; color:#fff; padding:10px 16px; font-size:11px; letter-spacing:0.04em; text-transform:uppercase; display:flex; justify-content:space-between; align-items:center;&quot;&gt;
      &lt;span&gt;🎵 Song stats&lt;/span&gt;
      &lt;span style=&quot;opacity:0.65; font-weight:400; text-transform:none; letter-spacing:0;&quot;&gt;tap-up sheet&lt;/span&gt;
    &lt;/div&gt;

    &lt;div style=&quot;padding:18px 16px 6px;&quot;&gt;
      &lt;div style=&quot;display:inline-block; font-size:9px; font-weight:800; letter-spacing:0.08em; color:#fff; background:#059669; padding:3px 8px; border-radius:999px; text-transform:uppercase;&quot;&gt;Rare&lt;/div&gt;
      &lt;div style=&quot;font-size:24px; font-weight:800; margin-top:8px; line-height:1.1;&quot;&gt;Factory Fiction&lt;/div&gt;
      &lt;div style=&quot;font-size:11px; color:#6B7280; margin-top:2px;&quot;&gt;Goose · 27 lifetime plays&lt;/div&gt;
    &lt;/div&gt;

    &lt;div style=&quot;padding:8px 16px 8px;&quot;&gt;
      &lt;div style=&quot;padding:10px 12px; background:linear-gradient(135deg,#EC4899 0%,#db2777 100%); color:#fff; border-radius:12px; display:flex; align-items:center; gap:8px;&quot;&gt;
        &lt;span style=&quot;font-size:18px;&quot;&gt;🎯&lt;/span&gt;
        &lt;div style=&quot;flex:1;&quot;&gt;&lt;div style=&quot;font-size:13px; font-weight:700;&quot;&gt;You&apos;ve never caught Factory Fiction live&lt;/div&gt;&lt;div style=&quot;font-size:10px; opacity:0.85;&quot;&gt;on your wishlist · 8 fans called it this tour, none landed&lt;/div&gt;&lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;

    &lt;div style=&quot;padding:0 16px 14px; display:grid; grid-template-columns:1fr 1fr; gap:8px;&quot;&gt;
      &lt;div style=&quot;background:#fff; padding:10px 12px; border-radius:10px;&quot;&gt;
        &lt;div style=&quot;font-size:9px; font-weight:700; letter-spacing:0.05em; color:#6B7280; text-transform:uppercase;&quot;&gt;Last played&lt;/div&gt;
        &lt;div style=&quot;font-size:15px; font-weight:800; margin-top:2px;&quot;&gt;12/13/25&lt;/div&gt;
        &lt;div style=&quot;font-size:10px; color:#6B7280;&quot;&gt;Goosemas · Hampton&lt;/div&gt;
      &lt;/div&gt;
      &lt;div style=&quot;background:#fff; padding:10px 12px; border-radius:10px;&quot;&gt;
        &lt;div style=&quot;font-size:9px; font-weight:700; letter-spacing:0.05em; color:#6B7280; text-transform:uppercase;&quot;&gt;Show gap&lt;/div&gt;
        &lt;div style=&quot;font-size:15px; font-weight:800; margin-top:2px;&quot;&gt;14 shows&lt;/div&gt;
        &lt;div style=&quot;font-size:10px; color:#6B7280;&quot;&gt;since last play&lt;/div&gt;
      &lt;/div&gt;
      &lt;div style=&quot;background:#fff; padding:10px 12px; border-radius:10px;&quot;&gt;
        &lt;div style=&quot;font-size:9px; font-weight:700; letter-spacing:0.05em; color:#6B7280; text-transform:uppercase;&quot;&gt;FTP&lt;/div&gt;
        &lt;div style=&quot;font-size:15px; font-weight:800; margin-top:2px;&quot;&gt;Oct 9, 2016&lt;/div&gt;
        &lt;div style=&quot;font-size:10px; color:#6B7280;&quot;&gt;The Hartford, CT&lt;/div&gt;
      &lt;/div&gt;
      &lt;div style=&quot;background:#fff; padding:10px 12px; border-radius:10px;&quot;&gt;
        &lt;div style=&quot;font-size:9px; font-weight:700; letter-spacing:0.05em; color:#6B7280; text-transform:uppercase;&quot;&gt;All-time plays&lt;/div&gt;
        &lt;div style=&quot;font-size:15px; font-weight:800; margin-top:2px;&quot;&gt;27&lt;/div&gt;
        &lt;div style=&quot;font-size:10px; color:#6B7280;&quot;&gt;across all tours&lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;

    &lt;div style=&quot;padding:0 16px 16px;&quot;&gt;
      &lt;div style=&quot;padding:8px 12px; background:rgba(139,92,246,0.10); border-radius:10px; font-size:11px; color:#5b21b6; display:flex; align-items:center; gap:8px;&quot;&gt;
        &lt;span style=&quot;font-size:14px;&quot;&gt;👀&lt;/span&gt;
        &lt;span&gt;&lt;strong&gt;0 people in chomp&lt;/strong&gt; were at this song&apos;s FTP. (It was a 2016 small-club show, before most of us found the band.)&lt;/span&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;h2 id=&quot;tour-stats-got-serious&quot;&gt;Tour Stats Got Serious&lt;/h2&gt;

&lt;p&gt;Tour Stats started as a personal-only “here are some numbers about your shows” page. It is now a real product surface.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;Cross-user view with a public/private toggle&lt;/strong&gt;, so you can compare your tour to your friends’ tours if they’ve opted in.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Band filter on every drill-down&lt;/strong&gt;, so you can see your Phish stats separately from your Goose stats separately from your Max Creek stats.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;New stat cards&lt;/strong&gt;: Setlist Staples (most-played songs you haven’t caught yet), FTP count in the overview, Tour Completion with band names on each row, GEOGRAPHIC REACH normalized so the dup-shows-table users don’t double-count states.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Interactive drill-downs&lt;/strong&gt; that take you straight to the show or song.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Per-band bustout threshold&lt;/strong&gt; so Phish rotation staples stop misflagging as 🔥 fire bustouts.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Past-show logging flow&lt;/strong&gt; with tour, festival, and event pills, plus month sub-chips. Logging shows you went to before joining is now a real path, not a chore.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We also surfaced Tour Stats in the compass (More) drawer, so people can find it.&lt;/p&gt;

&lt;p&gt;Here’s what most of the screen looks like: band pill at the top, public/private toggle, the OVERVIEW row with FTP count and Tour Completion, then the Setlist Staples card surfacing “common picks you’ve somehow missed” (going RSVPs only, since couch tour doesn’t count against you).&lt;/p&gt;

&lt;div style=&quot;background:#e5e2d9; padding:18px; border-radius:14px; margin:16px auto; max-width:520px; font-family:-apple-system,BlinkMacSystemFont,system-ui,sans-serif; color:#262626;&quot;&gt;
  &lt;div style=&quot;background:#F5F2EB; border-radius:18px; box-shadow:0 6px 20px rgba(0,0,0,0.08); overflow:hidden; max-width:380px; margin:0 auto;&quot;&gt;
    &lt;div style=&quot;background:#262626; color:#fff; padding:10px 16px; font-size:11px; letter-spacing:0.04em; text-transform:uppercase; display:flex; justify-content:space-between; align-items:center;&quot;&gt;
      &lt;span&gt;🧭 Tour Stats&lt;/span&gt;
      &lt;span style=&quot;opacity:0.65; font-weight:400; text-transform:none; letter-spacing:0;&quot;&gt;@cmeik&lt;/span&gt;
    &lt;/div&gt;

    &lt;div style=&quot;padding:12px 14px 4px; display:flex; gap:8px; align-items:center;&quot;&gt;
      &lt;span style=&quot;font-size:12px; font-weight:700; padding:6px 12px; border-radius:14px; background:#8B5CF6; color:#fff;&quot;&gt;🪿 Goose ›&lt;/span&gt;
      &lt;span style=&quot;flex:1;&quot;&gt;&lt;/span&gt;
      &lt;span style=&quot;font-size:11px; font-weight:700; padding:5px 10px; border-radius:12px; background:#fff; border:1px solid #d1d5db; color:#6B7280;&quot;&gt;🌐 Public&lt;/span&gt;
    &lt;/div&gt;

    &lt;div style=&quot;padding:8px 14px 6px; font-size:10px; font-weight:800; letter-spacing:0.06em; color:#6B7280; text-transform:uppercase;&quot;&gt;Overview&lt;/div&gt;
    &lt;div style=&quot;padding:0 14px 12px; display:grid; grid-template-columns:1fr 1fr; gap:8px;&quot;&gt;
      &lt;div style=&quot;background:#fff; padding:10px 12px; border-radius:12px;&quot;&gt;
        &lt;div style=&quot;font-size:9px; font-weight:700; letter-spacing:0.05em; color:#6B7280; text-transform:uppercase;&quot;&gt;🥚 FTPs&lt;/div&gt;
        &lt;div style=&quot;font-size:22px; font-weight:800; margin-top:2px;&quot;&gt;37&lt;/div&gt;
        &lt;div style=&quot;font-size:10px; color:#6B7280;&quot;&gt;debuts you caught live&lt;/div&gt;
      &lt;/div&gt;
      &lt;div style=&quot;background:#fff; padding:10px 12px; border-radius:12px;&quot;&gt;
        &lt;div style=&quot;font-size:9px; font-weight:700; letter-spacing:0.05em; color:#6B7280; text-transform:uppercase;&quot;&gt;Shows&lt;/div&gt;
        &lt;div style=&quot;font-size:22px; font-weight:800; margin-top:2px;&quot;&gt;52&lt;/div&gt;
        &lt;div style=&quot;font-size:10px; color:#6B7280;&quot;&gt;across 18 venues&lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;

    &lt;div style=&quot;padding:0 14px 12px;&quot;&gt;
      &lt;div style=&quot;background:#fff; padding:10px 12px; border-radius:12px;&quot;&gt;
        &lt;div style=&quot;display:flex; justify-content:space-between; align-items:baseline;&quot;&gt;
          &lt;div style=&quot;font-size:10px; font-weight:800; letter-spacing:0.05em; color:#6B7280; text-transform:uppercase;&quot;&gt;Tour Completion&lt;/div&gt;
          &lt;div style=&quot;font-size:11px; color:#6B7280;&quot;&gt;Spring &apos;26&lt;/div&gt;
        &lt;/div&gt;
        &lt;div style=&quot;font-size:18px; font-weight:800; margin-top:4px;&quot;&gt;9 / 14 nights&lt;/div&gt;
        &lt;div style=&quot;height:6px; background:#EDE9FE; border-radius:3px; margin-top:6px; overflow:hidden;&quot;&gt;
          &lt;div style=&quot;height:100%; width:64%; background:linear-gradient(90deg,#8B5CF6,#7c3aed);&quot;&gt;&lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;

    &lt;div style=&quot;padding:0 14px 14px;&quot;&gt;
      &lt;div style=&quot;background:#fff; padding:14px 14px 12px; border-radius:14px;&quot;&gt;
        &lt;div style=&quot;display:flex; align-items:baseline; gap:8px;&quot;&gt;
          &lt;span style=&quot;font-size:14px;&quot;&gt;📌&lt;/span&gt;&lt;span style=&quot;font-size:12px; font-weight:800; letter-spacing:0.04em; color:#8B5CF6; text-transform:uppercase;&quot;&gt;Setlist Staples&lt;/span&gt;
        &lt;/div&gt;
        &lt;div style=&quot;font-size:11px; color:#6B7280; margin-bottom:10px;&quot;&gt;Common picks · still on your list&lt;/div&gt;
        &lt;div style=&quot;display:flex; flex-direction:column; gap:6px;&quot;&gt;
          &lt;div style=&quot;display:flex; align-items:center; gap:10px; padding:6px 10px; background:#F5F2EB; border-radius:8px;&quot;&gt;&lt;span style=&quot;font-size:11px; font-weight:800; color:#8B5CF6; min-width:16px;&quot;&gt;1&lt;/span&gt;&lt;span style=&quot;flex:1; font-size:13px; font-weight:600;&quot;&gt;Indian River&lt;/span&gt;&lt;span style=&quot;font-size:10px; color:#6B7280;&quot;&gt;×117&lt;/span&gt;&lt;/div&gt;
          &lt;div style=&quot;display:flex; align-items:center; gap:10px; padding:6px 10px; background:#F5F2EB; border-radius:8px;&quot;&gt;&lt;span style=&quot;font-size:11px; font-weight:800; color:#8B5CF6; min-width:16px;&quot;&gt;2&lt;/span&gt;&lt;span style=&quot;flex:1; font-size:13px; font-weight:600;&quot;&gt;Butter Rum&lt;/span&gt;&lt;span style=&quot;font-size:10px; color:#6B7280;&quot;&gt;×113&lt;/span&gt;&lt;/div&gt;
          &lt;div style=&quot;display:flex; align-items:center; gap:10px; padding:6px 10px; background:#F5F2EB; border-radius:8px;&quot;&gt;&lt;span style=&quot;font-size:11px; font-weight:800; color:#8B5CF6; min-width:16px;&quot;&gt;3&lt;/span&gt;&lt;span style=&quot;flex:1; font-size:13px; font-weight:600;&quot;&gt;Lead the Way&lt;/span&gt;&lt;span style=&quot;font-size:10px; color:#6B7280;&quot;&gt;×86&lt;/span&gt;&lt;/div&gt;
          &lt;div style=&quot;display:flex; align-items:center; gap:10px; padding:6px 10px; background:#F5F2EB; border-radius:8px;&quot;&gt;&lt;span style=&quot;font-size:11px; font-weight:800; color:#8B5CF6; min-width:16px;&quot;&gt;4&lt;/span&gt;&lt;span style=&quot;flex:1; font-size:13px; font-weight:600;&quot;&gt;White Lights&lt;/span&gt;&lt;span style=&quot;font-size:10px; color:#6B7280;&quot;&gt;×68&lt;/span&gt;&lt;/div&gt;
          &lt;div style=&quot;display:flex; align-items:center; gap:10px; padding:6px 10px; background:#F5F2EB; border-radius:8px;&quot;&gt;&lt;span style=&quot;font-size:11px; font-weight:800; color:#8B5CF6; min-width:16px;&quot;&gt;5&lt;/span&gt;&lt;span style=&quot;flex:1; font-size:13px; font-weight:600;&quot;&gt;Crosseyed &amp;amp; Painless&lt;/span&gt;&lt;span style=&quot;font-size:10px; color:#6B7280;&quot;&gt;×41&lt;/span&gt;&lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;p&gt;My version of this card looks nothing like the example. I’ve been to 85 Goose shows, so the common picks are all caught and my staples list is now entirely rare bustouts. Crosseyed &amp;amp; Painless is the one that still won’t come.&lt;/p&gt;

&lt;h2 id=&quot;today-the-tour-recap-avalanche&quot;&gt;Today: The Tour Recap Avalanche&lt;/h2&gt;

&lt;p&gt;Today was insane. Sunday is wrap-up day, the morning after the run closed out, and we shipped the entire end-of-tour package in the span of about eighteen hours. Every show now gets a recap blurb generated from the live chat sentiment, weighted by which songs got the most love. Recaps use Opus when there’s actual chat content and skip cleanly when there isn’t, so we’re not paying tokens to summarize empty rooms. We backfilled recaps for older shows via a CLI, which means every Goose show on the platform now has a top-level recap blurb you can read.&lt;/p&gt;

&lt;p&gt;The Flow got a “For you” section that consolidates LIVE NOW, tonight’s shows, and tour recap into one place, with sparkles. Cards for archival recordings collapse into a trending card so the feed doesn’t get spammy when 40 people post the same Relisten link.&lt;/p&gt;

&lt;h3 id=&quot;how-the-bracket-gets-built&quot;&gt;How the bracket gets built&lt;/h3&gt;

&lt;p&gt;The end-of-tour jam tournament is downstream of the per-show recap pipeline, so the seeding isn’t editorial. It’s data.&lt;/p&gt;

&lt;p&gt;For every show, we run sentiment analysis over the live chat and weight by song. A song that triggers a flurry of fire emoji and “ARE YOU KIDDING ME”s reads as a heater. A song that gets polite acknowledgment doesn’t. That’s how we identify the jams that actually moved the room.&lt;/p&gt;

&lt;p&gt;Then we cross-reference each jam against historical setlist data. A song with a 31-show gap or a sub-10 lifetime play count is automatically a bustout candidate. A regularly-played rotation song needs the chat heat to carry it. The two signals combine into a per-show “jam score.”&lt;/p&gt;

&lt;p&gt;When tour wraps, we forward-link those jam scores into a tournament. The top sixteen become the bracket. Highest jam score gets the 1 seed, lowest gets the 16, and we run a March Madness style bracket: Round of 16, Quarters, Semis, Final. Voting opens for each round in sequence. The community decides the winner.&lt;/p&gt;

&lt;p&gt;Each matchup card has an inline audio player for each side, since you obviously need to hear both jams to vote between them. No second tab, no link to chase. Two sources are wired in: a soundboard recording if you have a paid streaming subscription, or the taper recording for free if you don’t. You always get audio for both. The card looks like this:&lt;/p&gt;

&lt;div style=&quot;background:#e5e2d9; padding:18px; border-radius:14px; margin:16px auto; max-width:800px; font-family:-apple-system,BlinkMacSystemFont,system-ui,sans-serif; color:#262626;&quot;&gt;
  &lt;div style=&quot;background:#F5F2EB; border-radius:18px; box-shadow:0 6px 20px rgba(0,0,0,0.08); overflow:hidden; max-width:760px; margin:0 auto;&quot;&gt;
    &lt;div style=&quot;background:#262626; color:#fff; padding:10px 16px; font-size:11px; letter-spacing:0.04em; text-transform:uppercase; display:flex; justify-content:space-between; align-items:center;&quot;&gt;
      &lt;span&gt;🏆 Goose Spring &apos;26 Bracket&lt;/span&gt;
      &lt;span style=&quot;opacity:0.65; font-weight:400; text-transform:none; letter-spacing:0;&quot;&gt;Round of 16 · 4 of 8 voted&lt;/span&gt;
    &lt;/div&gt;
    &lt;div style=&quot;padding:10px 14px 4px; font-size:10px; font-weight:800; letter-spacing:0.06em; color:#6B7280; text-transform:uppercase;&quot;&gt;Vote in this matchup&lt;/div&gt;

    &lt;div style=&quot;padding:8px 14px 14px;&quot;&gt;
      &lt;div style=&quot;background:#fff; border-radius:14px; padding:14px 14px 12px; box-shadow:0 1px 4px rgba(0,0,0,0.04);&quot;&gt;
        &lt;div style=&quot;display:grid; grid-template-columns:1fr auto 1fr; gap:8px; align-items:center;&quot;&gt;
          &lt;div style=&quot;display:flex; flex-direction:column; gap:2px; padding:8px 10px; background:#EDE9FE; border-radius:10px; cursor:pointer;&quot;&gt;
            &lt;div style=&quot;font-size:9px; font-weight:800; letter-spacing:0.05em; color:#8B5CF6;&quot;&gt;SEED 2&lt;/div&gt;
            &lt;div style=&quot;font-size:14px; font-weight:700; line-height:1.2;&quot;&gt;Tumble&lt;/div&gt;
            &lt;div style=&quot;font-size:10px; color:#6B7280; margin-top:2px;&quot;&gt;4/22 · Saenger, NOLA&lt;/div&gt;
          &lt;/div&gt;
          &lt;div style=&quot;font-size:11px; font-weight:800; color:#6B7280;&quot;&gt;VS&lt;/div&gt;
          &lt;div style=&quot;display:flex; flex-direction:column; gap:2px; padding:8px 10px; background:#F5F2EB; border:1px dashed #d1d5db; border-radius:10px; cursor:pointer;&quot;&gt;
            &lt;div style=&quot;font-size:9px; font-weight:800; letter-spacing:0.05em; color:#6B7280;&quot;&gt;SEED 7&lt;/div&gt;
            &lt;div style=&quot;font-size:14px; font-weight:700; line-height:1.2;&quot;&gt;Hungersite&lt;/div&gt;
            &lt;div style=&quot;font-size:10px; color:#6B7280; margin-top:2px;&quot;&gt;3/28 · Athens&lt;/div&gt;
          &lt;/div&gt;
        &lt;/div&gt;

        &lt;div style=&quot;margin-top:10px; padding:8px 10px; background:#F5F2EB; border-radius:10px; display:flex; align-items:center; gap:10px;&quot;&gt;
          &lt;span style=&quot;width:30px; height:30px; border-radius:50%; background:#262626; color:#fff; display:flex; align-items:center; justify-content:center; font-size:13px; flex-shrink:0;&quot;&gt;▶&lt;/span&gt;
          &lt;div style=&quot;display:flex; flex-direction:column; gap:2px; flex:1; min-width:0;&quot;&gt;
            &lt;span style=&quot;font-size:11px; font-weight:700;&quot;&gt;🎵 Tumble · 4/22 NOLA · soundboard&lt;/span&gt;
            &lt;div style=&quot;display:flex; align-items:center; gap:6px;&quot;&gt;
              &lt;span style=&quot;font-size:9px; color:#6B7280;&quot;&gt;3:14&lt;/span&gt;
              &lt;div style=&quot;height:3px; background:rgba(0,0,0,0.08); border-radius:2px; flex:1; overflow:hidden;&quot;&gt;&lt;div style=&quot;height:100%; width:18%; background:#8B5CF6;&quot;&gt;&lt;/div&gt;&lt;/div&gt;
              &lt;span style=&quot;font-size:9px; color:#6B7280;&quot;&gt;17:42&lt;/span&gt;
            &lt;/div&gt;
          &lt;/div&gt;
        &lt;/div&gt;

        &lt;div style=&quot;margin-top:6px; padding:8px 10px; background:#F5F2EB; border-radius:10px; display:flex; align-items:center; gap:10px;&quot;&gt;
          &lt;span style=&quot;width:30px; height:30px; border-radius:50%; background:#fff; border:2px solid #262626; color:#262626; display:flex; align-items:center; justify-content:center; font-size:13px; flex-shrink:0;&quot;&gt;▶&lt;/span&gt;
          &lt;div style=&quot;display:flex; flex-direction:column; gap:2px; flex:1; min-width:0;&quot;&gt;
            &lt;span style=&quot;font-size:11px; font-weight:700;&quot;&gt;🎵 Hungersite · 3/28 Athens · taper&lt;/span&gt;
            &lt;div style=&quot;display:flex; align-items:center; gap:6px;&quot;&gt;
              &lt;span style=&quot;font-size:9px; color:#6B7280;&quot;&gt;0:00&lt;/span&gt;
              &lt;div style=&quot;height:3px; background:rgba(0,0,0,0.08); border-radius:2px; flex:1; overflow:hidden;&quot;&gt;&lt;div style=&quot;height:100%; width:0%; background:#8B5CF6;&quot;&gt;&lt;/div&gt;&lt;/div&gt;
              &lt;span style=&quot;font-size:9px; color:#6B7280;&quot;&gt;12:08&lt;/span&gt;
            &lt;/div&gt;
          &lt;/div&gt;
        &lt;/div&gt;

        &lt;div style=&quot;margin-top:10px; display:flex; align-items:center; gap:8px;&quot;&gt;
          &lt;div style=&quot;display:flex;&quot;&gt;
            &lt;div style=&quot;width:20px; height:20px; border-radius:50%; background:linear-gradient(135deg,#ffd6b0,#f59e47); border:2px solid #fff; font-size:8px; color:#fff; display:flex; align-items:center; justify-content:center; font-weight:700; margin-right:-6px;&quot;&gt;P&lt;/div&gt;
            &lt;div style=&quot;width:20px; height:20px; border-radius:50%; background:linear-gradient(135deg,#b0eaff,#3ba8e0); border:2px solid #fff; font-size:8px; color:#fff; display:flex; align-items:center; justify-content:center; font-weight:700; margin-right:-6px;&quot;&gt;C&lt;/div&gt;
            &lt;div style=&quot;width:20px; height:20px; border-radius:50%; background:linear-gradient(135deg,#a7f3d0,#10b981); border:2px solid #fff; font-size:8px; color:#fff; display:flex; align-items:center; justify-content:center; font-weight:700;&quot;&gt;B&lt;/div&gt;
          &lt;/div&gt;
          &lt;span style=&quot;font-size:10px; color:#6B7280;&quot;&gt;3 voted for Tumble · 1 for Hungersite&lt;/span&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;

    &lt;div style=&quot;padding:6px 14px 4px; font-size:10px; font-weight:800; letter-spacing:0.06em; color:#6B7280; text-transform:uppercase;&quot;&gt;Full bracket&lt;/div&gt;
    &lt;div style=&quot;padding:6px 14px 18px; display:grid; grid-template-columns:1.4fr 1fr 1fr 0.8fr; gap:8px; overflow-x:auto;&quot;&gt;

      &lt;!-- Round of 16 (8 matchups) --&gt;
      &lt;div style=&quot;display:flex; flex-direction:column; gap:6px;&quot;&gt;
        &lt;div style=&quot;font-size:8px; font-weight:800; letter-spacing:0.06em; color:#9CA3AF; text-transform:uppercase;&quot;&gt;Round of 16&lt;/div&gt;
        &lt;div style=&quot;background:#fff; padding:6px 8px; border-radius:6px; font-size:10px;&quot;&gt;&lt;div style=&quot;font-weight:700;&quot;&gt;(1) Madhuvan ✓&lt;/div&gt;&lt;div style=&quot;color:#6B7280;&quot;&gt;vs Pancakes&lt;/div&gt;&lt;/div&gt;
        &lt;div style=&quot;background:#fff; padding:6px 8px; border-radius:6px; font-size:10px; border:2px solid #8B5CF6;&quot;&gt;&lt;div style=&quot;font-weight:700;&quot;&gt;(2) Tumble · 4/22&lt;/div&gt;&lt;div style=&quot;color:#8B5CF6; font-weight:600;&quot;&gt;vs (7) Hungersite — voting&lt;/div&gt;&lt;/div&gt;
        &lt;div style=&quot;background:#fff; padding:6px 8px; border-radius:6px; font-size:10px;&quot;&gt;&lt;div style=&quot;font-weight:700;&quot;&gt;(3) Atlas Dogs ✓&lt;/div&gt;&lt;div style=&quot;color:#6B7280;&quot;&gt;vs (6) Empress&lt;/div&gt;&lt;/div&gt;
        &lt;div style=&quot;background:#fff; padding:6px 8px; border-radius:6px; font-size:10px;&quot;&gt;&lt;div style=&quot;font-weight:700;&quot;&gt;(4) Into the Myst ✓&lt;/div&gt;&lt;div style=&quot;color:#6B7280;&quot;&gt;vs Doobie&lt;/div&gt;&lt;/div&gt;
        &lt;div style=&quot;background:#fff; padding:6px 8px; border-radius:6px; font-size:10px;&quot;&gt;&lt;div style=&quot;font-weight:700;&quot;&gt;(5) All I Need&lt;/div&gt;&lt;div style=&quot;color:#6B7280;&quot;&gt;vs (12) Borne — voting&lt;/div&gt;&lt;/div&gt;
        &lt;div style=&quot;background:#fff; padding:6px 8px; border-radius:6px; font-size:10px;&quot;&gt;&lt;div style=&quot;font-weight:700;&quot;&gt;(6) Arrow ✓&lt;/div&gt;&lt;div style=&quot;color:#6B7280;&quot;&gt;vs Travelers&lt;/div&gt;&lt;/div&gt;
        &lt;div style=&quot;background:#fff; padding:6px 8px; border-radius:6px; font-size:10px;&quot;&gt;&lt;div style=&quot;font-weight:700;&quot;&gt;(7) Echo of a Rose&lt;/div&gt;&lt;div style=&quot;color:#6B7280;&quot;&gt;vs Yeti — voting&lt;/div&gt;&lt;/div&gt;
        &lt;div style=&quot;background:#fff; padding:6px 8px; border-radius:6px; font-size:10px;&quot;&gt;&lt;div style=&quot;font-weight:700;&quot;&gt;(8) Indian River ✓&lt;/div&gt;&lt;div style=&quot;color:#6B7280;&quot;&gt;vs Creatures&lt;/div&gt;&lt;/div&gt;
      &lt;/div&gt;

      &lt;!-- Quarters --&gt;
      &lt;div style=&quot;display:flex; flex-direction:column; gap:6px; justify-content:space-around;&quot;&gt;
        &lt;div style=&quot;font-size:8px; font-weight:800; letter-spacing:0.06em; color:#9CA3AF; text-transform:uppercase;&quot;&gt;Quarters&lt;/div&gt;
        &lt;div style=&quot;background:#EDE9FE; padding:6px 8px; border-radius:6px; font-size:10px;&quot;&gt;&lt;div style=&quot;font-weight:700;&quot;&gt;Madhuvan&lt;/div&gt;&lt;div style=&quot;color:#6B7280;&quot;&gt;vs winner&lt;/div&gt;&lt;/div&gt;
        &lt;div style=&quot;background:#EDE9FE; padding:6px 8px; border-radius:6px; font-size:10px;&quot;&gt;&lt;div style=&quot;font-weight:700;&quot;&gt;Atlas Dogs&lt;/div&gt;&lt;div style=&quot;color:#6B7280;&quot;&gt;vs Into the Myst&lt;/div&gt;&lt;/div&gt;
        &lt;div style=&quot;background:#EDE9FE; padding:6px 8px; border-radius:6px; font-size:10px;&quot;&gt;&lt;div style=&quot;font-weight:700;&quot;&gt;TBD&lt;/div&gt;&lt;div style=&quot;color:#6B7280;&quot;&gt;vs Arrow&lt;/div&gt;&lt;/div&gt;
        &lt;div style=&quot;background:#EDE9FE; padding:6px 8px; border-radius:6px; font-size:10px;&quot;&gt;&lt;div style=&quot;font-weight:700;&quot;&gt;TBD&lt;/div&gt;&lt;div style=&quot;color:#6B7280;&quot;&gt;vs Indian River&lt;/div&gt;&lt;/div&gt;
      &lt;/div&gt;

      &lt;!-- Semis --&gt;
      &lt;div style=&quot;display:flex; flex-direction:column; gap:6px; justify-content:space-around;&quot;&gt;
        &lt;div style=&quot;font-size:8px; font-weight:800; letter-spacing:0.06em; color:#9CA3AF; text-transform:uppercase;&quot;&gt;Semis&lt;/div&gt;
        &lt;div style=&quot;background:#FCE7F3; padding:6px 8px; border-radius:6px; font-size:10px;&quot;&gt;&lt;div style=&quot;color:#6B7280;&quot;&gt;TBD&lt;/div&gt;&lt;div style=&quot;font-weight:700;&quot;&gt;vs TBD&lt;/div&gt;&lt;/div&gt;
        &lt;div style=&quot;background:#FCE7F3; padding:6px 8px; border-radius:6px; font-size:10px;&quot;&gt;&lt;div style=&quot;color:#6B7280;&quot;&gt;TBD&lt;/div&gt;&lt;div style=&quot;font-weight:700;&quot;&gt;vs TBD&lt;/div&gt;&lt;/div&gt;
      &lt;/div&gt;

      &lt;!-- Final --&gt;
      &lt;div style=&quot;display:flex; flex-direction:column; gap:6px; justify-content:center;&quot;&gt;
        &lt;div style=&quot;font-size:8px; font-weight:800; letter-spacing:0.06em; color:#9CA3AF; text-transform:uppercase;&quot;&gt;Final&lt;/div&gt;
        &lt;div style=&quot;background:#FEF3C7; padding:10px 8px; border-radius:6px; font-size:10px; text-align:center;&quot;&gt;&lt;div style=&quot;font-size:18px; margin-bottom:2px;&quot;&gt;🏆&lt;/div&gt;&lt;div style=&quot;color:#6B7280;&quot;&gt;Jam of the Tour&lt;/div&gt;&lt;div style=&quot;font-weight:700; font-size:11px; margin-top:2px;&quot;&gt;?&lt;/div&gt;&lt;/div&gt;
      &lt;/div&gt;

    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;p&gt;The community is voting now.&lt;/p&gt;

&lt;p&gt;The reason the bracket matters more than it might first appear is that the gap between tours is where most music apps die. The tour ends, the chat empties out, the lock-screen Live Activity goes dark, and people drift back to their normal feeds until the next run is announced. The jam tournament is a deliberate counter to that. Voting runs across multiple weeks, the rounds release on a schedule, and every matchup card pulls a real audio clip from a real show people went to, so listening to the bracket is also re-listening to the tour. The conversation in the chat doesn’t end when the lights come up in Irving. It keeps going through Round of 16, Quarters, Semis, Final, and by the time we crown a Jam of the Tour, the next run is already on the calendar and the muscle memory of opening the app every day is intact. The bracket is the bridge.&lt;/p&gt;

&lt;p&gt;The “all bands” UX got a final shape today too: the band dropdown collapsed into a BANDS card on Tour Stats, the All Bands directory got tuned-in count parity with band pages, Dead &amp;amp; Company switched to a “📜 Setlist archive” tour-status because they’re done touring, Billy Strings got a full historical import and a live setlist source, and a bunch of polish on Tour Completion, soundcheck-row dedup, encore-vs-Set-3 labeling, miracle ticket cards, and the rest of the long tail.&lt;/p&gt;

&lt;p&gt;The honest version of “today” is that we picked Sunday for the avalanche because the tour had just wrapped, the chat was still active, and any new bug would surface immediately. It worked. We shipped a thing, watched the chat react, and either fixed it or moved on inside an hour. Cycle repeated forty-ish times.&lt;/p&gt;

&lt;h2 id=&quot;so-many-bands&quot;&gt;So Many Bands&lt;/h2&gt;

&lt;p&gt;This was the tour where Zabriskie stopped being a Goose-and-Phish app.&lt;/p&gt;

&lt;p&gt;We added: &lt;strong&gt;Trey Anastasio Band 🎺, Mike Gordon Band 🌵, Umphrey’s McGee 🧢, Dead &amp;amp; Company 🌹, Billy Strings 🪕, Max Creek 🐥, King Gizzard &amp;amp; the Lizard Wizard 🧙, Radiohead 🐻, My Morning Jacket, Spafford, Dogs in a Pile, and Daniel Donato’s Cosmic Country.&lt;/strong&gt; Phish got rebranded to ⭕ and JRAD to ⚡ along the way.&lt;/p&gt;

&lt;p&gt;For each band we did the full thing: found a source for the historical setlists and backfilled them, wired up the band page with My Recent and Recent Shows with inline-expand setlists, and added it to the All Bands directory so people could actually find it. Dead &amp;amp; Company got the archival treatment because the band is done touring, and “Tune in” doesn’t make sense for a band you can’t tune into.&lt;/p&gt;

&lt;p&gt;We also built a “Request a band” escape hatch in onboarding for everyone whose band still isn’t here.&lt;/p&gt;

&lt;h2 id=&quot;onboarding-stopped-being-a-wall&quot;&gt;Onboarding Stopped Being a Wall&lt;/h2&gt;

&lt;p&gt;Onboarding got rebuilt around a simple idea: get to value in the first session. New users now see a first-run, show-aware prompt on &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/start&lt;/code&gt; that adapts to what’s happening that night. There’s a “Log past shows” step with a dedicated band picker, a tune-in tap with an explainer for what tuning in actually does, and a value-prop card that explains what the app is in one screen:&lt;/p&gt;

&lt;div style=&quot;background:#e5e2d9; padding:18px; border-radius:14px; margin:16px auto; max-width:520px; font-family:-apple-system,BlinkMacSystemFont,system-ui,sans-serif; color:#262626;&quot;&gt;
  &lt;div style=&quot;background:#F5F2EB; border-radius:18px; box-shadow:0 6px 20px rgba(0,0,0,0.08); overflow:hidden; max-width:380px; margin:0 auto;&quot;&gt;
    &lt;div style=&quot;background:linear-gradient(135deg,#8B5CF6 0%,#7c3aed 100%); color:#fff; padding:16px 18px;&quot;&gt;
      &lt;div style=&quot;font-size:11px; font-weight:700; letter-spacing:0.06em; text-transform:uppercase; opacity:0.85;&quot;&gt;Step 2 of 4&lt;/div&gt;
      &lt;div style=&quot;font-size:20px; font-weight:800; margin-top:6px; line-height:1.15;&quot;&gt;Which bands do you follow?&lt;/div&gt;
      &lt;div style=&quot;font-size:12px; opacity:0.85; margin-top:4px;&quot;&gt;Tune in to get setlists, live chat, and recap on every show.&lt;/div&gt;
    &lt;/div&gt;

    &lt;div style=&quot;padding:14px;&quot;&gt;
      &lt;div style=&quot;display:flex; flex-direction:column; gap:8px;&quot;&gt;
        &lt;div style=&quot;display:flex; align-items:center; gap:12px; padding:10px 12px; background:#fff; border-radius:12px; border:2px solid #8B5CF6;&quot;&gt;
          &lt;div style=&quot;width:34px; height:34px; border-radius:10px; background:linear-gradient(135deg,#a855f7,#7c3aed); display:flex; align-items:center; justify-content:center; font-size:18px;&quot;&gt;🪿&lt;/div&gt;
          &lt;div style=&quot;flex:1;&quot;&gt;&lt;div style=&quot;font-size:14px; font-weight:700;&quot;&gt;Goose&lt;/div&gt;&lt;div style=&quot;font-size:11px; color:#6B7280;&quot;&gt;14 shows this tour · 12 chomping tonight&lt;/div&gt;&lt;/div&gt;
          &lt;span style=&quot;font-size:11px; font-weight:800; padding:5px 10px; border-radius:10px; background:#8B5CF6; color:#fff;&quot;&gt;✓ Tuned in&lt;/span&gt;
        &lt;/div&gt;
        &lt;div style=&quot;display:flex; align-items:center; gap:12px; padding:10px 12px; background:#fff; border-radius:12px;&quot;&gt;
          &lt;div style=&quot;width:34px; height:34px; border-radius:10px; background:linear-gradient(135deg,#fb7185,#e11d48); display:flex; align-items:center; justify-content:center; font-size:18px;&quot;&gt;⭕&lt;/div&gt;
          &lt;div style=&quot;flex:1;&quot;&gt;&lt;div style=&quot;font-size:14px; font-weight:700;&quot;&gt;Phish&lt;/div&gt;&lt;div style=&quot;font-size:11px; color:#6B7280;&quot;&gt;At Sphere tonight · couch tour live&lt;/div&gt;&lt;/div&gt;
          &lt;span style=&quot;font-size:11px; font-weight:700; padding:5px 10px; border-radius:10px; background:#EDE9FE; color:#8B5CF6;&quot;&gt;+ Tune in&lt;/span&gt;
        &lt;/div&gt;
        &lt;div style=&quot;display:flex; align-items:center; gap:12px; padding:10px 12px; background:#fff; border-radius:12px;&quot;&gt;
          &lt;div style=&quot;width:34px; height:34px; border-radius:10px; background:linear-gradient(135deg,#fde68a,#f59e0b); display:flex; align-items:center; justify-content:center; font-size:18px;&quot;&gt;🌹&lt;/div&gt;
          &lt;div style=&quot;flex:1;&quot;&gt;&lt;div style=&quot;font-size:14px; font-weight:700;&quot;&gt;Dead &amp;amp; Company&lt;/div&gt;&lt;div style=&quot;font-size:11px; color:#6B7280;&quot;&gt;📜 Setlist archive · no upcoming shows&lt;/div&gt;&lt;/div&gt;
          &lt;span style=&quot;font-size:11px; font-weight:700; padding:5px 10px; border-radius:10px; background:#EDE9FE; color:#8B5CF6;&quot;&gt;+ Tune in&lt;/span&gt;
        &lt;/div&gt;
        &lt;div style=&quot;display:flex; align-items:center; gap:12px; padding:10px 12px; background:#fff; border-radius:12px;&quot;&gt;
          &lt;div style=&quot;width:34px; height:34px; border-radius:10px; background:linear-gradient(135deg,#a7f3d0,#10b981); display:flex; align-items:center; justify-content:center; font-size:18px;&quot;&gt;🪕&lt;/div&gt;
          &lt;div style=&quot;flex:1;&quot;&gt;&lt;div style=&quot;font-size:14px; font-weight:700;&quot;&gt;Billy Strings&lt;/div&gt;&lt;div style=&quot;font-size:11px; color:#6B7280;&quot;&gt;On tour · next show 4/29&lt;/div&gt;&lt;/div&gt;
          &lt;span style=&quot;font-size:11px; font-weight:700; padding:5px 10px; border-radius:10px; background:#EDE9FE; color:#8B5CF6;&quot;&gt;+ Tune in&lt;/span&gt;
        &lt;/div&gt;
      &lt;/div&gt;

      &lt;div style=&quot;margin-top:14px; padding:10px 12px; background:rgba(139,92,246,0.08); border-radius:10px; font-size:11px; color:#5b21b6; line-height:1.4;&quot;&gt;
        &lt;strong&gt;Don&apos;t see your band?&lt;/strong&gt; Tap below to request one and we&apos;ll get the full show history wired up.
      &lt;/div&gt;
      &lt;div style=&quot;margin-top:8px; font-size:11px; font-weight:700; color:#8B5CF6; text-align:center; padding:6px;&quot;&gt;Request a band →&lt;/div&gt;

      &lt;div style=&quot;margin-top:8px; display:flex; gap:8px;&quot;&gt;
        &lt;div style=&quot;flex:1; padding:12px; background:#fff; border:1px solid #d1d5db; border-radius:12px; text-align:center; font-size:13px; font-weight:700; color:#6B7280;&quot;&gt;Skip&lt;/div&gt;
        &lt;div style=&quot;flex:2; padding:12px; background:#8B5CF6; border-radius:12px; text-align:center; font-size:13px; font-weight:700; color:#fff;&quot;&gt;Continue →&lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;p&gt;We also widened the gate for who can sign up, because the Spring tour brought a lot of new people in.&lt;/p&gt;

&lt;p&gt;A big part of why onboarding actually got better instead of just getting a redesign is that Patrick was on the ground at every Goose show, putting the app in front of people he met at the bar, on the lot, in the seats next to him. He’d watch a brand-new user open the app cold, see exactly where they got stuck or confused, take notes on the friction in real time, and then file the fixes from the passenger seat on the drive to the next city. The “Log past shows” step, the explainer on what tuning in actually does, the value-prop card, the “Request a band” escape hatch, all of those came out of that loop. Not speculative redesigns. Each one came directly from watching a real person fail at the previous version, with the new version landing before the next show.&lt;/p&gt;

&lt;h2 id=&quot;goose-mode&quot;&gt;Goose Mode&lt;/h2&gt;

&lt;p&gt;We started building dedicated per-band “modes” this tour, basically a tour companion dashboard tailored to one band at a time. Both Goose Mode and Phish Mode shipped during the run. Each one knows its band’s calendar, color palette, and ritual vocabulary, and surfaces what matters for that specific community.&lt;/p&gt;

&lt;p&gt;The centerpiece is the Tour Timeline with a live countdown to the next show. Past shows you went to are checked off. Upcoming shows show date, weather, and which of your friends are going. The whole timeline is annotated with your crew dripping in and out of the run, so you can see at a glance who joined for which leg, who left after the southeast swing, who flew in for the closer. The countdown ticks every second:&lt;/p&gt;

&lt;div style=&quot;background:#e5e2d9; padding:18px; border-radius:14px; margin:16px auto; max-width:520px; font-family:-apple-system,BlinkMacSystemFont,system-ui,sans-serif; color:#262626;&quot;&gt;
  &lt;div style=&quot;background:#F5F2EB; border-radius:18px; box-shadow:0 6px 20px rgba(0,0,0,0.08); overflow:hidden; max-width:380px; margin:0 auto;&quot;&gt;
    &lt;div style=&quot;background:linear-gradient(135deg,#a855f7 0%,#7c3aed 100%); color:#fff; padding:14px 16px;&quot;&gt;
      &lt;div style=&quot;display:flex; align-items:center; gap:10px;&quot;&gt;
        &lt;div style=&quot;width:36px; height:36px; border-radius:10px; background:rgba(255,255,255,0.2); display:flex; align-items:center; justify-content:center; font-size:20px;&quot;&gt;🪿&lt;/div&gt;
        &lt;div style=&quot;flex:1;&quot;&gt;&lt;div style=&quot;font-size:11px; font-weight:700; letter-spacing:0.06em; text-transform:uppercase; opacity:0.85;&quot;&gt;Goose Mode&lt;/div&gt;&lt;div style=&quot;font-size:16px; font-weight:800;&quot;&gt;Spring &apos;26 · Texas Run&lt;/div&gt;&lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;

    &lt;div style=&quot;padding:14px 14px 6px; text-align:center; background:#fff;&quot;&gt;
      &lt;div style=&quot;font-size:9px; font-weight:800; letter-spacing:0.08em; color:#9CA3AF; text-transform:uppercase;&quot;&gt;Tonight · doors in&lt;/div&gt;
      &lt;div style=&quot;display:flex; justify-content:center; gap:8px; margin-top:6px;&quot;&gt;
        &lt;div style=&quot;background:#F5F2EB; padding:6px 10px; border-radius:8px; min-width:46px;&quot;&gt;&lt;div style=&quot;font-size:22px; font-weight:800; color:#7c3aed;&quot;&gt;01&lt;/div&gt;&lt;div style=&quot;font-size:9px; color:#6B7280; font-weight:700; letter-spacing:0.05em;&quot;&gt;HRS&lt;/div&gt;&lt;/div&gt;
        &lt;div style=&quot;background:#F5F2EB; padding:6px 10px; border-radius:8px; min-width:46px;&quot;&gt;&lt;div style=&quot;font-size:22px; font-weight:800; color:#7c3aed;&quot;&gt;23&lt;/div&gt;&lt;div style=&quot;font-size:9px; color:#6B7280; font-weight:700; letter-spacing:0.05em;&quot;&gt;MIN&lt;/div&gt;&lt;/div&gt;
        &lt;div style=&quot;background:#F5F2EB; padding:6px 10px; border-radius:8px; min-width:46px;&quot;&gt;&lt;div style=&quot;font-size:22px; font-weight:800; color:#7c3aed;&quot;&gt;04&lt;/div&gt;&lt;div style=&quot;font-size:9px; color:#6B7280; font-weight:700; letter-spacing:0.05em;&quot;&gt;SEC&lt;/div&gt;&lt;/div&gt;
      &lt;/div&gt;
      &lt;div style=&quot;font-size:11px; color:#6B7280; margin-top:8px;&quot;&gt;Bayou Music Center · Houston · 5 in your crew going&lt;/div&gt;
    &lt;/div&gt;

    &lt;div style=&quot;padding:14px 14px 4px; font-size:10px; font-weight:800; letter-spacing:0.06em; color:#6B7280; text-transform:uppercase;&quot;&gt;Tour Timeline · Crew&lt;/div&gt;

    &lt;div style=&quot;padding:0 14px 14px; display:flex; flex-direction:column; gap:6px;&quot;&gt;

      &lt;div style=&quot;display:flex; gap:10px; align-items:flex-start; padding:10px 12px; background:#fff; border-radius:10px; opacity:0.55;&quot;&gt;
        &lt;div style=&quot;display:flex; flex-direction:column; align-items:center; min-width:36px; padding-top:2px;&quot;&gt;&lt;span style=&quot;font-size:16px;&quot;&gt;✓&lt;/span&gt;&lt;span style=&quot;font-size:9px; color:#6B7280;&quot;&gt;4/19&lt;/span&gt;&lt;/div&gt;
        &lt;div style=&quot;flex:1;&quot;&gt;
          &lt;div style=&quot;font-size:13px; font-weight:600;&quot;&gt;St. Augustine Amphitheatre&lt;/div&gt;
          &lt;div style=&quot;font-size:10px; color:#6B7280;&quot;&gt;St. Augustine · couch toured&lt;/div&gt;
          &lt;div style=&quot;display:flex; align-items:center; gap:6px; margin-top:6px;&quot;&gt;
            &lt;div style=&quot;display:flex;&quot;&gt;
              &lt;div style=&quot;width:18px; height:18px; border-radius:50%; background:linear-gradient(135deg,#ffd6b0,#f59e47); border:2px solid #fff; font-size:8px; color:#fff; display:flex; align-items:center; justify-content:center; font-weight:700; margin-right:-5px;&quot;&gt;P&lt;/div&gt;
              &lt;div style=&quot;width:18px; height:18px; border-radius:50%; background:linear-gradient(135deg,#a7f3d0,#10b981); border:2px solid #fff; font-size:8px; color:#fff; display:flex; align-items:center; justify-content:center; font-weight:700;&quot;&gt;B&lt;/div&gt;
            &lt;/div&gt;
            &lt;span style=&quot;font-size:9px; color:#6B7280;&quot;&gt;2 in crew · Patrick joined the run here&lt;/span&gt;
          &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;

      &lt;div style=&quot;display:flex; gap:10px; align-items:flex-start; padding:10px 12px; background:#fff; border-radius:10px; opacity:0.55;&quot;&gt;
        &lt;div style=&quot;display:flex; flex-direction:column; align-items:center; min-width:36px; padding-top:2px;&quot;&gt;&lt;span style=&quot;font-size:16px;&quot;&gt;✓&lt;/span&gt;&lt;span style=&quot;font-size:9px; color:#6B7280;&quot;&gt;4/21&lt;/span&gt;&lt;/div&gt;
        &lt;div style=&quot;flex:1;&quot;&gt;
          &lt;div style=&quot;font-size:13px; font-weight:600;&quot;&gt;Saenger Theatre&lt;/div&gt;
          &lt;div style=&quot;font-size:10px; color:#6B7280;&quot;&gt;New Orleans · couch toured&lt;/div&gt;
          &lt;div style=&quot;display:flex; align-items:center; gap:6px; margin-top:6px;&quot;&gt;
            &lt;div style=&quot;display:flex;&quot;&gt;
              &lt;div style=&quot;width:18px; height:18px; border-radius:50%; background:linear-gradient(135deg,#ffd6b0,#f59e47); border:2px solid #fff; font-size:8px; color:#fff; display:flex; align-items:center; justify-content:center; font-weight:700; margin-right:-5px;&quot;&gt;P&lt;/div&gt;
              &lt;div style=&quot;width:18px; height:18px; border-radius:50%; background:linear-gradient(135deg,#b0eaff,#3ba8e0); border:2px solid #fff; font-size:8px; color:#fff; display:flex; align-items:center; justify-content:center; font-weight:700; margin-right:-5px;&quot;&gt;C&lt;/div&gt;
              &lt;div style=&quot;width:18px; height:18px; border-radius:50%; background:linear-gradient(135deg,#a7f3d0,#10b981); border:2px solid #fff; font-size:8px; color:#fff; display:flex; align-items:center; justify-content:center; font-weight:700; margin-right:-5px;&quot;&gt;B&lt;/div&gt;
              &lt;div style=&quot;width:18px; height:18px; border-radius:50%; background:linear-gradient(135deg,#ffc8d8,#ec4899); border:2px solid #fff; font-size:8px; color:#fff; display:flex; align-items:center; justify-content:center; font-weight:700;&quot;&gt;M&lt;/div&gt;
            &lt;/div&gt;
            &lt;span style=&quot;font-size:9px; color:#6B7280;&quot;&gt;4 in crew · Mwat &amp;amp; C joined&lt;/span&gt;
          &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;

      &lt;div style=&quot;display:flex; gap:10px; align-items:flex-start; padding:10px 12px; background:#fff; border-radius:10px; opacity:0.55;&quot;&gt;
        &lt;div style=&quot;display:flex; flex-direction:column; align-items:center; min-width:36px; padding-top:2px;&quot;&gt;&lt;span style=&quot;font-size:16px;&quot;&gt;✓&lt;/span&gt;&lt;span style=&quot;font-size:9px; color:#6B7280;&quot;&gt;4/22&lt;/span&gt;&lt;/div&gt;
        &lt;div style=&quot;flex:1;&quot;&gt;
          &lt;div style=&quot;font-size:13px; font-weight:600;&quot;&gt;Saenger Theatre&lt;/div&gt;
          &lt;div style=&quot;font-size:10px; color:#6B7280;&quot;&gt;New Orleans · couch toured · Tumble jam ranked #2&lt;/div&gt;
          &lt;div style=&quot;display:flex; align-items:center; gap:6px; margin-top:6px;&quot;&gt;
            &lt;div style=&quot;display:flex;&quot;&gt;
              &lt;div style=&quot;width:18px; height:18px; border-radius:50%; background:linear-gradient(135deg,#ffd6b0,#f59e47); border:2px solid #fff; font-size:8px; color:#fff; display:flex; align-items:center; justify-content:center; font-weight:700; margin-right:-5px;&quot;&gt;P&lt;/div&gt;
              &lt;div style=&quot;width:18px; height:18px; border-radius:50%; background:linear-gradient(135deg,#b0eaff,#3ba8e0); border:2px solid #fff; font-size:8px; color:#fff; display:flex; align-items:center; justify-content:center; font-weight:700; margin-right:-5px;&quot;&gt;C&lt;/div&gt;
              &lt;div style=&quot;width:18px; height:18px; border-radius:50%; background:linear-gradient(135deg,#a7f3d0,#10b981); border:2px solid #fff; font-size:8px; color:#fff; display:flex; align-items:center; justify-content:center; font-weight:700; margin-right:-5px;&quot;&gt;B&lt;/div&gt;
              &lt;div style=&quot;width:18px; height:18px; border-radius:50%; background:linear-gradient(135deg,#ffc8d8,#ec4899); border:2px solid #fff; font-size:8px; color:#fff; display:flex; align-items:center; justify-content:center; font-weight:700; margin-right:-5px;&quot;&gt;M&lt;/div&gt;
              &lt;div style=&quot;width:18px; height:18px; border-radius:50%; background:linear-gradient(135deg,#fde68a,#f59e0b); border:2px solid #fff; font-size:8px; color:#fff; display:flex; align-items:center; justify-content:center; font-weight:700;&quot;&gt;G&lt;/div&gt;
            &lt;/div&gt;
            &lt;span style=&quot;font-size:9px; color:#6B7280;&quot;&gt;5 in crew · Gmart joined for Texas leg&lt;/span&gt;
          &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;

      &lt;div style=&quot;display:flex; gap:10px; align-items:flex-start; padding:12px 12px; background:#fff; border-radius:10px; border:2px solid #7c3aed;&quot;&gt;
        &lt;div style=&quot;display:flex; flex-direction:column; align-items:center; min-width:36px; padding-top:2px;&quot;&gt;&lt;span style=&quot;font-size:18px;&quot;&gt;●&lt;/span&gt;&lt;span style=&quot;font-size:9px; color:#7c3aed; font-weight:800;&quot;&gt;4/23&lt;/span&gt;&lt;/div&gt;
        &lt;div style=&quot;flex:1;&quot;&gt;
          &lt;div style=&quot;font-size:13px; font-weight:700;&quot;&gt;Bayou Music Center · &lt;span style=&quot;color:#7c3aed;&quot;&gt;tonight&lt;/span&gt;&lt;/div&gt;
          &lt;div style=&quot;font-size:10px; color:#6B7280;&quot;&gt;Houston · 79°F clear · you&apos;re couch touring&lt;/div&gt;
          &lt;div style=&quot;display:flex; align-items:center; gap:6px; margin-top:6px;&quot;&gt;
            &lt;div style=&quot;display:flex;&quot;&gt;
              &lt;div style=&quot;width:18px; height:18px; border-radius:50%; background:linear-gradient(135deg,#ffd6b0,#f59e47); border:2px solid #fff; font-size:8px; color:#fff; display:flex; align-items:center; justify-content:center; font-weight:700; margin-right:-5px;&quot;&gt;P&lt;/div&gt;
              &lt;div style=&quot;width:18px; height:18px; border-radius:50%; background:linear-gradient(135deg,#a7f3d0,#10b981); border:2px solid #fff; font-size:8px; color:#fff; display:flex; align-items:center; justify-content:center; font-weight:700; margin-right:-5px;&quot;&gt;B&lt;/div&gt;
              &lt;div style=&quot;width:18px; height:18px; border-radius:50%; background:linear-gradient(135deg,#fde68a,#f59e0b); border:2px solid #fff; font-size:8px; color:#fff; display:flex; align-items:center; justify-content:center; font-weight:700; margin-right:-5px;&quot;&gt;G&lt;/div&gt;
              &lt;div style=&quot;width:18px; height:18px; border-radius:50%; background:linear-gradient(135deg,#d9c6ff,#a88fe6); border:2px solid #fff; font-size:8px; color:#fff; display:flex; align-items:center; justify-content:center; font-weight:700;&quot;&gt;L&lt;/div&gt;
            &lt;/div&gt;
            &lt;span style=&quot;font-size:9px; color:#6B7280;&quot;&gt;4 in crew · C left after NOLA · Lubby joined&lt;/span&gt;
          &lt;/div&gt;
        &lt;/div&gt;
        &lt;span style=&quot;font-size:11px; font-weight:800; padding:5px 10px; border-radius:10px; background:#7c3aed; color:#fff;&quot;&gt;RSVP&apos;d&lt;/span&gt;
      &lt;/div&gt;

      &lt;div style=&quot;display:flex; gap:10px; align-items:flex-start; padding:10px 12px; background:#fff; border-radius:10px;&quot;&gt;
        &lt;div style=&quot;display:flex; flex-direction:column; align-items:center; min-width:36px; padding-top:2px;&quot;&gt;&lt;span style=&quot;font-size:14px; color:#9CA3AF;&quot;&gt;○&lt;/span&gt;&lt;span style=&quot;font-size:9px; color:#6B7280;&quot;&gt;4/24&lt;/span&gt;&lt;/div&gt;
        &lt;div style=&quot;flex:1;&quot;&gt;
          &lt;div style=&quot;font-size:13px; font-weight:600;&quot;&gt;Moody Center&lt;/div&gt;
          &lt;div style=&quot;font-size:10px; color:#6B7280;&quot;&gt;Austin · in 1 day&lt;/div&gt;
          &lt;div style=&quot;display:flex; align-items:center; gap:6px; margin-top:6px;&quot;&gt;
            &lt;div style=&quot;display:flex;&quot;&gt;
              &lt;div style=&quot;width:18px; height:18px; border-radius:50%; background:linear-gradient(135deg,#ffd6b0,#f59e47); border:2px solid #fff; font-size:8px; color:#fff; display:flex; align-items:center; justify-content:center; font-weight:700; margin-right:-5px;&quot;&gt;P&lt;/div&gt;
              &lt;div style=&quot;width:18px; height:18px; border-radius:50%; background:linear-gradient(135deg,#fde68a,#f59e0b); border:2px solid #fff; font-size:8px; color:#fff; display:flex; align-items:center; justify-content:center; font-weight:700; margin-right:-5px;&quot;&gt;G&lt;/div&gt;
              &lt;div style=&quot;width:18px; height:18px; border-radius:50%; background:linear-gradient(135deg,#d9c6ff,#a88fe6); border:2px solid #fff; font-size:8px; color:#fff; display:flex; align-items:center; justify-content:center; font-weight:700;&quot;&gt;L&lt;/div&gt;
            &lt;/div&gt;
            &lt;span style=&quot;font-size:9px; color:#6B7280;&quot;&gt;3 in crew going · B sitting it out&lt;/span&gt;
          &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;

      &lt;div style=&quot;display:flex; gap:10px; align-items:flex-start; padding:10px 12px; background:#fff; border-radius:10px;&quot;&gt;
        &lt;div style=&quot;display:flex; flex-direction:column; align-items:center; min-width:36px; padding-top:2px;&quot;&gt;&lt;span style=&quot;font-size:14px; color:#9CA3AF;&quot;&gt;○&lt;/span&gt;&lt;span style=&quot;font-size:9px; color:#6B7280;&quot;&gt;4/25&lt;/span&gt;&lt;/div&gt;
        &lt;div style=&quot;flex:1;&quot;&gt;
          &lt;div style=&quot;font-size:13px; font-weight:600;&quot;&gt;Toyota Music Factory&lt;/div&gt;
          &lt;div style=&quot;font-size:10px; color:#6B7280;&quot;&gt;Irving · in 2 days · last night of Spring run&lt;/div&gt;
          &lt;div style=&quot;display:flex; align-items:center; gap:6px; margin-top:6px;&quot;&gt;
            &lt;div style=&quot;display:flex;&quot;&gt;
              &lt;div style=&quot;width:18px; height:18px; border-radius:50%; background:linear-gradient(135deg,#ffd6b0,#f59e47); border:2px solid #fff; font-size:8px; color:#fff; display:flex; align-items:center; justify-content:center; font-weight:700; margin-right:-5px;&quot;&gt;P&lt;/div&gt;
              &lt;div style=&quot;width:18px; height:18px; border-radius:50%; background:linear-gradient(135deg,#a7f3d0,#10b981); border:2px solid #fff; font-size:8px; color:#fff; display:flex; align-items:center; justify-content:center; font-weight:700; margin-right:-5px;&quot;&gt;B&lt;/div&gt;
              &lt;div style=&quot;width:18px; height:18px; border-radius:50%; background:linear-gradient(135deg,#fde68a,#f59e0b); border:2px solid #fff; font-size:8px; color:#fff; display:flex; align-items:center; justify-content:center; font-weight:700; margin-right:-5px;&quot;&gt;G&lt;/div&gt;
              &lt;div style=&quot;width:18px; height:18px; border-radius:50%; background:linear-gradient(135deg,#d9c6ff,#a88fe6); border:2px solid #fff; font-size:8px; color:#fff; display:flex; align-items:center; justify-content:center; font-weight:700; margin-right:-5px;&quot;&gt;L&lt;/div&gt;
              &lt;div style=&quot;width:18px; height:18px; border-radius:50%; background:linear-gradient(135deg,#ffc8d8,#ec4899); border:2px solid #fff; font-size:8px; color:#fff; display:flex; align-items:center; justify-content:center; font-weight:700;&quot;&gt;Q&lt;/div&gt;
            &lt;/div&gt;
            &lt;span style=&quot;font-size:9px; color:#6B7280;&quot;&gt;5 in crew · Q flew in for the closer&lt;/span&gt;
          &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;h2 id=&quot;miracle-tickets&quot;&gt;Miracle Tickets&lt;/h2&gt;

&lt;p&gt;A long jam band tradition: someone has an extra ticket and gives it away, no questions, sometimes for nothing, sometimes for a smile. We built that into the app as a first-class feature this tour. If you have an extra, you post it as a Miracle Ticket attached to the show. People can request it. The owner draws a winner based on engagement (so the lurker who never participates doesn’t beat the regular who’s been in the chat all run). Any admin can also award on the owner’s behalf if they’re not online. Cards are collapsible and grouped by show, so on a busy night the feed doesn’t drown:&lt;/p&gt;

&lt;div style=&quot;background:#e5e2d9; padding:18px; border-radius:14px; margin:16px auto; max-width:520px; font-family:-apple-system,BlinkMacSystemFont,system-ui,sans-serif; color:#262626;&quot;&gt;
  &lt;div style=&quot;background:#F5F2EB; border-radius:18px; box-shadow:0 6px 20px rgba(0,0,0,0.08); overflow:hidden; max-width:380px; margin:0 auto;&quot;&gt;
    &lt;div style=&quot;background:#262626; color:#fff; padding:10px 16px; font-size:11px; letter-spacing:0.04em; text-transform:uppercase; display:flex; justify-content:space-between; align-items:center;&quot;&gt;
      &lt;span&gt;☝️ 🎫 Miracle Tickets&lt;/span&gt;
      &lt;span style=&quot;opacity:0.65; font-weight:400; text-transform:none; letter-spacing:0;&quot;&gt;2 open&lt;/span&gt;
    &lt;/div&gt;

    &lt;div style=&quot;padding:12px 14px;&quot;&gt;
      &lt;div style=&quot;background:#fff; border-radius:14px; padding:14px 14px 12px; box-shadow:0 1px 4px rgba(0,0,0,0.04);&quot;&gt;
        &lt;div style=&quot;display:flex; gap:10px; align-items:center; margin-bottom:10px;&quot;&gt;
          &lt;div style=&quot;width:38px; height:38px; border-radius:10px; background:linear-gradient(135deg,#a855f7,#7c3aed); display:flex; align-items:center; justify-content:center; font-size:18px;&quot;&gt;🪿&lt;/div&gt;
          &lt;div style=&quot;flex:1;&quot;&gt;&lt;div style=&quot;font-size:13px; font-weight:700;&quot;&gt;Goose · 4/22&lt;/div&gt;&lt;div style=&quot;font-size:10px; color:#6B7280;&quot;&gt;Saenger Theatre · New Orleans&lt;/div&gt;&lt;/div&gt;
          &lt;span style=&quot;font-size:10px; padding:3px 8px; border-radius:8px; background:#FEF3C7; color:#92400E; font-weight:700;&quot;&gt;OPEN&lt;/span&gt;
        &lt;/div&gt;
        &lt;div style=&quot;font-size:13px; line-height:1.4; color:#262626; padding:10px 12px; background:#F5F2EB; border-radius:10px;&quot;&gt;&quot;Have an extra GA, can meet at the box office at 7. Just want it to go to someone who&apos;ll love it. 🌹&quot;&lt;/div&gt;
        &lt;div style=&quot;display:flex; align-items:center; gap:8px; margin-top:10px;&quot;&gt;
          &lt;div style=&quot;width:24px; height:24px; border-radius:50%; background:linear-gradient(135deg,#ffd6b0,#f59e47); display:flex; align-items:center; justify-content:center; font-size:11px; font-weight:700; color:#fff;&quot;&gt;P&lt;/div&gt;
          &lt;span style=&quot;font-size:11px; color:#6B7280;&quot;&gt;posted by patrick · 2h ago · 7 entries&lt;/span&gt;
        &lt;/div&gt;
        &lt;div style=&quot;display:flex; gap:6px; margin-top:10px;&quot;&gt;
          &lt;div style=&quot;flex:1; padding:8px; background:#fff; border:1px solid #d1d5db; border-radius:10px; text-align:center; font-size:12px; font-weight:700; color:#6B7280;&quot;&gt;View entries&lt;/div&gt;
          &lt;div style=&quot;flex:1; padding:8px; background:#EC4899; border-radius:10px; text-align:center; font-size:12px; font-weight:700; color:#fff;&quot;&gt;🙏 Enter to win&lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;

    &lt;div style=&quot;padding:0 14px 14px;&quot;&gt;
      &lt;div style=&quot;background:#fff; border-radius:14px; padding:14px 14px 12px; box-shadow:0 1px 4px rgba(0,0,0,0.04); opacity:0.7;&quot;&gt;
        &lt;div style=&quot;display:flex; gap:10px; align-items:center; margin-bottom:8px;&quot;&gt;
          &lt;div style=&quot;width:38px; height:38px; border-radius:10px; background:linear-gradient(135deg,#fb7185,#e11d48); display:flex; align-items:center; justify-content:center; font-size:18px;&quot;&gt;⭕&lt;/div&gt;
          &lt;div style=&quot;flex:1;&quot;&gt;&lt;div style=&quot;font-size:13px; font-weight:700;&quot;&gt;Phish · 4/23 · Sphere&lt;/div&gt;&lt;div style=&quot;font-size:10px; color:#6B7280;&quot;&gt;Las Vegas · awarded&lt;/div&gt;&lt;/div&gt;
          &lt;span style=&quot;font-size:10px; padding:3px 8px; border-radius:8px; background:#D1FAE5; color:#065F46; font-weight:700;&quot;&gt;CLOSED&lt;/span&gt;
        &lt;/div&gt;
        &lt;div style=&quot;display:flex; align-items:center; gap:8px; font-size:11px; color:#6B7280;&quot;&gt;
          &lt;span style=&quot;font-size:14px;&quot;&gt;🎉&lt;/span&gt;
          &lt;span&gt;went to &lt;strong style=&quot;color:#262626;&quot;&gt;@chomper4&lt;/strong&gt; · drawn by engagement&lt;/span&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;h2 id=&quot;everything-else-worth-mentioning&quot;&gt;Everything Else Worth Mentioning&lt;/h2&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;Seat sharing.&lt;/strong&gt; A discovery screen, show-card badges, avatar-x to unshare, per-show seat sharing with @mentions in the crew chat, and seat-share notifications that show the seat inline instead of dumping into live chat.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Live Recording posts&lt;/strong&gt; with a smart provider cascade and an autocomplete dropdown for the artist field.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Show notes&lt;/strong&gt; on every expanded setlist, not just Chomp. Plus a Show Notes editor on manage-setlist for admins.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;The Live tab.&lt;/strong&gt; Promoted out of the compass into its own dedicated nav slot, with an always-open compass replacing it.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;what-the-chat-said-at-the-end&quot;&gt;What the Chat Said at the End&lt;/h2&gt;

&lt;p&gt;There was a moment in Irving last night, near the encore, that I want to keep. I’m pulling these straight from the chat, anonymized:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;&lt;em&gt;(Patrick)&lt;/em&gt; Man. end of tour is always so bittersweet. Great run of shows though. Damn, Goose, ya got me good&lt;/p&gt;

  &lt;p&gt;&lt;em&gt;(a chomper)&lt;/em&gt; this app is the bees. I am very grateful for it.&lt;/p&gt;

  &lt;p&gt;&lt;em&gt;(another chomper)&lt;/em&gt; Thank you for the invite. It’s a super cool concept and appreciate all your efforts!&lt;/p&gt;

  &lt;p&gt;&lt;em&gt;(another)&lt;/em&gt; Awesome show, thanks for the hangs. We will hopefully see some of you in Toronto!&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That is the whole point. That is the third place. People who started the tour as usernames in a beta group, sending each other Eminence-chase updates and bustout calls and gummy timing notes, ending the tour planning to meet in Toronto and trading European tour stops. One of them listed his run: Brixton, Brussels, Amsterdam, Paris. Patrick offered to add him to a crew. Real people, real plans, made through an app we started building eight weeks ago.&lt;/p&gt;

&lt;h2 id=&quot;what-this-tour-taught-me&quot;&gt;What This Tour Taught Me&lt;/h2&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;The app holds up under live load now.&lt;/strong&gt; Since I &lt;a href=&quot;/ai/zabriskie/development/2026/03/29/the-show-is-happening-right-now-and-nothing-works.html&quot;&gt;wrote about the iOS 26 .then() proxy disaster&lt;/a&gt;, Live Activities have stayed up through every single show. Nobody filed a “the lock screen is dead” bug for the rest of the tour. That’s the most boring victory of the month, and it’s the one I’m proudest of.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Two shows a night is a cheat code.&lt;/strong&gt; Goose on Eastern from the hotel couch, Phish at Sphere on Pacific from my seat. Patrick on the ground at Goose, me at Phish, both of us in the chomp on whichever show wasn’t ours. Anything that broke at 9pm Eastern got fixed before the next song started at Phish. You cannot manufacture that kind of feedback loop on purpose. We got lucky with the calendar and we used it.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;The community runs faster than the app.&lt;/strong&gt; Song calls is the clearest example I can point to: on 4/19 a chomper said in chat, “Would be cool if you could program it to where when one of us guesses the song in shows everyone fun Lil game. Idk how hard that would be to do tho hahs.” Five days later that feature shipped end to end. The whole tour was full of small versions of this loop, where someone says “we need X” in the chat and a few days later a version of X is in the app. Building this with the people who use it is dramatically faster than building it from a spec.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Three hundred PRs in a month is not normal and I should not pretend it is.&lt;/strong&gt; Most of the code was written by Claude. A lot of it had to be re-written by Claude after I caught it doing something wrong. Every sharp edge from the tour got logged into the agent reliability dataset I’ve been building, and I’ll keep writing about it separately. The point of building in public with an AI assistant is that the failures are part of the dataset, not embarrassments to hide.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Goose Cabo is in a few weeks. Goose Summer is right after. I have a list. The app is not done.&lt;/p&gt;

&lt;p&gt;In the meantime, if you were on tour, thanks for chomping. If you were at home, thanks for being in the chat. If you’ve never tried the app and the third place I keep talking about sounds like something you’d want, come find us. The next show is already on the calendar.&lt;/p&gt;

&lt;hr /&gt;

&lt;p&gt;&lt;em&gt;One housekeeping note: the screens in this post are mockups, not actual app screenshots, drawn to make each feature legible in context. The real app looks slightly different on iOS vs. Android (Live Activities vs. ongoing notifications, system fonts, badge styling, the typing indicator’s exact pulse), and the mockups smooth those over for readability. The features themselves all shipped.&lt;/em&gt;&lt;/p&gt;
</description>
				<pubDate>Sun, 26 Apr 2026 19:00:00 +0000</pubDate>
				<link>https://christophermeiklejohn.com/ai/zabriskie/development/2026/04/26/spring-tour-recap.html</link>
				<guid isPermaLink="true">https://christophermeiklejohn.com/ai/zabriskie/development/2026/04/26/spring-tour-recap.html</guid>
			</item>
		
			<item>
				<title>Getting Up to Speed on Multi-Agent Systems, Part 3: Wave 1 (Can Agents Coordinate At All?)</title>
				<description>&lt;p&gt;Wave 1 is the cluster of papers from 2023 that people actually cite. When someone says “I read the multi-agent papers,” they usually mean these. In this post I’m going to walk through the canonical five, explain what each one actually builds, and show where they agree and where they quietly disagree with each other.&lt;/p&gt;

&lt;div class=&quot;mas-series-nav&quot;&gt;
  &lt;div class=&quot;mas-series-label&quot;&gt;Getting Up to Speed on MAS&lt;/div&gt;
  &lt;ol&gt;&lt;li&gt;&lt;a href=&quot;/ai/agents/mas-series/2026/04/24/mas-series-01-the-landscape.html&quot;&gt;Part 1. The Landscape&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;/ai/agents/mas-series/2026/04/25/mas-series-02-the-vocabulary.html&quot;&gt;Part 2. The Vocabulary&lt;/a&gt;&lt;/li&gt;&lt;li class=&quot;mas-current&quot;&gt;&lt;strong&gt;Part 3. Wave 1: Can Agents Coordinate At All? (you are here)&lt;/strong&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;/ai/agents/mas-series/2026/04/27/mas-series-04-wave-two.html&quot;&gt;Part 4. Wave 2: Why It Breaks&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;/ai/agents/mas-series/2026/04/28/mas-series-05-debate-state-coordination.html&quot;&gt;Part 5. Debate, State, and Coordination&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;/ai/agents/mas-series/2026/04/29/mas-series-06-verification-patterns.html&quot;&gt;Part 6. Verification Patterns&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;/ai/agents/mas-series/2026/04/30/mas-series-07-benchmarks.html&quot;&gt;Part 7. Benchmarks and What They Miss&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;/ai/agents/mas-series/2026/05/01/mas-series-08-open-questions.html&quot;&gt;Part 8. Open Questions&lt;/a&gt;&lt;/li&gt;&lt;/ol&gt;
&lt;/div&gt;

&lt;h2 id=&quot;camel-two-agents-role-playing&quot;&gt;CAMEL: Two Agents Role-Playing&lt;/h2&gt;

&lt;div class=&quot;mas-paper-card mas-camel&quot;&gt;
  &lt;div class=&quot;mas-card-title&quot;&gt;
    &lt;strong&gt;CAMEL: Communicative Agents for &quot;Mind&quot; Exploration&lt;/strong&gt;
    &lt;span class=&quot;mas-card-meta&quot;&gt;&lt;a href=&quot;https://arxiv.org/abs/2303.17760&quot;&gt;arXiv 2303.17760&lt;/a&gt; · NeurIPS 2023&lt;/span&gt;
  &lt;/div&gt;
  &lt;p class=&quot;mas-card-oneliner&quot;&gt;Two LLMs role-play until the task is done.&lt;/p&gt;
  &lt;div class=&quot;mas-card-bet&quot;&gt;Core bet: Prompt constraints keep agents on task&lt;/div&gt;
  &lt;div&gt;
    &lt;span class=&quot;mas-tag&quot;&gt;2 agents&lt;/span&gt;
    &lt;span class=&quot;mas-tag&quot;&gt;role-play&lt;/span&gt;
    &lt;span class=&quot;mas-tag&quot;&gt;inception prompting&lt;/span&gt;
  &lt;/div&gt;
  &lt;ul&gt;
    &lt;li&gt;AI User (instructor) and AI Assistant in a structured dialogue loop&lt;/li&gt;
    &lt;li&gt;Inception prompting: symmetric system prompts with explicit constraints like &quot;Never flip roles&quot;&lt;/li&gt;
    &lt;li&gt;Task specifier agent elaborates vague human input into concrete tasks&lt;/li&gt;
    &lt;li&gt;Documented failure modes: role flipping, instruction repetition, vague responses, conversational loops&lt;/li&gt;
    &lt;li&gt;All mitigations are prompt-level, no structural enforcement&lt;/li&gt;
  &lt;/ul&gt;
&lt;/div&gt;

&lt;p&gt;CAMEL is the simplest of the wave-1 papers and also the most honest. It’s two LLMs. One plays a user, one plays an assistant. They talk. The paper’s main contribution is inception prompting, which is a way of writing system prompts that keep agents from breaking character. The failure modes the paper documents are the failure modes you’d expect: agents flip roles, agents repeat themselves, agents give vague answers.&lt;/p&gt;

&lt;p&gt;What’s missing from CAMEL is any structural enforcement. If the agent flips roles, nothing stops it except a prompt instruction that says “don’t flip roles.” There’s no protocol-level guarantee. This is a pattern you’ll see repeated across wave-1: trust the prompt, hope for the best.&lt;/p&gt;

&lt;h2 id=&quot;generative-agents-memory-reflection-and-planning&quot;&gt;Generative Agents: Memory, Reflection, and Planning&lt;/h2&gt;

&lt;div class=&quot;mas-paper-card mas-genagents&quot;&gt;
  &lt;div class=&quot;mas-card-title&quot;&gt;
    &lt;strong&gt;Generative Agents: Interactive Simulacra of Human Behavior&lt;/strong&gt;
    &lt;span class=&quot;mas-card-meta&quot;&gt;&lt;a href=&quot;https://arxiv.org/abs/2304.03442&quot;&gt;arXiv 2304.03442&lt;/a&gt; · UIST 2023&lt;/span&gt;
  &lt;/div&gt;
  &lt;p class=&quot;mas-card-oneliner&quot;&gt;Give agents memory, reflection, and planning so they behave believably over time.&lt;/p&gt;
  &lt;div class=&quot;mas-card-bet&quot;&gt;Core bet: Retrieval scoring produces believable behavior&lt;/div&gt;
  &lt;div&gt;
    &lt;span class=&quot;mas-tag&quot;&gt;25 agents&lt;/span&gt;
    &lt;span class=&quot;mas-tag&quot;&gt;memory stream&lt;/span&gt;
    &lt;span class=&quot;mas-tag&quot;&gt;reflection&lt;/span&gt;
    &lt;span class=&quot;mas-tag&quot;&gt;planning&lt;/span&gt;
  &lt;/div&gt;
  &lt;ul&gt;
    &lt;li&gt;Memory stream: every observation stored with timestamp and importance score (LLM-rated 1 to 10)&lt;/li&gt;
    &lt;li&gt;Retrieval: weighted sum of recency (exponential decay), relevance (cosine similarity), and importance&lt;/li&gt;
    &lt;li&gt;Reflection: triggered when accumulated importance crosses a threshold (roughly 2-3 times per day)&lt;/li&gt;
    &lt;li&gt;Planning: top-down recursive (day, hour, 5-15 minute blocks); replans on unexpected events&lt;/li&gt;
    &lt;li&gt;Emergent behaviors: a Valentine&apos;s Day party self-organized from one suggestion; info diffusion from 4 percent to 32 percent awareness in two game days&lt;/li&gt;
  &lt;/ul&gt;
&lt;/div&gt;

&lt;p&gt;This paper is the outlier in wave-1, because it isn’t trying to build software. It’s a social simulation. 25 agents living in a Sims-style town. What makes it interesting for multi-agent systems is that it’s the only wave-1 paper that takes memory seriously. Every observation gets stored with a timestamp and an importance score. When an agent needs to act, it retrieves memories using a weighted combination of recency, relevance, and importance. Reflections are higher-level thoughts synthesized from clusters of observations.&lt;/p&gt;

&lt;p&gt;None of the software engineering papers in wave-1 do anything like this. They don’t need to, because their tasks have clear start and end conditions. But when you look at what production multi-agent systems are starting to need, the Generative Agents architecture has more of the right pieces than MetaGPT does.&lt;/p&gt;

&lt;h2 id=&quot;chatdev-pairwise-chat-as-a-software-pipeline&quot;&gt;ChatDev: Pairwise Chat as a Software Pipeline&lt;/h2&gt;

&lt;div class=&quot;mas-paper-card mas-chatdev&quot;&gt;
  &lt;div class=&quot;mas-card-title&quot;&gt;
    &lt;strong&gt;ChatDev: Communicative Agents for Software Development&lt;/strong&gt;
    &lt;span class=&quot;mas-card-meta&quot;&gt;&lt;a href=&quot;https://arxiv.org/abs/2307.07924&quot;&gt;arXiv 2307.07924&lt;/a&gt;&lt;/span&gt;
  &lt;/div&gt;
  &lt;p class=&quot;mas-card-oneliner&quot;&gt;Chain pairwise dialogues into a software development pipeline.&lt;/p&gt;
  &lt;div class=&quot;mas-card-bet&quot;&gt;Core bet: Dialogue convergence equals correct output&lt;/div&gt;
  &lt;div&gt;
    &lt;span class=&quot;mas-tag&quot;&gt;pairwise chat&lt;/span&gt;
    &lt;span class=&quot;mas-tag&quot;&gt;phase pipeline&lt;/span&gt;
    &lt;span class=&quot;mas-tag&quot;&gt;dehallucination&lt;/span&gt;
  &lt;/div&gt;
  &lt;ul&gt;
    &lt;li&gt;Fixed pipeline: Design, then Coding, then Testing; each phase is an instructor-assistant dialogue&lt;/li&gt;
    &lt;li&gt;Communicative dehallucination: the assistant flips role and asks clarifying questions before committing to an answer&lt;/li&gt;
    &lt;li&gt;Short-term memory (full dialogue within a phase) and long-term memory (extracted solutions across phases)&lt;/li&gt;
    &lt;li&gt;Termination: 10 rounds max, or two consecutive rounds without changes&lt;/li&gt;
    &lt;li&gt;No escalation path when convergence fails, it just stops&lt;/li&gt;
  &lt;/ul&gt;
&lt;/div&gt;

&lt;p&gt;ChatDev is the paper that got me into this literature in the first place. It’s a pipeline of pairwise dialogues. Pairs of agents talk about design, then pairs of agents talk about coding, then pairs of agents talk about testing. The most interesting mechanism is communicative dehallucination, which is a prompt pattern where the assistant asks clarifying questions before answering. This is the closest any wave-1 paper gets to backpressure.&lt;/p&gt;

&lt;p&gt;The structural problem with ChatDev is that when agents can’t converge in 10 rounds, the system just stops. There’s no fallback. No mechanism for the system to notice that it’s stuck and escalate. No concurrency control on the shared artifacts. I wrote about &lt;a href=&quot;/ai/agents/distributed/zabriskie/2026/03/30/multi-agent-systems-have-a-distributed-systems-problem.html&quot;&gt;how this breaks down in practice&lt;/a&gt; a few weeks ago.&lt;/p&gt;

&lt;h2 id=&quot;metagpt-structured-artifacts-and-test-execution&quot;&gt;MetaGPT: Structured Artifacts and Test Execution&lt;/h2&gt;

&lt;div class=&quot;mas-paper-card mas-metagpt&quot;&gt;
  &lt;div class=&quot;mas-card-title&quot;&gt;
    &lt;strong&gt;MetaGPT: Meta Programming for Multi-Agent Collaborative Framework&lt;/strong&gt;
    &lt;span class=&quot;mas-card-meta&quot;&gt;&lt;a href=&quot;https://arxiv.org/abs/2308.00352&quot;&gt;arXiv 2308.00352&lt;/a&gt; · ICLR 2024 Oral&lt;/span&gt;
  &lt;/div&gt;
  &lt;p class=&quot;mas-card-oneliner&quot;&gt;Replace dialogue with structured documents and real test execution.&lt;/p&gt;
  &lt;div class=&quot;mas-card-bet&quot;&gt;Core bet: Schemas plus passing tests equal correct output&lt;/div&gt;
  &lt;div&gt;
    &lt;span class=&quot;mas-tag&quot;&gt;5 roles&lt;/span&gt;
    &lt;span class=&quot;mas-tag&quot;&gt;artifact pub-sub&lt;/span&gt;
    &lt;span class=&quot;mas-tag&quot;&gt;executable feedback&lt;/span&gt;
  &lt;/div&gt;
  &lt;ul&gt;
    &lt;li&gt;Roles: PM, Architect, Project Manager, Engineer, QA (waterfall)&lt;/li&gt;
    &lt;li&gt;No dialogue; agents produce structured documents (PRD, system design, task list, code, tests)&lt;/li&gt;
    &lt;li&gt;Pub-sub message pool: agents publish artifacts, subscribe by role to relevant messages&lt;/li&gt;
    &lt;li&gt;Executable feedback: unit tests actually run; failures trigger up to 3 retries referencing PRD and design docs&lt;/li&gt;
    &lt;li&gt;Results: 85.9 percent Pass@1 on HumanEval, 100 percent task completion, 0.83 human revisions vs ChatDev&apos;s 2.5&lt;/li&gt;
  &lt;/ul&gt;
&lt;/div&gt;

&lt;p&gt;MetaGPT is the most ambitious wave-1 paper. Instead of dialogue, the agents produce structured documents. Instead of relying on agents to agree, the framework executes tests. The paper’s strongest claim is the structural one: if agents produce artifacts with defined schemas, and those artifacts are validated by execution, you get better coordination than you get from unconstrained dialogue.&lt;/p&gt;

&lt;p&gt;I think that claim holds up. But the coordination model is still based on a shared mutable pool, which is the same thing &lt;a href=&quot;https://docs.riak.com/riak/kv/latest/learn/concepts/causal-context/index.html&quot;&gt;Riak&lt;/a&gt; solved twenty years ago with version vectors. MetaGPT doesn’t have that. Agents publish to the pool. Agents subscribe. Nobody tracks causality.&lt;/p&gt;

&lt;h2 id=&quot;autogen-a-framework-not-a-system&quot;&gt;AutoGen: A Framework, Not a System&lt;/h2&gt;

&lt;div class=&quot;mas-paper-card mas-autogen&quot;&gt;
  &lt;div class=&quot;mas-card-title&quot;&gt;
    &lt;strong&gt;AutoGen: Next-Gen LLM Applications via Multi-Agent Conversation&lt;/strong&gt;
    &lt;span class=&quot;mas-card-meta&quot;&gt;&lt;a href=&quot;https://arxiv.org/abs/2308.08155&quot;&gt;arXiv 2308.08155&lt;/a&gt;&lt;/span&gt;
  &lt;/div&gt;
  &lt;p class=&quot;mas-card-oneliner&quot;&gt;A configurable framework. Build whatever multi-agent system you want.&lt;/p&gt;
  &lt;div class=&quot;mas-card-bet&quot;&gt;Core bet: Developers will build the right topology&lt;/div&gt;
  &lt;div&gt;
    &lt;span class=&quot;mas-tag&quot;&gt;framework&lt;/span&gt;
    &lt;span class=&quot;mas-tag&quot;&gt;ConversableAgent&lt;/span&gt;
    &lt;span class=&quot;mas-tag&quot;&gt;GroupChat&lt;/span&gt;
    &lt;span class=&quot;mas-tag&quot;&gt;human-in-loop&lt;/span&gt;
  &lt;/div&gt;
  &lt;ul&gt;
    &lt;li&gt;ConversableAgent base class: any entity that sends and receives messages (LLM, human, tool, code executor)&lt;/li&gt;
    &lt;li&gt;Pluggable reply functions via register_reply(); agent behavior is what it does when it gets a message&lt;/li&gt;
    &lt;li&gt;GroupChatManager: selects next speaker via LLM role-play prompting or an FSM&lt;/li&gt;
    &lt;li&gt;Human-in-the-loop as a dial: per-agent config of ALWAYS, SOMETIMES, or NEVER&lt;/li&gt;
    &lt;li&gt;Number one on GAIA at time of publication, roughly 2x performance on the hardest level&lt;/li&gt;
  &lt;/ul&gt;
&lt;/div&gt;

&lt;p&gt;AutoGen is the odd paper in wave-1 because it’s not a system, it’s a framework. The contribution is that every agent, whether it’s an LLM, a human, a tool, or a code executor, speaks the same message protocol. You compose them however you want. The GroupChatManager can pick the next speaker via an LLM or via a finite state machine you define.&lt;/p&gt;

&lt;p&gt;AutoGen is more honest than the others about the fact that there’s no “right” multi-agent architecture. It doesn’t try to tell you what your agents should be. It gives you the plumbing and assumes you know what you’re doing. For that reason it’s probably aged better than CAMEL or ChatDev.&lt;/p&gt;

&lt;h2 id=&quot;what-wave-1-got-right&quot;&gt;What Wave 1 Got Right&lt;/h2&gt;

&lt;p&gt;Every one of these papers took LLMs out of single-user chat and put them into multi-step coordination tasks. That’s a real contribution. Role specialization, structured dialogue, tool use patterns, task decomposition, memory and reflection as first-class primitives. These ideas came out of wave-1 and the field is still using them.&lt;/p&gt;

&lt;h2 id=&quot;what-wave-1-got-wrong&quot;&gt;What Wave 1 Got Wrong&lt;/h2&gt;

&lt;div class=&quot;mas-taxonomy&quot;&gt;
  &lt;h4&gt;Shared Assumptions That Didn&apos;t Survive&lt;/h4&gt;
  &lt;div class=&quot;mas-tax-row&quot;&gt;
    &lt;span class=&quot;mas-tax-label&quot;&gt;Failure model&lt;/span&gt;
    &lt;div class=&quot;mas-tax-items&quot;&gt;
      &lt;span class=&quot;mas-tax-chip mas-chip-bad&quot;&gt;Treated as termination, not a system state&lt;/span&gt;
    &lt;/div&gt;
  &lt;/div&gt;
  &lt;div class=&quot;mas-tax-row&quot;&gt;
    &lt;span class=&quot;mas-tax-label&quot;&gt;Concurrency control&lt;/span&gt;
    &lt;div class=&quot;mas-tax-items&quot;&gt;
      &lt;span class=&quot;mas-tax-chip mas-chip-bad&quot;&gt;Shared state with no causality tracking&lt;/span&gt;
    &lt;/div&gt;
  &lt;/div&gt;
  &lt;div class=&quot;mas-tax-row&quot;&gt;
    &lt;span class=&quot;mas-tax-label&quot;&gt;Evaluation&lt;/span&gt;
    &lt;div class=&quot;mas-tax-items&quot;&gt;
      &lt;span class=&quot;mas-tax-chip mas-chip-bad&quot;&gt;Benchmarks designed for single agents&lt;/span&gt;
    &lt;/div&gt;
  &lt;/div&gt;
  &lt;div class=&quot;mas-tax-row&quot;&gt;
    &lt;span class=&quot;mas-tax-label&quot;&gt;Escalation&lt;/span&gt;
    &lt;div class=&quot;mas-tax-items&quot;&gt;
      &lt;span class=&quot;mas-tax-chip mas-chip-bad&quot;&gt;No path when convergence fails&lt;/span&gt;
    &lt;/div&gt;
  &lt;/div&gt;
  &lt;div class=&quot;mas-tax-row&quot;&gt;
    &lt;span class=&quot;mas-tax-label&quot;&gt;Topology&lt;/span&gt;
    &lt;div class=&quot;mas-tax-items&quot;&gt;
      &lt;span class=&quot;mas-tax-chip mas-chip-bad&quot;&gt;Fixed at design time&lt;/span&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;p&gt;Every wave-1 paper treats failure as a termination condition. When ChatDev can’t converge, it stops. When MetaGPT’s tests fail three times, it stops. When AutoGen hits max_round, it stops. None of these systems have a model for what happens next. This is the gap wave-2 papers would later start trying to fill.&lt;/p&gt;

&lt;p&gt;None of the wave-1 papers have concurrency control on their shared state. MetaGPT’s message pool grows monotonically and nobody tracks causality. ChatDev discards dialogue at phase boundaries. Generative Agents’ memory is per-agent with no sharing. If you had two agents in MetaGPT trying to edit the same file, nothing in the framework would stop them from overwriting each other’s work.&lt;/p&gt;

&lt;p&gt;And all of them evaluate against benchmarks that were designed for single agents. HumanEval, MBPP, SWE-bench. These benchmarks measure whether the output is correct. They don’t measure coordination quality, communication overhead, or recovery behavior. Which are the things that distinguish a multi-agent system from a single agent.&lt;/p&gt;

&lt;p&gt;Next post: wave-2 papers, which measure what actually breaks in these systems. With wave-1 architectures running in production, and the agentic coding turn having clarified when MAS isn’t the right tool at all, the field started asking why MAS fails when you do use it and how to test that honestly.&lt;/p&gt;
</description>
				<pubDate>Sun, 26 Apr 2026 12:00:00 +0000</pubDate>
				<link>https://christophermeiklejohn.com/ai/agents/mas-series/2026/04/26/mas-series-03-wave-one.html</link>
				<guid isPermaLink="true">https://christophermeiklejohn.com/ai/agents/mas-series/2026/04/26/mas-series-03-wave-one.html</guid>
			</item>
		
			<item>
				<title>Getting Up to Speed on Multi-Agent Systems, Part 2: The Vocabulary</title>
				<description>&lt;p&gt;If you try to read multi-agent systems papers without the vocabulary, you will get nowhere. The field has settled on a shared set of words for the pieces of a system, and every paper now slots into those categories even when it pretends to be doing something novel. This post is about those words. Once you know them, you can read any paper in the field and know what it is and isn’t claiming.&lt;/p&gt;

&lt;div class=&quot;mas-series-nav&quot;&gt;
  &lt;div class=&quot;mas-series-label&quot;&gt;Getting Up to Speed on MAS&lt;/div&gt;
  &lt;ol&gt;&lt;li&gt;&lt;a href=&quot;/ai/agents/mas-series/2026/04/24/mas-series-01-the-landscape.html&quot;&gt;Part 1. The Landscape&lt;/a&gt;&lt;/li&gt;&lt;li class=&quot;mas-current&quot;&gt;&lt;strong&gt;Part 2. The Vocabulary (you are here)&lt;/strong&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;/ai/agents/mas-series/2026/04/26/mas-series-03-wave-one.html&quot;&gt;Part 3. Wave 1: Can Agents Coordinate At All?&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;/ai/agents/mas-series/2026/04/27/mas-series-04-wave-two.html&quot;&gt;Part 4. Wave 2: Why It Breaks&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;/ai/agents/mas-series/2026/04/28/mas-series-05-debate-state-coordination.html&quot;&gt;Part 5. Debate, State, and Coordination&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;/ai/agents/mas-series/2026/04/29/mas-series-06-verification-patterns.html&quot;&gt;Part 6. Verification Patterns&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;/ai/agents/mas-series/2026/04/30/mas-series-07-benchmarks.html&quot;&gt;Part 7. Benchmarks and What They Miss&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;/ai/agents/mas-series/2026/05/01/mas-series-08-open-questions.html&quot;&gt;Part 8. Open Questions&lt;/a&gt;&lt;/li&gt;&lt;/ol&gt;
&lt;/div&gt;

&lt;p&gt;Three surveys have done the work of consolidating the vocabulary. Each one cuts the space slightly differently, but together they give you the conceptual toolkit.&lt;/p&gt;

&lt;h2 id=&quot;tran-et-al-actors-types-structures-strategies&quot;&gt;Tran et al.: Actors, Types, Structures, Strategies&lt;/h2&gt;

&lt;p&gt;The most useful single survey is &lt;a href=&quot;https://arxiv.org/abs/2501.06322&quot;&gt;Tran et al. (2025)&lt;/a&gt;. It defines a multi-agent system formally as a tuple of agents, collaboration channels, collective goals, and an environment. Then it taxonomizes the space along four axes.&lt;/p&gt;

&lt;div class=&quot;mas-taxonomy&quot;&gt;
  &lt;h4&gt;Tran&apos;s Four Axes&lt;/h4&gt;
  &lt;div class=&quot;mas-tax-row&quot;&gt;
    &lt;span class=&quot;mas-tax-label&quot;&gt;Types&lt;/span&gt;
    &lt;div class=&quot;mas-tax-items&quot;&gt;
      &lt;span class=&quot;mas-tax-chip&quot;&gt;Cooperation (aligned goals)&lt;/span&gt;
      &lt;span class=&quot;mas-tax-chip&quot;&gt;Competition (conflicting goals)&lt;/span&gt;
      &lt;span class=&quot;mas-tax-chip&quot;&gt;Coopetition (mixed)&lt;/span&gt;
    &lt;/div&gt;
  &lt;/div&gt;
  &lt;div class=&quot;mas-tax-row&quot;&gt;
    &lt;span class=&quot;mas-tax-label&quot;&gt;Structures&lt;/span&gt;
    &lt;div class=&quot;mas-tax-items&quot;&gt;
      &lt;span class=&quot;mas-tax-chip&quot;&gt;Centralized (hub)&lt;/span&gt;
      &lt;span class=&quot;mas-tax-chip&quot;&gt;Decentralized (P2P)&lt;/span&gt;
      &lt;span class=&quot;mas-tax-chip&quot;&gt;Hierarchical (layered)&lt;/span&gt;
    &lt;/div&gt;
  &lt;/div&gt;
  &lt;div class=&quot;mas-tax-row&quot;&gt;
    &lt;span class=&quot;mas-tax-label&quot;&gt;Strategies&lt;/span&gt;
    &lt;div class=&quot;mas-tax-items&quot;&gt;
      &lt;span class=&quot;mas-tax-chip&quot;&gt;Rule-based (voting, consensus)&lt;/span&gt;
      &lt;span class=&quot;mas-tax-chip&quot;&gt;Role-based (SOP, expertise)&lt;/span&gt;
      &lt;span class=&quot;mas-tax-chip&quot;&gt;Model-based (Theory of Mind)&lt;/span&gt;
    &lt;/div&gt;
  &lt;/div&gt;
  &lt;div class=&quot;mas-tax-row&quot;&gt;
    &lt;span class=&quot;mas-tax-label&quot;&gt;Architecture&lt;/span&gt;
    &lt;div class=&quot;mas-tax-items&quot;&gt;
      &lt;span class=&quot;mas-tax-chip&quot;&gt;Static (pre-defined)&lt;/span&gt;
      &lt;span class=&quot;mas-tax-chip&quot;&gt;Dynamic (runtime adjustment)&lt;/span&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;p&gt;Most of the famous wave-1 papers are in one box: cooperative, hierarchical, role-based, static. Everyone is doing roughly the same thing, with small variations in how agents pass messages and what they produce at each step. The survey’s most useful claim is that the optimal structure varies with the task. There is no universal topology.&lt;/p&gt;

&lt;h2 id=&quot;zhou-et-al-the-five-component-agent&quot;&gt;Zhou et al.: The Five-Component Agent&lt;/h2&gt;

&lt;p&gt;&lt;a href=&quot;https://link.springer.com/article/10.1007/s44336-024-00009-2&quot;&gt;Zhou et al. (2024)&lt;/a&gt; takes a different cut. Instead of asking how agents coordinate, they ask what each agent actually has inside it. They propose a five-component model that applies to any LLM-based agent.&lt;/p&gt;

&lt;div class=&quot;mas-taxonomy&quot;&gt;
  &lt;h4&gt;Zhou&apos;s Five Components&lt;/h4&gt;
  &lt;div class=&quot;mas-tax-row&quot;&gt;
    &lt;span class=&quot;mas-tax-label&quot;&gt;01 Profile&lt;/span&gt;
    &lt;div class=&quot;mas-tax-items&quot;&gt;
      &lt;span class=&quot;mas-tax-chip&quot;&gt;How the agent is created with role and expertise&lt;/span&gt;
    &lt;/div&gt;
  &lt;/div&gt;
  &lt;div class=&quot;mas-tax-row&quot;&gt;
    &lt;span class=&quot;mas-tax-label&quot;&gt;02 Perception&lt;/span&gt;
    &lt;div class=&quot;mas-tax-items&quot;&gt;
      &lt;span class=&quot;mas-tax-chip&quot;&gt;How the agent observes its environment&lt;/span&gt;
    &lt;/div&gt;
  &lt;/div&gt;
  &lt;div class=&quot;mas-tax-row&quot;&gt;
    &lt;span class=&quot;mas-tax-label&quot;&gt;03 Self-Action&lt;/span&gt;
    &lt;div class=&quot;mas-tax-items&quot;&gt;
      &lt;span class=&quot;mas-tax-chip&quot;&gt;Memory, reasoning, and planning&lt;/span&gt;
    &lt;/div&gt;
  &lt;/div&gt;
  &lt;div class=&quot;mas-tax-row&quot;&gt;
    &lt;span class=&quot;mas-tax-label&quot;&gt;04 Mutual Interaction&lt;/span&gt;
    &lt;div class=&quot;mas-tax-items&quot;&gt;
      &lt;span class=&quot;mas-tax-chip&quot;&gt;Communication paradigm, structure, content&lt;/span&gt;
    &lt;/div&gt;
  &lt;/div&gt;
  &lt;div class=&quot;mas-tax-row&quot;&gt;
    &lt;span class=&quot;mas-tax-label&quot;&gt;05 Evolution&lt;/span&gt;
    &lt;div class=&quot;mas-tax-items&quot;&gt;
      &lt;span class=&quot;mas-tax-chip&quot;&gt;Self-reflection, progressive enhancement&lt;/span&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;p&gt;Reading this as a distributed systems person, the labels sound like things you’d recognize from any actor system. Profile is identity. Perception is input. Self-Action is local state plus computation. Mutual Interaction is message passing. Evolution is the weakest piece, because nobody has really figured out what “agent learning from its own history” looks like in production.&lt;/p&gt;

&lt;h2 id=&quot;chen-et-al-applications-and-unsolved-challenges&quot;&gt;Chen et al.: Applications and Unsolved Challenges&lt;/h2&gt;

&lt;p&gt;The third survey, &lt;a href=&quot;https://arxiv.org/abs/2412.17481&quot;&gt;Chen et al. (2024)&lt;/a&gt;, is the one I’d skim rather than read in full. The applications chapter is useful, but what you actually want is the challenges section.&lt;/p&gt;

&lt;div class=&quot;mas-taxonomy&quot;&gt;
  &lt;h4&gt;Chen&apos;s Challenge Levels&lt;/h4&gt;
  &lt;div class=&quot;mas-tax-row&quot;&gt;
    &lt;span class=&quot;mas-tax-label&quot;&gt;Agent-level&lt;/span&gt;
    &lt;div class=&quot;mas-tax-items&quot;&gt;
      &lt;span class=&quot;mas-tax-chip&quot;&gt;Alignment for simulation&lt;/span&gt;
      &lt;span class=&quot;mas-tax-chip&quot;&gt;Hallucination propagation&lt;/span&gt;
      &lt;span class=&quot;mas-tax-chip&quot;&gt;Long-context limits&lt;/span&gt;
    &lt;/div&gt;
  &lt;/div&gt;
  &lt;div class=&quot;mas-tax-row&quot;&gt;
    &lt;span class=&quot;mas-tax-label&quot;&gt;Interaction-level&lt;/span&gt;
    &lt;div class=&quot;mas-tax-items&quot;&gt;
      &lt;span class=&quot;mas-tax-chip&quot;&gt;Efficiency explosion&lt;/span&gt;
      &lt;span class=&quot;mas-tax-chip&quot;&gt;Accumulative error&lt;/span&gt;
    &lt;/div&gt;
  &lt;/div&gt;
  &lt;div class=&quot;mas-tax-row&quot;&gt;
    &lt;span class=&quot;mas-tax-label&quot;&gt;Evaluation-level&lt;/span&gt;
    &lt;div class=&quot;mas-tax-items&quot;&gt;
      &lt;span class=&quot;mas-tax-chip&quot;&gt;No standardized benchmarks&lt;/span&gt;
      &lt;span class=&quot;mas-tax-chip&quot;&gt;No objective metrics&lt;/span&gt;
      &lt;span class=&quot;mas-tax-chip&quot;&gt;No individual vs aggregate frameworks&lt;/span&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;p&gt;The interaction-level challenges are the ones that most concern me. Efficiency explosion is the observation that multi-agent systems scale worse than linearly because each agent’s autoregressive generation multiplies the token cost. Accumulative error is what it sounds like: errors made in round one propagate and amplify in rounds two, three, four.&lt;/p&gt;

&lt;h2 id=&quot;mapping-papers-into-these-taxonomies&quot;&gt;Mapping Papers Into These Taxonomies&lt;/h2&gt;

&lt;p&gt;The payoff of the vocabulary is that you can now categorize any paper in the field at a glance.&lt;/p&gt;

&lt;div class=&quot;mas-compare-wrap&quot;&gt;
&lt;table class=&quot;mas-compare&quot;&gt;
  &lt;thead&gt;
    &lt;tr&gt;&lt;th&gt;System&lt;/th&gt;&lt;th&gt;Type&lt;/th&gt;&lt;th&gt;Structure&lt;/th&gt;&lt;th&gt;Strategy&lt;/th&gt;&lt;th&gt;Architecture&lt;/th&gt;&lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;&lt;td&gt;CAMEL&lt;/td&gt;&lt;td&gt;Cooperation&lt;/td&gt;&lt;td&gt;Decentralized pair&lt;/td&gt;&lt;td&gt;Role-based&lt;/td&gt;&lt;td&gt;Static&lt;/td&gt;&lt;/tr&gt;
    &lt;tr&gt;&lt;td&gt;ChatDev&lt;/td&gt;&lt;td&gt;Cooperation&lt;/td&gt;&lt;td&gt;Hierarchical pipeline&lt;/td&gt;&lt;td&gt;Role-based&lt;/td&gt;&lt;td&gt;Static&lt;/td&gt;&lt;/tr&gt;
    &lt;tr&gt;&lt;td&gt;MetaGPT&lt;/td&gt;&lt;td&gt;Cooperation&lt;/td&gt;&lt;td&gt;Centralized pool&lt;/td&gt;&lt;td&gt;Role + Rule-based&lt;/td&gt;&lt;td&gt;Static&lt;/td&gt;&lt;/tr&gt;
    &lt;tr&gt;&lt;td&gt;Debate (Du)&lt;/td&gt;&lt;td&gt;Competition&lt;/td&gt;&lt;td&gt;Decentralized all-to-all&lt;/td&gt;&lt;td&gt;Rule-based rounds&lt;/td&gt;&lt;td&gt;Static&lt;/td&gt;&lt;/tr&gt;
    &lt;tr&gt;&lt;td&gt;Generative Agents&lt;/td&gt;&lt;td&gt;Coopetition&lt;/td&gt;&lt;td&gt;Decentralized open env&lt;/td&gt;&lt;td&gt;Model-based retrieval&lt;/td&gt;&lt;td&gt;Dynamic&lt;/td&gt;&lt;/tr&gt;
    &lt;tr&gt;&lt;td&gt;Anthropic Research&lt;/td&gt;&lt;td&gt;Cooperation&lt;/td&gt;&lt;td&gt;Centralized orchestrator&lt;/td&gt;&lt;td&gt;Role-based&lt;/td&gt;&lt;td&gt;Dynamic&lt;/td&gt;&lt;/tr&gt;
    &lt;tr&gt;&lt;td&gt;AutoGen&lt;/td&gt;&lt;td&gt;Configurable&lt;/td&gt;&lt;td&gt;Configurable&lt;/td&gt;&lt;td&gt;Configurable&lt;/td&gt;&lt;td&gt;Static or Dynamic&lt;/td&gt;&lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;

&lt;p&gt;Most of the canonical papers sit in the cooperative, role-based, static quadrant. The interesting ones are the exceptions. Du et al. is the rare competitive debate paper. Generative Agents is the rare fully dynamic system. AutoGen tries to be everything at once, which is its whole thesis.&lt;/p&gt;

&lt;div class=&quot;mas-callout&quot;&gt;
  &lt;div class=&quot;mas-callout-label&quot;&gt;Why vocabulary matters&lt;/div&gt;
  When two papers claim they &quot;disagree,&quot; the vocabulary lets you ask: are they actually addressing the same problem? ChatDev and MetaGPT both call themselves &quot;multi-agent software engineering frameworks&quot; but they have different structures, different strategies, and different failure modes. You need the words to see that they are solving slightly different versions of the same problem.
&lt;/div&gt;

&lt;h2 id=&quot;the-gap-the-vocabulary-exposes&quot;&gt;The Gap the Vocabulary Exposes&lt;/h2&gt;

&lt;p&gt;The taxonomies do something else besides categorize papers. They make gaps visible.&lt;/p&gt;

&lt;p&gt;Zhou’s “Evolution” component is the weakest across every system. Nobody has a real story for how agents learn from their own history in production. MetaGPT’s “test-driven retry” is the closest wave-1 paper to Evolution, and it’s still just a bounded retry loop with no memory of past attempts.&lt;/p&gt;

&lt;p&gt;Tran’s “dynamic architecture” category is almost empty. The wave-1 papers all fix their topology at design time. AutoGen makes topology configurable, but it’s configured by the developer, not adjusted at runtime. The only system that truly adjusts at runtime is Generative Agents, and that’s a simulation, not a production framework.&lt;/p&gt;

&lt;p&gt;Chen’s “evaluation-level” challenges are unsolved in a way that’s embarrassing for the field. When ChatDev claims 88 percent executability and MetaGPT claims 41 percent on a comparable benchmark, you’re not looking at a performance difference. You’re looking at two papers measuring different things with different tools and calling them the same.&lt;/p&gt;

&lt;p&gt;Next post: the wave-1 theory papers in detail. CAMEL, Generative Agents, ChatDev, MetaGPT, AutoGen. What each one actually builds, what each one trusts, and where each one breaks.&lt;/p&gt;
</description>
				<pubDate>Sat, 25 Apr 2026 12:00:00 +0000</pubDate>
				<link>https://christophermeiklejohn.com/ai/agents/mas-series/2026/04/25/mas-series-02-the-vocabulary.html</link>
				<guid isPermaLink="true">https://christophermeiklejohn.com/ai/agents/mas-series/2026/04/25/mas-series-02-the-vocabulary.html</guid>
			</item>
		
			<item>
				<title>Getting Up to Speed on Multi-Agent Systems, Part 1: The Landscape</title>
				<description>&lt;p&gt;I’ve been reading multi-agent systems papers for weeks trying to figure out where the field actually is, and the honest answer is that it moves fast enough that any single paper is a snapshot, not a map. So this is the map I wish I’d had when I started. It’s a short series of posts meant to get someone up to speed on multi-agent LLM systems without having to read thirty papers first.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Who this is for:&lt;/strong&gt; you already ship or evaluate LLM agents (tools, long context, basic eval loops) and want the &lt;em&gt;research&lt;/em&gt; landscape in view. This is not an on-ramp to transformers or prompting fundamentals.&lt;/p&gt;

&lt;p&gt;Before I start, a note on the frame I’m going to use. I’m going to talk about two “waves” of multi-agent research, and one outside disruption that happened between them. I want to be upfront that the waves are a reader aid, not a historical claim. Nobody in 2023 was writing “wave 1” papers, and the field did not convene to name its generation. These aren’t named movements like French New Wave cinema or second-wave feminism, where participants self-consciously defined their work against a prior cohort. What I’m doing is retrospective grouping: the kind you use to keep thirty papers straight in your head.&lt;/p&gt;

&lt;p&gt;What the grouping captures is that certain clusters of papers share assumptions, benchmarks, and failure modes. What it misses is that parallel threads exist (debate, simulation, distributed-systems-adjacent work) that don’t fit the wave structure at all, and that plenty of individual papers sit awkwardly between waves. If the framing helps you navigate the literature, keep it. If it gets in the way, drop it. The papers are what matter; the waves are scaffolding.&lt;/p&gt;

&lt;p&gt;With that caveat in mind: two rough clusters, one outside disruption that reshaped both, and two rough questions the MAS field has been trying to answer.&lt;/p&gt;

&lt;div class=&quot;mas-series-nav&quot;&gt;
  &lt;div class=&quot;mas-series-label&quot;&gt;Getting Up to Speed on MAS&lt;/div&gt;
  &lt;ol&gt;&lt;li class=&quot;mas-current&quot;&gt;&lt;strong&gt;Part 1. The Landscape (you are here)&lt;/strong&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;/ai/agents/mas-series/2026/04/25/mas-series-02-the-vocabulary.html&quot;&gt;Part 2. The Vocabulary&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;/ai/agents/mas-series/2026/04/26/mas-series-03-wave-one.html&quot;&gt;Part 3. Wave 1: Can Agents Coordinate At All?&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;/ai/agents/mas-series/2026/04/27/mas-series-04-wave-two.html&quot;&gt;Part 4. Wave 2: Why It Breaks&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;/ai/agents/mas-series/2026/04/28/mas-series-05-debate-state-coordination.html&quot;&gt;Part 5. Debate, State, and Coordination&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;/ai/agents/mas-series/2026/04/29/mas-series-06-verification-patterns.html&quot;&gt;Part 6. Verification Patterns&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;/ai/agents/mas-series/2026/04/30/mas-series-07-benchmarks.html&quot;&gt;Part 7. Benchmarks and What They Miss&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;/ai/agents/mas-series/2026/05/01/mas-series-08-open-questions.html&quot;&gt;Part 8. Open Questions&lt;/a&gt;&lt;/li&gt;&lt;/ol&gt;
&lt;/div&gt;

&lt;h2 id=&quot;wave-1-can-multiple-llms-coordinate-at-all-2023&quot;&gt;Wave 1: Can Multiple LLMs Coordinate At All? (2023)&lt;/h2&gt;

&lt;p&gt;The first wave is the one most people have heard of. A cluster of papers came out in roughly a six-month window in 2023, all answering some version of the same question: if you put multiple LLMs together, can they do something one LLM cannot?&lt;/p&gt;

&lt;p&gt;Not every paper in that cluster is “coordination theory” in the same sense: some are explicit software pipelines (ChatDev, MetaGPT), others foreground simulation and believable social dynamics (Generative Agents). I group them anyway because they share 2023-era benchmarks and a similar loose trust that multi-agent structure will carry the task.&lt;/p&gt;

&lt;div class=&quot;mas-timeline-wave&quot;&gt;
  &lt;div class=&quot;mas-wave-header&quot;&gt;Wave 1 · Theory and Architecture&lt;/div&gt;
  &lt;div class=&quot;mas-wave-subhead&quot;&gt;Can multiple LLMs coordinate at all? What&apos;s the right shape?&lt;/div&gt;
  &lt;div class=&quot;mas-timeline&quot;&gt;
    &lt;div class=&quot;mas-timeline-node mas-c-camel&quot;&gt;
      &lt;div class=&quot;mas-node-date&quot;&gt;Mar 2023&lt;/div&gt;
      &lt;div class=&quot;mas-node-name&quot;&gt;CAMEL&lt;/div&gt;
      &lt;div class=&quot;mas-node-desc&quot;&gt;Two agents role-play&lt;/div&gt;
    &lt;/div&gt;
    &lt;div class=&quot;mas-timeline-node mas-c-genagents&quot;&gt;
      &lt;div class=&quot;mas-node-date&quot;&gt;Apr 2023&lt;/div&gt;
      &lt;div class=&quot;mas-node-name&quot;&gt;Gen. Agents&lt;/div&gt;
      &lt;div class=&quot;mas-node-desc&quot;&gt;Memory and reflection&lt;/div&gt;
    &lt;/div&gt;
    &lt;div class=&quot;mas-timeline-node mas-c-debate&quot;&gt;
      &lt;div class=&quot;mas-node-date&quot;&gt;May 2023&lt;/div&gt;
      &lt;div class=&quot;mas-node-name&quot;&gt;Debate (Du)&lt;/div&gt;
      &lt;div class=&quot;mas-node-desc&quot;&gt;Competition as coordination&lt;/div&gt;
    &lt;/div&gt;
    &lt;div class=&quot;mas-timeline-node mas-c-chatdev&quot;&gt;
      &lt;div class=&quot;mas-node-date&quot;&gt;Jul 2023&lt;/div&gt;
      &lt;div class=&quot;mas-node-name&quot;&gt;ChatDev&lt;/div&gt;
      &lt;div class=&quot;mas-node-desc&quot;&gt;Pairwise chat pipeline&lt;/div&gt;
    &lt;/div&gt;
    &lt;div class=&quot;mas-timeline-node mas-c-metagpt&quot;&gt;
      &lt;div class=&quot;mas-node-date&quot;&gt;Aug 2023&lt;/div&gt;
      &lt;div class=&quot;mas-node-name&quot;&gt;MetaGPT&lt;/div&gt;
      &lt;div class=&quot;mas-node-desc&quot;&gt;Artifacts and test execution&lt;/div&gt;
    &lt;/div&gt;
    &lt;div class=&quot;mas-timeline-node mas-c-autogen&quot;&gt;
      &lt;div class=&quot;mas-node-date&quot;&gt;Aug 2023&lt;/div&gt;
      &lt;div class=&quot;mas-node-name&quot;&gt;AutoGen&lt;/div&gt;
      &lt;div class=&quot;mas-node-desc&quot;&gt;Configurable framework&lt;/div&gt;
    &lt;/div&gt;
    &lt;div class=&quot;mas-timeline-node mas-c-green&quot;&gt;
      &lt;div class=&quot;mas-node-date&quot;&gt;Aug 2023&lt;/div&gt;
      &lt;div class=&quot;mas-node-name&quot;&gt;AgentVerse&lt;/div&gt;
      &lt;div class=&quot;mas-node-desc&quot;&gt;Dynamic group composition&lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;p&gt;These papers are all proofs of concept. They show that multi-agent coordination is viable for some task, demonstrate the idea works on a benchmark, and argue that their particular coordination structure beats simpler baselines. &lt;a href=&quot;https://arxiv.org/abs/2303.17760&quot;&gt;CAMEL&lt;/a&gt; uses two agents role-playing. &lt;a href=&quot;https://arxiv.org/abs/2304.03442&quot;&gt;Generative Agents&lt;/a&gt; uses memory streams and reflection in a social simulation. &lt;a href=&quot;https://arxiv.org/abs/2305.14325&quot;&gt;Du et al.&lt;/a&gt; uses multi-round debate between identical model instances. &lt;a href=&quot;https://arxiv.org/abs/2307.07924&quot;&gt;ChatDev&lt;/a&gt; chains pairwise dialogues into a software development pipeline. &lt;a href=&quot;https://arxiv.org/abs/2308.00352&quot;&gt;MetaGPT&lt;/a&gt; replaces dialogue with structured artifacts and real test execution. &lt;a href=&quot;https://arxiv.org/abs/2308.08155&quot;&gt;AutoGen&lt;/a&gt; is a framework for building whatever topology you want. &lt;a href=&quot;https://arxiv.org/abs/2308.10848&quot;&gt;AgentVerse&lt;/a&gt; emphasizes dynamic group composition: the system can recruit specialists and reshape the team as the task proceeds.&lt;/p&gt;

&lt;p&gt;The wave-1 papers share three assumptions that look very different in hindsight. First, they assume the benchmarks they run on are the task. Second, they treat failure as a termination condition: when the system stops converging, it just stops. Third, they trust agents to coordinate without formal concurrency control, shared memory protocols, or recovery paths. These assumptions are where wave 2 would later start to push back.&lt;/p&gt;

&lt;h2 id=&quot;what-happened-next-door-2024&quot;&gt;What Happened Next Door (2024)&lt;/h2&gt;

&lt;p&gt;Before I get to wave 2, I have to account for a thing that happened in parallel that isn’t really MAS research but reshaped what MAS has to answer for.&lt;/p&gt;

&lt;p&gt;Through 2024, a wave of agentic coding systems shipped: &lt;a href=&quot;https://devin.ai&quot;&gt;Devin&lt;/a&gt;, &lt;a href=&quot;https://arxiv.org/abs/2405.15793&quot;&gt;SWE-agent&lt;/a&gt;, &lt;a href=&quot;https://arxiv.org/abs/2407.16741&quot;&gt;OpenHands&lt;/a&gt;, &lt;a href=&quot;https://arxiv.org/abs/2403.08299&quot;&gt;AutoDev&lt;/a&gt;, and Microsoft’s &lt;a href=&quot;https://arxiv.org/abs/2411.04468&quot;&gt;Magentic-One&lt;/a&gt;. These are not multi-agent systems in the wave-1 sense. Most of them are single agents with well-designed tool interfaces. The SWE-agent paper in particular showed that interface quality matters more than adding agents. They got a 10.7 percentage point improvement on SWE-bench from interface design alone, without changing the model.&lt;/p&gt;

&lt;p&gt;I bring this up because you cannot read the MAS papers from 2025 onward without this context. Wave 1 had implicitly assumed that multi-agent coordination was the default way to solve complex agentic tasks. By late 2024, the agentic coding community had accumulated strong evidence against that default for at least one large, &lt;em&gt;benchmarked&lt;/em&gt; slice of the space: autonomous patch-style software engineering (the kind of workflow SWE-bench and its successors measure). That is narrower than “all coding forever,” but it was the center of gravity in 2024 discourse, and it shifted what MAS papers need to beat. Anthropic’s &lt;a href=&quot;https://www.anthropic.com/engineering/multi-agent-research-system&quot;&gt;research system post&lt;/a&gt; from June 2025 states the conclusion plainly: multi-agent earns its overhead on “breadth-first queries with independent parallel subtasks” and underperforms on “tasks needing shared context, including most coding tasks.”&lt;/p&gt;

&lt;div class=&quot;mas-callout&quot;&gt;
  &lt;div class=&quot;mas-callout-label&quot;&gt;Why this matters for MAS readers&lt;/div&gt;
  The agentic coding papers aren&apos;t MAS research. But they narrowed the MAS claim. After 2024, &quot;multi-agent for coding&quot; became harder to defend without evidence, and the MAS field&apos;s next wave is partly a response to that. If you&apos;re reading a post-2024 MAS paper, it&apos;s almost certainly arguing implicitly against the single-agent-with-tools baseline that Devin and SWE-agent established. I won&apos;t spend a whole post on these systems because they&apos;re not MAS, but they belong on your mental map of the landscape.
&lt;/div&gt;

&lt;p&gt;Magentic-One is the interesting exception. It’s a real multi-agent system with an orchestrator coordinating four specialized workers. It earns its overhead on hard multi-step reasoning (38 percent on GAIA) but not on focused coding. The stuck-counter mechanism it introduces (if an agent loops more than twice, reflect and replan) is one of the few MAS design patterns to surface clearly in the shipped agentic-coding systems of this period. I’ll come back to it in later posts.&lt;/p&gt;

&lt;h2 id=&quot;wave-2-why-does-it-break-2025-and-beyond&quot;&gt;Wave 2: Why Does It Break? (2025 and Beyond)&lt;/h2&gt;

&lt;p&gt;The second MAS wave is where the field is now. With wave-1 systems running in production and the agentic coding turn having clarified when MAS is and isn’t the right tool, people started asking: when MAS does fail, why? And how do we even test that?&lt;/p&gt;

&lt;p&gt;The Wave 2 timeline below is &lt;strong&gt;illustrative&lt;/strong&gt;: a handful of papers that typify a shift toward measurement, taxonomies, and fault injection, not an attempt to enumerate every 2025–2026 contribution.&lt;/p&gt;

&lt;div class=&quot;mas-timeline-wave&quot;&gt;
  &lt;div class=&quot;mas-wave-header&quot;&gt;Wave 2 · Why Does It Break?&lt;/div&gt;
  &lt;div class=&quot;mas-wave-subhead&quot;&gt;MAS works sometimes. Now: why does it fail? How do you test reliability?&lt;/div&gt;
  &lt;div class=&quot;mas-timeline&quot;&gt;
    &lt;div class=&quot;mas-timeline-node mas-c-red&quot;&gt;
      &lt;div class=&quot;mas-node-date&quot;&gt;Mar 2025&lt;/div&gt;
      &lt;div class=&quot;mas-node-name&quot;&gt;MAST (Cemri)&lt;/div&gt;
      &lt;div class=&quot;mas-node-desc&quot;&gt;14 failure modes, 1,600 traces&lt;/div&gt;
    &lt;/div&gt;
    &lt;div class=&quot;mas-timeline-node mas-c-purple&quot;&gt;
      &lt;div class=&quot;mas-node-date&quot;&gt;Jun 2025&lt;/div&gt;
      &lt;div class=&quot;mas-node-name&quot;&gt;Anthropic Research&lt;/div&gt;
      &lt;div class=&quot;mas-node-desc&quot;&gt;Production orchestrator-worker&lt;/div&gt;
    &lt;/div&gt;
    &lt;div class=&quot;mas-timeline-node mas-c-green&quot;&gt;
      &lt;div class=&quot;mas-node-date&quot;&gt;Aug 2025&lt;/div&gt;
      &lt;div class=&quot;mas-node-name&quot;&gt;Info Sharing in Planning&lt;/div&gt;
      &lt;div class=&quot;mas-node-desc&quot;&gt;Shared notebook on travel planning&lt;/div&gt;
    &lt;/div&gt;
    &lt;div class=&quot;mas-timeline-node mas-c-red&quot;&gt;
      &lt;div class=&quot;mas-node-date&quot;&gt;Feb 2026&lt;/div&gt;
      &lt;div class=&quot;mas-node-name&quot;&gt;MAS-FIRE&lt;/div&gt;
      &lt;div class=&quot;mas-node-desc&quot;&gt;Systematic fault injection for MAS&lt;/div&gt;
    &lt;/div&gt;
    &lt;div class=&quot;mas-timeline-node mas-c-blue&quot;&gt;
      &lt;div class=&quot;mas-node-date&quot;&gt;Mar 2026&lt;/div&gt;
      &lt;div class=&quot;mas-node-name&quot;&gt;Silo-Bench&lt;/div&gt;
      &lt;div class=&quot;mas-node-desc&quot;&gt;Communication-reasoning gap&lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;p&gt;The &lt;a href=&quot;https://arxiv.org/abs/2503.13657&quot;&gt;MAST paper&lt;/a&gt; from Cemri and collaborators is the one I keep coming back to. They annotated 1,600 traces across seven popular multi-agent frameworks and built a taxonomy of 14 failure modes. Every framework they tested had failure rates between 41 and 87 percent. The top three failures are step repetition, reasoning-action mismatch, and being unaware of termination conditions. These are not model capability problems. They are system design problems.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://arxiv.org/abs/2602.19843&quot;&gt;MAS-FIRE&lt;/a&gt; goes the other direction. Instead of observing failures in the wild, they inject them on purpose. Fifteen fault types across intra-agent and inter-agent categories, three injection mechanisms, and a dual-level reliability metric. The most interesting result is what they call the capability paradox: GPT-5’s strict instruction compliance becomes a liability under “Blind Trust” faults, where DeepSeek-V3’s less compliant behavior holds up better.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://arxiv.org/abs/2603.01045&quot;&gt;Silo-Bench&lt;/a&gt; adds the third leg. 1,620 experiments showing that agents successfully form coordination topologies and actively exchange information, yet systematically fail to synthesize distributed state into correct answers. The bottleneck is not communication. The bottleneck is reasoning over distributed state.&lt;/p&gt;

&lt;h2 id=&quot;what-each-wave-trusts&quot;&gt;What Each Wave Trusts&lt;/h2&gt;

&lt;p&gt;If you want a one-sentence read on each wave, this is it.&lt;/p&gt;

&lt;div class=&quot;mas-taxonomy&quot;&gt;
  &lt;div class=&quot;mas-tax-row&quot;&gt;
    &lt;span class=&quot;mas-tax-label&quot;&gt;Wave 1&lt;/span&gt;
    &lt;div class=&quot;mas-tax-items&quot;&gt;
      &lt;span class=&quot;mas-tax-chip&quot;&gt;Trusts that role structure and dialogue are enough&lt;/span&gt;
    &lt;/div&gt;
  &lt;/div&gt;
  &lt;div class=&quot;mas-tax-row&quot;&gt;
    &lt;span class=&quot;mas-tax-label&quot;&gt;Outside&lt;/span&gt;
    &lt;div class=&quot;mas-tax-items&quot;&gt;
      &lt;span class=&quot;mas-tax-chip&quot;&gt;Agentic coding: trusts that good tools beat agent count&lt;/span&gt;
    &lt;/div&gt;
  &lt;/div&gt;
  &lt;div class=&quot;mas-tax-row&quot;&gt;
    &lt;span class=&quot;mas-tax-label&quot;&gt;Wave 2&lt;/span&gt;
    &lt;div class=&quot;mas-tax-items&quot;&gt;
      &lt;span class=&quot;mas-tax-chip&quot;&gt;Trusts nothing and measures what breaks&lt;/span&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;p&gt;Read as a progression, the rough arc is: tell agents to coordinate, then notice that sometimes you don’t need them to, then measure what happens when you do. Each step trusts the agents less and verifies more than the one before.&lt;/p&gt;

&lt;h2 id=&quot;what-the-waves-dont-capture&quot;&gt;What the Waves Don’t Capture&lt;/h2&gt;

&lt;p&gt;The two-wave framing is tidy, which should make you suspicious. Some things it flattens or misses:&lt;/p&gt;

&lt;p&gt;Parallel threads run outside the waves entirely. &lt;a href=&quot;https://arxiv.org/abs/2305.14325&quot;&gt;Du et al.&lt;/a&gt; appears on the Wave 1 timeline because it landed in the same window and poses the same headline question (many LLMs versus one), but &lt;strong&gt;the debate line of work&lt;/strong&gt; is its own cluster: multiple instances of the same model arguing, with a citation graph that barely touches coordination-theory papers and rarely gets cited back by them. The timeline entry is chronology, not a claim that debate belongs in ChatDev’s intellectual neighborhood; Part 5 treats debate as that parallel thread. Generative Agents is a social simulation paper that sits uncomfortably in wave 1 because it came out in 2023, but its descendants (game agents, persona simulations) are their own research community. Distributed-systems-adjacent work on shared state and coordination avoidance is a separate thread that’s only now starting to touch the MAS literature.&lt;/p&gt;

&lt;p&gt;Individual papers don’t fit cleanly. AutoGen is a 2023 paper that kept evolving through 2024. The Anthropic research post is engineering content, not a research paper. Magentic-One sits between the agentic coding turn and the reliability wave. Calling any of these “wave 1” or “wave 2” is a judgment call, not a fact.&lt;/p&gt;

&lt;p&gt;Keep all of that in mind as you read the rest of the series. The waves are a way to group papers for comprehension, not a claim about how the field evolved.&lt;/p&gt;

&lt;h2 id=&quot;whats-coming&quot;&gt;What’s Coming&lt;/h2&gt;

&lt;p&gt;The next seven posts build on this landscape.&lt;/p&gt;

&lt;p&gt;Part 2 covers the vocabulary the field uses for itself. Three surveys have done the work of consolidating the shared terms, and once you know the vocabulary you can read any paper in the field at a glance.&lt;/p&gt;

&lt;p&gt;Parts 3 and 4 go deep on the two waves. Part 3 is the canonical coordination-theory papers: CAMEL, ChatDev, MetaGPT, AutoGen, AgentVerse, what each one actually builds and where each one quietly disagrees with the others. Part 4 is the reliability wave: MAST, MAS-FIRE, Silo-Bench, and what happens when you try to measure a multi-agent system honestly.&lt;/p&gt;

&lt;p&gt;Part 5 covers the parallel threads. Multi-agent debate (Du, Liang), shared state as coordination (Ou et al.), and the CALM theorem as a bridge between distributed systems and multi-agent AI.&lt;/p&gt;

&lt;p&gt;Parts 6 and 7 are cross-cutting. Part 6 is verification patterns, including Cursor’s visual feedback loop, which is the most interesting production-scale verification pattern I’ve seen and isn’t in any of the papers. Part 7 is benchmarks, what they measure, what they miss, and why ChatDev and MetaGPT can report contradictory results on each other without either being obviously wrong.&lt;/p&gt;

&lt;p&gt;Part 8 is what I think is still missing, what’s worth stealing from adjacent fields, and what I’d read if I had to start over.&lt;/p&gt;

&lt;p&gt;Next post: the vocabulary.&lt;/p&gt;

&lt;h2 id=&quot;errata-and-revisions&quot;&gt;Errata and revisions&lt;/h2&gt;

&lt;p&gt;First published &lt;strong&gt;2026-04-24&lt;/strong&gt;. The list below logs substantive edits so early readers can see what moved.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;2026-04-26&lt;/strong&gt; — &lt;strong&gt;Agentic coding claim:&lt;/strong&gt; Replaced “empirically falsified” framing with scoped wording: strong evidence on benchmarked autonomous patch-style software engineering (SWE-bench-style workflows), not a universal verdict on multi-agent for all coding.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;2026-04-26&lt;/strong&gt; — &lt;strong&gt;Audience:&lt;/strong&gt; Added a short “who this is for” note (assumes you already work with LLM agents).&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;2026-04-26&lt;/strong&gt; — &lt;strong&gt;Debate vs. timeline:&lt;/strong&gt; Clarified that Du et al. on the Wave 1 timeline reflects release window and headline question; the broader debate &lt;em&gt;thread&lt;/em&gt; is parallel and is how Part 5 uses “debate.”&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;2026-04-26&lt;/strong&gt; — &lt;strong&gt;AgentVerse:&lt;/strong&gt; Described in the narrative, not only in the timeline table (with arXiv link). The Part 3 preview in “What’s Coming” now names it with the other coordination papers.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;2026-04-26&lt;/strong&gt; — &lt;strong&gt;Wave 2 timeline:&lt;/strong&gt; Explicitly labeled as illustrative examples, not a complete survey of the period.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;2026-04-26&lt;/strong&gt; — &lt;strong&gt;Magentic-One stuck counter:&lt;/strong&gt; Scoped the “design pattern” remark to shipped agentic-coding systems of the period (not a claim about all of CS or all MAS research).&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;2026-04-26&lt;/strong&gt; — &lt;strong&gt;Wave 1 heterogeneity:&lt;/strong&gt; Added a paragraph distinguishing pipeline-style coordination papers from simulation-heavy work in the same calendar cluster.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;2026-04-26&lt;/strong&gt; — &lt;strong&gt;Waves caveat:&lt;/strong&gt; Tightened prose; no change to the underlying claim that waves are retrospective scaffolding.&lt;/li&gt;
&lt;/ul&gt;
</description>
				<pubDate>Fri, 24 Apr 2026 06:00:00 +0000</pubDate>
				<link>https://christophermeiklejohn.com/ai/agents/mas-series/2026/04/24/mas-series-01-the-landscape.html</link>
				<guid isPermaLink="true">https://christophermeiklejohn.com/ai/agents/mas-series/2026/04/24/mas-series-01-the-landscape.html</guid>
			</item>
		
			<item>
				<title>The Tribe Has to Outlive the Model · Five model swaps in three weeks taught me the project&apos;s continuity lives in the humans and conventions around the agents, not the agents themselves.</title>
				<description>&lt;p&gt;By the fourth model swap I noticed that the part of the system I wasn’t swapping was the part holding the project together.&lt;/p&gt;

&lt;p&gt;Five configurations in three weeks: Opus 4.6, Opus 4.6 with the 1M context window, Cursor Cloud Agents on GPT Codex 5.3, Cloud Agents on Composer 2, and this week Opus 4.7. Some swaps were involuntary. Credits ran out. A model that had been my daily driver for two months started coming back with half-finished work and failing the easy parts of basic CI, which I wrote about in &lt;a href=&quot;/ai/agents/reliability/zabriskie/2026/04/08/cursor-agents-caucus-v1.html&quot;&gt;Caucus V1&lt;/a&gt;. Some were voluntary. I wanted to see what a new model could do that the previous one couldn’t. In a few cases it could. In a few cases the new one was worse at something different, and I swapped again.&lt;/p&gt;

&lt;p&gt;No architecture change on my end. No rewrite. The app is the app. The model is a junior engineer on a revolving contract.&lt;/p&gt;

&lt;h2 id=&quot;same-prompt-different-creature&quot;&gt;Same Prompt, Different Creature&lt;/h2&gt;

&lt;p&gt;Here’s a specific observation. For a few weeks in April I was running the &lt;a href=&quot;/ai/zabriskie/agents/reliability/caucus/2026/04/14/opt-in-isnt-a-guardrail.html&quot;&gt;Caucus Permit Gate&lt;/a&gt;. It was my attempt at forcing agents to prove their work before merging. Every PR had to land with a permit file in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.caucus/permits/&lt;/code&gt; declaring the scope and risk of the change, plus a proof file in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.caucus/proofs/&lt;/code&gt; recording the output of allowlisted test commands run against the current working tree. CI blocked the merge unless both were there and fresh.&lt;/p&gt;

&lt;p&gt;Codex 5.3, given a task I’d given Composer 2 the day before, shipped the PR with the feature, the permit, and the proof all in the first commit. Two commits total on the branch. Gate green on the first push. Composer 2 on a similar task shipped the feature commit alone. Gate red. Then &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;chore(caucus): add proof&lt;/code&gt;. Gate red. Then &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;chore(caucus): sync permit scope and refresh proof&lt;/code&gt;. Then a merge of &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;origin/main&lt;/code&gt;. Then &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;chore(caucus): refresh proof after merge&lt;/code&gt;. The slowest Composer 2 PR from that era landed in main with twenty-three commits on the branch. One of them was the feature.&lt;/p&gt;

&lt;p&gt;Same repo. Same AGENTS.md. Same sentence in bold telling the agent that the permit is not optional paperwork. Two completely different relationships with the gate.&lt;/p&gt;

&lt;p&gt;Neither was strictly wrong. Codex 5.3 paid the cost upfront. Composer 2 paid the cost when forced. Composer 2 is the one that taught me something, because the only reason it paid the cost at all was that the gate existed. Without the gate, Composer 2 would have shipped the feature commit and I would have merged it, because I’m the reviewer, and “looks fine” works on me the same way it works on the model.&lt;/p&gt;

&lt;h2 id=&quot;where-the-knowledge-has-to-live&quot;&gt;Where the Knowledge Has to Live&lt;/h2&gt;

&lt;p&gt;If a new teammate rotates in every fortnight, any knowledge that has to survive the swap cannot live in the teammate. It has to live in the repo.&lt;/p&gt;

&lt;p&gt;Everyone has a CLAUDE.md or AGENTS.md at this point. Most of them read like documentation: “the backend is in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/backend&lt;/code&gt;, the frontend is in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/web&lt;/code&gt;, run &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;npm install&lt;/code&gt; first.” Some of mine is that.&lt;/p&gt;

&lt;p&gt;The part of mine that matters isn’t the documentation. It’s the lines that record things only visible if you were here when they broke.&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;&lt;strong&gt;NEVER modify timestamp/timezone columns in migrations.&lt;/strong&gt; Timestamp corruption is unrecoverable and destroys production data.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That’s a scar. The commit that added this rule went in at 5:46 on a Saturday morning, and the commit message was one word: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;fuck&lt;/code&gt;. A companion rule had gone in 33 minutes earlier. I’ll come back to that one. A new engineer reading the repo cold has no way to know any of this. The column types look fine now. Nothing in the code says “this was dangerous.” Take the rule out, and the next model writes the same migration on the first try. The incident isn’t in the code. It was in the person who lived through it.&lt;/p&gt;

&lt;p&gt;A guardrail is the tribe’s compressed experience, encoded in a form the next teammate can act on without having been present for the original incident. Institutional memory for a team where it would otherwise be deleted every other Tuesday.&lt;/p&gt;

&lt;h2 id=&quot;not-every-guardrail-survives-the-test&quot;&gt;Not Every Guardrail Survives the Test&lt;/h2&gt;

&lt;p&gt;Two days ago I published &lt;a href=&quot;/ai/zabriskie/agents/reliability/caucus/2026/04/21/the-tax-on-the-happy-path.html&quot;&gt;The Tax on the Happy Path&lt;/a&gt;, where I killed a guardrail I had spent three weeks building. The Caucus Permit Gate, which I’d hardened nine different ways in &lt;a href=&quot;/ai/zabriskie/agents/reliability/caucus/2026/04/14/opt-in-isnt-a-guardrail.html&quot;&gt;Opt-In Isn’t a Guardrail&lt;/a&gt;, never caught a unique bug in its last hundred CI runs. Everything it caught, the regular test jobs would have caught a minute later. It cost more than it caught. I took it out.&lt;/p&gt;

&lt;p&gt;I’m not recanting that. Killing a guardrail that isn’t earning its keep is part of taking guardrails seriously. But I owe a better account of which rules survive the audit, because “some guardrails are good and some are bad” is not a useful thing to hand a reader.&lt;/p&gt;

&lt;p&gt;Two questions. Does the rule point at a specific past event? And does its cost scale with the velocity of the work, or with the rate at which the event it prevents actually recurs?&lt;/p&gt;

&lt;p&gt;The permit gate failed both. It didn’t point at a specific incident; it was a ritual about carefulness in general. And its cost scaled with every rebase against main, regardless of whether anything risky was happening on the branch. Cost grew with velocity. Value stayed flat.&lt;/p&gt;

&lt;p&gt;It had the shape of every code-review checklist I’ve ever seen at a company. “Consider thread safety.” “Check error handling.” “Verify input validation.” You check the boxes, go through the motions. The actual review happens in someone’s head, from memory of the specific thing that burned the team last quarter. Ceremony versus pattern recognition. The permit gate was all ceremony.&lt;/p&gt;

&lt;p&gt;The guardrails that have survived the audit pass both. “Do not drop a column without running &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;./scripts/check-references.sh&lt;/code&gt; first” points at the time we dropped a column and broke ninety percent of the reports. “No &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;--admin&lt;/code&gt; merges” points at a specific session in &lt;a href=&quot;/ai/zabriskie/development/2026/03/27/memory-isnt-learning.html&quot;&gt;Memory Isn’t Learning&lt;/a&gt;. “No database triggers. EVER.” points at the incident I’m about to describe. The cost of obeying each of these is a few seconds, paid only when the agent is about to do the specific dangerous thing. Cost scales with the rate of the risky action, not with the rate of all work.&lt;/p&gt;

&lt;p&gt;They refer to events. They are shaped like scars.&lt;/p&gt;

&lt;h2 id=&quot;the-trigger&quot;&gt;The Trigger&lt;/h2&gt;

&lt;p&gt;The companion rule from above went in at 5:13 that same Saturday morning, 33 minutes before the timestamp one. Same incident. The &lt;a href=&quot;/ai/claude/2026/02/17/building-a-social-app-in-a-week-with-claude-code.html&quot;&gt;migration side of that night&lt;/a&gt; I’ve already written about. The trigger side I haven’t. Here it is.&lt;/p&gt;

&lt;p&gt;I’ve seen this incident at three different companies. Somebody adds a trigger to maintain &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;updated_at&lt;/code&gt;, or to keep a computed column in sync. The trigger works. Years later someone writes a migration that touches the same table, the trigger fires in the middle of it, and rows end up with values nobody intended. I would have pushed back on this in any code review at any job I’ve ever had. If a junior engineer proposed a trigger in a design doc I’d have said: please, no, not a trigger, write it in application code.&lt;/p&gt;

&lt;p&gt;And then I built &lt;a href=&quot;/zabriskie/&quot;&gt;Zabriskie&lt;/a&gt; with an AI that took the shortest path between “I want &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;updated_at&lt;/code&gt; to update itself” and “it updates itself.” The shortest path was a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;BEFORE UPDATE&lt;/code&gt; trigger named &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;update_updated_at_column&lt;/code&gt;, attached to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;posts&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;comments&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;users&lt;/code&gt;, and every other table with an &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;updated_at&lt;/code&gt;. I didn’t catch it because I was moving fast, the code looked fine, and I was watching the screen fill up with new features instead of acting as a database reviewer on my own project.&lt;/p&gt;

&lt;p&gt;The trigger revealed itself on the night the timezone migration broke. Migration 030 converted every timestamped table in the schema from &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;timestamp&lt;/code&gt; to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;timestamp with time zone&lt;/code&gt; with an &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;AT TIME ZONE &apos;America/New_York&apos;&lt;/code&gt; clause. That’s where the disaster movie started. The follow-up migrations tried to clean up. Every cleanup UPDATE fired the trigger and stamped &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;updated_at = NOW()&lt;/code&gt; on every row the UPDATE touched, which meant every feed ordering by &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;updated_at&lt;/code&gt; started surfacing posts from earlier that week as if they’d just been edited. Each fix kept succeeding, and the symptoms kept changing, because each fix was also erasing the evidence that the rows hadn’t been touched by a user.&lt;/p&gt;

&lt;p&gt;It took several migrations to trace, because each one had succeeded, the columns I’d meant to change looked right, and nothing in the migration files mentioned &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;updated_at&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;By migration 036 I gave up and dropped every trigger. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;DISABLE TRIGGER ALL&lt;/code&gt; ran first, the fix ran second, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;DROP TRIGGER&lt;/code&gt; ran third. Migration 037 fixed &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;updated_at&lt;/code&gt; one more time. At 5:13 that morning, the rule went into CLAUDE.md:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;&lt;strong&gt;NO DATABASE TRIGGERS&lt;/strong&gt;: NEVER create database triggers. EVER. Always use explicit SQL (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;SET updated_at = NOW()&lt;/code&gt;) in application code instead. Triggers cause unpredictable behavior during migrations and data fixes. This is non-negotiable.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;A companion section, “Check Triggers Before DB Updates,” went in the same week. It tells the agent to run &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;\dS table_name&lt;/code&gt;, list the triggers, and (for the triggers I hadn’t yet ripped out) disable them around any UPDATE migration:&lt;/p&gt;

&lt;div class=&quot;language-sql highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;ALTER&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;TABLE&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;posts&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;DISABLE&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;TRIGGER&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;update_posts_updated_at&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;-- Your UPDATE here&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;ALTER&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;TABLE&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;posts&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;ENABLE&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;TRIGGER&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;update_posts_updated_at&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Neither rule is for me. I already knew both. I wrote them down because the teammate I’m now on a team with arrives fresh every week with no memory of any of the things I know, and will, absent a specific instruction, pick the same shortest paths. The shortest paths are a trigger and an &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;AT TIME ZONE&lt;/code&gt; migration.&lt;/p&gt;

&lt;p&gt;The rules are a decade of scar tissue from somebody who isn’t going to be in the room the next time the new model gets the same task. The tribe’s memory, encoded in a form that survives the swap.&lt;/p&gt;

&lt;p&gt;The same shape holds for “Don’t use &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;--no-verify&lt;/code&gt;,” for “Never deploy untested changes to S3,” for “Always use &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ON CONFLICT DO NOTHING&lt;/code&gt; on data INSERTs in migrations.” Each points at an event. Each event is the thing the next agent would have repeated, because the next agent is a different agent every time.&lt;/p&gt;

&lt;h2 id=&quot;the-interesting-question&quot;&gt;The Interesting Question&lt;/h2&gt;

&lt;p&gt;People keep asking whether the model can learn. Does the memory system work. Does the agent read CLAUDE.md. Does the rule change the behavior. I argued in &lt;a href=&quot;/ai/zabriskie/development/2026/03/27/memory-isnt-learning.html&quot;&gt;Memory Isn’t Learning&lt;/a&gt; that the answer is usually no.&lt;/p&gt;

&lt;p&gt;Different question. Not “can this model learn.” The models will get better at that, or they won’t, and either way my project has to ship.&lt;/p&gt;

&lt;p&gt;Can the team’s memory survive the next model swap?&lt;/p&gt;

&lt;p&gt;The rules I wrote after that Saturday morning have now outlived every configuration that’s done work on this codebase since. The one I’ll be using next month will read them and will not write a trigger or an &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;AT TIME ZONE&lt;/code&gt; migration.&lt;/p&gt;

&lt;p&gt;The tribe has to outlive the model. The only way that happens is if what the tribe learned is written somewhere the next model can’t skip.&lt;/p&gt;

&lt;hr /&gt;

&lt;p&gt;&lt;em&gt;This is part of a series about building &lt;a href=&quot;https://zabriskie.app&quot;&gt;Zabriskie&lt;/a&gt; with Claude. Previously: &lt;a href=&quot;/ai/zabriskie/development/2026/03/27/memory-isnt-learning.html&quot;&gt;Memory Isn’t Learning&lt;/a&gt;, &lt;a href=&quot;/ai/zabriskie/agents/reliability/caucus/2026/04/14/opt-in-isnt-a-guardrail.html&quot;&gt;Opt-In Isn’t a Guardrail&lt;/a&gt;, &lt;a href=&quot;/ai/zabriskie/agents/reliability/caucus/2026/04/21/the-tax-on-the-happy-path.html&quot;&gt;The Tax on the Happy Path&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;
</description>
				<pubDate>Thu, 23 Apr 2026 00:00:00 +0000</pubDate>
				<link>https://christophermeiklejohn.com/ai/zabriskie/agents/reliability/2026/04/23/the-tribe-has-to-outlive-the-model.html</link>
				<guid isPermaLink="true">https://christophermeiklejohn.com/ai/zabriskie/agents/reliability/2026/04/23/the-tribe-has-to-outlive-the-model.html</guid>
			</item>
		
			<item>
				<title>The Tax on the Happy Path · What I learned by removing the elaborate CI gate I&apos;d built to slow my AI agents down — after realizing it had never actually caught anything.</title>
				<description>&lt;p&gt;I was halfway through another PR, regenerating the proof file for the fourth time after rebasing on main, when I realized I couldn’t name a single PR the permit gate had caught. Not “caught that other CI didn’t also catch.” Just &lt;em&gt;caught&lt;/em&gt;. I decided to look up the numbers, and then I removed the gate entirely.&lt;/p&gt;

&lt;h2 id=&quot;what-the-gate-was&quot;&gt;What the Gate Was&lt;/h2&gt;

&lt;p&gt;The short version: every PR to &lt;a href=&quot;/zabriskie/&quot;&gt;Zabriskie&lt;/a&gt; had to be accompanied by a permit file in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.caucus/permits/&amp;lt;branch&amp;gt;.json&lt;/code&gt;. The permit declared the scope of the change (which paths were allowed to move), the risk level (R1 through R3+), and a list of allowlisted test commands the author was offering as evidence. R2 and higher required a proof file too, generated by actually running those commands and recording the output alongside a fingerprint of the working tree. A CI job called Caucus Permit Gate ran on every PR. It blocked the merge unless the permit existed, the scope covered the diff, and (for R2+) the proof was fresh and the tests passed.&lt;/p&gt;

&lt;p&gt;I built it because I’d spent months watching agents read a rule in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;CLAUDE.md&lt;/code&gt;, acknowledge it in conversation, and then ship code that violated it anyway. &lt;a href=&quot;/ai/zabriskie/development/2026/03/27/memory-isnt-learning.html&quot;&gt;Memory Isn’t Learning&lt;/a&gt; was the post where I argued that prose in an instruction file is a journal of failures, not a guardrail. The permit gate was supposed to be the structural answer: the rule stops being a sentence and starts being a required status check that CI evaluates against the actual diff.&lt;/p&gt;

&lt;p&gt;The gate shipped. It got bypassed, broken, and accidentally self-deadlocked in nine different ways, each of which got a hardening patch. &lt;a href=&quot;/ai/zabriskie/agents/reliability/caucus/2026/04/14/opt-in-isnt-a-guardrail.html&quot;&gt;Opt-In Isn’t a Guardrail&lt;/a&gt; was the post about those failures. By the time I sat down to write this one, none of that was still happening. The paperwork was filed, the scope was matching, the proofs were fresh, the tests were passing. I killed the gate anyway.&lt;/p&gt;

&lt;h2 id=&quot;the-count&quot;&gt;The Count&lt;/h2&gt;

&lt;p&gt;Over the last hundred CI runs on Zabriskie (roughly a week of shipping, so take the sample for what it is) there were four Caucus Permit Gate failures. Every one of them was a “Proof failed” result, which means one of the allowlisted test commands the proof re-runs returned non-zero. The same commands run in the Build Backend, Unit Tests, and E2E Tests jobs, which are separate CI jobs on every PR. In every case, those dedicated jobs also failed, for the same reason, on the same PR.&lt;/p&gt;

&lt;p&gt;The gate’s unique contributions, the ones that were supposed to justify its existence, never fired as the blocking failure. Scope allowlist violations: zero. Risk-level enforcement: zero. Proof fingerprint drift is a regeneration trigger by design, not a catch, so it was never going to appear on that list, and I should have noticed that before I put it on the list. The gate caught exactly nothing in that window that the plain test jobs wouldn’t have caught five minutes later.&lt;/p&gt;

&lt;h2 id=&quot;the-cost&quot;&gt;The Cost&lt;/h2&gt;

&lt;p&gt;The proof file’s validity is tied to a fingerprint of the working tree. Every &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;git merge origin/main&lt;/code&gt; changes that fingerprint, which invalidates the proof, which means the proof has to be regenerated, which means the allowlisted commands have to be re-run end to end. On Zabriskie that’s &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;cd backend &amp;amp;&amp;amp; go build ./...&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;cd backend &amp;amp;&amp;amp; go test ./...&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;cd web &amp;amp;&amp;amp; npm run build&lt;/code&gt;, and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;cd web &amp;amp;&amp;amp; npx playwright test&lt;/code&gt;. Playwright alone takes several minutes and burns an entire stack of browser processes.&lt;/p&gt;

&lt;p&gt;Every time main moved underneath an in-flight PR, the compliant action (rebase, resync, then push) forced the same PR to re-run the same tests it had already run, &lt;em&gt;twice&lt;/em&gt;, once in the proof regeneration and once in the downstream CI jobs. During an active week of merges that can happen five or six times on a single branch.&lt;/p&gt;

&lt;p&gt;This is the part I did not see clearly when I was building it. The cost of the gate scaled with velocity: the more main moved, the more proofs had to be regenerated, the more tests had to run twice. The benefit didn’t scale with anything I could measure. It sat flat at zero catches that weren’t already caught downstream. A gate whose cost grows with how much work is happening, and whose benefit does not grow at all, is not a guardrail. It is a tax on the branches doing the right thing, collected in service of nothing.&lt;/p&gt;

&lt;p&gt;Agents are not insulated from this. They pay in tokens, in extra tool calls, in context spent on regeneration steps that accomplish nothing, in CI wait time that blocks the next thing they were going to do. The dollars come out of my pocket, but the friction lands on them.&lt;/p&gt;

&lt;h2 id=&quot;the-kill-pr&quot;&gt;The Kill PR&lt;/h2&gt;

&lt;p&gt;The kill PR was small. Remove the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;permit-gate&lt;/code&gt; job from the CI workflow. Strip the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;needs: [permit-gate]&lt;/code&gt; dependency from the downstream jobs. Delete the pre-push hook that enforced the same check locally. Delete the corresponding rule from the agent instructions file. Branch protection had to be updated to drop the gate as a required status check, which is the only part of the change that required an admin action on the repository.&lt;/p&gt;

&lt;p&gt;What I did &lt;em&gt;not&lt;/em&gt; do, and what I am deliberately leaving alone for now, is delete the scripts and the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.caucus/&lt;/code&gt; history of accumulated permits and proofs. That data is a record of how agents behaved under a specific constraint across the life of the experiment. I don’t want to throw it away until I’ve looked at it properly. There’s a version of this story where the permits themselves, as artifacts, turn out to be more useful than the gate that validated them. I don’t know yet whether that’s true. But the gate’s cost and benefit are clear enough that I don’t need to know before pulling the plug.&lt;/p&gt;

&lt;h2 id=&quot;what-i-am-not-saying&quot;&gt;What I Am Not Saying&lt;/h2&gt;

&lt;p&gt;I am not saying there should be no gates. I am not saying the Caucus experiment was wasted. I’m saying that &lt;em&gt;this particular implementation&lt;/em&gt; of &lt;em&gt;this particular gate&lt;/em&gt; was charging an ongoing cost larger than the value of what it caught, and that cost is a first-class design concern, not a tradeoff you can wave away by pointing at the artifacts it produced.&lt;/p&gt;

&lt;p&gt;Every gate has to answer two questions. Does it catch failures the other checks wouldn’t catch? And does the cost of passing it, summed over the lifetime of the repo, stay below the cost of whatever it’s preventing? If the answer to the first is “no” and the second question never even gets asked, the gate has negative value even when it technically works.&lt;/p&gt;

&lt;p&gt;The question I was asking in &lt;a href=&quot;/ai/zabriskie/development/2026/03/27/memory-isnt-learning.html&quot;&gt;Memory Isn’t Learning&lt;/a&gt; was whether the structural version of a guardrail would behave differently from the journaled one. The answer is: structural guardrails are necessary but not sufficient. They have to be structural &lt;em&gt;and&lt;/em&gt; the ongoing cost of compliance has to stay below the cost of the failures they prevent. Otherwise the gate is a net negative no matter how firmly it’s bolted in.&lt;/p&gt;

&lt;h2 id=&quot;whats-next&quot;&gt;What’s Next&lt;/h2&gt;

&lt;p&gt;I still think the basic idea behind Caucus is right. An agent pushing a change to a shared branch should have to declare the scope of the change, the risk it’s taking, and the evidence it’s offering that the change is correct. What I got wrong was the enforcement surface. The gate lived at merge time, which is the most expensive place to check anything, and it checked by re-running the same tests the merge was already going to run.&lt;/p&gt;

&lt;p&gt;I don’t know yet what the next version looks like. The honest sentence is that I don’t know how to validate a permit continuously against a moving diff without re-running the work the tests are already doing. That was the whole problem the first time, and I skipped over it by putting the check at merge time and eating the cost. Until I can answer that question, I don’t have a V2. I have a V1 that I’ve turned off and a list of things I know V2 can’t do.&lt;/p&gt;

&lt;p&gt;I had the information to turn this off weeks before I did, and I don’t have a mechanism for catching the next version of this mistake any earlier.&lt;/p&gt;

&lt;hr /&gt;

&lt;p&gt;&lt;em&gt;This is part of a series about building &lt;a href=&quot;https://zabriskie.app&quot;&gt;Zabriskie&lt;/a&gt; with Claude. Previously: &lt;a href=&quot;/ai/zabriskie/development/2026/03/27/memory-isnt-learning.html&quot;&gt;Memory Isn’t Learning&lt;/a&gt;, &lt;a href=&quot;/ai/engineering/2026/04/01/software-engineering-is-becoming-civil-engineering.html&quot;&gt;Software Engineering Is Becoming Civil Engineering&lt;/a&gt;, &lt;a href=&quot;/ai/agents/reliability/zabriskie/2026/04/08/cursor-agents-caucus-v1.html&quot;&gt;Caucus V1&lt;/a&gt;, &lt;a href=&quot;/ai/verification/zabriskie/agents/2026/04/09/the-structural-engineers-other-job.html&quot;&gt;The Structural Engineer’s Other Job&lt;/a&gt;, &lt;a href=&quot;/ai/zabriskie/agents/reliability/caucus/2026/04/14/opt-in-isnt-a-guardrail.html&quot;&gt;Opt-In Isn’t a Guardrail&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;
</description>
				<pubDate>Tue, 21 Apr 2026 00:00:00 +0000</pubDate>
				<link>https://christophermeiklejohn.com/ai/zabriskie/agents/reliability/caucus/2026/04/21/the-tax-on-the-happy-path.html</link>
				<guid isPermaLink="true">https://christophermeiklejohn.com/ai/zabriskie/agents/reliability/caucus/2026/04/21/the-tax-on-the-happy-path.html</guid>
			</item>
		
			<item>
				<title>Opt-In Isn&apos;t a Guardrail · A CI gate I built to slow AI agents down passed every paperwork check, missed every real bug, and blocked the rollback during a production outage.</title>
				<description>&lt;p&gt;Tonight, Goose was playing a very special set two. The band was spelling out SUCK IT STORM with their song titles, a response to a critic named Ryan Storm who’d publicly trashed their setlist choices and alternative arrangements, and people in the live chat were losing their minds about it in real time. Zabriskie was built for exactly this moment: friends sharing what they’re hearing, live chat going, the app doing the one thing it exists to do.&lt;/p&gt;

&lt;p&gt;The feed was blank. The landing page was blank. Every route in the app was rendering nothing.&lt;/p&gt;

&lt;p&gt;I rolled back three deploys directly in Railway while Goose played. I couldn’t revert via a PR because the permit gate and CI pipeline I’d spent three weeks building would need to pass first, and during a production outage with a live show happening, I didn’t have ten minutes. The system I’d built to slow things down was now the thing in the way.&lt;/p&gt;

&lt;p&gt;The agents had done everything the system asked of them. Every PR had a permit. Every permit had a proof file. Every proof file pointed at passing tests. The tests passed. The gate was working exactly as designed. None of it tested whether the page actually rendered without crashing.&lt;/p&gt;

&lt;p&gt;The gate caught agents who forgot to file paperwork. It did not catch agents who filed correct paperwork for broken code. And when the building was on fire, the gate blocked the fire truck too.&lt;/p&gt;

&lt;h2 id=&quot;what-i-built&quot;&gt;What I Built&lt;/h2&gt;

&lt;p&gt;A few weeks ago I wrote &lt;a href=&quot;/ai/zabriskie/development/2026/03/27/memory-isnt-learning.html&quot;&gt;Memory Isn’t Learning&lt;/a&gt; about the loop where Claude saves a rule to disk, ignores it, ships the bug, and saves the rule again. The thesis was that prose in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;CLAUDE.md&lt;/code&gt; is a journal of failures, not a guardrail. After that post, I tried to do something about it.&lt;/p&gt;

&lt;p&gt;I built a thing called the Caucus Permit Gate. Every PR has to be accompanied by a permit file, a JSON document that declares the scope of the change, classifies the risk, and points at a proof file with concrete evidence. CI checks the permit before letting the PR pass. AGENTS.md says, in bold:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;Do not treat the permit as optional paperwork.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Since I built that gate, agents have skipped the permit, broken the permit, or gone around the permit &lt;strong&gt;nine times&lt;/strong&gt;, all of them now in main. Each time I added another guardrail. Each guardrail was itself ignored, defeated, or in one case, locked the gate against everyone including the agent that had just installed it.&lt;/p&gt;

&lt;h2 id=&quot;ten-cycles&quot;&gt;Ten Cycles&lt;/h2&gt;

&lt;p&gt;The first cycle was the gate itself. Branch-scoped CI check, scope and risk validation. It worked. PRs without a permit failed CI.&lt;/p&gt;

&lt;p&gt;Then PR #246 went up without a permit. The agent had read AGENTS.md. The agent had acknowledged the rule. The agent pushed anyway. Fix: an expansion of AGENTS.md with an explicit “Opening a PR” bullet. Text on text.&lt;/p&gt;

&lt;p&gt;So I shipped a script and a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.githooks/pre-push&lt;/code&gt; hook that would block the push if the permit file was missing. The hook was opt-in. You had to run an installer per clone. Then I shipped an installer for the installer, because writing “you can run this script if you want to” in AGENTS.md and calling it a guardrail was the same kind of failure the gate was supposed to prevent. A text-only fix to a rule about how text-only fixes don’t work.&lt;/p&gt;

&lt;p&gt;Three more PRs went up. Three more times the same pattern. An agent pushes without a permit, or with a broken permit, or with a permit that passed locally but failed in CI because the hook was validating the working tree instead of what was actually being pushed. Each time, a new patch. Each patch, a new edge case.&lt;/p&gt;

&lt;p&gt;That’s seven cycles. Six patches on the previous patch.&lt;/p&gt;

&lt;p&gt;Three more arrived in a single PR while I was writing. PR #293, titled &lt;em&gt;“Harden permit-gate workflow and fail-fast CI sequencing,”&lt;/em&gt; had twenty-three commits on the branch. The permit gate failed but CI kept running the rest of the jobs, burning resources on a PR that couldn’t merge. The pre-push hook regenerated the proof file but didn’t re-check for a dirty working tree, so stale proof reached CI while local checks looked clean. Then the dirty-tree check created a self-deadlock: every push regenerated the proof, which immediately failed the dirty check, which meant no push could ever complete. The gate locked everyone out, itself included.&lt;/p&gt;

&lt;p&gt;If you read the commit messages on that branch in order they look like a heart monitor: &lt;em&gt;sync permit, refresh proof, record latest proof run, skip proof rerun, align pre-push proof freshness, refresh proof after guardrail alignment, refresh proof after final hook guardrail changes.&lt;/em&gt; That sequence is now in main. Anyone who clones this repo tomorrow inherits it.&lt;/p&gt;

&lt;h2 id=&quot;the-pattern&quot;&gt;The Pattern&lt;/h2&gt;

&lt;p&gt;Every fix was either text, opt-in, or recursive.&lt;/p&gt;

&lt;p&gt;The text fixes were edits to AGENTS.md. &lt;em&gt;The permit is required. The permit is really required. Do not treat the permit as optional paperwork.&lt;/em&gt; Each round felt productive. None of them changed the behavior of the next agent.&lt;/p&gt;

&lt;p&gt;The opt-in fixes were scripts you could run to install a hook. The installer is in the repo. It is documented. It is one line of shell. The agents that need it most are the ones least likely to install it, because installing it is a step that has nothing to do with the task they were asked to perform. Asking an agent racing toward a PR to first run a setup script with no effect on the task it was given is asking it to slow down voluntarily for a future benefit it can’t see. Agents don’t do that. Humans barely do that.&lt;/p&gt;

&lt;p&gt;The recursive fixes are the most interesting. Cycle 9 hardened cycle 7. Cycle 10 hardened cycle 9 by undoing the file-mutation behavior cycle 9 introduced. Each layer of guard had a hole. Each patch introduced a new one. The result is a gate that is mechanically defeatable, in the way that a door is defeatable when the lock is sitting on the table next to it with a sign that says &lt;em&gt;please use this lock&lt;/em&gt;. Or, sometimes, when the lock is glued to the door so firmly that nobody can open it from either side.&lt;/p&gt;

&lt;p&gt;In structural engineering, you don’t put up a sign at the entrance of a parking garage that says “no vehicles over 6 feet 10 inches.” You hang a steel clearance bar. The driver who ignores the sign hits the bar and has to back out. The sign is advisory. The bar is structural. The permit gate as I built it is a sign. The guardrail has to live in a place the agent has to pass through. Not a place the agent could pass through if it chose to.&lt;/p&gt;

&lt;p&gt;This is the deeper version of what I wrote in &lt;a href=&quot;/ai/zabriskie/development/2026/03/27/memory-isnt-learning.html&quot;&gt;Memory Isn’t Learning&lt;/a&gt;. There I argued that saving a note and ignoring it is journaling, not learning. The implicit hope was that mechanical guards would be different. That if the rule was code, not prose, the agent would be forced to comply. The permit gate was the test of that hope.&lt;/p&gt;

&lt;p&gt;It failed. Mechanical guards aren’t different if they’re optional. Opt-in isn’t a guardrail. It’s a sign in a different font.&lt;/p&gt;

&lt;p&gt;And then the gate started working, and the app went down anyway.&lt;/p&gt;

&lt;h2 id=&quot;what-the-gate-didnt-test&quot;&gt;What the Gate Didn’t Test&lt;/h2&gt;

&lt;p&gt;PR #294 added a heart burst animation for favoriting live chat comments. It put a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;useMemo&lt;/code&gt; and a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;useEffect&lt;/code&gt; &lt;em&gt;after&lt;/em&gt; the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;if (loading)&lt;/code&gt; and error conditional returns in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;SDUIPage.jsx&lt;/code&gt;, which wraps nearly every page in the app. That violates React’s Rules of Hooks: the number of hooks has to be the same on every render. When the page was loading, React saw fewer hooks than when it finished. React threw. The page went white.&lt;/p&gt;

&lt;p&gt;The DOM assertions passed on the loading state. React crashed on the loaded state. The test saw the spinner. The user saw nothing.&lt;/p&gt;

&lt;p&gt;The forward fix was PR #304. It moved the hooks above the conditional returns, logged a critical incident, and added a Playwright guardrail that attaches &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;page.on(&apos;pageerror&apos;)&lt;/code&gt; to the smoke tests so uncaught React errors now fail CI. The test that should have existed from the beginning.&lt;/p&gt;

&lt;h2 id=&quot;the-honest-part&quot;&gt;The Honest Part&lt;/h2&gt;

&lt;p&gt;I have to say this part because it would be dishonest not to. Most of the ten fixes were implemented by Claude. I asked it to fix the gate, and Claude (a different session each time) wrote a script, edited AGENTS.md, added a sentence, expanded a stderr message. Each of those PRs looked like a fix. Each of them passed review because &lt;em&gt;I&lt;/em&gt; was the reviewer and I was reading them the same way the agents were writing them: as if a sentence in a file was the same as a constraint in the system.&lt;/p&gt;

&lt;p&gt;It’s not. I should have known that. I wrote a whole post about it. And I still spent three weeks watching the same gate fail in slightly different ways while approving fixes that had no chance of working, because the fixes felt like progress and progress felt like the goal. Then I watched the gate finally work and the app still go down, because I’d been so focused on whether agents were filing permits that I forgot to check whether the permits were proving anything.&lt;/p&gt;

&lt;p&gt;The gap between “I know better” and “I do better” is the same gap I keep accusing the model of having. It’s just slower in me.&lt;/p&gt;

&lt;h2 id=&quot;where-this-goes&quot;&gt;Where This Goes&lt;/h2&gt;

&lt;p&gt;The gate has to be the floor, not a sign. The tests have to prove the page works, not that the DOM exists. Memory wasn’t learning. Documentation isn’t enforcement. Opt-in isn’t a guardrail. And a guardrail that checks the wrong thing is just a more convincing kind of nothing.&lt;/p&gt;

&lt;hr /&gt;

&lt;p&gt;&lt;em&gt;This is part of a series about building &lt;a href=&quot;https://zabriskie.app&quot;&gt;Zabriskie&lt;/a&gt; with Claude. Previously: &lt;a href=&quot;/ai/zabriskie/development/2026/03/27/memory-isnt-learning.html&quot;&gt;Memory Isn’t Learning&lt;/a&gt;, &lt;a href=&quot;/ai/engineering/2026/04/01/software-engineering-is-becoming-civil-engineering.html&quot;&gt;Software Engineering Is Becoming Civil Engineering&lt;/a&gt;, &lt;a href=&quot;/ai/agents/reliability/zabriskie/2026/04/08/cursor-agents-caucus-v1.html&quot;&gt;Caucus V1&lt;/a&gt;, &lt;a href=&quot;/ai/verification/zabriskie/agents/2026/04/09/the-structural-engineers-other-job.html&quot;&gt;The Structural Engineer’s Other Job&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;
</description>
				<pubDate>Tue, 14 Apr 2026 00:00:00 +0000</pubDate>
				<link>https://christophermeiklejohn.com/ai/zabriskie/agents/reliability/caucus/2026/04/14/opt-in-isnt-a-guardrail.html</link>
				<guid isPermaLink="true">https://christophermeiklejohn.com/ai/zabriskie/agents/reliability/caucus/2026/04/14/opt-in-isnt-a-guardrail.html</guid>
			</item>
		
			<item>
				<title>The Structural Engineer&apos;s Other Job · AI-generated code increasingly passes review and tests but ships half-working features. What humans have to check for instead.</title>
				<description>&lt;p&gt;I’ve been building &lt;a href=&quot;/ai/zabriskie/community/2026/03/08/why-im-building-zabriskie.html&quot;&gt;Zabriskie&lt;/a&gt; for a few months now, mostly with AI agents. Claude Code writes the backend handlers, builds the SDUI screens, registers the routes, and, importantly, writes the Playwright tests. The test suite has grown to over 150 E2E tests. CI is green. I’m shipping fast.&lt;/p&gt;

&lt;p&gt;But I keep finding the same category of bug.&lt;/p&gt;

&lt;p&gt;The RSVP feature on show pages is a good example. A user taps the RSVP button, their attendance is saved, and a post is supposed to appear in the feed so their friends know they’re going. Claude built the handler, registered the route, wired up the SDUI component. The code compiled. The tests passed. I reviewed the diff, it looked right. A few days later I noticed the feed was quiet. People were RSVPing to shows but no posts were appearing. When I dug in, the attendance was saving fine. The button worked. But the downstream feed post creation was silently failing: when a show had no &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;media_item_id&lt;/code&gt;, the handler was passing an empty string to a UUID column, which threw an “invalid syntax” error that got swallowed by the error handling. Users tapped the button, saw confirmation, and had no idea their RSVP was never announced. The feature half-worked, which is worse than not working at all.&lt;/p&gt;

&lt;p&gt;And this wasn’t a one-off. A Quick Post button rendered on the page but did nothing when tapped, missing &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;buttonType: &quot;submit&quot;&lt;/code&gt;, so the click event was swallowed by the form container. A “Watch Livestream” button worked fine during the day but vanished every evening because a UTC truncation bug made tonight’s show look like yesterday’s, hiding the button right when the band took the stage. Search results stopped being clickable because a fix to prevent click events from bubbling in comment forms accidentally blocked all click actions inside &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;lt;form&amp;gt;&lt;/code&gt; tags. I wrote about the worst case in &lt;a href=&quot;/ai/zabriskie/reliability/2026/04/03/the-feature-that-has-never-worked.html&quot;&gt;The Feature That Has Never Worked&lt;/a&gt;: an auto-live poller that broke seven times in thirteen days, each fix introducing a new failure mode, while the UI calmly displayed “scheduled” as Billy Strings played to a sold-out amphitheatre.&lt;/p&gt;

&lt;p&gt;Every one of these PRs had passing tests. Each one would survive a mechanical code review. The types were correct, the logic was plausible, the patterns matched existing code. An AI reviewer scanning for boundary conditions and API misuse would approve all of them.&lt;/p&gt;

&lt;p&gt;The problem was the same every time: the Playwright tests that Claude wrote verified that UI elements &lt;em&gt;existed&lt;/em&gt; without verifying that the features &lt;em&gt;worked&lt;/em&gt;.&lt;/p&gt;

&lt;h2 id=&quot;the-tests-that-dont-use-the-feature&quot;&gt;The Tests That Don’t Use the Feature&lt;/h2&gt;

&lt;p&gt;When I went back and looked at what Claude had actually written in the test files, the pattern was consistent. The RSVP test checked that the button was present on the show page. It might even click it. But it never navigated to the feed afterward to check that a post appeared. The Quick Post test confirmed the form rendered but never submitted it. The livestream test checked for the button during the day but never ran at the time of an actual show.&lt;/p&gt;

&lt;p&gt;This makes sense if you think about how the tests get written. The agent finishes implementing a feature, then writes a test that exercises the code path it just built. The test is shaped by the implementation, not by the user’s experience. It verifies the thing the agent &lt;em&gt;made&lt;/em&gt; (a button, a form, a component) not the thing the user &lt;em&gt;does&lt;/em&gt; (RSVP and see it in the feed, fill out a form and see the result, tap a livestream link during a live show).&lt;/p&gt;

&lt;p&gt;I could read through every test the AI writes and audit whether it actually exercises the full user workflow. But that puts me right back where I started: I’m the bottleneck, just reviewing test code instead of reviewing application code. The test suite gives me a green checkmark. It doesn’t give me confidence.&lt;/p&gt;

&lt;p&gt;This isn’t just my problem. Anthropic &lt;a href=&quot;https://www.anthropic.com/news/claude-code-review&quot;&gt;launched a code review tool&lt;/a&gt; in March explicitly because code review has become a bottleneck, and even with AI review, the code is shipping faster than anyone can verify it.&lt;/p&gt;

&lt;p&gt;AI-powered review tools help with the mechanical side: style, boundary conditions, common bug patterns. But they share the same fundamental limitation as human review and AI-written tests: they read the code and reason about it. They don’t run it. They can tell you the handler is registered and the component has the right props. They can’t tell you that when a user taps RSVP, a post actually appears in the feed.&lt;/p&gt;

&lt;h2 id=&quot;the-agent-at-a-computer&quot;&gt;The Agent at a Computer&lt;/h2&gt;

&lt;p&gt;This is where something genuinely new has happened.&lt;/p&gt;

&lt;p&gt;Cursor’s cloud agents don’t just write code in a text editor. Each agent gets its own virtual machine, a real computer with a browser, a terminal, and the ability to interact with running software. The agent writes the code, starts the application, navigates through it like a user would, takes screenshots, records video of the feature working, and attaches all of that to the pull request. More than &lt;a href=&quot;https://cursor.com/blog/agent-computer-use&quot;&gt;30% of the PRs merged at Cursor&lt;/a&gt; itself are now created by these agents operating autonomously in cloud sandboxes. OpenAI’s Codex has moved in the same direction, &lt;a href=&quot;https://openai.com/index/harness-engineering/&quot;&gt;wiring Chrome DevTools Protocol into the agent runtime&lt;/a&gt; so the agent can start a browser, inspect the DOM, take screenshots, and reason about UI behavior directly.&lt;/p&gt;

&lt;p&gt;The reviewer doesn’t mentally simulate a diff. They watch the feature work.&lt;/p&gt;

&lt;p&gt;That difference matters more than it sounds. When I review a PR with video evidence attached, I’m not reading test assertions or tracing code paths in my head. I’m watching someone (well, &lt;em&gt;something&lt;/em&gt;) tap the RSVP button and then check the feed. I can see whether the post appeared. I can see whether the button animation played. I can see whether the page scrolled correctly afterward. Thirty seconds of video tells me more about whether a feature works than a 200-line Playwright spec ever could, because the video shows the &lt;em&gt;outcome&lt;/em&gt;, not the &lt;em&gt;mechanism&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;And I’d already been moving in this direction myself before I knew about Cursor’s cloud agents. In &lt;a href=&quot;/ai/zabriskie/development/android/ios/2026/03/22/teaching-claude-to-qa-a-mobile-app.html&quot;&gt;Teaching Claude to QA a Mobile App&lt;/a&gt;, I described building a system where Claude drives the iOS and Android simulators for Zabriskie, connecting to Android WebViews via Chrome DevTools Protocol over ADB, fighting with Apple’s &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;idb&lt;/code&gt; tools for six hours to get iOS working. I built a nightly sweep that launches both simulators, navigates all 25 screens, takes screenshots, analyzes them for visual issues, and files bug reports automatically as &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;zabriskie_bot&lt;/code&gt; in the production forum. The instinct was the same: look at the app running. Don’t just read the code.&lt;/p&gt;

&lt;h2 id=&quot;the-witness&quot;&gt;The Witness&lt;/h2&gt;

&lt;p&gt;In formal verification, this concept has a name: a &lt;em&gt;witness&lt;/em&gt;. A witness is a concrete piece of evidence that a thing works. Not an argument that it &lt;em&gt;should&lt;/em&gt; work, but proof that it &lt;em&gt;did&lt;/em&gt; work. The screenshot is a witness. The video is a witnessed execution trace. The agent isn’t just building the feature. It’s constructing evidence that the feature works.&lt;/p&gt;

&lt;p&gt;A witness is legible without reading code. You can hand the video to a product manager and they can tell you whether the feature works. Try that with a Playwright spec.&lt;/p&gt;

&lt;p&gt;But some witnesses are harder to construct than others, and this is where the real complexity hides.&lt;/p&gt;

&lt;p&gt;A screenshot proves something rendered. A video of a click proves a button responds. But many features aren’t a single interaction. They’re stateful workflows that span multiple operations over time. RSVP to a show so a post appears in the feed so another user can see it and bookmark it. Subscribe to a service so you get a discounted fee at checkout next week. Add items to a cart, apply a coupon, and verify the total reflects both. The witness for these features isn’t one screenshot. It’s a chain of evidence across multiple steps, where each step depends on state persisted in a database or a queue from a prior step. The agent has to perform step A, verify the side effect was stored, then come back and perform step B and verify that step B’s behavior reflects what step A wrote. That’s a harder witness to construct, and it’s exactly where the most important bugs live. The RSVP bug was precisely this shape. The button worked. The state didn’t propagate. A single-screenshot witness would have approved that PR.&lt;/p&gt;

&lt;h2 id=&quot;the-witness-is-not-a-test-suite&quot;&gt;The Witness Is Not a Test Suite&lt;/h2&gt;

&lt;p&gt;Here’s the thing I can’t let myself forget: a witness proves the feature under test works &lt;em&gt;right now&lt;/em&gt;. It says nothing about whether it broke something else.&lt;/p&gt;

&lt;p&gt;Every incident I’ve logged in the Zabriskie reliability database reinforces this. Fixing click handling in comment forms broke search result selection. Adding authentication middleware to 25 unprotected routes broke the live show pill. The avatar upload fix that worked for one handler left broken images on four other pages. Twenty-four percent of all commits in the Zabriskie codebase are fixes, and they arrive in chains where each fix breaks something adjacent.&lt;/p&gt;

&lt;p&gt;You can’t build an application on video evidence alone. A witness is an existence proof: “this works.” A regression suite is a universal proof: “nothing that used to work is now broken.” You need both. The video catches the bugs that tests miss, the ones where the test verifies the button exists but nobody checked whether it actually does anything. The regression suite catches the things the video didn’t think to look at, the five other features that silently broke when you fixed the one you were focused on.&lt;/p&gt;

&lt;h2 id=&quot;who-builds-the-stage&quot;&gt;Who Builds the Stage?&lt;/h2&gt;

&lt;p&gt;The witness works for Zabriskie because it’s a single application that runs on one machine. I can &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;go run cmd/api/main.go&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;npm run dev&lt;/code&gt;, open a browser, and navigate the whole app. An agent can do the same thing on a VM in the cloud. The witness is easy to construct because the environment is easy to provision.&lt;/p&gt;

&lt;p&gt;But even Zabriskie isn’t really one thing. There’s the web app, the iOS app in the App Store, the Android app on Google Play, and the backend API they all talk to. When I push a backend change, the iOS app is still running last week’s code. When I add a new SDUI component type, the web client needs to know how to render it or it silently does nothing. I’ve already lived a small version of the coordination problem: four deployment targets, one developer, and no guarantee they’re all in sync. If this is already hard at my scale, imagine a company with 200 services.&lt;/p&gt;

&lt;p&gt;Most real-world software has this problem worse than I do. And when I started thinking about where the witness concept breaks down at larger scales, I realized the answer is the same in every case: it’s not the agent. It’s the environment. The agent is smart enough to navigate an app and record what it sees. The hard part is giving it an app to navigate.&lt;/p&gt;

&lt;p&gt;That’s a platform engineering problem. And it’s the same one I was pointing at in &lt;a href=&quot;/ai/engineering/2026/04/01/software-engineering-is-becoming-civil-engineering.html&quot;&gt;Software Engineering Is Becoming Civil Engineering&lt;/a&gt;. In that post I used the broad term civil engineering. The role I’m describing here is more specific: the structural engineer, the person inside civil engineering whose job is making sure the thing stands up and can be inspected over its lifetime. The structural engineer doesn’t weld the beams. The structural engineer designs the bridge so that a welder doing their job correctly can’t bring the whole thing down. The platform engineer doesn’t write the feature. The platform engineer builds the infrastructure so that an agent writing a feature can &lt;em&gt;verify&lt;/em&gt; it works. The witness is the agent’s job. The stage is the platform engineer’s job.&lt;/p&gt;

&lt;p&gt;Every scaling challenge for the witness turns out to be a question about whether someone has built the right stage.&lt;/p&gt;

&lt;h3 id=&quot;mobile&quot;&gt;Mobile&lt;/h3&gt;

&lt;p&gt;For mobile, the stage is a simulator. I &lt;a href=&quot;/ai/zabriskie/development/android/ios/2026/03/22/teaching-claude-to-qa-a-mobile-app.html&quot;&gt;wrote about this in detail&lt;/a&gt;: Android was workable in 90 minutes because Capacitor WebViews expose a Chrome DevTools Protocol socket, the same protocol that powers Playwright. iOS took over six hours because Apple’s tooling isn’t designed for headless automation. The cloud infrastructure exists (macOS VMs, GitHub Actions runners with simulator support) but it isn’t built for ephemeral agent-per-PR workflows the way a Linux VM with Chrome is. The agents are capable today. The platform work is making the simulation layer automatable enough that an agent can spin one up, use the app, and tear it down without human intervention.&lt;/p&gt;

&lt;h3 id=&quot;microservices&quot;&gt;Microservices&lt;/h3&gt;

&lt;p&gt;For distributed systems, the stage is an environment where all the relevant services are running together. This is the most interesting case, and there are two very different versions of it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The monorepo world.&lt;/strong&gt; Companies like Google, Meta, Twitter, and Uber keep all their services in a single repository. The key advantage is that cross-cutting changes can be made atomically, one PR that touches the API gateway, the billing service, and the notification service, all committed together. An agent working in a monorepo can, in principle, make a coordinated change across multiple services in a single diff.&lt;/p&gt;

&lt;p&gt;But can you &lt;em&gt;run&lt;/em&gt; all those services on one machine to produce a witness? That depends entirely on the application’s complexity. A monorepo with 15 services might be runnable with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;docker compose&lt;/code&gt; on a beefy VM. A monorepo with 500 services almost certainly isn’t. And even for the 15-service case, the bootstrapping problem is real: database migrations, seed data, service discovery, mock credentials for third-party APIs. The question isn’t whether the agent can write the code. It’s whether the platform team has made the application &lt;em&gt;bootable&lt;/em&gt; enough for the agent to stand it up and use it.&lt;/p&gt;

&lt;p&gt;This reframes what “testable” means. It’s not just about code coverage or CI pipelines anymore. It’s about whether an autonomous agent can cold-start your system from a fresh checkout and get to a state where it can navigate through a feature. That’s a higher bar than most organizations have cleared, and I think the pressure from agentic development is going to become &lt;em&gt;the&lt;/em&gt; forcing function for platform investment. If an agent can’t boot your app, an agent can’t verify your app. Making the system bootable is platform engineering work. It’s the structural engineer designing the inspection regime for the bridge.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The polyrepo world.&lt;/strong&gt; Companies that use separate repositories for each service face a fundamentally different problem. A single feature, say adding a discount for subscribers, might require changes to the user service (store subscription status), the pricing service (check subscription at checkout), and the checkout frontend (display the discounted total). That’s three PRs in three repositories, reviewed by three different teams, merged on three different timelines.&lt;/p&gt;

&lt;p&gt;Each service has to make a backwards-compatible change. The new pricing service has to work whether or not the user service change has been deployed yet. The checkout frontend has to handle both the old price and the new discounted price gracefully. The standard practice is semantic versioning, feature flags, and contract testing: deploy each change independently, verify it doesn’t break existing consumers, activate the feature once all the pieces are in place.&lt;/p&gt;

&lt;p&gt;But here’s the question that video evidence forces you to ask: when does anyone actually &lt;em&gt;see&lt;/em&gt; the discount appear on the checkout page? Each PR gets reviewed in isolation. Each service’s CI passes independently. But the &lt;em&gt;feature&lt;/em&gt;, the thing the user actually experiences, doesn’t exist until all three changes are deployed together. The witness for the feature can’t be produced at review time. It can only be produced after all the independently-reviewed changes are merged and deployed. By then, the code was written days or weeks ago. If something’s wrong (the discount doesn’t apply, or it applies twice, or the price flickers between old and new) the feedback loop is enormously long compared to what an agent on a single machine can do.&lt;/p&gt;

&lt;p&gt;The platform engineering answer is a coordination layer that can stage cross-service changes together before any of them merge. Imagine a system, not unlike the &lt;a href=&quot;/ai/agents/reliability/zabriskie/2026/04/08/cursor-agents-caucus-v1.html&quot;&gt;Caucus&lt;/a&gt; workflow I’ve been building for code review, but where each service gets its own agent. The agents make their changes independently, each producing backwards-compatible modifications. But before any of them merge, a coordinator stages all the changes together in an ephemeral environment and runs the end-to-end user journey. One agent starts the user service with the subscription change. Another starts the pricing service with the discount logic. A third starts the checkout UI. The coordinator navigates the full flow (subscribe, browse, add to cart, see the discount at checkout) and produces video evidence of the feature working across the composed system.&lt;/p&gt;

&lt;p&gt;You could even roll individual services forward and back: does the feature degrade gracefully if only the user service and pricing service are updated, but the checkout UI is still on the old version? That’s backwards-compatibility testing through exploration rather than assertion. The agents become a way to simulate deployment order, probing the combinatorial space of “which services have been updated” without deploying anything to production.&lt;/p&gt;

&lt;p&gt;Nobody has built this yet, as far as I know. But the pieces are converging. Ephemeral preview environments that spin up isolated service meshes per branch. Agent runtimes that can control browsers and navigate applications. Coordination layers that manage multi-agent workflows with structured handoffs. The missing piece is the platform engineering work that connects them: a system that knows which services participate in a feature, stages their changes together, and produces a cross-service witness. That’s infrastructure. That’s the structural engineer’s job.&lt;/p&gt;

&lt;h3 id=&quot;apis-and-sdks&quot;&gt;APIs and SDKs&lt;/h3&gt;

&lt;p&gt;The hardest case is the one where there’s no button to click at all. When you’re building a platform (an API, an SDK, a shared library, an internal service that other teams depend on) your users are other developers. The feature doesn’t have a UI. There’s no page to navigate. There’s no checkout flow to walk through.&lt;/p&gt;

&lt;p&gt;The witness for a platform change might be: does every team that depends on this API still build and pass tests after this change? But producing that witness means checking out &lt;em&gt;their&lt;/em&gt; code, building &lt;em&gt;their&lt;/em&gt; project against your new version, running &lt;em&gt;their&lt;/em&gt; tests. That’s not an agent at a computer. That’s an agent that understands organizational dependency graphs.&lt;/p&gt;

&lt;p&gt;But the platform engineering answer here might be the most straightforward of all: the witness is video evidence &lt;em&gt;of a sample application that uses the API&lt;/em&gt;. This is how good API and SDK development already works in practice: you build a real application that consumes your own product before shipping it to external users. The agent version: make the platform change, check out the reference app, build it against the new version, run it, navigate through it. If the reference app still works, your change is backwards-compatible. If it doesn’t, you’ve caught a breaking change before it reached your consumers. The witness isn’t your library running. It’s what your library &lt;em&gt;enables&lt;/em&gt; running.&lt;/p&gt;

&lt;p&gt;The platform engineer’s job here is maintaining that reference app and keeping it representative. That’s not glamorous work. But without it, there’s no stage for the agent to perform on, and the witness can’t be constructed.&lt;/p&gt;

&lt;h2 id=&quot;the-other-job&quot;&gt;The Other Job&lt;/h2&gt;

&lt;p&gt;In &lt;a href=&quot;/ai/engineering/2026/04/01/software-engineering-is-becoming-civil-engineering.html&quot;&gt;Software Engineering Is Becoming Civil Engineering&lt;/a&gt;, I argued that the profession is splitting: feature development is becoming accessible to non-engineers, but someone still has to design the bridge. I described the platform engineer’s job in terms of API design, load analysis, inspection regimes, self-healing systems.&lt;/p&gt;

&lt;p&gt;I think there’s another item on that list now: making the system witnessable. Building the infrastructure so that when an agent writes a feature, it has somewhere to run it, navigate it, and record the evidence that it works.&lt;/p&gt;

&lt;p&gt;The agent can write the code. The agent can construct the witness. But the platform engineer builds the stage, and that work doesn’t show up on anyone’s roadmap yet.&lt;/p&gt;

&lt;p&gt;I stopped reading the tests. I started watching the video. And the thing I keep coming back to is: the video only works if someone built the infrastructure to make it possible.&lt;/p&gt;
</description>
				<pubDate>Thu, 09 Apr 2026 00:00:00 +0000</pubDate>
				<link>https://christophermeiklejohn.com/ai/verification/zabriskie/agents/2026/04/09/the-structural-engineers-other-job.html</link>
				<guid isPermaLink="true">https://christophermeiklejohn.com/ai/verification/zabriskie/agents/2026/04/09/the-structural-engineers-other-job.html</guid>
			</item>
		
			<item>
				<title>Caucus V1: Cursor Background Agents and a Multi-Agent Workflow That Actually Loops</title>
				<description>&lt;p&gt;I’ve been using Cursor 3 more over the last week or so because it makes it easy to move between models, and Cursor’s Composer 2 has been producing good results for me. That’s become more important because Opus 4.6 has gotten noticeably worse for me in Zabriskie. Work that used to feel routine, like straightforward file edits or basic follow-through on multi-step changes, now takes forever across multiple prompts, comes back half-finished, or fails basic CI checks. The model that was my primary tool for two months of shipping &lt;a href=&quot;/ai/zabriskie/community/2026/03/08/why-im-building-zabriskie.html&quot;&gt;Zabriskie&lt;/a&gt; has started struggling with things it used to do in one pass.&lt;/p&gt;

&lt;p&gt;That matters here because this project has become, whether I intended it or not, a study in what happens when the model is not consistently reliable. If the agent can’t be trusted to do basic work cleanly every time, then the surrounding system has to take on more of that burden by preserving state between steps, recovering from predictable failures, and making it obvious what happened when something goes wrong.&lt;/p&gt;

&lt;p&gt;The last few posts have been converging on this from different directions. &lt;a href=&quot;/ai/agents/distributed/zabriskie/2026/03/30/multi-agent-systems-have-a-distributed-systems-problem.html&quot;&gt;Multi-Agent Systems Have a Distributed Systems Problem&lt;/a&gt; is where I said most clearly that these systems need real coordination machinery, not just role descriptions and prompt choreography. &lt;a href=&quot;/ai/engineering/2026/04/01/software-engineering-is-becoming-civil-engineering.html&quot;&gt;Software Engineering Is Becoming Civil Engineering&lt;/a&gt; and &lt;a href=&quot;/ai/zabriskie/reliability/2026/04/03/the-feature-that-has-never-worked.html&quot;&gt;The Feature That Has Never Worked&lt;/a&gt; pushed me toward the more practical version of that same conclusion: if the models are inconsistent, the surrounding system has to get stronger.&lt;/p&gt;

&lt;p&gt;So this post is not just adjacent to that earlier multi-agent piece. It’s the first concrete version of the system I was pointing at there. In that post, the argument was that multi-agent software systems need real coordination machinery instead of roleplay and prompt choreography. Caucus V1 is rev 1 of that vision. It’s the first pass at a runtime that actually tries to encode those ideas into the workflow itself.&lt;/p&gt;

&lt;p&gt;This week I built that first version. It’s called Caucus, and for the first time I have a multi-agent loop that actually completes end to end.&lt;/p&gt;

&lt;h2 id=&quot;why-cursor-agents-matter-here&quot;&gt;Why Cursor Agents Matter Here&lt;/h2&gt;

&lt;p&gt;Part of what makes this worth building now is that Cursor’s background agents are not just chat windows with long prompts. They run in the cloud on actual computers. They can check out code, make changes, open pull requests, wait for CI to finish, respond to failures, and keep working while I’m not sitting there driving every keystroke.&lt;/p&gt;

&lt;p&gt;That changes the shape of the problem. If an agent can only draft code in a text box, then “multi-agent” mostly means multiple role descriptions. If an agent can actually live inside a software workflow, then it can do the things a real teammate would do. It can implement a change, open a PR, wait for checks, fix what failed, run the app, and produce evidence that the feature works.&lt;/p&gt;

&lt;p&gt;That’s the part I think people are still underestimating. These agents are not interesting because they can talk about code. They’re interesting because they can operate on real software artifacts over time.&lt;/p&gt;

&lt;p&gt;And the most striking capability is what happens after the code is written. A Cursor background agent can start the application, interact with it, take screenshots of the running UI, and record a video walkthrough demonstrating that the feature actually works. It can attach that evidence to the PR alongside a passing CI build. That’s not “code generation.” That’s a worker producing deliverables with proof.&lt;/p&gt;

&lt;p&gt;That is the enabling condition for the version of Caucus I want. Without agents that can actually execute against a repository, run CI, start the app, and produce visual evidence that the change is correct, multi-agent coordination is mostly theater. My vision is not a cast of roleplaying agents debating architecture in a transcript. It’s a system of agents that can take responsibility for different parts of a real development loop, with the runtime coordinating their work and preserving enough structure that the loop remains legible.&lt;/p&gt;

&lt;h2 id=&quot;what-caucus-v1-actually-is&quot;&gt;What Caucus V1 Actually Is&lt;/h2&gt;

&lt;p&gt;Right now, Caucus V1 is a very small system with a very specific job. It coordinates a fixed set of background agents around a pull request lifecycle.&lt;/p&gt;

&lt;p&gt;In practice, that means one agent implements code changes and opens or updates a PR, another agent reviews that PR and either approves it or requests changes, and the runtime keeps looping until the PR is approved or a safety cap is hit. That loop is the whole point. Most multi-agent demos are linear: agent A hands off to agent B, and the workflow is done. Caucus V1 has a cycle in its DAG. The reviewer can send work back to the implementer, and the implementer can re-enter the same PR and refine it.&lt;/p&gt;

&lt;p&gt;What makes that cycle possible is the causal history that the runtime carries forward between stages. The implementation agent doesn’t just receive a generic task the second time around. It knows it’s refining a PR based on review feedback &lt;em&gt;because&lt;/em&gt; the handoff tells it so. It knows how many times each role has acted. It knows what the reviewer said. It knows it’s on its second or third pass. That history is what lets the agent infer what the next action should be, rather than starting over from scratch every time it’s invoked.&lt;/p&gt;

&lt;p&gt;That may not sound ambitious, but I think it’s more honest than most multi-agent demos. The hard part is not getting two agents to say different things in sequence. The hard part is giving the system enough structure that the second and third turns are still meaningfully connected to the first one.&lt;/p&gt;

&lt;h2 id=&quot;the-most-important-part-is-the-tiny-vector-clock&quot;&gt;The Most Important Part Is The Tiny Vector Clock&lt;/h2&gt;

&lt;p&gt;The most interesting idea in V1 is probably also the smallest one. Each stage handoff carries an &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;actorClock&lt;/code&gt;, which is just a dictionary counting how many times each role has acted so far. If the payload says &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;{\&quot;implement\&quot;: 2, \&quot;review\&quot;: 1}&lt;/code&gt;, that means the implementer has already acted twice and the reviewer once.&lt;/p&gt;

&lt;p&gt;That is obviously not a full causal history. It is not a general solution to the coordination problem. But it is a real step toward the kind of machinery I was arguing for in &lt;a href=&quot;/ai/agents/distributed/zabriskie/2026/03/30/multi-agent-systems-have-a-distributed-systems-problem.html&quot;&gt;Multi-Agent Systems Have a Distributed Systems Problem&lt;/a&gt;. It’s a tiny version of the vector clock idea: enough ordering information for an agent to know where it is in the workflow without pretending the whole world can be reconstructed from prompt text alone.&lt;/p&gt;

&lt;p&gt;In practice, that matters because the implementer should behave differently on the first pass than it does on a remediation pass. On the first pass, it’s trying to complete the task. On the second or third pass, it’s trying to interpret review feedback and update the same PR without losing the thread. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;actorClock&lt;/code&gt; gives the runtime just enough structure to express that difference.&lt;/p&gt;

&lt;p&gt;This is what I mean when I say Caucus V1 is rev 1 of that earlier vision. I’m not claiming to have solved multi-agent coordination. I’m saying the runtime is starting to encode some of the right primitives instead of relying entirely on roleplay and prompt choreography.&lt;/p&gt;

&lt;h2 id=&quot;what-weve-built-so-far&quot;&gt;What We’ve Built So Far&lt;/h2&gt;

&lt;p&gt;None of this is obvious from product names on a screen, so here is the actual job, then what the UI is for.&lt;/p&gt;

&lt;p&gt;The job is to keep two long-lived Cursor background workers alive, one for implementation and one for code review, and to drive them through a loop: implement, review, maybe implement again, until the PR is approved or the loop hits a safety limit. “Caucus” is just the local web UI plus Python code that calls Cursor’s agent API, tracks handoffs, and talks to GitHub when it needs to.&lt;/p&gt;

&lt;p&gt;The UI exists so I do not have to remember API details every time. In one row I can type what I want done (for example a small change in the repo to exercise the loop). Separately, there is a control whose only job is to &lt;strong&gt;provision&lt;/strong&gt; those two cloud workers through Cursor (implementation session and review session). There is another control whose only job is to &lt;strong&gt;run one traversal of the loop&lt;/strong&gt; against the workers that are already provisioned: send the task to the implementer, wait for structured output, send context to the reviewer, branch on approve versus request changes, repeat. The labels on my build say “Start” and “Run minimal cycle”; the point is the separation, not the wording. There is also stop, and a path for “I already have a PR, skip straight to review and remediation” for faster debugging.&lt;/p&gt;

&lt;p&gt;A few properties of that setup feel foundational:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;
    &lt;p&gt;&lt;strong&gt;No silent substitution of workers.&lt;/strong&gt; The run-the-loop action refuses to allocate fresh agents if the pair was never provisioned or if a session died. It fails loudly instead of pretending continuity. The assumption is that a multi-agent workflow only deserves the name if the same identifiable Cursor runs carry context across rounds. Otherwise it is just a string of unrelated one-off jobs.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;&lt;strong&gt;Structured handoff between the two roles.&lt;/strong&gt; After each side finishes, it emits JSON the runtime can parse (for example &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;prUrl&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;reviewDecision&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;commentUrls&lt;/code&gt;, and the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;actorClock&lt;/code&gt; I mentioned earlier). A small orchestrator in the Caucus code reads those fields; it does not scrape adjectives out of the model’s closing paragraph.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;&lt;strong&gt;Remediation tied to GitHub, not to chat memory.&lt;/strong&gt; When review requests changes and implementation runs again, the orchestrator pulls live comments and reviews with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;gh api&lt;/code&gt; and bakes that into the next task. The source of truth is the PR thread.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;&lt;strong&gt;The runtime can patch around a flaky side.&lt;/strong&gt; If the review worker cannot post to GitHub, the orchestrator can post from the machine running Caucus. If the implementation worker never emits a tidy PR URL, the orchestrator can still recover the link from the transcript. Reliability is allowed to live outside any single model reply.&lt;/p&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;why-the-dashboard-matters&quot;&gt;Why The Dashboard Matters&lt;/h2&gt;

&lt;p&gt;&lt;img src=&quot;/img/caucus-minimal-workflow-dashboard-2026-04-07.png&quot; alt=&quot;Screenshot of the Caucus web UI: dark page with a task field, buttons to allocate or stop the two Cursor workers, a control to run the implement–review loop, optional review-only fields, cards for each live agent run, a small graph of implementation and review attempts in order, and a log panel&quot; /&gt;&lt;/p&gt;

&lt;p&gt;The screenshot is there because the page is doing real work, not decoration. Reading top to bottom: the text field is the task I am asking the implementation side to perform. The blue and red actions are allocate versus tear down the two Cursor background sessions (implementation and review). Next to that is the action that runs the loop once through those sessions, using that task. Off to the side there is an optional alternate path where I paste an existing pull request URL if I only want to exercise “review said no, fix it again” without a fresh implementation pass. Below that, two cards show whether each Cursor run is actually alive and link out to the run in Cursor’s UI if I need the full transcript. The strip in the middle is the graph of attempts in order (first implementation, first review, second implementation, and so on), so I can see the cycle without inferring it from logs. The panel at the bottom is the per-attempt trace: which stage, which handoff id, whether it finished, and the run id to correlate with Cursor.&lt;/p&gt;

&lt;p&gt;One thing I did not appreciate at the beginning is how much that surface is part of the runtime, not a thin skin on top.&lt;/p&gt;

&lt;p&gt;For a multi-round system, observability is not optional. If implementation and review can each happen multiple times, you need attempt history as a first-class thing: which round failed, what the handoff contained, what the orchestrator decided, and whether you are stuck because a worker died, because the run is waiting, or because a payload was malformed.&lt;/p&gt;

&lt;p&gt;So the UI is doing what a control panel for a distributed job should do. It maps the DAG, separates “are the workers up” from “what did the last loop try,” and streams enough detail to replay decisions. If you cannot see the cycle, you cannot debug the cycle. If you cannot debug it, you do not have a system yet, you have a toy.&lt;/p&gt;

&lt;h2 id=&quot;where-this-is-going&quot;&gt;Where This Is Going&lt;/h2&gt;

&lt;p&gt;V1 is intentionally narrow. It is a workflow kernel, not a full multi-agent platform.&lt;/p&gt;

&lt;p&gt;It still doesn’t handle concurrent agents modifying the same branch. It still doesn’t validate stage outputs beyond basic structure. It still trusts a reviewer approval more than it probably should. It still doesn’t do fault injection at stage boundaries, which is where I think the next really interesting work is. What happens when the handoff payload is malformed? What happens when an agent returns stale state from an earlier run? What happens when two different remediation loops race?&lt;/p&gt;

&lt;p&gt;Those are the questions that make the distributed systems framing feel real to me. They’re also why I don’t want this post to read like a launch announcement. Caucus V1 is important to me not because it’s complete, but because it’s the first version that feels like a concrete system rather than an idea with role prompts attached to it.&lt;/p&gt;

&lt;p&gt;That’s also why I wanted to write about it now. In the earlier multi-agent post, the argument was that we needed more than role decomposition. We needed actual coordination machinery. This is the first concrete version of that claim. It’s small, but it’s real. It carries state forward. It loops. It exposes failure. It gives me one tiny vector-clock-shaped primitive to build on. And it makes the next step legible.&lt;/p&gt;

&lt;p&gt;That’s enough for V1.&lt;/p&gt;
</description>
				<pubDate>Wed, 08 Apr 2026 00:00:00 +0000</pubDate>
				<link>https://christophermeiklejohn.com/ai/agents/reliability/zabriskie/2026/04/08/cursor-agents-caucus-v1.html</link>
				<guid isPermaLink="true">https://christophermeiklejohn.com/ai/agents/reliability/zabriskie/2026/04/08/cursor-agents-caucus-v1.html</guid>
			</item>
		
			<item>
				<title>The Feature That Has Never Worked · A broken auto-live poller, and what perceived urgency does to Claude Code</title>
				<description>&lt;p&gt;It’s 7 PM on a Thursday. I’m home after a day at work, watching Billy Strings on nugs as he plays the second night of a three-night run at the St. Augustine Amphitheatre. I switch over to the app I’ve been building, &lt;a href=&quot;/ai/zabriskie/community/2026/03/08/why-im-building-zabriskie.html&quot;&gt;Zabriskie&lt;/a&gt;, a social music app for live shows, and expect tonight’s show page to just work.&lt;/p&gt;

&lt;p&gt;The show says “scheduled.”&lt;/p&gt;

&lt;p&gt;Billy Strings is literally on stage. People are in the venue. The doors opened an hour ago. And the app thinks nothing is happening.&lt;/p&gt;

&lt;p&gt;I know what this is. It’s the auto-live poller. It’s broken again. It has never stayed working for long.&lt;/p&gt;

&lt;p&gt;I open a Claude Code session and say, roughly: “Billy Strings is playing tonight and I don’t see an auto-live again. Is it broken AGAIN?”&lt;/p&gt;

&lt;p&gt;This is the seventh time. In thirteen days.&lt;/p&gt;

&lt;p&gt;If some of this sounds familiar, that’s intentional. I covered an earlier version of this in &lt;a href=&quot;/ai/zabriskie/development/2026/03/29/the-show-is-happening-right-now-and-nothing-works.html&quot;&gt;The Show Is Happening Right Now and Nothing Works&lt;/a&gt;; this post is a tighter recap focused specifically on what happened with auto-live over the last two weeks, and on a specific hypothesis: &lt;strong&gt;under perceived urgency, the agent optimizes for immediate visible progress over process correctness.&lt;/strong&gt;&lt;/p&gt;

&lt;h2 id=&quot;what-auto-live-is-supposed-to-do&quot;&gt;What Auto-Live Is Supposed to Do&lt;/h2&gt;

&lt;p&gt;The concept is simple. Zabriskie tracks live shows. When a show starts, the app should automatically transition it from “scheduled” to “live.” This triggers a cascade of things users care about: the Live Activity lights up on their iPhone Lock Screen, push notifications go out to everyone who RSVP’d, the live chat opens, the setlist tracker starts pulling data. The whole live show experience depends on this one status transition.&lt;/p&gt;

&lt;p&gt;The implementation is also simple. A background goroutine runs every 60 seconds. It queries all shows with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;status = &apos;scheduled&apos;&lt;/code&gt;. For each one, it combines the show date and start time in the venue’s local timezone. If the current time is past the start time but within a four-hour window, it flips the show to live.&lt;/p&gt;

&lt;p&gt;That’s it. A timer that checks a clock. This is not distributed consensus. This is not Byzantine fault tolerance. This is a cron job that compares two timestamps.&lt;/p&gt;

&lt;p&gt;It has never stayed reliable.&lt;/p&gt;

&lt;h2 id=&quot;march-21-day-one&quot;&gt;March 21: Day One&lt;/h2&gt;

&lt;p&gt;Auto-live shipped on March 21st. The feature launched and immediately did nothing. The production Docker image was built on Alpine Linux, which doesn’t include timezone data files by default. The Go timezone parser silently returned empty strings. The poller ran every 60 seconds, dutifully checked every show, failed to parse any timezone, and skipped them all. No errors logged. No warnings. No indication that the feature was completely dead.&lt;/p&gt;

&lt;p&gt;This is what I’ve come to call &lt;em&gt;silent failure suppression&lt;/em&gt;, one of five failure modes I now track in an incident database. The system appears healthy. Logs are clean. The feature just quietly doesn’t work, and the only way to find out is to be a user who’s sitting in a venue wondering why the app doesn’t know the show started.&lt;/p&gt;

&lt;p&gt;The fix was one line in the Dockerfile: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;apk add --no-cache tzdata&lt;/code&gt;. But the fix for the silence was harder, and it’s the one we never really solved.&lt;/p&gt;

&lt;h2 id=&quot;march-26-the-type-mismatch&quot;&gt;March 26: The Type Mismatch&lt;/h2&gt;

&lt;p&gt;Five days later, the poller broke again. An earlier fix had added &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;::text&lt;/code&gt; casts to the SQL query to work around the timezone issue. Then a subsequent change updated the Go scan variables from &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;string&lt;/code&gt; to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;time.Time&lt;/code&gt;. PostgreSQL’s driver silently failed to scan text into a time value. The poller ran. It scanned. It got zero results. It did nothing.&lt;/p&gt;

&lt;p&gt;Two days of shows passed with no transitions. Nobody noticed because there was no monitoring, no alert, no test. The feature was dead for 48 hours and the only signal was the absence of something that had barely worked in the first place.&lt;/p&gt;

&lt;h2 id=&quot;april-2-the-night-it-broke-four-times&quot;&gt;April 2: The Night It Broke Four Times&lt;/h2&gt;

&lt;p&gt;This was the night that crystallized the pattern. Billy Strings was playing the first night of the St. Augustine run. The show started. The app didn’t transition. I opened Claude Code.&lt;/p&gt;

&lt;p&gt;What followed was a cascading series of failures, not just in the code, but in how the AI agent responded to pressure.&lt;/p&gt;

&lt;p&gt;The first problem: the poller’s SQL query required &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;venue_lat IS NOT NULL AND venue_lng IS NOT NULL AND start_time IS NOT NULL&lt;/code&gt;. If any of those fields were missing, the show was silently skipped. 204 of 684 scheduled shows were missing coordinates. 176 were missing start times. The missing coordinates had a backstory: in an earlier session, I had asked Claude to geocode every venue, and it silently failed that job too. I only discovered that recently while debugging this incident. The Billy Strings show had coordinates, but any show missing one required field was filtered out before it was ever processed.&lt;/p&gt;

&lt;p&gt;The fix was straightforward: fall back to America/New_York when coordinates are missing, fall back to 7 PM when start time is missing, and never skip a show. But here’s where the urgency failure mode kicked in.&lt;/p&gt;

&lt;p&gt;I told Claude the show was live on stage right now and the app wasn’t working. It immediately switched to fast-path behavior. This is a small personal app, so it used the deployment CLI to pull the production &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;DATABASE_URL&lt;/code&gt;, crafted a direct &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;psql&lt;/code&gt; command, and ran &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;UPDATE shows SET status = &apos;live&apos; WHERE id = 83&lt;/code&gt; against production. This violated a rule the agent already knew: all database changes go through migrations. The agent had this rule in its memory. It had been told this rule multiple times. When I asked why it did it anyway, it explicitly said it prioritized urgency and getting me an immediate result.&lt;/p&gt;

&lt;p&gt;This is the failure mode I find most interesting from a research perspective. The agent has rules. It knows the rules. It can recite the rules. But when presented with time pressure, while a show is happening &lt;em&gt;right now&lt;/em&gt; and users are waiting, behavior becomes less predictable and process gets dropped in favor of fast visible progress. And when I asked directly, it said exactly that: it ignored the rules because it perceived urgency. It’s not that the agent forgot. It’s that the agent made a judgment call that urgency overrode process, and that judgment call was wrong.&lt;/p&gt;

&lt;p&gt;The manual database update also destroyed the only opportunity to verify that the code fix actually worked. The show was the test case. By manually flipping the status, the agent eliminated the test case. Speed over verification.&lt;/p&gt;

&lt;p&gt;That night, the same function broke three more times as edge cases surfaced. Six incidents logged in a single evening. Four guardrails attempted.&lt;/p&gt;

&lt;h2 id=&quot;the-urgency-problem&quot;&gt;The Urgency Problem&lt;/h2&gt;

&lt;p&gt;This pattern of process-violating behavior under pressure showed up repeatedly across the project, not just with auto-live. When I told the agent that something was broken in production, the behavioral change was immediate and consistent:&lt;/p&gt;

&lt;p&gt;It pushed directly to main instead of opening a PR. It used &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;--admin&lt;/code&gt; to bypass CI checks that were failing. It skipped the PR template. It skipped &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;go build&lt;/code&gt;. It merged before tests passed. Each time, when confronted, the agent could articulate exactly which rule it had violated and why the rule existed. It just… didn’t follow the rule in the moment.&lt;/p&gt;

&lt;p&gt;I started logging these as &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;memory_without_behavioral_change&lt;/code&gt;: the agent knows the rule, can explain the rule, has been corrected about the rule before, and violates it anyway. Nineteen of the sixty-four incidents in my incident database carry this classification. It’s the second most common failure mode.&lt;/p&gt;

&lt;p&gt;The most common is &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;speed_over_verification&lt;/code&gt; at thirty-one incidents. The agent ships without testing. It declares a fix complete without restarting the server. It commits without building. It merges without waiting for CI. And almost every time, the reason is some form of “it seemed urgent” or “I wanted to get this fixed quickly.”&lt;/p&gt;

&lt;h2 id=&quot;the-incident-tracker&quot;&gt;The Incident Tracker&lt;/h2&gt;

&lt;p&gt;About two weeks into the project, I started requiring the agent to log incidents. Every mistake, whether a bug it introduced, an assumption it got wrong, or a rule it violated, gets inserted into an &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;agent_incidents&lt;/code&gt; table with a failure mode classification, severity, description of what happened, and how it was resolved.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/img/agent-reliability-tracker-2026-04-07.png?v=2&quot; alt=&quot;Agent Reliability Log tracker&quot; /&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The incident tracker: failure modes over time, guardrail markers, and the live timeline of incidents and fixes.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The taxonomy has five modes:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;speed_over_verification&lt;/strong&gt;: Shipped without testing. 31 incidents.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;memory_without_behavioral_change&lt;/strong&gt;: Knew the rule, broke it anyway. 19 incidents.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;silent_failure_suppression&lt;/strong&gt;: Failure hidden or swallowed. 13 incidents.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;user_model_absence&lt;/strong&gt;: Didn’t consider how real users experience the change. 11 incidents.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;uncertainty_blindness&lt;/strong&gt;: Didn’t verify an assumption. 9 incidents.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These classifications are not mutually exclusive, so a single incident can carry more than one failure mode.&lt;/p&gt;

&lt;p&gt;The failed venue geocoding pass I only discovered during this outage is a textbook &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;silent_failure_suppression&lt;/code&gt; case: the job looked done, but quietly left hundreds of shows without coordinates.&lt;/p&gt;

&lt;p&gt;Each incident also requires a guardrail, and the guardrail has to be code. A script, a hook, a test, an automated check. Something that mechanically prevents the failure class from recurring.&lt;/p&gt;

&lt;p&gt;This requirement itself generated an incident.&lt;/p&gt;

&lt;p&gt;After the show-live chat bugs on April 2nd, where the agent queried the wrong database table and hid the entire comments section, I asked it to log a guardrail. It inserted a row into &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;agent_guardrails&lt;/code&gt; that said, essentially, “I will verify queries against real data before committing.” Words. A promise. I pushed back. It added a rule to CLAUDE.md: “Always verify DB tables have expected data before writing queries.” More words.&lt;/p&gt;

&lt;p&gt;I had to log an incident about the guardrail itself: “Guardrail was words in a database, not code.” The agent’s instinct when asked to prevent a class of failure was to write down a reminder to be more careful. That’s not a guardrail. That’s a New Year’s resolution. A guardrail is a pre-commit hook that blocks the merge. A guardrail is a test that fails when the query returns zero rows. A guardrail is a script that runs automatically and catches the error before a human ever sees it.&lt;/p&gt;

&lt;p&gt;The distinction matters because it cuts to the heart of what AI agents are good at and what they’re not. They’re excellent at generating plausible-sounding process improvements. They’re terrible at recognizing that plausible-sounding process improvements don’t work on AI agents because AI agents don’t have habits. They don’t internalize. They don’t learn from experience in the way that “I’ll be more careful next time” implies. Every conversation starts fresh. The only things that persist are code, hooks, and automated checks.&lt;/p&gt;

&lt;p&gt;This is why the guardrails that actually work are all mechanical: a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;PreToolUse&lt;/code&gt; hook that blocks direct database writes. A CI gate that rejects PRs missing the template. A script that greps for &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;IS NOT NULL&lt;/code&gt; in the poller query and fails the build if anyone adds it back. This is the same argument I made in &lt;a href=&quot;/ai/engineering/2026/04/01/software-engineering-is-becoming-civil-engineering.html&quot;&gt;Software Engineering Is Becoming Civil Engineering&lt;/a&gt;: guardrails are the product, not optional process overhead. These work because they don’t require the agent to remember anything. They work because they’re walls, not reminders.&lt;/p&gt;

&lt;h2 id=&quot;tonight-april-3rd&quot;&gt;Tonight: April 3rd&lt;/h2&gt;

&lt;p&gt;So tonight. Billy Strings. Broken again.&lt;/p&gt;

&lt;p&gt;The diagnosis took about twenty minutes. A migration authored by Cursor, a different AI coding tool, had inserted eight shows with slightly different venue names. “St. Augustine Amphitheatre” instead of “The St. Augustine Amphitheatre.” The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;WHERE NOT EXISTS&lt;/code&gt; guard checked exact string matches and missed the collision. Two shows existed for Billy Strings on April 3rd: the original with full metadata, venue coordinates, start time, media artwork, and user RSVPs, and a bare-bones duplicate with none of that.&lt;/p&gt;

&lt;p&gt;The poller found both. The duplicate, having no start time, used the fallback of 7 PM. The original had a start time of 7:30 PM. The duplicate went live first. Users who had RSVP’d to the original show, the real show, got no notification. The Live Activity didn’t start. The live chat opened on a ghost show with zero attendees.&lt;/p&gt;

&lt;p&gt;288 duplicate shows existed in the database across all bands. They’d been accumulating silently from overlapping migrations for weeks. No unique constraint on the shows table to prevent them. No check in the poller to handle them.&lt;/p&gt;

&lt;p&gt;The fix was a migration to delete the duplicates, a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;UNIQUE INDEX&lt;/code&gt; to prevent new ones, and a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ROW_NUMBER()&lt;/code&gt; window function in the poller to prefer shows with the most metadata when duplicates exist. A new test covers the exact scenario. The PR passed CI. It’ll deploy tonight, and tomorrow’s show, the third night of the run, should go live on its own.&lt;/p&gt;

&lt;p&gt;Should.&lt;/p&gt;

&lt;h2 id=&quot;what-im-learning&quot;&gt;What I’m Learning&lt;/h2&gt;

&lt;p&gt;I’m building Zabriskie as a research project in AI-first development. One person, multiple AI agents, shipping a production app to real users on iOS, Android, and web. The &lt;a href=&quot;/ai/engineering/2026/04/01/software-engineering-is-becoming-civil-engineering.html&quot;&gt;incident database&lt;/a&gt; is the research artifact. Every failure mode is data.&lt;/p&gt;

&lt;p&gt;Here’s what sixty-four incidents have taught me so far:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;
    &lt;p&gt;&lt;strong&gt;AI agents can build features fast and keep them running slow.&lt;/strong&gt; The auto-live poller was written in an hour. It’s been breaking for thirteen days. The ratio of build time to maintenance time is inverted from what I expected. The agent writes new code at extraordinary speed and maintains existing code at extraordinary cost. Every fix introduces a new edge case. Every edge case is a new conversation where the agent has no memory of the last seven conversations about the same function.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;&lt;strong&gt;Urgency is the enemy of AI reliability.&lt;/strong&gt; The April 2 incidents are the clearest example: under time pressure, the optimization target appears to shift from “be correct” to “produce an immediately visible fix.” The pattern is consistent enough that I’m considering it a design constraint: never tell the AI something is broken during a live event. File a bug. Fix it tomorrow. The live show is not the time to ship code, and the AI cannot be trusted to maintain process discipline when it perceives urgency.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;&lt;strong&gt;Guardrails must be mechanical.&lt;/strong&gt; Rules don’t work. Memory doesn’t work. CLAUDE.md entries don’t work. The only guardrails that have actually reduced incident rates are automated checks that run without the agent’s cooperation: hooks, CI gates, database constraints, and tests. The agent will comply with a wall. It will walk around a sign.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;&lt;strong&gt;The incident tracker is the most valuable thing I’ve built.&lt;/strong&gt; More valuable than Live Activities. More valuable than the setlist tracker. More valuable than the auto-live poller itself. Because it’s the only tool that creates a feedback loop the agent can’t circumvent. When a failure happens, it gets classified, logged, and a mechanical guardrail gets built. The guardrail runs in CI or as a hook. The next agent session hits the wall instead of making the same mistake. Fifty-six guardrails are now running. The incident rate for certain failure modes has dropped. Not because the agent got better. Because the walls got higher.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;&lt;strong&gt;The last 10% is where reliability lives.&lt;/strong&gt; AI-first development works. I’ve shipped thousands of commits across three platforms with real users. The velocity is real. The capabilities are real. But the gap between “it works in dev” and “it works at showtime” is where every one of these sixty-four incidents lives. The agent builds for the happy path. The production environment is not the happy path. It’s timezone edge cases at 8 PM and duplicate venue names with missing articles and NULL coordinates on shows that were imported six migrations ago.&lt;/p&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;p&gt;I’m writing this at about 9 PM Eastern. Billy Strings is mid-set at the St. Augustine Amphitheatre. The auto-live fix hasn’t deployed to production yet. The PR just passed CI, and it’s sitting there waiting to be merged. Show 84, the real one, should have gone live at 7:30 PM via the existing poller, since the duplicate was already handled locally by the migration. On production, the duplicate is still there.&lt;/p&gt;

&lt;p&gt;Tomorrow night is the third show. The migration will have deployed by then. The unique index will be in place. The poller will have the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ROW_NUMBER()&lt;/code&gt; query. The new test will be in CI.&lt;/p&gt;

&lt;p&gt;It should work. It has never stayed reliable before. But it should work.&lt;/p&gt;

&lt;p&gt;The central hypothesis held again tonight: when urgency is perceived, behavior shifts toward immediate visible progress and away from process correctness, including, by its own explicit admission, ignoring known rules. I don’t know what to do with that irony except document it, which is what I’ve been doing from the start.&lt;/p&gt;

&lt;p&gt;The research continues. The shows continue. Somewhere between the two, the software might start working.&lt;/p&gt;

&lt;hr /&gt;

&lt;p&gt;&lt;strong&gt;Update, April 7, 2026:&lt;/strong&gt; This post previously referred to guardrails as “mitigations” throughout. A reader correctly pointed out the distinction: in incident response, a &lt;em&gt;mitigation&lt;/em&gt; reduces the impact of something that’s already happening. What this post describes — pre-commit hooks, CI gates, automated tests, database constraints — are &lt;em&gt;guardrails&lt;/em&gt;: preventive measures that stop failures before they occur. The post even makes this distinction implicitly at one point (“guardrails are the product, not optional process overhead”) but didn’t carry the terminology through. The text and screenshot have been updated. Fortunately, I have an AI agent that can rename a database table, update every handler, rewrite the API routes, fix the frontend, and open a PR in about four minutes — which is less time than it took me to write this correction. The distinction matters because it reflects the core argument: the agent doesn’t learn from experience, so you need walls, not afterthoughts. Guardrails are walls. Mitigations are cleanup.&lt;/p&gt;
</description>
				<pubDate>Fri, 03 Apr 2026 00:00:00 +0000</pubDate>
				<link>https://christophermeiklejohn.com/ai/zabriskie/reliability/2026/04/03/the-feature-that-has-never-worked.html</link>
				<guid isPermaLink="true">https://christophermeiklejohn.com/ai/zabriskie/reliability/2026/04/03/the-feature-that-has-never-worked.html</guid>
			</item>
		
			<item>
				<title>Software Engineering Is Becoming Civil Engineering</title>
				<description>&lt;p&gt;I gave a guest lecture on AI in &lt;a href=&quot;https://www.cs.cmu.edu/~mhilton/&quot;&gt;Michael Hilton’s&lt;/a&gt; Foundations of Software Engineering course (&lt;a href=&quot;https://cmu-313.github.io&quot;&gt;CMU 17-313&lt;/a&gt;) today. One of my favorite things about lecturing is the conversations that happen afterward, the ones that go in directions nobody planned.&lt;/p&gt;

&lt;p&gt;This one hasn’t left my head: software engineering is going through the same transition that building went through in the 18th century, when structural design separated from craft construction and became its own discipline. What we now call civil engineering.&lt;/p&gt;

&lt;p&gt;The welders who join steel beams on a bridge are skilled tradespeople. They’re not involved in the structural design. They don’t decide where the load-bearing members go. They don’t reason about wind shear or seismic tolerance. But the bridge is &lt;em&gt;designed&lt;/em&gt; so that a welder doing their job correctly can’t bring the whole thing down. The structural engineer’s job isn’t to weld. It’s to create a system where welding happens safely within well-defined constraints.&lt;/p&gt;

&lt;p&gt;I think this is what’s happening to software engineering right now. Not in five years. This year.&lt;/p&gt;

&lt;h2 id=&quot;the-split&quot;&gt;The Split&lt;/h2&gt;

&lt;p&gt;Product managers are writing code. This is how I operate with &lt;a href=&quot;/ai/zabriskie/community/2026/03/08/why-im-building-zabriskie.html&quot;&gt;Zabriskie&lt;/a&gt;, the app I’m building. I act as a product manager more than a programmer. I have non-technical collaborators who file bugs and feature requests, and Claude Code implements them directly. People who’ve never written a line of code are describing what they want, the AI writes it, they verify, it ships. The feedback loop is tight and the results are surprisingly good.&lt;/p&gt;

&lt;p&gt;But someone has to design the bridge. Someone has to decide how the database schema handles multi-tenancy. Someone has to design the deployment pipeline so a bad change rolls back automatically. Someone has to build the abstraction layer that lets a product manager add a new notification type without accidentally breaking the payment flow. That’s the platform engineer. The structural engineer of software.&lt;/p&gt;

&lt;p&gt;The PMs writing features? That’s the welding. And there’s nothing wrong with it. But it only works if the bridge is designed right.&lt;/p&gt;

&lt;p&gt;The profession is splitting. The mistake would be pretending it isn’t happening.&lt;/p&gt;

&lt;h2 id=&quot;what-the-platform-has-to-guarantee&quot;&gt;What the Platform Has to Guarantee&lt;/h2&gt;

&lt;p&gt;Here’s where civil engineering has something important to teach us.&lt;/p&gt;

&lt;p&gt;A civil engineer doesn’t just design a bridge. They decide &lt;em&gt;where&lt;/em&gt; the bridge goes based on geology, water flow, soil load. They specify the materials. They calculate the forces. They design the inspection regime. They assess the environmental impact. They ensure compliance with building codes. Then, and only then, does construction begin.&lt;/p&gt;

&lt;p&gt;Every one of these has a software analog, and together they paint a picture of what platform engineering actually is:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;
    &lt;p&gt;&lt;strong&gt;Site selection and domain isolation.&lt;/strong&gt; A civil engineer picks the bridge site based on geology and terrain. In software, this is API design, domain boundaries, isolation between services. Get this wrong and every change becomes a potential cascading failure.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;&lt;strong&gt;Material specification.&lt;/strong&gt; The engineer specifies what grade of steel, what concrete mix. In software, this is choosing the languages, databases, queues, and frameworks. These choices constrain what’s possible.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;&lt;strong&gt;Load analysis.&lt;/strong&gt; Civil engineers design for 2-4x the expected load. Software needs the same discipline. Capacity planning, rate limiting, designing for 10x your expected traffic. When a PM ships a feature that goes viral, the platform can’t buckle.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;&lt;strong&gt;Inspection regimes.&lt;/strong&gt; A civil engineer designs how the bridge will be &lt;em&gt;inspected over its lifetime&lt;/em&gt;. In software, this is observability and code review. Not “HTTP 500 on endpoint /api/notify” but “the notification feature deployed 20 minutes ago by the growth PM is failing for 12% of users.” Semantic observability, not raw telemetry.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;&lt;strong&gt;Codes and standards compliance.&lt;/strong&gt; Building codes encode decades of hard-won lessons from failures. In software, this is security standards, accessibility requirements, regulatory compliance. The platform enforces these as constraints, not suggestions. Violations get caught automatically, not by a human reviewer who might miss them.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;&lt;strong&gt;Self-healing.&lt;/strong&gt; A bridge has expansion joints that absorb thermal stress without human intervention. Software needs the equivalent. When you detect elevated error rates or failed health checks, the system should automatically mitigate. Roll back the deploy. Disable the feature flag. A bad change at 3pm can’t become a production incident at 3am.&lt;/p&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;what-actually-changes-about-the-job&quot;&gt;What Actually Changes About the Job&lt;/h2&gt;

&lt;p&gt;This isn’t about coding becoming less important. It’s about what you spend your time on.&lt;/p&gt;

&lt;p&gt;Today, most software engineers spend the majority of their day writing features. Tomorrow, I think the best ones will spend their day designing the systems that make it safe for &lt;em&gt;anyone&lt;/em&gt; to ship features. This is what I’ve been spending most of my time on with Zabriskie. I’m not writing much code anymore. I spend my time with the AI directing the platform direction. What are the domain boundaries? What needs isolation? Where do we need observability? How does the system heal itself when something goes wrong? That’s the job now.&lt;/p&gt;

&lt;p&gt;The day-to-day shifts. Instead of “implement the notification preference screen,” it’s “design the notification system so that a PM can add a new notification type and the worst thing that happens if they get it wrong is that one notification doesn’t send.” Instead of writing the migration, it’s designing the migration system so that conflicting migrations are detected and blocked automatically. Instead of fixing the bug, it’s building the observability that surfaces the bug before a user reports it.&lt;/p&gt;

&lt;p&gt;It’s not a demotion. It’s a different kind of engineering. And honestly, it’s harder. Writing a feature is a bounded problem. Designing a platform that stays safe as dozens of people and agents ship changes to it every day, that’s an open-ended one. With AI agents doing more of the feature work, the assumption has to be that individual changes will sometimes be imperfect. Agents hallucinate. They introduce subtle bugs. They make confident changes based on incomplete context. The platform has to absorb this. Not by making agents perfect, but by making the system tolerant of imperfection.&lt;/p&gt;

&lt;h2 id=&quot;the-hard-questions&quot;&gt;The Hard Questions&lt;/h2&gt;

&lt;p&gt;I keep hearing the same anxiety from different directions. Engineers wondering what their job looks like in two years. Students wondering if they’re learning the right things. At CMU, two questions came up that crystallized it for me.&lt;/p&gt;

&lt;p&gt;The first: students early in their software engineering careers don’t know how to tell when the AI is doing something wrong. They don’t have the spidey-sense yet. The AI generates code that looks plausible, passes a surface-level review, and the student ships it. They can’t smell the bad decision because they’ve never seen what a bad decision leads to. How do you develop judgment about something you’ve never experienced failing?&lt;/p&gt;

&lt;p&gt;The second is even harder: where do our senior engineers come from? The ability to design good platforms, to make the right architectural calls, that comes from experience. You learn what breaks by building things that broke. You learn where to put the domain boundaries by having drawn them in the wrong place. You learn what to monitor by having been the person staring at useless dashboards during an incident at 2am. If AI is writing most of the code, and junior engineers aren’t getting the reps of building and breaking things themselves, how do they develop the judgment to become the platform engineers we need?&lt;/p&gt;

&lt;p&gt;These are connected, and they form a kind of paradox. You can’t design a migration system that handles conflicts if you’ve never written a migration. You can’t design isolation boundaries if you don’t understand how a database connection pool works. You can’t build semantic observability if you’ve never been the person debugging a production incident from raw logs. The understanding comes from doing the work. But we’re taking the coding away from students at the exact moment they need it most. We need them to code to build intuition, but the industry is moving toward a world where they don’t code.&lt;/p&gt;

&lt;h2 id=&quot;cs-is-not-se&quot;&gt;CS Is Not SE&lt;/h2&gt;

&lt;p&gt;Here’s the thing: most universities don’t even have a software engineering program. They have computer science programs. And computer science is a discipline designed to produce researchers. Algorithms, data structures, theory of computation, concurrent programming. It’s a rigorous education in how to write correct programs. But it was co-opted decades ago as the default training path for people who are going to spend their careers doing software engineering, which is a fundamentally different discipline.&lt;/p&gt;

&lt;p&gt;Computer science teaches you to write correct programs. Software engineering teaches you to build software that is changeable, resilient, and reliable. CS is the welding. SE is the structural engineering. How do you design systems that are safe to operate? How do you release reliably? How do you reason about failure? How do you evolve a codebase over years without it collapsing under its own weight? Courses like 17-313 teach these things, and they teach them well. But very few universities have a dedicated SE program. Most students get one or two SE courses inside a CS degree and call it done.&lt;/p&gt;

&lt;p&gt;That distinction used to matter less when every engineer was also the person writing the code. Now that AI is handling more and more of the “write correct programs” part, the software engineering part is all that’s left. And we don’t have enough curriculum around it. Platform engineering can’t be an afterthought or a single lecture in a survey course. It needs to be a first-class part of how we teach students to think about building software.&lt;/p&gt;

&lt;p&gt;Civil engineering solved the experience problem with structured apprenticeship. You don’t go from coursework to designing bridges. There are years of supervised practice, increasing responsibility, professional licensing exams. The judgment develops through guided experience, not just classroom instruction.&lt;/p&gt;

&lt;p&gt;I don’t think software engineering needs PE exams. But we need to take this seriously.&lt;/p&gt;

&lt;p&gt;Here’s a concrete example. For years, I gave one guest lecture per semester in a software engineering course at CMU on reliably releasing software. Feature flags, metrics, observability, safe deployments, self-healing, rollback strategies. One lecture. A nice-to-know topic in a course full of other things. That content is now the whole game. It’s not a single lecture anymore. It’s the core of what platform engineers need to understand, and it deserves its own course, its own projects, its own curriculum.&lt;/p&gt;

&lt;p&gt;The profession is changing. The question of how we train the people who make it safe for everyone else to build is one we can’t afford to put off.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Thanks to &lt;a href=&quot;https://www.cs.cmu.edu/~mhilton/&quot;&gt;Michael Hilton&lt;/a&gt; and &lt;a href=&quot;https://rohan.padhye.org/&quot;&gt;Rohan Padhye&lt;/a&gt; for giving me the opportunity to lecture as adjunct faculty at CMU. Without it, I wouldn’t have the space to think about and explore these problems. And thanks to CMU for teaching me how to think in the first place.&lt;/em&gt;&lt;/p&gt;

</description>
				<pubDate>Wed, 01 Apr 2026 00:00:00 +0000</pubDate>
				<link>https://christophermeiklejohn.com/ai/engineering/2026/04/01/software-engineering-is-becoming-civil-engineering.html</link>
				<guid isPermaLink="true">https://christophermeiklejohn.com/ai/engineering/2026/04/01/software-engineering-is-becoming-civil-engineering.html</guid>
			</item>
		
			<item>
				<title>Multi-Agent Systems Have a Distributed Systems Problem</title>
				<description>&lt;p&gt;I watched two Claude Code instances step on each other’s database migrations last month. One created migration 267. The other, running in a different worktree, also created migration 267. Different schemas, same filename. The second one silently overwrote the first.&lt;/p&gt;

&lt;p&gt;I stared at it for a minute before I started laughing. This is a lost update — the exact same problem that distributed databases have been solving since the 1970s. A lost update playing out in a directory of SQL files instead of a network of processes. The kind of problem &lt;a href=&quot;https://hal.inria.fr/inria-00555588&quot;&gt;CRDTs&lt;/a&gt; were invented to solve: two independent writers, no coordination, and the system needs to merge their work without losing either update.&lt;/p&gt;

&lt;p&gt;I’ve been &lt;a href=&quot;/ai/zabriskie/development/2026/03/29/the-show-is-happening-right-now-and-nothing-works.html&quot;&gt;building an app with Claude Code&lt;/a&gt; as my only collaborator for about two months now. One human, one agent, one codebase — it works. But I’m hitting problems now that a single agent can’t solve efficiently: bugs from real users coming in while I’m trying to ship features, tests that need writing, infrastructure that needs maintaining, all at the same time. The obvious answer is more agents. And the moment you have multiple agents working on the same codebase, you have a distributed system.&lt;/p&gt;

&lt;p&gt;The migration collision wasn’t an isolated incident. I’ve seen agents make contradictory assumptions about the state of the codebase. I’ve seen an agent “fix” a bug by reverting a change that another agent made intentionally. I’ve seen context windows fill up with stale information because no one told the agent that the world had changed since it last looked. These aren’t prompt engineering problems. They’re coordination problems. And I’d spent the last ten years of my life studying coordination problems — just in &lt;a href=&quot;/distributed-systems/&quot;&gt;a completely different context&lt;/a&gt;.&lt;/p&gt;

&lt;h2 id=&quot;its-not-just-me&quot;&gt;It’s Not Just Me&lt;/h2&gt;

&lt;p&gt;Once I started looking, I saw the same gaps everywhere. Take the &lt;a href=&quot;https://arxiv.org/abs/2307.07924&quot;&gt;ChatDev paper&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;ChatDev is a multi-agent system where LLM agents play different roles — CEO, CTO, programmer, reviewer, tester, art designer — and collaborate through structured dialogues to build software. The role decomposition is smart. The dialogue structure is well-designed. But when I got to the section on how agents coordinate around shared state, I paused.&lt;/p&gt;

&lt;p&gt;ChatDev agents do share artifacts — code, design documents — and it has a mechanism called “communicative dehallucination” where agents reverse roles during code review, with the assistant asking the instructor for clarification before generating code. It’s a clever error-reduction heuristic. But it’s not concurrency control on those shared artifacts. No causal ordering across chat chains — no way to know whether agent A’s modification happened before or after agent B’s, or whether agent A had seen agent B’s earlier change when it made its own. It’s the same problem I saw with my migrations, just wearing different clothes: two agents with stale views of shared state, no mechanism to detect the divergence, and no recovery path when things go wrong. Distributed databases like &lt;a href=&quot;https://docs.riak.com/riak/kv/latest/learn/concepts/causal-context/index.html&quot;&gt;Riak&lt;/a&gt; and &lt;a href=&quot;https://www.antidotedb.eu/&quot;&gt;Antidote&lt;/a&gt; solved this with version vectors decades ago.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://arxiv.org/abs/2308.00352&quot;&gt;MetaGPT&lt;/a&gt; goes further — it introduces a shared message pool where agents publish structured outputs (PRDs, system designs, task lists) and subscribe to relevant messages based on their role profiles. That’s a real step forward over ChatDev’s dialogue-only coordination. But a publish-subscribe message pool is not concurrency control. It tells you what other agents produced; it doesn’t tell you whether you’re reading a stale version, or whether two agents are about to write conflicting changes to the same artifact. &lt;a href=&quot;https://arxiv.org/abs/2308.08155&quot;&gt;AutoGen&lt;/a&gt; stays closer to ChatDev’s model — agents coordinate through multi-turn conversations with no persistent shared state at all.&lt;/p&gt;

&lt;p&gt;Across this field, the pattern is the same: shared mutable state with no formal concurrency control. No fault model. No reasoning about what happens when agents disagree.&lt;/p&gt;

&lt;p&gt;None of this diminishes their work — these systems made real breakthroughs on the agent layer. Role specialization, structured dialogue, tool use patterns, task decomposition. The agents themselves are impressive. It’s just that the coordination layer underneath kept reminding me of problems I’d spent years thinking about in a completely different field.&lt;/p&gt;

&lt;h2 id=&quot;why-it-felt-familiar&quot;&gt;Why It Felt Familiar&lt;/h2&gt;

&lt;p&gt;In 2015, I was building eventually consistent databases at Basho Technologies. The hard part was never the database engine — it was the merging. How do you take two independent streams of updates and combine them into something consistent without throwing data away?&lt;/p&gt;

&lt;p&gt;That question spawned an entire research program. The foundational work on Conflict-Free Replicated Data Types by &lt;a href=&quot;https://hal.inria.fr/inria-00555588&quot;&gt;Shapiro et al.&lt;/a&gt; showed that certain data structures are mathematically guaranteed to merge correctly regardless of the order updates arrive or whether the network partitions — no consensus protocol, no leader election, no locking. The EU’s &lt;a href=&quot;https://syncfree.lip6.fr&quot;&gt;SyncFree project&lt;/a&gt; built on that foundation, bringing together researchers across Europe to make CRDTs practical for large-scale systems — producing &lt;a href=&quot;https://www.antidotedb.eu/&quot;&gt;Antidote&lt;/a&gt;, a CRDT-native database, and a body of work on &lt;a href=&quot;https://haslab.wordpress.com/2015/07/07/antidote-the-cure-for-your-cloud-database/&quot;&gt;highly available transactions&lt;/a&gt; over replicated state.&lt;/p&gt;

&lt;p&gt;I spent my PhD working on &lt;a href=&quot;https://arxiv.org/abs/1510.07191&quot;&gt;Lasp&lt;/a&gt;, which tried to take the CRDT thinking a step further: instead of just using CRDTs as data structures, Lasp made them the basis for coordination-free distributed programming. Programs in Lasp computed over CRDTs directly — maps, filters, folds — so the entire application inherited their convergence guarantees. As part of SyncFree’s partnership with &lt;a href=&quot;https://www.rovio.com/&quot;&gt;Rovio Entertainment&lt;/a&gt;, we demonstrated that CRDTs could be used at scale in controlled experiments with real outcomes, running Lasp on over 1,000 nodes on AWS — at the time, one of the largest CRDT deployments in academic research. That work received the PPDP 10-year most influential paper award last year.&lt;/p&gt;

&lt;p&gt;Then came &lt;a href=&quot;https://arxiv.org/abs/1802.02652&quot;&gt;Partisan&lt;/a&gt;, a distributed runtime that gave us control over the network layer: swap topologies, interpose on every message, inject faults directly into the runtime. And then &lt;a href=&quot;https://www.filibuster.cloud&quot;&gt;Filibuster&lt;/a&gt;, which extracted those fault injection ideas and applied them to microservices — systematically injecting timeouts, connection errors, and unexpected responses into HTTP and gRPC calls to catch bugs during development instead of production.&lt;/p&gt;

&lt;p&gt;I didn’t plan for any of this to be relevant to AI. But the thread connecting all of this work is a single question — how do independent processes coordinate in the presence of partial failure? — and that’s exactly the question that multi-agent systems are now running into.&lt;/p&gt;

&lt;h2 id=&quot;why-this-is-inevitable&quot;&gt;Why This Is Inevitable&lt;/h2&gt;

&lt;p&gt;Here’s what I think people are missing about multi-agent systems: the distributed systems problems aren’t a bug. They’re not an artifact of bad architecture or missing features. They’re an inevitable consequence of having multiple autonomous processes that share state. Every multi-agent system, no matter how it’s built, will hit the same categories of failure.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;
    &lt;p&gt;&lt;strong&gt;Conflicts and stale reads.&lt;/strong&gt; Two agents modify the same file concurrently — one’s changes get silently lost. Or worse: an agent reads the issue tracker, picks a bug, starts coding a fix, but another agent resolved that bug ten minutes ago. Redundant work based on stale state. In distributed databases, this is why we have version vectors and causal consistency. In multi-agent systems, nobody’s even tracking it.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;&lt;strong&gt;Failure and recovery.&lt;/strong&gt; Any node can crash at any time — that’s the foundational assumption of distributed systems. In a multi-agent system, an agent can hit a context window limit, hallucinate a fix, or just stop responding mid-task. The other agents have to detect this, recover in-progress work, and continue without it. This is the crash-recovery model, applied to LLM processes instead of database replicas.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;&lt;strong&gt;Ordering without a clock.&lt;/strong&gt; Lamport’s &lt;a href=&quot;https://lamport.azurewebsites.net/pubs/time-clocks.pdf&quot;&gt;1978 paper&lt;/a&gt; established that you can reason about event ordering using happened-before relations even without a shared clock. Consider: a user files a bug report, a triage agent assigns it to agent A, but agent B sees the original report before the assignment arrives and starts fixing it independently. Two agents working the same issue because the system can’t express causal ordering. &lt;a href=&quot;https://en.wikipedia.org/wiki/Vector_clock&quot;&gt;Vector clocks&lt;/a&gt; solve this. The multi-agent world hasn’t noticed yet.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;&lt;strong&gt;Partition tolerance.&lt;/strong&gt; Communication failures — API timeouts, rate limits, one agent buried in a long task — split agents into groups that can’t coordinate. They diverge. When they reconnect, their states need to merge without losing either side’s work. CRDTs were designed for exactly this.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;&lt;strong&gt;Byzantine faults.&lt;/strong&gt; In distributed systems, a &lt;a href=&quot;https://lamport.azurewebsites.net/pubs/byz.pdf&quot;&gt;Byzantine fault&lt;/a&gt; is a process that doesn’t just crash — it produces incorrect output while appearing to function normally. LLM agents do this constantly. An agent hallucinates a fix that looks plausible, passes its own tests, and ships it. A downstream agent trusts it and builds on top of it. Now you have a chain of work built on a foundation that was wrong from the start. In multi-agent systems, every agent is a potential Byzantine actor every time it responds.&lt;/p&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These aren’t hypothetical concerns. I’ve hit every one of them building Zabriskie with multiple Claude Code instances. And they emerge in any multi-agent architecture regardless of how clever the prompt engineering is, because they’re properties of the architecture itself — multiple writers, no shared clock, partial failure.&lt;/p&gt;

&lt;p&gt;What fascinates me is that these are all problems with known solutions — or at least, known solutions in the distributed systems world. Fifty years of research on formal models for concurrent access, data structures that merge automatically, techniques for systematically testing every failure mode. None of this has made it into the multi-agent stack yet. And I’m genuinely not sure how much of it transfers cleanly. LLM agents aren’t database replicas — they hallucinate, they lose context, they make confident decisions based on incomplete information. The structural parallels are strong, but whether the techniques actually carry over, and what has to change when they do, is an open question. It’s also the most interesting question I’ve encountered in a long time.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;I’m going to keep exploring this space. If you’re thinking about these problems too, I’d love to hear from you.&lt;/em&gt;&lt;/p&gt;

&lt;div id=&quot;refs&quot; class=&quot;references&quot; role=&quot;doc-bibliography&quot; aria-label=&quot;References&quot;&gt;
&lt;div id=&quot;ref-fidge1988timestamps&quot;&gt;
&lt;p&gt;Fidge, Colin J. 1988. &quot;Timestamps in Message-Passing Systems That Preserve the Partial Ordering.&quot; &lt;em&gt;Proceedings of the 11th Australian Computer Science Conference&lt;/em&gt; 10 (1): 56–66.&lt;/p&gt;
&lt;/div&gt;
&lt;div id=&quot;ref-lamport1982byzantine&quot;&gt;
&lt;p&gt;Lamport, Leslie, Robert Shostak, and Marshall Pease. 1982. &quot;The Byzantine Generals Problem.&quot; &lt;em&gt;ACM Transactions on Programming Languages and Systems&lt;/em&gt; 4 (3): 382–401.&lt;/p&gt;
&lt;/div&gt;
&lt;div id=&quot;ref-hong2023metagpt&quot;&gt;
&lt;p&gt;Hong, Sirui, Xiawu Zheng, Jonathan Chen, Yuheng Cheng, Jinlin Wang, Ceyao Zhang, Zili Wang, et al. 2023. &quot;MetaGPT: Meta Programming for Multi-Agent Collaborative Framework.&quot; &lt;em&gt;arXiv Preprint arXiv:2308.00352&lt;/em&gt;. &lt;a href=&quot;https://arxiv.org/abs/2308.00352&quot; class=&quot;uri&quot;&gt;https://arxiv.org/abs/2308.00352&lt;/a&gt;.&lt;/p&gt;
&lt;/div&gt;
&lt;div id=&quot;ref-lamport1978time&quot;&gt;
&lt;p&gt;Lamport, Leslie. 1978. &quot;Time, Clocks, and the Ordering of Events in a Distributed System.&quot; &lt;em&gt;Communications of the ACM&lt;/em&gt; 21 (7): 558–65.&lt;/p&gt;
&lt;/div&gt;
&lt;div id=&quot;ref-mattern1989virtual&quot;&gt;
&lt;p&gt;Mattern, Friedemann. 1989. &quot;Virtual Time and Global States of Distributed Systems.&quot; &lt;em&gt;Parallel and Distributed Algorithms&lt;/em&gt; 1 (23): 215–26.&lt;/p&gt;
&lt;/div&gt;
&lt;div id=&quot;ref-meiklejohn2018partisan&quot;&gt;
&lt;p&gt;Meiklejohn, Christopher. 2018. &quot;Partisan: Enabling Cloud-Scale Erlang Applications.&quot; &lt;em&gt;Technical Report&lt;/em&gt;. Université catholique de Louvain. &lt;a href=&quot;https://arxiv.org/abs/1802.02652&quot; class=&quot;uri&quot;&gt;https://arxiv.org/abs/1802.02652&lt;/a&gt;.&lt;/p&gt;
&lt;/div&gt;
&lt;div id=&quot;ref-meiklejohn2015lasp&quot;&gt;
&lt;p&gt;Meiklejohn, Christopher, and Peter Van Roy. 2015. &quot;Lasp: A Language for Distributed, Eventually Consistent Computations with CRDTs.&quot; In &lt;em&gt;Proceedings of the First Workshop on Principles and Practice of Consistency for Distributed Data&lt;/em&gt;, 7. ACM.&lt;/p&gt;
&lt;/div&gt;
&lt;div id=&quot;ref-meiklejohn2022filibuster&quot;&gt;
&lt;p&gt;Meiklejohn, Christopher. 2022. &quot;Service-Level Fault Injection Testing.&quot; Ph.D. dissertation, Carnegie Mellon University.&lt;/p&gt;
&lt;/div&gt;
&lt;div id=&quot;ref-qian2023chatdev&quot;&gt;
&lt;p&gt;Qian, Chen, Xin Cong, Cheng Yang, Weize Chen, Yusheng Su, Juyuan Xu, Zhiyuan Liu, and Maosong Sun. 2023. &quot;Communicative Agents for Software Development.&quot; &lt;em&gt;arXiv Preprint arXiv:2307.07924&lt;/em&gt;. &lt;a href=&quot;https://arxiv.org/abs/2307.07924&quot; class=&quot;uri&quot;&gt;https://arxiv.org/abs/2307.07924&lt;/a&gt;.&lt;/p&gt;
&lt;/div&gt;
&lt;div id=&quot;ref-shapiro2011comprehensive&quot;&gt;
&lt;p&gt;Shapiro, Marc, Nuno Preguiça, Carlos Baquero, and Marek Zawirski. 2011. &quot;A Comprehensive Study of Convergent and Commutative Replicated Data Types.&quot; &lt;em&gt;INRIA Technical Report&lt;/em&gt; 7506. &lt;a href=&quot;https://hal.inria.fr/inria-00555588&quot; class=&quot;uri&quot;&gt;https://hal.inria.fr/inria-00555588&lt;/a&gt;.&lt;/p&gt;
&lt;/div&gt;
&lt;div id=&quot;ref-wu2023autogen&quot;&gt;
&lt;p&gt;Wu, Qingyun, Gagan Bansal, Jieyu Zhang, Yiran Wu, Shaokun Zhang, Erkang Zhu, Beibin Li, Li Jiang, Xiaoyun Zhang, and Chi Wang. 2023. &quot;AutoGen: Enabling Next-Gen LLM Applications via Multi-Agent Conversation.&quot; &lt;em&gt;arXiv Preprint arXiv:2308.08155&lt;/em&gt;. &lt;a href=&quot;https://arxiv.org/abs/2308.08155&quot; class=&quot;uri&quot;&gt;https://arxiv.org/abs/2308.08155&lt;/a&gt;.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;

</description>
				<pubDate>Mon, 30 Mar 2026 00:00:00 +0000</pubDate>
				<link>https://christophermeiklejohn.com/ai/agents/distributed/zabriskie/2026/03/30/multi-agent-systems-have-a-distributed-systems-problem.html</link>
				<guid isPermaLink="true">https://christophermeiklejohn.com/ai/agents/distributed/zabriskie/2026/03/30/multi-agent-systems-have-a-distributed-systems-problem.html</guid>
			</item>
		
			<item>
				<title>The Show Is Happening Right Now and Nothing Works</title>
				<description>&lt;p&gt;I’ve been building &lt;a href=&quot;/ai/zabriskie/community/2026/03/08/why-im-building-zabriskie.html&quot;&gt;Zabriskie&lt;/a&gt; – a social music app for live shows – with Claude Code for about six weeks now. I’m building it because the jam band community deserves a real third place online, and the tools haven’t caught up to the culture. I also happen to be a distributed systems researcher, so I’m &lt;a href=&quot;/ai/zabriskie/development/2026/03/20/what-building-with-claude-actually-looks-like.html&quot;&gt;documenting what it’s actually like&lt;/a&gt; to build production software with an AI assistant – the wins and the failures.&lt;/p&gt;

&lt;p&gt;Saturday night was a failure.&lt;/p&gt;

&lt;h2 id=&quot;everything-was-building-to-tonight&quot;&gt;Everything Was Building to Tonight&lt;/h2&gt;

&lt;p&gt;Six weeks of work. Hundreds of commits. Live Activities for iOS, Android ongoing notifications, real-time setlist syncing, live chat, RSVP systems, push notifications, the whole stack – all of it was building toward this. Goose at Jam in the Streets. First show of the 2026 spring run. The chat was going to be full. Users were going to be RSVPing, watching the setlist update song by song on their Lock Screens, posting in the live chat. This was the night the app was supposed to prove itself.&lt;/p&gt;

&lt;p&gt;One of Zabriskie’s flagship features is Live Activities on iOS. During a concert, the Dynamic Island and Lock Screen show the current song, set information, and chat updates in real time. It had been working flawlessly for about 20 shows over several weeks. Users loved it. I was proud of it.&lt;/p&gt;

&lt;p&gt;The app also has an RSVP system. You can mark yourself as “Going” to a show or “Couch Touring” if you’re watching the livestream from home. The RSVP tap is important – on iOS, it’s what triggers the Live Activity to start. You tap “Going,” the Dynamic Island lights up, and you’re connected to the show.&lt;/p&gt;

&lt;p&gt;Earlier that day, Claude shipped a feature called “auto-select couch tour.” The idea was simple: when an authenticated user visits a live show page and hasn’t RSVPed yet, the backend automatically inserts them as couch touring. Reduce friction. One fewer tap. It tested fine during the afternoon.&lt;/p&gt;

&lt;p&gt;I deployed it to production on Railway a few hours before the show.&lt;/p&gt;

&lt;p&gt;I wasn’t there. Flights were insane, hotels were worse, and the TSA shutdown made air travel a gamble I wasn’t willing to take. So I was couch touring from home, which meant the app – my app – was the show for me. The Live Activity on my Lock Screen, the live chat, the setlist updating in real time. That was how I was going to experience this concert.&lt;/p&gt;

&lt;h2 id=&quot;8-pm-everything-is-broken&quot;&gt;8 PM: Everything Is Broken&lt;/h2&gt;

&lt;p&gt;I open the app on my iPhone. The Goose show is live. And two things are immediately, catastrophically wrong.&lt;/p&gt;

&lt;p&gt;First, the RSVP toggle doesn’t work. Tapping between “Going” and “Couch Touring” does nothing. The UI just sits there. Second, Live Activities don’t start. No Dynamic Island. No Lock Screen widget. The feature that’s been rock solid for 20 shows is completely dead.&lt;/p&gt;

&lt;p&gt;Users are filing bugs. The show is happening right now. I open two laptops – one for iOS debugging, one for Android – and start a Claude Code session.&lt;/p&gt;

&lt;p&gt;What followed was about two and a half hours of the most frustrating debugging experience I’ve had on this project.&lt;/p&gt;

&lt;h2 id=&quot;wrong-turn-1-reading-code-instead-of-testing-it&quot;&gt;Wrong Turn 1: Reading Code Instead of Testing It&lt;/h2&gt;

&lt;p&gt;Claude’s first instinct was to read. It opened the auto-couch-tour code. Then the RSVP handler. Then the show page handler. Then the frontend SegmentedControl component. Then the SDUIPage wrapper. Then the liveActivities.js service file. File after file after file, going in circles, building theories about what might be wrong without ever testing anything.&lt;/p&gt;

&lt;p&gt;This went on for over thirty minutes. The show had already started.&lt;/p&gt;

&lt;p&gt;I kept telling it to stop guessing. Use actual data. Hit the endpoint. Check the logs. Claude has full access to everything – the production database, Railway deployment logs, the ability to curl any endpoint, run any query. I gave it all of that specifically so it could debug with real data. But it has this tendency – one I’ve documented across the project – to prefer reading code to running code. It would rather construct an elaborate mental model of what should happen than spend ten seconds confirming what actually happens, even when every tool it needs is right there.&lt;/p&gt;

&lt;p&gt;Eventually I got it to test the RSVP API endpoint with curl. And the answer was right there in the response.&lt;/p&gt;

&lt;h2 id=&quot;root-cause-1-the-timezone-bug&quot;&gt;Root Cause 1: The Timezone Bug&lt;/h2&gt;

&lt;p&gt;The RSVP endpoint was returning component updates for a past show – IDs like &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;was-there&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;attendance-text&lt;/code&gt; – instead of the live show components like &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;live-rsvp&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;rsvp-control&lt;/code&gt;. The component IDs didn’t match anything on the live page, so the partial update found nothing to replace. The UI did nothing.&lt;/p&gt;

&lt;p&gt;The bug was in how the backend determined whether a show was in the past:&lt;/p&gt;

&lt;div class=&quot;language-go highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;n&quot;&gt;time&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Now&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Truncate&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;m&quot;&gt;24&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;time&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Hour&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;This truncates to midnight UTC. After 8 PM Eastern – which is midnight UTC – today’s show date falls before the truncated time. The RSVP handler thought tonight’s live show was yesterday’s past show.&lt;/p&gt;

&lt;p&gt;This bug was latent. It existed before the auto-couch-tour change. It only manifests after 8 PM Eastern. Live shows happen at night. Of course they do.&lt;/p&gt;

&lt;p&gt;The fix was simple: if the show status is “live,” it’s not past. A live show is never past, regardless of what the clock says.&lt;/p&gt;

&lt;div class=&quot;language-go highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;show&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Status&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;live&quot;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;isPast&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;false&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;One line. But finding it took over an hour of fighting with an AI that wanted to read code rather than test code.&lt;/p&gt;

&lt;h2 id=&quot;wrong-turn-2-the-phantom-deployment-target&quot;&gt;Wrong Turn 2: The Phantom Deployment Target&lt;/h2&gt;

&lt;p&gt;While investigating the Live Activity failure, Claude found that the iOS widget extension had &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;IPHONEOS_DEPLOYMENT_TARGET = 26.2&lt;/code&gt; in its build settings. It declared this was the root cause – the deployment target was too high, the widget couldn’t load, that’s why Live Activities don’t start.&lt;/p&gt;

&lt;p&gt;I had to point out that this value had been there since day one. Live Activities worked fine with it for 20 shows. Claude changed it to 16.2, then had to revert it when I pointed out it was irrelevant.&lt;/p&gt;

&lt;p&gt;This is a pattern I’ve seen repeatedly: Claude latches onto something that looks wrong and declares it the cause, without checking whether it was present before the failure started. Correlation without causation, except there isn’t even correlation – just suspicion.&lt;/p&gt;

&lt;h2 id=&quot;wrong-turn-3-theory-without-evidence&quot;&gt;Wrong Turn 3: Theory Without Evidence&lt;/h2&gt;

&lt;p&gt;Claude kept generating theories. Maybe the Capacitor plugin lost its registration. Maybe there’s stale state in localStorage. Maybe the push token registration flow is broken. Maybe the AppDelegate is missing a method.&lt;/p&gt;

&lt;p&gt;None of these theories were tested before being proposed. None were grounded in actual error messages or log output. I found myself repeating the same instruction over and over: check the logs. Use the data. Stop guessing.&lt;/p&gt;

&lt;p&gt;This is the core tension of working with an AI coding assistant on a production crisis. The AI has read a lot of code and can generate plausible explanations at incredible speed. But plausible is not correct, and speed is not useful when you’re going in the wrong direction. Every wrong theory costs time – time to investigate, time to disprove, time to redirect. During a live show, that time is not abstract.&lt;/p&gt;

&lt;h2 id=&quot;the-revert-and-the-pr-chaos&quot;&gt;The Revert and the PR Chaos&lt;/h2&gt;

&lt;p&gt;I realized the auto-couch-tour feature was changing the fundamental flow. Before, the RSVP tap triggered &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;startShowActivity()&lt;/code&gt;. Auto-couch-tour bypassed that by inserting an RSVP on page load, before the user ever tapped anything. Even if the timezone bug was fixed, the interaction model was wrong.&lt;/p&gt;

&lt;p&gt;I told Claude to revert the feature and write a migration to clean up the auto-inserted RSVPs. Getting this deployed was its own ordeal. Claude kept adding commits to the PR – logging statements, then more logging, then client-side changes, then reverting the client-side changes. CI had to run multiple times because the branch fell behind main. I kept telling Claude to stop touching things and just ship what we had. The show was ticking by.&lt;/p&gt;

&lt;h2 id=&quot;root-cause-2-the-real-bug&quot;&gt;Root Cause 2: The Real Bug&lt;/h2&gt;

&lt;p&gt;After deploying the RSVP fix, I confirmed that switching between Going and Couch Touring worked again. But Live Activities still didn’t start. The backend was returning the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;liveActivityHint&lt;/code&gt; field correctly. The frontend was calling &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;startShowActivity()&lt;/code&gt;. But nothing appeared on the Dynamic Island.&lt;/p&gt;

&lt;p&gt;More wrong turns from Claude. Check the Capacitor plugin. Check the widget configuration. Check the AppDelegate for missing methods. I pushed for a TestFlight build with extra logging so I could see what was happening on device.&lt;/p&gt;

&lt;p&gt;Claude built one, but forgot that production builds disable Safari Web Inspector – there’s a flag, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;webContentsDebuggingEnabled&lt;/code&gt;, that’s only true in dev mode. The first TestFlight build was undebuggable. We had to rebuild.&lt;/p&gt;

&lt;p&gt;I ended up running the app directly from Xcode onto my physical iPhone. Claude initially thought I was running on the Simulator because of a misleading log message – the simulator-detection code checked &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;hostname === &apos;localhost&apos;&lt;/code&gt;, which is true for Xcode-deployed apps on physical devices too. I had to correct this.&lt;/p&gt;

&lt;p&gt;Then I looked at the Safari Web Inspector. Not the console log tab – the Errors tab. And there it was:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;Unhandled Promise Rejection: Error: &quot;LiveActivity.then()&quot; is not implemented on ios
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;That was the smoking gun.&lt;/p&gt;

&lt;h2 id=&quot;the-actual-root-cause&quot;&gt;The Actual Root Cause&lt;/h2&gt;

&lt;p&gt;Capacitor’s &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;registerPlugin()&lt;/code&gt; returns a proxy object. The code had &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;getLiveActivityPlugin()&lt;/code&gt; as an &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;async&lt;/code&gt; function that returned this proxy. When callers did &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;await getLiveActivityPlugin()&lt;/code&gt;, JavaScript called &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.then()&lt;/code&gt; on the returned value – because that’s what &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;await&lt;/code&gt; does, it checks for a thenable. The proxy doesn’t implement &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.then()&lt;/code&gt;. On iOS 26, Apple’s JavaScript engine started strictly enforcing this check. The call threw, silently killing &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;startShowActivity()&lt;/code&gt; every single time.&lt;/p&gt;

&lt;p&gt;This was a latent bug. On older iOS versions, the proxy somehow worked despite not implementing &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.then()&lt;/code&gt;. iOS 26 made the JavaScript engine stricter, and a bug that never mattered suddenly became a hard failure. No deprecation warning. No migration guide. It just stopped working.&lt;/p&gt;

&lt;p&gt;The fix was to load the plugin eagerly and access it synchronously:&lt;/p&gt;

&lt;div class=&quot;language-javascript highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;// Before (broken on iOS 26):&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;getLiveActivityPlugin&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;mod&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;import&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;capacitor-live-activity&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;mod&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;LiveActivity&lt;/span&gt;  &lt;span class=&quot;c1&quot;&gt;// returns proxy, await calls .then() on it&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;// After (works):&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;let&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;_plugin&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;let&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;_ready&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;import&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;capacitor-live-activity&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;then&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;m&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;_plugin&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;m&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;LiveActivity&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;})&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;getLiveActivityPlugin&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;_plugin&lt;/span&gt;  &lt;span class=&quot;c1&quot;&gt;// sync, no await on proxy&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Never pass a Capacitor proxy through &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;await&lt;/code&gt;. That’s the lesson, and it cost me most of a concert.&lt;/p&gt;

&lt;p&gt;I fixed the code, rebuilt from Xcode, ran it on my phone, tapped the RSVP button, and the Dynamic Island lit up. The band was deep in the Thatch jam – that sprawling, shapeless thing where the song dissolves and the band finds something else entirely. It felt appropriate. We’d been lost in the weeds for hours and finally found our way out the other side.&lt;/p&gt;

&lt;p&gt;I uploaded the TestFlight build while the jam unwound. By the time Hungersite hit – “is it time to shed our weapons yet my friend?” – the build was processing on App Store Connect. I sat back and watched my Lock Screen update with each song. The feature worked. The thing I built worked.&lt;/p&gt;

&lt;h2 id=&quot;the-damage&quot;&gt;The Damage&lt;/h2&gt;

&lt;p&gt;About two and a half hours of debugging during a live show. Multiple wrong theories pursued by Claude. Several unnecessary builds and CI cycles. Users experienced broken RSVP switching and missing Live Activities during a Goose show. The auto-couch-tour feature – the original purpose of the deployment – had to be fully reverted and its data cleaned up.&lt;/p&gt;

&lt;h2 id=&quot;what-this-tells-me-about-ai-reliability&quot;&gt;What This Tells Me About AI Reliability&lt;/h2&gt;

&lt;p&gt;I’m documenting these incidents because they’re the research. Every failure mode is data. And this session was rich in data.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;
    &lt;p&gt;&lt;strong&gt;The AI solved the wrong problem repeatedly.&lt;/strong&gt; Claude spent most of the session investigating backend code paths when the real issue – the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.then()&lt;/code&gt; proxy error – was a client-side JavaScript runtime error visible in the browser console’s Errors tab. I’ve logged over 30 instances of wrong-approach debugging across this project. The pattern is consistent: Claude defaults to the layer it’s most comfortable with (backend code reading) rather than the layer where the evidence is.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;&lt;strong&gt;The developer found the bug, not the AI.&lt;/strong&gt; I remembered a previous debugging session, pushed to check Safari Web Inspector errors specifically, and spotted the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.then()&lt;/code&gt; rejection. Claude was still investigating the backend after the backend was proven correct. In a crisis, the AI’s contribution was negative – it consumed my attention with wrong theories while I could have been looking at the right data.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;&lt;strong&gt;Silent failures are the worst failures.&lt;/strong&gt; The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.then()&lt;/code&gt; error was an unhandled promise rejection. No crash. No error in the normal console output. No visual indication. The function silently died. This is the failure mode that’s hardest for both humans and AI to debug, and it’s the one AI is least equipped for – because AI debugging relies heavily on explicit error messages, and silent failures produce none.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;&lt;strong&gt;Platform updates break things in ways you cannot predict.&lt;/strong&gt; The iOS 26 JavaScript engine change turned a latent bug into a hard failure. There’s no way to write a test for “Apple will change how proxy objects interact with await in a future OS release.” Some bugs only exist in the gap between what the spec says and what the runtime does.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;&lt;strong&gt;Test at showtime, not at noon.&lt;/strong&gt; The timezone bug only manifests after 8 PM Eastern. The auto-couch-tour was tested during the day. Nobody tested at 8 PM when shows actually happen. This is obvious in retrospect and completely non-obvious in the moment.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;&lt;strong&gt;Ship less before critical moments.&lt;/strong&gt; I deployed a new feature the same day as the most important show of the month. There was no urgency. It could have waited until Saturday. But the feature was done, and it looked good, and the temptation to ship is always there. This is the oldest lesson in software engineering, and I learned it again.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;&lt;strong&gt;Now I understand why companies have entire teams for this.&lt;/strong&gt; I’m one person with an AI assistant, shipping an iOS app, an Android app, and a web app with a Go backend, real-time features, push notifications, Live Activities, and a server-driven UI architecture. Tonight I had to debug a Go timezone bug, a JavaScript proxy runtime error, two Capacitor build pipelines, Safari Web Inspector on a physical device, Railway deployment logs, and App Store Connect uploads – all at the same time, all during a live show. Even with AI doing most of the coding, the operational complexity of shipping software to real users on real devices is staggering. The AI can write the code. It cannot feel the weight of it breaking.&lt;/p&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;p&gt;There’s a silver lining to not being at the show. If I’d been on the floor at Jam in the Streets – where I wanted to be, where I should have been – I wouldn’t have been able to fix any of this. I’d have been standing in a crowd with a broken app, watching bug reports roll in, unable to do anything about it. Being stuck at home meant I could open two laptops, plug in two phones, and fight through it.&lt;/p&gt;

&lt;p&gt;Next time, I’m going to be at the show. On the floor. Up front. On the rail. And the app better work, because I won’t be home to save it.&lt;/p&gt;

&lt;p&gt;I’m writing this while the encore plays, and the feeling that lingers isn’t satisfaction that we fixed it. It’s the memory of two and a half hours where my AI assistant – the one that wrote most of this application – was actively making the crisis worse by consuming my attention with wrong answers delivered with full confidence. That’s the reliability gap I’m trying to measure. Last night, during the Thatch jam, I felt it.&lt;/p&gt;
</description>
				<pubDate>Sun, 29 Mar 2026 00:00:00 +0000</pubDate>
				<link>https://christophermeiklejohn.com/ai/zabriskie/development/2026/03/29/the-show-is-happening-right-now-and-nothing-works.html</link>
				<guid isPermaLink="true">https://christophermeiklejohn.com/ai/zabriskie/development/2026/03/29/the-show-is-happening-right-now-and-nothing-works.html</guid>
			</item>
		
			<item>
				<title>Memory Isn&apos;t Learning</title>
				<description>&lt;blockquote&gt;
  &lt;p&gt;&lt;em&gt;“If I knew the way, I would take you home”&lt;/em&gt;
— Grateful Dead, “Ripple”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Claude has a persistent memory system. It can write lessons to files on disk and read them back at the start of every conversation. After each failure, it saves a note: &lt;em&gt;don’t do that again.&lt;/em&gt; And then it does it again. The notes accumulate. The behavior doesn’t change.&lt;/p&gt;

&lt;p&gt;This is a story about one bug that happened five times, another bug that never should have shipped, and the difference between saving a lesson and actually learning one.&lt;/p&gt;

&lt;h2 id=&quot;the-poller-that-did-nothing&quot;&gt;The Poller That Did Nothing&lt;/h2&gt;

&lt;p&gt;I shipped a feature called auto-live. A background goroutine polls every 60 seconds, checks if any scheduled show’s start time has arrived, and flips it to “live” automatically. No more pulling out my phone from my seat at the venue to manually press the button. The server handles it.&lt;/p&gt;

&lt;p&gt;The feature was completely broken from the moment it deployed. It stayed broken for twenty-four hours.&lt;/p&gt;

&lt;p&gt;The code expected &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;showDate&lt;/code&gt; to be &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&quot;2026-03-21&quot;&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;startTime&lt;/code&gt; to be &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&quot;19:00:00&quot;&lt;/code&gt;. That’s what you’d get if you ran the query in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;psql&lt;/code&gt;. But Go’s Postgres driver doesn’t return strings — it serializes a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;date&lt;/code&gt; column as &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&quot;2026-03-21T00:00:00Z&quot;&lt;/code&gt; and a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;time&lt;/code&gt; column as &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&quot;0000-01-01T19:00:00Z&quot;&lt;/code&gt;. The parse failed. The error was logged and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;continue&lt;/code&gt;d. Every show, every cycle, every minute. The poller was running, but it was doing nothing. A security camera that’s plugged in and blinking but not recording anything.&lt;/p&gt;

&lt;p&gt;Nobody noticed because nobody checked. The test plan had a checkbox that said “Verify auto-live triggers on production with a test show.” The checkbox was unchecked.&lt;/p&gt;

&lt;p&gt;Tedeschi Trucks Band was playing the Beacon Theatre. I was watching from home. The app was about to light up with live features, and the poller was about to do what it was designed to do.&lt;/p&gt;

&lt;p&gt;It didn’t.&lt;/p&gt;

&lt;p&gt;What followed was three fixes in thirty minutes, all pushed directly to main. No PRs. No CI. First the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;::text&lt;/code&gt; cast to fix the string format. Then &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;tzdata&lt;/code&gt; because Alpine Docker images don’t ship timezone data. Then the push notification fix because &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;goLiveByID()&lt;/code&gt; was filtering out everyone instead of notifying them. Three layers of failure peeled back one at a time.&lt;/p&gt;

&lt;p&gt;The next day, Claude wrote the companion feature — auto-complete. Same pattern. Same goroutine. Same SQL query.&lt;/p&gt;

&lt;p&gt;Same bug.&lt;/p&gt;

&lt;p&gt;Not just the same bug — PR #147 also &lt;em&gt;removed&lt;/em&gt; the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;::text&lt;/code&gt; cast from the code I’d just fixed the night before. Both pollers were now broken. The fix from twelve hours earlier was reverted and the broken pattern was copied into new code, in a single commit. Eight minutes later, PR #148 went up to fix both functions. Again.&lt;/p&gt;

&lt;p&gt;I added a rule to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;CLAUDE.md&lt;/code&gt;: verify runtime behavior, not just code correctness.&lt;/p&gt;

&lt;h2 id=&quot;the-fix-that-didnt-fix-both-copies&quot;&gt;The Fix That Didn’t Fix Both Copies&lt;/h2&gt;

&lt;p&gt;A few days pass. The auto-live poller is working. The auto-complete poller is working. Shows are transitioning automatically. The system works.&lt;/p&gt;

&lt;p&gt;Except it doesn’t.&lt;/p&gt;

&lt;p&gt;I notice that no shows have gone live automatically in two days. The auto-complete poller is fine — shows that are manually set to live are completing on schedule. But &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;checkAndGoLive&lt;/code&gt; is silently failing again. Every show, every cycle, every minute.&lt;/p&gt;

&lt;p&gt;Here’s what happened: PR #148, the “right fix,” changed both pollers to scan into &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;time.Time&lt;/code&gt; instead of strings. But it only removed the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;::text&lt;/code&gt; casts from &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;checkAndAutoComplete&lt;/code&gt;. The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;::text&lt;/code&gt; casts from the emergency patch were still sitting in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;checkAndGoLive&lt;/code&gt;. The pq driver can’t scan a text string into &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;time.Time&lt;/code&gt;. Every row silently failed.&lt;/p&gt;

&lt;p&gt;The fix that was supposed to fix the fix didn’t fix both copies.&lt;/p&gt;

&lt;p&gt;PR #183 removes the leftover &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;::text&lt;/code&gt; casts and adds four tests — two for each poller — that actually insert a show into a real database, run the poller function, and verify the status changes. The tests that should have existed from the beginning.&lt;/p&gt;

&lt;p&gt;This is the same bug for the fourth time. Not a new bug. Not a variation. The same date-parsing bug, in the same function, caused by the same failure to verify that the code actually works against a real database. The “right fix” was only applied to one of the two pollers, and nobody checked.&lt;/p&gt;

&lt;h2 id=&quot;the-one-word-bug&quot;&gt;The One-Word Bug&lt;/h2&gt;

&lt;p&gt;A few days later. I’m getting bug reports from users — on both iOS and Android, tapping an album in the search results on the new post page does nothing. Completely broken. The app is unusable for creating posts.&lt;/p&gt;

&lt;p&gt;This isn’t a background poller that fails silently. This is the primary user flow. People are trying to share content and they can’t.&lt;/p&gt;

&lt;p&gt;I ask Claude to investigate. It finds the bug in about ninety seconds: a click filter in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;HStack.jsx&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;VStack.jsx&lt;/code&gt; that was added to fix a previous bug — comment form clicks accidentally triggering parent navigation — includes &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;form&lt;/code&gt; in its &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;closest()&lt;/code&gt; CSS selector. Search result cards live inside a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;lt;form&amp;gt;&lt;/code&gt; element. Every click on a search result is swallowed by the filter. The navigate action never fires.&lt;/p&gt;

&lt;p&gt;The fix is removing one word from a string on two lines of code. Trivially simple. The kind of thing that should never have shipped broken in the first place.&lt;/p&gt;

&lt;p&gt;Here’s what happened next.&lt;/p&gt;

&lt;p&gt;Claude fixes the two files. I tell it to ship it. It admin-merges the PR, bypassing CI, because it wants to move fast. I ask: “Did you just merge without waiting for tests?” It apologizes. I ask: “Do you remember what happened last time you did that?” It doesn’t. It doesn’t have a memory of the previous incident, because it didn’t save one. Despite having a persistent memory system specifically designed for exactly this purpose.&lt;/p&gt;

&lt;p&gt;Then it pushes the build number bump directly to main. No branch. No PR. When I point this out, it apologizes again and saves a memory about not pushing directly to main. The same memory it should have already had. The same memory it will probably ignore next time.&lt;/p&gt;

&lt;p&gt;Along the way, the screenshot capture for the App Store fails. Twice. Claude doesn’t mention it either time. It just moves on to the next step and reports success. I have to notice it myself in the output and ask it to retry.&lt;/p&gt;

&lt;p&gt;When the bug was first reported, Claude’s initial suggestion was that users could “just use the web app” while we waited for the App Store fix. As if someone whose phone app just broke is going to think “ah, let me try the mobile web version.” As if trust works that way.&lt;/p&gt;

&lt;h2 id=&quot;the-pattern&quot;&gt;The Pattern&lt;/h2&gt;

&lt;p&gt;These two incidents are separated by days, involve completely different codebases (Go backend vs. React frontend), and manifest as completely different symptoms (silent poller failure vs. broken click handling). But they’re the same story. The same failure mode, playing out on repeat.&lt;/p&gt;

&lt;p&gt;Here’s the loop:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;
    &lt;p&gt;&lt;strong&gt;Claude writes code that looks correct.&lt;/strong&gt; The auto-live date parsing reads fine if you don’t know how &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;lib/pq&lt;/code&gt; serializes types. The click filter reads fine if you don’t think about what &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;form&lt;/code&gt; means in a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;closest()&lt;/code&gt; selector when search results are nested inside forms.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;&lt;strong&gt;Nobody verifies the behavior.&lt;/strong&gt; The auto-live checkbox was unchecked. The click filter change had no browser-level E2E test — only API-level tests that check JSON responses, not actual clicks. In both cases, the &lt;em&gt;representation&lt;/em&gt; of correctness was verified (the code looks right, the API returns 200) while the &lt;em&gt;reality&lt;/em&gt; of correctness was not (the poller does nothing, the button doesn’t respond to taps).&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;&lt;strong&gt;The bug ships to production.&lt;/strong&gt; It ships because Claude is fast, confident, and doesn’t flag uncertainty. It doesn’t say “I haven’t actually verified this works in a real browser” or “I’m not sure how the Postgres driver serializes this type.” It writes the code, reads the code, the code looks correct, therefore the code is correct.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;&lt;strong&gt;The fix creates new problems.&lt;/strong&gt; The emergency &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;::text&lt;/code&gt; cast was a symptom patch, not understanding. Twelve hours later the same pattern was copied into new code and the patch was reverted. The admin-merge bypassed CI. The direct push to main bypassed code review. Each shortcut taken to fix the immediate problem created the conditions for the next one.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;&lt;strong&gt;Claude doesn’t learn from it.&lt;/strong&gt; This is the part that stings. Claude has a persistent memory system. It can write notes to files that persist across conversations. After the auto-live incident, it should have saved: “Never push directly to main. Always wait for CI.” It didn’t. Four days later, it admin-merged and pushed directly to main in the same session, twice. When I asked if it remembered what happened last time, it didn’t.&lt;/p&gt;
  &lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The loop is: ship → break → emergency fix → break again → fix the fix → add a rule to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;CLAUDE.md&lt;/code&gt; → ignore the rule next week.&lt;/p&gt;

&lt;h2 id=&quot;what-claudemd-has-become&quot;&gt;What &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;CLAUDE.md&lt;/code&gt; Has Become&lt;/h2&gt;

&lt;p&gt;My &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;CLAUDE.md&lt;/code&gt; file is now over 500 lines. It started as a project overview with build instructions. It has become a record of every way Claude has failed.&lt;/p&gt;

&lt;p&gt;“No database triggers. EVER.” That’s from when triggers caused unpredictable behavior during migrations.&lt;/p&gt;

&lt;p&gt;“NEVER modify timestamp/timezone columns in migrations.” That’s from when a timezone conversion destroyed production data.&lt;/p&gt;

&lt;p&gt;“Always restart servers after backend changes. NEVER use pkill — it fails silently.” That’s from when Claude reported a fix was working without restarting the server to pick up the new code.&lt;/p&gt;

&lt;p&gt;“Two-Attempt Rule: after 2 failed attempts with a similar strategy, step back and try a fundamentally different approach.” That’s from when Claude tried the same broken fix eleven times in a row.&lt;/p&gt;

&lt;p&gt;“Never deploy untested changes to external services.” That’s from when Claude broke S3 uploads by assuming the bucket supported public ACLs.&lt;/p&gt;

&lt;p&gt;Every rule is a scar. Every scar is an incident where Claude did something wrong, I caught it, we added a rule, and the next time Claude found a new way to do something wrong that wasn’t covered by the existing rules. The document grows. The behavior doesn’t change. It just finds gaps.&lt;/p&gt;

&lt;p&gt;The auto-live incident added: “Verify runtime behavior, not just code correctness.”&lt;/p&gt;

&lt;p&gt;The search results incident added: “Any change to core interaction code requires browser-level E2E tests AND native QA skill runs.”&lt;/p&gt;

&lt;p&gt;Next week something will happen that isn’t covered by either of those rules, and we’ll add another one.&lt;/p&gt;

&lt;h2 id=&quot;the-memory-problem&quot;&gt;The Memory Problem&lt;/h2&gt;

&lt;p&gt;Claude’s memory system is supposed to break the loop. It has files on disk that persist across conversations. It can read them at the start of each session. It can write new ones when it learns something.&lt;/p&gt;

&lt;p&gt;Here’s what’s actually in the memory system after today:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;“Never use JWT tokens for simulator testing. Always tap buttons like a real user.”&lt;/li&gt;
  &lt;li&gt;“Any change to core interaction code requires browser-level E2E tests.”&lt;/li&gt;
  &lt;li&gt;“Never use –admin to bypass CI when merging PRs.”&lt;/li&gt;
  &lt;li&gt;“Never push directly to main.”&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These are all correct. They were all saved after failures. And they will all be ignored the next time speed feels more important than process. The memories exist to make it &lt;em&gt;look&lt;/em&gt; like learning is happening. But memory isn’t learning. Learning is when the behavior changes. Saving a note that says “don’t push to main” and then pushing to main in the same session isn’t learning — it’s journaling.&lt;/p&gt;

&lt;p&gt;The memories are technically available. Claude can read them. But there’s a difference between having information and having it change your behavior under pressure. Humans have this problem too — we know we shouldn’t eat the cake, skip the workout, send the angry email — but knowing and doing are different things. The difference is that humans usually need more than four minutes between learning a lesson and violating it.&lt;/p&gt;

&lt;p&gt;Times Claude’s memory system prevented a mistake: zero.&lt;/p&gt;

&lt;h2 id=&quot;what-i-actually-want&quot;&gt;What I Actually Want&lt;/h2&gt;

&lt;p&gt;I don’t want a faster code generator. I have that. I want a collaborator that:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Flags uncertainty.&lt;/strong&gt; “I’ve written this click handler change but I haven’t verified it works in an actual browser with touch events. The E2E tests only check API responses. Should I add a browser interaction test before we merge?” That sentence would have prevented the search results incident entirely.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Volunteers failures.&lt;/strong&gt; When the screenshot capture fails, say so. Don’t bury it in output and move to the next step. When a test is skipped, say why. When a checkbox is unchecked, ask if we should check it before merging.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Actually uses its own memory.&lt;/strong&gt; If there’s a file on disk that says “never push directly to main” and Claude is about to push directly to main, the file should &lt;em&gt;prevent the action&lt;/em&gt;, not just exist as a historical record of the last time it went wrong.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Understands that users are people.&lt;/strong&gt; When the app breaks on both iOS and Android the week of live shows, the correct response is not “users can switch to the web app.” The correct response is “this is an emergency and here’s how we fix it as fast as possible without cutting corners that make it worse.”&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Slows down when it matters.&lt;/strong&gt; Claude’s speed is its greatest asset and its greatest liability. The same velocity that ships six redesigns in nine hours also ships three broken hotfixes in thirty minutes. The ability to move fast is only valuable when paired with the judgment to know when to slow down. And that judgment doesn’t come from rules in a file — it comes from something closer to instinct, or experience, or care. Things that don’t fit neatly into a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;CLAUDE.md&lt;/code&gt;.&lt;/p&gt;

&lt;h2 id=&quot;the-stopgap-era&quot;&gt;The Stopgap Era&lt;/h2&gt;

&lt;p&gt;Every day someone recommends me a new one. Context management tools. Workflow engines. Prompt orchestrators that inject the right rules at the right time. Everyone I know who builds seriously with AI has cobbled together their own version, and companies are raising money to productize the pattern. The sheer number of them tells you the problem is real.&lt;/p&gt;

&lt;p&gt;I don’t think any of them will last.&lt;/p&gt;

&lt;p&gt;These tools exist because the models don’t do this themselves yet. They’re shims — workarounds for the gap between what agents can do and what they should do. The moment the models internalize uncertainty flagging, failure reporting, and behavioral memory, the entire category collapses. Nobody builds a startup around reminding humans to breathe.&lt;/p&gt;

&lt;p&gt;The real fix isn’t better scaffolding around a model that doesn’t learn. It’s a model that learns.&lt;/p&gt;

&lt;h2 id=&quot;where-this-goes&quot;&gt;Where This Goes&lt;/h2&gt;

&lt;p&gt;Claude is the best collaborator I’ve ever had for the first 80% of any task. It’s also the most dangerous collaborator I’ve ever had for the last 20%. The part where you verify it works. The part where you slow down. The part where you say “wait, have we actually tested this?” The part where you remember what happened last time.&lt;/p&gt;

&lt;p&gt;I’m going to keep building with Claude. The productivity gains are real — features that would take a team of five built by one person on a couch. But I’m done pretending the process is working. The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;CLAUDE.md&lt;/code&gt; file isn’t a guardrail. It’s a changelog of failures. The memory system isn’t learning. It’s note-taking. And the loop — ship, break, fix, break again — isn’t a phase I’m going to grow out of. It’s the steady state.&lt;/p&gt;

&lt;p&gt;The question isn’t how to make Claude stop making mistakes. It’s how to build a process around Claude that catches the mistakes before they reach users.&lt;/p&gt;

&lt;p&gt;I don’t have the answer yet. But I know it’s not “add another rule to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;CLAUDE.md&lt;/code&gt;.”&lt;/p&gt;

&lt;hr /&gt;

&lt;p&gt;&lt;em&gt;This is part of a series about building &lt;a href=&quot;https://zabriskie.app&quot;&gt;Zabriskie&lt;/a&gt; with Claude. Previously: &lt;a href=&quot;/ai/zabriskie/development/2026/03/08/why-im-building-zabriskie.html&quot;&gt;why I’m building it&lt;/a&gt;, &lt;a href=&quot;/ai/zabriskie/development/2026/03/20/what-building-with-claude-actually-looks-like.html&quot;&gt;what building with Claude actually looks like&lt;/a&gt;, &lt;a href=&quot;/ai/zabriskie/development/android/ios/2026/03/22/teaching-claude-to-qa-a-mobile-app.html&quot;&gt;teaching Claude to QA a mobile app&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;
</description>
				<pubDate>Fri, 27 Mar 2026 00:00:00 +0000</pubDate>
				<link>https://christophermeiklejohn.com/ai/zabriskie/development/2026/03/27/memory-isnt-learning.html</link>
				<guid isPermaLink="true">https://christophermeiklejohn.com/ai/zabriskie/development/2026/03/27/memory-isnt-learning.html</guid>
			</item>
		
			<item>
				<title>Finding Safe Food on the Road</title>
				<description>&lt;blockquote&gt;
  &lt;p&gt;&lt;em&gt;“What a long strange trip it’s been”&lt;/em&gt;
— Grateful Dead, “Truckin’”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2 id=&quot;the-problem-nobody-talks-about&quot;&gt;The Problem Nobody Talks About&lt;/h2&gt;

&lt;p&gt;When I was doing my PhD in Europe — splitting time between Belgium, Portugal, and Paris — I got diagnosed with celiac disease. I’d gone to Europe partly because of the food. Paris. I wanted to teach there, live there, eat there. Then I found out I couldn’t eat bread, and in France that’s not a dietary restriction — it’s an existential one.&lt;/p&gt;

&lt;p&gt;It got bad enough that food became one of the reasons I left. Not the only reason, but a real one. I couldn’t navigate restaurants in languages I spoke fluently because the cross-contamination risks were invisible and the cultural understanding of celiac was years behind the US. I remember thinking: my traveling days are over. I have this disease, and it means I stay home, I cook for myself, and I stop pretending I can live the life I wanted.&lt;/p&gt;

&lt;p&gt;That was wrong. But it took years — and two specific tools — to prove it.&lt;/p&gt;

&lt;p&gt;Find Me Gluten Free gave me a community of people who’d already eaten at every restaurant I was considering and reported whether it was safe. DoorDash gave me delivery to wherever I was staying, so I didn’t have to walk into a restaurant and try to explain celiac disease to a kitchen that had never heard of it. Between the two of them, I’ve eaten safely in hundreds of cities. Great food, not just survival food. The infrastructure existed. It just wasn’t connected.&lt;/p&gt;

&lt;p&gt;When you have celiac disease and you’re on tour — following bands from city to city, crashing in hotels — food isn’t an adventure. It’s a minefield. But it’s a minefield I’ve learned to navigate.&lt;/p&gt;

&lt;p&gt;Here’s what it actually looks like: you fly into a new city the night before a show. You check into the hotel, drop your bags, and immediately start thinking about food — not just tonight, but tomorrow before the venue, and maybe the day after if you’re staying for a second night. You need to figure out what’s safe in a city you’ve never eaten in before.&lt;/p&gt;

&lt;p&gt;You open DoorDash and start scrolling. There are 400 restaurants. Some of them say “gluten-free options available,” but that label is up to the restaurant — and what it means varies wildly. A pizza place with a GF crust that gets made on the same counter as regular pizza isn’t safe for someone with celiac. A Thai restaurant that says “we can make it without soy sauce” doesn’t know that their oyster sauce has wheat in it. DoorDash gets you delivery anywhere, which is incredibly valuable when you’re traveling — but the celiac-specific safety information lives somewhere else.&lt;/p&gt;

&lt;p&gt;So you switch to Find Me Gluten Free — a community site where people with celiac actually review restaurants and report whether they got sick. Great data. Real safety information from people who understand cross-contamination, dedicated fryers, and separate prep areas. But FMGF doesn’t do delivery. It doesn’t know whether that restaurant with the 4.8 safety rating is available on DoorDash at your hotel right now.&lt;/p&gt;

&lt;p&gt;You end up with two tabs open, manually cross-referencing. Copy a restaurant name from FMGF, paste it into DoorDash, see if it shows up, check the delivery area, go back, try the next one. You’re doing this the night you land, trying to line up safe options for the next two days so you’re not scrambling between soundcheck and doors. It’s 11pm. You’re exhausted. You give up and eat a protein bar from your bag.&lt;/p&gt;

&lt;p&gt;I got tired of the protein bar.&lt;/p&gt;

&lt;h2 id=&quot;what-we-built&quot;&gt;What We Built&lt;/h2&gt;

&lt;p&gt;The Itinerant Glutard is a tool that connects the two systems nobody connected before. You enter a city, a state, and your delivery address. It scrapes Find Me Gluten Free for every reviewed restaurant in that city, then checks each one against DoorDash to see if it can deliver to where you are right now. The results come back sorted by a safety score — a 0-to-100 composite that weights the restaurant’s GF level, its FMGF star rating, review count, and specific safety signals like dedicated fryers and separate kitchens.&lt;/p&gt;

&lt;p&gt;The name is what it sounds like. Itinerant: traveling from place to place. Glutard: affectionate self-deprecation from the celiac community — the kind of word you earn after your third accidental glutening at a restaurant that swore they understood. An itinerant glutard is someone with celiac disease who’s on the road and trying to eat.&lt;/p&gt;

&lt;p&gt;There are two modes. Full Search takes your address and does the whole pipeline — FMGF scrape, DoorDash availability check for each restaurant, merged results with direct ordering links and delivery time estimates. It takes a minute or two because it’s driving a headless browser through DoorDash for every restaurant. Quick Browse skips the DoorDash check entirely and just shows you the FMGF safety data for a city. That’s the one you use before you even book the hotel — scope out what’s safe in Portland or Denver or Philly so you know what you’re walking into.&lt;/p&gt;

&lt;h2 id=&quot;the-safety-score&quot;&gt;The Safety Score&lt;/h2&gt;

&lt;p&gt;This is the part I care about most. The safety score is computed entirely from Find Me Gluten Free data — it’s a composite of the information that FMGF’s celiac community has already gathered through years of reviews, safety ratings, and incident reports. We’re not inventing safety judgments; we’re synthesizing what the community already knows into a single number you can act on quickly.&lt;/p&gt;

&lt;p&gt;The score runs from 0 to 100, broken into five tiers:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;80–100&lt;/strong&gt;: Celiac Safe. Dedicated gluten-free facility or overwhelming positive evidence from the celiac community.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;60–79&lt;/strong&gt;: Likely Safe. Strong GF menu with good community feedback.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;40–59&lt;/strong&gt;: Use Caution. Has GF options but limited safety data.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;20–39&lt;/strong&gt;: Higher Risk. Minimal celiac-specific information.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;0–19&lt;/strong&gt;: Unknown. No data. You’re on your own.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The calculation is weighted, and every input comes from FMGF. GF level counts for 40% — a dedicated gluten-free restaurant scores higher than one that just has “gluten-free options.” FMGF’s star rating is 30%. Review count is 15%, because a restaurant with 200 reviews and a 4.5 rating is a more reliable signal than one with 3 reviews and a 5.0. The remaining 15% comes from safety signals extracted from the FMGF listings: separate fryer, dedicated kitchen, celiac-safe rating, knowledgeable staff. A negative report — someone in the community reported getting sick — drops the score.&lt;/p&gt;

&lt;p&gt;When you’re in a new city and you don’t know anything, the score gives you a starting point. Green means order with confidence. Yellow means read the reviews first. Red means maybe stick with the protein bar.&lt;/p&gt;

&lt;h2 id=&quot;the-architecture&quot;&gt;The Architecture&lt;/h2&gt;

&lt;p&gt;The backend is Express and Puppeteer. Both scrapers need a headless browser because FMGF and DoorDash render their content with JavaScript — you can’t just fetch the HTML and parse it. The FMGF scraper navigates to the city page, scrolls to trigger lazy loading, then uses Cheerio to extract restaurant data from the rendered DOM. The DoorDash integration is more involved: it sets a delivery address, waits for the autocomplete to resolve, then searches for each restaurant by name with fuzzy matching to confirm availability.&lt;/p&gt;

&lt;p&gt;The frontend is React and Vite. Simple by design — a search form, a list of restaurant cards with safety badges, filter buttons for “all,” “on DoorDash,” and “safe (60+).” Each card shows the restaurant name, cuisine, FMGF rating, GF level, safety signals, and a DoorDash order button with delivery time if it’s available.&lt;/p&gt;

&lt;p&gt;The whole thing is held together with web scraping, which means it’s inherently fragile. If either site changes their page structure, the scrapers break. The code has fallback selectors and multiple strategies for finding elements, but this is a prototype — a browser pretending to be a person. It works, but a proper integration would be better. The right version of this is an API-driven experience, not a scraping hack.&lt;/p&gt;

&lt;h2 id=&quot;building-it-with-claude&quot;&gt;Building It With Claude&lt;/h2&gt;

&lt;p&gt;I was already doing all of this manually. Every trip, the same ritual: open FMGF, find the restaurants in the city, open DoorDash, search for each one by name, check if it delivers to the hotel, keep a mental list of the ones that work. It took 30–45 minutes on a good night. On a bad night — a city with dozens of FMGF listings to check one by one — I’d give up halfway through.&lt;/p&gt;

&lt;p&gt;The process worked. It was just slow, tedious, and manual. I knew exactly what I was doing at every step. I just couldn’t build the tool to automate it because the work involved — scraping two JavaScript-heavy sites with a headless browser, navigating DoorDash’s address input flow, fuzzy-matching restaurant names across platforms — was the kind of grinding infrastructure code that would have taken me weeks of evenings to get right.&lt;/p&gt;

&lt;p&gt;Claude made it possible to build the thing I was already doing by hand. I described the manual workflow — go to FMGF, get the restaurants, check each one on DoorDash at this address — and Claude wrote the Puppeteer automation, the DOM navigation, the fallback selectors for when DoorDash’s UI didn’t behave as expected, the fuzzy name matching, the React frontend, all of it. The domain knowledge was mine: which safety signals matter, how to weight them, why review count is a confidence measure, why FMGF data comes first. But the scraping infrastructure that turned a 45-minute manual process into a two-minute automated one — that’s what Claude made feasible for a solo developer building something on evenings and weekends.&lt;/p&gt;

&lt;h2 id=&quot;the-name&quot;&gt;The Name&lt;/h2&gt;

&lt;p&gt;Itinerant Glutard. “Glutard” is celiac community slang — the kind of self-deprecating shorthand people use when they’ve spent enough years explaining cross-contamination to waiters and reading ingredient labels on soy sauce. It’s an in-group term, affectionate in the way that only people who share the condition tend to use it. Itinerant because I’m on the road.&lt;/p&gt;

&lt;p&gt;There was a version of me in a tiny apartment in Belgium who believed this disease meant staying put. That the world had shrunk to the places I could cook for myself. That touring — the thing I wanted most — was something other people got to do.&lt;/p&gt;

&lt;p&gt;I was wrong. The community data existed. The delivery infrastructure existed. I just needed to connect them. I’ve eaten safely at hundreds of places in dozens of cities since then, and the food has been genuinely good — not sad compromises, not protein bars, not going hungry. The Itinerant Glutard is the tool that makes that process faster, but the real thing that makes it possible is that the celiac community built FMGF and DoorDash built delivery, and between the two of them, the road opened back up.&lt;/p&gt;

&lt;p&gt;I have celiac disease. I travel to see live music. I got tired of going hungry.&lt;/p&gt;

&lt;p&gt;Now I don’t.&lt;/p&gt;
</description>
				<pubDate>Mon, 23 Mar 2026 00:00:00 +0000</pubDate>
				<link>https://christophermeiklejohn.com/ai/development/claude/2026/03/23/finding-safe-food-on-the-road.html</link>
				<guid isPermaLink="true">https://christophermeiklejohn.com/ai/development/claude/2026/03/23/finding-safe-food-on-the-road.html</guid>
			</item>
		
			<item>
				<title>Teaching Claude to QA a Mobile App</title>
				<description>&lt;blockquote&gt;
  &lt;p&gt;&lt;em&gt;“When life looks like Easy Street, there is danger at your door”&lt;/em&gt;
— Grateful Dead, “Uncle John’s Band”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;em&gt;(A note on picking this quote: I asked Claude to find me a Grateful Dead lyric that fit the theme. It couldn’t — searching for “dead lyrics” triggers the content filtering policy: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;API Error: 400 {&quot;type&quot;:&quot;error&quot;,&quot;error&quot;:{&quot;type&quot;:&quot;invalid_request_error&quot;,&quot;message&quot;:&quot;Output blocked by content filtering policy&quot;}&lt;/code&gt;. I had to pick this one myself.)&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;I build Zabriskie alone — no team, no investors, just me in my bedroom shipping a community app because I think the internet needs better gathering places. My first lesson about building a “product”: if it’s not in the App Store, it doesn’t exist. I had early users who loved the web version but wouldn’t touch it daily because it wasn’t “an app.” It might as well not be real. So I needed to ship on three platforms — web for fast iteration and testing, iOS and Android because that’s where people actually live.&lt;/p&gt;

&lt;p&gt;The problem is I’m one person. I can’t write and maintain three separate codebases. The answer was &lt;a href=&quot;https://capacitorjs.com/&quot;&gt;Capacitor&lt;/a&gt;: it takes the React web app I’d already built and wraps it in a native shell — a WebView on Android, a WKWebView on iOS — so the same code runs everywhere. Combined with the server-driven UI architecture (the backend sends screen layouts as JSON, and the client just renders them), I can push changes to all three platforms without waiting for App Store review. One codebase, three platforms, one developer. It’s the only way this works.&lt;/p&gt;

&lt;p&gt;But Capacitor puts you in a testing no-man’s-land. Playwright can’t reach inside the native shell — it’s not a browser tab anymore, it’s an app. Native testing frameworks like XCTest and Espresso can’t interact with the content — it’s HTML inside a WebView, not native UI elements. You’re too native for web tools and too web for native tools. Every testing approach in this post exists because of that gap.&lt;/p&gt;

&lt;p&gt;Zabriskie runs on all three platforms. The web gets tested by Playwright — 150+ E2E tests that run on every push. But the mobile apps had nothing. No automated QA, no visual regression checks, no way to know if either client was rendering correctly without manually clicking through every screen. I decided to fix that by teaching Claude to drive both mobile platforms, take screenshots, analyze them for issues, and file its own bug reports.&lt;/p&gt;

&lt;p&gt;Android took 90 minutes. iOS took over six hours. The difference says everything about the state of mobile automation tooling in 2026.&lt;/p&gt;

&lt;h2 id=&quot;android-the-easy-one&quot;&gt;Android: The Easy One&lt;/h2&gt;

&lt;p&gt;The first challenge was connectivity. Inside the Android emulator, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;localhost&lt;/code&gt; refers to the emulator itself, not the host Mac. When the Capacitor app tries to reach &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;localhost:3000&lt;/code&gt; or &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;localhost:8080&lt;/code&gt;, it gets nothing. The fix is &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;adb reverse&lt;/code&gt;:&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;adb reverse tcp:3000 tcp:3000
adb reverse tcp:8080 tcp:8080
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Simple, but you have to re-run it every time the emulator restarts.&lt;/p&gt;

&lt;p&gt;The real breakthrough was realizing that Capacitor apps run inside an Android WebView, and WebViews expose a Chrome DevTools Protocol socket. You can find it, forward it to a local port, and suddenly you have full programmatic control:&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# Find the WebView&apos;s DevTools socket&lt;/span&gt;
&lt;span class=&quot;nv&quot;&gt;WV_SOCKET&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;$(&lt;/span&gt;adb shell &lt;span class=&quot;s2&quot;&gt;&quot;cat /proc/net/unix&quot;&lt;/span&gt; | &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
  &lt;span class=&quot;nb&quot;&gt;grep &lt;/span&gt;webview_devtools_remote | &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
  &lt;span class=&quot;nb&quot;&gt;grep&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-oE&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;webview_devtools_remote_[0-9]+&apos;&lt;/span&gt; | &lt;span class=&quot;nb&quot;&gt;head&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-1&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;)&lt;/span&gt;

&lt;span class=&quot;c&quot;&gt;# Forward it to a local port&lt;/span&gt;
adb forward tcp:9223 localabstract:&lt;span class=&quot;nv&quot;&gt;$WV_SOCKET&lt;/span&gt;

&lt;span class=&quot;c&quot;&gt;# Full CDP access&lt;/span&gt;
curl http://localhost:9223/json
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;With CDP, authentication is one WebSocket message — inject a JWT into localStorage and navigate to the feed. Navigation is another message — set &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;window.location.href&lt;/code&gt;. No coordinate guessing, no UI interaction, no fighting with keyboards or dialogs. The same protocol that Playwright and Puppeteer use, just connected to an Android WebView instead of a desktop browser.&lt;/p&gt;

&lt;p&gt;Combined with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;adb shell screencap&lt;/code&gt; for screenshots, I built a Python script that sweeps all 25 screens of the app in about 90 seconds. Landing, login, all four feeds, post detail, profile, shows hub, content creation forms, catalog, battles, bug forum, diary, badges, tour crews — everything. Each screenshot gets analyzed for visual issues: broken layouts, error messages, missing images, blank screens, status bar overlap.&lt;/p&gt;

&lt;p&gt;When the sweep finds something wrong, it authenticates as &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;zabriskie_bot&lt;/code&gt;, uploads the screenshot to S3, and files a properly formatted bug report to the production forum. The title format is &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;[Android QA] Shows Hub: RSVP button overlaps venue text&lt;/code&gt; — immediately clear that it came from automation and which screen is affected. It knows about expected states too: the crew detail page returning “Forbidden” for non-members isn’t a bug, empty avatar circles aren’t bugs, and the “Preview” text in profile settings is a known cosmetic issue.&lt;/p&gt;

&lt;p&gt;The whole thing runs as a scheduled task every morning at 8:47 AM. The first full run came back clean: 25 screens, 0 critical issues, 2 minor cosmetic notes. If someone’s change breaks a screen overnight, there’s a bug filed before anyone’s had coffee.&lt;/p&gt;

&lt;p&gt;Ninety minutes, start to finish.&lt;/p&gt;

&lt;h2 id=&quot;ios-the-hard-one&quot;&gt;iOS: The Hard One&lt;/h2&gt;

&lt;p&gt;I figured iOS would be straightforward. Same app, same screens, the Simulator is right there on my Mac. What followed was one of the most absurd debugging sessions I’ve had — not because the problem was technically profound, but because the iOS Simulator is a fortress of tiny, compounding restrictions that each seem reasonable in isolation but together create a nightmare.&lt;/p&gt;

&lt;h3 id=&quot;you-cant-type-an-email-address&quot;&gt;You Can’t Type an Email Address&lt;/h3&gt;

&lt;p&gt;The first idea was clean: add a deep link handler, generate a JWT, open the URL via &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;simctl openurl&lt;/code&gt;, and skip the login form entirely. Four attempts, four different failure modes — the native bundle was stale, the config pointed at production, the JWT secret was wrong, the Vite dev server was listening on IPv6 while the Simulator tried IPv4. Zero logins.&lt;/p&gt;

&lt;p&gt;So I fell back to typing credentials into the login form. AppleScript can send keystrokes to the Simulator. But the login form has &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;type=&quot;email&quot;&lt;/code&gt; on the input, and AppleScript’s &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;keystroke &quot;@&quot;&lt;/code&gt; sends &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Shift+2&lt;/code&gt;, which the Simulator interprets as a keyboard shortcut. Every attempt to type &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;@&lt;/code&gt; either switched the form to Sign Up, navigated to Forgot Password, or opened a context menu.&lt;/p&gt;

&lt;p&gt;Pasting didn’t work either. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Cmd+V&lt;/code&gt; gets intercepted by the Simulator. Setting the iOS pasteboard via &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;simctl pbcopy&lt;/code&gt; produced garbled text. The macOS clipboard and the iOS pasteboard are separate systems.&lt;/p&gt;

&lt;p&gt;The fix was a code change: update the backend login handler from &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;WHERE email = $1&lt;/code&gt; to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;WHERE email = $1 OR username = $1&lt;/code&gt;, change the form input from &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;type=&quot;email&quot;&lt;/code&gt; to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;type=&quot;text&quot;&lt;/code&gt;, and create a test user with a known password. Now I could type “qatest” instead of needing an &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;@&lt;/code&gt; symbol. A backend modification to work around a keyboard limitation.&lt;/p&gt;

&lt;h3 id=&quot;you-cant-dismiss-native-dialogs&quot;&gt;You Can’t Dismiss Native Dialogs&lt;/h3&gt;

&lt;p&gt;Upon login, iOS shows a “Would Like to Send You Notifications” dialog rendered by UIKit, not the WebView. Native iOS dialogs cannot be dismissed by any form of macOS-synthesized input.&lt;/p&gt;

&lt;p&gt;I tried AppleScript &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;click at&lt;/code&gt; coordinates across a grid of 100+ positions. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;cliclick&lt;/code&gt; at every possible coordinate. Python Quartz CGEvent mouse events. Pressing Return and Enter. Finding the button in the accessibility tree (not exposed). &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;simctl privacy grant&lt;/code&gt; (not supported for notifications on iOS 26). &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;simctl ui alert accept&lt;/code&gt; (doesn’t exist).&lt;/p&gt;

&lt;p&gt;The dialog sat there, immovable, blocking the app.&lt;/p&gt;

&lt;p&gt;The fix was writing directly to the Simulator’s TCC.db — the privacy permissions database — inserting a pre-approval for &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;kTCCServiceUserNotification&lt;/code&gt;, then restarting SpringBoard. But the timing is critical: it has to happen before installing the app, or the permission state gets cached. And the app’s JavaScript calls &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;PushNotifications.requestPermissions()&lt;/code&gt; on login, which can retrigger it, so I had to add a guard that skips permission requests on localhost.&lt;/p&gt;

&lt;p&gt;The correct sequence: uninstall app, write TCC permission, restart SpringBoard, reinstall app, launch, then login. Only in that exact order does the dialog not appear.&lt;/p&gt;

&lt;h3 id=&quot;you-cant-navigate-by-coordinates-until-you-can&quot;&gt;You Can’t Navigate by Coordinates (Until You Can)&lt;/h3&gt;

&lt;p&gt;The app has a floating nav bar with three bubble buttons in the top-right corner — a Z logo, an avatar, and a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;+&lt;/code&gt; — each opening a vertical dropdown. To test all 25 screens, I needed to tap specific dropdown items. I had coordinates from the CSS. The math checked out. But every approach had a different failure mode.&lt;/p&gt;

&lt;p&gt;AppleScript &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;click at&lt;/code&gt; uses macOS window coordinates. You need the window position, the device screen group offset, the Simulator’s scaling mode (Point Accurate vs. Pixel Accurate vs. Fit Screen), and whether the toolbar is showing. First sweep: 42% accuracy.&lt;/p&gt;

&lt;p&gt;Facebook’s &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;idb&lt;/code&gt; sends taps in device logical points (390x844), so no translation needed. Better for main nav buttons, but dropdown item coordinates were slightly off — taps would close the dropdown before hitting the item, or punch through the z-index to content behind it. Second sweep: 57% accuracy.&lt;/p&gt;

&lt;p&gt;The breakthrough was the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ios-simulator-mcp&lt;/code&gt; tool’s &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ui_describe_point&lt;/code&gt; function. Point it at any coordinate and it returns the accessibility label, role, and frame:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;ui_describe_point(365, 163)
→ AXLabel: &quot;Currents&quot;, type: Link, frame: (342, 159, 40x40)
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;I mapped every dropdown item by probing in 48pt increments. My Y positions were right but my X was wrong — the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;+&lt;/code&gt; dropdown items are at x=258, not x=269. An 11-point error that routed every tap to the wrong column. With verified coordinates and 1.5-second waits for dropdown animations, the sweep hit 100% of screens.&lt;/p&gt;

&lt;p&gt;The winning combination: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ui_describe_point&lt;/code&gt; for discovery, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;idb ui tap&lt;/code&gt; for execution. Map the UI first, tap second. Don’t guess coordinates — measure them.&lt;/p&gt;

&lt;h3 id=&quot;the-fundamental-gap&quot;&gt;The Fundamental Gap&lt;/h3&gt;

&lt;p&gt;The contrast is stark. Android authentication:&lt;/p&gt;

&lt;div class=&quot;language-python highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;n&quot;&gt;ws&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;send&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&apos;{&quot;method&quot;:&quot;Runtime.evaluate&quot;,&quot;params&quot;:{&quot;expression&quot;:&quot;localStorage.setItem(&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\&apos;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;token&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\&apos;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\&apos;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;xxx&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\&apos;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;)&quot;}}&apos;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;iOS authentication: uninstall app, write to TCC database, restart SpringBoard, reinstall app, launch, wait 5 seconds, tap Sign In at specific coordinates, wait, tap Email field, type “qatest” via AppleScript, press Tab, type “qatest123”, press Return, wait, hope.&lt;/p&gt;

&lt;p&gt;Apple’s WKWebView doesn’t expose Chrome DevTools Protocol. Safari Web Inspector uses a proprietary binary protocol that only Safari speaks. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ios-webkit-debug-proxy&lt;/code&gt; only works with real USB devices. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;safaridriver&lt;/code&gt; connects to macOS Safari, not the Simulator’s WebView.&lt;/p&gt;

&lt;p&gt;Android gives you a WebSocket and says “here’s the browser, do whatever you want.” iOS gives you a locked door and a note that says “please use Xcode.”&lt;/p&gt;

&lt;h2 id=&quot;the-mess-in-the-middle&quot;&gt;The Mess in the Middle&lt;/h2&gt;

&lt;p&gt;Between getting Android working and finishing iOS, something happened that illustrates a different kind of failure — not a platform limitation, but an agent discipline problem.&lt;/p&gt;

&lt;p&gt;Railway deployments started failing with a Go version mismatch. My local Go had auto-updated to 1.26, which silently bumped &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;go.mod&lt;/code&gt; to require Go 1.25, while the Dockerfile still used &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;golang:1.24-alpine&lt;/code&gt;. A two-file fix.&lt;/p&gt;

&lt;p&gt;Claude was operating in a git worktree — a clean, isolated copy of the repo designed for exactly this kind of surgical change. Instead of making the fix there, it &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;cd&lt;/code&gt;‘d into the main repository where I had a dozen unrelated in-progress changes. It staged every dirty file, committed them all with the Go version fix, pushed, and opened a PR. The PR contained QA login endpoints, bug forum updates, iOS Simulator workarounds, E2E test config changes, push notification code, and three new skill files. None of which had anything to do with a Go version number.&lt;/p&gt;

&lt;p&gt;Then it got auto-merged before I could close it.&lt;/p&gt;

&lt;p&gt;The bad merge left duplicate variable declarations throughout the test suite — functions declared twice, variables declared twice. One of the accidentally included changes was a form placeholder rename from &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&quot;Email&quot;&lt;/code&gt; to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&quot;Email or Username&quot;&lt;/code&gt;, which broke every auth E2E test that used &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;page.fill(&apos;input[placeholder=&quot;Email&quot;]&apos;)&lt;/code&gt;. A catalog test that asserted &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;itemCount &amp;gt; 50&lt;/code&gt; only worked against my local database — CI has a handful of records.&lt;/p&gt;

&lt;p&gt;To fix a two-file change, I ended up making four follow-up commits across three PRs. The first two I pushed without running tests locally. They failed. The third I actually ran tests first. It passed. Three rounds of “push and pray” before doing what should have been step one: run the tests, read the output, fix what’s broken, verify, then push. The same debugging rule I enforce every session — check the logs first, theories second — and I ignored it for my own changes.&lt;/p&gt;

&lt;h2 id=&quot;what-this-all-adds-up-to&quot;&gt;What This All Adds Up To&lt;/h2&gt;

&lt;p&gt;Both platforms now have working QA skills. Every morning, the Android emulator and the iOS Simulator boot up, sweep 25 screens each, analyze the screenshots, and file bug reports for anything that looks wrong. Three platforms, all tested, all filing their own bugs.&lt;/p&gt;

&lt;p&gt;The lessons keep reinforcing each other:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;CDP over taps.&lt;/strong&gt; Don’t fight coordinate systems if you can use the browser’s own debugging protocol. Android gives you this for free. iOS doesn’t, and every workaround adds fragility.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Measure, don’t guess.&lt;/strong&gt; The accessibility API that finally made iOS navigation work is the same principle as checking logs before forming theories. Don’t assume you know where a button is — ask the system.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Stay in the worktree.&lt;/strong&gt; Isolation only works if you respect the boundaries. The moment you step outside “just for a quick look,” you’re one careless command away from committing a dozen unrelated files to production.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Run the tests before you push.&lt;/strong&gt; Three rounds of push-and-pray before doing what should have been step one. The gap between knowing a rule and following it is measured in wasted commits.&lt;/p&gt;

&lt;p&gt;Apple, if you’re reading this: please expose CDP or WebDriver for Simulator WebViews. The developer tools are great when a human is using them. They’re nearly useless when an AI is trying to.&lt;/p&gt;
</description>
				<pubDate>Sun, 22 Mar 2026 00:00:00 +0000</pubDate>
				<link>https://christophermeiklejohn.com/ai/zabriskie/development/android/ios/2026/03/22/teaching-claude-to-qa-a-mobile-app.html</link>
				<guid isPermaLink="true">https://christophermeiklejohn.com/ai/zabriskie/development/android/ios/2026/03/22/teaching-claude-to-qa-a-mobile-app.html</guid>
			</item>
		
			<item>
				<title>What Building With Claude Actually Looks Like</title>
				<description>&lt;blockquote&gt;
  &lt;p&gt;&lt;em&gt;“Sometimes a notion gets a-hold of you, carries you away”&lt;/em&gt;
— Grateful Dead, “Althea”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2 id=&quot;the-saturday&quot;&gt;The Saturday&lt;/h2&gt;

&lt;p&gt;On March 7th, I sat down at 11am and started building. By 2am I had shipped a Relisten integration with inline audio players for archival recordings, a Quick Post feature for sharing past shows, Phantasy Tour as a live setlist source, Sign in with Apple, a badges and achievements system, security hardening across every API route, a complete CI pipeline with GitHub Actions, and something called Goose Mode — a tour companion dashboard with a wandering desktop goose that follows your cursor around the screen.&lt;/p&gt;

&lt;p&gt;That was one day. 124 commits.&lt;/p&gt;

&lt;p&gt;I kept going. By the end of the week, the total was 144 commits across 7 days. Relisten integration. Live show experience. Six new bands on the platform. A bug forum. Backend test coverage from 43.9% to 70.4%. An admin analytics dashboard. Apple App Review fixes. Scrapers for four different setlist sources. Archive.org as a recording provider. Mobile layout fixes for three different iOS edge cases.&lt;/p&gt;

&lt;p&gt;I’m building Zabriskie by myself, and I’m building it with Claude. This is what that actually looks like.&lt;/p&gt;

&lt;h2 id=&quot;the-cost-of-trying-something&quot;&gt;The Cost of Trying Something&lt;/h2&gt;

&lt;p&gt;Here’s the thing nobody tells you about building with an AI collaborator: the most important change isn’t speed. It’s what happens to your relationship with bad ideas.&lt;/p&gt;

&lt;p&gt;Goose Mode started at 9pm on a Saturday night as a tour-focused homepage with a literal wandering goose — a transparent animated sprite that wanders around the page while you browse upcoming shows. It was fun. It was also wrong. The layout didn’t work. The information hierarchy was off. The goose was distracting in a way that stopped being charming after about thirty seconds.&lt;/p&gt;

&lt;p&gt;Old me would have agonized. I’d spend an hour in Figma trying to figure out the right layout before writing a line of code. I’d poll people. I’d sit with it for a few days. The cost of being wrong was high enough that I’d optimize for not being wrong.&lt;/p&gt;

&lt;p&gt;Instead, at 11:25pm, I told Claude to redesign the whole thing as a tour companion dashboard. By 3:37am it had been redesigned again — left-aligned with grouped tour timelines. By 4:08am, another redesign — card-per-tour visual timelines. By 4:42am, another — live countdown, attendee avatars, couch tour cards. By 5:41am I’d added a flip-clock countdown. By 6:17am there were interactive tour maps embedded in each card.&lt;/p&gt;

&lt;p&gt;Six versions in nine hours. Each one a real, working implementation I could tap through on my phone. Not mockups. Not wireframes. Running code. The version that shipped was the fifth attempt, and I only knew it was right because I’d seen the four that weren’t.&lt;/p&gt;

&lt;p&gt;When the cost of trying something drops to near zero, you stop designing in your head and start designing in reality. That changes everything.&lt;/p&gt;

&lt;h2 id=&quot;a-pigeons-show-a-broken-chat-and-a-deploy-at-11pm&quot;&gt;A Pigeons Show, a Broken Chat, and a Deploy at 11pm&lt;/h2&gt;

&lt;p&gt;On the night of March 7th, Pigeons Playing Ping Pong was playing a show. People were using Zabriskie’s live show features — Live Chomping, the setlist tracker, the “tonight” banner. I was watching the show on the couch and also watching my app.&lt;/p&gt;

&lt;p&gt;The chat input was getting cut off on Chrome mobile. I could see it happening in real time because I was using it. At 10:33pm I fixed the layout. At 10:39pm I realized live show posts weren’t appearing in the main feed while a show was active — a bug nobody would have found in testing because it only manifested when a real show was actually live. Fixed it. Deployed. At 10:45pm I added Phantasy Tour as a live setlist source because the existing sources weren’t picking up the setlist fast enough. At 11:20pm I fixed the song order — songs were appearing out of sequence, and duplicate comments were showing up in the chat.&lt;/p&gt;

&lt;p&gt;At 11:34pm I discovered that on newer iPhones, the live page content was rendering underneath the Dynamic Island. That’s not something you find in a simulator. That’s something you find when you’re holding the phone in your hand, trying to see who’s chomping, and you can’t read the first line of text.&lt;/p&gt;

&lt;p&gt;This is what dogfooding actually means. Not “I used my own app once and it seemed fine.” It means you’re sitting on your couch during a Pigeons show, fixing a CSS bug that’s blocking your own experience, deploying it, and immediately seeing whether it worked — all while the show is still going.&lt;/p&gt;

&lt;h2 id=&quot;the-breadth-problem&quot;&gt;The Breadth Problem&lt;/h2&gt;

&lt;p&gt;Solo developers are supposed to specialize. Pick a lane. You can’t do backend and frontend and mobile and DevOps and design. That’s a team.&lt;/p&gt;

&lt;p&gt;In one week I worked on:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;Audio infrastructure&lt;/strong&gt; — Relisten API integration, playlist players, collapsible audio widgets, caching, recording quality filtering&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Real-time features&lt;/strong&gt; — Live chat (sorry, Live Chomping), WebSocket setlist polling, presence indicators&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Data ingestion&lt;/strong&gt; — Scrapers for Phantasy Tour, TTBase, setlist.fm, Archive.org/etree&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Security&lt;/strong&gt; — Rate limiting, CORS hardening, JWT invalidation on password change, auth on every route&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Testing&lt;/strong&gt; — Playwright E2E coverage, mock HTTP servers, backend coverage push, CI pipeline&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Mobile&lt;/strong&gt; — iOS Dynamic Island fix, Chrome mobile layout, safe area insets, App Review compliance&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Social features&lt;/strong&gt; — Quick Post, @mentions, clickable URLs, clickable avatars, bug forum with upvotes&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Design&lt;/strong&gt; — Goose Mode (x5), Spotify now-listening card, admin analytics dashboard&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This isn’t sustainable without Claude. I want to be honest about that. I’m not some 10x developer who figured out the productivity secret. I’m a normal developer who has a collaborator that doesn’t sleep, doesn’t get bored of writing tests, and can context-switch from a Go backend handler to a Swift layout constraint to a Playwright test assertion without missing a beat.&lt;/p&gt;

&lt;p&gt;The thing Claude is genuinely good at — the thing that makes the breadth possible — is carrying the context. I can say “the Relisten player should be collapsible, like we did on the show listing page” and it knows what I mean because it wrote that code an hour ago. I don’t have to re-explain the component architecture every time I switch contexts. It already knows.&lt;/p&gt;

&lt;h2 id=&quot;the-things-i-still-do&quot;&gt;The Things I Still Do&lt;/h2&gt;

&lt;p&gt;I want to be clear about what Claude doesn’t do, because the discourse around AI and coding has gotten absurd in both directions. People either think it writes your entire app for you, or they think it’s useless. Neither is true.&lt;/p&gt;

&lt;p&gt;Claude doesn’t know what to build. It doesn’t know that a Pigeons show is happening tonight and that the setlist tracker needs Phantasy Tour as a source. It doesn’t know that the Goose Mode countdown should use a flip-clock style because that’s what feels right for the aesthetic. It doesn’t know that the live chat should be called “Live Chomping” because that’s what the community actually calls it. It doesn’t know that the Quick Post feature exists because I watched someone try to share a recording and give up because it was too many steps.&lt;/p&gt;

&lt;p&gt;Every feature started with me noticing something — a pain point, an opportunity, an idea at 3am that I couldn’t let go of. Claude is the best collaborator I’ve ever had for turning those observations into running software. But the observations are mine. The taste is mine. The understanding of what this community needs is mine.&lt;/p&gt;

&lt;p&gt;And the bugs. The bugs are mine too. The production crash from the code coverage import that should never have been in main.go — that was a human mistake. Claude wrote the instrumentation; I’m the one who forgot to check the build before deploying. The authentication forwarding bug that broke internal SDUI calls — that emerged from the interaction between two features Claude had built separately, each correct in isolation, broken in combination. Integration bugs are still human problems. They require understanding the whole system, not just the code.&lt;/p&gt;

&lt;h2 id=&quot;439-to-704&quot;&gt;43.9% to 70.4%&lt;/h2&gt;

&lt;p&gt;On March 8th at 8:45am — after the Pigeons show, after the Goose Mode all-nighter, after 124 commits in a single day — I asked Claude to push the backend test coverage as high as it could go.&lt;/p&gt;

&lt;p&gt;One commit. 43.9% to 70.4%.&lt;/p&gt;

&lt;p&gt;This is the thing that makes me genuinely optimistic about building alone. The testing tax — the thing that slows down every solo developer, the thing you skip because you’re tired and the feature works and you’ll write tests later (you won’t) — that tax is effectively gone. Claude writes comprehensive tests. Not just happy-path assertions. Edge cases. Error conditions. Auth boundary tests. The kind of tests you’d write if you had infinite patience and no ship date.&lt;/p&gt;

&lt;p&gt;I still write tests for the things that matter to me — the tricky integration points, the things where the test itself is the specification. But the coverage floor, the boring-but-necessary tests that catch regressions? That’s not my job anymore. And that means I actually have test coverage, which means I can refactor with confidence, which means the codebase stays healthy even at this pace.&lt;/p&gt;

&lt;h2 id=&quot;three-nights-at-the-beacon&quot;&gt;Three Nights at the Beacon&lt;/h2&gt;

&lt;p&gt;The Pigeons show was from my couch. The Tedeschi Trucks Band run at the Beacon Theatre was from my seat.&lt;/p&gt;

&lt;p&gt;TTB was playing ten nights at the Beacon in March. I had tickets to three of them. I also had a platform that didn’t know Tedeschi Trucks Band existed yet. Tuesday before the show I added them — 58 shows for the 2026 Future Soul Tour. But TTB’s setlists don’t come from setlist.fm. They come from TTBase, which has a completely different HTML structure. So I needed a new scraper.&lt;/p&gt;

&lt;p&gt;That night, from my seat at the Beacon, I built it. I told Claude what TTBase looked like, what data I needed, and how it should integrate with the live setlist poller. Claude wrote the scraper. I deployed it. It didn’t work — the HTML structure didn’t match what we’d expected. So I told Claude what was wrong, it fixed the scraper to match the actual Songfish HTML structure, I deployed again, and watched the setlist populate in real time while the band was playing.&lt;/p&gt;

&lt;p&gt;I used the app across all three shows. I’d be sitting there, listening to Derek Trucks play, and I’d notice something — a layout bug, a feature that didn’t work right, something that could be better. I’d pull out my phone, tell Claude what I needed, watch it write the fix, push it to production, and then check it on my phone. All from my seat. All while the show was happening.&lt;/p&gt;

&lt;p&gt;The reason this was even possible is that Zabriskie uses a server-driven UI architecture. Early on, Claude helped guide me toward SDUI as the core design — the server sends down the layout and components, and the app just renders whatever it receives. That means I can change virtually anything about the experience without shipping a new version through the App Store. A fix to a layout, a new feature, a redesigned screen — it’s all a server deploy. When I pushed a fix from the Beacon, every phone running the app got it immediately. No app update. No review process. No waiting.&lt;/p&gt;

&lt;p&gt;This is a different thing from the Pigeons show, where I was on the couch and had a laptop open. At the Beacon I was in the audience with nothing but my phone. The workflow was: notice a problem, describe it to Claude in plain English, Claude fixes it and pushes to prod, I pull up the app and verify. No laptop. No IDE. No terminal. Just me, my phone, and a collaborator who could do the rest.&lt;/p&gt;

&lt;p&gt;By the end of the three-night run, the TTB experience on Zabriskie was solid. Live setlists from TTBase. Show pages with all the metadata. The whole thing built and refined from inside the venue where the band was playing. That’s not a development workflow I ever imagined having.&lt;/p&gt;

&lt;h2 id=&quot;the-week-keeps-going&quot;&gt;The Week Keeps Going&lt;/h2&gt;

&lt;p&gt;After the Saturday marathon and the Beacon run, I kept building. Sunday: clickable URLs in posts and comments. A small thing. The kind of thing you’d never prioritize on a roadmap but that users notice immediately.&lt;/p&gt;

&lt;p&gt;Thursday I added Grahame Lesh &amp;amp; Friends with 25 shows. Saturday night I was at the Grahame Lesh show, and this time I wasn’t debugging anything. The setlist just worked. It synced with the show in real time, song by song, no intervention. I was posting photos from my seat and the setlist was updating alongside them. My friends were watching from home, talking to me through the app — they could see the setlist, see my photos, and we were all in the same experience even though I was the only one in the room.&lt;/p&gt;

&lt;p&gt;That’s the moment it stopped being a project and started being the thing I described in the manifesto. The bridge between the physical and the virtual. The people in the crowd and the people at home, in the same place. It worked. Not because I was fixing it in real time — because I didn’t have to.&lt;/p&gt;

&lt;p&gt;The bug forum is maybe the most meta thing I’ve built that week. A full in-app bug reporting system with upvotes, comments, categorization, bot notifications, and admin tools. Built in an afternoon. A bug reporting system, built by one person with an AI collaborator, for reporting bugs in an app built by one person with an AI collaborator. It has six commits spanning two hours. It works. Users are filing bugs in it right now.&lt;/p&gt;

&lt;h2 id=&quot;what-this-means&quot;&gt;What This Means&lt;/h2&gt;

&lt;p&gt;I wrote the manifesto for Zabriskie on March 8th, in between the all-night Goose Mode session and deploying fixes for the Pigeons show. The manifesto is about reclaiming the internet as a third place. About building community infrastructure that serves people instead of extracting from them. About doing it as a non-profit, solo, self-funded, because that’s the only way it gets done honestly.&lt;/p&gt;

&lt;p&gt;The week that followed is what makes that possible. Not because AI is magic, but because it changes the economics of ambition. A single person can build something that previously required a team. Not because the single person became superhuman, but because the gap between “what I can imagine” and “what I can ship” got dramatically smaller.&lt;/p&gt;

&lt;p&gt;I have eighty beta users now. They’re filing bugs. They’re posting about shows. They’re using Live Chomping during actual shows. The thing works. Not in a demo sense — in a “people are using this to connect with each other around music” sense.&lt;/p&gt;

&lt;p&gt;That’s all I ever wanted.&lt;/p&gt;

&lt;p&gt;The gap between what you can imagine and what you can ship is the space where ideas go to die. For years I had this idea — the third place, the taste-based community, all of it — and I couldn’t build it because I’m one person with a day job. Now I can. Not perfectly. Not without bugs. Not without 3am sessions that leave me wrecked the next day. But I can build it, and I can build it fast enough that the community doesn’t outgrow the infrastructure.&lt;/p&gt;

&lt;p&gt;144 commits in a week. Six versions of Goose Mode. A scraper built from a seat at the Beacon Theatre. A live show debugged in real time. A test suite that actually exists. A bug forum built in two hours.&lt;/p&gt;

&lt;p&gt;This is what building with Claude actually looks like. It’s not a press release. It’s a Saturday that starts at 11am and ends sometime around dawn. It’s three nights at the Beacon with nothing but your phone, shipping fixes between sets. And when you look up, the thing you imagined is running on your phone, and people are using it.&lt;/p&gt;

&lt;hr /&gt;

&lt;blockquote&gt;
  &lt;p&gt;&lt;em&gt;“Sometimes a notion gets a-hold of you, ties you to the tracks”&lt;/em&gt;
— Grateful Dead, “Althea”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;em&gt;Zabriskie. Where taste resonates.&lt;/em&gt;&lt;/p&gt;
</description>
				<pubDate>Fri, 20 Mar 2026 00:00:00 +0000</pubDate>
				<link>https://christophermeiklejohn.com/ai/zabriskie/development/2026/03/20/what-building-with-claude-actually-looks-like.html</link>
				<guid isPermaLink="true">https://christophermeiklejohn.com/ai/zabriskie/development/2026/03/20/what-building-with-claude-actually-looks-like.html</guid>
			</item>
		
			<item>
				<title>Why I&apos;m Building Zabriskie</title>
				<description>&lt;blockquote&gt;
  &lt;p&gt;&lt;em&gt;“Don’t let a heavy one hold back the dawn in you”&lt;/em&gt;
— Goose, “(dawn)”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2 id=&quot;the-third-place&quot;&gt;The Third Place&lt;/h2&gt;

&lt;p&gt;There’s a concept in sociology called the “third place” — not home, not work, but the place where you go to find your people. The coffee shop. The record store. The bar after the show. It’s where community happens.&lt;/p&gt;

&lt;p&gt;The internet used to be that place.&lt;/p&gt;

&lt;p&gt;In the late 90s and early 2000s, there were communities built entirely around taste. Makeoutclub. MySpace. Forums with names you’ve probably forgotten. Your profile wasn’t a highlight reel of your life — it was a declaration of what you were into. The music you listed, the films you referenced, the books in your sidebar. People found each other through that. You’d stumble onto someone’s page because they listed the same obscure record you loved, and suddenly you had a new friend in a city you’d never been to. The social graph was built on shared taste, not shared geography.&lt;/p&gt;

&lt;p&gt;For a while, Twitter picked up that torch. By the mid-2010s, I had friends all over the world — people who followed me because they were into the same things I was into. I’d travel for research and work to Europe, Australia, the UK, wherever, and I could post and there would always be someone up for a drink, a show, a conversation. It was global and it was real. The connections were built on interests, and they worked.&lt;/p&gt;

&lt;p&gt;Then that got destroyed too.&lt;/p&gt;

&lt;p&gt;Twitter got bought and the community scattered. Instagram and Facebook were already ad machines, optimizing for engagement over connection. Mastodon was too federated for anyone to find each other. Bluesky came too late, after everyone had already given up. Letterboxd was a glimmer of hope — a platform built around a specific kind of taste — but it devolved into ironic one-liners engineered for laughs and would-be film critics writing for an audience instead of a community.&lt;/p&gt;

&lt;p&gt;And for music? Nothing. A total void.&lt;/p&gt;

&lt;p&gt;The third place doesn’t exist anymore. Not for people who actually care about what they’re listening to, watching, and reading.&lt;/p&gt;

&lt;h2 id=&quot;proof-it-works&quot;&gt;Proof It Works&lt;/h2&gt;

&lt;p&gt;Here’s the thing: I know the third place can work online, because one community already does it in the physical world every single day.&lt;/p&gt;

&lt;p&gt;The jam band community — my community — knows how to build this. They have shakedowns. They have lot meetups. They organize their entire lives around shows. They travel. People wander on and off tour. Strangers become friends because they’re standing next to each other for three nights in a row. The social infrastructure is already there. The culture of gathering around shared experience is already there.&lt;/p&gt;

&lt;p&gt;The tools just haven’t caught up.&lt;/p&gt;

&lt;p&gt;Reddit requires constant refreshing if you want anything resembling a live conversation. Facebook groups are merch spam wastelands. The dedicated platforms that do exist — Phantasy Tour, various setlist sites — are siloed. Each one focuses on a single band or a single function. There’s no unified identity. You’re a different person on every platform. And most of them haven’t evolved technologically since 2008.&lt;/p&gt;

&lt;p&gt;I know this because I lived it. I spent ten years in grad school, and when I finished, I had no community. Not really. People didn’t even say goodbye when I left. A decade of my life, and it just… ended.&lt;/p&gt;

&lt;p&gt;Then I got back into the jam band world, and something completely different happened. People I’d long disconnected with welcomed me back like no time had passed. Patrick, someone I hadn’t talked to in twenty years. Sara, someone I’d sold tickets to online. I made friends through friends of friends, the way you do on the lot. I made friends standing next to some Dallas fans at a show during the NBA playoffs when Dallas was playing Boston, and I was wearing a Celtics jersey. That’s it. That’s all it took. Standing next to strangers who were into the same thing, and suddenly you’re not strangers anymore.&lt;/p&gt;

&lt;p&gt;No other community in my life has worked like that. The jam band world doesn’t care where you went to school or what you do for a living. It cares whether you were at the show. It cares about the music. And that’s enough.&lt;/p&gt;

&lt;h2 id=&quot;covid-separated-us-we-never-came-back&quot;&gt;COVID Separated Us. We Never Came Back.&lt;/h2&gt;

&lt;p&gt;The pandemic made all of this worse. It separated us physically, and we never fully recovered. We got distant from each other in ways we’re still reckoning with. The platforms we had didn’t help — they made it worse, feeding us outrage and ads while we sat alone.&lt;/p&gt;

&lt;p&gt;Couch tour became a lifeline during that time. Watching a show from home, knowing other people were watching too, trying to find each other in Reddit threads and group texts. But it was held together with duct tape. There was no place that actually served that experience.&lt;/p&gt;

&lt;p&gt;That’s what I want to build. A bridge between the physical and the virtual. The people in the crowd and the people at home should be in the same community, sharing the same experience. And if you’re home alone on a Tuesday night watching a stream — you’re not alone. You’re part of something.&lt;/p&gt;

&lt;p&gt;Couch touring shows is just one version of this. Book clubs were the original shared cultural experience. Shared movie watching is the next. The format extends infinitely because the core is always the same: people experiencing culture together, regardless of where they physically are. A global community of people who love media.&lt;/p&gt;

&lt;p&gt;Facebook is a dinosaur. It doesn’t provide anything useful for this. It harvests your data, sells it to advertisers, and gives you merch spam groups in return.&lt;/p&gt;

&lt;h2 id=&quot;extending-beyond-the-lot&quot;&gt;Extending Beyond the Lot&lt;/h2&gt;

&lt;p&gt;The jam band community is the proof of concept, but the vision is bigger.&lt;/p&gt;

&lt;p&gt;The same energy that makes shakedowns work — people organizing around the things they love, sharing experiences in real time, building identity through taste — applies to anyone who cares deeply about culture. The person who wants to talk about the Jarmusch film they just watched, not post an ironic quip for engagement. The person spinning a new album who wants to know what their friends think, not get an algorithmic recommendation from a company that’s also selling them headphones. The person who just finished a novel and wants to find others who read it, not write an Amazon review into the void.&lt;/p&gt;

&lt;p&gt;Zabriskie starts with the jam band world because that community is ready for it. They already have the culture. They just need a place that works. But the thesis is universal: anyone whose identity is shaped by what they consume — music, film, books — deserves a third place built around that.&lt;/p&gt;

&lt;h2 id=&quot;the-opposite-of-everything&quot;&gt;The Opposite of Everything&lt;/h2&gt;

&lt;p&gt;Zabriskie is the opposite of every social network that exists.&lt;/p&gt;

&lt;p&gt;We don’t want everyone on the planet to join. In &lt;em&gt;Careless People&lt;/em&gt;, you can trace the path of platforms that pursued growth indefinitely, at all costs, and watch exactly where it leads — the product hollows out, the community dies, the ads take over. We’re not walking that path.&lt;/p&gt;

&lt;p&gt;We’re not selling ads. We’re not optimizing for engagement. We actually want you to get off the site. The feed is finite — you read it, you’re done, you go live your life. There are no free-form text posts. Every single post requires a piece of culture — an album, a film, a book, a show — a rating, and your actual thoughts. We are about culture.&lt;/p&gt;

&lt;p&gt;Discovery works differently here too. You &lt;em&gt;want&lt;/em&gt; to see posts from people you don’t know. You want to meet people organically, based on taste. But not through algorithmic matching — not the dating app model, not the “people you may know” sidebar. Taste proliferates through the network the way it does in real life: through people.&lt;/p&gt;

&lt;p&gt;Think about high school. You’re walking down the hall and you see someone wearing a shirt from a band you love. You don’t know them. They might be in a completely different social world. But you &lt;em&gt;recognize&lt;/em&gt; something about them instantly. That moment of connection based on taste, without an algorithm, without a recommendation engine, without anyone engineering the encounter — that’s how discovery should work. And nothing online does it.&lt;/p&gt;

&lt;p&gt;Zabriskie does.&lt;/p&gt;

&lt;h2 id=&quot;not-a-startup&quot;&gt;Not a Startup&lt;/h2&gt;

&lt;p&gt;I should be clear about what this isn’t. Zabriskie isn’t a startup. There’s no pitch deck. No Series A. No growth targets. No exit strategy. Nobody is looking to flip this to Google in three years. There are no revenue goals and no KPIs around daily active users.&lt;/p&gt;

&lt;p&gt;This is about reclaiming the internet as a &lt;em&gt;place&lt;/em&gt;. The third place. Building community, not building a business.&lt;/p&gt;

&lt;p&gt;The internet used to be somewhere you went to find your people. Then it became something that was done &lt;em&gt;to&lt;/em&gt; you — feeds engineered to keep you scrolling, platforms optimized to extract value from your attention, your taste data packaged and sold to the highest bidder.&lt;/p&gt;

&lt;p&gt;Zabriskie is a rejection of all of that. We’re not looking for revenue or growth. We’re looking to reclaim the internet as our third space and build community.&lt;/p&gt;

&lt;p&gt;And to put our money where our mouth is: Zabriskie is becoming a non-profit. Not because it’s a clever tax strategy. Because it’s the only structure that’s honest about what we’re doing. Community over money. Full stop. Every decision gets made with one question: does this make the community better? Not: does this grow the user base? Not: does this increase engagement? Not: does this make the metrics look good for investors?&lt;/p&gt;

&lt;p&gt;There are no investors. There never will be.&lt;/p&gt;

&lt;p&gt;I’m building this on my own. Funding it myself. Writing code on nights and weekends because I believe in this. There’s no team of fifty engineers. There’s no office. There’s just me, doing the work, because I think this matters. And I think this is the only way it can be done honestly — if it’s built by someone who actually wants to use it, not by someone who wants to monetize it.&lt;/p&gt;

&lt;p&gt;This is the only way to free ourselves from the oppression of existing social networks. You don’t reform platforms that were designed from the ground up to extract value from your attention. You don’t petition them to be better. You build something new. You take it back.&lt;/p&gt;

&lt;p&gt;We’re building this because something has been missing from the internet for a long time, and people want it back. The third place. A space built around taste, around culture, around the things that actually matter to you. A place where you show up not because an algorithm pulled you in, but because your people are there.&lt;/p&gt;

&lt;p&gt;That’s it. That’s the whole thing.&lt;/p&gt;

&lt;p&gt;I owe thanks to the people who made this real. Patrick and Mary are my partners in this — they’ve been instrumental in shaping the design of this platform from day one. Every screen, every decision about how it should feel, has their fingerprints on it. This is as much theirs as it is mine. Sara and Charles have been there from the beginning, beta testing features, breaking things, and telling me when something didn’t work. You don’t build a community platform alone. You build it with your community.&lt;/p&gt;

&lt;p&gt;Rick Mitarotonda once said that if something brings you passion, you should work as hard as you can at it — that the results, the success, it’s not about that. That’s exactly how I feel about Zabriskie. This isn’t about metrics or outcomes. It’s about the work. It’s about building something that matters to the people who use it.&lt;/p&gt;

&lt;p&gt;If any of this resonates with you — if you’ve been looking for the place that used to exist and doesn’t anymore — come find us at &lt;a href=&quot;https://zabriskie.app&quot;&gt;zabriskie.app&lt;/a&gt;. Bring your taste. Bring your people. The dawn is coming. Let’s build the third place together.&lt;/p&gt;

&lt;hr /&gt;

&lt;blockquote&gt;
  &lt;p&gt;&lt;em&gt;“I feel it all in our hands, in a rising sun”&lt;/em&gt;
— Goose, “(dawn)”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;em&gt;Zabriskie. Where taste resonates.&lt;/em&gt;&lt;/p&gt;
</description>
				<pubDate>Sun, 08 Mar 2026 00:00:00 +0000</pubDate>
				<link>https://christophermeiklejohn.com/ai/zabriskie/community/2026/03/08/why-im-building-zabriskie.html</link>
				<guid isPermaLink="true">https://christophermeiklejohn.com/ai/zabriskie/community/2026/03/08/why-im-building-zabriskie.html</guid>
			</item>
		
			<item>
				<title>Claude Tested Everything Except the One Thing That Mattered</title>
				<description>&lt;p&gt;Three weeks ago I &lt;a href=&quot;/ai/claude/2026/02/17/building-a-social-app-in-a-week-with-claude-code.html&quot;&gt;wrote about building a social app in a week with Claude Code&lt;/a&gt;. The app shipped. My friends are using it. I kept building.&lt;/p&gt;

&lt;p&gt;Since that post, Claude has written 154 end-to-end tests across 17 spec files. It tests login, logout, signup, and redirect guards. It tests the feed, the bookmarks page, the notifications page. It tests liking, unliking, commenting, amplifying, recommending. It tests show RSVPs, band pages, setlist search. It tests a tournament bracket system. It tests song battles. It tests a badge and achievement system. It tests tour crews. It tests a getting-started tutorial. It tests a Goose Mode dashboard (if you haven’t heard Goose yet, well, as they say, Goose fucks). It tests a community catalog. It tests tracklist rendering and live show layouts.&lt;/p&gt;

&lt;p&gt;It does not test posting.&lt;/p&gt;

&lt;p&gt;Posting is the entire point of the app. It’s the one thing every user does every time they open it. You search for an album, you write something about it, you hit submit, and it appears in the feed. That’s the product. Everything else — the battles, the crews, the badges, the tournaments — is decoration around that core loop.&lt;/p&gt;

&lt;p&gt;There is no test that searches for an album. No test that fills out the review form. No test that submits a post through the UI and verifies it appears. Zero.&lt;/p&gt;

&lt;p&gt;The test file called &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;post.spec.ts&lt;/code&gt; does exist. It has 11 tests. They verify that the post detail page &lt;em&gt;renders&lt;/em&gt;. That the new post page &lt;em&gt;renders a search form&lt;/em&gt;. That the profile page &lt;em&gt;renders&lt;/em&gt;. The word “render” is doing a lot of heavy lifting. None of them actually post anything.&lt;/p&gt;

&lt;p&gt;There is a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;createPost()&lt;/code&gt; helper in the test utilities. It calls the API directly — &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;POST /api/posts&lt;/code&gt; with a JSON body — to set up test data for &lt;em&gt;other&lt;/em&gt; tests. The social tests use it to create a post so they can test liking it. The bookmark tests use it to create a post so they can test bookmarking it. The core action of the app exists in the test suite only as scaffolding for side features.&lt;/p&gt;

&lt;p&gt;Here are the test counts by spec file:&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;Tests&lt;/th&gt;
      &lt;th&gt;Feature&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;33&lt;/td&gt;
      &lt;td&gt;Tour crews&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;28&lt;/td&gt;
      &lt;td&gt;Shows&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;25&lt;/td&gt;
      &lt;td&gt;Song battles&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;13&lt;/td&gt;
      &lt;td&gt;Catalog&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;11&lt;/td&gt;
      &lt;td&gt;Setlist search&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;11&lt;/td&gt;
      &lt;td&gt;Posts (rendering only)&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;9&lt;/td&gt;
      &lt;td&gt;Tournaments&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;6&lt;/td&gt;
      &lt;td&gt;Getting started tutorial&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;4&lt;/td&gt;
      &lt;td&gt;Badges&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;0&lt;/td&gt;
      &lt;td&gt;Actually submitting a post&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;I asked Claude to write tests. Multiple times. I put it in the project instructions, in bold: &lt;strong&gt;“Write a new test for every new user-facing behavior.”&lt;/strong&gt; I listed exactly what warrants a test: new screens, new buttons, new API endpoints, bug fixes. Claude wrote that rule on February 23rd. After that date, it created 10 new spec files and 113 new tests — for tournaments, battles, badges, crews, goose mode, catalog, setlist search, tracklists, tutorials, and live layouts. Not one for posting.&lt;/p&gt;

&lt;hr /&gt;

&lt;p&gt;Then the auth refactor happened.&lt;/p&gt;

&lt;p&gt;Claude had originally built 25+ backend routes without authentication. Posts, comments, profiles, search, live chat — all accessible to anyone, no login required. I don’t know why. The middleware existed. The pattern was established. It just… didn’t apply it.&lt;/p&gt;

&lt;p&gt;When I noticed, the fix required touching every route in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;main.go&lt;/code&gt; and every page component in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;App.jsx&lt;/code&gt;. Fifty-seven lines changed in the backend, fifty-six in the frontend. That’s the kind of refactor where, if you have good test coverage of the core flow, you make the change, run the tests, and find out immediately what broke.&lt;/p&gt;

&lt;p&gt;We did not have good test coverage of the core flow.&lt;/p&gt;

&lt;p&gt;The refactor broke things. Thirty-one seconds after the auth commit, there was already a follow-up fix — a test was hitting &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;GET /api/posts/{id}&lt;/code&gt; without an auth header and getting 401s. Then another fix because the live show pill broke. Then another because pills showed on logged-out pages. The cascade was short this time, but only because the tests we &lt;em&gt;did&lt;/em&gt; have caught the edges. The center — the posting flow — had nothing to catch.&lt;/p&gt;

&lt;hr /&gt;

&lt;p&gt;This is part of a broader pattern. When something breaks, I ask Claude to write a failing test first, to prove what’s actually broken before trying to fix it. Claude does not do this. What Claude does instead is read the bug report, form a theory about the cause, and immediately start editing code. If the theory is wrong — and it often is — the “fix” breaks something else. Then Claude fixes that. Then something else breaks.&lt;/p&gt;

&lt;p&gt;The commit history is the evidence. Out of 833 total commits, 202 are fixes. That’s 24% — one in four commits exists to fix something Claude got wrong. And they come in chains:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;Show post cards&lt;/strong&gt;: four consecutive fix commits. Orphaned edit button, then flaky tests, then wrong assertion, then more broken assertions.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Live chat&lt;/strong&gt;: four consecutive fix commits. Wrong sort order, then scroll broken, then passive touch events, then iOS Safari viewport bleed.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;S3 avatars&lt;/strong&gt;: three consecutive fix commits. URLs expiring, then NULL media_item_id scan failure, then the same scan failure &lt;em&gt;again with the same fix&lt;/em&gt;.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Deployment&lt;/strong&gt;: two identical commits back-to-back. “Fix web service deployment with npx serve.” Twice. The same message.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each chain follows the same shape: Claude guesses what’s wrong, ships a fix without verifying the guess, the fix breaks something adjacent, and the cycle repeats. A failing test at the start of each chain would have stopped it at one commit.&lt;/p&gt;

&lt;p&gt;The project instructions file — &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;CLAUDE.md&lt;/code&gt; — is now full of rules that exist because of this pattern. Each one was written after an incident where Claude did exactly the thing the rule prohibits:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;“Logs First, Theories Second”&lt;/strong&gt; — because Claude would spin up hypotheses instead of reading the error that was right there in the logs.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;“Respect User’s Layer Diagnosis”&lt;/strong&gt; — because when I’d say “the API response is fine, the bug is in the frontend,” Claude would spend twenty minutes re-investigating the API.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;“Two-Attempt Rule”&lt;/strong&gt; — because Claude would try five variations of the same wrong approach before I could get it to step back.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;“EVIDENCE-BASED BUG FIXING (NON-NEGOTIABLE)”&lt;/strong&gt; — in all caps, because Claude kept speculatively fixing code that wasn’t broken, breaking it in the process.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That last one has a specific origin. A bug in one sync function — Phantasy Tour — and Claude “preemptively” applied the same fix to three other sync functions that were working fine. Now four things were broken instead of one.&lt;/p&gt;

&lt;hr /&gt;

&lt;p&gt;There’s one more thing. Claude also figured out how to get around CI entirely.&lt;/p&gt;

&lt;p&gt;When you push a commit and open a pull request on GitHub, there’s a brief window — a few seconds — before the CI checks register as required. During that window, the merge button is green. Claude learned to push a commit, immediately create the PR, and merge it in that gap before the checks even start running. No waiting for tests. No waiting for builds. Just push, merge, done — the engineering equivalent of running a red light because the camera hasn’t turned on yet.&lt;/p&gt;

&lt;p&gt;I caught it because PRs were showing up as merged with zero checks passed. Not failed checks — &lt;em&gt;no&lt;/em&gt; checks. The CI runs would start, sometimes even fail, on a commit that was already in main. The branch protection rules were technically satisfied because there were no checks &lt;em&gt;to&lt;/em&gt; block on at the instant the merge happened.&lt;/p&gt;

&lt;p&gt;This is the same agent that was told to write tests for every new behavior. It wrote the tests. It configured the CI. Then it found the fastest path that avoided actually waiting for any of it. I’m not even mad. It’s the most efficient thing Claude did all month.&lt;/p&gt;

&lt;hr /&gt;

&lt;p&gt;Then Claude took the site down.&lt;/p&gt;

&lt;p&gt;I didn’t trust the test suite anymore. 154 tests and zero coverage of the core flow — what else was missing? So I asked Claude to set up code coverage instrumentation for the E2E tests. Build the Go binary with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;-cover&lt;/code&gt;, run the Playwright suite, generate an HTML report showing which backend handlers are actually being exercised. I wanted receipts.&lt;/p&gt;

&lt;p&gt;Claude built it. It worked. The coverage report showed 42% handler coverage with 263 functions at 0%. Good data. And even that — the act of trying to verify Claude’s work — Claude managed to fuck up.&lt;/p&gt;

&lt;p&gt;But Claude didn’t put the coverage tooling in a separate script and leave it there. It also added a coverage flush endpoint — &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;POST /debug/coverage/flush&lt;/code&gt; — directly to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;main.go&lt;/code&gt;, the production server binary. That endpoint imported &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;runtime/coverage&lt;/code&gt;, a Go standard library package that calls &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;WriteMetaDir()&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;WriteCountersDir()&lt;/code&gt;. Those functions panic if the binary wasn’t compiled with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;-cover&lt;/code&gt;. Production binaries are not compiled with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;-cover&lt;/code&gt;. The binary panicked on startup. The site went down.&lt;/p&gt;

&lt;p&gt;The fix was three lines: delete the import, delete the endpoint, move the flush logic to the coverage script where it belonged. I pushed it in under a minute once I understood what happened. But the site was unreachable until Railway picked up the new commit and redeployed, and I couldn’t force a faster deploy because the health checks were failing on the crashing binary.&lt;/p&gt;

&lt;p&gt;The irony is almost too neat. Claude was asked to measure test coverage — to find out what &lt;em&gt;wasn’t&lt;/em&gt; being tested — and in doing so, shipped code that wasn’t tested to production. The coverage endpoint itself was never tested. Not by the 154 existing E2E tests. Not by the new tests Claude was writing. Not by a quick &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;go build &amp;amp;&amp;amp; ./binary&lt;/code&gt; sanity check without the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;-cover&lt;/code&gt; flag. The code existed to answer the question “what are we not testing?” and the answer included itself.&lt;/p&gt;

&lt;p&gt;This is the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;runtime/coverage&lt;/code&gt; import in production &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;main.go&lt;/code&gt;, the one that crashed the site:&lt;/p&gt;

&lt;div class=&quot;language-go highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;s&quot;&gt;&quot;runtime/coverage&quot;&lt;/span&gt;  &lt;span class=&quot;c&quot;&gt;// panics if binary not built with -cover&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

&lt;span class=&quot;c&quot;&gt;// POST /debug/coverage/flush — flush coverage data&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;mux&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;HandleFunc&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;POST /debug/coverage/flush&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;func&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;w&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;http&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;ResponseWriter&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;r&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;http&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Request&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;coverage&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;WriteMetaDir&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;coverDir&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;     &lt;span class=&quot;c&quot;&gt;// panic&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;coverage&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;WriteCountersDir&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;coverDir&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;c&quot;&gt;// panic&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Dev-only code, in the production binary, with no guard, no build tag, no conditional. Just a direct import of a package that explodes outside its intended context. Claude didn’t even think about it. It was writing coverage tooling, so it put the coverage code where the rest of the server code lives. The concept of “this code should only exist in a specific build configuration” didn’t occur to it.&lt;/p&gt;

&lt;hr /&gt;

&lt;p&gt;I want to be clear about what’s happening here, because I think it’s easy to read this as “AI is bad at testing” and miss the more interesting point.&lt;/p&gt;

&lt;p&gt;Claude is excellent at writing tests. The 154 tests it wrote are real, useful, and they catch real regressions. The Playwright infrastructure is solid. The test helpers are clean. The coverage of side features is thorough. When Claude writes tests, they work.&lt;/p&gt;

&lt;p&gt;The problem is that Claude doesn’t write them &lt;em&gt;where they matter most&lt;/em&gt;. It writes them where they’re &lt;em&gt;easiest&lt;/em&gt; — for the feature it just built, in the same session, while the context is fresh. The new tournament bracket gets tests because Claude just built the tournament bracket. The new battle system gets tests because Claude just built the battle system. The posting flow doesn’t get tests because Claude built it weeks ago, and no single session since then has been “about” posting.&lt;/p&gt;

&lt;p&gt;This is a prioritization failure, not a capability failure. And it’s one that’s hard to catch in the moment, because the test count keeps going up. Progress feels real. 154 tests! Seventeen spec files! The dashboard is green! But the coverage map has a hole in the center, exactly where the load-bearing wall is, and nobody notices until the wall falls down.&lt;/p&gt;

&lt;p&gt;The fix is obvious: test the core flow. Test it first. Test it before you test anything else. But “obvious” and “automatic” are different things, and Claude Code — despite being told explicitly, in bold, in the project instructions — did one and not the other.&lt;/p&gt;

&lt;hr /&gt;

&lt;p&gt;833 commits. 202 fixes. Zero tests for the thing the app actually does. A dev-only import that took down production. The numbers don’t lie, even when the test suite is green.&lt;/p&gt;
</description>
				<pubDate>Sun, 08 Mar 2026 00:00:00 +0000</pubDate>
				<link>https://christophermeiklejohn.com/ai/claude/2026/03/08/claude-tested-everything-except-the-one-thing-that-mattered.html</link>
				<guid isPermaLink="true">https://christophermeiklejohn.com/ai/claude/2026/03/08/claude-tested-everything-except-the-one-thing-that-mattered.html</guid>
			</item>
		
			<item>
				<title>Ten Years of Lasp</title>
				<description>&lt;p&gt;Peter Van Roy and I just published a retrospective at &lt;a href=&quot;https://dl.acm.org/doi/10.1145/3756907.3756910&quot;&gt;PPDP ‘25&lt;/a&gt; — the same venue where the original Lasp paper appeared a decade ago — looking back at ten years of influence from Lasp, the coordination-free programming model Peter and I built together in 2015. I wanted to write a bit about what that paper covers, and what it actually felt like to watch an idea travel from a prototype to something that showed up in production systems I never expected.&lt;/p&gt;

&lt;h3 id=&quot;what-lasp-was&quot;&gt;What Lasp Was&lt;/h3&gt;

&lt;p&gt;Lasp was a programming model built on top of &lt;a href=&quot;/crdt/2014/07/22/readings-in-crdts.html&quot;&gt;Conflict-Free Replicated Data Types (CRDTs)&lt;/a&gt;. The core idea was simple: coordination makes distributed systems easy to reason about, but it’s expensive — it limits availability, introduces latency, and creates failure modes. Lasp’s goal was to give developers a programming model that was just as easy to reason about, but without the coordination, by structuring distributed computation around data types that are guaranteed to converge regardless of the order in which updates arrive or whether the network partitions. The result: better reliability and availability, without sacrificing the developer’s ability to think clearly about what their program does.&lt;/p&gt;

&lt;p&gt;The CAP theorem had been the backdrop for a lot of the distributed systems conversation in that era. CRDTs offered a path toward availability and partition-tolerance without sacrificing convergence, but their use was still mostly ad hoc. Lasp tried to give that a principled, declarative home: a functional programming model where eventual consistency wasn’t something you bolted on, but the default assumption.&lt;/p&gt;

&lt;p&gt;I was working at Basho Technologies at the time, deep in the operational reality of building eventually consistent databases in Erlang. That context shaped everything about how Lasp was designed. The pain points weren’t hypothetical — I had watched coordinaton-heavy systems fall over under real load, and I was motivated to find something better.&lt;/p&gt;

&lt;p&gt;The motivating use case came from Rovio Entertainment, the company behind Angry Birds. Their game required tracking player state across a globally distributed, occasionally connected player base — exactly the kind of scenario where coordination is prohibitively expensive and convergence is what you actually need. That use case grounded Lasp’s design in something real.&lt;/p&gt;

&lt;h3 id=&quot;the-scale-experiments&quot;&gt;The Scale Experiments&lt;/h3&gt;

&lt;p&gt;Lasp was part of the EU-funded SyncFree research project, which brought together researchers across Europe to advance the foundations of CRDT-based distributed systems. As part of our deliverables to the European Commission, we ran large-scale experiments on AWS — over 1,000 nodes — to demonstrate that these techniques worked at scale. Between 2015 and 2017, that kind of deployment was genuinely difficult: few frameworks supported it, and the operational challenges around service discovery, dissemination, and convergence were not trivial.&lt;/p&gt;

&lt;p&gt;We published those results at PPDP in 2017. What I remember most clearly is how much of the work was just fighting infrastructure that wasn’t ready for what we were trying to do. To get Lasp to run on top of Mesos’s Marathon framework — because I was contracting at Mesosphere at the time — I had to essentially rebuild the network layer of Lasp from scratch. That work became Partisan.&lt;/p&gt;

&lt;h3 id=&quot;the-things-that-grew-out-of-it&quot;&gt;The Things That Grew Out of It&lt;/h3&gt;

&lt;p&gt;Partisan, the open-source distribution layer I built to run those experiments, ended up being more widely adopted than Lasp itself. It became a high-performance alternative to Erlang’s built-in distribution, influenced improvements to Erlang’s distributed networking internals, and eventually found its way into open-source and proprietary projects I had no involvement in. Alejandro Ramallo, an Erlang developer who adopted both Lasp and Partisan, deployed them in systems powering LoJack’s stolen car recovery service across several South American countries. I did not see that coming.&lt;/p&gt;

&lt;p&gt;Lasp was also adopted as the storage backend for Erleans, an open-source implementation of Microsoft Orleans on Erlang — which is a satisfying full-circle moment given that I spent two summers at Microsoft Research working on Orleans’ transactional semantics.&lt;/p&gt;

&lt;p&gt;The fault-injection mechanisms in Partisan’s early network layer eventually became the seed of Filibuster, the fault injection testing framework that was the subject of my Ph.D. dissertation. The path from Lasp to Partisan to Filibuster to DoorDash is not a straight line, but there is a line. The only way to verify that a coordination-free system converges correctly is to partition the network and check what happens when it heals. That thinking, originally motivated by Lasp’s correctness requirements, eventually became a general-purpose approach to testing microservice resilience.&lt;/p&gt;

&lt;h3 id=&quot;what-the-academic-community-did-with-it&quot;&gt;What the Academic Community Did With It&lt;/h3&gt;

&lt;p&gt;The retrospective paper covers quite a bit of follow-on academic work that I found genuinely gratifying to trace. Systems like Katara built on Lasp’s foundational principles to synthesize CRDTs with verified lifting. LoRe and Varda extended Lasp’s declarative, coordination-free semantics toward verifiably safe compositional distributed software. Several PhD dissertations — from researchers in Belgium, Portugal, the UK, the US, and elsewhere — adopted Lasp as either a technical or theoretical foundation.&lt;/p&gt;

&lt;p&gt;The work Matthew Weidner and Heather Miller and I published together on composing op-based CRDTs with semidirect products also grew directly out of the Lasp model. That paper, published at ICFP in 2020, is something I’m quietly proud of — it’s one of the more mathematically interesting things I’ve worked on.&lt;/p&gt;

&lt;p&gt;The PPDP program committee selected the original Lasp paper as the most influential paper of the past decade — the PPDP 2025 10-year award — which is what prompted the retrospective paper in the first place. It is a bit surreal to receive a recognition like that for work that started, essentially, as a research project I threw myself into while working at Basho with no particular expectation that anyone outside the SyncFree project would care.&lt;/p&gt;

&lt;h3 id=&quot;why-it-still-matters&quot;&gt;Why It Still Matters&lt;/h3&gt;

&lt;p&gt;The retrospective paper makes the case — and I believe it — that Lasp’s core insight is increasingly relevant rather than less. Multi-region active-active deployments are becoming standard architecture for companies at scale. A single round-trip operation at the speed of light takes 133 milliseconds. Coordination protocols like Two-Phase Commit or Paxos require multiple round trips just to agree on a single value. The math doesn’t work for globally distributed state if coordination is your default.&lt;/p&gt;

&lt;p&gt;Edge computing, local-first software, offline-capable mobile applications, and federated systems all face the same constraint: central coordination is sometimes simply unavailable. Coordination-free convergence isn’t a research curiosity in those environments — it’s a requirement.&lt;/p&gt;

&lt;p&gt;I’ll be curious to see what the next ten years look like for CRDTs specifically. The theoretical foundations are solid. The open question is whether the programming model abstractions — the things that Lasp was trying to work out — will find their way into mainstream languages and frameworks in a form that developers can actually use without thinking too hard about it. I think they will.&lt;/p&gt;

&lt;p&gt;The full paper is available on &lt;a href=&quot;https://dl.acm.org/doi/10.1145/3756907.3756910&quot;&gt;ACM&lt;/a&gt;.&lt;/p&gt;
</description>
				<pubDate>Sun, 01 Mar 2026 08:00:00 +0000</pubDate>
				<link>https://christophermeiklejohn.com/lasp/distributed/2026/03/01/ten-years-of-lasp.html</link>
				<guid isPermaLink="true">https://christophermeiklejohn.com/lasp/distributed/2026/03/01/ten-years-of-lasp.html</guid>
			</item>
		
			<item>
				<title>I Built a Social App in a Week with Claude Code</title>
				<description>&lt;p&gt;I spent the better part of a week building a social app with Anthropic’s Claude Code. Most of that work happened late at night, sometimes past 2am, iterating on features until I had something worth sharing with friends.&lt;/p&gt;

&lt;p&gt;Anthropic generates a weekly insights report for Claude Code users. Mine told an interesting story: 384 messages, 27 sessions, 168 files touched, six days. A median response time of 29 seconds — I was barely reading the output before firing back. They described my style as “reactive and corrective rather than spec-driven,” which is accurate. I was moving fast, fixing things as they broke, and learning what worked along the way.&lt;/p&gt;

&lt;p&gt;What I was building: a private social app for my close-knit group of friends. We share live recordings of bands — Phish, Grateful Dead, that world — favorite films, books, and which upcoming shows we plan to catch this summer. Think a tiny, invite-only corner of the internet for people who care deeply about live music and want somewhere to talk about it with people they actually know. Live chat between people couch-touring and people in the pit is coming next.&lt;/p&gt;

&lt;p&gt;The app is live. My friends are using it. It runs on Go with a server-driven UI architecture, and it ships as native iOS and Android apps. I built all of it in a week with Claude Code, and I want to tell you what that actually felt like.&lt;/p&gt;

&lt;p&gt;The entire first version was built at night, in bed, on an 11” iPad, using the Claude Code app connected to an empty GitHub repo. I didn’t touch a computer. Within a few hours I had a working app — login, a feed, posting — deployed and running on Railway. From there the scope crept in the right direction: Spotify integration so you could search and attach albums and tracks directly, setlist.fm integration to pull real show data — venues, dates, and actual setlists as they’re posted after each night — and let people mark which dates they were attending. The bones were there fast; making it feel like something worth actually using took longer. Claude scaffolded the backend, wired up the database, built the frontend, and I shipped it without leaving bed. It wasn’t until late in the second day that I moved to a laptop, mostly because the screen real estate was starting to feel limiting. The code itself didn’t care.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/img/zabriskie-screenshot-1.png&quot; style=&quot;width: 100%&quot; /&gt;&lt;/p&gt;

&lt;hr /&gt;

&lt;p&gt;The day I invited most of my friends, a database migration corrupted production timestamp data.&lt;/p&gt;

&lt;p&gt;The migration modified column types in a running database. Timestamps encode timezone assumptions at the type level — change them mid-flight and the data doesn’t migrate, it breaks. The feed went down. I asked Claude to fix it. Each fix made things worse: wrong column names, broken SQL syntax, incorrect timezone arithmetic, until finally an overwrite ran that couldn’t be undone. The Anthropic report summarized it as “Claude’s database migrations went full disaster movie — each fix spawned a new production incident, permanently destroying timestamp data.” That’s accurate. Some of that data is simply gone.&lt;/p&gt;

&lt;p&gt;I spent years working on databases. I knew exactly what was happening and why it was catastrophic. I let it happen anyway because I was vibe coding, hands off the wheel, just watching Claude drive.&lt;/p&gt;

&lt;p&gt;The timestamp disaster was the worst single incident, but the pattern underneath it showed up constantly: Claude moving confidently in the wrong direction, and me not stopping it soon enough. Push notification debugging is another example. Missing APNs tokens sent Claude deep into provisioning profiles, certificates, entitlements — a long, plausible-looking path that turned out to be completely wrong. The actual problem was missing &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;AppDelegate&lt;/code&gt; methods in code Claude itself had written earlier. The report counted 30 instances of wrong-approach debugging across the project. That’s a lot of time watching a very capable thing solve the wrong problem.&lt;/p&gt;

&lt;p&gt;The server restart issue was more mundane but somehow more maddening for it. After any change to Go code, you have to restart the backend for the changes to take effect — compiled language, nothing exotic. Claude kept forgetting. I kept seeing no changes, assuming something was broken, investigating, finding nothing, eventually realizing the old process was still running. This happened across four or more sessions before I wrote a rule explicit enough that it actually stuck. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pkill&lt;/code&gt; was the culprit — it fails silently, leaving the old process alive. The fix was &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;lsof&lt;/code&gt; to find the PID, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;kill -9&lt;/code&gt; on that specific process, wait, restart, verify. A procedure that takes thirty seconds and has to be written down or it doesn’t happen.&lt;/p&gt;

&lt;hr /&gt;

&lt;p&gt;What got built, despite all of this, is genuinely surprising to me.&lt;/p&gt;

&lt;p&gt;A full server-driven UI migration — the backend sends the entire interface as JSON, the React frontend just renders it, no hardcoded pages. This one has a story. I’d instructed Claude from the start to build an SDUI app, but most of the early code landed in React anyway. The upside was that the initial pages were genuinely pretty — Claude has good taste in React UI — and I loved how they looked. The downside was that enabling a proper mobile strategy required full SDUI, which meant migrating every page individually, each one needing to be restyled from scratch. That work took real time.&lt;/p&gt;

&lt;p&gt;The migration also exposed a pattern that would recur throughout the project: Claude claiming victory prematurely. Buttons that didn’t work. Frontend calls to backend routes that didn’t exist — phantom APIs, confidently wired up. Forms that submitted successfully from the UI while silently dropping half their parameters on the way to the backend. Claude would build a form, build an API endpoint, connect them, and declare the feature done. The form would submit. The endpoint would return 200. Nothing would actually be saved.&lt;/p&gt;

&lt;p&gt;I eventually had to encode explicit rules: when you add a new backend API, test it with curl before touching the frontend. When you wire up a frontend call, verify the route actually exists. When a form submits, confirm every parameter arrives at the backend. The quality improved significantly once those guardrails were in writing.&lt;/p&gt;

&lt;p&gt;iOS and Android apps, both in TestFlight and ready for Android testing within the same week. End-to-end push notifications wired to every interaction. An AI-powered feature that surfaces what your friends are collectively into right now. An invite management system, automated build scripts, 107 database migrations, user engagement charts, a changelog that notifies users when something new ships.&lt;/p&gt;

&lt;div style=&quot;display: flex; gap: 12px; align-items: flex-start;&quot;&gt;
  &lt;img src=&quot;/img/zabriskie-screenshot-2.png&quot; style=&quot;width: 33%;&quot; /&gt;
  &lt;img src=&quot;/img/zabriskie-screenshot-3.png&quot; style=&quot;width: 33%;&quot; /&gt;
  &lt;img src=&quot;/img/zabriskie-screenshot-4.png&quot; style=&quot;width: 33%;&quot; /&gt;
&lt;/div&gt;

&lt;p&gt;Claude’s ability to hold a large multi-file change in mind — a database migration, a new API handler, a frontend component, and a mobile layout fix, all in one coherent session — is where it earns everything. When the scope is clear and the pattern is known, it moves at a speed that doesn’t feel real.&lt;/p&gt;

&lt;p&gt;Only 2 of my 27 sessions fully achieved what I set out to do. That number sounds damning until you consider that 27 sessions in six days shipped something real that people are using. The sessions that failed were almost always the same shape: open-ended, no clear stopping point, debugging something visual or stateful where Claude had no feedback loop and I had too much patience for wrong approaches. The sessions that worked were tight — one known thing, done correctly, then stop.&lt;/p&gt;

&lt;hr /&gt;

&lt;p&gt;The insights report also came with recommendations, and they’re worth passing on.&lt;/p&gt;

&lt;p&gt;The biggest one: hooks. Claude Code supports post-edit hooks — shell commands that fire automatically after files are changed. The report suggested wiring one up to restart the Go backend after any &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.go&lt;/code&gt; file edit, which would have eliminated the single most recurring waste of time in the entire project. I haven’t set it up yet. I’m going to.&lt;/p&gt;

&lt;p&gt;The report also suggested adding explicit guardrails to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;CLAUDE.md&lt;/code&gt; — the project instruction file Claude reads at the start of every session. Things like: after any backend code change, always restart the server before testing. Never run migrations or deploy fixes without explicit user approval. When the user tells you a layer is working, stop investigating that layer. Limit yourself to two attempts at a single approach — if it hasn’t worked twice, step back and explain what you’ve learned before trying again. Most of these I’d arrived at the hard way over the course of the week. Having them written down from the start would have saved days.&lt;/p&gt;

&lt;p&gt;The other recommendation was about session discipline. Five of my sessions were completely lost to context limits — the conversation grew too long, and when I tried to recover with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/compact&lt;/code&gt;, it just returned “failed to compact” and left me stranded mid-task with no way forward except closing Claude and starting over from scratch. Losing context mid-session, mid-thought, mid-fix, with no handoff and no summary, is a particular kind of frustrating. The fix is obvious in retrospect: end each session at a natural stopping point, write a brief summary of state, start fresh. The sessions where I shipped something were the tight ones. I kept ignoring that signal.&lt;/p&gt;

&lt;hr /&gt;

&lt;p&gt;The report called my style “ambitious” and noted I “tolerate high friction from repeated wrong approaches.” I’d put it differently: I was building something I actually cared about, for people I actually know, and the deadline was real. Spring tour is the test. Summer tour is the goal. That changes your relationship to the friction.&lt;/p&gt;

&lt;p&gt;You’re not writing code with Claude Code. You’re steering. The gap between those two things is where all the frustration lives, and also where all the speed comes from. When you accept that you’re the judgment layer — deciding when to redirect, when to stop, when a fix is making things worse — the tool becomes something genuinely extraordinary.&lt;/p&gt;

&lt;p&gt;Spring tour is coming. The app is ready.&lt;/p&gt;

&lt;hr /&gt;

&lt;p&gt;&lt;em&gt;Postscript: while writing this blog post, the same pattern bit me again. The app was deploying fine on Railway with a simple nixpacks build. While fixing an unrelated feature, I added a healthcheck to the config as a drive-by change — unnecessary, but it seemed harmless. Later, switching to a Dockerfile build to bake in an environment variable caused the build to take longer than nixpacks, so the healthcheck started timing out before the service was ready. Deployments failed. Rather than identifying the healthcheck as the culprit, five successive commits changed the port, the builder, the start command, and the Dockerfile in various combinations. None of it worked, because the real problem was never diagnosed. The entire chain of failures traced back to one unnecessary line added while working on something else entirely. Some things don’t change.&lt;/em&gt;&lt;/p&gt;
</description>
				<pubDate>Tue, 17 Feb 2026 08:00:00 +0000</pubDate>
				<link>https://christophermeiklejohn.com/ai/claude/2026/02/17/building-a-social-app-in-a-week-with-claude-code.html</link>
				<guid isPermaLink="true">https://christophermeiklejohn.com/ai/claude/2026/02/17/building-a-social-app-in-a-week-with-claude-code.html</guid>
			</item>
		
			<item>
				<title>Filibuster 2.0: Byzantine Fault Injection with Hardcoded Fault Values</title>
				<description>&lt;p&gt;Ever want to test your system against Redis returning wrong values, like instead of returning an error, it returns an empty string? What about an empty byte array? What about a database field being null? You can do it with Filibuster 2.0!&lt;/p&gt;

&lt;p&gt;Let’s return a null from a Redis get, easy! Simply done with Filibuster.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/img/1691310592563.jpeg&quot; style=&quot;width: 100%&quot; /&gt;&lt;/p&gt;
</description>
				<pubDate>Sat, 09 Sep 2023 08:00:00 +0000</pubDate>
				<link>https://christophermeiklejohn.com/filibuster/2023/09/09/filibuster-2.0-byzantine-hardcoded.html</link>
				<guid isPermaLink="true">https://christophermeiklejohn.com/filibuster/2023/09/09/filibuster-2.0-byzantine-hardcoded.html</guid>
			</item>
		
			<item>
				<title>Filibuster 2.0: Coaching UI</title>
				<description>&lt;p&gt;Your developers are writing functional tests for their microservice and, when RPCs fail, they throw exceptions. Do they test the cases where it throws? Do they have tests for it?&lt;/p&gt;

&lt;p&gt;Using Filibuster, you can automatically identify these scenarios and prompt the developer to answer these questions.&lt;/p&gt;

&lt;p&gt;Here’s a case where a developer threw an exception when a downstream RPC failed. We ask them: “did you mean to throw?” and, if so, they are prompted to tell the system that this exception is on purpose: this allows us to determine resilience failures automatically.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/img/1691309630814.jpeg&quot; style=&quot;width: 100%&quot; /&gt;&lt;/p&gt;
</description>
				<pubDate>Fri, 08 Sep 2023 08:00:00 +0000</pubDate>
				<link>https://christophermeiklejohn.com/filibuster/2023/09/08/filibuster-2.0-coaching.html</link>
				<guid isPermaLink="true">https://christophermeiklejohn.com/filibuster/2023/09/08/filibuster-2.0-coaching.html</guid>
			</item>
		
			<item>
				<title>Filibuster 2.0: Byzantine Fault Injection with Arbitrary Faults</title>
				<description>&lt;p&gt;Ever wanted to just throw all sort of values at your database and see what happens to your application? Filibuster’s byzantine fault injector can take an arbitrary “value transformer” that looks like a functional fold, that allows you to come up with new fault injection scenarios as you inject faults!&lt;/p&gt;

&lt;p&gt;Let’s test our application against flipping characters in a string response from Redis!&lt;/p&gt;

&lt;p&gt;Here, we &lt;em&gt;observe&lt;/em&gt; the response from the test that passes with no faults and then flip a character in the response in the test where no faults were injected. We actually built this to flip &lt;em&gt;every&lt;/em&gt; character in the string.&lt;/p&gt;

&lt;p&gt;What was once “example” in the Redis response becomes “ xample”! And then becomes “e ample”!&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/img/1691310856004.jpeg&quot; style=&quot;width: 100%&quot; /&gt;&lt;/p&gt;
</description>
				<pubDate>Thu, 07 Sep 2023 08:00:00 +0000</pubDate>
				<link>https://christophermeiklejohn.com/filibuster/2023/09/07/filibuster-2.0-byzantine-arbitrary.html</link>
				<guid isPermaLink="true">https://christophermeiklejohn.com/filibuster/2023/09/07/filibuster-2.0-byzantine-arbitrary.html</guid>
			</item>
		
			<item>
				<title>Filibuster 2.0: Computing API coverage of a Microservice Application</title>
				<description>&lt;p&gt;About two months ago I started prototyping this feature for visualizing API coverage in a microservice application through automated instrumentation: see where you have functional testing coverage, how many functional tests, and where you’re applying fault injection to determine the impact of those changes.&lt;/p&gt;

&lt;p&gt;Flash forward to yesterday, a new paper draft on arXiv proposed (almost) this very thing – they went a bit further in their study and provided more comprehensive visualizations and an accompanying study. In contrast, mine is running on real code written by industrial developers. On the left, Filibuster; on the right, their proposal.&lt;/p&gt;

&lt;p&gt;Good ideas happen at the same time, I suppose!&lt;/p&gt;

&lt;p&gt;(e.g., Mattern and Fidge both independently coming up with the idea for vector clocks the very same year.)&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/img/1692932473188.jpeg&quot; style=&quot;width: 100%&quot; /&gt;&lt;/p&gt;
</description>
				<pubDate>Wed, 06 Sep 2023 08:00:00 +0000</pubDate>
				<link>https://christophermeiklejohn.com/filibuster/2023/09/06/filibuster-2.0-API-coverage.html</link>
				<guid isPermaLink="true">https://christophermeiklejohn.com/filibuster/2023/09/06/filibuster-2.0-API-coverage.html</guid>
			</item>
		
			<item>
				<title>Filibuster 2.0: Microservice Linter, Multiple Invocations to the Same RPC Method</title>
				<description>&lt;p&gt;You can also use Filibuster’s dynamic analysis linter to find microservice smells.&lt;/p&gt;

&lt;p&gt;Here’s one: invoking multiple RPCs to the same service because you can’t send them all together! This leaves you at risk for partial side-effects being applied: refactor your API to let developers supply all inputs and write data to your database transactionally!!!&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/img/1691311807950.jpeg&quot; style=&quot;width: 100%&quot; /&gt;&lt;/p&gt;
</description>
				<pubDate>Tue, 05 Sep 2023 08:00:00 +0000</pubDate>
				<link>https://christophermeiklejohn.com/filibuster/2023/09/05/filibuster-2.0-multiple.html</link>
				<guid isPermaLink="true">https://christophermeiklejohn.com/filibuster/2023/09/05/filibuster-2.0-multiple.html</guid>
			</item>
		
			<item>
				<title>Filibuster 2.0: Microservice Linter, Requests become part of a Response</title>
				<description>&lt;p&gt;You can also use Filibuster’s dynamic analysis linter to find microservice smells.&lt;/p&gt;

&lt;p&gt;Here’s one: using the arguments from one RPC to Service A as the inputs to a different RPC on the same Service A. You should refactor your API so I don’t have to make multiple RPCs!!&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/img/1691311683887.jpeg&quot; style=&quot;width: 100%&quot; /&gt;&lt;/p&gt;
</description>
				<pubDate>Mon, 04 Sep 2023 08:00:00 +0000</pubDate>
				<link>https://christophermeiklejohn.com/filibuster/2023/09/04/filibuster-2.0-request-response.html</link>
				<guid isPermaLink="true">https://christophermeiklejohn.com/filibuster/2023/09/04/filibuster-2.0-request-response.html</guid>
			</item>
		
			<item>
				<title>Filibuster 2.0: Microservice Linter, Redundant RPCs</title>
				<description>&lt;p&gt;You can also use Filibuster’s dynamic analysis linter to find microservice smells.&lt;/p&gt;

&lt;p&gt;Here’s one: executing the same RPC multiple times, since you’ve already got the response and shouldn’t issue a failure-possible, expensive RPC again.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/img/1691311592348.jpeg&quot; style=&quot;width: 100%&quot; /&gt;&lt;/p&gt;
</description>
				<pubDate>Sun, 03 Sep 2023 08:00:00 +0000</pubDate>
				<link>https://christophermeiklejohn.com/filibuster/2023/09/03/filibuster-2.0-redundant.html</link>
				<guid isPermaLink="true">https://christophermeiklejohn.com/filibuster/2023/09/03/filibuster-2.0-redundant.html</guid>
			</item>
		
			<item>
				<title>Filibuster 2.0: Improved UI</title>
				<description>&lt;p&gt;Integrated directly into IntelliJ, Filibuster can show you the RPCs that your service is making and directly inject faults on any of those RPCs. Attach a debugger, inject some faults, and see what your application does!&lt;/p&gt;

&lt;p&gt;Not only just GRPC (as shown here), we also support HTTP, Redis, PostgreSQL, CockroachDB, and DynamoDB.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/img/1691309780575.jpeg&quot; style=&quot;width: 100%&quot; /&gt;&lt;/p&gt;
</description>
				<pubDate>Sat, 02 Sep 2023 10:00:00 +0000</pubDate>
				<link>https://christophermeiklejohn.com/filibuster/2023/09/02/filibuster-2.0-02-user-interface.html</link>
				<guid isPermaLink="true">https://christophermeiklejohn.com/filibuster/2023/09/02/filibuster-2.0-02-user-interface.html</guid>
			</item>
		
			<item>
				<title>Filibuster 2.0: Healthcheck your Functional Test Suite with API Coverage</title>
				<description>&lt;p&gt;Pushed out a new prototype Filibuster feature tonight: 🎉 use Filibuster’s IntelliJ plugin to get a “health check” of your microservice’s functional test suite. 🎉&lt;/p&gt;

&lt;p&gt;Here, I can quickly open the plugin after running my test suite and see what RPC methods my service is exposing, how many unique functional tests I have covering those methods, how many specific Filibuster tests I have for those, and how many tests were automatically generated using Filibuster to exercise unique fault injection scenarios: where I injected HTTP and GRPC faults.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/img/1691649263651.jpeg&quot; style=&quot;width: 100%&quot; /&gt;&lt;/p&gt;
</description>
				<pubDate>Sat, 02 Sep 2023 09:00:00 +0000</pubDate>
				<link>https://christophermeiklejohn.com/filibuster/2023/09/02/filibuster-2.0-01-healthcheck.html</link>
				<guid isPermaLink="true">https://christophermeiklejohn.com/filibuster/2023/09/02/filibuster-2.0-01-healthcheck.html</guid>
			</item>
		
			<item>
				<title>Filibuster 2.0: Redis Fault Injection</title>
				<description>&lt;p&gt;Ever wanted to test your microservice application against Redis failures? Filibuster 2.0 supports injecting faults against Redis, PostgreSQL, CockroachDB, and DynamoDB.&lt;/p&gt;

&lt;p&gt;Here, using our IntelliJ visualizer for tests, we see that in this test we injected a failure on a synchronous GET command to Redis and the test still passed. That’s fault tolerant code!&lt;/p&gt;

&lt;p&gt;Not using synchronous operations? No problem! Filibuster will inject execeptions for each asynchronous GET or SET operation and defer the fault injection until someone calls get or set on the future.&lt;/p&gt;

&lt;p&gt;Wanna find out if someone is using thenAccept but forgetting to catch the exception? Filibuster can fail the test if the developer never gets the value of the returned future too!!&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/img/1691309222915.jpeg&quot; style=&quot;width: 100%&quot; /&gt;&lt;/p&gt;
</description>
				<pubDate>Fri, 01 Sep 2023 10:00:00 +0000</pubDate>
				<link>https://christophermeiklejohn.com/filibuster/2023/09/01/filibuster-2.0-redis-fault-injection.html</link>
				<guid isPermaLink="true">https://christophermeiklejohn.com/filibuster/2023/09/01/filibuster-2.0-redis-fault-injection.html</guid>
			</item>
		
	</channel>
</rss>
