<?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: Taranity</title>
    <description>The latest articles on DEV Community by Taranity (@voyagi).</description>
    <link>https://dev.to/voyagi</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%2F4101757%2F86d813a3-4dfa-4d5c-ab6e-6acc7d593dfc.png</url>
      <title>DEV Community: Taranity</title>
      <link>https://dev.to/voyagi</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/voyagi"/>
    <language>en</language>
    <item>
      <title>Agents for Humans: an agent that refuses to guess what your letter says</title>
      <dc:creator>Taranity</dc:creator>
      <pubDate>Mon, 14 Sep 2026 17:09:18 +0000</pubDate>
      <link>https://dev.to/voyagi/agents-for-humans-an-agent-that-refuses-to-guess-what-your-letter-says-2f98</link>
      <guid>https://dev.to/voyagi/agents-for-humans-an-agent-that-refuses-to-guess-what-your-letter-says-2f98</guid>
      <description>&lt;p&gt;Every country sends people letters they can't read. Many answer it the same way: a free help desk, usually in a library, where a volunteer sits down with you and the envelope. The Netherlands runs 861 of them. I live here, these letters come to me too, and I don't read Dutch. The first ten minutes at that desk decide everything: what is this, how bad is it, what happens if I ignore it, what do I do, and by when.&lt;/p&gt;

&lt;p&gt;Plainletter is my entry for the Good Neighbor track. It's the agent behind that desk.&lt;/p&gt;

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

&lt;p&gt;A volunteer photographs or uploads the letter. Plainletter names the sender and the kind of letter, pulls out every amount, reference number and deadline, and marks each one on the letter with a numeral in the margin. It explains the letter side by side in Dutch and in the visitor's own language, counts the days left, lays out the next steps with official routes, drafts the reply when that's the right move, and prints a desk card with a calendar reminder.&lt;/p&gt;

&lt;h2&gt;
  
  
  One rule: nothing that isn't in the letter
&lt;/h2&gt;

&lt;p&gt;Five stages are a model speaking. Everything between them is plain Python, including the verifier that decides whether a reading may reach a person. For every date, amount and reference, it checks that the cited passage really stands in the letter and that the value follows from it.&lt;/p&gt;

&lt;p&gt;Strands Agents made the enforcement cheap to build. Every structured answer is a tool call, so I put an intervention on that boundary. A date the verifier didn't ground is refused before it exists, and the refusal goes back to the model as the tool's result, so it writes again. Official routes come from a knowledge base through a tool, and each entry carries the page it was read on. A photographed letter is transcribed by one turn and read by a separate one, which means an invented fact would also have to appear in a transcript written without it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What running it for real taught me
&lt;/h2&gt;

&lt;p&gt;The first live run on a photographed page did the thing I built it for, and I only believed it once I watched it happen. A line was clipped by the fold. The transcription turn wrote an unreadable marker instead of guessing. The extractor quietly filled the gap in anyway, and the verifier sided with the transcript, refused the fact, and handed the letter to a person.&lt;/p&gt;

&lt;p&gt;Then I folded a health insurer letter, put it in shadow and photographed it in a hurry. Four facts were refused. One was EUR 149,38 where the page says EUR 149,35. Nobody at a desk would catch that by eye.&lt;/p&gt;

&lt;p&gt;It isn't finished. On a letter where part of the reference number can't be read, the desk asks the visitor for it, but the payment step still quotes the number with its gap. A volunteer catches that at the counter today. Holding that step until the number is confirmed is next on my list.&lt;/p&gt;

&lt;p&gt;The guard only knew Dutch month names at first, so a wrong date written in Ukrainian would have passed unread. It reads five languages now.&lt;/p&gt;

&lt;p&gt;My first deployed run put all sixteen lines of a letter into the CloudWatch traces: name, street, reference, amounts. That was fixed and re-checked, and the prompts in the traces read REDACTED.&lt;/p&gt;

&lt;p&gt;Money was the last surprise. One reading is six Bedrock calls and costs about USD 0.107. The daily ceiling I had first set, 200 readings, meant one bad day could cost more than my whole monthly budget alarm of 20 dollars. It's 50 now.&lt;/p&gt;

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

&lt;p&gt;Strands Agents SDK on Amazon Bedrock with EU inference, AgentCore Runtime and Memory in Frankfurt, and a Next.js console on Vercel. Memory only happens with the visitor's consent, and then only checked, masked values for thirty days.&lt;/p&gt;

&lt;p&gt;A second country would be one locale file, a knowledge base for its senders, and sample letters to prove it. A test fails if a Dutch word leaks outside that boundary.&lt;/p&gt;

&lt;p&gt;Try the desk: &lt;a href="https://plainletter-web.vercel.app/desk" rel="noopener noreferrer"&gt;https://plainletter-web.vercel.app/desk&lt;/a&gt;&lt;br&gt;
Code, with two real readings you can open without installing anything: &lt;a href="https://github.com/voyagi/plainletter" rel="noopener noreferrer"&gt;https://github.com/voyagi/plainletter&lt;/a&gt;&lt;br&gt;
Demo video: &lt;a href="https://youtu.be/sXkQvS_dTos" rel="noopener noreferrer"&gt;https://youtu.be/sXkQvS_dTos&lt;/a&gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  AgentsforHumans
&lt;/h1&gt;

</description>
      <category>aws</category>
      <category>ai</category>
      <category>python</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Ringbolt: an on-call line that has to hear you say it</title>
      <dc:creator>Taranity</dc:creator>
      <pubDate>Mon, 14 Sep 2026 14:44:04 +0000</pubDate>
      <link>https://dev.to/voyagi/ringbolt-an-on-call-line-that-has-to-hear-you-say-it-497c</link>
      <guid>https://dev.to/voyagi/ringbolt-an-on-call-line-that-has-to-hear-you-say-it-497c</guid>
      <description>&lt;h2&gt;
  
  
  The problem
&lt;/h2&gt;

&lt;p&gt;On-call tools can already ring you. PagerDuty's phone alert is a recording that asks you to press 4 to acknowledge and 6 to resolve, and you still have to get up and find a laptop to learn what actually broke.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Ringbolt does
&lt;/h2&gt;

&lt;p&gt;Any monitor posts a plain JSON alert. Policy decides whether it's worth waking somebody, going by the service's severity threshold, its quiet hours and whether the same problem already rang a phone. If it is, Ringbolt places a call through the CALL-E API to whoever is on the rota. On the call you can ask what broke and what else is affected, and then say what to do. The whole exchange lands in one audit record: the transcript, the decision, who authorized it, what ran, and the state before and after.&lt;/p&gt;

&lt;p&gt;It runs on Cloudflare Workers, with one Durable Object per incident holding the state machine and the escalation timer, and D1 for storage.&lt;/p&gt;

&lt;h2&gt;
  
  
  The idea that shaped it
&lt;/h2&gt;

&lt;p&gt;Handing a phone call the power to change production is only reasonable if the refusals are strict, so most of the design is about what does not run. CALL-E delivers its terminal webhook unsigned, so Ringbolt never trusts it and fetches the call again under its own key before acting. The decision has to validate against a schema, clear a confidence floor and name an action the service's policy already allows. For anything destructive, the confirmation phrase has to appear as whole words in a transcript turn from the responder, and "don't roll it back" does not count as "roll it back".&lt;/p&gt;

&lt;p&gt;A call where the responder was never heard is refused outright, whatever the confidence score says. The demo carries an example of exactly that: a call that came back completed, at high confidence, with a valid decision to change production, where not one word from the person who answered was transcribed. It never ran.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I learned
&lt;/h2&gt;

&lt;p&gt;A create that times out isn't a create that didn't happen. On 22 August my build created 23 separate call tasks in half an hour, and each one is billed whether it connects or not. Two rules came out of that. A failed create is retried once under the same idempotency key, because a repeat under a fresh key bills as a second call to the same person. And there's now a hard ceiling of three real calls in ten minutes, because every one of those 23 was a different logical call, so no per-call check could have stopped any of them.&lt;/p&gt;

&lt;p&gt;Transcription is lossy in both directions. The transcript check will sometimes refuse a call someone really did authorize. I took that trade on purpose, because the refusal falls toward calling the next person on the rota and never toward an unauthorized change.&lt;/p&gt;

&lt;p&gt;The last one came from the provider's region table, which lists the Netherlands with English. The call setup I used was still refused, with "Calls to the Netherlands in English are not supported for this call setup". That's why the public demo runs the same loop against a local stand-in of their API, and why one contract suite runs every case against both the stand-in and the real adapter, so the two can't drift apart. There are 555 tests in total.&lt;/p&gt;

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

&lt;p&gt;The hosted demo has a service called dockside that exists to be broken. Press the button and watch the alert, the call, the transcript and the rollback. Nothing you press there can ring a phone.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Demo: &lt;a href="https://ringbolt.taranity.com" rel="noopener noreferrer"&gt;https://ringbolt.taranity.com&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Code: &lt;a href="https://github.com/voyagi/ringbolt" rel="noopener noreferrer"&gt;https://github.com/voyagi/ringbolt&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Video: &lt;a href="https://www.youtube.com/watch?v=EIm87clsks8" rel="noopener noreferrer"&gt;https://www.youtube.com/watch?v=EIm87clsks8&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;The same walkthrough in words: &lt;a href="https://github.com/voyagi/ringbolt/blob/main/examples/demo-transcript.md" rel="noopener noreferrer"&gt;https://github.com/voyagi/ringbolt/blob/main/examples/demo-transcript.md&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Built for the CALL-E hackathon.&lt;/p&gt;

</description>
      <category>automation</category>
      <category>devops</category>
      <category>monitoring</category>
      <category>sre</category>
    </item>
    <item>
      <title>Seven designs, one URL: how I built a site that changes its whole appearance without changing its address</title>
      <dc:creator>Taranity</dc:creator>
      <pubDate>Thu, 10 Sep 2026 15:04:25 +0000</pubDate>
      <link>https://dev.to/voyagi/seven-designs-one-url-how-i-built-a-site-that-changes-its-whole-appearance-without-changing-its-h0l</link>
      <guid>https://dev.to/voyagi/seven-designs-one-url-how-i-built-a-site-that-changes-its-whole-appearance-without-changing-its-h0l</guid>
      <description>&lt;p&gt;Most sites that offer a theme swap a few colour variables. I wanted something harder to fake: seven genuinely different designs of the same content, switchable in place, with the URL never changing and search engines still seeing exactly one canonical page.&lt;/p&gt;

&lt;p&gt;Here is what that took, and the four things I got wrong on the way.&lt;/p&gt;

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

&lt;p&gt;taranity.com is a static Astro build on Cloudflare Pages. It renders seven complete design trees from the same content: seven typographic systems, seven page skeletons, seven motion grammars. A visitor picks one, a cookie remembers it, and a Pages Function at the edge serves that design's prebuilt HTML at the same canonical URL. Crawlers get the default. There is exactly one URL per piece of content, and the variant trees carry noindex.&lt;/p&gt;

&lt;p&gt;Seventy pages, all prerendered. No server rendering, no per-request work beyond reading a cookie and choosing a file.&lt;/p&gt;

&lt;h2&gt;
  
  
  The one idea that shaped it
&lt;/h2&gt;

&lt;p&gt;Do the branching at the edge, not in the browser.&lt;/p&gt;

&lt;p&gt;The obvious approach is to ship every design's CSS and swap a class on the root element. That means every visitor downloads seven designs to use one, and the flash before hydration is unavoidable. Instead each design is a separate prebuilt tree, and a tiny middleware picks the tree. The visitor downloads one design. The address bar never moves. And because the swap happens before any HTML reaches the browser, there is nothing to flash.&lt;/p&gt;

&lt;p&gt;The cost is honest and worth naming: the response now varies by cookie, so the caching has to say so. Every response carries Vary: Cookie, and the variant responses are marked private. Getting that wrong would mean serving one visitor's design to another.&lt;/p&gt;

&lt;h2&gt;
  
  
  Four things I got wrong
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;My type checking had never actually run.&lt;/strong&gt; It passed on my machine for months. It could not pass anywhere else. The command ran the type checker before the step that generates the content types, and that generated folder is gitignored, so my machine had a stale copy and a fresh clone had nothing. The first time CI ran on a clean checkout, it went red immediately. If a gate reads something git does not carry, it has not been tested until CI runs it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A test key does not remove a third party from your tests.&lt;/strong&gt; My end to end tests use Cloudflare's published dummy Turnstile key, the one documented as always passing. One morning the contact suite dropped from 75 of 75 to 42 of 75 with no code change. The dummy key removes the scoring, not the browser integrity handshake, so the suite was still hostage to Cloudflare's opinion of my machine. Thirty three failures, and not one of them was about my site.&lt;/p&gt;

&lt;p&gt;The fix was structural rather than clever: an explicit declaration that the live challenge is unavailable, which makes the form tests run against a stand in value while the widget checks report honestly. Without the declaration a refusal still fails. A banner says in plain words that the widget was not exercised, so a green run can never quietly mean the bot check is broken.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A cleanup block is not a guard.&lt;/strong&gt; That declaration is passed as a small file, removed when the run finishes. A review caught what I had missed: a run interrupted before the cleanup leaves the file behind, and the next run reads it and opts itself out silently. I had written a comment describing that exact hazard and then guarded only the exit path. Interrupts skip the exit path. It now clears the file on the way in too.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Every dependency advisory had a smaller fix than I reached for.&lt;/strong&gt; Four advisories, including one rated critical. In every single case the parent package's own declared version range already permitted the patched release, and only the lockfile was pinning something older. No overrides, no major upgrades, one lockfile change each. Worth checking before proposing anything with blast radius.&lt;/p&gt;

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

&lt;p&gt;411 tests across 29 files. Zero dependency advisories at any severity. A strict content security policy with exactly one pinned inline script hash, which survived four dependency upgrades because I re-measured it against each one. An accessibility audit that drives a real browser across every page type in every design in both colour modes: 50 checks, zero violations, with contrast measured from painted pixels rather than computed styles, because a decorative layer over a control will fool a computed style.&lt;/p&gt;

&lt;p&gt;The accessibility statement is the part I would point at. It says what is tested, and then it says what is not: no independent audit, no screen reader testing, and that the audit runs on my machine rather than automatically. That last line is there because it used to claim otherwise, and that was not true.&lt;/p&gt;

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

</description>
      <category>architecture</category>
      <category>frontend</category>
      <category>webdev</category>
    </item>
    <item>
      <title>bumpwarden: a dependency-triage agent that never merges</title>
      <dc:creator>Taranity</dc:creator>
      <pubDate>Sun, 30 Aug 2026 20:04:36 +0000</pubDate>
      <link>https://dev.to/voyagi/bumpwarden-a-dependency-triage-agent-that-never-merges-2ci7</link>
      <guid>https://dev.to/voyagi/bumpwarden-a-dependency-triage-agent-that-never-merges-2ci7</guid>
      <description>&lt;p&gt;Every neglected repository has the same pile: seven Dependabot pull requests, each one a question nobody has time to answer. Which of these breaks the build? That question is the actual work. The pull requests are just its packaging.&lt;/p&gt;

&lt;p&gt;I built bumpwarden as my entry to Google's All Things Agentic hackathon, and this writeup is part of that entry. It is a background agent that does the triage itself.&lt;/p&gt;

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

&lt;p&gt;Twice a day, Cloud Scheduler wakes a Cloud Run service. It reads the manifest and lockfile from GitHub, resolves candidate versions from the npm registry and deps.dev, and scores every pending bump from 0 to 100 against a fixed rubric: nine factors, published weights. Then it acts. A clear bump becomes a pull request that edits package.json and nothing else. A caution bump becomes an issue. A risky one becomes a hold issue with a migration plan. Run it again tomorrow and it updates the same items in place instead of opening more. There is no merge action anywhere in its policy. A person merges, or nobody does.&lt;/p&gt;

&lt;h2&gt;
  
  
  Arithmetic decides, the model explains
&lt;/h2&gt;

&lt;p&gt;That one rule shaped the whole design. The score comes from checkable facts: semver distance, release age, advisories, deprecation, breaking markers in the release notes, engine and peer ranges, and whether this repository's own code calls a symbol the release evidence says changed. Gemini (gemini-3.5-flash, through an ADK agent) writes the brief: what changed upstream, what breaks in this codebase down to file and line, how to migrate, and how confident it is. Every claim is checked against the material the model was handed, and a claim that cannot be traced is dropped rather than shown. The model cannot move the score and has no write tools.&lt;/p&gt;

&lt;h2&gt;
  
  
  What building it taught me
&lt;/h2&gt;

&lt;p&gt;The free tier is a design constraint, and a good one. Five model requests a minute, twenty a day, and a brief costs two. So briefs go out two at a time, riskiest first, paced against a sliding minute, and cached by bump and rubric version. A repeat run costs zero model requests, which also means a judge pressing the button twice gets an idempotent demo instead of a quota error.&lt;/p&gt;

&lt;p&gt;/healthz is not yours on Cloud Run. Google's frontend reserves the path and answers before your app ever sees the request. Every test passes locally, and the deployed route is dead anyway. I renamed mine to /health.&lt;/p&gt;

&lt;p&gt;Do not read npm packuments whole. @prisma/client's weighs 68 MB. Reading the two small per-version documents instead took a full run from 197 MB of registry traffic down to 30.&lt;/p&gt;

&lt;p&gt;GitHub drops labels silently when the token cannot push. No error, just an issue created without its labels. And if your idempotency looks issues up by label, every run now duplicates its own work. My test double modeled the documented API, not this quirk, which is exactly why 785 green tests missed it.&lt;/p&gt;

&lt;h2&gt;
  
  
  See it run
&lt;/h2&gt;

&lt;p&gt;The dashboard, the scoring rubric and the audit log are public: &lt;a href="https://bumpwarden-729965149669.europe-west1.run.app" rel="noopener noreferrer"&gt;https://bumpwarden-729965149669.europe-west1.run.app&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The code: &lt;a href="https://github.com/voyagi/bumpwarden" rel="noopener noreferrer"&gt;https://github.com/voyagi/bumpwarden&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The four-minute demo, with the run itself one unbroken take: &lt;a href="https://youtu.be/5WJTfWbNeQ8" rel="noopener noreferrer"&gt;https://youtu.be/5WJTfWbNeQ8&lt;/a&gt;&lt;/p&gt;

</description>
      <category>agents</category>
      <category>automation</category>
      <category>devops</category>
      <category>github</category>
    </item>
  </channel>
</rss>
