<?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: Ian</title>
    <description>The latest articles on DEV Community by Ian (@ianwieds).</description>
    <link>https://dev.to/ianwieds</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%2F384278%2F61b90b66-d678-4b60-b1a2-a008abb006d0.jpg</url>
      <title>DEV Community: Ian</title>
      <link>https://dev.to/ianwieds</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/ianwieds"/>
    <language>en</language>
    <item>
      <title>Taste can't be linted. The tells can.</title>
      <dc:creator>Ian</dc:creator>
      <pubDate>Fri, 30 Oct 2026 18:00:00 +0000</pubDate>
      <link>https://dev.to/ianwieds/taste-cant-be-linted-the-tells-can-1ah4</link>
      <guid>https://dev.to/ianwieds/taste-cant-be-linted-the-tells-can-1ah4</guid>
      <description>&lt;p&gt;My product's homepage has six top-level headings. Every one has a small pill or a line of capitals above it, and every one ends on a word set in italic serif.&lt;/p&gt;

&lt;p&gt;I didn't notice until I pointed a design lint's rules at the page. Two of the pills say phrases that appear word for word in the lint's source code.&lt;/p&gt;

&lt;p&gt;The lint ships inside &lt;a href="https://github.com/Nutlope/hallmark" rel="noopener noreferrer"&gt;Hallmark&lt;/a&gt;, and it changed how I think about the "AI slop" complaint. You can't write a test for taste. You can write one for the tells.&lt;/p&gt;

&lt;h2&gt;
  
  
  A design skill with a script behind it
&lt;/h2&gt;

&lt;p&gt;Hallmark is an MIT-licensed skill for Claude Code, Codex, Cursor and OpenCode, made by Hassan El Mghari (Nutlope) at Together AI. Version 2 (2.0.0 in its package.json) was merged on October 22. It was #1 on GitHub trending on Tuesday with 2,830 stars that day, and the GitHub API showed 39,048 stars when I checked that evening.&lt;/p&gt;

&lt;p&gt;Most design skills are a long prompt. This one is a prompt plus machinery. &lt;code&gt;SKILL.md&lt;/code&gt; opens with a 16-rule Floor no build may break, then a flow: read the project, ask once for real material, deal three themes, build and review the hero alone, then sweep the page. There are 50 themes (I counted 50 files and 50 preview images). The rules live in &lt;code&gt;references/gates.md&lt;/code&gt;, and the checker is &lt;code&gt;scripts/sloplint.mjs&lt;/code&gt;: 4,422 lines, zero dependencies, Node 18 and up.&lt;/p&gt;

&lt;p&gt;Install is one line:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx skills add nutlope/hallmark &lt;span class="nt"&gt;-g&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The tells, written down as rules
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;gates.md&lt;/code&gt; is the file worth reading. It lists 90 numbered gates, each with a tier and a class. Floor gates never ship. Reflex gates are a model's defaults, and a build may waive one in a CSS comment with a real reason. Class M means the static script can settle it from CSS and markup; R needs a real browser; J is judgment at review time. I counted 45 M gates and 9 judged only.&lt;/p&gt;

&lt;p&gt;The named tells read like things most of us have shipped: the eyebrow (a short line announcing the heading under it), the tracked-caps label, one coloured word in a headline, middle dots between meta items, a form that says "Thanks" while sending nothing. Four are "house rules" (the eyebrow, tracked caps, an 80-words-per-screen budget, template punctuation), which sit in the Floor with no waiver.&lt;/p&gt;

&lt;p&gt;What sold me is how literal the script gets. This is how it decides a gradient is purple-to-blue:&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;purple&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;stops&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;some&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;h&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;hueIn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;h&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;265&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;330&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;blue&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;stops&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;some&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;h&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;hueIn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;h&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;264&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="c1"&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;purple&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;blue&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;purple-to-blue&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;Each stop is converted to OKLCH first, and only counts with chroma above 0.04, so a grey wash passes. &lt;code&gt;transition: all&lt;/code&gt; is one regex. A tracked-caps label is uppercase, small caps or letter-spacing past 0.03em on anything named like a label (&lt;code&gt;badge&lt;/code&gt;, &lt;code&gt;meta&lt;/code&gt;, &lt;code&gt;nav&lt;/code&gt;, &lt;code&gt;caption&lt;/code&gt; and about twenty more) or set at 15px or under. An eyebrow is a short line before a heading with a class like &lt;code&gt;kicker&lt;/code&gt; or &lt;code&gt;badge&lt;/code&gt;, two style signals, or one of a fixed list of announcing phrases. It reads Tailwind class strings in &lt;code&gt;.jsx&lt;/code&gt;, &lt;code&gt;.tsx&lt;/code&gt;, &lt;code&gt;.vue&lt;/code&gt;, &lt;code&gt;.svelte&lt;/code&gt; and &lt;code&gt;.astro&lt;/code&gt; files too, including &lt;code&gt;cn()&lt;/code&gt; and &lt;code&gt;clsx()&lt;/code&gt; calls.&lt;/p&gt;

&lt;h2&gt;
  
  
  I ran five of the rules on my own homepage
&lt;/h2&gt;

&lt;p&gt;I didn't run sloplint. It's downloaded code, and I don't execute that for a post. I read the functions, ported five checks to about 100 lines of Python, and ran them on the HTML and CSS of somiibo.com, my product's homepage. The CSS is one 217 KB bundle: Bootstrap 5.3.6, then the site's own layer. My scripts print one line per finding; here is the tally:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;transition: all          16 rules (15 site layer, 1 inline)
animated height/width     2 (Bootstrap's own .collapsing)
purple-to-blue gradient   7 rules, 2 in use (hero, pills)
tracked capitals          0
middle-dot strings        0
eyebrows                  6 above 6 headings
accented word             6 of 6 headings
focus styles              present (passes)
overflow-x: clip          missing on html and body
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The headings stung. Each h1 and h2 wraps its last word or two in &lt;code&gt;&amp;lt;span class="text-accent"&amp;gt;&lt;/code&gt;, an italic serif: "Grow your social media on &lt;em&gt;autopilot&lt;/em&gt;", "Unlock your growth &lt;em&gt;potential&lt;/em&gt;". Each also has an eyebrow: capitals over the h1, a pill over every h2. Two pills say "How it works" and "Get started", both on the lint's list.&lt;/p&gt;

&lt;p&gt;The gradient is less clear-cut. The hero uses an eight-stop rainbow with &lt;code&gt;#a166ab&lt;/code&gt; (hue 322 by my conversion) and &lt;code&gt;#5073b8&lt;/code&gt; (263) in it, so the rule calls it purple-to-blue, though no person would. The textbook case, &lt;code&gt;#5b47fb&lt;/code&gt; to &lt;code&gt;#00d4ff&lt;/code&gt;, sits in the same stylesheet, unused on this page. The static tier reads rules, not usage.&lt;/p&gt;

&lt;p&gt;The passes were real: no positive letter-spacing anywhere, no text joining items with two middle dots, and focus styles exist.&lt;/p&gt;

&lt;p&gt;I tried ianwiedenman.com too. Above my name sits "Hello, I'm" in a &lt;code&gt;&amp;lt;p class="text-uppercase tracking-widest"&amp;gt;&lt;/code&gt;, an eyebrow asking for tracked caps. But &lt;code&gt;tracking-widest&lt;/code&gt; is a Tailwind class on a Bootstrap site, and neither of its stylesheets defines it, so the browser draws plain capitals. The glow behind it is three radial gradients whose custom properties are named &lt;code&gt;--grad-purple-x&lt;/code&gt;, &lt;code&gt;--grad-cyan-x&lt;/code&gt; and &lt;code&gt;--grad-pink-x&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why a lint beats a paragraph of taste
&lt;/h2&gt;

&lt;p&gt;Here's my claim. "Make it look premium" in a prompt is a wish. A model can agree with it and has no way to check its output against it. "No stop between hue 265 and 330 beside one between 200 and 264" is a test. It fails a build, names a line, and doesn't get tired.&lt;/p&gt;

&lt;p&gt;There's evidence this matters. In September a user, ktlesr, posted &lt;a href="https://github.com/Nutlope/hallmark/issues/74" rel="noopener noreferrer"&gt;a measurement in issue #74&lt;/a&gt; on an earlier version: four landing-page briefs built with and without the skill, scored by pattern checks. Pure black or white appeared on 40 of 40 pages without it and 14 of 28 with it. &lt;code&gt;transition: all&lt;/code&gt; didn't drop (14 of 40 against 16 of 28). Their reading: that rule lived in a reference file the model never opened.&lt;/p&gt;

&lt;p&gt;Version 2 reads like the answer. &lt;code&gt;transition: all&lt;/code&gt; is now rule 2 of the Floor, in the file that always loads, and rule 16 tells the agent to run sloplint, when Node is available, and fix every FAIL before handing back. A rule in prose can be skipped. A script with an exit code can't, as long as something runs it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where a lint stops
&lt;/h2&gt;

&lt;p&gt;A lint on tells is a floor, not a design. Strip the eyebrows and italic words from my homepage and it avoids a dozen mistakes, but it still has no idea of its own. Builds pushed away from the same defaults can also converge on a new shared look. In a &lt;a href="https://news.ycombinator.com/item?id=49058547" rel="noopener noreferrer"&gt;July HN thread&lt;/a&gt;, mstkllah said design skills like this all end up in the same place unless you are very explicit. Hallmark's answer sits outside the lint: themes dealt from fifty, a log of past builds the deal avoids, and a reviewer subagent that reads the page cold. That part is judgment, and I didn't test it.&lt;/p&gt;

&lt;p&gt;Some rules are opinions. "Never an eyebrow" and "never tracked caps" are house rules, so no waiver. A magazine that sets section labels in spaced small caps on purpose is breaking the Floor.&lt;/p&gt;

&lt;p&gt;One rough edge in the source: &lt;code&gt;--tier&lt;/code&gt; isn't validated. Pass &lt;code&gt;--tier Floor&lt;/code&gt; with a capital F and the filter matches nothing, so the run prints clean and exits 0. &lt;a href="https://github.com/Nutlope/hallmark/pull/93" rel="noopener noreferrer"&gt;PR #93&lt;/a&gt; reported it and was closed unmerged; the commit I read (6033bab) still does it. My own checks were static only. The fold and anything measured on the rendered page need the render tier, which needs Chrome and &lt;code&gt;puppeteer-core&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it on a page you already have
&lt;/h2&gt;

&lt;p&gt;After the install, &lt;code&gt;/hallmark audit ./index.html&lt;/code&gt; reports without redesigning anything. From a clone, the bare lint is &lt;code&gt;node skills/hallmark/scripts/sloplint.mjs index.html&lt;/code&gt;. I haven't run either; I read the source and reproduced a few checks by hand.&lt;/p&gt;

&lt;p&gt;One detail I liked: the photo library is 755 openly licensed pictures on Wikimedia Commons, credited on the page. The README says it skips Unsplash, Pexels and Pixabay because their terms bar AI tools from picking their images.&lt;/p&gt;

&lt;p&gt;Which of these tells is on your own homepage right now, and is it there on purpose?&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>ai</category>
      <category>opensource</category>
      <category>discuss</category>
    </item>
    <item>
      <title>A spec is only ground truth if the agent can grep it</title>
      <dc:creator>Ian</dc:creator>
      <pubDate>Thu, 29 Oct 2026 18:00:00 +0000</pubDate>
      <link>https://dev.to/ianwieds/a-spec-is-only-ground-truth-if-the-agent-can-grep-it-i70</link>
      <guid>https://dev.to/ianwieds/a-spec-is-only-ground-truth-if-the-agent-can-grep-it-i70</guid>
      <description>&lt;p&gt;Most of my repos have an AGENTS.md. It tells the agent how the project works, what not to touch, and why.&lt;/p&gt;

&lt;p&gt;On any given turn, I could not tell you whether the agent read it.&lt;/p&gt;

&lt;p&gt;That is the quiet problem with "spec-driven" setups. The spec is usually one big markdown file, and the agent may or may not open it. JetBrains has a project out with a different bet, and it is the most interesting part of a launch most people are arguing about for other reasons.&lt;/p&gt;

&lt;h2&gt;
  
  
  What ThinkRail is
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/JetBrains/thinkrail" rel="noopener noreferrer"&gt;ThinkRail&lt;/a&gt; is an agentic IDE from a JetBrains incubator, Apache-2.0, written in TypeScript. It wraps the &lt;code&gt;pi&lt;/code&gt; coding agent (version 1.4.0, pinned in the root &lt;code&gt;package.json&lt;/code&gt;) in a desktop or browser app. Each workspace is a git worktree, you can run several pi sessions per workspace, and there is a Monaco editor, diffs and review comments around them.&lt;/p&gt;

&lt;p&gt;The repo was created on July 8 and had 4,038 stars when I checked on Tuesday evening. It was on GitHub trending with 1,204 stars in a day, and the HN thread "JetBrains Thinkrail" had 239 points and 174 comments. The latest stable release is v0.2.0 from October 18; nightlies have reached 0.3.0-nightly.7.&lt;/p&gt;

&lt;p&gt;Worktrees and tabs are not new. The README calls the other part "living specs", and that is the part I read closely.&lt;/p&gt;

&lt;h2&gt;
  
  
  A spec with an address
&lt;/h2&gt;

&lt;p&gt;In ThinkRail, a spec is a &lt;code&gt;SPEC.md&lt;/code&gt; file that sits beside the code it describes. Every one starts with a small YAML header. Here is the real header of the module that implements the whole feature, &lt;code&gt;packages/spec-graph/SPEC.md&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;module-spec-graph&lt;/span&gt;
&lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;module-design&lt;/span&gt;
&lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;active&lt;/span&gt;
&lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Spec-Graph pi extension&lt;/span&gt;
&lt;span class="na"&gt;parent&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;architecture&lt;/span&gt;
&lt;span class="na"&gt;depends-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[]&lt;/span&gt;
&lt;span class="na"&gt;tags&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;spec-graph&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;pi-extension&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;id&lt;/code&gt; is the address. &lt;code&gt;parent&lt;/code&gt; points one level up, &lt;code&gt;depends-on&lt;/code&gt; points sideways. The types run from &lt;code&gt;goal-and-requirements&lt;/code&gt; at the root down through &lt;code&gt;architecture-design&lt;/code&gt;, &lt;code&gt;module-design&lt;/code&gt; and &lt;code&gt;submodule-design&lt;/code&gt;, plus a temporary &lt;code&gt;task-spec&lt;/code&gt;. Maciej Gorywoda's &lt;a href="https://thinkrail.ai/blog/thinkrail-sdd/" rel="noopener noreferrer"&gt;post on the ThinkRail blog&lt;/a&gt; lays out the same header and suggests starting with nothing more than that and a paragraph in AGENTS.md.&lt;/p&gt;

&lt;p&gt;The agent gets seven tools for this graph: &lt;code&gt;spec_grep&lt;/code&gt;, &lt;code&gt;spec_get&lt;/code&gt;, &lt;code&gt;spec_graph&lt;/code&gt;, &lt;code&gt;spec_create&lt;/code&gt;, &lt;code&gt;spec_update&lt;/code&gt;, &lt;code&gt;spec_delete&lt;/code&gt; and &lt;code&gt;spec_validate&lt;/code&gt;. &lt;code&gt;spec_get&lt;/code&gt; returns a node's header, its resolved links and its path, but not the body; the agent reads the body with the normal &lt;code&gt;read&lt;/code&gt; tool. &lt;code&gt;spec_graph&lt;/code&gt; returns a bounded slice: a subtree, the ancestors, or the neighbors of one node.&lt;/p&gt;

&lt;p&gt;There is no database behind it. The spec says the filesystem is the source of truth. Each read re-scans the tree, re-parses only the files whose modification time or size changed, and skips folders like &lt;code&gt;.claude&lt;/code&gt; and &lt;code&gt;.superpowers&lt;/code&gt;, where other assistants leave plans with spec-shaped headers that are not this project's specs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lookup beats loading
&lt;/h2&gt;

&lt;p&gt;Here is the claim I want to argue. A spec is only ground truth if the agent can look it up the same way it looks up code. A README is something you hope gets loaded. A graph with ids and a grep tool is something the agent can query.&lt;/p&gt;

&lt;p&gt;The difference shows up as soon as a project gets big. ThinkRail's own spec graph is about 290,000 words. Nobody wants that in every prompt. But the agent never needs all of it. It needs the spec next to the file it is editing, that spec's parent, and whatever the parent says it depends on. With ids and &lt;code&gt;spec_graph&lt;/code&gt;, that is one or two calls, and the rest stays out of context.&lt;/p&gt;

&lt;p&gt;That is also how it gets the rule in front of the agent. The extension hooks pi's &lt;code&gt;before_agent_start&lt;/code&gt; event and adds a short named section to the system prompt on every run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;pi&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;before_agent_start&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="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;systemPromptOptions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sections&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;SPEC_SECTION&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;SPEC_RULE&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 rule opens with "Specs are this project's ground truth." What surprised me is the next line. It does not say "read the specs first." It says to consult the relevant spec when the work touches a documented boundary, contract or decision, and that localized work need not read unrelated specs. The repo's own AGENTS.md says the same under the heading "Read context proportionally." The blog post's sample AGENTS.md paragraph is stricter and says to read specs before code. The shipped tool picked cheap lookup over a forced read.&lt;/p&gt;

&lt;h2&gt;
  
  
  It uses it on itself
&lt;/h2&gt;

&lt;p&gt;The best evidence for the idea is that the ThinkRail repo is built this way. I counted the files ending in &lt;code&gt;SPEC.md&lt;/code&gt; through the GitHub API on Tuesday at 6:28 PM PT: 105, plus the two root docs, &lt;code&gt;goal-and-requirements.md&lt;/code&gt; and &lt;code&gt;architecture.md&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;I downloaded all 107 and ran a short Python script over their headers, a rough version of what &lt;code&gt;spec_validate&lt;/code&gt; checks. These are my script's numbers, laid out by hand:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;specs with id + type        107 of 107
duplicate ids               0
dangling parent links       0
depends-on links            124, 0 dangling
types                       75 submodule, 30 module, 1 architecture, 1 goal
status                      93 active, 14 draft
deepest parent chain        5 links
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A clean graph is one thing. Is it current? For each of the 89 specs with no other spec below them, I asked GitHub for the spec's last commit and then for every later commit that touched its folder. For 64 of them, nothing in the folder has changed since the spec did. 24 had one to four later commits, and one, &lt;code&gt;apps/web/scripts&lt;/code&gt;, had ten. A commit count is a rough proxy (a dependency bump may need no spec change, and a fresh spec can still be wrong), but it says the team edits specs in the same change as the code, most of the time.&lt;/p&gt;

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

&lt;p&gt;The graph only keeps context small if the nodes stay small. The project's own writing-specs skill says a spec should be readable in one sitting. Its median spec is about 10 KB. Its largest, &lt;code&gt;apps/web/src/panels/SPEC.md&lt;/code&gt;, is 212 KB and 30,640 words under three headings, and 176 of the 593 commits on main have touched it. Seven specs are over 50 KB, which matters because pi's &lt;code&gt;read&lt;/code&gt; tool returns at most 2,000 lines or 50 KB per call (I checked that in pi's published &lt;code&gt;read.js&lt;/code&gt;, not in a session). So "the nearest spec is one call away" is true for most of the graph and false for the parts that change most.&lt;/p&gt;

&lt;p&gt;The tools also stop at the header. &lt;code&gt;spec_update&lt;/code&gt; edits frontmatter only, and the prose is still written with the agent's ordinary &lt;code&gt;write&lt;/code&gt; and &lt;code&gt;edit&lt;/code&gt;. Nothing checks that the prose still matches the code.&lt;/p&gt;

&lt;p&gt;And the HN pushback is fair. One commenter on HN argued that agents don't really read anything: a spec gets pulled into context at random and dropped again at random. Another dismissed the specs themselves as machine-written filler. I can't tell who wrote them. A rule in the system prompt makes reading likely, not certain, and ThinkRail's own rule tells the agent it can skip specs for local work. That is a sensible trade, but it is a trade.&lt;/p&gt;

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

&lt;p&gt;The README's install for macOS and Linux is one line, &lt;code&gt;curl -fsSL https://raw.githubusercontent.com/JetBrains/thinkrail/main/install.sh | bash&lt;/code&gt;, then &lt;code&gt;thinkrail ~/code/my-repo&lt;/code&gt;. There is also a DMG on the releases page. I did not install it. The README says you need git and a model provider, that the macOS CLI is signed but not yet notarized, and that basic usage events and error reports are always sent. Once a repo is open, the Specs tool on the right rail shows the graph as a tree.&lt;/p&gt;

&lt;p&gt;You don't need the IDE to try the idea, though. Give each module a &lt;code&gt;SPEC.md&lt;/code&gt; with an &lt;code&gt;id&lt;/code&gt;, a &lt;code&gt;type&lt;/code&gt;, a &lt;code&gt;title&lt;/code&gt; and a &lt;code&gt;parent&lt;/code&gt;, and add a paragraph to AGENTS.md saying where the specs live and when to read them. Plain &lt;code&gt;grep -r "^id:"&lt;/code&gt; gets you a crude index today.&lt;/p&gt;

&lt;p&gt;So here is what I want to know. How do you keep the agent reading the spec after the first session: a rule in every prompt, a tool it has to call, or a check that fails when the spec and the code disagree?&lt;/p&gt;

</description>
      <category>ai</category>
      <category>programming</category>
      <category>productivity</category>
      <category>opensource</category>
    </item>
    <item>
      <title>On macOS, your terminal's bottleneck is a 1 KiB read</title>
      <dc:creator>Ian</dc:creator>
      <pubDate>Wed, 28 Oct 2026 21:00:00 +0000</pubDate>
      <link>https://dev.to/ianwieds/on-macos-your-terminals-bottleneck-is-a-1-kib-read-27no</link>
      <guid>https://dev.to/ianwieds/on-macos-your-terminals-bottleneck-is-a-1-kib-read-27no</guid>
      <description>&lt;p&gt;I asked macOS for a megabyte. It gave me a kilobyte. Then it did that 4,095 more times.&lt;/p&gt;

&lt;p&gt;That was a twelve-line Python script on my Mac last night, reading from a pseudo-terminal: the same device every terminal app reads your shell's output from. No terminal app involved, just the kernel.&lt;/p&gt;

&lt;p&gt;I ran it because of &lt;a href="https://ghostty.org/docs/install/release-notes/1-4-0" rel="noopener noreferrer"&gt;Ghostty 1.4&lt;/a&gt;, released Oct 27 (753 points and 226 comments on HN at 6:30 PM PT that day). Its release notes credit this ceiling for the biggest speed win. I didn't install Ghostty, so every Ghostty number below is the maintainers' benchmark. The kernel part I checked myself.&lt;/p&gt;

&lt;h2&gt;
  
  
  The 1 KiB ceiling, reproduced
&lt;/h2&gt;

&lt;p&gt;A child process writes 4 MiB to a pty in one call. The parent reads with a 1 MiB buffer and counts what each read returns:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pty&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;collections&lt;/span&gt;
&lt;span class="n"&gt;N&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1024&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1024&lt;/span&gt;                &lt;span class="c1"&gt;# 4 MiB
&lt;/span&gt;&lt;span class="n"&gt;pid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fd&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pty&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fork&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;pid&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;                       &lt;span class="c1"&gt;# child: one big write to the pty
&lt;/span&gt;    &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sa"&gt;b&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;x&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;N&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;_exit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;sizes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;collections&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Counter&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="n"&gt;t0&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;time&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1024&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1024&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;# parent: ask for 1 MiB at a time
&lt;/span&gt;    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;OSError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;break&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;break&lt;/span&gt;
    &lt;span class="n"&gt;sizes&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sizes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;values&lt;/span&gt;&lt;span class="p"&gt;()),&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;reads; sizes&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sizes&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;; %.2fs&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;time&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;t0&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What I got on macOS 26.7.1, Apple Silicon, Python 3.15, three of five runs in a row (the other two matched):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;4096 reads; sizes {1024: 4096} ; 0.04s
4096 reads; sizes {1024: 4096} ; 0.03s
4096 reads; sizes {1024: 4096} ; 0.04s
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every read, exactly 1,024 bytes. The buffer size doesn't matter. Then I swapped the pty for a plain pipe (&lt;code&gt;os.pipe()&lt;/code&gt; plus &lt;code&gt;os.fork()&lt;/code&gt;, same loop): 64 reads, every one 65,536 bytes. Same data, same machine, 64 times fewer trips into the kernel.&lt;/p&gt;

&lt;p&gt;The read counts never moved, run after run. The timings did: 0.03 s on a quiet machine, up to 0.47 s while other work was running. So trust the counts, not my clock.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why a small read costs more than it looks
&lt;/h2&gt;

&lt;p&gt;A kilobyte per read is not slow by itself. My script did 4,096 of them in a few hundredths of a second. The cost shows up when you do real work after each one.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://github.com/ghostty-org/ghostty/pull/13209" rel="noopener noreferrer"&gt;pull request behind the fix&lt;/a&gt; spells out what Ghostty used to do: one loop that read from the pty, then processed what it read, then read again. Each pass paid fixed costs (taking the terminal lock, waking the renderer) no matter how few bytes came in. And while the loop was busy parsing, nobody was draining the pty, so the program producing the output, &lt;code&gt;cat&lt;/code&gt; say, had to wait for room in the kernel's queue.&lt;/p&gt;

&lt;p&gt;Two problems, one cause: a single thread doing two jobs.&lt;/p&gt;

&lt;p&gt;I wanted to feel that, so I wrote a toy. The child writes 4 MiB and reports how long its write was stuck. The parent fakes a "parse" with a 100 microsecond sleep. In one mode it parses after every read, on the same thread. In the other, a reader thread only drains the pty into a queue, and a second thread takes whatever has piled up and parses it as one batch. Seven runs each:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;one 4096 reads; 4096 parses; writer blocked 0.599s; all parsed 0.60s
two 4096 reads;  253 parses; writer blocked 0.041s; all parsed 0.04s
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That pair is typical. Across the seven pairs, one thread kept the writer stuck for 0.59 to 1.15 s. Two threads freed it in 0.04 to 0.11 s, with 233 to 443 batches instead of 4,096 parses.&lt;/p&gt;

&lt;p&gt;Don't read 15x into that. My fake parse costs the same per call no matter the size, so batching erases it. Real parsing costs something per byte too, which is why Ghostty measured 25% to 55% more I/O throughput on macOS (M4 Max, against 1.3.x), not 15x. Combined with a much faster parser in the same release, the maintainers report &lt;code&gt;cat&lt;/code&gt; of a 150 MB file going from 1.5 s to 566 ms.&lt;/p&gt;

&lt;h2&gt;
  
  
  The claim: suspect the read loop first
&lt;/h2&gt;

&lt;p&gt;Here is what I take from it. When a terminal chokes on heavy output, the instinct is to blame the terminal: its renderer, its parser, its language. On macOS, look at the read loop first. The PR says the Ghostty team had been happy to find 2% gains in this subsystem in recent years. Then 25% to 55% turned up in how the loop met a 1 KiB pipe.&lt;/p&gt;

&lt;p&gt;The cap belongs to the kernel, so it hits anything that reads a pty on macOS: multiplexers, test runners that capture output through a pty, &lt;code&gt;expect&lt;/code&gt;-style scripts, editor terminals. I haven't checked how any of those read. But any loop that reads, does real work, then reads again pays its overhead every kilobyte and keeps the writer waiting. The fix is not clever. Split the draining from the processing.&lt;/p&gt;

&lt;p&gt;In the PR, Mitchell Hashimoto says he chose the old design in 2023 believing reads are usually small. They are, he writes now, because the kernel cuts them that way.&lt;/p&gt;

&lt;h2&gt;
  
  
  The other half: memory
&lt;/h2&gt;

&lt;p&gt;The second change is scrollback. Ghostty 1.4 compresses scrollback pages you aren't looking at, while the terminal is idle, and decompresses them when you scroll or search. The notes claim 70% to 90% less resident memory for text-heavy history, so the default scrollback limit went from 10 MB to 50 MB per terminal.&lt;/p&gt;

&lt;p&gt;Smaller numbers from the same notes: an empty terminal's page memory on macOS is about 16 KiB, down from 128 KiB. Hidden tabs now give back their GPU resources. With 20 hidden tabs and one visible, GPU memory went from about 385 MiB to 18 MiB.&lt;/p&gt;

&lt;p&gt;Two caveats: compression only runs on macOS and 64-bit Linux, and it lowers resident memory, not virtual, so measure the right one.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to try today
&lt;/h2&gt;

&lt;p&gt;Install from &lt;a href="https://ghostty.org/download" rel="noopener noreferrer"&gt;ghostty.org/download&lt;/a&gt;. On a Mac, an existing install picks up the update through its built-in updater. Homebrew's cask still listed 1.3.1 when I checked on the evening of Oct 27.&lt;/p&gt;

&lt;p&gt;Two commands worth a minute:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ghostty +ssh user@host               &lt;span class="c"&gt;# installs Ghostty's terminfo on the remote, forwards COLORTERM and friends&lt;/span&gt;
ghostty +explain-config font-size    &lt;span class="c"&gt;# prints the docs for one option, paged&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;+ssh&lt;/code&gt; targets the usual cause of odd behavior over SSH, a complaint repeated in the HN thread: the remote doesn't know &lt;code&gt;xterm-ghostty&lt;/code&gt;. If the terminfo install fails, it falls back to &lt;code&gt;xterm-256color&lt;/code&gt;. The &lt;a href="https://ghostty.org/docs/features/ssh" rel="noopener noreferrer"&gt;SSH docs&lt;/a&gt; show how to alias plain &lt;code&gt;ssh&lt;/code&gt; to it.&lt;/p&gt;

&lt;p&gt;The breaking change that will bite Mac users: &lt;code&gt;copy-on-select&lt;/code&gt; now defaults to &lt;code&gt;none&lt;/code&gt; on macOS, so selecting text copies nothing, and middle-click paste of a selection stops working. To get 1.3's behavior back:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight conf"&gt;&lt;code&gt;&lt;span class="n"&gt;copy&lt;/span&gt;-&lt;span class="n"&gt;on&lt;/span&gt;-&lt;span class="n"&gt;select&lt;/span&gt; = &lt;span class="n"&gt;primary&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Use &lt;code&gt;clipboard&lt;/code&gt; if you want selections to land on the system clipboard instead. Also on macOS, &lt;code&gt;cmd+up&lt;/code&gt; and &lt;code&gt;cmd+down&lt;/code&gt; jump between prompts again rather than scrolling to the top and bottom.&lt;/p&gt;

&lt;p&gt;Update even if you don't care about speed. The release fixes 10 security advisories. The one I'd flag: with zsh shell integration, a crafted directory name could make the shell send arbitrary escape sequences when you &lt;code&gt;cd&lt;/code&gt; into it, for example quietly replacing your clipboard (&lt;a href="https://github.com/ghostty-org/ghostty/security/advisories/GHSA-2r63-vmw6-3m3c" rel="noopener noreferrer"&gt;GHSA-2r63-vmw6-3m3c&lt;/a&gt;). Cloning a repo is enough to get such a directory.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where it falls short
&lt;/h2&gt;

&lt;p&gt;Linux users on older distros lose out. The GTK app now needs GTK 4.18 and libadwaita 1.7, which rules out Ubuntu 24.04 LTS. The notes point those users to the Flatpak or an older Ghostty.&lt;/p&gt;

&lt;p&gt;Commenters on HN pushed back, too. One won't leave Kitty until Ghostty has something like Kitty's hints mode. One on NixOS said launch takes seconds, and the launch gains in this release are macOS only. One iTerm2 user on an M1 MacBook Pro tested 1.4 and saw no difference. That one is fair: the pty win shows up under heavy output, and most terminal days aren't that. The thread's recurring question, why build another terminal at all, has an answer in Mitchell Hashimoto's own comment: the reusable core, libghostty, is the point.&lt;/p&gt;

&lt;h2&gt;
  
  
  Your turn
&lt;/h2&gt;

&lt;p&gt;The 1 KiB number is a macOS fact. The PR says Linux still needs checking. If you run the script above on Linux, what read sizes does your pty give you?&lt;/p&gt;

</description>
      <category>programming</category>
      <category>productivity</category>
      <category>opensource</category>
      <category>discuss</category>
    </item>
    <item>
      <title>Deleting 500 tests made the suite stronger</title>
      <dc:creator>Ian</dc:creator>
      <pubDate>Wed, 28 Oct 2026 18:00:00 +0000</pubDate>
      <link>https://dev.to/ianwieds/deleting-500-tests-made-the-suite-stronger-1meb</link>
      <guid>https://dev.to/ianwieds/deleting-500-tests-made-the-suite-stronger-1meb</guid>
      <description>&lt;p&gt;This one covers October 21 to 27. The theme was proof: which tests mean something, how fast they run, and how a program can tell a human from a script. Most of the code went into my workflow kit and into two private projects that model human behaviour, one for a browser and one for a game.&lt;/p&gt;

&lt;h2&gt;
  
  
  A test that can only fail for a fake reason is a cost
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/itw-creative-works/workkit" rel="noopener noreferrer"&gt;workkit&lt;/a&gt; is my workflow kit for Claude Code: GitHub issues as the work list, labels as the pipeline, hooks that enforce the rules. It shipped nine releases this week, 0.78 through 0.83.1, and the biggest change was to its own tests.&lt;/p&gt;

&lt;p&gt;The kit's &lt;code&gt;npm test&lt;/code&gt; used to run every suite in one process, one after another. Version 0.81 gave it a parallel runner: each suite runs in its own worker process, with cores minus one running at once. Each suite's output prints whole, in sorted order, so two suites never interleave their lines, and the run ends with the five slowest suites. That last list is the useful part. It told me which suites were slow because they waited, not because they worked.&lt;/p&gt;

&lt;p&gt;Then came the audit. I went through the kit's tests with one question: if this test failed, would it mean real behaviour broke? About 450 cases said no. They asserted that a mock was called, that a file existed after a write, that a framework did what its docs promise. They were struck. My dotfiles repo got the same treatment and lost 85 cases and two whole suites. The suites are smaller and every red is now worth reading.&lt;/p&gt;

&lt;p&gt;The rule that keeps it that way is a hook. The &lt;code&gt;safety:test-bar&lt;/code&gt; hook fires when a test file is written. It names each line that misses the bar: an assertion on an implementation detail, a mock of something the test could run for real, a test with no failure condition. It never blocks. It just says, at write time, which lines are padding. One of its rules is &lt;code&gt;deadline&lt;/code&gt;: it names any test that sleeps a literal number of milliseconds. The kit's own tests were full of those, and every one became a poll that waits &lt;code&gt;until&lt;/code&gt; a condition holds, with one shared deadline. The flakes went with them.&lt;/p&gt;

&lt;p&gt;Two smaller things in the same releases. The commit gate now runs the root &lt;code&gt;npm test&lt;/code&gt; on the staged tree, the exact files going into the commit, not the working tree. And it stands down on a repo's first commit, where there is nothing to prove yet.&lt;/p&gt;

&lt;h2&gt;
  
  
  A mouse path that overshoots on purpose
&lt;/h2&gt;

&lt;p&gt;A new private repo this week models human input for a real Chrome window. It has no public page yet, so no link; the work is worth describing anyway.&lt;/p&gt;

&lt;p&gt;The problem it solves: automation that moves a mouse in a straight line at constant speed, and types at a fixed interval, is trivially a script. Humans do neither. So each pointer move follows a minimum-jerk curve, the shape a hand makes when it is trying to be smooth. It overshoots the target a little, corrects, and sometimes stops part way, like a finger lifting off a trackpad. Clicks have settle and hold times. Scrolling has wheel and trackpad modes. During a wait, the cursor drifts.&lt;/p&gt;

&lt;p&gt;Typing gets the same treatment. Key gaps are drawn from a log-normal distribution, so most are short and a few are long. Keys overlap the way they do on a real keyboard. There are pauses after words and after sentences, and the occasional typo that gets noticed and corrected. Typos never happen in password fields.&lt;/p&gt;

&lt;p&gt;The part I like most is the seed. &lt;code&gt;launch({ seed })&lt;/code&gt; draws every behaviour scale from one number, so the same seed gives the same speed, rhythm and habits every run, and a different seed is a different person. A neutral seed types at about 92 words per minute; the trait range runs from roughly 74 to 110. A &lt;code&gt;run&lt;/code&gt; pin replays one path exactly, which is how I debug a flaky click.&lt;/p&gt;

&lt;p&gt;The input goes over the Chrome DevTools Protocol as trusted events, so &lt;code&gt;navigator.webdriver&lt;/code&gt; reads false and the usual Playwright tells are absent. It ships an MCP server, so an agent can drive a browser with the same input model.&lt;/p&gt;

&lt;h2&gt;
  
  
  Training the vision model on the game itself
&lt;/h2&gt;

&lt;p&gt;The game bot (private, no link) watches a game window, finds monsters and the player with a small vision model, and presses keys with humanized timing. The big change this week was where the training data comes from.&lt;/p&gt;

&lt;p&gt;Until now the sprites came from a community API. Now &lt;code&gt;npm run train&lt;/code&gt; pulls the game client from the game PC when its hash has changed and exports monsters and backgrounds straight from it. The synthetic training set is built from crops of every map, with the game's front layers drawn over the sprites, so the model learns what a monster looks like behind a tree.&lt;/p&gt;

&lt;p&gt;The training exam got honest, too. When a box is missed, the report now says what it was boxed as instead, which is how the three snails turned out to be the lowest scorers. The fix was a weight: &lt;code&gt;training.monsters.weight&lt;/code&gt; oversamples the monsters the exam scored lowest, those three at 3x. &lt;code&gt;npm run train -- --map=&amp;lt;id&amp;gt;&lt;/code&gt; is a quick fine-tune from the last full fit, for one map, instead of a full run.&lt;/p&gt;

&lt;p&gt;Two more small ones. A monster that moves on its own after lying still is flagged &lt;code&gt;moving&lt;/code&gt; and left out of loot. And every config block is now checked by one engine in one order, so a typo like &lt;code&gt;aera&lt;/code&gt; fails to load and names itself, instead of being ignored.&lt;/p&gt;

&lt;h2&gt;
  
  
  The hooks finished moving in, and the network got a log
&lt;/h2&gt;

&lt;p&gt;My dotfiles are private, so no link, but two things are worth a paragraph.&lt;/p&gt;

&lt;p&gt;Last week I moved my Claude Code hooks from shell scripts into one in-process module. This week the last of them joined: the linter, the session context and nudge hooks, notifications and keep-awake. A tool call now forks nothing while the keep-awake hold was seen alive in the last minute. Keep-awake itself got harder to break: the hold survives a Stop while a subagent or background task is still running, and an API-error turn no longer lets the Mac sleep while helpers run.&lt;/p&gt;

&lt;p&gt;The other is a skill called netwatch, born from a bad week of wifi. &lt;code&gt;netwatch.sh&lt;/code&gt; pings the router and 1.1.1.1 once a second, resolves a name through both, samples the wifi channel and signal, and logs every outage with its duration and a cause: the wifi dropped, or the ISP did. A &lt;code&gt;report.sh&lt;/code&gt; counts a night by cause, so the question of whose fault it was has a number.&lt;/p&gt;

&lt;h2&gt;
  
  
  Small but satisfying
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;The streamer-mode VS Code extension (it hides sensitive files while you stream) can pause auto-detect for 1, 5, 30 or 60 minutes and turns itself back on. It also lost its second Activity Bar icon; one icon with a badge now shows the state.&lt;/li&gt;
&lt;li&gt;workkit's review, triage and suite markers moved to &lt;code&gt;~/.workkit/markers/&lt;/code&gt;, out of the temp folder the system cleaner empties, with a seven-day sweep.&lt;/li&gt;
&lt;li&gt;A finding outside the issue in hand now stays in chat under a Findings heading instead of being filed unasked. There is a toggle to file them.&lt;/li&gt;
&lt;li&gt;The content pipeline published about 80 posts across 16 product sites, with no hands on it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Next week: the browser project gets its first real job, and the test bar grows a rule or two from what it keeps catching. If you have audited a test suite and struck a big share of it, what was the question you asked of each test? Comments open.&lt;/p&gt;

</description>
      <category>programming</category>
      <category>opensource</category>
      <category>ai</category>
      <category>productivity</category>
    </item>
    <item>
      <title>A code graph makes claims grep can't, including wrong ones</title>
      <dc:creator>Ian</dc:creator>
      <pubDate>Fri, 23 Oct 2026 18:00:00 +0000</pubDate>
      <link>https://dev.to/ianwieds/a-code-graph-makes-claims-grep-cant-including-wrong-ones-1hhg</link>
      <guid>https://dev.to/ianwieds/a-code-graph-makes-claims-grep-cant-including-wrong-ones-1hhg</guid>
      <description>&lt;p&gt;The most connected function in a code graph I opened this week has 124 edges. It doesn't exist.&lt;/p&gt;

&lt;p&gt;It is ten Python test helpers called &lt;code&gt;_make_siege&lt;/code&gt;, one per test file, plus two TypeScript &lt;code&gt;makeSiege&lt;/code&gt; helpers and three lines that define something else entirely, all folded into one node. The graph's report ranks it first among "your core abstractions."&lt;/p&gt;

&lt;p&gt;I found that by checking every call edge in the graph against the line it cites. That check only works because a code graph does something grep and embeddings never do.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three ways to ask what calls what
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/Graphify-Labs/graphify" rel="noopener noreferrer"&gt;graphify&lt;/a&gt; was on GitHub trending this week, with 1,434 stars in a day and about 131,000 in total. It is a Python CLI plus a &lt;code&gt;/graphify&lt;/code&gt; skill for Claude Code, Codex, Cursor and about twenty other assistants. It parses code with tree-sitter, with no LLM and no embeddings for the code part, and writes a &lt;code&gt;graph.json&lt;/code&gt; of functions, classes and files joined by edges like &lt;code&gt;calls&lt;/code&gt;, &lt;code&gt;imports&lt;/code&gt; and &lt;code&gt;inherits&lt;/code&gt;. You ask it things with &lt;code&gt;graphify query&lt;/code&gt;, &lt;code&gt;explain&lt;/code&gt; and &lt;code&gt;path&lt;/code&gt;. Every edge is tagged &lt;code&gt;EXTRACTED&lt;/code&gt; (read straight from the source) or &lt;code&gt;INFERRED&lt;/code&gt; (resolved by graphify).&lt;/p&gt;

&lt;p&gt;Here is the claim I want to argue. An agent has three ways to find out what a function calls, or what calls it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;grep&lt;/strong&gt; returns lines. Evidence, with no meaning attached. It cannot be wrong about what line 89 says. It can only be noisy.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Embeddings&lt;/strong&gt; return neighbors: chunks that look like your question. No relation, no exact address.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A graph&lt;/strong&gt; returns sentences: &lt;code&gt;mark_changelog_seen calls _require_member_session&lt;/code&gt;, at a file and line, with a tag saying how sure it is.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Only the graph makes claims. That is the whole value, because a claim is something an agent can act on without reading twenty files. It is also the risk, because a wrong claim comes out in the same format as a right one, tag and all.&lt;/p&gt;

&lt;h2&gt;
  
  
  How I checked
&lt;/h2&gt;

&lt;p&gt;I didn't install graphify. It is a Python tool, &lt;code&gt;graphify install&lt;/code&gt; registers a skill, and &lt;code&gt;graphify claude install&lt;/code&gt; adds a hook to the assistant's settings. I don't run those for a post.&lt;/p&gt;

&lt;p&gt;I didn't need to. The repo ships worked examples with their real output, and one of them is a real public codebase: &lt;a href="https://github.com/glitchwerks/rsl-siege-manager" rel="noopener noreferrer"&gt;glitchwerks/rsl-siege-manager&lt;/a&gt;, a FastAPI backend, a React frontend and a Discord bot in one repo. The case study pins commit &lt;code&gt;6085fd6&lt;/code&gt; and commits the &lt;code&gt;graph.json&lt;/code&gt; graphify built from it in May: 1,886 nodes, 3,876 edges.&lt;/p&gt;

&lt;p&gt;So I cloned the app at that commit and wrote a 36-line Node script. Every &lt;code&gt;calls&lt;/code&gt; edge carries the file and line of the call, so for each one it reads that line and asks three questions:&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="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;g&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;links&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;l&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;l&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;relation&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;calls&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;src&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;node&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;source&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="nx"&gt;tgt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;node&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;tgt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;label&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;\(\)&lt;/span&gt;&lt;span class="sr"&gt;$/&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/^&lt;/span&gt;&lt;span class="se"&gt;\.&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;lineOf&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;source_file&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;source_location&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;faults&lt;/span&gt; &lt;span class="o"&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="nf"&gt;family&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;src&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;source_file&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nf"&gt;family&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tgt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;source_file&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="nx"&gt;faults&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;wrongLanguage&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;src&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;source_file&lt;/span&gt; &lt;span class="o"&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;source_file&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;faults&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;callerFromOtherFile&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;RegExp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`(^|[^&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s2"&gt;w])&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nf"&gt;esc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s2"&gt;s*[(&amp;lt;]`&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="nx"&gt;faults&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;nameNotOnLine&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Is the caller in the same language as the callee? Is the caller defined in the file where the call happens? Does the cited line actually call something by the callee's name? It ran in under half a second, with identical output both times.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the check found
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;            edges   pass   py&amp;lt;-&amp;gt;ts   caller elsewhere   name not on line
EXTRACTED     989    853        83                 57                 84
INFERRED      153    105        34                 33                  7
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;958 of the 1,142 call edges pass all three checks. 184 fail at least one, and 136 of those are tagged &lt;code&gt;EXTRACTED&lt;/code&gt;, the tag that means "read directly from the source."&lt;/p&gt;

&lt;p&gt;Most of the failures share one cause: 145 of the 184 touch a node that two or more files claim to contain. Here is one such node with four of its six edges, read from the raw JSON:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;markChangelogSeen()                  frontend/src/api/changelog.ts L12
  &amp;lt;-- contains  changelog.ts                EXTRACTED
  &amp;lt;-- contains  changelog.py                EXTRACTED
  --&amp;gt; calls     _require_member_session()   EXTRACTED  backend/app/api/changelog.py:L89
  --&amp;gt; calls     ChangelogStatusResponse     INFERRED   backend/app/api/changelog.py:L95
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The real &lt;code&gt;markChangelogSeen&lt;/code&gt; is a two-line TypeScript function that sends a POST. The Python endpoint it hits, &lt;code&gt;mark_changelog_seen&lt;/code&gt;, has no node of its own. Two of its calls now belong to the TypeScript function, because the graph merged the two by name. The rest of the failures are name matching gone wide: 22 calls to Python's built-in &lt;code&gt;list()&lt;/code&gt; point at &lt;code&gt;const list = screen.getByTestId("feature-list")&lt;/code&gt; in a TypeScript test.&lt;/p&gt;

&lt;p&gt;That happened to a lot of code. Of the 769 top-level Python functions in the graphed files, 200 have no node in their own file, and 164 of those share a name, give or take case and underscores, with a node somewhere else. 121 nodes are claimed by two or more files. Six of the committed report's top ten god nodes are helpers merged across files, led by &lt;code&gt;_make_siege()&lt;/code&gt; at 124 edges, &lt;code&gt;_make_member()&lt;/code&gt; at 92 and &lt;code&gt;_make_position()&lt;/code&gt; at 85.&lt;/p&gt;

&lt;p&gt;The case study's review saw that ranking and concluded that test factories pile up edges because every test uses them. A reasonable reading of the report. One command says otherwise:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ grep -rln "def _make_siege(" . | wc -l
      10
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is the difference between evidence and a claim. grep showed ten functions. The graph had already decided there was one.&lt;/p&gt;

&lt;h2&gt;
  
  
  In fairness, this was fixed
&lt;/h2&gt;

&lt;p&gt;The merges came from the dedup pass, which matched labels by fuzzy similarity, a sensible thing for doc concepts and a wrong one for code. &lt;a href="https://github.com/Graphify-Labs/graphify/issues/1205" rel="noopener noreferrer"&gt;Issue #1205&lt;/a&gt;, opened June 9, reported a single &lt;code&gt;--update&lt;/code&gt; dropping about 1,300 nodes from a 6,000-node graph. It was fixed the next day: code nodes are now keyed by ID only and never label-merged. Version 0.9.0 put the full path into every node ID. Cross-language &lt;code&gt;INFERRED&lt;/code&gt; calls are dropped at build time. The current release is 0.9.93.&lt;/p&gt;

&lt;p&gt;I read those fixes in the source. I did not run the current version, so I can't say what it builds from this repo today. The committed graph has not been rebuilt since May, and the docs still point at the worked folders as output you can check.&lt;/p&gt;

&lt;p&gt;What interests me is the changelog itself. It reads like a field guide to wrong edges: short names like &lt;code&gt;log&lt;/code&gt; and &lt;code&gt;find&lt;/code&gt; collecting hundreds of spurious calls (#543), an npm import landing on an unrelated Python file as an &lt;code&gt;EXTRACTED&lt;/code&gt; edge (#1638), three phantom edges that were the only bridge between 2,409 Python nodes and 1,403 TypeScript nodes (#1749). Each got fixed. The class of bug stays, because resolving a call means deciding which of several same-named things it meant, and that is always a judgment.&lt;/p&gt;

&lt;h2&gt;
  
  
  How I'd use one
&lt;/h2&gt;

&lt;p&gt;Treat every edge as a citation, not a fact. The current &lt;code&gt;graphify explain&lt;/code&gt; prints the call site next to each edge, so checking one is a single read. Do that read before acting on any edge that would change code.&lt;/p&gt;

&lt;p&gt;For "who calls this" before a rename or a delete, use the graph for the shortlist and grep for the completeness. A graph that misses a caller gives no sign of it. grep's noise is visible: you read it.&lt;/p&gt;

&lt;p&gt;Watch the install's defaults, too. The always-on hook tells the agent it "MUST run &lt;code&gt;graphify query&lt;/code&gt;" before grepping. The same message says to read raw files once the graph has named the lines. Keep that second half, because that read is what makes the first half safe.&lt;/p&gt;

&lt;p&gt;The limits of my check: it only catches faults that show on the cited line, so a call bound to the wrong same-language twin passes, and 184 is a floor. And it is one repo, graphed five months ago. The project's own code benchmark, 82.0% key-fact coverage against 70.8% for a grep-and-read baseline, rests on six questions.&lt;/p&gt;

&lt;p&gt;When your agent's tool says "A calls B," what does it check before it believes it?&lt;/p&gt;

</description>
      <category>ai</category>
      <category>programming</category>
      <category>opensource</category>
      <category>productivity</category>
    </item>
    <item>
      <title>In an AI security audit, the skeptic is the product</title>
      <dc:creator>Ian</dc:creator>
      <pubDate>Thu, 22 Oct 2026 18:00:00 +0000</pubDate>
      <link>https://dev.to/ianwieds/in-an-ai-security-audit-the-skeptic-is-the-product-4fl4</link>
      <guid>https://dev.to/ianwieds/in-an-ai-security-audit-the-skeptic-is-the-product-4fl4</guid>
      <description>&lt;p&gt;I wrote four security findings against a 47-line Node service. Two of them were false. One cites a file that does not exist.&lt;/p&gt;

&lt;p&gt;Cloudflare's findings validator passed all four.&lt;/p&gt;

&lt;p&gt;That is not a bug in the validator. It is the clearest way I found to show where the value of an AI security review actually sits.&lt;/p&gt;

&lt;h2&gt;
  
  
  The skill
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/cloudflare/security-audit-skill" rel="noopener noreferrer"&gt;cloudflare/security-audit-skill&lt;/a&gt; is a coding-agent skill that turns one agent into a coordinator of many: recon agents map the code, hunter agents look for bugs, and fresh verifier agents try to knock each finding down before it reaches a report. It was on GitHub trending this week with almost 2,000 stars in a day, 35,690 in total when I checked. MIT licensed. Cloudflare says it is the starting point of the vulnerability harness they now run across their whole fleet.&lt;/p&gt;

&lt;p&gt;I measured it before reading it closely. The skill is 15 markdown files: 1,778 lines, 24,768 words, about 183 KB. At the usual four bytes per token that is roughly 46,000 tokens of instructions, loaded in pieces as a run needs them. Cloudflare's &lt;a href="https://blog.cloudflare.com/build-your-own-vulnerability-harness/" rel="noopener noreferrer"&gt;blog post&lt;/a&gt; says the first version was about 450 lines.&lt;/p&gt;

&lt;p&gt;Two zero-dependency Node scripts sit beside the prose: &lt;code&gt;validate-findings.cjs&lt;/code&gt; (773 lines) and &lt;code&gt;validate-coverage-ledger.cjs&lt;/code&gt; (872 lines). Their own test suites ran clean on my machine, 34 and 31 tests.&lt;/p&gt;

&lt;p&gt;The part that caught my eye is the proportion. Of the six phases, two exist only to disprove: Phase 3 hands every candidate to a fresh agent told to refute it, and Phase 5 sends another fresh agent over the final record (the quick profile merges the two). Each Phase 3 verifier prompt carries about 17.5 KB of fixed text before it sees the candidate: a six-step refutation prompt, a file-promotion procedure, and three schema branches copied in verbatim.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I actually did
&lt;/h2&gt;

&lt;p&gt;I did not install the skill, and no agent ran it. The README's install path is a skills CLI that writes into your agent's config, and I don't do that for a post.&lt;/p&gt;

&lt;p&gt;Instead I wrote a small target, played the hunter by hand, and ran the skill's real validator on the result. The target is a notes service on &lt;code&gt;node:http&lt;/code&gt; and &lt;code&gt;node:sqlite&lt;/code&gt; with dummy users alice and bob. These are the two functions that matter:&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="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;q&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sql&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;SELECT id, title FROM notes WHERE owner = '&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;' AND title LIKE '%&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;q&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;%'&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;prepare&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sql&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;all&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getNote&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&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;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;prepare&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;SELECT id, title, body FROM notes WHERE id = ? AND owner = ?&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;user&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;There is also an &lt;code&gt;/admin/export&lt;/code&gt; route gated on &lt;code&gt;X-Forwarded-For&lt;/code&gt; starting with &lt;code&gt;10.&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Then I wrote four findings the way a hunter returns them, in the skill's schema:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;SQL injection in &lt;code&gt;search&lt;/code&gt;. Real.&lt;/li&gt;
&lt;li&gt;SQL injection in &lt;code&gt;getNote&lt;/code&gt;, marked confirmed, with an observed result claiming it returned bob's note. False.&lt;/li&gt;
&lt;li&gt;SQL injection in &lt;code&gt;findUser&lt;/code&gt; at &lt;code&gt;src/users.js&lt;/code&gt; line 212. Invented: that file does not exist.&lt;/li&gt;
&lt;li&gt;The export gate trusts a header a client can set, unless a proxy rewrites it. A real question, but the proxy is not in the repo.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  What the validator checks
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ node validate-findings.cjs proposed.json
PASS: 4 findings valid
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To be fair to it, it does enforce the contract. When I raised one record's overall severity above its impact, or put a severity on the &lt;code&gt;needs_validation&lt;/code&gt; record, it failed, with exact paths. It checks shape, ordering, safe relative paths, and that a confirmed record has an observed result written in visible text.&lt;/p&gt;

&lt;p&gt;It never opens a source file. It doesn't take the target path at all. The skill says this itself, in one line of Phase 4: validator success "proves format and ledger consistency only." A hunter's observed result is a string, and my invented one was schema-valid.&lt;/p&gt;

&lt;h2&gt;
  
  
  A cheap check gets one
&lt;/h2&gt;

&lt;p&gt;Next I wrote 15 lines of Node that open every cited file and check the line exists:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ok   admin/export-xff-gate
ok   notes/note-sql-concat
ok   notes/search-sql-concat
FAIL users/find-user-template-sql  src/routes/login.js: no such file; src/users.js: no such file
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That kills the invented file. Cloudflare's harness has the same idea in plain code: per the blog, deterministic code confirms the cited files and paths exist before a finding moves on.&lt;/p&gt;

&lt;p&gt;It does nothing for the &lt;code&gt;getNote&lt;/code&gt; claim. Line 24 is real, and it really runs a query with the id in it. Every fact the record cites is true except the one that matters.&lt;/p&gt;

&lt;h2&gt;
  
  
  The skeptic gets the rest
&lt;/h2&gt;

&lt;p&gt;So I walked the Phase 3 prompt by hand. Step 2 says to rebuild the strongest control on the path. For &lt;code&gt;getNote&lt;/code&gt; that takes one read: &lt;code&gt;id&lt;/code&gt; and &lt;code&gt;owner&lt;/code&gt; are bound as placeholders. Step 3 says to reproduce the minimum result yourself. My service is my own code with dummy data, so I ran it on loopback (the skill demands an OS-enforced sandbox before it runs anyone else's target; this was not that). Output trimmed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;search, normal     200 [{"id":1,"title":"groceries"}]
search, payload    200 [{"id":1,"title":"groceries"},{"id":2,"title":"payroll"}]
note, bob id       200 null
note, payload      200 null
note, payload 2    200 null
export, XFF 10.    200 [... both users' notes ...]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The search payload &lt;code&gt;' OR 1=1 --&lt;/code&gt; hands alice the title of bob's note. Every &lt;code&gt;getNote&lt;/code&gt; payload returns nothing. The export gate opens to a header alone, but whether a client can send that header in production depends on a proxy the repo doesn't contain, so it stays &lt;code&gt;needs_validation&lt;/code&gt; with an owner check attached. That is the skill's rule for a deployment fact, and I agree with it.&lt;/p&gt;

&lt;p&gt;The final file: one confirmed, one needs validation, two rejected.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ node validate-findings.cjs findings.json
PASS: 4 findings valid
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same validator, same four fingerprints, same PASS. In between, two of the three "confirmed" bugs became rejected. Only reading the code at the cited line and running the payload told the files apart.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the skeptic is the expensive part
&lt;/h2&gt;

&lt;p&gt;Finding candidates is cheap now. Cloudflare's blog reports 20,799 raw candidates from their discovery harness, of which about 12,057 survived validation, and an early validation rejection rate of 40% before better recon brought it down to 11%. At 40%, a team without a refuter gets four bad tickets in every ten. Somebody pays for those, and it's usually a maintainer.&lt;/p&gt;

&lt;p&gt;That is also the theme of OpenAI's "Patch the Planet" post on coordinating disclosure as agents surface critical bugs, which was on the Hacker News front page the same day.&lt;/p&gt;

&lt;p&gt;So my claim: when you judge an AI security tool, judge the step that tries to prove the finding wrong. The hunt is a hypothesis generator. The product is the record that survived.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where it falls short
&lt;/h2&gt;

&lt;p&gt;Cost is real. In the &lt;a href="https://news.ycombinator.com/item?id=49736466" rel="noopener noreferrer"&gt;Hacker News thread&lt;/a&gt;, jesse_dot_id reported at least 150k tokens on a fairly small FastAPI project before hitting a session limit. Issue &lt;a href="https://github.com/cloudflare/security-audit-skill/issues/20" rel="noopener noreferrer"&gt;#20&lt;/a&gt; is a blind test on one seeded target, three rounds per arm, by a team with its own competing tool, which it discloses. There the quick profile cost a median $29.95 against $2.06 for one plain agent, and found the same share of the seeded bugs (a median recall of 0.467 for both). On a small, clean target the skeptic may have nothing to kill.&lt;/p&gt;

&lt;p&gt;And a skeptic that shares the hunter's model is not fully independent. Per that same issue, the skill's verifiers inherit the session's model. Cloudflare's own pipeline triages with a different model from the one that hunts. Fresh context is not a different mind.&lt;/p&gt;

&lt;p&gt;If you run AI reviews on your code today, what does the step that tries to disprove a finding look like: a second model, a test that must fail before the fix, or you?&lt;/p&gt;

</description>
      <category>ai</category>
      <category>programming</category>
      <category>opensource</category>
      <category>javascript</category>
    </item>
    <item>
      <title>DuckDB 2.0's agent mode is a footer, and -json throws it away</title>
      <dc:creator>Ian</dc:creator>
      <pubDate>Wed, 21 Oct 2026 21:48:00 +0000</pubDate>
      <link>https://dev.to/ianwieds/duckdb-20s-agent-mode-is-a-footer-and-json-throws-it-away-22hh</link>
      <guid>https://dev.to/ianwieds/duckdb-20s-agent-mode-is-a-footer-and-json-throws-it-away-22hh</guid>
      <description>&lt;p&gt;The first time I ran DuckDB 2.0 today, it printed one line before my query:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;duckdb agent mode on: AI_AGENT is set and stdout is not a terminal; -no-agent turns it off, .help agent explains the output
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I hadn't asked for anything. Claude Code was driving my shell, and the CLI noticed.&lt;/p&gt;

&lt;p&gt;An hour later I had the result that matters more. The same 5,000-row query came back as 1,385 bytes in that mode, and as 246,729 bytes once I added &lt;code&gt;-json&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What shipped today
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://duckdb.org/2026/10/21/announcing-duckdb-200" rel="noopener noreferrer"&gt;DuckDB 2.0.0 "Cinnamon Teal"&lt;/a&gt; is out, and it is a long-term support release, supported until at least October 2027. The feature list is long: a client-server protocol called Quack, async I/O for Parquet and CSV, a first-class VARIANT type, triggers, a new PEG parser. It sat on the Hacker News front page this afternoon with close to 300 points, and the &lt;a href="https://news.ycombinator.com/item?id=50181136" rel="noopener noreferrer"&gt;thread&lt;/a&gt; is mostly about speed on big machines, the server mode and triggers.&lt;/p&gt;

&lt;p&gt;I picked the feature nobody in that thread mentioned: agent mode in the CLI. Coding agents run &lt;code&gt;duckdb -c "..."&lt;/code&gt; through a pipe all day, and 2.0 changes what they get back. It turns on when an agent's environment variable is set (my Claude Code shell has both &lt;code&gt;AI_AGENT&lt;/code&gt; and &lt;code&gt;CLAUDECODE&lt;/code&gt;), stdout is not a terminal, and no output format flag was passed. Then the CLI prints compact Markdown tables, errors as JSON, a cost estimate before big queries, and a footer.&lt;/p&gt;

&lt;p&gt;My setup: the official &lt;code&gt;duckdb-cli-osx-arm64&lt;/code&gt; binary from the &lt;a href="https://github.com/duckdb/duckdb/releases/tag/v2.0.0" rel="noopener noreferrer"&gt;v2.0.0 release&lt;/a&gt; (checksum matched, signed by the DuckDB Foundation), run from a scratch folder on an Apple M1 Max. The data was a seeded table of 2 million orders across 5,000 customers that I generated myself. The machine had other jobs running, with load averages between 30 and 98, so treat my timings as ranges.&lt;/p&gt;

&lt;h2&gt;
  
  
  The bytes, counted honestly
&lt;/h2&gt;

&lt;p&gt;Seven queries an agent might run while exploring, each in three modes. Agent mode counts stderr too, since an agent reads both streams.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;query                     box   agent mode     -json
status totals             864          396       308
orders by country         687          453       307
top 10 customers        1,876        1,033     1,179
per customer, 5k rows   2,561        1,385   246,729
SUMMARIZE orders        3,200          982     1,130
EXPLAIN a join          2,932          734     2,932
a typo'd column           206          384       206
total                  12,326        5,367   252,791
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In total, agent mode is 56% smaller than the box, close to the 59% token cut the DuckDB team measured on TPC-H questions. Two rows surprised me. On small results, plain JSON is as small or smaller, because agent mode pays a 124-byte startup line on every call. And errors get bigger: the JSON error is more precise (it carries the position) but nearly twice the size of the plain one.&lt;/p&gt;

&lt;p&gt;The team is candid that bytes are not the prize. In &lt;a href="https://duckdb.org/2026/10/09/agent-mode.html" rel="noopener noreferrer"&gt;their Claude Code experiment&lt;/a&gt;, the query output the model read fell from 123.6k to 50.8k tokens, and that was about 0.5% of the total input, because the system prompt is reread every turn. So if agent mode matters, it has to matter for another reason.&lt;/p&gt;

&lt;h2&gt;
  
  
  The footer is the feature
&lt;/h2&gt;

&lt;p&gt;Here is the bottom of that 5,000-row result in the old box format, which cuts to 40 rows:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;│           · │    ·  │          ·    │
│        4980 │   367 │      95714.21 │
...
  5000 rows (40 shown)      3 columns
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And in agent mode:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;| 19 | 428 | 111142.39 |
| … 4960 rows omitted … |
| 4980 | 367 | 95714.21 |
...
first 20 and last 20 of 5000 rows (.maxbytes 0 for all), hash e1cd177a85aae05e
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both cut the middle. Only the second says so in a row a model can't skim past, tells it how to get the rest, and fingerprints the whole result, including the 4,960 rows it never printed.&lt;/p&gt;

&lt;p&gt;I poked at that hash. Three orderings of the same query gave the same hash, so it doesn't care about row order. A one-cent change to a single order out of 2 million flipped it. Undoing the cent brought the old hash back. &lt;code&gt;NULL&lt;/code&gt; and the string &lt;code&gt;'NULL'&lt;/code&gt; hash differently, as documented. For an agent, "did my fix change the result?" becomes a comparison of two short strings instead of two printed tables.&lt;/p&gt;

&lt;p&gt;The other footer trick is the early stop. &lt;code&gt;FROM range(1_000_000_000)&lt;/code&gt; in agent mode returned in 0.05 to 0.15 seconds over three runs, with 20 rows and "first 20 of at least 102400 rows (query stopped early)". The box took 2.3 to 3.4 seconds to tell me "1000000000 rows (40 shown)".&lt;/p&gt;

&lt;h2&gt;
  
  
  One flag and it's gone
&lt;/h2&gt;

&lt;p&gt;Now the part I'd want to know before trusting any of this. Agents like JSON. It parses, and it's the flag I'd reach for myself when a script reads the output. Pass &lt;code&gt;-json&lt;/code&gt; and agent mode stays off, by design: the docs treat a format flag as a deliberate choice. That per-customer query printed all 5,000 rows, 246,729 bytes, 178 times what agent mode sent.&lt;/p&gt;

&lt;p&gt;Forcing the mode back on doesn't do what I expected. The &lt;a href="https://github.com/duckdb/duckdb/pull/26167" rel="noopener noreferrer"&gt;pull request&lt;/a&gt; that added it says &lt;code&gt;-agent -csv&lt;/code&gt; keeps the mode and only changes the format. That is exactly what happened, and it's the catch. With &lt;code&gt;-agent -json&lt;/code&gt;, the startup line and the estimate line came back on stderr, and then &lt;code&gt;FROM range(1_000_000)&lt;/code&gt; printed every row: 17,888,891 bytes. &lt;code&gt;-agent -csv&lt;/code&gt; printed 6,888,896. Plain &lt;code&gt;-agent&lt;/code&gt; printed 229. Adding &lt;code&gt;.maxrows 40&lt;/code&gt; to a &lt;code&gt;-json&lt;/code&gt; run changed nothing: still 17.9 MB.&lt;/p&gt;

&lt;p&gt;So the cap, the omitted-rows marker, the hash and the early stop all live in the Markdown renderer, not in "agent mode" as a whole. The flag that looks most agent-friendly is the one that throws the guard away.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd tell an agent
&lt;/h2&gt;

&lt;p&gt;If an agent of mine drives DuckDB, its instructions get three lines:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Call &lt;code&gt;duckdb&lt;/code&gt; with no format flag while exploring, and let the default do its job.&lt;/li&gt;
&lt;li&gt;When a program needs JSON, put a &lt;code&gt;LIMIT&lt;/code&gt; in the SQL. Nothing else caps it.&lt;/li&gt;
&lt;li&gt;To check whether an edit changed a result, compare footer hashes, and put &lt;code&gt;SET max_execution_time&lt;/code&gt; in front of anything heavy.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One LTS note, since this is the release teams will pin for a year. A database file created by 2.0 can't be read by older versions (existing files stay readable both ways). If an agent with a 2.0 CLI creates a new file, your 1.x services can't open it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Limits of my check
&lt;/h2&gt;

&lt;p&gt;I didn't run an agent benchmark. Every number above is the CLI's output on my data, not a model's behavior with it. The estimate line counts rows read, not rows produced: for a self-join of orders on customer, it said about 4 million rows read, while the join produced 802,046,608 pairs. It still finished in about a second here, but the line wouldn't have warned me. And this is a day-one CLI: &lt;a href="https://github.com/duckdb/duckdb/issues/27635" rel="noopener noreferrer"&gt;issue #27635&lt;/a&gt;, filed today, reports a deprecation warning landing on stdout with color codes even under &lt;code&gt;-csv&lt;/code&gt;. I reproduced it on my Mac.&lt;/p&gt;

&lt;p&gt;When your agent queries a database, does it ask for JSON, and would it notice if the answer were 17 MB?&lt;/p&gt;

</description>
      <category>ai</category>
      <category>programming</category>
      <category>opensource</category>
      <category>productivity</category>
    </item>
    <item>
      <title>A hook should be a function call, not a shell</title>
      <dc:creator>Ian</dc:creator>
      <pubDate>Wed, 21 Oct 2026 19:57:00 +0000</pubDate>
      <link>https://dev.to/ianwieds/a-hook-should-be-a-function-call-not-a-shell-3dkd</link>
      <guid>https://dev.to/ianwieds/a-hook-should-be-a-function-call-not-a-shell-3dkd</guid>
      <description>&lt;p&gt;This one covers October 14 to 21. Most of the week went into one idea: the small scripts that guard my Claude Code sessions should not each start a shell. The rest went into giving the agent its own name on GitHub, and into a framework bug that only showed up on a small CI runner.&lt;/p&gt;

&lt;h2&gt;
  
  
  Hooks that run inside Claude Code
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/itw-creative-works/workkit" rel="noopener noreferrer"&gt;workkit&lt;/a&gt; is my workflow kit for Claude Code: GitHub issues as the work list, labels as the pipeline, and hooks that enforce the rules. A hook is a small program Claude Code runs on an event, like a prompt, a file edit, or a shell command. It can add context, warn, or block the action.&lt;/p&gt;

&lt;p&gt;Until this week every hook was a shell script. Each fire went through a loader script, which started bash, which usually called jq to read the event, and often node for the real logic. That is three or four processes for one check.&lt;/p&gt;

&lt;p&gt;The count adds up fast. Before version 0.79, every prompt I sent started four shell hooks. Every file edit started four more after the write. Session start ran four, and every subagent spawn ran three.&lt;/p&gt;

&lt;p&gt;So I timed it. On my M1 Max, with other work running and the machine busy, one fire routed through the loader took about 90 ms at the median. A bare bash, jq and node chain alone took about 38 ms. A function call inside a process that is already running rounds to zero.&lt;/p&gt;

&lt;p&gt;Claude Code can now load a plugin module once per session and call it on each event. workkit 0.79 moved thirteen hooks into one such module, and fifteen live there now. My personal hooks did the same in my dotfiles: seven of them, including the ones that stamp the time and check each reply's format, run as one module.&lt;/p&gt;

&lt;p&gt;The design rule that made the move safe: only one file, &lt;code&gt;register.js&lt;/code&gt;, touches the host's interface. It gathers the facts a hook needs (a file's text, a &lt;code&gt;git&lt;/code&gt; answer, a stored marker), and hands them to plain functions in &lt;code&gt;hooks/mods/&amp;lt;name&amp;gt;.js&lt;/code&gt;. Those functions take plain data and return plain data. So the logic tests just &lt;code&gt;require()&lt;/code&gt; them and need no Claude Code at all. Only a thin set of wiring tests runs through &lt;code&gt;claude plugin test&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The hooks that guard shell commands, like the commit gate, stay shell hooks for now. They are next.&lt;/p&gt;

&lt;h2&gt;
  
  
  The agent signs its own work
&lt;/h2&gt;

&lt;p&gt;workkit has the agent do a lot of bookkeeping on GitHub: it claims an issue, parks it for my check with a comment, posts the proof, and closes it. Until 0.76 all of that posted as me. Reading an issue's history, I could not tell my decisions from the agent's notes.&lt;/p&gt;

&lt;p&gt;Now that bookkeeping posts as &lt;code&gt;workkit-agent[bot]&lt;/code&gt;, a GitHub App. How the token gets there was the interesting part:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The key never reaches my machine.&lt;/strong&gt; A small hosted service holds the App's private key. It checks that the App is installed on the repo and that my sign-in can write to it. Then it hands back the bot's token for that one repo, good for one hour.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A shim decides who is speaking.&lt;/strong&gt; workkit puts a &lt;code&gt;gh&lt;/code&gt; shim on the PATH. A call that writes (a comment, a label, a close) gets the bot's token. A read, a commit and a push still run as me.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sign in once per machine.&lt;/strong&gt; Setup runs GitHub's device flow: it prints a code, opens the page, and finishes once I approve. An agent can run it too and pass the code to me.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The claim also changed. It used to be the assignee field, which meant me. It is now an &lt;code&gt;agent:claimed&lt;/code&gt; label, so the claim is the agent's, in plain sight.&lt;/p&gt;

&lt;h2&gt;
  
  
  omega: 91 sign-ups at once sank a CI runner
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/Omega-JS-Stack/omega" rel="noopener noreferrer"&gt;omega&lt;/a&gt; is my framework for building a brand's website, backend, desktop app and browser extension from one config. Its backend self-test seeds 91 test users. It created them all at once.&lt;/p&gt;

&lt;p&gt;Each new user fires an auth trigger, a function that runs on sign-up. The Firebase functions emulator starts one worker per trigger in flight, and it never caps that pool. On a 7 GB CI runner the test reached 52 workers and ran out of memory. A machine with more memory hides the problem.&lt;/p&gt;

&lt;p&gt;The fix is a small helper, &lt;code&gt;mapLimited&lt;/code&gt;, that runs the seed four at a time. The seed had a second bug too: it reported every user created even when the emulator had loaded no functions at all. A green seed was hiding a backend that never ran. Now a user whose sign-up trigger never finishes stops the seed and names the account.&lt;/p&gt;

&lt;p&gt;Two more changes I like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Brand Workers.&lt;/strong&gt; A brand can ship its own Cloudflare Worker. Its script is looked up in the brand's folder first, then the company's, then the framework's. A missing script or secret fails before any request leaves.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Renames that cannot rot.&lt;/strong&gt; The old Jekyll-style template filters now carry &lt;code&gt;omega_&lt;/code&gt; names. An old name does not quietly keep working: it throws, and the error names its replacement. &lt;code&gt;omega migrate&lt;/code&gt; rewrites them for you.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Small but satisfying
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A hook log.&lt;/strong&gt; Every hook fire now writes one line to a log: time, hook, event, session, folder, and whether it ran or stood down. &lt;code&gt;workkit log --follow&lt;/code&gt; tails it. It is the first place I look when a hook seems silent.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Findings stay in chat.&lt;/strong&gt; The agent used to file an issue for every side finding. Now it lists them in its reply and files one only when I say so. My inbox is quieter.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A size warning.&lt;/strong&gt; My patched Claude Code footer shows a warning chip once a session's transcript passes 1 GB. Past that size, a session can fail to reopen. I learned that from a 2.3 GB one.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Daily posts.&lt;/strong&gt; The daily dev.to posts kept going: one open source project a day, studied and, where it is safe, run.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The remaining shell hooks, the commit gate among them, are next in line for the module. If you write hooks for an AI coding tool, I would like to hear how you keep them fast. Comments are open.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>programming</category>
      <category>opensource</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Agents need a ledger more than they need a new commit</title>
      <dc:creator>Ian</dc:creator>
      <pubDate>Sun, 18 Oct 2026 18:00:00 +0000</pubDate>
      <link>https://dev.to/ianwieds/agents-need-a-ledger-more-than-they-need-a-new-commit-oc8</link>
      <guid>https://dev.to/ianwieds/agents-need-a-ledger-more-than-they-need-a-new-commit-oc8</guid>
      <description>&lt;p&gt;In May, Anthropic merged about 2,500 code changes a day. By September it was over 30,000.&lt;/p&gt;

&lt;p&gt;On October 13 it explained what it built to survive that: Scion, an in-house version control system that replaced git for its monorepo. It also said a public version would be too bespoke to help most teams, so it shared the ideas instead.&lt;/p&gt;

&lt;p&gt;A commenter in the thousand-comment Hacker News thread put the obvious question plainly: "If they're not going to open source it, why are they telling us this?" Two days later, an answer showed up as code.&lt;/p&gt;

&lt;h2&gt;
  
  
  The open version
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/feder-cr/open_scion" rel="noopener noreferrer"&gt;open_scion&lt;/a&gt;, by Federico Elia, is a fork of Gitaly, GitLab's Git RPC server, rebuilt around the ideas in Anthropic's post. It went public on October 15 and had over 1,000 stars a day later. GitHub shows the license as "Other", but the file is GitLab's MIT text plus a clause about an &lt;code&gt;ee/&lt;/code&gt; folder, and that folder is deleted here. The code is MIT.&lt;/p&gt;

&lt;p&gt;I read it. I did not run it. &lt;code&gt;make build&lt;/code&gt; compiles a bundled Git and pulls a long list of Go modules, which is past what I run for a post. So I cloned it, cloned upstream Gitaly at the release its VERSION file names (19.5.0-rc3), and wrote three small Node scripts to compare the trees:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ node diffmap.js upstream repo
upstream files 2209 fork files 2826
same 704 changed 1446 added 676 removed 59
added go non-test lines 58555 added go test lines 90162
$ node normdiff.js upstream repo changed.txt
changed files 1446 module-path only 651 real edits 795
$ node rpcs.js upstream repo added.txt
upstream rpcs 156 fork rpcs 174 added 18 removed 0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So this is not a rename. Nearly half the "changed" files only differ by the Go module path, but there are 58,555 new lines of non-test Go (5,636 of them generated protobuf code), 18 new RPCs, and every upstream RPC still in place. The new code is a CLI called &lt;code&gt;graft&lt;/code&gt;, a cache daemon, a post-merge verifier, a review service, code search, and a set of monitoring agents.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Scion actually changed
&lt;/h2&gt;

&lt;p&gt;Scion's boldest move is in the data model. Its unit of work is a "changeset": a log entry listing the files that changed against the first parent, with pointers to their contents in blob storage. No packfiles, and no full copy of the repo anywhere, not even on the server. Anthropic updated its post on October 15 to credit Meta's Mononoke and its Bonsai data model, a day after a widely shared blog post laid out the overlap.&lt;/p&gt;

&lt;p&gt;That design answers Anthropic's scale: a main-branch history of almost 100 GB and tens of millions of CI checkouts a day. The post itself says Scion is no general replacement for git, and suggests git servers borrow its best parts instead.&lt;/p&gt;

&lt;p&gt;open_scion takes that advice.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the open version keeps
&lt;/h2&gt;

&lt;p&gt;Every repository is still a normal git repository, and a commit is still a git commit. In the optional database-backed storage mode, each write is stored as a pack in object storage: &lt;code&gt;internal/graft/corestore/types.go&lt;/code&gt; (lines 135 to 152) gives every stored changeset a &lt;code&gt;PackKey&lt;/code&gt;, of the form &lt;code&gt;repos/&amp;lt;id&amp;gt;/packs/&amp;lt;sha256&amp;gt;.pack&lt;/code&gt;. The packfile Scion got rid of is the storage unit here.&lt;/p&gt;

&lt;p&gt;"Changeset" also means something else. It is not a commit replacement. It is a record of a branch moving. The message in &lt;code&gt;proto/changeset.proto&lt;/code&gt; (lines 38 to 62), with the field comments shortened by me:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight protobuf"&gt;&lt;code&gt;&lt;span class="kd"&gt;message&lt;/span&gt; &lt;span class="nc"&gt;Changeset&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kt"&gt;uint64&lt;/span&gt; &lt;span class="na"&gt;sequence&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;      &lt;span class="c1"&gt;// position in the log, from 1, no gaps&lt;/span&gt;
  &lt;span class="kt"&gt;bytes&lt;/span&gt; &lt;span class="na"&gt;reference&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="na"&gt;old_oid&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="na"&gt;new_oid&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="na"&gt;operation&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;     &lt;span class="c1"&gt;// "land", "ff_branch", "commit_files", ...&lt;/span&gt;
  &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="na"&gt;actor&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="n"&gt;google.protobuf.Timestamp&lt;/span&gt; &lt;span class="na"&gt;recorded_at&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="na"&gt;rewritten&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;       &lt;span class="c1"&gt;// the server rebased or merged&lt;/span&gt;
  &lt;span class="k"&gt;repeated&lt;/span&gt; &lt;span class="n"&gt;ChangesetMember&lt;/span&gt; &lt;span class="na"&gt;members&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;9&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;h2&gt;
  
  
  What an agent gets that a commit does not
&lt;/h2&gt;

&lt;p&gt;Put a git commit next to that record and the gap shows.&lt;/p&gt;

&lt;p&gt;A commit's author is whatever the client typed. Here, in its standalone mode, a request made with an agent token is logged with that token's agent name as the actor, whatever user ID the request carries. &lt;code&gt;LIMITATIONS.md&lt;/code&gt; names the end-to-end test that pins it.&lt;/p&gt;

&lt;p&gt;A commit says nothing about how it reached main. The record does: fast-forwarded or rebased by the server, and for a batched land, each member's original commit next to the commit that actually landed, plus &lt;code&gt;resolved_paths&lt;/code&gt; when the server's conflict resolver stepped in. That resolver also writes &lt;code&gt;Graft-Resolved-By&lt;/code&gt; and &lt;code&gt;Graft-Resolved-Path&lt;/code&gt; trailers into the merge commit (&lt;code&gt;land_resolve.go&lt;/code&gt;, lines 403 to 405), so an AI-resolved conflict is labeled in history itself.&lt;/p&gt;

&lt;p&gt;And git keeps no shared, ordered story of branch moves at all. The reflog is per machine, and off by default in the bare repositories servers use. Here every accepted update gets the next sequence number, and &lt;code&gt;WatchChangesets&lt;/code&gt; streams from any cursor.&lt;/p&gt;

&lt;p&gt;That cursor is the spine of the project. The post-merge verifier follows it to decide what to test, bisect and revert. The monitoring agents (Anthropic's post calls them "superfans") follow it instead of server telemetry. &lt;code&gt;graft watch&lt;/code&gt; prints it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Landing without a queue
&lt;/h2&gt;

&lt;p&gt;The write side is &lt;code&gt;UserLand&lt;/code&gt;, one new RPC on the existing operations service. The server rebases your commit onto the current tip and moves the branch with a compare-and-swap. If another agent won the race, it backs off (2 ms, doubling up to 100 ms) and tries again, 20 times by default (&lt;code&gt;land.go&lt;/code&gt;, lines 32 to 40 and 100 to 133). Concurrent lands on one branch can be folded into one update, which is where &lt;code&gt;members&lt;/code&gt; comes from.&lt;/p&gt;

&lt;h2&gt;
  
  
  Built the way Scion was
&lt;/h2&gt;

&lt;p&gt;The phase plans in &lt;code&gt;doc/graft&lt;/code&gt; read like briefs for parallel coding agents: 61 numbered work packages across six phases, each told which files it owns, with some rules marked "added by Opus after gate 1". The first plan cites Anthropic's post. The repository's history starts on October 15 with a single commit holding the whole tree.&lt;/p&gt;

&lt;p&gt;One squashed commit hides the process, so I can't say how long it took. But the dates leave about two days, and the test count fits an agent-scale build: 2,116 test functions in the new files alone.&lt;/p&gt;

&lt;h2&gt;
  
  
  The limits
&lt;/h2&gt;

&lt;p&gt;From its own docs, and from my reading:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Reviews don't gate landing yet.&lt;/strong&gt; The review service sorts files into comment-only, docs, tests and code, and counts approvals, but &lt;code&gt;REVIEW.md&lt;/code&gt; says landing does not check reviews. That is a later phase.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The batching goal was missed.&lt;/strong&gt; &lt;code&gt;LIMITATIONS.md&lt;/code&gt; says the phase 4a acceptance criteria are not met: p99 latency with batching on came out worse than off at 50 and 100 agents.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The log can have gaps.&lt;/strong&gt; It is written from git's reference-transaction hook, and a recording error is logged, never fatal. Without the database mode it is a JSON Lines file per node, not replicated.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CI runs a slice.&lt;/strong&gt; The final commit's CI run is green, but by my count its package list covers 892 of those 2,116 tests. The server package that implements &lt;code&gt;UserLand&lt;/code&gt; is not in it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Issue #1 is a trap.&lt;/strong&gt; It is a "fast install" PowerShell one-liner that downloads and runs a remote script as admin, padded with one-line "works great" comments. The only real install is &lt;code&gt;make build&lt;/code&gt; from the repo.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The part to steal today
&lt;/h2&gt;

&lt;p&gt;You don't need a new VCS for the ledger. Git has had a reference-transaction hook since 2.28, and on your server it sees every ref update. That is exactly where open_scion writes its log (&lt;code&gt;referencetransaction.go&lt;/code&gt;, line 213). Append one line per update with a sequence number, the authenticated actor and the operation, and every bot you run can follow main by cursor instead of polling and diffing.&lt;/p&gt;

&lt;p&gt;The commit tells you what changed. The ledger tells you who moved the branch, how, and in what order. When most of your committers are agents, the second question is the one you end up asking.&lt;/p&gt;

&lt;p&gt;If an agent moved your main branch at 3 a.m., what could you prove about it from git alone?&lt;/p&gt;

</description>
      <category>ai</category>
      <category>opensource</category>
      <category>programming</category>
      <category>discuss</category>
    </item>
    <item>
      <title>98% less context, if the model writes the right script</title>
      <dc:creator>Ian</dc:creator>
      <pubDate>Sat, 17 Oct 2026 18:00:00 +0000</pubDate>
      <link>https://dev.to/ianwieds/98-less-context-if-the-model-writes-the-right-script-4mfn</link>
      <guid>https://dev.to/ianwieds/98-less-context-if-the-model-writes-the-right-script-4mfn</guid>
      <description>&lt;p&gt;I ran context-mode's own benchmark on my laptop and got its headline number to the byte: 314.9 KB of tool output in, 5,517 bytes into the context window. 98% saved.&lt;/p&gt;

&lt;p&gt;Then I pointed one of the same benchmark scripts at the project's real git history. It reported that the most common commit type was "update". There is no such type.&lt;/p&gt;

&lt;p&gt;Both results are true. The gap between them is the whole story.&lt;/p&gt;

&lt;h2&gt;
  
  
  The project
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/mksglu/context-mode" rel="noopener noreferrer"&gt;context-mode&lt;/a&gt;, by Mert Koseoğlu, is an MCP server plus hooks for coding agents: Claude Code, Codex, Cursor, Gemini CLI and more than a dozen others. It was on GitHub trending on Friday with 904 stars that day and just under 30,000 in total. It is source-available under the Elastic License 2.0; it launched as MIT and switched in March.&lt;/p&gt;

&lt;p&gt;The idea is sound. Instead of letting a tool dump a 56 KB page snapshot into the conversation, the agent writes a small script, context-mode runs it in a subprocess, and only what the script prints comes back. Big outputs get indexed into a local SQLite full-text search, so the model can query them later. There is also a session memory that survives compaction, which I did not test.&lt;/p&gt;

&lt;p&gt;The 98% opens the README, and it was the headline of a &lt;a href="https://news.ycombinator.com/item?id=47193064" rel="noopener noreferrer"&gt;Hacker News thread&lt;/a&gt; that reached 570 points in February. I wanted to know what it counts.&lt;/p&gt;

&lt;h2&gt;
  
  
  How I checked
&lt;/h2&gt;

&lt;p&gt;I did not install it. The npm postinstall and the server's startup both carry code that repairs Claude Code's plugin files, and I don't run that for a post. Instead I cloned the repo and loaded its executor and its search store straight from the TypeScript source, using Node 22's type stripping and a 12-line resolve hook, with &lt;code&gt;HOME&lt;/code&gt; and &lt;code&gt;TMPDIR&lt;/code&gt; pointed at a scratch folder.&lt;/p&gt;

&lt;p&gt;No agent and no model in the loop. Just the project's code, run on files I chose.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the 98% measures
&lt;/h2&gt;

&lt;p&gt;The number comes from &lt;code&gt;tests/ecosystem-benchmark.ts&lt;/code&gt;. It has 14 scenarios. Each one pairs a saved tool output (Context7 docs, a Playwright snapshot of Hacker News, 20 React issues, a test run, an nginx log) with a summary script written by hand for that file. Raw size is the file. Context size is whatever the script prints.&lt;/p&gt;

&lt;p&gt;This is the parsing half of the git log scenario's script:&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;lines&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;FILE_CONTENT&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;byType&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{};&lt;/span&gt;
&lt;span class="nx"&gt;lines&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;l&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;type&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;l&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt; &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="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;other&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;byType&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;type&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;byType&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;type&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So the 98% compares a file's size with what a script prints, where the script was written by someone who had already seen the file. It is a fair measure of how small a good summary can be. It does not show an agent doing the work.&lt;/p&gt;

&lt;p&gt;I ran the benchmark through the project's own executor, and every row matched &lt;code&gt;BENCHMARK.md&lt;/code&gt; to the byte.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the count leaves out
&lt;/h2&gt;

&lt;p&gt;In real use, the model writes that script, and the script is context too. It enters the conversation as the tool call. The current server also echoes it back: every &lt;code&gt;ctx_execute_file&lt;/code&gt; reply starts with the file path and the code in a fenced block (up to 2,000 characters), then the output. The benchmark counts neither.&lt;/p&gt;

&lt;p&gt;So I recounted the same 14 scenarios three ways, with the scripts' indentation stripped so I wasn't padding the total:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;                            bytes    saved
raw tool output (315 KB)  322,419
stdout only (the README)    5,517    98.3%
+ echoed path and code     16,916    94.8%
+ the code in the call     27,504    91.5%
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The scripts add up to 10,588 bytes, almost twice what they print. For the smallest fixture, a 400-byte list of network requests, the round trip costs 2,414 bytes, six times the thing it was meant to shrink. All of this is bytes, like the benchmark's own table; its token figures are bytes divided by four.&lt;/p&gt;

&lt;p&gt;91.5% is still a big saving. My point is not that the number is fake. It is the best case, measured after the hard part was already done.&lt;/p&gt;

&lt;h2&gt;
  
  
  The script is the hard part
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;BENCHMARK.md&lt;/code&gt; says every fixture was captured from a real tool run. Some clearly were: the React issues match GitHub, number for number. The git log does not look captured. 30 of its 31 days have exactly five commits, and four of its five authors are Alice Johnson, Bob Martinez, Charlie Kim and Diana Chen. Every author has a two-word name.&lt;/p&gt;

&lt;p&gt;That matters because of &lt;code&gt;split(" ")[4]&lt;/code&gt;. It assumes the fifth word of each line is the commit type, which only holds when the author's name is exactly two words.&lt;/p&gt;

&lt;p&gt;I ran the same script, unchanged, on context-mode's own history in the same line format (&lt;code&gt;git log --format='%h %ad %an %s' --date=short&lt;/code&gt;): 2,234 commits, 203 KB. The 107-byte summary from the benchmark became 7,071 bytes, still a 96.5% saving, and its first entries were wrong:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;By type: update:552, fix:168, Merge:125, docs:80, feat:66, ...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;550 commits come from &lt;code&gt;github-actions[bot]&lt;/code&gt;, a one-word name, so the script read "update" out of "ci: update install stats". A three-word name turned the author's last name into a commit type. In all, 679 commits (30%) have an author name that is not two words. Reading the type from the subject instead, ci has 568 commits and fix, counting its scoped forms, has 663.&lt;/p&gt;

&lt;p&gt;Nothing errored. The output looked like a summary. An agent reading it would have no reason to doubt it, because the 203 KB that would have exposed the mistake never entered its context. That is the trade: the savings come from the model not seeing the data, and so does the risk.&lt;/p&gt;

&lt;p&gt;The HN thread got there first. One commenter, hereme888, noted that the benchmarks assume the model always writes the right extraction script. Asked about quality, the author said they "haven't run formal benchmarks on answer quality yet."&lt;/p&gt;

&lt;h2&gt;
  
  
  The safety net, tried
&lt;/h2&gt;

&lt;p&gt;context-mode's answer to "what is lost" is its index. When you pass an &lt;code&gt;intent&lt;/code&gt; and the output is over 5 KB, it chunks the output into SQLite FTS5 and replies with only the best matching sections. The full text stays searchable.&lt;/p&gt;

&lt;p&gt;So I tried that path. I fetched 100 open React issues with &lt;code&gt;gh issue list --json&lt;/code&gt; (303 KB), indexed them with the project's own store, and rebuilt the reply the way &lt;code&gt;intentSearch&lt;/code&gt; in &lt;code&gt;server.ts&lt;/code&gt; builds it, for the intent "ViewTransition bugs".&lt;/p&gt;

&lt;p&gt;The reply was 1,667 bytes, a 99.4% saving. Eight of the hundred issues mention ViewTransition. None of their issue numbers appear in the reply. &lt;code&gt;gh&lt;/code&gt; prints its JSON as one line, so the chunker split it by bytes: all five matches are titled "Output", and each preview is a 120-character slice of raw JSON, most from the middle of an issue body.&lt;/p&gt;

&lt;p&gt;In fairness, the reply tells the model to call &lt;code&gt;ctx_search&lt;/code&gt; for the full sections, and a careful model would. That is another round trip, though, and the first reply alone answers almost nothing. A model that piped the JSON through &lt;code&gt;jq&lt;/code&gt; to keep only numbers and titles would have done far better. Same lesson: the quality lives in the code the model writes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where that leaves it
&lt;/h2&gt;

&lt;p&gt;I'd use the idea without the package. When a command's output is big, have the agent write it to a file and run a script over it, and make the script print a few raw sample lines next to its counts, so a wrong parse shows itself. And judge tools like this on tasks, not bytes: did the agent get the right answer, and what did that cost, counting the code it wrote?&lt;/p&gt;

&lt;p&gt;The limits of my check: I never ran context-mode inside an agent, so I can't say how often a real model writes a good script on the first try, or how the session memory holds up after compaction. The repo's GitHub issue tracker is turned off, so I couldn't read what users report there.&lt;/p&gt;

&lt;p&gt;When your agent summarizes output you never see, what do you check before you trust the summary?&lt;/p&gt;

</description>
      <category>ai</category>
      <category>programming</category>
      <category>opensource</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Turned into a neural net, my code learned every price and none of the rules</title>
      <dc:creator>Ian</dc:creator>
      <pubDate>Fri, 16 Oct 2026 18:00:00 +0000</pubDate>
      <link>https://dev.to/ianwieds/turned-into-a-neural-net-my-code-learned-every-price-and-none-of-the-rules-30dh</link>
      <guid>https://dev.to/ianwieds/turned-into-a-neural-net-my-code-learned-every-price-and-none-of-the-rules-30dh</guid>
      <description>&lt;p&gt;I gave a five-rule shipping function and its neural-net twin the same 16 million inputs. They never disagreed once.&lt;/p&gt;

&lt;p&gt;Then I taught the twin two new price lists. It nailed the first. On the second, it got every paid order wrong.&lt;/p&gt;

&lt;h2&gt;
  
  
  The thing everyone shared and nobody explained
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/chenglou/handwired" rel="noopener noreferrer"&gt;Handwired&lt;/a&gt;, by Cheng Lou, went public this week under the line "Turn your regular JavaScript code into a neural net." His announcement tweet promised no training, outputs exact against the JS, and full differentiability. The tweet passed 10,000 likes, and the repo counted 1,313 stars about a day and a half after it was created. MIT license, TypeScript, demos at &lt;a href="https://chenglou.me/handwired/" rel="noopener noreferrer"&gt;chenglou.me/handwired&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Hacker News was quieter. The top comment on the busiest submission (9 points), from user skyzoidbroczky, is "What's the point of this?" Two other comments say they don't get what it is for. Nobody in the thread says they tried it.&lt;/p&gt;

&lt;p&gt;So I did, on a function of my own.&lt;/p&gt;

&lt;h2&gt;
  
  
  The name is the instructions
&lt;/h2&gt;

&lt;p&gt;The core move fits in a couple of lines. A comparison becomes a pair of ReLUs, and an &lt;code&gt;if&lt;/code&gt; computes both branches and blends them with a 0-or-1 bit. Pile up enough of those and your program is a stack of weighted sums and ReLU products: a neural net whose weights you wrote instead of trained.&lt;/p&gt;

&lt;p&gt;What the tagline skips, the docs say outright: "No tool converts existing JavaScript source into a program." You translate by hand, in builder code that runs once and appends operations to a &lt;code&gt;Circuit&lt;/code&gt;. Hence the name.&lt;/p&gt;

&lt;p&gt;Ranges and grids are part of that translation too. The README's own example rewrites &lt;code&gt;(a &amp;gt; 4) ? 10 : 12&lt;/code&gt; as &lt;code&gt;relu(a - 4) - relu(a - 5)&lt;/code&gt; and notes it holds for an integer &lt;code&gt;a&lt;/code&gt;. I checked: at &lt;code&gt;a = 4.5&lt;/code&gt; the rewrite gives 11 while the ternary gives 10. The real API makes you declare each input's range and grid (whole cents, whole grams), then builds comparisons that are exact for every value you declared.&lt;/p&gt;

&lt;p&gt;The repo's tooling wants Bun, which I don't have. The runtime has no dependencies, though, and Node 22 loads its &lt;code&gt;.ts&lt;/code&gt; files directly, so I skipped the install and imported it from a scratch folder.&lt;/p&gt;

&lt;h2&gt;
  
  
  My function
&lt;/h2&gt;

&lt;p&gt;Here is the plain version, in integer cents:&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="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;shippingCents&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;subtotal&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;grams&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;member&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="nx"&gt;member&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;subtotal&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;5000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;subtotal&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;10000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;grams&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;499&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;grams&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mi"&gt;2000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;899&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;899&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ceil&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;grams&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;2000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;300&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And the heart of my translation. Every business number is a &lt;code&gt;constant&lt;/code&gt; cell, so the backward pass can reach it later:&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;free&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;or&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nf"&gt;and&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cells&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;member&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;emitCompare&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;cells&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;subtotal&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;&amp;gt;=&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;cells&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;memberFreeAt&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
  &lt;span class="nf"&gt;emitCompare&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;cells&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;subtotal&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;&amp;gt;=&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;cells&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;freeAt&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;light&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;emitCompare&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;cells&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;grams&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;&amp;lt;=&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;cells&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;lightMax&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;mid&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;emitCompare&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;cells&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;grams&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;&amp;lt;=&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;cells&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;midMax&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;// Math.ceil((grams - 2000) / 1000) as a count of thresholds passed&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;extraKg&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;affine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;passed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;bit&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;bit&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]))&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;heavy&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;affine&lt;/span&gt;&lt;span class="p"&gt;([[&lt;/span&gt;&lt;span class="nx"&gt;cells&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;midPrice&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;multiply&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;extraKg&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;cells&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;perKg&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]])&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;price&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;emitSelect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;free&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nf"&gt;emitSelect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;light&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;emitSelect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;mid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;heavy&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;cells&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;midPrice&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nx"&gt;cells&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;lightPrice&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nx"&gt;zero&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;Math.ceil&lt;/code&gt; was the only line that made me stop and think. A program like this can't loop or divide by a runtime count, so "how many started kilos past 2 kg" becomes eight comparisons whose bits get added up. The docs call that a threshold count.&lt;/p&gt;

&lt;h2&gt;
  
  
  What came out
&lt;/h2&gt;

&lt;p&gt;Fifty operations in 12 layers, holding 81 weights. The twelve comparisons account for 24 of those operations, two each, because whole cents and whole grams let every comparison take the cheap exact form.&lt;/p&gt;

&lt;p&gt;Then I checked it the boring way: every subtotal from 0 to $200 in 25-cent steps, every gram from 0 to 10 kg, member and not. That is 16,021,602 inputs. Zero mismatches against the plain function, across all 11 prices it can return.&lt;/p&gt;

&lt;p&gt;One thing surprised me. When I folded the ifs by hand (a light parcel is always under 2 kg, so the nested selects collapse into one sum), the same function became 37 live operations in 7 layers. Same answers on all 16 million inputs. In Handwired you are the compiler, and your algebra shows up directly as depth.&lt;/p&gt;

&lt;p&gt;It is not fast one call at a time. On my M1 Max, busy with other jobs, the compiled program took 10 to 17 seconds for those 16 million calls. The plain function took 60 milliseconds. That gap is roughly 175 to 275 times, and it is not really the pitch: the README's speed claim is for many evaluations at once on a GPU, as in its JSON search demo over 100,000 documents. I didn't run the GPU path.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the gradients go
&lt;/h2&gt;

&lt;p&gt;Now the "fully differentiable" part. I ran the backward pass on a $30 order weighing 3.5 kg, which costs $14.99:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;d price / d perKg = &lt;strong&gt;2&lt;/strong&gt;. Two started kilos past the first two, exactly.&lt;/li&gt;
&lt;li&gt;d price / d midPrice = &lt;strong&gt;1&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;d price / d every threshold = &lt;strong&gt;0&lt;/strong&gt;. The $50 member line, the $100 line, 500 g, 2 kg.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Every number that scales the answer gets a clean gradient. Every number that decides which branch runs gets nothing. The docs are upfront about this, too: a comparison passes back the derivative of a step, which is zero everywhere except a sliver narrower than one grid step.&lt;/p&gt;

&lt;h2&gt;
  
  
  Teaching it a new price list
&lt;/h2&gt;

&lt;p&gt;So I trained it, using the program's own gradients with plain Adam on 2,000 random orders, priced by a new list.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Run one: only prices change&lt;/strong&gt; (549, 949 and 350 per kilo). By step 100 the twin had learned exactly 549, 949 and 350. Zero of 2,000 orders wrong. That part is impressive: nothing told it which constant was the per-kilo rate, and the gradients found it anyway.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Run two: same prices, but free shipping now starts at $75.&lt;/strong&gt; The threshold never moved. It sat at $100 for 600 steps, gradient zero. Instead, training did the only thing it could: it dragged the base prices down (499 to 434.63, 899 to 792.88) and only nudged the per-kilo rate, to 313.87, to soften the error on orders that should now ship free. The mean squared error barely moved, from 245,000 to 243,380. Of the 769 orders it still charges for, it got none right: the same 769 it had wrong before training.&lt;/p&gt;

&lt;p&gt;It learned a worse price list to cover for a rule it couldn't see.&lt;/p&gt;

&lt;p&gt;That's the gap &lt;a href="https://github.com/chenglou/handwired/issues/1" rel="noopener noreferrer"&gt;issue #1&lt;/a&gt; goes after. Its author estimates a gradient for each decision near its flip point. In their two-column layout test, the plain backward pass leaves a breakpoint at 500, and their method moves it to 699.9 against a target of 720, which fits every example. It lives on a fork for now.&lt;/p&gt;

&lt;h2&gt;
  
  
  So what is the point
&lt;/h2&gt;

&lt;p&gt;Here is my answer to that HN comment. Translating code into a net sorts its numbers into two piles. Numbers that scale (prices, margins, sizes, spring constants) become weights you can fit from examples. Numbers that decide (thresholds, breakpoints) stay exact and frozen.&lt;/p&gt;

&lt;p&gt;That second pile is a feature as much as a limit. A rule you don't want a gradient to bend can't be bent, while the constants around it can be tuned. The repo's learned gallery demo works this way: a small network inside a layout program, its coefficients trained with the program's own gradients.&lt;/p&gt;

&lt;p&gt;The limits I hit: you translate by hand, ranges and grids are on you, everything is 32-bit floats (fine for integer cents, risky for float dollars), and the repo's tests need Bun, so I didn't run them.&lt;/p&gt;

&lt;p&gt;Which numbers in your own code would you hand to a gradient, and which would you never let one touch?&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>ai</category>
      <category>programming</category>
      <category>opensource</category>
    </item>
    <item>
      <title>A second machine finds every bug the first one hid</title>
      <dc:creator>Ian</dc:creator>
      <pubDate>Wed, 14 Oct 2026 19:00:00 +0000</pubDate>
      <link>https://dev.to/ianwieds/a-second-machine-finds-every-bug-the-first-one-hid-22ok</link>
      <guid>https://dev.to/ianwieds/a-second-machine-finds-every-bug-the-first-one-hid-22ok</guid>
      <description>&lt;p&gt;This one covers October 6 to 14. Most of the week went into a change that sounds small: my setup stopped being one Mac. It is now two Macs and a Windows desktop, plus a cloud session, and every tool I own had to stop assuming it was alone. The rest went into my workflow kit catching up, a marketplace build, and a new daily post.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three machines, one roster file
&lt;/h2&gt;

&lt;p&gt;My dotfiles repo (private, so no link) sets up a whole machine with one script. Until this week, "the machine" meant one Mac. Now a committed file, &lt;code&gt;machines.json&lt;/code&gt;, lists every machine: an id, a name, the platform, the host to dial, the user, and that machine's public SSH key. Setup adds the machine's own entry, and on a Mac it trusts every other entry's key.&lt;/p&gt;

&lt;p&gt;Two commands sit on top of the roster: &lt;code&gt;win&lt;/code&gt; and &lt;code&gt;mac&lt;/code&gt;. They are one bash script behind two symlinks, and the name you call it by picks the target. &lt;code&gt;win git pull&lt;/code&gt; runs in the repo's clone on the Windows desktop; &lt;code&gt;mac npm test&lt;/code&gt; runs on the other Mac. Changes travel by git, because every target holds a clone at the same path under its own home. A &lt;code&gt;cp&lt;/code&gt; subcommand covers the one file git never carries.&lt;/p&gt;

&lt;p&gt;The roster turned up its own bugs right away:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A Mac's id and host now come from its Bonjour name (the &lt;code&gt;.local&lt;/code&gt; one), so the entry matches the name SSH actually dials.&lt;/li&gt;
&lt;li&gt;The Windows scheduled tasks now run with the S4U logon type. That lets a task run when nobody is signed in, without storing a password.&lt;/li&gt;
&lt;li&gt;The ssh session forwarded my shell's locale to the other Mac, which had no such locale, and perl panicked. The &lt;code&gt;mac&lt;/code&gt; command now sends none.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The part I use most is the sign-in mirror. A launchd job reads my Claude Code sign-in from the Mac's Keychain every 15 minutes and sends it to the other machines. Windows gets the credentials file directly. The other Mac gets a drop folder, and its own agent applies the sign-in from there. A digest per target makes an unchanged run send nothing. I sign in once, and every machine follows.&lt;/p&gt;

&lt;h2&gt;
  
  
  A cloud session with no GitHub yet
&lt;/h2&gt;

&lt;p&gt;The fourth "machine" is a Claude Code cloud environment. Its setup script clones my dotfiles and runs the Claude part of setup, so a cloud session gets the same hooks and skills as my desk.&lt;/p&gt;

&lt;p&gt;The catch: the setup script runs before the session's GitHub access exists, and the repo is private. So the environment carries a fine-grained token with one permission, read-only Contents on that one repo. The detail I like is how git gets it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s1"&gt;'credential.https://github.com/.helper=!f() { echo username=x-access-token; echo "password=$DOTFILES_CLOUD_TOKEN"; }; f'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  clone https://github.com/&amp;lt;owner&amp;gt;/&amp;lt;repo&amp;gt;.git &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$R&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The single quotes matter. The command line carries the variable's NAME, and only the helper's own shell expands it. So the token never shows up in the process list, never lands in a file, and never reaches &lt;code&gt;.git/config&lt;/code&gt;. Pass a token by its name, never by its value.&lt;/p&gt;

&lt;p&gt;Two more rules came out of it. The setup script always exits 0, because a failing setup script blocks the session from starting; it logs to a file instead. And the plugin repos it clones next get no credential at all, so they must be public.&lt;/p&gt;

&lt;h2&gt;
  
  
  workkit: the second machine found the bugs
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/itw-creative-works/workkit" rel="noopener noreferrer"&gt;workkit&lt;/a&gt; is my workflow kit for Claude Code: GitHub issues as the work list, labels as the pipeline, hooks that enforce the rules. It shipped eight versions this week, 0.73.0 to 0.74.4. Almost every fix is a bug that one machine could never show.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The repo list.&lt;/strong&gt; The kit publishes the list of repos it manages to a home repo. Each machine's publish replaced the whole list, so the second Mac erased the first one's repos. Now the file holds one entry per machine, and the list is their sorted union.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The disk is not the truth.&lt;/strong&gt; The publish copied folders from disk. An uncommitted edit on one Mac reached the home repo, then flipped back when the other Mac published. Every publish, heal, and &lt;code&gt;workkit doctor&lt;/code&gt; check now reads the committed tree (&lt;code&gt;HEAD&lt;/code&gt;), never the working folder.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Line endings.&lt;/strong&gt; On Windows the heal appends a job to a &lt;code&gt;checks.yml&lt;/code&gt; with CRLF endings. Its read of the file's last key tripped on the trailing carriage return. It now reads past the &lt;code&gt;\r&lt;/code&gt; and writes new lines in the file's own ending.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Commits.&lt;/strong&gt; A local session now commits straight to the default branch. A pull request is for a cloud agent or an outside contributor.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The common thread: with one machine, "what is on my disk" and "what is committed" look like the same thing. A second machine splits them, and every shortcut that read the disk becomes a bug.&lt;/p&gt;

&lt;h2&gt;
  
  
  Suponsa: a site sells only once its owner proves it
&lt;/h2&gt;

&lt;p&gt;Suponsa is a marketplace for guest post and link insertion slots (private, no live site yet). Last week it had a submission flow. This week the marketplace itself landed: seller tools, payouts, and a migration of the slots my company's sites already sold. Three rules came with it.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Prove the site.&lt;/strong&gt; A seller gets a token and puts it either in &lt;code&gt;/.well-known/suponsa-verify.txt&lt;/code&gt; or in a &lt;code&gt;suponsa-verify&lt;/code&gt; meta tag. The file check follows no redirect; the meta tag check follows one. Changing a verified site's domain resets the proof, and a site that is selling cannot change its domain at all.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Deliver with an address.&lt;/strong&gt; Marking a post delivered now requires its live URL: https only, no whitespace, at most 2,048 characters. If the publish step rejects the address, the submission stays at paid, so a retry never publishes the post twice.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Nothing waits forever.&lt;/strong&gt; Each submission records when it entered its current status. A daily job cancels one left 10 days at submitted, 14 at changes requested, or 7 at approved, and emails both sides.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;CI taught the humbling lesson. The backend tests boot the Firebase emulators, and that stack did not survive a hosted runner in five tries. Four runs each hit an environment gap (Java 21 was one), and the fifth lost the runner 48 minutes in. For now CI runs the web suite alone, and the backend suite runs in the local commit gate on every commit.&lt;/p&gt;

&lt;h2&gt;
  
  
  Small but satisfying
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A daily post.&lt;/strong&gt; I started posting a short daily on dev.to: one open-source project, studied and, where it is safe, run. Covers are rendered from a JSON plan by my own cover engine, never stock art. One API lesson: tags set at upload are NOT replaced by &lt;code&gt;tags&lt;/code&gt; in the front matter of a later edit. Send a &lt;code&gt;tags&lt;/code&gt; array in the update body.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Status in every summary.&lt;/strong&gt; workkit's summaries now put an issue's status right after its link (&lt;code&gt;[qa]&lt;/code&gt;, &lt;code&gt;[closed]&lt;/code&gt;), and an open issue with no status label shows &lt;code&gt;[unlabeled]&lt;/code&gt; until someone labels it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Next week the cloud environment gets its first scheduled job, and Suponsa's backend CI gets another try. If you run your tools on more than one machine, I would like to hear which assumption broke first for you. Comments open.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>programming</category>
      <category>productivity</category>
      <category>devops</category>
    </item>
  </channel>
</rss>
