<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Tatted Dev</title>
    <description>The latest articles on DEV Community by Tatted Dev (@tatted_dev).</description>
    <link>https://dev.to/tatted_dev</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3518366%2Faa49110a-cf6b-4606-a2d8-56a044b78834.png</url>
      <title>DEV Community: Tatted Dev</title>
      <link>https://dev.to/tatted_dev</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/tatted_dev"/>
    <language>en</language>
    <item>
      <title>I Planned to Make Games in 2027. Here’s Where I Am Now.</title>
      <dc:creator>Tatted Dev</dc:creator>
      <pubDate>Sun, 25 Oct 2026 12:11:06 +0000</pubDate>
      <link>https://dev.to/tatted_dev/i-planned-to-make-games-in-2027-heres-where-i-am-now-3lci</link>
      <guid>https://dev.to/tatted_dev/i-planned-to-make-games-in-2027-heres-where-i-am-now-3lci</guid>
      <description>&lt;p&gt;Back in July, I wrote &lt;a href="https://tatteddev.com/blog/why-im-pointing-at-game-dev-in-2027/" rel="noopener noreferrer"&gt;a post about making games in 2027&lt;/a&gt;. Unity was picked. Blender was picked. The actual game was considerably less picked.&lt;/p&gt;

&lt;p&gt;At the time, that felt like a reasonable commitment. I had other software to build, plenty to learn, and a long history of being interested in game development without getting very far into it. Putting a year on it gave the idea somewhere to go.&lt;/p&gt;

&lt;p&gt;It is now October 2026, and there are two Unity game projects in my studio.&lt;/p&gt;

&lt;p&gt;Apparently, I started early.&lt;/p&gt;

&lt;p&gt;That deserves an update, especially because the interesting part is what has happened between deciding to make a game and having something concrete enough to change my mind about.&lt;/p&gt;

&lt;p&gt;The first project started as a magic shop.&lt;/p&gt;

&lt;p&gt;You stand behind a counter. Someone brings you an item, and you have to work out what it is, what their story is worth, and what you are willing to pay. There are instruments to examine things, references to consult, and opportunities to make a bad judgment. Behind the counter, workshop stations let you make or improve items.&lt;/p&gt;

&lt;p&gt;That gave the idea a place and a set of things to do. It also gave us something more useful than another page of possibilities: a project we could build, inspect, and revise.&lt;/p&gt;

&lt;p&gt;As the design developed, the shop grew toward an adventurers’ guild hall. Adventurers return from the road with loot, worn equipment, and wounds. They need somewhere to sleep, food, supplies, and help getting ready to leave again.&lt;/p&gt;

&lt;p&gt;Those activities connect. The person selling you something at the desk can also become your guest. Their damaged gear gives the workshop a purpose. Their next trip gives the supplies on your shelves a purpose. The building starts to feel like a place people depend on.&lt;/p&gt;

&lt;p&gt;The project now has implementation around those ideas. There is still work to do on how it feels, how clearly it explains itself, and whether its growing collection of activities makes a good day of play.&lt;/p&gt;

&lt;p&gt;I am being careful about that distinction. A feature existing in the project tells me we have something to evaluate. I still have to evaluate it.&lt;/p&gt;

&lt;p&gt;The second project has already given me a fairly direct lesson in that.&lt;/p&gt;

&lt;p&gt;It began as a different kind of tabletop game. We built the first version, added a guided introduction, and I played it. After that, I rejected the central tally mechanic and changed direction.&lt;/p&gt;

&lt;p&gt;It is now a creature-and-spell card battler.&lt;/p&gt;

&lt;p&gt;There are decks, elemental advantages, creatures with attack and health, spells, and an opponent to play against. The work has moved through changes to combat, a deck builder, class abilities, and stronger opponent decks.&lt;/p&gt;

&lt;p&gt;But the most useful part of that history might be the discarded version.&lt;/p&gt;

&lt;p&gt;We could implement its rules. We could check those rules. I could read the design and agree to build it. Playing it still changed the decision.&lt;/p&gt;

&lt;p&gt;That is a much more concrete encounter with game design than anything I could have learned by continuing to plan for next year. I had to put time into a version before I could decide that I wanted a different game.&lt;/p&gt;

&lt;p&gt;AI coding agents are a large part of how I have been able to start this work alongside everything else.&lt;/p&gt;

&lt;p&gt;I use Codex and Claude extensively in my software projects, and that carries into the games. They handle substantial implementation work. I describe behavior, work through the design, inspect what comes back, and decide what to change.&lt;/p&gt;

&lt;p&gt;I have not suddenly acquired years of Unity experience. There are still engine details, graphics concepts, and production skills I need to understand better.&lt;/p&gt;

&lt;p&gt;What I have gained is a shorter path from an idea to a version I can examine. That makes starting more practical. It also gives me more decisions to make, because every working addition raises another question about what belongs in the game.&lt;/p&gt;

&lt;p&gt;The guild hall is a good example. A kitchen makes sense. Lodging makes sense. Repairs make sense. Each addition can have a reasonable explanation, and I still need to consider the amount of work they create together.&lt;/p&gt;

&lt;p&gt;In July, scope was one of the things I said I needed to learn. Now I have specific projects in which to learn it.&lt;/p&gt;

&lt;p&gt;I also have a clearer picture of what my software background contributes. State, persistence, code organization, and tests are familiar territory. Those skills help me build something I can keep changing. The play sessions have already shown me how much more there is to judge once the rules work.&lt;/p&gt;

&lt;p&gt;So 2027 is still relevant. There is plenty of learning and development ahead. I have no release date to announce here.&lt;/p&gt;

&lt;p&gt;But the starting point has changed. I have projects with rules that have survived revision, rules I have thrown away, and builds that give me something specific to react to.&lt;/p&gt;

&lt;p&gt;The next useful update should have more of that: something I played, what I thought of it, and what changed afterward.&lt;/p&gt;

</description>
      <category>gamedev</category>
      <category>unity3d</category>
      <category>ai</category>
      <category>devjournal</category>
    </item>
    <item>
      <title>AI Can Write the Code. Who Owns the Architecture?</title>
      <dc:creator>Tatted Dev</dc:creator>
      <pubDate>Sat, 10 Oct 2026 20:45:55 +0000</pubDate>
      <link>https://dev.to/tatted_dev/ai-can-write-the-code-who-owns-the-architecture-4d9a</link>
      <guid>https://dev.to/tatted_dev/ai-can-write-the-code-who-owns-the-architecture-4d9a</guid>
      <description>&lt;p&gt;I use Codex and Claude extensively to build HoneyDrunk projects. They write a lot of the implementation. I describe the behavior, inspect what comes back, and decide whether it belongs in the system I am trying to build.&lt;/p&gt;

&lt;p&gt;Pocket Quests has been a useful reminder of how much work sits inside that last part.&lt;/p&gt;

&lt;p&gt;As the backend grew, I kept running into mutation and change-binding abstractions, repeated comparisons against historical values, large methods assembling entire aggregates, and field-by-field mapping mixed into business services. Each piece added something else I had to understand before I could follow the operation.&lt;/p&gt;

&lt;p&gt;What frustrated me was having to explain the same conventional structure repeatedly. I wanted to discuss what completing a quest should do. Instead, I was spending time explaining where validation belonged, why mapping needed its own files, and why we were building another mechanism around changes EF Core already tracks.&lt;/p&gt;

&lt;p&gt;A generated document can list KISS, SOLID, and DRY. I still have to open the code and check whether those principles affected any decisions.&lt;/p&gt;

&lt;p&gt;That is where architectural ownership becomes very concrete. Someone has to decide which responsibilities belong together, which dependencies are acceptable, and whether a new abstraction earns its place. In my projects, that responsibility stays with me.&lt;/p&gt;

&lt;p&gt;The structure I wanted was straightforward: minimal API endpoints call business service interfaces. Services own the business operation and call static validators, separate mapping extensions, and data-service interfaces. Data services handle queries and persistence through EF Core and the DbContext.&lt;/p&gt;

&lt;p&gt;An endpoint should make the HTTP boundary easy to understand. A service should make the business operation easy to understand. When I need to inspect a query, I should know where to find it.&lt;/p&gt;

&lt;p&gt;That is a deliberate choice for these applications. Another team might organize the same responsibilities differently and have good reasons for it. My concern is whether the chosen structure stays consistent enough that the next feature does not require another architecture discussion.&lt;/p&gt;

&lt;p&gt;Mapping is a good example because it looks harmless until it fills half a method.&lt;/p&gt;

&lt;p&gt;Copying a quest's title, description, category, and other selected values into a persistence model is routine mapping. I want that in a named mapping extension. The service can then show the decisions around it: validate the request, identify the account, select the applicable rules, decide what needs recording, and call the data services.&lt;/p&gt;

&lt;p&gt;There is a limit to that extraction. Deciding whether a quest needs a new historical revision is business behavior. Choosing which reward rules apply is business behavior. A mapper should receive those decisions as inputs. Moving them into a method called &lt;code&gt;ToEntity&lt;/code&gt; would make them harder to find.&lt;/p&gt;

&lt;p&gt;The same applies to large methods that construct a whole aggregate. Some coordination belongs in a service, and pure domain rules can remain in the domain model. But when the coordination is buried among dozens of assignments, it becomes difficult to see what the operation actually promises. Separating the field copying makes the remaining business decisions easier to review.&lt;/p&gt;

&lt;p&gt;Historical data needs particular care in Pocket Quests. Completing a quest, undoing a completion, and processing an action that arrived late can depend on earlier facts. EF can notice that a tracked property changed. It cannot decide whether the product needs another immutable revision or which earlier terms should govern an action.&lt;/p&gt;

&lt;p&gt;Those decisions need an explicit owner. If several paths compare the same historical values for the same reason, I want that rule in one focused place. I also want to understand any differences before combining them. Two similar comparisons can represent different business questions.&lt;/p&gt;

&lt;p&gt;For ordinary updates, the persistence path should stay ordinary: load the tracked entity, change the intended properties, and let EF detect the changes. A separate change-binding mechanism needs to solve a real problem beyond describing those assignments again.&lt;/p&gt;

&lt;p&gt;Transactions deserve the same restraint. A simple operation covered by one &lt;code&gt;SaveChanges&lt;/code&gt; call usually has the transactional behavior it needs with a relational provider. Explicit transaction control should follow the operation's consistency requirements. Pocket Quests has commands that coordinate history, progress, and receipts, with locking and retry concerns. Those requirements justify more care; a single write does not automatically justify another transaction wrapper.&lt;/p&gt;

&lt;p&gt;I want the EF configuration to be equally readable. Configure the contracts and differences that matter: keys, relationships, lengths, precision, concurrency, and required index behavior. Leave ordinary conventions alone. Removing a line still requires understanding its effect on the resulting model and schema.&lt;/p&gt;

&lt;p&gt;This carries through to the repository layout. Quest entities, mappings, queries, and SQL tables should sit under consistent domain folders in their respective projects. Following a feature across layers should be predictable.&lt;/p&gt;

&lt;p&gt;Table and column descriptions matter here too. A timestamp might mean when the server inserted a row or when an offline action actually happened. That distinction affects behavior. Metadata should explain it. An index should have a query or constraint behind it that I can point to.&lt;/p&gt;

&lt;p&gt;Ownership also extends beyond Pocket Quests. HoneyDrunk has shared repositories, and I want reusable components in the repository responsible for them. Before adding another helper, I want the agent to look at what already exists and verify that it actually does the required job.&lt;/p&gt;

&lt;p&gt;Quest progression rules belong with the product. Reusable persistence mechanics may belong in a shared library. The dependency should reflect that ownership. A plausible package name or a catalog entry is not enough evidence that the capability exists and works for this use case.&lt;/p&gt;

&lt;p&gt;This is what KISS, SOLID, and DRY mean to me in a review. I can follow the operation. Each component has a coherent job. The same policy has one maintained implementation. Adding interfaces everywhere or pushing unrelated helpers into a shared package does not establish any of that.&lt;/p&gt;

&lt;p&gt;I also need those expectations to survive the next session. The root &lt;code&gt;AGENTS.md&lt;/code&gt; should point to canonical standards and the repository's specific boundaries. Maintaining several copies gives me several places to correct the same decision later.&lt;/p&gt;

&lt;p&gt;That gives Codex and Claude a clear starting point. I still need to verify that the instructions were available and that the implementation follows them. Writing down the standard is one step; applying it across existing code takes further work.&lt;/p&gt;

&lt;p&gt;CI, Sonar, and reviews help with that work. Tests can check behavior. Analysis can identify duplication and other problems. Review can follow a request through its dependencies and question the design. A green result tells me something useful about the checks that ran. It cannot settle whether a responsibility belongs in this repository or whether an abstraction is worth maintaining.&lt;/p&gt;

&lt;p&gt;I want to keep using both tools to build more. I also want the resulting code to stay understandable when I come back to it, or when another agent starts the next feature.&lt;/p&gt;

&lt;p&gt;That means I still own the boundaries, the trade-offs, and the decision to accept the result. AI can write the implementation. I have to be able to explain why it is built that way.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>architecture</category>
      <category>dotnet</category>
      <category>honeydrunk</category>
    </item>
    <item>
      <title>Things I wish my Dot could do</title>
      <dc:creator>Tatted Dev</dc:creator>
      <pubDate>Sat, 03 Oct 2026 19:45:19 +0000</pubDate>
      <link>https://dev.to/tatted_dev/things-i-wish-my-dot-could-do-39oo</link>
      <guid>https://dev.to/tatted_dev/things-i-wish-my-dot-could-do-39oo</guid>
      <description>&lt;p&gt;I've liked having my Dot around this week. I can send it a message when an idea comes to me, call it when talking is easier, and keep working from my phone while it gets on with a task. Even the little icons are a detail I enjoy.&lt;/p&gt;

&lt;p&gt;OpenAI introduced Dots at &lt;a href="https://openai.com/index/devday-2026-recap/" rel="noopener noreferrer"&gt;DevDay on September 29&lt;/a&gt;. I've been using mine for a mix of things: working on a blog post, building PocketQuests, making prototypes, writing UI code, and organizing my learning schedule. Using it across all of that has given me a better sense of what I like about it and where I want it to go.&lt;/p&gt;

&lt;h2&gt;
  
  
  What has worked well
&lt;/h2&gt;

&lt;p&gt;The text-message style of interaction fits me. I can send an idea, add another thought, and keep the conversation going. Being able to call my Dot is a major plus. Sometimes talking through what I want is much easier than trying to type the whole thing out.&lt;/p&gt;

&lt;p&gt;My ideas don't arrive in an orderly queue. I can be thinking about a project, a prototype, a blog post, and something I want to learn almost at the same time. Having somewhere to put that stream of ideas, get it organized, and turn it into projects, coding sessions, or time on my schedule has been useful.&lt;/p&gt;

&lt;p&gt;I also like that my Dot has its own computer in the cloud. My computer doesn't have to be on for everything. When the work does need my computer, I can still keep the conversation going from my phone. I can kick things off during the day and get a message when it needs something from me.&lt;/p&gt;

&lt;p&gt;Other assistants have been doing versions of this. It's still one of the things I like about this product, because it fits how I want to work.&lt;/p&gt;

&lt;p&gt;So far, I also haven't had the constant Telegram or computer-connection debugging I've associated with OpenClaw, especially around updates. That's been a welcome difference in my own experience. I want to spend time using the assistant rather than maintaining the setup around it.&lt;/p&gt;

&lt;p&gt;The gaps have become clearer as I've tried to do more through that same conversation.&lt;/p&gt;

&lt;h2&gt;
  
  
  More control of my computer
&lt;/h2&gt;

&lt;p&gt;One of the things I want to do is start an app or game task from my phone and have my Dot work through it on my computer. That includes opening the application, moving through its screens, or walking through a game while I watch.&lt;/p&gt;

&lt;p&gt;This week, we could launch coding tasks on my Windows computer and run commands. That didn't give the launched session the keyboard and mouse controls it needed for the interactive parts.&lt;/p&gt;

&lt;p&gt;We ran into that distinction while trying to work with Unity. Starting a task and opening a command window got us only partway to actually interacting with the game. I ended up sorting out permissions myself, and the behavior differed depending on how the tools were launched.&lt;/p&gt;

&lt;p&gt;I'd like the Dot-to-computer connection to make those capabilities clearer. Can this session run commands? Can it see the application? Can it click and type? What does it need from me before it can continue?&lt;/p&gt;

&lt;p&gt;I'm describing the setup we encountered, rather than claiming this can never work on Windows. But for the way I want to use my Dot, working through an application on my computer is a pretty important part of the job.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keep working with Claude after starting it
&lt;/h2&gt;

&lt;p&gt;There was another distinction in our coding workflow. My Dot could start a Codex session and continue working with it. We could also launch Claude with an initial prompt, but we didn't establish a reliable way to send follow-up instructions into that same Claude session.&lt;/p&gt;

&lt;p&gt;That's a limitation I'd really like to see addressed.&lt;/p&gt;

&lt;p&gt;Right now, I pay $200 a month for OpenAI and $200 a month for Anthropic. I use them for different things because I think both bring value. I'd prefer those tools to work together more easily.&lt;/p&gt;

&lt;p&gt;The workflow I want is straightforward: talk to my Dot, choose Codex or Claude for the work, and keep directing that work through the same conversation. Starting the session is useful. Being able to follow up, answer a question, or change direction is what would let me keep using it from my phone.&lt;/p&gt;

&lt;p&gt;Voice is especially relevant here. The place I most want voice is in my coding workflow, where I can explain what I want and respond as the work develops. A voice conversation that can keep the chosen coding session moving would be much more useful to me than having to switch interfaces whenever it needs another instruction.&lt;/p&gt;

&lt;h2&gt;
  
  
  Give me a result I can use on my phone
&lt;/h2&gt;

&lt;p&gt;We ran into this while working on the PocketQuests prototype. I wanted a live link so I could open it on my phone and try the interface.&lt;/p&gt;

&lt;p&gt;The first preview didn't work. We then tried putting it into a Space, but that didn't give me a usable interactive experience either. Eventually, we published it as a private Site, and that worked.&lt;/p&gt;

&lt;p&gt;Somewhere in that process, I was offered a ZIP file.&lt;/p&gt;

&lt;p&gt;I was on my phone. What am I supposed to do with a ZIP?&lt;/p&gt;

&lt;p&gt;The files existed, but they weren't in a form I could use for what I'd asked to do. I wanted to try the prototype, and I was still helping figure out how to get it in front of me.&lt;/p&gt;

&lt;p&gt;I'd like my Dot to account for the device I'm using when it delivers something. If I'm on my phone asking to review an interactive interface, a working link is the useful result. If it isn't sure what I can open, it can ask.&lt;/p&gt;

&lt;p&gt;What I want is less involvement in finding the delivery workaround. Building the prototype, making it available, and checking that I can use it should be part of the same request. In this case, we eventually found a working route. I'd like fewer of those steps to depend on me suggesting what to try next.&lt;/p&gt;

&lt;h2&gt;
  
  
  Publishing this article became another example
&lt;/h2&gt;

&lt;p&gt;Even publishing this article to three different sites turned into a Herculean task. Last week, my Dot handled the same workflow in one shot. This time, I kept getting pulled back in to troubleshoot it.&lt;/p&gt;

&lt;p&gt;I wanted the post on my own site, DEV, and Medium. We had already made the article and its cover in the cloud. Then the publishing attempt ran into a GitHub connection that couldn't write to my personal blog repository.&lt;/p&gt;

&lt;p&gt;My laptop already had working GitHub access. I had to ask why we weren't just using Codex on that computer. When we checked, the local Git account was correct and could access the repository. The next failure was getting the approved files onto that machine: the file-transfer helper failed on Windows while trying to save file metadata.&lt;/p&gt;

&lt;p&gt;Those were separate problems. One connection lacked access to the repository. A different part of the workflow couldn't finish transferring the files. My local Git setup was working.&lt;/p&gt;

&lt;p&gt;The assistant also made the experience worse. It kept switching between approaches and asking me to move files, instead of checking a complete route from the draft to the published pages before committing to it. I expected the Codex workflow to handle that work. I ended up telling it to start over locally and add the experience to the article.&lt;/p&gt;

&lt;p&gt;That inconsistency is the frustrating part. After a workflow has worked in one shot, I expect to be able to ask for it again. If the available tools or connections are different this time, I want my Dot to find that out early, explain the actual limitation, and carry on through a route it has checked.&lt;/p&gt;

&lt;p&gt;Writing the article, creating the cover, moving the files, and publishing the result all belonged to the same request. I don't want to become the person coordinating the handoffs between my assistant's computers.&lt;/p&gt;

&lt;h2&gt;
  
  
  Show me more of the developer workflow
&lt;/h2&gt;

&lt;p&gt;OpenAI has already published examples of &lt;a href="https://openai.com/index/work-with-codex-from-anywhere/" rel="noopener noreferrer"&gt;working with Codex from a phone&lt;/a&gt;. The &lt;a href="https://chatgpt.com/features/dots/" rel="noopener noreferrer"&gt;Dots page&lt;/a&gt; also shows developer work, including API migrations and app improvements. Those are useful examples.&lt;/p&gt;

&lt;p&gt;What I'd like to see in more detail is the Dot coordinating a longer development session. How do OpenAI's own developers kick off several coding agents, follow up with them, and move between their phones and computers while building software?&lt;/p&gt;

&lt;p&gt;I'd find a walkthrough of that whole process useful, including what happens when an agent needs another instruction or a preview doesn't open. If there's already a good Dot-specific demonstration of that workflow, send me a link. That's the part I'm trying to make work consistently in my own development day.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where does this go next
&lt;/h2&gt;

&lt;p&gt;Beyond the things I've run into this week, I'm interested in how personal assistants develop from here.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://help.openai.com/en/articles/20001530-getting-started-with-your-dot" rel="noopener noreferrer"&gt;OpenAI's current guidance&lt;/a&gt; says the first Dot is included in eligible plans, with an allowance for deeper work and extended limits for the first month after launch. I'm curious how usage and subscriptions will evolve from there.&lt;/p&gt;

&lt;p&gt;How much control will we have over the models our Dots use? If people have teams of Dots, how will those work together?&lt;/p&gt;

&lt;p&gt;The larger question for me is whether I'll need several personal assistants. Each company may develop its own approach, and I can imagine having reasons to use more than one, just as I already use more than one coding tool.&lt;/p&gt;

&lt;p&gt;But I'd rather have a primary assistant that can coordinate the tools and other assistants I choose. Maybe it starts additional assistants when they're useful. Maybe it hands a task to a different provider and stays involved while that work happens.&lt;/p&gt;

&lt;p&gt;I'm interested in seeing whether these products move toward that kind of cooperation or whether using several of them will mean maintaining several separate conversations. I already get different value from different providers. I'd like my personal assistant to help me make use of that.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>programming</category>
      <category>productivity</category>
      <category>devjournal</category>
    </item>
    <item>
      <title>I Ran My Dev Studio From the Couch</title>
      <dc:creator>Tatted Dev</dc:creator>
      <pubDate>Sat, 26 Sep 2026 23:42:17 +0000</pubDate>
      <link>https://dev.to/tatted_dev/i-ran-my-dev-studio-from-the-couch-3mhg</link>
      <guid>https://dev.to/tatted_dev/i-ran-my-dev-studio-from-the-couch-3mhg</guid>
      <description>&lt;p&gt;Today I spent hours working on HoneyDrunk Studios from my couch. Some of that time I was playing a video game. Some of it I was watching TV. Mostly, I was talking.&lt;/p&gt;

&lt;p&gt;I took my two dogs out. I played with them. I stepped away and came back. The day had room for ordinary life while the agents worked through the tasks I had handed off.&lt;/p&gt;

&lt;p&gt;I had Codex open in voice mode. I would describe something I wanted done, talk through a decision, or ask where we were on a piece of work. It would coordinate separate agent tasks, bring questions back, and keep the conversation moving while those tasks ran.&lt;/p&gt;

&lt;p&gt;By the end of the package refresh, 61 public NuGet packages had been published and verified. We had also worked through product ideas, organized what I wanted to focus on next, and gotten Claude to build and render a scene inside Blender.&lt;/p&gt;

&lt;p&gt;There were interruptions, wrong turns, and a review system that needed more attention than I wanted to give it. I had to repeat myself a few times. But there was also this fairly ridiculous moment where I realized I was watching my studio move forward while sitting in the same spot where I normally wind down for the night.&lt;/p&gt;

&lt;p&gt;I want to write about that part.&lt;/p&gt;

&lt;h2&gt;
  
  
  One conversation, several pieces of work
&lt;/h2&gt;

&lt;p&gt;HoneyDrunk is my place to build things: software, experiments, and eventually games. It is also a collection of repositories that can accumulate a very impressive amount of maintenance when I have been away for a while.&lt;/p&gt;

&lt;p&gt;Today started with some of that maintenance. Dependencies needed updating. There were open pull requests to sort through. Documentation had drifted. I wanted the whole thing brought back to a place where I could start building again without first spending another evening remembering what was broken.&lt;/p&gt;

&lt;p&gt;The package upgrades were a good example of why having separate workers mattered. These packages depend on each other. Updating a foundational package means publishing it, verifying that it is actually available, and then moving its consumers forward. There are tests, compatibility changes, version bumps, changelogs, and release workflows along the way.&lt;/p&gt;

&lt;p&gt;While that work progressed, I could keep talking about something else. We discussed the app I want to build. We worked through a learning curriculum. We looked at how the game-development tools could fit together. Every so often I would ask for an update, answer a question, or narrow the scope.&lt;/p&gt;

&lt;p&gt;The work still took hours. I just did not have to fill every gap between those steps by sitting at a terminal.&lt;/p&gt;

&lt;h2&gt;
  
  
  Claude was in the conversation too
&lt;/h2&gt;

&lt;p&gt;Partway through, I asked whether we could bring Claude into the workflow.&lt;/p&gt;

&lt;p&gt;That required actual setup. Claude Code needed authentication, and we had to verify which model I could use. Blender needed its add-on enabled, the connection started, and the agent-side configuration in place. It was not a button that magically gave every tool access to everything else.&lt;/p&gt;

&lt;p&gt;Once that was working, I asked for something visual: a cyberpunk piece around Tatted Dev and HoneyDrunk Studios.&lt;/p&gt;

&lt;p&gt;Then I watched the default cube disappear from Blender.&lt;/p&gt;

&lt;p&gt;Objects and lettering started appearing in the viewport as Claude's scripts ran. It built a scene, configured the render, and produced an image and a short animation. I could watch the changes happen in the application I would have been using myself.&lt;/p&gt;

&lt;p&gt;The first result was a neon signature piece. It looked cool, but seeing it helped me realize that I wanted something different: a developer in a lab, surrounded by strange experiments. So I said that. The next scene went in that direction, with a floating honeycomb core, terminals, glowing vessels, and a little robot.&lt;/p&gt;

&lt;p&gt;That was the part that really landed for me. I could react to something visible and direct the next iteration. A vague idea had become an editable Blender scene I could open and inspect.&lt;/p&gt;

&lt;p&gt;It was a creative experiment, not a finished game asset pipeline. There is a lot of distance between making one interesting scene and shipping a coherent game. Still, getting to that first tangible result made the next step feel much more approachable.&lt;/p&gt;

&lt;h2&gt;
  
  
  I was still doing the deciding
&lt;/h2&gt;

&lt;p&gt;The conversation was full of decisions.&lt;/p&gt;

&lt;p&gt;Which old proposals were worth keeping? Which repositories were we done with? What belonged in a reusable HoneyDrunk package? What should the app actually help someone do? Was a render technically finished but creatively pointed in the wrong direction?&lt;/p&gt;

&lt;p&gt;Those questions kept coming back to me.&lt;/p&gt;

&lt;p&gt;Our app discussion is a good example. We started with familiar self-improvement mechanics: goals, tasks, XP, levels. As we talked, the idea became more personal. A virtual version of you, in a world that reflects what you do, where you go, and what you want to become. We brought that together with an older exploration concept under the working name Pocket Quests.&lt;/p&gt;

&lt;p&gt;We did not build the app today. We clarified it, recorded the decisions, and set up an empty repository. That distinction matters. A productive conversation can leave you with a better direction without leaving you with a working product.&lt;/p&gt;

&lt;p&gt;I also wanted those decisions somewhere a fresh session could find them. A long voice conversation is useful in the moment, but tomorrow's agent needs the current state, the open questions, and the next action written down.&lt;/p&gt;

&lt;h2&gt;
  
  
  The awkward parts belong in the story
&lt;/h2&gt;

&lt;p&gt;Voice was not seamless.&lt;/p&gt;

&lt;p&gt;Sometimes the assistant misunderstood which project I meant. Sometimes it answered a nearby question instead of the one I had asked. Background audio got picked up. I stepped away, missed an update, and asked for it again. We occasionally needed to stop and get back to the actual question.&lt;/p&gt;

&lt;p&gt;The engineering had its own friction. Reviews queued up. Release automation hit an edge case. The automated reviewer also ran into command-access and incomplete-context problems; that repair was still ongoing as I drafted this.&lt;/p&gt;

&lt;p&gt;Those failures are part of the work. A passing build does not mean a release has reached NuGet, and a reviewer that could not inspect the change has not completed a review. I kept asking for the distinction between prepared, merged, published, and verified because those are different outcomes.&lt;/p&gt;

&lt;p&gt;The final release accounting was 52 package updates and nine first publications. Three unimplemented AI providers were explicitly held back. That is a much more useful result than a blanket claim that everything was upgraded.&lt;/p&gt;

&lt;h2&gt;
  
  
  More room to actually make things
&lt;/h2&gt;

&lt;p&gt;In an earlier post, I wrote about wanting to make games and how easy it is for me to retreat into infrastructure work because I already know how to do it.&lt;/p&gt;

&lt;p&gt;I still like infrastructure. Today's package refresh was necessary. But the Blender experiment gave me something different: a result in an area where I usually feel much less capable, created through a process I could participate in immediately.&lt;/p&gt;

&lt;p&gt;There will still be desk time. There will be debugging that needs my full attention, code I need to understand, and creative choices I have to learn to judge. I would not describe watching TV as a strategy for reviewing a risky change.&lt;/p&gt;

&lt;p&gt;But I do not need every part of the process to look like that. I can talk through an idea, send work off, come back to a question, and inspect a result. I can keep several useful threads moving without personally typing every step into every tool.&lt;/p&gt;

&lt;p&gt;For a solo developer trying to find time for the things he actually wants to make, that is a meaningful change.&lt;/p&gt;

&lt;p&gt;Tonight, it looked like a controller in my hands, a conversation still going, and a Blender scene that had not existed earlier in the day.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>programming</category>
      <category>productivity</category>
      <category>devjournal</category>
    </item>
    <item>
      <title>Why I'm Pointing at Game Dev in 2027</title>
      <dc:creator>Tatted Dev</dc:creator>
      <pubDate>Sat, 04 Jul 2026 13:52:01 +0000</pubDate>
      <link>https://dev.to/tatted_dev/why-im-pointing-at-game-dev-in-2027-45ll</link>
      <guid>https://dev.to/tatted_dev/why-im-pointing-at-game-dev-in-2027-45ll</guid>
      <description>&lt;p&gt;In the very first post on this blog, back in September 2025, I said I was the kind of person who would write a backend service one day and model a 3D world in Blender the next. I meant it. I have written a lot of backends since then. The Blender file is still empty.&lt;/p&gt;

&lt;p&gt;So this post is me calling my own shot. In 2027 I am going to make games. Not "someday." Not "when things calm down." A specific year, written down in public where I can be held to it.&lt;/p&gt;

&lt;p&gt;I want to be honest about what this post is and is not, because I have read enough breathless "I'm becoming a game developer" threads to know the shape of the lie. This is not a launch. There is no game. The tools are locked in, Unity for the engine and Blender for the art, but there is no genre picked and no clever mechanic I am sitting on. If you came for a trailer, I have nothing for you yet. What I have is a direction and a reason, and I have learned that saying the direction out loud is the part that makes it real.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why Now, and Why Not Yet
&lt;/h2&gt;

&lt;p&gt;I run a small software studio by myself, on top of roughly fifty hours a week at my day job, which is also software. Most of the hours I can spare go to the unglamorous plumbing that makes software work: services that talk to each other, data that has to survive a deploy, automation that watches the whole thing so I do not have to. I like that work. I am good at it. It is also, if I am being real with myself, the most comfortable possible place for me to hide.&lt;/p&gt;

&lt;p&gt;Games are the thing I keep circling and never landing on. Every time I write about my week, some game-dev tangent sneaks in: a tool I read about, an engine release, a devlog that made me jealous. The interest has been sitting there the whole time, load-bearing but unbuilt, like a wire I keep meaning to connect.&lt;/p&gt;

&lt;p&gt;The reason for 2027 instead of right now comes down to groundwork. There are two things on my 2026 plate that I want to see through before I split my attention, and both of them, weirdly, are runway for the games. More on that below. Pointing at 2027 gives me a real year to close those out and to start learning the parts of game development that my day job has taught me nothing about. Naming the date is the commitment. The gap before it is the training.&lt;/p&gt;




&lt;h2&gt;
  
  
  What a Backend Brain Actually Brings
&lt;/h2&gt;

&lt;p&gt;Here is the part I have to be careful with, because it is where the delusion lives. A lot of engineers assume that being senior in one kind of software makes them senior in games. It does not. Games are their own discipline, with their own hard-won craft, and plenty of brilliant backend engineers have bounced off them hard.&lt;/p&gt;

&lt;p&gt;But some of what I do every day does transfer, and it is worth being clear-eyed about which parts.&lt;/p&gt;

&lt;p&gt;I build systems for a living. State that has to stay consistent, components that have to talk without knowing too much about each other, code organized so that a change in one place does not detonate three others. A game is, under the pretty surface, a very demanding real-time system: an update loop running dozens of times a second, entities with state, systems that read and mutate that state in a strict order. The architecture instincts I have sharpened on distributed services are not wasted here. They are just pointed at a different clock.&lt;/p&gt;

&lt;p&gt;My tools overlap more than I expected, too. My studio is built mostly in C# and Rust, and Unity scripts in C#. So the leap is not from a standing start. I am not learning a new language and a new craft and a new mental model all at once. I get to keep the language and spend all my learning budget on the craft.&lt;/p&gt;

&lt;p&gt;And the honest flip side: none of that helps me with the parts that actually make a game good. Systems thinking will not teach me to make a jump feel satisfying. Clean architecture will not tell me why one level is fun and the next is a slog. That stuff is a different muscle entirely, and I have barely used it.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I Actually Have to Learn
&lt;/h2&gt;

&lt;p&gt;So let me name the gaps plainly, because a commitment post that pretends there are none is worthless.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Unity, end to end.&lt;/strong&gt; Not a weekend tutorial. Actually shipping something small all the way through: scene, input, state, build, the boring bits at the end that nobody blogs about.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Graphics, at least enough to not be helpless.&lt;/strong&gt; I do not need to write a renderer. I do need to understand what a shader is, why a frame costs what it costs, and how to fix it when the scene chugs. Right now that is close to a black box for me.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Game feel.&lt;/strong&gt; The invisible craft. Timing, feedback, weight, the difference between a control scheme that reads your mind and one that fights you. This is the one that scares me, because you cannot architect your way into it. You have to build a hundred bad versions and develop taste.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scope.&lt;/strong&gt; The graveyard of solo game dev is paved with projects that were too big. I already fight this instinct in my studio work. In games it kills you faster.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Art, or a strategy for not doing art.&lt;/strong&gt; I have a great deal of respect for art, and I wish I were naturally good at it. I am not, and right now I simply do not have the time to change that. I am enrolled in &lt;a href="https://cubebrush.co/mb/products/0dpzeg/art-school" rel="noopener noreferrer"&gt;ART School by Marc Brunet&lt;/a&gt;, and both the course and Marc as an instructor are excellent. Nobody is paying me to say that, I just believe it is that good, and I plan to work through it as time allows. Until the hours exist, my plan has two lanes. Lane one is buying assets: the marketplaces are full of good, affordable work made by people who did put in those years, and a solo dev who refuses purchased art is choosing pride over shipping. Lane two is the one that actually excites me. Blender shipped an official MCP server this year, a small bridge that lets an AI assistant talk to Blender's Python scripting directly, and directing AI assistants through exactly that kind of bridge is already how I spend my days. Anthropic even joined the Blender Development Fund, so this lane is being paved on purpose. Which means "model a 3D world in Blender" might end up looking a lot like the rest of my work: I describe what I want, Claude does the first pass in the viewport, and I learn enough Blender to judge the result and fix what is wrong with it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of these are unknowable. All of them take reps I have not put in.&lt;/p&gt;




&lt;h2&gt;
  
  
  How 2026 Is Quietly Building the Runway
&lt;/h2&gt;

&lt;p&gt;This is the part that convinced me the timing is right rather than arbitrary.&lt;/p&gt;

&lt;p&gt;The first thing eating my 2026 is that I build with AI coding agents all day. I do not write most of my code by hand anymore. I direct a couple of coding assistants, review what they produce, and steer. I have been building my own control panel to run them, a local cockpit I call HoneyHub. It is transforming into a full-blown IDE with everything I love and need to do my work, and there will be more posts on that soon. The whole way I work now is agent-first. That matters for games in a blunt way: the thing that historically stopped a solo backend dev from making a game was raw hours. One person cannot do engine work and gameplay and tooling and art pipeline in a normal lifetime. That math has changed. The bottleneck is shifting from "can I type all of this" to "do I have the taste to know what to build and the judgment to know when it is right." Taste and judgment are exactly what I would be spending 2027 developing.&lt;/p&gt;

&lt;p&gt;The second thing is NovOutbox, the first product my studio is taking to market. It is a hosted notification service built specifically for .NET developers: your app hands it a message, and it handles the queueing, the retries, and the delivery so you do not have to build that plumbing yourself. It sounds boring because it is, in the way good infrastructure is boring. What makes it matter for this post is its planned first customer: me. The app I want to build on top of it is codenamed Arc, a gamified self-improvement app that is still an idea being stress-tested rather than a thing being built. I have always loved gamified apps, the ones that borrow their shapes from games: seasons, progression, a little story wrapped around real effort. And that love is quietly another pathway to the same destination. A gamified app is a halfway step between the software I already know how to build and the games I want to make. It forces me to learn reward pacing, feedback, and why a loop feels good or hollow, and it lets me practice all of that without an engine in sight. If the idea survives its stress test, Arc probably gets built in 2027 too, right alongside the first game. That crowding does not scare me. They pull on the same new muscles, and I would rather train them together than in sequence.&lt;/p&gt;

&lt;p&gt;I did not plan it this way. I looked up one day and realized the runway had been building itself while I was busy pointing at other things.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Plan, Such As It Is
&lt;/h2&gt;

&lt;p&gt;I am deliberately not over-specifying this, because a rigid plan a year out is a fantasy. But the shape is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Finish what 2026 is for. NovOutbox in front of real users and HoneyHub grown into the IDE I want to live in. Those two get the rest of my year, and nothing else does.&lt;/li&gt;
&lt;li&gt;Go deep on Unity. The engine is picked and I am not window shopping. It speaks the C# I write every day and the learning material is bottomless. Engine-shopping is just procrastination with a progress bar, so I skipped it.&lt;/li&gt;
&lt;li&gt;Put real hours into Blender. The goal is comfort in the viewport rather than artistry. Blockouts, simple props, and enough fluency that when an agent hands me a scene over MCP I can tell whether it is good, fix it when it is close, and redo it myself when it is garbage.&lt;/li&gt;
&lt;li&gt;Ship something embarrassingly small. A single mechanic. One screen. The game-dev equivalent of "hello world," finished and actually done, before I let myself dream bigger.&lt;/li&gt;
&lt;li&gt;Build taste in public. Devlogs here, honestly, including the ugly early builds.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That last one is the real point of writing this now. I have a documented history of letting game dev stay a "someday" while I retreat into the systems work I already know how to do. Putting a year on it, in public, is how I make it cost something to chicken out.&lt;/p&gt;

&lt;p&gt;The Blender file is still empty. The Unity project does not exist yet. Consider this post the title screen. New game starts in 2027.&lt;/p&gt;

</description>
      <category>personal</category>
      <category>unity3d</category>
      <category>blender</category>
    </item>
    <item>
      <title>Building HoneyDrunk.Lore: My LLM Wiki and Daily News Blast</title>
      <dc:creator>Tatted Dev</dc:creator>
      <pubDate>Sun, 21 Jun 2026 12:05:16 +0000</pubDate>
      <link>https://dev.to/tatted_dev/building-honeydrunklore-my-llm-wiki-and-daily-news-blast-2pfl</link>
      <guid>https://dev.to/tatted_dev/building-honeydrunklore-my-llm-wiki-and-daily-news-blast-2pfl</guid>
      <description>&lt;p&gt;I know I am late to the party talking about LLM wikis after Andrej Karpathy first put the pattern in public, but here is how I implemented it in my own flow.&lt;/p&gt;

&lt;p&gt;The pattern clicked for me because it names a problem I had been living inside for a while. I read a lot. Model announcements, agent infrastructure posts, .NET and Azure updates, architecture writeups, indie software notes, game-dev tooling, security research, random high-signal threads that appear before the official writeup exists. All of it can matter to HoneyDrunk eventually, but almost none of it matters at the exact moment I read it.&lt;/p&gt;

&lt;p&gt;That is the place where normal bookmarks fall apart. Bookmarks preserve the link and lose the understanding. Chat history preserves the conversation and loses the durable structure. RAG gives me retrievable chunks, then makes me rebuild the synthesis every time I ask a question.&lt;/p&gt;

&lt;p&gt;The LLM wiki idea works at a different layer. Raw sources go in. An LLM reads them, extracts the useful claims, updates topic pages, links concepts together, tracks contradictions, and keeps a maintained markdown wiki between me and the source pile. The wiki becomes a compiled artifact. The model rediscovers context once, writes it down, and keeps it warm for the next query.&lt;/p&gt;

&lt;p&gt;Karpathy's &lt;a href="https://gist.github.com/karpathy/442a6bf555914893e9891c11519de94f" rel="noopener noreferrer"&gt;LLM Wiki gist&lt;/a&gt;, created on April 4, 2026, and the &lt;a href="https://x.com/karpathy/status/2039805659525644595" rel="noopener noreferrer"&gt;X post that pointed people at it&lt;/a&gt; gave the pattern a crisp public shape. My implementation became HoneyDrunk.Lore.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Shape of Lore
&lt;/h2&gt;

&lt;p&gt;HoneyDrunk.Lore is my compiled research knowledge surface for the studio. It is a flat-file wiki: markdown on disk, source-backed, Obsidian-friendly, versioned in git, and maintained by agents under an explicit schema.&lt;/p&gt;

&lt;p&gt;The pipeline is intentionally boring:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;raw sources
  -&amp;gt; compiled wiki
  -&amp;gt; AGENTS/schema
  -&amp;gt; query/output
  -&amp;gt; compile/lint
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;raw/&lt;/code&gt; layer is the evidence locker. Articles, notes, clipped pages, selected social posts, and research outputs land there as immutable source material. Once something is in &lt;code&gt;raw/&lt;/code&gt;, the rule is simple: add, do not rewrite history.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;wiki/&lt;/code&gt; layer is the compiled surface. It is where source material turns into concept pages, entity pages, topic indexes, confidence notes, and links. This is where claims get reinforced, weakened, superseded, or connected to other things I already know. The work that makes it more than a pile of summaries happens here.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;AGENTS.md&lt;/code&gt; file is the schema and operating manual. It tells the agent what the directories mean, what operations exist, how to treat citations, when to record gaps, how to handle contradictions, and what counts as a durable claim. This matters more than it sounds. Without the schema, an LLM wiki is just a helpful assistant making markdown. With the schema, it becomes a maintained knowledge system with rules.&lt;/p&gt;

&lt;p&gt;Queries write back into &lt;code&gt;output/&lt;/code&gt;. If I ask, "what does today's agent security signal imply for HoneyHub?" I want the answer filed as a dated artifact with citations, confidence, and gaps. Later compile passes can crystallize durable parts of that answer back into the wiki. The question becomes another source of learning instead of disappearing into a chat transcript.&lt;/p&gt;

&lt;p&gt;Compile and lint are the maintenance loop. Compile ingests unprocessed sources, reconciles claims, updates indexes, and promotes durable query outputs. Lint looks for orphans, stale claims, contradictions, weak sourcing, and missing links. The payoff is the rhythm. The wiki has a standing process for staying healthy, so it keeps working long after the first burst of writing.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Daily Blast
&lt;/h2&gt;

&lt;p&gt;The most recent work on Lore made the system feel less like a personal archive and more like an operator surface.&lt;/p&gt;

&lt;p&gt;Every day, Lore can produce a news blast for Discord. It reviews the latest saved source window, pulls out the useful signal, and turns it into a compact daily briefing. The blast stays small on purpose: a decision-support summary for the parts of the world that matter to HoneyDrunk right now.&lt;/p&gt;

&lt;p&gt;The shape is deliberately strict:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Top 10 web stories.&lt;/li&gt;
&lt;li&gt;Top 10 X/Twitter posts.&lt;/li&gt;
&lt;li&gt;Actual source URLs and tweet URLs.&lt;/li&gt;
&lt;li&gt;Two or three sentence summaries.&lt;/li&gt;
&lt;li&gt;A HoneyDrunk angle for why each item matters.&lt;/li&gt;
&lt;li&gt;Discord messages split before they hit platform limits.&lt;/li&gt;
&lt;li&gt;No private internal tool names in the public-facing blast.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That last line matters. The public blast should say what happened, where it came from, and why it matters. It should not leak the names of whatever private worker, script, lane, or local process happened to collect the source. Public signal should be clean signal.&lt;/p&gt;

&lt;p&gt;The X/Twitter lane is selective on purpose. I do not want Lore broadly crawling social media. That turns into noise immediately, and it makes the wiki depend on the most chaotic surface in the stack. But sometimes the useful signal really does appear there first: a primary-source launch post, a developer thread, an early warning, a discussion around a new agent pattern before the canonical blog post exists.&lt;/p&gt;

&lt;p&gt;So Lore treats X/Twitter as an early-signal lane that has to earn its way into the durable record. Selected posts can be captured with the actual tweet URL, marked as early signal, and compiled like any other raw source. When a durable source appears later, an official blog post, docs page, changelog, model card, transcript, or technical writeup, that source should join or supersede the social capture. The tweet stays useful as first-report context, and it has to wait for corroboration before it carries any authority.&lt;/p&gt;

&lt;p&gt;That distinction is the difference between "I saw a thing" and "the wiki knows a thing."&lt;/p&gt;

&lt;h2&gt;
  
  
  Why I Wanted This
&lt;/h2&gt;

&lt;p&gt;HoneyDrunk is not a single-product company in my head. It is a many-decade personal workshop: a computing platform, a studio, a lab, and a place to keep building strange useful things with AI agents over a long horizon.&lt;/p&gt;

&lt;p&gt;That changes what research means.&lt;/p&gt;

&lt;p&gt;I collect sources because they eventually affect decisions. Agent security patterns can shape how I build the agent tooling. Delivery and idempotency writeups can affect the messaging pipeline. Unity, asset pipeline, and game tooling posts can feed the game-dev work. Pricing, product, and solo-dev notes can change how I package the next experiment. A random model-routing benchmark might not matter today, then become exactly the missing context three months from now.&lt;/p&gt;

&lt;p&gt;Lore gives that material somewhere to compound.&lt;/p&gt;

&lt;p&gt;It is important to say what Lore is not. It is not HoneyDrunk governance. Governance lives in the architecture docs, ADRs, invariants, and repo contracts. Lore can inform a decision, but it does not make the decision official.&lt;/p&gt;

&lt;p&gt;It is also not agent memory. Agent memory is runtime state, preferences, session history, and working context. Lore is source-backed decision support. If Lore says something, it should be able to point at the sources, report confidence, and name the gaps. I do not want uncited wiki prose becoming doctrine just because an agent wrote it once.&lt;/p&gt;

&lt;p&gt;That boundary is the whole point. Lore is allowed to be useful because it is not allowed to be magic.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Part That Feels Different
&lt;/h2&gt;

&lt;p&gt;The big unlock is that the wiki is a built artifact. It gets compiled, maintained, and carried forward the way code is.&lt;/p&gt;

&lt;p&gt;Search asks, "can I find the thing again?" Lore asks, "has the thing been digested into the shape of what I already know?" Those are different questions. One retrieves. The other compounds.&lt;/p&gt;

&lt;p&gt;The daily Discord blast made that visible. A source goes from a raw article or tweet into a reviewed daily signal. The signal keeps the original URL attached. The summary is short enough to read on a phone. The HoneyDrunk angle makes the relevance explicit. Later, if the item is durable, compile can fold it into the wiki. If it is weak, lint can let it decay. If a better source appears, the older claim can be superseded instead of silently overwritten.&lt;/p&gt;

&lt;p&gt;That is the kind of system I want around a long-running workshop. Something that runs on its own rhythm, keeps what I have already learned, and stays warm between sessions.&lt;/p&gt;

&lt;p&gt;A maintained wiki. A daily signal review. Source URLs attached. Confidence kept visible. Gaps named instead of hidden.&lt;/p&gt;

&lt;p&gt;It is still rough in places, because every useful personal system starts rough. But it already changes the feel of reading. Sources do not just pass through my attention anymore. They have a place to land, a way to be compiled, and a chance to become part of the workshop's long memory without pretending to be governance or truth.&lt;/p&gt;

&lt;p&gt;That is the version of the LLM wiki pattern I needed: a source-backed research surface for a studio I plan to keep building in for decades.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>automation</category>
      <category>honeydrunk</category>
      <category>llm</category>
    </item>
    <item>
      <title>Two Terminals, One Screen: Building an Agent Cockpit That Tells the Truth About Cost</title>
      <dc:creator>Tatted Dev</dc:creator>
      <pubDate>Sun, 14 Jun 2026 15:06:56 +0000</pubDate>
      <link>https://dev.to/tatted_dev/two-terminals-one-screen-building-an-agent-cockpit-that-tells-the-truth-about-cost-m56</link>
      <guid>https://dev.to/tatted_dev/two-terminals-one-screen-building-an-agent-cockpit-that-tells-the-truth-about-cost-m56</guid>
      <description>&lt;p&gt;The setup that pushed me to build this was embarrassingly low-tech. Two terminal windows, side by side. One running the Claude Code CLI on a refactor. One running Codex on a different repo. Two live coding agents, two different repos, two different ways of telling me what just happened.&lt;/p&gt;

&lt;p&gt;Each window has its own dialect for reporting a turn. Claude prints a tidy result line with tokens and a dollar figure. Codex streams whole messages and reports tokens but no money. So at any given moment I had a pair of agents running and no single place that could answer the one question I actually cared about: how much have I spent today, and on what.&lt;/p&gt;

&lt;p&gt;I kept Alt-Tabbing between windows trying to add that up in my head. That is a stupid way to run a studio. So I built a cockpit.&lt;/p&gt;

&lt;p&gt;This post is about that cockpit, which I call HoneyHub, and specifically about the part that was much harder than the windowing: making a single cost view that pulls from two tools which measure cost in completely different units, without lying to me about either of them.&lt;/p&gt;




&lt;h2&gt;
  
  
  What HoneyHub Is
&lt;/h2&gt;

&lt;p&gt;HoneyHub is a local control panel for AI coding agents. It runs on my own machine, serves a small web app I open in a browser (phone or desktop), and drives the official command-line tools I already pay for: the Claude Code CLI and the Codex CLI. Nothing gets proxied through a server I rent. Nothing holds my subscription credentials. HoneyHub shells out to the same binaries I was running by hand, reads their output, and shows it on one screen.&lt;/p&gt;

&lt;p&gt;The core is a Rust crate (&lt;code&gt;bridge&lt;/code&gt;) that owns the child processes, and a TypeScript web app that renders the sessions. Between them is a contract: every backend, no matter how differently its CLI behaves, gets adapted into the same stream of events. A message event. A status event. A usage event. The web app never has to know which tool produced a given line.&lt;/p&gt;

&lt;p&gt;That contract is the whole trick, and the usage event is where it earns its keep.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Problem: Two Tools, Two Definitions of "Cost"
&lt;/h2&gt;

&lt;p&gt;Before I wrote a line of the cost view, I ran each CLI and read exactly what it hands back when a turn finishes. The two could not report cost more differently.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Claude Code&lt;/strong&gt; is the generous one. When a turn completes, it emits a &lt;code&gt;result&lt;/code&gt; line carrying exact input and output token counts and a real dollar figure (&lt;code&gt;total_cost_usd&lt;/code&gt;) computed on its side. I don't have to guess anything. I take the number it gives me.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Codex&lt;/strong&gt; reports exact token counts but no money at all. It tells me a turn used 1,000 input tokens and 1,000 output tokens, and then it stops. If I want a dollar figure, I have to multiply those tokens by a rate I supply myself. The tokens are ground truth. The dollars are something I compute, and only if I've configured a rate.&lt;/p&gt;

&lt;p&gt;So even with just these two, I had two genuinely different kinds of number staring at me:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A measured dollar figure that a vendor computed (Claude).&lt;/li&gt;
&lt;li&gt;A dollar figure I derive from exact tokens and a rate I supply (Codex).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Those are not the same thing, and pretending they are is how a dashboard starts lying to you. The lazy move is to mash both into one "total spend" number and call it a day. I refused to, because the moment a derived estimate and a measured figure get added together you can no longer tell which dollars are real. The whole reason I wanted a cockpit was to &lt;em&gt;trust&lt;/em&gt; the readout. A blurred total is worse than no total.&lt;/p&gt;

&lt;p&gt;So the design rule became: the system has to know, for every cost figure it shows, how that figure came to exist. And it has to refuse to blend a measurement with a guess.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Solution: Fidelity Is a First-Class Field
&lt;/h2&gt;

&lt;p&gt;Inside the bridge, every usage report carries a &lt;code&gt;fidelity&lt;/code&gt; tag. There are exactly three values, ranked from most trustworthy to least, and the order matters:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="c1"&gt;// filepath: crates/bridge/src/session.rs&lt;/span&gt;
&lt;span class="nd"&gt;#[derive(Debug,&lt;/span&gt; &lt;span class="nd"&gt;Clone,&lt;/span&gt; &lt;span class="nd"&gt;PartialEq,&lt;/span&gt; &lt;span class="nd"&gt;Eq,&lt;/span&gt; &lt;span class="nd"&gt;PartialOrd,&lt;/span&gt; &lt;span class="nd"&gt;Ord,&lt;/span&gt; &lt;span class="nd"&gt;Hash,&lt;/span&gt; &lt;span class="nd"&gt;Serialize,&lt;/span&gt; &lt;span class="nd"&gt;Deserialize)]&lt;/span&gt;
&lt;span class="nd"&gt;#[serde(rename_all&lt;/span&gt; &lt;span class="nd"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"snake_case"&lt;/span&gt;&lt;span class="nd"&gt;)]&lt;/span&gt;
&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;enum&lt;/span&gt; &lt;span class="n"&gt;UsageFidelity&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Exact&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;Derived&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;Estimated&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;Exact&lt;/code&gt; means the CLI handed me the dollars directly. &lt;code&gt;Derived&lt;/code&gt; means I computed dollars from exact tokens and a rate table. &lt;code&gt;Estimated&lt;/code&gt; means the numbers are a proxy and should never be read as money. Today's two backends use only the first two tiers: Claude reports &lt;code&gt;Exact&lt;/code&gt;, Codex reports &lt;code&gt;Derived&lt;/code&gt;. &lt;code&gt;Estimated&lt;/code&gt; is built in and waiting for the kind of backend that can only guess at what it spent, a tool whose CLI hands back neither a dollar figure nor a real token count. I have no such backend wired in right now, but the tier exists so that when one shows up, its guesses can never quietly pass as measurements. Every adapter is required to tag its usage signal honestly, and each one does it differently because each tool is different.&lt;/p&gt;

&lt;h3&gt;
  
  
  Claude: take the number, don't touch it
&lt;/h3&gt;

&lt;p&gt;The Claude adapter reads the &lt;code&gt;result&lt;/code&gt; line and copies the dollar figure straight through. No arithmetic. The only cleverness here is folding cache-read and cache-creation tokens into the total so an "exact" signal never under-reports:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="c1"&gt;// filepath: crates/bridge/src/adapters/claude_local.rs&lt;/span&gt;
&lt;span class="n"&gt;UsageSignal&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;backend&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nn"&gt;AgentBackend&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;ClaudeLocal&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="c1"&gt;// Claude Code reports exact tokens + USD; taken directly, no computation.&lt;/span&gt;
    &lt;span class="c1"&gt;// USD is never derived from a rate table for this backend.&lt;/span&gt;
    &lt;span class="n"&gt;fidelity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nn"&gt;UsageFidelity&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Exact&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;input_tokens&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;output_tokens&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;total_tokens&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;total_usd&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="nf"&gt;.get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"total_cost_usd"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="nf"&gt;.and_then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;Value&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;as_f64&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;confidence&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;Some&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;UsageConfidence&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;High&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="c1"&gt;// ...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Codex: exact tokens, dollars only if I asked for them
&lt;/h3&gt;

&lt;p&gt;The Codex adapter has the exact tokens but has to derive the dollars. The rate lookup is &lt;em&gt;injected&lt;/em&gt; into the adapter rather than hardcoded, so the bridge crate never bakes a vendor's prices into itself. And here is the part I care about most: if no rate is configured, the tokens stay exact and the dollar figure is simply absent. It is never fabricated.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="c1"&gt;// filepath: crates/bridge/src/adapters/codex_local.rs&lt;/span&gt;
&lt;span class="c1"&gt;// Tokens are exact; USD is derived from the injected rate table.&lt;/span&gt;
&lt;span class="c1"&gt;// With no rate configured the cost is absent — never fabricated.&lt;/span&gt;
&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;total_usd&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;match&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;input_tokens&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;output_tokens&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;Some&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nf"&gt;Some&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nf"&gt;Some&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;output&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rate&lt;/span&gt;&lt;span class="p"&gt;)(&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;output&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="k"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The default rate lookup wired in when nothing is configured returns &lt;code&gt;None&lt;/code&gt; for everything, on purpose:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="c1"&gt;// filepath: crates/bridge/src/adapters/codex_local.rs&lt;/span&gt;
&lt;span class="cd"&gt;/// The default lookup: no rate table wired, so USD is always absent (tokens exact).&lt;/span&gt;
&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;no_rate_lookup&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;UsdRateLookup&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nn"&gt;Arc&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(|&lt;/span&gt;&lt;span class="n"&gt;_model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_input&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_output&lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="nb"&gt;None&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So out of the box, Codex shows you exact tokens and an honest blank where the dollars would be. You opt into derived dollars by handing it a rate. That ordering is the whole point. When the system has no grounds for a number, it says "I don't know" and leaves the space empty. It never fills that space with a guess.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Centerpiece: The Two-Way Usage Table
&lt;/h2&gt;

&lt;p&gt;Here is the whole problem on one page. Same question, two tools, and what each one can actually tell you:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Backend&lt;/th&gt;
&lt;th&gt;Tokens&lt;/th&gt;
&lt;th&gt;Dollars&lt;/th&gt;
&lt;th&gt;Real billing unit&lt;/th&gt;
&lt;th&gt;Fidelity tag&lt;/th&gt;
&lt;th&gt;Shown as&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Claude Code&lt;/td&gt;
&lt;td&gt;exact&lt;/td&gt;
&lt;td&gt;exact (&lt;code&gt;total_cost_usd&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;dollars&lt;/td&gt;
&lt;td&gt;&lt;code&gt;exact&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;$0.0123&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Codex&lt;/td&gt;
&lt;td&gt;exact&lt;/td&gt;
&lt;td&gt;derived from a rate, or absent&lt;/td&gt;
&lt;td&gt;tokens&lt;/td&gt;
&lt;td&gt;&lt;code&gt;derived&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;≈$0.0030&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The "shown as" column is not cosmetic. The display layer formats every figure with a prefix that encodes its fidelity, so an estimate can never visually pass as a measurement:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// filepath: packages/ui/src/usageFormat.ts&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;usdPrefix&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fidelity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;UsageFidelity&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fidelity&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;estimated&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;~$&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fidelity&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;derived&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;≈$&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;$&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A plain &lt;code&gt;$&lt;/code&gt; is a number a vendor measured. A &lt;code&gt;≈$&lt;/code&gt; is a number I computed from a rate. A &lt;code&gt;~$&lt;/code&gt; is a soft guess. You can tell at a glance which is which without reading a legend. And when a rollup has no dollar figure to show at all, the cell renders an em dash rather than a fabricated zero:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// filepath: packages/ui/src/routes/spend/spendModel.ts&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;rollupCost&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rollup&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;UsageRollup&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rollup&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;totalUsd&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;formatUsd&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rollup&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;totalUsd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;rollup&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;fidelity&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;—&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  The Headline Number Sums Only What It Can Measure
&lt;/h2&gt;

&lt;p&gt;The cockpit shows one big number at the top: today's spend. The temptation, the entire reason this was hard, is to make that number include everything. It doesn't. The grounded total sums only the dollar figures that are real, which is to say &lt;code&gt;exact&lt;/code&gt; and &lt;code&gt;derived&lt;/code&gt;. Anything tagged &lt;code&gt;estimated&lt;/code&gt; is excluded by construction, so the day a guessing backend gets wired in it still cannot touch the headline:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// filepath: packages/ui/src/routes/spend/spendModel.ts&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;grounded&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;rollups&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rollup&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;rollup&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;fidelity&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;exact&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;rollup&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;fidelity&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;derived&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;anyGroundedUsd&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;grounded&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;some&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;rollup&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;rollup&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;totalUsd&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;groundedTotalUsd&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;anyGroundedUsd&lt;/span&gt;
  &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;grounded&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reduce&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;rollup&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;sum&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rollup&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;totalUsd&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When nothing grounded has been recorded yet, the headline is &lt;code&gt;undefined&lt;/code&gt;, not &lt;code&gt;$0.00&lt;/code&gt;. That distinction is deliberate. There is a real difference between "you have spent zero dollars" and "I have no measured spend to report," and a tool that collapses the second into the first is lying to you about a number you might make decisions on. So the view says "no measured spend yet" rather than inventing a confident zero.&lt;/p&gt;

&lt;p&gt;The Rust side enforces the same rule, with a comment that is basically the thesis of the whole feature:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="c1"&gt;// filepath: crates/bridge/src/session.rs&lt;/span&gt;
&lt;span class="cd"&gt;/// Sum of USD across **exact + derived** rollups only — a real dollar figure.&lt;/span&gt;
&lt;span class="cd"&gt;/// Estimated rollups are excluded so a guess can never inflate the headline&lt;/span&gt;
&lt;span class="cd"&gt;/// spend. `None` when no grounded signal reported USD.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two implementations, one in Rust on the host and one in TypeScript for the offline mock, and they agree on the rollup shape down to the ordering. That redundancy is on purpose too: the formatting honesty lives in tested code on both sides of the wire, so neither can quietly drift into showing an estimate as a fact.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I Learned
&lt;/h2&gt;

&lt;p&gt;The engineering lesson here is smaller than it looks and more useful than I expected: &lt;strong&gt;fidelity is data, not presentation.&lt;/strong&gt; My first instinct was to compute one blended dollar total and then sprinkle a "(estimated)" label on the UI somewhere. That fails the moment anything reads the number, because by then the measurement and the guess are already added together and you can't pull them back apart. Tagging every single usage signal at the source, the instant it leaves the adapter, is what makes the honest rollup even possible downstream. The guess and the measurement never touch.&lt;/p&gt;

&lt;p&gt;The product lesson is the one I keep coming back to. I built this so I would trust the readout, and a tool you can't trust is worse than the two terminal windows I started with, because at least the windows weren't pretending. A dashboard that confidently sums a measurement and a wild guess into one number trains you to distrust it, and then you go back to adding it up in your head anyway. The blank where Codex's dollars would be, and the "no measured spend yet" instead of a fake zero, are the features. They are the reason I look at the number and believe it.&lt;/p&gt;




&lt;h2&gt;
  
  
  Next Steps
&lt;/h2&gt;

&lt;p&gt;The cost view is one panel. The cockpit also surfaces live sessions, per-session diagnostics, and cross-session coaching, and the same fidelity discipline runs through all of it: a capability either exists for a backend or it doesn't, and the UI declares which honestly rather than faking a uniform experience. Codex can't do a live mid-turn reply, so the cockpit doesn't pretend it can; it routes the follow-up through a fresh resumed run instead. That honesty-by-construction posture is the through-line of the whole thing.&lt;/p&gt;

&lt;p&gt;What's left is the part no amount of code can do for me: running it for a real day of work and seeing whether the number at the top actually changes how I spend. That's the test that matters.&lt;/p&gt;

&lt;p&gt;If a tool is going to tell you what something cost, it owes you the truth about how sure it is. A blank is more honest than a guess wearing a dollar sign.&lt;/p&gt;

</description>
      <category>rust</category>
      <category>tooling</category>
      <category>honeydrunk</category>
      <category>ai</category>
    </item>
    <item>
      <title>Stop Receiving Webhooks, Start Polling: Rebuilding the Grid Review Runner</title>
      <dc:creator>Tatted Dev</dc:creator>
      <pubDate>Sun, 07 Jun 2026 16:44:28 +0000</pubDate>
      <link>https://dev.to/tatted_dev/stop-receiving-webhooks-start-polling-rebuilding-the-grid-review-runner-3ofp</link>
      <guid>https://dev.to/tatted_dev/stop-receiving-webhooks-start-polling-rebuilding-the-grid-review-runner-3ofp</guid>
      <description>&lt;p&gt;Every pull request I open gets an automated code review from an AI agent before I merge it. It reads the diff, checks it against my project's conventions, and posts its findings as a comment, like a second set of eyes that never sleeps. I run a whole studio's worth of repos solo, so that reviewer is load-bearing infrastructure. I call it the Grid Review Runner.&lt;/p&gt;

&lt;p&gt;One weekend it went quiet, and I didn't notice for two days.&lt;/p&gt;

&lt;p&gt;A pull request sat in one of my repos with no review on it. I was on the road. When I finally opened my laptop, I had no idea whether the reviewer had looked at the PR and decided it was fine, or whether it had never been told the PR existed in the first place. There was no log to check. There was no signal either way. The review just... wasn't there.&lt;/p&gt;

&lt;p&gt;That ambiguity is the whole story. The way the system worked back then, GitHub was supposed to &lt;em&gt;push&lt;/em&gt; a notification to my reviewer the instant a PR opened (a webhook, in the usual web plumbing sense). If that push went missing, the reviewer never woke up, and nothing told me it had been skipped. Did the reviewer decline, or did the notification never fire? I couldn't tell. The runner worked beautifully when I was sitting at my desk and fell apart the moment I wasn't. So I rebuilt the transport from the ground up. This post is about that rebuild, and the one architectural decision underneath it: stop waiting to be told about work, and go looking for it instead.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Old Shape: Inbound Webhook Over a Tunnel
&lt;/h2&gt;

&lt;p&gt;The Grid Review Runner is the piece of the HoneyDrunk Grid that runs an AI code review on pull requests. The review logic was never the problem. The rubric, the context-loading contract, the advisory posture all worked. The problem was the rail it rode in on.&lt;/p&gt;

&lt;p&gt;The original design (ADR-0044) looked like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;GitHub PR event
  → GitHub Action fires an HMAC-signed webhook
    → GitHub pushes it over a Cloudflare Tunnel
      → a local webhook bridge on my home server
        → which invokes OpenClaw/Codex to run the review
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every arrow there is a thing that has to be up at the exact moment a PR event happens. OpenClaw was the execution runtime. The Cloudflare Tunnel was its inbound rail. The home server (ADR-0081) was the always-on box that hosted both. On paper it's clean: GitHub talks to a narrow, signed, authenticated endpoint, and the endpoint does the work.&lt;/p&gt;

&lt;p&gt;In practice it was three single points of failure stacked on top of each other, and all three were coupled to me.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why It Failed
&lt;/h2&gt;

&lt;p&gt;Three failure modes compounded over the weeks after it landed:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Webhook delivery flaked.&lt;/strong&gt; GitHub redelivery is best-effort. On long PR sessions the bridge missed &lt;code&gt;synchronize&lt;/code&gt; events, and I had no inexpensive way to tell whether a missing review meant "the runner declined" or "the event never arrived."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tunnel uptime was coupled to me.&lt;/strong&gt; The Cloudflare Tunnel ran on the home server, and it was the single inbound path. When I traveled, when the box rebooted, when the tunnel daemon hiccuped, the rail was down. No rail, no review.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;OpenClaw process stability wasn't set-and-forget.&lt;/strong&gt; Crashes, session-credential expiry, dashboard-coupled state. "Did the review run?" was a non-trivial question to answer, every time.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The symptoms were all different. They had one root cause: an always-on inbound HTTP receiver on operator infrastructure does not fit an operator whose laptop travels.&lt;/p&gt;

&lt;p&gt;That shape works for a team with an on-call rotation absorbing the misses. It is mis-sized for one person and a single low-power box at home. I could have kept patching it (better redelivery handling, a hardier tunnel daemon, hardening OpenClaw against crashes), but every one of those is repair work on a house that doesn't fit the lot. The failure modes are intrinsic to the inbound-webhook shape on a solo setup. No tactical fix removes the category.&lt;/p&gt;

&lt;p&gt;So I stopped trying to fix the rail and changed the direction it ran.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Pivot: Push Becomes Pull
&lt;/h2&gt;

&lt;p&gt;Here is the load-bearing decision, and everything else is downstream of it: invert the transport. Instead of GitHub pushing a trigger into my infrastructure, my worker reaches out and pulls the trigger inward. The queue it pulls from is GitHub itself, so there's no server of mine that has to stay alive to hold it.&lt;/p&gt;

&lt;p&gt;The new critical path:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;GitHub PR event
  → cheap GitHub Action (no LLM work)
    → enqueue: add `needs-agent-review` label + structured queue comment
      → local worker polls GitHub on a 60s tick
        → claims one PR, runs the review locally under Codex CLI + Claude Code CLI
          → posts the verdict, swaps the label to its final state
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No inbound webhook. No tunnel for review traffic. No OpenClaw on the path at all.&lt;/p&gt;

&lt;p&gt;The GitHub Action got dumber on purpose. It does exactly two API calls and exits: it normalizes the PR's managed labels, adds &lt;code&gt;needs-agent-review&lt;/code&gt;, and upserts a single structured queue comment carrying the repo, PR number, head SHA, author class, and the resolved review config. It invokes no LLM. Its entire cost is the GitHub Actions minute floor: a label write, a comment write, done. No tokens are spent in the cloud, by design.&lt;/p&gt;

&lt;p&gt;The label is the queue index. The comment is the audit trail and the metadata carrier. That's the trick that makes the whole thing durable: the queue lives in GitHub. When my worker is offline (traveling, home server rebooting, machine powered off) PRs just accumulate in the &lt;code&gt;needs-agent-review&lt;/code&gt; state and wait. Nothing is lost, because there's no local queue to lose. When the machine comes back, the next tick reads GitHub's state and resumes. A powered-off worker just pauses the poll, and the events are still sitting in GitHub when it wakes up.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Worker (and Why the Scheduler Is Boring on Purpose)
&lt;/h2&gt;

&lt;p&gt;The worker is a PowerShell script invoked by Windows Task Scheduler on the same home-server hardware that used to host OpenClaw. The mini-PC survived the rebuild. Only OpenClaw, the webhook bridge, and the tunnel hostname came off it.&lt;/p&gt;

&lt;p&gt;One thing is easy to misread here, so I'll say it plainly: the headline is push → pull, and Task Scheduler is just the boring scheduler adapter sitting on top of that decision. The scheduler could be cron, systemd, or a cloud VM tomorrow without changing a single job spec. PowerShell + Task Scheduler is simply the lowest-friction option on a Windows box that's already on: no compile step, no runtime to deploy, sub-second per-tick boot. I deliberately wanted boring plumbing here.&lt;/p&gt;

&lt;p&gt;The Task Scheduler entry starts at logon and startup, repeats on a 60-second interval, restarts on failure, and refuses to overlap runs. The claim protocol uses GitHub's own primitives as the atomic operations:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;List&lt;/strong&gt; every PR carrying &lt;code&gt;needs-agent-review&lt;/code&gt; (one search call per tick, cheap at solo-dev volume).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Claim&lt;/strong&gt; the oldest one by swapping the label &lt;code&gt;needs-agent-review&lt;/code&gt; → &lt;code&gt;agent-review-in-progress&lt;/code&gt; and recording &lt;code&gt;claimed_by&lt;/code&gt; and &lt;code&gt;head_sha&lt;/code&gt; in the queue comment.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Run&lt;/strong&gt; the canonical &lt;code&gt;.claude/agents/review.md&lt;/code&gt; agent locally under subscription-auth CLIs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Post&lt;/strong&gt; the verdict and swap to &lt;code&gt;agent-reviewed&lt;/code&gt; (clean) or &lt;code&gt;changes-requested-by-agent&lt;/code&gt; (findings).&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If the worker crashes mid-review, a &lt;strong&gt;stale-claim sweep&lt;/strong&gt; runs at the top of each tick and releases any &lt;code&gt;agent-review-in-progress&lt;/code&gt; claim older than ~15 minutes back to the queue. If a new commit lands while a review is in flight, the head SHA in the comment no longer matches the claim, the now-stale verdict is discarded, and the next tick re-reviews against the new head. The marginal cost of that wasted run is operator-machine CPU only. The review runs under my existing subscription CLIs, so the LLM cost is $0.&lt;/p&gt;

&lt;p&gt;The job spec itself is just data. Here's the relevant slice of the review job:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="c"&gt;# filepath: infrastructure/workers/grid-agent-runner/config/jobs/grid-review.psd1&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;@{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nx"&gt;JobId&lt;/span&gt;&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"grid-review"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nx"&gt;TriggerKind&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"label-queue"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nx"&gt;Schedule&lt;/span&gt;&lt;span class="w"&gt;     &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;@{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Type&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"interval"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;IntervalSeconds&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;AtStartup&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="bp"&gt;$true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;AtLogon&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="bp"&gt;$true&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nx"&gt;PromptPath&lt;/span&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;".claude/agents/review.md"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nx"&gt;AgentCommands&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;@(&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;@{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Name&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"codex"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nx"&gt;Executable&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"codex"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nx"&gt;PromptStdin&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="bp"&gt;$true&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;@{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Name&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"claude"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Executable&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"claude"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;PromptStdin&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="bp"&gt;$true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;RiskClasses&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;@(&lt;/span&gt;&lt;span class="s2"&gt;"high"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Optional&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="bp"&gt;$true&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nx"&gt;WriteMode&lt;/span&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"comment-only"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The worker holds no secret-bearing inbound port. Its trust boundary collapsed to GitHub auth plus the local filesystem. There's no public ingress for review traffic to attack anymore, because there's no public ingress at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Learned
&lt;/h2&gt;

&lt;p&gt;The most useful thing this rebuild taught me: the discipline was right the whole time. The shape it rode on was the part that was broken.&lt;/p&gt;

&lt;p&gt;Everything that made the reviewer good came through untouched: the context-loading contract, the review rubric, the advisory posture (a missing review never blocks a merge). All of that is a property of the agent prompt, and the prompt didn't change. The transport only ever carried diffs. So I got to rip out an entire failure-prone rail and the actual review behavior survived intact. When you can cleanly separate "what hurts" from "what works," a scary-sounding rebuild turns out to be a narrow one.&lt;/p&gt;

&lt;p&gt;The second thing: inverting the transport let me delete infrastructure instead of adding it. OpenClaw, the webhook bridge, and the tunnel hostname all came off the home server (ADR-0088 finished that teardown). The HMAC webhook-signing secret got retired instead of perpetually rotated. The box itself stayed. The hardware was always fine. What was dead was the OpenClaw-centric way of organizing everything on top of it.&lt;/p&gt;

&lt;p&gt;And a bonus I didn't fully plan for: once the runner stopped being a one-off review script and became a job framework with a scheduler adapter, every other scheduled agent job I'd been parking on OpenClaw came home to it too. &lt;code&gt;hive-sync&lt;/code&gt;, the Lore sourcing and ingest passes, all of it. One substrate, one scheduler story, one recovery story.&lt;/p&gt;

&lt;h2&gt;
  
  
  Next Steps
&lt;/h2&gt;

&lt;p&gt;The worker runs two model families on every high-risk review: Codex CLI under one subscription, Claude Code CLI under another, synthesized into a single verdict. Why the Grid runs that many review layers (this runner plus CodeRabbit, Copilot, the Actions gates, and SonarQube) is a whole post on its own, and it's the next one I'm writing. This post was only ever about the transport.&lt;/p&gt;

&lt;p&gt;The lesson I'm keeping: when an always-on inbound receiver keeps failing for a solo operator, a more reliable receiver is rarely the fix. Stop receiving. Reach out and pull instead, and let the queue live somewhere that's already always on.&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>automation</category>
      <category>devops</category>
      <category>honeydrunk</category>
    </item>
    <item>
      <title>Treating Azure Infrastructure Like a Pull Request: Bicep and what-if</title>
      <dc:creator>Tatted Dev</dc:creator>
      <pubDate>Sun, 07 Jun 2026 16:42:51 +0000</pubDate>
      <link>https://dev.to/tatted_dev/treating-azure-infrastructure-like-a-pull-request-bicep-and-what-if-1nme</link>
      <guid>https://dev.to/tatted_dev/treating-azure-infrastructure-like-a-pull-request-bicep-and-what-if-1nme</guid>
      <description>&lt;p&gt;For a long time, the complete and authoritative definition of my production infrastructure was a scratch file of &lt;code&gt;az&lt;/code&gt; commands and the order I remembered running them in. I'd click through the Azure Portal, paste in a few commands, and the service would come up: a container app, a Key Vault, some App Configuration, a Service Bus namespace, the role assignments that wire them together. It worked.&lt;/p&gt;

&lt;p&gt;It worked because I remembered the sequence. There was an order to it, a set of steps that mattered, and the only place that order lived was in my head. That holds up fine when there's one environment and you set it up once. It falls apart the moment you need a second one. I run a lot of these services solo, so "I'll just remember it" had quietly become the load-bearing plan across every repo I own.&lt;/p&gt;

&lt;p&gt;This post is about the moment I needed a second environment, watched my from-memory process fall apart on contact, and decided to stop provisioning from memory entirely. The fix has a thesis I'll keep coming back to: infrastructure should be a diff you review before it lands, the same way code is.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Problem: I Provisioned From Memory
&lt;/h2&gt;

&lt;p&gt;Here's what "provisioning from memory" actually looked like in practice.&lt;/p&gt;

&lt;p&gt;My dev environment apps started life as hello-world placeholders. I'd create a container app in the Portal, get something trivial running, and then evolve it into the real service over time. Every time I did that, the real service needed things the placeholder didn't, and I added them by hand, one at a time, as I noticed they were missing.&lt;/p&gt;

&lt;p&gt;The list of things I noticed missing, the hard way:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The app's managed identity had no &lt;strong&gt;AcrPull&lt;/strong&gt; role, so it couldn't pull its own container image from the registry.&lt;/li&gt;
&lt;li&gt;It had no &lt;strong&gt;Key Vault&lt;/strong&gt; or &lt;strong&gt;App Configuration&lt;/strong&gt; read access, so it couldn't load any of its config or secrets.&lt;/li&gt;
&lt;li&gt;It had never actually &lt;strong&gt;joined the shared Container Apps environment&lt;/strong&gt;, so it wasn't wired into the logging and networking I assumed it was.&lt;/li&gt;
&lt;li&gt;The &lt;strong&gt;App Configuration store was never seeded&lt;/strong&gt; with the keys the app expected to find.&lt;/li&gt;
&lt;li&gt;Ingress was configured for &lt;strong&gt;port 80&lt;/strong&gt; with health probes pointed there, while the app actually listens on &lt;strong&gt;8080&lt;/strong&gt;. So it was "running" and failing every health check.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Every one of those was fixed live, by hand, while the thing was already deployed and broken. And the role assignments in particular fought me: the obvious &lt;code&gt;az role assignment create&lt;/code&gt; path kept throwing a &lt;code&gt;MissingSubscription&lt;/code&gt; error in my setup, so I ended up making the RBAC grants through raw &lt;code&gt;az rest&lt;/code&gt; calls against the management API instead. Each fix was a small thing. The sum of them was a service that only worked because I'd personally walked it through a recovery I never wrote down.&lt;/p&gt;

&lt;p&gt;The real failure was bigger than any single missing role. The complete, correct sequence for standing up one of my services lived only in my own hands, a thing I had done and could repeat but could never open and read. There was no artifact. There was no review. If someone asked me "what does it take to bring this service up in a fresh region for disaster recovery," the honest answer was "give me a day and let me remember."&lt;/p&gt;

&lt;p&gt;So when a second environment showed up on the roadmap, I didn't want to redo the from-memory walk. I wanted the walk to be a file.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Bicep Is (and What It Buys a Solo Dev)
&lt;/h2&gt;

&lt;p&gt;The tool I committed to is Bicep. If you haven't used it: Bicep is Microsoft's own infrastructure-as-code language for Azure. You describe the resources you want in a terse, typed DSL, and it compiles down to ARM, the JSON template format Azure deploys natively. There's no separate state file to manage the way Terraform has one; a deployment reconciles directly against whatever already exists in Azure. For an Azure-only shop run by one person, that's one fewer artifact to secure and back up, which I appreciated.&lt;/p&gt;

&lt;p&gt;I put all of it in one repo. Reusable modules are organized by concern (compute, data, secrets, messaging, observability) and a composition template stitches them together per environment. The modules are referenced by plain local file paths. I looked at publishing them to a Bicep registry and decided against it; for a single operator, a registry was overhead with no payoff, so modules just sit next to the templates that consume them and resolve off the filesystem at build time.&lt;/p&gt;

&lt;p&gt;The shared foundation for an environment composes the pieces that no single service owns:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// filepath: platform/main.bicep
module containerAppEnvironment '../modules/compute/containerAppEnvironment.bicep' = {
  name: 'platform-cae'
  params: {
    env: env
    location: location
    tags: tags
    logAnalyticsWorkspaceId: logAnalyticsWorkspace.outputs.id
  }
}

module appConfigurationStore '../modules/secrets/appConfigurationStore.bicep' = {
  name: 'platform-appcs'
  params: {
    env: env
    location: location
    tags: tags
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The interesting part is what's now impossible to forget. Remember the port-80-versus-8080 mess? Here's the relevant slice of the container app module:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// filepath: modules/compute/containerApp.bicep
@description('Ingress target port the container listens on.')
param targetPort int = 8080

resource containerApp 'Microsoft.App/containerApps@2025-07-01' = {
  name: 'ca-hd-${service}-${env}'
  identity: { type: 'SystemAssigned' }
  properties: {
    managedEnvironmentId: containerAppEnvironmentId
    configuration: {
      ingress: {
        external: externalIngress
        targetPort: targetPort
        transport: 'auto'
      }
    }
    // ...
  }
}

@description('Principal ID of the system-assigned managed identity. Grant AcrPull / Key Vault / App Configuration RBAC to this.')
output principalId string = containerApp.identity.principalId
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The port default is 8080, written down once, applied everywhere. The module won't deploy a container app without joining the managed environment, because &lt;code&gt;managedEnvironmentId&lt;/code&gt; is a required parameter. The managed identity's principal ID comes out as an output specifically so the role assignments (the AcrPull, the Key Vault read, the App Configuration read) get wired to it in the same deploy instead of being remembered later. Every individual thing that bit me by hand is now a property of a file that gets compiled and checked.&lt;/p&gt;

&lt;p&gt;That's the difference between memory and code: the file can't quietly skip a step.&lt;/p&gt;




&lt;h2&gt;
  
  
  Infra as a Diff You Review Before It Lands
&lt;/h2&gt;

&lt;p&gt;Having the resources in Bicep is half of it. The half I actually care about is what happens when I change them.&lt;/p&gt;

&lt;p&gt;Azure has a command called &lt;code&gt;what-if&lt;/code&gt;. It's a dry run: you point it at a template and it tells you exactly what a real deployment &lt;em&gt;would&lt;/em&gt; do (what it would create, what it would modify, what it would delete) without touching anything. It prints a diff of your infrastructure.&lt;/p&gt;

&lt;p&gt;My deploy pipeline is built around that command. Before it applies anything, it runs the same three preflight steps every time: compile the Bicep, lint it, then run &lt;code&gt;what-if&lt;/code&gt;. Only after that does it consider applying. And whether it applies at all is a switch:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# filepath: .github/workflows/job-deploy-bicep.yml&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;What-if preflight&lt;/span&gt;
  &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
    &lt;span class="s"&gt;az deployment group what-if \&lt;/span&gt;
      &lt;span class="s"&gt;--resource-group "$RG" \&lt;/span&gt;
      &lt;span class="s"&gt;--parameters "$PARAMS"&lt;/span&gt;

&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Apply deployment&lt;/span&gt;
  &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ inputs.what-if-only != &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="s"&gt; }}&lt;/span&gt;
  &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
    &lt;span class="s"&gt;az deployment group create \&lt;/span&gt;
      &lt;span class="s"&gt;--name "hd-infra-${ENV}-${RUN_ID}-${RUN_ATTEMPT}" \&lt;/span&gt;
      &lt;span class="s"&gt;--resource-group "$RG" \&lt;/span&gt;
      &lt;span class="s"&gt;--parameters "$PARAMS"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I trigger deploys in two modes. &lt;strong&gt;Plan&lt;/strong&gt; runs the build, lint, and &lt;code&gt;what-if&lt;/code&gt;, then stops and tells me nothing was applied. &lt;strong&gt;Apply&lt;/strong&gt; does all of that and then actually deploys. The default is plan. So my normal loop is: run plan, read the diff, confirm it's doing what I expect, then re-run as apply.&lt;/p&gt;

&lt;p&gt;Reading the diff is the whole point. The discipline I hold myself to is simple: every resource that should already exist needs to show up as &lt;strong&gt;no change&lt;/strong&gt;. If something I wasn't expecting shows as Modify or, worse, Delete, that's the signal to stop and look before anything happens to live infrastructure. The dry run turns "I think this is safe" into "I can see that this is safe."&lt;/p&gt;

&lt;p&gt;That's why I call it treating infrastructure like a pull request. A pull request is a diff you read before you merge it. &lt;code&gt;what-if&lt;/code&gt; is a diff you read before you deploy it. Same loop, same safety, just pointed at Azure resources instead of source files. The change stops being an action I take from memory and becomes a proposal I review.&lt;/p&gt;

&lt;p&gt;Two supporting pieces, each worth only a sentence here because each could be its own post. The pipeline authenticates to Azure with GitHub's OIDC federation, so there are no long-lived Azure credentials stored anywhere; the deploy identity is allowed to provision resources but not to read secret values. And promotion across environments is gated: dev is easy, while staging and prod sit behind GitHub Environment approvals so nothing reaches them without a deliberate sign-off. Both of those deserve their own write-ups later.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Payoff
&lt;/h2&gt;

&lt;p&gt;The real fix was turning that memory into an artifact, something I could read, review, and re-run. Trying to remember harder was never going to survive past one environment.&lt;/p&gt;

&lt;p&gt;Once the infrastructure was a set of files, the part that used to terrify me (standing up another environment) became boring in the best way. A new environment is a new parameter file pointed at the same modules. The sequence I used to carry in my head is now enforced by required parameters and module composition. The container app can't come up without joining its environment. The identity comes out as an output so its roles get granted in the same breath. The port is 8080 because the file says so.&lt;/p&gt;

&lt;p&gt;And the dry run changed how changes &lt;em&gt;feel&lt;/em&gt;. Before, every &lt;code&gt;az&lt;/code&gt; command against a real environment was a small leap of faith. Now I get to look first. The worst-case surprise moved from "I deleted something in production" to "the plan showed something I didn't expect, so I didn't run apply." That's a much better worst case to live with when there's no one else around to catch the mistake.&lt;/p&gt;




&lt;h2&gt;
  
  
  What's Left to Codify
&lt;/h2&gt;

&lt;p&gt;There's more to build on top of this. The role assignments and App Configuration seeding that I described as the painful hand-fixes are exactly the parts I want fully codified into the per-service templates next, so that "fresh environment" means one plan-and-apply with nothing left to remember. And the environment-gated promotion model deserves its own post, because the approval gates are doing real work.&lt;/p&gt;

&lt;p&gt;But the load-bearing change already happened. My infrastructure stopped being a thing I remember how to do and became a thing I can read. Provision from a file, not from memory. Look at the diff before it lands.&lt;/p&gt;

</description>
      <category>devops</category>
      <category>azure</category>
      <category>architecture</category>
      <category>honeydrunk</category>
    </item>
    <item>
      <title>A Field Guide to Hand-Provisioning Azure Functions and Container Apps</title>
      <dc:creator>Tatted Dev</dc:creator>
      <pubDate>Sun, 07 Jun 2026 16:41:06 +0000</pubDate>
      <link>https://dev.to/tatted_dev/a-field-guide-to-hand-provisioning-azure-functions-and-container-apps-3n4</link>
      <guid>https://dev.to/tatted_dev/a-field-guide-to-hand-provisioning-azure-functions-and-container-apps-3n4</guid>
      <description>&lt;p&gt;I asked the Azure CLI for my function app's hostname and it handed me back &lt;code&gt;null&lt;/code&gt;. The app was running. The hostname existed. The tool whose entire job is to tell me about the app just refused to say what it was. That was the first of a week's worth of moments where the platform looked me in the eye and lied.&lt;/p&gt;

&lt;p&gt;I'd been standing up cloud services on Azure by hand, one small repo at a time, some running as Azure Functions and some as Container Apps. I do this solo, so each one starts as a hello-world placeholder, gets something trivial running, then grows into the real service as I add whatever it turns out to need. The work itself was fine. The part that cost me evenings was Azure's own tooling handing me wrong answers while I did it. I'd ask the CLI a direct question and get back &lt;code&gt;null&lt;/code&gt;. I'd hit the hostname the docs implied and get a connection that failed completely. I'd reach for the official deploy action and find it blocked. Every one of these burned an evening of squinting at output that made no sense, usually around midnight, usually while googling the exact error and finding nothing.&lt;/p&gt;

&lt;p&gt;This is the post I wish I'd found on one of those nights. Five concrete gotchas, each with the symptom I actually saw and the fix that actually worked. If you're hand-standing-up Functions or Container Apps on Azure and the platform is gaslighting you, start here.&lt;/p&gt;

&lt;p&gt;A bit of grounding first, because this blog gets read by people who don't know my stack. &lt;strong&gt;Flex Consumption&lt;/strong&gt; is a newer Azure Functions hosting plan, the pay-for-what-you-use serverless tier with faster scaling than the old Consumption plan. &lt;strong&gt;Container Apps&lt;/strong&gt; is Azure's managed platform for running containers without standing up Kubernetes yourself. A &lt;strong&gt;managed identity&lt;/strong&gt; is an Azure-issued identity attached to your app so it can authenticate to other Azure services without you storing a password. An &lt;strong&gt;RBAC role assignment&lt;/strong&gt; is the grant that says "this identity is allowed to do this specific thing." Those four show up in every story below.&lt;/p&gt;




&lt;h2&gt;
  
  
  Gotcha 1: &lt;code&gt;az functionapp show&lt;/code&gt; returns null on Flex Consumption
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Symptom.&lt;/strong&gt; I deployed a Function on the Flex Consumption plan and went to grab its basic facts the obvious way:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;az functionapp show &lt;span class="nt"&gt;--name&lt;/span&gt; &amp;lt;app&amp;gt; &lt;span class="nt"&gt;--resource-group&lt;/span&gt; &amp;lt;rg&amp;gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--query&lt;/span&gt; &lt;span class="s2"&gt;"{host:defaultHostName, state:state, names:hostNames}"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Back came &lt;code&gt;null&lt;/code&gt; for &lt;code&gt;defaultHostName&lt;/code&gt;, &lt;code&gt;null&lt;/code&gt; for &lt;code&gt;state&lt;/code&gt;, &lt;code&gt;null&lt;/code&gt; for &lt;code&gt;hostNames&lt;/code&gt;. Not an error. Not an empty string. Just null fields on an app I could see existed in the Portal.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cause.&lt;/strong&gt; On Flex Consumption those properties simply aren't populated on the object &lt;code&gt;az functionapp show&lt;/code&gt; reads. The command works fine elsewhere. On this plan it just doesn't surface those fields through that path, so you ask a perfectly reasonable question and the tool answers with a confident nothing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix.&lt;/strong&gt; Go around it. Read the resource directly through the generic resource commands or the ARM REST API:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Generic resource read returns the populated properties&lt;/span&gt;
az resource show &lt;span class="nt"&gt;--ids&lt;/span&gt; &amp;lt;&lt;span class="k"&gt;function&lt;/span&gt;&lt;span class="nt"&gt;-app-resource-id&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--query&lt;/span&gt; &lt;span class="s2"&gt;"properties.defaultHostName"&lt;/span&gt;

&lt;span class="c"&gt;# Or hit ARM directly&lt;/span&gt;
az rest &lt;span class="nt"&gt;--method&lt;/span&gt; get &lt;span class="nt"&gt;--url&lt;/span&gt; &lt;span class="s2"&gt;"&amp;lt;function-app-resource-id&amp;gt;?api-version=&amp;lt;api-version&amp;gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;az resource show&lt;/code&gt; and &lt;code&gt;az rest&lt;/code&gt; read the underlying resource record, where the real values live. The lesson I took from this: when a high-level &lt;code&gt;az&lt;/code&gt; command hands you null on a newer plan, drop down a layer before you assume your app is broken. The app was fine. The convenience command just couldn't see it.&lt;/p&gt;




&lt;h2&gt;
  
  
  Gotcha 2: the hostname you expect returns HTTP 000
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Symptom.&lt;/strong&gt; Once I had a hostname, I tried to hit the app at the address you'd guess from years of Azure muscle memory: &lt;code&gt;&amp;lt;app-name&amp;gt;.azurewebsites.net&lt;/code&gt;. The request didn't return a 404. It didn't return a 503. It returned HTTP 000, which is curl's way of telling you the connection never completed at all. The host wasn't answering on that name, period.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cause.&lt;/strong&gt; Flex Consumption apps answer only on a regional hostname shaped like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;app-name&amp;gt;-&amp;lt;hash&amp;gt;.&amp;lt;region&amp;gt;-01.azurewebsites.net
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The bare &lt;code&gt;&amp;lt;app-name&amp;gt;.azurewebsites.net&lt;/code&gt; you'd expect, the one every older tutorial uses, just isn't a live endpoint for these apps. So you're hitting an address that resolves to nothing useful and getting a dead connection for your trouble.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix.&lt;/strong&gt; Use the regional hostname, and get it from the resource itself rather than constructing it by hand:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;az resource show &lt;span class="nt"&gt;--ids&lt;/span&gt; &amp;lt;&lt;span class="k"&gt;function&lt;/span&gt;&lt;span class="nt"&gt;-app-resource-id&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--query&lt;/span&gt; &lt;span class="s2"&gt;"properties.defaultHostName"&lt;/span&gt; &lt;span class="nt"&gt;--output&lt;/span&gt; tsv
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This one is nasty because it compounds with Gotcha 1. The tool that should hand you the correct hostname returns null, so you fall back to guessing, and the address you guess refuses the connection. Two failures stacked: the right answer is hidden, and the wrong answer fails silently. Once I knew the regional shape was the only one that answers, the dead connections stopped being a mystery.&lt;/p&gt;




&lt;h2&gt;
  
  
  Gotcha 3: the official deploy GitHub Action was blocked
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Symptom.&lt;/strong&gt; I was deploying my Functions from GitHub Actions using &lt;code&gt;azure/functions-action&lt;/code&gt;, the official Microsoft-published action for the job. At some point it became unusable in my setup for terms-of-service reasons. The action I'd built my deploy step around was off the table.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cause.&lt;/strong&gt; This one came down to a licensing and terms constraint on using that action in my context. Doesn't matter how clean your workflow is if the step it depends on is contractually off the table.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix.&lt;/strong&gt; Drop the action and deploy with the &lt;code&gt;az&lt;/code&gt; CLI directly inside the workflow. The CLI does the same job without the dependency on a third-party action:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# filepath: .github/workflows/deploy.yml&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Deploy function app&lt;/span&gt;
  &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
    &lt;span class="s"&gt;az functionapp deployment source config-zip \&lt;/span&gt;
      &lt;span class="s"&gt;--name "$APP_NAME" \&lt;/span&gt;
      &lt;span class="s"&gt;--resource-group "$RG" \&lt;/span&gt;
      &lt;span class="s"&gt;--src "$ZIP_PATH"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I made this change in my Actions repo and never looked back (the commit that dropped the action lives in my history as &lt;code&gt;17b7362&lt;/code&gt;). The broader takeaway: a deploy step that leans on a single vendor action is a step that can disappear out from under you. The &lt;code&gt;az&lt;/code&gt; CLI is the stable substrate. When an action gets blocked or deprecated, the CLI underneath it almost always still does the work.&lt;/p&gt;




&lt;h2&gt;
  
  
  Gotcha 4: Container Apps probes defaulted to port 80, my app listens on 8080
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Symptom.&lt;/strong&gt; A hand-provisioned Container App came up and looked dead. Ingress was reachable in name but every health probe failed, so the platform kept treating the revision as unhealthy and cycling it. The container itself was running fine. The platform just couldn't get a healthy response out of it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cause.&lt;/strong&gt; The app came up with ingress and health probes pointed at port 80. My container listens on 8080. So every probe knocked on a door nobody was behind, marked the app unhealthy, and the app looked broken while being completely fine internally.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix.&lt;/strong&gt; Set the ingress target port and the probe ports to where the app actually listens:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;az containerapp ingress update &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--name&lt;/span&gt; &amp;lt;app&amp;gt; &lt;span class="nt"&gt;--resource-group&lt;/span&gt; &amp;lt;rg&amp;gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--target-port&lt;/span&gt; 8080
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And make sure any liveness and readiness probes point at 8080 too, not the default. This is a small fix that wastes a lot of time, because "running but failing every health check" presents exactly like a crashed app from the outside. The first thing I check now on any new Container App is whether the probe port and the listen port agree.&lt;/p&gt;




&lt;h2&gt;
  
  
  Gotcha 5: role assignments threw MissingSubscription, so I used az rest
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Symptom.&lt;/strong&gt; A freshly hand-made app has none of the permissions it needs. I went to grant its managed identity the roles it required:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;AcrPull&lt;/strong&gt;, so it can pull its own container image from the registry.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Key Vault&lt;/strong&gt; access, so it can read its secrets (Key Vault is Azure's managed secret store).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;App Configuration Data Reader&lt;/strong&gt;, so it can read its config (App Configuration is Azure's managed settings store).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The obvious command for each grant is &lt;code&gt;az role assignment create&lt;/code&gt;. In my environment it threw a &lt;code&gt;MissingSubscription&lt;/code&gt; error and refused to make the assignment. The subscription context was set, other commands worked, but this specific path insisted it couldn't find a subscription.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cause.&lt;/strong&gt; I never got a clean root cause for the &lt;code&gt;MissingSubscription&lt;/code&gt; failure on that command in my setup. What I could prove was that the same role assignment went through fine when I made it against the ARM REST API directly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix.&lt;/strong&gt; Create the role assignments through &lt;code&gt;az rest&lt;/code&gt;, calling the management API yourself:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;az rest &lt;span class="nt"&gt;--method&lt;/span&gt; put &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--url&lt;/span&gt; &lt;span class="s2"&gt;"&amp;lt;scope&amp;gt;/providers/Microsoft.Authorization/roleAssignments/&amp;lt;guid&amp;gt;?api-version=&amp;lt;api-version&amp;gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--body&lt;/span&gt; &lt;span class="s1"&gt;'{
    "properties": {
      "roleDefinitionId": "&amp;lt;role-definition-id&amp;gt;",
      "principalId": "&amp;lt;app-managed-identity-principal-id&amp;gt;",
      "principalType": "ServicePrincipal"
    }
  }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It's more verbose, you build the request body by hand, but it works where the convenience command wouldn't.&lt;/p&gt;

&lt;p&gt;The deeper problem here is the shape of the whole situation, well beyond the one error. A freshly hand-made app is missing every one of these by default: AcrPull, Key Vault access, App Configuration Data Reader, joining the managed environment, and having its App Configuration store seeded with the keys it expects. Nothing tells you any of them are missing. The app doesn't announce "I have no permission to pull my image." It just fails quietly, one missing grant at a time, and you discover each one by watching the app fail in a slightly new way and reasoning backward to what it must have needed.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I Took From All of It
&lt;/h2&gt;

&lt;p&gt;Every fix above was applied live, by hand, to a running dev environment, discovered by trial and error at the moment the thing broke. None of it was in a runbook, because there was no runbook. The whole sequence lived in trial-and-error scar tissue and nowhere a teammate, or a future me, could go look it up.&lt;/p&gt;

&lt;p&gt;There's a thread running through all five. The platform's convenient answer was the wrong one often enough that I learned to drop a layer down by reflex. &lt;code&gt;az functionapp show&lt;/code&gt; lies on Flex, so read the resource directly. The friendly hostname is dead, so pull the regional one off the resource. The official action is blocked, so call the CLI underneath it. The defaults point at the wrong port, so set them explicitly. The role-assignment helper fails, so hit ARM directly. The generic, lower-level path was the reliable one almost every time.&lt;/p&gt;

&lt;p&gt;This pile of hand-fixes is exactly what pushed me to describe my infrastructure in Bicep instead, so the next environment is reviewed as code before it exists rather than debugged after (that's its own post). But that's the cure. This post is the disease, written down, so the next person googling &lt;code&gt;HTTP 000 azurewebsites.net flex consumption&lt;/code&gt; at midnight finds something.&lt;/p&gt;

&lt;p&gt;When the platform tells you null, don't believe it. Go ask the resource.&lt;/p&gt;

</description>
      <category>azure</category>
      <category>devops</category>
      <category>serverless</category>
      <category>honeydrunk</category>
    </item>
    <item>
      <title>Four Reviewers and a Gauntlet: Verifying AI-Authored Code</title>
      <dc:creator>Tatted Dev</dc:creator>
      <pubDate>Sun, 07 Jun 2026 16:39:22 +0000</pubDate>
      <link>https://dev.to/tatted_dev/four-reviewers-and-a-gauntlet-verifying-ai-authored-code-2eob</link>
      <guid>https://dev.to/tatted_dev/four-reviewers-and-a-gauntlet-verifying-ai-authored-code-2eob</guid>
      <description>&lt;p&gt;Almost none of the code I ship was typed by me. I describe what I want, an AI coding agent writes the implementation, and I read what comes back. That one fact, still strange to say out loud, has quietly reorganized my entire job. I run a studio's worth of services solo, and across all of those repos the bulk of the actual authoring now happens on the other side of a prompt.&lt;/p&gt;

&lt;p&gt;That changes the job. When you're the one writing every line, your judgment lives in the writing. You feel the design as you type it. When an AI writes the lines, that feeling is gone, and you can fool yourself into thinking the work is done the moment the code looks plausible. Plausible is exactly the failure mode. AI-authored code is usually fluent, often correct, and occasionally confidently wrong in a way that reads great and breaks in production.&lt;/p&gt;

&lt;p&gt;So my judgment had to move somewhere. It moved to the verification layer. These days my attention goes to one thing: making sure what lands is solid. Every pull request in my repos runs a gauntlet of layered review before it's allowed to merge. This post is about that gauntlet, why it has so many layers, and why the economics of it actually favor a one-person shop.&lt;/p&gt;




&lt;h2&gt;
  
  
  What "AI Writes the Code" Means Here
&lt;/h2&gt;

&lt;p&gt;Quick grounding before the jargon, because this is a public post and the setup is specific.&lt;/p&gt;

&lt;p&gt;I'm a solo founder building HoneyDrunk Studios. The bulk of the code authoring is done by AI coding agents (think command-line tools that take a task and produce a diff). My leverage is in two places now: specifying intent clearly enough that the agent builds the right thing, and verifying the output well enough that I trust it in production. The first half is prompts and design notes. The second half is the review gauntlet.&lt;/p&gt;

&lt;p&gt;A couple of terms I'll use repeatedly:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A &lt;strong&gt;CI gate&lt;/strong&gt; is an automated check that runs on every pull request and has to pass before the code can merge. Build the project, run the tests, check formatting, measure test coverage. If any required gate fails, the merge button stays off. CI stands for continuous integration; for my purposes it's just "the robot that won't let bad code in."&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;pull request&lt;/strong&gt; (PR) is the unit of change. One proposed diff, opened for review, merged when it passes. Every layer below operates on a PR.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;With that established, here are the layers.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Layers
&lt;/h2&gt;

&lt;p&gt;Every PR I open can face up to five distinct reviewers before it merges. Each one is good at something different, and the overlap is the point.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. CodeRabbit.&lt;/strong&gt; An AI service that reviews pull requests. It reads the diff, comments inline on specific lines, flags likely bugs and convention violations, and summarizes the change. I deploy it across all my repos from one org-level configuration rather than copying a config file into every repo by hand. One file governs the whole studio, which matters when "the whole studio" is dozens of repos and one person maintaining them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. GitHub Copilot review.&lt;/strong&gt; GitHub's own built-in AI reviewer. It runs inside the GitHub PR interface and posts its own read of the diff. It's a second AI opinion from a different vendor with a different training and a different rubric, sitting right where I'm already looking.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. My own local AI review runner.&lt;/strong&gt; This is the one I built. It runs an AI code review on my own hardware, under my existing CLI subscriptions, and posts its verdict as a PR comment. On high-risk changes it runs the review through two different model families, Codex and Claude, and synthesizes their findings into one verdict. Two independent models, two independent blind spots. I wrote a whole separate post on how this runner's transport works (it polls GitHub and treats labels as a durable queue rather than waiting for a webhook); if you want the plumbing, read &lt;a href="https://tatteddev.com/blog/pull-based-grid-review-runner/" rel="noopener noreferrer"&gt;Stop Receiving Webhooks, Start Polling&lt;/a&gt;. Here it's just one layer in the stack.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. GitHub Actions CI gates.&lt;/strong&gt; The automated required checks. Build succeeds, tests pass, coverage holds, formatting is clean, code quality thresholds are met. These are pass/fail facts rather than opinions, and a failing one blocks the merge outright. Where the AI reviewers say "this looks risky," the gates say "this is broken, full stop."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. SonarQube.&lt;/strong&gt; A static-analysis tool that scans the code without running it, hunting for bugs, code smells, security-sensitive patterns, and coverage gaps, then gates the PR on a quality threshold. It's the deterministic, rules-based counterweight to the four probabilistic AI opinions above it. SonarQube finds the same kind of issue the same way every single time, which is exactly what you want sitting next to a stack of models that each answer a little differently on every run.&lt;/p&gt;

&lt;p&gt;Five layers, three flavors: AI reviewers reading intent (CodeRabbit, Copilot, my runner), hard pass/fail gates (Actions), and deterministic static analysis (SonarQube). A change has to satisfy all of the ones that apply to it before it merges.&lt;/p&gt;




&lt;h2&gt;
  
  
  Risk-Based Escalation, and Why This Is Affordable
&lt;/h2&gt;

&lt;p&gt;A reasonable reaction here is that five reviewers on every pull request sounds slow and expensive. It would be, if every layer ran at full weight on every change. They don't.&lt;/p&gt;

&lt;p&gt;The stack is risk-scored. Each change gets a rough risk assessment, and the most expensive layers escalate only for the riskiest changes. A one-line copy fix in a README doesn't earn a dual-model deep review. A change to authentication, a database migration, or a deploy pipeline does. The heavyweight pass, my local runner spending the time to run both Codex and Claude and reconcile them, kicks in where the cost of a missed bug is highest. That's a deliberate cost-and-coverage tradeoff: spend the expensive attention where a mistake actually hurts, and let the cheap layers carry the routine changes.&lt;/p&gt;

&lt;p&gt;The economics work out in favor of layering, and that surprised me. Here's the shape of it for a solo dev:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;CodeRabbit and Copilot review run on flat-rate subscriptions. Their cost is the same whether they review one PR a week or thirty.&lt;/li&gt;
&lt;li&gt;The GitHub Actions gates run on CI minutes that, at my volume, sit comfortably inside the free allowance.&lt;/li&gt;
&lt;li&gt;My local runner executes under CLI subscriptions I already pay for, on hardware I already own and leave running. Each review it performs adds no marginal token bill.&lt;/li&gt;
&lt;li&gt;The risk scoring caps how often the genuinely expensive path runs at all.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Put those together and adding another reviewer to the gauntlet is mostly free at the margin. The fixed costs are paid; the per-PR cost of one more layer rounds to nothing. For a single operator, that's a rare situation where the safe choice and the cheap choice are the same choice. I'll take more independent eyes when more eyes are close to free.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why So Many: The Thesis
&lt;/h2&gt;

&lt;p&gt;The reason for all of this fits in one observation about probability.&lt;/p&gt;

&lt;p&gt;A bug that one reviewer misses is common. Every reviewer, human or AI, has blind spots. A bug that two independent reviewers both miss is less common, because their blind spots rarely line up. A bug that survives an AI reviewer, a second AI reviewer from a different vendor, a dual-model local review, a battery of pass/fail gates, and a deterministic static analyzer is rare, because for that to happen, every one of those different rubrics has to fail in the same place at the same time. That's defense in depth: stack independent checks so that no single failure gets the whole way through.&lt;/p&gt;

&lt;p&gt;This is exactly why the high-risk path runs two different model families instead of running one model twice. Two passes of the same model share the same blind spots; they tend to agree, including when they're both wrong. Two different models disagree productively. One flags something the other waved through. The disagreement is the value. So the principle I hold is that the riskiest changes always get two genuinely independent model perspectives, two different rubrics from two different vendors looking at the same diff.&lt;/p&gt;

&lt;p&gt;And the human is still in this, just not where I used to be. I'm not reading every line as it's written anymore. I'm reading verdicts, resolving the cases where the layers disagree, and deciding what's actually true when CodeRabbit loves a change and SonarQube hates it. The judgment is still mine. It moved from the keyboard to the merge button.&lt;/p&gt;

&lt;p&gt;I want to be honest that this is an opinionated setup built for one specific situation: a solo dev whose code is overwhelmingly AI-authored. A team with humans writing and reviewing code line by line already has a lot of this coverage built into how they work, and stacking five automated layers on top might be redundant for them. For me, where no human wrote the code in the first place, the verification layer is the only place human judgment touches the work at all. So it gets all of it.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Close
&lt;/h2&gt;

&lt;p&gt;When you stop writing the code, you don't stop being the engineer. The engineering just moves downstream, from authoring to specifying and verifying. The gauntlet is where I spend my judgment now, and I built it deep on purpose.&lt;/p&gt;

&lt;p&gt;Let the cheap, fast layers catch the routine misses. Let two different models argue over the dangerous changes. Let the deterministic gates have the final, unarguable say. Write less code, verify it harder. That's the job now.&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>automation</category>
      <category>devops</category>
      <category>honeydrunk</category>
    </item>
    <item>
      <title>Discord as My Operator Pager: One Timeline for a Whole Studio</title>
      <dc:creator>Tatted Dev</dc:creator>
      <pubDate>Sun, 07 Jun 2026 16:37:00 +0000</pubDate>
      <link>https://dev.to/tatted_dev/discord-as-my-operator-pager-one-timeline-for-a-whole-studio-2aga</link>
      <guid>https://dev.to/tatted_dev/discord-as-my-operator-pager-one-timeline-for-a-whole-studio-2aga</guid>
      <description>&lt;p&gt;A deploy of mine broke, and I found out about it the way you never want to: days later, by stumbling onto it myself. The failure had been recorded. An email went out. A status flipped red in a dashboard somewhere. Every system did its job. I just never saw any of it, because the signal was scattered across so many places that none of them was the place I actually looked.&lt;/p&gt;

&lt;p&gt;That's the failure mode of running a lot of automation alone. Under my repos there's a constant hum of machinery: deploys firing, CI pipelines passing and failing, scheduled jobs waking up to do work, secrets quietly aging toward expiry. When you're one person, the hard question stops being "can I build it" and becomes "do I know what it's doing right now." For a long time the honest answer was no.&lt;/p&gt;

&lt;p&gt;The signal was everywhere and nowhere. GitHub notifications were supposed to be my feed, except they were buried under a huge issue backlog, so the inbox that should have told me a deploy broke was the same inbox screaming about forty open issues. Actions failures arrived as email, which I learned to skim past because most of them were noise. Some things only showed up if I went and looked: opening a dashboard, checking a deploy status, remembering to verify a job actually ran. There was no single place I could glance at and trust to tell me the truth. So I missed things. The information existed the whole time. It just had no home.&lt;/p&gt;

&lt;p&gt;This post is about giving it one. I made Discord the place where everything my automation does reports in, and I gave that reporting a structure so the important events stay glanceable.&lt;/p&gt;




&lt;h2&gt;
  
  
  When the Signal Has No Home
&lt;/h2&gt;

&lt;p&gt;The pattern was worse than any one missed alert.&lt;/p&gt;

&lt;p&gt;A deploy would go out and I'd find out it worked by using the thing, not by being told. A CI run would fail on a repo I hadn't touched in a week, and the email telling me sat unread between two GitHub digest mails. A scheduled job would skip a night and I'd discover the gap days later. None of these are catastrophic on their own. The problem is the pattern: every kind of operational event lived in a different place, in a different format, with a different chance of me ever seeing it.&lt;/p&gt;

&lt;p&gt;On a team, someone is usually watching the boards while someone else builds. Solo, the watching is also my job, layered on top of the building, and human attention does not scale across a dozen notification surfaces. I needed to collapse all of those surfaces into one.&lt;/p&gt;

&lt;p&gt;The requirement was simple to state. I wanted a single timeline where, if something happened that I'd care about, it showed up, and where the things I'd care about most were easy to pick out from the things that are just routine chatter.&lt;/p&gt;




&lt;h2&gt;
  
  
  What a Webhook Is, and Why Discord
&lt;/h2&gt;

&lt;p&gt;The mechanism underneath all of this is a webhook, and it's worth a plain definition because the whole design rests on it.&lt;/p&gt;

&lt;p&gt;A Discord webhook is a URL you POST a message to. You send an HTTP request to that URL with some content, and the message appears in a Discord channel. That's the entire contract. There's no bot to host, no gateway connection to keep alive, no library to babysit. Any piece of automation that can make an HTTP request can speak into a channel, which means every part of my studio (a GitHub Action in the cloud, a script on a machine at home) can report in with the same trivial primitive.&lt;/p&gt;

&lt;p&gt;Discord earns the role for a boring reason: I'm already in it, on my phone and my desktop, all day. The pager has to be somewhere I already look. Anywhere else just becomes one more dashboard I forget to open. Channels give me natural separation. Mentions give me a way to make the loud things loud. It was already running, so I made it load-bearing.&lt;/p&gt;

&lt;p&gt;I'll borrow a word for the messages it carries: operator alerts. An operator alert is a message meant for me, the person running the studio, telling me something about the state of my own systems. A deploy finished. A pipeline failed. A secret is about to expire. The audience is exactly one person, and that fact ends up mattering a lot, which I'll get to.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Taxonomy: Every Event Has a Known Home
&lt;/h2&gt;

&lt;p&gt;A single channel with everything dumped into it would just be the email inbox again, reinvented. The structure is the point.&lt;/p&gt;

&lt;p&gt;So I split operator alerts into channels by signal type. Each kind of event has a known home, and I know what each home means without reading closely. The split looks roughly like this (these are examples of the shape, not a literal channel list):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Deploys.&lt;/strong&gt; Something went out, where it went, and whether it landed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CI failures.&lt;/strong&gt; A pipeline broke, on which repo, with a link to the run.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Security and credential events.&lt;/strong&gt; A secret or credential did something I need to know about.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rotation and escalation.&lt;/strong&gt; A scheduled or sensitive job needs my attention now.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The reason this works is that the channel itself carries meaning before I read a word. A new message in the deploys channel is routine and I can let it scroll by. A new message in the security channel makes me stop. By routing each event type to its own home, the important signals stop competing with the routine ones for the same glance. Glanceability is a property of the layout, not of any single message.&lt;/p&gt;

&lt;p&gt;A webhook emits a structured message into the right channel. The emitter knows what kind of event it's reporting, so it knows which channel URL to POST to. That routing decision is the taxonomy, encoded.&lt;/p&gt;




&lt;h2&gt;
  
  
  Two Stores, Because Alerts Come From Two Places
&lt;/h2&gt;

&lt;p&gt;There's a credential wrinkle here that shaped the implementation, and it's a good illustration of how a small system grows real edges.&lt;/p&gt;

&lt;p&gt;Those webhook URLs are secrets. Anyone holding one can post into your channel, so they have to be stored and handed to the emitters carefully. The catch is that my alerts originate from two very different places, and those two places have different ways of holding a secret.&lt;/p&gt;

&lt;p&gt;Some alerts come from GitHub Actions, running in the cloud. Those read their webhook URLs from organization secrets, which is GitHub's built-in store for values that workflows are allowed to use. Other alerts come from a local automation worker running on my own hardware. That worker reads its webhook URLs from Key Vault, which is Azure's cloud secret store. Two execution contexts, two secret stores, the same destination channels on the other end.&lt;/p&gt;

&lt;p&gt;I could have forced everything through one store, but the contexts genuinely live in different trust domains, and each already had a native, well-guarded place to keep a secret. So I let each emitter use the store that fits where it runs. The taxonomy is shared. The credential plumbing is local to the emitter.&lt;/p&gt;




&lt;h2&gt;
  
  
  Redaction Is Not Optional
&lt;/h2&gt;

&lt;p&gt;There's one rule in this system I treat as absolute: secrets get redacted before anything is posted.&lt;/p&gt;

&lt;p&gt;The reason is uncomfortable once you say it out loud. The whole point of a security and credential channel is to tell me when something is wrong with a secret. If the message announcing that a token is expiring includes the token, then my alerting surface just leaked a credential into a chat application. An alert that leaks the thing it's alerting about is its own incident, and a worse one than the event that triggered it.&lt;/p&gt;

&lt;p&gt;So redaction is a property of the emitter, enforced before the POST, every time. The emitter's job is to describe an event in enough detail that I can act, and never enough detail to compromise anything. "A secret in this vault is expiring in three days" is the alert. The secret's value never goes near the channel. I'd rather an alert be slightly too vague than carry a payload I'd regret.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Boundary: Operator Alerts and Customer Notifications Are Different Concerns
&lt;/h2&gt;

&lt;p&gt;This is the part I hold most firmly, so I'll state it plainly as two separate things.&lt;/p&gt;

&lt;p&gt;Operator alerts are signal for me, the person running the studio. The audience is one. The job is to tell me what my automation is doing so I can intervene when I need to. Discord is that pager, and this entire post is about it.&lt;/p&gt;

&lt;p&gt;Customer-facing notifications are a product capability, the features that email or notify the people who use what I build. The audience is users. The job is reliable, durable delivery to people who are not me. That's a separate system entirely, with its own delivery guarantees, its own retries, its own failure handling, and its own place in the architecture.&lt;/p&gt;

&lt;p&gt;These two have different audiences, different reliability needs, and different failure modes, so they live in different systems. If my operator pager misses a message, I notice the next time I glance and I go look. If a customer notification system misses a message, that's a product defect with someone else's expectations attached. Mixing them would drag the casual reliability of a personal pager into a place that demands real guarantees, or drag heavyweight delivery machinery into something that just needs to ping my phone. Keeping them apart lets each one be exactly as serious as it should be.&lt;/p&gt;

&lt;p&gt;Discord is the operator pager. The customer notification product is its own thing, and it's a different post.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Shipped, and What I Learned
&lt;/h2&gt;

&lt;p&gt;The taxonomy is in place, both webhook stores are wired, redaction is enforced at the emitter, and the most recent piece to land was a credential pager: alerts specifically for credential and secret events. A secret nearing expiry, a rotation that needs attention, these now route to their own channel instead of dissolving into the general stream. The events that are easiest to forget and most painful to miss got the most deliberate home.&lt;/p&gt;

&lt;p&gt;The thing I learned building this is that scattered signal gets solved by choosing one surface and committing to it, not by polishing the surfaces you already ignore. The technology here is almost embarrassingly simple. A webhook is a URL you POST to. All the real work lived in the taxonomy, the discipline of redaction, and the boundary that keeps my pager out of my product.&lt;/p&gt;

&lt;p&gt;A solo operator can't watch a dozen places. So I stopped trying, and built the one place worth watching.&lt;/p&gt;

</description>
      <category>devops</category>
      <category>automation</category>
      <category>architecture</category>
      <category>honeydrunk</category>
    </item>
  </channel>
</rss>
