<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
	<channel>
		<title>Bash on tsvinc</title>
		<link>https://tsvinc.dev/tags/bash/</link>
		<description>Recent content in Bash on tsvinc</description>
		<generator>Hugo</generator>
		<language>en-us</language>
		
		
		
		
			<lastBuildDate>Sat, 31 Oct 2026 00:00:00 +0000</lastBuildDate>
		
			<atom:link href="https://tsvinc.dev/tags/bash/index.xml" rel="self" type="application/rss+xml" />
			<item>
				<title>My drift check said 0 differences. It checked 4 arrays.</title>
				<link>https://tsvinc.dev/posts/drift-check-checked-four-arrays/</link>
				<pubDate>Sat, 31 Oct 2026 00:00:00 +0000</pubDate>
				<guid>https://tsvinc.dev/posts/drift-check-checked-four-arrays/</guid>
				<description>&lt;p&gt;My LAN proxy for local LLMs keeps its own catalog of how to serve each model, &lt;code&gt;models.json&lt;/code&gt;. Each of the two GPU boxes behind it keeps the same settings again in its own &lt;code&gt;models.sh&lt;/code&gt;, a bash script. I wrote a drift check to compare the two, and its first run said &amp;quot;0 differences&amp;quot;.&lt;/p&gt;&#xA;&lt;p&gt;I read that as a sentence about the config files. It was a sentence about my parser, which had pulled four arrays out of two bash scripts of about 500 and 600 lines, compared them, and reported back with total calm.&lt;/p&gt;</description>
			</item>
			<item>
				<title>An exception in one layer deleted a block in another</title>
				<link>https://tsvinc.dev/posts/exception-removed-a-block/</link>
				<pubDate>Mon, 26 Oct 2026 00:00:00 +0000</pubDate>
				<guid>https://tsvinc.dev/posts/exception-removed-a-block/</guid>
				<description>&lt;p&gt;&lt;em&gt;Builds on &lt;a href=&#34;https://tsvinc.dev/posts/guard-missed-bash-lc/&#34;&gt;My guard blocked bash -c. Models write bash -lc.&lt;/a&gt;, which explains the guard&#39;s command-position rules and lists &lt;code&gt;$(echo rm) -rf victim&lt;/code&gt; among the things a regex can&#39;t see.&lt;/em&gt;&lt;/p&gt;&#xA;&lt;p&gt;At 20:01 I committed a convenience: my coding agents could now delete reverse-engineering output without asking. At 20:14 I committed a new deny rule for a command my own guard post had filed under &amp;quot;what a regex still can&#39;t see&amp;quot;. In the thirteen minutes between them, one end-to-end run showed that the convenience had quietly switched off the only thing covering that command, in the one agent where anything covered it at all.&lt;/p&gt;</description>
			</item>
			<item>
				<title>My guard blocked bash -c. Models write bash -lc.</title>
				<link>https://tsvinc.dev/posts/guard-missed-bash-lc/</link>
				<pubDate>Mon, 26 Oct 2026 00:00:00 +0000</pubDate>
				<guid>https://tsvinc.dev/posts/guard-missed-bash-lc/</guid>
				<description>&lt;p&gt;&lt;em&gt;Assumes you&#39;ve run a coding agent with auto-approval on, or written a hook that vets its shell commands.&lt;/em&gt;&lt;/p&gt;&#xA;&lt;p&gt;The guard looked at &lt;code&gt;bash -lc &#39;rm -rf build&#39;&lt;/code&gt; and said allow.&lt;/p&gt;&#xA;&lt;p&gt;It denied &lt;code&gt;bash -c &#39;rm -rf build&#39;&lt;/code&gt;. It denied &lt;code&gt;bash -l -c &#39;rm -rf build&#39;&lt;/code&gt;, the same two flags passed as separate arguments. Pack the &lt;code&gt;l&lt;/code&gt; and the &lt;code&gt;c&lt;/code&gt; into one argument and the guard waved the recursive delete through without so much as a question. The guard had applied its rule exactly as written. The rule had been written for a different spelling of the same command.&lt;/p&gt;</description>
			</item>
			<item>
				<title>My unit tests never typed `; echo`</title>
				<link>https://tsvinc.dev/posts/sql-rule-fix-broke-echo/</link>
				<pubDate>Mon, 26 Oct 2026 00:00:00 +0000</pubDate>
				<guid>https://tsvinc.dev/posts/sql-rule-fix-broke-echo/</guid>
				<description>&lt;p&gt;&lt;em&gt;Builds on &lt;a href=&#34;https://tsvinc.dev/posts/guard-missed-bash-lc/&#34;&gt;My guard blocked bash -c. Models write bash -lc.&lt;/a&gt;, which explains the guard and its command-position rules.&lt;/em&gt;&lt;/p&gt;&#xA;&lt;p&gt;Both guards agreed on more than 700 cases. Four coding agents then failed one end-to-end case, and the failures were right.&lt;/p&gt;&#xA;&lt;p&gt;The guards are the TypeScript and Python halves of the guard in my agent-config templates: one shared file of regex rules that hard-denies destructive shell commands even when an agent runs with auto-approval. Both halves have their own unit tests, and the two must agree. The case that failed was &lt;code&gt;git log -S &#39;DROP TABLE&#39; --oneline&lt;/code&gt;, which searches history and destroys nothing. It has been in the end-to-end suite as a near miss, a harmless command that has to keep running, ever since a harness&#39;s own &lt;code&gt;*DROP TABLE*&lt;/code&gt; permission glob blocked it. I had just made the SQL rule stricter, every unit case was green, and the agents now refused to run a search.&lt;/p&gt;</description>
			</item>
			<item>
				<title>Stop docker compose from orphaning anonymous volumes</title>
				<link>https://tsvinc.dev/guides/compose-remove-anonymous-volumes/</link>
				<pubDate>Sun, 25 Oct 2026 00:00:00 +0000</pubDate>
				<guid>https://tsvinc.dev/guides/compose-remove-anonymous-volumes/</guid>
				<description>&lt;p&gt;Tear a script&#39;s compose stack down with &lt;code&gt;docker compose rm -fsv&lt;/code&gt;, then &lt;code&gt;docker compose down&lt;/code&gt;, in one EXIT trap, so each run&#39;s anonymous volumes are deleted with their containers and the named volumes stay.&lt;/p&gt;&#xA;&#xA;&lt;dl class=&#34;guide-scope&#34;&gt;&lt;div class=&#34;guide-scope-row&#34;&gt;&lt;dt&gt;Tested on&lt;/dt&gt;&lt;dd&gt;a Fedora 44 workstation (x86_64), Docker Engine 29.9.1 with the containerd image store, Docker Compose v5.6.0, GNU bash 5.3.9, GNU coreutils 9.10 for &lt;code&gt;timeout&lt;/code&gt; (2026-10-25), against a throwaway test stack.&lt;/dd&gt;&lt;/div&gt;&lt;div class=&#34;guide-scope-row is-gap&#34;&gt;&lt;dt&gt;Not verified&lt;/dt&gt;&lt;dd&gt;Docker Desktop, rootless Docker, Podman, other Compose versions, one-off containers from &lt;code&gt;docker compose run&lt;/code&gt;, moving data out of an anonymous volume.&lt;/dd&gt;&lt;/div&gt;&lt;div class=&#34;guide-scope-row&#34;&gt;&lt;dt&gt;Needs&lt;/dt&gt;&lt;dd&gt;Docker Engine with the Compose plugin and permission to run &lt;code&gt;docker&lt;/code&gt;; the script that brings your stack up and down; network access to pull &lt;code&gt;alpine:3.24.2&lt;/code&gt; for the test stack.&lt;/dd&gt;&lt;/div&gt;&lt;div class=&#34;guide-scope-row&#34;&gt;&lt;dt&gt;Time&lt;/dt&gt;&lt;dd&gt;15 min.&lt;/dd&gt;&lt;/div&gt;&lt;div class=&#34;guide-scope-row&#34;&gt;&lt;dt&gt;Risk&lt;/dt&gt;&lt;dd&gt;&lt;code&gt;rm -v&lt;/code&gt; deletes the anonymous volumes of the containers it removes, and there is no undo for that. Anything you want to keep must sit in a named volume or a bind mount before you change the teardown (checked in Before you start). Named volumes and bind mounts are not touched.&lt;/dd&gt;&lt;/div&gt;&lt;/dl&gt;&#xA;&lt;h2 id=&#34;before-you-start&#34;&gt;Before you start&lt;a class=&#34;heading-anchor&#34; href=&#34;#before-you-start&#34; aria-label=&#34;Link to section: Before you start&#34;&gt;#&lt;/a&gt;&lt;/h2&gt;&lt;ul&gt;&#xA;&lt;li&gt;&#xA;&lt;p&gt;Docker, Compose and bash versions, and whether the test image tag is already on the box (&lt;code&gt;0&lt;/code&gt; = no; Undo removes the tag only in that case):&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-console&#34; data-lang=&#34;console&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;gp&#34;&gt;$&lt;/span&gt; docker version --format &lt;span class=&#34;s1&#34;&gt;&amp;#39;{{.Server.Version}}&amp;#39;&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;go&#34;&gt;29.9.1&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;gp&#34;&gt;$&lt;/span&gt; docker compose version&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;go&#34;&gt;Docker Compose version v5.6.0&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;gp&#34;&gt;$&lt;/span&gt; bash --version &lt;span class=&#34;p&#34;&gt;|&lt;/span&gt; head -1&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;go&#34;&gt;GNU bash, version 5.3.9(1)-release (x86_64-redhat-linux-gnu)&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;gp&#34;&gt;$&lt;/span&gt; docker image ls --filter &lt;span class=&#34;s1&#34;&gt;&amp;#39;reference=alpine:3.24.2&amp;#39;&lt;/span&gt; --format &lt;span class=&#34;s1&#34;&gt;&amp;#39;{{.Repository}}:{{.Tag}}&amp;#39;&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;|&lt;/span&gt; wc -l&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;go&#34;&gt;0&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/li&gt;&#xA;&lt;li&gt;&#xA;&lt;p&gt;Placeholders used below:&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;&lt;code&gt;&amp;lt;SCRIPT&amp;gt;&lt;/code&gt; — the script that runs your stack (benchmark, test run).&lt;/li&gt;&#xA;&lt;li&gt;&lt;code&gt;&amp;lt;PROJECT&amp;gt;&lt;/code&gt; — the Compose project name it uses: the &lt;code&gt;-p&lt;/code&gt; value; without &lt;code&gt;-p&lt;/code&gt;, Compose takes &lt;code&gt;COMPOSE_PROJECT_NAME&lt;/code&gt;, then a top-level &lt;code&gt;name:&lt;/code&gt; in the compose file, then the name of the directory holding the compose file.&lt;/li&gt;&#xA;&lt;li&gt;&lt;code&gt;&amp;lt;CONTAINER&amp;gt;&lt;/code&gt; — one container of that stack, as listed by &lt;code&gt;docker compose -p &amp;lt;PROJECT&amp;gt; ps&lt;/code&gt;.&lt;/li&gt;&#xA;&lt;li&gt;&lt;code&gt;&amp;lt;VOLUME&amp;gt;&lt;/code&gt; — the name of one anonymous volume (64 hex characters).&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;/li&gt;&#xA;&lt;li&gt;&#xA;&lt;p&gt;Back up the script: &lt;code&gt;cp &amp;lt;SCRIPT&amp;gt; &amp;lt;SCRIPT&amp;gt;.bak&lt;/code&gt;. Edit &lt;code&gt;&amp;lt;SCRIPT&amp;gt;&lt;/code&gt; only while no run of it is in progress: bash reads a script as it executes it.&lt;/p&gt;&#xA;&lt;/li&gt;&#xA;&lt;li&gt;&#xA;&lt;p&gt;With your stack up, run this for each container listed by &lt;code&gt;docker compose -p &amp;lt;PROJECT&amp;gt; ps&lt;/code&gt;. Every &lt;code&gt;volume&lt;/code&gt; row with a 64-character hex name is anonymous, and the new teardown deletes it:&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;docker inspect -f &lt;span class=&#34;s1&#34;&gt;&amp;#39;{{.Name}}{{range .Mounts}}{{println}}  {{.Type}} {{.Name}} -&amp;gt; {{.Destination}}{{end}}&amp;#39;&lt;/span&gt; &amp;lt;CONTAINER&amp;gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Output for a stand-in image that declares MongoDB&#39;s two &lt;code&gt;VOLUME&lt;/code&gt; paths, run with a named volume on &lt;code&gt;/data/db&lt;/code&gt; only (how it was built: Field notes):&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-console&#34; data-lang=&#34;console&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;gp&#34;&gt;$&lt;/span&gt; docker inspect -f &lt;span class=&#34;s1&#34;&gt;&amp;#39;{{.Name}}{{range .Mounts}}{{println}}  {{.Type}} {{.Name}} -&amp;gt; {{.Destination}}{{end}}&amp;#39;&lt;/span&gt; anonvol2-db-1&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;go&#34;&gt;/anonvol2-db-1&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;go&#34;&gt;  volume 5f8dc2fb359c192091a7e14bd087a2251ed275b05247a9d3289028cfe992a54a -&amp;gt; /data/configdb&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;go&#34;&gt;  volume anonvol2_dbdata -&amp;gt; /data/db&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Anonymous volumes come from two places: a Compose &lt;code&gt;volumes:&lt;/code&gt; entry with only a container path (&lt;code&gt;- /anon&lt;/code&gt;), and every &lt;code&gt;VOLUME&lt;/code&gt; path in the image that nothing is mounted onto. Current official images declare, for example: &lt;code&gt;mongo&lt;/code&gt; &lt;code&gt;VOLUME /data/db /data/configdb&lt;/code&gt; (7.0 to 9.0); &lt;code&gt;postgres&lt;/code&gt; &lt;code&gt;VOLUME /var/lib/postgresql/data&lt;/code&gt; (16, 17) and &lt;code&gt;VOLUME /var/lib/postgresql&lt;/code&gt; (18); &lt;code&gt;mysql&lt;/code&gt; and &lt;code&gt;mariadb&lt;/code&gt; &lt;code&gt;VOLUME /var/lib/mysql&lt;/code&gt;.&lt;/p&gt;&#xA;&lt;p&gt;If a row you need is anonymous, stop here. Pointing that path at a new named volume does not move the data: in a probe on the same box (a &lt;code&gt;- /data&lt;/code&gt; entry, and an image &lt;code&gt;VOLUME&lt;/code&gt; path), the recreated container saw an empty directory both times, and in the &lt;code&gt;- /data&lt;/code&gt; case the old data was still in the previous anonymous volume, now attached to nothing, which is the kind of volume &lt;code&gt;docker volume prune&lt;/code&gt; deletes. Copy the data into a named volume first; that procedure is not covered here.&lt;/p&gt;&#xA;&lt;/li&gt;&#xA;&lt;/ul&gt;</description>
			</item>
			<item>
				<title>&#34;Terminating immediately&#34; took 90 seconds</title>
				<link>https://tsvinc.dev/posts/terminating-immediately-took-90-seconds/</link>
				<pubDate>Sat, 24 Oct 2026 00:00:00 +0000</pubDate>
				<guid>https://tsvinc.dev/posts/terminating-immediately-took-90-seconds/</guid>
				<description>&lt;p&gt;llama-server keeps a message for the moment you run out of patience with it: &lt;code&gt;Received second interrupt, terminating immediately.&lt;/code&gt; On the 3090 box it printed that at 09:45:04. The next thing systemd had to say about the unit came at 09:46:34, and it was &lt;code&gt;Aborting&lt;/code&gt;.&lt;/p&gt;&#xA;&lt;p&gt;That stop was half of an engine switch. My LAN proxy for local models stops one llama.cpp build and starts another when a request names a model that only the other build has (&lt;a href=&#34;https://tsvinc.dev/posts/cutting-a-stream-doesnt-cancel-the-turn/&#34;&gt;why it does that&lt;/a&gt;). The proxy logged the new engine up after 1m36s. A switch between those two builds takes 2 to 4 seconds. Two signals did this, the second one came from my own script, and the &amp;quot;immediately&amp;quot; path deadlocked inside glibc.&lt;/p&gt;</description>
			</item>
			<item>
				<title>Every way my GPU job queue went quiet</title>
				<link>https://tsvinc.dev/posts/gpu-job-queue-went-quiet/</link>
				<pubDate>Fri, 16 Oct 2026 00:00:00 +0000</pubDate>
				<guid>https://tsvinc.dev/posts/gpu-job-queue-went-quiet/</guid>
				<description>&lt;p&gt;Six days, one GPU, and every way the queue went quiet: idle with work waiting, stuck with nothing logged, or failing in words that pointed somewhere else.&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;&lt;strong&gt;A model download on the net lane:&lt;/strong&gt; GPU idle 3 h 56 min, 3 h 32 min of it behind one 56.3 GB file at about 3 MB/s.&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;A &lt;code&gt;llama-server&lt;/code&gt; left behind by the previous job:&lt;/strong&gt; GPU lane blocked 3 h 44 min. One event, then nothing.&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;A &lt;code&gt;REQUIRES&lt;/code&gt; naming a file nothing would ever write:&lt;/strong&gt; job 84c could not run for 1 h 07 min and logged nothing. The GPU kept busy; the job just disappeared.&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;One model&#39;s launch flags breaking every model&#39;s smoke test:&lt;/strong&gt; the bug lived 16 h 56 min, and three intakes failed their gate, each one looking like the model&#39;s fault.&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;A seed script that stops at the first bad job:&lt;/strong&gt; every job after it is never written. Caught once by a scratch render and reproduced below; no count of what it would have dropped.&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;The watcher that reads the queue:&lt;/strong&gt; froze on a hung ssh overnight. Not measured, by construction.&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;&lt;strong&gt;TL;DR —&lt;/strong&gt; A queue that logs state changes cannot report a state that doesn&#39;t change. Give every kind of waiting an event or a watcher condition: a download that runs too long, a GPU held by a process you didn&#39;t start, a &lt;code&gt;REQUIRES&lt;/code&gt; that has been false for hours, and nothing running at all. Make jobs kill their own servers. Render the seed before it reaches the box. Put timeouts on the thing that watches.&lt;/p&gt;</description>
			</item>
			<item>
				<title>The benchmark was talking to the previous server</title>
				<link>https://tsvinc.dev/posts/benchmark-talking-to-previous-server/</link>
				<pubDate>Tue, 13 Oct 2026 00:00:00 +0000</pubDate>
				<guid>https://tsvinc.dev/posts/benchmark-talking-to-previous-server/</guid>
				<description>&lt;p&gt;Strata v0.1.42 is an inference engine for one model family. Its loader accepts exactly one architecture, &lt;code&gt;qwen4exp&lt;/code&gt;: Qwen3.8-Flash-Next and the models built on it. On the A9 its own &lt;code&gt;setup.sh&lt;/code&gt; compiled the engine against ROCm 7.14 nightly wheels in about seven minutes, no root, and reused the GGUFs already sitting in &lt;code&gt;~/models&lt;/code&gt;. No new model download.&lt;/p&gt;&#xA;&lt;p&gt;So far, a short and happy engine review. Then I asked it to compare its own modes, and the second mode&#39;s result was measured on the first mode&#39;s server.&lt;/p&gt;</description>
			</item>
			<item>
				<title>Designing alerts you&#39;ll actually understand at 3am</title>
				<link>https://tsvinc.dev/posts/alerts-you-understand-at-3am/</link>
				<pubDate>Wed, 08 Jul 2026 00:00:00 +0000</pubDate>
				<guid>https://tsvinc.dev/posts/alerts-you-understand-at-3am/</guid>
				<description>&lt;div class=&#34;tldr&#34;&gt;&#xA;  &lt;span class=&#34;tldr-label&#34;&gt;TL;DR&lt;/span&gt;&#xA;  A digest that dumps raw SMART counters is complete and useless: &lt;code&gt;re=344&lt;/code&gt; means nothing at 3am. Alert on &lt;em&gt;growth&lt;/em&gt; — compare today&#39;s counters against yesterday&#39;s row for the same serial, and push urgently only when a number moved. Seed the first sighting of a drive silently, or every new disk pages you on day one. Two channels, two intents: a calm daily digest that reassures, and an urgent push that interrupts.&#xA;&lt;/div&gt;&#xA;&#xA;&lt;p&gt;Here is the notification my daily SMART logger pushed on day one. Four drives, one line each, everything the tool knew:&lt;/p&gt;</description>
			</item>
			<item>
				<title>Four Bugs, Three Exit Codes From a Benchmark Orchestrator</title>
				<link>https://tsvinc.dev/posts/four-exit-codes-benchmark/</link>
				<pubDate>Thu, 02 Jul 2026 00:00:00 +0000</pubDate>
				<guid>https://tsvinc.dev/posts/four-exit-codes-benchmark/</guid>
				<description>&lt;p&gt;The orchestrator reported ALL_DONE after one branch. The map held fourteen branches and 36 scenarios.&lt;/p&gt;&#xA;&lt;p&gt;Twenty-four framework branches. Docker Compose up, k6 ramp to 200 VUs, Prometheus scrape, tear down, next branch. The loop script is a few dozen lines of bash — read a branch map, check out worktrees, run &lt;code&gt;benchmark-all.sh&lt;/code&gt;, collect results. None of this is complicated, which is exactly why all four bugs took so long to find.&lt;/p&gt;</description>
			</item>
	</channel>
</rss>
