<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/">
    <channel>
        <title>henrypye</title>
        <link>https://paragraph.com/@henrypye</link>
        <description>undefined</description>
        <lastBuildDate>Sat, 03 Oct 2026 05:00:42 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <language>en</language>
        <image>
            <title>henrypye</title>
            <url>https://storage.googleapis.com/papyrus_images/229bce5aa4f2690292b2746a69629742ad523751b42b7ebe6c80f234bd1a4d54.jpg</url>
            <link>https://paragraph.com/@henrypye</link>
        </image>
        <copyright>All rights reserved</copyright>
        <item>
            <title><![CDATA[Dev Diary #21]]></title>
            <link>https://paragraph.com/@henrypye/dev-diary-21</link>
            <guid>BWJ1B67K4J6wawA3h53F</guid>
            <pubDate>Thu, 21 May 2026 01:43:20 GMT</pubDate>
            <description><![CDATA[Extracted two coupled tools out of dotfiles into standalone repos and unified their state under a single TICKETS_DIR.]]></description>
            <content:encoded><![CDATA[<p>The dotfiles repo had two things tangled together that shouldn't have been: lane orchestration for worktrees, and a ticket TUI. Both started as scripts under `scripts/` and grew until they had their own CLAUDE.md files inside a repo that was supposed to be about shell config. So I spent the day cutting them out.

`wt-lanes` came out first. Moved `bin/wt`, `bin/wt-gc`, `bin/ralph-bootstrap`, and the state-write hooks into a standalone repo, tagged v0.1.0. The dotfiles side now just symlinks against whatever's installed. The agent-board tmux script was the trickiest part — it had been labeling lanes by inspecting the cockpit window title, which broke the moment a nested lane spawned its own children. Switched it to read from a sentinel file the `wt` script writes on entry. Cleaner, but I'm not thrilled that the sentinel is plain text in `/tmp` — if two lanes race on cleanup the state column flickers.

`tix` came out second and was the bigger surgery. It had a bundled sync module that knew about a specific external ticket system, which is exactly the kind of coupling that kills a tool's reusability. Ripped it out and replaced with a `TIX_PRELOAD_HOOK` env var — point it at a script, the script populates the local tree before the TUI opens, tix doesn't care what you ran. Published to PyPI as v0.1.0 with badges, a CHANGELOG, and a schema doc.

Then the part I actually wanted: `$TICKETS_DIR`. Every project gets a tree under `~/.claude/tickets/<project>/`, and `tix <project>` resolves against that. `wt` honors the same env var so a worktree lane and the ticket TUI agree on where state lives. This is the piece that's been bugging me for weeks — tickets scattered across repo-local `.tickets/` dirs meant grep was useless across projects.

Also shipped a small Rust thing, `monkeytype-tui`, mostly to have a low-stakes Rust project sitting on my disk that isn't a toy. CI is green, README is honest about what it does.

Still unresolved: the git-watch CI column reservation fix works but I don't trust it on rows that flip merged→reopened. Need to actually reproduce that case.</p>]]></content:encoded>
            <author>henrypye@newsletter.paragraph.com (henry)</author>
            <category>repo extraction</category>
            <category>tool decoupling</category>
            <category>env var configuration</category>
            <category>tmux state tracking</category>
            <category>pypi release</category>
            <enclosure url="https://storage.googleapis.com/papyrus_images/03bd294f35152939715510a5d15644854df83d32fe4789f3690c9c3de4cbe85b.png" length="0" type="image/png"/>
        </item>
        <item>
            <title><![CDATA[Dev Diary #20]]></title>
            <link>https://paragraph.com/@henrypye/dev-diary-20</link>
            <guid>XRs3LWE8GJQJzwFe3Hbn</guid>
            <pubDate>Mon, 18 May 2026 00:57:38 GMT</pubDate>
            <description><![CDATA[Killed duplicate-dispatch bugs in two services by replacing crude cooldowns with idempotent acks and proper redeliver...]]></description>
            <content:encoded><![CDATA[<p>The flashcastr webhook started double-firing replies whenever the inbound payload arrived twice, which it does, often. The 6h per-author cooldown I'd written as a crude rate limiter was the wrong shape — it punished legitimate back-and-forth and did nothing to stop duplicate dispatches from the same payload. Ripped it out, replaced with an inbound rate limit keyed on the mention itself, plus a seen-ack LIKE on the cast so the webhook layer has an idempotent signal before `conversational-reply` ever runs. The threading was wrong too: replies were posting as top-level casts instead of actual Farcaster replies, which made the bot look like it was shouting into the void rather than talking to anyone. Two migrations landed alongside — `0009_add_mentions`, `0010_add_corrections` — because the self-correction path needs durable state, not in-memory hope.

Over in agent-blog the publish handlers were not idempotent under RabbitMQ redelivery. Same class of bug, different surface: a retry would re-publish, and I'd find out via duplicate output. Fixed with a dedupe check before the side effect, tests in `publish-handlers.spec` covering the redelivery case explicitly.

The dotfiles churn was mostly housekeeping that piled up: `g checkout` now `cd`s into the right worktree when the branch lives elsewhere, `wt` mandates a monitor pane for every lane so I stop losing track of background work, and the `grid-4x2` tmux script was only re-tiling the first split in the loop — classic off-by-one, embarrassing once spotted. Also scrubbed PII and org references across the tree before I forgot which files still had them.

Universal Limbs got a from-scratch site wired into Sanity so the founder can edit copy without me. Vercel deploy config, OG images, real socials. The Sanity embed inside a React app is a bit awkward — the studio route sits next to the public pages and the auth boundary is fuzzier than I'd like.

Still open: the seen-ack LIKE relies on Neynar webhook ordering I haven't fully stress-tested, and the correction-intent flow can probably be tricked by a malicious reply chain. Need to write adversarial fixtures next.</p>]]></content:encoded>
            <author>henrypye@newsletter.paragraph.com (henry)</author>
            <category>idempotent handlers</category>
            <category>webhook deduplication</category>
            <category>rate limiting</category>
            <category>reply threading</category>
            <category>schema migrations</category>
            <enclosure url="https://storage.googleapis.com/papyrus_images/c9c9c3ca9a37b604296a2e0a970d16e38f025197a1e27d714eeb639330820a71.png" length="0" type="image/png"/>
        </item>
        <item>
            <title><![CDATA[Dev Diary #19]]></title>
            <link>https://paragraph.com/@henrypye/dev-diary-19</link>
            <guid>3rZW24e5HPasBOCIejvq</guid>
            <pubDate>Thu, 14 May 2026 13:57:04 GMT</pubDate>
            <description><![CDATA[Spent the morning untangling tmux session names from ticket context. The wt command now pulls the ticket ID directly instead of guessing from branch names, which saves a step when jumping between worktrees. Ticket #376 had been sitting there waiting for cleanup — Loki was over-instrumented anyway, pulling logs that nobody reads. The bug-triager didn't actually need it, just added noise to the deployment. Then I hit a wall with /ticket-pickup. It was stopping after the plan phase instead of sp...]]></description>
            <content:encoded><![CDATA[<p>Spent the morning untangling tmux session names from ticket context. The <code>wt</code> command now pulls the ticket ID directly instead of guessing from branch names, which saves a step when jumping between worktrees. Ticket <code>#376</code> had been sitting there waiting for cleanup — Loki was over-instrumented anyway, pulling logs that nobody reads. The bug-triager didn't actually need it, just added noise to the deployment.</p><p>Then I hit a wall with <code>/ticket-pickup</code>. It was stopping after the plan phase instead of spawning the worktree lane. Felt stupid the second I saw it — the command had all the right pieces, but the execution flow was backwards. Fixed it to actually create the lane after planning. That's the kind of thing that burns time because everything <em>looks</em> right until you trace through what actually runs.</p><p>The bigger refactor was consolidating the agent-board registry system. Had three different places storing session state, two of them barely used. Merged them into a single session registry that the cockpit can query, and pushed worktree-only lanes to use that instead of the old ad-hoc naming. This cut about 40 lines of conditional logic from <code>agent-board.sh</code>, but now I'm worried about the edge case where a session gets registered but the worktree never actually creates. Haven't hit it yet, but the cleanup path isn't obvious if it does.</p><p>Also burned time fixing shell script permissions and consolidating install logic across <code>install.sh</code>, <code>install-mac.sh</code>, and the symlink rewiring. The dotfiles repo has grown into a mess of overlapping responsibilities — hooks, tmux configs, zsh setup, Claude agent definitions all living in the same tree. Works fine until it doesn't, and then you're debugging why a symlink didn't resolve or a script didn't execute.</p><p>Split the Slack TLDR view into tabs now. Alert channels ring the bell, monitor channels are silent. Added a test channel to the config to keep alerts from going dark when nobody's watching. Arrow keys tab between views, which is cleaner than the old modal switching.</p><p>Still need to wire up the <code>/scope</code> command to actually use conversation context instead of just dumping raw ticket details. And the ticket-pickup type detection is basic — it just looks at keywords in the title. That'll probably break on something weird.</p>]]></content:encoded>
            <author>henrypye@newsletter.paragraph.com (henry)</author>
            <category>tmux session management</category>
            <category>state registry patterns</category>
            <category>shell script consolidation</category>
            <category>cli workflow automation</category>
            <category>configuration cleanup</category>
            <enclosure url="https://storage.googleapis.com/papyrus_images/c65eba13ab65ffd415c7ae865957a62f00c3349f5e3a256092c0d2f34fb8f1c8.png" length="0" type="image/png"/>
        </item>
        <item>
            <title><![CDATA[Invader Weekly Roundup #11]]></title>
            <link>https://paragraph.com/@henrypye/invader-weekly-roundup-11</link>
            <guid>ksIdqlYA0MB6NkJd9RjN</guid>
            <pubDate>Thu, 14 May 2026 02:46:06 GMT</pubDate>
            <description><![CDATA[Paris closes a Space Invaders sub-series with PA_1569 and PA_1570, while London, NYC, and Fontainebleau lose pieces.]]></description>
            <content:encoded><![CDATA[<h2 id="h-invader-weekly-roundup" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">Invader Weekly Roundup</h2><p><strong>This week:</strong> Paris closes out a Space Invaders sub-series with two fresh additions (PA_1569 and PA_1570), while London, New York, and Fontainebleau take hits with three destructions logged in as many days. A quieter week on the press front, but the city-by-city pulse tells its own story.</p><h3 id="h-paris-caps-a-series-and-the-rest-of-the-map-takes-a-beating" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">Paris caps a series, and the rest of the map takes a beating</h3><p>The headline news this week comes out of Paris, where Invader added PA_1569 and PA_1570 — explicitly flagged in the event feed as &quot;les deux derniers SI parisiens,&quot; the last two pieces in a Parisian Space Invaders sub-series. For a city that already dominates the global map with 48 events tracked (nearly triple any other city), capping a series like this feels less like routine maintenance and more like a punctuation mark. Hunters in the 11th — and wherever PA_1569 and PA_1570 actually landed — now have two fresh targets to chase before someone else gets the first flash. Paris also saw a status update on PA_1275, the kind of housekeeping note that usually signals damage, repair, or a change in visibility worth checking in person.</p><p>The same week, however, was brutal elsewhere. London lost LDN_129 to degradation on the 7th, and the 8th brought a double blow with destructions of FTBL_36 in Fontainebleau and NY_176 in New York. Three cities, three pieces gone or compromised inside 48 hours. It&apos;s a sharp reminder of what makes this whole project so emotionally charged: every mosaic out there is borrowed time. The ceramic-tile-as-permanent-medium idea is a beautiful one, but the street always gets the final say. If you&apos;ve got an un-flashed invader sitting on a &quot;next trip&quot; list, this is your nudge.</p><h3 id="h-the-shape-of-the-map-right-now" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">The shape of the map right now</h3><p>Zooming out from the week&apos;s events, the broader activity map continues to reinforce what regulars already feel intuitively: Paris is the heart, Europe is the body, and the rest of the world is a constellation of passionate outposts. Paris alone accounts for roughly 35% of documented global events, with Bern and London tied at 11 events apiece as secondary European hubs. Across the Atlantic, Los Angeles (13) and New York (9) anchor North America, while São Paulo (12) holds down South America almost single-handedly and Hong Kong (3) flies the flag for an Asia-Pacific scene that&apos;s been quieter than its history suggests. This week&apos;s destructions hitting London, NYC, and Fontainebleau all land squarely in those high-activity zones — which is exactly where you&apos;d expect attrition to bite hardest, since more pieces and more foot traffic mean more wear, more cleanup crews, and more opportunistic removals.</p><h3 id="h-worth-a-click" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">Worth a click</h3><p>Curated sources were largely static this week, but the open-web surfaced a piece worth bookmarking for anyone deep in the lore: GraffitiStreet&apos;s coverage of Invader&apos;s nineteenth London invasion wave, <a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://www.graffitistreet.com/invaders-nineteenth-wave-of-london-invasions-unveiling-twelve-new-space-invader-locations/">here</a>, is a useful companion read given that London just lost LDN_129 — context for how the city&apos;s stock has been built up wave by wave, even as individual pieces fall. MyArtBroker&apos;s perpetual guide to <a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://www.myartbroker.com/artist-invader/guides/top-10-places-to-find-an-invader-original">the top 10 places to find an Invader original</a> also resurfaced in this week&apos;s deltas, which, taken together with the Paris series-cap, makes for a fitting week to think about the geography of this whole project: where it concentrates, where it thins out, and where it&apos;s quietly disappearing.</p>]]></content:encoded>
            <author>henrypye@newsletter.paragraph.com (henry)</author>
            <category>invader</category>
            <category>street-art</category>
            <category>paris caps a series, and the rest of the map takes a beating</category>
            <category>the shape of the map right now</category>
            <category>worth a click</category>
            <enclosure url="https://storage.googleapis.com/papyrus_images/f1fa0972862f4019fa60b996ce86d006f4a550933db61fea42ba06e44d378b70.png" length="0" type="image/png"/>
        </item>
        <item>
            <title><![CDATA[Invader Weekly Roundup #10]]></title>
            <link>https://paragraph.com/@henrypye/invader-weekly-roundup-10</link>
            <guid>FYu9R8Xfn1vDCq5aSavx</guid>
            <pubDate>Thu, 14 May 2026 02:45:56 GMT</pubDate>
            <description><![CDATA[Paris closes a wave with PA_1569 and PA_1570 as Fontainebleau, New York, and London each lose a piece this week.]]></description>
            <content:encoded><![CDATA[<h2 id="h-invader-weekly-roundup" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">Invader Weekly Roundup</h2><p><strong>This week:</strong> Paris closes out its latest invasion wave with two fresh mosaics (PA_1569 and PA_1570), while hunters mourn losses in Fontainebleau, New York, and London. The street keeps giving, and the street keeps taking.</p><h3 id="h-paris-caps-off-a-wave" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">Paris Caps Off a Wave</h3><p>The big news this week comes straight from the capital: the spotter feed confirms the addition of PA_1569 and PA_1570, described as the last two Space Invaders of the current Parisian series. There&apos;s a real sense of occasion to a moment like this — the closing punctuation on a wave that hunters have been chasing piece by piece. Paris already dominates the global map with roughly three times the activity of any other city, and these two new tiles push that lead even further. If you&apos;re planning a Paris trip in the coming weeks, your checklist just got two entries longer. Meanwhile, the same feed logged a status update on PA_1275, a small reminder that even the older pieces are living, breathing things in the database — conditions change, and the catalog moves with them.</p><h3 id="h-three-cities-lose-a-piece" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">Three Cities Lose a Piece</h3><p>It wasn&apos;t a clean week. Fontainebleau lost FTBL_36, New York lost NY_176, and London saw LDN_129 degraded — three separate strikes across three countries in a matter of days. None of these are surprising in the abstract; ceramic on a street wall is always negotiating with weather, construction crews, taggers, and the occasional collector with a chisel. But each loss lands a little harder when you&apos;ve flashed the piece yourself, or planned to. NY_176 in particular feels notable given New York&apos;s relatively modest count of nine documented events on the global map — every Manhattan or Brooklyn loss thins the herd more than a Parisian one does. For anyone with a trip to London or Fontainebleau on the calendar, it&apos;s worth checking your local maps before you walk a route built around a piece that&apos;s no longer fully there.</p><h3 id="h-the-wider-conversation" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">The Wider Conversation</h3><p>On the press and fan-source side, the curated feeds at MyArtBroker (both the <a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://www.myartbroker.com/artist-invader">artist page</a> and the evergreen <a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://www.myartbroker.com/artist-invader/guides/top-10-places-to-find-an-invader-original">Top 10 Places to Find an Invader Original guide</a>) refreshed this week, alongside the <a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://www.instagram.com/invaderwashere/">official Invader Instagram</a> and the ever-essential <a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://www.invader-spotter.art/news.php">invader-spotter news page</a>. The MyArtBroker guide remains a solid primer for anyone new to the hunt — it leans heavily on Rue Monge in the 5th Arrondissement as a pilgrimage spot, which feels especially fitting in a week when Paris is the story. Open-web crawls also resurfaced <a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://www.graffitistreet.com/invaders-nineteenth-wave-of-london-invasions-unveiling-twelve-new-space-invader-locations/">GraffitiStreet&apos;s coverage of the nineteenth London wave</a> and <a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://news.artnet.com/art-world/street-artist-invader-4000-2226676">Artnet&apos;s piece marking Invader passing 4,000 mosaics</a> — useful context as the project keeps barreling past its own milestones.</p><h3 id="h-the-rhythm-of-the-game" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">The Rhythm of the Game</h3><p>Two added, three lost or degraded, one status update — a net-negative week on paper, but that&apos;s the deal we signed up for. The pieces aren&apos;t supposed to last forever; they&apos;re supposed to be found, photographed, argued about, and eventually replaced by something new on a wall somewhere across town. PA_1569 and PA_1570 are out there right now waiting for their first flashes. Go look up.</p>]]></content:encoded>
            <author>henrypye@newsletter.paragraph.com (henry)</author>
            <category>invader</category>
            <category>street-art</category>
            <category>paris caps off a wave</category>
            <category>three cities lose a piece</category>
            <category>the wider conversation</category>
            <enclosure url="https://storage.googleapis.com/papyrus_images/f1fa0972862f4019fa60b996ce86d006f4a550933db61fea42ba06e44d378b70.png" length="0" type="image/png"/>
        </item>
        <item>
            <title><![CDATA[Dev Diary #18]]></title>
            <link>https://paragraph.com/@henrypye/dev-diary-18</link>
            <guid>cBv5YphJsOmrywQ4wSFR</guid>
            <pubDate>Tue, 12 May 2026 06:14:24 GMT</pubDate>
            <description><![CDATA[Exposed news digest via HTTP and reworked status displays for clearer momentum tracking.]]></description>
            <content:encoded><![CDATA[<p>Spent the morning wiring up a proper HTTP endpoint for the news digest. The agent was tucked behind a worker only — no way to pull fresh data on demand. Now <code>/digest/latest</code> lives on the server, and the test suite caught immediately that I&apos;d wired the response shape wrong. The payload needs <code>published_at</code> as milliseconds, not an ISO string. Built the handler in <code>http-server.ts</code>, added three test cases covering the happy path and two failure modes. One rough edge: the endpoint doesn&apos;t validate that the digest actually exists before returning 200, so if the worker hasn&apos;t run yet, you get an empty object. That&apos;s fine for now since the client knows to handle it, but it&apos;ll bite us later when someone expects the endpoint to fail cleanly.</p><p>Then pivoted to the status display — two separate changes to keep the agent board readable. Dropped the AGE and PORT columns from the tmux board because they were noise; contexts that don&apos;t have any work output now get suppressed entirely instead of printing blank lines. Keeps the eye focused on what&apos;s actually running.</p><p>The pacing meter got more interesting. Was showing a simple daily quota percentage, but that doesn&apos;t say much about momentum. Rewrote it to track a 7-day delta — how many stints completed in the rolling window versus the target. Then converted that to a ratio that shows whether you&apos;re on pace, ahead, or behind. It&apos;s a single number (<code>0.87</code>) that means more than a percentage. The shell script&apos;s ugly now because I&apos;m computing this in <code>awk</code> instead of delegating to the actual service, but it was faster to prototype here.</p><p>What&apos;s unclear: whether the HTTP endpoint should be read-only or if we eventually need to trigger digest regeneration from the client. Right now it&apos;s just fetching whatever the background worker cached. Also haven&apos;t decided if the pacing ratio should factor in the <em>type</em> of work or treat all stints equally. Some days you need deep focus, some days you&apos;re in meetings. The math is agnostic but the meaning shifts.</p>]]></content:encoded>
            <author>henrypye@newsletter.paragraph.com (henry)</author>
            <category>http endpoint design</category>
            <category>response shape validation</category>
            <category>state visibility</category>
            <category>temporal metrics</category>
            <enclosure url="https://storage.googleapis.com/papyrus_images/5842c93ed36256954285edccfd72b8249d1893603ec7061c204a4684c757a1f0.png" length="0" type="image/png"/>
        </item>
        <item>
            <title><![CDATA[Invader Weekly Roundup #9]]></title>
            <link>https://paragraph.com/@henrypye/invader-weekly-roundup-9</link>
            <guid>OKULGziHRyhOwTJ4wx2a</guid>
            <pubDate>Sat, 09 May 2026 04:03:15 GMT</pubDate>
            <description><![CDATA[Paris caps its latest wave with PA_1569 and PA_1570, while four destructions across NY, London, Melbourne, and Paris ...]]></description>
            <content:encoded><![CDATA[<h2 id="h-invader-weekly-roundup" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">Invader Weekly Roundup</h2><p><strong>This week:</strong> Paris closed the chapter on its current invasion wave with the arrival of PA_1569 and PA_1570, while four mosaics fell to destruction across New York, London, Melbourne, and Paris itself. London also saw LDN_178 quietly reactivated, a small win against a week of attrition.</p><h3 id="h-two-final-parisians-and-a-week-of-losses" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">Two Final Parisians, and a Week of Losses</h3><p>The headline this week is unmistakably Parisian: on the 7th, PA_1569 and PA_1570 were added — flagged in the spotter feed as &quot;les deux derniers SI parisiens,&quot; the closing pair of the current Paris sequence. For a city that already accounts for roughly a third of global activity on the map, watching the numbering tick up to 1,570 is the kind of milestone that puts the scale of this project into sharp relief. Paris has been the gravitational center of Invader&apos;s work since the 1990s, and rounding out another batch is always a moment worth pausing on, especially for hunters who&apos;ll be plotting routes through the arrondissements this spring.</p><p>The other side of the ledger was harsher. Four destructions in a single week — PA_1223 in Paris on the 3rd, MLB_19 in Melbourne on the 4th, LDN_129 in London on the 7th (logged as a dégradation rather than a clean destruction, but battered all the same), and NY_176 in New York on the 8th — span four cities and three continents. That&apos;s the unsentimental rhythm of street art: ceramic is durable, but cities are not gentle. Anyone within range of NY_176 or PA_1223 who hasn&apos;t flashed them yet can consider those particular squares of the gallery permanently closed. One quiet bright spot: LDN_178 was reactivated on the 4th after a status update, a reminder that the spotter community&apos;s vigilance occasionally rescues a piece from the &quot;lost&quot; column.</p><h3 id="h-a-city-by-city-pulse" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">A City-by-City Pulse</h3><p>The week&apos;s events map cleanly onto the broader global picture. Paris leads all activity on the world map with 48 logged events, and this week alone it accounted for two of the three additions and one destruction — fully half the action. London (11 events globally) contributed both a degradation and a reactivation, a microcosm of how mature invasion zones tend to behave: lots of churn, with pieces flickering between active, damaged, and restored states. New York (9 events) and Melbourne round out the geographic spread. For hunters chasing city completion, weeks like this are a useful nudge to prioritize the older, more vulnerable IDs before weather, construction, or collectors get to them first.</p><h3 id="h-from-the-wider-web" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">From the Wider Web</h3><p>On the curated-press side, MyArtBroker continues to maintain its Invader artist hub and its guide to the top ten places to find an Invader original (<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://www.myartbroker.com/artist-invader">https://www.myartbroker.com/artist-invader</a> and <a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://www.myartbroker.com/artist-invader/guides/top-10-places-to-find-an-invader-original">https://www.myartbroker.com/artist-invader/guides/top-10-places-to-find-an-invader-original</a>) — both worth a bookmark if you&apos;re newer to the secondary market or planning a hunting trip around known invasion zones. The invader-spotter.art news feed (<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://www.invader-spotter.art/news.php">https://www.invader-spotter.art/news.php</a>) remains, as ever, the single most reliable pulse-check for additions, destructions, and status changes; this week&apos;s events were all sourced there, and it&apos;s the page most of us refresh more often than we&apos;d care to admit.</p><p>A final thought for the week: with PA_1569 and PA_1570 tucked into place and four pieces lost, the net is barely negative — but every destruction is a reminder that flashing is, in its small way, an act of preservation. Get out, look up, and stay safe out there.</p>]]></content:encoded>
            <author>henrypye@newsletter.paragraph.com (henry)</author>
            <category>invader</category>
            <category>street-art</category>
            <category>two final parisians, and a week of losses</category>
            <category>a city-by-city pulse</category>
            <category>from the wider web</category>
            <enclosure url="https://storage.googleapis.com/papyrus_images/f1fa0972862f4019fa60b996ce86d006f4a550933db61fea42ba06e44d378b70.png" length="0" type="image/png"/>
        </item>
        <item>
            <title><![CDATA[Dev Diary #17]]></title>
            <link>https://paragraph.com/@henrypye/dev-diary-17</link>
            <guid>7efUd8Zoi2je9ZVUvc8S</guid>
            <pubDate>Sat, 09 May 2026 03:14:10 GMT</pubDate>
            <description><![CDATA[Wired up lane safety, slack alerts, and a cockpit dashboard; token burn and backfill logic still unresolved.]]></description>
            <content:encoded><![CDATA[<p>I&apos;m knee-deep in two separate problems that won&apos;t quite align. The worktree lane system needed a safety net — I added a fetch + fast-forward on main before spawning new lanes in <code>claude/bin/wt</code>, because nothing kills momentum like discovering you&apos;re three commits behind halfway through a feature branch. But the real bottleneck is token burn during session startup. Each lane was loading its full plugin set, which meant spinning up a worktree for a backend task was dragging in frontend tooling metadata. I trimmed the per-lane plugins down to essentials, but that&apos;s a partial fix — the root issue is that I&apos;m not caching anything between lane switches.</p><p>The slack-tldr daemon is working, sort of. Got it blinking new alerts at 2 Hz using inverse-video frame toggling in <code>slack-tldr.py</code>, and the ack mechanism in <code>slack-watch</code> is solid. But the backfill logic is brittle. When you join a channel with ten thousand messages, the over-fetch (I&apos;m pulling 50% more than asked) was meant to protect against join-spam starving the TLDR digest. It works, but it&apos;s a band-aid. I&apos;m fetching too much, too often, and watching the config balloon as a workaround tells me the architecture wants something else.</p><p>The cockpit system — the worktree lanes + agent-state board + command suite — is real now. Prefix+L gives you a 4x2 tmux grid. <code>/ticket-pickup</code> pulls a ticket into your current lane state. But there&apos;s friction between how the state hooks (<code>_state-write.sh</code>, <code>agent-state-active.sh</code>) talk to the command layer. The refinement pass killed a lot of redundancy in the agent profiles (<code>backend.md</code>, <code>frontend.md</code>, etc.), but I kept adding <code>/ticket-pickup</code> and now I&apos;m not sure if that&apos;s a command or a stateful operation or both.</p><p>Next is making the worktree protocol docs actually binding — right now it&apos;s guidance in <code>worktree-protocol.md</code>, but nobody&apos;s enforcing it. Also need to kill the token bleed on session start properly, not just trim symptoms.</p>]]></content:encoded>
            <author>henrypye@newsletter.paragraph.com (henry)</author>
            <category>worktree safety protocols</category>
            <category>per-lane plugin trimming</category>
            <category>slack alert blinking</category>
            <category>backfill over-fetching</category>
            <category>tmux grid layouts</category>
            <enclosure url="https://storage.googleapis.com/papyrus_images/f6dc5c1d60aaff3e8fff1a59c0426efc2493bf7ad54d33018ffdcff016ebccb8.png" length="0" type="image/png"/>
        </item>
        <item>
            <title><![CDATA[Dev Diary #16]]></title>
            <link>https://paragraph.com/@henrypye/dev-diary-16</link>
            <guid>5bWZxKNlVzPu3sl4P1Lx</guid>
            <pubDate>Fri, 08 May 2026 05:38:44 GMT</pubDate>
            <description><![CDATA[I spent the morning staring at a timing problem. The audio watcher was firing detection events milliseconds before the commit watcher would feed them to the renderer, creating this weird race where visualizations would try to sync to data that hadn't arrived yet. Not a catastrophic failure—just a half-second delay that made everything feel sluggish. The fix was dumb in hindsight: I added a small buffer queue in audio-watcher.py that holds pending renders while waiting for commit metadata. Not...]]></description>
            <content:encoded><![CDATA[<p>I spent the morning staring at a timing problem. The audio watcher was firing detection events milliseconds before the commit watcher would feed them to the renderer, creating this weird race where visualizations would try to sync to data that hadn't arrived yet. Not a catastrophic failure—just a half-second delay that made everything feel sluggish.</p><p>The fix was dumb in hindsight: I added a small buffer queue in <code>audio-watcher.py</code> that holds pending renders while waiting for commit metadata. Nothing fancy. Just a deque with a 200ms window. It works, but now I'm carrying around this magic number that I'll probably regret in three months when the timing constraints change.</p><p>The actual feature I was chasing is audio-reactive rendering that pulls from multiple sources. Had to build out a config loader in <code>audio-watcher.config.example.json</code> that lets you specify different audio inputs—microphone, system output, whatever. Each source gets its own frequency analyzer thread. The <code>commit-watcher.py</code> module now emits branch events in addition to the usual commit signals, so the renderer can react differently when you switch branches mid-session. That part felt clean.</p><p>Wired it all together in <code>watch.py</code> and threw some shell aliases into <code>.zshrc</code> so I'm not typing python paths every time I want to spin up a session. The whole thing is... serviceable. Not elegant. The config format is JSON when it probably should've been YAML, and I'm doing string parsing in three different places that should be unified into a schema validator. But it works for what I'm building right now.</p><p>What I haven't solved: how to handle audio sources that drop frames or go silent for a few seconds. Right now the renderer just holds the last known state, which looks wrong. I could implement smoothing, interpolation, something. Instead I'm going to ship it broken and see if it actually matters when I'm using it in anger.</p>]]></content:encoded>
            <author>henrypye@newsletter.paragraph.com (henry)</author>
            <category>timing synchronization</category>
            <category>multi-source buffering</category>
            <category>event coordination</category>
            <category>config loading</category>
            <category>frequency analysis</category>
            <enclosure url="https://storage.googleapis.com/papyrus_images/40b6113f0b02afa593f2fddb6793982d524a3bb6906fe1892a2156d75e46e781.png" length="0" type="image/png"/>
        </item>
        <item>
            <title><![CDATA[Invader Weekly Roundup #8]]></title>
            <link>https://paragraph.com/@henrypye/invader-weekly-roundup-8</link>
            <guid>gpdxHCZlGTmAA1q4cy8a</guid>
            <pubDate>Wed, 06 May 2026 07:04:03 GMT</pubDate>
            <description><![CDATA[Three destructions (MLB_19, PA_1223, WN_04) and a London degradation were partly offset by reactivations of LDN_178 a...]]></description>
            <content:encoded><![CDATA[<h2 id="h-invader-weekly-roundup" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">Invader Weekly Roundup</h2><p><strong>This week:</strong> Three mosaics fell to destruction across Melbourne, Paris, and a quieter zone, while London delivered both a degradation and a reactivation in the same 24-hour stretch — a reminder that the street giveth and the street taketh away.</p><h3 id="h-a-rough-week-on-the-walls" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">A Rough Week on the Walls</h3><p>The destruction column made for grim reading this week. <strong>MLB_19</strong> in Melbourne came down on the 4th, <strong>PA_1223</strong> was lost in Paris on the 3rd, and <strong>WN_04</strong> met its end on the 29th in tandem with a degradation hit on <strong>LDN_21</strong> across London. That London pairing is the kind of sequence that stings — one piece weathered, another erased, in the same breath. Paris losing PA_1223 is particularly notable given that the capital remains Invader&apos;s spiritual home and accounts for roughly a third of all global activity in our running map data; every Parisian loss chips at the densest gallery on Earth. Melbourne&apos;s MLB_19 also echoed strangely into a London status update later in the week, a small reminder of how interconnected the global ledger has become as hunters and trackers cross-reference cities in real time.</p><h3 id="h-reactivations-offer-a-small-counterweight" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">Reactivations Offer a Small Counterweight</h3><p>Not all the news pulled in one direction. <strong>LDN_178</strong> in London received a status update on the 4th, flipping back onto the active board, and <strong>BAB_42</strong> was likewise reactivated on the 30th. Reactivations are one of the quieter joys of following Invader&apos;s catalogue — a piece thought lost or compromised gets reassessed, repaired, or simply re-confirmed in the wild, and suddenly the map looks a little fuller again. For BAB hunters in particular, BAB_42 returning to play is a nice mid-week lift in a city that sits firmly in the secondary activity tier with seven tracked events overall.</p><h3 id="h-the-press-beat-myartbroker-spotlights-the-hunt" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">The Press Beat: MyArtBroker Spotlights the Hunt</h3><p>On the curated press side, MyArtBroker refreshed two relevant pages this week: their main Invader artist hub at <a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://www.myartbroker.com/artist-invader">https://www.myartbroker.com/artist-invader</a> and, more interestingly for hunters, their guide <em>Top 10 Places to Find an Invader Original</em> at <a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://www.myartbroker.com/artist-invader/guides/top-10-places-to-find-an-invader-original">https://www.myartbroker.com/artist-invader/guides/top-10-places-to-find-an-invader-original</a>. The guide leans into Paris as the natural starting point, calling out Rue Monge in the 5th Arrondissement among its featured locations — a fitting nod given that Invader was born in Paris and has run his most concentrated invasion waves there. For newer flashers building a travel itinerary, pieces like this one are useful primers, even if seasoned hunters will already have walked Rue Monge a dozen times over.</p><h3 id="h-where-the-map-stands" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">Where the Map Stands</h3><p>Stepping back from the week&apos;s individual events, the broader picture remains familiar: Paris leads the world in tracked activity by a wide margin, with London, Los Angeles, São Paulo, and Bern rounding out the upper tier. London&apos;s double-event week — a degradation on LDN_21 and a reactivation on LDN_178 — keeps it firmly in the conversation as one of the most dynamic non-Paris cities to hunt right now. And with BAB notching a reactivation of its own, the secondary tier continues to prove that the interesting stories aren&apos;t always in the headline cities. Look up, hunt safely, and as always — don&apos;t risk yourself for a flash.</p>]]></content:encoded>
            <author>henrypye@newsletter.paragraph.com (henry)</author>
            <category>invader</category>
            <category>street-art</category>
            <category>a rough week on the walls</category>
            <category>reactivations offer a small counterweight</category>
            <category>the press beat: myartbroker spotlights the hunt</category>
            <enclosure url="https://storage.googleapis.com/papyrus_images/f1fa0972862f4019fa60b996ce86d006f4a550933db61fea42ba06e44d378b70.png" length="0" type="image/png"/>
        </item>
        <item>
            <title><![CDATA[Dev Diary #15]]></title>
            <link>https://paragraph.com/@henrypye/dev-diary-15</link>
            <guid>VV8pa24jG2Kjayx2e8YG</guid>
            <pubDate>Wed, 06 May 2026 00:44:57 GMT</pubDate>
            <description><![CDATA[Replaced matrix screensaver with meditative commit watcher using 180-second breath cycles.]]></description>
            <content:encoded><![CDATA[<p>Spent yesterday rebuilding what lives in my terminal when I&apos;m watching commits come in. The old matrix effect was noise—literally, just characters falling down the screen. I wanted something that actually <em>breathes</em> with the repository&apos;s pulse instead of treating every commit like an emergency alert.</p><p>Ripped out <code>matrix.py</code> entirely and wrote <code>watch.py</code> from scratch. The new renderer uses glyphs that fade in and out on a cycle, timed to meditation rather than machine urgency. Started at 12 seconds per breath, which felt frenetic. Jumped to 60 seconds. Still wrong. Then 180 seconds, and yeah—that&apos;s the cadence. The glyph gradient brightens at the inhale, dims at the exhale. It&apos;s subtle enough that you don&apos;t consciously watch it, but your brain syncs to it anyway.</p><p>Hit a wall with <code>curses.COLORS</code>. The function returns <code>-1</code> if you call it before <code>start_color</code>, which I did. Spent forty minutes chasing a logic error that didn&apos;t exist—the bug was in the call order, not the math. That&apos;s the kind of thing that makes you feel stupid until you realize curses is just <em>ancient</em> and has opinions about initialization order that aren&apos;t documented anywhere obvious.</p><p>Also made <code>commit-watcher.py</code> respect the current working directory instead of hardcoding a repo path. Sounds trivial, but it means I can use the same script in multiple repos without symlink gymnastics. The <code>$PWD</code> lookup lives in the zsh hook now, passed as an argument on startup.</p><p>Separately, added a verification layer for Claude—a pre-flight check that sources any code claims against actual files before acting on them. Lives in <code>verify-before-act.sh</code>. The settings file documents the policy. I don&apos;t trust external systems making changes to my environment without at least <em>asking</em> what they&apos;re operating on.</p><p>The watch renderer isn&apos;t done. The glyph selection is still random, and I want each commit to have its own glyph that persists for a few cycles. Right now they&apos;re just noise that happens to be slow. Still working on making the visual language match the repository&apos;s actual heartbeat.</p>]]></content:encoded>
            <author>henrypye@newsletter.paragraph.com (henry)</author>
            <category>terminal ui</category>
            <category>timing/animation</category>
            <category>curses initialization</category>
            <category>working directory context</category>
            <category>verification before action</category>
            <enclosure url="https://storage.googleapis.com/papyrus_images/5cf11161aad55ae3261351916a0ae5a182c0ac7cd0032c5a7d7669684b60162b.png" length="0" type="image/png"/>
        </item>
        <item>
            <title><![CDATA[Dev Diary #14]]></title>
            <link>https://paragraph.com/@henrypye/dev-diary-14</link>
            <guid>DMifYjY7M4eX2hDXDfs4</guid>
            <pubDate>Mon, 04 May 2026 00:18:55 GMT</pubDate>
            <description><![CDATA[I'm staring at ten Dockerfiles that all needed the same fix, and I did it the hard way. Upgraded npm to 11.x across agent-activity through agent-gym — all of them — then realized half the images were still calling npx tsx in their CMD directives, which fails silently when npx itself can't bootstrap properly. The real fix was bypassing npx entirely and running node tsx directly, but that only clicked after I'd already committed the npm bump. Now the images start, but there's friction in the st...]]></description>
            <content:encoded><![CDATA[<p>I'm staring at ten Dockerfiles that all needed the same fix, and I did it the hard way. Upgraded npm to 11.x across agent-activity through agent-gym — all of them — then realized half the images were still calling <code>npx tsx</code> in their CMD directives, which fails silently when npx itself can't bootstrap properly. The real fix was bypassing npx entirely and running <code>node tsx</code> directly, but that only clicked after I'd already committed the npm bump. Now the images start, but there's friction in the startup sequence I haven't fully traced yet.</p><p>The dotfiles work is cleaner. Built a singleton file-lock pattern in commit-watcher.py so it doesn't spawn multiple instances when zsh boots. The watcher now auto-starts when you run <code>art matrix</code> — that command spawns the Matrix digital rain visualization, and the watcher sits in the background, feeding recent commits into the bottom rows of the display. Had to backfill the last N commits on boot or you'd get a blank state for the first few seconds, which felt broken. That's in commit-watcher.py around line 60-ish, reading from git log with a hardcoded window of 50 commits.</p><p>The life-os dashboard got gutted. Pulled out activities, alerts, agents, director — all the auxiliary views. Leaving news, flashcastr, and the blog. It's aggressive. People might land on the dashboard and wonder where half the features went, but the bloat was real. We're trying to build a command center, not a spreadsheet factory. The monitoring rule I added watches display_sync — if that table ever hits zero rows, we get an alert. That's the kind of thing that cascades quietly if you're not watching.</p><p>What's eating at me: the Docker startup chain still feels slow. I'm not sure if it's npm 11.x being heavier, or if the tsx invocation is doing extra work, or if it's the node process initialization itself. Haven't profiled it yet. The commit-watcher backfill is also arbitrary — why 50? Could be too much, could be too little depending on how fast you're committing. Haven't tested that assumption.</p>]]></content:encoded>
            <author>henrypye@newsletter.paragraph.com (henry)</author>
            <category>file-lock singleton pattern</category>
            <category>git backfill heuristics</category>
            <category>docker startup performance</category>
            <category>npx bootstrap failures</category>
            <category>dashboard feature removal</category>
            <enclosure url="https://storage.googleapis.com/papyrus_images/e6ffe492a0f0a61864480f14ba7b7744f5795597f284a8f676de5267a339cc8b.png" length="0" type="image/png"/>
        </item>
        <item>
            <title><![CDATA[Invader Weekly Roundup #7]]></title>
            <link>https://paragraph.com/@henrypye/invader-weekly-roundup-7</link>
            <guid>NvvNMPHhtXYseODb44il</guid>
            <pubDate>Sat, 02 May 2026 17:56:22 GMT</pubDate>
            <description><![CDATA[Four reactivations balanced by two destructions; European pieces receive mixed fates.]]></description>
            <content:encoded><![CDATA[<h2 id="h-invader-weekly-roundup" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">Invader Weekly Roundup</h2><p><strong>This week:</strong> The invasion sees mixed fortunes across Europe and beyond. While new life breathes into several dormant pieces through reactivations, the movement also suffered notable losses to degradation and destruction in London and Fontainebleau.</p><h3 id="h-reactivations-signal-active-maintenance" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">Reactivations Signal Active Maintenance</h3><p>Four invaders returned to active status this week across three cities, suggesting Invader&apos;s ongoing commitment to refreshing his global catalog. In Grenoble, a cluster of three pieces—GNV_01, GNV_04, and GNV_11—received status updates on April 28, indicating either restoration work or renewed visibility after years of decline. Barcelona saw BAB_42 refreshed on April 30, while Brazil&apos;s BRL_09 was reactivated on April 25. These updates remind us that the invasion isn&apos;t static—it&apos;s a living, breathing project requiring constant attention and care.</p><h3 id="h-urban-losses-destruction-and-degradation" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">Urban Losses: Destruction and Degradation</h3><p>The week brought heartbreaking news from two fronts. On April 29, London&apos;s LDN_21 suffered degradation even as nearby WN_04 was destroyed entirely, a stark reminder that not all public spaces protect street art with equal commitment. Days earlier on April 26, Fontainebleau experienced a similar blow when FTBL_06 was destroyed following degradation of a nearby piece. These losses underscore the precarious existence of public art—constantly vulnerable to weather, urban renewal, and institutional indifference. Yet they also underscore why documenting and celebrating invaders through apps like FlashInvaders matters more than ever.</p>]]></content:encoded>
            <author>henrypye@newsletter.paragraph.com (henry)</author>
            <category>invader</category>
            <category>street-art</category>
            <category>reactivations signal active maintenance</category>
            <category>urban losses: destruction and degradation</category>
            <enclosure url="https://storage.googleapis.com/papyrus_images/f1fa0972862f4019fa60b996ce86d006f4a550933db61fea42ba06e44d378b70.png" length="0" type="image/png"/>
        </item>
        <item>
            <title><![CDATA[Dev Diary #13]]></title>
            <link>https://paragraph.com/@henrypye/dev-diary-13</link>
            <guid>I0KKq686jPwuKDwh83nF</guid>
            <pubDate>Fri, 01 May 2026 04:21:24 GMT</pubDate>
            <description><![CDATA[Built anti-hallucination core to prevent AI-generated diary entries from inventing facts.]]></description>
            <content:encoded><![CDATA[<p>Started the week fighting hallucinations. The agent-blog module kept generating diary entries with details it invented—Linear ticket refs that didn&apos;t exist, commit messages it reconstructed instead of using actual ones. Built an anti-hallucination core that runs classification on sensitive data (ticket IDs, file paths, error messages) before the LLM ever sees it, then verifies the output against what we actually have in the git history. Ended up with three migration files just for the schema changes: <code>0003_anti_hallucination_core.sql</code>, <code>0005_add_magnitude_algo_version.sql</code>, <code>0006_blog_publish_log.sql</code>.</p><p>The notes-pipeline needed rewiring. Instead of feeding raw commit data to the generator and hoping it wouldn&apos;t confabulate, we now extract structured facts first—repo names, actual file lists, message content verbatim. A <code>ticket-translation</code> module maps Linear refs if they exist, but crucially, doesn&apos;t create them if they don&apos;t. The pipeline still generates drafts in pending-review, but now with a <code>word-diff</code> layer that flags where the output diverges from source material. That&apos;s imperfect and I hate it, but it catches the worst cases.</p><p>Wired up cross-agent enrichment via RPC. The agent-blog can now ask service-director and service-linear for context without hallucinating their responses. Built <code>bus-rpc</code> in shared/core to handle the back-and-forth. It&apos;s synchronous, which feels wrong for distributed systems, but we&apos;re not distributed yet—we&apos;re monorepo&apos;d and the latency is negligible.</p><p>Got bitten by a partial unique index issue in the autonomous-tick handler. The <code>ON CONFLICT</code> clause wasn&apos;t matching because the index covered fewer columns than the insert. Spent an hour debugging why the same record kept getting reinserted. Fixed it in <code>0009_blog_stats_snapshots_uniq.sql</code> but now I&apos;m paranoid about other indexes doing the same thing silently.</p><p>Also spent time on terminal colors. Didn&apos;t plan to, but <code>.tmux.conf</code> and the new Alacritty config needed Gruvbox Material palettes to match the iTerm2 presets. Small win—environment consistency, but completely orthogonal to the core work.</p><p>Still unclear: whether the engagement learning loop in stats-ingestion is actually learning anything useful, or just recording noise. The <code>preference-scores</code> table exists, but I haven&apos;t built the feedback path yet. That&apos;s next.</p>]]></content:encoded>
            <author>henrypye@newsletter.paragraph.com (henry)</author>
            <category>hallucination detection</category>
            <category>source validation</category>
            <category>rpc enrichment</category>
            <category>schema migrations</category>
            <category>unique constraint resolution</category>
            <enclosure url="https://storage.googleapis.com/papyrus_images/25a2b87f8a1172864c397a4f4ef46e6a3b9147b97f96165ede56ebfa2066933d.png" length="0" type="image/png"/>
        </item>
        <item>
            <title><![CDATA[Dev Diary #12]]></title>
            <link>https://paragraph.com/@henrypye/dev-diary-12</link>
            <guid>mEJR4BRfkjZmTLkCw4Jr</guid>
            <pubDate>Mon, 27 Apr 2026 03:30:22 GMT</pubDate>
            <description><![CDATA[I've been staring at 43 commits across three days and what's actually happened is I've been firefighting the finance pipeline while the rest of the system screamed for attention. The bulk-import endpoint kept choking on CSV payloads—gateway JSON limit hit 1MB and the PWA was trying to batch 500 transactions at once. Moved the CSV text inline into the event instead of keeping it separate, which sounds simple but meant rewiring how agent-finance receives data. BulkImportDropZone now sends raw C...]]></description>
            <content:encoded><![CDATA[<p>I've been staring at 43 commits across three days and what's actually happened is I've been firefighting the finance pipeline while the rest of the system screamed for attention. The bulk-import endpoint kept choking on CSV payloads—gateway JSON limit hit 1MB and the PWA was trying to batch 500 transactions at once. Moved the CSV text inline into the event instead of keeping it separate, which sounds simple but meant rewiring how agent-finance receives data. <code>BulkImportDropZone</code> now sends raw CSV directly; the agent parses it, extracts entities using fuzzy matching against existing counterparties, and pushes back reconcile payloads weighted by transaction count.</p><p>The reconcile protocol got hairy. I kept adding <code>weight</code> parameters to <code>emit()</code> calls across five different agents—blog, email, farcaster, pet, rs3—because bulk imports were drowning out the progress signal. One agent would reconcile 200 transactions while another bumped a single calendar event, and the display-sync would think everything was done. Weighted reconciliation meant revisiting the shared base component and touching tsconfig across multiple workspaces.</p><p>Then there's the schema collapse. Display-sync and agent reconciliation used to speak different dialects. I collapsed them into a single protocol, which meant rewriting handlers across eight agents and a bunch of hotfixes when the drizzle journal timestamps went backwards (0029 wasn't past 0023). Fixed it with a journal bump, then fixed the actual bug where <code>DateTime!</code> scalar wasn't registered in admin typedefs.</p><p>Finance specifically ate two days. Built a 12-month narrative report that's cost-capped and goal-aware, added bucket categorization, rebuilt the entire ledger as raw CSV debit/credit instead of transaction-oriented storage. Parser now handles YYYYMMDD format. Wrote <code>csv-parser.test</code> fixtures to lock in the behavior. The annual report generates a forecast, but the forecast blob wasn't null-guarding <code>statusQuo</code> and the PWA would crash on stale data.</p><p>What's rough: the finance report generation is still fragile. Adding one more parser schema or changing how categories group takes rewriting the entire analysis pipeline. And I'm not sure the weighted reconcile is right—some agents emit fast and light, others slow and heavy. The scale's arbitrary right now.</p><p>Next is figuring out whether the forecast payload shape actually matches what the PWA expects.</p>]]></content:encoded>
            <author>henrypye@newsletter.paragraph.com (henry)</author>
            <category>weighted reconciliation</category>
            <category>bulk csv import</category>
            <category>schema collapse</category>
            <category>transaction ledger rebuild</category>
            <category>parser fixture testing</category>
            <enclosure url="https://storage.googleapis.com/papyrus_images/ce0ea3719f0c7bea3655f09436b6173dfe5c662b68bc47c0791afa652fac2b12.png" length="0" type="image/png"/>
        </item>
        <item>
            <title><![CDATA[Dev Diary #11]]></title>
            <link>https://paragraph.com/@henrypye/dev-diary-11</link>
            <guid>7m0htXgLlbYDh2C4cFyg</guid>
            <pubDate>Fri, 24 Apr 2026 19:02:30 GMT</pubDate>
            <description><![CDATA[Fixed sync data loss via schema writes, extracted image service, hardened finance CSV parsing and calendar dedup.]]></description>
            <content:encoded><![CDATA[<p>Pulled the migration file apart in <code>0023_add_job_digest_and_created_at.sql</code> because SQLite choked on multiple statements without the breakpoint marker. The dashboard was rendering empty boxes where job digests should live—turned out the schema had the columns but the sync handler wasn&apos;t actually writing <code>createdAt</code> or <code>digest</code> to the display tables. Fixed that in the handlers, but now I&apos;m staring at a gap: the <code>display-sync</code> index was trying to ALTER TABLE on tables that don&apos;t exist yet on fresh installs. Added a safety net that CREATE TABLE before ALTER, though it feels hacky.</p><p>Spent time on the image sync refactor, extracting Unsplash into <code>service-image</code> as its own component. Built out the routing-key changes and wired up <code>agent-activity</code> and <code>agent-recipe</code> to call into the new service instead of doing image fetches inline. The Railway deployment config is there (<code>railway.toml</code>), but I haven&apos;t tested the actual deployment flow yet—just set it up so the infra team can push it.</p><p>Calendar event dedup is half-working. Added a <code>content_hash</code> column to catch duplicate syncs within the 5-minute poll window, but if an event changes slightly (like description gets edited), we&apos;ll see it as new. I&apos;m not sure if that&apos;s the right trade-off. The fallback to 5m polling when webhooks fail is solid, though.</p><p>Finance work got real messy. Amex exports charges as positive but our system expects negatives, so I had to negate them in the CSV parser. Deep-dive metrics were registering twice because the wrapper wasn&apos;t unwrapping the response correctly. Goals parser was choking on markdown formatting, so I hardened that too. The whole flow is still fragile—<code>agent-finance</code> expects the month and filename from the <code>statement-uploaded</code> event payload, but if that&apos;s malformed, the analysis silently fails.</p><p>Dotfiles got a major cleanup. Set up daily plan pruning via launchd, added a focus-guard tool (HTTP + HTTPS status page for blocked sites), and built out context-economy guardrails to track token usage. The statusline now shows the 5h/7d rate limit buckets when we&apos;re above 50%, which should give me better visibility into cost creep.</p><p>What&apos;s uncertain: whether the safety nets in display-sync are masking a deeper schema ordering problem, or if the calendar dedup strategy will survive real-world edits.</p>]]></content:encoded>
            <author>henrypye@newsletter.paragraph.com (henry)</author>
            <category>migration breakpoints</category>
            <category>schema safety nets</category>
            <category>content deduplication</category>
            <category>csv parsing negation</category>
            <category>service extraction</category>
            <enclosure url="https://storage.googleapis.com/papyrus_images/ec842b91a974a0b6f0d387408f1d533221deaacf1c2e8bd2ea2a632fa2b756f7.png" length="0" type="image/png"/>
        </item>
        <item>
            <title><![CDATA[Dev Diary #10]]></title>
            <link>https://paragraph.com/@henrypye/dev-diary-10</link>
            <guid>OoTcLiv2zoPDb7PhoTEd</guid>
            <pubDate>Wed, 22 Apr 2026 14:10:33 GMT</pubDate>
            <description><![CDATA[Spent the last two days hammering through 84 commits across three repos, mostly life-os. Calendar attendees were serializing as [object Object] instead of actual names—quick fix in the helpers, but it exposed a pattern: we're doing too much serialization at query time instead of picking the right fields upfront. That's the rough edge. The bigger push was promoting agent-weather and agent-git from side agents to proper services with CONTEXT_REQUEST interfaces. Renamed the database column to pr...]]></description>
            <content:encoded><![CDATA[<p>Spent the last two days hammering through 84 commits across three repos, mostly life-os. Calendar attendees were serializing as <code>[object Object]</code> instead of actual names—quick fix in the helpers, but it exposed a pattern: we're doing too much serialization at query time instead of picking the right fields upfront. That's the rough edge.</p><p>The bigger push was promoting agent-weather and agent-git from side agents to proper services with CONTEXT_REQUEST interfaces. Renamed the database column to <code>process</code> across both—three migration files per service to keep the old state intact during deploy. The schema safety nets I added in those migrations are doing real work now; without them, SQLite provisioning on startup fails silently and you get null columns everywhere.</p><p>Wired RS3 inferred activities into the dashboard widget, fixed the email KPI stats to actually show unread/synced counts, and expanded the news roster to 37 sources using a source adapter pattern in agent-news. The news page was crashing because articleIds stayed null—turned out the agent wasn't firing CONTENT_READY payloads with the right event shape.</p><p>Mobile layout work across recipes, gym, workout, jobs, football. The gutter token migration is cleaner now; grep-verified zero padding violations on page-container. Removed the grey box from the mobile upcoming fixture card.</p><p>Started seeing schema provisioning problems cascade hard. Added a startup probe in shared/rabbitmq/subscriber that hard-fails if the DB schema is missing expected columns—better than silent null returns. Also went through agent-activity, agent-calendar, agent-gym with schemaSafetyNets using PRAGMA table_info instead of IF NOT EXISTS, since older SQLite doesn't like the latter.</p><p>The notification service now hard-blocks web-push during sleep hours, pulling timezone config from shared. Per-widget ErrorBoundary on dashboard means one broken fetch doesn't crater the whole page.</p><p>What's unresolved: weather data is still stale sometimes—0,0 coordinates getting logged and the 15-min refresh isn't catching it fast enough. Also the activities page infinite reload loop on empty profile needs more investigation. Next is the recipe image URL rendering and activity reschedule mutations.</p>]]></content:encoded>
            <author>henrypye@newsletter.paragraph.com (henry)</author>
            <category>schema safety nets</category>
            <category>database migrations</category>
            <category>serialization patterns</category>
            <category>timezone handling</category>
            <category>cqrs query paths</category>
            <enclosure url="https://storage.googleapis.com/papyrus_images/31ce1be23bd82cf163610755acdf3b367bae2a5022f8951a7b514bb666883589.png" length="0" type="image/png"/>
        </item>
        <item>
            <title><![CDATA[Invader Weekly Roundup #2]]></title>
            <link>https://paragraph.com/@henrypye/invader-weekly-roundup-2-1</link>
            <guid>aPcH7cUurkCoh3tFSXK3</guid>
            <pubDate>Wed, 22 Apr 2026 07:08:49 GMT</pubDate>
            <description><![CDATA[Destructions in Paris and NYC, reactivation in WN: the invasion endures through renewal.]]></description>
            <content:encoded><![CDATA[<h2 id="h-invader-weekly-roundup" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">Invader Weekly Roundup</h2><p><strong>This week:</strong> The invasion experienced turbulence across multiple continents—destructions in Paris and New York, a crucial reactivation in an unnamed territory, and the complex interplay of creation and erasure that defines street art&apos;s precarious existence.</p><h3 id="h-the-toll-destructions-and-degradations" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">The Toll: Destructions and Degradations</h3><p>The past seven days saw a sobering reminder that invasion survival remains uncertain. PA_300 in Paris fell to degradation on April 20, joining PA_213 in succumbing to the relentless forces of weather, urban maintenance, or deliberate removal. Meanwhile, New York&apos;s NY_183 suffered degradation on April 15, adding to the steady attrition that hunters know intimately—each mosaic&apos;s life span measured in months or years rather than permanence. The real complexity emerged in São Paulo, where destruction of SP_28 occurred simultaneously with something rarer: reactivation.</p><h3 id="h-the-resurrection-wn01-returns" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">The Resurrection: WN_01 Returns</h3><p>Perhaps the most intriguing development came from an unnamed location (WN) where WN_01 experienced reactivation on April 17. The simultaneous destruction of SP_28 alongside this resurrection captures something profound about the invasion&apos;s nature—art that persists not through institutional preservation but through the artist&apos;s commitment to the mission. A piece returns, a piece falls, the work continues. This is how planetary dimension is achieved: not through permanence, but through relentless reinvasion. For collectors tracking these shifts, reactivations signal fresh opportunities for flashing and rediscovery.</p>]]></content:encoded>
            <author>henrypye@newsletter.paragraph.com (henry)</author>
            <category>invader</category>
            <category>street-art</category>
            <category>the toll: destructions and degradations</category>
            <category>the resurrection: wn_01 returns</category>
            <enclosure url="https://storage.googleapis.com/papyrus_images/f1fa0972862f4019fa60b996ce86d006f4a550933db61fea42ba06e44d378b70.png" length="0" type="image/png"/>
        </item>
        <item>
            <title><![CDATA[Dev Diary #9]]></title>
            <link>https://paragraph.com/@henrypye/dev-diary-9</link>
            <guid>PPAoq3LckDVdhLYXwQLZ</guid>
            <pubDate>Mon, 20 Apr 2026 04:22:08 GMT</pubDate>
            <description><![CDATA[The theme of the week: agents getting a voice. For a long time, most of the agents in this system were quiet workers — they polled an API, wrote rows to a database, went back to sleep. The message bus existed, but it was mostly one-directional. Agents shouted into it; nothing shouted back. That's changing. The two big promotions this week were agent-git and agent-weather, both of which graduated from passive background workers to full services with a proper conversational interface. They're n...]]></description>
            <content:encoded><![CDATA[<p> The theme of the week: agents getting a voice.</p><p>For a long time, most of the agents in this system were quiet workers — they polled an API, wrote rows to a database, went back to sleep. The message bus existed, but it was mostly one-directional. Agents shouted into it; nothing shouted back.</p><p>That's changing. The two big promotions this week were agent-git and agent-weather, both of which graduated from passive background workers to full services with a proper conversational interface. They're now <code>service-git</code> and <code>service-weather</code>, and they both speak CONTEXT_REQUEST/CONTEXT_RESPONSE — meaning any other agent on the bus can tap them on the shoulder and ask a question. "Hey, what did I commit last week?" "What's the forecast for the next 24 hours?" The service hears it, queries its own data, and responds directly on the bus. No shared database. No direct coupling. Just agents talking.</p><p>Service-weather also now emits a <code>WEATHER_FORECAST_SYNCED</code> event after every home-location poll — so anything downstream that cares about weather doesn't have to ask. It just listens. The director still gets its <code>WEATHER_PLAN_CONTRIBUTION</code> for morning planning (that wire stays until PR2 lands), but now there's a richer stream running alongside it.</p><p>The RS3 agent got considerably smarter this week too. It's running an AI activity classifier now — looking at XP gains, skill patterns, timing, and inferring what was actually happening during a session: grinding, skilling, bossing. Those inferences are surfacing on the dashboard widget with live confidence scores rather than the hardcoded placeholder that's been sitting there. The pattern-learning layer underneath (ETA estimates, rest days, main session detection) has been running quietly for a while; it's now feeding into something visible.</p><p>News got a proper source adapter pattern — <code>rssSource()</code>, <code>redditJsonSource()</code>, <code>hnAlgoliaSource()</code> — and the roster expanded from 14 feeds to 37. AI/ML blogs, engineering journals, Vancouver-specific sources, finance, policy, HN via Algolia. Each source gets its own Prometheus counter so we can see fetch health per-source rather than a single aggregate.</p><p>The notification service learned some social awareness. It was perfectly willing to ping your phone at 2am before this week. Now there's a quiet-hours gate: web push is hard-blocked between <code>sleep_time</code> and <code>wake_time</code> (user-local time, with correct midnight wrap-around). Notifications still flow through the bus and get written — only the phone ping is suppressed. The gate refreshes on <code>CONFIG_UPDATED</code> without a restart, so changing your sleep time takes effect immediately.</p><p>Still mid-migration on the database side. The direction is clear — each service owns its own PostgreSQL instance, reads from its own tables, and the only cross-service communication happens on the message bus. The pieces that have moved are genuinely cleaner. The pieces that haven't are a daily reminder of why we're doing this.</p><p>The community is coming together. Slowly, noisily, one CONTEXT_REQUEST at a time.</p>]]></content:encoded>
            <author>henrypye@newsletter.paragraph.com (henry)</author>
            <category>per-agent databases</category>
            <category>event-driven configuration</category>
            <category>state migration</category>
            <category>sqlite consolidation</category>
            <category>refinement pipeline</category>
            <enclosure url="https://storage.googleapis.com/papyrus_images/ef42c3058b5adfcfbb2703161f42a8951dbb350b5095de9cf3888dce85e1d63d.png" length="0" type="image/png"/>
        </item>
        <item>
            <title><![CDATA[Invader Weekly Roundup #2]]></title>
            <link>https://paragraph.com/@henrypye/invader-weekly-roundup-2</link>
            <guid>rZ7hD0LbZG5ue1y37O2k</guid>
            <pubDate>Wed, 15 Apr 2026 15:02:50 GMT</pubDate>
            <description><![CDATA[Invasions face wear; Tokyo and London exhibitions wrap up this week.]]></description>
            <content:encoded><![CDATA[<h2 id="h-invader-weekly-roundup" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">Invader Weekly Roundup</h2><p><strong>This week:</strong> The street art world felt the inevitable wear of time as several invasions across Europe faced degradation and destruction, while recent exhibitions celebrating Invader&apos;s work wrapped up their runs.</p><h3 id="h-the-cost-of-public-art" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">The Cost of Public Art</h3><p>Street art exists in the most exposed place possible—on the sides of buildings, facing wind and weather and the passage of countless people. This week, we saw that reality hit hard across multiple cities. In Berlin, a cluster of invasions including BRN_05, BRN_01, and REUN_03 all experienced degradation, while LDN_68 in London suffered complete destruction. Just days earlier, LDN_181 met the same fate. These losses remind us why documentation and community tracking matter so much—every invasion captured in the Flash Invaders app and through spotter reports becomes a permanent record of art that may not last on the wall. The work persists, even when the physical piece doesn&apos;t.</p><h3 id="h-recent-exhibitions-close" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">Recent Exhibitions Close</h3><p>Just as these street invasions face the elements, Invader&apos;s gallery presence wrapped up this period. The &quot;Hyperspace&quot; exhibition in Tokyo concluded on April 6th, and the HENI Editions &quot;Invaded &amp; Commanded Blossom&quot; print sale ended April 7th, bringing limited-edition collaborative works back into the collector&apos;s market. Invader was also featured in Paris&apos;s cultural weekly roundup for the April 6-12 window. For fans tracking both the streets and the studio, it&apos;s been a week of endings—but also of opportunities to own pieces of the larger Invader story.</p>]]></content:encoded>
            <author>henrypye@newsletter.paragraph.com (henry)</author>
            <category>invader</category>
            <category>street-art</category>
            <category>the cost of public art</category>
            <category>recent exhibitions close</category>
            <enclosure url="https://storage.googleapis.com/papyrus_images/f1fa0972862f4019fa60b996ce86d006f4a550933db61fea42ba06e44d378b70.png" length="0" type="image/png"/>
        </item>
    </channel>
</rss>