<?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: Kevin Julián Martínez Escobar</title>
    <description>The latest articles on DEV Community by Kevin Julián Martínez Escobar (@kevinccbsg).</description>
    <link>https://dev.to/kevinccbsg</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%2F304931%2Fc83bb861-920b-46b2-a2d4-09428d595e85.jpeg</url>
      <title>DEV Community: Kevin Julián Martínez Escobar</title>
      <link>https://dev.to/kevinccbsg</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/kevinccbsg"/>
    <language>en</language>
    <item>
      <title>Chain of Custody: Every Requirement Arrives Signed For</title>
      <dc:creator>Kevin Julián Martínez Escobar</dc:creator>
      <pubDate>Thu, 05 Nov 2026 18:40:00 +0000</pubDate>
      <link>https://dev.to/kevinccbsg/chain-of-custody-every-requirement-arrives-signed-for-29co</link>
      <guid>https://dev.to/kevinccbsg/chain-of-custody-every-requirement-arrives-signed-for-29co</guid>
      <description>&lt;p&gt;In a forensic investigation, a piece of evidence with a gap in its history is not wrong. It is &lt;strong&gt;inadmissible&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Nobody claims the knife was swapped. The problem is that nobody can prove it was not. Between the crime scene and the courtroom, every person who held it signed for it, dated it, and handed it on. One missing signature and the evidence is worthless, no matter how good it is.&lt;/p&gt;

&lt;p&gt;That is the discipline software built with agents is missing. Not for evidence, but for &lt;strong&gt;intent&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The gap everyone knows
&lt;/h2&gt;

&lt;p&gt;A product owner writes a ticket. It says roughly what they want. A developer fills the gaps with their own judgement and starts building. Halfway through, the product owner asks for a change in a chat. The developer makes it. The ticket never hears about it.&lt;/p&gt;

&lt;p&gt;Six weeks later nobody can say what was agreed, why the code does what it does, or whether it does what was asked. The knowledge lived in two heads, and one of them has left.&lt;/p&gt;

&lt;p&gt;This is not a process failure. It is the default state of software work.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why agents make it worse
&lt;/h2&gt;

&lt;p&gt;The instinct is that AI closes the gap: the machine reads the ticket, the machine writes the code.&lt;/p&gt;

&lt;p&gt;It does the opposite. An agent given an ambiguous requirement does not stop and ask. It resolves the ambiguity confidently, silently, and in seconds, then builds a complete, tested, plausible implementation of the wrong thing. A human who misunderstands a ticket produces a mess you can see. An agent that misunderstands a ticket produces something that looks finished.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Speed does not cause drift. Speed removes the pauses in which drift used to be noticed.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The chain
&lt;/h2&gt;

&lt;p&gt;Six links. Each one is held by a person, produces an artifact, and hands it to the next. The rule that matters is not the number of steps. It is that &lt;strong&gt;no link reads anything the previous link did not write down.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Link&lt;/th&gt;
&lt;th&gt;Produces&lt;/th&gt;
&lt;th&gt;Guards against&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Intake&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;The raw request&lt;/td&gt;
&lt;td&gt;Nothing yet. Under-specified is normal here&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Refinement&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;A spec with a definition of done&lt;/td&gt;
&lt;td&gt;Building a confident interpretation of an ambiguity&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Planning&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;An approach, reviewed before code exists&lt;/td&gt;
&lt;td&gt;Discovering the approach was wrong after building it&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Execution&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;The change, tests written first, divergences recorded&lt;/td&gt;
&lt;td&gt;A plan followed into a wall, or work that quietly went elsewhere&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Verification&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;A verdict: does the delivery match the ticket &lt;em&gt;as it stands now&lt;/em&gt;?&lt;/td&gt;
&lt;td&gt;The untracked request and the moved goalpost&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Handover&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Everything attached to the change, legible to a stranger&lt;/td&gt;
&lt;td&gt;The amnesia that made the original problem unfixable&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;If you work in security, the name will sound familiar. There, chain of custody means build provenance (SLSA, SBOMs): proof that the binary you deploy came from the source you reviewed, and nobody touched it on the way. That chain protects the &lt;strong&gt;code&lt;/strong&gt;. This one protects the &lt;strong&gt;reason the code exists&lt;/strong&gt;: proof that what was built is what was asked for. Same three questions: who held it, when, and what did they sign for.&lt;/p&gt;

&lt;p&gt;Verification is the link most teams have never had. It compares what was delivered against the live ticket, not the frozen spec. The frozen spec and the code were agreed at the same moment, so comparing them always agrees. Comparing against the live ticket is where the chat conversation finally surfaces: something in the delivery that nothing in the record asked for.&lt;/p&gt;

&lt;h2&gt;
  
  
  The principles
&lt;/h2&gt;

&lt;p&gt;The chain rests on a handful of rules. Three carry most of the weight:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;An unwritten agreement did not happen.&lt;/strong&gt; Not because people are dishonest, but because memory is not a medium and chat is not a record.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No definition of done, no start.&lt;/strong&gt; The most important question anyone can answer about a request is &lt;em&gt;how would you know it works?&lt;/em&gt; A request without an answer cannot be built, only approximated. Sending it back with that one question is a valid answer.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Humans hold the gates.&lt;/strong&gt; Every step runs because a person decided it should. Agents do not advance themselves through the chain. The permission to proceed is the human contribution at every link.&lt;/p&gt;

&lt;p&gt;The rest follow from those: review the plan before the code exists, because correcting a document costs a sentence and correcting a codebase costs a day. Freeze the target, so a change of mind becomes a new, dated agreement instead of a silent edit. Treat evidence as a test, not an assertion: "it works" is a claim, a test that fails when the behaviour disappears is evidence.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the developer becomes
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;A developer is no longer responsible for producing the output. They are responsible for the output being correct.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;It is not a demotion. It is the difference between the person who builds the bridge and the person who signs that it is safe. The work moves from authorship to custody.&lt;/p&gt;

&lt;p&gt;If that sounds like less work, try doing it honestly for a week.&lt;/p&gt;

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

&lt;p&gt;A model that hides its costs is the thing this model exists to prevent, so here they are.&lt;/p&gt;

&lt;p&gt;It front-loads work: the first day looks idle to anyone counting commits. It means saying no, sometimes to senior people. It creates artifacts that need a home. And it turns the quick corridor request into a visible amendment, which will feel like friction to the person making it. That friction is the point.&lt;/p&gt;

&lt;h2&gt;
  
  
  "This is slow. My way is fast."
&lt;/h2&gt;

&lt;p&gt;It is the first objection, it is made in good faith, and half of it is true. Skipping the chain is faster to the first commit. It is not faster to something correct.&lt;/p&gt;

&lt;p&gt;The fast way survived because nobody ever saw the bill. Nobody writes "two days reconstructing what we meant" on a ticket. That time gets charged to the next estimate, the onboarding, the rewrite, the quarter where velocity mysteriously halved.&lt;/p&gt;

&lt;p&gt;A wrong assumption costs a sentence in refinement, a paragraph in planning, a day in review, and a fortune in production. Same assumption, four prices.&lt;/p&gt;

&lt;p&gt;And agents add an argument that did not exist before. Sloppy thinking used to hide inside a long build: when a feature took a week, the thinking was a rounding error against the typing. Take the build down to eleven minutes and there is nothing left for it to hide behind.&lt;/p&gt;

&lt;p&gt;So stop measuring time to first commit. Measure time to &lt;strong&gt;knowing you are done&lt;/strong&gt;.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>productivity</category>
      <category>agile</category>
      <category>softwareengineering</category>
    </item>
    <item>
      <title>Your Software Factory Is Not an Engineering Project</title>
      <dc:creator>Kevin Julián Martínez Escobar</dc:creator>
      <pubDate>Wed, 04 Nov 2026 18:38:00 +0000</pubDate>
      <link>https://dev.to/kevinccbsg/your-software-factory-is-not-an-engineering-project-3554</link>
      <guid>https://dev.to/kevinccbsg/your-software-factory-is-not-an-engineering-project-3554</guid>
      <description>&lt;p&gt;I have seen teams afraid to ask their product owner to create a GitHub account.&lt;/p&gt;

&lt;p&gt;Not unwilling. Afraid. So instead they build a bridge: a bot that copies tickets from the tool product uses into issues, a sync job that copies comments back, a dashboard that renders the markdown specs somewhere product will look at them. Weeks of engineering so that nobody outside engineering has to change anything.&lt;/p&gt;

&lt;p&gt;Every bridge is a place where the record can split in two. And the record is the whole point.&lt;/p&gt;

&lt;h2&gt;
  
  
  Software is not only engineering
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://kevinmartinez.dev/posts/a-software-factory-is-a-trust-problem-more-than-a-speed-problem/" rel="noopener noreferrer"&gt;A software factory is a trust problem more than a speed problem&lt;/a&gt;, and trust needs a chain: request, spec, plan, change, verification, handover. Every link reads what the previous one wrote down. I call it &lt;strong&gt;chain of custody&lt;/strong&gt;, and it deserves its own post.&lt;/p&gt;

&lt;p&gt;Look at who holds the links. The request comes from product or a stakeholder. The definition of done needs product. Acceptance needs whoever asked. Design decides what the screen looks like before anyone builds it. Engineering holds maybe half of the chain.&lt;/p&gt;

&lt;p&gt;If those people work outside it, the chain does not exist. You have an engineering pipeline with a chat window bolted to the front, and the chat window is where every important decision happens.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two ways to handle it
&lt;/h2&gt;

&lt;p&gt;You can &lt;strong&gt;build around people&lt;/strong&gt;. Keep product in their tool, keep design in theirs, and engineer the sync. It feels kind. It is also the overengineered option: more moving parts, more places for agents to read a stale copy, and a record that nobody fully owns.&lt;/p&gt;

&lt;p&gt;Or you can &lt;strong&gt;bring people in&lt;/strong&gt;. Product writes and answers in the same issues the agents read. Design attaches the decision to the ticket, not to a thread. Stakeholders accept or reject a pull request by watching the video of the tests running, not by asking someone in a meeting whether it is done.&lt;/p&gt;

&lt;p&gt;I do not think one is always right. But be honest about which one you chose, and why. "They would never use GitHub" is often a guess nobody has tested.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you are actually asking for
&lt;/h2&gt;

&lt;p&gt;It helps to be precise about what joining the chain means for a non-engineer, because it is less than people fear:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Write the request where the work happens.&lt;/strong&gt; One place, not a chat plus a doc plus a call.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Answer one question: how would you know it works?&lt;/strong&gt; That is the definition of done, and it is a product question, not a technical one.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Never answer a technical question.&lt;/strong&gt; If the answer requires reading the code, it is not theirs to give. A good chain asks product only what product can answer.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Accept against evidence.&lt;/strong&gt; A short video, a run report, a plain-language explanation of the pull request. Not the diff.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is it. No markdown expertise, no git, no terminal. A browser and an account.&lt;/p&gt;

&lt;h2&gt;
  
  
  The real resistance
&lt;/h2&gt;

&lt;p&gt;The resistance is rarely about the tool. It is about the corridor.&lt;/p&gt;

&lt;p&gt;Today, a product owner can ask for a change in a chat and get it by the afternoon. In a factory, that same request becomes a visible amendment to the ticket: dated, re-approved, verified. To the person making the request, that feels like friction, and it is. It is the friction of a decision becoming a record.&lt;/p&gt;

&lt;p&gt;The same goes for engineers. The developer who "just does it" without an issue, the hero who fixes production from memory, the cowboy who ships faster than the chain allows. Each one is faster today and breaks the chain for everyone tomorrow. If one link skips the record, every link after it is working from a guess.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part tooling cannot fix
&lt;/h2&gt;

&lt;p&gt;Everything else in a factory can be bought, installed or written: CI, linters, tests, observability, agents. This cannot. If the people who define the work are not willing to work inside the chain, no amount of tooling will produce a factory. You will produce faster engineering, which is not the same thing.&lt;/p&gt;

&lt;p&gt;So before choosing tools, ask the question that decides whether they will matter: &lt;strong&gt;is everyone who holds a link willing to hold it in the open?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If the answer is no, that is the first project. Not the pipeline.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>productivity</category>
      <category>agile</category>
      <category>teamwork</category>
    </item>
    <item>
      <title>A Software Factory Is a Trust Problem More Than a Speed Problem</title>
      <dc:creator>Kevin Julián Martínez Escobar</dc:creator>
      <pubDate>Tue, 03 Nov 2026 17:31:00 +0000</pubDate>
      <link>https://dev.to/kevinccbsg/a-software-factory-is-a-trust-problem-more-than-a-speed-problem-57o4</link>
      <guid>https://dev.to/kevinccbsg/a-software-factory-is-a-trust-problem-more-than-a-speed-problem-57o4</guid>
      <description>&lt;p&gt;Everyone building with agents is chasing the same number: how fast can we go from idea to merged pull request. That number is the easy one now. An agent can produce a complete, tested, plausible feature in eleven minutes.&lt;/p&gt;

&lt;p&gt;The hard number is the other one. &lt;strong&gt;How long until you know it is right?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That is what a software factory actually has to solve. Not producing output, which is cheap, but trusting it, which is not. And trust does not come from a better prompt. It comes from tooling that tells you, quickly and without interpretation, what changed and whether it still works.&lt;/p&gt;

&lt;p&gt;So this is not a list of tools. It is what I look for in any tool before it goes anywhere near a factory.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. It tells you what changed
&lt;/h2&gt;

&lt;p&gt;A factory where you cannot see what happened is a factory you will stop trusting the first time something breaks.&lt;/p&gt;

&lt;p&gt;That starts with &lt;strong&gt;observability in the product&lt;/strong&gt;: traces, metrics and logs from day one, ideally on an open standard like &lt;a href="https://opentelemetry.io/" rel="noopener noreferrer"&gt;OpenTelemetry&lt;/a&gt; so the data is not trapped in one vendor. When an agent ships a change, production should be able to tell you what it did before a user does. I put this first because it is the one most teams postpone, and postponing it means every agent change goes out blind.&lt;/p&gt;

&lt;p&gt;It continues with &lt;strong&gt;observability of the factory itself&lt;/strong&gt;. Every step an agent took should leave something a human can read at human pace: a ticket comment, a run report, a spec, a video of the tests running. If the only evidence of a change is the diff, you are reviewing the agent's opinion of what it built, not what it built.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. It is deterministic and fast
&lt;/h2&gt;

&lt;p&gt;Agents do not need long markdown files listing every rule of your architecture. They need checks that fail.&lt;/p&gt;

&lt;p&gt;A rule written in prose is a suggestion. A rule enforced by a linter is a fact. I have written about this before in &lt;a href="https://kevinmartinez.dev/posts/prose-does-not-beat-determinism-stop-asking-your-ai-agent-to-remember/" rel="noopener noreferrer"&gt;Prose does not beat determinism&lt;/a&gt;, and it holds for the whole factory:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;CI from the first commit.&lt;/strong&gt; Not when the project "gets serious". The first agent pull request is the first one that needs it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Linters and type checks&lt;/strong&gt; for the rules you would otherwise write in a &lt;code&gt;CONVENTIONS.md&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Commit conventions&lt;/strong&gt; enforced by a hook, so agent commits read the same as human ones and changelogs write themselves.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Static analysis that always runs&lt;/strong&gt;: secret scanning like &lt;a href="https://github.com/gitleaks/gitleaks" rel="noopener noreferrer"&gt;gitleaks&lt;/a&gt;, code analysis like SonarCloud or whatever fits your stack. These are boring, and boring is the point. They do not depend on the agent remembering anything.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Markdown still has a job. It records &lt;strong&gt;intent&lt;/strong&gt;: what was agreed and why. Checks record &lt;strong&gt;rules&lt;/strong&gt;. Mixing the two is how you end up with a 900 line instructions file that the agent reads, half follows, and nobody can verify.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. It is simple enough to change
&lt;/h2&gt;

&lt;p&gt;The easier the architecture is for a human to read, the easier it is for an agent to work in. And the easier it is to work in, the easier it is to throw away.&lt;/p&gt;

&lt;p&gt;That last part matters more every month. The tools and models you use today will not be the ones you use in a year. A factory that cannot drop a piece and replace it is a factory that will slowly calcify around its first decisions.&lt;/p&gt;

&lt;p&gt;Two things keep it changeable:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No speculative architecture.&lt;/strong&gt; Build for the requirements you know. An agent asked to "make it extensible" will happily generate a framework for a problem you never had.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No lock-in you cannot price.&lt;/strong&gt; Hosted is fine. The question is how much it costs to leave. If your framework decides where you deploy, or your test runner only works inside one platform, you have handed a decision that should be yours to someone whose business model depends on it.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  4. It proves its own quality
&lt;/h2&gt;

&lt;p&gt;This is my strongest filter, and the one that rules out popular tools most often.&lt;/p&gt;

&lt;p&gt;If a tool cannot show me how it checks itself, I do not use it. That goes double for anything that is itself an AI component: a skill, a plugin, an MCP server, an agent prompt. Before adopting one I look at three things:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Its CI.&lt;/strong&gt; Does it run anything on every change, or does a new version just appear?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;How it is distributed.&lt;/strong&gt; Versioned, with a changelog, or a file you copy and hope?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Its evals.&lt;/strong&gt; Is there evidence the skill does something useful, measured over more than one run, or is the evidence "I tried it and it worked"?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The third is the one almost nobody has yet. I wrote about why it matters in &lt;a href="https://kevinmartinez.dev/posts/i-tried-it-and-it-worked-is-not-a-test-for-your-claude-code-skills/" rel="noopener noreferrer"&gt;"I tried it and it worked" is not a test&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;A note on popularity, because it is the objection I hear most. &lt;strong&gt;Popularity is no longer a proxy for ease of use, because agents read the docs for you.&lt;/strong&gt; A small, well-tested tool with clear docs is as easy for an agent to adopt as the market leader. Popularity still matters for maintenance: you want the thing to exist in three years. It is a tiebreaker, not the deciding vote.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Testing you can review at human pace
&lt;/h2&gt;

&lt;p&gt;Testing is where trust is either earned or faked, and agents make faking it very easy. When the same agent writes the code and the test, a green tick is the same model's opinion expressed twice.&lt;/p&gt;

&lt;p&gt;What I look for in frontend testing specifically:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Every scenario mocked and deterministic&lt;/strong&gt;, so the suite can run on every iteration of an agent loop without flaking.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Contract testing against the backend&lt;/strong&gt;, so the mocks the tests rely on cannot drift away from the real API.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Something a human can watch.&lt;/strong&gt; A short video of each test running, attached to the pull request, tells a reviewer in thirty seconds what the diff takes five minutes to suggest.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is the set I ended up building into &lt;a href="https://twd.dev" rel="noopener noreferrer"&gt;TWD&lt;/a&gt; for my own work, which is why I am biased. But the criteria matter more than the tool: whatever you use for any framework, ask whether it gives you all three.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. The chain has no shortcuts
&lt;/h2&gt;

&lt;p&gt;None of the above survives a team that routes around it.&lt;/p&gt;

&lt;p&gt;A factory is a chain. A request becomes a ticket, the ticket becomes a spec, the spec a plan, the plan a change, the change gets verified against the ticket and handed over. Break one link (a requirement agreed in a chat, a fix pushed without an issue, a hero developer who "just does it") and you lose the thing the whole factory exists to produce: a record you can trust.&lt;/p&gt;

&lt;p&gt;I call this &lt;strong&gt;chain of custody&lt;/strong&gt;, borrowing the forensic term, and it deserves its own post.&lt;/p&gt;

&lt;p&gt;It also means the factory is not an engineering project. Product, design and stakeholders work inside the same chain or the chain does not exist. That is the hardest part of all of this, and &lt;a href="https://kevinmartinez.dev/posts/your-software-factory-is-not-an-engineering-project/" rel="noopener noreferrer"&gt;it gets its own post too&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The one question
&lt;/h2&gt;

&lt;p&gt;Strip all of this down and every tool gets the same question:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Can it tell me, quickly and deterministically, what changed and whether it still works?&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If yes, it belongs in the factory, popular or not. If no, it does not, popular or not. And when the answer changes, throw it away. A factory you are afraid to rebuild is not a factory, it is legacy that happens to be automated.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>devops</category>
      <category>testing</category>
      <category>architecture</category>
    </item>
    <item>
      <title>Evals tell you which parts of your skill to delete</title>
      <dc:creator>Kevin Julián Martínez Escobar</dc:creator>
      <pubDate>Sun, 01 Nov 2026 10:38:17 +0000</pubDate>
      <link>https://dev.to/kevinccbsg/evals-tell-you-which-parts-of-your-skill-to-delete-cai</link>
      <guid>https://dev.to/kevinccbsg/evals-tell-you-which-parts-of-your-skill-to-delete-cai</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;A rule the model follows without being told is a rule you're paying to repeat.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Skills grow in one direction. The agent forgets an &lt;code&gt;await&lt;/code&gt;: add a line. It mocks the wrong thing: add a paragraph. A new model ships, the old problems stop happening, and the lines stay, because nobody knows which of them are still doing something.&lt;/p&gt;

&lt;p&gt;In the &lt;a href="https://kevinmartinez.dev/posts/i-tried-it-and-it-worked-is-not-a-test-for-your-claude-code-skills/" rel="noopener noreferrer"&gt;first article&lt;/a&gt; I described how &lt;code&gt;claude plugin eval&lt;/code&gt; runs every case twice, with your plugin and without it. That second run is the tool for this problem. It measures what the model already does on its own.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the model does without you
&lt;/h2&gt;

&lt;p&gt;These are the eight cases of &lt;a href="https://github.com/BRIKEV/twd-ai" rel="noopener noreferrer"&gt;twd-ai&lt;/a&gt;, the plugin I use as the example, on Opus 5.5. Three runs per case, same judge for every run.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Case&lt;/th&gt;
&lt;th&gt;With&lt;/th&gt;
&lt;th&gt;Without&lt;/th&gt;
&lt;th&gt;Δ&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Set the library up in a project&lt;/td&gt;
&lt;td&gt;1.00&lt;/td&gt;
&lt;td&gt;0.10&lt;/td&gt;
&lt;td&gt;+0.90&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Write the CI workflow&lt;/td&gt;
&lt;td&gt;1.00&lt;/td&gt;
&lt;td&gt;0.30&lt;/td&gt;
&lt;td&gt;+0.70&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Test a component in isolation&lt;/td&gt;
&lt;td&gt;1.00&lt;/td&gt;
&lt;td&gt;0.50&lt;/td&gt;
&lt;td&gt;+0.50&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Get a failing suite green&lt;/td&gt;
&lt;td&gt;1.00&lt;/td&gt;
&lt;td&gt;0.60&lt;/td&gt;
&lt;td&gt;+0.40&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Fix a hand-written test with lint errors&lt;/td&gt;
&lt;td&gt;1.00&lt;/td&gt;
&lt;td&gt;0.86&lt;/td&gt;
&lt;td&gt;+0.14&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Write page flow tests&lt;/td&gt;
&lt;td&gt;0.97&lt;/td&gt;
&lt;td&gt;0.87&lt;/td&gt;
&lt;td&gt;+0.10&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Replace a third-party component with a fake&lt;/td&gt;
&lt;td&gt;1.00&lt;/td&gt;
&lt;td&gt;1.00&lt;/td&gt;
&lt;td&gt;0.00&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Stay out of a unit-test request&lt;/td&gt;
&lt;td&gt;1.00&lt;/td&gt;
&lt;td&gt;1.00&lt;/td&gt;
&lt;td&gt;0.00&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The top of the table is where the skills earn their place. Without them, Opus sets the library up from what it remembers, and what it remembers is out of date. The bottom is where Opus already does the job on its own. The last case is different: it checks that the skill stays out of a request it isn't for, so its Δ is always 0.&lt;/p&gt;

&lt;h2&gt;
  
  
  Knowledge and discipline
&lt;/h2&gt;

&lt;p&gt;The table splits along a line I wrote about in &lt;a href="https://kevinmartinez.dev/posts/your-library-is-not-in-the-training-data-making-ai-agents-use-it-well/" rel="noopener noreferrer"&gt;Your library isn't in the training data&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;A &lt;strong&gt;knowledge&lt;/strong&gt; rule teaches the model something it can't know: your library's config, your project's conventions, the helper your team always uses. Its Δ is large and stays large, because no new model will have learned your internal conventions.&lt;/p&gt;

&lt;p&gt;A &lt;strong&gt;discipline&lt;/strong&gt; rule reminds the model of something it already knows how to do: run the filtered test first, read the report, don't touch the app. Its Δ is small, depends on the model, and shrinks with every release.&lt;/p&gt;

&lt;p&gt;The third-party component case is the clearest example. The skill explains how to replace a component with a fake, but the library's public API for that is in Opus's training data. Δ is 0.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the extra lines cost
&lt;/h2&gt;

&lt;p&gt;Every run also records its cost and turns. That changes the question from "does this rule help?" to "what does this rule cost when it doesn't?"&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Case, Opus&lt;/th&gt;
&lt;th&gt;Score with / without&lt;/th&gt;
&lt;th&gt;Cost with / without&lt;/th&gt;
&lt;th&gt;Turns with / without&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Replace a third-party component&lt;/td&gt;
&lt;td&gt;1.00 / 1.00&lt;/td&gt;
&lt;td&gt;$0.38 / $0.24&lt;/td&gt;
&lt;td&gt;15 / 10&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Set the library up&lt;/td&gt;
&lt;td&gt;1.00 / 0.10&lt;/td&gt;
&lt;td&gt;$0.36 / $0.18&lt;/td&gt;
&lt;td&gt;21 / 7&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Both cases cost more with the skill. They mean opposite things.&lt;/p&gt;

&lt;p&gt;In the component case, the score is the same and the skill makes every run 57% more expensive, with five extra turns. For Opus, there the skill is pure cost.&lt;/p&gt;

&lt;p&gt;In the setup case, the skill doubles the cost, and without it the setup is wrong. That extra cost is what doing it right costs. Fewer turns and a lower bill only mean something when the score is the same.&lt;/p&gt;

&lt;h2&gt;
  
  
  Don't delete for one model
&lt;/h2&gt;

&lt;p&gt;Before deleting the component instructions, look at the same case on the other models:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Replace a third-party component&lt;/th&gt;
&lt;th&gt;Δ&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Opus 5.5&lt;/td&gt;
&lt;td&gt;0.00&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sonnet 5.5&lt;/td&gt;
&lt;td&gt;+0.29&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Haiku 5.5&lt;/td&gt;
&lt;td&gt;+0.48&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The smaller the model, the more the skill adds. The rule Opus doesn't need is the one Haiku needs most. So the question isn't "is this rule useful?" but "is it useful for the models my users run?"&lt;/p&gt;

&lt;p&gt;The rule I follow now: a section is a candidate for deletion when its Δ is near zero on &lt;strong&gt;every&lt;/strong&gt; model I support. Then I delete it and run the suite again. The score with the plugin has to stay where it was. If it drops, the section was doing something the Δ didn't show, and it goes back.&lt;/p&gt;

&lt;p&gt;It's TDD in reverse: the eval stays green while the skill gets shorter.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>claudecode</category>
      <category>testing</category>
      <category>productivity</category>
    </item>
    <item>
      <title>The score tells you what failed. The trace tells you why.</title>
      <dc:creator>Kevin Julián Martínez Escobar</dc:creator>
      <pubDate>Fri, 30 Oct 2026 10:51:27 +0000</pubDate>
      <link>https://dev.to/kevinccbsg/the-score-tells-you-what-failed-the-trace-tells-you-why-1fkj</link>
      <guid>https://dev.to/kevinccbsg/the-score-tells-you-what-failed-the-trace-tells-you-why-1fkj</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Don't change a skill because of a score. Change it because of what the trace showed.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;A case scores 0.25. Three graders failed: the test file is missing, so every check on its contents failed too. That's all the score can tell you. Was it the skill, the model, the sandbox, or the eval itself? Each one needs a different fix, and a score can't tell them apart.&lt;/p&gt;

&lt;p&gt;The trace can. Every eval run with &lt;code&gt;claude plugin eval&lt;/code&gt; records one: a log of every step the agent took, what each tool returned, and how the run ended.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keep the trace
&lt;/h2&gt;

&lt;p&gt;By default the eval deletes each run's folder. Pass &lt;code&gt;--keep-temp&lt;/code&gt; and it prints where each one is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;claude plugin &lt;span class="nb"&gt;eval&lt;/span&gt; &lt;span class="nb"&gt;.&lt;/span&gt; &lt;span class="nt"&gt;--scaffold&lt;/span&gt; &lt;span class="nt"&gt;--case&lt;/span&gt; setup-vite-react-query &lt;span class="nt"&gt;--runs&lt;/span&gt; 1 &lt;span class="nt"&gt;--keep-temp&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--allow-tools&lt;/span&gt; Write Edit
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The folder is sealed read-only, so open it first, and don't run git inside it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;chmod &lt;/span&gt;700 /private/tmp/e-XXXX /private/tmp/e-XXXX/sealed
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The trace is &lt;code&gt;out/trace.jsonl&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's in it
&lt;/h2&gt;

&lt;p&gt;JSONL: one JSON object per line, each with a &lt;code&gt;type&lt;/code&gt;. A run of 20 turns had almost a thousand lines, but only a few kinds matter:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;code&gt;type&lt;/code&gt;&lt;/th&gt;
&lt;th&gt;What it is&lt;/th&gt;
&lt;th&gt;Read it?&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;system&lt;/code&gt; / &lt;code&gt;init&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;First line: model, available tools, permission mode&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;system&lt;/code&gt; / &lt;code&gt;thinking_tokens&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Reasoning token counters, most of the file&lt;/td&gt;
&lt;td&gt;No, noise&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;system&lt;/code&gt; / &lt;code&gt;permission_denied&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;A tool call the sandbox refused&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;assistant&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;What the model did: text, or a tool call&lt;/td&gt;
&lt;td&gt;Yes, this is the story&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;user&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;What the tool returned&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;result&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Last line: turns, cost, final message&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Make it readable with jq
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://jqlang.org/" rel="noopener noreferrer"&gt;jq&lt;/a&gt; queries JSON from the command line, and JSONL is exactly what it's good at. This filter prints the conversation, one line per step:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;select(.type=="assistant" or .type=="user") | .type as $who | .message.content[]? |
  if .type=="text" then "\($who) 💬 \(.text[0:150])"
  elif .type=="tool_use" then "\($who) 🔧 \(.name) \(.input|tostring|.[0:120])"
  elif .type=="tool_result" then "\($who) ↩️  \(.content|tostring|.[0:150])"
  else empty end
&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;jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; conversation.jq /private/tmp/e-XXXX/out/trace.jsonl
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A few more I run on every trace:&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;# Which tools, how often&lt;/span&gt;
jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s1"&gt;'select(.type=="assistant")|.message.content[]?|select(.type=="tool_use")|.name'&lt;/span&gt; trace.jsonl | &lt;span class="nb"&gt;sort&lt;/span&gt; | &lt;span class="nb"&gt;uniq&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt;

&lt;span class="c"&gt;# Which files it read: the context it loaded&lt;/span&gt;
jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s1"&gt;'select(.type=="assistant")|.message.content[]?|select(.type=="tool_use" and .name=="Read")|.input.file_path'&lt;/span&gt; trace.jsonl

&lt;span class="c"&gt;# What was refused&lt;/span&gt;
jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s1"&gt;'select(.type=="user")|.message.content[]?|select(.type=="tool_result" and .is_error==true)|.content|tostring|.[0:100]'&lt;/span&gt; trace.jsonl

&lt;span class="c"&gt;# Turns and cost&lt;/span&gt;
jq &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s1"&gt;'select(.type=="result")|{num_turns,total_cost_usd,duration_ms}'&lt;/span&gt; trace.jsonl
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The method: find where they split
&lt;/h2&gt;

&lt;p&gt;One trace on its own is hard to judge. Two traces of the same case, one that passed and one that failed, are easy. Put their tool calls side by side and look for the &lt;strong&gt;first step where they differ&lt;/strong&gt;. Everything after it is a consequence.&lt;/p&gt;

&lt;p&gt;This is a setup skill on Opus, with the plugin (left, score 1.00) and without it (right, score 0.10):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Bash cat package.json vite.config.ts ...  |  Bash cat package.json vite.config.ts ...
Read package.json                         |  Bash cat tsconfig.app.json eslint.config.js ...
Read vite.config.ts                       |  Write src/main.tsx
Read index.html                           |  Write tsconfig.app.json
Read eslint.config.js                     |  Write src/twd-tests/todos.twd.test.ts
Glob **/*                                 |  Edit package.json
Read src/query-client.ts                  &amp;lt;
Read src/main.tsx                         &amp;lt;
Read .../skills/setup/references/server-state-cache.md
Read .../skills/setup/references/twd-lint.md
Read tsconfig.app.json                    &amp;lt;
Write .claude/twd-patterns.md             &amp;lt;
Write vite.config.ts                      &amp;lt;
Write twd.config.json                     &amp;lt;
...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;By the third step, one of them is writing. The run without the plugin is already writing, and its first file is the app's entry point, the old way to set the library up. The run with the plugin keeps detecting, and reads the two reference files that match what it found in the project. The model's own words at the split confirm it: "The &lt;code&gt;/twd:setup&lt;/code&gt; command isn't installed in this session, so I'm doing the TWD setup by hand from what I know of &lt;code&gt;twd-js&lt;/code&gt;."&lt;/p&gt;

&lt;p&gt;Then four questions:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;init&lt;/code&gt;&lt;/strong&gt;: which model, which tools?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The split&lt;/strong&gt;: the first step where the passing and failing runs differ.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Just before it&lt;/strong&gt;: what did the last tool return?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;At it&lt;/strong&gt;: what did the model decide with that? Read its text around this point only.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Strict on purpose
&lt;/h2&gt;

&lt;p&gt;Reading denials, I found the run with the plugin had a shell command refused: one long detection command chaining &lt;code&gt;cat&lt;/code&gt;, &lt;code&gt;find&lt;/code&gt;, &lt;code&gt;git&lt;/code&gt; and &lt;code&gt;grep&lt;/code&gt;. The model switched to reading the files one by one and still scored 1.00.&lt;/p&gt;

&lt;p&gt;The eval runs in a mode that refuses anything not allowed in advance. In daily use you'd approve that command, or in auto mode a classifier would. So the eval is stricter than real use, which is the right way round: a skill that passes here passes in your terminal. The opposite, an eval more permissive than reality, would hand you inflated scores.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I check, in order
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;The score, to know which case and which graders.&lt;/li&gt;
&lt;li&gt;Two traces side by side, one passing and one failing, to find the split.&lt;/li&gt;
&lt;li&gt;The model's text around the split, to see the decision.&lt;/li&gt;
&lt;li&gt;Only then, the fix: the skill, the eval, or neither.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Most of the surprises in my eval work came from this file, not from the scores: a forked skill that never received its task, a sandbox that couldn't run &lt;code&gt;npx&lt;/code&gt;, a model that stopped after the first green run. The score said something failed. The trace said what to do about it.&lt;/p&gt;

&lt;p&gt;Next in this series: what the score without your skill tells you about which parts of it to delete.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>claudecode</category>
      <category>testing</category>
      <category>debugging</category>
    </item>
    <item>
      <title>"I tried it and it worked" is not a test for your Claude Code skills</title>
      <dc:creator>Kevin Julián Martínez Escobar</dc:creator>
      <pubDate>Thu, 29 Oct 2026 09:11:13 +0000</pubDate>
      <link>https://dev.to/kevinccbsg/i-tried-it-and-it-worked-is-not-a-test-for-your-claude-code-skills-2h39</link>
      <guid>https://dev.to/kevinccbsg/i-tried-it-and-it-worked-is-not-a-test-for-your-claude-code-skills-2h39</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;A skill that works when you try it has passed one test, graded by you, once.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;For months, this is how I tested my Claude Code skills. Open a sample project, create a clean branch, run the skill, read what it wrote. If it looked right, ship it.&lt;/p&gt;

&lt;p&gt;That isn't nothing. It's a real test with a human grader: you set up the project, you write the prompt, you judge the result. The problem is everything around it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;It runs once.&lt;/strong&gt; An agent doesn't do the same thing twice. One good run can be luck.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It runs on your project&lt;/strong&gt;, the one you wrote the skill against.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It runs on today's model.&lt;/strong&gt; When the model changes, nobody tries it again.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It needs you.&lt;/strong&gt; So it doesn't run in CI, and it doesn't run after the small edit that "can't break anything".&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The example I use throughout this series is &lt;a href="https://github.com/BRIKEV/twd-ai" rel="noopener noreferrer"&gt;twd-ai&lt;/a&gt;, the Claude Code plugin for &lt;a href="https://twd.dev" rel="noopener noreferrer"&gt;TWD&lt;/a&gt;, a browser testing library I maintain. It has a skill that sets the library up in a project, one that writes the CI workflow, and one that writes, runs and fixes tests. Nothing in these articles depends on it: swap in your own plugin and every step applies.&lt;/p&gt;

&lt;h2&gt;
  
  
  What an eval is
&lt;/h2&gt;

&lt;p&gt;An eval is a test for an agent's behaviour. Instead of checking that a function returns a value, it checks that Claude, with your skill loaded, does what the skill promises. It runs for real: Claude reads files, writes code and runs commands, and when it's done, a set of checks scores the result.&lt;/p&gt;

&lt;p&gt;Claude Code ships one: &lt;a href="https://code.claude.com/docs/en/plugin-evals" rel="noopener noreferrer"&gt;&lt;code&gt;claude plugin eval&lt;/code&gt;&lt;/a&gt;. Each case has three parts:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;A scaffold&lt;/strong&gt;: a script that creates a small starting project in a temporary folder.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A prompt&lt;/strong&gt;: what a user would type.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Graders&lt;/strong&gt;: the checks that score what happened.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A trimmed case from my suite:&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;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;setup-vite-react-query&lt;/span&gt;
&lt;span class="na"&gt;context&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;scaffold_script&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;scaffold.sh&lt;/span&gt;
&lt;span class="na"&gt;execution&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;allowed_tools&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;Read&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;Glob&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;Grep&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;Skill&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
  &lt;span class="na"&gt;prompt&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/twd:setup&lt;/span&gt;
&lt;span class="na"&gt;graders&lt;/span&gt;&lt;span class="pi"&gt;:&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;vite-plugin-imported&lt;/span&gt;
    &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;regex&lt;/span&gt;
    &lt;span class="na"&gt;pattern&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;from\s+["'&lt;/span&gt;&lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;]twd-js/vite-plugin["'&lt;/span&gt;&lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;]'&lt;/span&gt;
    &lt;span class="na"&gt;target&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;source&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;file&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;vite.config.ts&lt;/span&gt; &lt;span class="pi"&gt;}&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;no-entry-file-code&lt;/span&gt;
    &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;regex&lt;/span&gt;
    &lt;span class="na"&gt;pattern&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;initTWD|twd-js'&lt;/span&gt;
    &lt;span class="na"&gt;match&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;not_contains&lt;/span&gt;
    &lt;span class="na"&gt;target&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;source&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;file&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;src/main.tsx&lt;/span&gt; &lt;span class="pi"&gt;}&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;patterns-file-attempted&lt;/span&gt;
    &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;tool_used&lt;/span&gt;
    &lt;span class="na"&gt;tool&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Write&lt;/span&gt;
    &lt;span class="na"&gt;input_match&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;\.claude/twd-patterns\.md'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The last grader reads the agent's actions, not the disk: eval runs refuse writes into &lt;code&gt;.claude/&lt;/code&gt;, so it checks that the skill tried to write its notes file there.&lt;/p&gt;

&lt;p&gt;The scaffold writes a small Vite + React app with heredocs. It installs nothing, so the case needs no network.&lt;/p&gt;

&lt;p&gt;Graders come in two families. Some check &lt;strong&gt;what was written&lt;/strong&gt;: a pattern is in a file, or isn't. Others check &lt;strong&gt;what the agent did&lt;/strong&gt;: it called a tool, with this input, before that other call. For what a regex can't decide, there's an &lt;code&gt;llm&lt;/code&gt; grader: a judge model reads the file against a criterion you write and votes pass or fail, three times.&lt;/p&gt;

&lt;h2&gt;
  
  
  With and without your skill
&lt;/h2&gt;

&lt;p&gt;This is the part I didn't expect to matter most. Every case runs twice: &lt;strong&gt;with&lt;/strong&gt; your plugin and &lt;strong&gt;without&lt;/strong&gt; it. The difference, Δ, is what your skill adds on top of what the model already knows.&lt;/p&gt;

&lt;p&gt;My setup case on Opus:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Score&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;With the plugin&lt;/td&gt;
&lt;td&gt;1.00&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Without the plugin&lt;/td&gt;
&lt;td&gt;0.10&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Δ&lt;/td&gt;
&lt;td&gt;+0.90&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Without the skill, the model reads the same project files, says the setup command "isn't installed in this session", and does it by hand from what it knows. It wires the library into the app's entry file, which the library no longer needs, and never writes the config file.&lt;/p&gt;

&lt;p&gt;A Δ near zero doesn't mean a case is useless. It means the model already knows how to do it. The case still guards against an edit to the skill that breaks it: if that happens, the score with the plugin drops.&lt;/p&gt;

&lt;h2&gt;
  
  
  Run it more than once
&lt;/h2&gt;

&lt;p&gt;Because an agent doesn't behave the same every time, each case runs three times and the score is the average. A run's score is the share of graders that passed. &lt;code&gt;--threshold 0.9&lt;/code&gt; decides what counts as a pass: it tolerates a grader failing once in three runs, not one that fails every time.&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;# While editing a skill: one run, with the plugin only&lt;/span&gt;
claude plugin &lt;span class="nb"&gt;eval&lt;/span&gt; &lt;span class="nb"&gt;.&lt;/span&gt; &lt;span class="nt"&gt;--scaffold&lt;/span&gt; &lt;span class="nt"&gt;--runs&lt;/span&gt; 1 &lt;span class="nt"&gt;--ablation&lt;/span&gt; none &lt;span class="nt"&gt;--allow-tools&lt;/span&gt; Write Edit

&lt;span class="c"&gt;# Before a release: three runs, with and without the plugin&lt;/span&gt;
claude plugin &lt;span class="nb"&gt;eval&lt;/span&gt; &lt;span class="nb"&gt;.&lt;/span&gt; &lt;span class="nt"&gt;--scaffold&lt;/span&gt; &lt;span class="nt"&gt;--threshold&lt;/span&gt; 0.9 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--model&lt;/span&gt; claude-opus-5-5 &lt;span class="nt"&gt;--judge-model&lt;/span&gt; claude-sonnet-5-5 &lt;span class="nt"&gt;--allow-tools&lt;/span&gt; Write Edit
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The full suite, eight cases on Opus, costs about $10 and 25 minutes. The quick mode is under $2.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fakes: mock your tools like you mock your APIs
&lt;/h2&gt;

&lt;p&gt;The eval sandbox has no browser and no network, and one of my skills runs tests in a browser. So the scaffold writes a stand-in test runner: it honours the filter flag, writes a report in the same format as the real one, and fails a test that mocks a URL the app never requests.&lt;/p&gt;

&lt;p&gt;It's the same idea as mocking an API in a test. You're testing how the skill reacts to the tool, not the tool. The real runner has its own tests in its own repo.&lt;/p&gt;

&lt;h2&gt;
  
  
  Graders that lie
&lt;/h2&gt;

&lt;p&gt;A failing grader is fine: it tells you something. The dangerous one passes when it shouldn't, because nobody looks at a green check. It happened to me three times:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A regex that read too far.&lt;/strong&gt; &lt;code&gt;beforeEach\([\s\S]*?twd\.clearComponentMocks&lt;/code&gt; was meant to check that mocks are cleared in &lt;code&gt;beforeEach&lt;/code&gt;. The lazy match kept going into the &lt;code&gt;afterEach&lt;/code&gt; below it and passed a file that cleared them in the wrong place.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"The skill was invoked" passed, and the skill did nothing.&lt;/strong&gt; The skill ran in a forked subagent that only saw its arguments. Invoked without arguments, it replied that it had no goal, and the main agent wrote the tests on its own, without the skill's rules. The test looked fine.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A check on the call, not the result.&lt;/strong&gt; Two graders checked that the skill ran the type checker and the linter. They looked at whether the command was called, not whether it worked. For a while every one of those commands failed for an unrelated reason (Node was installed under my home folder, which the sandbox can't read), and both graders stayed green.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Before trusting a new grader, run it against a file that should pass and one that should fail.&lt;/p&gt;

&lt;h2&gt;
  
  
  It doesn't replace trying it
&lt;/h2&gt;

&lt;p&gt;I still run my skills by hand on real projects. The eval doesn't replace that; it tells me when to. When a case drops after an edit, or after a model release, I know exactly where to look.&lt;/p&gt;

&lt;p&gt;The scenarios twd-ai guards, and their scores per model, are on &lt;a href="https://twd.dev/twd-ai/evals" rel="noopener noreferrer"&gt;twd.dev&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Next in this series: reading the trace, the file that turns a score into a reason.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>claudecode</category>
      <category>testing</category>
      <category>productivity</category>
    </item>
    <item>
      <title>The Testing Trophy, and Where TWD Fits</title>
      <dc:creator>Kevin Julián Martínez Escobar</dc:creator>
      <pubDate>Mon, 26 Oct 2026 10:42:31 +0000</pubDate>
      <link>https://dev.to/kevinccbsg/the-testing-trophy-and-where-twd-fits-2mjm</link>
      <guid>https://dev.to/kevinccbsg/the-testing-trophy-and-where-twd-fits-2mjm</guid>
      <description>&lt;p&gt;Most frontend teams I talk to have plenty of tests and still find out about broken screens from users. The unit tests are green, the refactor broke every one of them anyway, and nobody is sure the checkout flow works until someone clicks through it.&lt;/p&gt;

&lt;p&gt;That tension is exactly what the Testing Trophy is about.&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%2Fe0ltjh086x4dovro2kcq.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%2Fe0ltjh086x4dovro2kcq.png" alt="The Testing Trophy with TWD on the integration layer" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The trophy
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://kentcdodds.com/blog/the-testing-trophy-and-testing-classifications" rel="noopener noreferrer"&gt;Kent C. Dodds&lt;/a&gt; drew testing as a trophy instead of a pyramid. Four layers, and the size of each one is roughly how much of your effort it deserves:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Static:&lt;/strong&gt; typos and type errors caught as you write the code. TypeScript, ESLint.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Unit:&lt;/strong&gt; one function, hook or component checked in isolation. Vitest, Jest.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Integration:&lt;/strong&gt; several units working together. The biggest part of the trophy.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;End to End:&lt;/strong&gt; a robot clicking through the deployed app, real backend included. Playwright, Cypress.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The shape comes from one line, written by Guillermo Rauch and popularized by Kent:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Write tests. Not too many. Mostly integration.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Each layer trades speed for confidence. Unit tests are cheap, but they can pass while the screen is broken. End-to-end tests give the most confidence, but they need the whole system running and are the slowest to write and run. Integration tests sit in the middle: they use the app the way a user does, and they stay fast and deterministic because the only thing faked is what you don't own.&lt;/p&gt;

&lt;p&gt;Kent's guiding principle for them:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The more your tests resemble the way your software is used, the more confidence they can give you.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  What integration means on the frontend
&lt;/h2&gt;

&lt;p&gt;For a frontend app, an integration test renders the real thing (routes, providers, state, components) and mocks only the network. Then it does what a user would: visit a page, type, click, and check what is on the screen.&lt;/p&gt;

&lt;p&gt;The classic way to do this is &lt;a href="https://www.npmjs.com/package/@testing-library/dom" rel="noopener noreferrer"&gt;Testing Library&lt;/a&gt; with jsdom, in Vitest or Jest. It is a good setup and it is where most of us learned this style. It runs in a simulated DOM in your terminal, though, separate from the app you have open while you build.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where TWD fits
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://www.npmjs.com/package/twd-js" rel="noopener noreferrer"&gt;TWD&lt;/a&gt; is built for that middle layer, and it runs the tests in the browser you develop in:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Next to your app.&lt;/strong&gt; A sidebar in your dev server runs the tests against the real app. You watch each one drive the page.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Mock what you don't own.&lt;/strong&gt; API calls are answered by a service worker, and contract testing checks every mock against your OpenAPI spec, so the mocks don't drift from the real API.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tests that read like the user.&lt;/strong&gt; Testing Library queries find elements by role, label and text.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The same tests everywhere.&lt;/strong&gt; &lt;a href="https://www.npmjs.com/package/twd-cli" rel="noopener noreferrer"&gt;twd-cli&lt;/a&gt; runs them headlessly in CI, with coverage and a video per test if you want one, and your AI agent can write, run and fix them.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In practice this layer can carry a lot. On two production apps we run 872 tests on every pull request, mostly user flows, in one headless Chrome on a 2-vCPU GitHub runner, in about 5 minutes per app.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it doesn't replace
&lt;/h2&gt;

&lt;p&gt;TWD doesn't cover the whole trophy, and it shouldn't.&lt;/p&gt;

&lt;p&gt;Keep a few end-to-end tests with Playwright or Cypress for the critical paths, the ones that need the real backend, the payment provider or the third-party SDK. Use unit tests for logic that is easier to check on its own. Keep TypeScript and ESLint at the base (and &lt;a href="https://twd.dev/linting" rel="noopener noreferrer"&gt;twd-lint&lt;/a&gt; for your test files). Then let integration tests carry most of the weight.&lt;/p&gt;

&lt;h2&gt;
  
  
  A map, not a ritual
&lt;/h2&gt;

&lt;p&gt;TWD doesn't ask you to pick a layer before you write a test. You automate the check you were already doing by hand while you build. The trophy just shows where those checks end up: mostly in the middle.&lt;/p&gt;

&lt;p&gt;The full page, with the trophy and the tools per layer, is at &lt;a href="https://twd.dev/testing-trophy" rel="noopener noreferrer"&gt;twd.dev/testing-trophy&lt;/a&gt;. For the strategy itself, go to the source: Kent's &lt;a href="https://www.testingjavascript.com/" rel="noopener noreferrer"&gt;TestingJavaScript.com&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>testing</category>
      <category>twd</category>
      <category>frontend</category>
      <category>webdev</category>
    </item>
    <item>
      <title>One CLI, one config file: adding browser tests to a Vite+ project</title>
      <dc:creator>Kevin Julián Martínez Escobar</dc:creator>
      <pubDate>Fri, 16 Oct 2026 12:32:44 +0000</pubDate>
      <link>https://dev.to/kevinccbsg/one-cli-one-config-file-adding-browser-tests-to-a-vite-project-2onn</link>
      <guid>https://dev.to/kevinccbsg/one-cli-one-config-file-adding-browser-tests-to-a-vite-project-2onn</guid>
      <description>&lt;p&gt;A typical frontend setup has an ESLint config, a Prettier config, lint-staged and Husky for the commit hook, and the Vite config, each with its own command and a &lt;code&gt;package.json&lt;/code&gt; script to glue them together.&lt;/p&gt;

&lt;p&gt;A fresh &lt;a href="https://viteplus.dev/guide/" rel="noopener noreferrer"&gt;Vite+&lt;/a&gt; project has one config file, &lt;code&gt;vite.config.ts&lt;/code&gt;. I wanted to see if a test tool could join that file without bringing its own sprawl. This is what happened when I added &lt;a href="https://twd.dev" rel="noopener noreferrer"&gt;TWD&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Vite+ actually unifies
&lt;/h2&gt;

&lt;p&gt;Vite+ is a toolchain from VoidZero that puts Vite, Vitest, &lt;a href="https://oxc.rs/docs/guide/usage/linter.html" rel="noopener noreferrer"&gt;Oxlint&lt;/a&gt;, Oxfmt, Rolldown and tsdown behind a single CLI called &lt;code&gt;vp&lt;/code&gt;. It also manages Node and your package manager.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;vp &lt;span class="nb"&gt;install&lt;/span&gt;   &lt;span class="c"&gt;# the right package manager, picked for you&lt;/span&gt;
vp dev       &lt;span class="c"&gt;# Vite&lt;/span&gt;
vp check     &lt;span class="c"&gt;# format + lint + type check, in one pass&lt;/span&gt;
vp build     &lt;span class="c"&gt;# production build&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The command I like most is &lt;code&gt;vp check&lt;/code&gt;. It runs formatting, linting and &lt;strong&gt;type checking&lt;/strong&gt; together, the type checking through Oxlint's type-aware path on the TypeScript Go toolchain. On the demo app it checks 22 files in about a second.&lt;/p&gt;

&lt;p&gt;Configuration follows the same idea. Lint rules, formatter options and the pre-commit hook all live in &lt;code&gt;vite.config.ts&lt;/code&gt;:&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;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nf"&gt;defineConfig&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;staged&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;*&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;vp check --fix&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="c1"&gt;// the pre-commit hook. No Husky, no lint-staged&lt;/span&gt;
  &lt;span class="na"&gt;lint&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;options&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;typeAware&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;typeCheck&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;fmt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;semi&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;singleQuote&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;plugins&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;lazyPlugins&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;vue&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;That is the bar any new tool has to clear: &lt;strong&gt;does it fit in here, or does it bring a new file and a new command?&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The dev server: one plugin
&lt;/h2&gt;

&lt;p&gt;TWD runs tests inside the app you are already developing, in a sidebar next to the page. It is wired in as a Vite plugin:&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="nx"&gt;plugins&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;lazyPlugins&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="nf"&gt;vue&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="nf"&gt;twd&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;testFilePattern&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/**/*.twd.test.ts&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;That is all. The plugin finds the test files and injects the sidebar while &lt;code&gt;vp dev&lt;/code&gt; is running, and does nothing in &lt;code&gt;vp build&lt;/code&gt;. &lt;code&gt;main.ts&lt;/code&gt; has no test code in it, and the production bundle has none either.&lt;/p&gt;

&lt;h2&gt;
  
  
  Linting: an ESLint plugin, running in Oxlint
&lt;/h2&gt;

&lt;p&gt;This was the part I expected to lose.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.npmjs.com/package/twd-lint" rel="noopener noreferrer"&gt;twd-lint&lt;/a&gt; catches the mistakes that make a browser test fail in confusing ways: a missing &lt;code&gt;await&lt;/code&gt; on &lt;code&gt;twd.visit()&lt;/code&gt;, a stray &lt;code&gt;it.only&lt;/code&gt;, &lt;code&gt;describe&lt;/code&gt; imported from Vitest instead of the TWD runner. It ships as an ESLint plugin. Vite+ lints with Oxlint, not ESLint.&lt;/p&gt;

&lt;p&gt;Oxlint can load ESLint plugins as JS plugins, unchanged. So the rules go into the same &lt;code&gt;lint&lt;/code&gt; block as everything else, scoped to test files with an override:&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="nx"&gt;lint&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;jsPlugins&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;twd&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;specifier&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;twd-lint&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}],&lt;/span&gt;
  &lt;span class="nx"&gt;overrides&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;files&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;**/*.twd.test.ts&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
      &lt;span class="na"&gt;rules&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;twd/no-focused-tests&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;error&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;twd/runner-imports&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;error&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;twd/no-missing-await&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;error&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;twd/no-request-body&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;error&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;span class="p"&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;I tested it against a deliberately broken test file. &lt;code&gt;vp lint&lt;/code&gt; reported the same five errors as the standalone &lt;code&gt;twd-lint&lt;/code&gt; CLI, word for word. &lt;code&gt;vp lint --fix&lt;/code&gt; rewrote the Vitest import and added the missing &lt;code&gt;await&lt;/code&gt;. The rules did not fire on the same code outside a test file.&lt;/p&gt;

&lt;p&gt;Since the hook already runs &lt;code&gt;vp check --fix&lt;/code&gt; on staged files, every commit lints the tests too. Nothing to wire up.&lt;/p&gt;

&lt;h2&gt;
  
  
  The tests: mocking at the network layer
&lt;/h2&gt;

&lt;p&gt;The demo is a Todos page with no backend. Every API call in the tests is answered by &lt;code&gt;twd.mockRequest&lt;/code&gt;, which makes the states that are annoying to reach by hand easy to test:&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;await&lt;/span&gt; &lt;span class="nx"&gt;twd&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;mockRequest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;getTodos&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;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;GET&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api/todos&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;response&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;todoList&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;delay&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;twd&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;visit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/todos&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;loading&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;screenDom&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findByText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Loading todos...&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nx"&gt;twd&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;should&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;loading&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;be.visible&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;twd&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;waitForRequest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;getTodos&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;delay&lt;/code&gt; holds the response long enough to assert the loading state. The other tests use the same idea:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Error and retry:&lt;/strong&gt; a &lt;code&gt;500&lt;/code&gt; shows the error, then the test mocks a good response and clicks Retry.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Request body:&lt;/strong&gt; &lt;code&gt;waitForRequest('createTodo')&lt;/code&gt; returns the parsed JSON the page sent.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No call at all:&lt;/strong&gt; &lt;code&gt;getRequestCount('createTodo')&lt;/code&gt; is &lt;code&gt;0&lt;/code&gt; when validation blocks an empty title.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Network failure:&lt;/strong&gt; &lt;code&gt;status: 0&lt;/code&gt; simulates a dropped connection.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Eight tests, headless, in about four seconds.&lt;/p&gt;

&lt;h2&gt;
  
  
  Coverage: one more plugin
&lt;/h2&gt;

&lt;p&gt;Coverage is &lt;code&gt;vite-plugin-istanbul&lt;/code&gt; added to the same plugin list, switched on only when &lt;code&gt;CI=true&lt;/code&gt; so the normal dev server stays fast:&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="nf"&gt;istanbul&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;include&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;src/*&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;exclude&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;node_modules&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;**/*.twd.test.ts&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;**/twd-tests/**&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;requireEnv&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;CI&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;extension&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;.ts&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;.vue&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;&lt;a href="https://www.npmjs.com/package/twd-cli" rel="noopener noreferrer"&gt;twd-cli&lt;/a&gt; collects the coverage at the end of a headless run, and &lt;code&gt;nyc&lt;/code&gt; prints the report. The Todos page came out at 100% of lines.&lt;/p&gt;

&lt;p&gt;One thing tripped me up: &lt;code&gt;nyc report&lt;/code&gt; ignores &lt;code&gt;.vue&lt;/code&gt; files by default, and &lt;code&gt;--extension .vue&lt;/code&gt; replaces the default list instead of adding to it. My first report had the &lt;code&gt;.vue&lt;/code&gt; files and had lost every &lt;code&gt;.ts&lt;/code&gt; file. Pass both:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;nyc report &lt;span class="nt"&gt;--reporter&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;text &lt;span class="nt"&gt;--temp-dir&lt;/span&gt; .nyc_output &lt;span class="nt"&gt;--extension&lt;/span&gt; .ts &lt;span class="nt"&gt;--extension&lt;/span&gt; .vue
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  CI: the same commands, in a workflow
&lt;/h2&gt;

&lt;p&gt;Vite+ has a GitHub Action, &lt;code&gt;voidzero-dev/setup-vp&lt;/code&gt;, that installs Node, the package manager and the dependencies in one step. After that the workflow is just the commands from the terminal:&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="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;voidzero-dev/setup-vp@v1.21.2&lt;/span&gt;
  &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;node-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;24'&lt;/span&gt;
    &lt;span class="na"&gt;cache&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;vp check&lt;/span&gt;                       &lt;span class="c1"&gt;# lint includes the TWD rules&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
    &lt;span class="s"&gt;nohup vp run dev:ci &amp;gt; vite.log 2&amp;gt;&amp;amp;1 &amp;amp;&lt;/span&gt;
    &lt;span class="s"&gt;timeout 60 bash -c 'until curl -s -o /dev/null http://localhost:5173; do sleep 1; done'&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;vp exec twd-cli run&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;vp run collect:coverage:text&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two details are specific to Vite+ on pnpm:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;npx&lt;/code&gt; does not work here.&lt;/strong&gt; The project pins pnpm through &lt;code&gt;devEngines&lt;/code&gt;, and npm refuses to run (&lt;code&gt;EBADDEVENGINES&lt;/code&gt;). Use &lt;code&gt;vp exec&lt;/code&gt;. That also rules out the twd-cli composite actions for now, because they call &lt;code&gt;npx&lt;/code&gt; internally.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;pnpm blocks install scripts by default&lt;/strong&gt;, including the one where puppeteer downloads Chrome. One line in &lt;code&gt;pnpm-workspace.yaml&lt;/code&gt; (&lt;code&gt;allowBuilds: { puppeteer: true }&lt;/code&gt;) and &lt;code&gt;vp install&lt;/code&gt; fetches it, locally and in CI.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What it adds up to
&lt;/h2&gt;

&lt;p&gt;Counting what the test setup added to the project:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a plugin in &lt;code&gt;vite.config.ts&lt;/code&gt;, plus two more entries in its &lt;code&gt;lint&lt;/code&gt; block&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;twd.config.json&lt;/code&gt;, two lines long&lt;/li&gt;
&lt;li&gt;one generated service worker in &lt;code&gt;public/&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;a &lt;code&gt;test:ci&lt;/code&gt; script&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;No new linter, no new hook manager, no new config format. TWD fits into the tools Vite+ already gives you.&lt;/p&gt;

&lt;p&gt;The whole project is on GitHub: &lt;a href="https://github.com/BRIKEV/twd-vp-vue" rel="noopener noreferrer"&gt;BRIKEV/twd-vp-vue&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>vite</category>
      <category>vue</category>
      <category>testing</category>
      <category>twd</category>
    </item>
    <item>
      <title>Your library isn't in the training data: making AI agents use it well</title>
      <dc:creator>Kevin Julián Martínez Escobar</dc:creator>
      <pubDate>Tue, 13 Oct 2026 22:04:47 +0000</pubDate>
      <link>https://dev.to/kevinccbsg/your-library-isnt-in-the-training-data-making-ai-agents-use-it-well-5a34</link>
      <guid>https://dev.to/kevinccbsg/your-library-isnt-in-the-training-data-making-ai-agents-use-it-well-5a34</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;A model that doesn't know your library doesn't fail by doing nothing. It fails by confidently writing code for a library it does know.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Ask an AI agent to write a React component with Jest tests and it'll do a decent job. There are millions of examples of both in its training data.&lt;/p&gt;

&lt;p&gt;Now ask it to use something it has never seen:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;your company's internal API client;&lt;/li&gt;
&lt;li&gt;your design system;&lt;/li&gt;
&lt;li&gt;a niche open source library;&lt;/li&gt;
&lt;li&gt;a new major version that changed everything.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You'll get code that looks right, compiles in the agent's head, and is wrong in small, confident ways. The agent isn't guessing at random. It fills every gap with the closest thing it knows. An option name from a similar library. An import from the popular alternative. A return shape it assumed.&lt;/p&gt;

&lt;p&gt;I've spent the last months on this problem with &lt;a href="https://twd.dev" rel="noopener noreferrer"&gt;TWD&lt;/a&gt;, a testing library I maintain that almost no model knows yet, and a Claude Code plugin that writes tests with it. This is the toolkit I ended up with. None of it is specific to my library.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Give the model knowledge, not just rules
&lt;/h2&gt;

&lt;p&gt;Whatever you hand the agent (a skill, an &lt;code&gt;AGENTS.md&lt;/code&gt;, an &lt;code&gt;llms.txt&lt;/code&gt;, a docs page), it is the only place the model can learn your API. So spend that space on what teaches:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Signatures written out in full&lt;/strong&gt;, with the option names spelled correctly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;What things return&lt;/strong&gt;, especially when it differs from the popular library the model will assume.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The patterns&lt;/strong&gt;: the order things have to happen in, the setup every file needs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Complete, correct examples.&lt;/strong&gt; With a model, one working example beats three paragraphs of explanation.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What you shouldn't fill it with is warnings. A list of "never do X" written three times doesn't stop the mistake; a program checking for it does. I wrote a whole post about why: &lt;a href="https://kevinmartinez.dev/posts/prose-does-not-beat-determinism-stop-asking-your-ai-agent-to-remember/" rel="noopener noreferrer"&gt;Prose does not beat determinism&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Load it in layers
&lt;/h3&gt;

&lt;p&gt;Context isn't free, and the agent pays for every byte on every request. My first reference doc was 32 KB, and the agent loaded all of it to write a three-test file.&lt;/p&gt;

&lt;p&gt;Now it's split: a core of about 8 KB with what most tasks need, and a second file with the rest that the agent only reads when the task calls for it. Claude Code skills support this directly, since files under &lt;code&gt;references/&lt;/code&gt; are loaded on demand. For any other setup it's the same idea: a short core, with links to the details.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Make the compiler teach your API
&lt;/h2&gt;

&lt;p&gt;A typed API is documentation the model can't ignore, as long as somebody runs the compiler.&lt;/p&gt;

&lt;p&gt;When the agent invents an option name or passes arguments in the wrong order, a type check turns that into an error in seconds, instead of a bug found at runtime. So:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Type your public API strictly.&lt;/strong&gt; An option that's silently ignored when misspelled is a trap for people and agents alike. Make it a compile error.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tell the agent to type-check what it wrote before running anything.&lt;/strong&gt; It's one line in the instructions, and it's the cheapest check you have.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  3. Lint what types can't see
&lt;/h2&gt;

&lt;p&gt;Some mistakes are valid code: a missing &lt;code&gt;await&lt;/code&gt;, an import from the popular alternative library, a debugging leftover like &lt;code&gt;.only&lt;/code&gt;, reading a property that's always undefined for your API.&lt;/p&gt;

&lt;p&gt;That's what a small, focused linter is for. For my library it's four rules. Three properties made it work well with agents:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Zero config.&lt;/strong&gt; One command, no setup, the same result in every project. If the agent first has to configure your linter, it won't run it reliably.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Autofix for the safe cases.&lt;/strong&gt; If the fix is always correct (adding &lt;code&gt;await&lt;/code&gt; inside an &lt;code&gt;async&lt;/code&gt; function), just apply it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Messages that say what to do.&lt;/strong&gt; Not "invalid usage", but the cause and the fix in one sentence. The agent's next action is in the error message, and a human gets the same help.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Before trusting it, run it against every real project you can find. A rule that fires on working code teaches the agent something false.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Design your output for two readers
&lt;/h2&gt;

&lt;p&gt;An agent reading a console log will grep for checkmarks, count lines, and sometimes get it wrong. Your CLI has two readers now: a human and a program.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Write a JSON report&lt;/strong&gt; next to the pretty output. Mine has the outcome, each failure with its error, and the diagnostics a person would look for.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Offer &lt;code&gt;--format json&lt;/code&gt;&lt;/strong&gt; on every command that reports problems.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Document your exit codes&lt;/strong&gt;, and make them mean something: clean, problems found, nothing to do.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  5. Test your instructions with evals
&lt;/h2&gt;

&lt;p&gt;Instructions regress like any code. A well-meant edit can quietly drop a rule the agent depended on, and nothing tells you.&lt;/p&gt;

&lt;p&gt;An eval is a test for an agent: set up a small project, give the agent a task, then grade what it produced. In Claude Code that's &lt;code&gt;claude plugin eval&lt;/code&gt;. A case is a folder with a scaffold script, a prompt and a list of graders:&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;prompt&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
  &lt;span class="s"&gt;Write tests for the todo page: the list, the empty state,&lt;/span&gt;
  &lt;span class="s"&gt;a failed load, and adding a todo.&lt;/span&gt;
&lt;span class="na"&gt;graders&lt;/span&gt;&lt;span class="pi"&gt;:&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;right-imports&lt;/span&gt;
    &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;regex&lt;/span&gt;
    &lt;span class="na"&gt;pattern&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;from\s+["'&lt;/span&gt;&lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;]twd-js/runner["'&lt;/span&gt;&lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;]'&lt;/span&gt;
    &lt;span class="na"&gt;target&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;source&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;file&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;src/twd-tests/todos.twd.test.ts&lt;/span&gt; &lt;span class="pi"&gt;}&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;linter-ran&lt;/span&gt;
    &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;tool_used&lt;/span&gt;
    &lt;span class="na"&gt;tool&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Bash&lt;/span&gt;
    &lt;span class="na"&gt;input_match&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;twd-lint'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What I learned running them:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;They find real gaps.&lt;/strong&gt; Before one round of fixes, my old instructions scored 0.50 on a case. Half the checks failed, on a task I thought was covered.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Prefer deterministic graders.&lt;/strong&gt; A regex on the file the agent wrote, or "did it call this tool", gives the same answer every time. Keep an LLM judge for what can't be matched, like "is this one test per flow, or one per element".&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Make judge criteria precise.&lt;/strong&gt; My judge failed a correct file because the criteria said "awaits every call", and one of those calls is synchronous. Listing the async calls by name fixed it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Plant mistakes.&lt;/strong&gt; One of my cases starts from a file with four planted errors and checks that the agent runs the linter and leaves the file clean.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Don't run them on every PR.&lt;/strong&gt; They cost real money: about $1.80 for my five cases. I run them before releases and after changing the instructions. On every PR, CI runs &lt;code&gt;claude plugin validate --strict&lt;/code&gt;, which is free and offline, plus a small script that checks every file the instructions reference exists.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  6. Keep the instructions honest
&lt;/h2&gt;

&lt;p&gt;The quietest failure is drift: the library moves on, and the instructions don't.&lt;/p&gt;

&lt;p&gt;When I compared my agent's instructions with the library's docs and source, I found an option documented that the library ignores, and a default stated backwards. The agent followed the instructions faithfully. The instructions were wrong.&lt;/p&gt;

&lt;p&gt;Two habits help:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No version notes.&lt;/strong&gt; "Fixed in 1.8" or "requires 1.9+" means nothing to a model that doesn't know which version you're on. Describe the current behaviour.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Check claims against the source, not your memory.&lt;/strong&gt; And if a rule in your instructions has a reason attached, make sure the reason is true. One of mine wasn't, for months.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The checklist
&lt;/h2&gt;

&lt;p&gt;If your library, SDK or design system isn't something models know yet:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Teach the API once, with complete examples, in layers.&lt;/li&gt;
&lt;li&gt;Type it so wrong calls fail to compile, and have the agent run the compiler.&lt;/li&gt;
&lt;li&gt;Lint what types miss, with zero config, autofix and messages that say what to do.&lt;/li&gt;
&lt;li&gt;Give agents JSON and exit codes, not logs.&lt;/li&gt;
&lt;li&gt;Write evals with mostly deterministic graders, and run them before releases.&lt;/li&gt;
&lt;li&gt;Keep the instructions honest against the source.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Maybe one day the models will know your library the way they know React. Until then, your library has to make up for what the model doesn't know.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>claudecode</category>
      <category>opensource</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Prose does not beat determinism: stop asking your AI agent to remember</title>
      <dc:creator>Kevin Julián Martínez Escobar</dc:creator>
      <pubDate>Tue, 13 Oct 2026 08:46:07 +0000</pubDate>
      <link>https://dev.to/kevinccbsg/prose-does-not-beat-determinism-stop-asking-your-ai-agent-to-remember-29di</link>
      <guid>https://dev.to/kevinccbsg/prose-does-not-beat-determinism-stop-asking-your-ai-agent-to-remember-29di</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;If a rule can be checked by a program, stop asking the model to remember it.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Most of us improve an AI agent the same way. It makes a mistake, so we add a rule to the prompt, the skill or the &lt;code&gt;CLAUDE.md&lt;/code&gt;. It makes the mistake again, so we make the rule bold. Then capitals. Then a section called &lt;strong&gt;NEVER DO THIS&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;I did exactly that for months, on a Claude Code plugin I maintain that writes and runs frontend tests. This post is about what replaced it, and why I think it applies to any agent setup, not just mine.&lt;/p&gt;

&lt;h2&gt;
  
  
  The instructions that said everything three times
&lt;/h2&gt;

&lt;p&gt;The plugin's agent writes tests with &lt;a href="https://twd.dev" rel="noopener noreferrer"&gt;TWD&lt;/a&gt;, a testing library I maintain. The important rules looked like this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;At the top, in &lt;strong&gt;Hard Constraints&lt;/strong&gt;: "Always &lt;code&gt;await&lt;/code&gt; async methods."&lt;/li&gt;
&lt;li&gt;In a &lt;strong&gt;Do / Don't&lt;/strong&gt; list in the middle: the same rule, worded differently.&lt;/li&gt;
&lt;li&gt;In a &lt;strong&gt;Common Mistakes&lt;/strong&gt; list at the end: "1. A missing &lt;code&gt;await&lt;/code&gt;."&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Every important rule existed three times, with &lt;code&gt;WRONG&lt;/code&gt; / &lt;code&gt;RIGHT&lt;/code&gt; code blocks in between. A single "write tests for this page" request loaded 56 KB of instructions.&lt;/p&gt;

&lt;p&gt;And the agent still forgot an &lt;code&gt;await&lt;/code&gt; now and then.&lt;/p&gt;

&lt;p&gt;When it did, nothing caught it until the tests ran in a browser: start the dev server, run the suite, read the failure, find the line, fix it, run again. That's the most expensive possible moment to find a missing keyword, for a person and even more so for an agent paying for every step in tokens.&lt;/p&gt;

&lt;p&gt;Writing a rule a third time doesn't make the model more likely to follow it. A program checking for it does.&lt;/p&gt;

&lt;h2&gt;
  
  
  The ladder: from hope to guarantee
&lt;/h2&gt;

&lt;p&gt;The fix was to take each rule and move it down a ladder, from "the model has to remember" to "a program verifies it":&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Step&lt;/th&gt;
&lt;th&gt;Who checks&lt;/th&gt;
&lt;th&gt;When you find out&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Prose in the instructions&lt;/td&gt;
&lt;td&gt;The model, if it remembers&lt;/td&gt;
&lt;td&gt;After a full test run, if at all&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Type check&lt;/td&gt;
&lt;td&gt;The compiler&lt;/td&gt;
&lt;td&gt;Seconds&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Linter&lt;/td&gt;
&lt;td&gt;A lint rule&lt;/td&gt;
&lt;td&gt;Seconds, often fixed automatically&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Structured tool output&lt;/td&gt;
&lt;td&gt;The tool itself&lt;/td&gt;
&lt;td&gt;One read, no guessing&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Each step down is cheaper and more reliable than the one above it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1: run the compiler you already have
&lt;/h3&gt;

&lt;p&gt;A lot of what an agent gets wrong is calling an API the wrong way: a misspelled option, arguments in the wrong order, a method from a different library. If your API is typed, all of those are already compile errors.&lt;/p&gt;

&lt;p&gt;My agent just never ran &lt;code&gt;tsc&lt;/code&gt;. Now its instructions say: type-check the files you wrote before the first test run. One line, and a whole category of mistakes is caught in seconds, without starting anything.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2: lint what the types can't see
&lt;/h3&gt;

&lt;p&gt;Some mistakes are perfectly valid code:&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="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;visit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/todos&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                       &lt;span class="c1"&gt;// forgot the await&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;describe&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;it&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="s1"&gt;vitest&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;      &lt;span class="c1"&gt;// wrong test framework for this runner&lt;/span&gt;
&lt;span class="nx"&gt;it&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;only&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;creates a todo&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;span class="c1"&gt;// silently skips every other test&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The type checker is happy with all three. A linter isn't. So I wrote one for my library, with a handful of rules for exactly the mistakes the agent kept making, and the agent now runs it after the type check, with &lt;code&gt;--fix&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Two details made a big difference:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Autofix the safe cases.&lt;/strong&gt; Adding a missing &lt;code&gt;await&lt;/code&gt; inside an &lt;code&gt;async&lt;/code&gt; function is always correct, so the linter just does it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Write messages that say what to do.&lt;/strong&gt; Not "invalid usage at line 33", but the cause and the fix in one sentence. The agent reads the message, and its next action is right there. So does the human.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Step 3: give the agent data, not logs
&lt;/h3&gt;

&lt;p&gt;The last step is the output of your tools. An agent reading a console log will grep for checkmarks, count lines, and occasionally get it wrong. My test runner writes a JSON report on every run: the outcome, each failed test with its error, which network mocks were never used. The agent reads a file instead of guessing from text.&lt;/p&gt;

&lt;h2&gt;
  
  
  The instructions got shorter, but not empty
&lt;/h2&gt;

&lt;p&gt;With those checks in place, I removed the repetition, following one rule:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The instructions teach the correct form once, in positive form, with an example. The tooling makes sure it's followed.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That distinction matters. There are two kinds of text in agent instructions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Knowledge&lt;/strong&gt;: how the API works, what things return, complete examples. No linter replaces this. If the model doesn't know your library, this is the only place it can learn it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Policing&lt;/strong&gt;: the repeated "never do X". This is the part the tooling replaces.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The policing went away. The words &lt;code&gt;NEVER&lt;/code&gt;, &lt;code&gt;WRONG&lt;/code&gt; and &lt;code&gt;ALWAYS&lt;/code&gt; went from 13 occurrences to 1. A "write tests" request now loads 32 KB of instructions instead of 56 KB.&lt;/p&gt;

&lt;p&gt;But the bigger saving isn't in the instructions. One failed run caused by a missing &lt;code&gt;await&lt;/code&gt; costs more tokens than any paragraph: the run, the report, the diagnosis, the fix, the re-run. Every mistake caught by the compiler or the linter is a loop that never happens.&lt;/p&gt;

&lt;h2&gt;
  
  
  A linter can't believe something false
&lt;/h2&gt;

&lt;p&gt;My favourite part of this story is a rule that turned out to be wrong.&lt;/p&gt;

&lt;p&gt;Rule number one in my instructions, in capitals, was: &lt;strong&gt;"ONE top-level &lt;code&gt;describe()&lt;/code&gt; per file. Multiple top-level describes break the test runner."&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;So the first version of the linter had a rule for it. I ran it against every project using the library: 494 test files across React, Vue, Angular, Svelte and others.&lt;/p&gt;

&lt;p&gt;It reported 44 files. Every one of those tests ran and passed.&lt;/p&gt;

&lt;p&gt;I read the runner's code. It handles any number of top-level blocks. The rule I had given the agent, at the very top, for months, was a style preference with a made-up reason. The agent obeyed it faithfully, and nobody ever checked it.&lt;/p&gt;

&lt;p&gt;That's the deeper problem with prose: it can say anything, and nothing verifies it. A lint rule has to survive real code. This one didn't, so it was removed, and the instructions now call it what it is, a convention.&lt;/p&gt;

&lt;p&gt;The rules that survived were worth it. Across those 494 files there were no false positives, and every report was a real bug, including dozens of missing &lt;code&gt;await&lt;/code&gt;s and assertions that had never actually run because nobody awaited them. Some of those were in code written by people, long before any agent.&lt;/p&gt;

&lt;h2&gt;
  
  
  What stays prose
&lt;/h2&gt;

&lt;p&gt;Not everything can move down the ladder. "Set up the mock before the page loads" depends on when the app makes its request, and that isn't visible in the test's source. "Reset the store between tests" depends on the app. Those stay in the instructions, written once, with an example.&lt;/p&gt;

&lt;p&gt;That's fine. The goal isn't zero prose. The goal is that prose only carries what a program can't check.&lt;/p&gt;

&lt;h2&gt;
  
  
  The takeaway
&lt;/h2&gt;

&lt;p&gt;If you maintain a skill, an &lt;code&gt;AGENTS.md&lt;/code&gt;, a &lt;code&gt;CLAUDE.md&lt;/code&gt; or a system prompt, go through it rule by rule and ask:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Could the compiler catch this?&lt;/strong&gt; Have the agent run it before anything else.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Could a linter catch this?&lt;/strong&gt; Use an existing rule or write one, with a message that says how to fix it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Could the tool report this?&lt;/strong&gt; Give the agent structured output, not logs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;None of the above?&lt;/strong&gt; Keep it in prose. Once, in positive form, with an example.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Telling the model three times and hoping doesn't work. Tell it once and check.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>claudecode</category>
      <category>testing</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Your Repo Can Now Tell People It Cares About Accessibility</title>
      <dc:creator>Kevin Julián Martínez Escobar</dc:creator>
      <pubDate>Mon, 05 Oct 2026 11:37:26 +0000</pubDate>
      <link>https://dev.to/kevinccbsg/your-repo-can-now-tell-people-it-cares-about-accessibility-3al</link>
      <guid>https://dev.to/kevinccbsg/your-repo-can-now-tell-people-it-cares-about-accessibility-3al</guid>
      <description>&lt;p&gt;Open any repository on GitHub and you see the same tabs: README, Code of Conduct, Contributing, Security. As of October 1, there can be one more: &lt;strong&gt;Accessibility&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;GitHub now &lt;a href="https://github.blog/changelog/2026-10-01-accessibility-statements-highlighted-on-repository-overview/" rel="noopener noreferrer"&gt;highlights an &lt;code&gt;ACCESSIBILITY.md&lt;/code&gt; file&lt;/a&gt; on the repository overview and links to it from the About section. It is a small change, and it puts accessibility on the front page of your project, next to the files every contributor already reads.&lt;/p&gt;

&lt;p&gt;We added one to &lt;a href="https://github.com/BRIKEV/twd" rel="noopener noreferrer"&gt;TWD&lt;/a&gt; right away. Here is why, and what we learned.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why it matters
&lt;/h2&gt;

&lt;p&gt;Picture someone who uses a screen reader trying out your library. Something in the docs doesn't work for them. Today, they have to guess: is this a known issue? Does anyone care? Where do I even report it?&lt;/p&gt;

&lt;p&gt;An accessibility page answers all three before they ask. It says this project takes accessibility seriously, here is what we have tested, and here is how to tell us when something gets in your way.&lt;/p&gt;

&lt;p&gt;It also helps the other side. Contributors see, in plain words, what is expected of a change that touches the UI.&lt;/p&gt;

&lt;h2&gt;
  
  
  What we put in ours
&lt;/h2&gt;

&lt;p&gt;TWD had a head start. In June we had it independently audited against WCAG 2.2 AA, fixed what the auditors found, and published a formal &lt;a href="https://twd.dev/accessibility-statement" rel="noopener noreferrer"&gt;accessibility statement&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;We expected to copy and paste that statement. We didn't. A formal statement is written for compliance. The GitHub page is written for people, so we wrote it in plain language around three ideas:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Be honest about what was tested.&lt;/strong&gt; Our audit covered the sidebar and the main docs pages, using a keyboard and the NVDA screen reader. So that is what the page claims. For everything else, WCAG 2.2 AA is our goal, and we say so.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Make reporting easy.&lt;/strong&gt; Open an issue, send an email, or ask in Discussions. No need to know which guideline is involved, and no need to share anything personal. We commit to replying within 5 business days.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tell contributors what good looks like.&lt;/strong&gt; Keep it usable with a keyboard, keep focus visible, don't rely on colour alone, and include light and dark screenshots with UI changes.&lt;/p&gt;

&lt;p&gt;You can read the result here: &lt;a href="https://github.com/BRIKEV/twd/blob/main/ACCESSIBILITY.md" rel="noopener noreferrer"&gt;TWD's ACCESSIBILITY.md&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Add one to your project
&lt;/h2&gt;

&lt;p&gt;You don't need an audit to start. You need an honest page.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Create &lt;code&gt;ACCESSIBILITY.md&lt;/code&gt; in your repo root (or in &lt;code&gt;.github/&lt;/code&gt; or &lt;code&gt;docs/&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;Start from &lt;a href="https://docs.github.com/en/communities/setting-up-your-project-for-healthy-contributions/adding-an-accessibility-page-to-your-repository" rel="noopener noreferrer"&gt;GitHub's template&lt;/a&gt;, or add it from your repo's &lt;strong&gt;Community Standards&lt;/strong&gt; page.&lt;/li&gt;
&lt;li&gt;Fill in the two sections that matter most: how to report a barrier, and what you have and haven't tested.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That's it. The tab appears on your repo's front page.&lt;/p&gt;

&lt;p&gt;Accessibility work usually happens out of sight. Now it has a place on your repo's front page, where users and contributors will see it.&lt;/p&gt;

</description>
      <category>a11y</category>
      <category>github</category>
      <category>opensource</category>
    </item>
    <item>
      <title>3 Developer Archetypes That Can Break a Team</title>
      <dc:creator>Kevin Julián Martínez Escobar</dc:creator>
      <pubDate>Tue, 22 Sep 2026 08:31:47 +0000</pubDate>
      <link>https://dev.to/kevinccbsg/3-developer-archetypes-that-can-break-a-team-44co</link>
      <guid>https://dev.to/kevinccbsg/3-developer-archetypes-that-can-break-a-team-44co</guid>
      <description>&lt;p&gt;There are developers who make a team faster. There are developers who make a team better. And there are developers who slowly make a team dependent on them, without anyone noticing.&lt;/p&gt;

&lt;p&gt;The three I want to talk about are usually rewarded for it. They look experienced, productive, sometimes indispensable. AI is making all three of them easier to scale.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. The Senior Cynic
&lt;/h2&gt;

&lt;p&gt;You have met this one. Years of experience and a long list of things that do not work. Every new approach reminds them of something they tried five years ago. A simpler architecture will not scale. A different testing strategy was already tried. Another tool is not going to solve anything.&lt;/p&gt;

&lt;p&gt;Experience is valuable. It becomes a problem when it turns into a filter that rejects anything unfamiliar. The Senior Cynic is usually not protecting the architecture, they are protecting the mental model they have built over their career. Knowing why something failed in the past is useful. Assuming it cannot work in a different context is not.&lt;/p&gt;

&lt;p&gt;AI makes this cheaper. Ask for arguments and you get arguments: comparisons, trade-off tables, a full technical rationale for the position you already held. You no longer just have a strong opinion, you have twenty pages backing it up in ten seconds.&lt;/p&gt;

&lt;p&gt;The antidote is not optimism, it is evidence. Try the thing. Measure it. Let the current context weigh more than the muscle memory.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. The Guess Coder
&lt;/h2&gt;

&lt;p&gt;This one is easier to spot, because it shows up in the code. They build for a future that does not exist yet. A simple requirement becomes an abstraction. The abstraction becomes a framework. The framework needs configuration, the configuration needs an extension system, and now you maintain infrastructure for a problem you have never actually had.&lt;/p&gt;

&lt;p&gt;The intentions are usually good. They want to avoid technical debt. They want something that scales. They have read about SOLID, clean architecture, hexagonal architecture, event-driven systems, distributed everything. The catch is that principles without context turn into cargo cult engineering, and often the best architecture for the future is the simplest one you can change today.&lt;/p&gt;

&lt;p&gt;AI makes this effortless. Ask an agent to "make this extensible" and you get hundreds of lines of perfectly reasonable looking code. It compiles. The structure looks sophisticated. And you have just spent an afternoon solving a problem you do not have.&lt;/p&gt;

&lt;p&gt;The fix is not to stop thinking about the future, it is to separate &lt;strong&gt;known future requirements from imagined ones&lt;/strong&gt;. Build for what you know, keep the code easy to change, and validate an assumption before you turn it into architecture.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. The Superhero
&lt;/h2&gt;

&lt;p&gt;This is the one I worry about most.&lt;/p&gt;

&lt;p&gt;The Superhero knows everything. The weird legacy service, how the deploy actually works, why that obscure config exists. When production breaks, everybody knows who to call. They are genuinely valuable, and that is exactly the problem.&lt;/p&gt;

&lt;p&gt;The team learns that the fastest route to a solution is to ask them. So people stop investigating. They stop learning the system. They stop taking ownership. The Superhero rarely pushes back on that, and sometimes reinforces it: "I'll just do it", "it's faster if I handle it", "I'll explain it later". Because they are good at solving problems, it works. In the short term.&lt;/p&gt;

&lt;p&gt;Then the organization starts rewarding the behavior. They become the person who saves releases and fixes incidents, and being indispensable starts to look like career progress. But there is a difference between being &lt;strong&gt;valuable&lt;/strong&gt; and being &lt;strong&gt;a dependency&lt;/strong&gt;. A healthy team should not need one specific person to keep operating.&lt;/p&gt;

&lt;p&gt;The goal is not to stop the Superhero from solving incidents. It is to make sure the next incident does not need the same person. Fix the problem, share what you learned, improve the system, hand the responsibility over, reduce the odds of it happening again. Otherwise you are not removing the fire, you are building a very efficient fire department around one person.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the three have in common
&lt;/h2&gt;

&lt;p&gt;They look nothing alike, and they optimize for the same thing: themselves, instead of the team's ability to operate without them. The Cynic protects their experience. The Guess Coder protects their imagined future. The Superhero protects their indispensability.&lt;/p&gt;

&lt;p&gt;AI amplifies all three. More arguments, more complexity, more changes landing faster than the rest of the team can follow them.&lt;/p&gt;

&lt;p&gt;Which is why I do not think AI adoption is mostly a tooling problem. It is a team problem. A team with a strong engineering culture gets amplified. A team with weak delegation, thin tests, knowledge silos and a taste for over-engineering gets amplified too, and much faster.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to optimize for instead
&lt;/h2&gt;

&lt;p&gt;Not developers who know everything, but developers who make knowledge accessible. Not developers who build for every possible future, but developers who keep today's system easy to change. Not developers who are indispensable, but developers who make the team less dependent on them.&lt;/p&gt;

&lt;p&gt;The strongest developer on a team is probably not the one who can solve every problem. It is &lt;strong&gt;the one who makes sure the team does not have to solve the same problem twice&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;With AI in the loop, that distinction matters more, not less.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>career</category>
      <category>teamwork</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
