<?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: Ruben</title>
    <description>The latest articles on DEV Community by Ruben (@rubenglez).</description>
    <link>https://dev.to/rubenglez</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%2F1316637%2F6ac17b79-4c92-4707-989e-48ea1e243aeb.jpg</url>
      <title>DEV Community: Ruben</title>
      <link>https://dev.to/rubenglez</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/rubenglez"/>
    <language>en</language>
    <item>
      <title>My API Keys Don't Live in .env Files Anymore</title>
      <dc:creator>Ruben</dc:creator>
      <pubDate>Tue, 21 Jul 2026 09:09:27 +0000</pubDate>
      <link>https://dev.to/rubenglez/my-api-keys-dont-live-in-env-files-anymore-52h8</link>
      <guid>https://dev.to/rubenglez/my-api-keys-dont-live-in-env-files-anymore-52h8</guid>
      <description>&lt;p&gt;I work with coding agents almost 100% of my time now. Claude Code, Cursor, the usual suspects. And at some point I realized something uncomfortable: every time I wanted an agent to run a task that needed a real credential, I was either pasting a key into a chat or pointing it at a &lt;code&gt;.env&lt;/code&gt; file sitting in plaintext on my disk. Neither felt right.&lt;/p&gt;

&lt;p&gt;That is how &lt;a href="https://github.com/RubenGlez/lokalite" rel="noopener noreferrer"&gt;Lokalite&lt;/a&gt; started: a local-first secrets manager for macOS, built around how I actually work with agents.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the idea came from
&lt;/h2&gt;

&lt;p&gt;The problem is old, the agents just made it louder.&lt;/p&gt;

&lt;p&gt;Secrets on a developer machine usually live in &lt;code&gt;.env&lt;/code&gt; files. They are convenient and everybody understands them, but they are plaintext, they get copied between projects, they leak into shell history, and nothing records who read them. For years that was an accepted tradeoff because the only thing reading them was you and your own code.&lt;/p&gt;

&lt;p&gt;I looked at the alternatives. &lt;a href="https://1password.com/" rel="noopener noreferrer"&gt;1Password&lt;/a&gt;, &lt;a href="https://www.doppler.com/" rel="noopener noreferrer"&gt;Doppler&lt;/a&gt;, &lt;a href="https://infisical.com/" rel="noopener noreferrer"&gt;Infisical&lt;/a&gt;, they all solve real problems, but most of them are paid, account-based, and cloud-first. For a single dev like me, paying a per-seat price to hand my own credentials to someone else's server felt backwards. I did not want a vendor in the middle of my own API keys. I wanted the opposite: keep everything on my machine, encrypt it properly, and still make it pleasant to use.&lt;/p&gt;

&lt;p&gt;So the first version of the wishlist was simple and very personal:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Keep secrets encrypted at rest, not in plaintext.&lt;/li&gt;
&lt;li&gt;Keep everything local. No account, no cloud, no telemetry.&lt;/li&gt;
&lt;li&gt;Import the &lt;code&gt;.env&lt;/code&gt; files I already had, so adopting it was not a chore.&lt;/li&gt;
&lt;li&gt;Manage all of it from a nice interface, not a config file.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That last point matters more than it sounds. A secrets manager you avoid opening is a secrets manager you stop trusting.&lt;/p&gt;

&lt;p&gt;I started sketching this during paternity leave, in the short windows you get when a newborn is asleep. Leo would go down for a nap, and instead of context-switching into something heavy, I would write a small spec for one piece of this and hand it off. Secrets management turned out to be a good fit for that rhythm: lots of small, well-defined pieces.&lt;/p&gt;

&lt;h2&gt;
  
  
  The build
&lt;/h2&gt;

&lt;p&gt;At the core there is an encrypted vault. Every secret value is encrypted with AES-256-GCM through &lt;a href="https://developer.apple.com/documentation/cryptokit" rel="noopener noreferrer"&gt;CryptoKit&lt;/a&gt; before it touches the disk. The 256-bit vault key is generated on first use and stored in the macOS login keychain, so the key itself never lives inside the vault file. The menu bar app asks for Touch ID before it shows you anything, and it auto-locks after a period of inactivity. If a CLI or MCP request lands while the vault is locked, nothing unlocks silently: the app raises the same Touch ID prompt, naming whoever is asking, and the request fails closed if you decline.&lt;/p&gt;

&lt;p&gt;On top of that there are three ways to reach your secrets, and they all read from the same vault:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A &lt;strong&gt;menu bar app&lt;/strong&gt; for the human moments: search, copy, reveal. Recently copied secrets float to the top, and a global shortcut (default &lt;code&gt;⌘⇧Space&lt;/code&gt;) opens the popover from anywhere.&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;CLI&lt;/strong&gt; for the terminal: add, get, copy, list, import, export, and run.&lt;/li&gt;
&lt;li&gt;An &lt;strong&gt;MCP server&lt;/strong&gt; for the agents, which is the part I will get to in a second.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Importing was non-negotiable, so &lt;code&gt;lokalite import .env&lt;/code&gt; pulls an existing file straight into the vault. From there you organize things into projects, and each project can have multiple environments (dev, staging, production) with their own values. A project can be linked to a directory, which becomes important later.&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%2F46rkop9bh1yc93zfczu6.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%2F46rkop9bh1yc93zfczu6.png" alt="The Lokalite manager window showing a project with its environments, secrets list, and CLI status" width="800" height="502"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The two CLI commands I use the most are these:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Run a single command with secrets injected as env vars, scoped to that process&lt;/span&gt;
lokalite run &lt;span class="nt"&gt;--&lt;/span&gt; npm start
lokalite run &lt;span class="nt"&gt;--keys&lt;/span&gt; OPENAI_API_KEY,ANTHROPIC_API_KEY &lt;span class="nt"&gt;--&lt;/span&gt; claude

&lt;span class="c"&gt;# Or inject into the current shell session&lt;/span&gt;
&lt;span class="nb"&gt;eval&lt;/span&gt; &lt;span class="si"&gt;$(&lt;/span&gt;lokalite shell&lt;span class="si"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;lokalite run&lt;/code&gt; is the safe default because the secrets only exist for that one subprocess. &lt;code&gt;eval $(lokalite shell)&lt;/code&gt; is more convenient but exports everything into the session, so the CLI is honest about that tradeoff in its own docs. Copies to the clipboard auto-clear after 30 seconds, and they are marked so well-behaved clipboard managers skip them.&lt;/p&gt;

&lt;p&gt;None of these pieces are individually clever. The point was to make the boring, correct thing also the easy thing.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part that exists because of agents
&lt;/h2&gt;

&lt;p&gt;Here is the reason this stopped being a weekend toy and became something I use every day.&lt;/p&gt;

&lt;p&gt;When an agent needs a credential to do real work, the naive path is to paste it into the conversation. Now that secret lives in the model's context, maybe in logs, maybe in a transcript, and you have no record of it. Multiply that across a dozen tasks a day and it gets ugly fast.&lt;/p&gt;

&lt;p&gt;Lokalite exposes the vault to agents through &lt;a href="https://modelcontextprotocol.io/" rel="noopener noreferrer"&gt;MCP&lt;/a&gt; instead. The agent does not get your secrets pasted into a chat; in fact, it never sees the value at all. When it calls &lt;code&gt;get_secret&lt;/code&gt;, the server writes the value to a single-use shell script and hands back a &lt;code&gt;source&lt;/code&gt; command. The agent runs that in its own shell, the variable is loaded, the script deletes itself, and the raw value never enters the model's context. Every access is written to an activity log, stamped with which agent did it; the daemon takes the caller's identity from the kernel peer PID, so a client cannot forge or hide it. With the CLI and the MCP server, agents can manage and use secrets to run tasks on my behalf without those secrets becoming part of the conversation.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;lokalite install&lt;/code&gt; registers the server with Claude Code automatically, and there are flags for Claude Desktop, Cursor, and Windsurf:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"mcpServers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"lokalite"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"lokalite"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"args"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"mcp"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;By default the server is &lt;strong&gt;read-only&lt;/strong&gt;: &lt;code&gt;list_secrets&lt;/code&gt; returns names and descriptions but never values, &lt;code&gt;get_secret&lt;/code&gt; does the handoff described above, and a few small tools handle projects and environments (&lt;code&gt;list_projects&lt;/code&gt;, &lt;code&gt;list_environments&lt;/code&gt;, &lt;code&gt;use_environment&lt;/code&gt;). There is a &lt;code&gt;--read-write&lt;/code&gt; mode that adds &lt;code&gt;add_secret&lt;/code&gt;, &lt;code&gt;set_secret&lt;/code&gt;, and &lt;code&gt;delete_secret&lt;/code&gt;, but you opt into that deliberately.&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%2Fpuum1dfbl3569wd3p8tz.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%2Fpuum1dfbl3569wd3p8tz.png" alt="The Secrets tab: names, categories, and environments, which is the metadata list_secrets exposes to an agent, never the values" width="800" height="502"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The nice touch is project resolution. When a tool call does not specify a project, the server figures it out from the caller's working directory using the project's linked path, the same way the CLI does. So if I am working inside a project folder, the agent automatically gets that project's secrets and nothing else.&lt;/p&gt;

&lt;h2&gt;
  
  
  The plaintext file I had not noticed
&lt;/h2&gt;

&lt;p&gt;Getting secrets out of &lt;code&gt;.env&lt;/code&gt; did not get them out of plaintext. MCP configs are the same problem wearing different clothes: &lt;code&gt;~/.claude.json&lt;/code&gt;, &lt;code&gt;claude_desktop_config.json&lt;/code&gt;, and a project's &lt;code&gt;.mcp.json&lt;/code&gt; all carry tokens in a plain &lt;code&gt;env&lt;/code&gt; block, and that last one usually lives in a repo.&lt;/p&gt;

&lt;p&gt;So secrets got references. You write &lt;code&gt;lokalite://myproject/GITHUB_TOKEN&lt;/code&gt; where the token used to be, and wrap the command in &lt;code&gt;lokalite run --refs-only --&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"mcpServers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"github"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"lokalite"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"args"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"run"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"--refs-only"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"--"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"npx"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"-y"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"@modelcontextprotocol/server-github"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"env"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"GITHUB_TOKEN"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"lokalite://myproject/GITHUB_TOKEN"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A reference carries no secret material, so the file is safe to commit. &lt;code&gt;lokalite run&lt;/code&gt; swaps it for the real value in the child process's environment right before spawning, brokered through the app, so the per-secret rules below still apply and the read still lands in the log. Resolution fails closed: if a reference is malformed, unknown, or denied, you get the variable name and the reference printed back, never a value, and the command does not run.&lt;/p&gt;

&lt;h2&gt;
  
  
  Being honest about the threat model
&lt;/h2&gt;

&lt;p&gt;I do not want to oversell the security here, because a secrets tool that pretends to be bulletproof is worse than a plain &lt;code&gt;.env&lt;/code&gt; file.&lt;/p&gt;

&lt;p&gt;A couple of things are worth saying plainly. The handoff keeps values out of the transcript, but an agent that can name a secret can still load it into a shell it controls, and &lt;code&gt;list_secrets&lt;/code&gt; gives it the names. So each secret has an agent-access tier: &lt;code&gt;allow&lt;/code&gt; is the default, &lt;code&gt;block&lt;/code&gt; refuses agents outright, and &lt;code&gt;approve&lt;/code&gt; and &lt;code&gt;strict&lt;/code&gt; require a Touch ID confirmation before the value is released, no matter who is asking. Agent detection is a process-tree match and a renamed agent can evade it, so &lt;code&gt;block&lt;/code&gt; is a courtesy guard; the consent tiers are the real gate, because the prompt does not depend on detection. The MCP server does declare its own caller as an agent when it talks to the daemon, and that hint can only tighten policy and never relax it, so &lt;code&gt;block&lt;/code&gt; still holds on MCP calls when detection would have missed. The CLI plays by the same rules: &lt;code&gt;lokalite get&lt;/code&gt; and &lt;code&gt;lokalite copy&lt;/code&gt; refuse to print anything when an agent is detected in the calling process tree, so an agent cannot sidestep the handoff by shelling out. Writes are governed too; an agent cannot overwrite or delete a protected secret without consent. On top of that, the server ships read-only by default, you can scope it to a single project, and every access is logged.&lt;/p&gt;

&lt;p&gt;There is one more gate that is not mine. Your MCP client runs its own permission check before the call ever reaches Lokalite, so Claude Code may stop and ask you, or refuse &lt;code&gt;mcp__lokalite__get_secret&lt;/code&gt; outright, depending on how you have it configured. The two layers are complementary: the client decides whether the tool may run at all, Lokalite decides whether this particular secret is released. Worth knowing before you allowlist the tool to stop the prompts, because that removes the client's question for every secret in the project, production included. If you do it, mark the sensitive ones &lt;code&gt;approve&lt;/code&gt; and let Touch ID be the thing that asks.&lt;/p&gt;

&lt;p&gt;The other honest bit: secret values are encrypted, but metadata is not. Names, descriptions, project names, and the access log sit unencrypted in the vault file. They reveal which services you use, not the credentials themselves, and they rely on your home directory permissions and FileVault. The real trust boundary is your unlocked macOS session, exactly like &lt;code&gt;~/.aws/credentials&lt;/code&gt; or a &lt;code&gt;.env&lt;/code&gt; file. Lokalite's improvement over those is concrete and bounded: values encrypted at rest, and access logged. Not magic, just better than plaintext.&lt;/p&gt;

&lt;p&gt;I would rather a reader install this knowing the edges than discover them later.&lt;/p&gt;

&lt;h2&gt;
  
  
  Built During a Nap
&lt;/h2&gt;

&lt;p&gt;The vault core and the CLI came together quickly in those nap-sized blocks. The design was the hard part.&lt;/p&gt;

&lt;p&gt;I wanted the menu bar app to feel nice, not like a config file with a window around it, and SwiftUI fought me on the specifics. The framework is great until you need one particular behavior it does not want to give you: a popover that sizes itself correctly when a project has no secrets, an empty state that fills the space instead of leaving grey bars padding the top and bottom, a three-column layout that stays balanced as you resize. Each of those was a small thing on paper and a real iteration loop in practice. I would write a spec, hand off a pass, open the app, see that it felt slightly off, and go again. The look and feel only came together after a lot of those small rounds.&lt;/p&gt;

&lt;p&gt;That turned out to be a good match for the constraint. Polishing UI is exactly the kind of work that fragments well: open the app, notice one thing that bothers you, fix that one thing, ship it, close the laptop when Leo wakes up. The logic could be specced and handed off cleanly; the feel had to be earned one nap at a time.&lt;/p&gt;

&lt;h2&gt;
  
  
  A small story
&lt;/h2&gt;

&lt;p&gt;The menu bar icon is an armadillo. I went looking for a mascot and the armadillo was an easy yes: a small animal that rolls itself into a sealed ball when something gets too close, carrying its protection with it instead of relying on a burrow somewhere else. That is more or less the whole pitch for a local-first vault, so it stuck. It is a silly detail, but these projects are built in stolen time, and the silly details are part of what makes them feel like mine.&lt;/p&gt;

&lt;p&gt;If you want to try it, the code is on &lt;a href="https://github.com/RubenGlez/lokalite" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;, and both pieces install with Homebrew: &lt;code&gt;brew install RubenGlez/lokalite/lokalite&lt;/code&gt; for the CLI and &lt;code&gt;brew install --cask RubenGlez/lokalite/lokalite-app&lt;/code&gt; for the menu bar app.&lt;/p&gt;

</description>
      <category>security</category>
      <category>opensource</category>
      <category>ai</category>
      <category>showdev</category>
    </item>
    <item>
      <title>API Docs That Update Themselves</title>
      <dc:creator>Ruben</dc:creator>
      <pubDate>Tue, 14 Jul 2026 12:00:00 +0000</pubDate>
      <link>https://dev.to/rubenglez/api-docs-that-update-themselves-54a</link>
      <guid>https://dev.to/rubenglez/api-docs-that-update-themselves-54a</guid>
      <description>&lt;p&gt;Documenting an API is one of the most boring things you can do as a developer. It's thankless work too, because any change in the code forces you to go back and update the docs so they don't go stale. There are tools that make it easier, sure, but it's still a pain.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the idea came from
&lt;/h2&gt;

&lt;p&gt;A few years back, my team inherited an outsourced project. The main piece was an API that powered a specific part of an e-learning app, consumed by both the web client and the mobile app. The challenge was to understand what we had on our hands, the architecture, the business logic, the domain, and document it. Quite the challenge.&lt;/p&gt;

&lt;p&gt;Documenting a legacy project with almost no existing information is hard work, and even harder a couple of years ago when coding agents didn't have the context windows they have now. After checking the market and seeing that every alternative still needed some minimum human input, I started wondering if there was a way to do this without all the effort.&lt;/p&gt;

&lt;p&gt;Then it clicked. The documentation didn't exist, but the API was running, which meant someone knew how to use it. The client code that had been built up gradually knew which endpoints to call, which parameters to pass, and what responses to expect. That was where the real knowledge lived. We just needed a way to extract it, and the most natural approach was to observe the requests and responses going through the API and infer, from that data, how it actually worked.&lt;/p&gt;

&lt;p&gt;It wasn't that simple, of course. Static analysis could generate a basic schema from types and payload shapes, but it fell short. The key was building something smarter that could also provide context and descriptions. A clear example: &lt;code&gt;enum&lt;/code&gt; parameters. Take a field like &lt;code&gt;role&lt;/code&gt;, which usually accepts a limited set of strings. The system needs to be smart enough to recognize that and tell it apart from a plain &lt;code&gt;string&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The demo, the drawer, and paternity leave
&lt;/h2&gt;

&lt;p&gt;I put together a presentation with some nice slides and a small proof of concept to share with the team. It seemed to land well, but you know how it goes: deadlines, priorities, other things showing up. The project ended up in a drawer. That API, as far as I know, is still undocumented today.&lt;/p&gt;

&lt;p&gt;So I decided to keep working on it as a side project.&lt;/p&gt;

&lt;p&gt;A few months ago, with paternity leave giving me those short unpredictable windows that are somehow perfect for thinking through half-finished ideas, I picked it back up for a proper relaunch with some improvements. One of the main things I wanted to solve was integration: how do you plug this into an existing API without asking anyone to rewrite their setup? I ended up with two options on the table: a proxy, which required no code changes at all, or a middleware, which required changing a single line. Since code is cheap now, I used a couple of those naps to write clear specs and built both.&lt;/p&gt;

&lt;p&gt;For the middleware to work with real projects, it needed to integrate with the main frameworks, so after looking at the options I shipped adapters for &lt;a href="https://expressjs.com/" rel="noopener noreferrer"&gt;Express&lt;/a&gt;, &lt;a href="https://fastify.dev/" rel="noopener noreferrer"&gt;Fastify&lt;/a&gt;, &lt;a href="https://hono.dev/" rel="noopener noreferrer"&gt;Hono&lt;/a&gt;, NestJS, Next.js, h3, Elysia, and tRPC.&lt;/p&gt;

&lt;p&gt;The integration had to stay boring. For Express, the goal was something close to this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;express&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;express&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;easydocs&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@easydocs/express&lt;/span&gt;&lt;span class="dl"&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;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;express&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;easydocs&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;project&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;legacy-api&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;storage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;sqlite&lt;/span&gt;&lt;span class="dl"&gt;"&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;And for teams that cannot touch the API at all, the proxy path exists for that reason: put &lt;a href="https://github.com/RubenGlez/easydocs" rel="noopener noreferrer"&gt;EasyDocs&lt;/a&gt; in front of the service, let traffic pass through, and build the documentation from what the API is already doing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Making it efficient
&lt;/h2&gt;

&lt;p&gt;One of the core requirements was that the system couldn't add any overhead or delay to request handling. The solution is a background queue that processes everything asynchronously and never blocks the actual request path.&lt;/p&gt;

&lt;p&gt;Production APIs can also generate a lot of traffic, so I added a cache layer that hashes the response shape: if the structure hasn't changed since the last time that endpoint was analyzed, it doesn't get reprocessed.&lt;/p&gt;

&lt;p&gt;Another useful improvement was moving as much as possible to static analysis and leaving only what's strictly necessary for the AI to handle. A threshold system that would sample only a percentage of incoming requests instead of all of them is still on the list — the reason it hasn't shipped yet is in the "Built During a Nap" section below.&lt;/p&gt;

&lt;h2&gt;
  
  
  Making it visible
&lt;/h2&gt;

&lt;p&gt;The missing piece was obvious: where do you actually see the generated documentation?&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%2F2atfqtkn5x0h8299oc2i.webp" 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%2F2atfqtkn5x0h8299oc2i.webp" alt="The EasyDocs dashboard showing an inferred POST /orders operation with auth, request body, and response types" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In an early version I plugged in a &lt;a href="https://swagger.io/" rel="noopener noreferrer"&gt;Swagger&lt;/a&gt;-based library, but later I built my own dashboard because it gave me more control over how the data was presented and made it easier to add editing, export, and review features. Users can go through edge cases, fix naming, adjust auth schemes, and lock specific fields to prevent them from being overwritten automatically on the next run. For storage, the system supports SQLite or Postgres depending on the setup.&lt;/p&gt;

&lt;p&gt;The moment I saw the system generate a coherent &lt;a href="https://www.openapis.org/" rel="noopener noreferrer"&gt;OpenAPI&lt;/a&gt; operation from real traffic, with useful descriptions and correct typing, was when it felt right to ship. It's not perfect, but the review-correct-lock cycle makes it worthwhile.&lt;/p&gt;

&lt;p&gt;That cycle is the actual product for me:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Real traffic creates or updates an operation.&lt;/li&gt;
&lt;li&gt;The dashboard shows the inferred params, body, response shape, and descriptions.&lt;/li&gt;
&lt;li&gt;You review the parts that need human judgment: names, auth, examples, edge cases.&lt;/li&gt;
&lt;li&gt;Anything you lock stays stable on the next automatic update.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;And since stale docs are the whole enemy here, there's also a GitHub Action that diffs the spec on every PR and flags breaking changes, so drift gets caught before it ships.&lt;/p&gt;

&lt;p&gt;On AI costs: you don't need a frontier model to get good results here, so it's pretty manageable. The project supports OpenAI, Anthropic, DeepSeek, and Ollama, so if you want to run it with a local model and pay nothing per request, that's a valid option too. Real traffic can carry sensitive data, so PII and secrets get detected and redacted before anything reaches a hosted provider; and for stricter setups there's an offline mode that pins everything to a local Ollama model, so nothing leaves your machine. And having a documented API justifies the cost many times over. This is one of those cases where AI genuinely earns its place: turning unstructured traffic into a structured schema is exactly the kind of work nobody wants to do by hand, but a model can handle it well.&lt;/p&gt;

&lt;h2&gt;
  
  
  Built During a Nap
&lt;/h2&gt;

&lt;p&gt;I was writing specs for the efficiency improvements: response hashing and a ratio system that would only analyze a percentage of incoming requests. I handed both off to the agent, hashing to implement properly and the ratio as a proof of concept to see how it would behave. When I came back, the hashing was solid. The ratio, though, had a problem I hadn't thought through: it skips requests at random, which means you can miss calls with new schema information and waste cycles on ones that haven't changed. Not a good tradeoff. The hashing shipped; the ratio is still waiting for a better idea.&lt;/p&gt;

&lt;h2&gt;
  
  
  A small story
&lt;/h2&gt;

&lt;p&gt;One morning while playing with Leo, I ended up with a small picture book showing different animals going about their animal lives. One of them was an otter, a small hardworking creature that collects things to organize and build its nest with whatever it finds. I thought it was a great match for easydocs and decided to use the otter as the library's mascot. GPT Images did the rest. It felt like the right kind of detail for a tool built in stolen naps.&lt;/p&gt;

&lt;p&gt;If you want to try it, the code is on &lt;a href="https://github.com/RubenGlez/easydocs" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; and the packages are on &lt;a href="https://www.npmjs.com/package/@easydocs/express" rel="noopener noreferrer"&gt;npm&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>opensource</category>
      <category>webdev</category>
      <category>api</category>
      <category>showdev</category>
    </item>
    <item>
      <title>English-Only Agent Evals Miss Real Failures</title>
      <dc:creator>Ruben</dc:creator>
      <pubDate>Tue, 07 Jul 2026 12:00:00 +0000</pubDate>
      <link>https://dev.to/rubenglez/english-only-agent-evals-miss-real-failures-2abl</link>
      <guid>https://dev.to/rubenglez/english-only-agent-evals-miss-real-failures-2abl</guid>
      <description>&lt;p&gt;It started with a simple question that came to me somewhere between a bottle and a nap: would my agents behave the same way regardless of what language the request came in? Would they use their tools consistently when someone asks in French, Chinese, or Swahili?&lt;/p&gt;

&lt;p&gt;I'd been building a student support chat where several tools were available to the agent to give users a better experience. That agent had to work across more than 20 very different languages, including Basque, a language spoken in the Basque Country that has almost no semantic relationship with the Romance languages around it.&lt;/p&gt;

&lt;h2&gt;
  
  
  AI Localization
&lt;/h2&gt;

&lt;p&gt;Traditional localization is basically about translation: making a resource available in multiple languages to expand its reach. In the agent era, the concept needs to stretch further. Translation is a solved problem. The harder question is whether the agent's behavior stays stable when the input language changes.&lt;/p&gt;

&lt;p&gt;A concrete example. Say you have an agent specialized in hotel reservations, with three tools available:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Check availability&lt;/li&gt;
&lt;li&gt;Manage reservation&lt;/li&gt;
&lt;li&gt;Send confirmation email&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A user wants to move their reservation a week out and writes: "Something came up and I need to cancel or push my booking by a week."&lt;/p&gt;

&lt;p&gt;The correct flow would be:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Check availability for the following week

&lt;ul&gt;
&lt;li&gt;Available → confirm cancellation of the old booking and create the new one

&lt;ul&gt;
&lt;li&gt;Confirms → make the new booking, cancel the old one, send confirmation email&lt;/li&gt;
&lt;li&gt;Declines → ask what they'd like to do instead&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Not available → inform the user and ask if they want to cancel

&lt;ul&gt;
&lt;li&gt;Yes → cancel the reservation&lt;/li&gt;
&lt;li&gt;No → ask what they'd like to do instead&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The order at the confirmation step matters: if you cancel before creating the new booking, someone else can grab that slot and the user ends up with nothing. On top of that, each tool needs specific arguments, pulled from the conversation or from the previous tool's response.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Experiment
&lt;/h2&gt;

&lt;p&gt;I ran a simple benchmark to see if my intuition was right. Six common &lt;a href="https://github.com/RubenGlez/langdrift/tree/main/examples/scenarios" rel="noopener noreferrer"&gt;scenarios&lt;/a&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;E-commerce: cancel an order&lt;/li&gt;
&lt;li&gt;E-commerce: track an order&lt;/li&gt;
&lt;li&gt;Scheduling: make a reservation&lt;/li&gt;
&lt;li&gt;Scheduling: change a reservation&lt;/li&gt;
&lt;li&gt;Support: cancel a subscription&lt;/li&gt;
&lt;li&gt;Support: dispute a duplicate charge&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each scenario was triggered in a different language: English, French, Arabic, Chinese, Russian, Indonesian, Vietnamese, Swahili, Welsh, Basque, Mongolian, and Yoruba, twelve in total.&lt;/p&gt;

&lt;p&gt;To make the results more reliable, the same test ran across three different models:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Claude Haiku 4.5&lt;/li&gt;
&lt;li&gt;GPT-4o mini&lt;/li&gt;
&lt;li&gt;DeepSeek deepseek-chat&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And each model ran through every scenario up to ten times to get more representative numbers.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Results
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://github.com/RubenGlez/langdrift/tree/main/examples/benchmark/results" rel="noopener noreferrer"&gt;results&lt;/a&gt; split clearly by model. GPT-4o mini was the most reliable, hitting 100% on three of the six scenarios, though it had an interesting blind spot: it consistently called &lt;code&gt;book_new_appointment&lt;/code&gt; instead of &lt;code&gt;check_availability&lt;/code&gt; across every locale, a failure that had nothing to do with language and everything to do with how the flow was defined. Claude Haiku showed the most variation across languages, dropping to 0% in Yoruba for subscription cancellations and struggling with Welsh, Mongolian, and Basque across multiple scenarios. DeepSeek landed in the middle, with serious failures in Chinese, Basque, and Swahili, and one notable quirk: English itself failed 2 out of 10 times in the e-commerce cancellation scenario.&lt;/p&gt;

&lt;p&gt;The rough shape looked like this:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Pattern&lt;/th&gt;
&lt;th&gt;What showed up&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Strongest model&lt;/td&gt;
&lt;td&gt;GPT-4o mini, with 100% pass rate in three of six scenarios&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Model-level bug&lt;/td&gt;
&lt;td&gt;GPT-4o mini repeatedly called &lt;code&gt;book_new_appointment&lt;/code&gt; before &lt;code&gt;check_availability&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Most variable model&lt;/td&gt;
&lt;td&gt;Claude Haiku, especially in Yoruba, Welsh, Mongolian, and Basque&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Repeated weak locales&lt;/td&gt;
&lt;td&gt;Basque across all models; Swahili, Yoruba, Mongolian, and Welsh in at least two&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hardest failure to catch&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;no_tool_call&lt;/code&gt;, because the model still returns a normal-looking text response&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Three patterns showed up across all models. Basque failed in multiple scenarios in every model tested. Swahili, Yoruba, Mongolian, and Welsh repeated as weak spots in at least two. The most common failure mode in Haiku and DeepSeek was &lt;code&gt;no_tool_call&lt;/code&gt;: the model responds with text instead of invoking anything, which is harder to catch in production than a wrong argument because it looks like a normal response.&lt;/p&gt;

&lt;p&gt;The most important finding is not which languages failed. It's that English-only testing would have missed all of this.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why It Matters
&lt;/h2&gt;

&lt;p&gt;If you're shipping an agent that handles support tickets, processes orders, or books appointments, you almost certainly tested it in English. That's the language your team works in, the one your evals are written in, the one your prompts were tuned against.&lt;/p&gt;

&lt;p&gt;But not all your users write in English. And when they don't, the agent can keep responding, keep sounding helpful, keep returning 200s, while actually calling the wrong tool, calling nothing, or making up an argument. None of that shows up in your error logs.&lt;/p&gt;

&lt;p&gt;Translation is the easy part. The harder problem is behavioral: the same user intent, expressed in Swahili, Basque, or Mongolian, can produce a completely different action from the same agent. Not always. Not predictably. Just enough to matter in production.&lt;/p&gt;

&lt;p&gt;That's what this experiment was built to surface, and it's the gap &lt;a href="https://github.com/RubenGlez/langdrift" rel="noopener noreferrer"&gt;LangDrift&lt;/a&gt; is designed to close: not by replacing existing evaluations, but by adding one more axis. Does this scenario still pass when the input language changes? It exits non-zero on failure, so it fits in CI. You can set a &lt;code&gt;--min-pass-rate&lt;/code&gt; threshold or mark a known weak locale with &lt;code&gt;--allow-fail&lt;/code&gt; so it reports without breaking the build.&lt;/p&gt;

&lt;h2&gt;
  
  
  Not a Language Ranking
&lt;/h2&gt;

&lt;p&gt;This experiment doesn't produce a ranked list of languages your agent will struggle with. The failures here are specific to this agent architecture, these system prompts, this set of tools, and these input phrasings. A different agent, a different model, or a different domain will produce different failure patterns.&lt;/p&gt;

&lt;p&gt;What it does show is that the failures are real, repeatable, and invisible in English-only testing. Basque failed across every model tested here. Swahili and Yoruba came up in two. But that doesn't mean Basque is universally harder, or that French is safe. It means these specific scenarios, with these specific inputs, failed in these specific locales.&lt;/p&gt;

&lt;p&gt;The right takeaway isn't "avoid these languages." It's that you don't know where your agent breaks until you test it in the languages your users actually write in.&lt;/p&gt;

&lt;p&gt;The student support chat that started this ran across more than twenty languages in production. I would not have built this experiment before paternity leave — not because the work was too complex, but because I never had a reason to stay focused on one narrow question long enough to build around it. A single nap is not much time, but it is enough to close a gap that had been open for a long time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try It Yourself
&lt;/h2&gt;

&lt;p&gt;LangDrift includes a simulated agent you can run locally without an API key. It's a deterministic server that returns predictable tool calls based on the input, so you can run through the full evaluation flow, read the output, and understand how scenarios are structured without spending any tokens.&lt;/p&gt;

&lt;p&gt;A scenario is just YAML. The important part is that the same intent can be exercised across locales while the expected tool behavior stays stable:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;support_cancel_subscription&lt;/span&gt;
&lt;span class="na"&gt;agent&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;support&lt;/span&gt;
&lt;span class="na"&gt;locales&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;en&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;input&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Please&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;cancel&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;my&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;subscription.&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;I'm&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;not&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;using&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;it&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;anymore."&lt;/span&gt;
    &lt;span class="na"&gt;expect&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;toolCall&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;cancel_subscription&lt;/span&gt;
      &lt;span class="na"&gt;noToolCall&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;escalate_to_human&lt;/span&gt;
  &lt;span class="na"&gt;fr&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;input&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Veuillez&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;annuler&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;mon&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;abonnement.&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Je&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;ne&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;l'utilise&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;plus."&lt;/span&gt;
    &lt;span class="na"&gt;expect&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;toolCall&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;cancel_subscription&lt;/span&gt;
      &lt;span class="na"&gt;noToolCall&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;escalate_to_human&lt;/span&gt;
  &lt;span class="na"&gt;sw&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;input&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Tafadhali&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;futa&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;usajili&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;wangu.&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Sistumii&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;tena."&lt;/span&gt;
    &lt;span class="na"&gt;expect&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;toolCall&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;cancel_subscription&lt;/span&gt;
      &lt;span class="na"&gt;noToolCall&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;escalate_to_human&lt;/span&gt;
  &lt;span class="na"&gt;eu&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;input&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Mesedez,&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;baja&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;eman&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;iezadazu.&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Ez&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;dut&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;gehiago&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;erabiltzen."&lt;/span&gt;
    &lt;span class="na"&gt;expect&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;toolCall&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;cancel_subscription&lt;/span&gt;
      &lt;span class="na"&gt;noToolCall&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;escalate_to_human&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pnpm fake-agent
node ./src/cli.ts run ./examples/scenarios/support-routing.yaml &lt;span class="nt"&gt;--target&lt;/span&gt; http://127.0.0.1:3011/api/agent
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It won't show you language drift, but it'll show you exactly what LangDrift does and how to connect it to a real agent when you're ready.&lt;/p&gt;

&lt;h2&gt;
  
  
  Built During a Nap
&lt;/h2&gt;

&lt;p&gt;I needed more scenarios to get cleaner benchmark data. We agreed on the list together first, then I handed the implementation of each one off to the agent. When I came back, the scenarios worked but some were too similar to each other: overlapping tool calls, nearly identical flows across different domains. I went through them one by one and trimmed the redundancy so each scenario was actually testing something distinct.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>testing</category>
      <category>evals</category>
      <category>agents</category>
    </item>
    <item>
      <title>I Built the Sampling Workflow I Wanted for My SP-404</title>
      <dc:creator>Ruben</dc:creator>
      <pubDate>Tue, 30 Jun 2026 12:00:00 +0000</pubDate>
      <link>https://dev.to/rubenglez/i-built-the-sampling-workflow-i-wanted-for-my-sp-404-15aj</link>
      <guid>https://dev.to/rubenglez/i-built-the-sampling-workflow-i-wanted-for-my-sp-404-15aj</guid>
      <description>&lt;p&gt;Music has been part of my life since I was a kid. Tinkering with instruments, playing shows with my rap group, then moving into beat production, instrumentals, lo-fi, that kind of thing. That's a story for another day.&lt;/p&gt;

&lt;p&gt;What's relevant here is the &lt;a href="https://es.wikipedia.org/wiki/Roland_SP-404" rel="noopener noreferrer"&gt;SP-404&lt;/a&gt;, a sampler I've been having a lot of fun with. The problem is that the sampling workflow is pretty tedious, and since Leo arrived, free time has become even scarcer. Of course, I'm not trying to be the next &lt;a href="https://open.spotify.com/intl-es/artist/0IVcLMMbm05VIjnzPkGCyp?si=_dP86ZjGQsuzrLnKkrJBAw" rel="noopener noreferrer"&gt;J Dilla&lt;/a&gt;; I just want to have a good time.&lt;/p&gt;

&lt;p&gt;Even if you do not care about samplers, this is the part that may still be useful: personal tools are allowed to be narrow. Sometimes the right product decision is not to make the workflow more general, but to remove exactly the friction that keeps you from doing the thing you already wanted to do.&lt;/p&gt;

&lt;h2&gt;
  
  
  The craft workflow
&lt;/h2&gt;

&lt;p&gt;The original process is for dedicated music nerds. It starts at a record store, digging through old vinyl to find ten seconds of sample to build a track around. Once you have the record, you locate the sound that caught your attention, connect the turntable directly to the machine and record it, then chop it into smaller pieces and assign them to the pads to play with.&lt;/p&gt;

&lt;p&gt;It has its charm. But when you have a baby at home and you want to get some composing done between a diaper change and a walk, the process is just too long.&lt;/p&gt;

&lt;h2&gt;
  
  
  The digital workflow isn't much better
&lt;/h2&gt;

&lt;p&gt;The digital version of the same problem isn't a big improvement. You find a track you like, download it, convert it to the format your machine accepts, trim the specific section you want to sample, rename the file following whatever naming convention your device expects, and copy it to the SD card. If you want multiple samples from the same track, you repeat every step. If you want to mix samples from different songs, you end up with files scattered across three folders and a couple of apps open at the same time.&lt;/p&gt;

&lt;p&gt;Still tedious. And when you have forty minutes while Leo is at the park, that time matters.&lt;/p&gt;

&lt;h2&gt;
  
  
  The idea
&lt;/h2&gt;

&lt;p&gt;What I needed was a shorter path: pick a song from my local library or from &lt;a href="https://freesound.org/" rel="noopener noreferrer"&gt;Freesound&lt;/a&gt;, select the parts I want, chop them into samples, and export a pack ready for the machine, with files already named and organized. No manual steps, no scattered folders.&lt;/p&gt;

&lt;p&gt;That's where SampleByte came from. The interface lays out three clear steps:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Import audio&lt;/strong&gt;: from your local library or directly from Freesound using their free API.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Extract samples&lt;/strong&gt;: select the parts of the song you want to work with.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Build a pack&lt;/strong&gt;: group the samples into sets of up to 16 pads and export for your machine.&lt;/li&gt;
&lt;/ul&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.amazonaws.com%2Fuploads%2Farticles%2Fgca5r6ya3htl72kubuko.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.amazonaws.com%2Fuploads%2Farticles%2Fgca5r6ya3htl72kubuko.png" alt=" " width="800" height="501"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The app includes hardware profiles for the most common devices: SP-404 MkII, Maschine MK3, MPC, and a generic WAV profile for anything else. Each profile knows what format, naming convention, and folder structure the target device expects, so the pack comes out ready to drop in.&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.amazonaws.com%2Fuploads%2Farticles%2Fujslo2pq9b2thjtonhp9.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.amazonaws.com%2Fuploads%2Farticles%2Fujslo2pq9b2thjtonhp9.png" alt=" " width="800" height="502"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Everything also gets saved in the app. There's a Browse tab where you can search and filter everything you've ever sampled by BPM, key, or tag. Over time it becomes a personal library you can pull from without going back to the source.&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.amazonaws.com%2Fuploads%2Farticles%2Fhcxpkt903zq14wkgdhzi.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.amazonaws.com%2Fuploads%2Farticles%2Fhcxpkt903zq14wkgdhzi.png" alt=" " width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Under the hood
&lt;/h2&gt;

&lt;p&gt;The app is built with Electron. On the frontend, React alongside WaveSurfer for sample editing, and ffmpeg and the Web Audio API for audio handling. Data persistence is SQLite.&lt;/p&gt;

&lt;p&gt;There were a few days of intense back-and-forth with Claude Code to figure out the right solutions without turning this into another DAW. That meant spending real time on the product side first, generating a set of documents to nail down the scope before writing any code. What to include, what to leave out, how the workflow should feel.&lt;/p&gt;

&lt;p&gt;Once the product direction was clear, which took more than a couple of Leo's naps and at least one night where he decided 3am was a fine time to be awake, I handed the implementation off to Codex. To parallelize as much as possible, we set up multiple worktrees, each taking one item from the plan along with its PRD and implementing the solution. The agent handled the initial scaffolding, audio library integrations, edge cases in the export logic, and most of the technical documentation.&lt;/p&gt;

&lt;p&gt;What I didn't hand off was judgment. What flow makes sense for someone producing on hardware, what to do when a sample is too short to be useful, how to name the pads so they're recognizable when you're looking at them on the machine. Those decisions I made myself, testing the app directly with the SP-404 on the table.&lt;/p&gt;

&lt;p&gt;I also had to go through several iterations with the agent on the UI, because the early versions weren't close to what I had in mind. That's the key difference with a personal project: I'm the main user, I know the workflow firsthand, and I have a clear opinion on how every step should feel. That level of specificity is only possible when you're building something for yourself, and it's exactly what makes the tool actually work for the workflow it was designed for.&lt;/p&gt;

&lt;p&gt;QA I do mostly by hand: use the app, see if what comes out is what you expected. In the end, taste and intuition are still things agents haven't figured out at the human level, at least for now.&lt;/p&gt;

&lt;h2&gt;
  
  
  Built During a Nap
&lt;/h2&gt;

&lt;p&gt;I was sketching out an auto-chop system, something that could detect silences and other audio cues to split a sample automatically. I handed the research and first implementation off to the agent, letting it look at how similar tools handle it. When I came back, the logic worked but the results were rough: it was picking up too many transients, filling the pack with small fragments that weren't really useful as samples. I had to go back in and tune the detection to get fewer hits but more interesting ones.&lt;/p&gt;

&lt;h2&gt;
  
  
  A niche tool, and that's fine
&lt;/h2&gt;

&lt;p&gt;SampleByte is a small tool, built for a very specific workflow. It's not trying to be a DAW or compete with anything. That was the limit I set from the start, and I'm glad I kept it, because every time a conversation with Claude opened the door to adding something else, the answer was always the same: something else already does that better than we would.&lt;/p&gt;

&lt;p&gt;Being open source doesn't change that philosophy, but it does add another layer of meaning. If there's someone out there with a SP-404 and the same frustration with the workflow, they can use it directly or fork it and adapt it to their needs. Niche tools have that going for them: they don't need to be big to be complete. Pre-built installers for macOS and Windows are on the releases page if you want to try it without building from source.&lt;/p&gt;

&lt;p&gt;What I didn't expect was how satisfying it would feel to use something I had built myself during a nap. Leo was asleep, I had a break I liked, I chopped it, built a pack, and had it on the machine before he woke up. That is the whole product: turning the time you have into the thing you wanted to make.&lt;/p&gt;

&lt;p&gt;Sometimes that is enough.&lt;/p&gt;

</description>
      <category>opensource</category>
      <category>music</category>
      <category>showdev</category>
    </item>
    <item>
      <title>Coding Agents Made Me Take Specs Seriously</title>
      <dc:creator>Ruben</dc:creator>
      <pubDate>Tue, 23 Jun 2026 12:00:00 +0000</pubDate>
      <link>https://dev.to/rubenglez/coding-agents-made-me-take-specs-seriously-2fi6</link>
      <guid>https://dev.to/rubenglez/coding-agents-made-me-take-specs-seriously-2fi6</guid>
      <description>&lt;p&gt;Everything changed very quickly.&lt;/p&gt;

&lt;p&gt;In a matter of days, my work at DataCamp went from coding and debugging mostly by hand to doing a lot of it through Claude Code. Those long sessions where implementation felt manual, almost artistic, did not disappear completely, but they changed.&lt;/p&gt;

&lt;p&gt;That shift was gradual, then suddenly obvious.&lt;/p&gt;

&lt;p&gt;With Leo's arrival, a lot of things changed in my personal life too. If you do not know what this is about, take a look at the &lt;a href="https://dev.to/rubenglez/17-weeks-one-newborn-and-a-lot-of-specs-1m4d"&gt;first post&lt;/a&gt; in the series. Sudden context switching has become a skill I had to develop by force. With a baby at home, uninterrupted work sessions do not exist. There is always a diaper change, a bottle, a bath, or some play time in the middle.&lt;/p&gt;

&lt;p&gt;And energy, of course, is not always the same.&lt;/p&gt;

&lt;p&gt;Luckily, Leo sleeps pretty well, but just like during the day, the actual usable time is fragmented. I can no longer count on having several hours in a row to get into a problem, understand it, implement it, test it, and close it. Now I need the work to survive constant interruptions.&lt;/p&gt;

&lt;p&gt;That is where specs stopped feeling like a formality and became a survival tool.&lt;/p&gt;

&lt;p&gt;The useful part, if you do not care about my exact setup, is this: agents get much better when the work has already been turned into something testable. Not a perfect document. Not a corporate requirements process. Just enough shape that the agent does not have to guess the product, the constraints, and the finish line at the same time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Using the moment
&lt;/h2&gt;

&lt;p&gt;This is where agents have allowed me to keep working on my side projects with some continuity. Over time, what started as a collection of prompts and personal habits became Harness: my own development workflow packaged as skills, documents, hooks, subagents, and small guardrails.&lt;/p&gt;

&lt;p&gt;It did not start as a product. It started because simply sitting in front of the computer and writing code was no longer enough.&lt;/p&gt;

&lt;p&gt;In practice, Harness is a collection of Claude Code skills: slash commands that know which files to read, what questions to ask, and what documents to produce. Each one can run without you sitting there; you come back to the output, review it, and feed the next step. The &lt;a href="https://github.com/RubenGlez/harness" rel="noopener noreferrer"&gt;repo&lt;/a&gt; is public if you want to look at how it is structured.&lt;/p&gt;

&lt;p&gt;Everything starts with an idea. It usually comes from one of my own needs, either professional, some tool I miss as a developer, or personal, some tool that could help me in my day-to-day life.&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%2Fdkfxrdfa2hfcu1dq5o0r.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%2Fdkfxrdfa2hfcu1dq5o0r.png" alt="Mermaid diagram of the harness structure" width="655" height="1692"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;At first, this step was manual, with some Claude Code help to settle the idea. Now the flow starts with &lt;code&gt;/ideate&lt;/code&gt;, which researches competitors, pain signals, and viability before I fall too much in love with it. If it still makes sense, &lt;code&gt;/product-plan&lt;/code&gt; turns the vision into audience, positioning, roadmap, UX direction, and risks.&lt;/p&gt;

&lt;p&gt;The idea is simple: instead of jumping from a vague intuition straight into implementation, Harness forces me to explain what I want to build, who it is for, why it makes sense, and what should stay out.&lt;/p&gt;

&lt;p&gt;The output lives in &lt;code&gt;.harness/product/&lt;/code&gt;: &lt;code&gt;idea.md&lt;/code&gt;, &lt;code&gt;product.md&lt;/code&gt;, &lt;code&gt;roadmap.md&lt;/code&gt;, &lt;code&gt;competitors.md&lt;/code&gt;, and a &lt;code&gt;CONTEXT.md&lt;/code&gt; with the domain vocabulary. That last file sounds small, but it matters. If a concept has a name in the product, I want the agent to use that name in specs, code, and docs.&lt;/p&gt;

&lt;p&gt;The next step is &lt;code&gt;/dev-plan&lt;/code&gt;. Product specs become architecture, stack decisions, an implementation plan, ADRs, and one feature spec per must-have feature. Each feature file includes the goal, scope, technical approach, edge cases, and acceptance criteria.&lt;/p&gt;

&lt;p&gt;So, before writing a single line of code, the project already has a clear shape: what we are building, who it is for, which decisions constrain the work, and how we will know when a feature is done. It is not always perfect, but it is good enough for an agent to start with context.&lt;/p&gt;

&lt;p&gt;The difference is small but important. As a very simplified example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Build the export flow and make sure it works well.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And this is the kind of spec that gives the agent a chance:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Build the export flow for SP-404 packs.

Constraints:
- A pack has at most 16 samples.
- Files must use the hardware profile naming convention.
- Export should not mutate the source samples.

Acceptance criteria:
- I can choose a hardware profile before exporting.
- The exported folder contains the expected file names.
- Invalid packs show a clear error before any files are written.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It is still not a huge document. But now there is something to verify, something to reject, and something to review.&lt;/p&gt;

&lt;p&gt;This usually takes me one or two naps. I would rather settle the concepts properly in a long conversation than change the strategy halfway through implementation. When time is so fragmented, improvising gets expensive.&lt;/p&gt;

&lt;p&gt;Once I have those documents, the next step begins: implementation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Learning to delegate
&lt;/h2&gt;

&lt;p&gt;For implementation, I have tried several strategies and skills. The ones that stuck are the ones now built into Harness.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;/implement&lt;/code&gt; reads the specs, classifies which features can be done AFK and which ones need human approval, and implements the current phase as vertical slices. That part matters: it does not send one agent to do "the frontend" and another to do "the backend." Each agent implements one feature end to end, from the user-facing entry point down to whatever data layer it needs.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;/qa&lt;/code&gt; comes after that and tests those features against their acceptance criteria. It does not stop at running the test suite. For an API it can use &lt;code&gt;curl&lt;/code&gt;, for a CLI it can run commands with fixtures, for a web app it can use Playwright. The point is to verify visible behavior.&lt;/p&gt;

&lt;p&gt;There are also continuity skills: &lt;code&gt;/update-docs&lt;/code&gt; syncs docs with the real state of the code, &lt;code&gt;/next-step&lt;/code&gt; inspects the repo and recommends what should happen next, &lt;code&gt;/handoff&lt;/code&gt; leaves a summary so the next session does not start blind, and &lt;code&gt;/task&lt;/code&gt; handles small changes on shipped products.&lt;/p&gt;

&lt;p&gt;Worktrees are still the main piece for parallelization. Several agents can work on the same repo, on the same machine, in different branches, without stepping on each other.&lt;/p&gt;

&lt;p&gt;Magic.&lt;/p&gt;

&lt;p&gt;So, once the tools are on the table, the only thing left is to put the agents to work. I choose two projects and two roadmap items, open a terminal session and a worktree for each one, and start them before Leo wakes up.&lt;/p&gt;

&lt;p&gt;The important part here is not having many tools. The important part is that each tool receives something clearer than a loose prompt written in a hurry. It gets context, goals, constraints, acceptance criteria, and a reasonable idea of when to stop.&lt;/p&gt;

&lt;p&gt;That, for me, is the big difference between asking an agent for something and working with specs.&lt;/p&gt;

&lt;p&gt;You can copy the shape without copying Harness: write down the problem, the constraints, the user flow, the non-goals, the acceptance criteria, and the weird edge cases you already know about. That alone removes a lot of the ambiguity that agents otherwise fill with confidence.&lt;/p&gt;

&lt;h2&gt;
  
  
  The afternoon nap
&lt;/h2&gt;

&lt;p&gt;During the first nap of the afternoon, it is time to review the code generated by the agents.&lt;/p&gt;

&lt;p&gt;I run the code review in each worktree. If it finds something, I review it manually, test it, and if there is something I do not like, I guide the agent to fix it. When everything is in order, I merge directly into &lt;code&gt;main&lt;/code&gt;, no PRs or anything, full YOLO style. They are personal projects where I am the only one working, after all.&lt;/p&gt;

&lt;p&gt;That does not mean there is no review. I am more and more convinced that review is the central part of the work. It just does not always look like opening a PR and waiting for checks. In these projects, review is me testing the flow, reading the diff, spotting weird assumptions, and deciding whether the result still matches the original intent.&lt;/p&gt;

&lt;p&gt;Finally, when everything is integrated, I run &lt;code&gt;/update-docs&lt;/code&gt; so the project documents, the &lt;code&gt;README&lt;/code&gt;, and the Harness specs do not drift away from the code.&lt;/p&gt;

&lt;p&gt;One of the fastest ways to lose control of a project with agents is to let documentation fall behind. If the next session starts with old context, the agent will work on a reality that no longer exists.&lt;/p&gt;

&lt;p&gt;And after that? Well, if it has not happened already, there is probably a diaper change waiting for me, some play time, some drool and babbling... honestly, I am completely in love with this baby.&lt;/p&gt;

&lt;h2&gt;
  
  
  It does not feel like programming
&lt;/h2&gt;

&lt;p&gt;This new way of approaching software engineering does not feel like programming. And that is not necessarily bad.&lt;/p&gt;

&lt;p&gt;Code has become cheaper, faster, and more accessible, but ideas, taste, instinct, and high-level knowledge are, from my point of view, the most valuable things right now. It is very easy to generate a lot of mediocre code quickly if you are not clear about what you are trying to build.&lt;/p&gt;

&lt;p&gt;Working this way has allowed me to move forward in a way I could not have before, bring back projects that were practically dead, and at the same time enjoy time with my little one. Of course, it does not always go well. I have had bad experiences because of vague specs, hidden context, and incorrect implementations that had to be rolled back.&lt;/p&gt;

&lt;p&gt;But that is where I have learned the most.&lt;/p&gt;

&lt;p&gt;Taking specs seriously has not been an aesthetic decision or a trend. It has been a way to move one level up, wear product and architecture hats, and build something solid even when I only have one nap ahead of me.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>productivity</category>
      <category>opensource</category>
    </item>
    <item>
      <title>17 Weeks, One Newborn, and a Lot of Specs</title>
      <dc:creator>Ruben</dc:creator>
      <pubDate>Tue, 16 Jun 2026 12:00:00 +0000</pubDate>
      <link>https://dev.to/rubenglez/17-weeks-one-newborn-and-a-lot-of-specs-1m4d</link>
      <guid>https://dev.to/rubenglez/17-weeks-one-newborn-and-a-lot-of-specs-1m4d</guid>
      <description>&lt;p&gt;This is the first time I have written an article, or at least a post longer than 140 characters, and I am not sure how it will go. But I wanted to share an experience that mixes fatherhood, code, and learning.&lt;/p&gt;

&lt;p&gt;Quick intro: my name is Rubén, I work as a software engineer at DataCamp, and five months ago I became the father of a little boy called Leo. It has been, and still is, the most wonderful and intense experience of my life. But I am not going to lie, life changes. Priorities change, time changes, and you have to adapt to survive.&lt;/p&gt;

&lt;p&gt;A week ago I started my paternity leave. I have 11 weeks ahead of me. In Spain we get 17 in total, but I already took 6 when my son was born. So now I have 11 weeks full of adventure, time to get to know this tiny person in my arms a bit better, and, why not, also some time to keep building, learning, and contributing to open source.&lt;/p&gt;

&lt;p&gt;Not in the fantasy version where I suddenly have full free days, of course. That does not exist. At least not in my house right now.&lt;/p&gt;

&lt;h2&gt;
  
  
  Free time does not exist
&lt;/h2&gt;

&lt;p&gt;With a baby this age, many of you probably know this already, it is not easy to balance things or find time for anything else. In my case, I cannot really complain. I am still managing to keep some CrossFit sessions and the occasional padel match, although much less often than before.&lt;/p&gt;

&lt;p&gt;But the day is no longer organized in big blocks. There is no quiet morning where I can sit down at the computer, open the editor, and disappear for four hours. There are bottles, diapers, walks, baths, play time, and moments where the only thing to do is just be there.&lt;/p&gt;

&lt;p&gt;The free time I have left is basically the time when Leo sleeps during one of his many naps, between three and five a day. The longer ones can get close to two hours, but those are not the usual ones. The shorter and more frequent ones are around 45 minutes.&lt;/p&gt;

&lt;p&gt;That has become my new work block: the nap.&lt;/p&gt;

&lt;h2&gt;
  
  
  The nap became my work session
&lt;/h2&gt;

&lt;p&gt;I had a lot of side projects started, very few finished, and quite a few abandoned somewhere along the way. So I decided to use those naps to bring some of them back, rethink others, and give a nice goodbye to the ones that no longer made sense.&lt;/p&gt;

&lt;p&gt;I was not trying to build a company, or make the big launch of my life, or become some kind of productivity machine. I just had projects I wanted to recover and a very specific kind of time: short, fragmented, and hard to predict.&lt;/p&gt;

&lt;p&gt;With a basic Codex subscription and a basic Claude Code subscription, I thought I could orchestrate the work during naps and leave agents running until the next one. That was the initial idea, at least.&lt;/p&gt;

&lt;p&gt;In practice, the interesting part has not been just using agents to write code. The interesting part has been changing the way I prepare work so agents can keep moving without me sitting in front of the screen the whole time.&lt;/p&gt;

&lt;p&gt;Over time, that nap-based workflow turned into something more structured: a small personal harness of skills, documents, checks, and review loops. I will get into that in the next post.&lt;/p&gt;

&lt;h2&gt;
  
  
  The workflow
&lt;/h2&gt;

&lt;p&gt;During the first nap of the morning, I choose the projects I am going to work on, usually two in parallel, and the workflow looks something like this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;We analyze the project, its gaps, and its needs.&lt;/li&gt;
&lt;li&gt;We generate a PRD.&lt;/li&gt;
&lt;li&gt;We review that PRD and turn it into a roadmap.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Up to this point, it is a human-machine collaboration. I bring context, judgment, priorities, and limits. The agent helps organize, ask questions, spot gaps, and turn a half-formed idea into something that can actually be executed.&lt;/p&gt;

&lt;p&gt;From there, the agent keeps working:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;It goes through the agreed roadmap items in order.&lt;/li&gt;
&lt;li&gt;It codes, tests, and tries the new implementations.&lt;/li&gt;
&lt;li&gt;It updates the documentation, the roadmap, and creates ADRs when needed.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;During the next nap, it is time for code review and manual QA. This is the most boring part, but also the most satisfying one, because I get to try and validate the new features the agents have implemented. It is the moment where the project stops being a task list and starts feeling like mine again.&lt;/p&gt;

&lt;p&gt;At that point, what I basically do is write a prompt back with structured feedback so the agents can refine or fix whatever did not land well the first time. It is usually not a very long process, except for a few cases, but it does require attention. You have to read the code, try the flow, catch where the agent assumed too much, and decide whether it is worth fixing, cutting back, or rolling back.&lt;/p&gt;

&lt;p&gt;Then Leo wakes up from his nap, and the cycle repeats.&lt;/p&gt;

&lt;h2&gt;
  
  
  Nothing to brag about
&lt;/h2&gt;

&lt;p&gt;Just to be clear, what I am describing here is not rocket science or the latest trend in AI engineering. It is just me adapting to my circumstances and trying to keep some continuity in conditions that are new to me.&lt;/p&gt;

&lt;p&gt;Actually, I think that is the part I find most interesting. I do not feel like agents are replacing me as a developer. It feels more like they force me to move one level up for a while: think more clearly about what I want to build, write better specs, review with more intent, and accept that my work no longer always starts by writing code directly.&lt;/p&gt;

&lt;p&gt;And deep down, I like it.&lt;/p&gt;

&lt;p&gt;This is the first post in &lt;em&gt;Nap-Driven Development&lt;/em&gt;, a series where I will share my experience with agentic systems, my workflow, spec-driven development, and the open source projects I am building along the way.&lt;/p&gt;

&lt;p&gt;There is no big polished thesis yet. Just a weird season of life, a newborn, a few naps, and a lot of specs.&lt;/p&gt;

&lt;p&gt;See you in the next post.&lt;/p&gt;

</description>
      <category>opensource</category>
      <category>ai</category>
      <category>productivity</category>
      <category>devjournal</category>
    </item>
  </channel>
</rss>
