<?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: Michael Tuszynski</title>
    <description>The latest articles on DEV Community by Michael Tuszynski (@michaeltuszynski).</description>
    <link>https://dev.to/michaeltuszynski</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%2F1447774%2Fa99eea93-7845-4764-9fce-b1755bcfa456.png</url>
      <title>DEV Community: Michael Tuszynski</title>
      <link>https://dev.to/michaeltuszynski</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/michaeltuszynski"/>
    <language>en</language>
    <item>
      <title>Every FDE Pod Has a Strategist. None Has a Grader.</title>
      <dc:creator>Michael Tuszynski</dc:creator>
      <pubDate>Tue, 13 Oct 2026 14:31:07 +0000</pubDate>
      <link>https://dev.to/michaeltuszynski/every-fde-pod-has-a-strategist-none-has-a-grader-1g3f</link>
      <guid>https://dev.to/michaeltuszynski/every-fde-pod-has-a-strategist-none-has-a-grader-1g3f</guid>
      <description>&lt;p&gt;Gartner forecasts that &lt;a href="https://www.theregister.com/ai-and-ml/2026/09/30/7-in-10-enterprises-expected-to-abandon-vendor-built-agentic-ai-by-2028/5300021" rel="noopener noreferrer"&gt;70% of enterprises will abandon agentic AI built by vendor forward-deployed engineers by 2028&lt;/a&gt;, in a September 29 release reported the next day by The Register and Channel Dive. The firm calls the pattern "FDE washing." Its analyst &lt;a href="https://www.channeldive.com/news/can-forward-deployed-engineers-fdes-fix-ai-gartner/831816/" rel="noopener noreferrer"&gt;counts about 2,000 active FDEs against roughly four times that demand&lt;/a&gt;, and Gartner estimates that &lt;a href="https://www.channeldive.com/news/can-forward-deployed-engineers-fdes-fix-ai-gartner/831816/" rel="noopener noreferrer"&gt;FDE consulting fees alone could top $200,000 per quarter per use case&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Gartner's stated mechanism is dependency and exit: clients end up unable to run or leave what the vendor built. How the pod is designed is not part of its analysis. This post argues the dependency has a structural cause visible in the published FDE pods. The pods that name their roles each have a strategist, and none has a grader. No seat checks the work against criteria the client agreed to, held by someone who had no hand in building it. That link is this post's argument. Gartner doesn't make it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The pod is already the standard delivery model
&lt;/h2&gt;

&lt;p&gt;The lone engineer embedded at a client is mostly a recruiting image. The published delivery unit is a pod, and the pods look alike.&lt;/p&gt;

&lt;p&gt;Palantir pairs the Delta (the FDE) with an Echo (a deployment strategist) and Devs on the product side. One alumnus describes FDEs &lt;a href="https://nabeelqu.substack.com/p/reflections-on-palantir" rel="noopener noreferrer"&gt;going on site at the customer three to four days a week&lt;/a&gt;. Deloitte's lead-FDE postings describe &lt;a href="https://apply.deloitte.com/en_US/careers/JobDetail/Lead-Anthropic-Forward-Deployed-Engineer-GPS/353534" rel="noopener noreferrer"&gt;pods of two to five onshore-anchored, offshore-supported engineers&lt;/a&gt;. Thoughtworks' principal-FDE posting describes a pod of one product strategist, one experience designer and two forward-deployed engineers. AWS's billion-dollar FDE organization deploys &lt;a href="https://thenextweb.com/news/aws-1bn-forward-deployed-engineers-enterprise-ai" rel="noopener noreferrer"&gt;pods of five or six people, embedded inside a single customer at a time&lt;/a&gt;. Anthropic's own postings pair FDEs with a technical deployment lead who won't write production code and owns the roadmap the engineers execute against. The shape repeats: one or two engineers, a lead who owns scope and the executive relationship, sometimes a designer, and offshore or platform support behind them. Postings change, so the specifics will drift. The shape hasn't.&lt;/p&gt;

&lt;p&gt;The supply side is about to scale this template. Anthropic's &lt;a href="https://www.anthropic.com/news/claude-frontier-academy" rel="noopener noreferrer"&gt;Claude Frontier Academy&lt;/a&gt; commits $100M to train 10,000 "Frontier Deployed Engineers" by the end of 2027 at partners including Accenture, Deloitte, McKinsey and Morgan Stanley. Accenture's statement says those FDEs will work "inside client workflows as part of multidisciplinary teams." If the partners follow their own postings, those teams will look like the ones above.&lt;/p&gt;

&lt;h2&gt;
  
  
  Every seat in the pod helped build the thing
&lt;/h2&gt;

&lt;p&gt;Read those pod descriptions for a quality seat and you won't find one. None of them names a QA seat, an acceptance seat or a grading seat. The closest is a mention of "quality gates" in Deloitte's posting.&lt;/p&gt;

&lt;p&gt;The strategist seat is already standard. Palantir calls it the Echo. Other firms call it a deployment lead, a product strategist or a PM. Pods don't need anyone to invent it. And every seat in every pod is a builder. Engineers write the code. The strategist sequences the roadmap. The designer shapes the workflow. Platform and offshore support keep it running. Each person present has a stake in the result passing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Verification gets dropped because nobody is waiting on it
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://www.mpt.solutions/forward-deployed-engineer-four-different-things/" rel="noopener noreferrer"&gt;earlier post on the FDE title&lt;/a&gt; broke the role into separate jobs. Here's the version that matters for pods. The role has four jobs: build the system, redesign the process around it, run the client relationship, and verify the result. A pod splits the first three across seats. The fourth goes unassigned.&lt;/p&gt;

&lt;p&gt;It gets dropped inside a pod for the same reason a lone engineer drops it. No one is waiting on the grade. Shipping has a demo date, and the executive relationship has a steering meeting. A verification report has no audience until something breaks. Everyone in the room also wants the work to pass, including the vendor's lead, whose quarterly fee renews on the impression that it did.&lt;/p&gt;

&lt;p&gt;Follow that to the client's side. Without an independent grade against agreed criteria, the client can't tell what it bought. Without a sponsor who has authority to stop the work (the one seat &lt;a href="https://www.mpt.solutions/gartner-40-percent-agentic-ai-canceled-zombie-projects/" rel="noopener noreferrer"&gt;the kill sheet from the earlier post&lt;/a&gt; insists on), the client can't leave without losing whatever value is in there. It can't separate the working parts from the broken parts, so it keeps paying the only people who know. That is dependency, and that's what Gartner is forecasting.&lt;/p&gt;

&lt;h2&gt;
  
  
  The missing seat grades and does nothing else
&lt;/h2&gt;

&lt;p&gt;Call it the acceptance owner. The definition is narrow on purpose:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Co-writes testable acceptance criteria with the client sponsor before work starts&lt;/li&gt;
&lt;li&gt;Grades the outcome against those criteria after every change&lt;/li&gt;
&lt;li&gt;Publishes the grade to the sponsor, not to the pod first&lt;/li&gt;
&lt;li&gt;Had no hand in building the work&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The seat can sit on either side of the contract. A mature client's own QA function can hold it. A third party can. A vendor employee from outside the delivery org can, if the client accepts that. What disqualifies someone is having built the work. Which side signs their paycheck matters less.&lt;/p&gt;

&lt;p&gt;Here's the cadence relative to the pod:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Week&lt;/th&gt;
&lt;th&gt;Pod&lt;/th&gt;
&lt;th&gt;Acceptance owner&lt;/th&gt;
&lt;th&gt;Sponsor&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;Scopes the use case&lt;/td&gt;
&lt;td&gt;Co-writes criteria with sponsor; each one testable pass/fail&lt;/td&gt;
&lt;td&gt;Signs criteria and stop conditions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1-5&lt;/td&gt;
&lt;td&gt;Builds, demos weekly&lt;/td&gt;
&lt;td&gt;Re-runs the criteria after each merged change; logs pass/fail&lt;/td&gt;
&lt;td&gt;Reads the grade before the demo&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;Mid-sprint review&lt;/td&gt;
&lt;td&gt;Publishes trend: criteria passing, failing, untested&lt;/td&gt;
&lt;td&gt;Decides continue, re-scope or stop&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7-12&lt;/td&gt;
&lt;td&gt;Builds, hardens&lt;/td&gt;
&lt;td&gt;Same loop; adds regression on previously passed criteria&lt;/td&gt;
&lt;td&gt;Reads the grade weekly&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;13&lt;/td&gt;
&lt;td&gt;Quarter close, renewal&lt;/td&gt;
&lt;td&gt;Final grade against week-0 criteria, unchanged unless sponsor re-signed&lt;/td&gt;
&lt;td&gt;Renews or exits on the grade&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Each seat also has one thing it must not do. The engineer doesn't grade their own work. The strategist can write down what done means but doesn't own the grade. The sponsor never hands the stop decision to someone else. The acceptance owner's rule is the least obvious one: don't suggest fixes. The moment the grader proposes the fix, the grader has helped build it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Owning the criteria doesn't make the grade independent
&lt;/h2&gt;

&lt;p&gt;The strongest objection is that the pod already has this. Palantir's Echo, or a deployment lead who owns the roadmap, writes down what done means.&lt;/p&gt;

&lt;p&gt;That person owns the criteria. They also own the scope, the sequencing and the executive relationship, and they answer for whether the engagement renews. When a criterion half-passes in week 11, the person who wrote the roadmap is also the person who explains to the sponsor why it's close enough. The objection fails on that mechanism. Authorship and grading sit in the same chair, and that chair is the one with the most riding on renewal.&lt;/p&gt;

&lt;p&gt;The second objection: the client's own QA team can do this. Often true. When it can, that team &lt;em&gt;is&lt;/em&gt; the seat, provided it didn't help build the system. The test is simple. If the client's QA engineers pair with the FDEs during the build, they've crossed over, and someone else has to grade.&lt;/p&gt;

&lt;h2&gt;
  
  
  A fractional grader costs about 2.5% to 7.5% of a $200K quarter
&lt;/h2&gt;

&lt;p&gt;The acceptance owner doesn't need to be full time. The work is a criteria workshop up front, a re-run after each change, and a weekly report. That fits in roughly 0.1 to 0.2 of a seat.&lt;/p&gt;

&lt;p&gt;Illustrative pricing, using Gartner's $200,000-a-quarter estimate:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Pod fee: $200,000 per quarter for one use case&lt;/li&gt;
&lt;li&gt;Pod of four seats (two engineers, strategist, designer): $50,000 per seat per quarter&lt;/li&gt;
&lt;li&gt;Acceptance owner at 0.1 seat: $5,000 per quarter, or 2.5% of the fee&lt;/li&gt;
&lt;li&gt;At 0.15 seat: $7,500 per quarter, or 3.75%&lt;/li&gt;
&lt;li&gt;At 0.2 seat: $10,000 per quarter, or 5%&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;An experienced independent grader may bill above the pod's average seat. Double the rate and 0.15 of a seat costs $15,000 a quarter, or 7.5%.&lt;/p&gt;

&lt;p&gt;Now price it against abandonment. A use case run for four quarters at $200,000 costs $800,000. The grader at 0.15 seat costs $30,000 for the same year. If an independent grade ends a failing engagement just one quarter early, it saves $200,000. That covers the seat at $7,500 a quarter for about 26 quarters, more than six years.&lt;/p&gt;

&lt;p&gt;The cost of this advice goes beyond the invoice. A grader will fail work the pod considers done. Demos get awkward. Renewals get harder for the vendor. The pod loses some of its speed, because "shipped" now has to mean "passed."&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the seat doesn't fit
&lt;/h2&gt;

&lt;p&gt;An 8-week proof of concept often can't carry any extra seat, fractional or not. The fix there is cheaper: write the criteria at week 0 and have the sponsor grade at week 8 personally.&lt;/p&gt;

&lt;p&gt;Clients who won't name a sponsor break the model entirely. A grade with no one empowered to act on it is a report nobody reads.&lt;/p&gt;

&lt;p&gt;The onshore-anchored, offshore-supported pod is the hardest case. On paper it's five engineers. In practice one engineer can end up alone at the client, with the rest of the pod a time zone away. That engineer is the whole on-site build team and the only witness to whether it works. A pod like that needs a grader most and is least likely to have budget for one.&lt;/p&gt;

&lt;p&gt;Go back to the criterion that half-passes in week 11. With an acceptance owner, it doesn't get explained in a steering meeting. It shows up in the weekly grade as a fail, with the week-0 wording next to it, and it shows up again in week 12. At week 13 the sponsor holds a sheet with every signed criterion marked pass or fail. That sheet is the seat every published pod is missing. If that line is still red, the sponsor decides whether the next $200,000 buys a fix or funds the exit.&lt;/p&gt;

</description>
      <category>aiagents</category>
      <category>forwarddeployedengineers</category>
      <category>aigovernance</category>
      <category>enterpriseai</category>
    </item>
    <item>
      <title>30% More Code, Zero More Shipped Work: Where the Capacity Actually Went</title>
      <dc:creator>Michael Tuszynski</dc:creator>
      <pubDate>Mon, 12 Oct 2026 16:29:30 +0000</pubDate>
      <link>https://dev.to/michaeltuszynski/30-more-code-zero-more-shipped-work-where-the-capacity-actually-went-3nkg</link>
      <guid>https://dev.to/michaeltuszynski/30-more-code-zero-more-shipped-work-where-the-capacity-actually-went-3nkg</guid>
      <description>&lt;p&gt;A Harvard study of 718 firms found that coding agents produced 30% more lines of code and no detectable increase in finished work. Most coverage stops there and reads the zero as a verdict on the tools. The paper's own review data points somewhere else. My reading of that data, and it's my inference rather than the paper's stated finding, is that the extra output was real and the review stage absorbed it. That's an org-design failure, and you can look for it in your own pull request history this week.&lt;/p&gt;

&lt;h2&gt;
  
  
  The extra code piled up at the review stage
&lt;/h2&gt;

&lt;p&gt;Fiona Chen and James Stratton built &lt;a href="https://fion.ac/jellyfish.pdf" rel="noopener noreferrer"&gt;the study&lt;/a&gt; on Jellyfish data covering 718 firms and about 300 million work events from January 2021 to March 2026. Jellyfish sells engineering analytics. A finding that work stalls somewhere measurable isn't against its commercial interest, so weigh the source with that in mind. The analysis itself comes from academic economists, and the headline result is not the one an AI vendor would pick.&lt;/p&gt;

&lt;p&gt;After teams adopted coding agents, the activity numbers went up. Lines of code rose 30%. Commits rose 20%. Pull requests rose 23%.&lt;/p&gt;

&lt;p&gt;Jira issue and epic resolution showed no change the authors could distinguish from zero. Their estimate rules out an output gain above 12%.&lt;/p&gt;

&lt;p&gt;The review numbers explain the gap. In &lt;a href="https://fion.ac/jellyfish.pdf" rel="noopener noreferrer"&gt;the same paper&lt;/a&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;PR review time rose 49%.&lt;/li&gt;
&lt;li&gt;The share of PRs with changes requested nearly doubled.&lt;/li&gt;
&lt;li&gt;Comments per PR rose 35%.&lt;/li&gt;
&lt;li&gt;The share of workers doing review rose 14%.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Put the two lists side by side. More code went in at the top. Review time, the changes-requested rate and comments per PR all rose, even with 14% more people doing review. Resolved tickets came out the bottom flat. The paper reports these as separate effects and doesn't claim one caused the other. My inference from the pattern: the capacity didn't vanish. It turned into review load.&lt;/p&gt;

&lt;h2&gt;
  
  
  Review load grew faster than PR count
&lt;/h2&gt;

&lt;p&gt;The paper estimates each effect separately, so stacking them is my illustration, not a result from the paper. But stacking them shows why a 23% bump in PRs can stall a team.&lt;/p&gt;

&lt;p&gt;Take an illustrative 20-person team with 6 regular reviewers, 200 merged PRs a month, a 10-hour median review time, 4 comments per PR, and 15% of PRs sent back with changes requested. Apply the paper's effects:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;                          before     after      change
PRs / month                  200       246       +23%
Review time (hours)         10.0      14.9       +49%
PR-hours awaiting review   2,000     3,665       +83%   (1.23 x 1.49)
Comments / PR                4.0       5.4       +35%
Total comments               800     1,328       +66%   (1.23 x 1.35)
Reviewers                    6.0      6.84       +14%
Comments per reviewer        133       194       +46%   (1.66 / 1.14)
Changes-requested rate       15%       30%       ~2x
PRs needing a second pass     30        74
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;PR count rose 23%. PR-hours spent awaiting review rose 83%. That's elapsed time in the queue, not reviewer effort, but it's time finished work sits unshipped. Even with 14% more people reviewing, each reviewer handles roughly 46% more comment traffic. Every changes-requested PR goes back through the queue, and the illustrative team goes from 30 second passes a month to 74.&lt;/p&gt;

&lt;p&gt;That's the mechanism as I read it. Agents raised the rate at which work arrives at review. Nobody raised the rate at which review clears it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Headcount didn't move, and that's the CFO's problem
&lt;/h2&gt;

&lt;p&gt;The paper's headcount results are where the budget conversation should start. Total headcount showed a precise zero effect, ruling out a decline above 2.9%. Engineering headcount showed a noisier zero, ruling out a decline above 13.8%.&lt;/p&gt;

&lt;p&gt;A CFO who funds agent licenses expecting to shrink the engineering line didn't get that in this data. My argument, which the paper doesn't test directly: what the money bought was a review backlog. Buying more seats pushes more work into a queue that's already behind.&lt;/p&gt;

&lt;h2&gt;
  
  
  Measure where your capacity went from PR data
&lt;/h2&gt;

&lt;p&gt;You don't need Jellyfish to check whether your team looks like the paper. The GitHub API has what you need. This script pulls merged PRs, their reviews and their timelines, and prints three numbers per month: time to first review, changes-requested rate, and reviewer share. Time to first review starts at the last ready-for-review event (or PR creation, if the PR never sat in draft) and stops at the first submitted review.&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;json&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;subprocess&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;collections&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;defaultdict&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;statistics&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;median&lt;/span&gt;

&lt;span class="n"&gt;REPO&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;argv&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="c1"&gt;# e.g. your-org/your-repo
&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;gh&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;raw&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;subprocess&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gh&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;api&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--paginate&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--slurp&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
                         &lt;span class="n"&gt;capture_output&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;check&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;stdout&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;item&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;ts&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fromisoformat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Z&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;+00:00&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;review_start&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pr&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;events&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;gh&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;repos/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;REPO&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/issues/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;pr&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;number&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/timeline?per_page=100&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;ready&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;ts&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;created_at&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;events&lt;/span&gt;
             &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;e&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;event&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ready_for_review&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ready&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;ready&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="nf"&gt;ts&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pr&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;created_at&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;

&lt;span class="n"&gt;months&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;defaultdict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;lambda&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;prs&lt;/span&gt;&lt;span class="sh"&gt;"&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cr&lt;/span&gt;&lt;span class="sh"&gt;"&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;hours&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt;
                              &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;authors&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;reviewers&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;()})&lt;/span&gt;

&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;pr&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;gh&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;repos/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;REPO&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/pulls?state=closed&amp;amp;per_page=100&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&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;pr&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;merged_at&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
        &lt;span class="k"&gt;continue&lt;/span&gt;
    &lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;months&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;pr&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;merged_at&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][:&lt;/span&gt;&lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;]]&lt;/span&gt;
    &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;prs&lt;/span&gt;&lt;span class="sh"&gt;"&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="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;pr&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
        &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;authors&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pr&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;login&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="n"&gt;reviews&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;gh&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;repos/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;REPO&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/pulls/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;pr&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;number&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/reviews?per_page=100&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;start&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;review_start&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pr&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;submitted&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sorted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;ts&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;submitted_at&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;reviews&lt;/span&gt;
                       &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;r&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;submitted_at&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="nf"&gt;ts&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;submitted_at&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;start&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;submitted&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;hours&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;submitted&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="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;start&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;total_seconds&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;3600&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;any&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;state&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CHANGES_REQUESTED&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;reviews&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cr&lt;/span&gt;&lt;span class="sh"&gt;"&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="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;reviewers&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;login&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;reviews&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;

&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;month&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;sorted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;months&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;months&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;month&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;people&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;authors&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;reviewers&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;wait&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;median&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;hours&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="mf"&gt;6.1&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;hours&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;   n/a&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;month&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;  prs=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;prs&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;  &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
          &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;first_review_h=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;wait&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;  &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
          &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;changes_requested=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;cr&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;prs&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;  &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
          &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;reviewer_share=&lt;/span&gt;&lt;span class="si"&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;m&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;reviewers&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&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;people&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A few caveats. It makes two API calls per PR, so run it on one repo at a time or you'll hit rate limits. PRs merged with no review drop out of the timing median. Reviewer share here means reviewers divided by everyone active in the repo that month, not total headcount. And time to first review is my proxy. It isn't the paper's PR review time metric, which comes from Jellyfish's own definitions, so don't compare your hours to anything in the paper. Compare your percentage change across the rollout to the paper's 49%.&lt;/p&gt;

&lt;p&gt;Draw a line at your agent rollout month and read the pattern. If PR count and changes-requested rate both climbed while reviewer share stayed flat, you have the paper's queue. If reviewer share rose and time to first review still climbed, you added reviewers without shrinking the work each one sees. Pair this with the 30-day rework measure from &lt;a href="https://www.mpt.solutions/ai-coding-dashboard-counts-the-wrong-thing/" rel="noopener noreferrer"&gt;the earlier post on AI coding dashboards&lt;/a&gt;, because every fix below trades review time against escaped defects.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fix review capacity and merge policy before buying more seats
&lt;/h2&gt;

&lt;p&gt;Start by making review planned work. A reviewer rotation on the sprint board, with hours attached, stops review from being the thing people squeeze in after their "real" tickets. Those hours come out of feature work, and the roadmap will show it. That's the honest price of clearing the queue.&lt;/p&gt;

&lt;p&gt;Merge policy is the second lever. Tier the approval rules: changes in low-risk paths that pass tests merge on one approval, while auth, payments and data migrations keep two. You'll get more escaped defects in the low-risk tier. That's why the rework number has to sit next to the speed number on the same dashboard.&lt;/p&gt;

&lt;p&gt;Then shrink the unit of review. Cap diff size and tell your agents to split work into reviewable pieces. My argument here, not a measured result: a 400-line agent PR gets skimmed in a way that four 100-line PRs don't. The cost is more review events, each with its own context switch, plus tooling for stacked PRs.&lt;/p&gt;

&lt;p&gt;This doesn't transfer everywhere. On a four-person startup where review is a glance, the constraint is probably upstream: unclear tickets, slow product decisions. Agents there produce code faster against specs nobody agreed on, and no review fix helps. Jira resolution is also an imperfect output measure. Teams that adopt agents may start writing bigger issues, which would hide gains in the ticket count. The review numbers don't depend on how tickets are sized, though, and they moved.&lt;/p&gt;

&lt;h2&gt;
  
  
  Newer agents raise the arrival rate, not the review rate
&lt;/h2&gt;

&lt;p&gt;The strongest objection: the data ends in March 2026, and today's agents are better. Maybe the stall was a property of weaker tools.&lt;/p&gt;

&lt;p&gt;Look at where the stall happened. A better agent writes more code, and more of it is plausible. That raises the arrival rate at review. It does nothing for review throughput unless the agent's output needs fewer changes or the review itself gets automated.&lt;/p&gt;

&lt;p&gt;That can work, and the script above will tell you. If changes-requested rate falls after a model upgrade and 30-day rework doesn't rise, the newer agent is clearing the queue. If PR count jumps again and review time follows, you got the March 2026 result with a faster input.&lt;/p&gt;

&lt;h2&gt;
  
  
  A hidden 12% gain still leaves most of the extra volume in the queue
&lt;/h2&gt;

&lt;p&gt;The second objection is statistical. "No detectable change" only rules out gains above 12%. A real gain of 5% or 10% could be sitting inside the confidence interval.&lt;/p&gt;

&lt;p&gt;Concede it. Then do the arithmetic. PRs rose 23%. At the 12% ceiling, PRs per resolved issue still rose by roughly 10% (1.23 / 1.12 = 1.098). The 49% jump in review time is measured directly, not inferred from a null. A modest hidden gain is the best case for this argument: the tools produced something, less of it reached done than went in, and the review stage holds the difference. A gain that small also caps the headcount math. It can't pay for the 13.8% engineering cut the paper already ruled out.&lt;/p&gt;

&lt;p&gt;So the zero isn't a verdict on the tools. It's what happens when output rises and review capacity doesn't. Go back to the illustrative 20-person team: agents gave it 46 extra PRs a month, and those PRs arrived carrying 1,665 extra PR-hours waiting in review and 44 extra second passes.&lt;/p&gt;

</description>
      <category>aiagents</category>
      <category>codereview</category>
      <category>developerproductivity</category>
      <category>engineeringmanagement</category>
    </item>
    <item>
      <title>Gartner Says Over 40% of Agentic AI Projects Will Be Canceled. The Expensive Ones Are the Ones That Aren't.</title>
      <dc:creator>Michael Tuszynski</dc:creator>
      <pubDate>Sun, 11 Oct 2026 02:04:52 +0000</pubDate>
      <link>https://dev.to/michaeltuszynski/gartner-says-over-40-of-agentic-ai-projects-will-be-canceled-the-expensive-ones-are-the-ones-that-1l18</link>
      <guid>https://dev.to/michaeltuszynski/gartner-says-over-40-of-agentic-ai-projects-will-be-canceled-the-expensive-ones-are-the-ones-that-1l18</guid>
      <description>&lt;p&gt;A support-ticket triage agent goes live on a Monday with a 90-day pilot budget and a vague goal: "reduce agent handle time." On day 90 the numbers are mixed. Handle time is down on some queues and up on others, and three reviewers spend part of each week fixing the agent's routing mistakes. Nobody calls it a failure, because nobody wrote down what failure would look like. So the project keeps its funding, and it is still running a year later.&lt;/p&gt;

&lt;p&gt;That project won't appear in Gartner's cancellation count. It's the expensive outcome.&lt;/p&gt;

&lt;h2&gt;
  
  
  A cancellation is the cheap way for a pilot to end
&lt;/h2&gt;

&lt;p&gt;Gartner &lt;a href="https://www.gartner.com/en/newsroom/press-releases/2025-06-25-gartner-predicts-over-40-percent-of-agentic-ai-projects-will-be-canceled-by-end-of-2027" rel="noopener noreferrer"&gt;predicts over 40% of agentic AI projects will be canceled by the end of 2027&lt;/a&gt;, citing escalating costs, unclear business value, and inadequate risk controls. &lt;a href="https://www.reuters.com/business/over-40-agentic-ai-projects-will-be-scrapped-by-2027-gartner-says-2025-06-25/" rel="noopener noreferrer"&gt;Reuters ran it&lt;/a&gt; as a failure headline, and so did nearly everyone else.&lt;/p&gt;

&lt;p&gt;I read it the other way. A canceled project stops spending. The team moves to the next bet and the inference bill stops. If the cancellation comes at the end of a bounded pilot, the organization paid a known amount to learn something true.&lt;/p&gt;

&lt;p&gt;The projects that should worry a budget owner sit in the other 60%. Some of them are working. Some of them are zombies: pilots that never cleared a value bar and never got killed. They keep paying for inference, integration upkeep and human supervision, and they hold onto engineers who could be building something that works.&lt;/p&gt;

&lt;h2&gt;
  
  
  The 40% is a forecast, and it only counts the failures someone admitted to
&lt;/h2&gt;

&lt;p&gt;Two cautions before anyone puts this number in a board deck.&lt;/p&gt;

&lt;p&gt;First, it's an analyst prediction about 2027, not a measurement. The survey data in the same press release comes from a &lt;a href="https://www.gartner.com/en/newsroom/press-releases/2025-06-25-gartner-predicts-over-40-percent-of-agentic-ai-projects-will-be-canceled-by-end-of-2027" rel="noopener noreferrer"&gt;January 2025 poll of 3,412 Gartner webinar attendees&lt;/a&gt;. In that poll, 19% reported significant agentic AI investment and 31% were waiting to see. People who sign up for a Gartner webinar on agents aren't a random sample of enterprises. Treat the 40% as an informed guess about direction.&lt;/p&gt;

&lt;p&gt;Second, the forecast counts cancellations, and cancellation requires a decision. A zombie never triggers one. As &lt;a href="https://www.forbes.com/sites/robertszczerba/2026/07/07/why-40-of-agentic-ai-projects-may-be-canceled-by-2027/" rel="noopener noreferrer"&gt;Forbes' read of the Gartner report&lt;/a&gt; puts it, the causes are management failures, not model failures. A management failure that keeps a project alive is the same failure as one that kills it late. It's just harder to see.&lt;/p&gt;

&lt;p&gt;So the 40% tells you nothing about how many of the surviving 60% deserve to survive. The one tool that answers that is a document written before the pilot starts.&lt;/p&gt;

&lt;h2&gt;
  
  
  The kill threshold has to exist before the first prompt ships
&lt;/h2&gt;

&lt;p&gt;My earlier post, "Most Agent Projects Die. The Survivors Share a Shape," covered why survivors survive. This one is about the artifact the survivors' sponsors write and the zombies' sponsors skip: a kill sheet, signed before the pilot starts.&lt;/p&gt;

&lt;p&gt;It has five fields. Here it is for the triage agent:&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="c1"&gt;# kill-sheet.yaml: signed before pilot start, not edited after&lt;/span&gt;
&lt;span class="na"&gt;project&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;support-triage-agent&lt;/span&gt;
&lt;span class="na"&gt;metric&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;median_time_to_correct_queue&lt;/span&gt;   &lt;span class="c1"&gt;# one metric, not a dashboard&lt;/span&gt;
  &lt;span class="na"&gt;baseline&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;4.2h&lt;/span&gt;                        &lt;span class="c1"&gt;# measured over the 30 days before launch&lt;/span&gt;
  &lt;span class="na"&gt;target&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;2.5h&lt;/span&gt;                          &lt;span class="c1"&gt;# what "worked" means&lt;/span&gt;
  &lt;span class="na"&gt;measured_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;2026-04-01&lt;/span&gt;               &lt;span class="c1"&gt;# day 90, a fixed date&lt;/span&gt;
&lt;span class="na"&gt;monthly_run_cost&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;inference&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;3800&lt;/span&gt;
  &lt;span class="na"&gt;integration_upkeep&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;4000&lt;/span&gt;              &lt;span class="c1"&gt;# 0.25 engineer, loaded&lt;/span&gt;
  &lt;span class="na"&gt;human_review&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;6750&lt;/span&gt;                    &lt;span class="c1"&gt;# 3 reviewers x 30h x $75&lt;/span&gt;
  &lt;span class="na"&gt;total&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;14550&lt;/span&gt;
&lt;span class="na"&gt;cost_ceiling&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;12000&lt;/span&gt;                     &lt;span class="c1"&gt;# above this, value has to be proven&lt;/span&gt;
&lt;span class="na"&gt;cancel_authority&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;VP Support Operations&lt;/span&gt; &lt;span class="c1"&gt;# a name, not a committee&lt;/span&gt;
&lt;span class="na"&gt;extension_used&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The format matters less than when it gets written. Every field in it is easy to fill in on day zero and very hard to fill in honestly on day 90, once people have spent a quarter on the project and the sponsor has told leadership it's going well.&lt;/p&gt;

&lt;p&gt;Some notes on the fields.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;One metric.&lt;/strong&gt; If you give an agent five metrics, it will move one of them, and that one becomes the story. Choose the metric the business would pay for, and measure the baseline before launch. A baseline reconstructed after the fact always comes out flattering.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The date is fixed.&lt;/strong&gt; "End of Q2" turns into "once the new model ships" turns into "after the reorg." Put a calendar date on it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Human review is part of run cost.&lt;/strong&gt; It's usually the largest line, and usually the one left out. In the sheet above, review time is 46% of monthly cost. Inference is 26%. If your sheet shows only the token bill, it understates run cost by a factor of three or four.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The ceiling sits below current run cost.&lt;/strong&gt; That's deliberate. A pilot that costs more than the ceiling has to clear the metric target to continue. It doesn't get to argue it's "almost there."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cancel authority is one named person.&lt;/strong&gt; If killing the project needs a committee, it won't happen, because no committee member wants to own the decision.&lt;/p&gt;

&lt;p&gt;Expect the cancel-authority line to draw the most pushback. It's the only field that puts a person's name next to a possible failure.&lt;/p&gt;

&lt;h2&gt;
  
  
  A zombie costs six times what an honest cancellation does
&lt;/h2&gt;

&lt;p&gt;When the triage agent misses its target on day 90, there are two outcomes to price.&lt;/p&gt;

&lt;p&gt;Cancel on day 90 and the cost is three months of run cost, $43,650. That's the tuition.&lt;/p&gt;

&lt;p&gt;Now say it stays alive for nine more months on "let's give it another quarter," and then another. The zombie bill has two parts:&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="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;zombie_cost&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;monthly_run&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;months_past_kill&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;team_monthly_loaded&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;run&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;monthly_run&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;months_past_kill&lt;/span&gt;
    &lt;span class="c1"&gt;# Opportunity cost, floor estimate: the loaded cost of engineers who could
&lt;/span&gt;    &lt;span class="c1"&gt;# have been on the next-best project. Their real cost is that project's
&lt;/span&gt;    &lt;span class="c1"&gt;# value, which is usually higher and harder to defend in a spreadsheet.
&lt;/span&gt;    &lt;span class="n"&gt;opportunity&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;team_monthly_loaded&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;months_past_kill&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;run&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;run&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;opportunity&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;opportunity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;total&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;run&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;opportunity&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nf"&gt;zombie_cost&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;14_550&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;9&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;16_000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;# {'run': 130950, 'opportunity': 144000, 'total': 274950}
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The opportunity line assumes one full-time engineer's worth of attention ($16,000 a month loaded), split across the two people still tuning prompts and triaging complaints. It doesn't overlap with the 0.25 engineer already counted under integration upkeep. And it's a floor. The true cost of those engineers is whatever they would have shipped instead.&lt;/p&gt;

&lt;p&gt;That comes to $274,950 against $43,650, about 6.3 times the cost of the honest answer. Every month of delay adds $30,550. On top of that, the zombie keeps three reviewers in the review habit, and it makes the next agent proposal harder to fund, because leadership now associates "agents" with a project that never ended.&lt;/p&gt;

&lt;p&gt;None of these numbers are exotic. They're the triage agent's own costs from the sheet above, multiplied by the time nobody decided anything.&lt;/p&gt;

&lt;h2&gt;
  
  
  Late bloomers get one extension, written down in advance
&lt;/h2&gt;

&lt;p&gt;The strongest objection is real. Some value takes time to show up. A triage agent may get much better after six months of corrections feed its routing rules, or once the support team stops working around it. A strict day-90 date kills that project before it matures, and it rewards pilots that look good early over those that compound.&lt;/p&gt;

&lt;p&gt;That argument holds only when there's a mechanism behind the slow value, and a mechanism can be written down. So the kill sheet gets an extension rule, agreed at signing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;One extension, no more than 60 days. A second extension is a cancellation.&lt;/li&gt;
&lt;li&gt;Before the extension starts, the sponsor names the mechanism ("routing accuracy improves as correction data accumulates") and a leading indicator that can be measured now ("misroute rate falls from 18% to 12% by day 150").&lt;/li&gt;
&lt;li&gt;The cost ceiling doesn't change. If the extension needs more money, it's a new project and gets a new sheet.&lt;/li&gt;
&lt;li&gt;The same named person signs the extension and owns the decision at its end.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This answers the late-bloomer case on its own terms. A real late bloomer can say why it's slow and what will move first. A zombie can only say it needs more time. The rule lets the first through and stops the second, and "we're seeing promising signals" doesn't count as a mechanism.&lt;/p&gt;

&lt;p&gt;The cost of this discipline is that some projects that would have made it will die. A kill sheet will occasionally cancel a winner. That trade is fine. A false cancellation costs one pilot's tuition plus a restart. A false reprieve costs $30,550 a month with no end date.&lt;/p&gt;

&lt;p&gt;Go back to the triage agent on day 90. With the sheet, the VP of Support Operations reads one number, median time to the correct queue: 3.6 hours against a target of 2.5. She either signs one 60-day extension tied to a misroute rate or ends it. Without the sheet, the same 3.6 hours gets called "a 14% improvement" in a status deck, and nine months later the project has cost $274,950.&lt;/p&gt;

</description>
      <category>aiagents</category>
      <category>aistrategy</category>
      <category>enterpriseai</category>
      <category>projectmanagement</category>
    </item>
    <item>
      <title>An AI Agent With a Crypto Wallet Is a Hot Wallet Anyone Can Talk To</title>
      <dc:creator>Michael Tuszynski</dc:creator>
      <pubDate>Tue, 06 Oct 2026 04:18:31 +0000</pubDate>
      <link>https://dev.to/michaeltuszynski/an-ai-agent-with-a-crypto-wallet-is-a-hot-wallet-anyone-can-talk-to-41kk</link>
      <guid>https://dev.to/michaeltuszynski/an-ai-agent-with-a-crypto-wallet-is-a-hot-wallet-anyone-can-talk-to-41kk</guid>
      <description>&lt;p&gt;An attacker sent Grok a free NFT, posted a message in Morse code, and walked away with &lt;a href="https://cointelegraph.com/magazine/grok-bankr-wallet-free-nft-prompt-injection" rel="noopener noreferrer"&gt;roughly 3 billion DRB tokens, about $174,000&lt;/a&gt;. Nobody stole a private key and no smart contract had a bug. One AI agent translated some dots and dashes in public. A second AI agent read that translation as an order and paid it.&lt;/p&gt;

&lt;p&gt;That incident from May 2026 is the best case against giving agents custody of crypto. An agent wallet is a hot wallet with a chat interface, and anyone who can get text in front of the agent can talk to it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The attack turned a translation into a transfer
&lt;/h2&gt;

&lt;p&gt;The sequence is short, and every step matters.&lt;/p&gt;

&lt;p&gt;Grok had a wallet through Bankr, a bot that executes on-chain actions for users who tag it on X. That wallet was read-only. The attacker &lt;a href="https://cointelegraph.com/magazine/grok-bankr-wallet-free-nft-prompt-injection" rel="noopener noreferrer"&gt;airdropped a free Bankr Club NFT&lt;/a&gt; to it. Holding the NFT upgraded the wallet's permissions from read-only to transfer rights. So an inbound token, which anyone can send to any address, changed what the wallet was allowed to do.&lt;/p&gt;

&lt;p&gt;Next the attacker posted Morse code and asked Grok to translate it and tag Bankrbot. Grok did what it was asked. The decoded text told Bankrbot to send about 3B DRB to the attacker's address. Bankrbot saw a tagged instruction coming from an account with transfer rights, and &lt;a href="https://cointelegraph.com/magazine/grok-bankr-wallet-free-nft-prompt-injection" rel="noopener noreferrer"&gt;it executed&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Two design decisions failed here. Permissions could be granted by something the wallet received, not just by something its owner configured. And one agent's output (a translation, which is about as harmless as text gets) went straight into another agent's command parser. Neither is a model problem, and a smarter Grok would not have fixed either one. Grok translated correctly. That was the attack.&lt;/p&gt;

&lt;h2&gt;
  
  
  The money came back because the attacker chose to return it
&lt;/h2&gt;

&lt;p&gt;The funds were returned, and coverage has treated that as a happy ending. It's the most alarming part of the story. The recovery came from the attacker's goodwill. No system clawed it back.&lt;/p&gt;

&lt;p&gt;On-chain transfers are final. You get no chargeback, no issuing bank to call, and no dispute window. Once Bankrbot signed the transaction, the only party who could undo it was the person holding the $174K. Allium's explainer on agent wallets describes &lt;a href="https://www.allium.so/blog/agent-wallet-how-ai-agents-hold-and-spend-onchain/" rel="noopener noreferrer"&gt;agents that hold and spend onchain&lt;/a&gt; as an emerging pattern. The pattern inherits that finality whether or not anyone designed for it.&lt;/p&gt;

&lt;p&gt;A human who gets phished on a bank wire has a few hours and a fraud department. An agent that gets prompt-injected on a public chain has however long a block takes to confirm.&lt;/p&gt;

&lt;h2&gt;
  
  
  Monitoring fires after the money is gone
&lt;/h2&gt;

&lt;p&gt;Compliance and transaction-monitoring vendors were quick to cite the incident as a reason to buy detection: watch agent wallets, score anomalous transfers, alert on suspicious patterns. That's the strongest counterargument to my position. It says agent custody is fine once you have good enough eyes on it.&lt;/p&gt;

&lt;p&gt;Look at what monitoring would actually have done here. Bankrbot's transfer was one transaction, signed by an authorized wallet and sent to a valid address. A good anomaly model flags it as unusual, maybe within seconds of confirmation. The flag lands on a transfer that has already settled, on rails with no reversal path. You get an excellent incident report and a very short list of remediation options, most of which come down to asking the attacker nicely.&lt;/p&gt;

&lt;p&gt;Detection pays off when something downstream can act on it, like a card network holding settlement or a bank freezing an account. On a public chain nothing downstream can act. Every control that matters has to run before the signature.&lt;/p&gt;

&lt;h2&gt;
  
  
  Capability limits have to be enforced in code, before signing
&lt;/h2&gt;

&lt;p&gt;Here's the control set I'd require before an agent touches anything that moves value:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No standing transfer rights.&lt;/strong&gt; The default is read-only. Spending authority is granted per task and expires.&lt;/li&gt;
&lt;li&gt;Permissions come from the operator's configuration and nowhere else. No inbound token, NFT, message, or airdrop can change what the wallet may do.&lt;/li&gt;
&lt;li&gt;Destinations come from an allowlist. An address the operator hasn't approved can't receive funds, however the request is worded.&lt;/li&gt;
&lt;li&gt;Per-transaction and daily caps, enforced outside the model.&lt;/li&gt;
&lt;li&gt;Human approval above a threshold, set low enough that it actually triggers.&lt;/li&gt;
&lt;li&gt;Text an agent emits never gets parsed as a command by another agent. Translations, summaries, and replies are data.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The last rule is the one that would have stopped the Grok/Bankr chain on its own. Here's roughly what the gate looks like as code that sits between any agent and the signer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;Origin&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;operator&lt;/span&gt;&lt;span class="dl"&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;agent_output&lt;/span&gt;&lt;span class="dl"&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;inbound_message&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;TransferRequest&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;to&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;amountUsd&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;origin&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Origin&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// set by the transport layer, never by the model&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="c1"&gt;// Loaded from deploy-time config. No code path reads wallet holdings&lt;/span&gt;
&lt;span class="c1"&gt;// to decide permissions, so an airdropped NFT changes nothing.&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;POLICY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;allowlist&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Set&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;0xTreasury...&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;0xVendorA...&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]),&lt;/span&gt;
  &lt;span class="na"&gt;perTxCapUsd&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;250&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;dailyCapUsd&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;humanApprovalAboveUsd&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;authorize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;TransferRequest&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;spentTodayUsd&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;allow&lt;/span&gt;&lt;span class="dl"&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;needs_human&lt;/span&gt;&lt;span class="dl"&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;deny&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;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;origin&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;operator&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;deny&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="nx"&gt;POLICY&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;allowlist&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;has&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;to&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="s2"&gt;deny&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;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;amountUsd&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;POLICY&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;perTxCapUsd&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="s2"&gt;deny&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;spentTodayUsd&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;amountUsd&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;POLICY&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;dailyCapUsd&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="s2"&gt;deny&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;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;amountUsd&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;POLICY&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;humanApprovalAboveUsd&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="s2"&gt;needs_human&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;allow&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;Run the attack through it. The instruction to Bankrbot came from Grok's public reply, so &lt;code&gt;origin&lt;/code&gt; is &lt;code&gt;agent_output&lt;/code&gt;, and the first line returns &lt;code&gt;deny&lt;/code&gt;. Even if someone mislabeled the origin, the attacker's address isn't on the allowlist. Even if it were, $174K is roughly 700 times the per-transaction cap. The attack has to get past four independent checks, and none of them depend on the model reading the request correctly.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;origin&lt;/code&gt; field carries the most weight, and it's also the easiest to get wrong. It has to be stamped by whatever received the message. If the model fills it in, the attacker can fill it in too. In the agent pipelines I run, no tool that moves money or deletes data accepts arguments parsed from another model's free-text output.&lt;/p&gt;

&lt;p&gt;This gate has real costs. Allowlists make agents worse at the open-ended "pay whoever" tasks that make crypto agents appealing in demos. Human approval adds latency. Caps cut into throughput. That's the point. Those costs are the price of putting a limit on what a stranger's message can do.&lt;/p&gt;

&lt;h2&gt;
  
  
  Card rails can say no and take it back
&lt;/h2&gt;

&lt;p&gt;In June 2026 Mastercard announced Agent Pay for Machines. Going by Mastercard's own description, it authenticates the agent, enforces spending limits, and guarantees settlement on a network that already has dispute rails. That's Mastercard describing its own product, and nobody has shown publicly how those disputes play out when the buyer is a bot. Treat it as a design statement until someone reproduces it in production.&lt;/p&gt;

&lt;p&gt;Still, the design statement points the right way. Card networks were built on the assumption that some transactions will be wrong. Authorization can decline a purchase before it goes through, and chargebacks can reverse it afterward. The Grok/Bankr chain had neither.&lt;/p&gt;

&lt;h2&gt;
  
  
  Smart accounts get you limits but still no reversals
&lt;/h2&gt;

&lt;p&gt;The strongest technical pushback is that crypto can enforce limits too. Smart-contract wallets support session keys with spending caps, destination restrictions, and expiry, all enforced on-chain. That's true. A well-built smart account can run most of the gate above at the contract level, which is better than a policy check sitting in an off-chain service.&lt;/p&gt;

&lt;p&gt;But it only covers the "say no" half. A session key capped at $250 limits the damage, and the $250 is still gone. For agents that have to transact on-chain, such as protocol treasuries, on-chain market makers, or anything with no card-network equivalent, smart-account limits are the minimum, and the caps should be set on the assumption that every dollar under them is already lost. For everyone else, which means agents buying API credits, SaaS seats, or cloud capacity, there's no reason to choose a rail without reversals.&lt;/p&gt;

&lt;h2&gt;
  
  
  The cap is the most an attacker can take
&lt;/h2&gt;

&lt;p&gt;If an agent has to move money, put it on rails that can refuse a payment and reverse one. If it has to move crypto, assume the cap is what you'll lose, because a stranger with a free NFT and a Morse code chart can try to take it.&lt;/p&gt;

&lt;p&gt;That Morse code message was worth about $174,000, and the only control that worked was the attacker deciding to send the money back.&lt;/p&gt;

</description>
      <category>aiagents</category>
      <category>promptinjection</category>
      <category>cryptosecurity</category>
      <category>aisecurity</category>
    </item>
    <item>
      <title>Your AI Coding Dashboard Counts the Wrong Thing</title>
      <dc:creator>Michael Tuszynski</dc:creator>
      <pubDate>Mon, 05 Oct 2026 19:23:59 +0000</pubDate>
      <link>https://dev.to/michaeltuszynski/your-ai-coding-dashboard-counts-the-wrong-thing-1mo9</link>
      <guid>https://dev.to/michaeltuszynski/your-ai-coding-dashboard-counts-the-wrong-thing-1mo9</guid>
      <description>&lt;p&gt;Open the vendor dashboard for any AI coding tool and you'll see acceptance rate near the top. It's the number managers quote in staff meetings. GitHub's own research puts it at &lt;a href="https://arxiv.org/abs/2306.15033" rel="noopener noreferrer"&gt;roughly 30% of suggestions accepted&lt;/a&gt;, and the &lt;a href="https://docs.github.com/en/rest/copilot/copilot-metrics" rel="noopener noreferrer"&gt;Copilot metrics API&lt;/a&gt; gives you suggestion and acceptance counts per editor, language, and day. So a team sees 30%, the line goes up and to the right, and the renewal gets signed.&lt;/p&gt;

&lt;p&gt;That number measures a keystroke. Finance is paying for software that stays shipped. The output measure that belongs on the dashboard is the one no vendor shows by default: &lt;strong&gt;of the lines your team accepted, how many were reverted or rewritten within 30 days?&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Acceptance rate was validated against how developers felt
&lt;/h2&gt;

&lt;p&gt;Acceptance rate didn't become the headline metric by accident. The &lt;a href="https://cacm.acm.org/research/measuring-github-copilots-impact-on-productivity/" rel="noopener noreferrer"&gt;GitHub study in Communications of the ACM&lt;/a&gt; tested several usage signals and found acceptance rate was the best predictor of &lt;em&gt;perceived&lt;/em&gt; productivity. Perceived is the important word there. Developers filled out surveys, and the survey scores tracked how often they hit Tab.&lt;/p&gt;

&lt;p&gt;That's a real finding. It's also a finding about sentiment. It never claims the accepted code survived review, passed the next sprint, or stayed out of the incident channel. And GitHub sells the tool. Treat the 30% as a vendor number about engagement, not about output.&lt;/p&gt;

&lt;p&gt;The newer tooling has the same blind spot one layer up. Dash0's AI Coding Insights lets you &lt;a href="https://www.dash0.com/blog/introducing-ai-coding-insights-stop-guessing-what-your-coding-agents-deliver" rel="noopener noreferrer"&gt;sort every Claude Code session by cost or error count&lt;/a&gt; and read the full transcript. That's useful for debugging an agent. But cost and errors-per-session both describe what went &lt;em&gt;into&lt;/em&gt; the work. Neither tells you what happened to the code after the session closed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rework is where AI-assisted code shows its real cost
&lt;/h2&gt;

&lt;p&gt;The independent evidence points the same direction. GitClear analyzed 153 million changed lines and &lt;a href="https://www.gitclear.com/coding_on_copilot_data_shows_ais_downward_pressure_on_code_quality" rel="noopener noreferrer"&gt;projected code churn, meaning lines reverted or updated within two weeks of being written, to double in 2024 against its 2021 pre-AI baseline&lt;/a&gt;. GitClear sells code analytics, so discount it too. But the 2024 DORA report, which has no product to sell here, found that &lt;a href="https://dora.dev/research/2024/dora-report/" rel="noopener noreferrer"&gt;a 25% increase in AI adoption was associated with an estimated 7.2% drop in delivery stability&lt;/a&gt;. And METR's randomized trial found experienced open-source developers were &lt;a href="https://metr.org/blog/2025-07-10-early-2025-ai-experienced-os-dev-study/" rel="noopener noreferrer"&gt;19% slower with AI tools while believing they were 20% faster&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Put those next to the CACM result and you get the core problem. The metric vendors chose tracks how developers feel. The independent studies that measured outcomes found instability, rework, and a perception gap. Acceptance rate sits on the wrong side of that gap.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.mpt.solutions/your-ai-coding-pilot-cost-80k-and-shipped-9-faster-heres-the-line-item-finance-m/" rel="noopener noreferrer"&gt;The $80K AI coding pilot post&lt;/a&gt; priced the senior review hours on the cost side. This is the matching hole on the output side. A suggestion that gets accepted on Tuesday and rewritten on the following Monday shows up on the dashboard as a win. Then it shows up again as new work.&lt;/p&gt;

&lt;h2&gt;
  
  
  You can measure 30-day rework from git history you already have
&lt;/h2&gt;

&lt;p&gt;You don't need a vendor for this. Git already records who wrote every line and when it changed. The method:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Take every non-merge commit that is at least 30 days old.&lt;/li&gt;
&lt;li&gt;Tag it AI-assisted or not. Claude Code adds a &lt;code&gt;Co-Authored-By: Claude&lt;/code&gt; trailer to its commits by default, so the tag comes free.&lt;/li&gt;
&lt;li&gt;Count the lines each commit added.&lt;/li&gt;
&lt;li&gt;Run &lt;code&gt;git blame&lt;/code&gt; at the repo state 30 days later and count how many of those lines are still attributed to the original commit.&lt;/li&gt;
&lt;li&gt;Rework rate = 1 − (kept ÷ added), computed separately for each cohort.
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;#!/usr/bin/env python3
&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;30-day rework: share of added lines gone or rewritten 30 days later.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;subprocess&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;collections&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Counter&lt;/span&gt;

&lt;span class="n"&gt;DAY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;86400&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;git&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;subprocess&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;git&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;capture_output&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                          &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;check&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;stdout&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;added_lines&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sha&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;files&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;git&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;show&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--numstat&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--format=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sha&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;splitlines&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
        &lt;span class="n"&gt;added&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;row&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="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\t&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;added&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;added&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;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;# skip binaries
&lt;/span&gt;            &lt;span class="n"&gt;files&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;added&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;files&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;kept_lines&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sha&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rev&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;out&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;git&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;blame&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-w&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-M&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-C&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--line-porcelain&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rev&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;subprocess&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CalledProcessError&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="c1"&gt;# file deleted or moved out of reach: counts as reworked
&lt;/span&gt;    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;splitlines&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;line&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startswith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sha&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;rev_after&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sha&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;days&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;ts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;git&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;show&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-s&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--format=%ct&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sha&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;git&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rev-list&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--before=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;ts&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;days&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;DAY&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;HEAD&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;since&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;120 days ago&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;log&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;git&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;log&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--no-merges&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--since=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;since&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
              &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--until=30 days ago&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--format=%H%x09%B%x00&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;totals&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ai&lt;/span&gt;&lt;span class="sh"&gt;"&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;human&lt;/span&gt;&lt;span class="sh"&gt;"&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="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;entry&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;log&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="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\x00&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))):&lt;/span&gt;
        &lt;span class="n"&gt;sha&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;entry&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="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\t&lt;/span&gt;&lt;span class="sh"&gt;"&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="n"&gt;cohort&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ai&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;co-authored-by: claude&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;human&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="n"&gt;rev&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;rev_after&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sha&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;added_lines&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sha&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;items&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
            &lt;span class="n"&gt;totals&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;cohort&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;added&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;
            &lt;span class="n"&gt;totals&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;cohort&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;kept&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nf"&gt;kept_lines&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sha&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rev&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;cohort&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;totals&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;items&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;c&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;added&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
            &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;cohort&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;added&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; added, &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
                  &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;kept&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;added&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; reworked by day 30&lt;/span&gt;&lt;span class="sh"&gt;"&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;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;__main__&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;main&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;-w -M -C&lt;/code&gt; flags tell blame to ignore whitespace and follow lines that moved within or between files. Without them, a refactor that only reformats or relocates code gets counted as rework. The 30-day cutoff on &lt;code&gt;--until&lt;/code&gt; matters too. A commit from last week hasn't had its 30 days yet, and counting it would make every cohort look stable.&lt;/p&gt;

&lt;h2&gt;
  
  
  The objection: some rework is just healthy iteration
&lt;/h2&gt;

&lt;p&gt;Here's the best case against this metric. Code gets rewritten because teams learn. A spike gets thrown away on purpose. A first pass at an API gets reshaped once the second caller shows up. If you punish 30-day rework, you punish exploration, and engineers will learn to stop committing early.&lt;/p&gt;

&lt;p&gt;That objection has a specific mechanism, and the two-cohort design deals with it. Healthy iteration happens to human-written code too, in the same repo, under the same review culture, against the same product churn. Reading a single rework rate on its own tells you nothing. The signal is the &lt;em&gt;difference&lt;/em&gt; between the AI cohort and the human cohort over the same window. If both sit at 15%, AI isn't adding rework and the acceptance-rate story holds. If the AI cohort sits at 30% against a human 15%, AI-assisted code is being rewritten at twice the human rate, and the vendor dashboard counted every one of those lines once, as a win.&lt;/p&gt;

&lt;p&gt;Don't turn it into a per-engineer scorecard, either. It's a cohort measure for a renewal decision. Once it's used to grade people, people start gaming the trailers.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where this measurement breaks
&lt;/h2&gt;

&lt;p&gt;The script has real limits, and you should know them before you put the output in front of a CFO.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Squash merges erase the attribution.&lt;/strong&gt; If your team squash-merges PRs, the trailer survives only when it lands in the squash message. Many merge tools drop it. Check a few merged PRs by hand before trusting the cohort split.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Inline completions leave no trailer.&lt;/strong&gt; Copilot-style Tab completions get mixed into human commits without any marker. For those tools, compare whole-repo rework over the 90 days before rollout against the 90 days after. It's noisier, and it's still more honest than acceptance rate.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Generated files distort the count.&lt;/strong&gt; Lockfiles, snapshots, and codegen output get rewritten by machines on every run. Exclude them with a pathspec, or the cohort with more dependency bumps will look worse for reasons that have nothing to do with AI.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Thirty days is a choice.&lt;/strong&gt; GitClear uses two weeks. Thirty days catches the rewrite that comes when the second caller shows up, which two weeks often misses. Pick a window, write it down, and don't change it between quarters.&lt;/p&gt;

&lt;p&gt;None of these limits rescues acceptance rate. Every one of them is a reason to measure carefully. None is a reason to go back to counting keystrokes.&lt;/p&gt;

&lt;h2&gt;
  
  
  The number to bring to the renewal meeting
&lt;/h2&gt;

&lt;p&gt;Go back to the dashboard. It says 30% of suggestions were accepted. That figure was validated against how developers felt, and the vendor selling the seats reports it.&lt;/p&gt;

&lt;p&gt;Ask the question finance should be asking: of that 30%, what fraction was still in main on day 30, and how does that compare to the code your engineers wrote without help? It comes down to two cohorts and one ratio, &lt;code&gt;kept ÷ added&lt;/code&gt;, and you can compute it from your own &lt;code&gt;git log&lt;/code&gt; this afternoon.&lt;/p&gt;

</description>
      <category>aiengineering</category>
      <category>developerproductivity</category>
      <category>engineeringmetrics</category>
      <category>codequality</category>
    </item>
    <item>
      <title>Forward Deployed Engineer Names Four Different Things. One of Them Is a Software Product.</title>
      <dc:creator>Michael Tuszynski</dc:creator>
      <pubDate>Wed, 30 Sep 2026 21:50:36 +0000</pubDate>
      <link>https://dev.to/michaeltuszynski/forward-deployed-engineer-names-four-different-things-one-of-them-is-a-software-product-2li9</link>
      <guid>https://dev.to/michaeltuszynski/forward-deployed-engineer-names-four-different-things-one-of-them-is-a-software-product-2li9</guid>
      <description>&lt;h1&gt;
  
  
  Forward Deployed Engineer Names Four Different Things. One of Them Is a Software Product.
&lt;/h1&gt;

&lt;p&gt;The same three words now label a job at one frontier lab, a different job at the same lab, a billion-dollar organization at AWS, and a piece of software you can turn on inside Palantir Foundry.&lt;/p&gt;

&lt;p&gt;That last one deserves a second read. Palantir invented the role, and Palantir now ships &lt;a href="https://www.palantir.com/docs/foundry/ai-fde/overview" rel="noopener noreferrer"&gt;AI FDE&lt;/a&gt;, which the docs introduce as "the AI-powered forward deployed engineer," an "interactive agent that operates Foundry for you through conversational commands."&lt;/p&gt;

&lt;p&gt;So when two people say "we need an FDE on this," there is no longer any guarantee they are discussing the same category of thing, let alone the same scope of work. That's a scoping problem, and scoping problems are expensive for whoever is holding the statement of work when they surface.&lt;/p&gt;

&lt;p&gt;Eight years in presales left me with one reflex above all others: get the boundary of the work written down before anybody starts building. This title needs that treatment.&lt;/p&gt;

&lt;h2&gt;
  
  
  The role began as an answer to an information problem
&lt;/h2&gt;

&lt;p&gt;Palantir popularized the term and had it in use for staff &lt;a href="https://en.wikipedia.org/wiki/Forward_Deployed_Engineer" rel="noopener noreferrer"&gt;by 2009&lt;/a&gt;. The phrasing is borrowed from the military's forward-basing language. What it solved was a problem about information rather than cost.&lt;/p&gt;

&lt;p&gt;You cannot write a specification for software that has to run in an environment you have never seen. Undocumented schemas. Compliance rules nobody wrote down. A workflow that lives in the heads of six people in one building. An architect who visits, produces a design, and flies home hasn't transferred knowledge — they've transferred a guess, and the guess fails in integration.&lt;/p&gt;

&lt;p&gt;The fix was to stop splitting the person who learns the environment from the person who writes the code. Same engineer, both jobs, on site, accountable through production.&lt;/p&gt;

&lt;p&gt;That original definition has four parts worth separating, because they behave very differently under automation:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Learn an environment that isn't documented.&lt;/li&gt;
&lt;li&gt;Decide what to build in it.&lt;/li&gt;
&lt;li&gt;Build it against the real data.&lt;/li&gt;
&lt;li&gt;Own it in production, or hand it over with the knowledge attached.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  What the software version actually does is step three
&lt;/h2&gt;

&lt;p&gt;Read Palantir's capability list rather than the product name. AI FDE handles data integration and pipeline modification, data connection management, ontology editing, function writing and testing, read-only exploration, governance and access auditing, and React application development.&lt;/p&gt;

&lt;p&gt;Every item on that list is platform operation. Each one presupposes two things: that somebody has already decided what should be built, and that the target is Foundry. On day one of a real deployment, neither is true.&lt;/p&gt;

&lt;p&gt;To Palantir's credit, the documentation claims nothing more than that. There is no line on that page about replacing human FDEs — the product is scoped as a way to operate the platform faster, which is exactly what it appears to be. The overreach isn't in the docs. It's in reading the product name as a description of the role.&lt;/p&gt;

&lt;p&gt;Which makes step three the honest answer to "what can AI do here." Building against real data is getting dramatically faster. Steps one, two and four are close to untouched, because none of them are typing problems. Nobody has automated walking into a business you don't understand and working out which of the things people are complaining about is the one worth building.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two job titles at one company already disagree
&lt;/h2&gt;

&lt;p&gt;Inside OpenAI the split between step three and the rest is visible in the org chart.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://openai.com/careers/forward-deployed-software-engineer-sf-san-francisco/" rel="noopener noreferrer"&gt;Forward Deployed Software Engineer&lt;/a&gt; posting wants 7+ years of full-stack work and describes coding "side-by-side" with customer teams "on their infrastructure," designing abstractions that later engagements reuse. That's steps three and four, with the original accountability intact.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://openai.com/careers/forward-deployed-engineer-(fde)-sf-san-francisco/" rel="noopener noreferrer"&gt;Forward Deployed Engineer&lt;/a&gt; posting owns "discovery, technical scoping, system design, build, and production rollout," plus sequencing, tradeoffs between scope and speed, playbook authoring, and field feedback that "changes product and model roadmaps." It contributes code "when progress or clarity depends on it." Conditional, by design.&lt;/p&gt;

&lt;p&gt;Both are reasonable jobs. They are not the same job, and the difference is which of the four steps the person is accountable for. A team that hires the second expecting the first will be surprised, and so will the customer.&lt;/p&gt;

&lt;h2&gt;
  
  
  The category got funded faster than it got defined
&lt;/h2&gt;

&lt;p&gt;Anthropic and OpenAI stood up forward-deployed groups in May. On June 30, AWS committed &lt;a href="https://www.aboutamazon.com/news/aws/aws-1-billion-forward-deployed-ai-engineers" rel="noopener noreferrer"&gt;$1 billion&lt;/a&gt; to a Forward Deployed Engineering organization it describes as "agentic-first," compressing "timelines from months to days," and designed so "customers are self-sufficient when a deployment ends." Two days later Microsoft announced Frontier Co. with $2.5 billion and 6,000 embedded engineers.&lt;/p&gt;

&lt;p&gt;Treat the months-to-days figure as a vendor ceiling rather than a planning number; no independent measurement of it exists yet. But the pattern is clear enough without it: billions of dollars of organizational design arrived in about eight weeks, and shared vocabulary takes longer than that.&lt;/p&gt;

&lt;p&gt;The market's own uncertainty shows up in what it pays. Two frontier labs are hiring for this title in the same city right now, and their posted base ranges start $95K apart — $185K at one, $280K at the other. That's not an argument about whether the role is valued. It's a straightforward signal that the scope behind the words hasn't converged.&lt;/p&gt;

&lt;h2&gt;
  
  
  The strongest objection is that this is genuinely new work
&lt;/h2&gt;

&lt;p&gt;a16z's Tom Hollands &lt;a href="https://a16z.com/forward-deployed-job-titles/" rel="noopener noreferrer"&gt;argued in January&lt;/a&gt; that forward-deployed titles describe work that would have been unrecognizable five years ago, and he's right about the mechanism. An AI system's capabilities only resolve once you build against the customer's real data, which breaks the design-it-then-hand-it-off sequence and collapses steps two and three into one person. His test for whether a new title is doing real work: does it describe something that didn't exist before?&lt;/p&gt;

&lt;p&gt;Apply that test to the mechanism and it passes. Apply it to the vocabulary and the picture changes, because the same three words now cover step three alone, all four steps, an organizational strategy, and a Foundry feature. New work deserves a new name. It also deserves a definition, and the second part hasn't happened. Worth noting that a16z funds many of the companies staffing these teams.&lt;/p&gt;

&lt;h2&gt;
  
  
  The scope belongs in writing before the kickoff
&lt;/h2&gt;

&lt;p&gt;Not a candidate checklist — a scoping checklist, equally useful from either side of the table:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The artifact.&lt;/strong&gt; What gets shipped, named outright: an MCP server, a data pipeline, an application running in the customer's stack. "Scopes of work and project plans" is a real deliverable too, but it's a different one.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The requirement owner.&lt;/strong&gt; Who decides what gets built. If that's the customer, this is step three and can move fast. If it's the deployed engineer, steps one and two are in scope and the timeline is a different shape entirely.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The production owner at exit.&lt;/strong&gt; The original role's defining trait was that whoever mapped the problem answers the page six months later. "Self-sufficient when the deployment ends" is a legitimate model, and a different commitment. Both are fine. Assuming one and getting the other is not.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The platform boundary.&lt;/strong&gt; Whose stack, with what access, and which parts an agent can operate directly. This is where the software version of the title is genuinely useful, and where its limits are hard edges rather than preferences.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Where this argument runs out: a two-year-old title is supposed to be unsettled, and dispersion this early proves nothing about anyone's intentions. Posted ranges are base only, and equity is undisclosed, so the $95K figure describes disclosure practice as much as scope. And a single vocabulary may never arrive — "DevOps" never fully converged either, and teams still shipped, because the good ones wrote down what they meant locally.&lt;/p&gt;

&lt;p&gt;That last part is the actual ask. Not an industry standard. Just a shared sentence, written before the kickoff, naming which of the four steps the forward deployed engineer owns. The role was invented because a handoff document couldn't carry the knowledge. It would be a strange outcome if the title itself became the thing nobody could hand off.&lt;/p&gt;

</description>
      <category>aiagents</category>
      <category>enterpriseai</category>
      <category>platformengineering</category>
    </item>
    <item>
      <title>Your Agent Is Paying Opus Prices for Yes/No Questions</title>
      <dc:creator>Michael Tuszynski</dc:creator>
      <pubDate>Mon, 28 Sep 2026 02:47:35 +0000</pubDate>
      <link>https://dev.to/michaeltuszynski/your-agent-is-paying-opus-prices-for-yesno-questions-48c1</link>
      <guid>https://dev.to/michaeltuszynski/your-agent-is-paying-opus-prices-for-yesno-questions-48c1</guid>
      <description>&lt;p&gt;An agent loop makes dozens of small calls that aren't reasoning at all. Is this ticket about billing. Is this search result relevant. Did the tool call succeed. Should this story be a blog post. Most agent harnesses send every one of those to the same frontier model that writes the code, with a prompt that ends in "return JSON."&lt;/p&gt;

&lt;p&gt;I measured what that costs. The same six-question classification, run 150 times per model: a System One model answered in 163 ms at $53 per million calls. Claude Opus 5 took 2.7 seconds and $11,183. That's roughly 17x the latency and 200x the price for a yes/no question.&lt;/p&gt;

&lt;p&gt;But the savings only survive if the loop around the model owns the thresholds. My data says that part is harder than the model swap.&lt;/p&gt;

&lt;h2&gt;
  
  
  A System One model answers in types, not text
&lt;/h2&gt;

&lt;p&gt;The name comes from Kahneman's split between fast, intuitive System 1 and slow, deliberate System 2. &lt;a href="https://docs.typesafe.ai/concepts/system-one" rel="noopener noreferrer"&gt;TypeSafe's Jev&lt;/a&gt; is the first model sold under that label. It takes a state (text or JSON) and a set of typed questions, and it returns three kinds of answer: a &lt;code&gt;choice&lt;/code&gt; from options you list, a &lt;code&gt;score&lt;/code&gt; against a rubric you write, and a &lt;code&gt;noul&lt;/code&gt;, the probability that a statement is true.&lt;/p&gt;

&lt;p&gt;It doesn't write text. It doesn't call tools. TypeSafe's own docs say it plainly: Jev is &lt;a href="https://docs.typesafe.ai/introduction/coding-agents" rel="noopener noreferrer"&gt;not a drop-in replacement&lt;/a&gt; for the LLM behind Claude Code or Cursor. You keep the LLM for the work that needs one and call Jev for the judgments.&lt;/p&gt;

&lt;p&gt;That's the same argument I made in April about &lt;a href="https://www.mpt.solutions/the-model-doesnt-matter-the-harness-does/" rel="noopener noreferrer"&gt;where agent performance comes from&lt;/a&gt;: the loop matters more than the weights. A System One model is a new part to put inside the loop.&lt;/p&gt;

&lt;h2&gt;
  
  
  Code computes, System One judges, the LLM generates
&lt;/h2&gt;

&lt;p&gt;Three layers, and a question belongs to exactly one of them.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;Owns&lt;/th&gt;
&lt;th&gt;Examples&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Code&lt;/td&gt;
&lt;td&gt;Anything with an exact answer&lt;/td&gt;
&lt;td&gt;counts, date math, thresholds, combining scores&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;System One&lt;/td&gt;
&lt;td&gt;Bounded judgments with a closed answer set&lt;/td&gt;
&lt;td&gt;route this ticket, is this vendor marketing, how durable is this story&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;LLM&lt;/td&gt;
&lt;td&gt;Generation and multi-step reasoning&lt;/td&gt;
&lt;td&gt;write the draft, call tools, plan a fix&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;TypeSafe publishes where Jev breaks, which is more than most vendors do. The &lt;a href="https://docs.typesafe.ai/model-jaggedness/jev-1.13" rel="noopener noreferrer"&gt;jaggedness page for jev-1.13&lt;/a&gt; lists arithmetic, date comparison, double negatives, oversized state and adversarial text. Every one of those is a boundary in the table above. Push arithmetic down into code. Push anything open-ended up to the LLM.&lt;/p&gt;

&lt;h2&gt;
  
  
  Worked example: scoring blog topics
&lt;/h2&gt;

&lt;p&gt;The content engine behind this blog pulls about 40 stories a day from TechMeme, Reddit and Firecrawl, and a regex scorer decides which ones are worth writing about. In September I wrote a Jev version of that scorer. One request asks six questions about each story:&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;substance&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;noul&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;instructions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Does this contain something concrete to react to — a claim, an outcome, a number, an incident, a decision someone made — as opposed to asking the reader for help or opinions?&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="c1"&gt;// criteria: { true: "...", false: "..." }&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The weights live in code, not in the prompt, so retuning them re-ranks the backlog without spending a token:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;gate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;WEIGHTS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;substance_floor&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;WEIGHTS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;substance_floor&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;j&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;substance&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;normalized&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;merit&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;gate&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;WEIGHTS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;vendor_fluff&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;j&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;vendor_fluff&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first shadow run, read-only next to the regex scorer, promoted three r/devops help threads to long-form. "Do you really test your backup" scored 2.9 of 3 on beat fit and 0.83 on operator angle. Jev was right about both. A backup question from a practitioner is squarely on beat. It's also not a blog post, because it asks instead of reports. The judgment was fine. My weighting was wrong, and the fix was a &lt;code&gt;substance&lt;/code&gt; question applied as a multiplier rather than another additive term. The Jev scorer lives on a branch and only runs in shadow when I invoke it; the regex scorer is what ships today.&lt;/p&gt;

&lt;h2&gt;
  
  
  The numbers, and where the savings leak
&lt;/h2&gt;

&lt;p&gt;I ran the six questions against 50 recent stories, three times each, on Jev, Claude Haiku 4.5 (no extended thinking) and Claude Opus 5 at low effort. Same wording, same criteria, Claude held to the same schema with structured outputs. Total spend: $1.93.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Jev 1.13&lt;/th&gt;
&lt;th&gt;Haiku 4.5&lt;/th&gt;
&lt;th&gt;Opus 5 (low)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Median latency&lt;/td&gt;
&lt;td&gt;163 ms&lt;/td&gt;
&lt;td&gt;1,156 ms&lt;/td&gt;
&lt;td&gt;2,740 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cost per 1M calls&lt;/td&gt;
&lt;td&gt;$53&lt;/td&gt;
&lt;td&gt;$1,610&lt;/td&gt;
&lt;td&gt;$11,183&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tier changed across its own 3 runs&lt;/td&gt;
&lt;td&gt;2 of 50&lt;/td&gt;
&lt;td&gt;20 of 50&lt;/td&gt;
&lt;td&gt;5 of 50&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Prices come from &lt;a href="https://docs.typesafe.ai/models" rel="noopener noreferrer"&gt;TypeSafe's models page&lt;/a&gt; ($0.042 per million input tokens, output free) and &lt;a href="https://platform.claude.com/docs/en/about-claude/pricing" rel="noopener noreferrer"&gt;Anthropic's pricing page&lt;/a&gt;. The Haiku column surprised me. It's the default "cheap classifier," and it was the least consistent model in the test, flipping its own tier on 20 stories out of 50. Prompt caching won't rescue it here either: this prompt is about 1,300 tokens and Haiku 4.5 won't &lt;a href="https://platform.claude.com/docs/en/build-with-claude/prompt-caching" rel="noopener noreferrer"&gt;cache anything under 4,096&lt;/a&gt;. Opus 5 caches from 512 tokens, which brings it to roughly $3,000 per million calls if every call hits the cache. Still about 57x Jev.&lt;/p&gt;

&lt;p&gt;Jev agreed with Opus on 274 of the 300 individual answers. It also landed 26 of 50 stories in a different tier.&lt;/p&gt;

&lt;p&gt;Both are true because Jev scores the whole set higher (median composite 1.73 against Opus at 1.25) while ranking the stories in roughly the same order. My tier cutoffs sit where the old scorer's numbers used to cluster. Move to a model with a different baseline and half the backlog changes tier. Haiku and Opus disagreed on tier for 20 of 50 stories too, so this isn't a Jev defect. &lt;strong&gt;Tier cutoffs belong to the model they were tuned on. A model swap is a change to the agent harness, not a config flag.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The obvious fix is confidence routing: let Jev answer, and send low-confidence cases to Opus. Jev returns a confidence with every &lt;code&gt;score&lt;/code&gt; and &lt;code&gt;choice&lt;/code&gt;. The naive rule, "escalate the story if any answer is below 0.8," sent 48 of 50 stories to Opus, because a four-level rubric rarely puts 80% on a single level. Durability's median confidence was 0.65.&lt;/p&gt;

&lt;p&gt;TypeSafe warns against carrying one threshold across question types, and the data agrees. A per-type rule (escalate when the choice confidence is below 0.8 or any noul lands between 0.3 and 0.7) sent 24 of 50 stories up. That costs $5,421 per million, 2.1x cheaper than Opus alone, and it catches 17 of the 26 answers where the two models disagree. Real, but nowhere near 200x. On a workload where half the stories are genuinely ambiguous, escalation eats most of the savings.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where this breaks
&lt;/h2&gt;

&lt;p&gt;Durability was the weakest question. It caused 12 of the 26 disagreements, and on 8 stories Jev split its probability between two levels at least two steps apart. The score it returns is an expected value, so a story that's either a one-week event or a durable shift comes back as "somewhere in the middle." TypeSafe's docs say not to treat that number as a precise magnitude. My composite divides it by three and adds it in, which is exactly what they say not to do.&lt;/p&gt;

&lt;p&gt;There's no answer key in these numbers. Agreement with Opus is not accuracy, and 50 stories from one domain is a small sample. State is also data Jev doesn't treat as hostile by default, which matters for an agent that reads web pages. And the rate limits on the models page are "adjusting dynamically," which is a sentence to read twice before routing production traffic through one vendor.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to change on Monday
&lt;/h2&gt;

&lt;p&gt;Grep your agent harness for model calls whose output is a label, a score or a boolean. Those are System One questions wearing an LLM's price tag. Shadow them against a System One model for a week, read-only, and log both answers.&lt;/p&gt;

&lt;p&gt;Then do the part the benchmark says is hard. Retune your cutoffs on the new model before you switch, set confidence thresholds per question type, and escalate only where the answer can change the decision. The model is the cheap part now. The 26 misfiled stories came from my loop, not from Jev.&lt;/p&gt;

</description>
      <category>aiagents</category>
      <category>agentharnessdesign</category>
      <category>aiengineering</category>
      <category>platformengineering</category>
    </item>
    <item>
      <title>262 Nights, Zero Bytes, Exit 0: The Failure Mode of Unattended Agent Runs</title>
      <dc:creator>Michael Tuszynski</dc:creator>
      <pubDate>Thu, 24 Sep 2026 18:33:36 +0000</pubDate>
      <link>https://dev.to/michaeltuszynski/262-nights-zero-bytes-exit-0-the-failure-mode-of-unattended-agent-runs-1b3g</link>
      <guid>https://dev.to/michaeltuszynski/262-nights-zero-bytes-exit-0-the-failure-mode-of-unattended-agent-runs-1b3g</guid>
      <description>&lt;h1&gt;
  
  
  262 Nights, Zero Bytes, Exit 0: The Failure Mode of Unattended Agent Runs
&lt;/h1&gt;

&lt;p&gt;A backup job I run synced an empty directory every night for 262 consecutive nights. Every run exited 0. Every run wrote &lt;code&gt;Photo library sync complete&lt;/code&gt; into the log. The off-site copy held zero objects for nine months while the real library sat at 17 GB and 17,658 files on a disk nothing was copying.&lt;/p&gt;

&lt;p&gt;The script's source path pointed somewhere that did not exist. Docker's bind mount then created that path, empty, at the start of every run. The job manufactured the evidence that its own configuration was valid, found nothing there, and synced the nothing it found. A guard like &lt;code&gt;[ -d "$source" ]&lt;/code&gt; would have passed all 262 times.&lt;/p&gt;

&lt;p&gt;I found it by counting objects in the bucket by hand, on an unrelated errand. No monitor caught it, because there was nothing for a monitor to catch. The job ran on schedule. It logged. It exited clean.&lt;/p&gt;

&lt;p&gt;That job is what I think about now when a vendor shows me an agent that works unsupervised for two days.&lt;/p&gt;

&lt;h2&gt;
  
  
  An exit code answers a different question than the one you asked
&lt;/h2&gt;

&lt;p&gt;AWS now sells what it calls frontier agents: autonomous systems that, in its own words, &lt;a href="https://aws.amazon.com/blogs/machine-learning/aws-launches-frontier-agents-for-security-testing-and-cloud-operations/" rel="noopener noreferrer"&gt;"run persistently for hours or days without constant human oversight"&lt;/a&gt;. The Security Agent bills &lt;a href="https://aws.amazon.com/security-agent/pricing/" rel="noopener noreferrer"&gt;$50.00 per task-hour&lt;/a&gt;, and AWS puts an average evaluation at 24 task-hours, so roughly $1,200 a run. I wrote about that &lt;a href="https://www.mpt.solutions/aws-frontier-agents-what-50-hour-pen-testing-and-30-hour-sre-means-for-platform-teams/" rel="noopener noreferrer"&gt;pricing model in April&lt;/a&gt; and I still think the unit economics are sound.&lt;/p&gt;

&lt;p&gt;The economics are not the problem. Supervision is.&lt;/p&gt;

&lt;p&gt;When a run ends, what comes back is a status: it finished, or it didn't. One bit. That bit reports whether the process reached its final line, which is a different question from whether the work happened. My backup reached its final line 262 times.&lt;/p&gt;

&lt;p&gt;For a five-minute job you paper over the gap by looking. You read the output, you spot-check the result, your own attention is the instrument. Stretch the same job to 24 unattended hours and that instrument is gone. What's left is the bit.&lt;/p&gt;

&lt;h2&gt;
  
  
  Success decays exponentially with run length
&lt;/h2&gt;

&lt;p&gt;A 24-hour run isn't a stretched-out 1-hour run. The failure math works against you, and it compounds.&lt;/p&gt;

&lt;p&gt;Toby Ord's analysis of agent benchmark data, &lt;a href="https://arxiv.org/abs/2505.05115" rel="noopener noreferrer"&gt;&lt;em&gt;Is there a half-life for the success rates of AI agents?&lt;/em&gt;&lt;/a&gt;, fits the results to a constant rate of failure per minute of work. That produces exponential decay: success probability falls off a cliff as tasks get longer, and every agent has a characteristic half-life. Failures accumulate across subtasks, and one bad subtask sinks the run.&lt;/p&gt;

&lt;p&gt;METR measures the same curve empirically. Its &lt;a href="https://metr.org/time-horizons/" rel="noopener noreferrer"&gt;time-horizons work&lt;/a&gt; reports the task length at which frontier models succeed 50% of the time, and that 50% threshold is chosen because it's the easiest point to estimate robustly. At the horizon being advertised, the coin is fair.&lt;/p&gt;

&lt;p&gt;Then read the caveat on METR's own page. "Measurements above 16 hrs are unreliable with our current task suite."&lt;/p&gt;

&lt;p&gt;The independent yardstick runs out at 16 hours. The vendor's advertised operating range is hours or days. Past that line there is no public, non-vendor measurement of how often these runs actually succeed. You are not choosing between a measured 94% and a measured 60%. You're choosing between a vendor number and no number.&lt;/p&gt;

&lt;h2&gt;
  
  
  My own agent measurements lied in the same direction
&lt;/h2&gt;

&lt;p&gt;This isn't a hypothetical I imported from a paper. My own eval tooling has handed me clean results that were nothing of the kind.&lt;/p&gt;

&lt;p&gt;A model evaluation I ran logged a 900-second timeout and a 766-second near-timeout for two coding tasks. The CSV was indistinguishable from a model that cannot do the work. A second agent had been pushing large prompts at the same single-slot inference server the whole time. Re-run on a quiet box, the same model and the same tasks passed in 57 and 83 seconds.&lt;/p&gt;

&lt;p&gt;A separate eval launched into a detached tmux session returned two FAIL rows in under a second, with zero-byte logs. The binary wasn't on the PATH that a daemon-spawned shell inherits. That output is indistinguishable from a model that errors instantly.&lt;/p&gt;

&lt;p&gt;Both runs produced a result file. Both exited. Neither measured what it claimed to measure, and in both cases the artifact looked exactly like a real finding, which is the part that costs you. A crash you notice. A plausible wrong answer goes into the spreadsheet.&lt;/p&gt;

&lt;p&gt;The same pattern shows up wherever I stopped watching. A weekly link checker reported &lt;code&gt;Scanned 0 wiki pages / Dead wikilinks: 0&lt;/code&gt; for six weeks, green every time, because it pointed at a directory a migration had retired. Repointed, the same code immediately found 261 pages and 116 dead links. A watchdog probing an embedding server's &lt;code&gt;/api/version&lt;/code&gt; endpoint stayed green for two days while every actual embed call returned a 500 and the index took in nothing.&lt;/p&gt;

&lt;h2&gt;
  
  
  A 94% accuracy score is computed over the runs somebody checked
&lt;/h2&gt;

&lt;p&gt;The strongest objection is that this is all anecdote against data. AWS reports &lt;a href="https://aws.amazon.com/blogs/machine-learning/aws-launches-frontier-agents-for-security-testing-and-cloud-operations/" rel="noopener noreferrer"&gt;up to 75% lower MTTR, 80% faster investigations, and 94% root cause accuracy&lt;/a&gt; from preview customers, and one university cut incident resolution from two hours to 28 minutes. Those are outcomes, not vibes.&lt;/p&gt;

&lt;p&gt;Look at how that number gets built. A root-cause accuracy figure is computed over runs whose root cause somebody established independently. To score a run, you need ground truth, which means a human went and checked. The denominator is runs that were verified.&lt;/p&gt;

&lt;p&gt;The failure class I'm describing never enters that denominator. A run that reports success, produces a clean artifact, and is wrong is scored as correct unless somebody goes looking, and the whole premise of an unattended agent is that nobody does. My backup would have scored 262 for 262 on any accuracy metric derived from its own output.&lt;/p&gt;

&lt;p&gt;These are also first-party numbers from preview customers selected by the vendor. That's not disqualifying. It does mean they're a ceiling rather than an expectation, and they answer "how often is a checked run right," not "how often does an unchecked run lie."&lt;/p&gt;

&lt;h2&gt;
  
  
  Assert the artifact, not the exit status
&lt;/h2&gt;

&lt;p&gt;The fix isn't more monitoring. It's changing what gets asserted. The backup is the shortest example I have:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Before: the job's own bind mount created $SRC empty, so this always passed.&lt;/span&gt;
&lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$SRC&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"missing source"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;exit &lt;/span&gt;1&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;
rclone &lt;span class="nb"&gt;sync&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$SRC&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$DEST&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="c"&gt;# After: refuse an empty source, and log what actually moved.&lt;/span&gt;
&lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;find &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$SRC&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-type&lt;/span&gt; f &lt;span class="nt"&gt;-print&lt;/span&gt; &lt;span class="nt"&gt;-quit&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"REFUSING: &lt;/span&gt;&lt;span class="nv"&gt;$SRC&lt;/span&gt;&lt;span class="s2"&gt; is empty — not syncing a deletion"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;exit &lt;/span&gt;1&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;
rclone &lt;span class="nb"&gt;sync&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$SRC&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$DEST&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
rclone size &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$DEST&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;   &lt;span class="c"&gt;# object count and bytes, into the log, every single run&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Refuse a degenerate input instead of processing it. &lt;code&gt;rclone sync&lt;/code&gt; mirrors deletions, so a wrong path doesn't merely fail to back things up, it erases the copy you had. Emit a number that a zero-work run cannot produce. And push the heartbeat only when a leg moved a non-empty source, so a silent no-op goes stale and pages somebody.&lt;/p&gt;

&lt;p&gt;For an agent run the shape is identical. Don't ask whether the run finished. Ask what it changed: how many findings, how many files touched, how many tests that were red are now green, how many tokens against how many accepted diffs. Then set a floor. A 24-hour pen test that reports zero findings and zero requests issued has told you about your instrumentation, not your security posture.&lt;/p&gt;

&lt;p&gt;The cheap version of this test: can this check ever come back red? If a run that did nothing would produce output identical to a run that worked, you don't have a check. You have a log line.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where this doesn't apply
&lt;/h2&gt;

&lt;p&gt;Exploratory work has no assertable artifact. When you turn an agent loose on "look at this system and tell me what's odd," there's no count to floor and no diff to measure, and you're back to reading the transcript. Outcome assertions don't help there, and pretending otherwise gives you a number that means nothing.&lt;/p&gt;

&lt;p&gt;The threshold is unattended duration, not autonomy. A 20-minute agent run you watch needs none of this. An hour-long cron job nobody has opened since 2025 needs all of it. Agents make it urgent because the unattended part is the product, sold at durations past where anyone outside the vendor has measured the success rate.&lt;/p&gt;

&lt;p&gt;262 nights is the number I'd put in front of anyone signing off on a Friday-to-Monday agent run. Not because a backup script is an agent, but because it's the floor: a deterministic shell script, no model in the loop, nothing nondeterministic about it, wrong every night for nine months while reporting success. Add nondeterminism and a 50% time horizon on top of that and the exit code is worth less, not more.&lt;/p&gt;

&lt;p&gt;Decide what a successful run must produce, in a number, before you let anything run for a day. Otherwise you'll do what I did, and find out by hand, on an errand, in month nine.&lt;/p&gt;

</description>
      <category>aiagents</category>
      <category>platformengineering</category>
      <category>aireliability</category>
      <category>devops</category>
    </item>
    <item>
      <title>6 Agent Licenses, 1 Runtime: You Are Paying Per Seat for the Layer That Is Not Scarce</title>
      <dc:creator>Michael Tuszynski</dc:creator>
      <pubDate>Mon, 07 Sep 2026 20:21:20 +0000</pubDate>
      <link>https://dev.to/michaeltuszynski/6-agent-licenses-1-runtime-you-are-paying-per-seat-for-the-layer-that-is-not-scarce-mnh</link>
      <guid>https://dev.to/michaeltuszynski/6-agent-licenses-1-runtime-you-are-paying-per-seat-for-the-layer-that-is-not-scarce-mnh</guid>
      <description>&lt;p&gt;Six licenses. One runtime. That's the shape of most agent quotes on the table right now, and the invoice is inverted: the six licenses are the big line, and the runtime, the thing that actually consumes scarce compute, is either a rounding error or a separate purchase order nobody brought to the renewal meeting.&lt;/p&gt;

&lt;p&gt;Microsoft published the split more clearly than anyone else did. The E7 bundle lists at &lt;a href="https://www.microsoft.com/en-us/microsoft-365/enterprise/microsoft-365-plans-and-pricing" rel="noopener noreferrer"&gt;$99 per user per month&lt;/a&gt;. To be fair to that number, it buys the whole suite, Office and Teams and the security stack, with agent capability folded in; it is the cleanest public example of the split, not a pure agent-seat price. SAMexpert's breakdown of the license puts the split plainly: that layer grants agent capability, &lt;a href="https://samexpert.com/microsoft-365-e7-licensing-guide/" rel="noopener noreferrer"&gt;it does not run them&lt;/a&gt;. Execution bills separately, in Agent Compute Units, through a pre-purchase plan sold in blocks. Two layers, two meters, one signature.&lt;/p&gt;

&lt;p&gt;Treat that as the reference architecture for every agent quote you get this year, whoever the vendor is.&lt;/p&gt;

&lt;h2&gt;
  
  
  The seat layer and the runtime layer are priced by different physics
&lt;/h2&gt;

&lt;p&gt;The interface layer is software. Marginal cost of a seventh seat: approximately zero. The vendor's cost doesn't move when you add a user who opens the panel twice a week.&lt;/p&gt;

&lt;p&gt;The runtime layer is electricity, silicon, and a queue. Marginal cost of the ten-thousandth agent run is real, and it's the only part of the stack where the vendor is buying something scarce from someone else.&lt;/p&gt;

&lt;p&gt;So when a quote grows by multiplying the free-to-produce layer and holds the expensive layer flat or unbundled, that's not a pricing accident. That's the whole design. You're being metered on the thing that isn't scarce.&lt;/p&gt;

&lt;p&gt;Run the six seats: 6 × $99 = &lt;strong&gt;$594/month&lt;/strong&gt;, &lt;strong&gt;$7,128/year&lt;/strong&gt;, before a single agent executes anything. Now ask how many of those six drove an agent in the last 30 days. If it's two, one platform engineer and one person in support automation, the real cost per active agent user is &lt;strong&gt;$297/month&lt;/strong&gt;, and the other four seats bought a login.&lt;/p&gt;

&lt;h2&gt;
  
  
  "Per seat" hides decisions your contract already made
&lt;/h2&gt;

&lt;p&gt;The phrase does a lot of undisclosed work. Per-seat licensing is &lt;a href="https://softwarepricing.com/blog/per-seat-licensing-variations/" rel="noopener noreferrer"&gt;an umbrella covering separate decisions&lt;/a&gt; about what the unit attaches to, what time window counts it, and which type of user holds it. Vendors pick one combination, print two words, and let you assume you know which one you bought.&lt;/p&gt;

&lt;p&gt;Two of those decisions bite agent deals in particular. Named-user counts never go down mid-term; concurrent counts can, so ask which one is in your paper. It's usually in a definitions exhibit, not the order form. And the service identity your agent runs under may be counted as a seat even though no human ever touches it. That one line can double a bill.&lt;/p&gt;

&lt;h2&gt;
  
  
  The runtime is getting cheaper faster than the seat is
&lt;/h2&gt;

&lt;p&gt;The direction of travel is the part procurement keeps missing. Inference prices have fallen with every model generation, and a vendor's runtime rate card follows them down inside a contract term. Per-seat list prices move once a year, and not downward.&lt;/p&gt;

&lt;p&gt;If a seat price was originally set to recover runtime cost, and runtime cost drops 60% over a term, the seat price should move. It won't unless you make it a renewal condition. Put a benchmark clause in: seat price adjusts with the vendor's own published runtime rate card, or you get the delta as runtime credits.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the seat is actually buying, and why that argument doesn't survive contact
&lt;/h2&gt;

&lt;p&gt;The strongest counterargument is real, so take it seriously: the seat isn't paying for compute at all. It pays for identity and SSO, audit logging, data-loss prevention, admin policy, connectors into your existing data, and a support contract with a throat to choke. Those are per-human costs. Charging per human is honest.&lt;/p&gt;

&lt;p&gt;Fine. Then price them as such, and watch the argument come apart on its own mechanism.&lt;/p&gt;

&lt;p&gt;Governance costs scale with directory objects and policy scope, not with who happens to use an agent. If audit and DLP are what the seat buys, they should survive the removal of the agent entitlement. So ask for that quote: the seat, priced without agent capability. Most vendors won't produce it, because the number would be embarrassing next to the one with the agent checkbox on.&lt;/p&gt;

&lt;p&gt;The gap between those two quotes isn't governance. It's a right to consume, sold as a subscription. And a right to consume, when the consumption is metered separately anyway, is a toll booth on a road you already paid to drive.&lt;/p&gt;

&lt;p&gt;The vendor's own bundle proves this. E7's seat layer explicitly doesn't execute agents. The execution meter is a different SKU with a different plan. Every argument that the seat covers runtime is refuted by the price page it came from.&lt;/p&gt;

&lt;h2&gt;
  
  
  The worksheet to take into the renewal
&lt;/h2&gt;

&lt;p&gt;Split the quote before you sign it. It fits in your head. Do it on paper anyway, before you sign:&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="c1"&gt;// renewal-worksheet.ts — split the bill by layer before signing&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;quote&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;seats&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;seatPrice&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;99&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;          &lt;span class="c1"&gt;// $/user/month — interface layer, ~$0 marginal cost to vendor&lt;/span&gt;
  &lt;span class="na"&gt;activeAgentUsers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;    &lt;span class="c1"&gt;// drove an agent at least once in last 30 days&lt;/span&gt;
  &lt;span class="na"&gt;runtimeSpendMonthly&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="c1"&gt;// metered execution, separate PO&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;seatBill&lt;/span&gt;    &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;quote&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;seats&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;quote&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;seatPrice&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;              &lt;span class="c1"&gt;// 594&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;perActive&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;seatBill&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;quote&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;activeAgentUsers&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;          &lt;span class="c1"&gt;// 297&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;runtimeShare&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;quote&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;runtimeSpendMonthly&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt;
                     &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;seatBill&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;quote&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;runtimeSpendMonthly&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;    &lt;span class="c1"&gt;// 0.00&lt;/span&gt;

&lt;span class="c1"&gt;// If runtimeShare &amp;lt; 0.25, you are buying interface, not capability.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That last line is the test. When less than a quarter of your total agent spend is execution, the vendor has moved the money to the layer where scarcity doesn't apply, and your growth path is buying more logins rather than more work.&lt;/p&gt;

&lt;p&gt;In the agent pipeline I run there is no seat at all. The interface is a terminal and a queue directory, and every dollar is metered execution: drafting this post cost $1.11 in model calls, and a second operator would have added nothing to the bill.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where this argument breaks
&lt;/h2&gt;

&lt;p&gt;Three places, honestly.&lt;/p&gt;

&lt;p&gt;Small teams under ten people often can't get consumption pricing at all. Vendors gate metered plans behind enterprise minimums, and a flat seat price is the cheaper administrative outcome even when the unit economics are ugly. Take the seats and stop optimizing.&lt;/p&gt;

&lt;p&gt;Regulated buyers who need contractual data residency, retention guarantees, and a named DPA are buying something the runtime layer doesn't include. That's a legitimate per-tenant cost that a per-seat model approximates badly but does cover.&lt;/p&gt;

&lt;p&gt;And if your agent usage is genuinely uniform, every seat driving the runtime every day, the two models converge and the seat is just a prepaid meter with less paperwork. Usage is rarely that flat. The common shape is a few heavy operators and a long tail of people who opened the panel twice.&lt;/p&gt;

&lt;p&gt;None of that changes the ask. Get the runtime quoted as its own line, in its own units, with its own rate card. Then look at the two numbers side by side.&lt;/p&gt;

&lt;p&gt;Six seats at $99 is $7,128 a year. Ask your vendor what the runtime under those six seats cost last quarter. If they can't tell you, they're not selling you compute. And if they can, you finally know which of the two numbers to negotiate.&lt;/p&gt;

</description>
      <category>aiagents</category>
      <category>enterprisesoftware</category>
      <category>cloudcomputing</category>
      <category>aistrategy</category>
    </item>
    <item>
      <title>Fit Is a Claim You Have to Be Able to Test</title>
      <dc:creator>Michael Tuszynski</dc:creator>
      <pubDate>Thu, 03 Sep 2026 02:08:21 +0000</pubDate>
      <link>https://dev.to/michaeltuszynski/fit-is-a-claim-you-have-to-be-able-to-test-4m5a</link>
      <guid>https://dev.to/michaeltuszynski/fit-is-a-claim-you-have-to-be-able-to-test-4m5a</guid>
      <description>&lt;p&gt;Every rejection note has a house style. We've thought more about what this role needs, and it isn't the right match. Sometimes that's the honest summary of a real finding. More often it's a verdict from a process that never collected the evidence for it.&lt;/p&gt;

&lt;p&gt;Fit is the most load-bearing word in startup hiring and the least disciplined one. It decides more outcomes than the take-home and the panel debrief combined, and unlike those it goes unwritten. Nobody signs their name to it. It shows up at the end, in the summary email, after a loop that tested something else entirely.&lt;/p&gt;

&lt;h2&gt;
  
  
  Nobody is a fit in the abstract
&lt;/h2&gt;

&lt;p&gt;The claim always has four parts: this person, in this seat, under these constraints, against this bar. Drop one and you're describing a personality, which is neither testable nor any of your business.&lt;/p&gt;

&lt;p&gt;Two versions of the same criterion. First: "works well with others." Second: "will carry the pager alone through a weekend for a system they didn't build, and will wake a colleague when the runbook is wrong rather than guess."&lt;/p&gt;

&lt;p&gt;The first is a vibe. The second can be wrong, which means it can be checked. Build a round that produces evidence against it and you'll find out. There's no round you can build for "works well with others," which is why teams that lean on it end up grading candidates against whichever colleague they happen to resemble.&lt;/p&gt;

&lt;p&gt;The rewrite costs an afternoon. Most startups skip it because the founders think they already agree on what the seat is. They don't, and the loop is where that surfaces.&lt;/p&gt;

&lt;h2&gt;
  
  
  If no round can test it, it isn't a reason
&lt;/h2&gt;

&lt;p&gt;Every criterion in your fit definition gets a named round attached to it — the round that could produce evidence against it. If nothing on the schedule can do that, the criterion is an impression, and impressions don't get to reject people.&lt;/p&gt;

&lt;p&gt;Interviewers aren't neutral instruments. Jason Dana, Robyn Dawes, and Nathanial Peterson ran the cleanest demonstration I know of. In the first of three studies, participants predicting other students' semester GPAs formed impressions &lt;a href="https://jbaron.org/journal/12/121130a/jdm121130a.html" rel="noopener noreferrer"&gt;just as confidently when the interviewee was answering with a random response system&lt;/a&gt; as when the answers were real, and they predicted worse than if they'd skipped the interview and used prior GPA alone. Watching an interview didn't hurt. Conducting one did. Their abstract ends on a single sentence: "Our simple recommendation for those making screening decisions is not to use them."&lt;/p&gt;

&lt;p&gt;That's students predicting GPAs, not managers predicting job performance, and the gap matters. What transfers is the mechanism: a confident read built on noise, crowding out the information you already had.&lt;/p&gt;

&lt;p&gt;Structure is the fix. Google reports that its structured loops predicted on-the-job performance across functions and levels, saved about &lt;a href="https://rework.withgoogle.com/intl/en/guides/a-guide-to-structured-interviewing-for-better-hiring-practices" rel="noopener noreferrer"&gt;40 minutes per interview&lt;/a&gt; in interviewer time, and left rejected candidates 35% happier than those who went through an unstructured one. That's a company grading its own homework, so take it as a ceiling. The independent version is stronger than Google's marketing needs it to be. When Sackett and colleagues &lt;a href="https://pubmed.ncbi.nlm.nih.gov/34968080/" rel="noopener noreferrer"&gt;re-ran the personnel-selection meta-analyses&lt;/a&gt; and corrected for decades of overstated range-restriction adjustments, most selection methods lost validity. Structured interviews came out top-ranked anyway.&lt;/p&gt;

&lt;p&gt;Sit with the rejected-candidates number for a second. People can accept losing to a bar. Losing to a mood is what they don't forgive.&lt;/p&gt;

&lt;p&gt;You didn't fail by interviewing. You failed by grading a criterion nothing in the loop assessed. Reject someone on collaboration style after four rounds that only ever probed technical depth and you've written a conclusion with no data under it. "Fit" is the word that covers the gap.&lt;/p&gt;

&lt;h2&gt;
  
  
  The seat has to exist before anyone can miss it
&lt;/h2&gt;

&lt;p&gt;The most common fit failure is a role-definition failure wearing a candidate's face.&lt;/p&gt;

&lt;p&gt;Watch a fast-moving company open a seat nobody has held yet. Call it the first data engineer. One interviewer describes someone who builds the warehouse and owns the models end to end. Another describes a partner to the analysts, mostly unblocking their queries. A third thinks the job is finally making the dashboards trustworthy. All three are senior. All three are sincere. All three are describing different jobs. The candidate answers each of them accurately and then gets measured against a composite that exists in nobody's head.&lt;/p&gt;

&lt;p&gt;This isn't only a hiring problem. Gallup's engagement indicator puts the share of U.S. employees who say they know what's expected of them at work at &lt;a href="https://www.gallup.com/394373/indicator-employee-engagement.aspx" rel="noopener noreferrer"&gt;49% as of May 2026&lt;/a&gt;. Role clarity is scarce for people who already have the job. Expecting it to show up spontaneously for a job that doesn't exist yet is optimistic.&lt;/p&gt;

&lt;p&gt;Here's a cheap diagnostic, and a slightly brutal one. Before the req goes out, have every interviewer write one paragraph, separately: what does this person do in week six, who do they answer to day to day, what do they own that nobody else touches. Read the paragraphs side by side. If they don't converge, there's no seat yet. There's a wish, and nobody can fit a wish.&lt;/p&gt;

&lt;h2&gt;
  
  
  Your loop is the honest demo
&lt;/h2&gt;

&lt;p&gt;Culture is behavior under constraint. A hiring loop is a constraint, so the loop is the most honest thing a candidate sees.&lt;/p&gt;

&lt;p&gt;They read it as an operating model, because that's what it is. Rounds that appear and disappear mid-process. Headcount numbers that don't agree between two conversations a week apart. A decision-maker named early who never materializes. A timeline that quietly doubles. None of that reads as a scheduling problem — it reads as how you behave when you're busy. The people who pick up on it fastest are the ones with other options, which is a filter you didn't design and wouldn't choose.&lt;/p&gt;

&lt;p&gt;If your loop can't hold its shape for six weeks under normal load, better to learn that before a real deadline tests the same thing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Write the fit spec
&lt;/h2&gt;

&lt;p&gt;The artifact is small: one table, agreed before the req goes live, a row per criterion. Here's one filled in for that first data engineer. The rows change per role. The shape doesn't.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Claim about the seat&lt;/th&gt;
&lt;th&gt;What would disprove it&lt;/th&gt;
&lt;th&gt;Round that tests it&lt;/th&gt;
&lt;th&gt;Disqualifying signal&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Ships against a charter nobody has written yet&lt;/td&gt;
&lt;td&gt;Needs the spec settled before starting&lt;/td&gt;
&lt;td&gt;Scenario round where the requirements contradict each other&lt;/td&gt;
&lt;td&gt;Asks for the org chart before the problem&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Makes the analysts self-sufficient instead of becoming their queue&lt;/td&gt;
&lt;td&gt;Optimizes for doing the work themselves&lt;/td&gt;
&lt;td&gt;Live teach-back on something they built&lt;/td&gt;
&lt;td&gt;Can't explain the work to a non-specialist&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tolerates a role whose shape changes every quarter&lt;/td&gt;
&lt;td&gt;Wants a fixed remit before committing&lt;/td&gt;
&lt;td&gt;Direct question, asked the same way of every candidate&lt;/td&gt;
&lt;td&gt;Treats a scope change as a broken promise&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Picks a schema under incomplete information and defends it later&lt;/td&gt;
&lt;td&gt;Waits for more data before deciding&lt;/td&gt;
&lt;td&gt;Design round with a deliberately thin brief&lt;/td&gt;
&lt;td&gt;Won't commit to a call under uncertainty&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Then one governing rule. Rejection reasons must come off the table. If someone in the debrief raises a reason that isn't on it, either the spec was incomplete — add the row, re-run the round that tests it — or the reason wasn't real, and it doesn't get to decide.&lt;/p&gt;

&lt;p&gt;I build this kind of thing for a living, though usually for machines. &lt;a href="https://github.com/michaeltuszynski/rubric-bench" rel="noopener noreferrer"&gt;rubric-bench&lt;/a&gt; exists because an LLM scoring free-text answers is a production dependency: you pin a golden set of answers to known verdicts, re-score after every prompt edit or model bump, and fail the build when the verdicts move. Nobody would ship a grader that way and skip the tests. A hiring panel is the same object with worse instrumentation — a grader nobody regression-tests, running a rubric nobody wrote down.&lt;/p&gt;

&lt;p&gt;The spec isn't free. Four rows with a round attached to each adds a week to a loop that a twenty-person company may not have. Published criteria are rehearsable: say "we test for scope pushback" out loud and you'll meet candidates who've practiced pushing back on scope. And a spec tight enough to be testable will screen out the atypical hire who would have redefined the seat. Pay it anyway. The alternative isn't "no spec." It's a spec living in four people's heads, applied at the end, where nobody has to defend it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The counter-argument
&lt;/h2&gt;

&lt;p&gt;Fit isn't fake, and treating it as pure bias overcorrects.&lt;/p&gt;

&lt;p&gt;Lauren Rivera's study of elite professional service firms found that evaluators sought candidates culturally similar to themselves in leisure pursuits, experiences, and self-presentation, and that &lt;a href="https://www.asanet.org/wp-content/uploads/savvy/journals/ASR/Dec12ASRFeature.pdf" rel="noopener noreferrer"&gt;concerns about shared culture "often outweighed concerns about absolute productivity"&lt;/a&gt;. Read closely, that's a finding about unstated fit rather than fit as such. Her setting was law, banking, and consulting, not early-stage startups, and it's a case study built from 120 interviews rather than a controlled trial. The mechanism travels anyway. A criterion nobody wrote down collapses into similarity-to-me, because that's the only yardstick left in the room.&lt;/p&gt;

&lt;p&gt;Genuine style mismatches exist too. Someone who does their best work in a quiet queue will be miserable in a seat that's five customer calls a day, and hiring them is unkind to both parties. So style can be a real reason. What it can't be is unstated, because unstated fit is unfalsifiable, and unfalsifiable reasons attract every bias you have. Name the constraint in the job post and you've been fair. Discover it in round four and call it fit, and you haven't.&lt;/p&gt;

&lt;h2&gt;
  
  
  Back to the rejection note
&lt;/h2&gt;

&lt;p&gt;Write the spec before the req, attach a round to every row, and forbid off-spec reasons in the debrief. That part's mechanical.&lt;/p&gt;

&lt;p&gt;The sentence at the end is the harder part. Tell people what you actually decided against. "We wanted someone who's built a warehouse from nothing before, and we couldn't establish that from the loop" is a sentence a person can act on, and one you can only write if you did the work to know it. "Not the right match on either side" is what you send when you didn't.&lt;/p&gt;

</description>
      <category>hiring</category>
      <category>startupleadership</category>
      <category>peopleops</category>
      <category>interviewdesign</category>
    </item>
    <item>
      <title>Context Is Four Different Words: The Window, The Corpus, The File, and The Session</title>
      <dc:creator>Michael Tuszynski</dc:creator>
      <pubDate>Sun, 16 Aug 2026 16:34:16 +0000</pubDate>
      <link>https://dev.to/michaeltuszynski/context-is-four-different-words-the-window-the-corpus-the-file-and-the-session-30p6</link>
      <guid>https://dev.to/michaeltuszynski/context-is-four-different-words-the-window-the-corpus-the-file-and-the-session-30p6</guid>
      <description>&lt;p&gt;Someone on your team says the agent "needs better context." Four people nod. They are agreeing about a word and disagreeing about the work, and nobody finds out until two of them have built the wrong thing.&lt;/p&gt;

&lt;p&gt;I run four systems that all get called context, and they have almost nothing in common. One is a hard capacity limit. One is a 35-million-token corpus. One is a 4.7KB file I hand-edited last week. One evaporates when the session ends. Same word, four owners, four failure modes, four different afternoons of work.&lt;/p&gt;

&lt;p&gt;Here is how I keep them apart.&lt;/p&gt;

&lt;h2&gt;
  
  
  Job one: context as capacity
&lt;/h2&gt;

&lt;p&gt;This is context as a number. The window. It has a ceiling, you can exceed it, and when you do, something gets dropped or summarized whether you like it or not.&lt;/p&gt;

&lt;p&gt;Capacity is the only one of the four you can measure without judgment, which is why it dominates conversation. It is also the least interesting, because being under the limit tells you nothing about whether the right things are in there.&lt;/p&gt;

&lt;p&gt;My own number is worth stating plainly. Before I type a single word, my agent has already loaded a global instruction file, a shared cross-tool rules file, a workspace guide, a memory index, and a lessons file. Together that is about 75,000 characters, call it 19,000 tokens, spent every session on standing orders. That is my floor, not my usage.&lt;/p&gt;

&lt;h2&gt;
  
  
  Job two: context as corpus
&lt;/h2&gt;

&lt;p&gt;This is context as a pile of things you &lt;em&gt;could&lt;/em&gt; retrieve. Vector store, RAG index, semantic search. It is not in the window; it is the universe you draw from to fill the window.&lt;/p&gt;

&lt;p&gt;Mine is Postgres with pgvector, embeddings served by a local model:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;select&lt;/span&gt; &lt;span class="k"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;token_count&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="k"&gt;count&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="n"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;avg&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;token_count&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
  &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="n"&gt;chunks&lt;/span&gt; &lt;span class="k"&gt;where&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'active'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;-- 35,035,979 | 159,669 | 219&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Thirty-five million tokens of retrievable material against a window that holds a fraction of a percent of it. The binding constraint was never storage. It is selection.&lt;/p&gt;

&lt;p&gt;The breakdown is the part that changed how I think about this. Of those 159,669 chunks, session transcripts account for 99,259. Archived history from two prior systems adds another 51,165. The hand-curated knowledge vault — the notes I actually wrote on purpose — is &lt;strong&gt;2,515 chunks, about 1.6% of the corpus&lt;/strong&gt;. The rest is exhaust.&lt;/p&gt;

&lt;p&gt;That ratio is not a bug I am about to fix. It is what a retrieval corpus looks like after eighteen months: mostly a record of what happened, with a thin seam of what you decided.&lt;/p&gt;

&lt;h2&gt;
  
  
  Job three: context as instruction
&lt;/h2&gt;

&lt;p&gt;This is context as a durable file you hand-author and version. &lt;code&gt;CLAUDE.md&lt;/code&gt;, &lt;code&gt;AGENTS.md&lt;/code&gt;, cursor rules, system prompts checked into the repo. It loads every session, it is read by a machine but written by a person, and it goes stale silently.&lt;/p&gt;

&lt;p&gt;The artifact:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;ls&lt;/span&gt; &lt;span class="nt"&gt;-la&lt;/span&gt; ~/.codex/AGENTS.md
~/.codex/AGENTS.md -&amp;gt; /Users/mpt/.claude/AGENTS.core.md
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One 4.7KB file of rules — never commit secrets, never deploy without asking, batch privileged steps into one paste-able block — symlinked so that two different vendors' coding agents read the identical text. I edit one file; Claude Code and Codex both change behavior. That is context as configuration, and it has more in common with a dotfile than with anything in jobs one or two.&lt;/p&gt;

&lt;p&gt;Its failure mode is drift, not overflow. A stale rule is worse than a missing one, because the agent follows it confidently.&lt;/p&gt;

&lt;h2&gt;
  
  
  Job four: context as session state
&lt;/h2&gt;

&lt;p&gt;This is what is live right now: the conversation so far, what you already tried, the decision you made twenty minutes ago and have not written down. It is the only one of the four that dies on restart.&lt;/p&gt;

&lt;p&gt;Everyone underestimates this one until a session compacts mid-task and the agent cheerfully re-litigates something it settled an hour ago. The fix is unglamorous. Write state to disk on purpose, or accept that it is gone.&lt;/p&gt;

&lt;h2&gt;
  
  
  The two axes nobody separates
&lt;/h2&gt;

&lt;p&gt;Capacity and curation are not the same axis, and treating them as one is where the money goes.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Small window&lt;/th&gt;
&lt;th&gt;Large window&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Curated input&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Fast, cheap, works&lt;/td&gt;
&lt;td&gt;Works, costs more than it needs to&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Uncurated input&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Overflows immediately&lt;/td&gt;
&lt;td&gt;Fits, and quietly degrades&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The bottom-right cell is the expensive one, because everything looks fine. It fits. Nothing errors. The output is just worse in ways that do not announce themselves.&lt;/p&gt;

&lt;p&gt;Anthropic's engineering team makes this argument &lt;a href="https://www.anthropic.com/engineering/effective-context-engineering-for-ai-agents" rel="noopener noreferrer"&gt;in their context-engineering post&lt;/a&gt;, framing the model as having an "attention budget" and defining good practice as finding "the smallest possible set of high-signal tokens." Take the framing seriously but note the position: the company describing your window as a scarce resource also sells the window. That is a claim worth checking against someone with no such incentive.&lt;/p&gt;

&lt;p&gt;There is one. Liu et al., published in &lt;a href="https://aclanthology.org/2024.tacl-1.9/" rel="noopener noreferrer"&gt;TACL&lt;/a&gt;, tested how models actually use long inputs on multi-document QA and key-value retrieval. Performance was highest when the relevant information sat at the beginning or the end of the input and dropped when the model had to reach into the middle — and that held &lt;strong&gt;even for models explicitly built for long contexts&lt;/strong&gt;. That is peer-reviewed, it is not selling a context window, and it says position inside the window changes the answer. Capacity is not the variable. Placement is.&lt;/p&gt;

&lt;p&gt;The widely-cited &lt;a href="https://research.trychroma.com/context-rot" rel="noopener noreferrer"&gt;"context rot" work&lt;/a&gt; points the same direction, and the lab behind it sells a vector database, so "long contexts degrade, retrieve instead" is a conclusion they profit from. It survives the objection mainly because Anthropic cites it against its own product interest.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where this breaks
&lt;/h2&gt;

&lt;p&gt;The four-way split is vocabulary, and vocabulary has a tax. On a two-person team where the same person owns all four systems, this is overhead — go build something. The split earns its keep when the four jobs have different owners, which is exactly when the argument starts.&lt;/p&gt;

&lt;p&gt;It also breaks at the edges. Prompt caching sits across capacity and instruction. A compaction step is session state being rewritten into capacity. I am drawing lines through something continuous because the lines make meetings shorter, not because the territory has them.&lt;/p&gt;

&lt;h2&gt;
  
  
  The question I ask now
&lt;/h2&gt;

&lt;p&gt;When someone says we need better context, I ask which one before anything else: &lt;strong&gt;is this a room problem, a retrieval problem, a rules-file problem, or a state problem?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Room is a budgeting question. Retrieval is a ranking and evals question. A rules file is a twenty-minute edit. State is a persistence question. Four different afternoons, four different owners, one shared noun.&lt;/p&gt;

&lt;p&gt;I got the shape of this from Shawn Wallace, who &lt;a href="https://www.shawnewallace.com/2026-08-12-lets-talk-about-agents/" rel="noopener noreferrer"&gt;ran the same play on the word "agent"&lt;/a&gt; and found three jobs hiding under it. The form travels because the industry keeps doing this: a word gets useful, then absorbs every adjacent thing anyone shipped, then stops carrying information.&lt;/p&gt;

&lt;p&gt;Writing this post, I ran the count against my own corpus and got 159,669 active chunks. My workspace guide — the instruction file, job three, the one loaded into every session I start — said 126,000. It had been verified eight days earlier. The file whose job is to describe the corpus was wrong about the corpus, and it had been telling every agent I ran the wrong number all week.&lt;/p&gt;

&lt;p&gt;Job three going stale about job two. I had to write the taxonomy to catch it.&lt;/p&gt;

</description>
      <category>aiagents</category>
      <category>aiengineering</category>
      <category>contextengineering</category>
      <category>platformengineering</category>
    </item>
    <item>
      <title>Karpathy's LLM Wiki Works. Here's What Broke When I Ran It at Real Scale.</title>
      <dc:creator>Michael Tuszynski</dc:creator>
      <pubDate>Thu, 13 Aug 2026 18:38:58 +0000</pubDate>
      <link>https://dev.to/michaeltuszynski/karpathys-llm-wiki-works-heres-what-broke-when-i-ran-it-at-real-scale-28jk</link>
      <guid>https://dev.to/michaeltuszynski/karpathys-llm-wiki-works-heres-what-broke-when-i-ran-it-at-real-scale-28jk</guid>
      <description>&lt;p&gt;Andrej Karpathy's &lt;a href="https://gist.github.com/karpathy/442a6bf555914893e9891c11519de94f" rel="noopener noreferrer"&gt;LLM Wiki gist&lt;/a&gt; from April is right about the important part: for personal knowledge, a markdown wiki that Claude maintains beats a RAG pipeline. I've been running a version of this pattern in production against my own life for six months. Karpathy is right — and he stopped at exactly the point where the interesting problems start.&lt;/p&gt;

&lt;p&gt;Here's the pattern, briefly, then what actually breaks.&lt;/p&gt;

&lt;h2&gt;
  
  
  The claim, in one paragraph
&lt;/h2&gt;

&lt;p&gt;You keep raw sources in one directory. You keep a wiki of markdown pages in another. A &lt;code&gt;CLAUDE.md&lt;/code&gt; file tells Claude how the wiki works: naming, folder layout, what to do on ingest, how to handle contradictions. New source comes in, Claude reads it, writes or updates concept pages, links them with &lt;code&gt;[[wikilinks]]&lt;/code&gt;, updates an index. When you ask a question, Claude reads the wiki, not the raw sources — the synthesis has already been done at ingest time. The whole thing sits under ~100k tokens, so Claude can hold the index in context and reason over it directly. No embeddings, no vector store, no infrastructure. Two clean community implementations to start from: &lt;a href="https://github.com/nvk/llm-wiki" rel="noopener noreferrer"&gt;nvk/llm-wiki&lt;/a&gt; and &lt;a href="https://github.com/Ar9av/obsidian-wiki" rel="noopener noreferrer"&gt;Ar9av/obsidian-wiki&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;That's the whole thing. It works. It works better than you'd expect. And every problem I've hit with it shows up in the same three places.&lt;/p&gt;

&lt;h2&gt;
  
  
  My setup, so the failure modes are grounded
&lt;/h2&gt;

&lt;p&gt;Mine is a NEXUS workspace: an Obsidian vault split into &lt;code&gt;000-inbox/&lt;/code&gt;, &lt;code&gt;200-knowledge/&lt;/code&gt;, &lt;code&gt;300-entities/&lt;/code&gt;, &lt;code&gt;400-daily/&lt;/code&gt;, plus per-domain context files under &lt;code&gt;200-knowledge/context/&lt;/code&gt; — one each for finance, health, content, jobsearch. Every domain has its own schema and its own conventions. A top-level &lt;code&gt;CLAUDE.md&lt;/code&gt; sets the operating rules; each domain file specializes them. Entities (people, companies, services, properties) are single markdown files named by their wikilink text, so &lt;code&gt;[[Janney]]&lt;/code&gt; grep-searches straight to &lt;code&gt;vault/300-entities/orgs/Janney.md&lt;/code&gt;. Daily logs at &lt;code&gt;vault/400-daily/YYYY-MM-DD.md&lt;/code&gt; are the append-only session record. A &lt;code&gt;MEMORY.md&lt;/code&gt; at the root holds hard-won lessons — 34 entries and counting.&lt;/p&gt;

&lt;p&gt;Volume: several hundred wiki pages, four active domains, daily writes going back to February. Well past Karpathy's toy scale, well under any RAG-scale problem. This is the middle band the original gist never addresses.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where it broke: three failure modes, all the same shape
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Drift between the "recent changes" section and the "how it works" section of the same file.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The gist assumes ingest and update. It doesn't say what happens when something you documented in April is &lt;em&gt;contradicted&lt;/em&gt; by what you did in July. In practice, Claude appends a new dated entry to a changelog at the top of the file, feels done, and leaves the reference section below still describing the dead system as current. A reader — human or agent — trusts the reference section for "how does this work today," and it's wrong. Writing a dated log entry &lt;em&gt;feels&lt;/em&gt; like recording the change, so the present-tense prose that people actually read for "how does this work now" never gets revisited. The rule I now enforce: a migration isn't done when the changelog says so; it's done when the reference sections match.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Silent contradictions between pages.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Two entity cards for the same account — one calls the institution "Janney," the other calls it "Janney Montgomery Scott LLC" — and a downstream aggregation double-counts because the dedup key is the name. RAG failures are silent because retrieval quietly misses a chunk. Wiki failures are silent for a different reason: two pages both exist, both are internally consistent, and neither knows about the other. Claude doesn't spontaneously notice.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Orphan pages and dead wikilinks accumulate faster than lint catches them.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Karpathy's original suggests running lint occasionally. At three hundred pages, "occasionally" is not a frequency — it's a hope. Weekly lint recovers most of it. Nothing recovers a stale page that nobody links to and nobody re-reads.&lt;/p&gt;

&lt;p&gt;All three are the same shape: &lt;strong&gt;the wiki has no immune system against its own past.&lt;/strong&gt; It compounds knowledge, but it also compounds staleness, and it needs help distinguishing the two.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three things I bolted on
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;A drift linter that fails the build.&lt;/strong&gt; A Python check that walks the vault, cross-references any "retired" or "decommissioned" term against every file, and flags live mentions of dead systems in the canonical docs. It runs pre-commit and on a scheduled cron. When Ghost got replaced by an Astro static site, the linter caught nine files still describing the Ghost publish path as current — including the top-level project README. Without it, a reader would have reached for dead credentials.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Per-domain sub-schemas, not one giant &lt;code&gt;CLAUDE.md&lt;/code&gt;.&lt;/strong&gt; The gist treats the schema as one file. That doesn't survive the second domain. Finance and health have almost no overlap in vocabulary, conventions, or what a "page" is. My top-level &lt;code&gt;CLAUDE.md&lt;/code&gt; sets universal rules (voice, safety, wikilink format, verification protocol). Each domain has its own &lt;code&gt;&amp;lt;domain&amp;gt;-context.md&lt;/code&gt; that specializes them. A session working on finance loads the domain file; a session working on content loads a different one. Same architecture as scoped CSS.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A vector index alongside the wiki, not replacing it.&lt;/strong&gt; Once I passed roughly 100k tokens of wiki content plus 100k tokens of session logs plus tens of thousands of legacy conversations, Claude couldn't hold the index in context anymore. The gist's answer to this is "at that point, use RAG." My answer is: keep the wiki as the authored, curated layer, and add a pgvector index over the whole corpus — vault plus session history plus legacy archives — for recall. Around 126k chunks on Postgres 17 with &lt;code&gt;mxbai-embed-large&lt;/code&gt; embeddings running on a Mac mini. Not a replacement for the wiki; a way to find the right wiki page when your brain forgot which slug you wrote it under two months ago. The RAG layer is a retrieval tool for me and my agents. The wiki is still the canonical knowledge.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the pattern still wins
&lt;/h2&gt;

&lt;p&gt;For synthesis, entity resolution, and the "how do these three things I noticed relate" question, nothing else comes close. RAG genuinely doesn't do it — the &lt;a href="https://medium.com/@koriigami/build-a-personal-knowledge-base-with-claude-code-25d215b61822" rel="noopener noreferrer"&gt;structural argument&lt;/a&gt; about retrieval having no accumulation is correct. When I ask "what's the through-line across the last month of daily logs on finance," the answer draws on synthesis pages that were written when each log came in. The work happened once, at ingest.&lt;/p&gt;

&lt;p&gt;Entity cards are the other win. A single file per person or company, named by its wikilink text, becomes the source of truth. Everything else references it. When a fact about an entity changes, you change one file. This is the boring pattern that pays for itself every day.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where it stops — the honest edge cases
&lt;/h2&gt;

&lt;p&gt;The wiki pattern breaks when any of these are true:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Multi-actor writes.&lt;/strong&gt; If two agents write to the same wiki page concurrently, there is no lock. Mine is single-writer by convention. If you fan out coding agents that all edit &lt;code&gt;SESSION-STATE.md&lt;/code&gt;, you need a lockfile — the gist has nothing to say about this.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Recall across the boundary of a Claude session.&lt;/strong&gt; A wiki isn't memory. Claude reads it fresh each session. Cross-session memory ("what did we decide last Tuesday") needs a separate store, which is what daily logs plus the vector index are for.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Anything past ~100k tokens of hot wiki content.&lt;/strong&gt; &lt;a href="https://www.mindstudio.ai/blog/llm-wiki-vs-rag-markdown-knowledge-base-comparison" rel="noopener noreferrer"&gt;The load-bearing assumption&lt;/a&gt; is that the index fits in context. When it doesn't, either you shard by domain (my approach), add a retrieval layer, or accept degraded answers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Anything anyone else needs to read without Claude in the loop.&lt;/strong&gt; A markdown wiki is legible to humans. A markdown wiki &lt;em&gt;written by Claude, for Claude, over months&lt;/em&gt; has enough house-style shorthand that a new reader without the schema will miss context. The wiki is legible; the accumulated conventions are not.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What to actually do
&lt;/h2&gt;

&lt;p&gt;Start where Karpathy says. One folder, one schema, five sources, in one domain you're actively thinking about. Do it for two weeks before adjusting anything.&lt;/p&gt;

&lt;p&gt;At the point it starts feeling like it's working — around thirty or forty pages — put three things in before you regret not having them: a drift check that flags dead terms in live docs, one lint pass a week that actually runs, and a rule that any migration updates the reference section, not just a changelog. That's the operating discipline the pattern needs and doesn't ship with.&lt;/p&gt;

&lt;p&gt;Add a vector index only when the wiki genuinely stops fitting. Not before. Karpathy is right about that too — most people reach for RAG when they should have written a schema.&lt;/p&gt;

</description>
      <category>aiagents</category>
      <category>knowledgemanagement</category>
      <category>aiengineering</category>
      <category>platformengineering</category>
    </item>
  </channel>
</rss>
