<?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: Alexandre Oliveira</title>
    <description>The latest articles on DEV Community by Alexandre Oliveira (@alegauss).</description>
    <link>https://dev.to/alegauss</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%2F679600%2Fcd704729-f8b0-4350-8188-438a89accb70.jpg</url>
      <title>DEV Community: Alexandre Oliveira</title>
      <link>https://dev.to/alegauss</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/alegauss"/>
    <language>en</language>
    <item>
      <title>Your coding agent is bad at Docker, and it isn't the agent's fault</title>
      <dc:creator>Alexandre Oliveira</dc:creator>
      <pubDate>Thu, 15 Oct 2026 15:11:27 +0000</pubDate>
      <link>https://dev.to/alegauss/your-coding-agent-is-bad-at-docker-and-it-isnt-the-agents-fault-5el6</link>
      <guid>https://dev.to/alegauss/your-coding-agent-is-bad-at-docker-and-it-isnt-the-agents-fault-5el6</guid>
      <description>&lt;p&gt;I let a coding agent run my local stack for a few months. It writes the compose file, brings the services up, reads the logs, fixes the bug, and checks the fix worked.&lt;/p&gt;

&lt;p&gt;It's good at most of that. Docker is where it struggles every time, and after a while I stopped blaming the model. The problem is Docker's interface, which was designed for a person sitting at a terminal. That person can open a browser and has some context about the machine. They also notice when a log repeats itself. An agent gets stdout once the process exits and pays for every byte of it.&lt;/p&gt;

&lt;p&gt;Here are the five places where it breaks, with the workaround I use for each using plain &lt;code&gt;docker&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. "port is already allocated" names no culprit
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Error response from daemon: driver failed programming external connectivity:
Bind for 0.0.0.0:8080 failed: port is already allocated
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The daemon knows the bind failed but has no idea who holds the port. The operating system knows. An agent that sees this error usually guesses: it changes the port in the compose file, or restarts the engine, or both. Then you end up with a stack on 8081 and nobody remembers why.&lt;/p&gt;

&lt;p&gt;On Windows the answer takes two lines:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$c&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Get-NetTCPConnection&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-LocalPort&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;8080&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-State&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Listen&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;Get-Process&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Id&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$c&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;OwningProcess&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Select-Object&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;ProcessName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Path&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On Linux or macOS, &lt;code&gt;lsof -iTCP:8080 -sTCP:LISTEN&lt;/code&gt; does the same job.&lt;/p&gt;

&lt;p&gt;The fix is to put this in the agent's instructions as the &lt;strong&gt;first&lt;/strong&gt; step after a bind failure, before it is allowed to edit any port. "Find out who holds it" is a rule the agent can follow. "Use judgment" isn't.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. &lt;code&gt;docker ps&lt;/code&gt; and &lt;code&gt;docker rm -f -v&lt;/code&gt; look the same to an allowlist
&lt;/h2&gt;

&lt;p&gt;Agent tools ask permission before running shell commands, and an allowlist removes the prompt for the safe ones. The trouble is that Docker doesn't split commands into reads and writes. Every verb hangs off the same binary, so you have two bad options:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;allow &lt;code&gt;docker *&lt;/code&gt;, which also allows &lt;code&gt;docker volume rm&lt;/code&gt; and &lt;code&gt;docker system prune -a --volumes&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;approve every call by hand, and after the fortieth &lt;code&gt;docker ps&lt;/code&gt; you stop reading the prompts&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You can allowlist subcommand by subcommand (&lt;code&gt;docker ps&lt;/code&gt;, &lt;code&gt;docker logs&lt;/code&gt;, &lt;code&gt;docker inspect&lt;/code&gt;, &lt;code&gt;docker images&lt;/code&gt;), and that's what I do. It's still a list you maintain, though, and there are holes in it. &lt;code&gt;docker exec&lt;/code&gt; can be a read or a write depending on what it runs. &lt;code&gt;docker compose&lt;/code&gt; mixes &lt;code&gt;ps&lt;/code&gt; and &lt;code&gt;down -v&lt;/code&gt; under one prefix. A global flag before the subcommand (&lt;code&gt;docker --context prod rm ...&lt;/code&gt;) gets around a naive prefix rule.&lt;/p&gt;

&lt;p&gt;My workaround: allowlist only the pure reads, and accept that every write gets a prompt. That's the place where you want the prompt anyway.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Logs are the biggest token sink in the session
&lt;/h2&gt;

&lt;p&gt;A container in a crash loop writes the same stack trace on every restart. Eight restarts means eight copies, and &lt;code&gt;docker logs api&lt;/code&gt; hands the agent all eight. Most of what it reads in a debugging session is repetition.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;--tail&lt;/code&gt; cuts the size but also cuts the part you needed, because the first error is usually at the top. &lt;code&gt;uniq&lt;/code&gt; doesn't help, since repeated traces aren't adjacent lines.&lt;/p&gt;

&lt;p&gt;What works best for me is to keep the log out of the context entirely:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker logs api &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; .agent/api.log 2&amp;gt;&amp;amp;1
&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="nt"&gt;-E&lt;/span&gt; &lt;span class="s2"&gt;"error|exception|fatal"&lt;/span&gt; .agent/api.log | &lt;span class="nb"&gt;head&lt;/span&gt; &lt;span class="nt"&gt;-40&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A file on disk costs nothing. The agent pays only for the lines grep returns, and it can go back for context around a specific line number. Put this in the instructions as the default way to read logs, and keep &lt;code&gt;docker logs&lt;/code&gt; without redirection for short-lived containers.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. "running" is not "answering"
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;docker ps&lt;/code&gt; says &lt;code&gt;Up 2 minutes&lt;/code&gt;. The agent concludes the fix worked. Then you open the browser and get connection refused, because the process inside bound &lt;code&gt;127.0.0.1&lt;/code&gt; instead of &lt;code&gt;0.0.0.0&lt;/code&gt;, or crashed after startup, or is still running migrations.&lt;/p&gt;

&lt;p&gt;Up to that point, the expensive step in the loop was me: open the browser, look, report back. The agent can't close that gap if all it checks is the container state.&lt;/p&gt;

&lt;p&gt;Give it an actual readiness check with a deadline:&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="nb"&gt;timeout &lt;/span&gt;60 bash &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s1"&gt;'until curl -sf http://localhost:8080/health; do sleep 2; done'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;echo &lt;/span&gt;READY &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"NOT ANSWERING after 60s"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And when it fails, read what the healthcheck itself said instead of guessing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker inspect &lt;span class="nt"&gt;--format&lt;/span&gt; &lt;span class="s1"&gt;'{{json .State.Health}}'&lt;/span&gt; api
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The healthcheck log usually says &lt;code&gt;ECONNREFUSED 127.0.0.1:8080&lt;/code&gt; in plain words, and that's the whole diagnosis.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Cleanup can't tell your work from the agent's
&lt;/h2&gt;

&lt;p&gt;After an afternoon of reproducing a bug, the agent has created six containers, two networks and a volume. When you want them gone, the only tool on offer is &lt;code&gt;prune&lt;/code&gt;, which works on the whole machine. It can't tell the agent's scaffolding from the Postgres volume you've been filling since March. So nobody delegates &lt;code&gt;prune&lt;/code&gt;, and the scaffolding piles up.&lt;/p&gt;

&lt;p&gt;Labels fix this, provided they're applied at creation time:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker run &lt;span class="nt"&gt;--label&lt;/span&gt; agent.session&lt;span class="o"&gt;=&lt;/span&gt;repro-17 ...
&lt;span class="c"&gt;# compose: add `labels: { agent.session: repro-17 }` to each service&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then the cleanup is scoped:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker ps &lt;span class="nt"&gt;-aq&lt;/span&gt; &lt;span class="nt"&gt;--filter&lt;/span&gt; &lt;span class="nv"&gt;label&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;agent.session&lt;span class="o"&gt;=&lt;/span&gt;repro-17 | xargs &lt;span class="nt"&gt;-r&lt;/span&gt; docker &lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Leave volumes out of that line on purpose. A container comes back from its image, but a deleted volume is gone for good.&lt;/p&gt;

&lt;p&gt;The catch is that the agent has to remember to add the label every time, and the one it forgets is the one that bites you later.&lt;/p&gt;

&lt;h2&gt;
  
  
  The pattern behind all five
&lt;/h2&gt;

&lt;p&gt;Each of these is a join Docker doesn't do: the daemon's view and the OS's view (ports), the command and its intent (allowlists), the raw log and what's new in it (dedup), the container state and the service's behavior (readiness), the object and who created it (labels).&lt;/p&gt;

&lt;p&gt;A person does those joins in their head without noticing. An agent does them with extra tool calls, which you pay for, or it skips them and guesses.&lt;/p&gt;

&lt;p&gt;None of the workarounds above need anything beyond plain &lt;code&gt;docker&lt;/code&gt; and a few lines in your agent's instructions file. Start with #3 and #4, since those two gave me the biggest drop in wasted turns.&lt;/p&gt;

&lt;p&gt;I wrote these wrappers in too many projects, and on Windows I ended up putting them into a small CLI with a &lt;code&gt;read&lt;/code&gt;/&lt;code&gt;do&lt;/code&gt; split that an allowlist can actually express. It's part of &lt;a href="https://alegauss.github.io/freewilly/" rel="noopener noreferrer"&gt;FreeWilly&lt;/a&gt;, if you're on Windows and want them prebuilt. The patterns work the same without it.&lt;/p&gt;

&lt;p&gt;What's the Docker failure your agent hits most? I'd like to know which ones I missed.&lt;/p&gt;

</description>
      <category>docker</category>
      <category>devops</category>
      <category>productivity</category>
      <category>ai</category>
    </item>
    <item>
      <title>Claude Code can write your game. Can it make the rest of it?</title>
      <dc:creator>Alexandre Oliveira</dc:creator>
      <pubDate>Tue, 13 Oct 2026 22:25:38 +0000</pubDate>
      <link>https://dev.to/alegauss/claude-code-can-write-your-game-can-it-make-the-rest-of-it-4caa</link>
      <guid>https://dev.to/alegauss/claude-code-can-write-your-game-can-it-make-the-rest-of-it-4caa</guid>
      <description>&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/VWih17gxWpg" width="710" height="399"&gt;
  &lt;/iframe&gt;
&lt;br&gt;
A coding agent can write a lot of a game today: the player controller, the menu logic, the save system. Then you need a mascot, a coin sound, a background loop and a third level, and the agent can't help anymore. It can't see a render, hear a sound or play a level, and it has no idea when any of those is good enough.&lt;/p&gt;

&lt;p&gt;AI generators don't fix that. You type a prompt and get a 3D model, a picture or a voice line in seconds, and then the real work starts. Does it match your sketch? Is it the right orange? Does the loop click at the seam? Does it look like it belongs in the same game as the other forty assets? The generator made a guess, and you're the one who has to check it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;polyweave&lt;/strong&gt; is a free, open-source plugin for Claude Code that takes on that checking.&lt;/p&gt;
&lt;h2&gt;
  
  
  Where it came from
&lt;/h2&gt;

&lt;p&gt;polyweave started inside a real game. &lt;strong&gt;Cottony&lt;/strong&gt; is a plush-themed match-3, with hearts, drops and stars made of felt that squash when they land. Its tiles are rendered in Blender, and getting them to look right meant tuning fourteen render settings by hand: change one number, wait about two minutes for a render, look at it, decide, repeat.&lt;/p&gt;

&lt;p&gt;Making the model was quick. Most of the time went into the stretch between &lt;em&gt;having a model&lt;/em&gt; and &lt;em&gt;knowing it's right&lt;/em&gt;. That stretch is what polyweave is for.&lt;/p&gt;
&lt;h2&gt;
  
  
  The idea: write down what "correct" means
&lt;/h2&gt;

&lt;p&gt;With polyweave, every part of your game starts as a short file that says what correct means for it. You don't write that file yourself. You describe what you want in plain words, and Claude Code writes it down as a list of checks with limits:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the silhouette has to match my sketch&lt;/li&gt;
&lt;li&gt;the face has to be this cream colour, within a small tolerance&lt;/li&gt;
&lt;li&gt;the sound has to be in C, not too loud, and loop without a click&lt;/li&gt;
&lt;li&gt;this level has to be a little harder than the last one&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Once that bar exists, the work no longer depends on someone squinting at renders. Claude Code can make a candidate, measure it against the bar, adjust the settings and try again, and the misses get thrown out before they ever reach you.&lt;/p&gt;
&lt;h2&gt;
  
  
  How it works, in four steps
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Ask.&lt;/strong&gt; Tell Claude Code what you need: &lt;em&gt;"a mascot like this sketch"&lt;/em&gt;, &lt;em&gt;"a coin sound in C"&lt;/em&gt;, &lt;em&gt;"make level 3 a bit harder"&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Make.&lt;/strong&gt; polyweave drives the right tool for the job, whether that's Blender, Godot, a 3D or picture generator, a synth or a voice service.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Check.&lt;/strong&gt; Each result is measured against the bar. A miss gets tuned and tried again.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Approve.&lt;/strong&gt; The best candidates show up side by side on a review page, each with its checks next to it. You approve, reject, or ask for a change in your own words ("make the ears rounder"), and Claude Code goes back to work.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;
  
  
  What it makes
&lt;/h2&gt;

&lt;p&gt;The same loop covers every layer of a game:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;3D models&lt;/strong&gt;, built in Blender or bought from a generator and fitted to your drawing&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Images&lt;/strong&gt;: concept art, icons, sprite sheets, store capsules&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Music&lt;/strong&gt; loops that close without a seam&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sound effects&lt;/strong&gt;, synthesised or bought, checked for loudness and clicks&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Visual effects&lt;/strong&gt; like particles, glows and hits&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Voices&lt;/strong&gt;, read in each character's own voice&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Words&lt;/strong&gt;: names, dialogue and text, checked against your world and your fonts&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Levels&lt;/strong&gt;, measured for difficulty before anyone plays them&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Animation&lt;/strong&gt;, kept as text so a timing change is one edit you can diff&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Game kits&lt;/strong&gt;: menus, settings, saves, controls and accessibility, installed already tested&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Every layer shares one config file, one record of how each part was made and one review page, so you judge the game's parts together instead of one tool at a time.&lt;/p&gt;
&lt;h2&gt;
  
  
  It works with the tools you already have
&lt;/h2&gt;

&lt;p&gt;polyweave doesn't replace Blender, Godot or your favourite generator. It sits in the middle and talks to each one in its own language:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;Tools it drives&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;3D&lt;/td&gt;
&lt;td&gt;Blender, Meshy&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pictures&lt;/td&gt;
&lt;td&gt;FLUX, Ideogram&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sound and music&lt;/td&gt;
&lt;td&gt;sfxr, MIDI&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Voice&lt;/td&gt;
&lt;td&gt;ElevenLabs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Game engine&lt;/td&gt;
&lt;td&gt;Godot&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Here's what that looks like for a mascot drawn from your sketch:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Before anything is bought, polyweave checks the request against your drawing, so a credit isn't spent on a request that plainly won't match.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Meshy&lt;/strong&gt; generates the model. polyweave keeps a copy in your project along with the prompt, seed, cost and licence, so nothing expires on the service's side.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Blender&lt;/strong&gt; renders it, and polyweave measures the render against your sketch: does the silhouette line up, and is the colour right where it should be?&lt;/li&gt;
&lt;li&gt;Claude Code adjusts the render settings until every check passes, and only then do the best candidates reach your review page.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Godot&lt;/strong&gt; opens the game and takes a screenshot, so you see the mascot where the player will actually meet it.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;There are plenty of tools that connect an agent to Blender or Godot. What they don't tell the agent is when to stop. In polyweave that's the job of the bar, so the agent works on the tools until the result is &lt;em&gt;right&lt;/em&gt;, not just until it's &lt;em&gt;done&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;Which tool you use is a line in a config file in your repo. Swap models whenever you like and the bar each part has to pass stays the same.&lt;/p&gt;
&lt;h2&gt;
  
  
  You stay in charge
&lt;/h2&gt;

&lt;p&gt;Handing an agent a credit card and your art direction is a fair thing to be nervous about. polyweave has three rules about that, and the agent can't get around any of them:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The agent can't approve its own work.&lt;/strong&gt; It can measure a part, but only you can accept it. Whether something &lt;em&gt;looks&lt;/em&gt; right, &lt;em&gt;sounds&lt;/em&gt; right or &lt;em&gt;feels&lt;/em&gt; fair is your call.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The agent can't spend money on its own judgement.&lt;/strong&gt; You set a spending ceiling once. Every paid request is quoted first and written to a ledger, and a request that clearly won't match your reference is refused before any credit is spent. Claude Code works freely under the ceiling and can never raise it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Everything stays on your machine.&lt;/strong&gt; There's no account and no hosted service. Specs, jobs and records are plain files in your repository, so a teammate can review or repeat anything.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each file also keeps its history: which model, prompt, seed, cost and licence produced it. Writing your credits screen and checking licences become a lookup.&lt;/p&gt;
&lt;h2&gt;
  
  
  Real games are being made with it
&lt;/h2&gt;

&lt;p&gt;Viglet Games is making its games with polyweave, and both are in development for Steam:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://cottony.viglet.com/" rel="noopener noreferrer"&gt;&lt;strong&gt;Cottony&lt;/strong&gt;&lt;/a&gt;, the plush match-3 where it all started&lt;/li&gt;
&lt;li&gt;
&lt;a href="http://spinhold.viglet.com/" rel="noopener noreferrer"&gt;&lt;strong&gt;Spinhold&lt;/strong&gt;&lt;/a&gt;, a neon ring shooter where you fly around the outside of a spinning colony&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;You need Claude Code and Python 3.11 or newer. Blender and Godot are optional: polyweave tells you which tools it found and what each missing one would unlock.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;git+https://github.com/alegauss/polyweave
/plugin marketplace add alegauss/polyweave
/plugin &lt;span class="nb"&gt;install &lt;/span&gt;polyweave@polyweave
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then ask:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Set up polyweave in this project, and make me a mascot from docs/sketch.png.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;ul&gt;
&lt;li&gt;Website: &lt;a href="https://alegauss.github.io/polyweave/" rel="noopener noreferrer"&gt;alegauss.github.io/polyweave&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Code: &lt;a href="https://github.com/alegauss/polyweave" rel="noopener noreferrer"&gt;github.com/alegauss/polyweave&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you're making a game with an agent, I'd like to hear which part of it is still eating your time. Leave a comment.&lt;/p&gt;

</description>
      <category>gamedev</category>
      <category>ai</category>
      <category>claude</category>
      <category>blender</category>
    </item>
    <item>
      <title>374 tests went in, 352 came out, and the build was green</title>
      <dc:creator>Alexandre Oliveira</dc:creator>
      <pubDate>Tue, 13 Oct 2026 14:24:20 +0000</pubDate>
      <link>https://dev.to/alegauss/374-tests-went-in-352-came-out-and-the-build-was-green-e89</link>
      <guid>https://dev.to/alegauss/374-tests-went-in-352-came-out-and-the-build-was-green-e89</guid>
      <description>&lt;p&gt;The last run had 374 tests. This one had 352. No failures, so the build was green.&lt;/p&gt;

&lt;p&gt;Twenty-two checks had disappeared. The test host died partway through, the runner counted what it managed to finish, and the only evidence was a total nobody had a reason to compare against yesterday's. Every tool in the chain did what it was designed to do. The result was still false.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/alegauss/winwright" rel="noopener noreferrer"&gt;winwright&lt;/a&gt; is a UI testing engine for Windows desktop apps (WPF, WinForms, tray icons, menus, dialogs), and that missing 22 is the reason it exists. Most of its design follows from refusing to let that happen again.&lt;/p&gt;

&lt;h2&gt;
  
  
  A test has three possible answers
&lt;/h2&gt;

&lt;p&gt;Most test runners give you two outcomes: it passed or it failed. Desktop UI testing has a third one, and it shows up all the time.&lt;/p&gt;

&lt;p&gt;Say a test needs to type into a text box. To send keys, Windows has to give your app the foreground. Sometimes it won't: you clicked into Slack, a notification took focus, the CI agent has no interactive session at all. The keystrokes go somewhere else, the read-back doesn't change, and the test goes red.&lt;/p&gt;

&lt;p&gt;The red is wrong. Nothing in your application broke. You'll spend twenty minutes reading code that is fine, because the report said &lt;em&gt;your app failed&lt;/em&gt; when what happened was &lt;em&gt;the machine never let me try&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;winwright calls that third outcome a &lt;strong&gt;hole&lt;/strong&gt;: an assertion that never ran because something it needed was missing. A hole isn't a pass and it isn't a failure, and the exit code keeps them apart:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Exit code&lt;/th&gt;
&lt;th&gt;Verdict&lt;/th&gt;
&lt;th&gt;What it means&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;0&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Passed&lt;/td&gt;
&lt;td&gt;Everything that was asked was checked, and held&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;1&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Failed&lt;/td&gt;
&lt;td&gt;Your application did something wrong&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;2&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Degraded&lt;/td&gt;
&lt;td&gt;Some checks could not run, and the report names why&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;3&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Broken&lt;/td&gt;
&lt;td&gt;The run itself threw, so nothing after that point was observed&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The summary line puts what &lt;em&gt;didn't&lt;/em&gt; happen first:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Passed: 1 of 9 cases, 8 not run, 3 assertions over case 'renaming a profile writes it back'.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A pass over one case out of nine is a weaker claim than a pass, and a report that buries "8 not run" at the end of the line is one people will skim past. If the 352 run had produced that sentence, someone would have noticed.&lt;/p&gt;

&lt;p&gt;Even a run that selects nothing is caught. A filter that matches zero cases is refused, because zero cases contain zero failures and would otherwise come back green.&lt;/p&gt;

&lt;h2&gt;
  
  
  The desk is not your application
&lt;/h2&gt;

&lt;p&gt;Most of the holes you'll meet belong to the machine, not to your code, and winwright says which side each one is on. "No foreground", "no interactive session", "a flyout the shell wouldn't open" and "somebody else touched the mouse during the case" are reported as conditions of the desk. None of them can turn a run red.&lt;/p&gt;

&lt;p&gt;One detail I like: a freshly launched app is often still coming forward when the first step runs. Measured against one adopter, that took 1,453 ms. A tool that checks the foreground once at that moment concludes another window owns the desk, which is true for that instant and false for the run. So winwright waits for the foreground up to the project's resolve timeout. Only after that does it report a hole, and the hole says how long it waited.&lt;/p&gt;

&lt;p&gt;There's also an easier way around most of it. The default act doesn't need the foreground at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ask the control, don't move the mouse
&lt;/h2&gt;

&lt;p&gt;Every accessible Windows control has a UI Automation peer: an API that lets a screen reader ask "what's your value?" or tell it "invoke yourself". winwright's default verbs (&lt;code&gt;invoke&lt;/code&gt;, &lt;code&gt;toggle&lt;/code&gt;, &lt;code&gt;set value&lt;/code&gt;, &lt;code&gt;select&lt;/code&gt;, &lt;code&gt;expand&lt;/code&gt;) go through those patterns. No synthetic mouse, no keyboard, no need to be in front.&lt;/p&gt;

&lt;p&gt;The verbs that &lt;em&gt;do&lt;/em&gt; put real input on the desk (&lt;code&gt;type&lt;/code&gt;, &lt;code&gt;click&lt;/code&gt;, &lt;code&gt;press&lt;/code&gt;, &lt;code&gt;drag&lt;/code&gt;, &lt;code&gt;nudge&lt;/code&gt;) are marked that way in the catalogue, and they prove something different from their pattern twins. &lt;code&gt;set value&lt;/code&gt; writes through the Value pattern. &lt;code&gt;type&lt;/code&gt; presses keys.&lt;/p&gt;

&lt;p&gt;That difference caught a real bug. A WPF window hosted under a WinForms message pump accepted &lt;strong&gt;no keyboard input at all&lt;/strong&gt;, and the &lt;code&gt;set value&lt;/code&gt; check passed anyway, because it never used the keyboard. The &lt;code&gt;type&lt;/code&gt; check failed. Neither one was flaky. They answered two separate questions, and only one of those questions was what a user would actually do.&lt;/p&gt;

&lt;p&gt;That's also why &lt;code&gt;click&lt;/code&gt; won't run until you give it a reason: &lt;code&gt;NoAutomationPeer&lt;/code&gt;, &lt;code&gt;NotificationArea&lt;/code&gt;, &lt;code&gt;CustomTemplate&lt;/code&gt;, or &lt;code&gt;PointerIsTheAct&lt;/code&gt;. If the reason defaults, every act quietly turns into a mouse click and your suite ends up driving the desktop instead of asking controls.&lt;/p&gt;

&lt;h2&gt;
  
  
  A test is a data file
&lt;/h2&gt;

&lt;p&gt;A winwright case isn't C#. It's JSON:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"cases"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"renaming a profile writes it back"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"catches"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"a rename that updates the list and never the file"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"tags"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"smoke"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"profiles"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"steps"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"locator"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"TabItem[name=&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;Profiles&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;]"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"act"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"select"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"locator"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Edit#profileName"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"act"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"set value"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"with"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Beta"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"expect"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Beta"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"reads"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"value"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"locator"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"CheckBox#autosave"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"act"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"toggle"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"expect"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"On"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"reads"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"toggle"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"locator"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Button#save"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"act"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"invoke"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"named"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"save the profile"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"locator"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Text#status"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"act"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"read"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"expect"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Saved"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"reads"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"text"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice what's missing: no &lt;code&gt;Thread.Sleep&lt;/code&gt;, no retry loop, no &lt;code&gt;WaitUntil&lt;/code&gt;. The engine owns the waits, the attempts and the verdict. The case only says what to do and what should be true afterwards.&lt;/p&gt;

&lt;p&gt;It even knows which acts are safe to retry. &lt;code&gt;set value&lt;/code&gt; can be repeated if the read-back is late. &lt;code&gt;toggle&lt;/code&gt; and &lt;code&gt;invoke&lt;/code&gt; get exactly one try, because a retried toggle leaves the checkbox in the opposite state and then fails about that.&lt;/p&gt;

&lt;p&gt;Locators use one grammar, read the same way by every verb:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Button#save                          automation id + control type
Button[name="Save as..."]            by name
MenuItem[nameStarts="Personal "]     the front of a name that carries live state after it
ComboBox|Slider|Edit                 any one of several types
Window#main &amp;gt; Pane &amp;gt; Button#save     a descendant, at any depth
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;&amp;gt;&lt;/code&gt; means &lt;em&gt;descendant at any depth&lt;/em&gt;, not &lt;em&gt;direct child&lt;/em&gt;. That's deliberate: UI Automation wraps controls in panes that differ between frameworks, between framework versions, and between a maximised window and a restored one. A direct-child locator is the kind that passes on your machine and breaks on a colleague's.&lt;/p&gt;

&lt;h2&gt;
  
  
  Typed expectations go stale
&lt;/h2&gt;

&lt;p&gt;This is the part that changed how I write UI tests.&lt;/p&gt;

&lt;p&gt;One adopter's hand-written harness checked a window's tabs. It listed the three tab labels by hand and reported &lt;em&gt;"all three tab headers read"&lt;/em&gt;. Then the window gained a fourth tab. The harness kept reporting all three tab headers read, against a four-tab window, and nothing went red. It was still accurate about the three tabs it knew. It had just stopped checking the window.&lt;/p&gt;

&lt;p&gt;A string you type into a test is a second copy of the truth, and that second copy is the one that goes stale. So winwright prefers expectations that are &lt;strong&gt;derived&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"locator"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"TabItem"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"act"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"read"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"covers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"stats.tab"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;stats.tab&lt;/code&gt; is a key in your application's own strings file. The claim goes &lt;strong&gt;both ways&lt;/strong&gt;: every string declared under that key appears in the window, &lt;em&gt;and nothing else does&lt;/em&gt;. Add a fifth tab to the strings file and this step fails until the window shows it. Add a fifth tab to the window without declaring it and the step fails too. The test file doesn't change either way.&lt;/p&gt;

&lt;p&gt;The same idea shows up throughout the format:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;label&lt;/code&gt;&lt;/strong&gt; compares a reading against a string from your language files, in whatever language the fixture launched the window in. Your &lt;code&gt;pt-BR&lt;/code&gt; run checks Portuguese without a separate test.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;forEach&lt;/code&gt;&lt;/strong&gt; runs a case once per string under a key, so a settings page with six panels gets six runs, and a seventh panel added next quarter gets a seventh with no edit.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;reportedSets&lt;/code&gt;&lt;/strong&gt; asks the application itself for sets that live in the machine's data rather than in strings (profiles, accounts, devices). Your test then holds on a machine with one profile and on a machine with five.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;moves&lt;/code&gt;&lt;/strong&gt;, &lt;strong&gt;&lt;code&gt;answers&lt;/code&gt;&lt;/strong&gt;, &lt;strong&gt;&lt;code&gt;matches&lt;/code&gt;&lt;/strong&gt;, &lt;strong&gt;&lt;code&gt;sameAs&lt;/code&gt;&lt;/strong&gt; and &lt;strong&gt;&lt;code&gt;unlike&lt;/code&gt;&lt;/strong&gt; cover values a case can't know in advance: the reading changed, it said &lt;em&gt;something&lt;/em&gt;, it has the right shape, it came back to what an earlier step read.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Even negative claims are honest about how hard they looked. &lt;code&gt;never&lt;/code&gt; asserts that a string (say, &lt;em&gt;"no readings yet"&lt;/em&gt;) never appeared while a step was waiting. One adopter's report comes back from cache in 12 ms or gets rebuilt in 961 ms with that placeholder flashing on screen, and once the waiting is over the two windows look identical. So the result reports how many times it checked. For a claim that something never appeared, the number of looks is the evidence.&lt;/p&gt;

&lt;h2&gt;
  
  
  Screenshots that refuse to lie
&lt;/h2&gt;

&lt;p&gt;A screenshot proves layout and nothing else. It says nothing about whether a button works, and winwright keeps those questions in separate loops: pictures and geometry for layout, act-then-read for input, render-twice-and-compare-bytes for determinism.&lt;/p&gt;

&lt;p&gt;Within its job, a capture is held to the same rule. A capture is &lt;strong&gt;refused&lt;/strong&gt; if a window covered it, if the backdrop showed through, if it came back as a single flat colour, or if the page in it was still showing its loading text. The refusal names the window that got in the way, its process and the rectangle. An optional in-app package (&lt;code&gt;Winwright.InApp&lt;/code&gt;) lets the application render its own visual tree to a PNG. That includes a popup nobody has clicked open yet, which no screen copy can capture.&lt;/p&gt;

&lt;h2&gt;
  
  
  Refusals are instructions
&lt;/h2&gt;

&lt;p&gt;winwright says "no" a lot, and it tries to make every "no" tell you what to do next:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A locator that doesn't parse names the character position and the reason.&lt;/li&gt;
&lt;li&gt;A misspelled control type gets the nearest name UI Automation actually has.&lt;/li&gt;
&lt;li&gt;An unknown key in a case file is refused, never ignored. &lt;code&gt;"expects"&lt;/code&gt; next to &lt;code&gt;"expect"&lt;/code&gt; would otherwise load, run, check nothing, and pass.&lt;/li&gt;
&lt;li&gt;A case whose steps expect nothing is refused: it acts, never looks, and passes on a build that still has the bug.&lt;/li&gt;
&lt;li&gt;Every refusal is addressed into the file, as in &lt;code&gt;cases[2].steps[1].act&lt;/code&gt;, so you fix a field and not a file.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The part built for AI agents
&lt;/h2&gt;

&lt;p&gt;This is where it gets a bit unusual.&lt;/p&gt;

&lt;p&gt;If you ask a coding agent to "test this window", it will write a harness script. The script is always available and always faster in the moment, and that's how one adopter ended up with a &lt;strong&gt;2,732-line&lt;/strong&gt; one.&lt;/p&gt;

&lt;p&gt;winwright ships as a &lt;a href="https://claude.com/claude-code" rel="noopener noreferrer"&gt;Claude Code&lt;/a&gt; plugin, and the plugin includes a &lt;code&gt;PreToolUse&lt;/code&gt; hook. Any write whose content references &lt;code&gt;Winwright.Acting&lt;/code&gt;, &lt;code&gt;Winwright.Locating&lt;/code&gt; or &lt;code&gt;Winwright.Asserting&lt;/code&gt; is &lt;strong&gt;denied&lt;/strong&gt;, and the denial points the agent to the case format and the tool that validates it. The agent gets the refusal before it writes the script, not after you've reviewed it.&lt;/p&gt;

&lt;p&gt;The MCP server then gives the agent four tools:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;winwright_format&lt;/code&gt;&lt;/strong&gt;: every field a case may carry, and the closed list of what each accepts.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;winwright_vocabulary&lt;/code&gt;&lt;/strong&gt;: every act, what it needs beside it, and whether the engine may retry it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;winwright_check&lt;/code&gt;&lt;/strong&gt;: validates a case before the file exists. Its input schema &lt;em&gt;is&lt;/em&gt; the loader's schema, so a misspelled key can't even be sent.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;winwright_run&lt;/code&gt;&lt;/strong&gt;: runs it and returns the verdict. A machine that can't observe returns a hole naming which condition is missing, not a red and not a green.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Two commands wire it into a repository, and committing &lt;code&gt;.claude/settings.json&lt;/code&gt; wires it for everyone who clones it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;claude plugin marketplace add alegauss/winwright &lt;span class="nt"&gt;--scope&lt;/span&gt; project
claude plugin &lt;span class="nb"&gt;install &lt;/span&gt;winwright@alegauss &lt;span class="nt"&gt;--scope&lt;/span&gt; project
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One deliberate rule: &lt;strong&gt;the tool never writes the test.&lt;/strong&gt; There's no recorder that turns clicks into a scenario and no assertion invented on your behalf. The engine owns the loop and the waits, and deciding what's worth asserting stays with you.&lt;/p&gt;

&lt;h2&gt;
  
  
  When you should &lt;em&gt;not&lt;/em&gt; use it
&lt;/h2&gt;

&lt;p&gt;The project site has &lt;a href="https://alegauss.github.io/winwright/" rel="noopener noreferrer"&gt;an honest comparison&lt;/a&gt; where every alternative keeps the column it wins:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Playwright&lt;/strong&gt; for anything in a browser. The name is a nod, not a rivalry.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Appium&lt;/strong&gt; when one suite has to drive more than Windows.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;WinAppDriver&lt;/strong&gt; when WebDriver compatibility is the requirement.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;FlaUI&lt;/strong&gt; when you want a mature UI Automation library without opinions attached.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Your existing script&lt;/strong&gt;, right up until the run where the total changes and the colour doesn't.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;winwright is Windows-only, .NET 10 only, and will stay that way: the whole engine is UI Automation and Win32. What it offers is narrower: a report that tells a failed check from a check that never ran, one locator grammar, expectations derived from the app instead of typed into the test, and screenshots that refuse instead of lying.&lt;/p&gt;

&lt;h2&gt;
  
  
  Trying it
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!-- the test project that drives your app --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;PackageReference&lt;/span&gt; &lt;span class="na"&gt;Include=&lt;/span&gt;&lt;span class="s"&gt;"Winwright"&lt;/span&gt; &lt;span class="na"&gt;Version=&lt;/span&gt;&lt;span class="s"&gt;"1.0.2"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;

&lt;span class="c"&gt;&amp;lt;!-- optional, inside the app, for the readings only it can take --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;PackageReference&lt;/span&gt; &lt;span class="na"&gt;Include=&lt;/span&gt;&lt;span class="s"&gt;"Winwright.InApp"&lt;/span&gt; &lt;span class="na"&gt;Version=&lt;/span&gt;&lt;span class="s"&gt;"1.0.2"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add a &lt;code&gt;winwright.json&lt;/code&gt; with your executable's path, write a &lt;code&gt;.cases.json&lt;/code&gt;, and run it with four calls:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;project&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ProjectDeclaration&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;repository&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;declared&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ScenarioFile&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Across&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ScenarioFile&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;LoadAll&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Combine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;repository&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"cases"&lt;/span&gt;&lt;span class="p"&gt;)));&lt;/span&gt;

&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;var&lt;/span&gt; &lt;span class="n"&gt;register&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ProcessRegister&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;For&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;project&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;   &lt;span class="c1"&gt;// anything the run started is stopped, and named if it won't stop&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;verdict&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Suite&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Launch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;declared&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Selection&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;All&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;register&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;project&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="n"&gt;Console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WriteLine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;verdict&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Sentence&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;verdict&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ExitCode&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One piece of advice from the docs that I'd underline: &lt;strong&gt;give the run a desk of its own.&lt;/strong&gt; Cases take the foreground and send real input. If you run them on the machine you're working at, your typing lands in the app under test. That isn't a flaky suite, it's two people sharing one keyboard. Use a VM with an interactive session, a dedicated runner, or any machine nobody is sitting at.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;📦 NuGet: &lt;a href="https://www.nuget.org/packages/Winwright" rel="noopener noreferrer"&gt;Winwright&lt;/a&gt; · &lt;a href="https://www.nuget.org/packages/Winwright.InApp" rel="noopener noreferrer"&gt;Winwright.InApp&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;🌎 Site: &lt;a href="https://alegauss.github.io/winwright/" rel="noopener noreferrer"&gt;alegauss.github.io/winwright/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;📖 Docs: &lt;a href="https://alegauss.github.io/winwright/docs/" rel="noopener noreferrer"&gt;alegauss.github.io/winwright/docs/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;💻 Source (Apache-2.0): &lt;a href="https://github.com/alegauss/winwright" rel="noopener noreferrer"&gt;github.com/alegauss/winwright&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;Has your suite ever passed with fewer tests than it had the day before? I'd like to hear how you noticed, or how long it took.&lt;/p&gt;

</description>
      <category>testing</category>
      <category>dotnet</category>
      <category>windows</category>
      <category>opensource</category>
    </item>
    <item>
      <title>I spent 10 years building enterprise search for clients. Then I open-sourced all of it.</title>
      <dc:creator>Alexandre Oliveira</dc:creator>
      <pubDate>Mon, 27 Jul 2026 22:53:54 +0000</pubDate>
      <link>https://dev.to/alegauss/i-spent-10-years-building-enterprise-search-for-clients-then-i-open-sourced-all-of-it-3nk5</link>
      <guid>https://dev.to/alegauss/i-spent-10-years-building-enterprise-search-for-clients-then-i-open-sourced-all-of-it-3nk5</guid>
      <description>&lt;p&gt;Every enterprise search project I did in the last decade ended the same way. A search engine (Solr or Elasticsearch), a pile of glue code for facets and relevance, then, in recent years, a RAG pipeline bolted on the side, and finally an agent framework on top of that. Three stacks, three sets of bugs, one recurring client requirement: &lt;strong&gt;the data cannot leave our network&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;After repeating that build enough times, I turned it into a single platform. This year I open-sourced the whole thing under Apache 2.0: &lt;a href="https://github.com/openviglet/turing-ce" rel="noopener noreferrer"&gt;Viglet Turing ES&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;This post is about the three design decisions that I think matter most, and how to try it in one command.&lt;/p&gt;

&lt;h2&gt;
  
  
  Decision 1: run on the engine you already have
&lt;/h2&gt;

&lt;p&gt;Most teams evaluating "AI search" already operate Solr or Elasticsearch. Asking them to migrate to a new engine just to get RAG is a non-starter.&lt;/p&gt;

&lt;p&gt;So Turing sits &lt;strong&gt;on top&lt;/strong&gt; of the engine instead of replacing it. Solr, Elasticsearch or an embedded Lucene index, behind one query API. Hybrid retrieval fuses keyword (BM25) and vector results with Reciprocal Rank Fusion, and a pluggable reranker (LLM, cross-encoder or Cohere) reorders the page. If you have nothing installed, the embedded Lucene engine means the whole stack runs in a single container.&lt;/p&gt;

&lt;p&gt;The same idea applies to models: OpenAI, Anthropic, Gemini, Azure, or fully local via Ollama. For air-gapped deployments, local models plus local embeddings plus embedded Lucene means literally nothing leaves the machine.&lt;/p&gt;

&lt;h2&gt;
  
  
  Decision 2: answers you can audit, including "no answer"
&lt;/h2&gt;

&lt;p&gt;The feature enterprises actually ask about is not chat. It's &lt;strong&gt;trust&lt;/strong&gt;. A support portal or an intranet cannot ship confident wrong answers.&lt;/p&gt;

&lt;p&gt;Turing's RAG chat streams answers with inline citations that trace back to the exact source passage. Before the LLM sees anything, a relevance gate drops weak matches. After generation, an optional groundedness check flags claims the sources don't support. And when nothing in the index clears the bar, the assistant gives a deterministic "not covered here" answer instead of improvising.&lt;/p&gt;

&lt;p&gt;That last behavior sounds small. In practice it's the difference between a demo and something a regulated organization will put in front of users.&lt;/p&gt;

&lt;h2&gt;
  
  
  Decision 3: agents grounded in the same index
&lt;/h2&gt;

&lt;p&gt;Once search and RAG share an index, agents get interesting: they can act on content instead of just answering. Turing's agents do tool calling, run skills in a Docker sandbox, and connect to MCP servers, so the same assistant that cites your docs can also call your internal APIs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;One command, no account, no telemetry:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker run &lt;span class="nt"&gt;-p&lt;/span&gt; 2700:2700 ghcr.io/openviglet/turing-ce
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The console comes up at &lt;code&gt;http://localhost:2700/console&lt;/code&gt; (set the admin password with the &lt;code&gt;TURING_ADMIN_PASSWORD&lt;/code&gt; env var on first run).&lt;/p&gt;

&lt;p&gt;Searching is a plain REST call:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="s2"&gt;"http://localhost:2700/api/sn/sample-site/search?q=enterprise+search&amp;amp;rows=10&amp;amp;_setlocale=en_US"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And the RAG chat streams over SSE:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="s2"&gt;"http://localhost:2700/api/sn/sample-site/chat?q=What+is+enterprise+search"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There are TypeScript SDKs (a zero-dependency vanilla one and a React one) if you're embedding search in a frontend. The live demo on the site runs on that same SDK, over Turing's own documentation, so you can see the cited answers before installing anything: &lt;a href="https://turing.viglet.org/#playground" rel="noopener noreferrer"&gt;turing.viglet.org/#playground&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  One honest note about the repo
&lt;/h2&gt;

&lt;p&gt;The public repository is a clean snapshot per release, because day-to-day development happens against client work I can't publish. I know a thin commit history looks odd at first glance, so I'd rather say it here than have you wonder. Issues and PRs are very real and ship in the next release.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd love feedback on
&lt;/h2&gt;

&lt;p&gt;If you self-host Elasticsearch or run local LLMs and have opinions about hybrid ranking, citation UX, or what a search platform should refuse to answer, I want to hear them. The repo is &lt;a href="https://github.com/openviglet/turing-ce" rel="noopener noreferrer"&gt;github.com/openviglet/turing-ce&lt;/a&gt; and the docs live at &lt;a href="https://docs.viglet.org/turing/" rel="noopener noreferrer"&gt;docs.viglet.org&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Thanks for reading!&lt;/p&gt;

</description>
      <category>opensource</category>
      <category>ai</category>
      <category>rag</category>
      <category>selfhosted</category>
    </item>
  </channel>
</rss>
