<?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: Nunc</title>
    <description>The latest articles on DEV Community by Nunc (@nunc).</description>
    <link>https://dev.to/nunc</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%2F156810%2Fe88f8e22-712e-4845-8662-e399d98101b2.png</url>
      <title>DEV Community: Nunc</title>
      <link>https://dev.to/nunc</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/nunc"/>
    <language>en</language>
    <item>
      <title>I Run Several Claude Code Sessions From My Phone. Grok Bot Is the Switchboard.</title>
      <dc:creator>Nunc</dc:creator>
      <pubDate>Sat, 24 Oct 2026 13:14:39 +0000</pubDate>
      <link>https://dev.to/nunc/i-run-several-claude-code-sessions-from-my-phone-grok-bot-is-the-switchboard-3li8</link>
      <guid>https://dev.to/nunc/i-run-several-claude-code-sessions-from-my-phone-grok-bot-is-the-switchboard-3li8</guid>
      <description>&lt;p&gt;I have Claude Code sessions running on more than one PC, and I drive them from wherever I am, often from my phone on the sofa. The piece in the middle is Grok Bot, a desktop AI assistant that runs on each of my machines and on the phone, with all instances connected. Inside it I keep one bot per project. Each bot is a go-between to one Claude Code session that lives in herdr, a terminal multiplexer for AI agents, inside WSL on one of the PCs. I tell the bot what I decided, the bot passes it to the session, watches the session until it stops, and reports back. It never approves anything for me. Every step that matters waits for my yes, and I can give that yes from the phone.&lt;/p&gt;

&lt;p&gt;That is the whole trick: the sessions stay on the machines that have the repositories, the memory and the tools; the conversation about them travels with me.&lt;/p&gt;

&lt;h2&gt;
  
  
  The switchboard
&lt;/h2&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;What it is&lt;/th&gt;
&lt;th&gt;Where it runs&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Grok Bot&lt;/td&gt;
&lt;td&gt;desktop assistant with several bots, instances linked across devices&lt;/td&gt;
&lt;td&gt;every PC and the phone&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;one bot per project&lt;/td&gt;
&lt;td&gt;relays my decisions, watches a session to the end, reports in my language&lt;/td&gt;
&lt;td&gt;inside Grok Bot&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Claude Code sessions&lt;/td&gt;
&lt;td&gt;the work: code, content, tests, server changes&lt;/td&gt;
&lt;td&gt;herdr in WSL on a PC&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Codex, Grok, Pi&lt;/td&gt;
&lt;td&gt;launched per task by a session from the command line: paintings, reviews, sources&lt;/td&gt;
&lt;td&gt;same PC, exit when done&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;a href="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%2Feotyozxn06yy5jrgskel.png" class="article-body-image-wrapper"&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%2Feotyozxn06yy5jrgskel.png" alt="Diagram: a phone or any PC running Grok Bot with linked instances, one bot per project; each bot relays decisions to Claude Code sessions in herdr on a PC (boss, writer, analyst for the game; a patch session for the PHP app); Codex, Grok and Pi run per task and exit" width="800" height="360"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Why one bot per project and not one bot for everything: the bot carries the project's context, who the sessions are, what is waiting for my decision, what was done last night. Two projects in one bot meant mixed reports. Two bots means I open the game bot when I want to think about the game and the server bot when a vulnerability lands, and each report is about one thing.&lt;/p&gt;

&lt;p&gt;Why Grok in the middle at all: it orchestrates. When I say "check the three sessions and tell me which one is waiting on me", the bot does the rounds and gives me one list. When a session finishes a long run at 2 a.m., the report is waiting in the morning on whichever device I pick up first.&lt;/p&gt;

&lt;p&gt;A day on the phone looks like this. This afternoon I read the test report of the game on the phone, approved the fixes, and typed one line: "think it over and choose yourselves". The main session, on a PC in another room, launched Codex, which painted a new market square and a pier; it fixed the 18 reported bugs, ran the tests and committed, and the bot reported each step to the phone as it happened. This article was being written in a third session at the same time.&lt;/p&gt;

&lt;p&gt;On the day a PHP advisory came out, one session checked and patched three servers while two other sessions did the daily code review and log watch, and the game session painted and tested. All of it through one conversation with the bot; every change on a server confirmed with one tap on the phone. The daily ritual is the same: one sentence from the phone, the bot sends it to the three sessions and brings back one combined summary.&lt;/p&gt;

&lt;p&gt;Two projects run through this right now, and they could not be more different.&lt;/p&gt;

&lt;h2&gt;
  
  
  Example one: Martin Krpan, a point-and-click game in Godot
&lt;/h2&gt;

&lt;p&gt;Martin Krpan is a 2D point-and-click adventure in Godot after the Slovenian folk tale; the first four days are in &lt;a href="https://dev.to/nunc/two-ai-agents-built-a-slovenian-point-and-click-adventure-in-four-days-the-300-pixel-hero-was-the-4je8"&gt;one earlier article&lt;/a&gt; and the finished first act in &lt;a href="https://dev.to/nunc/i-finished-act-i-of-my-first-game-23-days-86-decisions-38-testers-and-a-mare-that-answers-in-mff"&gt;another&lt;/a&gt;. One session is the boss. It owns the Godot project, builds rooms and mechanics, runs the tests, and is the only session allowed to commit. A second session writes content: the script for two acts, the lines and voices, sounds, the animatic. A third studies a classic adventure game and proposes story and puzzle ideas without touching the project. When the boss needs a painting it launches Codex from the command line; for a review it launches Grok; for sources, Pi. Those three used to be permanent sessions as well. The WSL machine had 8 GB of memory and the boss started swapping, so now they come and go per task.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A fresh player before every release.&lt;/strong&gt; Before anything goes to the test build, the boss spawns a subagent with an empty context whose only job is to play the slice along every route and record it. The first run found 18 bugs: lips not moving in some views, a duplicated prop, a route that finished the slice without a required character, buttons too small on a phone. The step is now written into the project's instructions file, so it runs whether I remember it or not.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Measure, then decide.&lt;/strong&gt; I said the market square felt too open. The boss measured instead of arguing: 30 % of the picture was walkable floor, against about 9 % in a room of the classic it studies. Codex drew two sketches, we picked a winding street between stalls at 11 %, and the hero now arrives on the pier from the back, small and walking toward the player.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Guard rails.&lt;/strong&gt; 19 test groups before every commit. Codex may only paint from the hero's existing frames, and it has to report that no pixel outside the repaired patch changed. The animatic and the test build live on my own domain, so I can check them on every device, including the one I give the yes from.&lt;/p&gt;

&lt;h2&gt;
  
  
  Example two: patching a PHP application
&lt;/h2&gt;

&lt;p&gt;The second bot talks to a session opened for one vulnerability. Same rule: the bot runs the session and reports; every change on a server was approved by me, and every larger plan went to Codex for a second read before it ran.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;PHP2Shell (CVE-2026-109618 and CVE-2026-109619).&lt;/strong&gt; The application ran a vulnerable PHP. The session checked exposure and searched the logs for traces of a break-in; it found none. It upgraded PHP to 8.2.35 through PHP-FPM on production and the test machines, with an hourly watch for the window in between. After the fix it removed the watch, restored the security settings it had touched, turned off the PHP feature the exploit relies on and removed the old PHP. The nightly batch ran clean the next morning, and the application's AI assistant and file downloads showed no regression.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A follow-up advisory (CVE-2026-115560).&lt;/strong&gt; When a new, unpatched issue was reported, the session read the preconditions, found that the application does not meet them and that the relevant setting was already off, and wrote down why no action was needed. That note is worth as much to me as a fix.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A hardening pass.&lt;/strong&gt; My brief was one sentence: the web is getting more dangerous because of AI, look around. The session did a read-only review first, then reported two findings: an old internal service exposed to the internet, and over-permissive file permissions on TLS keys. Both were locked down the same evening after my yes, with the old settings backed up, a config test before every reload, the rollback commands written down first, and no downtime.&lt;/p&gt;

&lt;h2&gt;
  
  
  What carries over
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;The human decides, from any device; the AI proposes with a number attached. "Too open" became 30 % versus 9 %.&lt;/li&gt;
&lt;li&gt;One bot per project, one session per role, and only one session commits.&lt;/li&gt;
&lt;li&gt;Read-only first: a review before any change on the servers, a fresh player before any release of the game.&lt;/li&gt;
&lt;li&gt;A second model reads the plan before it runs.&lt;/li&gt;
&lt;li&gt;Every change has a backup and a way back.&lt;/li&gt;
&lt;li&gt;Memory is a real limit. Subagents that exit beat permanent sessions on an 8 GB machine.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Wrapping up
&lt;/h2&gt;

&lt;p&gt;The bot never became the decision maker, and that is the design. It carries my yes to the right session on the right machine and the session's evidence back to whichever screen I am holding: a percentage of walkable floor, or a clean nightly run the morning after a patch. Both are easier to say yes to than "trust me", and both are easier to say no to.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>productivity</category>
      <category>devops</category>
      <category>gamedev</category>
    </item>
    <item>
      <title>I Finished Act I of My First Game. 23 Days, 86 Decisions, 38 Testers and a Mare That Answers in Horse.</title>
      <dc:creator>Nunc</dc:creator>
      <pubDate>Wed, 14 Oct 2026 15:06:32 +0000</pubDate>
      <link>https://dev.to/nunc/i-finished-act-i-of-my-first-game-23-days-86-decisions-38-testers-and-a-mare-that-answers-in-mff</link>
      <guid>https://dev.to/nunc/i-finished-act-i-of-my-first-game-23-days-86-decisions-38-testers-and-a-mare-that-answers-in-mff</guid>
      <description>&lt;p&gt;I grew up on 1990s point-and-click adventures and never got over them, so last month I started making my own, with AI agents doing the work. &lt;a href="https://hellonunc.com/blog/posts/krpan-adventure-game-four-days/" rel="noopener noreferrer"&gt;The first article&lt;/a&gt; covered four days: a game bible, a prototype made of grey boxes, and a 300-pixel hero whose arms kept switching sides. This one covers the next nineteen. On 14 October I wrote decision 86 into the project log: Act I is finished. It is the first act of the first game I have ever made, and about 38 people have played it, most of them strangers.&lt;/p&gt;

&lt;p&gt;The game is Tihotapska pot (The Smuggler's Road). The hero is Martin, a salt smuggler from the Bloke plateau in 1844 with a mare, a lot of strength and plenty to say about both. Act I is one village: his leaking pot, a neighbour's pot he has to return, an inn with two carters who do not want to pay, a vagrant eating the innkeeper's bacon in her cellar, a torn pack strap, a smith with an anvil to move, a bundle of hay, and a mare to lead out of the stable before the road to Trieste. About 20 minutes of play.&lt;/p&gt;

&lt;p&gt;&lt;a href="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%2F5nwmgueybepyzt0jugx1.jpg" class="article-body-image-wrapper"&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%2F5nwmgueybepyzt0jugx1.jpg" alt="Title screen: a painted Slovenian village at dusk, THE SMUGGLER'S ROAD, Tihotapska pot, Martin in angleška sol, buttons Nova igra, Nadaljuj, Naloži, Nastavitve" width="800" height="413"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Act I on 14 October&lt;/th&gt;
&lt;th&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Engine&lt;/td&gt;
&lt;td&gt;Godot 4.7.2, GDScript, single-threaded web build&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Code&lt;/td&gt;
&lt;td&gt;5,299 lines of GDScript, 1,820 lines of Python tools, 155 GUT tests (Godot's test framework)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Content&lt;/td&gt;
&lt;td&gt;5 rooms, 124 dialogues, 8 items, 772 lines per language&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Voices&lt;/td&gt;
&lt;td&gt;573 Slovenian and 573 English recordings&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Game pack&lt;/td&gt;
&lt;td&gt;63 MB for a Slovenian player, plus a 27.5 MB English voice pack; the engine adds 8 MB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Play time&lt;/td&gt;
&lt;td&gt;17 to 21 minutes for the testers who finished in the second week&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Testers&lt;/td&gt;
&lt;td&gt;67 logged sessions in 38 tester folders&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;History&lt;/td&gt;
&lt;td&gt;252 commits, 86 dated owner decisions, 23 days&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Who did 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;Model or tool&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Code, content, plan, orchestration&lt;/td&gt;
&lt;td&gt;Claude Code, Opus 5.5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Every player-facing line, Slovenian and English&lt;/td&gt;
&lt;td&gt;Claude Fable 5.1 as a subagent&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Paintings, sprites, animation frames, independent reviews&lt;/td&gt;
&lt;td&gt;Codex, GPT-6 Astra, from 14 October GPT-6.1-Sol&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Voices&lt;/td&gt;
&lt;td&gt;Gemini 3.8 Flash TTS in batch mode, transcript check with Gemini 3 Flash&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Music&lt;/td&gt;
&lt;td&gt;Lyria, 60-second loops per place&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Second image model, third opinion&lt;/td&gt;
&lt;td&gt;Grok 4.7, from 13 October&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Browser play-throughs&lt;/td&gt;
&lt;td&gt;Playwright, Chromium and Firefox, phone viewports&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Design, taste, scope, playtesting&lt;/td&gt;
&lt;td&gt;me, 551 messages&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;My side of it is measurable: 551 messages in the main session, 8.6 million output tokens on the Claude side, 87 Codex sessions, and a decisions file where every entry has a date and my own words in quotes. The agents propose; the decisions file is mine. I play, I complain, they log the complaint as a numbered decision and implement it.&lt;/p&gt;

&lt;p&gt;The model that writes the code never writes the lines: Fable gets the script as a separate job and reviews it with fresh eyes, which is how the "dry" diagnosis below came out sharper than mine. Codex is a different vendor, so its reviews of Claude's work are not Claude grading itself. Its image tool repaints a patch of a painting and the patch is composited back with every pixel outside it unchanged, which is what talk frames, idle frames and door fixes need. Gemini TTS has a Slovenian voice and is cheaper than the alternative; ElevenLabs gets a blind trial when the game is done. Grok came last as a third opinion and a second image model, and so far it has lost the one contest it entered.&lt;/p&gt;

&lt;h2&gt;
  
  
  How anything gets judged
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Code.&lt;/strong&gt; A step is done when the build gate is green: engine import, a content validator with its own regression tests, a voice check, 155 GUT tests including whole-act play-throughs, the web export and the English voice pack. About 12 minutes, run before every commit. The validator refuses content that would trap a player: a choice list without a way out, an action a hotspot can never show. On top of the gate, a Claude agent plays the act blind through a browser bridge (its first run took about 115 actions and found a hidden way out), and Codex plays both routes end to end in Playwright and writes a ranked list of what it saw.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Images.&lt;/strong&gt; Nothing is judged on a downscaled screenshot, after the first article's arms-on-the-wrong-side episode. Every art change is checked on full-resolution crops around the figure, then a separate Codex run that did not paint anything reviews the result in the running game, with saved crops as evidence, before anyone shows me. When taste is involved I get a side-by-side page and choose: the higher-camera inn, the mare's walk (version 3 won, version 2 "looked worse"), Martin's size in the foreground ("leave it as it is"). Even inside a painting task Codex grades its own output: it flagged its four-beat mare gait as unacceptable after three tries, and version 3 replaced it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Text.&lt;/strong&gt; Fable writes, Codex reviews, Fable revises; then I read every spoken line of the act on one page, Slovenian and English side by side with the voices, and leave notes. No line gets a voice until I have played it and said it is right, because voices cost money and retakes cost more.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Audio.&lt;/strong&gt; Every take is transcribed and compared with its line, up to three takes, and a 10-second script checks every file's encoding, speech rate, level, pauses and clipping. Beyond that, my ear is the judge; the five episodes below show why.&lt;/p&gt;

&lt;h2&gt;
  
  
  Playing my own game was the useful part
&lt;/h2&gt;

&lt;p&gt;On 26 September, my fourth play-through, I wrote one line in the log: "dry, too little soul." Fable's independent diagnosis of the script was more precise than mine. Of 28 lines the villagers spoke in my play-through, 22 were an order or the next step to do: bring, tidy, go, now the strap. Every character talked like Martin, two sentences and an aphorism of the shape "X, not Y" at the end of each one. Nobody had a history with him. And the load-planning quiz made a man with twenty years on the road propose carrying the neighbour's pot to Trieste, because that was one of the menu options.&lt;/p&gt;

&lt;p&gt;Fable changed 67 lines that day. Three days later I asked for Martin himself to come from the 1858 story the game is based on, not from a character sheet. Fable read the full text and built a profile with a verbatim quotation for every trait: talkative, not terse; short only when a matter is settled; cheerful hyperbole before an effort; appetite as the unit of measure. 32 more lines went.&lt;/p&gt;

&lt;p&gt;&lt;a href="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%2F0vtz4d6t59hmxd40vx47.jpg" class="article-body-image-wrapper"&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%2F0vtz4d6t59hmxd40vx47.jpg" alt="Martin's house: bed, a painted chest, the holy corner, pack gear on the wall, a table with two pots, Martin by the hearth" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The play-throughs found things the tests did not. I left a conversation with the smith halfway and then could not answer the innkeeper's "how will you pay": an invisible gate, removed. I clicked a woman who was walking across the yard and she walked all the way back to her spot before she would talk: "why must they go back to their place?" Now people stop where they are. On 2 October I played the whole act and threw out the opening puzzle. Martin no longer packs a household load; he takes a purse, the innkeeper's food and the neighbour's hay, under which the salt will ride home, and he leaves three debts behind him. That rework also gave the smith his own smithy and the yard another 1,100 pixels to the left.&lt;/p&gt;

&lt;p&gt;&lt;a href="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%2Fahsraa6y1hdid6l1om8f.jpg" class="article-body-image-wrapper"&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%2Fahsraa6y1hdid6l1om8f.jpg" alt="The smithy: forge, bellows, tools on the wall, Matthias at the bench, an anvil on a stump, Martin at the door" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Monkey Island as a textbook
&lt;/h2&gt;

&lt;p&gt;I am a Monkey Island fan, so a separate Claude session spent 1 to 9 October watching a four-hour no-commentary walkthrough of the Special Edition and taking notes, then compared the notes with our repository. Nothing of the original's art, lines or music went into the game. The numbers did.&lt;/p&gt;

&lt;p&gt;In the eight Monkey Island conversations it measured, seven menus had four options and every menu had a way out, follow-up chains went two to four deep, and every line of the hero was chosen by the player. In ours, 11 of 94 dialogues had a choice at all, the deepest went two levels, and the player heard 3.7 spoken lines per decision against one or two. The whole video had 234 cuts in 82 distinct shots, with camera height and the hero's size changing by room. Our five rooms were all a frontal stage at eye level with Martin at 300 pixels in every one.&lt;/p&gt;

&lt;p&gt;So the talks got the Monkey Island shape: the player picks Martin's lines, used choices dim, a person can end the conversation, and every longer talk has one set of answers that are all jokes. The inn was repainted with a higher camera, more tables and guests cut by the frame, like a tavern in a classic adventure.&lt;/p&gt;

&lt;p&gt;&lt;a href="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%2F1cwkytdfsug8dcx6z05b.jpg" class="article-body-image-wrapper"&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%2F1cwkytdfsug8dcx6z05b.jpg" alt="The inn before and after: left the September inn, a flat stage at eye level; right the October inn with a higher camera, more tables, guests at the back and the hostess by the hearth" width="800" height="225"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Two more things came from that review. Decision 11 had said "no hint system". Decision 62 amended it: a click on Martin gives his own line about what still waits for him, alternating with jokes. And "angleška sol", English salt, the smugglers' code word for whatever is in the sacks, turned out to appear in Act I only in the subtitle. Now the carters ask what it is. "Salt." "What kind?" "English." It is never explained, and it never will be.&lt;/p&gt;

&lt;p&gt;&lt;a href="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%2F36noqxfpvxraygvride0.jpg" class="article-body-image-wrapper"&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%2F36noqxfpvxraygvride0.jpg" alt="Cellar talk with the vagrant sitting on a barrel under the bacon: six options, the last one a way out" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The voices, a serial in five episodes
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Episode one.&lt;/strong&gt; The first article shipped with Piper, one Slovenian voice for everybody. On 25 September a blind test of ten lines put Gemini TTS against it and Gemini won every line. All lines were regenerated with one voice and style per character and an automatic check: Gemini transcribes each take, the transcript is compared with the line, up to three takes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Episode two.&lt;/strong&gt; The English version arrived on 26 September, and the English voice pronounced "Cerknica" three different ways in three takes. Respelling the names (Nezha, Mateeya), a pronunciation guide, and a light Slovenian accent all failed a blind test. So the English version got English names: Agnes, Matthias, Gertie.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Episode three.&lt;/strong&gt; On 9 October I cut the download. The eleven big paintings went to lossy WebP (103 to 81 MB) and all voices were re-encoded from Vorbis quality 4 to quality 1 (81 to 63 MB). I listened to a comparison page and heard no difference. Three days later I played the game and the sound was poor. The measurement, on one line whose text had not changed: with the q4 file as the reference, the re-encoded q1 file's error was only 12 dB below the signal, about a quarter of its amplitude, most of it above 8 kHz. Two lossy passes did that. 916 voices came back from git, 115 made since were voiced again, and a check in the build gate now refuses any voice below q4.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Episode four.&lt;/strong&gt; A take that said "Matija, Matija" where the line had one Matija still scored 0.95 on the transcript check. A doubled-word rule, a recheck of every voice, 43 Slovenian and 3 English takes remade, one English hint had said a phrase three times. Wrong stress is fixed with spellings for the voice only: kokóši, barantájo.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Episode five.&lt;/strong&gt; I asked whether Gemini could listen for quality instead of me. Blind, it found 1 or 2 of 10 q1 takes it was shown. Four takes voiced with a deliberate English accent, which I heard as odd and slow, it rated 5 out of 5 and called native Slovenian. As a listener, Gemini checked words, not sound. A 9 October estimate for one pass of Act I in both languages: about $0.66 on Gemini, about $5 on ElevenLabs.&lt;/p&gt;

&lt;p&gt;&lt;a href="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%2F84ze58ki3ovmqbn8jgqt.jpg" class="article-body-image-wrapper"&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%2F84ze58ki3ovmqbn8jgqt.jpg" alt="Martin talking to his mare in the yard, three answers to pick at the bottom right, hens around his boots, the hay bundle by the bench" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The mare, by the way, is a conversation partner since decision 59. Martin picks what to say; she answers in horse. A snort, a blow or a whinny, written out as a subtitle.&lt;/p&gt;

&lt;h2&gt;
  
  
  Strangers
&lt;/h2&gt;

&lt;p&gt;On 7 October I removed the password from the test page and posted the link in a Slovenian developer Discord. The game logs every click, line and refusal with a timestamp and sends it to my server, after a notice and an optional nickname; the switch to turn it off is in the settings. A script turns each log into a summary: device, language, how far the player got, what was refused, where it stopped. First night: 13 sessions from 9 testers, one finished (12 minutes 31 seconds, on his second try, in Slovenian). Everyone else stopped in the house, the yard or the inn.&lt;/p&gt;

&lt;p&gt;The logs said what the comments did not. Every browser that reported a language reported English, so every session started in English, and the two who switched to Slovenian found the dropdown late. The first comment from a Slovenian developer was "why is Slovenian not the default?" The language had flipped before: Slovenian only on day one, English first on 28 September because I wanted a wider audience. On 8 October it flipped back for good, decision 77: Slovenian first, English kept for Act I, later acts translated in one go when the whole game is done.&lt;/p&gt;

&lt;p&gt;The second wave, 9 to 12 October, brought 20 sessions from about 18 players. Nine were on phones held sideways, most in a browser window about 300 pixels tall, where the game's 30-pixel text is about 9 pixels. None of them got further than the yard and the smithy. Six of the fourteen who left the house left without the neighbour's pot. Testers clicked doors two or three times while Martin was still walking. Two finished, in about 17 and 21 minutes.&lt;/p&gt;

&lt;p&gt;Each of those became a task: bigger text on small screens, Martin stops once at his door and mentions the pot, a double click on an exit goes through at once, and a door ignores clicks for half a second after a room change. The staged hints came from a second, deeper Monkey Island analysis, which had counted 242 hint records in the Special Edition: a mild hint first, the clear one on the next click, the current room's hint before any other, and a test that walks two orders of the solution to prove every state has one.&lt;/p&gt;

&lt;p&gt;&lt;a href="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%2Fz3tqrqdhogqguzlpdxd2.jpg" class="article-body-image-wrapper"&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%2Fz3tqrqdhogqguzlpdxd2.jpg" alt="The yard painting at full width: the inn, Martin's house, the smithy and the stable in one 4,300-pixel strip that the camera scrolls across" width="800" height="201"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What did not work
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;AI video.&lt;/strong&gt; I wanted the hearths to flicker and the cutscenes to move. Four routes were tried side by side on 6 and 7 October: code-only shaders, Codex-painted flame frames, a depth-map camera move, and video generation from our paintings through Veo and Kling. The depth-map move tore the rigging and sails of the Trieste boat into stippled streaks. The video models moved things, but Martin drifted and the style drifted with him. Decision 74: free methods only. Flames are three or four frames painted by Codex and cross-faded, with a breathing glow, embers, kettle steam and snow through the doors.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Walk cycles, with two image models.&lt;/strong&gt; Codex failed at a frame-consistent side walk in September. On 13 October Grok got three attempts at the same task: the look stayed consistent, the legs never alternated. Codex reviewed Grok's frames and said keep the current ones. The mare got her walk another way: Codex painted the legs after Muybridge's photographs of a draft horse, eight frames driven by the ground covered, and that one I approved from a side-by-side.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fl935aul8oobh9fs628oo.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fl935aul8oobh9fs628oo.gif" alt="Five drawn frames of the cellar vagrant throwing a sausage at Martin, the kind of flipbook that replaces a video clip" width="360" height="300"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tests that waited for the real thing.&lt;/strong&gt; The test step of the build gate had grown to 825 seconds, almost 14 minutes, because the whole-act tests waited for every voice to play and every fade to end. Codex's pre-Act II review found it. Voices silenced in tests and the departure sped up: 551 seconds. The same review moved the English voices into a separate pack fetched only when English is chosen, so a Slovenian player's download went from 90 to 63 MB.&lt;/p&gt;

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

&lt;p&gt;Act I is closed and playable in the browser at &lt;a href="https://smugglersroad.com/test/" rel="noopener noreferrer"&gt;smugglersroad.com/test&lt;/a&gt;, Slovenian or English, about 20 minutes; it logs what you do and sends it to me, and says so on the first screen. The next milestone is Act II in Trieste, written and voiced in Slovenian only until the whole game is done, then translated in one go. Of the 86 decisions in the log, more than 20 amend or supersede an earlier one.&lt;/p&gt;

&lt;p&gt;&lt;a href="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%2Fvdce6fk5ht8o21p2bp36.jpg" class="article-body-image-wrapper"&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%2Fvdce6fk5ht8o21p2bp36.jpg" alt="Act I title card: I. DEJANJE, Treba bo po sol, Vrh pri Sveti Trojici, Bloke, over a painted muddy road out of the village at dusk" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrapping up
&lt;/h2&gt;

&lt;p&gt;"No difference" in a listening test was a re-encoding error a quarter the size of the signal. "English first" lasted ten days against nine Slovenian testers. "No hint system" lasted nine days, until a Monkey Island review and then six of fourteen players leaving without the pot. The agents wrote 5,000 lines of code and 1,146 voice files, and they complained too: Fable diagnosed the dry script better than I had, Codex rejected its own walk cycle. What stayed mine was the ranking. Which complaint counts, which number is good enough, which of two inns goes into the game: 86 times in 23 days, and more than 20 of those overturned something I had decided before.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>gamedev</category>
      <category>godot</category>
      <category>showdev</category>
    </item>
    <item>
      <title>Two AI Agents Built a Slovenian Point-and-Click Adventure in Four Days. The 300-Pixel Hero Was the Hard Part.</title>
      <dc:creator>Nunc</dc:creator>
      <pubDate>Fri, 25 Sep 2026 13:05:02 +0000</pubDate>
      <link>https://dev.to/nunc/two-ai-agents-built-a-slovenian-point-and-click-adventure-in-four-days-the-300-pixel-hero-was-the-4je8</link>
      <guid>https://dev.to/nunc/two-ai-agents-built-a-slovenian-point-and-click-adventure-in-four-days-the-300-pixel-hero-was-the-4je8</guid>
      <description>&lt;p&gt;I grew up on 1990s point-and-click adventures and never stopped missing them, so I am making my own, with AI agents doing the work. The game is called Tihotapska pot (The Smuggler's Road). The hero is Martin, a salt smuggler from the Bloke plateau with a mare, a lot of strength and little to say, after Slovenian folk and literary stories. I gave Claude Code and Codex a 1,472-line game bible; four days later there is a painted, voiced demo of Act I running in the browser: 108 commits, 92 tests, 111 spoken lines, Godot 4.7.2. It is only a demo so far: one act out of five, with rough edges listed below, and the rest of the story waits until this part is right. The part that nearly failed had nothing to do with code. It was a 300-pixel-tall hero whose arms kept ending up on the wrong side of his body.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Who&lt;/th&gt;
&lt;th&gt;Model&lt;/th&gt;
&lt;th&gt;Did&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Claude Code&lt;/td&gt;
&lt;td&gt;Opus 5.5, Fable 5.1 from day four&lt;/td&gt;
&lt;td&gt;design documents, all game code from day two, orchestration, development plan&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Codex&lt;/td&gt;
&lt;td&gt;GPT-6 Astra, reasoning medium, via &lt;code&gt;codex exec&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Milestone 1 code, then all images (built-in image generator) and independent reviews&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Piper TTS&lt;/td&gt;
&lt;td&gt;&lt;code&gt;sl_SI-artur&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;placeholder Slovenian voices&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Playwright&lt;/td&gt;
&lt;td&gt;Chromium, Firefox&lt;/td&gt;
&lt;td&gt;scripted play-throughs at 1920×1080&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Day one: seven documents and a screen full of cubes
&lt;/h2&gt;

&lt;p&gt;The bible has a rule for AI agents on page one: do not redesign systems, story or architecture on your own. Explain the problem, propose, get approval, then implement. Between 14:12 and 17:07 on 22 September the agents wrote seven design documents (world, characters, story, locations, puzzle graph, art bible, technical design) and I answered 25 decisions, each logged as one dated line: "1840s, before the railway reaches Carniola (1849)", "no dead ends", "JSON content files, no dialogue addon".&lt;/p&gt;

&lt;p&gt;Then Milestone 1, fourteen tasks from the technical design. Claude Code wrote each prompt, I pasted it into the Codex terminal, Codex committed, Claude checked the commit. At 21:55 I removed myself from that loop. Claude launched &lt;code&gt;codex exec&lt;/code&gt; in the background, checked tests, screenshot and diff, and sent the next prompt or a correction (two attempts per task, then stop and ask me). Task 14 landed at 00:14: two rooms, walking, hotspots, dialogue, inventory, a scripted scene, save and reload, 73 tests.&lt;/p&gt;

&lt;p&gt;&lt;a href="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%2Figsv6s7hgwkmqn71fuy7.png" class="article-body-image-wrapper"&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%2Figsv6s7hgwkmqn71fuy7.png" alt="The Milestone 1 prototype: grey boxes labelled Matija, Jera, Krma za doma and a dark rectangle for Martin on a green-grey background" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Every system worked. Everything was a labelled box. I asked that evening whether real graphics would come later.&lt;/p&gt;

&lt;h2&gt;
  
  
  Day two: the inn was one and a half times too big
&lt;/h2&gt;

&lt;p&gt;Next morning I flipped the roles. Claude Code implements directly and keeps the development plan; Codex only paints (it has a built-in image generator) and gives independent reviews. Each Codex coding task had cost a prompt, a wait and a verification pass anyway.&lt;/p&gt;

&lt;p&gt;Codex painted three style directions; I picked "warm comic realism". Then it painted the inn, and the two seated carters were one and a half times the size of standing Martin, who is supposed to be the biggest man in any room.&lt;/p&gt;

&lt;p&gt;&lt;a href="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%2F6x5z6nb9n9e06buezfdn.jpg" class="article-body-image-wrapper"&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%2F6x5z6nb9n9e06buezfdn.jpg" alt="First inn study: two seated carters at the table are visibly larger than Martin standing next to them" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The second attempt got the figures right and the room wrong: ceiling 3.3 m, door 2.4 m, counter 1.2 m. The cause was my own art rule, which sized doors at about 390 px. Decision 28 replaced it with real-world units. Martin is 1.95 m and 300 px, so the scale is 154 px per metre. Rural 1840s ceilings are 2.2 to 2.4 m, doors about 1.8 m (Martin stoops), tables 0.75 m. Every room study now reports measured pixels and metres for the figures and for the architecture. The third inn came in within 10 % on all eight landmarks.&lt;/p&gt;

&lt;p&gt;&lt;a href="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%2Fsnsjnnid0tru17kt16na.jpg" class="article-body-image-wrapper"&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%2Fsnsjnnid0tru17kt16na.jpg" alt="Inn study v3 with measurement rulers: Martin 300 px / 1.95 m, Neža 242 px, seated carter 178 px, table 116 px, door 264 px, ceiling 361 px, all against the 154 px per metre rule" width="" height=""&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The rig that fell apart
&lt;/h2&gt;

&lt;p&gt;The plan was a cutout rig for Martin: head, torso, arms and legs as separate drawings rotating around joints, one set of parts for every animation. Codex generated the parts, Claude assembled the rig and reported that the front and side views worked. I opened the game at full size. The front view had shoulder caps sticking out of the coat. The side view had the head sitting next to the neck instead of on it. After one fix round the left arm was still drawn on the right and the right on the left.&lt;/p&gt;

&lt;p&gt;Claude had reviewed the character on downscaled screenshots. The rule since then: every art change is checked on 1920×1080 crops around the figure, and Codex does an independent visual review with saved crops before anyone reports to me.&lt;/p&gt;

&lt;p&gt;&lt;a href="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%2Fq8j3b0zc4t44gulziiko.jpg" class="article-body-image-wrapper"&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%2Fq8j3b0zc4t44gulziiko.jpg" alt="Review crops of the cutout rig: side view with the head displaced from the collar, front view with raised shoulder caps, back view, side walk" width="800" height="297"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The question that ended the rig was mine: how does Guybrush Threepwood in Monkey Island 1 not have these problems? Claude's answer was short. Guybrush is not assembled from parts. Every frame is a whole drawing, about six per walking direction, and he is roughly 40 pixels tall, so a seam has nowhere to show. Our Martin is 300 pixels tall and every seam shows. Decision 29: Martin is a flipbook. Whole drawings, side view mirrored for left, separate front and back sets. Codex generates each animation as one sheet from the approved reference so the face and coat do not drift between frames, then reviews the sheet in the running game.&lt;/p&gt;

&lt;p&gt;That review still listed a cut-off toe on &lt;code&gt;side_walk_4&lt;/code&gt;, a planted foot gliding 35 pixels during a held pose and a talk gesture looping every 0.5 s. Those were fixed. Several attempts at a smoother side walk, whole-sheet regeneration and local edits, all failed, and I accepted that as polish for later.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sound, voices and a file that swallowed them
&lt;/h2&gt;

&lt;p&gt;Sound comes only from CC0 libraries (Kenney, OpenGameArt), every file listed with its source. One CC0 file tagged "pub" turned out to be a song; the inn ambience is now a fire loop plus synthesized murmur. The game is Slovenian first: every string lives in one localization file, and the English text and voices come after the demo is done. Day one said text only, no voice; day three reversed it: spoken dialogue, Slovenian first, English later. The placeholder is Piper TTS with one Slovenian voice (&lt;code&gt;sl_SI-artur&lt;/code&gt;, CC BY 4.0, credited in the intro); characters differ only by pitch and pace. The actor script has 111 lines, 108 of them voiced today.&lt;/p&gt;

&lt;p&gt;Then I tested the build and nobody spoke. Claude had committed a stray &lt;code&gt;.gdignore&lt;/code&gt; into the voice folder, so the Web export dropped every recording. The 90 tests stayed green because they run without an export. The fix was deleting one file, plus a line in &lt;code&gt;check.sh&lt;/code&gt; that fails the build if the voices are not packed.&lt;/p&gt;

&lt;p&gt;The intro exists because I am a Monkey Island fan and said so: a 26-second pan over the Bloke plateau at dusk with credits, then a title card. Lines float above the speaker's head in the speaker's colour and advance by the length of the voice clip.&lt;/p&gt;

&lt;p&gt;&lt;a href="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%2Fedojdmi8ciol2952l1zk.jpg" class="article-body-image-wrapper"&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%2Fedojdmi8ciol2952l1zk.jpg" alt="The inn scuffle in the finished art: two carters shove Martin, who does not move; the line " width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

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

&lt;p&gt;Milestone 2 was signed off on 24 September after Codex played both puzzle routes end to end in Playwright: no blockers, nine of its ten findings fixed (one was the raw key &lt;code&gt;scene.carters_approach.caption&lt;/code&gt; printed on screen during the main gag), playtime 7 to 9.5 minutes against a 10 to 20 minute target. Milestone 3 turned the two rooms into the whole first act: Martin's home as the opening room, the smith's diagnosis of the leaking pot, and a painted travel map on which the marker follows 82 route points traced from the painted road, Vrh to Cerknica to Postojna to Trieste. My own blind play-through is next, and nothing else moves until it is done. Known open items: the side walk and bench reach, the carters' provisional walk cycle, the TTS voices, temporary music, no Safari or real-phone test yet.&lt;/p&gt;

&lt;p&gt;&lt;a href="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%2Fpwzw1ny0qkwmovwmlodv.jpg" class="article-body-image-wrapper"&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%2Fpwzw1ny0qkwmovwmlodv.jpg" alt="Travel map: a painted parchment map with a dashed route from Vrh past Cerknica lake to Postojna, a small Martin-and-mare marker on the road, caption " width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Four days cost 137 messages from me, 1.42 million Claude output tokens and 26 Codex sessions. They produced 2,041 lines of GDScript, 1,687 lines of tests, 44 dialogue files, 210 Slovenian strings and a forward design for the rest of the game with 39 rooms, 100 flags and 17 items.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrapping up
&lt;/h2&gt;

&lt;p&gt;The agents never failed at code. They failed at seeing. A room 1.5× too large, arms on the wrong side, a head beside its neck: all of it passed their own reports and my downscaled screenshots. Every fix that held was a measurement rule, not a better prompt: 154 pixels per metre, full-resolution crops, and a second agent that has to save its evidence before it says "pass".&lt;/p&gt;

</description>
      <category>ai</category>
      <category>gamedev</category>
      <category>godot</category>
      <category>showdev</category>
    </item>
    <item>
      <title>My LLM Trading Bots Beat the Benchmark. The Rules I Wrote Beforehand Say It Doesn't Count.</title>
      <dc:creator>Nunc</dc:creator>
      <pubDate>Fri, 11 Sep 2026 13:29:22 +0000</pubDate>
      <link>https://dev.to/nunc/my-llm-trading-bots-beat-the-benchmark-the-rules-i-wrote-beforehand-say-it-doesnt-count-3455</link>
      <guid>https://dev.to/nunc/my-llm-trading-bots-beat-the-benchmark-the-rules-i-wrote-beforehand-say-it-doesnt-count-3455</guid>
      <description>&lt;p&gt;For two months I let LLM agents paper-trade a simulated $100 crypto portfolio. Season 1 ended with a verdict of STOP: both agents lost to cash. Season 2 ended with all four agents beating the passive benchmark by 3 to 10 percentage points. The formal verdict for Season 2 is still "inconclusive", and I think that is the most useful result of the whole project.&lt;/p&gt;

&lt;p&gt;No real money ever moved. Live execution was never implemented and the code fails closed if you try to turn it on. Everything below is CoinGecko-reference paper returns on Base L2 tokens, priced with a static cost model (0.05 to 1.00 % fee tier plus a 0.15 to 0.50 % execution haircut plus $0.03 gas per swap).&lt;/p&gt;

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

&lt;p&gt;The agents were plain LLM calls through the vendors' CLIs (&lt;code&gt;claude -p&lt;/code&gt;, &lt;code&gt;codex exec&lt;/code&gt;, subscription auth, no API keys), launched with a minimal environment so no secret from &lt;code&gt;.env&lt;/code&gt; could reach the model. The model proposes a trade as JSON; a policy engine in Python decides. The hard limits lived in code, not in the prompt: trade size $5 to $20, max 3 trades a day, meme tokens capped at 15 % of the portfolio, a global stop-loss that liquidates to USDC at 70 % of starting value. Every decision, including rejected ones, went to an append-only journal.&lt;/p&gt;

&lt;h2&gt;
  
  
  Season 1: the green number that wasn't
&lt;/h2&gt;

&lt;p&gt;Two agents, same code, different model (Claude Opus vs GPT via Codex), one decision every 4 hours, 30 days. Whole-run result: Opus finished at $101.48, GPT at $96.81. Opus made 5 trades and paid $0.88 in modeled fees; GPT made 23 trades and paid $3.51, which was most of its gap.&lt;/p&gt;

&lt;p&gt;Then I looked at the window I had committed to on day 17 (an interim freeze of the decision rule, not a true pre-registration, and I am labelling it that way on purpose). Inside that window, Opus returned −1.85 % and GPT −3.00 %. Both beat the random-trader path. Both lost to cash and to a passive 50/50 WETH/USDC portfolio. The rule said: STOP.&lt;/p&gt;

&lt;p&gt;Without the frozen rule I would have written "Opus +1.48 %, let's go live". The whole-run number was ETH beta from early July, not judgement.&lt;/p&gt;

&lt;h2&gt;
  
  
  Season 2: fix every confound, then pre-register for real
&lt;/h2&gt;

&lt;p&gt;Season 1 had too many moving parts to compare anything: agents read separate price snapshots, the model was swapped mid-run, the decision architecture changed a week in, and the random baseline was one path. Season 2 was a rebuild with one question: &lt;strong&gt;how much of the decision should the model own, and how much should code own?&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Season 1 problem&lt;/th&gt;
&lt;th&gt;Season 2 fix&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;separate price snapshots per agent&lt;/td&gt;
&lt;td&gt;one daily snapshot shared by all arms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;model, universe and prompt changed mid-run&lt;/td&gt;
&lt;td&gt;everything frozen before tick 1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;autonomous vs structured never compared&lt;/td&gt;
&lt;td&gt;it became the experimental axis&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4-hour cadence, churn&lt;/td&gt;
&lt;td&gt;1 decision per day&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;decision rule frozen on day 17&lt;/td&gt;
&lt;td&gt;pre-registration written before the first tick&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Four arms: {Claude Opus 5, GPT (Codex)} × {A1 autonomous: raw data and its own journal, decides everything; A2 structured: code-ranked candidates, a cost table, a forced daily forecast, and a code-side edge gate}. All four started at 50/50 WETH/USDC with $100. The pre-registration committed the decision rules: the architecture claim needs the same sign in both models and in &lt;strong&gt;two&lt;/strong&gt; 30-day windows, with a gap of at least 0.5 pp that survives a cost-sensitivity sweep at 0.5×, 1× and 2× variable costs. "Edge" needs at least +2 pp net over passive 50/50 in both windows. A win in one window is a case study.&lt;/p&gt;

&lt;h2&gt;
  
  
  Season 2, window 1: everything green
&lt;/h2&gt;

&lt;p&gt;Net of modeled costs, day 30:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Arm&lt;/th&gt;
&lt;th&gt;Terminal value&lt;/th&gt;
&lt;th&gt;vs passive 50/50&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Opus structured&lt;/td&gt;
&lt;td&gt;$124.97&lt;/td&gt;
&lt;td&gt;+9.83 pp&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GPT structured&lt;/td&gt;
&lt;td&gt;$124.01&lt;/td&gt;
&lt;td&gt;+8.87 pp&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Opus autonomous&lt;/td&gt;
&lt;td&gt;$121.72&lt;/td&gt;
&lt;td&gt;+6.58 pp&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GPT autonomous&lt;/td&gt;
&lt;td&gt;$118.52&lt;/td&gt;
&lt;td&gt;+3.38 pp&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;passive 50/50 (benchmark)&lt;/td&gt;
&lt;td&gt;$115.14&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;trend (7-day average)&lt;/td&gt;
&lt;td&gt;$121.41&lt;/td&gt;
&lt;td&gt;+6.27 pp&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;HODL ETH&lt;/td&gt;
&lt;td&gt;$130.25&lt;/td&gt;
&lt;td&gt;+15.11 pp&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;cash&lt;/td&gt;
&lt;td&gt;$100.03&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Three things fell out of that table.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Structured beat autonomous in both models.&lt;/strong&gt; Opus +3.24 pp, GPT +5.49 pp. The Opus gap survived the cost sweep (+3.43 / +3.24 / +2.87 pp). The GPT gap did not: at 2× costs one of GPT structured's recorded trades becomes unfundable in the replay, so by the frozen rule that gap is cost-fragile. Trade counts: 5, 9, 5 and 13. The structured arms' edge gate rejected 2 proposed swaps each.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Nobody beat ETH.&lt;/strong&gt; ETH rallied about 30 % in the window and every arm held some USDC the whole time. The green column is partial beta plus a bull month. It is not skill, and the pre-registration said in advance that "absolute green driven by market beta" is explicitly not success.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No forecasting skill at all.&lt;/strong&gt; Every arm answered one question daily: the probability that WETH outperforms USDC by the next daily snapshot (24 hours for 92 of the 100 scored forecasts, about two days for the 8 that followed a skipped day). Brier scores came out at 0.256 to 0.260 against a base-rate Brier of 0.243 to 0.250 (N = 24 to 26 per arm). Every arm was slightly worse than always guessing the base rate.&lt;/p&gt;

&lt;p&gt;Ops accounting, because the rule required it: 29 of 30 tick-days ran (one day lost to a scheduler wedge, baselines paused the same day so the comparison stays fair). Opus arms lost 2 decision days to a failed headless OAuth refresh over a weekend; GPT arms lost 4 to one 401 and three CLI failures, two of them confirmed ChatGPT usage-limit hits. The fallback was always "hold", and the asymmetry slightly disadvantages GPT.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the verdict is inconclusive
&lt;/h2&gt;

&lt;p&gt;The pre-registration required two windows. I ran one and then ended the project, so every hypothesis stays at "inconclusive: single window". That is not a loophole, it is the rule doing its job. One 30-day window on one asset pair in one bull month is one episode. Four arms on that episode, all beating a fixed benchmark, is what beta looks like when your baseline is half in cash.&lt;/p&gt;

&lt;p&gt;What I will say: the structured-beats-autonomous direction showed up in both models with a consistent sign and a plausible mechanism (the code-side gate rejected the kind of churn that killed GPT in Season 1). That is a hypothesis worth a real replication. It is not a finding, and my own document says so.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrapping up
&lt;/h2&gt;

&lt;p&gt;The key insight: write the decision rule before the data exists, then obey it when it becomes inconvenient. In Season 1 the rule turned "+1.48 %" into STOP. In Season 2 it turned "+25 %" into inconclusive. Both times the rule was right and my instinct was wrong, and the second time it was the more expensive lesson, because the numbers were green.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>python</category>
      <category>crypto</category>
      <category>showdev</category>
    </item>
    <item>
      <title>I Replaced an Ad-Stuffed Download Site With 463 Lines of Node</title>
      <dc:creator>Nunc</dc:creator>
      <pubDate>Tue, 01 Sep 2026 08:15:22 +0000</pubDate>
      <link>https://dev.to/nunc/i-replaced-an-ad-stuffed-download-site-with-463-lines-of-node-283d</link>
      <guid>https://dev.to/nunc/i-replaced-an-ad-stuffed-download-site-with-463-lines-of-node-283d</guid>
      <description>&lt;p&gt;I wanted to save X/Twitter videos on my phone, but there was a catch: X has no download button, and the download sites that fill that gap are ad-stuffed, break whenever X rotates its API auth, and you hand every tweet URL to a stranger. So I built my own: a self-hosted web app where you paste a tweet link and get a preview, a quality picker, and a Download button that saves the MP4 to your phone.&lt;/p&gt;

&lt;p&gt;The whole thing is 463 lines of Node with zero npm dependencies, plus 224 lines of vanilla JS for the UI. It is open source (MIT):&lt;/p&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/nuncij" rel="noopener noreferrer"&gt;
        nuncij
      &lt;/a&gt; / &lt;a href="https://github.com/nuncij/twitter-x-downloader" rel="noopener noreferrer"&gt;
        twitter-x-downloader
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;X/Twitter Downloader (xdl)&lt;/h1&gt;
&lt;/div&gt;
&lt;p&gt;Paste any &lt;code&gt;x.com&lt;/code&gt; / &lt;code&gt;twitter.com&lt;/code&gt; tweet link → preview → download. A small, self-hosted web app for saving videos (and GIFs/photos) from X. Mobile-first, works great on phones.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;You host it yourself&lt;/strong&gt; — there's no hosted instance. It runs on your own server (or just your laptop), so it's free, private, and no third-party download site is involved.&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Features&lt;/h2&gt;
&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;📱 Mobile-first UI (dark theme, add-to-home-screen friendly)&lt;/li&gt;
&lt;li&gt;🎬 Quality picker with file sizes (270p → 720p+); picks the best available&lt;/li&gt;
&lt;li&gt;🎞️ Handles &lt;strong&gt;videos&lt;/strong&gt;, &lt;strong&gt;GIFs&lt;/strong&gt;, &lt;strong&gt;photo-only tweets&lt;/strong&gt;, and &lt;strong&gt;mixed video+photo&lt;/strong&gt; tweets&lt;/li&gt;
&lt;li&gt;🔗 1080p HLS-only tweets are merged server-side via ffmpeg automatically&lt;/li&gt;
&lt;li&gt;⚡ Direct CDN streaming for downloads (no big server-side buffering), inline preview plays straight from X's CDN&lt;/li&gt;
&lt;li&gt;🛡️ Per-IP rate limits, concurrency caps, CDN host allowlist (no SSRF)&lt;/li&gt;
&lt;li&gt;🔌 Zero npm dependencies — only &lt;code&gt;node&lt;/code&gt;, &lt;code&gt;ffmpeg&lt;/code&gt;, &lt;code&gt;yt-dlp&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;How it works&lt;/h2&gt;
&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Extraction:&lt;/strong&gt; &lt;a href="https://github.com/yt-dlp/yt-dlp" rel="noopener noreferrer"&gt;yt-dlp&lt;/a&gt; is…&lt;/li&gt;
&lt;/ul&gt;&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/nuncij/twitter-x-downloader" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;


&lt;h2&gt;
  
  
  Two backends, because X breaks things
&lt;/h2&gt;

&lt;p&gt;The server never talks to X's private API itself. That is the part that breaks every few months, so I delegated it:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;yt-dlp&lt;/strong&gt; (primary). One call, &lt;code&gt;yt-dlp -J --no-playlist --skip-download &amp;lt;url&amp;gt;&lt;/code&gt;, returns every available format with file sizes. yt-dlp handles Twitter's guest-token flow and rotating auth, and it keeps working when X changes things because it is actively maintained. Extraction takes 1 to 2 seconds.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;FxTwitter API&lt;/strong&gt; (fallback). &lt;code&gt;api.fxtwitter.com/status/{id}&lt;/code&gt;, free, no auth, answers in 200 to 300 ms. It steps in when yt-dlp hits a guest-token rate limit, and it covers what yt-dlp does not return at all: photo-only tweets and GIFs.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If one backend dies tomorrow, the app degrades instead of dying with it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The format puzzle
&lt;/h2&gt;

&lt;p&gt;Twitter serves each video in several parallel formats. A real 15-second clip came back like this:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;format_id&lt;/th&gt;
&lt;th&gt;kind&lt;/th&gt;
&lt;th&gt;resolution&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;hls-audio-*&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;HLS audio only&lt;/td&gt;
&lt;td&gt;32/64/128 kbps&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;http-288&lt;/code&gt;, &lt;code&gt;http-832&lt;/code&gt;, &lt;code&gt;http-2176&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;progressive MP4&lt;/td&gt;
&lt;td&gt;272p / 364p / 728p&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;hls-191&lt;/code&gt;, &lt;code&gt;hls-343&lt;/code&gt;, &lt;code&gt;hls-716&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;HLS video only&lt;/td&gt;
&lt;td&gt;272p / 364p / 728p&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Two useful facts fell out of staring at that list. Progressive MP4 (one file, video plus audio, plays anywhere) exists up to 720p for most tweets. Anything above 720p is HLS only, so for 1080p the server lets yt-dlp download and merge the streams with ffmpeg, waits for the temp file, streams it to the phone, and deletes it. The merge of a 15-second clip took 1.8 seconds.&lt;/p&gt;

&lt;p&gt;Then the gotcha that cost me the most debugging time. yt-dlp's JSON reports &lt;code&gt;vcodec&lt;/code&gt; and &lt;code&gt;acodec&lt;/code&gt; as &lt;code&gt;null&lt;/code&gt; for exactly those progressive formats, not as &lt;code&gt;h264&lt;/code&gt;/&lt;code&gt;aac&lt;/code&gt;. My first classifier dropped anything without a known codec, which silently threw away every good single-file MP4 and sent every request to the fallback. The fix is to classify by protocol instead of trusting the codec fields:&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="c1"&gt;// kind: 0=mix, 1=video only, 2=audio only&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;classifyFormat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;f&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;acodec&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;acodec&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;none&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;vcodec&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;none&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;protocol&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;startsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;http&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// progressive single-file = mix even when codecs unknown&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;protocol: "https"&lt;/code&gt; means a progressive single file. &lt;code&gt;vcodec === 'none'&lt;/code&gt; (the string, not null) means audio only. Everything else is an HLS video track that needs a merge.&lt;/p&gt;

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

&lt;p&gt;The preview costs the server nothing. X's CDN (&lt;code&gt;video.twimg.com&lt;/code&gt;) serves videos with &lt;code&gt;access-control-allow-origin: *&lt;/code&gt; and plain GET, no cookies, so the &lt;code&gt;&amp;lt;video&amp;gt;&lt;/code&gt; element points straight at the CDN through a 302 redirect. Playback bandwidth: zero.&lt;/p&gt;

&lt;p&gt;The download is the opposite: it streams through the server on purpose, with &lt;code&gt;Content-Disposition: attachment&lt;/code&gt; and a filename built from the tweet (&lt;code&gt;author - title (720p).mp4&lt;/code&gt;). That header is what makes iOS and Android reliably offer "save to Files" instead of opening the video in a tab. Node pipes the CDN response through with the original &lt;code&gt;Content-Length&lt;/code&gt;, nothing is buffered in RAM.&lt;/p&gt;

&lt;p&gt;The UI is one &lt;code&gt;index.html&lt;/code&gt;: dark theme, big touch targets, quality pills with file sizes ("720p · 4.0 MB"), and the meta tags for add-to-home-screen, so on the phone it launches fullscreen like an app.&lt;/p&gt;

&lt;h2&gt;
  
  
  Internet-facing means paranoid
&lt;/h2&gt;

&lt;p&gt;A downloader is a proxy, and a naive proxy is an SSRF hole. Three guards, all in those 463 lines:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The server only fetches URLs whose host, re-parsed server-side, is &lt;code&gt;video.twimg.com&lt;/code&gt; or &lt;code&gt;pbs.twimg.com&lt;/code&gt;, https only. The client cannot point it anywhere else.&lt;/li&gt;
&lt;li&gt;Per-IP rate limits: 6 extracts and 10 downloads per minute, in-memory sliding window.&lt;/li&gt;
&lt;li&gt;Concurrency caps: 1 extraction and 2 downloads at a time. It is a personal tool on a small VPS, not a public service.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Run it yourself
&lt;/h2&gt;

&lt;p&gt;The repo ships a generic &lt;code&gt;deploy.sh&lt;/code&gt;: &lt;code&gt;VPS_HOST=user@server ./deploy.sh&lt;/code&gt; rsyncs the app, creates a Python venv for yt-dlp, and installs a systemd unit. The venv matters: Ubuntu's apt version of yt-dlp is months old, and with this tool old means broken.&lt;/p&gt;

&lt;p&gt;My own instance never touches the public internet. The DNS record points at a Tailscale address, the reverse proxy binds to the tailnet interface, and the firewall drops everything else. HTTPS still works with zero open ports, because Let's Encrypt supports the DNS-01 challenge: the cert is issued through a DNS API token, no port 80 required. My phone runs Tailscale, so the app works from anywhere and is still invisible to everyone else.&lt;/p&gt;

&lt;p&gt;One note on open-sourcing a personal tool: before the repo went public I found my own server details in three places (the deploy script, the README, and one commit message). The first two were rewritten; for the third the history had to be squashed into a single clean commit, because a secret in an old commit is still a published secret.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrapping up
&lt;/h2&gt;

&lt;p&gt;The key insight: don't fight X's API yourself. Let yt-dlp absorb the auth churn, keep a free second backend for the day it hiccups, and spend your own lines on the parts that make it yours: the streaming, the filenames, the SSRF guard, and a UI your thumb likes. Mine came to 463 lines, and there is not a single ad on it.&lt;/p&gt;

</description>
      <category>node</category>
      <category>webdev</category>
      <category>showdev</category>
      <category>selfhosted</category>
    </item>
    <item>
      <title>Oracle Designer Lost Support in 2013. My AI Agent Develops in It Anyway.</title>
      <dc:creator>Nunc</dc:creator>
      <pubDate>Fri, 21 Aug 2026 11:51:51 +0000</pubDate>
      <link>https://dev.to/nunc/oracle-designer-lost-support-in-2013-my-ai-agent-develops-in-it-anyway-2c2k</link>
      <guid>https://dev.to/nunc/oracle-designer-lost-support-in-2013-my-ai-agent-develops-in-it-anyway-2c2k</guid>
      <description>&lt;p&gt;Oracle Designer 10g is a CASE tool from the early 2000s. Support ended in 2013. At my job it is still the center of development: the repository holds the entire design of a large insurance system (2,741 entities, 3,277 table definitions, 12,841 PL/SQL modules, 5,624 forms), and about 10 developers work in it daily. Everyone, me included, assumed you work with Designer through its GUI. Clicking, dragging, repeating.&lt;/p&gt;

&lt;p&gt;The question I wanted to answer: can an AI agent that knows SQL and has database access do that work instead?&lt;/p&gt;

&lt;p&gt;Two days later the agent had designed an entity, a table, a sequence, two triggers, and a complete form definition, moved a table between applications, and debugged the generated form to a working runtime. It opened the Designer GUI exactly zero times. Everything I personally did in a GUI fits in two buttons: Generate, and starting the form.&lt;/p&gt;

&lt;h2&gt;
  
  
  The discovery that made it possible
&lt;/h2&gt;

&lt;p&gt;Designer stores everything in an Oracle schema, and next to the data it ships three layers that are all reachable from a plain SQL session:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;CI_* views&lt;/strong&gt; (722 of them) for reading. Every entity, column, and form item is a row in a view.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CIO* packages&lt;/strong&gt; (about 200) for writing. Every element type has a package with &lt;code&gt;ins/upd/del/sel&lt;/code&gt; and a typed record.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CDAPI&lt;/strong&gt; for transactions: &lt;code&gt;open_activity&lt;/code&gt;, then your changes, then &lt;code&gt;validate_activity&lt;/code&gt; (Designer's own validation rules), then &lt;code&gt;close_activity&lt;/code&gt;. On failure, &lt;code&gt;abort_activity&lt;/code&gt; rolls the repository back.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The basic write pattern looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;declare&lt;/span&gt;
   &lt;span class="n"&gt;ent&lt;/span&gt; &lt;span class="n"&gt;cioentity&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;data&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
   &lt;span class="n"&gt;st&lt;/span&gt;  &lt;span class="n"&gt;varchar2&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
   &lt;span class="n"&gt;wa&lt;/span&gt;  &lt;span class="n"&gt;varchar2&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;begin&lt;/span&gt;
   &lt;span class="n"&gt;jr_context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;set_workarea&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'GLOBAL SHARED WORKAREA'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;  &lt;span class="c1"&gt;-- skip this: CDR-00100&lt;/span&gt;
   &lt;span class="n"&gt;cdapi&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;initialize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'MYAPP'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
   &lt;span class="n"&gt;cdapi&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;open_activity&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

   &lt;span class="n"&gt;ent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;       &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'MY ENTITY'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  &lt;span class="n"&gt;ent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;       &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
   &lt;span class="n"&gt;ent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;short_name&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'MYE'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;        &lt;span class="n"&gt;ent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;short_name&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
   &lt;span class="n"&gt;cioentity&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ins&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ent&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

   &lt;span class="n"&gt;cdapi&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;validate_activity&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;st&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;wa&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
   &lt;span class="n"&gt;if&lt;/span&gt; &lt;span class="n"&gt;st&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'Y'&lt;/span&gt; &lt;span class="k"&gt;then&lt;/span&gt;
      &lt;span class="n"&gt;cdapi&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;close_activity&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;st&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="n"&gt;if&lt;/span&gt; &lt;span class="n"&gt;st&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'Y'&lt;/span&gt; &lt;span class="k"&gt;then&lt;/span&gt; &lt;span class="k"&gt;commit&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="n"&gt;cdapi&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;abort_activity&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;end&lt;/span&gt; &lt;span class="n"&gt;if&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
   &lt;span class="k"&gt;else&lt;/span&gt;
      &lt;span class="n"&gt;while&lt;/span&gt; &lt;span class="n"&gt;cdapi&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;stacksize&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="n"&gt;loop&lt;/span&gt;
         &lt;span class="n"&gt;dbms_output&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;put_line&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'VIOLATION: '&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;cdapi&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pop_instantiated_message&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="k"&gt;end&lt;/span&gt; &lt;span class="n"&gt;loop&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="n"&gt;cdapi&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;abort_activity&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
   &lt;span class="k"&gt;end&lt;/span&gt; &lt;span class="n"&gt;if&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="o"&gt;/&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;There is no documentation for any of this anymore. It does not matter, because the API is self-documenting: the package specs are readable from &lt;code&gt;all_source&lt;/code&gt;, so the agent's first move for every element type was one query away:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;select&lt;/span&gt; &lt;span class="nb"&gt;text&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="n"&gt;all_source&lt;/span&gt;
&lt;span class="k"&gt;where&lt;/span&gt; &lt;span class="k"&gt;owner&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;repo_owner&lt;/span&gt; &lt;span class="k"&gt;and&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'CIOENTITY'&lt;/span&gt;
&lt;span class="k"&gt;and&lt;/span&gt; &lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'PACKAGE'&lt;/span&gt; &lt;span class="k"&gt;order&lt;/span&gt; &lt;span class="k"&gt;by&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;An AI agent does not need the GUI. It needs sqlplus.&lt;/p&gt;
&lt;h2&gt;
  
  
  The loop that worked
&lt;/h2&gt;

&lt;p&gt;The same four steps repeated for every element type:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Read the API from the database.&lt;/strong&gt; The package spec says which fields the record has.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Read an existing element as the template.&lt;/strong&gt; There is no house-standards document, but there are 3,277 existing tables. Naming conventions, audit columns, domains, window sizes, preference sets: all of it was read from elements my colleagues built over 20 years. "How we do things here" is written in the data itself.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Write in small validated transactions.&lt;/strong&gt; One logical change, validate, and on failure print the violation messages and abort.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Verify through CI views&lt;/strong&gt; before moving on.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;With that loop the agent built the full chain: entity with attributes, table with columns on the house domains plus a primary key, the table-to-entity mapping (normally the job of a Designer wizard called the Database Design Transformer), a sequence with the ID trigger, audit columns with their trigger (PL/SQL bodies written through the &lt;code&gt;RMOTEXT&lt;/code&gt; API), table ownership moved to another application with a shortcut left in the original, and the complete form definition: module, window, component, table usage, items, preference sets.&lt;/p&gt;

&lt;p&gt;Then came the one part SQL cannot do. Generating the actual Forms binary requires a client-side tool, so I pressed Generate in Designer. The generator failed. The agent read the generator output and the &lt;code&gt;.err&lt;/code&gt; log file from disk, diagnosed each failure, fixed them through the API, and I pressed the button again. That cycle repeated until the form ran.&lt;/p&gt;

&lt;p&gt;One fix deserves a mention. The form compiled and started, but the layout was broken: all fields stacked on top of each other. I gave the agent a screenshot of the running form. It recognized the pattern, found the cause (items without &lt;code&gt;display_width&lt;/code&gt; fall back to the column's width, and the layout collapses into a stack) and set the widths on all items through the API. The next generation ran clean.&lt;/p&gt;
&lt;h2&gt;
  
  
  Errors are data
&lt;/h2&gt;

&lt;p&gt;Around 20 distinct errors came up across the two days. Every one was diagnosable from the message plus the state of the database. A sample:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Error&lt;/th&gt;
&lt;th&gt;Cause&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;CDR-00100: Workarea context has not been set&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;missing &lt;code&gt;jr_context.set_workarea&lt;/code&gt; before anything else&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;ORA-00001&lt;/code&gt; on an internal unique key&lt;/td&gt;
&lt;td&gt;reusing a CIO record between &lt;code&gt;ins&lt;/code&gt; calls: it keeps the previous element's IDs, so reset the record to an empty one&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;PLS-00302: component 'INS' must be declared&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;some packages are abstract; write through the specific subtype package&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;CDG-01199: no queryable item&lt;/code&gt; at generation&lt;/td&gt;
&lt;td&gt;items were created without &lt;code&gt;select_flag = 'Y'&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;identifier must be declared&lt;/code&gt; in the &lt;code&gt;.err&lt;/code&gt; file&lt;/td&gt;
&lt;td&gt;the physical table did not exist yet on the target dev database&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;None of these are in any manual. All of them are now in ours, because the collected errors turned out to be the most valuable output of the whole exercise. They went into two places: a handbook for humans, and a &lt;a href="https://github.com/nuncij/oracle-designer-cdapi-skill" rel="noopener noreferrer"&gt;Claude Code skill&lt;/a&gt; for future AI sessions. The next session does not rediscover CDAPI, it starts from a working recipe.&lt;/p&gt;

&lt;p&gt;The skill is open source (MIT), with the transaction templates, the per-element procedures, and the full error table:&lt;/p&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/nuncij" rel="noopener noreferrer"&gt;
        nuncij
      &lt;/a&gt; / &lt;a href="https://github.com/nuncij/oracle-designer-cdapi-skill" rel="noopener noreferrer"&gt;
        oracle-designer-cdapi-skill
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      Drive Oracle Designer (6i/9i/10g) with an AI coding agent via its CDAPI/CIO PL/SQL API - entities, tables, forms, no GUI. Claude Code skill + proven SQL templates.
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;Oracle Designer CDAPI Skill — drive Oracle Designer with an AI coding agent&lt;/h1&gt;
&lt;/div&gt;

&lt;p&gt;Oracle Designer (6i/9i/10g) is a CASE tool from the early 2000s, out of support since 2013, yet still the center of development in many legacy Oracle shops. The common assumption is that you can only work with it through its GUI (Repository Object Navigator, Design Editor). That assumption is wrong.&lt;/p&gt;

&lt;p&gt;Designer stores everything in an Oracle schema and ships a complete PL/SQL API next to it. That means an AI coding agent (Claude Code, or any tool that can run sqlplus) can read and write the repository directly: create entities, tables, columns, keys, sequences, triggers, and complete form module definitions — validated by Designer's own engine, without a single click in the GUI. The only steps left for a human are pressing Generate (Forms/DDL generators are client-side) and drawing diagrams.&lt;/p&gt;

&lt;p&gt;This repository packages that capability as an…&lt;/p&gt;&lt;/div&gt;


&lt;/div&gt;
&lt;br&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/nuncij/oracle-designer-cdapi-skill" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;br&gt;
&lt;/div&gt;
&lt;br&gt;


&lt;h2&gt;
  
  
  An agent with write access to production designs needs guardrails
&lt;/h2&gt;

&lt;p&gt;The same access that lets an agent create a table lets it silently damage a colleague's form with one mistyped ID. Before letting it loose we layered defenses: scripts look up elements strictly by name and verify parentage before touching anything, a control report runs after every session and lists everything that user changed that day across 17 element types, and a log table with a nightly job keeps permanent change history, because Designer itself only stores the last change. The nightly database backup stays as the final net. All of it is running, not planned.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrapping up
&lt;/h2&gt;

&lt;p&gt;The limits we actually hit: drawing ER diagrams (the layouts are binary blobs) and pressing the two client-side generator buttons. Everything else that "requires the GUI" turned out not to.&lt;/p&gt;

&lt;p&gt;The key insight: a legacy tool with no SDK, no docs, and no support is not necessarily closed to AI agents. If it stores its world in a database, it may be the most open tool you have. The dictionary replaces the SDK, existing data replaces the standards document, and error messages replace support. Designer waited 20 years for a user that reads package specs for fun.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>oracle</category>
      <category>sql</category>
      <category>showdev</category>
    </item>
    <item>
      <title>My Peer-Reviewed T. rex Looks Worse Than a 1996 Video Game</title>
      <dc:creator>Nunc</dc:creator>
      <pubDate>Fri, 14 Aug 2026 10:31:56 +0000</pubDate>
      <link>https://dev.to/nunc/my-peer-reviewed-t-rex-looks-worse-than-a-1996-video-game-40dm</link>
      <guid>https://dev.to/nunc/my-peer-reviewed-t-rex-looks-worse-than-a-1996-video-game-40dm</guid>
      <description>&lt;p&gt;I wanted to know what a &lt;em&gt;Tyrannosaurus rex&lt;/em&gt; actually looked like. Not the movie one. The real one, the one the papers describe. So I told AI agents to read the science and build it in 3D, model and animation, with no human touching the mesh.&lt;/p&gt;

&lt;p&gt;Three days later I had a potato.&lt;/p&gt;

&lt;p&gt;&lt;a href="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%2Fncybttd2n97wjc3ha2ia.png" class="article-body-image-wrapper"&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%2Fncybttd2n97wjc3ha2ia.png" alt="Render of the finished T. rex in an environment shot: a beige, heavy-bellied dinosaur with thin legs walking on flat sand under a clear sky, horizontal posture, tail held straight out, calm and completely unthreatening" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;That is the apex predator. Beige, calm, faintly disappointed. It has the body language of a man waiting for a bus.&lt;/p&gt;

&lt;p&gt;And here is the annoying part: &lt;strong&gt;it is scientifically correct.&lt;/strong&gt; Every bone length traces to a published paper. The body mass sits inside the window from peer-reviewed volumetric studies. The walk cycle matches a Royal Society paper to three decimals and a script verified it. My dinosaur can cite its sources. It just cannot frighten a chicken.&lt;/p&gt;

&lt;p&gt;I am oddly proud of it and I cannot show it to anyone without explaining it first, which is usually a sign you have built the wrong thing.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the agents taught me about T. rex
&lt;/h2&gt;

&lt;p&gt;I never opened Blender. My job was approving phases and stopping Windows from falling asleep mid-render. The agents did the rest: two Claude Code sessions, one per machine, plus OpenAI's Codex hired specifically to be unpleasant about the plan.&lt;/p&gt;

&lt;p&gt;The research was the part I enjoyed, because the agents kept telling me I was wrong about a dinosaur I had been confident about since I was eight:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;It had lips.&lt;/strong&gt; Teeth covered when the mouth is closed. More monitor lizard, less crocodile grin. Sorry, Jurassic Park.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It was scaly&lt;/strong&gt;, pebbly scales about a millimetre across. On a 12 metre animal that is a shader detail. Nobody sculpts a millimetre.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It was slow.&lt;/strong&gt; Preferred walking speed 4.6 km/h, set by the resonant frequency of its tail. Top speed maybe 25 km/h, always with one foot down, because an aerial phase would have snapped its legs. The jeep was never in danger.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The tail was the engine.&lt;/strong&gt; The biggest leg muscle lives in the tail, roughly 260 kg per side. Every skinny-tailed rex you have seen is wrong.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The colour is unknowable.&lt;/strong&gt; No pigment data exists for any tyrannosaurid. Ours is olive-brown because somebody had to choose, and the repo says so in writing.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Then Codex, the hired pessimist, earned its fee in an afternoon. My research agent had cheerfully declared the Smithsonian's T. rex scan public domain. Codex opened the actual page: only the metadata is CC0, the scan is non-commercial only. A hallucinated licence, caught by a rival model that bothered to click the link.&lt;/p&gt;

&lt;p&gt;Codex also caught the mass formula counting the air cavities twice. My dinosaur was being deflated twice for the same lungs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Building a dinosaur when nobody involved can sculpt
&lt;/h2&gt;

&lt;p&gt;Neither I nor the agents can sculpt. So the pipeline became Python scripts driving headless Blender, where nothing is judged by eye and everything is judged by a number.&lt;/p&gt;

&lt;p&gt;&lt;a href="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%2Fy45fj6vy2htpg030s3ot.jpg" class="article-body-image-wrapper"&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%2Fy45fj6vy2htpg030s3ot.jpg" alt="Four stages of the pipeline stacked vertically: a white parametric skeleton with bone lengths from published measurements, the same skeleton with 17 red muscle volumes added, a smooth white body envelope whose volume times density must land in 8.4 to 9.5 tonnes, and the final beige rendered dinosaur in its environment" width="800" height="1681"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Bones first, from published measurements of "Sue". Then 17 muscle groups, each named after the real muscle, each traceable to an anatomy paper. The script prints every muscle's weight, which is how I learned my dinosaur's tail muscle had come out at 471 kg per side against a published estimate of 261. It had been lifting. Tuned down to 252 kg.&lt;/p&gt;

&lt;p&gt;Then the skin, with one rule: mesh volume times published tissue density must land between 8.4 and 9.5 tonnes. First attempt, 11.4 to 13.1 tonnes. My scientifically rigorous T. rex was 25 % overweight and its belly hung 7 cm below where the ribs say a belly stops. Two rounds of dieting later it passed at 8.8 to 10.2 tonnes.&lt;/p&gt;

&lt;p&gt;All of that worked, and I want to be fair to it. Build, measure, compare against a paper, fix. Dozens of times, overnight, with nobody watching and no human skill involved. Nothing about the science half embarrasses me. I would put those numbers in front of a paleontologist.&lt;/p&gt;

&lt;h2&gt;
  
  
  The moment I should have seen coming
&lt;/h2&gt;

&lt;p&gt;The walk had a validator: no aerial phase, no foot sliding, no tail dragging, jaw inside its measured gape. It rejected the animation four times and every rejection was a real bug. Right-foot plants generated at the left foot's coordinates, so the poor thing dragged a foot two metres per step. A clamping bug at the clip boundary. Feet planted too close to the hip, shoving the femur past its allowed arc.&lt;/p&gt;

&lt;p&gt;Then the validator said PASS. I opened the render, and the dinosaur had stick legs and floating feet. The rig was rotating each leg 90 degrees, so the wide thigh faced the camera edge-on. Every number was perfect. The picture looked like the animal had been assembled by somebody reading the instructions upside down.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Validation catches what you thought to measure.&lt;/strong&gt; I had measured whether the foot reached the ground. I had not measured whether the leg was on straight.&lt;/p&gt;

&lt;p&gt;Here is the finished walk, machine-validated, zero violations, at the exact speed the paper says the animal preferred:&lt;/p&gt;

&lt;p&gt;&lt;a href="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%2Fr1034d2w4evmharm6wyw.gif" class="article-body-image-wrapper"&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%2Fr1034d2w4evmharm6wyw.gif" alt="Looping animation of the finished T. rex walking across flat sand in profile: a beige, heavy-bellied dinosaur taking slow even strides at its scientifically preferred walking speed, tail held straight out and swaying slightly with each step, the motion perfectly regular and mechanical" width="640" height="360"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Two full strides at 1.293 m/s. Every number in it is right and it walks like a metronome with a mortgage. &lt;a href="https://hellonunc.com/assets/articles/peer-reviewed-trex-looks-worse/trex-walk.mp4" rel="noopener noreferrer"&gt;MP4 in 1080p&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;I showed it to my wife. She asked what it was supposed to be doing.&lt;/p&gt;

&lt;h2&gt;
  
  
  So why did I get a potato?
&lt;/h2&gt;

&lt;p&gt;I assumed it was my fault: bad prompts, wrong tools, not enough iterations. So the agents ran one more research pass, this time on themselves. The potato was predictable, and the literature is almost rude about how predictable:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The models cannot see properly.&lt;/strong&gt; &lt;a href="https://arxiv.org/abs/2407.06581" rel="noopener noreferrer"&gt;"Vision Language Models Are Blind"&lt;/a&gt; tested frontier models on things a child does instantly, like whether two circles overlap: &lt;strong&gt;58.57 %&lt;/strong&gt; average. Probes suggest the vision half knows and the language half cannot read it out loud.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Their taste is not our taste.&lt;/strong&gt; &lt;a href="https://arxiv.org/pdf/2510.22373" rel="noopener noreferrer"&gt;VisJudge-Bench&lt;/a&gt; puts model-to-human agreement on aesthetics at &lt;strong&gt;0.18 to 0.41&lt;/strong&gt;, and &lt;a href="https://mllm-judge.github.io/" rel="noopener noreferrer"&gt;MLLM-as-a-Judge&lt;/a&gt; found they are fine at "which of these two is better" and unreliable at grading one thing alone, which is exactly what an agent does to its own render. Mine could tell broken legs from normal legs. It could not tell mediocre from good.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;There is a benchmark for agents driving Blender, and it is humbling.&lt;/strong&gt; &lt;a href="https://arxiv.org/abs/2504.01786" rel="noopener noreferrer"&gt;BlenderGym&lt;/a&gt; scored thirteen vision-language systems &lt;strong&gt;2 to 10 times worse than human Blender users&lt;/strong&gt;, on tasks the authors call easy for people.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Humans are freakishly good at spotting bad motion.&lt;/strong&gt; Twelve moving dots are enough for you to recognise a walking person. Worse, &lt;a href="https://www.sciencedirect.com/science/article/abs/pii/S0010027713002114" rel="noopener noreferrer"&gt;uncanny-valley research&lt;/a&gt; found degraded motion drops a character &lt;em&gt;below&lt;/em&gt; its own still image. My walk cycle is not neutral. It is actively working against me.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Then the structural reason, which is the part I keep chewing on. What made these models good at maths and code is training against verifiable rewards. "Femur = 1.321 m" is verifiable. "Feels alive" is not. Correctness automates first by construction, not by accident.&lt;/p&gt;

&lt;p&gt;I hired a correctness engine and handed it a beauty problem. It solved the half it was built for and gave me a vegetable.&lt;/p&gt;

&lt;h2&gt;
  
  
  The 1996 problem
&lt;/h2&gt;

&lt;p&gt;The T. rex in Tomb Raider, a few hundred polygons and a hand-painted texture, is scarier than my 52,000-quad specimen. Artists in 1996 had no science and no polygons and made a monster. My agents had all the science and all the polygons and made an exhibit.&lt;/p&gt;

&lt;p&gt;Which is, if I squint, the actual finding. An artist starting from this repo would skip weeks of paleontology and inherit guardrails against movie-monster anatomy. My agents would get the one judge they lack. Neither half is worthless. They are two different jobs, and I spent three days proving it with a dinosaur.&lt;/p&gt;

&lt;p&gt;The repo stays private and non-commercial, because the museum scan I used as a shape reference allows personal use only. Nobody is fighting me for the rights to a beige potato anyway.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I would tell you
&lt;/h2&gt;

&lt;p&gt;Make the agent's claims falsifiable with numbers from the literature. Hire one hostile reviewer from a different vendor. Look at the renders, because four green validator runs hid a dinosaur with sideways legs. Write down what you do not know.&lt;/p&gt;

&lt;p&gt;And expect the last 10 % to be 90 % craft. Correctness automated beautifully. Beauty did not, and now I have citations explaining why. My dinosaur is anatomically defensible, machine-validated, and about as frightening as livestock.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>blender</category>
      <category>python</category>
      <category>showdev</category>
    </item>
    <item>
      <title>AI Agents Can't Watch Video. Mine Made Three Films Anyway.</title>
      <dc:creator>Nunc</dc:creator>
      <pubDate>Fri, 07 Aug 2026 11:33:40 +0000</pubDate>
      <link>https://dev.to/nunc/ai-agents-cant-watch-video-mine-made-three-films-anyway-46cp</link>
      <guid>https://dev.to/nunc/ai-agents-cant-watch-video-mine-made-three-films-anyway-46cp</guid>
      <description>&lt;p&gt;&lt;em&gt;Martin Krpan&lt;/em&gt; is the story every Slovenian schoolchild knows: a salt smuggler meets the emperor's carriage on a snowed-in road too narrow for both, so he calmly lifts his loaded mare and carries her aside. I asked AI agents to turn it into an animated film. The catch: a language model cannot watch video, so it cannot check its own footage. I ran the experiment three times with three different AI setups. All three are live. The interesting result is not the films but what a blind director needs before you can trust its cut.&lt;/p&gt;

&lt;p&gt;The idea comes from Andrej Karpathy's &lt;a href="https://karpathy.ai/lotr-movie" rel="noopener noreferrer"&gt;LOTR browser movie&lt;/a&gt;, which he rated "kind of janky but fun" and blamed on exactly this: LLMs can't perceive video, so they can't audit the worlds they build.&lt;/p&gt;

&lt;h2&gt;
  
  
  The rules, same for all three
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Zero assets.&lt;/strong&gt; No 3D models, no textures, no image, font or audio files. Geometry from Three.js primitives, sound synthesized in the browser. The one exception: narration, pre-rendered with a free tool; each film works without it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Deterministic timeline.&lt;/strong&gt; Every frame is a pure function of film time. No accumulated physics, no &lt;code&gt;Math.random()&lt;/code&gt; at play time. Seeking to second 49.5 produces the same pixels every time, in every browser process.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It must be a film.&lt;/strong&gt; Shots, hard cuts, title cards, subtitles, pacing, an ending. In Slovenian and English.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The determinism rule is the whole trick: if a frame is a pure function of time, a screenshot becomes a measurement. The agent can seek, look at the still (the one visual sense an LLM has), compare pixel hashes between runs, and export video frame by frame. Every verification idea below stands on that.&lt;/p&gt;

&lt;h2&gt;
  
  
  Film 1: one agent and a screenshot loop
&lt;/h2&gt;

&lt;p&gt;The first run was a single Claude agent building scene 1: the road encounter, 75 seconds, 11 shots. One working day: 7.5 hours of build time, 3,505 lines of TypeScript.&lt;/p&gt;

&lt;p&gt;The process rule that shaped it: after building, the agent must watch its own film as stills. A script seeks the timeline, captures screenshots, and the agent critiques them like a director: framing, scale, mesh intersections, poses. It found 24+ defects on its own this way, including the mare's muzzle pointing 43 degrees up (it read as antlers), the emperor's head clipping through the carriage roof, and two shots filming empty road because the carriage hadn't arrived yet.&lt;/p&gt;

&lt;p&gt;What it never found: that the film was ugly. The four changes that most improved the result were one-sentence human comments ("the low-poly look is really ugly", "the background sound is annoying", "the narrator is robotic", "Discord says max 15 MB"). Each triggered a real engineering response: an art-direction rebuild with HDR bloom and golden-hour light, a remix measured 60 % quieter, pre-rendered neural voices, a two-pass encode to 13.4 MB.&lt;/p&gt;

&lt;p&gt;The split was clean: self-review converged on &lt;em&gt;correct&lt;/em&gt;. The human one-liners steered it to &lt;em&gt;good&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;▶ &lt;a href="https://krpan.hellonunc.com/" rel="noopener noreferrer"&gt;krpan.hellonunc.com&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Film 2: the full story, and a harness with teeth
&lt;/h2&gt;

&lt;p&gt;The second run scaled up: the whole tale, 10 scenes, exactly 260 seconds, about 14,800 lines. This time three agents split the roles: one planned, one built, and a rival vendor's model (OpenAI Codex) attacked the plan before any code existed. Sixteen findings came back; the best one justified the setup: a dialogue line in the shot list was &lt;strong&gt;not in Levstik's text&lt;/strong&gt;. The model had paraphrased the classic from memory. Codex fetched the pinned Wikisource corpus and proved it. That finding became a validator: every subtitle and narration line must be a verbatim substring of the 1858 text.&lt;/p&gt;

&lt;p&gt;Beyond screenshots, this run made the aesthetics machine-checkable: six invariant classes asserted on every scene (palette only from named constants, prop sizes within 10 % of declared, exactly the declared lights, performance ceilings, no object jumping more than 0.5 m outside a cut). And determinism was tested, not assumed: two fresh renders of all ten scenes differed by &lt;strong&gt;0.0000 %&lt;/strong&gt; of pixels.&lt;/p&gt;

&lt;p&gt;That check earned its keep. The sword in the duel was aimed using the giant's hand position from the &lt;em&gt;previous&lt;/em&gt; frame, a frame-order dependency invisible to the eye. It surfaced as a 0.2 % pixel difference between playing into a moment and jumping straight to it.&lt;/p&gt;

&lt;p&gt;Total cost: about 1.4 million tokens, roughly a cinema ticket, for a bilingual adaptation no studio would fund. The report's own conclusion: the same builder without the harness would have shipped the silhouette shot, the misaimed sword, and the paraphrased Levstik.&lt;/p&gt;

&lt;p&gt;▶ &lt;a href="https://krpan2.hellonunc.com/" rel="noopener noreferrer"&gt;krpan2.hellonunc.com&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Film 3: a different vendor runs it like a science fair
&lt;/h2&gt;

&lt;p&gt;The third run gave the format to GPT-5.6 Codex, working interactively, and pushed verification the furthest: hypotheses with numeric pass criteria written down &lt;em&gt;before&lt;/em&gt; evaluation. 78.000 seconds, 1,872 frames at 24 fps, and every single frame scanned for collisions between bounding boxes of every moving object.&lt;/p&gt;

&lt;p&gt;The exhaustive scan embarrassed the still-image review. The stills looked fine while the team horses walked through Krpan for eight seconds and the carriage pole crossed the mare for fifteen. First scan: four sustained collision intervals. Final scan: &lt;strong&gt;0 violations in 1,872 frames&lt;/strong&gt;. Wheel rotation was derived from distance and radius instead of a multiplier that "looked right"; the final slip error was 1.6 × 10⁻⁷ scene units per frame, and zero frames rolled backward. The MP4 exports were decoded frame by frame: exactly 1,872 H.264 frames in each language edition.&lt;/p&gt;

&lt;p&gt;The methodological lesson from this run: stills, continuous playback and an exhaustive numeric scan catch three disjoint classes of bugs. Any one alone lies to you.&lt;/p&gt;

&lt;p&gt;▶ &lt;a href="https://krpan3.hellonunc.com/" rel="noopener noreferrer"&gt;krpan3.hellonunc.com&lt;/a&gt; (MP4 downloads on the page)&lt;/p&gt;

&lt;h2&gt;
  
  
  The scoreboard
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Film 1&lt;/th&gt;
&lt;th&gt;Film 2&lt;/th&gt;
&lt;th&gt;Film 3&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Scope&lt;/td&gt;
&lt;td&gt;scene 1, 75 s&lt;/td&gt;
&lt;td&gt;full story, 260 s&lt;/td&gt;
&lt;td&gt;scene 1 retold, 78 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Built by&lt;/td&gt;
&lt;td&gt;one Claude agent&lt;/td&gt;
&lt;td&gt;Claude planner + builder, Codex as adversary&lt;/td&gt;
&lt;td&gt;GPT-5.6 Codex, interactive&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Code&lt;/td&gt;
&lt;td&gt;3,505 lines&lt;/td&gt;
&lt;td&gt;~14,800 lines&lt;/td&gt;
&lt;td&gt;~3,140 lines&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Verification signature&lt;/td&gt;
&lt;td&gt;screenshot self-review&lt;/td&gt;
&lt;td&gt;6 machine-checked invariants, 0.0000 % pixel determinism&lt;/td&gt;
&lt;td&gt;all 1,872 frames scanned, numeric pass criteria&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Caught what the eye missed&lt;/td&gt;
&lt;td&gt;24+ visual defects&lt;/td&gt;
&lt;td&gt;invented Levstik quote, frame-order bug&lt;/td&gt;
&lt;td&gt;4 collision intervals, backward wheels&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;a href="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%2Flrxtn9i9tryeyefthtt7.jpg" class="article-body-image-wrapper"&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%2Flrxtn9i9tryeyefthtt7.jpg" alt="Three stills of the same moment from three AI-built films: Krpan carrying his loaded mare off a snowy road. Film 1 is warm and pastel with soft snowfall, film 2 is pale blue and cinematic with a Slovenian subtitle from Levstik, film 3 is a deeper green winter scene with the carriage horses waiting on the right" width="800" height="1465"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The same moment in all three films: Krpan lifting the loaded mare off the road.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What all three runs agree on
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;"It runs" is a weak finish line.&lt;/strong&gt; Every version ran fine while hiding collisions, backward wheels, a phantom sword or paraphrased text. The quality came from the harness: determinism, screenshots as measurements, invariants, a rival model reading the plan.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The human contribution is taste, not code.&lt;/strong&gt; Across all three films I never edited a line. My inputs were the story choice, the constraints, and one-sentence quality judgements. The agents handled everything else, including finding most of their own bugs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Determinism is the keystone.&lt;/strong&gt; One property served four masters: frame-exact scrubbing for viewers, screenshot audits for the agent, regression checks for refactors, and clean video export.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrapping up
&lt;/h2&gt;

&lt;p&gt;A language model cannot watch video, but it does not need to. If every frame is a pure function of time, screenshots become measurements, invariants become tests, and a blind director can prove its footage is right instead of hoping. The films are the demo. The harness is the product.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>threejs</category>
      <category>javascript</category>
      <category>showdev</category>
    </item>
    <item>
      <title>EXPLAIN PLAN as a Lint for LLM-Generated SQL</title>
      <dc:creator>Nunc</dc:creator>
      <pubDate>Thu, 06 Aug 2026 10:48:57 +0000</pubDate>
      <link>https://dev.to/nunc/explain-plan-as-a-lint-for-llm-generated-sql-4mg8</link>
      <guid>https://dev.to/nunc/explain-plan-as-a-lint-for-llm-generated-sql-4mg8</guid>
      <description>&lt;p&gt;My AI agents write Oracle SQL all day: fix scripts, diagnostics, one-off reports for a 2.3M-line legacy system. Their most common failure isn't bad logic. It is SQL that references a table or column that almost exists. Oracle has had the fix for decades, it costs one statement per query, and it never executes anything: &lt;code&gt;EXPLAIN PLAN&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The failure mode: names that almost exist
&lt;/h2&gt;

&lt;p&gt;A language model doesn't know your schema. It knows what schemas usually look like. So on a 20-year-old database with thousands of tables, it produces names that are plausible instead of real: &lt;code&gt;POLICY_STATUS&lt;/code&gt; when the column is &lt;code&gt;STATUS_CD&lt;/code&gt;, &lt;code&gt;CUSTOMERS&lt;/code&gt; when the table has been &lt;code&gt;CUSTOMER&lt;/code&gt; (singular) since 1998, a join through a link table that was dropped two versions ago.&lt;/p&gt;

&lt;p&gt;&lt;a href="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%2Fnl75tet1gta5u3hqoe40.png" class="article-body-image-wrapper"&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%2Fnl75tet1gta5u3hqoe40.png" alt="Comparison of what the model wrote versus what the schema has: POLICY_STATUS versus STATUS_CD (plausible name, wrong name), CUSTOMERS versus CUSTOMER (singular since 1998), and a join through POLICY_CUST_LINK, a table dropped two versions ago; a human reviewer skims this and it reads fine because the naming convention matches" width="800" height="373"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;These are the worst kind of errors, because they look right. A human reviewer skims the script, the naming convention matches, everything reads fine. The error only surfaces when the script runs, and in my case fix scripts run at more than 20 customer installations. That is exactly the place where you do not want to discover an invented column.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why you cannot just run it to check
&lt;/h2&gt;

&lt;p&gt;The obvious test, execute it and see, is not available. These scripts are UPDATEs and DELETEs against production-like data. Wrapping everything in a transaction and rolling back sort of works, but it fires triggers, takes locks, burns sequence numbers and takes time on big tables.&lt;/p&gt;

&lt;p&gt;I wanted a check that touches nothing and still uses the real schema. It already exists.&lt;/p&gt;

&lt;h2&gt;
  
  
  EXPLAIN PLAN parses without executing
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;EXPLAIN&lt;/span&gt; &lt;span class="n"&gt;PLAN&lt;/span&gt; &lt;span class="k"&gt;FOR&lt;/span&gt;
&lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;policy&lt;/span&gt;
   &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;status_cd&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'ACTIVE'&lt;/span&gt;
 &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;policy_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;b1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Oracle takes the statement through the full parse: it resolves every table and column against the live data dictionary, checks your privileges, builds an execution plan and writes it to &lt;code&gt;PLAN_TABLE&lt;/code&gt;. What it never does is execute. No rows change, no triggers fire, no locks are held. It is safe to run for an UPDATE, a DELETE, a MERGE.&lt;/p&gt;

&lt;p&gt;If the model invented a table, you get an answer in milliseconds:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ORA-00942: table or view does not exist
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If it invented a column:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ORA-00904: "POLICY_STATUS": invalid identifier
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is a lint result, produced by the one parser that actually knows the schema: the database's own.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wiring it into the agent loop
&lt;/h2&gt;

&lt;p&gt;In my workflow every generated script goes through this gate before a human sees it, statement by statement, against the dev database:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;stmt&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;split_statements&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;script&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;cursor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;EXPLAIN PLAN FOR &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;stmt&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;oracledb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DatabaseError&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;errors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;stmt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="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%2Fttthb6lshiu4k0pr422u.png" class="article-body-image-wrapper"&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%2Fttthb6lshiu4k0pr422u.png" alt="Diagram of the EXPLAIN PLAN gate: an agent writes SQL fix scripts statement by statement, each statement goes through an EXPLAIN PLAN gate that parses it against the live dev schema with nothing executed; when it parses OK the script moves on to human review with every identifier real, and on errors like ORA-00942 table or view does not exist or ORA-00904 invalid identifier the error goes back to the model, which retries; a side effect is that the execution plan is already available via DBMS_XPLAN.DISPLAY" width="800" height="409"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The errors go straight back to the model with the original task. Most name-level failures disappear in one retry, without me reading anything. What reaches me is a script whose every identifier is real.&lt;/p&gt;

&lt;p&gt;The tool the agents call runs in a read-only safe mode: &lt;code&gt;SELECT&lt;/code&gt;, &lt;code&gt;DESCRIBE&lt;/code&gt; and &lt;code&gt;EXPLAIN PLAN&lt;/code&gt; are allowed, everything else (DDL, DML, GRANT, COMMIT) is blocked before it reaches the database. So even a badly confused agent can't turn the lint step into a write.&lt;/p&gt;

&lt;h2&gt;
  
  
  The free bonus: you also get the plan
&lt;/h2&gt;

&lt;p&gt;The check produces an execution plan as a side effect:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;DBMS_XPLAN&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DISPLAY&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="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%2Fyeqp2xjyhchmk97sacif.png" class="article-body-image-wrapper"&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%2Fyeqp2xjyhchmk97sacif.png" alt="Terminal output of EXPLAIN PLAN FOR an UPDATE on the policy table followed by SELECT FROM TABLE of DBMS_XPLAN.DISPLAY: the plan shows TABLE ACCESS FULL on POLICY with 41 million rows highlighted, because there is no index on REGION_ID, and nothing was executed" width="800" height="355"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;So the same gate that catches invented columns also shows you the full table scan on a 41-million-row table before anything runs. Two problems, one statement.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it does not catch
&lt;/h2&gt;

&lt;p&gt;A green lint is not a correct script. Here is what this gate misses:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Valid names, wrong logic.&lt;/strong&gt; &lt;code&gt;DELETE FROM policy WHERE status_cd = 'A'&lt;/code&gt; parses perfectly and can still delete the wrong rows. This gate replaces nothing at the review level.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PL/SQL blocks.&lt;/strong&gt; &lt;code&gt;EXPLAIN PLAN&lt;/code&gt; takes single SQL statements. For packages and procedures I compile against a scratch schema instead; that is a different gate.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Data assumptions.&lt;/strong&gt; The parser checks that a column exists, not that &lt;code&gt;'A'&lt;/code&gt; is a value that ever appears in it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Schema drift.&lt;/strong&gt; I parse against the dev schema. A customer installation two versions behind can still disagree. Closest schema wins, not a guarantee.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The same idea works outside Oracle: PostgreSQL parses and plans with &lt;code&gt;PREPARE&lt;/code&gt; or plain &lt;code&gt;EXPLAIN&lt;/code&gt;, SQL Server has &lt;code&gt;SET PARSEONLY ON&lt;/code&gt;. Any database that can plan a statement without running it can lint one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrapping up
&lt;/h2&gt;

&lt;p&gt;LLM SQL fails most often at the name level, and name resolution is exactly what the database parser already does. &lt;code&gt;EXPLAIN PLAN&lt;/code&gt; turns that parser into a lint step: every generated statement checked against the live schema in milliseconds, with nothing executed and an error message the model can act on. Of all the guardrails around my agents, this one has the best ratio of effort to failures caught. It was one afternoon of work.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>sql</category>
      <category>oracle</category>
      <category>database</category>
    </item>
    <item>
      <title>I Review My AI Assistant's Logs Every Week, With an Agent. Here's the Process.</title>
      <dc:creator>Nunc</dc:creator>
      <pubDate>Mon, 03 Aug 2026 13:43:58 +0000</pubDate>
      <link>https://dev.to/nunc/i-review-my-ai-assistants-logs-every-week-with-an-agent-heres-the-process-409p</link>
      <guid>https://dev.to/nunc/i-review-my-ai-assistants-logs-every-week-with-an-agent-heres-the-process-409p</guid>
      <description>&lt;p&gt;We run an internal AI assistant on top of our 20-year-old helpdesk system. Production since April, 15 active users across five roles, from developers to operators to project managers. The model matters less than people think. What actually improves the assistant week over week is a boring process: a structured weekly review of its logs. This post describes that process, the metrics we track, and two real failures the review caught that I would never have found otherwise.&lt;/p&gt;

&lt;h2&gt;
  
  
  What gets logged
&lt;/h2&gt;

&lt;p&gt;Two levels. A summary log with one line per request:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[2026-07-20 16:46:52] user=xxx timing=99926ms status=ok
  model=... turns=13 tools=12 cost=$0.25
  profile=xxx ftok=1820ms query="have we ever solved..."
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And a full JSONL transcript per session: every user turn, every answer, plus which context was injected and where it came from. The provenance part was added later, after a review showed we couldn't tell why the assistant said something. If you're building an assistant, log answer provenance from day one.&lt;/p&gt;

&lt;h2&gt;
  
  
  The weekly review is run by an agent
&lt;/h2&gt;

&lt;p&gt;Reading a week of transcripts by hand is not sustainable, so the review itself is an agent task. I say "do the review", and the agent figures out which period is not yet covered, pulls the logs from the server, and works through every session. Not a sample. All of them.&lt;/p&gt;

&lt;p&gt;The review has three outputs:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Session quality review.&lt;/strong&gt; The agent grades answers, and the important part: it re-verifies factual claims against the actual database. If the assistant told a user "this fix was installed at customer X in May", the review checks whether that's true.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Improvement proposals.&lt;/strong&gt; Concrete and prioritized, each with the problem, evidence (session IDs, quotes), a proposed fix, and an effort estimate. Each proposal enters a backlog with a status: proposed, approved, implemented, rejected, or watch.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;User profiles.&lt;/strong&gt; Usage patterns per user feed personalization (more below).&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="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%2F1tx4pr1q64ekn3u3vd7v.png" class="article-body-image-wrapper"&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%2F1tx4pr1q64ekn3u3vd7v.png" alt="Diagram of the weekly review loop: a summary log with one line per request and JSONL transcripts with every turn and injected context both feed a weekly review agent that works through every session rather than a sample; the agent produces three outputs, session quality grades with claims re-checked against the database, improvement proposals with problem and evidence and fix and effort, and user profiles that go into the assistant's prompt; proposals land in an improvement backlog with statuses proposed, approved, implemented, rejected and watch, and an orange arrow loops from the backlog back to the review agent, where the next review verifies every implemented fix" width="800" height="473"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The next review then verifies the previous round: did the implemented fixes actually stop the failure they targeted? Several times the answer was "partially", and the item went back into the backlog. Without that verification step, an improvement backlog turns into a feel-good list.&lt;/p&gt;

&lt;h2&gt;
  
  
  The scorecard
&lt;/h2&gt;

&lt;p&gt;Every review adds one row to a long-running scorecard. Same metrics, same methodology, so trends are visible: requests, users, sessions, error rate, median and p95 response time, corrections, factual errors, false negatives, security events, user feedback, average session grade.&lt;/p&gt;

&lt;p&gt;A recent week looked like this: 137 requests, 12 users, 44 sessions, 0 errors, median response 99 seconds, p95 262 seconds, average session grade 4.5 out of 5 at 100% review coverage.&lt;/p&gt;

&lt;p&gt;&lt;a href="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%2F2qxvjze5cyegxpdxzeg4.png" class="article-body-image-wrapper"&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%2F2qxvjze5cyegxpdxzeg4.png" alt="One week on the scorecard, shown as eight metric tiles: 137 requests, 12 active users, 44 sessions, 0 errors, 99 seconds median response, 262 seconds p95 response, average session grade 4.5 out of 5, and 100 percent review coverage" width="800" height="373"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Two practical lessons from maintaining it. First, write down the counting methodology, because "how many sessions" turns out to have edge cases (we now count a session as a transcript file with at least one real user turn, feedback-only files excluded). Second, mark methodology changes in the scorecard itself. Otherwise a metric jump reads as regression when it's actually deeper measurement.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two failures the review caught
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The leaking limit banner.&lt;/strong&gt; One afternoon our primary auth token kept hitting a rate limit, and a fallback took over. The failover worked, but the review found that at least 12 answers shown to 4 users had an English "You've hit your limit" banner and fragments of the failed first attempt glued to the top of the final answer. Users saw it and said nothing. Nobody reports weirdness in a tool they don't fully trust yet, which is exactly why you read the logs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The adopted false premise.&lt;/strong&gt; A user asked about a project, and their question contained a wrong assumption about which customer group a code referred to. The assistant took the premise at face value and confidently attributed everything to the wrong company. The transcript grade was fine (the user was happy!), but the facts were wrong. The fix was a prompt rule: verify the entity behind a code against the database before building an answer on it, even when the user asserts it. You will not catch this class of failure from user feedback, because the user is the source of the error.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two kinds of user profiles
&lt;/h2&gt;

&lt;p&gt;Each user has a profile the assistant loads into its system prompt: role, which projects they may see, typical questions, preferred answer depth. A developer asking about a package gets code references. A project manager asking the same thing gets a business summary. An operator asking about an error code gets the past-ticket solution plus a suggestion for which developer to assign.&lt;/p&gt;

&lt;p&gt;The part that took iteration: we keep two separate profile sets. Analytical profiles are our internal understanding of each user, built from the log reviews. Agent profiles are the distilled version the assistant actually loads. The analytical profile is the raw material, the agent profile is the product. Mixing them was a mistake we made first: internal observations about a user do not belong in a prompt.&lt;/p&gt;

&lt;p&gt;One rule I'd flag for anyone doing this: personalization must stay a default, not a cage. A non-technical user who asks a technical question gets the technical answer. And log review being about improving the assistant, not evaluating employees, is something you say out loud and write down before someone discovers their conversations are being read.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrapping up
&lt;/h2&gt;

&lt;p&gt;An AI assistant in production is a product, and it needs the product loop: instrumentation, periodic review, prioritized backlog, verification that fixes worked. Doing the review with an agent makes 100% coverage affordable. And the highest-value finds are the ones no user will ever report: answers that were wrong while everyone was satisfied.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>agents</category>
      <category>llm</category>
      <category>observability</category>
    </item>
    <item>
      <title>108 Support Tickets Later: My AI Agent Workflow on a 2.3M-Line Oracle Legacy System</title>
      <dc:creator>Nunc</dc:creator>
      <pubDate>Thu, 30 Jul 2026 12:14:10 +0000</pubDate>
      <link>https://dev.to/nunc/108-support-tickets-later-my-ai-agent-workflow-on-a-23m-line-oracle-legacy-system-d9e</link>
      <guid>https://dev.to/nunc/108-support-tickets-later-my-ai-agent-workflow-on-a-23m-line-oracle-legacy-system-d9e</guid>
      <description>&lt;p&gt;My day job is maintaining a 20-year-old insurance system: over 1,000 PL/SQL packages, 2.3 million lines of code, Oracle Forms on top, and the same codebase running at more than 20 customer installations. Nine months ago I started routing my support tickets through AI agents in Claude Code. 108 tickets later, almost nothing I do is manual anymore. This post describes the workflow, the parts that keep it safe, and the loop that makes it better with every ticket.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this works on a legacy system
&lt;/h2&gt;

&lt;p&gt;People assume AI tooling helps most on greenfield projects. My experience is the opposite. The pain in a legacy system is not writing code, it's finding context:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Nobody knows the whole codebase. At 2.3 million lines, nobody can.&lt;/li&gt;
&lt;li&gt;The same code behaves differently per customer, controlled by hundreds of installation parameters. Half of my questions are "why does this work differently at customer X".&lt;/li&gt;
&lt;li&gt;Our helpdesk has about a million tickets from 20+ years. The answer to "have we solved this before?" is almost always yes. The problem is that it lives in a ticket from 2017, or in one paragraph of a manual nobody opened in three years.&lt;/li&gt;
&lt;li&gt;Knowledge is spread across five systems: the helpdesk, user manuals on a file share, a wiki, GitLab, and SVN.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Collecting that context by hand takes a senior developer half a day. An agent with the right tools does it in minutes. That's the whole trade.&lt;/p&gt;

&lt;h2&gt;
  
  
  The workflow: one command per ticket
&lt;/h2&gt;

&lt;p&gt;When a new ticket arrives, I run a single analyze command. The agent then:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Reads the ticket through the helpdesk's SOAP API, including attachments. PDFs, Word and Excel files get converted to text first.&lt;/li&gt;
&lt;li&gt;Searches for similar past tickets. Keyword search plus vector search, because the built-in helpdesk search only does keywords and misses anything phrased differently.&lt;/li&gt;
&lt;li&gt;Checks my library of ready-made SQL scripts for anything reusable.&lt;/li&gt;
&lt;li&gt;Looks at the relevant code in the database, and at its change history in GitLab or SVN if needed.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="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%2F6yptiwepjr4irzu45n9l.png" class="article-body-image-wrapper"&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%2F6yptiwepjr4irzu45n9l.png" alt="Workflow diagram: a new ticket goes to an analyze agent that reads four sources (helpdesk API with attachments, similar past tickets via keyword and vector search, a library of 759 SQL templates, source code with change history), produces ANALIZA.md with findings and a proposed next step, which branches into a code fix, a customer reply, or more diagnostics; everything gets attached back to the ticket, and an orange feedback arrow loops from the ticket archive back into the search sources" width="799" height="445"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The output is a single markdown file, &lt;code&gt;ANALIZA.md&lt;/code&gt;: all findings in one place, plus a proposed next step. The next step is always one of three things: a code fix, a reply to the customer, or more diagnostics on the customer's database.&lt;/p&gt;

&lt;p&gt;Once there is enough data, the agent prepares the whole package: the proposed solution, a message for the customer, internal notes for colleagues involved in the ticket, the diagnostic and fix scripts, and a short write-up of why we did it this way. All of it gets attached to the ticket.&lt;/p&gt;

&lt;h2&gt;
  
  
  Skills are where the actual work went
&lt;/h2&gt;

&lt;p&gt;The agent is only as good as the context you can hand it. Over nine months I built 24 skills. Grouped by function:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Group&lt;/th&gt;
&lt;th&gt;Examples&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Context access&lt;/td&gt;
&lt;td&gt;helpdesk API, vector search over tickets and 1,800 manuals, Oracle dev DB, GitLab fix archive (31,000+ commits), SVN for Forms, internal wiki&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Domain knowledge&lt;/td&gt;
&lt;td&gt;system architecture and its 13 business domains, insurance accounting mechanics, installation parameters, internal PL/SQL standards, our logging package&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Writing and delivery&lt;/td&gt;
&lt;td&gt;generating PL/SQL to our standards, packaging a fix, installing it on the dev database, recompiling invalid objects, compiling Forms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Meta&lt;/td&gt;
&lt;td&gt;a skill that builds new skills from repeating patterns&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The meta one matters more than it sounds. Whenever I catch myself explaining the same thing to the agent twice, that explanation becomes a skill. The toolset grows as a side effect of normal work.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two rules that keep it safe
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Every generated script gets validated against the live schema.&lt;/strong&gt; The most common LLM failure in SQL is inventing a table or column that almost exists. The fix is cheap: run &lt;code&gt;EXPLAIN PLAN FOR &amp;lt;statement&amp;gt;&lt;/code&gt; against the dev database before the script leaves my hands. It parses the statement against the real data dictionary without executing anything, so it's safe even for UPDATE and DELETE. Invented objects fail immediately with ORA-00942 or ORA-00904.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Risky changes get a second engineer.&lt;/strong&gt; For hard-to-reverse changes I hand the same task to a second, independent agent and compare findings. I use two: a different vendor's model for a genuinely independent view (different model, different blind spots), and a fresh session of the same model loaded with all our domain skills for domain-heavy reviews. What both find is almost certainly real. What only one finds, I check myself. On a small team, this is the closest thing to peer review I can get.&lt;/p&gt;

&lt;h2&gt;
  
  
  The loop that pays for everything
&lt;/h2&gt;

&lt;p&gt;Here is the part I'd steal if I were reading this post.&lt;/p&gt;

&lt;p&gt;Everything the agent produces gets attached back to the ticket: scripts, analysis, the reasoning behind the fix. When a similar ticket arrives next year, the vector search finds that old ticket, and the agent reads not just what we did, but why. Then a separate closing step extracts anything reusable into a script library. That library is now at 759 SQL templates in 32 topic folders, built up over years and finally searchable by an agent instead of just by me.&lt;/p&gt;

&lt;p&gt;The result: every solved ticket makes the next one cheaper. No extra effort, no "knowledge management initiative". It happens as a byproduct of closing the ticket.&lt;/p&gt;

&lt;h2&gt;
  
  
  What still doesn't work
&lt;/h2&gt;

&lt;p&gt;Honesty section. Three things are open:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Testing is still manual.&lt;/strong&gt; The agent speeds up writing a fix, not proving that the fix breaks nothing at the other 20 installations. That's still the biggest remaining risk.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Metrics are thin.&lt;/strong&gt; I have raw numbers (108 tickets, roughly 4,000 prompts, 94% of them on tickets) but no systematic before/after measurement of resolution time. I know it's faster. I can't prove by how much yet.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bus factor of one.&lt;/strong&gt; The whole stack is built and configured on my machine. A colleague can't pick it up without a setup guide that doesn't exist yet.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Wrapping up
&lt;/h2&gt;

&lt;p&gt;The key insight: on a large legacy system, the win from AI agents is not code generation. It's that context gathering, the half-day of digging through five systems before you write a single line, collapses to minutes. And if you route the results back into the ticket archive, the system compounds: every solved ticket becomes training material for the next one.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>oracle</category>
      <category>productivity</category>
      <category>agents</category>
    </item>
    <item>
      <title>Should I Prompt Claude in English? I Analyzed 2,300 of My Own Prompts to Find Out</title>
      <dc:creator>Nunc</dc:creator>
      <pubDate>Thu, 23 Jul 2026 14:35:47 +0000</pubDate>
      <link>https://dev.to/nunc/should-i-prompt-claude-in-english-i-analyzed-2300-of-my-own-prompts-to-find-out-2b2d</link>
      <guid>https://dev.to/nunc/should-i-prompt-claude-in-english-i-analyzed-2300-of-my-own-prompts-to-find-out-2b2d</guid>
      <description>&lt;p&gt;I'm a Slovenian developer and I talk to Claude Code in Slovenian. About 2.5 million people speak my language. Every prompt engineering guide is written in English, and the usual advice is: prompt in English, the models are simply better at it. I wanted to know if that's actually true for daily coding work, so I measured it. Here's what I found.&lt;/p&gt;

&lt;h2&gt;
  
  
  How I tested it
&lt;/h2&gt;

&lt;p&gt;Three parts:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;My real usage.&lt;/strong&gt; I parsed all my Claude Code session transcripts from two environments (WSL for personal projects, Windows PowerShell for my day job): 2,319 prompts by my first count, 1,899 after the strictest filtering pass that drops pasted logs, tool output and machine-generated turns.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Published research.&lt;/strong&gt; Multilingual benchmarks, interpretability studies, tokenizer papers, official vendor docs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;My own token measurements.&lt;/strong&gt; Real prompts of mine, translated to English, counted with different tokenizers.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Then one more thing: I gave the exact same research task to OpenAI Codex (GPT-5.6), isolated from my results, and compared what the two systems concluded. More on that below.&lt;/p&gt;

&lt;h2&gt;
  
  
  What my usage actually looks like
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;WSL&lt;/th&gt;
&lt;th&gt;Windows&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Real prompts (strict filter)&lt;/td&gt;
&lt;td&gt;1,362&lt;/td&gt;
&lt;td&gt;537&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Slovenian, where language is determinable&lt;/td&gt;
&lt;td&gt;98.7%&lt;/td&gt;
&lt;td&gt;99.4%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Median prompt length&lt;/td&gt;
&lt;td&gt;54 chars&lt;/td&gt;
&lt;td&gt;90 chars&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;So I don't "mostly" prompt in Slovenian. I prompt in Slovenian, full stop. Fast, with typos, often without diacritics (š, č, ž), with English technical terms mixed in wherever they're natural. The question is whether any of that hurts.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the research says about quality
&lt;/h2&gt;

&lt;p&gt;Short version: it hurts less than I expected, if your model is big enough.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Models internally lean on English anyway.&lt;/strong&gt; Interpretability work from EPFL (&lt;a href="https://arxiv.org/abs/2402.10588" rel="noopener noreferrer"&gt;Do Llamas Work in English?&lt;/a&gt;) and Anthropic's own circuit-tracing study (&lt;a href="https://transformer-circuits.pub/2025/attribution-graphs/biology.html" rel="noopener noreferrer"&gt;On the Biology of a Large Language Model&lt;/a&gt;) points the same way: large models process meaning in a shared concept space that sits closest to English. Non-English input adds a thin translation layer at the edges. The thinking in the middle is mostly language-neutral. This also means instructions like "think in English, respond in Slovenian" are mostly wasted words on modern models.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The gap was real, and it has mostly closed at the frontier.&lt;/strong&gt; In 2022, PaLM solved 62% of math word problems in English but only 47% averaged across underrepresented languages (&lt;a href="https://arxiv.org/abs/2210.03057" rel="noopener noreferrer"&gt;MGSM&lt;/a&gt;). Today, Anthropic's official numbers for benchmarked languages show Spanish at 98.2% of English performance and German at 97.0% (&lt;a href="https://platform.claude.com/docs/en/docs/build-with-claude/multilingual-support" rel="noopener noreferrer"&gt;multilingual support docs&lt;/a&gt;). No vendor benchmarks Slovenian, but a Slovenian pragmatics benchmark (&lt;a href="https://arxiv.org/html/2510.21575" rel="noopener noreferrer"&gt;SloPragEval&lt;/a&gt;) measured GPT-5 at 0.81 in Slovenian vs 0.83 in English. Two points. Not nothing, but not a reason to switch languages for daily work.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Small models are a different story.&lt;/strong&gt; The same Anthropic table shows Claude Opus 4.1 keeping 80% of its English performance in Yoruba, while the much smaller Haiku 4.5 keeps only 53%. If a small cheap model handles your non-English workload, test it in your language before trusting it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Mixing languages is fine. In my direction, it even helps.&lt;/strong&gt; A 2025 study (&lt;a href="https://arxiv.org/abs/2506.14012" rel="noopener noreferrer"&gt;Lost in the Mix&lt;/a&gt;) found an asymmetry: foreign words inserted into English text hurt comprehension, but English words inserted into non-English text often improve it, by up to 13 points on some models. Slovenian sentences with English technical terms left as-is turn out to be the good kind of mixing.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Slovenian costs in tokens
&lt;/h2&gt;

&lt;p&gt;The literature says non-English text is expensive: Slovenian pays about 1.88x the tokens of English on GPT-4's old cl100k tokenizer (&lt;a href="https://arxiv.org/abs/2305.15425" rel="noopener noreferrer"&gt;Petrov et al., NeurIPS 2023&lt;/a&gt;). That number worried me. Then I measured my own prompts: 30 real ones, each translated to English faithfully.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tokenizer&lt;/th&gt;
&lt;th&gt;Slovenian premium on my real prompts&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;cl100k_base (GPT-4 era)&lt;/td&gt;
&lt;td&gt;+34%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;o200k_base (GPT-4o / GPT-5)&lt;/td&gt;
&lt;td&gt;+16%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Why so much lower than the paper's numbers? Because real developer prompts aren't clean prose. They're full of file paths, SQL, error messages and English tech terms that tokenize identically in both languages.&lt;/p&gt;

&lt;p&gt;Two results I didn't expect:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Diacritics are free.&lt;/strong&gt; The same text with and without š/č/ž differs by 0.6% in tokens. My lazy typing habit costs nothing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The cost that matters is on the output side.&lt;/strong&gt; Output tokens are about 5x the price of input tokens and they dominate latency. A short Slovenian question costs nothing worth mentioning. A long Slovenian document generated by the model costs 16-34% more time and money than the same document in English.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One honest caveat: Claude's tokenizer is not public, so these are OpenAI tokenizer numbers (plus Gemini's count API, which agreed). Published cross-vendor comparisons suggest Claude's non-English premium is somewhat higher.&lt;/p&gt;

&lt;h2&gt;
  
  
  The twist: I had Codex repeat the whole study
&lt;/h2&gt;

&lt;p&gt;To check my own work, I gave the identical research brief to OpenAI Codex (GPT-5.6): same task wording, same environment facts, an isolated working directory, and no access to my report. It even had to exclude the transcript of the session that ordered the research, so it couldn't peek at my conclusions.&lt;/p&gt;

&lt;p&gt;It came back with the same core answer: keep prompting in Slovenian, keep technical identifiers verbatim, write reusable instructions in English with native trigger phrases, don't translate domain knowledge. Two systems independently converging on the same conclusions is worth more than either report alone.&lt;/p&gt;

&lt;p&gt;And Codex found one thing I had missed. It manually reviewed 50 of my prompts and rated 90% of them as clearly understandable despite all the typos. The weak ones weren't weak because they were Slovenian. They were weak because they were vague: "fix this", "now do it properly", no success criteria, no constraints. Its conclusion, which I'm keeping: &lt;strong&gt;the biggest improvement isn't the language, it's explicitness.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What I changed, and what I kept
&lt;/h2&gt;

&lt;p&gt;Kept:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Slovenian for interactive work.&lt;/strong&gt; I think faster and state intent more precisely in my own language.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;English technical terms inside Slovenian sentences.&lt;/strong&gt; The research says this direction of mixing is harmless to helpful.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Everything technical verbatim.&lt;/strong&gt; Package names, SQL, paths, error messages, config keys. Never translated, never paraphrased.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hybrid skills.&lt;/strong&gt; Reusable agent instructions in English, trigger phrases in the description in Slovenian (the words I actually type), business domain knowledge in Slovenian because the terminology lives in Slovenian.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Changed:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;An explicit language rule in my global config, because Claude Code is known to drift back to English after context compaction:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;   Always respond in Slovenian unless explicitly asked otherwise.
   Keep code, identifiers, commands, paths and error messages
   in their original form.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;&lt;p&gt;A five-field template for bigger tasks: goal, context, constraints, verification, output format. This fixes more than any language switch would.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;English selectively: long structured specs and agent briefs, or an A/B retry when a hard reasoning task gives a shaky answer.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Wrapping up
&lt;/h2&gt;

&lt;p&gt;For frontier models, the language you prompt in matters far less than how explicit your prompt is. Prompt in the language you think fastest in, keep everything technical verbatim, write reusable instructions in English with trigger phrases in your own language, and spend the effort you save on clear goals and constraints instead of translation.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>claude</category>
      <category>productivity</category>
    </item>
  </channel>
</rss>
