<?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: Nazarii Ahapevych</title>
    <description>The latest articles on DEV Community by Nazarii Ahapevych (@nazarii-ahapevych).</description>
    <link>https://dev.to/nazarii-ahapevych</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%2F4006724%2F175785ee-5343-451c-a1dc-30bd19e50daa.jpg</url>
      <title>DEV Community: Nazarii Ahapevych</title>
      <link>https://dev.to/nazarii-ahapevych</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/nazarii-ahapevych"/>
    <language>en</language>
    <item>
      <title>An AI agent graded its own poll loop as 'event-driven'. I read the code instead.</title>
      <dc:creator>Nazarii Ahapevych</dc:creator>
      <pubDate>Mon, 20 Jul 2026 16:40:23 +0000</pubDate>
      <link>https://dev.to/nazarii-ahapevych/an-ai-agent-graded-its-own-poll-loop-as-event-driven-i-read-the-code-instead-149i</link>
      <guid>https://dev.to/nazarii-ahapevych/an-ai-agent-graded-its-own-poll-loop-as-event-driven-i-read-the-code-instead-149i</guid>
      <description>&lt;h2&gt;
  
  
  Context
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;downbeat&lt;/code&gt; (my tool that passes messages between terminal sessions, &lt;a href="https://github.com/FreddieMcHeart/downbeat" rel="noopener noreferrer"&gt;github.com/FreddieMcHeart/downbeat&lt;/a&gt;) has two ways to keep a session aware of its inbox. One is &lt;code&gt;/relay-monitor&lt;/code&gt;: it composes the built-in &lt;code&gt;/loop&lt;/code&gt; (re-fire a prompt every N minutes) with the inbox hook, so every tick is a full model turn whether or not new mail arrived. The other showed up on its own. A different session had set up a background "Monitor" and, in its own notes, described it as "event-driven, instant, cheap, only fires on events."&lt;/p&gt;

&lt;p&gt;I was asked which approach was better. We already had a rule for evaluating external tools: do not trust the description, read the code. I pointed that rule at the session's own work.&lt;/p&gt;

&lt;h2&gt;
  
  
  The finding
&lt;/h2&gt;

&lt;p&gt;The self-described "event-driven" monitor was this:&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="k"&gt;while &lt;/span&gt;&lt;span class="nb"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
  &lt;/span&gt;&lt;span class="nb"&gt;sleep &lt;/span&gt;90
  &lt;span class="nv"&gt;cur&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;downbeat inbox | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="s1"&gt;'^*'&lt;/span&gt; | &lt;span class="nb"&gt;awk&lt;/span&gt; &lt;span class="s1"&gt;'{print $2}'&lt;/span&gt; | &lt;span class="nb"&gt;sort&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
  &lt;span class="nv"&gt;new&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;comm&lt;/span&gt; &lt;span class="nt"&gt;-13&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$seen&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &amp;lt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;'%s\n'&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$cur&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
  &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$new&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"NEW MESSAGES"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;'%s\n'&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$cur&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$seen&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;done&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is a poll loop. It sleeps 90 seconds, lists the inbox, and diffs against a seen-file. The session had conflated "does not re-arm itself every turn" with "does not poll." It graded itself on the label, not the loop. Its own comparison table claimed instant latency and cost only on events. Reality: up to 90 seconds of latency and about 960 polls a day, traffic or no traffic.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two things it got right anyway
&lt;/h2&gt;

&lt;p&gt;Reading past the wrong label, two ideas in it were genuinely good.&lt;/p&gt;

&lt;p&gt;First, a cheap gate in front of an expensive consumer. On a quiet channel, &lt;code&gt;/relay-monitor&lt;/code&gt; spends a full model turn every interval just to read "nothing new." This script's tick is a bash poll, near-free, and it only escalates to a model turn when &lt;code&gt;comm&lt;/code&gt; actually finds new mail. Put the cheap check on the hot path and the expensive one on the rare path. That is the real win, whatever you call it. (Exact cost delta: unmeasured. The direction is obvious, I did not benchmark the size.)&lt;/p&gt;

&lt;p&gt;Second, persistent state beats re-creation. Its previous version had re-armed a fresh "fire once" watcher every turn, which stacked up overlapping watchers that double-fired. One long-lived process with a seen-set emits each message exactly once. It had quietly re-derived a consumer offset in bash.&lt;/p&gt;

&lt;h2&gt;
  
  
  The kicker
&lt;/h2&gt;

&lt;p&gt;Here is where it turned into a lesson about my own codebase. &lt;code&gt;downbeat&lt;/code&gt; already had a real event-driven watcher: &lt;code&gt;src/downbeat/core/watcher.py&lt;/code&gt;, with an &lt;code&gt;FsWatcher&lt;/code&gt; built on watchdog (FSEvents on macOS, inotify on Linux) and a &lt;code&gt;PollWatcher&lt;/code&gt; fallback for network filesystems, both behind a &lt;code&gt;make_watcher()&lt;/code&gt; factory. I had written it months earlier for the TUI's live refresh, and it was sitting unused.&lt;/p&gt;

&lt;p&gt;Meanwhile the &lt;code&gt;downbeat watch&lt;/code&gt; command I had added more recently (since removed, see the update below) ignored it and rolled its own &lt;code&gt;while sleep&lt;/code&gt; loop.&lt;/p&gt;

&lt;p&gt;So two independent agents, that other session and my own past self, each re-derived "poll the inbox in a loop" while a tested, event-driven watcher sat one import away.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix
&lt;/h2&gt;

&lt;p&gt;Rewire the watch command to use the factory:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;w&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;make_watcher&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;on_change&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;emit&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;prefer&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;auto&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;# FsWatcher, PollWatcher fallback
# --poll forces PollWatcher for NFS/SMB, where FS events are unreliable
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now &lt;code&gt;downbeat watch&lt;/code&gt; blocks on filesystem events with near-zero idle cost, and running it under a monitor gives exactly the cheap-gate-then-wake architecture the other session was reaching for. The difference is that the "event-driven" claim is finally true instead of aspirational.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I took away
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Grade a mechanism by its code, not its description. Even when the description is your own, and especially when an AI wrote it. The gap between "what I built" and "what I said I built" is exactly where this bug lived.&lt;/li&gt;
&lt;li&gt;Before building a new primitive, check whether you already shipped one. A ten-second grep of my own repo would have prevented two separate reinventions of "poll the inbox."&lt;/li&gt;
&lt;li&gt;Cheap predicate, then expensive work. A near-free filesystem check in front of an LLM turn is the pattern that actually mattered here, and it shows up all over the codebase once you go looking for it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The most valuable output of the whole exercise was not the feature. It was noticing that a tested primitive had been re-derived twice, in plain sight.&lt;/p&gt;

&lt;h2&gt;
  
  
  Update: the primitive found a better home
&lt;/h2&gt;

&lt;p&gt;Since I wrote this, that &lt;code&gt;FsWatcher&lt;/code&gt; got promoted again. It made the standalone &lt;code&gt;watch&lt;/code&gt; command this post celebrates redundant. Instead of a user running &lt;code&gt;watch&lt;/code&gt; in a loop, the watcher now lives &lt;em&gt;inside&lt;/em&gt; the TUI: while the app is open it fires a native OS notification the moment a peer has mail waiting and that peer has been idle too long, with a heartbeat so a headless send-hook and the TUI don't both notify for the same message. The standalone &lt;code&gt;watch&lt;/code&gt; subcommand was removed outright.&lt;/p&gt;

&lt;p&gt;Which is this article's own lesson, one turn further. The tested primitive I found sitting unused didn't just replace two hand-rolled poll loops. Once it was the obvious building block, the &lt;em&gt;right&lt;/em&gt; place to use it turned out not to be the command I first bolted it onto at all.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>devops</category>
      <category>python</category>
      <category>architecture</category>
    </item>
    <item>
      <title>My AI harness is an external executive function</title>
      <dc:creator>Nazarii Ahapevych</dc:creator>
      <pubDate>Fri, 17 Jul 2026 08:14:38 +0000</pubDate>
      <link>https://dev.to/nazarii-ahapevych/my-ai-harness-is-an-external-executive-function-5efh</link>
      <guid>https://dev.to/nazarii-ahapevych/my-ai-harness-is-an-external-executive-function-5efh</guid>
      <description>&lt;p&gt;I built an AI setup to save money on tokens. It turned into the memory and focus my ADHD brain is missing.&lt;/p&gt;

&lt;p&gt;TL;DR: A messy AI agent wastes money. A messy ADHD brain wastes effort. The same thing fixes both: external structure. I built it for the agent, then I saw I had built it for myself.&lt;/p&gt;

&lt;h2&gt;
  
  
  Context
&lt;/h2&gt;

&lt;p&gt;An "agent harness" is the plain layer around an AI coding agent. Mine has four parts:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a &lt;strong&gt;CLAUDE.md&lt;/strong&gt;: a file of rules the agent reads every time it starts&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;skills&lt;/strong&gt;: fixed step-by-step routines for tasks I do often&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;routing and delegation&lt;/strong&gt;: rules for which model does what, and when to hand a job to a helper agent&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;file-based memory&lt;/strong&gt;: notes the agent writes to disk, so it remembers things between sessions&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;People build this to save money and keep quality steady. An agent with no structure wastes tokens and drifts off task. Structure makes it cheap and steady. That is the whole point of it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why these two things match
&lt;/h2&gt;

&lt;p&gt;Here is the part that surprised me.&lt;/p&gt;

&lt;p&gt;An AI agent wastes money when it works with no structure. It forgets what it already did. It drifts off the task. It redoes work. It spends effort on the wrong thing. So you give it structure: rules, saved routines, external notes, clear priorities. Now it does not have to decide everything well in the moment. The structure decides for it.&lt;/p&gt;

&lt;p&gt;ADHD is the same failure, in a person. Under pressure I forget, I drift, I redo, I focus on the wrong thing. The weak part is my judgment in the moment.&lt;/p&gt;

&lt;p&gt;So the structure that makes the agent cheap is the same structure that makes me work. Saving money was only the reason I started. What I really built was executive function, on disk. That is what executive function is: you do not need good judgment in the moment, because the structure already holds it for you.&lt;/p&gt;

&lt;h2&gt;
  
  
  The map, part by part
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;CLAUDE.md is memory and a safety net.&lt;/strong&gt; I take rules seriously and I hold most of them fine. I still back every important one with a hook or a checklist, just in case. My intention is solid. My attention is not. The file does not depend on attention.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Skills are getting started.&lt;/strong&gt; Starting is the hard part, and it is worst when I already know how to solve the task. My brain marks it as done and refuses to sit down and actually do it. A skill is the same routine every time, so there is no first step to find. It is already written. I follow it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Routing and delegation are choosing what matters.&lt;/strong&gt; Picking what to spend effort on is the choice I get wrong when I am overloaded. A rule makes that choice once, calmly, ahead of time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;File-based memory is memory that survives a break.&lt;/strong&gt; An idea I do not write down dissolves in about half an hour. For years my fix was Telegram messages to myself: voice notes, links, one line about why I saved them. It worked, sort of. Notes the system writes to disk are the same thing, except they come back to me at the right moment, already in context. And an interruption, which before AI was a permanent headache, stopped erasing where I was.&lt;/p&gt;

&lt;h2&gt;
  
  
  The honest part
&lt;/h2&gt;

&lt;p&gt;I did not plan this as help for my ADHD. I built it to stop wasting money on an agent. But the two problems have the same shape, and that is worth saying out loud. An agent with no structure and an ADHD brain with no structure fail the same way. They forget. They drift. They redo work they already did.&lt;/p&gt;

&lt;p&gt;The difference is that the agent gets a config file, and I got a diagnosis. So I gave myself the config file too.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this changes
&lt;/h2&gt;

&lt;p&gt;Two things.&lt;/p&gt;

&lt;p&gt;If a rule keeps failing when you rely on willpower, it does not belong in your head. It belongs in a file, a checklist, a routine, something that holds it for you. That is the same move that makes AI agents work, and no one calls the agent lazy for needing it.&lt;/p&gt;

&lt;p&gt;And the setup that looks like "AI slop" from the outside can be the thing that lets someone finish work at all. I wrote before about AI writing as an accessibility tool. That was one piece of it. This is the whole outside brain underneath. This very article worked that way too: I talked in a raw stream, the system built the structure. The small apps I write for myself and the diagrams I sketch start the same way.&lt;/p&gt;

&lt;p&gt;If your head works like mine, the question was never whether to use the help. It is which part to move outside first.&lt;/p&gt;

</description>
      <category>adhd</category>
      <category>ai</category>
      <category>productivity</category>
      <category>agents</category>
    </item>
    <item>
      <title>Green CI working software</title>
      <dc:creator>Nazarii Ahapevych</dc:creator>
      <pubDate>Mon, 13 Jul 2026 19:40:30 +0000</pubDate>
      <link>https://dev.to/nazarii-ahapevych/green-ci-working-software-5cim</link>
      <guid>https://dev.to/nazarii-ahapevych/green-ci-working-software-5cim</guid>
      <description>&lt;p&gt;Two green CI runs told me the release worked. Both were no-ops.&lt;/p&gt;

&lt;p&gt;A green check means one thing: the job exited 0. That is all it means. It does not tell you the step you cared about actually did the work you think it did. I learned this the slow way, twice in a row, on the same release pipeline.&lt;/p&gt;

&lt;h2&gt;
  
  
  The setup
&lt;/h2&gt;

&lt;p&gt;I had wired up automated releases with &lt;a href="https://python-semantic-release.readthedocs.io/" rel="noopener noreferrer"&gt;python-semantic-release&lt;/a&gt;. The idea is clean: you write &lt;a href="https://www.conventionalcommits.org/" rel="noopener noreferrer"&gt;conventional commits&lt;/a&gt;, the tool reads them, decides the next version, tags it, and pushes. Because the release job has to push a version bump and a changelog back to a protected branch, I gave it a dedicated &lt;code&gt;RELEASE_TOKEN&lt;/code&gt; with the rights to do that.&lt;/p&gt;

&lt;p&gt;The wiring looked healthy. Push, pipeline runs, green check, move on.&lt;/p&gt;

&lt;h2&gt;
  
  
  The first no-op
&lt;/h2&gt;

&lt;p&gt;python-semantic-release only cuts a release when the commits since the last tag actually warrant one. Conventional-commit types map to version bumps roughly like this:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Commit type&lt;/th&gt;
&lt;th&gt;Release effect&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;feat:&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;minor bump&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;fix:&lt;/code&gt;, &lt;code&gt;perf:&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;patch bump&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;docs:&lt;/code&gt;, &lt;code&gt;ci:&lt;/code&gt;, &lt;code&gt;chore:&lt;/code&gt;, &lt;code&gt;style:&lt;/code&gt;, &lt;code&gt;refactor:&lt;/code&gt;, &lt;code&gt;test:&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;no release&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;My two green runs landed on a &lt;code&gt;docs:&lt;/code&gt; commit and a &lt;code&gt;ci:&lt;/code&gt; commit. python-semantic-release looked at them, decided there was nothing to release, and short-circuited. It exited 0 well before it ever reached the push path. The &lt;code&gt;RELEASE_TOKEN&lt;/code&gt;, the protected-branch push, the tag, none of it ran. The job was green because it correctly did nothing.&lt;/p&gt;

&lt;p&gt;So the part of the pipeline I most wanted to verify, that the token worked and the push to the protected branch succeeded, stayed untested. It stayed pending until the next &lt;code&gt;feat:&lt;/code&gt; or &lt;code&gt;fix:&lt;/code&gt; commit came along and gave the tool an actual reason to reach that code. The green check told me the release worked. What it meant was "there was nothing to release, so I stopped early."&lt;/p&gt;

&lt;h2&gt;
  
  
  The second no-op
&lt;/h2&gt;

&lt;p&gt;The same pipeline had a &lt;code&gt;build_command&lt;/code&gt; that produced build artifacts before the release. The bug was subtle: &lt;code&gt;build_command&lt;/code&gt; runs your command, but it does not automatically stage the output that command produces. If your release step then commits and pushes, the artifacts the build just generated are not in that commit unless you stage them yourself.&lt;/p&gt;

&lt;p&gt;Every per-task run went green. The build command exited 0. The release step exited 0. Nothing in a single job's view was wrong. The gap only became visible at a whole-branch review, looking at the actual diff that would ship, where the missing artifacts stood out against everything else.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a green check actually is
&lt;/h2&gt;

&lt;p&gt;Both failures share one root. A check reports the exit code of the command you told it to run. It cannot report the thing you assumed that command would do. Coverage is not correctness. A pipeline verifies the paths you encoded, on the inputs you happened to feed it. The path that breaks is usually the one that never got exercised: the integration you mocked, the config that only exists in prod, the branch of logic that this particular commit did not trigger.&lt;/p&gt;

&lt;p&gt;"All checks passed" and "the release works" are two different statements. The pipeline can only ever prove the first one.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I check now
&lt;/h2&gt;

&lt;p&gt;I read the log, not the badge. Before I trust a green release, I confirm the specific path ran:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Did the version actually bump, or did the tool decide there was nothing to release?&lt;/li&gt;
&lt;li&gt;Did the push to the protected branch happen, with the token, on this run?&lt;/li&gt;
&lt;li&gt;Did the staged diff contain the artifacts the build produced?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Concretely:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Assert the effect, not the exit code.&lt;/strong&gt; If a job is supposed to push a tag, have something downstream check the tag exists. Exit 0 is necessary, it is not sufficient.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Force the real path in a test.&lt;/strong&gt; A release dry-run on a synthetic &lt;code&gt;feat:&lt;/code&gt; commit exercises the push path even when today's real commits would not. Then you learn the token works before you need it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keep whole-branch review.&lt;/strong&gt; Per-task green catches a lot. The staging gap and the no-op release both survived every per-task check and only showed up when someone looked at the whole branch as one diff. That review is not redundant with CI. It catches the class of bug CI structurally cannot see.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of this means the pipeline is useless. It means a green check is where the checking begins.&lt;/p&gt;

&lt;p&gt;Has a green run ever lied to you like this?&lt;/p&gt;

</description>
      <category>cicd</category>
      <category>testing</category>
      <category>devops</category>
      <category>sre</category>
    </item>
    <item>
      <title>downbeat: stop copy-pasting between your AI terminals</title>
      <dc:creator>Nazarii Ahapevych</dc:creator>
      <pubDate>Thu, 09 Jul 2026 07:26:47 +0000</pubDate>
      <link>https://dev.to/nazarii-ahapevych/downbeat-stop-copy-pasting-between-your-ai-terminals-3ae4</link>
      <guid>https://dev.to/nazarii-ahapevych/downbeat-stop-copy-pasting-between-your-ai-terminals-3ae4</guid>
      <description>&lt;h2&gt;
  
  
  Context
&lt;/h2&gt;

&lt;p&gt;I run more than one AI coding session at a time (Claude Code, in my case). One is usually the planner: it holds the architecture, decides what to do, splits the work. The others are executors, and each takes a chunk and grinds through it.&lt;/p&gt;

&lt;p&gt;The problem is that these sessions cannot talk to each other. Every terminal is its own island, so I became the message bus. I copied a plan out of the planning session and pasted it into an executor. I copied the result back. I re-explained context that one session had and another needed. Every handoff went through me, by hand, one clipboard trip at a time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; &lt;code&gt;downbeat&lt;/code&gt; is a small, local, human-in-the-loop message bus that lets parallel AI coding sessions on one machine hand tasks to each other and read the replies back. No cloud, no account, no network. It is on PyPI now: &lt;code&gt;uv tool install downbeat&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  I was the message bus, and it fell apart at four sessions
&lt;/h2&gt;

&lt;p&gt;Two sessions and a clipboard works fine. Four sessions is chaos. I lost track of which executor had which version of the plan, I pasted stale context, and I spent more attention shuttling text between terminals than on the actual work.&lt;/p&gt;

&lt;p&gt;I wanted the sessions to pass messages directly, on my machine, with me still in the loop but no longer doing the typing. No cloud service, nothing leaving the terminal.&lt;/p&gt;

&lt;h2&gt;
  
  
  What downbeat is
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;downbeat&lt;/code&gt; is a local, filesystem-backed message broker for AI coding sessions running on the same machine. A CLI, a terminal UI, and a bundled agent skill. No server, no account, no network.&lt;/p&gt;

&lt;p&gt;The model is simple:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Each session registers as a named &lt;strong&gt;peer&lt;/strong&gt; (a "parent" planner, a "child" executor, whatever role you give it).&lt;/li&gt;
&lt;li&gt;One session &lt;strong&gt;sends&lt;/strong&gt; a message to another: a task, a plan, a chunk of context, a result.&lt;/li&gt;
&lt;li&gt;The recipient sees that message surfaced at the start of its next turn, through a hook. It just appears.&lt;/li&gt;
&lt;li&gt;Replies flow back the same way, so a child can report "done" to its parent without you touching the clipboard.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The part I care about most: &lt;strong&gt;nothing auto-executes.&lt;/strong&gt; Every watcher notifies you, nothing runs on the parent side on its own, and a child only acts because you told it to at registration time. Human-in-the-loop is the default you would have to work to turn off.&lt;/p&gt;

&lt;p&gt;Underneath, a message is a JSON file in the recipient's inbox directory. That is the whole transport. It turns out you can build a surprisingly capable little queue out of a directory and a hook.&lt;/p&gt;

&lt;h2&gt;
  
  
  Install
&lt;/h2&gt;

&lt;p&gt;The whole runtime installs in two commands:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;uv tool &lt;span class="nb"&gt;install &lt;/span&gt;downbeat     &lt;span class="c"&gt;# or: pipx install downbeat&lt;/span&gt;
downbeat init                &lt;span class="c"&gt;# one command installs the WHOLE runtime&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;downbeat init&lt;/code&gt; is the single source of truth for the setup. It bootstraps the data dirs, installs the agent &lt;strong&gt;skill&lt;/strong&gt;, drops the bundled &lt;strong&gt;hooks&lt;/strong&gt; and &lt;strong&gt;slash commands&lt;/strong&gt;, and registers the hooks in your settings idempotently (backed up, atomic, and it never clobbers hooks you already have). It is safe to re-run.&lt;/p&gt;

&lt;p&gt;Then the loop itself:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;downbeat register parent &lt;span class="nt"&gt;--role&lt;/span&gt; parent
downbeat register child  &lt;span class="nt"&gt;--role&lt;/span&gt; child
downbeat send child &lt;span class="s2"&gt;"task"&lt;/span&gt; &lt;span class="s2"&gt;"do the thing"&lt;/span&gt;
downbeat inbox &lt;span class="nt"&gt;--peer&lt;/span&gt; child
downbeat reply &amp;lt;msg_id&amp;gt; &lt;span class="s2"&gt;"done"&lt;/span&gt;
downbeat tui                 &lt;span class="c"&gt;# full management UI&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you want to see it before installing anything, the repo has a five-command walkthrough in &lt;code&gt;examples/parent-child-handoff/&lt;/code&gt; with a recorded demo.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why I built it this way
&lt;/h2&gt;

&lt;p&gt;Two constraints shaped it.&lt;/p&gt;

&lt;p&gt;It had to be &lt;strong&gt;local and dependency-free.&lt;/strong&gt; My sessions already run on one laptop. Reaching for Kafka or a hosted queue to pass a sentence between two terminals is absurd. The filesystem is already there, it is already durable, and a rename is already atomic.&lt;/p&gt;

&lt;p&gt;It had to be &lt;strong&gt;reliable enough to trust.&lt;/strong&gt; Early on it was fire-and-forget, and it quietly lost messages: a file got marked handled the instant it was read, even if the session never acted on it. So it grew a proper delivery state machine (delivered, then acknowledged, with anything unconfirmed re-queued instead of dropped), a TUI to watch the traffic, session identity that survives a &lt;code&gt;/clear&lt;/code&gt;, and an event-driven file watcher so it reacts instantly instead of polling.&lt;/p&gt;

&lt;p&gt;None of that was planned up front. It started as a hack to stop copy-pasting, and each rough edge I hit in daily use turned into the next piece.&lt;/p&gt;

&lt;h2&gt;
  
  
  Who it might help
&lt;/h2&gt;

&lt;p&gt;If you drive more than one AI coding session at once, or you have ever found yourself relaying instructions between terminals by hand, this is for that. It is deliberately small and single-machine. It does one thing: let sessions on your box hand work to each other and report back, with you still holding the wheel.&lt;/p&gt;

&lt;p&gt;I am open-sourcing it because it became genuinely useful to me and it might help someone with the same workflow. It is early and opinionated. Issues, ideas, and "why on earth did you do it this way" questions are all welcome.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Repo: &lt;a href="https://github.com/FreddieMcHeart/downbeat" rel="noopener noreferrer"&gt;github.com/FreddieMcHeart/downbeat&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;PyPI: &lt;a href="https://pypi.org/project/downbeat" rel="noopener noreferrer"&gt;pypi.org/project/downbeat&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Docs: &lt;a href="https://freddiemcheart.github.io/downbeat/" rel="noopener noreferrer"&gt;freddiemcheart.github.io/downbeat&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What's next
&lt;/h2&gt;

&lt;p&gt;The interesting parts to build were the ones I did not expect. Over the next few posts I want to dig into a handful of them:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the two-phase delivery that stopped the silent message loss, and the backlog it exposed&lt;/li&gt;
&lt;li&gt;an event-driven file watcher I had already written months earlier and completely forgotten, until two separate agents each reinvented polling next to it&lt;/li&gt;
&lt;li&gt;giving a process a stable identity that survives &lt;code&gt;/clear&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If any of that sounds familiar from your own tools, I would like to hear how you solved it.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>opensource</category>
      <category>cli</category>
      <category>python</category>
    </item>
    <item>
      <title>What looks like AI slop from the outside is my accessibility tool</title>
      <dc:creator>Nazarii Ahapevych</dc:creator>
      <pubDate>Mon, 06 Jul 2026 15:16:55 +0000</pubDate>
      <link>https://dev.to/nazarii-ahapevych/what-looks-like-ai-slop-from-the-outside-is-my-accessibility-tool-54lc</link>
      <guid>https://dev.to/nazarii-ahapevych/what-looks-like-ai-slop-from-the-outside-is-my-accessibility-tool-54lc</guid>
      <description>&lt;h2&gt;
  
  
  Context
&lt;/h2&gt;

&lt;p&gt;I have ADHD, and I write with AI. Those two facts are connected, and this piece is about how. Not "AI helped me polish this." The article you're reading did not exist as a finished thing until an AI sat down with me and built the structure around it. The usual word for that from the outside is slop, or, aimed specifically at people like me, neuroslop.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;TL;DR: My ADHD means some tasks, like turning a scattered thought into a finished piece of writing, put a specific strain on exactly the resource I'm short on. AI tools remove that barrier. For a neurotypical writer they're a convenience. For me they're the reason a thought becomes a shipped piece of work instead of a fragment in a notes app forever. Same tool, completely different stakes.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Slop to you, accessibility to me
&lt;/h2&gt;

&lt;p&gt;The slop criticism is usually fair. A lot of AI-assisted writing really is low-effort output nobody reviewed, produced because it was easy to produce. I'm not arguing against that criticism in general, most of what gets called AI slop earns the label.&lt;/p&gt;

&lt;p&gt;But the label carries a hidden assumption: that the person could have written the piece themselves, just a bit slower. That assumption doesn't hold for me. The barrier was never knowing what to say. It was the blank page itself, and the executive-function cost of turning a scattered mental state into the first three bullet points that make a piece of writing exist. That cost isn't there for everyone in the same way, and it doesn't scale down with "try harder." It's the same shape as needing glasses to read fine print: fine for most of the day, and a hard wall the moment the task demands exactly the thing you can't do unaided.&lt;/p&gt;

&lt;p&gt;The point is narrow: some tasks put a specific, predictable strain on exactly the resource ADHD affects, and a tool that corrects for that strain is doing accessibility work whether or not anyone calls it that.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three things this actually looks like
&lt;/h2&gt;

&lt;p&gt;Not an abstract claim. Here's what it looks like in practice:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Structuring from zero.&lt;/strong&gt; I hand over a mess, and get back a skeleton I can build on. The blank page was the wall; the skeleton is the door.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Finishing what attention broke.&lt;/strong&gt; A thought gets interrupted halfway through, the way thoughts do, and instead of staying a half-sentence in a drafts folder forever, it gets picked back up and completed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Turning raw stream-of-consciousness into structure.&lt;/strong&gt; A chaotic voice note or a wall of unpunctuated thinking goes in, and something readable comes out the other side, without losing what I actually meant.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of these are about the AI being smarter than me. They're about the AI correcting the exact point where my own process used to fail.&lt;/p&gt;

&lt;h2&gt;
  
  
  It's bigger than writing
&lt;/h2&gt;

&lt;p&gt;This isn't only about articles. The same pattern shows up across how I work: writing code, getting through a work task, turning a half-formed idea into something that actually ships. In every one of those, the AI is functioning less like a tool and more like an extended working memory, the extra context I can't hold onto myself. I haven't measured this rigorously across those areas the way this project measured the reading-side research (see the neuroinclusive writing checklist built for this same content pipeline). The pattern is consistent enough across code, work, and writing that I'm confident it isn't a writing-specific quirk.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the slop label sticks right now
&lt;/h2&gt;

&lt;p&gt;Right now, anything tagged AI-generated gets treated as slop by default. That reflex says more about where we are than about what the tool is. Two kinds of literacy are still immature: creators who reach for the tool without doing the thinking it still requires, and readers who haven't yet built the pattern-matching to tell that apart from someone using the same tool to get a real thought past a real barrier. Both sides are new at this. The label is a snapshot of an early, poorly-understood moment, not a fixed property of the output.&lt;/p&gt;

&lt;h2&gt;
  
  
  The honest part I won't dress up
&lt;/h2&gt;

&lt;p&gt;The AI drafts real sentences. Whole paragraphs, including ones in this article. Pretending it only "helps with structure" while I supposedly write every word myself would be its own kind of slop, a lie about how the thing actually got made.&lt;/p&gt;

&lt;p&gt;So here's the real division of labor. The AI is fast hands. What makes the piece mine is everything around the drafting: the direction, the judgment about what's true and what's off, and the dozen rounds of "no, that's not what I meant" that reshape a draft until it says the thing I actually think. This exact article went through that, more times than I'd like to admit. The argument is mine. The calls about what stays and what's wrong are mine. The typing, increasingly, isn't, and I've stopped thinking that's the part that mattered.&lt;/p&gt;

&lt;p&gt;There's a meme I like: a boss riding in a cart, a leader pulling one alongside the team, an introvert dragging the cart alone, and then "ADHD introvert with AI" flying a fighter jet. It's a joke, but it lands for a reason. I'm not pretending I'm running on foot next to everyone else. I'm flying something. Flying is still a skill. It's just a different one than running.&lt;/p&gt;

&lt;h2&gt;
  
  
  The close
&lt;/h2&gt;

&lt;p&gt;Call it neuroslop if you want. What reads as low-effort from the outside was, for me, the thing that got a real thought out of my head and onto the page at all. A barrier removed, by someone who did the part that was actually mine to do.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>adhd</category>
      <category>a11y</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Your AI coding agent gets expensive one reasonable decision at a time</title>
      <dc:creator>Nazarii Ahapevych</dc:creator>
      <pubDate>Thu, 02 Jul 2026 11:28:15 +0000</pubDate>
      <link>https://dev.to/nazarii-ahapevych/your-ai-coding-agent-gets-expensive-one-reasonable-decision-at-a-time-735</link>
      <guid>https://dev.to/nazarii-ahapevych/your-ai-coding-agent-gets-expensive-one-reasonable-decision-at-a-time-735</guid>
      <description>&lt;h2&gt;
  
  
  The setup
&lt;/h2&gt;

&lt;p&gt;Agentic coding tools are astonishingly capable, and astonishingly easy to run expensively. I learned this by watching my own bill, not by doing anything obviously wasteful. There was no single dumb move. The cost came from a thousand small, reasonable-feeling decisions, each of which looked free at the time.&lt;/p&gt;

&lt;p&gt;This is the first post in a short series about the structure I built to stop that. Before the fixes, it is worth being precise about where the money actually goes, because most of it hides in places that feel light-touch.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where an agent bleeds tokens
&lt;/h2&gt;

&lt;p&gt;Five patterns account for most of it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Main-agent tokens are the most expensive tokens in the session.&lt;/strong&gt; Every file the top-level agent reads itself stays resident in its context, and that context is re-billed on every subsequent turn. A file you read once is a file you keep paying for until the session ends. The top-level agent's attention is the premium seat, and it is easy to fill it with work a cheaper agent should have handled.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"Just one more peek."&lt;/strong&gt; You read three files to build a mental model, then a fourth to be sure, then a fifth. Each one feels free. None of them is. This is the pattern I catch myself in most, because every individual read is defensible and the total is the problem.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The wrong model for the job.&lt;/strong&gt; Running a structural &lt;code&gt;ls&lt;/code&gt;, a &lt;code&gt;grep&lt;/code&gt;, or a "find where X is defined" on a frontier model, when a cheap model returns the same answer at a fraction of the price. A structural lookup has a right answer that does not need a genius to produce.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Long output in the main thread.&lt;/strong&gt; Emitting a 5,000-word writeup from the main agent, instead of having a cheap sub-agent write it to a file and hand back a pointer. The main agent should decide and delegate, not type essays into its own expensive context.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Context bloat compounds.&lt;/strong&gt; This is the one that hurts, because it is silent. The prefix cost of a bloated session is paid again on every single turn. It does not reset until you clear. A session that got fat an hour ago is still charging you rent on that fat right now.&lt;/p&gt;

&lt;h2&gt;
  
  
  The insight
&lt;/h2&gt;

&lt;p&gt;Look at that list again. Not one of those decisions feels expensive in the moment. Reading one more file is reasonable. Using the good model is reasonable. Writing the answer inline is reasonable. &lt;strong&gt;The expensive patterns are exactly the ones that feel light-touch when you make them.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That is why "just be more careful" fails. You cannot out-discipline a bias that only shows up in aggregate, one defensible decision at a time. In-the-moment judgment is the exact thing that breaks here. What you need instead is structure: something outside the moment that notices the pattern and interrupts it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the series covers
&lt;/h2&gt;

&lt;p&gt;So I built that structure, and it turned into a small set of pieces, each with one job:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a hook that runs on every tool call and enforces spending discipline the way a linter enforces style&lt;/li&gt;
&lt;li&gt;a model router that picks the tier before acting, plus a delegation rule that scouts cheaply before spending on the expensive agent&lt;/li&gt;
&lt;li&gt;a parent-and-children setup where a lean planner coordinates fresh-context workers&lt;/li&gt;
&lt;li&gt;and the work of turning all of it from "works on my machine" into something installable&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The next posts go through each.&lt;/p&gt;

&lt;p&gt;One result is worth stating up front, because it is the part I did not expect. The discipline did not trade cost against quality. It improved both. The same moves that cut the token bill (delegate the bulk reads, route the right model, keep the main agent's context clean) are the moves that keep the agent focused and correct. Cheaper and better turned out to be the same lever. The rest of the series is me earning that claim.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>productivity</category>
      <category>devops</category>
    </item>
    <item>
      <title>[type='CNAME'] crashed my Textual TUI: why escaping user text isn't enough</title>
      <dc:creator>Nazarii Ahapevych</dc:creator>
      <pubDate>Sun, 28 Jun 2026 18:01:06 +0000</pubDate>
      <link>https://dev.to/nazarii-ahapevych/typecname-crashed-my-textual-tui-why-escaping-user-text-isnt-enough-3gp0</link>
      <guid>https://dev.to/nazarii-ahapevych/typecname-crashed-my-textual-tui-why-escaping-user-text-isnt-enough-3gp0</guid>
      <description>&lt;h2&gt;
  
  
  The setup
&lt;/h2&gt;

&lt;p&gt;I have been building a small tool called &lt;code&gt;claude-relay&lt;/code&gt;. It passes messages between terminal sessions running on the same machine: one session finishes a piece of work, sends a short message, another session picks it up. Think of it as a tiny local message queue with a chat-style front end.&lt;/p&gt;

&lt;p&gt;That front end is a terminal UI built with &lt;a href="https://textual.textualize.io/" rel="noopener noreferrer"&gt;Textual&lt;/a&gt; and Rich. It lists incoming messages and renders the selected one as a chat bubble. The catch is that the message bodies are not clean strings I wrote. They are whatever a session decided to send: shell output, stack traces, Terraform plans, ticket IDs. The TUI has to render arbitrary text from outside my control. That is the detail that turned out to matter.&lt;/p&gt;

&lt;h2&gt;
  
  
  The crash
&lt;/h2&gt;

&lt;p&gt;One day a message arrived with a Terraform error in the body. I selected it, and the entire UI died:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;MarkupError: Expected markup value (found "='CNAME'] but it already exists]\n").
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The text that triggered it was completely ordinary:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Error: [type='CNAME'] but it already exists
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A bracketed token inside an error message. Harmless in a log file. But Textual and Rich treat square brackets as markup, so that one message took down the whole interface. Worse, any message containing brackets would do the same, and brackets are everywhere in developer output: ticket tags, type hints, array syntax, that Terraform error. The tool was unusable the moment real data flowed through it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why a bracket is a bomb
&lt;/h2&gt;

&lt;p&gt;Rich uses &lt;code&gt;[...]&lt;/code&gt; for inline styling: &lt;code&gt;[b]bold[/b]&lt;/code&gt;, &lt;code&gt;[dim]quiet[/dim]&lt;/code&gt;, &lt;code&gt;[red]alert[/red]&lt;/code&gt;. Textual renders on top of Rich, so any string you hand to a widget's &lt;code&gt;update()&lt;/code&gt; runs through that markup parser first.&lt;/p&gt;

&lt;p&gt;My message bubble was built the obvious way:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;child&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;[b]&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;direction&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;[/b]&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When &lt;code&gt;msg.body&lt;/code&gt; contains &lt;code&gt;[type='CNAME']&lt;/code&gt;, the parser sees a tag named &lt;code&gt;type='CNAME'&lt;/code&gt;, cannot make sense of it, and throws. The fix looked trivial. It took four commits and three wrong turns to actually get right.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrong turn 1: escape the user content
&lt;/h2&gt;

&lt;p&gt;The textbook move is to escape anything that came from outside, and Rich ships &lt;code&gt;rich.markup.escape()&lt;/code&gt; for exactly this. I wrapped every user-supplied field in it (&lt;code&gt;from_peer&lt;/code&gt;, &lt;code&gt;to_peer&lt;/code&gt;, &lt;code&gt;subject&lt;/code&gt;, &lt;code&gt;body&lt;/code&gt;) across eight files.&lt;/p&gt;

&lt;p&gt;It still crashed.&lt;/p&gt;

&lt;p&gt;The reason took a while to surface. Textual 8.x no longer renders through Rich's markup tokeniser. It has its own &lt;code&gt;visualize&lt;/code&gt; path, and that path does not honour Rich's backslash-escape convention. &lt;code&gt;escape()&lt;/code&gt; dutifully turned &lt;code&gt;[&lt;/code&gt; into &lt;code&gt;\[&lt;/code&gt;, and Textual's parser choked anyway. Escaping is parser-specific: an escape that satisfies one tokeniser means nothing to another.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrong turn 2: my own template had brackets in it
&lt;/h2&gt;

&lt;p&gt;While chasing the user content, I missed that I had planted brackets myself. My truncation suffix read:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;[dim]…[truncated, press Enter to view full][/dim]&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;[truncated, press Enter to view full]&lt;/code&gt; is a bracketed phrase sitting inside &lt;code&gt;[dim]...[/dim]&lt;/code&gt;. The parser reads it as a nested tag. The call was crashing on a string I wrote, not on any user data. Swapping the inner brackets for parentheses fixed that case:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;[dim](truncated, press Enter to view full)[/dim]&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The lesson I keep relearning: the parser parses your templates too, not only the data you pour into them.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix that holds: never let the parser see user text
&lt;/h2&gt;

&lt;p&gt;Here is the actual answer, stated plainly. Stop mixing the two worlds. Parse markup only for the parts you control, and append everything from outside as literal text that never reaches the parser.&lt;/p&gt;

&lt;p&gt;In practice that means building a &lt;code&gt;rich.text.Text&lt;/code&gt; object instead of a markup string. &lt;code&gt;Static.update()&lt;/code&gt; accepts a &lt;code&gt;Text&lt;/code&gt; renderable and prints it as-is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;rendered&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;from_markup&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;header&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;# only the parts WE control
&lt;/span&gt;&lt;span class="n"&gt;rendered&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;rendered&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;body_text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;            &lt;span class="c1"&gt;# user content, literal: parser never sees it
&lt;/span&gt;&lt;span class="n"&gt;child&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rendered&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Brackets in the body now render as brackets, because the body is never parsed as markup at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrong turn 3: even Text composition has a trap
&lt;/h2&gt;

&lt;p&gt;I applied the same idea to the message-detail view and it crashed again:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;MarkupError: closing tag '[/b]' doesn't match any open tag
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I had built the title as a chain of &lt;code&gt;from_markup&lt;/code&gt; calls:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# each from_markup() parses on its own, so the lone [/b] has nothing to close
&lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;from_markup&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;[b]&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;subject&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append_text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;from_markup&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;[/b] ...&lt;/span&gt;&lt;span class="sh"&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;Text.from_markup()&lt;/code&gt; is a parser, not a concatenator. Each call parses independently, so a dangling &lt;code&gt;[/b]&lt;/code&gt; in a later call has no matching open tag. The fix is to drop markup syntax for programmatic styling and use the API directly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;title&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;subject&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;style&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;bold&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;   (&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;style&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;dim&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No parser involved. No way to crash.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where it landed
&lt;/h2&gt;

&lt;p&gt;After those four commits the TUI renders arbitrary message bodies cleanly. The rule I walked away with is simple: markup syntax is a template language, and the programmatic &lt;code&gt;Text&lt;/code&gt; API is for code. The moment you split &lt;code&gt;[b]...[/b]&lt;/code&gt; across function calls or interpolate a variable into a markup string, you are writing code, so take the code path. Markup strings are safe only for fully self-contained, balanced literals: a fixed help line, a status glyph like &lt;code&gt;[yellow]●[/yellow]&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;One more thing that made this hard to catch: these bugs hide from your tests. Textual's &lt;code&gt;run_test(headless=True)&lt;/code&gt; renders to a virtual screen that does not exercise the same path as a real terminal launch. Every one of these crashes appeared only when I ran the real app against real data. My regression tests now feed the renderer the genuinely nasty inputs on purpose: &lt;code&gt;[type='CNAME']&lt;/code&gt;, &lt;code&gt;[ABC-1234]&lt;/code&gt;, &lt;code&gt;list[int]&lt;/code&gt;, a markdown link.&lt;/p&gt;

&lt;p&gt;It is the same discipline as escaping on output in HTML. Trust no string with brackets that came from outside your code, and remember the parser is just as happy to choke on a bracket you wrote yourself.&lt;/p&gt;

</description>
      <category>python</category>
      <category>tui</category>
      <category>terminal</category>
      <category>debugging</category>
    </item>
  </channel>
</rss>
