<?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: Isamu Arimoto</title>
    <description>The latest articles on DEV Community by Isamu Arimoto (@isamu).</description>
    <link>https://dev.to/isamu</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%2F4043370%2F2e4876ea-5961-4167-9592-045173eb6fef.jpg</url>
      <title>DEV Community: Isamu Arimoto</title>
      <link>https://dev.to/isamu</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/isamu"/>
    <language>en</language>
    <item>
      <title>ever-better: 13 commands that make a codebase only able to get better</title>
      <dc:creator>Isamu Arimoto</dc:creator>
      <pubDate>Sat, 15 Aug 2026 09:43:57 +0000</pubDate>
      <link>https://dev.to/isamu/ever-better-13-commands-that-make-a-codebase-only-able-to-get-better-35d0</link>
      <guid>https://dev.to/isamu/ever-better-13-commands-that-make-a-codebase-only-able-to-get-better-35d0</guid>
      <description>&lt;p&gt;Adding a strict linter to an existing repository produces four thousand errors and gets reverted. The usual workaround — set everything to &lt;code&gt;warn&lt;/code&gt; — enforces nothing, and the count grows quietly.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/isamu/ever-better" rel="noopener noreferrer"&gt;ever-better&lt;/a&gt; is the way around that. It records every violation that exists today as a &lt;strong&gt;ceiling&lt;/strong&gt;: old code is grandfathered, new code is held to the whole rule set, and the ceiling can fall but never rise.&lt;/p&gt;

&lt;p&gt;Below is every command, what it does, and the problem it exists for.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx ever-better &amp;lt;&lt;span class="nb"&gt;command&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;MIT, zero runtime dependencies, Node 20.11+.&lt;/p&gt;




&lt;h2&gt;
  
  
  &lt;code&gt;diagnose&lt;/code&gt; — what is missing, and what each gap costs
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ever-better diagnose            &lt;span class="c"&gt;# read-only&lt;/span&gt;
ever-better diagnose &lt;span class="nt"&gt;--write&lt;/span&gt;    &lt;span class="c"&gt;# also writes QUALITY.md&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;What it does.&lt;/strong&gt; Surveys the repository — package manager, language, framework, runtime, ESLint setup, formatter, test runner, dead-code and duplication scans, agent instructions, CI, file sizes — and lists the gaps with the phase each belongs to.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why.&lt;/strong&gt; "Add linting" is not a task; it is six of them in an order that matters. Formatting has to land before linting or the first cleanup PR is a diff nobody can read. Type-aware rules cannot run without TypeScript. This prints that order instead of leaving you to discover it.&lt;/p&gt;




&lt;h2&gt;
  
  
  &lt;code&gt;bootstrap&lt;/code&gt; — install the tooling, generate the configs
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ever-better bootstrap
ever-better bootstrap &lt;span class="nt"&gt;--dry-run&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;What it does.&lt;/strong&gt; Adds the dependencies, writes &lt;code&gt;eslint.config.mjs&lt;/code&gt;, Prettier config, &lt;code&gt;.gitattributes&lt;/code&gt;, &lt;code&gt;knip.json&lt;/code&gt;, the package scripts CI needs, workflows for three platforms, &lt;code&gt;dependabot.yml&lt;/code&gt;, and a secret-scan workflow. Prices each TypeScript strictness flag by enabling it in a temporary config and counting the errors — turning on the free ones, reporting what the rest would cost.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why.&lt;/strong&gt; Every rule it writes is an &lt;strong&gt;error&lt;/strong&gt;, not a warning. That is only survivable because of the next command.&lt;/p&gt;




&lt;h2&gt;
  
  
  &lt;code&gt;freeze&lt;/code&gt; — pin today's violations as the ceiling
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ever-better freeze
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;What it does.&lt;/strong&gt; Runs &lt;code&gt;eslint --suppress-all&lt;/code&gt;, which records how many violations each rule has in each file, then stores the totals in &lt;code&gt;.ever-better/state.json&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Baseline pinned: 4,942 violations across 72 rules are now grandfathered.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Why.&lt;/strong&gt; From this commit on, the 4,942 are silent and violation 4,943 fails the build. You do not have to fix anything today to start enforcing everything tomorrow.&lt;/p&gt;

&lt;p&gt;The ratchet itself is ESLint's own bulk-suppressions feature. ever-better is the part around it: knowing which rules to add, keeping the ledger, and failing CI when a number goes up.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It refuses to run twice.&lt;/strong&gt; A second freeze would grandfather everything added since — which is the one thing the baseline exists to prevent.&lt;/p&gt;




&lt;h2&gt;
  
  
  &lt;code&gt;check&lt;/code&gt; — the gate
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ever-better check
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;What it does.&lt;/strong&gt; Compares current counts against the ceiling and exits non-zero if anything rose.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;8 unsuppressed error(s) — these are new since the baseline:
  2  id-length
  1  prettier/prettier
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Why.&lt;/strong&gt; A baseline is only a ratchet if something rejects a regression. Without CI it is a note.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Do not pipe it. &lt;code&gt;ever-better check | tail&lt;/code&gt; exits with &lt;code&gt;tail&lt;/code&gt;'s status and reports a failing gate as success.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  &lt;code&gt;prune&lt;/code&gt; — lower the ceiling after a fix
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ever-better prune
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;What it does.&lt;/strong&gt; Removes suppressions for violations that no longer exist, so the recorded ceiling matches reality.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why.&lt;/strong&gt; Without it, fixing 100 violations changes nothing: the ceiling still permits them, and they can come back. This is what makes the number monotonic.&lt;/p&gt;




&lt;h2&gt;
  
  
  &lt;code&gt;next&lt;/code&gt; — what to drain first
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ever-better next
ever-better next &lt;span class="nt"&gt;--fan-in&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;What it does.&lt;/strong&gt; Ranks the backlog by &lt;strong&gt;which edit enforces the most&lt;/strong&gt;, not by which rule is biggest.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;take these first — one or two edits, and the rule is enforced in that file for good:
    1  src/browser.js  @typescript-eslint/no-require-imports
    1  src/common.js   no-param-reassign
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Why.&lt;/strong&gt; The ratchet is per file per rule. A file with no suppression left for a rule fails on its next violation, whatever that rule's total is elsewhere — so two violations in one file buy more enforcement than twenty spread across twenty files.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;--fan-in&lt;/code&gt; adds how many files import each one, because a fix in a widely imported module lands errors in files the diff never opened.&lt;/p&gt;




&lt;h2&gt;
  
  
  &lt;code&gt;report&lt;/code&gt; — what the debt actually looks like
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ever-better report
ever-better report &lt;span class="nt"&gt;--json&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;What it does.&lt;/strong&gt; Prints the backlog as a &lt;strong&gt;rule × area&lt;/strong&gt; table, and appends it to &lt;code&gt;$GITHUB_STEP_SUMMARY&lt;/code&gt; when that is set.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why.&lt;/strong&gt; &lt;code&gt;next&lt;/code&gt; answers "which edit enforces the most". This answers "what shape is this repository's debt". And because the generated workflow runs it after &lt;code&gt;check&lt;/code&gt; with &lt;code&gt;if: always()&lt;/code&gt;, &lt;strong&gt;the run where the gate just failed is the run where the backlog is visible&lt;/strong&gt; — no one has to edit a workflow to get a report.&lt;/p&gt;




&lt;h2&gt;
  
  
  &lt;code&gt;secrets&lt;/code&gt; — scan the history for committed credentials
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ever-better secrets
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;What it does.&lt;/strong&gt; Runs &lt;code&gt;gitleaks&lt;/code&gt; over &lt;strong&gt;both&lt;/strong&gt; the history and the working tree, failing on any finding.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why both.&lt;/strong&gt; Either alone passes a repository that is holding a secret. A history scan misses the key you pasted an hour ago and have not committed. A working-tree scan misses the key that was committed and then deleted — which is still in every clone.&lt;/p&gt;

&lt;p&gt;This is the one check with no baseline. A leaked key is not debt you ratchet down.&lt;/p&gt;




&lt;h2&gt;
  
  
  &lt;code&gt;migrate&lt;/code&gt; — JavaScript to TypeScript
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ever-better migrate            &lt;span class="c"&gt;# show the plan&lt;/span&gt;
ever-better migrate &lt;span class="nt"&gt;--all&lt;/span&gt;      &lt;span class="c"&gt;# rename everything in one pass&lt;/span&gt;
ever-better migrate &lt;span class="nt"&gt;--file&lt;/span&gt; src/foo.js
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;What it does.&lt;/strong&gt; Renames with &lt;code&gt;git mv&lt;/code&gt; so history follows, and lets the lint fallout land in the ratchet instead of blocking the migration.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why.&lt;/strong&gt; Types are the cheapest rule set there is, and the tier that finds the most real bugs cannot run without them. On a 13-year-old JavaScript project, renaming 49 files and changing no logic produced &lt;strong&gt;2,641 type errors&lt;/strong&gt; — every one of them a question nobody had answered.&lt;/p&gt;




&lt;h2&gt;
  
  
  &lt;code&gt;emit-diff&lt;/code&gt; — prove a refactor changed nothing
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ever-better emit-diff &lt;span class="nt"&gt;--against&lt;/span&gt; main
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;What it does.&lt;/strong&gt; Compiles the working tree and a git ref, then compares the emitted JavaScript.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why.&lt;/strong&gt; Types erase at compile time, so a change that only moves types must produce &lt;strong&gt;byte-identical output&lt;/strong&gt;. That is a proof, in seconds. No amount of test coverage states it as strongly, because tests only cover the paths someone thought of.&lt;/p&gt;




&lt;h2&gt;
  
  
  &lt;code&gt;catalog&lt;/code&gt; — the helpers that already exist
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ever-better catalog
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;What it does.&lt;/strong&gt; Writes &lt;code&gt;docs/shared-helpers.md&lt;/code&gt;: every exported function, grouped by directory, with the first sentence of its doc comment. Point your CLAUDE.md at it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why.&lt;/strong&gt; A linter sees inside one file. Duplication detection only notices copies once they are textually similar, and two independent implementations of the same idea rarely are. &lt;strong&gt;Nothing else reports the same function written a sixth time under a sixth name.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  &lt;code&gt;status&lt;/code&gt; — where you are
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ever-better status
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;What it does.&lt;/strong&gt; Prints the phase, the frozen date, the current backlog, which rules improved or regressed, and the smallest remaining backlogs.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;STALE      100 commits since the diagnosis; re-run diagnose before trusting it
phase      drain
backlog    3532
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Why.&lt;/strong&gt; That &lt;code&gt;STALE&lt;/code&gt; line matters more than it looks. A diagnosis from a hundred commits ago is a description of a repository that no longer exists.&lt;/p&gt;




&lt;h2&gt;
  
  
  &lt;code&gt;log&lt;/code&gt; — why, not how many
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ever-better log &lt;span class="nt"&gt;--kind&lt;/span&gt; drained  &lt;span class="nt"&gt;--rule&lt;/span&gt; max-depth &lt;span class="s2"&gt;"6 violations, 1 real bug"&lt;/span&gt;
ever-better log &lt;span class="nt"&gt;--kind&lt;/span&gt; deferred &lt;span class="nt"&gt;--rule&lt;/span&gt; max-lines &lt;span class="s2"&gt;"router.ts is 1400 lines; its own project"&lt;/span&gt;
ever-better log &lt;span class="nt"&gt;--kind&lt;/span&gt; issue    &lt;span class="nt"&gt;--rule&lt;/span&gt; no-floating-promises &lt;span class="s2"&gt;"opened #42 — product decision"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;What it does.&lt;/strong&gt; Records what happened against the current commit — the only command that writes the &lt;strong&gt;Work log&lt;/strong&gt; in &lt;code&gt;QUALITY.md&lt;/code&gt;. Everything else records counts.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why.&lt;/strong&gt; &lt;code&gt;deferred&lt;/code&gt; is the one that earns its keep. It renders as a &lt;strong&gt;Carried over&lt;/strong&gt; checklist stamped with the commit it was seen at, because "router.ts needs splitting" is useless four hundred commits later unless a reader can tell when it was true.&lt;/p&gt;




&lt;h2&gt;
  
  
  The whole loop, in five commands
&lt;/h2&gt;

&lt;p&gt;The other eight are for when you need them. This is the shape:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx ever-better diagnose     &lt;span class="c"&gt;# what is missing&lt;/span&gt;
npx ever-better bootstrap    &lt;span class="c"&gt;# install it&lt;/span&gt;
npx ever-better freeze       &lt;span class="c"&gt;# pin today's count&lt;/span&gt;
npx ever-better check        &lt;span class="c"&gt;# gate CI on it&lt;/span&gt;
npx ever-better prune        &lt;span class="c"&gt;# lower the ceiling as you fix&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Or hand the repository to an agent
&lt;/h2&gt;

&lt;p&gt;The commands are the deterministic half — count, record, gate, refuse. The judgement half is eight Claude Code skills that ship with it: which violation is a real bug, when to stop and ask, what deserves an issue rather than a fix.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;/plugin marketplace add isamu/ever-better
/plugin &lt;span class="nb"&gt;install &lt;/span&gt;ever-better
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then say &lt;code&gt;clean this repo up&lt;/code&gt;. It formats, installs, freezes, and works the backlog down one rule per pull request — fixing violations, extracting the pure functions that make the fixes testable, writing the tests, and lowering the ceiling as it goes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It is not unattended.&lt;/strong&gt; Anything it can decide from the code, it decides. Anything it cannot — ambiguous behaviour, a public API change, a refactor that is its own project — becomes an issue with the options written out, and it moves on.&lt;/p&gt;




&lt;h2&gt;
  
  
  Try it on something you did not write
&lt;/h2&gt;

&lt;p&gt;That is the honest test. Pick a repository you have never read:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone &lt;span class="nt"&gt;--depth&lt;/span&gt; 1 https://github.com/debug-js/debug.git
&lt;span class="nb"&gt;cd &lt;/span&gt;debug
npx ever-better diagnose
npx ever-better bootstrap
npx prettier &lt;span class="nt"&gt;--write&lt;/span&gt; &lt;span class="nb"&gt;.&lt;/span&gt;
npx ever-better freeze
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then add a file with a violation and run &lt;code&gt;npx ever-better check&lt;/code&gt;. The 72 existing violations stay silent; the new ones fail.&lt;/p&gt;

&lt;p&gt;On this particular repository you will also see five &lt;code&gt;(parse error)&lt;/code&gt; entries — &lt;code&gt;/* eslint-env */&lt;/code&gt;&lt;br&gt;
comments and stale &lt;code&gt;eslint-disable&lt;/code&gt; directives that flat config rejects. Those are real, they have&lt;br&gt;
no rule id, and &lt;code&gt;--suppress-all&lt;/code&gt; cannot record them, so &lt;code&gt;freeze&lt;/code&gt; names the file and line for each.&lt;br&gt;
It is a fair first impression of what the tool does: it tells you where, and it does not pretend the&lt;br&gt;
number is smaller than it is.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Results welcome&lt;/strong&gt; — &lt;a href="https://github.com/isamu/ever-better/blob/main/docs/RESULTS.md" rel="noopener noreferrer"&gt;&lt;code&gt;docs/RESULTS.md&lt;/code&gt;&lt;/a&gt; collects what people froze and how much of it turned out to be real defects. It has two rows and both are mine, which is enough to notice a pattern and nowhere near enough to claim one. A run that found &lt;strong&gt;zero&lt;/strong&gt; real bugs is the most useful row that table could get.&lt;/p&gt;

&lt;p&gt;→ &lt;strong&gt;&lt;a href="https://github.com/isamu/ever-better" rel="noopener noreferrer"&gt;github.com/isamu/ever-better&lt;/a&gt;&lt;/strong&gt; (MIT)&lt;/p&gt;

</description>
      <category>eslint</category>
      <category>typescript</category>
      <category>javascript</category>
      <category>devops</category>
    </item>
    <item>
      <title>Handing an AI a big spec and letting it build the whole thing is like buying a lottery ticket</title>
      <dc:creator>Isamu Arimoto</dc:creator>
      <pubDate>Fri, 07 Aug 2026 07:27:28 +0000</pubDate>
      <link>https://dev.to/isamu/handing-an-ai-a-big-spec-and-letting-it-build-the-whole-thing-is-like-buying-a-lottery-ticket-380o</link>
      <guid>https://dev.to/isamu/handing-an-ai-a-big-spec-and-letting-it-build-the-whole-thing-is-like-buying-a-lottery-ticket-380o</guid>
      <description>&lt;p&gt;Ever since I wrote &lt;a href="https://dev.to/isamu/i-stopped-reviewing-my-own-code-heres-what-had-to-be-true-first-4nh0"&gt;I stopped reviewing my own code&lt;/a&gt;, I keep getting the same question.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;I ended up with a huge pile of code and I can't review it. What should I do?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The comments say the same thing in different words. &lt;strong&gt;The volume the AI writes went up. The reading side did not keep up.&lt;/strong&gt; So what now?&lt;/p&gt;

&lt;p&gt;Here is the blunt answer first.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Don't get into that state.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Taking something that already exists and rescuing it with review alone is, I think, very hard. &lt;strong&gt;You can split it, scope it down, throw parts away — but none of those is a substitute for actually reading it.&lt;/strong&gt; And "just have the AI review it" probably isn't the answer either.&lt;/p&gt;

&lt;p&gt;And &lt;strong&gt;this is not only a team problem.&lt;/strong&gt; It happens exactly the same way when you work alone. &lt;strong&gt;The cause isn't headcount. It's writing a big spec up front and having the whole thing built in one go.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That is like &lt;strong&gt;buying a single lottery ticket where you don't find out whether you won until the very end.&lt;/strong&gt; This post is about why, with arithmetic and with measurements.&lt;/p&gt;

&lt;p&gt;The second half spills over into a related question: &lt;strong&gt;how many people should be on a product.&lt;/strong&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; the numbers here are measured from my own repository. But treat commit and PR counts as &lt;strong&gt;observational data about how I work, not as evidence of productivity&lt;/strong&gt;.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Why you end up with "too big to review"
&lt;/h2&gt;

&lt;p&gt;Most of the time, I think it's because &lt;strong&gt;the whole spec was decided first and built in one shot.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;You write the spec. You hand it to the AI. A large amount of code comes out. &lt;strong&gt;And now, to judge whether any of it is right, you have to read all of it.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;But there's a prior question.&lt;/p&gt;

&lt;h3&gt;
  
  
  You can't write a spec for something when you don't know if it's possible or if you even want it
&lt;/h3&gt;

&lt;p&gt;The starting point for &lt;a href="https://github.com/receptron/mulmoterminal" rel="noopener noreferrer"&gt;MulmoTerminal&lt;/a&gt;, which I build, was &lt;strong&gt;an experiment: can a terminal run inside a browser?&lt;/strong&gt; It worked, so I widened it into a grid. After that I just used it and added what I wanted. That's all.&lt;/p&gt;

&lt;p&gt;There was no room for a spec at that moment.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;I didn't know if it was possible&lt;/strong&gt; — nine real terminals side by side in a browser tab, usable in practice?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;I didn't know if I wanted it&lt;/strong&gt; — would putting them side by side actually be nice? I couldn't tell.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;With those two unknown, what exactly is there to write?&lt;/strong&gt; Anything you write is a wish, not a spec.&lt;/p&gt;

&lt;p&gt;There are, clearly, cases where a spec works well. Building an Amazon clone. Building an internal system where the UI doesn't matter much. &lt;strong&gt;The reason a spec is writable there is that the thing already exists in the world.&lt;/strong&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;A spec is a compression of knowledge that already exists. It is not a way to discover what doesn't exist yet.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;You cannot compress something that isn't there to compress.&lt;/p&gt;

&lt;h3&gt;
  
  
  It's like buying a single lottery ticket
&lt;/h3&gt;

&lt;p&gt;Deciding everything before you know anything is, to me, &lt;strong&gt;betting the outcome on one ticket.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;And it's worse than a lottery in one specific way.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;A lottery tells you whether you won on the spot.&lt;/strong&gt;&lt;br&gt;
&lt;strong&gt;A big spec doesn't tell you until you've finished building and started using it.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Let's compute the odds that "all of it works"
&lt;/h3&gt;

&lt;p&gt;Hand over a big spec and let it build. &lt;strong&gt;Some of it comes out good, some of it doesn't. You can specify the UI in detail and sometimes get exactly that, sometimes not.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;So what are the odds that &lt;strong&gt;all of it&lt;/strong&gt; comes out the way you wanted?&lt;/p&gt;

&lt;p&gt;Say each individual decision has a &lt;strong&gt;90% chance&lt;/strong&gt; of landing the way you intended. That's a generous assumption.&lt;/p&gt;

&lt;p&gt;(This is a simple calculation that assumes the items don't affect each other. In reality they interact, so don't take it literally — &lt;strong&gt;it's for the order of magnitude.&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;Number of decisions&lt;/th&gt;
&lt;th&gt;Chance all of them land&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;73%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;59%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;35%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;20&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;12%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;50&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.5%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;It's a product, so it falls much faster than people expect.&lt;/strong&gt; Even at a near-ideal 95% per item, twenty items gives you 36%.&lt;/p&gt;

&lt;p&gt;And &lt;strong&gt;a big spec holds twenty or fifty decisions without trying.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Here's the part that matters. At twenty items, &lt;strong&gt;the chance that at least one of them does not land is about 88%.&lt;/strong&gt; &lt;strong&gt;So what happened to that "at least one"?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Usually it gets accepted with "&lt;strong&gt;well, it runs, good enough&lt;/strong&gt;." &lt;strong&gt;Because nobody checked which parts didn't land.&lt;/strong&gt; Checking would mean reading all of it, and the amount that came out is exactly the amount you can't read.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Are you actually satisfied with that?&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  "Just write a more detailed spec" ends at implementation
&lt;/h3&gt;

&lt;p&gt;Some of you are thinking:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Then write the spec in enough detail that it can't miss?&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;Correct.&lt;/strong&gt; The only way to raise the per-item odds is to remove ambiguity.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;But writing it out in full means enumerating, in advance, every decision you would otherwise make while implementing.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Where the button goes, what happens when you press it, what shows up when it fails, what to do when the value is empty. &lt;strong&gt;Write all of that and yes, you get closer to what you wanted — but the volume you write, and the volume you think, moves toward implementation.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;And there is one decisive difference.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;A spec doesn't run.&lt;/strong&gt;&lt;br&gt;
&lt;strong&gt;No matter how detailed it is, it will not tell you whether it's right.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;Detailing a spec looks like raising your odds. In practice it tends to be "postponing the answer while increasing the bet."&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  And AI flipped this trade-off
&lt;/h3&gt;

&lt;p&gt;"Settle it on paper first" &lt;strong&gt;used to be the correct call.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Before   rewriting is expensive  → building big and then fixing costs a lot
                                 → cheaper to be wrong on paper

Now      rewriting is cheap      → rebuilding doesn't cost much
                                 → cheaper to be wrong in working code
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Being wrong on paper was rational because rewriting was expensive.&lt;/strong&gt; That premise moved.&lt;/p&gt;

&lt;p&gt;That said, &lt;strong&gt;not everything got cheap.&lt;/strong&gt; What got cheap is &lt;strong&gt;transcribing something whose shape you can already see.&lt;/strong&gt; Migrations, public APIs, anything wired to another company — rewriting those is still expensive. &lt;strong&gt;Settle those up front.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;So: &lt;strong&gt;once you've reached the point of "let me spec this out in detail," you may as well build it there.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Give the AI instructions in small increments and &lt;strong&gt;you get something running in about the time it takes to write the detailed spec.&lt;/strong&gt; And the running version has a decisive advantage.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;A spec stays silent when it's wrong.&lt;/strong&gt;&lt;br&gt;
&lt;strong&gt;Something that runs tells you immediately.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If you're spending the time either way, &lt;strong&gt;spend it on the one that tells you.&lt;/strong&gt; That's the whole argument.&lt;/p&gt;

&lt;h3&gt;
  
  
  So how do you build? Tighten the implementation, not the spec
&lt;/h3&gt;

&lt;p&gt;First, look at what happens &lt;strong&gt;when you build big in one go and it turns out to be wrong.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Everything produced up to that point &lt;strong&gt;sits on top of that spec.&lt;/strong&gt; You aren't fixing a part; you're restacking. &lt;strong&gt;And you usually notice "restacking would have been faster" only after you've finished stacking.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;So keep it small enough to throw away.&lt;/strong&gt; There's one thing to do.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Cut down to the smallest unit that needs a judgement, implement them one at a time, and judge each time.&lt;/strong&gt;&lt;br&gt;
&lt;strong&gt;What you tighten is the implementation, not the spec.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;On odds, it's just the table read backwards: &lt;strong&gt;decide five at a time and you're at 59%; three and you're at 73%.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;But odds aren't the only thing that improves. &lt;strong&gt;Cutting small changes three other things.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. The judgement itself gets more accurate&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Looking at twenty things at once and asking "is this fine?" versus looking at them one at a time — &lt;strong&gt;the second is more accurate.&lt;/strong&gt; Detail drops out when you look at everything together. And as above, &lt;strong&gt;you end up waving it through with "well, it runs."&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. You can change direction partway&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If you realise at the third one that the whole direction is wrong, &lt;strong&gt;you can turn there.&lt;/strong&gt; Realise it after finishing all twenty and the cost of turning is &lt;strong&gt;everything you already stacked.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. You can put it in front of people early&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This might be the biggest one. &lt;strong&gt;Getting something running early also means getting it used early.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Later in this post I describe &lt;strong&gt;how the purpose I built the thing for turned out to be wrong.&lt;/strong&gt; The only reason I found that out is that &lt;strong&gt;people could actually touch something running.&lt;/strong&gt; Had I shown nobody until it was all finished, &lt;strong&gt;it would have been completed against the wrong purpose.&lt;/strong&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  "Isn't one at a time slow?"
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Yes.&lt;/strong&gt; Done serially, of course it is.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;So do it in parallel.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Run about ten independent things at the same time.&lt;/strong&gt; Then &lt;strong&gt;evaluate them as they land and feed corrections back.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;cut small, serially     →  slow. which makes you want to batch it up
cut small, in parallel  →  not slow. it can stay small
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Cutting small only becomes practical once you can run things in parallel.&lt;/strong&gt; Conversely, &lt;strong&gt;in an environment where you can't run in parallel, betting big is all that's left.&lt;/strong&gt; That, I suspect, is why "write the whole spec first" still looks rational.&lt;/p&gt;

&lt;p&gt;And what happens when you do run in parallel? &lt;strong&gt;You end up evaluating finished work, one after another.&lt;/strong&gt; When that clogs, everything stops. &lt;strong&gt;If there's one person evaluating, their spare attention is the ceiling.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;That's why the second half of this post ends up being about headcount and parallelism.&lt;/strong&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  That said, cutting small isn't automatically safe
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;It isn't as simple as "cut it smaller and per-item accuracy is preserved."&lt;/strong&gt; Cut in the wrong place and new mistakes appear at the seams.&lt;/p&gt;

&lt;p&gt;What is decisively different is this: &lt;strong&gt;you find out you were wrong sooner, and you can throw away only what you built so far.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Every number in the second half of this post exists to serve that.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  This isn't AI's fault. It was always like this
&lt;/h2&gt;

&lt;p&gt;Something worth remembering.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Back when only humans wrote code, specs were wrong too.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;You write the spec, hand it to engineering, and it comes back: "this doesn't work as written." You touch something running and only then realize "no, not this." &lt;strong&gt;Those round trips did not happen because implementation was slow.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;They happened because the spec was wrong and nobody could tell until they touched it.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;What AI made fast is &lt;strong&gt;mostly implementation, and the planning around it.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;what was slow       implementation  → got fast
what was wrong      the spec        → barely changed
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;The cause of failure is largely unaddressed.&lt;/strong&gt; If anything, &lt;strong&gt;wrong specs now become real faster and in larger volume.&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  The same thing happens under Agile
&lt;/h3&gt;

&lt;p&gt;"That's why we do Agile" is a fair objection. Short cycles, build, check, correct. The distance really does shrink.&lt;/p&gt;

&lt;p&gt;But &lt;strong&gt;it stops when the product owner and the team are separated.&lt;/strong&gt; The person deciding is in a meeting, not at a keyboard. Decisions lag, and in the worst case &lt;strong&gt;the kind of judgement you only discover while writing code never reaches anyone.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I don't think the real variable is waterfall vs. Agile.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;The distance between the person deciding and the code.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;
&lt;/blockquote&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;spec everything first   the decider is farthest from the code, and decides once, from there
Agile (separated)       the decider is in a meeting; decisions come at sprint granularity
working alongside       the decider is at the keyboard
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Agile shortened the distance. It didn't take it to zero. &lt;strong&gt;And there are decisions that only reach you at zero.&lt;/strong&gt; The examples below are what I mean.&lt;/p&gt;




&lt;h2&gt;
  
  
  The tool got smarter. You didn't
&lt;/h2&gt;

&lt;p&gt;Some of you are thinking:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;But AI helps write the spec now. Isn't that different from before?&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I think this is the biggest misunderstanding around.&lt;/p&gt;

&lt;p&gt;AI did get smarter. Throw it requirements and a plausible-looking spec comes back. &lt;strong&gt;And that makes it feel like your own ability went up.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It didn't.&lt;/strong&gt; The tool got smarter. &lt;strong&gt;You are still the bottleneck.&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Leverage is a multiplier, not the number being multiplied
&lt;/h3&gt;

&lt;p&gt;AI is powerful leverage. &lt;strong&gt;People with a clear vision of what they want to build use it astonishingly well.&lt;/strong&gt; That part is real.&lt;/p&gt;

&lt;p&gt;But &lt;strong&gt;leverage is multiplication, and the thing being multiplied hasn't changed.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A plausible spec appears instantly. &lt;strong&gt;But reading it and deciding "this is sound" or "this is wrong" is still you.&lt;/strong&gt; And &lt;strong&gt;the ability to produce a good spec is still yours to have or not have.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Writing makes this easy to see.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Plausible prose is easy to get out of an AI.&lt;/strong&gt;&lt;br&gt;
&lt;strong&gt;Getting genuinely excellent prose out of one is still extremely hard.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Anyone who writes knows this in their gut. &lt;strong&gt;Specs and design are exactly the same.&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Former CTOs joining AI labs as individual contributors is probably the same story
&lt;/h3&gt;

&lt;p&gt;This is actually happening.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Peter Bailis&lt;/strong&gt; — CTO of Workday (from May 2025). Left in March 2026 and joined Anthropic as a &lt;strong&gt;Member of Technical Staff&lt;/strong&gt;, working on reinforcement learning engineering (&lt;a href="https://thenextweb.com/news/workday-cto-bailis-anthropic-member-technical-staff" rel="noopener noreferrer"&gt;The Next Web, 2026-04-09&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bryan McCann&lt;/strong&gt; — co-founder and CTO of You.com. Also moved to Anthropic as a Member of Technical Staff (&lt;a href="https://www.theinformation.com/briefings/ai-startup-com-appoints-new-cto-co-founder-joins-anthropic" rel="noopener noreferrer"&gt;The Information&lt;/a&gt;)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;CTOs of public companies and fast-growing startups, dropping the title for an IC seat.&lt;/strong&gt; At minimum, several moves in the same direction have been reported recently.&lt;/p&gt;

&lt;p&gt;The Next Web characterises it this way:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;executives at the top of established technology companies choosing proximity to cutting-edge research over the authority that comes with managing large organisations&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;That is where the reporting ends.&lt;/strong&gt; Both moves are documented. &lt;strong&gt;Why they did it is not in those articles.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;From here on, this is my speculation.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If the goal were a bigger title or better terms, being CTO somewhere else was available. Not taking that reads, to me, like a statement about priorities.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Until now, building something large required a large team.&lt;/strong&gt; For a CTO-level person to realise an idea, they needed capable people and an organisation. &lt;strong&gt;AI is filling that part.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If so, &lt;strong&gt;taking an IC seat where the smartest model and unlimited access to it live, instead of leading an organisation&lt;/strong&gt;, is an entirely reasonable choice.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;This is also a story about people with a clear vision being able to move with fewer people&lt;/strong&gt; — the same direction as this post's conclusion.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;But again.&lt;/strong&gt; What changed is leverage.&lt;/p&gt;

&lt;p&gt;AI increases the material for a decision. It lists options, it catches omissions. &lt;strong&gt;But the criteria for picking among those options exist only inside you.&lt;/strong&gt; More material without criteria means &lt;strong&gt;you feel like you can choose, and you can't.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Without something to multiply, a multiplier means nothing.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Some decisions don't exist until you build
&lt;/h2&gt;

&lt;p&gt;Abstractions don't land, so here are two real ones.&lt;/p&gt;

&lt;h3&gt;
  
  
  Example 1: the purpose I built it for was wrong
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://github.com/receptron/mulmoterminal" rel="noopener noreferrer"&gt;MulmoTerminal&lt;/a&gt;, which I build, is &lt;strong&gt;a tool for running many AI agents side by side.&lt;/strong&gt; The screen is a grid of cells, each running its own work.&lt;/p&gt;

&lt;p&gt;My assumption while building was clear.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;This raises the ceiling on how many you can run in parallel.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If I were writing a spec, I would have written exactly that. &lt;strong&gt;It's why I built it.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Once people were actually using it, I asked four of them. How many do you actually run? What's good about it?&lt;/p&gt;

&lt;p&gt;Here's what came back.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;agents actually run in parallel                1 to 6
people who said "I can run more now"           0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Not one.&lt;/strong&gt; What all of them said was something else entirely.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;I can handle the same number more easily.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;One person runs one to three and dropped VS Code entirely.&lt;/strong&gt; The count didn't go up. They switched anyway.&lt;/p&gt;

&lt;p&gt;So &lt;strong&gt;the value of this tool was not "raising the ceiling." It was "not losing track."&lt;/strong&gt; The person who built it had it wrong.&lt;/p&gt;

&lt;h3&gt;
  
  
  And then it got worse
&lt;/h3&gt;

&lt;p&gt;From the same conversations:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;I got up to eight, but &lt;strong&gt;the notifications became a storm and I got tired and stopped&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;Notifications exist to let you run more.&lt;/strong&gt; So you can walk away and still be called back.&lt;/p&gt;

&lt;p&gt;Those notifications &lt;strong&gt;were creating a new ceiling.&lt;/strong&gt; One part of the tool was lowering the ceiling another part had raised.&lt;/p&gt;

&lt;p&gt;The cause was concrete: "finished" and "waiting on you" &lt;strong&gt;played the same sound.&lt;/strong&gt; Indistinguishable, so you end up caring about all of them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;There is no way to know this before building.&lt;/strong&gt; At the moment you add notifications, you assume they only push in one direction. &lt;strong&gt;Nobody finds out until someone actually runs eight and gets tired.&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Example 2: the AI said "don't do it" — and it was wrong
&lt;/h3&gt;

&lt;p&gt;There are tools that find duplicated code automatically. At one point ours reported a lot of "the same shape appears in many places."&lt;/p&gt;

&lt;p&gt;The shape was something like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;read A → error if missing → catch the failure and log it
read B → error if missing → catch the failure and log it
read C → error if missing → catch the failure and log it
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When I went to consolidate it, &lt;strong&gt;the AI told me not to.&lt;/strong&gt; From the dev log:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The checker reports many duplicates, but this is a structural repetition — read, error if not found, catch the exception — and &lt;strong&gt;each site is readable precisely because it is self-contained&lt;/strong&gt;. Consolidating would mean rewriting large parts of a 2,700-line file, and &lt;strong&gt;readability would get worse, not better&lt;/strong&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;The reasoning holds together.&lt;/strong&gt; "A wrong abstraction is worse than duplication" is in fact a principle we've written down. I read it and thought, fair enough.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;But when I opened the code myself later, it was wrong.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;What had been flagged was not a coincidental resemblance. It was &lt;strong&gt;genuinely the same thing written over and over.&lt;/strong&gt; Consolidating it made things &lt;strong&gt;noticeably cleaner.&lt;/strong&gt; We ended up doing it as a separate task.&lt;/p&gt;

&lt;p&gt;Laid out, what happened was:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;the tool said "duplicate"              →  correct
the AI said "don't consolidate"        →  plausible, and wrong
I opened the file and checked          →  consolidating read better
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;AI can justify not doing something with a plausible reason.&lt;/strong&gt; And the principle it cites is &lt;strong&gt;generally correct&lt;/strong&gt;, which is why you nod along.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Whether an argument is convincing and whether it's right for this code are different questions.&lt;/strong&gt; And the second one &lt;strong&gt;can't be settled until you open the file.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Which brings us back to distance.&lt;/p&gt;

&lt;p&gt;If I had turned this into a ticket that said "reduce duplication" and handed it to someone, &lt;strong&gt;the AI's explanation would probably have won.&lt;/strong&gt; It's well written; there's nothing to argue against. &lt;strong&gt;Only the person who opened the file can overturn it.&lt;/strong&gt;&lt;/p&gt;




&lt;p&gt;What these two share is that &lt;strong&gt;the right answer wasn't knowable until something actually ran.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Example 1&lt;/strong&gt; — &lt;strong&gt;the purpose it was built for&lt;/strong&gt; turned out to be wrong, and only usage revealed it&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Example 2&lt;/strong&gt; — a plausible explanation from an AI was, in fact, wrong&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Neither fits in a spec or a ticket.&lt;/strong&gt; By the time it fits, you've already checked.&lt;/p&gt;

&lt;p&gt;What you can decide before building is &lt;strong&gt;only what you already know.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  AI doesn't shorten the distance
&lt;/h2&gt;

&lt;p&gt;This is where the original question comes back.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What AI made fast is mostly implementation and planning — and the benefit lands on whoever is at the keyboard.&lt;/strong&gt; If the authority to decide sits far from the keyboard, that speedup doesn't reach it as-is.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;authority at the keyboard   →  fast, and corrected as you go
authority still far away    →  fast, and still far away
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;"Fast while still far away" is the worst state.&lt;/strong&gt; When the person judging stays distant, &lt;strong&gt;the speed at which mistakes are noticed barely changes, while the speed at which they accumulate goes up.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The result of that is "&lt;strong&gt;too big to review&lt;/strong&gt;."&lt;/p&gt;

&lt;h3&gt;
  
  
  Why "just have the AI review it" doesn't fix it
&lt;/h3&gt;

&lt;p&gt;AI reads far more text far faster than a human. That's true. &lt;strong&gt;So I won't claim "the AI can't read it."&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Two things matter, though.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;First, there's a ceiling on how much it can hold at once&lt;/strong&gt; (the context — how much text the model can look at simultaneously). Depending on the model and your tooling, a large diff may not fit in one pass. Split it up and it fits, but &lt;strong&gt;splitting makes it much harder to surface "A and B don't line up."&lt;/strong&gt; It only ever sees one side. And bugs usually live at exactly that seam.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Second, passing review doesn't mean the design is good.&lt;/strong&gt; I have a concrete example of what survives after every machine check is green.&lt;/p&gt;

&lt;p&gt;At one point I eliminated every "function that is too long" in my repo. Machines had nothing to complain about. Three months later:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;CollectionView.vue   2,945 lines   187 functions   longest 61 lines
server/index.ts      3,020 lines   212 functions   longest 82 (most under 10)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;There are no long functions. Nobody is being warned. And the files are 3,000 lines.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Because the machine only looks at "how long is this one function," never "how many different jobs live in this one file." &lt;code&gt;CollectionView.vue&lt;/code&gt; had &lt;strong&gt;eleven separate jobs&lt;/strong&gt; in it — list rendering, filtering, editing, chat, and more.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;187 small functions added up to 3,000 lines, and the machine never once complained.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;"The machine passed it, so it's fine" doesn't hold for review or for design. &lt;strong&gt;And a setup that puts AI review as the last line of defence collapses right here.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  So what do I actually do
&lt;/h2&gt;

&lt;p&gt;Numbers from my own way of working. Again: &lt;strong&gt;this is not proof of correctness, it's observational data about how I work.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;MulmoTerminal (2026-06-14 → 2026-08-06, 53 days)

  commits        3,235
  merged PRs     1,170
  releases          64      about 1.2 per day
  written by     effectively 2 people (2,990 / 221, plus one at 24)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Time from opening to closing, last 400 closed issues
  median          1.6 hours
  within 6h       77%
  within 24h      92%
  over a week      2%

Diff size, last 400 merged PRs
  median          286 lines
  under 300       51%
  files changed   median 7
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;How I measured&lt;/p&gt;

&lt;p&gt;Closed issues: &lt;code&gt;gh issue list --state closed --limit 400&lt;/code&gt;, taking the difference between created and closed timestamps. PRs: the last 400, additions plus deletions. &lt;strong&gt;Both are cut as "the most recent 400," so if the way I work changed partway through, that shows up in the number.&lt;/strong&gt; Commits and releases are &lt;code&gt;git rev-list --count&lt;/code&gt; and &lt;code&gt;git tag&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Issues shaped like this &lt;strong&gt;are not specs written before building.&lt;/strong&gt; They're notes of something I noticed while using the thing, fixed the same day. That's why they're short-lived. &lt;strong&gt;If everything had been decided up front, the distribution wouldn't look like this.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Also: &lt;strong&gt;all these numbers can show is that things were built fast.&lt;/strong&gt; Whether what got built is good is a separate question. Please read them separately.&lt;/p&gt;

&lt;p&gt;On that basis, &lt;strong&gt;I keep the state from ever producing a giant diff.&lt;/strong&gt; Not "what do I do with unreviewable code" — &lt;strong&gt;don't get there.&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  I do write plans. 351 of them. But the AI writes them.
&lt;/h3&gt;

&lt;p&gt;To be clear, I'm not building with nothing written down.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;351 files in plans/
  features 166 / fixes 132 / cleanups 37 / other 16

  278 of them (79%) carry an issue number
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But these are &lt;strong&gt;not specs. They're the steps for clearing one issue.&lt;/strong&gt; And &lt;strong&gt;the AI writes them&lt;/strong&gt; — I read and judge.&lt;/p&gt;

&lt;p&gt;The order looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1  I notice "this is wrong" while using it
2  I open an issue          ← me. deciding what to do
3  the AI writes the steps   ← automatic
4  I read it, correct it     ← me again
5  it gets implemented       ← automatic
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;The AI can only write step 3 after step 2 is settled.&lt;/strong&gt; For something where what-to-do isn't decided, having it write is pointless. &lt;strong&gt;As above: plausible output appears, and whether to take it is still my call.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Which is to say —&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;You can only write it once the shape is already known.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Our own process turned out to be the inverse statement of the argument. &lt;strong&gt;I'm not rejecting writing things down. I'm keeping to the conditions under which it can be written.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Adding one person doesn't add just one channel
&lt;/h2&gt;

&lt;p&gt;From here on, this is &lt;strong&gt;about having more than one person.&lt;/strong&gt; If you work alone, feel free to skip.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Judgement and alignment grow with the number of channels, not the number of people.&lt;/strong&gt; It's pairs, so for n people it's &lt;code&gt;n(n-1)/2&lt;/code&gt;.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;People&lt;/th&gt;
&lt;th&gt;Channels&lt;/th&gt;
&lt;th&gt;vs. 2 people&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;1&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;3×&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;6&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;6×&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;10×&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;28&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;28×&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;45&lt;/td&gt;
&lt;td&gt;45×&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Go from two to four and you double the hands — and multiply channels by six.&lt;/strong&gt; At eight, four times the hands and &lt;strong&gt;twenty-eight times the channels&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;This is a simplified model; not everyone talks to everyone. But it does show that &lt;strong&gt;what grows when you add someone isn't only hands.&lt;/strong&gt; Brooks' &lt;em&gt;The Mythical Man-Month&lt;/em&gt; — "adding people to a late project makes it later" — is rooted in the same place.&lt;/p&gt;

&lt;p&gt;The question is &lt;strong&gt;what AI changed and what it didn't.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Before
  what you gain   implementation   grows with headcount (n×)
  what you pay    channels         grows as n(n-1)/2
  → there's a point where it stops paying, but below it the gain won

Now
  what you gain   implementation   the AI produces it; barely tied to headcount
  what you pay    channels         still n(n-1)/2. unchanged
  → the gain side shrank; the cost side stayed
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;At least the reason "we need more hands to write it" got weaker, while the price of adding people stayed put.&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  This connects back to the former CTOs
&lt;/h3&gt;

&lt;p&gt;Earlier I mentioned &lt;strong&gt;CTOs from Workday and You.com joining Anthropic as individual contributors.&lt;/strong&gt; I think that's this formula too.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Lead a large organisation and you get implementation capacity proportional to headcount.&lt;/strong&gt; At the same time, &lt;strong&gt;you carry &lt;code&gt;n(n-1)/2&lt;/code&gt; channels.&lt;/strong&gt; A decision only becomes real after it has been through all of them.&lt;/p&gt;

&lt;p&gt;What happens if you step off that?&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;take it from an organisation   capacity × headcount  +  carry n(n-1)/2 channels
take it from AI                capacity              +  one person's worth of channels
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Sit somewhere you can focus purely on judgement and use AI without limits.&lt;/strong&gt; Get the implementation capacity from the AI rather than from an organisation.&lt;/p&gt;

&lt;p&gt;None of them said this, of course, so &lt;strong&gt;this is my reading.&lt;/strong&gt; But looking at &lt;strong&gt;what the people closest to the frontier just gave up and what they went to get&lt;/strong&gt;, it points this way.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What they gave up was the organisation. What they went for was distance — between judgement and the AI.&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  That said, the formula doesn't give you a number
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;This formula does not produce "so the optimal number is N."&lt;/strong&gt; Channel count is neither productivity nor decision speed. All the formula says is that &lt;strong&gt;one side of the scale got lighter&lt;/strong&gt;; &lt;strong&gt;the specific number comes from my own experience below.&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  There's a ceiling on how much &lt;em&gt;you&lt;/em&gt; can run in parallel
&lt;/h3&gt;

&lt;p&gt;The same thing happens inside one person. Honestly, here's mine:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Within one project
  just skimming and judging        about 10 is reachable
  thinking about design            5 is the limit

Across projects
  work that takes no thought       3 to 4 projects
  dependencies / design work /
  chasing a repro                  can't parallelise (1)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;The tool can show nine. The one that can't is me.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;And adding "one more, I can probably take it" &lt;strong&gt;slows everything down.&lt;/strong&gt; The closer your own utilisation gets to 100%, &lt;strong&gt;the more sharply the agents' waiting time grows&lt;/strong&gt; (picture a convenience store with one register and a clerk who is essentially never idle — a small bump and the queue stretches).&lt;/p&gt;

&lt;p&gt;So the way to raise parallelism isn't "add agents," it's &lt;strong&gt;reduce the part that can't move without you.&lt;/strong&gt; Decide design up front, write the repro steps first, cut dependencies early. &lt;strong&gt;Though that carving-out is also your work, so it never reaches zero.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Which leads to: teams should be small too
&lt;/h2&gt;

&lt;p&gt;Everything up to here holds for one person. &lt;strong&gt;What follows is the multi-person case.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The variable that should set headcount is not implementation volume. It's the amount of judgement.&lt;/strong&gt; By judgement I mean setting priorities, settling design, deciding how exceptions are handled — &lt;strong&gt;the kind of decision that loses accuracy when handed to someone else.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The premise of sizing by implementation volume has broken down, at least around me, because &lt;strong&gt;implementation volume no longer tracks headcount.&lt;/strong&gt; &lt;strong&gt;What's left is judgement, and splitting judgement is where the formula above bites.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;So here's where I land.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;If you're building something whose shape isn't settled yet, and the owner can touch the code:&lt;/strong&gt;&lt;br&gt;
&lt;strong&gt;one to three people per product.&lt;/strong&gt;&lt;br&gt;
The owner, or someone who can back them, does the implementing.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;This is not a universal optimum. It's a conditional rule of thumb.&lt;/strong&gt; It roughly holds when these are true:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;What to build isn't fully decided&lt;/strong&gt; — you're still exploring the shape&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The product's owner can touch the code&lt;/strong&gt; — the decider is at the keyboard&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One person can hold the whole thing in their head&lt;/strong&gt; — when they can't, that's your split line&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You can ship on your own judgement&lt;/strong&gt; — no approval queue&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;If any of those is missing, you can't cut headcount.&lt;/strong&gt; Work that needs deep specialist knowledge, anything running 24/7, industries where the author legally can't review their own work. Those aren't headcount problems.&lt;/p&gt;

&lt;h3&gt;
  
  
  The two-pizza rule isn't about meetings any more
&lt;/h3&gt;

&lt;p&gt;Amazon's &lt;a href="https://docs.aws.amazon.com/whitepapers/latest/introduction-devops-aws/two-pizza-teams.html" rel="noopener noreferrer"&gt;two-pizza team&lt;/a&gt; idea was, originally, a design principle for &lt;strong&gt;small autonomous teams&lt;/strong&gt; — meetings being part of it. When implementation needed people, capping communication on top of that was the rational move.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Implementation no longer scales with headcount.&lt;/strong&gt; What's left is judgement, and judgement is what eats alignment cost.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;then   "we need the people. at least keep meetings and teams small."
now    "we don't need the people. the unit of building can be small."
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  There are things you genuinely want more people for
&lt;/h3&gt;

&lt;p&gt;Read this far and it may sound like "cut headcount, always." It isn't.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The only thing that should be small is the unit of building.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The number of ideas&lt;/strong&gt; — more is better&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The number of people giving you feedback&lt;/strong&gt; — more is better&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The number of people using it&lt;/strong&gt; — more is better&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;As in Example 1, &lt;strong&gt;I did not find out that the purpose was wrong until I asked four people.&lt;/strong&gt; That doesn't happen without people around you.&lt;/p&gt;

&lt;p&gt;So it splits like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;the unit of building     smaller is better    because judgement can't be divided
the people around it     more is better       because it grows the material for judgement
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;That "around" is your community, and your company.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Separate the place you build from the place opinions gather. &lt;strong&gt;Two people inside is fine; outside, more is better.&lt;/strong&gt; If anything, the smaller you make the unit of building, &lt;strong&gt;the more you need material from outside&lt;/strong&gt; — with fewer people inside, your perspective narrows.&lt;/p&gt;

&lt;h3&gt;
  
  
  Cut people per product, not people
&lt;/h3&gt;

&lt;p&gt;Easy to misread, so: &lt;strong&gt;this is not "turn eight people into two."&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;✗  make eight people into two
✓  stop putting eight people on one product
   → 2 people × 4 products
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Reduce the headcount per product, not the headcount.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Though, as above, &lt;strong&gt;three or four projects at once only works for the low-thought work.&lt;/strong&gt; If all of them are in design mode, one is the limit — so &lt;strong&gt;"two people, therefore six to eight products" doesn't follow.&lt;/strong&gt; In practice it's one you're thinking hard about, plus a few you're coasting.&lt;/p&gt;




&lt;h2&gt;
  
  
  In the end, this is what I think it comes down to
&lt;/h2&gt;

&lt;p&gt;I've written a lot above, but the essence is one line.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;The product's owner, in a small group, building it while using it themselves in detail.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That's the shape I think fits building products in the AI era.&lt;/p&gt;

&lt;h3&gt;
  
  
  If you aren't the heaviest user, it won't get good
&lt;/h3&gt;

&lt;p&gt;In Example 1, the purpose I built it for turned out to be wrong. &lt;strong&gt;The only reason I caught that is that I use it every day too.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If I didn't, someone saying "I can handle the same number more easily" would mean nothing to me. &lt;strong&gt;I wouldn't know the weight of the answer.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The material for judgement only accumulates through use. &lt;strong&gt;A tool its owner doesn't use can't be fixed by anyone's judgement.&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  You probably don't need to be a strong programmer
&lt;/h3&gt;

&lt;p&gt;This is the hopeful part, I think.&lt;/p&gt;

&lt;p&gt;What's required is &lt;strong&gt;being clear about what you want to build&lt;/strong&gt;, and &lt;strong&gt;being able to look at the output and say "no, not this."&lt;/strong&gt; &lt;strong&gt;Not writing code quickly.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If you have those two, &lt;strong&gt;an era where non-engineers build good products is coming into view.&lt;/strong&gt; If anything, the clearer you are about what you want, the more the leverage pays.&lt;/p&gt;

&lt;h3&gt;
  
  
  But you can't just hand it off
&lt;/h3&gt;

&lt;p&gt;You give the AI the spec and the implementation, and what comes out is "somehow not right."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Of course it is.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Without settling the details yourself, you don't get a product with substance.&lt;/strong&gt; Micromanagement is not usually a compliment when the subject is a person. &lt;strong&gt;With an AI, though, I think it's simply the correct approach.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;It doesn't complain. It'll redo it as many times as you like. &lt;strong&gt;So you may as well push until you're satisfied.&lt;/strong&gt;&lt;/p&gt;




&lt;h3&gt;
  
  
  What this post said
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Rescuing "too big to review" &lt;strong&gt;with review alone is very hard&lt;/strong&gt;. The answer is to not get there&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The cause is writing a big spec up front.&lt;/strong&gt; This happens when you work alone too&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;You can't write a spec for something when you don't know if it's possible or whether you want it&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;This isn't AI's fault.&lt;/strong&gt; Specs were wrong in the human-only era too, and nobody could tell until they touched it&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Even when AI helps write the spec, deciding whether to take it is still you.&lt;/strong&gt; The tool got smarter, not you&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Leverage is a multiplier, not the number being multiplied.&lt;/strong&gt; Without something to multiply, it means nothing&lt;/li&gt;
&lt;li&gt;Adding people grows channels as &lt;code&gt;n(n-1)/2&lt;/code&gt;. Hence &lt;strong&gt;one to three per product&lt;/strong&gt; (a conditional rule of thumb)&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;Handing over a big spec and letting it build in one go is &lt;strong&gt;buying a single lottery ticket where you don't learn the result until the end.&lt;/strong&gt; And if you try to fix that by writing the spec in more detail, where you arrive is implementation. &lt;strong&gt;Same effort — write the one that runs.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Rough out the base, then work alongside it, using it yourself in detail. &lt;strong&gt;That isn't "being sloppy." It means increasing the number of judgements.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;And the only person who can increase that number is &lt;strong&gt;the one sitting at the keyboard, using the thing themselves.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  One small plug at the end
&lt;/h2&gt;

&lt;p&gt;Every number above came out of building &lt;strong&gt;MulmoTerminal&lt;/strong&gt;. And I'll admit it: &lt;strong&gt;it's also the tool for working the way this post describes.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Cut small and draw often, and this always happens:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Wait — which one is waiting on me right now?&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Run three or four agents and &lt;strong&gt;you'll leave one waiting for tens of minutes without noticing.&lt;/strong&gt; Since your own attention is what caps parallelism, everything stalls when that clogs.&lt;/p&gt;

&lt;p&gt;I built it to solve that with &lt;strong&gt;cell colour, sound, and a push to your phone.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;This tool is specialised for how far one person can run in parallel.&lt;/strong&gt; There is nothing in it for splitting work across a team. &lt;strong&gt;If you're comfortable in a terminal and don't mind working in parallel&lt;/strong&gt;, it probably suits you best.&lt;/p&gt;

&lt;h3&gt;
  
  
  In practice, ten is reachable
&lt;/h3&gt;

&lt;p&gt;Earlier I wrote "measured, it's one to six." That was &lt;strong&gt;the result of asking four people.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I myself run &lt;strong&gt;about ten when I'm purely skimming and judging.&lt;/strong&gt; Someone at a US startup I spoke to said &lt;strong&gt;twelve.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That may look like a contradiction. It isn't.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;thinking about design       5 is the limit
just skimming and judging   about 10 is reachable
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Mix them and you get dragged down to the lower number.&lt;/strong&gt; Which also means: &lt;strong&gt;reduce the things that can't move without you, and the number goes up.&lt;/strong&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Ten to twenty for one person.&lt;/strong&gt; If you're comfortable in a terminal and don't mind parallelism, &lt;strong&gt;I think that's reachable.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Getting there doesn't take more agents. It takes &lt;strong&gt;exactly what this post has been about&lt;/strong&gt;: cut to the smallest unit that needs a judgement, cut dependencies early, write the repro steps first. &lt;strong&gt;Shave down the part that can't move without you, as far as it will go.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What raises the ceiling isn't the tool. It's how tightly you work.&lt;/strong&gt; Give it a go.&lt;/p&gt;

&lt;h3&gt;
  
  
  But the tool alone won't get you to ten
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;If you go and read every one of them thinking "is this okay?", you'll cap out at five.&lt;/strong&gt; To grow the number you have to &lt;strong&gt;reduce the attention each one costs.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;What I do for that is written up in the other posts in this series (Japanese):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://dev.to/isamu/i-stopped-reviewing-my-own-code-heres-what-had-to-be-true-first-4nh0"&gt;I stopped reviewing my own code&lt;/a&gt; — the whole picture of &lt;strong&gt;not breaking without reading&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://dev.to/isamu/green-is-not-evidence-two-of-my-checks-were-covering-less-than-i-thought-j99"&gt;Green is not evidence&lt;/a&gt; — whether &lt;strong&gt;the checks you have are actually checking anything&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Without that machinery in place, ten in parallel doesn't hold.&lt;/strong&gt; With it, &lt;strong&gt;your attention goes to judgement only.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It's all connected.&lt;/strong&gt; "Cut small and run in parallel" here, and "don't break without reading" over there, &lt;strong&gt;are the same one thing written from two sides.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;It runs on your own machine and &lt;strong&gt;you look at it in a browser.&lt;/strong&gt; It starts with &lt;code&gt;npx&lt;/code&gt;, so there's nothing to install first.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx mulmoterminal@latest
&lt;span class="c"&gt;# → http://localhost:34567&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;→ &lt;strong&gt;&lt;a href="https://github.com/receptron/mulmoterminal" rel="noopener noreferrer"&gt;receptron/mulmoterminal&lt;/a&gt;&lt;/strong&gt; (MIT)&lt;/p&gt;

&lt;p&gt;If you're running things in parallel on your own, or with a small group, it may well help.&lt;/p&gt;




</description>
      <category>ai</category>
      <category>productivity</category>
      <category>programming</category>
      <category>softwareengineering</category>
    </item>
    <item>
      <title>When your GUI isn't the whole input language</title>
      <dc:creator>Isamu Arimoto</dc:creator>
      <pubDate>Mon, 03 Aug 2026 20:59:42 +0000</pubDate>
      <link>https://dev.to/isamu/when-your-gui-isnt-the-whole-input-language-2a05</link>
      <guid>https://dev.to/isamu/when-your-gui-isnt-the-whole-input-language-2a05</guid>
      <description>&lt;p&gt;I spent a week of my life on where a button should go. I don't think I'll do that again, and it took porting a benchmark tool to work out why.&lt;/p&gt;

&lt;p&gt;Let me concede the obvious thing first, because otherwise it's the only thing anyone will want to talk about: &lt;strong&gt;GUIs are still input devices.&lt;/strong&gt; Direct manipulation, menus, palettes, keyboard shortcuts, drag targets, accessibility affordances — all input, all irreplaceable. I'm not arguing for chat-instead-of-UI. Command palettes, scripting and macro systems have absorbed open-ended commands inside GUIs for decades, and they work.&lt;/p&gt;

&lt;p&gt;The argument is narrower, and it's about a specific failure: &lt;strong&gt;designing controls first, for a system whose set of valid operations won't hold still.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Where I noticed
&lt;/h2&gt;

&lt;p&gt;I'd built a benchmark tool. It evaluates combinations of &lt;strong&gt;LLM/SLM models × skills&lt;/strong&gt;, and scores each combination on four things: accuracy, runtime, turn count, and total cost.&lt;/p&gt;

&lt;p&gt;It also tracks the moving parts. &lt;strong&gt;New models arrive constantly. Prices change constantly.&lt;/strong&gt; Both are part of the tool's state.&lt;/p&gt;

&lt;p&gt;It ran inside Claude Code. I decided to port it to a proper web tool, sat down to design the interface, and got about as far as two dropdowns before it fell apart.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The two dropdowns were fine.&lt;/strong&gt; Pick a model. Pick a skill set. Both are finite, both need to be &lt;em&gt;shown&lt;/em&gt; to you — you can't ask for a model if you don't know it exists — and both are cheap to render.&lt;/p&gt;

&lt;p&gt;Everything else was the problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the work actually is
&lt;/h2&gt;

&lt;p&gt;The tool isn't really "run a benchmark." It's &lt;em&gt;maintenance&lt;/em&gt;. Every time a model or a skill is added or updated, work appears:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A model was added. Re-run it against which skills? All of them, or the ones where the previous best is close? Compare against which baseline?&lt;/li&gt;
&lt;li&gt;A price changed. Recompute historical cost, or only from here? Are last month's cost comparisons still meaningful?&lt;/li&gt;
&lt;li&gt;A skill was updated. Which stored results are now invalid? Re-run the whole matrix, or the affected column?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I kept trying to design controls for these and kept discovering the same thing: &lt;strong&gt;each one is a slightly different task.&lt;/strong&gt; Not a variant of a task — a different task, shaped by what changed and what I already knew.&lt;/p&gt;

&lt;p&gt;To put that in a UI I'd have to enumerate them. Every one, in advance, before knowing what the next model release would make me want. And each new axis creates more cross-combinations with all the others.&lt;/p&gt;

&lt;p&gt;In chat it's a sentence. &lt;em&gt;"gpt-5-mini is in — run it against every skill and put it next to haiku."&lt;/em&gt; The agent works out which runs are actually needed. Nothing was designed for that request specifically, and nothing had to be.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;That's the whole of it.&lt;/strong&gt; Not "chat is better." Just: when the set of valid operations keeps changing, a control surface has to keep growing, and it's always one release behind.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the line fell
&lt;/h2&gt;

&lt;p&gt;Not "finite versus infinite." I had it wrong at first. The line is:&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;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;UI keeps it&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Things that must be &lt;strong&gt;enumerated and shown&lt;/strong&gt; — available models, available skills, the price table, the four scores&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Chat takes it&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Things where &lt;strong&gt;the task is different every time&lt;/strong&gt; — what to re-run, against which baseline, which results to invalidate&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The model list has to be a UI, and not because it's short. It's because &lt;strong&gt;you cannot ask for something you don't know exists.&lt;/strong&gt; Enumeration is the whole value.&lt;/p&gt;

&lt;p&gt;The four scores are the same. Accuracy, runtime, turns, cost — you read them. They update when a price moves. There is nothing to click, and clicking would not help.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part I didn't expect
&lt;/h2&gt;

&lt;p&gt;When a user gets stuck in a GUI, you often get nothing usable. They can't find the control, conclude the thing can't be done, and leave. You may see that they dropped off; &lt;strong&gt;you rarely learn what they were trying to do.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;When a user gets stuck in chat, they type what they were trying to do. Now you have a sentence describing a task your product doesn't support, in their words.&lt;/p&gt;

&lt;p&gt;I want to be careful here, because there's an obvious objection and it's correct: &lt;strong&gt;chat users also leave silently.&lt;/strong&gt; Plenty do. And GUIs have telemetry, funnels, session recordings — this isn't a categorical difference, and I'd be overselling it if I said it was.&lt;/p&gt;

&lt;p&gt;What it is, I think, is a difference in &lt;em&gt;recovery&lt;/em&gt;. A failed GUI interaction usually leaves you an event, not an intention. A failed chat interaction leaves a sentence, which means someone — you, a support person, or the agent itself — has something to answer while the user is still there. Not always. But it's the difference between a signal you have to interpret and one that states itself.&lt;/p&gt;

&lt;p&gt;For a tool whose task list changes every week, that mattered more than I expected. The unsupported requests &lt;em&gt;are&lt;/em&gt; the roadmap.&lt;/p&gt;

&lt;h2&gt;
  
  
  A second data point, honestly labelled
&lt;/h2&gt;

&lt;p&gt;I also maintain a browser terminal grid for supervising several coding agents at once. Different product, different problem, built for different reasons.&lt;/p&gt;

&lt;p&gt;Counting one cell's header: roughly &lt;strong&gt;11 buttons and 9 status chips.&lt;/strong&gt; The chips are directory, git branch and change count, model and context percentage, token counts, which PR or issue the cell is on, a diff badge, usage, and a one-line summary of what the agent is currently doing. &lt;strong&gt;None of them is clickable in that configuration.&lt;/strong&gt; They are read.&lt;/p&gt;

&lt;p&gt;And every button that survived does the same kind of thing: expand, close, show files, open the canvas, open the timeline. &lt;strong&gt;They change where you look.&lt;/strong&gt; Not one of them makes the agent do anything — that gets typed.&lt;/p&gt;

&lt;p&gt;The same boundary appeared in another product, one I wasn't thinking about the benchmark tool while building.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;But I should not call that convergence.&lt;/strong&gt; Same author, same period, same tools, same assumptions. It's a personal design pattern showing up twice, not independent validation. Take it as evidence that the boundary isn't specific to one benchmark workflow, and no more.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where this stops being true
&lt;/h2&gt;

&lt;p&gt;Precisely, because a claim that never fails isn't saying much:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Precision and spatial feedback.&lt;/strong&gt; You do not describe a crop, a curve, or a fillet. Photo editors and CAD tools aren't going to move that to a prompt, and shouldn't.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Repetition and muscle memory.&lt;/strong&gt; An action you take fifty times a day should be one keystroke. Typing it is a downgrade every single time.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Discovery.&lt;/strong&gt; A new user doesn't know what to ask for. This is the strongest argument for keeping the control surface, and it's why the model list stayed a list.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Safety.&lt;/strong&gt; "Delete the failed runs" should require pointing at something, not a sentence that could be parsed generously.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Spreadsheets, IDEs and design tools stay hybrids. They should. Chat is an orchestration layer over stable primitives, not a replacement for them.&lt;/p&gt;

&lt;h2&gt;
  
  
  The thesis, as narrowly as I can put it
&lt;/h2&gt;

&lt;p&gt;A visual interface is excellent at exposing &lt;strong&gt;stable objects, state, constraints, and high-frequency actions.&lt;/strong&gt; It becomes expensive and brittle when the valid operations are numerous, compositional, state-dependent, and continuously changing.&lt;/p&gt;

&lt;p&gt;In those systems: &lt;strong&gt;let the UI enumerate what has to be chosen and inspected, and let a conversational layer absorb the requests that would otherwise need a new control every week.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The gain isn't that chat replaces the interface. It's that changing intent becomes &lt;strong&gt;observable, recoverable and extensible&lt;/strong&gt; without shipping a button for every new combination.&lt;/p&gt;

&lt;p&gt;So: stop agonising over button placement — in the parts of your product where the verbs won't hold still. Everywhere else, the button is still the right answer — as it has been the whole time.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;If you've shipped a control surface for a workflow that kept changing underneath it, I'd like to hear how that went. My honest position is that I found the boundary in two of my own products and I don't yet know how far it generalises.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ux</category>
      <category>ai</category>
      <category>architecture</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Green is not evidence. Two of my checks were covering less than I thought.</title>
      <dc:creator>Isamu Arimoto</dc:creator>
      <pubDate>Mon, 03 Aug 2026 08:25:55 +0000</pubDate>
      <link>https://dev.to/isamu/green-is-not-evidence-two-of-my-checks-were-covering-less-than-i-thought-j99</link>
      <guid>https://dev.to/isamu/green-is-not-evidence-two-of-my-checks-were-covering-less-than-i-thought-j99</guid>
      <description>&lt;p&gt;Someone left this on &lt;a href="https://dev.to/isamu/i-stopped-reviewing-my-own-code-heres-what-had-to-be-true-first-4nh0"&gt;a post of mine&lt;/a&gt; about how I stopped reviewing my own code:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Most teams test the output of a rule (e.g., does this code pass linting?) but rarely test the rule itself.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I'd been writing about pulling decisions into pure functions so they can be tested. They took it one level up: fine, but who tests the thing doing the checking?&lt;/p&gt;

&lt;p&gt;I had two answers to that lying around. I just hadn't connected them to each other.&lt;/p&gt;

&lt;h2&gt;
  
  
  The first one: a typecheck that checked half the repo
&lt;/h2&gt;

&lt;p&gt;We run &lt;code&gt;yarn typecheck&lt;/code&gt; in CI on every PR. It had been green for months.&lt;/p&gt;

&lt;p&gt;It had also never looked at our server code, or any of our tests.&lt;/p&gt;

&lt;p&gt;The root &lt;code&gt;tsconfig.json&lt;/code&gt; referenced two of five projects. &lt;code&gt;vue-tsc -b&lt;/code&gt; walks the references it's given, so everything else was simply out of scope. Not skipped with a warning — never enumerated in the first place.&lt;/p&gt;

&lt;p&gt;Here's the part I want to underline. I fixed the references — and the check stayed green, exactly as green as before. Of course it did; the code was fine. &lt;strong&gt;Green looks identical whether you're checking five projects or two&lt;/strong&gt;, which means it could not tell me whether my fix had worked. So I did this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;x&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;nope&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;One of those in each of the four areas I cared about, then run the check and confirm it fails.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Where I planted it&lt;/th&gt;
&lt;th&gt;Before&lt;/th&gt;
&lt;th&gt;After&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;server/config/workspace.ts&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;passed&lt;/td&gt;
&lt;td&gt;✗ caught&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;src/utils/focusTrap.ts&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;✗ caught&lt;/td&gt;
&lt;td&gt;✗ caught&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;test/common/readString.spec.ts&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;passed&lt;/td&gt;
&lt;td&gt;✗ caught&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;test/server/git/prs.spec.ts&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;passed&lt;/td&gt;
&lt;td&gt;✗ caught&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Three of four had been silently passing. The one that already worked is why nobody noticed — you get &lt;em&gt;some&lt;/em&gt; type errors, so the check is obviously running, so you stop wondering. (Fixed now: the root config references all five.)&lt;/p&gt;

&lt;p&gt;There's a footnote to this. Our contributing guide had a line saying "&lt;code&gt;yarn typecheck&lt;/code&gt; alone passes while CI fails — run all three commands." Somebody had hit this, worked around it in prose, and moved on. &lt;strong&gt;A rule in your docs that tells people to be careful is often a bug you can fix in config.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The second one: a rule set to &lt;code&gt;error&lt;/code&gt; that reports zero
&lt;/h2&gt;

&lt;p&gt;This one is better, because there is nothing to fix in the config. The config is correct.&lt;/p&gt;

&lt;p&gt;We ban type assertions. Not "discourage" — banned, at error severity:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;npx eslint &lt;span class="nt"&gt;--print-config&lt;/span&gt; src/components/SettingsField.vue &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="go"&gt;    | jq '.rules["@typescript-eslint/consistent-type-assertions"]'

[2, {"assertionStyle": "never"}]
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Severity 2. Error. For that exact file.&lt;/p&gt;

&lt;p&gt;That exact file, line 14:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight vue"&gt;&lt;code&gt;@input="$emit('update:modelValue', ($event.target as HTMLInputElement).value)"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;npx eslint src/components/SettingsField.vue
&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Nothing. &lt;code&gt;yarn lint&lt;/code&gt; reported &lt;strong&gt;0 errors&lt;/strong&gt; across the repo.&lt;/p&gt;

&lt;p&gt;Both facts are true and neither is a bug. &lt;code&gt;vue-eslint-parser&lt;/code&gt; exposes the &lt;code&gt;&amp;lt;template&amp;gt;&lt;/code&gt; as a separate AST, and typescript-eslint rules don't walk it. They see &lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt;. A cast in a template was never in scope for the rule — it just looks like it is, because &lt;code&gt;--print-config&lt;/code&gt; will happily tell you the rule is on.&lt;/p&gt;

&lt;p&gt;I measured the other frameworks, expecting this to be a template-language problem generally. It isn't:&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;script side&lt;/th&gt;
&lt;th&gt;template / JSX side&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;React, Solid, Preact (&lt;code&gt;.tsx&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Svelte&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Astro&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Vue&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;JSX is part of TypeScript's own grammar, so it lands in the same tree. The Svelte and Astro parsers expose their template expressions in a way these rules can visit. Vue keeps them separate. It's an implementation choice, not something inherent to having a template language — which is what I'd assumed before measuring.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;And it's fixable.&lt;/strong&gt; typescript-eslint can't reach that AST, but &lt;code&gt;eslint-plugin-vue&lt;/code&gt; can — &lt;code&gt;vue/no-restricted-syntax&lt;/code&gt; is the one rule that walks it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&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="s2"&gt;**/*.vue&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="nx"&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="s2"&gt;vue/no-restricted-syntax&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&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="na"&gt;selector&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;TSAsExpression&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Narrow in &amp;lt;script&amp;gt;, pass the result to the template.&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;selector&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;TSNonNullExpression&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Same.&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One subtlety worth copying: exclude &lt;code&gt;as const&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;selector&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;TSAsExpression:not([typeAnnotation.typeName.name="const"])&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;&lt;code&gt;consistent-type-assertions&lt;/code&gt; already exempts it, and the two halves of one SFC must not disagree about what's banned. It's also not what the ban is for — a const assertion narrows a literal the compiler can already see, rather than claiming a type it couldn't prove.&lt;/p&gt;

&lt;p&gt;We ran this on a sibling project first and it found &lt;strong&gt;16&lt;/strong&gt; casts that had been outside the gate the entire time the ban was at error. Six were already unnecessary: a &lt;code&gt;v-if&lt;/code&gt; above them had started narrowing the type at some point, and nobody re-checked, because nothing was looking.&lt;/p&gt;

&lt;p&gt;The rest moved into &lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt;, which is where a DOM type check belongs anyway:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight vue"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt; &lt;span class="err"&gt;@&lt;/span&gt;&lt;span class="na"&gt;input=&lt;/span&gt;&lt;span class="s"&gt;"onInput"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;onInput&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt; &lt;span class="k"&gt;instanceof&lt;/span&gt; &lt;span class="nx"&gt;HTMLInputElement&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;   &lt;span class="c1"&gt;// bail if it isn't&lt;/span&gt;
  &lt;span class="nf"&gt;emit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;update:modelValue&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&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;The cast wasn't protecting anything. It was hiding that the check had been written in the wrong place.&lt;/p&gt;

&lt;p&gt;Same file, same line, after the rule went in:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;npx eslint src/components/SettingsField.vue
&lt;span class="go"&gt;
&lt;/span&gt;&lt;span class="gp"&gt;  18:41  error  Do not use type assertions — narrow in &amp;lt;script&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;and pass
&lt;span class="go"&gt;                the result to the template   vue/no-restricted-syntax

✖ 1 problem (1 error, 0 warnings)
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is the whole point of this post, in one command. &lt;strong&gt;The config didn't get stricter — &lt;code&gt;consistent-type-assertions&lt;/code&gt; was already at error.&lt;/strong&gt; What changed is that something is now looking at the place where the violation lives.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this keeps happening: the config composes
&lt;/h2&gt;

&lt;p&gt;Neither failure was carelessness, and I don't think either was avoidable by being more careful. Lint and compiler config compose, and composition is where simple pieces stop behaving simply.&lt;/p&gt;

&lt;p&gt;Two examples I only found by measuring.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;strict&lt;/code&gt; doesn't include what you'd guess.&lt;/strong&gt; It's eight flags. These six are not among them:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight properties"&gt;&lt;code&gt;&lt;span class="err"&gt;noUncheckedIndexedAccess&lt;/span&gt;        &lt;span class="err"&gt;exactOptionalPropertyTypes&lt;/span&gt;
&lt;span class="err"&gt;noImplicitReturns&lt;/span&gt;               &lt;span class="err"&gt;noPropertyAccessFromIndexSignature&lt;/span&gt;
&lt;span class="err"&gt;noImplicitOverride&lt;/span&gt;              &lt;span class="err"&gt;noFallthroughCasesInSwitch&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;The rule that bans &lt;code&gt;as&lt;/code&gt; is not in &lt;code&gt;strict&lt;/code&gt;.&lt;/strong&gt; I loaded each typescript-eslint preset and read back what was enabled:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;rule&lt;/th&gt;
&lt;th&gt;&lt;code&gt;recommended&lt;/code&gt;&lt;/th&gt;
&lt;th&gt;&lt;code&gt;strict&lt;/code&gt;&lt;/th&gt;
&lt;th&gt;&lt;code&gt;stylistic&lt;/code&gt;&lt;/th&gt;
&lt;th&gt;&lt;code&gt;recommendedTypeChecked&lt;/code&gt;&lt;/th&gt;
&lt;th&gt;&lt;code&gt;strictTypeChecked&lt;/code&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;no-explicit-any&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;no-non-null-assertion&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;&lt;code&gt;consistent-type-assertions&lt;/code&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;—&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;✅&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;—&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;no-floating-promises&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;no-unsafe-assignment&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;It lives in &lt;code&gt;stylistic&lt;/code&gt; only. Not in &lt;code&gt;strict&lt;/code&gt;, not even in &lt;code&gt;strictTypeChecked&lt;/code&gt;. If you reached for &lt;code&gt;strict&lt;/code&gt; because casts worried you — which is exactly what I did — you got the opposite of what you wanted, and nothing told you.&lt;/p&gt;

&lt;p&gt;Then there's a second-order version, which is the one that actually unsettled me.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;sonarjs/different-types-comparison&lt;/code&gt; flagged nine comparisons as always-false. All nine were false positives, and they looked like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;command&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;argv&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;command&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* the rule says this can't happen */&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Without &lt;code&gt;noUncheckedIndexedAccess&lt;/code&gt;, TypeScript types &lt;code&gt;process.argv[2]&lt;/code&gt; as &lt;code&gt;string&lt;/code&gt;. The rule believed it and concluded the guard was dead. &lt;strong&gt;Following the advice would have deleted a real check.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Turning that flag on took it from nine to four — and the remaining four turned out to be genuinely redundant. The rule was never wrong. It was reasoning correctly from a type that was lying to it.&lt;/p&gt;

&lt;p&gt;So: &lt;strong&gt;when a rule produces false positives, suspect a missing setting upstream before you suspect the rule.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The method, such as it is
&lt;/h2&gt;

&lt;p&gt;Three commands. None of them take more than a few seconds.&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;# what is ACTUALLY enabled for this file, after all inheritance&lt;/span&gt;
npx eslint &lt;span class="nt"&gt;--print-config&lt;/span&gt; src/index.ts

&lt;span class="c"&gt;# effective tsconfig, after all extends&lt;/span&gt;
./node_modules/.bin/tsc &lt;span class="nt"&gt;-p&lt;/span&gt; tsconfig.app.json &lt;span class="nt"&gt;--showConfig&lt;/span&gt;

&lt;span class="c"&gt;# how many errors would this flag produce, before I commit to it&lt;/span&gt;
./node_modules/.bin/tsc &lt;span class="nt"&gt;-p&lt;/span&gt; tsconfig.app.json &lt;span class="nt"&gt;--noEmit&lt;/span&gt; &lt;span class="nt"&gt;--pretty&lt;/span&gt; &lt;span class="nb"&gt;false&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--noUncheckedIndexedAccess&lt;/span&gt; 2&amp;gt;&amp;amp;1 | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"error TS"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three notes, each from getting it wrong.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Use the project's own &lt;code&gt;tsc&lt;/code&gt;.&lt;/strong&gt; A global one installed by a package manager can be a completely different program wearing the same name, and it will tell you so in a way you won't expect.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pass &lt;code&gt;--pretty false&lt;/code&gt;.&lt;/strong&gt; With pretty output, &lt;code&gt;tsc&lt;/code&gt; writes colour escapes &lt;em&gt;between&lt;/em&gt; the two words you're grepping for:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;- \x1b[91merror\x1b[0m\x1b[90m TS2322: \x1b[0mType 'string' is not …
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The literal string &lt;code&gt;error TS&lt;/code&gt; isn't in there any more, so &lt;code&gt;grep -c "error TS"&lt;/code&gt; returns &lt;strong&gt;0&lt;/strong&gt; — which reads exactly like "no errors." Mine didn't drop colour when piped, either. (And &lt;code&gt;grep -c&lt;/code&gt; counts matching &lt;em&gt;lines&lt;/em&gt;, so treat it as a gauge, not a census.)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Point &lt;code&gt;-p&lt;/code&gt; at a real project, not a solution file.&lt;/strong&gt; This one is the same bug as the whole post, and I walked into it while writing this section. Our root &lt;code&gt;tsconfig.json&lt;/code&gt; is &lt;code&gt;files: []&lt;/code&gt; plus five &lt;code&gt;references&lt;/code&gt; — a solution file. &lt;code&gt;-p&lt;/code&gt; doesn't follow references, so it compiles nothing and exits clean. I planted one error in &lt;code&gt;server/&lt;/code&gt; and measured:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;command&lt;/th&gt;
&lt;th&gt;errors reported&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;tsc -p tsconfig.json --noEmit&lt;/code&gt; (the root)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;tsc -b tsconfig.json&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;72&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;tsc -p tsconfig.server.json --noEmit&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;1&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Zero, from a repo with a deliberate type error in it. If your root is a solution file, &lt;code&gt;-p&lt;/code&gt; on it is a green light that means nothing — use &lt;code&gt;-b&lt;/code&gt;, or name the leaf project.&lt;/p&gt;

&lt;p&gt;Then the part no command does for you:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Break it on purpose.&lt;/strong&gt; &lt;code&gt;--print-config&lt;/code&gt; tells you what the config says. It cannot tell you whether the rule can reach your code, and &lt;code&gt;-p&lt;/code&gt; can't tell you whether it compiled any. The only thing that answers either question is planting a violation and watching the check fail.&lt;/p&gt;

&lt;h2&gt;
  
  
  What turned up once the checks were actually running
&lt;/h2&gt;

&lt;p&gt;The point of all this is that the checks then find things. A sample of what came out:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A type claimed a field the API never sent.&lt;/strong&gt; &lt;code&gt;/api/session/:id&lt;/code&gt; doesn't return &lt;code&gt;id&lt;/code&gt;. Adding a runtime guard broke four tests, which is how we found out. The type had said &lt;code&gt;id&lt;/code&gt; was there since the beginning.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;fetchJson&amp;lt;T&amp;gt;&lt;/code&gt; returned whatever the caller named it.&lt;/strong&gt; No validation. If you wrote &lt;code&gt;fetchJson&amp;lt;Config&amp;gt;(...)&lt;/code&gt;, you got &lt;code&gt;Config&lt;/code&gt; — as a claim, not a fact.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Config was validated on save and not on load.&lt;/strong&gt; Broken entries came straight back in at startup.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;String(x ?? "")&lt;/code&gt;, seventeen times.&lt;/strong&gt; When &lt;code&gt;x&lt;/code&gt; is an object you get &lt;code&gt;"[object Object]"&lt;/code&gt;, no exception. That string was used as a lookup key and rendered as a session title. The real damage isn't the display — it's that &lt;em&gt;missing&lt;/em&gt; and &lt;em&gt;corrupted&lt;/em&gt; stop being distinguishable.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;An &lt;code&gt;await&lt;/code&gt; on a synchronous function.&lt;/strong&gt; It waited for nothing and told every future reader "this line is I/O."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A sort whose answer depended on who ran it.&lt;/strong&gt; A rule suggested &lt;code&gt;localeCompare&lt;/code&gt; for filenames — which are zero-padded dates and ISO timestamps. Locale order would have made the result machine-dependent. Following that advice would have introduced the bug.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A guard that could never fire.&lt;/strong&gt; &lt;code&gt;matchAll&lt;/code&gt; always populates &lt;code&gt;index&lt;/code&gt;, per spec.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And on the server side, where I cleared 145 of the 407 &lt;code&gt;no-unsafe-*&lt;/code&gt; findings, they came from just three entry points:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;entry point&lt;/th&gt;
&lt;th&gt;why it's &lt;code&gt;any&lt;/code&gt;
&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;await import(name)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;a dynamic import with a computed specifier returns &lt;code&gt;any&lt;/code&gt;, and everything reached through it stays outside the type checker&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;JSON.parse(...)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;returns &lt;code&gt;any&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;req.body&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Express types it &lt;code&gt;any&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;All three are "a value from outside." One validating function at each boundary cleared all 145. The client side had a wider spread — socket frames, &lt;code&gt;Response.json()&lt;/code&gt; — but the shape was the same every time: a boundary where an untyped value walked in and nothing stopped to look at it.&lt;/p&gt;

&lt;p&gt;If you only do one thing from this post, grep for those three.&lt;/p&gt;

&lt;p&gt;One trap in there: &lt;code&gt;typeof x === "function"&lt;/code&gt; doesn't narrow enough. You get &lt;code&gt;Function&lt;/code&gt;, and calling a &lt;code&gt;Function&lt;/code&gt; returns &lt;code&gt;any&lt;/code&gt;, so the result escapes again.&lt;/p&gt;

&lt;h2&gt;
  
  
  Not everything is worth turning on
&lt;/h2&gt;

&lt;p&gt;Two flags I measured and deliberately left off.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;noImplicitReturns&lt;/code&gt;&lt;/strong&gt; — 58 findings, and nearly all of them are this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;bad&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;   &lt;span class="c1"&gt;// no explicit return&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's correct Express. Nobody reads a handler's return value. Satisfying the flag means adding 58 meaningless &lt;code&gt;return&lt;/code&gt;s and closing zero holes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;noPropertyAccessFromIndexSignature&lt;/code&gt;&lt;/strong&gt; — 1,785 findings, all of them &lt;code&gt;obj.key&lt;/code&gt; → &lt;code&gt;obj["key"]&lt;/code&gt;. Access safety is unchanged.&lt;/p&gt;

&lt;p&gt;Compare with &lt;code&gt;noUncheckedIndexedAccess&lt;/code&gt;: 118 findings in shipped code, every one of them "write down what the code already assumed." All 118 were worth it.&lt;/p&gt;

&lt;p&gt;The count isn't the signal. &lt;strong&gt;The question is whether the work closes a hole&lt;/strong&gt;, and 118 real fixes beat 1,785 renames.&lt;/p&gt;

&lt;p&gt;(Tests got their own answer: &lt;code&gt;noUncheckedIndexedAccess&lt;/code&gt; is off there. A test indexing its own fixture with &lt;code&gt;rows[0]&lt;/code&gt; doesn't need &lt;code&gt;T | undefined&lt;/code&gt; — the test is the thing guaranteeing that value. That's 233 findings that were pure noise. &lt;code&gt;exactOptionalPropertyTypes&lt;/code&gt; stays on in tests, because that one is about meaning, not fixture ergonomics.)&lt;/p&gt;

&lt;h2&gt;
  
  
  The thing I'd tell myself six months ago
&lt;/h2&gt;

&lt;p&gt;Both failures had the same shape, and so did the sibling project's — where &lt;code&gt;yarn lint&lt;/code&gt; turned out never to reach &lt;code&gt;scripts/&lt;/code&gt;, &lt;code&gt;batch/&lt;/code&gt; or &lt;code&gt;config/&lt;/code&gt;. That's 21 files, 92 errors, nine of them the very casts we'd banned. &lt;strong&gt;The code that decides whether a PR can merge had no gate on it.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Gates don't fail loudly. A gate that isn't running produces the same output as a gate that's running and finding nothing. There is no error message for "this never executed," because from the inside those two states look identical.&lt;/p&gt;

&lt;p&gt;So the commenter had it right, and their framing is better than what I'd written. Testing that your code passes the check is not the same as testing the check.&lt;/p&gt;

&lt;p&gt;You can't fix that by reading configs more carefully. You fix it by making the check fail on purpose, once, and watching.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Anything you've found this way, I'd like to hear it — particularly if your green was hiding something dumber than mine.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>typescript</category>
      <category>eslint</category>
      <category>testing</category>
      <category>devops</category>
    </item>
    <item>
      <title>Two things broke when I shipped my Claude Code plugin — and one of my fixes was cargo cult</title>
      <dc:creator>Isamu Arimoto</dc:creator>
      <pubDate>Mon, 03 Aug 2026 06:26:37 +0000</pubDate>
      <link>https://dev.to/isamu/two-things-broke-when-i-shipped-my-claude-code-plugin-and-one-of-my-fixes-was-cargo-cult-78p</link>
      <guid>https://dev.to/isamu/two-things-broke-when-i-shipped-my-claude-code-plugin-and-one-of-my-fixes-was-cargo-cult-78p</guid>
      <description>&lt;p&gt;My skills worked. I'd been using them locally for weeks. Then I packaged them as a plugin, installed it the way a stranger would, and hit things that don't exist locally — and that don't produce an error message pointing at the cause.&lt;/p&gt;

&lt;p&gt;Two of them were real. The third was a rule I'd been repeating for months, which turned out to be about something else entirely. Writing this post is what made me check it.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Your skill's name changes, and skill-to-skill calls break silently
&lt;/h2&gt;

&lt;p&gt;A skill in &lt;code&gt;~/.claude/skills/story/SKILL.md&lt;/code&gt; is invoked as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/story
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The same file, shipped inside a plugin named &lt;code&gt;mulmocast&lt;/code&gt;, is invoked as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/mulmocast:story
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;The plugin name becomes a prefix.&lt;/strong&gt; Which is fine, until you remember that skills can call other skills.&lt;/p&gt;

&lt;p&gt;My &lt;code&gt;mulmocast&lt;/code&gt; skill is a router: it reads what you asked for and hands off to &lt;code&gt;story&lt;/code&gt;, &lt;code&gt;narrate&lt;/code&gt;, &lt;code&gt;illustrate&lt;/code&gt; and so on. Written the obvious way, that hand-off says &lt;code&gt;story&lt;/code&gt;. So:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;locally&lt;/strong&gt; — &lt;code&gt;mulmocast&lt;/code&gt; → &lt;code&gt;story&lt;/code&gt; → works&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;installed from a marketplace&lt;/strong&gt; — &lt;code&gt;mulmocast&lt;/code&gt; → &lt;code&gt;story&lt;/code&gt; → &lt;strong&gt;no such skill&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I never saw an error that named the cause. The hand-off simply didn't happen, and the skill carried on as though that step had been optional.&lt;/p&gt;

&lt;p&gt;The fix is to write both names, qualified one first:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="p"&gt;1.&lt;/span&gt; Try &lt;span class="sb"&gt;`mulmocast:story`&lt;/span&gt; → if not found, try &lt;span class="sb"&gt;`story`&lt;/span&gt;
&lt;span class="p"&gt;2.&lt;/span&gt; Try &lt;span class="sb"&gt;`mulmocast:narrate`&lt;/span&gt; → if not found, try &lt;span class="sb"&gt;`narrate`&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One note on notation: a user types &lt;code&gt;/story&lt;/code&gt; to invoke a skill. Inside a &lt;code&gt;SKILL.md&lt;/code&gt; you are naming a skill for the model to dispatch to, not typing a slash command — so write it without the slash, as above.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Your CLI's name changes too
&lt;/h2&gt;

&lt;p&gt;Same shape of problem, different layer.&lt;/p&gt;

&lt;p&gt;My skills drive a CLI that ships as its own npm package. The plugin repo carries the skills, references and example scripts — not the CLI. While developing the CLI, I ran it from inside &lt;em&gt;its&lt;/em&gt; repo:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;yarn run cli images script.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That line is fine in a terminal I own and wrong in a &lt;code&gt;SKILL.md&lt;/code&gt;, because the person reading it installed a plugin. They don't have that checkout, so &lt;code&gt;yarn run cli&lt;/code&gt; resolves to nothing.&lt;/p&gt;

&lt;p&gt;The obvious fix is to write the published &lt;code&gt;bin&lt;/code&gt; name instead:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"mulmocast"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"bin"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"mulmo"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"lib/cli/bin.js"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"mulmocast"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"lib/cli/bin.js"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That works — if they installed the package globally. Which they may not have, and which you have no way to check from inside a skill.&lt;/p&gt;

&lt;p&gt;So what my skills actually say is this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx mulmocast@latest movie script.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;npx&lt;/code&gt; resolves the package and downloads a temporary copy when what's installed doesn't satisfy the spec. &lt;strong&gt;No install step to document, and no version to keep in sync with your prose.&lt;/strong&gt; If you keep a &lt;code&gt;yarn run cli&lt;/code&gt; line at all, mark it as the one for people working in the repo — it is not a fallback for your users, it's a note to yourself.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Both of these have the same root cause:&lt;/strong&gt; you are writing instructions from inside an environment your reader is not in. Every name you put in a &lt;code&gt;SKILL.md&lt;/code&gt; — a skill, a command, a path — gets resolved somewhere else.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. The one I was wrong about
&lt;/h2&gt;

&lt;p&gt;Here is advice I had been giving, including in writing:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Never give your marketplace and your plugin the same &lt;code&gt;name&lt;/code&gt;. If you do, installation fails on Linux with an &lt;code&gt;EXDEV&lt;/code&gt; error.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I believed it. I set my repo up that way. I couldn't tell you where I first picked it up.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;I don't use Linux.&lt;/strong&gt; I never hit this, never reproduced it, never checked it. Writing this post is what finally sent me looking for the source — &lt;a href="https://github.com/anthropics/claude-code/issues/14799" rel="noopener noreferrer"&gt;anthropics/claude-code#14799&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;It has nothing to do with names.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;Error: Failed to install: EXDEV: cross-device link not permitted,
&lt;/span&gt;&lt;span class="gp"&gt;rename '/home/user/.claude/plugins/cache/…' -&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'/tmp/claude-plugin-temp-…'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On most current Linux distributions &lt;code&gt;/tmp&lt;/code&gt; is &lt;strong&gt;tmpfs&lt;/strong&gt;, while &lt;code&gt;~/.claude&lt;/code&gt; sits on your real disk. Two filesystems. &lt;code&gt;fs.rename()&lt;/code&gt; can't move a file across that boundary, and the installer was renaming between them.&lt;/p&gt;

&lt;p&gt;So it fired for &lt;strong&gt;any&lt;/strong&gt; plugin, whatever anything was called. The workaround was to put the temp directory on the same filesystem:&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;export &lt;/span&gt;&lt;span class="nv"&gt;TMPDIR&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$HOME&lt;/span&gt;&lt;span class="s2"&gt;/.claude/tmp"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And it's &lt;strong&gt;fixed&lt;/strong&gt; — the issue closed in February. On a current Claude Code there's nothing here for you to do.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why a wrong rule survived that long
&lt;/h3&gt;

&lt;p&gt;This is the part I want to keep.&lt;/p&gt;

&lt;p&gt;The rule was harmless. Giving the marketplace and the plugin different names costs nothing, breaks nothing, and looks tidy. So I followed it, everything worked, and &lt;strong&gt;nothing ever contradicted me.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A rule that's expensive gets challenged — sooner or later someone asks whether it's worth it. A rule that's free just accumulates. I'd been passing on a diagnosis I had never once tested, and the only reason I found out is that I sat down to write it as fact.&lt;/p&gt;

&lt;p&gt;For the record, here are the names in a working setup — and note that two of them are &lt;em&gt;supposed&lt;/em&gt; to match:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json-doc"&gt;&lt;code&gt;&lt;span class="c1"&gt;// .claude-plugin/marketplace.json&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"mulmocast-plugins"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;          &lt;/span&gt;&lt;span class="c1"&gt;// the marketplace&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"plugins"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"mulmocast"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;...&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="c1"&gt;// ← must MATCH plugin.json&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="c1"&gt;// .claude-plugin/plugin.json&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"mulmocast"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  While we're here: there are three names, not one
&lt;/h2&gt;

&lt;p&gt;The reason the above is confusing at all is that a plugin involves &lt;strong&gt;three separate names&lt;/strong&gt;, and the docs use them in different places:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;receptron/mulmocast-claude-plugin        ← GitHub repo
        ↓
marketplace.json  "name": "mulmocast-plugins"     ← marketplace name
marketplace.json  plugins[0].name: "mulmocast"    ← plugin name
        ↓
install:  mulmocast@mulmocast-plugins
          ^^^^^^^^^ ^^^^^^^^^^^^^^^^
          plugin    marketplace
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Name&lt;/th&gt;
&lt;th&gt;Defined in&lt;/th&gt;
&lt;th&gt;Used by&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;GitHub repo&lt;/td&gt;
&lt;td&gt;GitHub&lt;/td&gt;
&lt;td&gt;&lt;code&gt;marketplace add&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Marketplace name&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;marketplace.json&lt;/code&gt; → &lt;code&gt;name&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;after the &lt;code&gt;@&lt;/code&gt; in &lt;code&gt;install&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Plugin name&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;marketplace.json&lt;/code&gt; → &lt;code&gt;plugins[].name&lt;/code&gt;, and the plugin's own &lt;code&gt;plugin.json&lt;/code&gt; → &lt;code&gt;name&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;before the &lt;code&gt;@&lt;/code&gt; in &lt;code&gt;install&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The repo name is used &lt;strong&gt;once&lt;/strong&gt; in this sequence, when registering the marketplace. After that you work from the names inside the JSON:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;claude plugin marketplace add receptron/mulmocast-claude-plugin   &lt;span class="c"&gt;# repo name&lt;/span&gt;
claude plugin &lt;span class="nb"&gt;install &lt;/span&gt;mulmocast@mulmocast-plugins                 &lt;span class="c"&gt;# plugin@marketplace&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Minimum layout. These can be two repos or one — mine is one:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;my-plugin-repo/
  .claude-plugin/
    marketplace.json     # the catalogue
    plugin.json          # this plugin's metadata
  skills/
    my-skill/
      SKILL.md           # the actual skill
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Which distribution method to pick
&lt;/h2&gt;

&lt;p&gt;There are three, and they're not competing for the same job.&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;Reach&lt;/th&gt;
&lt;th&gt;Ships MCP servers&lt;/th&gt;
&lt;th&gt;Ships hooks&lt;/th&gt;
&lt;th&gt;Install&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Your own marketplace&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Claude Code&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/plugin install name@market&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Official store&lt;/strong&gt; (&lt;a href="https://github.com/anthropics/claude-plugins-official" rel="noopener noreferrer"&gt;claude-plugins-official&lt;/a&gt;)&lt;/td&gt;
&lt;td&gt;Claude Code, no setup step&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/plugin install name@claude-plugins-official&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;&lt;a href="https://github.com/vercel-labs/skills" rel="noopener noreferrer"&gt;Skills CLI&lt;/a&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Cursor, Codex, OpenCode and dozens more&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;&lt;code&gt;npx skills add owner/repo&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Roughly:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Claude Code only, and you use MCP servers or hooks&lt;/strong&gt; → plugin. Start with your own marketplace, apply to the official store once it's proven.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You want it to work in Cursor, Codex and the rest too&lt;/strong&gt; → Skills CLI. It symlinks &lt;code&gt;SKILL.md&lt;/code&gt; into each agent's directory, so one repo covers all of them. You give up MCP and hooks.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The columns say &lt;em&gt;ships&lt;/em&gt; deliberately. Skills CLI distributes &lt;code&gt;SKILL.md&lt;/code&gt; files; it does not install plugin manifests, so MCP servers and plugin hooks don't come with them. The target agent may well support hooks on its own.&lt;/p&gt;

&lt;p&gt;And the thing I'd actually weigh about the official store isn't discovery — it's that users skip &lt;code&gt;marketplace add&lt;/code&gt; entirely. One less step where someone gives up.&lt;/p&gt;

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

&lt;p&gt;The first two live in the same place: &lt;strong&gt;I had only ever run this where the names happen to be right.&lt;/strong&gt; Locally the skill is &lt;code&gt;story&lt;/code&gt; and the CLI is &lt;code&gt;yarn run cli&lt;/code&gt;. Published, it's &lt;code&gt;mulmocast:story&lt;/code&gt; and &lt;code&gt;mulmocast&lt;/code&gt;. Both failures sit in that gap.&lt;/p&gt;

&lt;p&gt;The third is the same gap from the other side. I'm on macOS, so a Linux-only failure was never going to reach me — and because my workaround for it was free, no evidence was ever going to reach me either. I'd built a belief that couldn't be falsified from where I was standing.&lt;/p&gt;

&lt;p&gt;So the useful habit isn't "test the plugin" — it's &lt;strong&gt;install it the way a stranger would, on a machine that isn't yours&lt;/strong&gt;, before you tell anyone it exists.&lt;/p&gt;

&lt;p&gt;And if you're carrying a rule you've never seen fail: that isn't evidence it works. It might just be cheap.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Docs: &lt;a href="https://code.claude.com/docs/en/plugins" rel="noopener noreferrer"&gt;Claude Code plugins&lt;/a&gt; · &lt;a href="https://code.claude.com/docs/en/plugin-marketplaces" rel="noopener noreferrer"&gt;Marketplaces&lt;/a&gt; · &lt;a href="https://agentskills.io/specification" rel="noopener noreferrer"&gt;Agent Skills spec&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>claudecode</category>
      <category>ai</category>
      <category>opensource</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>I stopped reviewing my own code. Here's what had to be true first.</title>
      <dc:creator>Isamu Arimoto</dc:creator>
      <pubDate>Sat, 01 Aug 2026 06:38:10 +0000</pubDate>
      <link>https://dev.to/isamu/i-stopped-reviewing-my-own-code-heres-what-had-to-be-true-first-4nh0</link>
      <guid>https://dev.to/isamu/i-stopped-reviewing-my-own-code-heres-what-had-to-be-true-first-4nh0</guid>
      <description>&lt;p&gt;Most days now, I merge pull requests without reading the diff.&lt;/p&gt;

&lt;p&gt;That sentence used to describe someone I would not have hired. So let me be precise about what changed, because it isn't confidence and it isn't recklessness. It's that &lt;strong&gt;I moved the things review was catching to somewhere that catches them earlier.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Here's the honest version of how that happened.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem was arithmetic, not philosophy
&lt;/h2&gt;

&lt;p&gt;I run several coding agents in parallel. That produces more diff per day than I can read. Not "more than I feel like reading" — genuinely more than fits in a working day.&lt;/p&gt;

&lt;p&gt;When that happens you have exactly two options:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Generate less, so it fits what you can read.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Make it safe to not read.&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I picked the second one. Not because I'm brave, but because option 1 means throwing away the reason I set this up.&lt;/p&gt;

&lt;p&gt;The uncomfortable part: option 2 is not a mindset. It's a list of specific things that have to be true. Here's mine.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. The rules live in a file, not in review comments
&lt;/h2&gt;

&lt;p&gt;Every code review I've ever done, the majority of my comments were mechanical. &lt;em&gt;This function is too long. This nesting is too deep. Why is this &lt;code&gt;any&lt;/code&gt;?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Machines can say all of that. So I made them say it, as &lt;strong&gt;errors&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;max-lines-per-function&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&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="na"&gt;max&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;skipBlankLines&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="nx"&gt;complexity&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="s2"&gt;error&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;max-depth&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;error&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;max-nested-callbacks&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;error&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Plus &lt;code&gt;eslint-plugin-sonarjs&lt;/code&gt; with cognitive-complexity as an error, and &lt;code&gt;@typescript-eslint&lt;/code&gt;'s strict preset — &lt;code&gt;any&lt;/code&gt; banned, non-null assertions banned.&lt;/p&gt;

&lt;p&gt;Nothing here is novel. What's different is the next part.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. The rules are stricter than a human team would tolerate
&lt;/h2&gt;

&lt;p&gt;This is the part I find genuinely interesting.&lt;/p&gt;

&lt;p&gt;If you put those thresholds on a human team, you get a PR relaxing them within a week. Not because engineers are lazy — because "this function is 63 lines and splitting it makes it worse" is &lt;em&gt;sometimes true&lt;/em&gt;, and arguing about it every time is exhausting.&lt;/p&gt;

&lt;p&gt;Lint strictness has always been a trade-off between &lt;strong&gt;machine correctness&lt;/strong&gt; and &lt;strong&gt;human patience&lt;/strong&gt;. And loosening the rules was never really a technical decision. It was a social one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;An agent doesn't get annoyed.&lt;/strong&gt; It reads the rule, splits the function, moves on. It has no opinion about being told to do it again tomorrow.&lt;/p&gt;

&lt;p&gt;So the social cost went to zero, and once that happens the trade-off only tips one way. I turned everything up until it hurt, and nobody complained, because nobody was there to complain.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Exceptions live in the config with a reason, never inline
&lt;/h2&gt;

&lt;p&gt;The moment rules get strict, real exceptions appear. If you allow &lt;code&gt;// eslint-disable-next-line&lt;/code&gt;, the rules are dead within a month — that comment is invisible in review and permanent in practice.&lt;/p&gt;

&lt;p&gt;So exceptions go in the config file, &lt;strong&gt;one line per reason&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&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="s2"&gt;src/components/Sidebar.vue&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;       &lt;span class="c1"&gt;// @keyframes — the "thinking" spinner ring&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;src/components/GuiPanel.vue&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;      &lt;span class="c1"&gt;// `.frame + .frame` sibling-combinator spacing&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;src/components/FilesOverlay.vue&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;// :deep into CodeMirror's injected root&lt;/span&gt;
&lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="nx"&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="s2"&gt;vue/no-restricted-block&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="s2"&gt;off&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;Every entry says &lt;em&gt;why&lt;/em&gt;. And the comment above the block says: delete the entry when the reason goes away.&lt;/p&gt;

&lt;p&gt;The difference is visibility. An inline disable is invisible. A growing allowlist in a config file is a &lt;strong&gt;thing you can look at and be embarrassed by&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. The rules themselves are pure functions, and they're tested
&lt;/h2&gt;

&lt;p&gt;This is the part most setups skip.&lt;/p&gt;

&lt;p&gt;If a rule decides something — which file extension goes where, how a path is normalised, what counts as a valid session id — that decision is &lt;em&gt;code&lt;/em&gt;, and code that is only exercised through a UI is code nobody tests.&lt;/p&gt;

&lt;p&gt;So decisions get pulled out into pure functions in a shared module, and those functions get tested against the awkward cases: empty, null, boundary, wrong-cased, wrong-platform. That project currently has a few thousand test cases, and the vast majority are testing small pure functions rather than flows.&lt;/p&gt;

&lt;p&gt;The point is not the count. The point is: &lt;strong&gt;when the rule is a pure function, the rule can be tested. When it's embedded in a component, it can only be reviewed.&lt;/strong&gt; And I stopped reviewing.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. CI runs on the OS your users have, not the one you have
&lt;/h2&gt;

&lt;p&gt;Our whole team is on macOS. Our users are not.&lt;/p&gt;

&lt;p&gt;So CI runs Linux and macOS on every PR, and Windows on a nightly schedule (it's slow, and daily is enough to catch drift).&lt;/p&gt;

&lt;p&gt;This one paid for itself immediately, and not in the way I expected. &lt;strong&gt;It's the only environment where I can reproduce a Windows bug report at all.&lt;/strong&gt; Before, a Windows issue meant asking the reporter to test my guesses. Now I push a branch.&lt;/p&gt;

&lt;p&gt;We also write Windows-specific test cases deliberately — path separators, &lt;code&gt;realpathSync&lt;/code&gt; behaviour, &lt;code&gt;fs.watch&lt;/code&gt; differences — so that fixing one doesn't quietly break the others.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Something else reads the code — and it isn't the thing that wrote it
&lt;/h2&gt;

&lt;p&gt;Claude Code writes. Then &lt;strong&gt;Codex reviews&lt;/strong&gt;, and CodeRabbit reviews, and the loop runs until they stop objecting.&lt;/p&gt;

&lt;p&gt;The mechanism that matters here isn't "AI review is good." It's that &lt;strong&gt;the writer and the reader are different models.&lt;/strong&gt; A model reviewing its own output shares its own blind spots. Two different ones don't, mostly.&lt;/p&gt;

&lt;p&gt;This is the closest thing to a replacement for what I stopped doing. It isn't as good as a careful human reviewer. It is &lt;em&gt;much&lt;/em&gt; better than a tired human reviewer at 11pm on the fortieth PR of the day, which was the realistic alternative.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I still look at
&lt;/h2&gt;

&lt;p&gt;I want to be honest about the boundary, because "I don't review anything" would be a lie:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;UI changes.&lt;/strong&gt; Nothing in the list above can tell me a layout is ugly or a flow is confusing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Anything I couldn't verify by running it.&lt;/strong&gt; If the change is about behaviour under conditions CI doesn't reproduce, I go look.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Anything touching auth, permissions, or data loss.&lt;/strong&gt; The blast radius is wrong for automation.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Everything else, I let through on green.&lt;/p&gt;

&lt;h2&gt;
  
  
  The honest cost
&lt;/h2&gt;

&lt;p&gt;Three things I'd want to know if I were reading this skeptically:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It front-loads a lot of work.&lt;/strong&gt; None of the six items above is free. If you set up two of them and stop, you have strictness without a safety net, which is worse than neither.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It only works if the stack is uniform.&lt;/strong&gt; The reason my per-repo config files are nearly empty is that every project uses the same language, the same test runner, the same CI shape. If your repos disagree with each other, you'll be writing the same rules over and over. Fix that first; it's cheaper.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Speed is not correctness.&lt;/strong&gt; Things do get shipped fast and fixed fast. What this setup buys is not "no bugs" — it's that &lt;strong&gt;the bugs that survive are the ones review wouldn't have caught either.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The thing I actually took away
&lt;/h2&gt;

&lt;p&gt;I set all this up to save time, and it did. But that isn't the interesting part.&lt;/p&gt;

&lt;p&gt;The interesting part is that lint strictness, test coverage, CI breadth — the whole category of "engineering discipline we know we should do but don't" — was &lt;strong&gt;never really blocked on knowing better.&lt;/strong&gt; It was blocked on how much friction a human team will absorb before it starts negotiating.&lt;/p&gt;

&lt;p&gt;That constraint just got removed. Not gradually. It's gone.&lt;/p&gt;

&lt;p&gt;I don't think most of us have updated for that yet.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Everything above runs in the open: &lt;a href="https://github.com/isamu/claude" rel="noopener noreferrer"&gt;my global config&lt;/a&gt;&lt;br&gt;
  and the &lt;a href="https://github.com/receptron/mulmoterminal" rel="noopener noreferrer"&gt;project it runs on&lt;/a&gt;, both MIT.&lt;br&gt;
  Copy whatever's useful.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>programming</category>
      <category>productivity</category>
      <category>claudecode</category>
    </item>
    <item>
      <title>AI Changed the Bottleneck of Software Development</title>
      <dc:creator>Isamu Arimoto</dc:creator>
      <pubDate>Thu, 23 Jul 2026 08:27:52 +0000</pubDate>
      <link>https://dev.to/isamu/ai-changed-the-bottleneck-of-software-development-58og</link>
      <guid>https://dev.to/isamu/ai-changed-the-bottleneck-of-software-development-58og</guid>
      <description>&lt;p&gt;For decades, software engineering was constrained by one thing: writing code.&lt;/p&gt;

&lt;p&gt;Today, that bottleneck is disappearing.&lt;/p&gt;

&lt;p&gt;With tools like Claude Code, Codex, and other coding agents, generating thousands of lines of code is almost free. The new constraint is no longer how fast we can write code—it's how fast we can keep a codebase healthy.&lt;/p&gt;

&lt;p&gt;That is a fundamentally different problem.&lt;/p&gt;

&lt;p&gt;AI Doesn't Create Bad Code. It Creates Code Too Easily.&lt;/p&gt;

&lt;p&gt;Modern coding agents are remarkably good at producing working code.&lt;/p&gt;

&lt;p&gt;But they also tend to:&lt;/p&gt;

&lt;p&gt;create another helper function instead of reusing an existing one&lt;br&gt;
leave dead utilities after refactoring&lt;br&gt;
grow functions to hundreds of lines&lt;br&gt;
fall back to any when type inference becomes difficult&lt;br&gt;
introduce subtle architectural violations that are hard to notice in code review&lt;/p&gt;

&lt;p&gt;None of these are catastrophic individually.&lt;/p&gt;

&lt;p&gt;Together, they slowly increase technical debt.&lt;/p&gt;

&lt;p&gt;The challenge isn't intelligence—it's scale.&lt;/p&gt;

&lt;p&gt;When an AI can generate code 10× faster than a human, it can also generate technical debt 10× faster.&lt;/p&gt;

&lt;p&gt;Human Code Review Doesn't Scale Either&lt;/p&gt;

&lt;p&gt;Even experienced reviewers can't remember a 300,000-line codebase.&lt;/p&gt;

&lt;p&gt;Suppose a pull request adds another truncate() function.&lt;/p&gt;

&lt;p&gt;Is there already one somewhere else?&lt;/p&gt;

&lt;p&gt;Maybe.&lt;/p&gt;

&lt;p&gt;Maybe there are six.&lt;/p&gt;

&lt;p&gt;No reviewer can reliably answer that from memory.&lt;/p&gt;

&lt;p&gt;The same applies to unused exports, circular dependencies, or architectural boundaries.&lt;/p&gt;

&lt;p&gt;These aren't problems humans should spend their time searching for.&lt;/p&gt;

&lt;p&gt;They're mechanical problems.&lt;/p&gt;

&lt;p&gt;Let Machines Enforce Code Hygiene&lt;/p&gt;

&lt;p&gt;The obvious solution is to automate the boring parts.&lt;/p&gt;

&lt;p&gt;Our development workflow combines several layers of static analysis:&lt;/p&gt;

&lt;p&gt;ESLint for complexity and coding standards&lt;br&gt;
SonarJS for maintainability and type safety&lt;br&gt;
jscpd for duplicated code&lt;br&gt;
Knip for dead code detection&lt;/p&gt;

&lt;p&gt;Each tool catches a different class of problems.&lt;/p&gt;

&lt;p&gt;No single tool is sufficient.&lt;/p&gt;

&lt;p&gt;Together, they continuously protect the codebase while developers—and AI agents—focus on solving actual problems.&lt;/p&gt;

&lt;p&gt;DRY Matters Even More in the AI Era&lt;/p&gt;

&lt;p&gt;Many people think DRY ("Don't Repeat Yourself") is mainly about maintainability.&lt;/p&gt;

&lt;p&gt;Today, it has another benefit.&lt;/p&gt;

&lt;p&gt;AI agents consume context.&lt;/p&gt;

&lt;p&gt;Six duplicated implementations don't just increase maintenance cost—they also increase token consumption.&lt;/p&gt;

&lt;p&gt;Large functions require more context.&lt;/p&gt;

&lt;p&gt;Duplicate utilities require more searching.&lt;/p&gt;

&lt;p&gt;Dead code increases noise.&lt;/p&gt;

&lt;p&gt;A cleaner codebase is also a cheaper codebase for AI.&lt;/p&gt;

&lt;p&gt;Good architecture now directly improves AI efficiency.&lt;/p&gt;

&lt;p&gt;CI Should Prevent Regression, Not Punish History&lt;/p&gt;

&lt;p&gt;One lesson we've learned is that not every static analysis tool should block a pull request.&lt;/p&gt;

&lt;p&gt;Some checks can accurately identify new problems.&lt;/p&gt;

&lt;p&gt;Those belong in CI.&lt;/p&gt;

&lt;p&gt;Others can only report the current state of the repository.&lt;/p&gt;

&lt;p&gt;Those should provide visibility rather than block development.&lt;/p&gt;

&lt;p&gt;The goal isn't to eliminate all technical debt overnight.&lt;/p&gt;

&lt;p&gt;The goal is simple:&lt;/p&gt;

&lt;p&gt;Never create new debt faster than you remove the old one.&lt;/p&gt;

&lt;p&gt;The New Engineering Skill&lt;/p&gt;

&lt;p&gt;As AI continues to improve, writing code becomes less valuable as a competitive advantage.&lt;/p&gt;

&lt;p&gt;The differentiator shifts toward something else:&lt;/p&gt;

&lt;p&gt;Designing systems that remain understandable, modular, and maintainable—even when most of the code is written by machines.&lt;/p&gt;

&lt;p&gt;Software engineering is becoming less about typing code and more about managing the quality of an ever-growing codebase.&lt;/p&gt;

&lt;p&gt;The bottleneck has moved.&lt;/p&gt;

&lt;p&gt;The teams that recognize this early will build software faster—not because their AI writes more code, but because their systems stay clean enough for AI to keep writing it.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>programming</category>
      <category>softwaredevelopment</category>
      <category>softwareengineering</category>
    </item>
  </channel>
</rss>
