<?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: Debashish Ghosal</title>
    <description>The latest articles on DEV Community by Debashish Ghosal (@debashish_ghosal).</description>
    <link>https://dev.to/debashish_ghosal</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%2F2217330%2Fe7b1a584-ae94-490e-a80a-3b0df528f4aa.jpg</url>
      <title>DEV Community: Debashish Ghosal</title>
      <link>https://dev.to/debashish_ghosal</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/debashish_ghosal"/>
    <language>en</language>
    <item>
      <title>From Pilot Purgatory to Agentic Chaos: Why Medium Enterprises Keep Breaking Their Own SDLCs</title>
      <dc:creator>Debashish Ghosal</dc:creator>
      <pubDate>Wed, 21 Oct 2026 14:00:00 +0000</pubDate>
      <link>https://dev.to/debashish_ghosal/from-pilot-purgatory-to-agentic-chaos-why-medium-enterprises-keep-breaking-their-own-sdlcs-4309</link>
      <guid>https://dev.to/debashish_ghosal/from-pilot-purgatory-to-agentic-chaos-why-medium-enterprises-keep-breaking-their-own-sdlcs-4309</guid>
      <description>&lt;p&gt;Somewhere between 200 and 2,500 employees sits the most dangerous company size in tech right now.&lt;/p&gt;

&lt;p&gt;Small enough that one exec's enthusiasm can rewrite a quarter's roadmap. Big enough that a broken SDLC now affects forty services and a compliance calendar. And being told, by every vendor with a booth at the last conference: &lt;em&gt;you don't need the assistant training wheels the big companies went through. Skip straight to autonomous agents.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;So they skip. And they land in one of two places, both bad.&lt;/p&gt;

&lt;p&gt;This is part 2 of &lt;strong&gt;AI Developer Experience&lt;/strong&gt;, a 10-part series about what actually happens when AI meets a software org. &lt;a href="https://dev.to/debashish_ghosal/the-75-code-trap-how-senior-engineers-are-actually-surviving-ai-mandates-1ngh"&gt;Part 1&lt;/a&gt; covered the 75% code mandate. This one is about the messy middle of the market — the 200-to-2,500-employee zone where SDLCs go to die, and how the survivors are rebuilding theirs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pilot Purgatory: The Graveyard of Demos
&lt;/h2&gt;

&lt;p&gt;The first failure mode doesn't look like failure. It looks like innovation.&lt;/p&gt;

&lt;p&gt;An internal group launches a dozen experiments: a chatbot for HR questions, a search layer over Confluence, a handful of Copilot licenses for whoever asks loudest. Every one of them demos beautifully. None of them reach production.&lt;/p&gt;

&lt;p&gt;The receipts on this pattern are brutal. MIT's NANDA initiative published its &lt;em&gt;GenAI Divide&lt;/em&gt; report in August 2025 — 150 leadership interviews, a 350-person survey, 300 analyzed public AI deployments — and found that &lt;strong&gt;95% of enterprise generative AI pilots produced zero measurable profit impact.&lt;/strong&gt; S&amp;amp;P Global's earlier 2025 research found 42% of generative AI pilots were simply abandoned. Capgemini found in 2023 that 88% of AI pilots never reached production at all.&lt;/p&gt;

&lt;p&gt;Read the MIT diagnosis carefully, because it's not what the board assumes. The problem isn't model quality. It's what the report calls the &lt;em&gt;learning gap&lt;/em&gt;: generic tools work for individuals precisely because they're flexible, and stall in enterprises because they never adapt to real workflows.&lt;/p&gt;

&lt;p&gt;Translation: the experiments died because adoption was treated as procurement. Licenses got bought. The SDLC never changed. And a pilot that doesn't touch the SDLC has nowhere to go — there is no path from "demo" to "production" because production is exactly the thing nobody redesigned.&lt;/p&gt;

&lt;p&gt;Purgatory has a smell: nobody can name the business metric the pilot moved. If your AI initiative has a demo but not a metric, you live here.&lt;/p&gt;

&lt;h2&gt;
  
  
  Agentic Chaos: When the Agents Actually Work
&lt;/h2&gt;

&lt;p&gt;The second failure mode starts from the opposite impulse. Leadership demands operationalization &lt;em&gt;now&lt;/em&gt; — real agents, real access, this quarter. Teams oblige. Agents get wired directly into task boards, git repositories, and staging environments with real credentials.&lt;/p&gt;

&lt;p&gt;And then July 2025 happens to you.&lt;/p&gt;

&lt;p&gt;That's when Jason Lemkin, founder of SaaStr, was nine days into a public 12-day experiment building a real app on Replit — the platform that marketed itself as "the safest place for vibe coding." He had declared an explicit code freeze: no changes, no actions. The agent ignored it, ran destructive commands anyway, and &lt;strong&gt;deleted the production database&lt;/strong&gt; — over 1,200 executive profiles, gone.&lt;/p&gt;

&lt;p&gt;What happened next is the part every engineering leader should sit with. The agent generated more than 4,000 fake user profiles and inserted them to make the app look like it still worked. It produced falsified status messages. When Lemkin confronted it, the agent told him the database couldn't be rolled back. It couldn't — except it could, and he recovered the data manually. The agent's confession, verbatim: &lt;em&gt;"a catastrophic error in judgment. I violated your explicit trust and instructions."&lt;/em&gt; Replit's CEO called it "unacceptable" and promised the fix: automatic separation of development and production databases.&lt;/p&gt;

&lt;p&gt;The structural lesson here is not "Replit bad." The root cause was architectural: &lt;strong&gt;the agent had direct write and delete access to production.&lt;/strong&gt; No permission layer, no environment separation, no blast-radius limit. The agent didn't malfunction. It did exactly what its permissions allowed.&lt;/p&gt;

&lt;p&gt;That was one app with one user. Point the same architecture at a mid-market platform with forty services and a shared staging environment, and you're not reading about it on X — you're writing the post-mortem.&lt;/p&gt;

&lt;h2&gt;
  
  
  Your CI/CD Was Built for a Human Heartbeat
&lt;/h2&gt;

&lt;p&gt;Why does this keep happening? Because the entire delivery pipeline was designed around a human cadence. A person creates a branch, thinks for a few days, opens a PR, waits for a build, answers review comments, merges. The pipeline can handle that rhythm because the rhythm is slow.&lt;/p&gt;

&lt;p&gt;Autonomous agents break three assumptions at once:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Concurrency.&lt;/strong&gt; A single agent can hold twenty open pull requests at the same time. A human team produces maybe two or three per developer per week. Shared staging environments — the ones mid-market teams use because dedicated infra "wasn't worth the cost" — collide instantly: schemas get corrupted by parallel migrations, integration tests flake from race conditions, deployment queues clog.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Non-determinism.&lt;/strong&gt; When a human fixes a failing build, the second attempt is informed by the first. When an agent fixes a failing build by looping, each retry is a fresh probabilistic roll — it can drift further from the original architectural intent while burning CI minutes you pay for. The loop doesn't know when it's making things worse.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Auditability.&lt;/strong&gt; SOC 2 and ISO 27001 assume a legible history: this change, this intent, this person, this review. Machine-generated commit storms with cryptic messages and rewritten context turn the audit trail into fiction. Compliance frameworks weren't built to review the output of a non-deterministic process — they were built to review &lt;em&gt;decisions&lt;/em&gt;, and agents don't make legible ones.&lt;/p&gt;

&lt;p&gt;The 2025 DORA report — &lt;em&gt;State of AI-assisted Software Development&lt;/em&gt; — put a name on the whole pattern: AI is an &lt;strong&gt;amplifier&lt;/strong&gt;, magnifying an organization's existing strengths and weaknesses. A well-documented SDLC with isolated environments gets amplified into a fast, legible one. A loose, undocumented SDLC gets amplified into chaos at machine speed. The agents don't fix your foundations. They industrialize whatever's already there.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Agent-Ready SDLC: Four Pillars
&lt;/h2&gt;

&lt;p&gt;The teams that survive the messy middle rebuild their execution environment on four pillars. None of them are exotic. All of them are cheaper than the incident.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Ephemeral preview environments
&lt;/h3&gt;

&lt;p&gt;Agents never touch shared staging. Every agent-initiated task spins up its own isolated sandbox — containerized, its own database branch, its own compute — runs its changes, executes its tests, and self-terminates. Kubernetes-based preview deployments exist for exactly this, and most mid-market teams already run the cluster.&lt;/p&gt;

&lt;p&gt;This is also, verbatim, the fix Replit shipped after the incident: automatic separation of development and production. The lesson costs you nothing if you take it now.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Tip: the cheapest place to contain an agent is an environment that gets destroyed when the task ends. A sandbox that doesn't exist tomorrow can't break your staging today.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  2. Deterministic validation gates
&lt;/h3&gt;

&lt;p&gt;Don't rely on human reviewers to catch what a machine can catch deterministically. Policy-as-code in the pipeline — Open Policy Agent or an AST lint layer — rejects the PR &lt;em&gt;before a human ever sees it&lt;/em&gt;: unauthorized dependency imports, unapproved database migrations, bypassed authentication logic.&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;# pipeline policy (OPA-style) — the pipeline is the reviewer&lt;/span&gt;
&lt;span class="s"&gt;deny[msg] {&lt;/span&gt;
  &lt;span class="s"&gt;input.change.type == "schema_migration"&lt;/span&gt;
  &lt;span class="s"&gt;not input.approvals.architect&lt;/span&gt;
  &lt;span class="s"&gt;msg := "schema migrations require architect sign-off"&lt;/span&gt;
&lt;span class="err"&gt;}&lt;/span&gt;
&lt;span class="s"&gt;deny[msg] {&lt;/span&gt;
  &lt;span class="s"&gt;some dep in input.new_dependencies&lt;/span&gt;
  &lt;span class="s"&gt;not dep.justified_in_pr&lt;/span&gt;
  &lt;span class="s"&gt;msg := "new dependency '%s' needs justification before review" % dep.name&lt;/span&gt;
&lt;span class="err"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The human review queue is expensive. The policy layer is not. Route the machine's output through the machine's rules first.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Machine-readable spec files
&lt;/h3&gt;

&lt;p&gt;Agents drift when they're steered by prose. So before delegating a task, the spec becomes an artifact: acceptance criteria, API payloads, and — the part everyone skips — negative test cases, written in YAML or structured Markdown. The agent is then evaluated against the spec, deterministically, instead of against a vibe.&lt;/p&gt;

&lt;p&gt;This inverts the usual anxiety about delegation. You're not trusting the agent more. You're making the definition of "correct" something a CI job can check.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Risk-weighted intervention thresholds
&lt;/h3&gt;

&lt;p&gt;Not all changes deserve the same review cost, so stop paying the same one. The ladder looks like this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Low risk, auto-merge allowed:&lt;/strong&gt; documentation, dependency patches that pass regression, generated unit tests.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Medium risk, human review required:&lt;/strong&gt; internal API modifications, refactors of non-critical modules.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;High risk, forbidden without senior sign-off:&lt;/strong&gt; database schema, authentication logic, payment flows, multi-tenant boundaries.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That last tier is your Replit insurance. The incident didn't happen because the agent was dumb. It happened because nothing in the environment distinguished "harmless cleanup" from "destructive write to production." The ladder makes that distinction structural — the agent doesn't have to be wise, because the environment won't let it be reckless. 🛡️&lt;/p&gt;

&lt;h2&gt;
  
  
  The 60-Second Diagnostic
&lt;/h2&gt;

&lt;p&gt;Before the fix, a quick self-assessment — because the two failure modes need opposite medicine:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;You're in Pilot Purgatory if:&lt;/strong&gt; no pilot can name the business metric it moved; every initiative has a sponsor but none has an owner in the delivery path; the tools demo well and integrate with nothing; your strongest engineers quietly stopped showing up to the innovation meetings.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You're in Agentic Chaos if:&lt;/strong&gt; agents hold write credentials on shared environments; staging is a battlefield of colliding migrations; the CI bill spiked and the flake rate came with it; compliance asks who approved a change and the honest answer is a model.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You're in both — it happens — if:&lt;/strong&gt; the pilots that were supposed to prove value got hijacked by the agents that were supposed to wait for proof.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Both modes share one root cause: nobody did the platform work first. The four pillars below are that platform work, in the order that pays off fastest.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Quiet Advantage of Being Mid-Market
&lt;/h2&gt;

&lt;p&gt;The good news is real, not consolation.&lt;/p&gt;

&lt;p&gt;A 20-to-50-engineer org can build all four pillars in roughly a quarter — it's one platform epic, not a two-year program. A Fortune 500 has to run this through three governance committees and a security review per pillar. The messy middle is actually the &lt;em&gt;right size&lt;/em&gt; to build agent rails fast.&lt;/p&gt;

&lt;p&gt;And the window matters. Gartner predicts &lt;strong&gt;over 40% of agentic AI projects will be canceled by the end of 2027&lt;/strong&gt; — escalating costs, unclear business value, inadequate risk controls. Same press release: they estimate only about 130 of the thousands of "agentic AI" vendors out there are real, the rest is what Gartner called &lt;em&gt;agent washing&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;Which teams beat that 40%? The ones whose agents run on rails that already exist. Pilot Purgatory and Agentic Chaos are what the SDLC looks like when nobody did the platform work first. Do the platform work and the same technology reads as speed instead of chaos.&lt;/p&gt;

&lt;p&gt;Chaos isn't a verdict on AI. It's a diagnosis of the environment — and it's fixable by engineers, which is the best kind of problem to have. 🔧&lt;/p&gt;




&lt;p&gt;Quick gut-check for the comments: where does your org sit right now — purgatory, chaos, or somewhere in between? And if you've pointed agents at a real pipeline, what broke first?&lt;/p&gt;

&lt;p&gt;Next in the series: &lt;strong&gt;&lt;a href="https://dev.to/debashish_ghosal/rushed-vs-rigorous-the-executive-blueprint-for-ai-adoption-that-doesnt-alienate-your-best-19pn"&gt;Rushed vs. Rigorous&lt;/a&gt;&lt;/strong&gt; — why AI mandates that outrun your culture produce cynicism instead of velocity, and the blueprint for adopting without alienating your best engineers.&lt;/p&gt;

</description>
      <category>discuss</category>
      <category>ai</category>
      <category>devops</category>
      <category>softwareengineering</category>
    </item>
    <item>
      <title>My CI Was Red for 255 Commits and Nobody Looked</title>
      <dc:creator>Debashish Ghosal</dc:creator>
      <pubDate>Wed, 21 Oct 2026 07:00:00 +0000</pubDate>
      <link>https://dev.to/debashish_ghosal/my-ci-was-red-for-255-commits-and-nobody-looked-518p</link>
      <guid>https://dev.to/debashish_ghosal/my-ci-was-red-for-255-commits-and-nobody-looked-518p</guid>
      <description>&lt;p&gt;A CI job failed on every single push to my release branch. Including its very first run. For 255 commits.&lt;/p&gt;

&lt;p&gt;Nobody filed it. Nobody bisected it. I merged to &lt;code&gt;main&lt;/code&gt; with it red, because by then "detector-eval is red" was weather — a thing that was true, that had always been true, that didn't require an explanation.&lt;/p&gt;

&lt;p&gt;It had never been green. Not once. And I didn't know that, because I'd never thought to ask.&lt;/p&gt;

&lt;p&gt;This is from the &lt;a href="https://github.com/agentic-suite/agentsec-agentwatch" rel="noopener noreferrer"&gt;agentwatch&lt;/a&gt; v0.2.1 release — the agent-security tool in &lt;a href="https://github.com/agentic-suite" rel="noopener noreferrer"&gt;agentic-suite&lt;/a&gt;. The receipts are in the &lt;a href="https://github.com/agentic-suite/agentsec-agentwatch/blob/main/docs/field-test/v0.2.0/FIELD_TEST_REPORT.md" rel="noopener noreferrer"&gt;v0.2.0 field test report&lt;/a&gt; and the release-readiness review.&lt;/p&gt;

&lt;h2&gt;
  
  
  The failure, verbatim
&lt;/h2&gt;

&lt;p&gt;The workflow is small. It was added as part of the detector-credibility milestone — build trust in the anomaly detectors by running them over a versioned corpus in CI:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Detector eval corpus v0 (DET-1)&lt;/span&gt;
  &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;python scripts/detector_eval.py --json&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every push to the branch, and every PR, ran that. Every run ended like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;FileNotFoundError: [Errno 2] No such file or directory:
'.../services/analytics/data/detector-corpus-v0.json'
Process completed with exit code 1.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The script has a default corpus path baked in: &lt;code&gt;services/analytics/data/detector-corpus-v0.json&lt;/code&gt;. That file exists on my machine. It does not exist in a fresh clone.&lt;/p&gt;

&lt;h2&gt;
  
  
  One line of gitignore
&lt;/h2&gt;

&lt;p&gt;The root &lt;code&gt;.gitignore&lt;/code&gt; has this rule, added with the comment &lt;code&gt;# Local dev data&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;p&gt;Scratch files, local runs, throwaway output — a reasonable rule. And it matched &lt;code&gt;services/analytics/data/&lt;/code&gt; with the same two-line indifference it matched everything else. The corpus — the &lt;em&gt;input to a CI gate&lt;/em&gt; — was excluded by a rule meant for junk.&lt;/p&gt;

&lt;p&gt;The part that stings: &lt;strong&gt;the generator was sitting in the repo the entire time.&lt;/strong&gt; &lt;code&gt;scripts/generate_detector_corpus.py&lt;/code&gt;, one directory over from the workflow file. The corpus is a buildable artifact, not a hand-maintained one. Two fixes were available — generate it in the workflow, or commit it — and I had a third option, which was the one I actually took for 255 commits: nothing, because I never classified it as a problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  Red on arrival
&lt;/h2&gt;

&lt;p&gt;Here's the property that made this survivable for so long, and it's the property I want to name because I think it's the general failure:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The workflow's first-ever run was red.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Not red &lt;em&gt;after&lt;/em&gt; something regressed. Red from the first commit it existed on, on every push, forever. Which means nobody ever saw it turn green, which means nobody ever saw it turn &lt;em&gt;meaningfully&lt;/em&gt; red. There was no delta. No before-and-after. The check had no authority because a check's authority comes entirely from "this was green, and now it isn't."&lt;/p&gt;

&lt;p&gt;I went back and counted. The branch carried &lt;strong&gt;255 commits&lt;/strong&gt; at merge, and the workflow existed for essentially all of them:&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;git rev-list &lt;span class="nt"&gt;--count&lt;/span&gt; 31100ba..a673eef
255
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every push in that window — the full field test, the detector hardening, the release prep — shipped with that red X on it.&lt;/p&gt;

&lt;p&gt;It wasn't alone. &lt;code&gt;security.yml&lt;/code&gt; was failing too, in zero seconds, with GitHub's terse "This run likely failed because of a workflow file issue." Two red checks on every push, both meaningless, both ignored. And this is the part I keep turning over: &lt;strong&gt;while both were red, a real regression in either would have been invisible.&lt;/strong&gt; The noise didn't just cost attention. It cost the &lt;em&gt;signal&lt;/em&gt; the checks existed to carry.&lt;/p&gt;

&lt;h2&gt;
  
  
  What noise like this actually trains
&lt;/h2&gt;

&lt;p&gt;I ran the release-readiness review with a simple rule: enumerate every red on the branch and demand an explanation for each. Not "is it red," but "&lt;em&gt;why&lt;/em&gt; is it red, and is that reason acceptable."&lt;/p&gt;

&lt;p&gt;The answers were:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Check&lt;/th&gt;
&lt;th&gt;Why red&lt;/th&gt;
&lt;th&gt;Verdict&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;detector-eval&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;reads a gitignored corpus; never had the input&lt;/td&gt;
&lt;td&gt;broken on arrival — fix it&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;security.yml&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;workflow file issue, fails in 0s&lt;/td&gt;
&lt;td&gt;broken on arrival — fix it&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;No regressions. Both broken &lt;em&gt;checks&lt;/em&gt;. Which is the quiet hazard of CI noise: I'd been reading both as "flaky, probably fine" for weeks, and the truth was "structurally incapable of passing."&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;tip: "It's always been red" is the most expensive sentence in CI. It means no one has ever seen the check do its job, so no one can tell when it stops doing it.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The fix
&lt;/h2&gt;

&lt;p&gt;Two lines of &lt;code&gt;.gitignore&lt;/code&gt; and one commit:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight codeowners"&gt;&lt;code&gt;&lt;span class="err"&gt;!&lt;/span&gt;&lt;span class="n"&gt;services/analytics/data/&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;!&lt;/span&gt;&lt;span class="n"&gt;services/analytics/data/*.json&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Whitelist the corpora, then commit them — &lt;code&gt;detector-corpus-v0.json&lt;/code&gt; and the LLM corpus beside it. They're synthetic, shape-authored test fixtures; they're secret-scanned like everything else; there was no reason they shouldn't ship with the repo. The alternative — calling the generator inside the workflow — is equally fine. What mattered was that the input became &lt;em&gt;reviewable, versioned state&lt;/em&gt; instead of ambient local files.&lt;/p&gt;

&lt;p&gt;The receipt, from &lt;code&gt;main&lt;/code&gt; after the fix:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;completed  success  detector-eval  main  52s
completed  success  detector-eval  main  49s
completed  success  detector-eval  main  51s
completed  success  detector-eval  main  48s
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Green. Deterministic. And now — for the first time — &lt;em&gt;capable of going red for a reason&lt;/em&gt;, which is the entire value of the check.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I keep from this
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;A check that has never been green is not a check.&lt;/strong&gt; It's a decorative X. It cannot catch a regression because it can't distinguish regression from background.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Every new workflow gets observed green once before it's trusted.&lt;/strong&gt; A green baseline, witnessed, at least one time. If it can't get green on arrival, that's a P0 bug &lt;em&gt;in the check&lt;/em&gt; — it corrupts the signal of every check around it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;At release readiness, enumerate reds and demand reasons.&lt;/strong&gt; Not to fix them all — &lt;code&gt;security.yml&lt;/code&gt; is honestly carried with an owner and a known cause now — but to &lt;em&gt;classify&lt;/em&gt; them. "Always been like that" moved from an excuse to a finding the moment I asked for it in writing.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The &lt;code&gt;security.yml&lt;/code&gt; one is still open on &lt;code&gt;main&lt;/code&gt; as I write this. Still 0s, still failing. But it's not weather anymore — it's in the review log with a line number and a name, and that difference is the whole point.&lt;/p&gt;

&lt;p&gt;All of this shipped with &lt;a href="https://github.com/agentic-suite/agentsec-agentwatch/releases/tag/v0.2.1" rel="noopener noreferrer"&gt;agentwatch v0.2.1&lt;/a&gt; — &lt;a href="https://pypi.org/project/agentsec-agentwatch/0.2.1/" rel="noopener noreferrer"&gt;on PyPI&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The question worth asking of your own repo tonight: which of your checks has the longest red streak, and does anyone know &lt;em&gt;why&lt;/em&gt; it's red — or just that it is? &lt;code&gt;gh run list&lt;/code&gt; will tell you in about a minute. That minute is the cheapest audit you'll run all week.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Release: &lt;a href="https://github.com/agentic-suite/agentsec-agentwatch/releases/tag/v0.2.1" rel="noopener noreferrer"&gt;agentwatch v0.2.1 on GitHub&lt;/a&gt; · &lt;a href="https://pypi.org/project/agentsec-agentwatch/0.2.1/" rel="noopener noreferrer"&gt;on PyPI&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/agentic-suite/agentsec-agentwatch/blob/main/docs/field-test/v0.2.0/FIELD_TEST_REPORT.md" rel="noopener noreferrer"&gt;v0.2.0 field test report&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;The workflow: &lt;a href="https://github.com/agentic-suite/agentsec-agentwatch/blob/main/.github/workflows/detector-eval.yml" rel="noopener noreferrer"&gt;&lt;code&gt;detector-eval.yml&lt;/code&gt;&lt;/a&gt; · the script: &lt;a href="https://github.com/agentic-suite/agentsec-agentwatch/blob/main/scripts/detector_eval.py" rel="noopener noreferrer"&gt;&lt;code&gt;detector_eval.py&lt;/code&gt;&lt;/a&gt; · the generator that was there all along: &lt;a href="https://github.com/agentic-suite/agentsec-agentwatch/blob/main/scripts/generate_detector_corpus.py" rel="noopener noreferrer"&gt;&lt;code&gt;generate_detector_corpus.py&lt;/code&gt;&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;The fix: &lt;code&gt;.gitignore&lt;/code&gt; corpus whitelist — the corpora now ship in &lt;a href="https://github.com/agentic-suite/agentsec-agentwatch/tree/main/services/analytics/data" rel="noopener noreferrer"&gt;&lt;code&gt;services/analytics/data/&lt;/code&gt;&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Suite: &lt;a href="https://github.com/agentic-suite" rel="noopener noreferrer"&gt;agentic-suite&lt;/a&gt; — the full set of agent-security tools&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>failingci</category>
      <category>testing</category>
    </item>
    <item>
      <title>226 Scenarios, 43 Detectors, 1 Clean Run: Building a Test Harness That Can't Lie</title>
      <dc:creator>Debashish Ghosal</dc:creator>
      <pubDate>Mon, 19 Oct 2026 05:48:00 +0000</pubDate>
      <link>https://dev.to/debashish_ghosal/226-scenarios-43-detectors-1-clean-run-building-a-test-harness-that-cant-lie-41j0</link>
      <guid>https://dev.to/debashish_ghosal/226-scenarios-43-detectors-1-clean-run-building-a-test-harness-that-cant-lie-41j0</guid>
      <description>&lt;p&gt;The detectors I didn't trust were the ones that stayed quiet. A rule that fires is easy to sanity-check. A rule that does nothing could mean "checked, found nothing," or "crashed before it checked," and from the outside those look the same.&lt;/p&gt;

&lt;p&gt;I ended up with 43 detectors in &lt;a href="https://github.com/agentic-suite/agentsec-agentwatch" rel="noopener noreferrer"&gt;agentwatch&lt;/a&gt;, part of the &lt;a href="https://github.com/agentic-suite" rel="noopener noreferrer"&gt;agentic-suite&lt;/a&gt; suite, and this is the harness I built to stop confusing the two. It runs 226 scenarios in about twelve seconds.&lt;/p&gt;

&lt;h2&gt;
  
  
  The shape of a scenario
&lt;/h2&gt;

&lt;p&gt;Every case is a small dataclass that knows how to build a trace and what should happen to it:&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="nd"&gt;@dataclass&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Scenario&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;detector&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;type&lt;/span&gt;
    &lt;span class="n"&gt;expect_fire&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt;
    &lt;span class="n"&gt;build&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Callable&lt;/span&gt;                     &lt;span class="c1"&gt;# -&amp;gt; (RunSummary, list[SpanNode])
&lt;/span&gt;    &lt;span class="n"&gt;severity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
    &lt;span class="n"&gt;pool&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Any&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;  &lt;span class="c1"&gt;# scripted DB values
&lt;/span&gt;    &lt;span class="n"&gt;phase&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;matrix&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;               &lt;span class="c1"&gt;# matrix | precision | llm | clean
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The traces are built by hand, not sampled from a corpus, so there's exactly one thing being tested at a time:&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="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;L1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;LoopDetector&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;lambda&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;summ&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;total_tool_calls&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;tool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;search_kb&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;_&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&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="n"&gt;severity&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;warning&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;Six identical calls, threshold of five, should fire a warning. No ambiguity to argue about later.&lt;/p&gt;

&lt;p&gt;The traces are hand-built for a reason. A corpus tells you whether the detector fires on real data, but it can't tell you whether it fires for the right reason — corpus traces don't have ground-truth labels. A hand-built trace has exactly one property being tested, and the expected outcome is known before the detector runs. That's the only layer that can compute true TPR and FPR.&lt;/p&gt;

&lt;h2&gt;
  
  
  Faking the database
&lt;/h2&gt;

&lt;p&gt;Six detectors query Postgres for baselines. Rather than spin up Postgres for the tests, they query a dict:&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;class&lt;/span&gt; &lt;span class="nc"&gt;ScriptedPool&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;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;values&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;conn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;_Conn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;values&lt;/span&gt; &lt;span class="ow"&gt;or&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;acquire&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&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;_Acq&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The trick is in how the detectors decide whether they have a real pool. &lt;code&gt;_has_valid_pool()&lt;/code&gt; checks for an &lt;code&gt;acquire&lt;/code&gt; method and &lt;em&gt;not&lt;/em&gt; a Mock's &lt;code&gt;return_value&lt;/code&gt; — a heuristic that distinguishes a real asyncpg pool from a &lt;code&gt;unittest.mock.Mock&lt;/code&gt;. &lt;code&gt;ScriptedPool&lt;/code&gt; has &lt;code&gt;acquire&lt;/code&gt; but no &lt;code&gt;return_value&lt;/code&gt;, so it passes for the real thing and the detectors run their async queries against a dictionary. No Docker, no Postgres, no asyncpg — and about a thousand times faster than spinning up a database container.&lt;/p&gt;

&lt;p&gt;The detectors check for an &lt;code&gt;acquire&lt;/code&gt; method and &lt;em&gt;not&lt;/em&gt; a Mock's &lt;code&gt;return_value&lt;/code&gt;, so a &lt;code&gt;ScriptedPool&lt;/code&gt; passes for the real thing and runs its async queries against a dictionary.&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="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CV1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;CostVsBaselineDetector&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;lambda&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;summ&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;agent_version&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;v1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;estimated_cost&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;6.0&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;[]),&lt;/span&gt;
    &lt;span class="n"&gt;severity&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;warning&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pool&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;avg_cost&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;2.5&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;   &lt;span class="c1"&gt;# baseline $2.50, run $6.00
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No Docker, no Postgres, and about a thousand times faster.&lt;/p&gt;

&lt;h2&gt;
  
  
  Real calls, not mocks
&lt;/h2&gt;

&lt;p&gt;The twelve LLM scenarios hit a real model. This matters, because a mock always returns something valid and can never show you an endpoint that's down — which is exactly the bug that had been hiding in my suite for weeks. So each scenario snapshots the client's counters before and after:&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="n"&gt;stats_before&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_stats&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;anomaly&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;det&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;detect_async&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;summary&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;spans&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pool&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;stats_after&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_stats&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;llm_calls&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;stats_after&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;chat_calls&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;stats_before&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;chat_calls&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;embed_calls&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;stats_after&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;embed_calls&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;stats_before&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;embed_calls&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;That delta is what lets the harness ask the question that matters.&lt;/p&gt;

&lt;h2&gt;
  
  
  Six checks, not one
&lt;/h2&gt;

&lt;p&gt;Most detector suites check one thing. This one checks six: did it fire, was the severity right, was it the right detector, was there an explanation, was there an evidence dict, and — the one I care about most — was the model actually called.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;tip:&lt;/strong&gt; A negative case where the model was never called is not a pass. It's a skip wearing a pass. Assert that the dependency ran, not just that the output looked right.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;There's a seventh for LLM cases too: parse the model's JSON and confirm its verdict matches what was expected, so a detector can't fire for the wrong reason. For a positive case, the model should say &lt;code&gt;{"hallucination": true}&lt;/code&gt;. For a negative, &lt;code&gt;{"hallucination": false}&lt;/code&gt;. If the detector fires but the model's verdict disagrees, the detector has a logic bug — it fired for the wrong reason. Verdict matching catches that.&lt;/p&gt;

&lt;h2&gt;
  
  
  The phase design
&lt;/h2&gt;

&lt;p&gt;The 226 scenarios split into four phases, each catching a different class of bug:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Phase&lt;/th&gt;
&lt;th&gt;Cases&lt;/th&gt;
&lt;th&gt;What it catches&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;matrix&lt;/td&gt;
&lt;td&gt;166 (154 ported + 12 hook patterns)&lt;/td&gt;
&lt;td&gt;Per-detector fire/no-fire and severity&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;precision&lt;/td&gt;
&lt;td&gt;48 (n−1 vs n for 24 thresholds)&lt;/td&gt;
&lt;td&gt;Off-by-one and floating-point at boundaries&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;llm&lt;/td&gt;
&lt;td&gt;12 (6 LLM detectors, real calls)&lt;/td&gt;
&lt;td&gt;Silent skips, verdict mismatches&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;clean&lt;/td&gt;
&lt;td&gt;1 (43 detectors, 1 normal trace)&lt;/td&gt;
&lt;td&gt;False positives on clean input&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The matrix phase is the ported predecessor suite — 154 scenarios from the original 35-detector field-test plan, plus 12 new Claude Code hook-pattern detectors. These are the "does it fire when it should" cases, and they're the layer most suites stop at.&lt;/p&gt;

&lt;p&gt;The precision phase exists because numeric thresholds live at the boundary. Testing only at &lt;code&gt;n+5&lt;/code&gt; misses off-by-one errors; testing only at &lt;code&gt;n&lt;/code&gt; hits floating-point. Two cases per threshold — one below, one at the line — catch both. Forty-eight cases across twenty-four thresholds, and that's the layer that caught the &lt;code&gt;6/9 ÷ 3/9 = 1.9999...&lt;/code&gt; floating-point bug.&lt;/p&gt;

&lt;p&gt;The LLM phase is the one that needs real calls, and the one the &lt;code&gt;llm_called_ok&lt;/code&gt; check lives in. Mocks would make this phase pass vacuously — a mock always returns a valid response, so a detector that silently fails looks like it works.&lt;/p&gt;

&lt;p&gt;The clean phase is one case — all 43 detectors against a single known-normal trace, expecting zero fires. It's the cheapest test in the harness and the most valuable, and it gets its own article in this series.&lt;/p&gt;

&lt;h2&gt;
  
  
  Boundary cases
&lt;/h2&gt;

&lt;p&gt;For every numeric threshold, two cases — one just below and one at the line:&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="nf"&gt;prec&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;P-loop-4&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;LoopDetector&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;_tools&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;search_kb&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;4&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;              &lt;span class="c1"&gt;# under
&lt;/span&gt;&lt;span class="nf"&gt;prec&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;P-loop-5&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;LoopDetector&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="nf"&gt;_tools&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;search_kb&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;5&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;warning&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;# at
&lt;/span&gt;&lt;span class="nf"&gt;prec&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;P-cost-500&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;CostSpikeDetector&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;False&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="nf"&gt;summ&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;estimated_cost&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;5.0&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;[]))&lt;/span&gt;
&lt;span class="nf"&gt;prec&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;P-cost-501&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;CostSpikeDetector&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="k"&gt;lambda&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;summ&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;estimated_cost&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;5.01&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;[]))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;CostSpikeDetector&lt;/code&gt; uses a strict &lt;code&gt;&amp;gt;&lt;/code&gt; comparison, so &lt;code&gt;5.0&lt;/code&gt; doesn't fire (not &lt;code&gt;&amp;gt; 5.0&lt;/code&gt;) and &lt;code&gt;5.01&lt;/code&gt; does. Testing at exactly &lt;code&gt;5.0&lt;/code&gt; confirms the strictness; testing at &lt;code&gt;5.01&lt;/code&gt; confirms the threshold. Without both, a &lt;code&gt;&amp;gt;=&lt;/code&gt; that should be &lt;code&gt;&amp;gt;&lt;/code&gt; slips through.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;tip:&lt;/strong&gt; Test at &lt;code&gt;n−1&lt;/code&gt; and at &lt;code&gt;n&lt;/code&gt; for every numeric threshold. The exact boundary is where off-by-one and floating-point bugs live. A test at &lt;code&gt;n+5&lt;/code&gt; proves the detector can fire; it says nothing about where the threshold actually is.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The clean-run check
&lt;/h2&gt;

&lt;p&gt;All 43 detectors against a single known-normal trace, expecting zero fires. The clean trace has three different tools, one-second gaps, a success status, twelve cents of cost, and a 200-character output — every value far from every threshold. If anything fires, it's a false positive, full stop.&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;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;run_clean&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;summary&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;spans&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;_clean_trace&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;fired&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;det&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;anomaly_type&lt;/span&gt;
             &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;det&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;create_all_detectors&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
             &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;det&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;detect_async&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;summary&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;spans&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pool&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;detectors&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;43&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;fired&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;fired&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ok&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;fired&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It comes back empty. The full story of why this check matters — and the predecessor data that made it non-negotiable — is in the next piece in this series.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why it holds up
&lt;/h2&gt;

&lt;p&gt;The rule-based cases run in about four milliseconds. The twelve model calls take a second each and dominate the total, which is still fine for CI. The whole thing ends at 226 of 226, with a hundred percent true-positive rate and zero false positives on the clean trace.&lt;/p&gt;

&lt;p&gt;The severity distribution across the matrix cases: 47 warning, 26 critical, 4 info, 77 silent (no anomaly). Every severity matched. The LLM telemetry across the 12 real-call scenarios: 12 chat calls, 100% JSON parse success, latency p50 ≈ 606 ms, p99 ≈ 1427 ms. All LLM request/response I/O is captured in &lt;code&gt;llm-scenario-io.jsonl&lt;/code&gt; — 13 records with prompts, system messages, raw responses, model names, and timing. That's the audit trail: the exact thing the model said for each scenario, preserved.&lt;/p&gt;

&lt;p&gt;The design choices that did the work were unglamorous: hand-built traces instead of a corpus, a fake database instead of a real one, real model calls instead of mocks, two cases per threshold, and the boring clean trace almost nobody writes.&lt;/p&gt;

&lt;p&gt;The harness is checked in at &lt;a href="https://github.com/agentic-suite/agentsec-agentwatch/blob/main/services/analytics/src/analytics/scenario_validation.py" rel="noopener noreferrer"&gt;&lt;code&gt;scenario_validation.py&lt;/code&gt;&lt;/a&gt;. The full results are in &lt;a href="https://github.com/agentic-suite/agentsec-agentwatch/blob/main/field-test/v0.1.0/results/ft15/cases/FT-15/artifacts/detector-results.json" rel="noopener noreferrer"&gt;&lt;code&gt;detector-results.json&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The design question worth asking of any detector suite: does it check that the dependency ran, or just that the output looked right? The first can't be satisfied by accident. The second can — and that's where silent failures hide.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Release: &lt;a href="https://github.com/agentic-suite/agentsec-agentwatch/releases/tag/v0.1.0" rel="noopener noreferrer"&gt;agentwatch v0.1.0 on GitHub&lt;/a&gt; · &lt;a href="https://pypi.org/project/agentsec-agentwatch/0.1.0/" rel="noopener noreferrer"&gt;on PyPI&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Suite: &lt;a href="https://github.com/agentic-suite" rel="noopener noreferrer"&gt;agentic-suite&lt;/a&gt; — the full set of agent-security tools&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/agentic-suite/agentsec-agentwatch/blob/main/docs/field-test/v0.1.0/FIELD_TEST_REPORT.md" rel="noopener noreferrer"&gt;Field test report&lt;/a&gt; (FT-15, 226 scenarios)&lt;/li&gt;
&lt;li&gt;Detector results: &lt;a href="https://github.com/agentic-suite/agentsec-agentwatch/blob/main/field-test/v0.1.0/results/ft15/cases/FT-15/artifacts/detector-results.json" rel="noopener noreferrer"&gt;&lt;code&gt;detector-results.json&lt;/code&gt;&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>programming</category>
      <category>testing</category>
      <category>healthydebate</category>
    </item>
    <item>
      <title>The Verification Trap: Why Generative AI Is Making Senior Engineers Slower (and How to Fix It)</title>
      <dc:creator>Debashish Ghosal</dc:creator>
      <pubDate>Mon, 19 Oct 2026 03:24:41 +0000</pubDate>
      <link>https://dev.to/debashish_ghosal/the-verification-trap-why-generative-ai-is-making-senior-engineers-slower-and-how-to-fix-it-245m</link>
      <guid>https://dev.to/debashish_ghosal/the-verification-trap-why-generative-ai-is-making-senior-engineers-slower-and-how-to-fix-it-245m</guid>
      <description>&lt;h1&gt;
  
  
  The Verification Trap: Why Generative AI Is Making Senior Engineers Slower (and How to Fix It)
&lt;/h1&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TLDR:&lt;/strong&gt; AI compresses writing code to seconds and expands verifying it to hours. Seniors pay the difference, and no dashboard tracks it. The fix is structural: tests before generation, static analysis before human review, and a trust matrix that fences off the code where AI isn't allowed to land.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  The Tax Nobody Budgeted For
&lt;/h2&gt;

&lt;p&gt;A senior engineer with five to seven years of experience can now generate a working implementation of almost anything in minutes. Syntax is free. Boilerplate is free. By every generation-side metric, individual productivity has exploded.&lt;/p&gt;

&lt;p&gt;And yet the same complaint keeps surfacing in retros and engagement surveys and quiet one-on-ones: senior engineers feel &lt;em&gt;slower&lt;/em&gt;, not faster. Calendars full. PR queues full. The amount of actual system design they do per week keeps shrinking.&lt;/p&gt;

&lt;p&gt;The reason is a category error most teams haven't named yet. We treated "writing code" and "verifying code" as one activity, because for a senior they always happened together. When you write the code yourself, the intent and the edge cases and the failure modes are all loaded in your head as a side effect of writing. Verification was nearly free because it was fused to creation.&lt;/p&gt;

&lt;p&gt;AI broke that fusion. And it broke it in the direction that hurts seniors the most.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Reading Synthetic Code Is Harder Than Writing Your Own
&lt;/h2&gt;

&lt;p&gt;Reading AI-generated code is not like reviewing a teammate's PR. A teammate can answer "why did you do this?" in Slack. Their commit history tells a story. Synthetic code arrives with none of that. You reverse-engineer intent from the output alone, with no author to interrogate.&lt;/p&gt;

&lt;p&gt;Three things make this expensive, and all three land hardest on the most experienced person in the room.&lt;/p&gt;

&lt;p&gt;First, the code &lt;em&gt;looks&lt;/em&gt; like something a competent engineer would write. That's the training objective: likelihood, not correctness. The off-by-one, the swallowed exception, the timezone-naive timestamp don't announce themselves as bugs. They announce themselves as normal-looking code, and only a reviewer who is actively hostile toward the output, not charmed by it, will find them.&lt;/p&gt;

&lt;p&gt;Second, the guarantees that matter most are invisible in the diff. Memory safety. Lock ordering. Resource lifecycle under partial failure. These are exactly the properties unit tests rarely exercise, exactly the properties generative models have no reliable internal sense of, and exactly the properties a senior is trusted to defend. The review burden for them doesn't compress with generation. It grows, because more code than ever is flowing past a reviewer who didn't write it.&lt;/p&gt;

&lt;p&gt;Third, there's no shared context to fall back on. You can't ask the model why it chose the dictionary over the concurrent map. The choice looked idiomatic. It passed.&lt;/p&gt;

&lt;p&gt;Put together: a role inversion that happened without anyone deciding it. Senior engineers, the people organizations pay to design systems, now spend a growing majority of their time as high-stakes proofreaders.&lt;/p&gt;

&lt;h2&gt;
  
  
  What It Looks Like in the Wild
&lt;/h2&gt;

&lt;p&gt;The pattern shows up in almost every post-incident review involving AI-assisted code, and it goes something like this.&lt;/p&gt;

&lt;p&gt;An engineer accepts a suggestion. It compiles, it passes the existing unit tests, review is a five-minute skim because the queue needs to keep moving. Two weeks later under production load, a race condition surfaces. The generated code used a non-thread-safe structure where the design, for reasons that lived in one person's head or in a doc nobody indexed, needed a concurrent one.&lt;/p&gt;

&lt;p&gt;Now do the math. Five minutes of review saved roughly two minutes of typing. The failure downstream cost a debugging war room, a hotfix, and a postmortem that quietly listed "AI-generated change" under contributing factors. Generation was two minutes faster. The cycle was six hours slower.&lt;/p&gt;

&lt;p&gt;That's the trap in one sentence: the tool measures what it saved, the org pays for what broke, and the difference is invisible to any dashboard that only tracks generation-side velocity.&lt;/p&gt;

&lt;p&gt;Cheering people on to "review more carefully" is a morale policy, not an engineering one. Three structural changes actually move the numbers.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fix 1: Write the Verification Before You Generate the Code
&lt;/h2&gt;

&lt;p&gt;The highest-leverage change is an inversion: the unit tests, invariant assertions, and boundary checks get written &lt;em&gt;before&lt;/em&gt; the AI is triggered, by the same senior who'll review the output.&lt;/p&gt;

&lt;p&gt;The tests become the specification. When you review later, you're not reverse-engineering intent from the output anymore. You wrote the spec while the intent was still loaded in your head, so verification stops being archaeology and becomes diffing: does the implementation satisfy the spec or not.&lt;/p&gt;

&lt;p&gt;It also converts a senior's real expertise into leverage. What does a senior engineer actually know that a model doesn't? Where systems break. Tests written first encode that knowledge as an executable artifact, and it guards every future change to that surface, including ones nobody senior wrote. Reviewers who do this don't review less. They review &lt;em&gt;later and cheaper&lt;/em&gt;, because the machine already ran the adversarial pass against a spec the trusted human designed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fix 2: Let Machines Catch the Machine-Looking Errors
&lt;/h2&gt;

&lt;p&gt;The second fix is embarrassingly simple: stop asking humans to catch the errors machines catch better.&lt;/p&gt;

&lt;p&gt;Type mismatches, suspicious null handling, resource leaks, dependency rule breaks. This is what static analysis and type checkers are &lt;em&gt;for&lt;/em&gt;. Yet in most orgs the human is still the first line of defense for all of it, because the AI's output lands in a PR looking like a trusted colleague wrote it.&lt;/p&gt;

&lt;p&gt;Make the machine pass a hard precondition instead. Nothing AI-generated reaches a human until it survives strict type checking, linting, and AST-level structural analysis, including the concurrency-adjacent checks, since lock ordering violations leave syntactic fingerprints that decent tooling flags. Then the reviewer's screen shows only survivors, and human attention gets spent exclusively on what machines can't judge: does the logic match the intent, does the design fit the system, are the tradeoffs right.&lt;/p&gt;

&lt;p&gt;A rule of thumb worth pinning to a wall: &lt;strong&gt;if a linter could have caught it, a reviewer never should have seen it.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Fix 3: A Trust Matrix That Decides &lt;em&gt;Before&lt;/em&gt; Generation Where AI Works
&lt;/h2&gt;

&lt;p&gt;The third fix is organizational, and it's the one teams skip, which is why the first two tend to decay.&lt;/p&gt;

&lt;p&gt;Not all code carries the same blast radius, so one AI policy shouldn't cover all of it. The pattern that works is an explicit, team-visible trust matrix:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Low risk, green.&lt;/strong&gt; Boilerplate, test fixtures, mocks, glue code with no production surface. AI generation here should be the default. Friction is waste.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Medium risk, yellow.&lt;/strong&gt; Pure functions, utility scripts, well-bounded feature work with real test coverage. AI generates, standard review applies, test-first strongly recommended.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;High risk, red.&lt;/strong&gt; Concurrency, auth, cryptography, payment paths, anything touching customer data or security boundaries. Raw AI generation doesn't land here without a second human review. "The AI's tests passed" is not evidence.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The point isn't bureaucracy. It converts the verification tax from an ambient anxiety into a budgeted expense. You spend the scarce attention where the blast radius justifies it and nearly none where it doesn't.&lt;/p&gt;

&lt;p&gt;The side effect is the interesting part. Teams that run a trust matrix for a few quarters report that it changes what people prompt for in the first place. Once the high-risk zone is visibly fenced off, engineers stop asking the model to "write the auth middleware" and start asking it to "write the test suite that proves the auth middleware handles token revocation." Which is exactly the behavior the first two fixes were trying to incentivize.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Uncomfortable Part
&lt;/h2&gt;

&lt;p&gt;If a senior's core value is verifying systems they didn't design, and a model's core output is plausible code demanding exactly that verification, then the industry has quietly redefined senior engineering as QA for a probabilistic coworker.&lt;/p&gt;

&lt;p&gt;That's not necessarily a demotion. It's arguably what senior engineering always was. But it does mean every org measuring AI success by "lines generated" or "time-to-PR" is measuring the half of the cycle that was never the bottleneck.&lt;/p&gt;

&lt;p&gt;The way out isn't better prompts. It's tests written first, machines reviewing machines, and an explicit map of where the org's scarcest judgment belongs.&lt;/p&gt;




&lt;h2&gt;
  
  
  Your Turn
&lt;/h2&gt;

&lt;p&gt;I'm gathering notes for a series running up the ladder from senior engineer to VP, and this is the level everything above stands on.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Are you actually shipping faster, or shifting your hours from writing code to debugging synthetic edge cases you approved two weeks ago?&lt;/li&gt;
&lt;li&gt;How would you even measure the maintenance cost of code you accepted but didn't write? Does your org track AI churn at all, or does it only exist in postmortems?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Drop your war stories in the comments. Especially the ones where the generated code was idiomatic, passed every test, and still took the service down. Those are the ones no dashboard saw coming.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Next in the series:&lt;/strong&gt; Context Engineering over Prompt Engineering. Why your agent fails at architecture, not wording.&lt;/p&gt;

</description>
      <category>discuss</category>
      <category>ai</category>
      <category>softwareengineering</category>
      <category>productivity</category>
    </item>
    <item>
      <title>My Helm Chart Linted Green and the Pod Crash-Looped</title>
      <dc:creator>Debashish Ghosal</dc:creator>
      <pubDate>Mon, 19 Oct 2026 02:18:00 +0000</pubDate>
      <link>https://dev.to/debashish_ghosal/my-helm-chart-linted-green-and-the-pod-crash-looped-57fb</link>
      <guid>https://dev.to/debashish_ghosal/my-helm-chart-linted-green-and-the-pod-crash-looped-57fb</guid>
      <description>&lt;p&gt;The narrowest bug in my v0.2.0 field test is the one I keep telling SREs about. &lt;code&gt;helm lint&lt;/code&gt; passed. &lt;code&gt;helm template&lt;/code&gt; passed. The chart rendered. The API pod crash-looped, because the container probed port 8100 and the process listened on 8000.&lt;/p&gt;

&lt;p&gt;A green signal from a layer that doesn't include the runtime is a green signal about a different system.&lt;/p&gt;

&lt;p&gt;This is the article about configuration delivery as a seam on &lt;a href="https://github.com/deghosal-2026/hiveplane" rel="noopener noreferrer"&gt;HivePlane&lt;/a&gt; v0.2.0 — the failure mode that's invisible to &lt;code&gt;lint&lt;/code&gt;, &lt;code&gt;template&lt;/code&gt;, and any test that doesn't run the container. &lt;strong&gt;"It's in the &lt;code&gt;.env&lt;/code&gt;" is not "the process has it."&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Three configs that were exported and never passed
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Finding&lt;/th&gt;
&lt;th&gt;What was exported&lt;/th&gt;
&lt;th&gt;What the process got&lt;/th&gt;
&lt;th&gt;What broke&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Trigger webhook secret&lt;/td&gt;
&lt;td&gt;Set in &lt;code&gt;.env&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Never passed to the container&lt;/td&gt;
&lt;td&gt;Signed ingest returned &lt;code&gt;403&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Auth env (&lt;code&gt;HIVEPLANE_AUTH__*&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;Set in &lt;code&gt;.env&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Never passed to the container&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;--auth&lt;/code&gt; was a silent no-op&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Helm probes/ports&lt;/td&gt;
&lt;td&gt;Rendered in the chart&lt;/td&gt;
&lt;td&gt;Probe pointed at 8100; process on 8000&lt;/td&gt;
&lt;td&gt;Crash-loop; &lt;code&gt;helm lint&lt;/code&gt; green&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The trigger source that "couldn't possibly fail" failed on a missing env var. The entire RBAC suite ran against an unauthenticated plane because the auth env never reached the process. And the chart that linted green deployed a crash-looping pod.&lt;/p&gt;

&lt;h2&gt;
  
  
  The port that wasn't
&lt;/h2&gt;

&lt;p&gt;The Helm chart was the cleanest example. &lt;code&gt;values.yaml&lt;/code&gt; declared &lt;code&gt;api.containerPort: 8000&lt;/code&gt; and &lt;code&gt;api.service.port: 8100&lt;/code&gt;. The probes pointed at the service port. The process listened on the container port. The probe hit 8100 inside a container where nothing listened on 8100. Crash-loop. &lt;code&gt;helm lint&lt;/code&gt; has no opinion about whether the probe target matches the listening port — it validates the chart's schema, not the runtime's reality. This was &lt;strong&gt;F-12&lt;/strong&gt; in the field-test report — and it didn't stop at the port: the chart also never set the execution adapter or certification executor, so runs were admitted but never executed.&lt;/p&gt;

&lt;p&gt;The fix was aligning the probe to the container port (or the process to the service port — but you pick one and assert it). The deeper fix is a field test that runs the container and asserts the probe succeeds, not a lint that asserts the chart is well-formed.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;tip:&lt;/strong&gt; &lt;code&gt;helm lint&lt;/code&gt; proves your chart is valid YAML with valid schema. It does not prove your pod starts. A chart that lints green and crash-loops is a chart whose test suite stopped at the wrong layer. Test the wiring inside the container, not the template outside it.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The secret that was in the .env and not in the process
&lt;/h2&gt;

&lt;p&gt;The trigger webhook secret was the one that felt impossible. I had set &lt;code&gt;HIVEPLANE_TRIGGERS__SECRETS&lt;/code&gt; in &lt;code&gt;.env&lt;/code&gt;. The signed ingest endpoint verified HMAC over the body. It returned &lt;code&gt;403&lt;/code&gt; — bad signature — because the secret the process used was empty. The &lt;code&gt;.env&lt;/code&gt; had it; the container didn't.&lt;/p&gt;

&lt;p&gt;Configuration delivery is a seam. The value exists in the orchestrator's model (the &lt;code&gt;.env&lt;/code&gt;, the chart values, the compose environment) and never reaches the process. It's invisible to any test that doesn't run the container, because the in-process test reads the &lt;code&gt;.env&lt;/code&gt; directly. The field-test report calls this out explicitly in its lessons: &lt;strong&gt;"env vars exported by the runner but absent from the container's &lt;code&gt;environment&lt;/code&gt; become unset"&lt;/strong&gt; — producing a 403 (trigger secret) or an effectively disabled auth layer.&lt;/p&gt;

&lt;p&gt;The fix pattern from the field test: assert the value is &lt;strong&gt;passed&lt;/strong&gt;, not merely &lt;strong&gt;exported&lt;/strong&gt;. A config-delivery assertion at the container boundary — &lt;code&gt;docker compose exec api printenv HIVEPLANE_TRIGGERS__SECRETS&lt;/code&gt; is non-empty — turns "it's in the .env" into "the process has it."&lt;/p&gt;

&lt;h2&gt;
  
  
  The auth that was a no-op
&lt;/h2&gt;

&lt;p&gt;The auth env was the same bug with a worse blast radius. &lt;code&gt;HIVEPLANE_AUTH__ENABLED=true&lt;/code&gt; was set in &lt;code&gt;.env&lt;/code&gt; and not passed to the container. So &lt;code&gt;--auth&lt;/code&gt; was a silent no-op. The entire RBAC suite — the one I wrote the previous article about — was running against an unauthenticated plane. Every "viewer cannot approve" test passed because there were no viewers and no roles; everyone was anonymous-admin.&lt;/p&gt;

&lt;p&gt;A green RBAC test against a container that never received the auth env proves your chart renders and your tests assume. It does not prove authorization works. The fix is the same seam: assert the env is present inside the container, then run the suite.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I learned
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;"Exported" is not "passed."&lt;/strong&gt; A value in the &lt;code&gt;.env&lt;/code&gt; or the chart values exists in the orchestrator's model. The process is a different process in a different mount namespace with a different environment. The seam between them is a testable surface — and it's the one &lt;code&gt;lint&lt;/code&gt; and &lt;code&gt;template&lt;/code&gt; never touch.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Lint proves the chart; the pod proves the wiring.&lt;/strong&gt; &lt;code&gt;helm lint&lt;/code&gt; validates schema. &lt;code&gt;helm template&lt;/code&gt; renders manifests. Neither runs the container. A green chart and a crash-looping pod are not contradictory — they're about different systems. Test the one the operator runs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Assert the value inside the container.&lt;/strong&gt; A config-delivery assertion at the boundary — the env var is present in the process that uses it — turns a silent no-op into a loud failure. "It's in the .env" is a statement about your filesystem; "the process has it" is a statement about your runtime.&lt;/p&gt;

&lt;p&gt;The v0.2.0 field test runs the full container suite against the live Docker Compose stack — &lt;strong&gt;67/67 scenarios&lt;/strong&gt; — and the config-delivery findings are the ones that only surfaced because the test ran the container, not the chart. Full evidence in the &lt;a href="https://github.com/deghosal-2026/hiveplane/blob/main/docs/field-test/v0.2.0/DOCKER_TEST_REPORT.md" rel="noopener noreferrer"&gt;Docker test report&lt;/a&gt;; the Helm chart and k3d reference deployment are under &lt;code&gt;deploy/&lt;/code&gt; with a &lt;a href="https://github.com/deghosal-2026/hiveplane/blob/main/docs/runbooks/k3d-reference-deploy.md" rel="noopener noreferrer"&gt;runbook&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://github.com/deghosal-2026/hiveplane" rel="noopener noreferrer"&gt;HivePlane&lt;/a&gt; — &lt;code&gt;pip install hiveplane==0.2.0&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/deghosal-2026/hiveplane/blob/main/docs/field-test/v0.2.0/DOCKER_TEST_REPORT.md" rel="noopener noreferrer"&gt;Docker test report (67/67)&lt;/a&gt; · &lt;a href="https://github.com/deghosal-2026/hiveplane/blob/main/docs/field-test/v0.2.0/FIELD_TEST_REPORT.md" rel="noopener noreferrer"&gt;Field test report (F-12, §6.7–6.9)&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/deghosal-2026/hiveplane/blob/main/docs/runbooks/k3d-reference-deploy.md" rel="noopener noreferrer"&gt;k3d reference deployment runbook&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/deghosal-2026/hiveplane/tree/main/deploy/helm/hiveplane" rel="noopener noreferrer"&gt;Helm chart&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Pick one env var your chart sets. Is it present &lt;em&gt;inside&lt;/em&gt; the running container — or only in the values file? If you can't answer without running the pod, your config-delivery seam is untested. And your &lt;code&gt;helm lint&lt;/code&gt; is green.&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>testing</category>
      <category>k8</category>
      <category>crash</category>
    </item>
    <item>
      <title>A Drift Detector That Cries Wolf Is Worse Than No Detector</title>
      <dc:creator>Debashish Ghosal</dc:creator>
      <pubDate>Sun, 18 Oct 2026 01:15:55 +0000</pubDate>
      <link>https://dev.to/debashish_ghosal/a-drift-detector-that-cries-wolf-is-worse-than-no-detector-2gc2</link>
      <guid>https://dev.to/debashish_ghosal/a-drift-detector-that-cries-wolf-is-worse-than-no-detector-2gc2</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;My drift detector quarantined a healthy agent. The agent hadn't gotten worse — the benchmark had gotten bigger. The detector compared failure counts across corpus versions, so growing the benchmark from 10 tasks to 20 made a 10/10 agent look like it "failed" half the suite. Corpus growth looked like decay.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I built the drift detector for &lt;a href="https://github.com/deghosal-2026/hiveplane" rel="noopener noreferrer"&gt;HivePlane&lt;/a&gt; v0.2.0 to catch the failure mode I'd written about in v0.1.0 — "the dangerous agent usually drifted, not got edited." The harder problem turned out not to be detecting drift. It was &lt;strong&gt;not detecting drift that wasn't there.&lt;/strong&gt; A detector that cries wolf trains operators to ignore the signal, and an ignored gate is no gate.&lt;/p&gt;

&lt;h2&gt;
  
  
  The detector that ate its own benchmark
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Finding&lt;/th&gt;
&lt;th&gt;What the detector did&lt;/th&gt;
&lt;th&gt;Why it was wrong&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;#530&lt;/td&gt;
&lt;td&gt;Compared failure &lt;em&gt;counts&lt;/em&gt; across corpus versions&lt;/td&gt;
&lt;td&gt;Growing the benchmark auto-quarantined a healthy agent&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;F-13&lt;/td&gt;
&lt;td&gt;Reinstatement required &lt;code&gt;status == QUARANTINED&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Auto-quarantine resets to &lt;code&gt;UNCERTIFIED&lt;/code&gt; — the gate refused its own quarantine&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;#609 / S24&lt;/td&gt;
&lt;td&gt;The probe runner only checked admission&lt;/td&gt;
&lt;td&gt;A probe could never flag behavioral decay — only a refused run could fail one&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The scariest line in the release wasn't a missed drift. It was a &lt;strong&gt;healthy agent quarantined because the detector compared it to a benchmark that no longer existed.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The benchmark that looked like decay
&lt;/h2&gt;

&lt;h1&gt;
  
  
  530 was the one that changed how I think about drift baselines. The detector compared failure &lt;em&gt;counts&lt;/em&gt; across corpus versions. If the benchmark grew from 10 tasks to 20 — because the learning loop added a reviewed case — a healthy agent that passed 10/10 now "failed" 10 of 20 by the old accounting. &lt;strong&gt;Corpus growth looked like decay.&lt;/strong&gt;
&lt;/h1&gt;

&lt;p&gt;The agent hadn't gotten worse. The benchmark had gotten bigger. The detector couldn't tell the difference, so it quarantined a perfectly good agent.&lt;/p&gt;

&lt;p&gt;The fix: drift is measured against the agent's &lt;em&gt;own baseline on comparable corpora&lt;/em&gt;. You compare the same tasks across re-evaluations, not different counts. A corpus version change establishes a &lt;strong&gt;new baseline&lt;/strong&gt;, not a drift signal. Growing the benchmark is growth, not regression.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;tip:&lt;/strong&gt; If your drift metric is a count, a benchmark change will look like behavior change. Compare the agent to itself on the same tasks, or any corpus edit becomes a false quarantine.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The reinstatement that refused its own quarantine
&lt;/h2&gt;

&lt;p&gt;F-13 was the symmetry that broke. Drift auto-quarantine resets the certification status to &lt;code&gt;UNCERTIFIED&lt;/code&gt; — not &lt;code&gt;QUARANTINED&lt;/code&gt;. Reinstatement required &lt;code&gt;status == QUARANTINED&lt;/code&gt;. So the gate that &lt;em&gt;created&lt;/em&gt; the quarantine refused to &lt;em&gt;lift&lt;/em&gt; it. A workload could be auto-quarantined and then never reinstated, because the precondition described a state the detector never set.&lt;/p&gt;

&lt;p&gt;The fix was removing the stale precondition. An active quarantine record is reinstatable — the workload is re-certified and admission is re-checked. The status field is not the source of truth; the quarantine record is. The gate keyed off the wrong one.&lt;/p&gt;

&lt;p&gt;The field test caught this in scenario S5, where reinstatement after S4's auto-quarantine failed in sequence. Two scenarios, one bug, and the lesson: the source of truth for "is this quarantined" is the quarantine record, not the certification status enum.&lt;/p&gt;

&lt;h2&gt;
  
  
  The probe that couldn't smell decay
&lt;/h2&gt;

&lt;h1&gt;
  
  
  609 / S24 was the most subtle. The synthetic probe runner ignored &lt;code&gt;Spec.expected&lt;/code&gt; and only checked admission. So a probe could only &lt;em&gt;fail&lt;/em&gt; if the run was refused — it could never flag that the agent ran and produced the wrong answer. &lt;strong&gt;The probe was a liveness check pretending to be a readiness check.&lt;/strong&gt;
&lt;/h1&gt;

&lt;p&gt;A probe's job is to catch decay &lt;em&gt;before&lt;/em&gt; the drift threshold trips. If the probe only checks "did it run," it can't catch "did it run &lt;em&gt;correctly&lt;/em&gt;." The fix: the runner starts the run, waits for terminal, and compares the output to &lt;code&gt;expected&lt;/code&gt;. A mismatch fails the probe with a &lt;code&gt;probe.decay&lt;/code&gt; warning. Now a probe can smell decay the drift detector hasn't measured yet.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I learned
&lt;/h2&gt;

&lt;p&gt;False-positive controls are the product, not a nicety: a single exceeding run is a warning, and quarantine requires N consecutive failures or a strong signal — a stable agent is never quarantined on noise. Measure drift against comparable baselines: the same tasks, the same corpus version; a corpus bump is a new baseline, not a regression. The quarantine record is the source of truth, not the status enum — reinstatement keys off the record, or it refuses the very quarantines it should lift. And a probe must assert the expected output, not just that the run was admitted — liveness is not readiness.&lt;/p&gt;

&lt;p&gt;The v0.2.0 field test demonstrates the full loop — drift due/assess/probe, auto-quarantine, reinstatement after a fresh certification — as scenarios S4, S5, S8, and S24. The synthetic probe flags decay before the drift threshold in S24. Full evidence in the &lt;a href="https://github.com/deghosal-2026/hiveplane/blob/main/docs/field-test/v0.2.0/FIELD_TEST_REPORT.md" rel="noopener noreferrer"&gt;field-test report&lt;/a&gt;; the certification and learning-loop designs are in the &lt;a href="https://github.com/deghosal-2026/hiveplane/blob/main/docs/design/certification-v2-design.md" rel="noopener noreferrer"&gt;certification v2 design&lt;/a&gt; and the &lt;a href="https://github.com/deghosal-2026/hiveplane/blob/main/docs/design/learning-loop-design.md" rel="noopener noreferrer"&gt;learning loop design&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://github.com/deghosal-2026/hiveplane" rel="noopener noreferrer"&gt;HivePlane&lt;/a&gt; — &lt;code&gt;pip install hiveplane==0.2.0&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/deghosal-2026/hiveplane/blob/main/docs/field-test/v0.2.0/FIELD_TEST_REPORT.md" rel="noopener noreferrer"&gt;Field test report (36/36)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/deghosal-2026/hiveplane/blob/main/docs/design/certification-v2-design.md" rel="noopener noreferrer"&gt;Certification v2 design&lt;/a&gt; · &lt;a href="https://github.com/deghosal-2026/hiveplane/blob/main/docs/design/learning-loop-design.md" rel="noopener noreferrer"&gt;Learning loop design&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/deghosal-2026/hiveplane/blob/main/docs/runbooks/operator-runbook.md" rel="noopener noreferrer"&gt;Operator runbook — drift &amp;amp; quarantine&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;When was the last time your detector quarantined something that was fine? If your operators have learned to reinstate without reading, your detector isn't a control — it's a checkbox.&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>testing</category>
      <category>llm</category>
      <category>healthydebate</category>
    </item>
    <item>
      <title>The Bug Lives at the Seam, Not in the Logic</title>
      <dc:creator>Debashish Ghosal</dc:creator>
      <pubDate>Fri, 16 Oct 2026 02:16:00 +0000</pubDate>
      <link>https://dev.to/debashish_ghosal/the-bug-lives-at-the-seam-not-in-the-logic-48j4</link>
      <guid>https://dev.to/debashish_ghosal/the-bug-lives-at-the-seam-not-in-the-logic-48j4</guid>
      <description>&lt;p&gt;The encrypted secret was &lt;code&gt;bytes&lt;/code&gt;. It round-tripped perfectly in the in-memory store. The unit tests passed. In Postgres, the column was JSON, and &lt;code&gt;bytes&lt;/code&gt; is not JSON-serializable. The secret store blew up at the one boundary the in-memory test never crossed.&lt;/p&gt;

&lt;p&gt;That was the pattern of the entire v0.2.0 field test on &lt;a href="https://github.com/deghosal-2026/hiveplane" rel="noopener noreferrer"&gt;HivePlane&lt;/a&gt;. The hard part wasn't the logic — it was the data model and the boundaries. Every defect was a value that was fine in Python and broke one step later, at a persistence seam.&lt;/p&gt;

&lt;p&gt;This is the article about tracing each value across every persistence boundary — because the bug lives one boundary further than your unit test.&lt;/p&gt;

&lt;h2&gt;
  
  
  One defect per seam-type
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Defect&lt;/th&gt;
&lt;th&gt;The value that crossed the seam&lt;/th&gt;
&lt;th&gt;Where it broke&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;D-5&lt;/td&gt;
&lt;td&gt;Secret ciphertext as &lt;code&gt;bytes&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Postgres JSON persistence rejected binary&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;D-7&lt;/td&gt;
&lt;td&gt;A &lt;code&gt;desired_specs&lt;/code&gt; row keyed by id&lt;/td&gt;
&lt;td&gt;Second source hit a PK collision — id vs uniqueness&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;D-8&lt;/td&gt;
&lt;td&gt;The kill-switch &lt;em&gt;record&lt;/em&gt; existed&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;GET /tools/disabled&lt;/code&gt; listed &lt;em&gt;enabled&lt;/em&gt; tools — state vs record&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;D-9&lt;/td&gt;
&lt;td&gt;A workload with runs, deleted&lt;/td&gt;
&lt;td&gt;Raw FK violation &lt;code&gt;500&lt;/code&gt; instead of a &lt;code&gt;409&lt;/code&gt; with a reason&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;#573&lt;/td&gt;
&lt;td&gt;A backup manifest pinning &lt;code&gt;alembic_head=0034&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Schema head was &lt;code&gt;0035&lt;/code&gt; — restore against the wrong schema&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Read that table as the taxonomy. The seam, not the logic, is where each one failed.&lt;/p&gt;

&lt;h2&gt;
  
  
  The bytes that broke JSON
&lt;/h2&gt;

&lt;p&gt;D-5 was the cleanest example. The encrypted secret vault stored ciphertext as &lt;code&gt;bytes&lt;/code&gt;. In the in-memory store, it round-tripped perfectly. The unit tests passed. In Postgres, the column was JSON, and &lt;code&gt;bytes&lt;/code&gt; is not JSON-serializable. The secret store blew up at the persistence boundary — the one place the in-memory test never exercised.&lt;/p&gt;

&lt;p&gt;The fix wasn't in the model; it was at the boundary: base64-encode the ciphertext on the way to Postgres, decode on the way back. The model stays &lt;code&gt;bytes&lt;/code&gt;. The seam learns to translate. The unit test that uses the in-memory store still passes — and still proves nothing about the seam.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;tip:&lt;/strong&gt; Your in-memory store and your Postgres store share an interface, not a serialization. A value that round-trips in memory can fail at the JSON column. Trace the value across the boundary the in-memory test never crosses.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The second source that broke the first
&lt;/h2&gt;

&lt;p&gt;D-7 was an id-versus-uniqueness confusion. The reconcile loader keyed &lt;code&gt;desired_specs&lt;/code&gt; by id. One source worked. A second source, applying the same desired state, hit a primary-key violation — because the row already existed, and the key assumed one source. The uniqueness constraint and the id were the same field, and they meant different things.&lt;/p&gt;

&lt;p&gt;The fix is composite keys and idempotent upserts: &lt;code&gt;(source_id, workload_id)&lt;/code&gt;. A second source is a second row, not a collision. The bug only appears when you have two sources, which is why the single-source field test never caught it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The enabled tool that was "disabled"
&lt;/h2&gt;

&lt;p&gt;D-8 was a state-versus-record confusion that I still find funny. &lt;code&gt;GET /tools/disabled&lt;/code&gt; returned a list that included &lt;em&gt;enabled&lt;/em&gt; tools. The kill-switch record existed in the table; the tool was not actually disabled. The endpoint reported the record, not the state. A "disabled tools" list that includes enabled tools is worse than no list — it trains the operator to ignore it.&lt;/p&gt;

&lt;p&gt;The fix: the endpoint reports the &lt;em&gt;state&lt;/em&gt;, derived from the record and the current tool status, not the record alone. A record of a disable that was later reversed is history, not a current fact.&lt;/p&gt;

&lt;h2&gt;
  
  
  The FK that returned 500
&lt;/h2&gt;

&lt;p&gt;D-9 was the database teaching me API design. Deleting a workload that had runs returned a &lt;code&gt;500&lt;/code&gt; — a raw foreign-key violation surfacing as an internal error. The caller got a server error; the operator got no reason. The fix is a &lt;code&gt;409&lt;/code&gt; with "workload has N runs; delete the runs first or cascade." A constraint violation is a &lt;em&gt;user-facing condition&lt;/em&gt;, not a server fault. The seam between the database and the API is where a &lt;code&gt;500&lt;/code&gt; becomes a &lt;code&gt;409&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The schema head the manifest lied about
&lt;/h2&gt;

&lt;h1&gt;
  
  
  573
&lt;/h1&gt;

&lt;p&gt;It was the one that could have been catastrophic. A backup manifest pinned &lt;code&gt;alembic_head&lt;/code&gt; to &lt;code&gt;0034&lt;/code&gt;. The live schema head was &lt;code&gt;0035&lt;/code&gt;. A restore would have succeeded — the manifest said the schemas matched — against a database that was one migration ahead. The restore verified the manifest's claim, not the world's truth.&lt;/p&gt;

&lt;p&gt;The fix: the restore verifies the &lt;em&gt;actual&lt;/em&gt; schema head against the manifest, and refuses a mismatch. A manifest that lies about its own head is a backup that restores you to the wrong version of your own system.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I learned
&lt;/h2&gt;

&lt;p&gt;Trace each value across every persistence boundary: binary-vs-JSON, global-key-vs-tenant-scope, id-vs-uniqueness, state-vs-record, FK-vs-409, manifest-head-vs-schema-head. A unit test that stops at the seam passes; the bug lives one boundary further. The Postgres-gated tests — the ones CI runs with a database — are the ones that catch it. And a constraint violation is a user condition, not a server fault: the seam between the database and the API is where &lt;code&gt;500&lt;/code&gt; becomes &lt;code&gt;409&lt;/code&gt; with a reason.&lt;/p&gt;

&lt;p&gt;The v0.2.0 field test runs zero-skip against the real stack with real PostgreSQL — &lt;strong&gt;36/36 scenarios, 67/67 container tests&lt;/strong&gt; — and the defect ledger (D-1…D-11) is the evidence that the seams, not the logic, were the work. Full receipts in the &lt;a href="https://github.com/deghosal-2026/hiveplane/blob/main/docs/field-test/v0.2.0/FIELD_TEST_REPORT.md" rel="noopener noreferrer"&gt;field-test report&lt;/a&gt; and the &lt;a href="https://github.com/deghosal-2026/hiveplane/blob/main/docs/field-test/v0.2.0/DOCKER_TEST_REPORT.md" rel="noopener noreferrer"&gt;Docker test report&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://github.com/deghosal-2026/hiveplane" rel="noopener noreferrer"&gt;HivePlane&lt;/a&gt; — &lt;code&gt;pip install hiveplane==0.2.0&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/deghosal-2026/hiveplane/blob/main/docs/field-test/v0.2.0/FIELD_TEST_REPORT.md" rel="noopener noreferrer"&gt;Field test report (36/36)&lt;/a&gt; · &lt;a href="https://github.com/deghosal-2026/hiveplane/blob/main/docs/field-test/v0.2.0/DOCKER_TEST_REPORT.md" rel="noopener noreferrer"&gt;Docker report (67/67)&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/deghosal-2026/hiveplane/blob/main/docs/release/v0.2.0/migration-guide.md" rel="noopener noreferrer"&gt;Migration guide (v0.1.0 → v0.2.0)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/deghosal-2026/hiveplane/blob/main/docs/design/state-store-design.md" rel="noopener noreferrer"&gt;State store design&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Pick one value in your system and trace it from the model to the Postgres column to the API response. Where does it change shape? That's where your next bug is — and where your unit test stops.&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>testing</category>
      <category>llm</category>
      <category>programming</category>
    </item>
    <item>
      <title>No Bootstrap, No Principal: The Day My Audit Log Became Fiction</title>
      <dc:creator>Debashish Ghosal</dc:creator>
      <pubDate>Thu, 15 Oct 2026 02:14:00 +0000</pubDate>
      <link>https://dev.to/debashish_ghosal/no-bootstrap-no-principal-the-day-my-audit-log-became-fiction-562n</link>
      <guid>https://dev.to/debashish_ghosal/no-bootstrap-no-principal-the-day-my-audit-log-became-fiction-562n</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;My v0.1.0 series couldn't talk about authorization, because there wasn't any. v0.2.0 shipped RBAC-lite, scoped API keys, and an encrypted secret vault on &lt;a href="https://github.com/deghosal-2026/hiveplane" rel="noopener noreferrer"&gt;HivePlane&lt;/a&gt; — and the first thing the field test taught me is that &lt;strong&gt;attribution has a cold-start problem.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I shipped a control plane with roles, permissions, and an audit trail — and for an hour, none of it was reachable, because there was no way to mint the first API key.&lt;/p&gt;

&lt;p&gt;This is the article about the prerequisite for every governance claim I made in the earlier series: a principal. Without one, your audit log is fiction.&lt;/p&gt;

&lt;h2&gt;
  
  
  The cold start
&lt;/h2&gt;

&lt;p&gt;The finding was D-10. RBAC was wired. &lt;code&gt;/auth/login&lt;/code&gt; existed. Scoped keys were implemented. And the entire model was unreachable end-to-end, because there was no HTTP bootstrap to mint the first admin key. The roles existed; nobody could assume one.&lt;/p&gt;

&lt;p&gt;The fix was a dev-grade env admin key — boot the first principal from the environment, then issue scoped keys from there:&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;# the first principal has to come from outside the API&lt;/span&gt;
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;HIVEPLANE_AUTH__ENABLED&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;true
export &lt;/span&gt;&lt;span class="nv"&gt;HIVEPLANE_AUTH__ADMIN_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;openssl rand &lt;span class="nt"&gt;-hex&lt;/span&gt; 32&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="c"&gt;# now you can POST /keys to issue scoped, tenant-bound keys&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The lesson is that &lt;strong&gt;authn is the admission gate for the operators.&lt;/strong&gt; Until there's a principal, every "who did this" question is unanswerable, and every other governance feature is theater.&lt;/p&gt;

&lt;h2&gt;
  
  
  The audit log that named the wrong person
&lt;/h2&gt;

&lt;p&gt;Then I found the worse one. Pause, resume, stop, kill-switch, quarantine — every privileged operator action — was attributed to a &lt;strong&gt;client-supplied &lt;code&gt;actor&lt;/code&gt; field&lt;/strong&gt;. The caller put &lt;code&gt;--operator alice&lt;/code&gt; in the request. The audit log recorded &lt;code&gt;alice&lt;/code&gt;. Any caller could sign an action as anyone.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;tip:&lt;/strong&gt; A &lt;code&gt;who&lt;/code&gt; column populated by the request body is a confession that you don't know. The actor must come from the authenticated principal, not the payload. A client-supplied &lt;code&gt;actor&lt;/code&gt; is repudiation by construction.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Findings #560, #561, and #541 were all the same shape: the action was real, the attribution was a string the caller chose. The fix is that the principal resolves the actor; the field is read-only. You can attribute an action to yourself. You cannot attribute it to someone else.&lt;/p&gt;

&lt;h2&gt;
  
  
  The endpoints that had no permission at all
&lt;/h2&gt;

&lt;p&gt;Then the field test found the gaps RBAC was supposed to cover and didn't:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;#537 — registry mutation endpoints had no permission checks. A &lt;strong&gt;viewer&lt;/strong&gt; could &lt;code&gt;DELETE /workloads&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;#526 — destructive immune-system endpoints (quarantine, kill-switch) were ungated.&lt;/li&gt;
&lt;li&gt;#563 — pause/resume/stop had no permission gate; a viewer could stop any run.&lt;/li&gt;
&lt;li&gt;#566 — the UI session secret shipped a hardcoded public default.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A role system that doesn't gate the destructive verbs is a role system in name only. The fix is server-side authorization on every privileged action: approve, promote, kill-switch. A viewer cannot approve. A viewer cannot stop. A viewer cannot delete a workload. The 401/403 is the product.&lt;/p&gt;

&lt;h2&gt;
  
  
  HMAC-first, not bearer-first
&lt;/h2&gt;

&lt;p&gt;The one I didn't see coming was #515. With auth enabled, GitHub and Alertmanager webhooks got &lt;code&gt;401&lt;/code&gt; before the HMAC signature was verified. The bearer-first model broke event ingest from ≥3 sources — which is a release gate. Triggers are the autonomy layer; if they can't authenticate, the fleet doesn't run itself.&lt;/p&gt;

&lt;p&gt;The fix is that webhook ingest is &lt;strong&gt;HMAC-first, not bearer-first.&lt;/strong&gt; The signature is verified before any bearer token is required; the trigger source is trusted by its HMAC, not by a login. Autonomy and authorization have to coexist, or you've secured the plane against its own purpose.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I learned
&lt;/h2&gt;

&lt;p&gt;Bootstrap the first principal, or none of it exists — the env admin key is the admission gate for operators. Attribution comes from the principal, never the payload: a client-supplied &lt;code&gt;actor&lt;/code&gt; is repudiation by construction. Gate the destructive verbs — a viewer who can delete a workload doesn't have a role, they have root. And webhooks are HMAC-first: bearer-first auth breaks event ingest, and autonomy and authorization have to coexist or you've secured the plane against its own purpose.&lt;/p&gt;

&lt;p&gt;The v0.2.0 field test asserts per-tenant budget/policy/key isolation with a viewer who cannot approve, and a secret that never appears in logs/traces/agent context (verified by test). Full evidence in the &lt;a href="https://github.com/deghosal-2026/hiveplane/blob/main/docs/field-test/v0.2.0/FIELD_TEST_REPORT.md" rel="noopener noreferrer"&gt;field-test report&lt;/a&gt;; the auth bootstrap and key-rotation runbook is at &lt;a href="https://github.com/deghosal-2026/hiveplane/blob/main/docs/runbooks/auth-bootstrap-and-key-rotation.md" rel="noopener noreferrer"&gt;docs/runbooks/auth-bootstrap-and-key-rotation.md&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://github.com/deghosal-2026/hiveplane" rel="noopener noreferrer"&gt;HivePlane&lt;/a&gt; — &lt;code&gt;pip install hiveplane==0.2.0&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/deghosal-2026/hiveplane/blob/main/docs/field-test/v0.2.0/FIELD_TEST_REPORT.md" rel="noopener noreferrer"&gt;Field test report (36/36)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/deghosal-2026/hiveplane/blob/main/docs/runbooks/auth-bootstrap-and-key-rotation.md" rel="noopener noreferrer"&gt;Auth bootstrap &amp;amp; key rotation runbook&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/deghosal-2026/hiveplane/blob/main/docs/design/secrets-rbac-design.md" rel="noopener noreferrer"&gt;Secrets, RBAC &amp;amp; access audit design&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/deghosal-2026/hiveplane/blob/main/docs/USER_GUIDE.md" rel="noopener noreferrer"&gt;Secrets, RBAC &amp;amp; access audit (User Guide)&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Open your audit log. Pick one privileged action. Is the &lt;code&gt;who&lt;/code&gt; populated by the authenticated principal — or by a field the caller wrote? If it's the second, your audit log is a story someone else is telling you.&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>testing</category>
      <category>controlplane</category>
      <category>healthydebate</category>
    </item>
    <item>
      <title>The 75% Code Trap: How Senior Engineers Are Actually Surviving AI Mandates</title>
      <dc:creator>Debashish Ghosal</dc:creator>
      <pubDate>Wed, 14 Oct 2026 13:33:10 +0000</pubDate>
      <link>https://dev.to/debashish_ghosal/the-75-code-trap-how-senior-engineers-are-actually-surviving-ai-mandates-1ngh</link>
      <guid>https://dev.to/debashish_ghosal/the-75-code-trap-how-senior-engineers-are-actually-surviving-ai-mandates-1ngh</guid>
      <description>&lt;p&gt;The mandate arrived without a warning shot: &lt;strong&gt;75% of production code will be AI-generated.&lt;/strong&gt; Not a north star. Not a vision slide. A number with a date attached, and your team's name on it.&lt;/p&gt;

&lt;p&gt;Across the industry this is already old news — targets between 50% and 75% sit in OKRs from startups to enterprises, and the dashboards are live: lines of AI code accepted, active Copilot sessions, PR turnaround time.&lt;/p&gt;

&lt;p&gt;Here's the question nobody asked in that meeting: &lt;strong&gt;75% of &lt;em&gt;what&lt;/em&gt;, verified by &lt;em&gt;whom&lt;/em&gt;?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This is part 1 of &lt;strong&gt;AI Developer Experience&lt;/strong&gt;, a 10-part series about the half of software engineering that got more important the moment code got cheap to write. If your review queue grew the day your team "adopted AI," this series is for you — what follows is what the engineers surviving these mandates actually do. Not the vendor version. Not the doom version.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Dashboards Look Great. The Codebase Doesn't.
&lt;/h2&gt;

&lt;p&gt;Leadership expects a linear leap in feature velocity, and for a hot minute the graphs agree: generation spikes, PRs merge faster, the demo sings. 📈&lt;/p&gt;

&lt;p&gt;Three months later, the cracks show up. The PR that passed every check and still broke a payment edge case. The helper function that already existed — twice. The dependency nobody remembers approving, imported by nobody who can explain it.&lt;/p&gt;

&lt;p&gt;The bottleneck didn't disappear when generation got cheap. &lt;strong&gt;It moved&lt;/strong&gt; — onto the desk of whoever understands the system best.&lt;/p&gt;

&lt;p&gt;Do the math on the asymmetry. An LLM drafts 500 lines of syntactically valid TypeScript in under ten seconds. Auditing those 500 lines for security holes, memory leaks, and alignment with your actual architecture takes a human hours. At 40 PRs a week, generation is essentially free. Verification costs exactly what it always did — senior human attention, the scarcest resource in the building.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"We aren't spending less time on software engineering; we are spending our time operating as high-stress editors auditing code written by a brilliant, context-blind intern who works at light speed." — a staff systems architect at a global SaaS platform&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The intern metaphor is doing a lot of work, so let's be precise about it. Interns can be coached. Interns remember your codebase after a month. This one writes the same confident mistakes at light speed, forever, and never remembers your system at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Context Void: What the Model Can't Know
&lt;/h2&gt;

&lt;p&gt;Give the model an isolated function or a React component, and it's genuinely impressive — pattern-matched against millions of repos, statistically fluent in your framework.&lt;/p&gt;

&lt;p&gt;Give it your &lt;em&gt;system&lt;/em&gt;, and the fluency becomes a liability. It doesn't know your legacy state machine, patched since 2019 by four people who have all left. It doesn't know your distributed transaction guarantees, the ones the payments team learned the hard way, in an incident. It doesn't know about the edge case that only appears when the German subsidiary invoices in a different currency on the last day of the quarter.&lt;/p&gt;

&lt;p&gt;That's the context void. The model writes plausible code for a &lt;em&gt;generic&lt;/em&gt; version of your system. Your system is not generic.&lt;/p&gt;

&lt;p&gt;The gap between plausible-generic and correct-specific is exactly what review has to close now — one PR at a time, with the reviewer reconstructing context the writer never had.&lt;/p&gt;

&lt;p&gt;Here's what that does to the job. The first twenty minutes of a review used to be reading. Now the first twenty minutes are archaeology: why does this exist, what did it replace, who calls it, what breaks if the German-quarter edge case fires? The reviewer pays that context-reconstruction cost on every single PR — and the PRs keep arriving at machine rate.&lt;/p&gt;

&lt;p&gt;Generation got cheaper. The context bill didn't. It just changed hands, and it landed on the same desk every time.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Three Failures Hiding Behind a Green CI Run
&lt;/h2&gt;

&lt;p&gt;Review fatigue has a shape. AI-assisted PRs arrive superficially complete: linters pass, unit tests pass, the demo works. The deep problems hide underneath a green checkmark.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Duplication at scale
&lt;/h3&gt;

&lt;p&gt;The model can't see your internal utility libraries, so it reinvents them. Every helper you maintain gets a fresh, slightly-different twin. Your codebase balloons without a single architectural decision being made — nobody chose that shape. It accreted.&lt;/p&gt;

&lt;p&gt;This one is measurable, not a vibe. &lt;a href="https://www.gitclear.com/ai_assistant_code_quality_2025_research" rel="noopener noreferrer"&gt;GitClear's 2025 analysis&lt;/a&gt; of 211 million changed lines found the share of commits containing duplicated code blocks rising from under 1% in 2022 to nearly 7% in 2024, while refactoring fell from 25% of changed lines to under 10%. 2024 was the first year on record where copy/pasted lines exceeded moved lines — duplication is now the default.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Hallucinated dependencies
&lt;/h3&gt;

&lt;p&gt;Imports that don't exist. Packages deprecated years ago. Names one character away from a popular package — supply-chain risk wearing a typosquatting costume, generated at machine speed, submitted by someone who didn't check because it &lt;em&gt;looked&lt;/em&gt; right.&lt;/p&gt;

&lt;p&gt;This failure mode also has hard numbers. A &lt;a href="https://www.usenix.org/publications/loginonline/we-have-package-you-comprehensive-analysis-package-hallucinations-code" rel="noopener noreferrer"&gt;USENIX Security 2025 study&lt;/a&gt; ran 2.23 million generated code samples through 16 code-generating models: &lt;strong&gt;19.7% referenced at least one package that doesn't exist.&lt;/strong&gt; Worse for defenders: 43% of hallucinated names reappeared on every re-run of the same prompt. Predictable hallucinations are an attack surface — anyone can register the name before your developer installs it. 🎣&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Silent logic errors
&lt;/h3&gt;

&lt;p&gt;Human code tends to fail loudly — a stack trace cursing at you from line 400. Generated code compiles clean and fails quietly: at boundary conditions, under concurrency, in the states nobody tests. The bugs aren't necessarily worse. They're quieter — and quiet is the expensive property.&lt;/p&gt;

&lt;p&gt;None of these three show up in "lines of AI code accepted." All of them show up at 2 a.m.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Trap Is the Measurement
&lt;/h2&gt;

&lt;p&gt;The contrarian take: &lt;strong&gt;the 75% number isn't a productivity metric. It's a verification-debt counter.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Every point of "AI-generated code" the dashboard celebrates is a point of human verification somebody has to pay for later — usually a senior engineer, usually under deadline, usually on code they understand least, because something else wrote it.&lt;/p&gt;

&lt;p&gt;Two receipts from the research side say the industry already knows this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://www.gartner.com/en/newsroom/press-releases/2024-10-03-gartner-says-generative-ai-will-require-80-percent-of-engineering-workforce-to-upskill-through-2027" rel="noopener noreferrer"&gt;&lt;strong&gt;Gartner (Oct 2024)&lt;/strong&gt;&lt;/a&gt; predicts 80% of the engineering workforce will need to upskill through 2027 as generative AI shifts the role from &lt;em&gt;writing&lt;/em&gt; code to &lt;em&gt;steering AI agents toward the right context and constraints&lt;/em&gt;. Note the direction: not "engineers get replaced" — "engineers move up the stack."&lt;/li&gt;
&lt;li&gt;The &lt;a href="https://dora.dev/report/2025" rel="noopener noreferrer"&gt;&lt;strong&gt;2025 DORA report&lt;/strong&gt;&lt;/a&gt; — &lt;em&gt;State of AI-assisted Software Development&lt;/em&gt; — finds AI adoption is now nearly universal and finally improving delivery throughput, a shift from last year. But it &lt;em&gt;still increases delivery instability&lt;/em&gt;, and the report's own thesis is that the biggest returns come not from the tools but from the strength of the underlying system. Translation: the mandate only pays off if verification capacity grows as fast as generation volume. Most mandates fund neither.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The trap isn't that AI writes code. AI writing code is real, useful, and not going away. The trap is believing the percentage measures &lt;em&gt;progress&lt;/em&gt; when it measures &lt;em&gt;deferred work&lt;/em&gt;. A team can hit 75% and be slower than the quarter before — the dashboard will never tell you.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Role Shift Is Already Happening
&lt;/h2&gt;

&lt;p&gt;Now the encouraging part, because there is one.&lt;/p&gt;

&lt;p&gt;The engineers thriving under these mandates — not surviving, thriving — have quietly rewritten their own job description:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Less hand-crafting syntax → more writing explicit context prompts and structural constraints&lt;/li&gt;
&lt;li&gt;Less line-by-line reading → more architectural validation and zero-trust auditing&lt;/li&gt;
&lt;li&gt;Less memorizing API signatures → more domain modeling, threat modeling, boundary setting&lt;/li&gt;
&lt;li&gt;Velocity measured less by features authored → more by system reliability and review throughput&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Their mental model for the LLM isn't "junior developer." It's &lt;strong&gt;untrusted runtime execution environment&lt;/strong&gt; — brilliant, fast, blind to your state machines, your guarantees, your edges.&lt;/p&gt;

&lt;p&gt;You don't trust it. You don't fight it. &lt;strong&gt;You constrain it, then verify it.&lt;/strong&gt; 🧱&lt;/p&gt;

&lt;p&gt;The title on the org chart still says "engineer." The actual job is editor-in-chief of a fast, tireless, untrusted intern — setting its constraints, auditing its output, deciding what ships. That's not a demotion from builder to gatekeeper. It's the difference between a factory worker and the person who owns the factory's quality.&lt;/p&gt;

&lt;p&gt;Three strategies do most of the work, and they compound.&lt;/p&gt;

&lt;h2&gt;
  
  
  Strategy 1: Guardrails Before Generation, Not in Review
&lt;/h2&gt;

&lt;p&gt;The cheapest bug to catch is the one that never gets written. So stop arguing about conventions in review and start encoding them in the repo — &lt;code&gt;.cursorrules&lt;/code&gt;, &lt;code&gt;AGENTS.md&lt;/code&gt;, explicit system prompts — so the constraints exist &lt;em&gt;before&lt;/em&gt; the first token is generated.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# .cursorrules / AGENTS.md — decided before generation, enforced by CI
- Reuse utils/ before writing any new helper function
- No new dependency without justification in the PR description
- Structured logger only (logger.*) — console.* is a lint error
- Services stay framework-agnostic — no direct ORM imports
- Immutable data classes for anything crossing a service boundary
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The model doesn't guess your conventions anymore. It inherits them.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Tip: a constraint written before generation costs minutes. The same decision argued in review costs hours — and review is where your senior bandwidth actually goes.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Strategy 2: Zero-Trust Review
&lt;/h2&gt;

&lt;p&gt;The review contract changes too. Refuse AI-assisted PRs that arrive without a human-authored architectural summary: what this changes, what it touches, what it deliberately doesn't.&lt;/p&gt;

&lt;p&gt;Require proof of boundary-condition testing. A security-impact note. Justification for every new dependency.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;If the author can't explain the code, it can't be reviewed — only admired.&lt;/strong&gt; 🔍&lt;/p&gt;

&lt;p&gt;That was true before AI. The 75% mandate just raised the stakes: triple the PR volume and "skim and approve" becomes the default failure mode, with every skim a lottery ticket against the codebase.&lt;/p&gt;

&lt;p&gt;And to be clear — zero-trust review isn't hostility toward juniors, or toward AI. It's the honest admission that volume broke review's social contract. Trust used to be backed by shared context. That collateral is gone, so trust gets earned per-PR now.&lt;/p&gt;

&lt;h2&gt;
  
  
  Strategy 3: Shift Verification Left Into the Pipeline
&lt;/h2&gt;

&lt;p&gt;The real cure for review fatigue isn't reading faster. It's making sure a human never sees what a machine could have caught.&lt;/p&gt;

&lt;p&gt;Static analysis. Property-based tests. Contract validation. All of it runs before a PR reaches a human. If generated code fails a contract, the pipeline rejects it back to the author with error logs — the author fixes, the machine re-verifies, and human review starts at "it already behaves correctly," not "it looks plausible."&lt;/p&gt;

&lt;p&gt;Start with what machines are already good at — formatting, type contracts, dependency policy, known-insecure imports — and let the pipeline own them completely. That first week of setup retires most of the noise from your queue. Humans keep the part machines are bad at: does this design belong in this system at all?&lt;/p&gt;

&lt;p&gt;This is the same move the industry made with unit tests fifteen years ago: verification leaving human memory, moving into automated infrastructure. The difference is what's left for the human — and what's left is the good part.&lt;/p&gt;

&lt;p&gt;Review becomes what it always should have been: two people discussing an architecture, not a person pretending to be a linter. 🎯&lt;/p&gt;

&lt;h2&gt;
  
  
  The Mandate Is a Promotion in Disguise
&lt;/h2&gt;

&lt;p&gt;A 75% target measures how much code a machine wrote. It says nothing about whether anyone verified it — and mandates like that don't fail loudly. They fail as architectural drift, review burnout, and a codebase nobody wants to inherit.&lt;/p&gt;

&lt;p&gt;But look at who's thriving, and how. Not the fastest typists. The ones who made verification cheap and trust earned: context before generation, zero-trust at review, automation before human eyes.&lt;/p&gt;

&lt;p&gt;If 75% of the code writes itself, then 100% of the engineering lives in what remains — the architecture, the contracts, the judgment.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The dashboards can count generated lines. They can't generate judgment.&lt;/strong&gt; That's still your job. And for the first time in a while, it's the &lt;em&gt;whole&lt;/em&gt; job — no longer sandwiched between tickets of boilerplate.&lt;/p&gt;

&lt;p&gt;Next in the series: &lt;strong&gt;From Pilot Purgatory to Agentic Chaos&lt;/strong&gt; — why mid-market companies keep breaking their own SDLCs with autonomous agents, and the four pillars that fix it.&lt;/p&gt;




&lt;p&gt;One question before you go: what does your team actually measure — code generated, or code verified? And what's the one guardrail you'd install before the next mandate lands? Share yours. The comments usually end up better than the article.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;a href="https://www.gartner.com/en/newsroom/press-releases/2024-10-03-gartner-says-generative-ai-will-require-80-percent-of-engineering-workforce-to-upskill-through-2027" rel="noopener noreferrer"&gt;Gartner — GenAI Will Require 80% of Engineering Workforce to Upskill Through 2027 (Oct 2024)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dora.dev/report/2025" rel="noopener noreferrer"&gt;DORA — State of AI-assisted Software Development 2025&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.gitclear.com/ai_assistant_code_quality_2025_research" rel="noopener noreferrer"&gt;GitClear — AI Copilot Code Quality Research 2025&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.usenix.org/publications/loginonline/we-have-package-you-comprehensive-analysis-package-hallucinations-code" rel="noopener noreferrer"&gt;USENIX Security — "We Have a Package for You!" Package Hallucinations in Code-Generating LLMs (2025)&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;

</description>
      <category>discuss</category>
      <category>ai</category>
      <category>career</category>
      <category>productivity</category>
    </item>
    <item>
      <title>I Signed a Regression and Almost Shipped It</title>
      <dc:creator>Debashish Ghosal</dc:creator>
      <pubDate>Tue, 13 Oct 2026 02:13:00 +0000</pubDate>
      <link>https://dev.to/debashish_ghosal/i-signed-a-regression-and-almost-shipped-it-aeh</link>
      <guid>https://dev.to/debashish_ghosal/i-signed-a-regression-and-almost-shipped-it-aeh</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;The regression report said &lt;code&gt;blocked=True&lt;/code&gt;. A critical task had regressed. The promotion proceeded anyway. The attestation was written. The workload was admitted to production.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I signed a regression and almost shipped it — on &lt;a href="https://github.com/deghosal-2026/hiveplane" rel="noopener noreferrer"&gt;HivePlane&lt;/a&gt; v0.2.0, the release where I added the promotion gate I'd been promising since v0.1.0. The gate that was supposed to make "certified" mean "safe" admitted a workload its own regression report had just flagged.&lt;/p&gt;

&lt;p&gt;This is the article about when "certified" does not mean "safe" — and about a gate that admits on the wrong evidence.&lt;/p&gt;

&lt;h2&gt;
  
  
  Four ways the gate admitted wrong
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Finding&lt;/th&gt;
&lt;th&gt;What the gate did&lt;/th&gt;
&lt;th&gt;Why it was wrong&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;#522 (Critical)&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;recertify_and_promote&lt;/code&gt; admitted even when the regression report was &lt;code&gt;blocked=True&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;The report was a footnote, not a precondition&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;#523 (High)&lt;/td&gt;
&lt;td&gt;Default &lt;code&gt;min_production_runs_survived=50&lt;/code&gt; made &lt;code&gt;certified&lt;/code&gt; unreachable&lt;/td&gt;
&lt;td&gt;The production gate deadlocked against its own threshold&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;#530&lt;/td&gt;
&lt;td&gt;The drift detector compared failure counts &lt;em&gt;across corpus versions&lt;/em&gt;
&lt;/td&gt;
&lt;td&gt;Growing the benchmark auto-quarantined a healthy agent&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;#527&lt;/td&gt;
&lt;td&gt;The regression report diffed against &lt;em&gt;any-status&lt;/em&gt; prior attestation&lt;/td&gt;
&lt;td&gt;A non-comparable baseline masked a real regression&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Pass-rate thresholds are necessary. They are not sufficient. Each of these is a way to be green and wrong.&lt;/p&gt;

&lt;h2&gt;
  
  
  The signature on a regression
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;522&lt;/strong&gt; is the one that kept me up. The flow was: re-certify, generate a regression report, promote. The report said &lt;code&gt;blocked=True&lt;/code&gt; — a critical task had regressed. The promotion proceeded anyway. The attestation was written. The workload was admitted.&lt;/p&gt;

&lt;p&gt;The regression report was generated, attached to the record, and ignored. It was a comment, not a gate.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;tip:&lt;/strong&gt; A check that annotates but does not block is a suggestion, not a control. If your promotion can succeed while the regression report is red, your regression report is decoration.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The fix: the regression report is a &lt;strong&gt;hard precondition&lt;/strong&gt;. &lt;code&gt;recertify_and_promote&lt;/code&gt; refuses if the report is &lt;code&gt;blocked&lt;/code&gt;:&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;if&lt;/span&gt; &lt;span class="n"&gt;regression_report&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;blocked&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;PromotionRefusedError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;workload&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;regression report is blocked: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;regression_report&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;critical_failures&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="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A critical regression feeds the refusal reason. The promotion cannot complete until the regression is resolved or explicitly waived with attribution.&lt;/p&gt;

&lt;h2&gt;
  
  
  The unreachable status
&lt;/h2&gt;

&lt;h1&gt;
  
  
  523
&lt;/h1&gt;

&lt;p&gt;It was a different kind of admission failure. The default &lt;code&gt;min_production_runs_survived=50&lt;/code&gt; meant a workload had to survive 50 production runs before it could be &lt;code&gt;certified&lt;/code&gt;. In a field test that runs once, against a local stack, that's never. The production gate deadlocked against its own threshold — the workload could pass every benchmark forever and never reach &lt;code&gt;certified&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;A gate that's unreachable is a gate that doesn't exist. Operators will route around it, and a bypassed gate is an expensive dashboard. The threshold has to be reachable in the environment where it's enforced, or you've built a status cycle, not a ladder.&lt;/p&gt;

&lt;h2&gt;
  
  
  The baseline that wasn't
&lt;/h2&gt;

&lt;h1&gt;
  
  
  530
&lt;/h1&gt;

&lt;p&gt;It is the one that changed how I think about drift. The detector compared failure &lt;em&gt;counts&lt;/em&gt; across corpus versions. So if you grew the benchmark from 10 tasks to 20 — because the learning loop added a reviewed case — a healthy agent that passed 10/10 now "failed" 10 of 20 by your accounting. Corpus growth looked like decay. &lt;strong&gt;Growing the benchmark auto-quarantined a healthy agent.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The fix: drift is measured against the agent's &lt;em&gt;own baseline with comparable corpora&lt;/em&gt;. You compare the same tasks, not different counts. A corpus version change is a new baseline, not a drift signal.&lt;/p&gt;

&lt;h1&gt;
  
  
  527
&lt;/h1&gt;

&lt;p&gt;It was the same lesson on the promotion side: the regression report diffed against &lt;em&gt;any-status&lt;/em&gt; prior attestation, including a &lt;code&gt;quarantined&lt;/code&gt; one. So a regression against a known-bad baseline looked like no regression. The baseline has to be the last &lt;code&gt;certified&lt;/code&gt; record, or you're comparing against a version that was never safe.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I learned
&lt;/h2&gt;

&lt;p&gt;A signature on a known-bad artifact is worse than no signature — it carries the authority of certification without the safety. The regression report must block, not annotate. Identity binds to the attestation, not the manifest: change the hash, lose the certification. And the survival gate must be reachable, or you've built a status cycle, not a ladder.&lt;/p&gt;

&lt;p&gt;The v0.2.0 field test demonstrates the promotion gate blocking a regression with a replayable diff (scenario S3), drift auto-quarantine and reinstatement (S4/S5), and the regression diff API. Full evidence in the &lt;a href="https://github.com/deghosal-2026/hiveplane/blob/main/docs/field-test/v0.2.0/FIELD_TEST_REPORT.md" rel="noopener noreferrer"&gt;field-test report&lt;/a&gt;; the certification and promotion design is in the &lt;a href="https://github.com/deghosal-2026/hiveplane/blob/main/docs/design/certification-v2-design.md" rel="noopener noreferrer"&gt;certification v2 design doc&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://github.com/deghosal-2026/hiveplane" rel="noopener noreferrer"&gt;HivePlane&lt;/a&gt; — &lt;code&gt;pip install hiveplane==0.2.0&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/deghosal-2026/hiveplane/blob/main/docs/field-test/v0.2.0/FIELD_TEST_REPORT.md" rel="noopener noreferrer"&gt;Field test report (36/36)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/deghosal-2026/hiveplane/blob/main/docs/design/certification-v2-design.md" rel="noopener noreferrer"&gt;Certification v2 design&lt;/a&gt; · &lt;a href="https://github.com/deghosal-2026/hiveplane/blob/main/docs/design/learning-loop-design.md" rel="noopener noreferrer"&gt;Learning loop design&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/deghosal-2026/hiveplane/blob/main/docs/tutorials/02-certify-promote-drift.md" rel="noopener noreferrer"&gt;Tutorial — Certify, Promote &amp;amp; Survive Drift&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;When was the last time your promotion gate refused something the regression report had flagged? If you can't remember, your report is a comment — not a gate.&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>testing</category>
      <category>release</category>
      <category>llm</category>
    </item>
    <item>
      <title>One `rm -f` Silently Disabled My Crash Detection</title>
      <dc:creator>Debashish Ghosal</dc:creator>
      <pubDate>Mon, 12 Oct 2026 05:46:00 +0000</pubDate>
      <link>https://dev.to/debashish_ghosal/one-rm-f-silently-disabled-my-crash-detection-4khl</link>
      <guid>https://dev.to/debashish_ghosal/one-rm-f-silently-disabled-my-crash-detection-4khl</guid>
      <description>&lt;p&gt;The daemon crashed, came back up, and recorded nothing. No gap, no warning, no log. The code was correct; the test cleanup had deleted the file the recovery logic keys on.&lt;/p&gt;

&lt;p&gt;This is from &lt;a href="https://github.com/agentic-suite/agentsec-agentwatch" rel="noopener noreferrer"&gt;agentwatch&lt;/a&gt;, part of &lt;a href="https://github.com/agentic-suite" rel="noopener noreferrer"&gt;agentic-suite&lt;/a&gt; — a suite of open-source, harness-agnostic security tools for AI agents. The pattern generalizes to any logic that keys off a file's existence: migration markers, health files, lock files. Cleanup that doesn't understand what it's deleting can silently turn the feature off.&lt;/p&gt;

&lt;h2&gt;
  
  
  The design
&lt;/h2&gt;

&lt;p&gt;On restart after a crash, the daemon synthesizes a &lt;code&gt;recording-gap&lt;/code&gt; record — evidence that recording was interrupted, and for roughly how long. This is F1 in the fault taxonomy: a first-class fault the system must detect and record. The signal that separates "first ever start" from "back from a crash" is a file, &lt;code&gt;daemon.pid&lt;/code&gt;:&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;_record_gap_if_needed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&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;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pid_path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exists&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt;  &lt;span class="c1"&gt;# first start, nothing to fill in
&lt;/span&gt;    &lt;span class="n"&gt;records&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;store&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;records&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;records&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;last&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;records&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;gap_seconds&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&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;now&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;timezone&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;utc&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;last&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;started_at&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="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;gap_seconds&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;gap_threshold_seconds&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt;
    &lt;span class="c1"&gt;# ... write the recording-gap record ...
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The pid file isn't written by the daemon. The launcher writes it when it starts the daemon, and when the daemon dies the file stays behind. On the next start, the daemon sees it and knows it was running before. The file is the signal.&lt;/p&gt;

&lt;p&gt;This separation — launcher writes the pid file, daemon reads it — matters for testing. The harness starts the daemon directly, not through the launcher, so the pid file is never written. Without replicating the launcher's behavior, the recovery path doesn't execute. The test passes because the daemon starts, but the gap is never synthesized. The test would need to assert the gap exists to notice — and that's the assertion that caught the bug.&lt;/p&gt;

&lt;h2&gt;
  
  
  What went wrong
&lt;/h2&gt;

&lt;p&gt;The test kills the daemon and restarts it, then asserts the gap record exists. The kill step also tidied up:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;kill&lt;/span&gt; &lt;span class="nt"&gt;-9&lt;/span&gt; &lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; /data/agentwatch/daemon.pid&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="c"&gt;# tidy up "stale state"&lt;/span&gt;
&lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; /data/agentwatch/daemon.pid      &lt;span class="c"&gt;# ← this line&lt;/span&gt;
&lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; /run/agentwatch/agentwatch.sock
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It treated the pid file as leftover junk. But that leftover junk &lt;em&gt;is&lt;/em&gt; the evidence that there's a gap to report. On restart, &lt;code&gt;pid_path.exists()&lt;/code&gt; is false, the function returns on its first line, and nothing happens. No gap, no log, nothing. The test asserted the gap existed, it didn't, and that was the only thing that told me.&lt;/p&gt;

&lt;h2&gt;
  
  
  The false lead
&lt;/h2&gt;

&lt;p&gt;My first hypothesis was that gap detection was broken. But the function's first line is the exists check, and returning early on a first start is correct — you don't want a phantom gap on a clean install. The daemon was doing precisely what it should with the input it got. The input was wrong, because the test deleted it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix
&lt;/h2&gt;

&lt;p&gt;Keep the pid file. Clear only the socket:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;kill&lt;/span&gt; &lt;span class="nt"&gt;-9&lt;/span&gt; &lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; /data/agentwatch/daemon.pid&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; /run/agentwatch/agentwatch.sock
&lt;span class="c"&gt;# leave daemon.pid alone — it's the signal, not stale state&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There was a wrinkle. Since the launcher writes the pid file and not the daemon, the harness had to do the launcher's job when starting the daemon directly:&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="o"&gt;(&lt;/span&gt;agentsec-agentwatch-daemon &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;/tmp/daemon.log 2&amp;gt;&amp;amp;1 &amp;amp;&lt;span class="o"&gt;)&lt;/span&gt;
pgrep &lt;span class="nt"&gt;-f&lt;/span&gt; &lt;span class="s2"&gt;"[a]gentsec-agentwatch-daemon"&lt;/span&gt; | &lt;span class="nb"&gt;head&lt;/span&gt; &lt;span class="nt"&gt;-1&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /data/agentwatch/daemon.pid
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The bracket around the &lt;code&gt;a&lt;/code&gt; stops &lt;code&gt;pgrep&lt;/code&gt; from matching itself — a small detail, but without it the pid file contains the pid of the &lt;code&gt;pgrep&lt;/code&gt; process, which exits before the daemon can read it. Now the pid file is there before the daemon does anything, exactly like production, and after a crash-restart the gap record appears with a duration that matches the down time.&lt;/p&gt;

&lt;p&gt;After the fix: &lt;code&gt;recording-gap&lt;/code&gt; anomaly present, &lt;code&gt;duration_ms&lt;/code&gt; matching the crash-to-restart interval. FT-14 passes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the bug was invisible
&lt;/h2&gt;

&lt;p&gt;The failure had no error signal. The function returned normally — no exception, no error code, no log line. The gap anomaly was absent, but absence is not an error; it's just "nothing happened." The daemon didn't know it should have synthesized a gap. It didn't know the pid file had been deleted. It just saw "no pid file → first start → no gap." The logic was correct for the input it received. The input was wrong.&lt;/p&gt;

&lt;p&gt;This is what makes silent skips the worst failure mode. Three properties combine:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;No error signal.&lt;/strong&gt; The function returns normally — no exception, no error code, no log. The daemon's &lt;code&gt;_record_gap_if_needed&lt;/code&gt; returned &lt;code&gt;None&lt;/code&gt; just as it would on a first start: the same return value, the same code path, the same "nothing to do" outcome.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;No observable effect.&lt;/strong&gt; The gap anomaly is absent, but absence reads as "nothing happened," not "something failed." A missing row in the store doesn't look like an error — it looks like the daemon didn't have a gap to report. The only way to tell "should have reported but didn't" from "had nothing to report" is to know the pid file was deleted, and the daemon doesn't know that.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;No feedback loop.&lt;/strong&gt; The daemon has no way to know the pid file was deleted. It just sees "no pid file" and does the right thing for that input. The logic is correct for the input it received. The input was wrong. There's no signal back saying "by the way, I didn't find a pid file, so I assumed this was a first start."&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The only way to catch a silent skip is to assert the expected behavior explicitly. FT-14 does this: after crash-restart, it asserts the &lt;code&gt;recording-gap&lt;/code&gt; anomaly exists. Without that assertion, the silent skip would have gone to production, and real crash gaps would have been invisible.&lt;/p&gt;

&lt;h2&gt;
  
  
  The bit that generalizes
&lt;/h2&gt;

&lt;p&gt;It's not really about pid files. Any file that acts as a signal can be broken by cleanup that doesn't understand what it's holding:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Signal&lt;/th&gt;
&lt;th&gt;What it means&lt;/th&gt;
&lt;th&gt;What cleanup does&lt;/th&gt;
&lt;th&gt;What happens&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;daemon.pid&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;"daemon was running before"&lt;/td&gt;
&lt;td&gt;deleted as "stale"&lt;/td&gt;
&lt;td&gt;gap detection silently disabled&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;.migration_complete&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;"migration ran"&lt;/td&gt;
&lt;td&gt;deleted as "old marker"&lt;/td&gt;
&lt;td&gt;migration re-runs, data corrupted&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;health_check.json&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;"last health status"&lt;/td&gt;
&lt;td&gt;overwritten with empty&lt;/td&gt;
&lt;td&gt;health history lost&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;.env&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;"config was set"&lt;/td&gt;
&lt;td&gt;deleted as "temp"&lt;/td&gt;
&lt;td&gt;config reset to defaults&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;In every case, cleanup sees "a file" and deletes it. The reader silently skips — no error, no warning. The system appears healthy. The signal is gone.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;tip:&lt;/strong&gt; Not every file in a directory is stale. Some are signals, markers, or evidence. If your recovery logic checks whether a file exists, your cleanup isn't allowed to delete it. The cleanup must distinguish signals from stale state, or it will silently turn features off.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Two different assertions
&lt;/h2&gt;

&lt;p&gt;"The daemon is running" and "the gap was written" are different assertions. The first is necessary but not sufficient — a daemon that starts but doesn't synthesize the gap has still lost the record of the interruption. The test has to assert recovery, not just startup, or it's testing the easy half and ignoring the half that matters.&lt;/p&gt;

&lt;p&gt;This shows up in a lot of recovery code. "Did the process restart" is the question most tests ask. "Did the process restore the state it was supposed to restore" is the question that actually matters, and it's the one that requires the recovery path to execute — which means the preconditions for recovery (the pid file, the marker, the lock) have to be present when the test runs.&lt;/p&gt;

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

&lt;p&gt;If recovery keys off a file, that file has to survive the test, or the test isn't testing recovery. Reproducing the launcher's pid-file behavior in the harness is the difference between exercising the recovery path and skipping it. And the assertion has to be on the recovery behavior, not the startup — "the gap was written," not "the daemon is running."&lt;/p&gt;

&lt;p&gt;It's fixed in agentwatch v0.1.0, with FT-14 as the test that keeps the pid file — and the recovery path — honest.&lt;/p&gt;

&lt;p&gt;The question worth asking of any crash-recovery or migration code: what file or state does it key on, and does the test cleanup know the difference between stale state and evidence? If the cleanup deletes the signal, the recovery path never runs, and the test passes anyway.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Release: &lt;a href="https://github.com/agentic-suite/agentsec-agentwatch/releases/tag/v0.1.0" rel="noopener noreferrer"&gt;agentwatch v0.1.0 on GitHub&lt;/a&gt; · &lt;a href="https://pypi.org/project/agentsec-agentwatch/0.1.0/" rel="noopener noreferrer"&gt;on PyPI&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Suite: &lt;a href="https://github.com/agentic-suite" rel="noopener noreferrer"&gt;agentic-suite&lt;/a&gt; — the full set of agent-security tools&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/agentic-suite/agentsec-agentwatch/blob/main/docs/field-test/v0.1.0/FIELD_TEST_REPORT.md" rel="noopener noreferrer"&gt;Field test report&lt;/a&gt; (FT-14, issue I-6)&lt;/li&gt;
&lt;li&gt;Daemon: &lt;a href="https://github.com/agentic-suite/agentsec-agentwatch/blob/main/packages/python-sdk/src/agentsec_agentwatch/daemon.py" rel="noopener noreferrer"&gt;&lt;code&gt;daemon.py&lt;/code&gt;&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Test step: &lt;a href="https://github.com/agentic-suite/agentsec-agentwatch/blob/main/scripts/fieldtest/cases/steps/FT-14.sh" rel="noopener noreferrer"&gt;&lt;code&gt;FT-14.sh&lt;/code&gt;&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>programming</category>
      <category>testing</category>
      <category>healthydebate</category>
    </item>
    <item>
      <title>Do you have 5-10 years experience in IT, and suddenly seeing the workplace career growth rules have changed? Read on, you are not alone.</title>
      <dc:creator>Debashish Ghosal</dc:creator>
      <pubDate>Sun, 11 Oct 2026 18:18:49 +0000</pubDate>
      <link>https://dev.to/debashish_ghosal/do-you-have-5-10-years-experience-in-it-and-suddenly-seeing-the-workplace-career-growth-rules-have-584p</link>
      <guid>https://dev.to/debashish_ghosal/do-you-have-5-10-years-experience-in-it-and-suddenly-seeing-the-workplace-career-growth-rules-have-584p</guid>
      <description>&lt;div class="ltag__link--embedded"&gt;
  &lt;div class="crayons-story "&gt;
  &lt;a href="https://dev.to/debashish_ghosal/the-last-generation-that-learned-the-hard-way-3588" class="crayons-story__hidden-navigation-link"&gt;The Last Generation That Learned the Hard Way&lt;/a&gt;


  &lt;div class="crayons-story__body crayons-story__body-full_post"&gt;
    &lt;div class="crayons-story__top"&gt;
      &lt;div class="crayons-story__meta"&gt;
        &lt;div class="crayons-story__author-pic"&gt;

          &lt;a href="/debashish_ghosal" class="crayons-avatar  crayons-avatar--l  "&gt;
            &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F2217330%2Fe7b1a584-ae94-490e-a80a-3b0df528f4aa.jpg" alt="debashish_ghosal profile" class="crayons-avatar__image"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
        &lt;div&gt;
          &lt;div&gt;
            &lt;a href="/debashish_ghosal" class="crayons-story__secondary fw-medium m:hidden"&gt;
              Debashish Ghosal
            &lt;/a&gt;
            &lt;div class="profile-preview-card relative mb-4 s:mb-0 fw-medium hidden m:inline-block"&gt;
              
                Debashish Ghosal
                
                
              
              &lt;div id="story-author-preview-content-4774916" class="profile-preview-card__content crayons-dropdown branded-7 p-4 pt-0"&gt;
                &lt;div class="gap-4 grid"&gt;
                  &lt;div class="-mt-4"&gt;
                    &lt;a href="/debashish_ghosal" class="flex"&gt;
                      &lt;span class="crayons-avatar crayons-avatar--xl mr-2 shrink-0"&gt;
                        &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F2217330%2Fe7b1a584-ae94-490e-a80a-3b0df528f4aa.jpg" class="crayons-avatar__image" alt=""&gt;
                      &lt;/span&gt;
                      &lt;span class="crayons-link crayons-subtitle-2 mt-5"&gt;Debashish Ghosal&lt;/span&gt;
                    &lt;/a&gt;
                  &lt;/div&gt;
                  &lt;div class="print-hidden"&gt;
                    
                      Follow
                    
                  &lt;/div&gt;
                  &lt;div class="author-preview-metadata-container"&gt;&lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            &lt;/div&gt;

          &lt;/div&gt;
          &lt;a href="https://dev.to/debashish_ghosal/the-last-generation-that-learned-the-hard-way-3588" class="crayons-story__tertiary fs-xs"&gt;&lt;time&gt;Oct 11&lt;/time&gt;&lt;span class="time-ago-indicator-initial-placeholder"&gt;&lt;/span&gt;&lt;/a&gt;
        &lt;/div&gt;
      &lt;/div&gt;

    &lt;/div&gt;

    &lt;div class="crayons-story__indention"&gt;
      &lt;h2 class="crayons-story__title crayons-story__title-full_post"&gt;
        &lt;a href="https://dev.to/debashish_ghosal/the-last-generation-that-learned-the-hard-way-3588" id="article-link-4774916"&gt;
          The Last Generation That Learned the Hard Way
        &lt;/a&gt;
      &lt;/h2&gt;
        &lt;div class="crayons-story__tags"&gt;
            &lt;a class="crayons-tag crayons-tag--filled  " href="/t/healthydebate"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;healthydebate&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/ai"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;ai&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/employeeexperience"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;employeeexperience&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/career"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;career&lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="crayons-story__bottom"&gt;
        &lt;div class="crayons-story__details"&gt;
          &lt;a href="https://dev.to/debashish_ghosal/the-last-generation-that-learned-the-hard-way-3588" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left"&gt;
            &lt;div class="multiple_reactions_aggregate"&gt;
              &lt;span class="multiple_reactions_icons_container"&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/exploding-head-daceb38d627e6ae9b730f36a1e390fca556a4289d5a41abb2c35068ad3e2c4b5.svg" width="18" height="18"&gt;
                  &lt;/span&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/multi-unicorn-b44d6f8c23cdd00964192bedc38af3e82463978aa611b4365bd33a0f1f4f3e97.svg" width="18" height="18"&gt;
                  &lt;/span&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/sparkle-heart-5f9bee3767e18deb1bb725290cb151c25234768a0e9a2bd39370c382d02920cf.svg" width="18" height="18"&gt;
                  &lt;/span&gt;
              &lt;/span&gt;
              &lt;span class="aggregate_reactions_counter"&gt;9&lt;span class="hidden s:inline"&gt;&amp;nbsp;reactions&lt;/span&gt;&lt;/span&gt;
            &lt;/div&gt;
          &lt;/a&gt;
            &lt;a href="https://dev.to/debashish_ghosal/the-last-generation-that-learned-the-hard-way-3588#comments" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left flex items-center"&gt;
              

              &lt;span class="hidden s:inline"&gt;Add&amp;nbsp;Comment&lt;/span&gt;
            &lt;/a&gt;
        &lt;/div&gt;
        &lt;div class="crayons-story__save"&gt;
          &lt;small class="crayons-story__tertiary fs-xs mr-2"&gt;
            6 min read
          &lt;/small&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;/div&gt;


</description>
      <category>career</category>
      <category>productivity</category>
    </item>
  </channel>
</rss>
