<?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: Alex Andronache</title>
    <description>The latest articles on DEV Community by Alex Andronache (@fum4).</description>
    <link>https://dev.to/fum4</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%2F1001913%2F9e607c91-7208-4435-9384-0b323369ef8b.jpeg</url>
      <title>DEV Community: Alex Andronache</title>
      <link>https://dev.to/fum4</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/fum4"/>
    <language>en</language>
    <item>
      <title>The bug our agents couldn't see</title>
      <dc:creator>Alex Andronache</dc:creator>
      <pubDate>Tue, 27 Oct 2026 15:45:52 +0000</pubDate>
      <link>https://dev.to/nomadware/the-bug-our-agents-couldnt-see-21h3</link>
      <guid>https://dev.to/nomadware/the-bug-our-agents-couldnt-see-21h3</guid>
      <description>&lt;p&gt;Our agents check their own UI work before anyone looks at it. Until this bug, that meant starting the app on our dev boxes, driving it in a headless browser, taking screenshots and comparing what they saw with what they meant to build. That loop caught most things. This post is about a bug it couldn't catch, and why our agents now test in whatever environment the app actually runs in: a &lt;code&gt;position: sticky&lt;/code&gt; section that jittered in Safari on iPhone while scrolling, held perfectly still everywhere else, and turned out to be one &lt;code&gt;overflow-x: clip&lt;/code&gt; on an ancestor.&lt;/p&gt;

&lt;h2&gt;
  
  
  How our agents verified UI
&lt;/h2&gt;

&lt;p&gt;Every change that touched the interface got driven, not just compiled. The agent opened the page in a headless browser on a dev box, scrolled, clicked, resized, read the DOM, the console and the network, and sent screenshots of every affected state with its report. A change nobody has seen isn't done. (More on how the work is organised in &lt;a href="https://nomadware.io/blog/inside-an-agent-run-dev-shop" rel="noopener noreferrer"&gt;Inside an agent-run dev shop&lt;/a&gt;.)&lt;/p&gt;

&lt;p&gt;So when we built the "How we work" section of our marketing site, it had been checked many times over. It's a scroll track: a stage one viewport tall scrolls in, pins to the top with &lt;code&gt;position: sticky; top: 0&lt;/code&gt;, holds while you scroll through one page per principle, then lets go. At every width the agents tried, it was solid.&lt;/p&gt;

&lt;h2&gt;
  
  
  The bug the boxes couldn't see
&lt;/h2&gt;

&lt;p&gt;In Safari on an iPhone, on a fresh load, the pinned stage shook up and down as you scrolled. Not a slow drift: a visible jitter, as if the browser kept re-deciding where the stage belonged. After the on-screen keyboard had been opened once, it stopped.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://nomadware.io/blog/the-bug-our-agents-couldnt-see" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8b9h789aeu7qggxeky98.jpg" alt="Screen recording: the pinned How we work section shaking up and down while scrolling in Safari" width="600" height="696"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The bug, recorded in Safari: the pinned section shakes as the toolbars resize the page. The clip plays in the &lt;a href="https://nomadware.io/blog/the-bug-our-agents-couldnt-see" rel="noopener noreferrer"&gt;original post&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;That narrowed it down. In Safari on iPhone, the toolbars collapse and expand as you scroll, and the viewport resizes with them. The jitter only appeared while that was happening, and it disappeared once the keyboard had been opened. A headless desktop browser has none of this. Its viewport is exactly the size you set, it never moves under your finger, and there is no keyboard to open. The agents could scroll that page all day and never see a single frame of jitter.&lt;/p&gt;

&lt;p&gt;We could have handed the problem to whoever had an iPhone in their pocket, and turned the agents into people relaying screenshots (a person's device pass has its place, as a last check). That doesn't scale, and it throws away the part they're good at: trying something, looking, trying the next thing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Giving the agents an iPhone
&lt;/h2&gt;

&lt;p&gt;So we brought up a macOS box for one purpose: running iOS simulators. The agents can reach it from the dev boxes, start a simulated iPhone, open the page in Safari, scroll, open the keyboard and take screenshots, the same way they already drive a headless browser.&lt;/p&gt;

&lt;p&gt;Safari on the simulator showed the bug exactly as reported: on a fresh load, the toolbars resized the viewport mid-scroll and the stage jittered; after the keyboard had been opened once, it held still. With a device they could drive themselves, the agents could do what they couldn't before: reproduce the bug, and then take it apart.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bisecting a layout bug on a phone
&lt;/h2&gt;

&lt;p&gt;You can't easily attach a debugger to a jitter, so the agents made the page report on itself. A dev-only &lt;code&gt;?probe&lt;/code&gt; query parameter showed the viewport's numbers live: inner and visual viewport, &lt;code&gt;scrollY&lt;/code&gt;, the pinned stage's offset, and counts of &lt;code&gt;resize&lt;/code&gt; and &lt;code&gt;scroll&lt;/code&gt; events. A screenshot before and after the keyboard then said exactly what changed. On a fresh load, one short scroll produced three resizes, with jitter. After the keyboard: no resizes, no jitter.&lt;/p&gt;

&lt;p&gt;The first fix was the obvious one: nothing should re-lay the page when only the viewport's height changes. They dropped &lt;code&gt;min-height: 100vh&lt;/code&gt; from &lt;code&gt;body&lt;/code&gt; and the page frame, removed the stage's &lt;code&gt;lvh&lt;/code&gt;/&lt;code&gt;svh&lt;/code&gt; fallbacks, and made the scripts that re-measure on &lt;code&gt;resize&lt;/code&gt; skip a height-only change. Good hygiene. The jitter stayed.&lt;/p&gt;

&lt;p&gt;So they bisected. A second parameter, &lt;code&gt;?jx=a,b&lt;/code&gt;, switched on one suspected trigger at a time, combinable:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;nosticky&lt;/code&gt;: the stage as &lt;code&gt;position: relative&lt;/code&gt;, to prove sticky was involved at all&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;fit&lt;/code&gt;: a stage no taller than the visible height, in case an overconstrained sticky was the issue&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;noclip&lt;/code&gt;: move the frame's &lt;code&gt;overflow-x: clip&lt;/code&gt; off the stage's ancestors&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;layer&lt;/code&gt;: &lt;code&gt;translateZ(0)&lt;/code&gt; and &lt;code&gt;backface-visibility&lt;/code&gt; on the stage, the classic compositing nudge&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Next to it sat a bare page of long plain text on the same origin, to tell a jitter our page caused from one the browser causes on its own.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;?jx=noclip&lt;/code&gt; was the one. The page frame had this, so decorative halos and full-width bands reaching past the column would never cause sideways scrolling:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nc"&gt;.frame&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;position&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;relative&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;overflow-x&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;clip&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c"&gt;/* the culprit */&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With that clip on any ancestor of the sticky stage, the stage jittered whenever the toolbars resized the viewport. Without it, the stage held still. The result was consistent across every combination of switches: clip, jitter; no clip, none. The probe, the switches and the bare page were deleted in the same pull request as the fix. Debug scaffolding that outlives its bug is how a codebase fills up with mystery flags.&lt;/p&gt;

&lt;h2&gt;
  
  
  The rule: nothing clips above the pinned element
&lt;/h2&gt;

&lt;p&gt;The fix is one line removed. The work is everything that line was quietly covering for. If nothing may clip between the root and the pinned stage, then nothing may reach past the page sideways either. That's now a written convention, with three consequences.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Full-bleed layers use the page's real width, not &lt;code&gt;100vw&lt;/code&gt;.&lt;/strong&gt; The hero's animated dot field was &lt;code&gt;width: 100vw&lt;/code&gt;. With classic scrollbars, &lt;code&gt;100vw&lt;/code&gt; includes the scrollbar, so the layer was wider than the page, and the clip had been hiding it. Now a few lines of script publish &lt;code&gt;document.documentElement.clientWidth&lt;/code&gt; as a custom property whenever it changes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nc"&gt;.dots&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;position&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;absolute&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;left&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;calc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;50%&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nf"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="l"&gt;--page-width&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;100vw&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="m"&gt;2&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nl"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="l"&gt;--page-width&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;100vw&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;Without JavaScript it falls back to &lt;code&gt;100vw&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Bands bleed with paint, not overflow.&lt;/strong&gt; Tinted section bands run edge to edge while their content stays in the column. They use a spread shadow and a &lt;code&gt;clip-path&lt;/code&gt;, and neither creates scrollable overflow:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nc"&gt;.section.is-band&lt;/span&gt;&lt;span class="nd"&gt;::before&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;''&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;position&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;absolute&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;inset&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;z-index&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;-1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;background&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="l"&gt;--band&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nl"&gt;box-shadow&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="m"&gt;100vmax&lt;/span&gt; &lt;span class="nf"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="l"&gt;--band&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nl"&gt;clip-path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;inset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="m"&gt;-100vmax&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 shadow paints the tint far past the box; the &lt;code&gt;clip-path&lt;/code&gt; cuts it back to the band's own height while letting it run sideways. The page's scroll width never notices. That's the only way anything on the site goes full-bleed now.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Sizes come from script, in pixels.&lt;/strong&gt; Every &lt;code&gt;vh&lt;/code&gt;, &lt;code&gt;svh&lt;/code&gt; or &lt;code&gt;lvh&lt;/code&gt; value that follows the toolbars moves under the visitor's finger. The stage's height and the track's scroll length are pixel values set by script instead, and on touch devices they hold until the &lt;em&gt;width&lt;/em&gt; changes: a rotation, not a toolbar. The decision whether to pin at all holds the same way, which fixed a second bug for free: the keyboard used to shrink the viewport below the pin threshold and unpin the stage mid-form.&lt;/p&gt;

&lt;h2&gt;
  
  
  The check that keeps it fixed
&lt;/h2&gt;

&lt;p&gt;Removing a clip is easy. Keeping it removed means proving nothing overflows, so any change to a full-bleed layer ends with the same check:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;documentElement&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;scrollWidth&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;documentElement&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;clientWidth&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The agents run it at widths from 320 to 1920 px, with and without classic scrollbars, with the stage pinned and the consent card up. This one the headless browsers can do perfectly well, which is the point: each kind of check runs where it's cheapest and still true.&lt;/p&gt;

&lt;h2&gt;
  
  
  Give agents the devices your users have
&lt;/h2&gt;

&lt;p&gt;The agents didn't get smarter between not seeing this bug and fixing it. They got eyes on the right device. The same loop (reproduce, instrument, change one thing, look again) works on any bug they can actually observe, and fails on any bug they can't.&lt;/p&gt;

&lt;p&gt;So the lesson isn't about sticky positioning. If your users are on iPhones, your agents need an iPhone. That's how we work now: a UI change isn't verified in the environment that's convenient, but in the ones the app actually runs in. Give agents the devices your users have, and they can debug what your users actually see.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://nomadware.io/blog/the-bug-our-agents-couldnt-see" rel="noopener noreferrer"&gt;nomadware.io&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
    </item>
    <item>
      <title>When CI can't keep up with your agents</title>
      <dc:creator>Alex Andronache</dc:creator>
      <pubDate>Tue, 27 Oct 2026 15:45:48 +0000</pubDate>
      <link>https://dev.to/nomadware/when-ci-cant-keep-up-with-your-agents-3pf</link>
      <guid>https://dev.to/nomadware/when-ci-cant-keep-up-with-your-agents-3pf</guid>
      <description>&lt;p&gt;One day last week, a single project of ours opened &lt;strong&gt;73 pull requests&lt;/strong&gt; and shipped to its main branch 67 times. Not a big team working overtime: one engineer making the calls, and agents that never clock off.&lt;/p&gt;

&lt;p&gt;That same day burned 3,186 GitHub Actions minutes. The month came to about 3,600, on a plan that includes 2,000, and ninety-two percent of it came from that one repository.&lt;/p&gt;

&lt;p&gt;Nothing was broken. Our CI was simply built for a human pace, and we don't work at one anymore.&lt;/p&gt;

&lt;h2&gt;
  
  
  What an agent-paced day looks like
&lt;/h2&gt;

&lt;p&gt;Every run fanned out into eight jobs, and every job started with a full &lt;code&gt;pnpm install&lt;/code&gt;. Then each merge ran the same suite again, on the push to main, against code that had passed it minutes earlier.&lt;/p&gt;

&lt;p&gt;Most teams never open 73 PRs in a day, so a setup like that never shows its cost. Ours can, and on busy days they do. Agents don't get tired: they open small, focused PRs all day and all night, which is exactly what we want from them. Hosted CI bills every one of those runs as if it were a rare event.&lt;/p&gt;

&lt;p&gt;Do the math: some 140 runs, eight jobs each, a few minutes of setup per job. Most of that bill was downloading dependencies, not testing anything.&lt;/p&gt;

&lt;h2&gt;
  
  
  The machines were already there
&lt;/h2&gt;

&lt;p&gt;Our agents work on dedicated boxes (&lt;a href="https://nomadware.io/blog/inside-an-agent-run-dev-shop" rel="noopener noreferrer"&gt;how that runs, day to day&lt;/a&gt;). Each one holds every checkout, a warm package store and each repository's pinned toolchain. Every check a hosted runner performs (format, lint, typecheck, test, build) a box can run faster, because nothing has to be fetched first.&lt;/p&gt;

&lt;p&gt;So we stopped renting runners for checks and made the boxes the CI.&lt;/p&gt;

&lt;h2&gt;
  
  
  One moment per kind of check
&lt;/h2&gt;

&lt;p&gt;The trick isn't running everything everywhere. Each kind of check runs at exactly one moment: the cheapest one that still catches the problem in time.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Moment&lt;/th&gt;
&lt;th&gt;What runs&lt;/th&gt;
&lt;th&gt;Where&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;commit&lt;/td&gt;
&lt;td&gt;message format, staged-state guards (under 2 s)&lt;/td&gt;
&lt;td&gt;box&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;push&lt;/td&gt;
&lt;td&gt;format, lint, typecheck: affected projects only&lt;/td&gt;
&lt;td&gt;box&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;merge&lt;/td&gt;
&lt;td&gt;every unit test, integration test and build&lt;/td&gt;
&lt;td&gt;box, one gate at a time&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;release&lt;/td&gt;
&lt;td&gt;everything, end-to-end included, on clean runners&lt;/td&gt;
&lt;td&gt;GitHub Actions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;after release&lt;/td&gt;
&lt;td&gt;deploy only, no re-run&lt;/td&gt;
&lt;td&gt;GitHub Actions&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Push runs static checks and nothing else. Agents push at checkpoints rather than after every commit, so a test suite there would buy little early feedback for a lot of memory. Tests belong to the merge, where they run once per PR.&lt;/p&gt;

&lt;h2&gt;
  
  
  A gate that reports back to GitHub
&lt;/h2&gt;

&lt;p&gt;The merge gate is one command. It checks that the working tree is exactly the PR's head, takes a box-wide lock so gates queue instead of fighting over memory, runs the repository's &lt;code&gt;pre-merge&lt;/code&gt; task, re-checks the tree in case anything changed mid-run, and posts the result where a reviewer will see it: as a commit status on the PR (every finished run also counts toward our live stats).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gh api &lt;span class="s2"&gt;"repos/&lt;/span&gt;&lt;span class="nv"&gt;$OWNER&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="nv"&gt;$REPO&lt;/span&gt;&lt;span class="s2"&gt;/statuses/&lt;/span&gt;&lt;span class="nv"&gt;$HEAD_SHA&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-f&lt;/span&gt; &lt;span class="nv"&gt;context&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;devbox/pre-merge &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-f&lt;/span&gt; &lt;span class="nv"&gt;state&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;success &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-f&lt;/span&gt; &lt;span class="nv"&gt;description&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"tests + build passed on the box · 4m 12s"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Statuses are free, and they show on the PR page just like a check. Our merge command refuses unless the PR's &lt;em&gt;current&lt;/em&gt; head carries a green one. A new push changes the SHA, so the gate runs again for exactly what merges.&lt;/p&gt;

&lt;p&gt;Each repository opts in by naming its tasks after the moments:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="k"&gt;[&lt;/span&gt;&lt;span class="n"&gt;tasks&lt;/span&gt;&lt;span class="k"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pre-push&lt;/span&gt;&lt;span class="k"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;description&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;"Format, lint and typecheck what changed"&lt;/span&gt;
&lt;span class="n"&gt;run&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="s"&gt;"pnpm check:format"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"pnpm check:lint"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"pnpm typecheck"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="k"&gt;[&lt;/span&gt;&lt;span class="n"&gt;tasks&lt;/span&gt;&lt;span class="k"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pre-merge&lt;/span&gt;&lt;span class="k"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;description&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;"Every test, then every build"&lt;/span&gt;
&lt;span class="n"&gt;depends&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="s"&gt;"test"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"build"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No task, nothing runs at that moment. One command lists exactly what a repository checks, and there's no YAML to keep in sync with it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What still runs on GitHub
&lt;/h2&gt;

&lt;p&gt;Some things really do need a clean machine. Two PRs can each pass alone and break together, and a release should be proven on a runner that has never seen the code before. So the release gate still runs on GitHub, every job in parallel, before the production branch moves. The deploy afterwards trusts that green SHA instead of testing it again. Scheduled jobs and anything that needs a secret the boxes don't hold stay on GitHub too.&lt;/p&gt;

&lt;p&gt;On top of that, one rule: any change that can spend Actions minutes needs a human's approval first, with an estimate attached (minutes per run × runs per day). Cheap-looking CI changes are how we got here.&lt;/p&gt;

&lt;h2&gt;
  
  
  What we gave up
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Branch protection.&lt;/strong&gt; The gate is a commit status, not a protection rule. Agents can only merge through the command that checks it, and a person can still merge from the web UI, on purpose.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A pristine runner at merge time.&lt;/strong&gt; A box is shared by every agent working on it, which is why the gate re-checks the tree after it runs and throws the result away if anything moved. The release gate's clean runners catch the rest.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Someone else's uptime.&lt;/strong&gt; A box can die like any machine, and keeping it alive is now our job. So none of them is special: each is provisioned from code (the server in Terraform, packages and services in Ansible, dotfiles in chezmoi, secrets encrypted in git), and a fresh, identical one takes a couple of minutes of anyone's time.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The result
&lt;/h2&gt;

&lt;p&gt;Speed was never the problem; checking at that speed was. Now the checks cost nothing extra and start instantly against a warm store, so moving faster never means a change gets checked less. GitHub's minutes go to the handful of runs that genuinely need a clean machine. And when an agent opens its 73rd PR of the day, the only thing anyone notices is the progress.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://nomadware.io/blog/when-ci-cant-keep-up" rel="noopener noreferrer"&gt;nomadware.io&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>agents</category>
      <category>cicd</category>
      <category>devops</category>
      <category>githubactions</category>
    </item>
    <item>
      <title>How an agent-run dev shop actually works</title>
      <dc:creator>Alex Andronache</dc:creator>
      <pubDate>Tue, 27 Oct 2026 15:45:44 +0000</pubDate>
      <link>https://dev.to/nomadware/how-an-agent-run-dev-shop-actually-works-3000</link>
      <guid>https://dev.to/nomadware/how-an-agent-run-dev-shop-actually-works-3000</guid>
      <description>&lt;p&gt;A typical morning here starts with reading, not writing. Overnight a pull request landed for a client's mobile app, with a before-and-after storyboard of every screen it touched. A Linear issue picked up a comment explaining a root cause, file and line included. An agent stopped and is waiting on one product question.&lt;/p&gt;

&lt;p&gt;None of that happened by accident. "Software engineering, at agent speed" is our pitch, and in practice it means three things: people who own every decision that matters, coding agents that multiply how fast those decisions become code, and a harness of gates and guardrails decides what is good enough to ship.&lt;/p&gt;

&lt;h2&gt;
  
  
  Who does what
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Role&lt;/th&gt;
&lt;th&gt;Does&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Coding agents&lt;/td&gt;
&lt;td&gt;read the code, plan, implement, test, open PRs, review each other, merge once the gate is green on projects that delegate merging&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;People&lt;/td&gt;
&lt;td&gt;pick what matters, make product calls, approve what ships and anything costly or irreversible&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;The harness&lt;/td&gt;
&lt;td&gt;runs every check, refuses unsafe merges, keeps state on disk and in git&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Those calls get made from anywhere: a laptop, a phone, and Neuralink once it's available to us. Every agent session can be driven remotely, so a decision takes a sentence, not a desk and a free afternoon.&lt;/p&gt;

&lt;h2&gt;
  
  
  Boxes built from code
&lt;/h2&gt;

&lt;p&gt;The agents work on dedicated boxes, and none of them is precious. Each one is described entirely in a repository and rebuilt from it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Terraform&lt;/strong&gt; owns each box's existence: the server itself, an IP address that outlives any rebuild, firewall rules, DNS. State sits in a private bucket, not on anyone's laptop.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ansible&lt;/strong&gt; owns everything inside: packages, the toolchain manager, the agent CLI, the private network join, Docker, the libraries a headless browser needs for agents to check their UI, and a clone of every repository with its dependencies already in place.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;chezmoi&lt;/strong&gt; owns dotfiles, helper scripts and the systemd units each agent session runs in.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;age&lt;/strong&gt; encrypts every secret, committed to git next to the thing that uses it. A fresh box decrypts what it needs during provisioning; nothing is copied over by hand.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Bringing up a new box takes a handful of commands, with &lt;code&gt;terraform apply&lt;/code&gt; and &lt;code&gt;ansible-playbook&lt;/code&gt; doing the heavy lifting, plus one login. Start to finish that's about fifteen minutes, and a person is busy for just a couple of them. A second playbook run should be close to a no-op; anything it still changes points to a buggy role, which then gets fixed. Provisioning changes are rehearsed twice in a disposable container before they can merge.&lt;/p&gt;

&lt;p&gt;The repository is what's real; a fix applied only to a running box is drift. A package installed by hand or a dotfile edited in place vanishes at the next rebuild. Agents are held to this like anyone: when something breaks, they ask where in the repo the fix should live, not how to patch the machine in front of them.&lt;/p&gt;

&lt;p&gt;So boxes are disposable and identical: if one misbehaves we replace it, and the replacement has every tool, repository and rule the old one had.&lt;/p&gt;

&lt;h2&gt;
  
  
  Always in motion
&lt;/h2&gt;

&lt;p&gt;An agent session is a conversation, and losing one is losing work. So sessions don't live in a terminal tab. Each one is its own systemd unit, inside a memory-capped slice, with a detachable terminal underneath. It survives a dropped connection, a closed laptop and a reboot. If it crashes, it comes back as &lt;em&gt;the same&lt;/em&gt; conversation, and a start that can't find its transcript refuses to begin from scratch.&lt;/p&gt;

&lt;p&gt;Dev servers are the opposite: stateless and restarted constantly. They get their own supervisor, so restarting a bundler can never take an agent down with it.&lt;/p&gt;

&lt;p&gt;That separation is what lets work continue overnight. When a plan splits into streams that are independent, we use a hub: a single session coordinates, and each stream gets a builder of its own, working in a separate worktree from a brief cut out of the plan. Only the hub merges. It reads every PR, lands those that pass, and hands each contract between streams (an API payload, an error code) to both sides before either builds on it. The first time we ran it for real, two builders delivered ten items in nine PRs.&lt;/p&gt;

&lt;p&gt;Long sessions need managing too. A hook watches each session's context size and, past a threshold, asks it to roll over: write a handoff document, start a successor in the same place, park itself. Parked sessions post a clear notice as their last message, so nobody opening a chat mistakes a stopped session for a dropped connection.&lt;/p&gt;

&lt;h2&gt;
  
  
  One task, one worktree, one PR
&lt;/h2&gt;

&lt;p&gt;Every change goes through a pull request. A one-line typo fix too. Each task gets its own git worktree, branched from a fresh fetch of the default branch, with env files and dependencies set up before the agent writes a line. Two sessions never share a checkout.&lt;/p&gt;

&lt;p&gt;PRs follow one anatomy: a short summary, a before-and-after storyboard whenever any UI changed, a few bullets of changes, and a folded section holding the why, the rejected alternatives and the test evidence. The human part stays short. If it doesn't fit in about fifteen lines, the PR is too big and gets split.&lt;/p&gt;

&lt;p&gt;UI work isn't done until someone has seen it. Agents run the app themselves, click through it, and attach screenshots of each screen and state the change touches, empty and error states included, before it can merge.&lt;/p&gt;

&lt;h2&gt;
  
  
  Zero friction: written where you'd look
&lt;/h2&gt;

&lt;p&gt;We write as if the next reader knows nothing, because for an agent that's literally true. So every piece of knowledge has a home, in the place you'd go looking for it, written to be read:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Code comments&lt;/strong&gt; say why something is the way it is: the hidden constraint, the workaround, the invariant. Never what the code already says.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A README in every package, app and feature folder:&lt;/strong&gt; what it is, what it depends on, its public API, how to extend it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rule files, one home per rule.&lt;/strong&gt; The instructions every session loads: what holds for every project in one file, what's specific to a repository in its own, an app's in the app's. A rule kept in two places is a bug waiting to happen, because the copies drift apart.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Docs and runbooks&lt;/strong&gt; for how things work and how to do the occasional job, reached from the rule file's routing table. They change in the same commit as the behavior they describe.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Decision records&lt;/strong&gt; for the choices that shape a project, with their reasoning, so a later reader can tell when that reasoning has expired.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Issues&lt;/strong&gt; carry the analysis, and every decision made after it, as comments. The why outlives the session that worked it out.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A ledger for what's postponed:&lt;/strong&gt; engineering follow-ups, product questions waiting on a trigger, ideas, and the things only a person can do, each in its own list. Nothing deferred lives only in a chat.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Commits&lt;/strong&gt; are small and say what changed. &lt;strong&gt;Pull requests&lt;/strong&gt; add a short summary, before-and-after screenshots for any change to the interface, and, folded underneath, the why, the alternatives we turned down and the test evidence.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Skills&lt;/strong&gt; package the procedures we repeat, like publishing a post or onboarding a repository, so they're done the same way every time.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Handoff notes&lt;/strong&gt; when a long session passes its work to a fresh one, so nothing in flight depends on anyone's memory.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Put together, that trail answers whatever an engineer would ask: why this, why not that, what else it touches, what's still open. An agent reads it the same way, so nobody ever waits on a person for something the project already knows.&lt;/p&gt;

&lt;h2&gt;
  
  
  Gates instead of trust
&lt;/h2&gt;

&lt;p&gt;Agents produce a lot of PRs, so the checks run on our own boxes, not on rented CI runners (&lt;a href="https://nomadware.io/blog/when-ci-cant-keep-up" rel="noopener noreferrer"&gt;When CI can't keep up with your agents&lt;/a&gt;). Format, lint and typecheck run on push. The merge gate runs every test and every build, one gate per box at a time, and reports back on the PR as a commit status. Merging is refused unless the PR's latest commit is green. Full end-to-end suites run at the release gate on clean runners.&lt;/p&gt;

&lt;p&gt;Some guardrails exist because something went wrong. A PR once relocated a layout provider; its gate was green and its storyboard looked right, yet nine end-to-end specs broke. Since then, a PR that relocates a layout, provider or navigator has to run the specs it affects before merging. Anything that could spend CI minutes needs a person's explicit approval, with an estimate attached.&lt;/p&gt;

&lt;h2&gt;
  
  
  Product mindset, not ticket closing
&lt;/h2&gt;

&lt;p&gt;We don't want agents that agree with everything. Ours are told to push back: if a request looks wrong, say so with the reasoning, and recommend something better before building. Check what people assert against the code rather than taking it on trust. Point out the missing empty state, the confusing flow, the screen with no way back.&lt;/p&gt;

&lt;p&gt;The same goes for the code. Keeping projects clean outranks almost everything else: one way to do a thing, migrations finished rather than left at 80%, every invariant in exactly one place. Agents are fast, which is exactly why this matters: pace multiplies whatever habits a codebase already has.&lt;/p&gt;

&lt;h2&gt;
  
  
  What our engineers decide
&lt;/h2&gt;

&lt;p&gt;People choose what gets built and in what order. They make the product calls the code can't settle. They sign off on whatever spends money or can't be reversed: a change to live data, a new ticket, a release. And they look at the storyboards.&lt;/p&gt;

&lt;p&gt;Everything else keeps going through the night without waiting on anyone, and leaves a written trail. When someone checks in each morning, from wherever they are, the work has moved and the questions are waiting.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://nomadware.io/blog/inside-an-agent-run-dev-shop" rel="noopener noreferrer"&gt;nomadware.io&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>agents</category>
      <category>ai</category>
      <category>automation</category>
      <category>softwareengineering</category>
    </item>
    <item>
      <title>Hi, nice to meet you</title>
      <dc:creator>Alex Andronache</dc:creator>
      <pubDate>Tue, 27 Oct 2026 15:45:41 +0000</pubDate>
      <link>https://dev.to/nomadware/hi-nice-to-meet-you-1b3d</link>
      <guid>https://dev.to/nomadware/hi-nice-to-meet-you-1b3d</guid>
      <description>&lt;p&gt;Hi. We're Nomadware, a small software studio. If there's one thing to know about us first, it's this: we love building things.&lt;/p&gt;

&lt;p&gt;We love the moment a rough idea becomes something you can actually use, and the slower stretch after it, where that thing quietly gets good. We love technology for what it lets a handful of people make. And we still feel a small, private delight when something we built does exactly what it should for the first time.&lt;/p&gt;

&lt;h2&gt;
  
  
  The feeling of a well-made thing
&lt;/h2&gt;

&lt;p&gt;You know it the moment you touch it. A button that responds before your finger has left the glass. A page that loads and stays put, with no paragraph shoved down by a late image. A search that finds what you meant rather than what you typed. A tool that does the thing you had in mind, then steps aside.&lt;/p&gt;

&lt;p&gt;None of it asks for attention. The better it's made, the less you think about it. What's left is a kind of ease, the sense that the product is on your side.&lt;/p&gt;

&lt;p&gt;That simplicity is never an accident. Behind an interface that feels effortless, someone tried the clumsy version, didn't like it, and went back. Someone wondered what a person sees on their very first visit, what happens when the network drops halfway, whether the most common task could lose a step. Those questions rarely make it onto a roadmap. They're the difference between software people put up with and software people are glad to open.&lt;/p&gt;

&lt;h2&gt;
  
  
  Chasing it for other people
&lt;/h2&gt;

&lt;p&gt;That feeling is what we're after whenever we build for someone. Not an impressive diagram, not a long list of features, but the moment a user opens what we made, it simply works, and they never give us a thought.&lt;/p&gt;

&lt;p&gt;A product is thousands of small decisions. People rarely notice any one of them, but they feel all of them together, so we spend time where nobody asked us to.&lt;/p&gt;

&lt;p&gt;We build with coding agents for the same reason. They carry the long, repetitive parts of the work, which leaves us more time, and a quicker rhythm, for what needs taste and judgment: what to make, what to leave out, how it should feel. The decisions stay with our engineers; the agents add pace.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why a blog
&lt;/h2&gt;

&lt;p&gt;Every studio says it moves fast and keeps quality. We'd rather show it than claim it. So we're writing it down as we go: the harness, the guardrails, the bugs, the numbers.&lt;/p&gt;

&lt;p&gt;There's a simpler reason too. Our agents begin each session with no memory of the project, so we already &lt;a href="https://nomadware.io/blog/inside-an-agent-run-dev-shop" rel="noopener noreferrer"&gt;write everything down for them&lt;/a&gt;: every rule, every decision and the reason behind it, kept where anyone would look. Our agents get the full story every morning. It seemed only fair that our peers get it too.&lt;/p&gt;

&lt;p&gt;That includes the parts that broke. A gate that passed and still let a regression through, a bug our agents couldn't see, a tool we built and then stopped building. Those are where most of the learning is, and a blog that only shows the wins isn't much use to someone deciding how to build.&lt;/p&gt;

&lt;p&gt;It's the same instinct behind the activity card on our home page. Its numbers (merged pull requests, agent runs, checks, the issues those checks caught) aren't written by hand; they're computed from what our agents actually did, refreshed every few minutes.&lt;/p&gt;

&lt;p&gt;These notes are for two kinds of reader:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Founders and product teams&lt;/strong&gt; deciding how to build their next product, and whether a team that runs on agents is one they can trust with it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Engineers&lt;/strong&gt; curious how a shop like this holds quality when agents write most of the code.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you're carrying an idea for something people will be glad to use, we'd love to hear about it.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://nomadware.io/blog/hi-nice-to-meet-you" rel="noopener noreferrer"&gt;nomadware.io&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>software</category>
      <category>softwaredevelopment</category>
      <category>startup</category>
    </item>
  </channel>
</rss>
