<?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: Jennifer Smith</title>
    <description>The latest articles on DEV Community by Jennifer Smith (@jenatechio).</description>
    <link>https://dev.to/jenatechio</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%2F4055605%2F179be11d-7c47-43b8-a2b9-c43ffcc15cef.png</url>
      <title>DEV Community: Jennifer Smith</title>
      <link>https://dev.to/jenatechio</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/jenatechio"/>
    <language>en</language>
    <item>
      <title>I saw the broken email in the preview, blamed my phone, and approved the send anyway.</title>
      <dc:creator>Jennifer Smith</dc:creator>
      <pubDate>Sat, 15 Aug 2026 19:42:44 +0000</pubDate>
      <link>https://dev.to/jenatechio/i-saw-the-broken-email-in-the-preview-blamed-my-phone-and-approved-the-send-anyway-25n3</link>
      <guid>https://dev.to/jenatechio/i-saw-the-broken-email-in-the-preview-blamed-my-phone-and-approved-the-send-anyway-25n3</guid>
      <description>&lt;p&gt;Three days after I sent an email to the subscriber list of one of my brands, a reader replied with a screenshot. The subject line was fine. The sender was fine. The body was a smear: every line of text stacked onto the same narrow band of pixels, sentence printed over sentence until the whole message was one unreadable stripe.&lt;/p&gt;

&lt;p&gt;That screenshot was the first evidence of what the send had actually done inside real inboxes. The template bug itself had been caught and fixed the same day the email went out — I will come back to that, because it makes this story worse rather than better — but the fix faced forward, toward future sends, and nobody looked back at the copies already delivered. So the first report from the receiving end was not a bounce report, a monitor, or a dashboard alert. It was a human being, doing unpaid quality assurance for a list they had voluntarily joined, three days after the fact.&lt;/p&gt;

&lt;p&gt;This is the postmortem, and the four guards that now exist so no reader ever holds that job again.&lt;/p&gt;

&lt;h2&gt;
  
  
  What actually broke
&lt;/h2&gt;

&lt;p&gt;The email platform I use has its own template format, and that format expresses line height as a percentage of font size. I had given it the value &lt;code&gt;1.65&lt;/code&gt; — a perfectly ordinary number if you have spent your life reading it as a CSS ratio, where it means "165% of the font size, comfortable reading rhythm."&lt;/p&gt;

&lt;p&gt;The template format read it as 1.65 percent.&lt;/p&gt;

&lt;p&gt;At 1.65 percent of the font size, every line of the email is drawn essentially on top of the line before it. The platform did nothing wrong: it stored the value I gave it and served it faithfully to every inbox on the list. One number, valid in one language and catastrophic in a neighboring dialect, and no part of my pipeline knew the difference.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why every gate waved it through
&lt;/h2&gt;

&lt;p&gt;The inventory is uncomfortable, and the pattern only becomes visible when you line the gates up.&lt;/p&gt;

&lt;p&gt;The words themselves were right. I had read them, revised them, and read them again, and they said exactly what I meant. But every check that email ever received was a check of words, and words are not what an inbox renders.&lt;/p&gt;

&lt;p&gt;I approved the send from my phone, between other things, and the preview in front of me looked wrong. The lines were crushed together, and I could see that they were. I decided the phone was the problem — small screens do strange things to email previews all the time — and I approved the send anyway. That is the worst line in this inventory, so I am not going to soften it: the system showed me the failure before it shipped, and I explained the evidence away.&lt;/p&gt;

&lt;p&gt;The platform accepted the template upload without complaint, because &lt;code&gt;1.65&lt;/code&gt; is a legal value for the field. Legal and correct are also different sentences.&lt;/p&gt;

&lt;p&gt;And nothing anywhere in the pipeline ever looked at the published artifact — the thing the platform actually stored after the upload and would actually serve — as opposed to the thing I intended to upload. Twelve people received the smear. When the bug surfaced later that same day, the template got fixed and the pipeline moved on, but the twelve copies already sitting in inboxes belonged to nobody's checklist. The email had announced a time-limited offer, its original window expired while the announcement was illegible, and the offer ended up being extended. For three days, the only sensor pointed at the subscriber side of the system was the goodwill of readers.&lt;/p&gt;

&lt;h2&gt;
  
  
  The pattern underneath
&lt;/h2&gt;

&lt;p&gt;This estate has published a postmortem before. In that one, an automated pipeline shipped a two-year-old news story as if it were new — and the gate that should have caught it was not missing; it existed and stood at the wrong door. This failure is the same species, and I think its most general form, because once again nothing was absent. Every gate I had was present, functioning, and pointed at the wrong object.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Every gate audited intent. None audited the artifact.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;By "artifact" I mean the thing that ships: what the third-party platform stored and will serve to a real inbox — not the local file, not the draft in the editor, and not the words I approved. Config files, deploy logs, reviews, and approvals are all claims about a system. The artifact is the fact.&lt;/p&gt;

&lt;p&gt;Once you see the pattern, you also see how common it is. Anyone who publishes through a platform they don't control — an email service, a CMS, an app store, a template engine — has a gap between the thing they approved and the thing that ships, and most pipelines audit only the near side of it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The four guards
&lt;/h2&gt;

&lt;p&gt;I pointed my agent at the problem the same day, and we built four layers. I contributed the part that twenty-five years of infrastructure actually trains: knowing the names of the guards an organization would deploy, and insisting on all of them instead of the first one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Round-trip every publish.&lt;/strong&gt; The sync script that pushes a template to the platform now immediately fetches back what the platform stored and validates that, not the local copy. A bad value can no longer survive a publish silently, because the publish itself reads its own result.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Gate the send on the artifact, and treat "cannot audit" as "do not send."&lt;/strong&gt; The bulk-send tool now fetches the live template and lints it before the first recipient. It refuses to send if the check fails — and it equally refuses if the check cannot run, because the template is unpublished, the API errors, or the format is one the linter cannot parse. Unverifiable is not safe. That sentence took me three days and a screenshot to learn, and it is the one I most want you to take.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Make approval mean "I saw what recipients see."&lt;/strong&gt; The send command now delivers one real copy to my own inbox and stops. The bulk send fires only when I run it again with a flag whose only meaning is "I looked at the proof." This is the guard that covers everything the other three cannot, because it catches the failure modes no linter anticipates yet — and it is aimed squarely at the excuse I used, because a real delivery rendered in a real mail client is the artifact itself, not a preview you can second-guess. If the proof in my inbox looks broken, there is no "it's probably just the phone" left to reach for.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Sweep daily.&lt;/strong&gt; A watchdog now lints every live template every morning and raises a flag only on failure. Templates can drift between sends — a stray edit in the platform's UI that no script ever saw — and the sweep exists so drift gets caught by a machine before breakfast instead of by a subscriber after dinner.&lt;/p&gt;

&lt;p&gt;There is also a fifth item, which is not a guard but a discipline: &lt;strong&gt;name your blind spots out loud.&lt;/strong&gt; Part of this platform's content lives in automation sequences the API cannot read, so no script can lint it. The guard prints that limitation in every single run instead of silently skipping it, because an audit that quietly narrows its own scope reports coverage it does not have. After any manual edit to those sequences, the check is my own eyes, and the tooling reminds me of exactly that.&lt;/p&gt;

&lt;p&gt;The watchdog's first sweep ran the day the guards landed and came back clean across every live template. The house rule holds here as everywhere: it is measured, not promised.&lt;/p&gt;

&lt;h2&gt;
  
  
  Steal this
&lt;/h2&gt;

&lt;p&gt;The condensed version, for any pipeline that publishes through a platform you don't control:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;After every push to a third-party platform, read back what it stored and validate that.&lt;/li&gt;
&lt;li&gt;Bulk operations refuse to run when the pre-flight audit fails, and equally when it cannot run at all.&lt;/li&gt;
&lt;li&gt;Approval requires seeing the rendered artifact: proof-to-yourself before send-to-everyone.&lt;/li&gt;
&lt;li&gt;A daily sweep of the live artifacts, alarming on failure only.&lt;/li&gt;
&lt;li&gt;Any surface the audit cannot reach gets named in the audit's own output, every run.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;The embarrassing part of this story is not the attribute. Numbers that mean different things in adjacent formats are a tale as old as software, and the platform behaved exactly as documented.&lt;/p&gt;

&lt;p&gt;The embarrassing part is that I saw it. The preview on my phone showed me the damage before the send, and I dismissed it as a rendering quirk of the device, because the alternative was inconvenient. Twenty-five years of infrastructure taught me that an anomaly is a claim that deserves investigation before dismissal, and I have said versions of that sentence to entire organizations. At my own kitchen table, with my own send, I treated the anomaly as noise. Every professional instinct I have would have flagged that override in someone else's operation, and I performed it in my own without a second thought.&lt;/p&gt;

&lt;p&gt;The subscriber who replied did a job my monitoring should have been doing. They did it for free, three days after the send, because on the receiving side of my system a reader with a screenshot was the only sensor actually deployed. The bitterest detail is that the bug was already fixed by then — fixed for the future, on the very day it shipped, while the past sat unread in twelve inboxes. Repairing a system and repairing its consequences turn out to be different jobs, and I had only staffed the first one. The four guards exist so that the next render failure — and there will be one; there is always one — is caught by a machine at seven in the morning instead of a reader at dinner.&lt;/p&gt;

&lt;p&gt;This estate is supposed to run while I sleep. The readers should get to sleep too.&lt;/p&gt;

</description>
      <category>automation</category>
      <category>testing</category>
      <category>email</category>
      <category>ai</category>
    </item>
    <item>
      <title>I set out to climb "graph engineering." It was the ladder I'd been standing on for 25 years.</title>
      <dc:creator>Jennifer Smith</dc:creator>
      <pubDate>Mon, 10 Aug 2026 17:22:03 +0000</pubDate>
      <link>https://dev.to/jenatechio/i-set-out-to-climb-graph-engineering-it-was-the-ladder-id-been-standing-on-for-25-years-1jla</link>
      <guid>https://dev.to/jenatechio/i-set-out-to-climb-graph-engineering-it-was-the-ladder-id-been-standing-on-for-25-years-1jla</guid>
      <description>&lt;p&gt;Part 1 of this series left one rung unclimbed. Rung 12, graph-based orchestration, was the frontier where I assumed the real engineers lived. Before I climb anything, I do my homework, so I set out to answer a prior question first: what does "graph" actually buy you? The term is everywhere right now (graph engineering, GraphRAG, knowledge graphs, graph databases), and it arrives wrapped in the specific tone vendors use when they want you to feel behind.&lt;/p&gt;

&lt;p&gt;Somewhere in the second hour of reading, I stopped taking notes on what was new and started keeping a different list: what wasn't.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the term means, with the noise filtered out
&lt;/h2&gt;

&lt;p&gt;Strip the vendor language and graph engineering is this: you structure knowledge as &lt;strong&gt;entities and typed relationships&lt;/strong&gt; instead of flat documents, so that a system (an AI, a search engine, a query) can &lt;em&gt;traverse the connections&lt;/em&gt; rather than just keyword-match the text. A customer isn't a row; it's a node with edges to orders, tickets, and other customers. A function isn't a string in a file; it's a node with edges to everything that calls it. Once knowledge has that shape, you can ask questions that flat storage can't answer well: not "which documents mention X" but "what is two hops downstream of X, and what breaks if it goes away?"&lt;/p&gt;

&lt;p&gt;That's the whole idea. The three flavors you'll see hyped are GraphRAG (knowledge graphs feeding LLM retrieval), code graphs (call chains and dependency structure for AI coding agents), and entity graphs for search (schema.org structured data feeding Google's Knowledge Graph).&lt;/p&gt;

&lt;p&gt;Read that definition again, slowly, and if you've spent time in enterprise infrastructure you may feel the same recognition I did.&lt;/p&gt;

&lt;h2&gt;
  
  
  The list of what wasn't new
&lt;/h2&gt;

&lt;p&gt;I spent twenty-five years in enterprise infrastructure (virtualization, storage, cloud, data centers) and the last stretch of it running an operations organization from the executive seat. Here is what that work actually consists of, underneath the job titles.&lt;/p&gt;

&lt;p&gt;It consists of dependency maps: which application sits on which server, which server sits in which cluster, which cluster depends on which storage array and which network path. Every serious infrastructure organization keeps some version of this — a CMDB, a topology diagram, an impact matrix — because the alternative is finding out the dependencies exist during the outage.&lt;/p&gt;

&lt;p&gt;It consists of impact analysis, which is the question "if this component fails, what stops working three layers up?" I want you to notice the shape of that question. It is a reachability query. It is graph traversal. When an architect walks the room through the blast radius of a change, she is doing at a whiteboard exactly what a graph database does with an index.&lt;/p&gt;

&lt;p&gt;It consists of entity-relationship modeling: the literal ERD, drawn for decades before anyone attached "engineering" to "graph." It consists of failure domains, which are subgraphs. It consists of the org chart you redraw during an incident to figure out who owns the broken edge.&lt;/p&gt;

&lt;p&gt;We did not call the dependency map a knowledge graph — we called it Tuesday.&lt;/p&gt;

&lt;p&gt;I want to be precise about the claim I'm making, because there's an overreach version of this article and I don't intend to write it. I am not claiming that maintaining topology diagrams makes anyone a graph engineer, and the people who build Neo4j clusters and tune traversal performance for a living are doing real, specialized work I have not done. The claim is narrower and, I think, more useful: &lt;strong&gt;graph engineering is a new name for entity-relationship thinking, and entity-relationship thinking is the load-bearing discipline of enterprise architecture.&lt;/strong&gt; The database is new to me. The way of seeing is not — it's the thing the career was made of.&lt;/p&gt;

&lt;h2&gt;
  
  
  What actually is new
&lt;/h2&gt;

&lt;p&gt;Because the honest version of this article has to include it: some of it is genuinely new, and pretending otherwise would be its own kind of hype.&lt;/p&gt;

&lt;p&gt;What's new is who builds the graph. Constructing these things used to be the expensive part — armies of analysts hand-curating entities and relationships, which is why so many CMDB projects died of staleness. Large language models changed that: they can now extract entities and relationships from unstructured text at scale, which means the graph can be built and maintained by machines. That one is real, not marketing.&lt;/p&gt;

&lt;p&gt;What's new is who traverses it. GraphRAG (letting an LLM walk a knowledge graph at question-answering time instead of fuzzy-matching document chunks) measurably improves the multi-hop questions that plain retrieval fumbles, the "how is A connected to C through B" class of question. On enterprise corpora of millions of documents, or codebases of hundreds of thousands of lines spread across dozens of services, this earns its infrastructure.&lt;/p&gt;

&lt;p&gt;That last clause is the pivot of this whole piece, so let me say it plainly: &lt;strong&gt;the technique is real, and it is scale-gated.&lt;/strong&gt; Every impressive graph result I found lives where the corpus is enormous, the hops are many, and the alternative is armies of humans. Which raises the question I actually needed answered — what happens at my scale?&lt;/p&gt;

&lt;h2&gt;
  
  
  The measurement, because I report measurements
&lt;/h2&gt;

&lt;p&gt;I have a code knowledge-graph tool wired into my agent sessions — full graph queries, call-chain tracing, the works. It arrived with the same energy as the rest of the hype cycle: always use the graph first, says its own protocol.&lt;/p&gt;

&lt;p&gt;This week I checked what it had actually indexed, and the answer was a single project. And of everything on this machine, the one it holds is my command center, the folder that is almost entirely markdown. The tool built to trace call chains through half a million lines of enterprise code sat down at my estate and indexed a pile of prose.&lt;/p&gt;

&lt;p&gt;That's not a failure; it's a finding. My repositories are small enough that an agent reading files and searching text handles them without ceremony; the graph never gets the chance to earn its overhead. The scale gate swings both ways, and at solo scale it swings shut.&lt;/p&gt;

&lt;p&gt;But here's the part that made me laugh, and then made me write this article. The graph that &lt;em&gt;does&lt;/em&gt; run my operation — the one doing real work every single day — isn't in a database at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  The knowledge graph in markdown
&lt;/h2&gt;

&lt;p&gt;Readers of Part 1 know my estate runs on durable memory: context files in every repository, facts in markdown files, an index the agent loads at the start of every session, session records with closed doors no future session may reopen. What I didn't say in Part 1, because I didn't have the vocabulary yet, is what that system &lt;em&gt;is&lt;/em&gt; structurally.&lt;/p&gt;

&lt;p&gt;The memory files are nodes. The links between them are edges: most files carry references to their related files by name. The index is the entry point, and the traversal engine is whichever model shows up that morning: it loads the index, follows the links relevant to the day's work, and ignores the rest. When a fact changes, one node gets edited and everything pointing at it stays consistent, which is precisely the maintenance property people buy graph databases to get.&lt;/p&gt;

&lt;p&gt;That's entities, typed relationships, and traversal: a knowledge graph. It's written in the world's most boring format, it costs nothing, it survived a model upgrade without blinking (models are cattle, the files are the pet), and I built it by instinct, because a career of watching knowledge die in flat documents teaches you to link things or lose them.&lt;/p&gt;

&lt;p&gt;At enterprise scale you need the database. At my scale, the discipline &lt;em&gt;is&lt;/em&gt; the product, and the discipline is free.&lt;/p&gt;

&lt;h2&gt;
  
  
  The decision rule
&lt;/h2&gt;

&lt;p&gt;I used to sit on the buying side of the table when vendors pitched, so here is the rule I'd give any small operator who is currently being made to feel behind about graphs. It has three lines.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;If your revenue depends on search, you should already be doing the one graph flavor that pays at small scale: structured data.&lt;/strong&gt; Schema.org markup on your site is you feeding the world's largest knowledge graph, Google's, and telling it exactly what your business, products, and pages are. It's unglamorous, it's a solved practice, and it moves the only graph that sends small businesses money.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;If you have millions of documents, dozens of services, or questions that hop across many systems, the new tooling is real — evaluate it.&lt;/strong&gt; This is where GraphRAG and code graphs live honestly. If a vendor can't show you the multi-hop question your current stack fails at, they're selling you the enormity of the technology instead of a problem you have.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Everything else is an infrastructure hobby.&lt;/strong&gt; A graph database serving a corpus that fits in a filing cabinet is a motorcycle for hauling a wallet. Link your files, keep an index, maintain the discipline of entities and relationships in whatever boring format survives — and spend the difference on something that ships.&lt;/p&gt;

&lt;p&gt;Buy the discipline before the database. If you already have the discipline, check whether you need the database at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  The third verdict
&lt;/h2&gt;

&lt;p&gt;Part 1's scoreboard had a bullet I called the graduation evals: I formally evaluated the no-code builder everyone recommends for people like me and found I was already doing everything it offered; I read up on "loop engineering" and found I'd been doing it before I knew the term. Both produced the same strange, quiet pride — progress measured by a side-by-side verdict, not a feeling.&lt;/p&gt;

&lt;p&gt;This is the third one, and it's the biggest, because this time the verdict covered more than the past seven months. It reached back through the whole career. The industry drew a frontier, I packed for the expedition, and the frontier turned out to be a territory I'd been mapping since before the term existed.&lt;/p&gt;

&lt;p&gt;Rung 12 still stands, for the record. Graph-based &lt;em&gt;orchestration&lt;/em&gt;, agents coordinated through graph structures, is its own climb, I haven't made it yet, and it will get its installment when there are results to report rather than intentions. The house rule holds: measured, not promised.&lt;/p&gt;

&lt;p&gt;But when I climb it, the posture will be different. The rung I was afraid of sits on a ladder I've been standing on for twenty-five years.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>architecture</category>
      <category>career</category>
      <category>devjourney</category>
    </item>
    <item>
      <title>I was released from the role of a lifetime. So I built an AI estate that runs while I sleep.</title>
      <dc:creator>Jennifer Smith</dc:creator>
      <pubDate>Wed, 05 Aug 2026 17:25:34 +0000</pubDate>
      <link>https://dev.to/jenatechio/i-was-released-from-the-role-of-a-lifetime-so-i-built-an-ai-estate-that-runs-while-i-sleep-bp</link>
      <guid>https://dev.to/jenatechio/i-was-released-from-the-role-of-a-lifetime-so-i-built-an-ai-estate-that-runs-while-i-sleep-bp</guid>
      <description>&lt;p&gt;I went from all-hours calls and escalations to deafening silence.&lt;/p&gt;

&lt;p&gt;In January, I was unexpectedly released from the role of a lifetime — VP of Operations, a seat on the executive leadership team. Before that role I'd worked from home for more than four years; I was honestly terrified of going back to an office, and then surprised myself by getting in earlier and earlier until I was often the first one in the building. I loved that job. When it ended, the house got very quiet.&lt;/p&gt;

&lt;p&gt;I gave myself a month or two to sit with three options: return to the same track and accept the risk of a repeat; go back to school — an MBA or formal technical training; or do what I've done at every turn of a 25-year infrastructure career — pick the unknown technology everyone is circling and deep-dive it until it isn't unknown anymore. I'll be honest about door one: I flirted with the conventional path and even wrote the intro emails. I couldn't bring myself to pull the trigger. The industry is changing, I've changed, and I didn't want to pursue more of the same. So I took the third door and taught myself by trial and error (mostly error, at first). I only learn in battle anyway — I build first and read the instructions later. All the furniture in my house confirms this.&lt;/p&gt;

&lt;p&gt;Fair warning about the emotional register of what follows: I vacillated between days of crowning myself a genius and days of contemplating launching my off-brand laptop through a wall. Both were correct.&lt;/p&gt;

&lt;p&gt;One more confession, because it frames everything: &lt;strong&gt;I was never a developer.&lt;/strong&gt; I spent twenty-five years in enterprise infrastructure — virtualization, storage, cloud, data centers — and five and a half years managing technical teams: developers, QA, and project managers, and finally a full operations organization from the executive seat. But I never lived in the code myself. What management gave me was the &lt;em&gt;names&lt;/em&gt; of all the work: smoke tests, PR gates, runbooks, failure domains. It turns out that with an AI that can build anything you can name, the names are the skill. I could ask for the right things and judge whether what came back was real.&lt;/p&gt;

&lt;p&gt;What follows is the progression — where I started, where it stands, and the rungs in between. Each rung got easier to climb than the last, and that compounding is the real story.&lt;/p&gt;

&lt;h2&gt;
  
  
  The progression: what I could ask for, at each rung
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Rung 1 — Chat.&lt;/strong&gt; I started with ChatGPT, like everyone else. Except my export says December 28, 2022, four weeks after launch, which means I was earlier than almost everyone, and then treated it as a toy for the better part of two years. My first recorded uses were birthday ideas, tactics for getting my kids to clean their rooms, and the cost of a furnace. Then, as an executive, I leaned on it hard: my usage went from a handful of chats a month to dozens, starting within weeks of taking the VP seat, and stayed there for over a year. I could ask for &lt;em&gt;answers&lt;/em&gt;: advice, troubleshooting, explanations. Nothing persisted; every conversation started from zero. Then January came, I lost the job, and my usage fell off a cliff — by then the energy had moved to tools I could build with.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Rung 2 — Chat with connectors.&lt;/strong&gt; Next came Perplexity, on a free year. It wired up its own connectors and took plain language, so I could ask for &lt;em&gt;work products&lt;/em&gt;: research reports, a recurring bourbon-market monitor, business plans. In one session I pasted in a report I was already generating and walked out with an MVP roadmap and a financial model. In another, two turns produced my company's logo and first web page. It was still chat, but chat that produced artifacts.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Rung 3 — An agent with a workspace.&lt;/strong&gt; I installed OpenClaw on my Windows workstation under WSL, with a great deal of trial and error, and found it too complex, or so I thought at the time. The honest diagnosis: I'd spent my recent years producing board reports and KPI dashboards (quite well, if I do say so myself), and this on-ramp was a Linux subsystem, a terminal, and PowerShell all at once. So I stepped down a rung on purpose: KimiClaw, which matched my skill level. Now I could ask for &lt;em&gt;systems, crudely&lt;/em&gt;: a named agent with its own identity files, shell scripts on a cron, JSON data synced to GitHub, my first site live on Wix with an embedded data feed. It was rough — the agent's logs were stamped with the wrong &lt;em&gt;year&lt;/em&gt; (its training data leaking into production), its hourly cron mostly logged "no changes," and my first committed-secret scare was two commits into the repo. But things ran without me watching. That was new.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Rung 4 — Tokens teach economics.&lt;/strong&gt; I burned credits fast, upgraded Perplexity for 30,000 bonus tokens, and learned that bonus tokens feel free and spend easy — their value only became visible when they ran out. Some went to frivolity (a dead app called "Upon My Death," and polished solutions to other businesses' problems that I never sent). The bulk went where it mattered: the primary framework of my first real platform.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Rung 5 — The migration, and the prompt-library dead end.&lt;/strong&gt; Credits gone, allotments evaporating, I made the heavy move from Perplexity to Claude — for Cowork, among other reasons. The move was daunting, clumsy, and frustrating. For a stretch I only had plain chat, no Claude Code, and it was terrible. Everything had to move: files, memory, and my scheduled automations — I hit a scheduler cap on at least two platforms along the way, and I've genuinely lost track of which; there were that many walls. Where Perplexity had cooperated with plain language, Claude and I fought. I downloaded multiple prompt libraries. To my knowledge I use none of them today, because the fix wasn't better phrasing. It was the next rung.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Rung 6 — Context instead of prompts.&lt;/strong&gt; The fix was to stop crafting requests and start building the world the AI wakes up into. That meant a &lt;code&gt;CLAUDE.md&lt;/code&gt; context file in every repository, durable memory in markdown files with an index (because chat memory doesn't survive sessions), session records with explicit "closed doors" that no future session may reopen, and guardrails written into the load path itself, so no session starts blind and no session re-litigates what's settled. This was tedious to build and forced a directory restructure I wish I'd done on day one. And it changed everything about what I could ask for: requests stopped being paragraphs and became sentences, because the context was already on disk.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Rung 7 — Scheduled automation with real infrastructure.&lt;/strong&gt; I re-homed all thirteen of my Perplexity-era cron jobs onto GitHub Actions and Cloudflare Workers in a single cutover week. This rung is also where secrets stopped being someone else's problem — hosted platforms had held my keys; now Actions, Workers, Loops, and Stripe each wanted credentials in their own place, and I bled on that for weeks until Doppler became the single source of truth, syncing daily into Cloudflare and piped by hand into GitHub Actions, with written rotation runbooks for both. That migration was life-changing, and it will get its own installment.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Rung 8 — Pipelines that judge.&lt;/strong&gt; The closed loop: scrape → extract → stage in KV → &lt;strong&gt;an LLM judge scores every item against an editorial rubric&lt;/strong&gt; → publish or reject, site rebuilt on commit, no human in the middle, daily at 14:00 UTC, LLM spend capped under $25/month. The judge's failure semantics took three incidents to get right and are the most transferable design I own: a legitimate editorial block exits 0 and posts a Discord notice; a judge or infrastructure error leaves the item PENDING for automatic retry; &lt;strong&gt;red means "a human is needed" and nothing else.&lt;/strong&gt; Every mistake this pipeline has shipped is now a layer in it: &lt;a href="https://dev.to/jenatechio/my-auto-publish-pipeline-shipped-a-two-year-old-news-story-heres-the-fix-all-three-layers-of-it-4750"&gt;the two-year-old story it published as news&lt;/a&gt; became a three-layer recency guard (deterministic date check on every category, currency in the judge's rubric, prior-year exclusion at extraction); the duplicate it shipped under a retitled headline became a near-dup matcher (≥3 shared title tokens covering ≥60% of the smaller set, with a numeric guard so a 10-year and a 17-year bottling don't merge). The test suite went 12 → 88 across July, almost entirely post-incident.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Rung 9 — Self-healing.&lt;/strong&gt; This one arrived in four layers, accreted incident by incident. (1) Every scheduled job writes a heartbeat, and &lt;a href="https://dev.to/jenatechio/a-dead-mans-switch-for-scheduled-jobs-because-silent-failures-rot-data-3nmh"&gt;a dead-man's-switch watcher alerts on staleness&lt;/a&gt;, because a job that fails loudly is a solved problem; the one that silently stops is the one that rots your data. My first agent taught me that by logging "no changes to push" hourly for weeks over a dead upstream. (2) A 4 AM morning check walks the estate's &lt;code&gt;/health&lt;/code&gt; endpoints live (it knows which reds self-recover and checks reality before alarming), then posts a Discord brief, plus a queue of changes it thinks I should approve to a separate proposals channel. (3) The routines themselves are declared in one file, and a nightly audit diffs the declaration against the live scheduler and auto-repairs drift: creates missing tasks, fixes cron mismatches, flags orphans, never deletes. It's GitOps logic pointed at my own calendar. The same nightly run regenerates a full platform-handoff snapshot so any fresh session, or a different AI platform entirely, can pick up every thread cold; the handoff folder prunes itself at 14 days. (4) The code layer heals too, and that is the next rung.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Rung 10 — An autonomous agent with governance.&lt;/strong&gt; Every midnight, &lt;a href="https://dev.to/jenatechio/i-gave-an-ai-agent-nightly-merge-rights-to-every-repo-i-own-with-a-budget-and-a-kill-switch-5b6k"&gt;an agent surveys every active repo I own, two dozen as of this writing, and opens and merges its own pull requests&lt;/a&gt; under rules I'd defend in an enterprise architecture review. The tiers are drawn by blast radius, and the review gates scale with the risk: mechanical, additive changes move fastest, anything touching real behavior has to satisfy the target repo's CI, and the riskiest tier — changes to the agent's own code — merges and then &lt;em&gt;next night's run judges the change and reverts it if things got worse&lt;/em&gt;, which is self-modification with built-in undo, capped at one per night. Every tier runs against a nightly budget. The kill switch is a committed file, deliberately dumb, thirty seconds from my phone. Its first night it crashed six times, blew both budgets across restarts (each restart counted only still-open PRs), and then did the thing that made me trust it: &lt;strong&gt;it diagnosed the hole in its own cap accounting, wrote up the fix, and declined to ship it because its self-change budget was already spent.&lt;/strong&gt; It shipped the fix the next night, inside the rules. It has since merged a fix for a shell bug my own review missed, and adopted its own queued improvements unprompted.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Rung 11 — Orchestration.&lt;/strong&gt; Multi-agent work as a routine tool: an eight-agent audit swarm with an adversarial verification pass produced the ranked business plan my daily briefs still cite; my de-AI-ing check runs one verifier agent per rule plus a skeptic pass to kill false positives; a four-agent sweep recently reconstructed this whole timeline from my own repos in an afternoon. Above it all sits a command center named after an old office joke — we used to say "I need to check with the AI overlords," so: &lt;code&gt;overlord&lt;/code&gt;. It holds the master tracker, a daily brief that opens with the top three &lt;em&gt;revenue&lt;/em&gt; actions, a deadline nag, weekly and monthly briefs scored against a declared revenue gate, and the memory system underneath. I think of it as the Eye of Sauron, if the Eye mostly sent encouraging morning briefings. It also has a front door: a dashboard the morning brief regenerates and my machine opens at login, showing what is waiting on me, what needs a decision, and which rails ran overnight. I spent years building dashboards so an executive team could see the state of the business. This one exists so I can see mine, and the two of us make the smallest executive leadership team I have ever sat on. For a person with extreme ADHD, this is the rung that matters most: it's executive function as infrastructure. I run tangent to tangent; the rails hold.&lt;/p&gt;

&lt;p&gt;I didn't notice the symmetry until the tier rules were already written. For five and a half years my job was deciding which work a person could take unsupervised, which needed a review gate, and which had to come to me before it shipped. A tier system is that same decision written in code. The primitives were all there before, just recorded in approval matrices instead of YAML: budgets, escalation paths, a defined scope of authority, and a way to reverse a bad call. The parallel breaks in the obvious places. An agent has no career to develop and no morale to damage; its performance review runs the next night and is enforced by a revert; and I never had to write a kill switch for a person. But the hard part of managing people was never the code I couldn't write. It was deciding how much autonomy to hand out and what to require in return, and that is the whole design problem here too.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Rung 12 — next.&lt;/strong&gt; Graph-based orchestration is the rung I haven't climbed yet. That's the frontier, and it'll be a part of this series when I have.&lt;/p&gt;

&lt;p&gt;The memory system got its real test recently: a model upgrade arrived knowing nothing about any of this, and it didn't matter — the knowledge lives in markdown the next model reads, not in any model's head. Models are cattle. The files are the pet.&lt;/p&gt;

&lt;h2&gt;
  
  
  The same climb, told in stack
&lt;/h2&gt;

&lt;p&gt;For the engineers: here is what was actually running at each stage, dated from my own git history.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;March 23 – mid-May: the KimiClaw stack.&lt;/strong&gt; An OpenClaw gateway ran as root on WSL2, driving Kimi (Moonshot's k2p5 coding model) as the agent brain. Shell scripts on the gateway's own cron (&lt;code&gt;openclaw cron create&lt;/code&gt;) synced a JSON data layer (&lt;code&gt;bourbon-drops.json&lt;/code&gt;) to GitHub — with the PAT embedded in the remote URL, which I did not yet know enough to be horrified by. Scraping was Python: BeautifulSoup4 and feedparser where sites cooperated, an attempted Playwright fallback that timed out and never worked. Publishing was a Jekyll site on GitHub Pages (first &lt;code&gt;index.html&lt;/code&gt; April 13), embedded into a Wix landing page as the bridge, with the app itself hosted on Kimi's infrastructure at a &lt;code&gt;.kimi.page&lt;/code&gt; URL. The agent's own architecture docs specced Next.js 15/TypeScript/Tailwind, Supabase for auth and data, and Mapbox GL for the map. The agent declared the MVP complete on March 24. Its logs were dated 2025.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;May 17–29: the client interlude.&lt;/strong&gt; The events board I built for a client was React 19 + Vite + Tailwind + shadcn/ui on the front, with a Python scraper behind it and static JSON between them. The agent's environment had no git auth, so versions moved as &lt;code&gt;.bundle&lt;/code&gt; and &lt;code&gt;.zip&lt;/code&gt; files, hand-carried, with commits made through GitHub's web-upload UI. This is what "before CI" looks like.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;May 27 – June: the production migration.&lt;/strong&gt; The rebuild that stuck was &lt;strong&gt;Astro&lt;/strong&gt; (v4, later 6.4.8) compiled to &lt;strong&gt;Cloudflare&lt;/strong&gt; — Workers for every API route, KV for state, static assets on the edge, &lt;code&gt;wrangler&lt;/code&gt; for deploys, content as markdown collections validated by schema (&lt;code&gt;astro:content&lt;/code&gt;). On June 1, CI moved to Node 24 action majors, and the thirteen Perplexity-era cron jobs were re-homed onto &lt;strong&gt;GitHub Actions&lt;/strong&gt; and &lt;strong&gt;Cloudflare Workers&lt;/strong&gt; in one cutover week (June 11–16). Scraping's bot-block problem — GitHub runner IPs are blacklisted across half the internet — was solved with &lt;strong&gt;Bright Data Web Unlocker&lt;/strong&gt; (June 23), with the hard-won config note that only &lt;code&gt;data_format: markdown&lt;/code&gt; returns judgeable pages; raw mode hands you empty SPA shells that look like success. Email went to &lt;strong&gt;Loops&lt;/strong&gt; (transactional + audience), alerts to &lt;strong&gt;Discord webhooks&lt;/strong&gt;, deliverability through an SPF/DKIM/DMARC fixpack. And on July 6, after the leak that taught me why, &lt;strong&gt;Doppler&lt;/strong&gt; became the single source of secrets, syncing each morning into Worker secrets via a &lt;code&gt;wrangler secret bulk&lt;/code&gt; workflow, with the GitHub Actions copy piped over by hand — a gap that bit me exactly once, when a rotation updated Doppler and Cloudflare and left Actions stale.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;July: the intelligence layer.&lt;/strong&gt; The pipeline: scrapers → extraction → a &lt;strong&gt;KV staging queue&lt;/strong&gt; → an &lt;strong&gt;LLM judge&lt;/strong&gt; (Claude Sonnet, key from Doppler, spend capped under $25/month) scoring each item against an editorial rubric → on approve, a &lt;strong&gt;commit to the content repo&lt;/strong&gt;, which triggers the build — git as both database and audit log, every published item a commit with provenance. Around it: &lt;strong&gt;Stripe&lt;/strong&gt; live mode with webhook + magic-link member auth, &lt;strong&gt;Turnstile&lt;/strong&gt; on forms, a CSP that taught me &lt;code&gt;form-action&lt;/code&gt; governs the &lt;em&gt;redirect target&lt;/em&gt; of a POST (dead checkout buttons, no visible error), geolocation-matched sends through Loops transactional, and &lt;strong&gt;Cloudflare Analytics Engine&lt;/strong&gt; for hit tracking on the client board. The &lt;code&gt;gh&lt;/code&gt; CLI finally got the access it needed mid-July, which retired an entire category of "open this PR manually" churn. The test suite went 12 → 88, almost every test the scar of a specific incident.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Late July – now: the autonomy layer.&lt;/strong&gt; The nightly estate agent runs as a GitHub Action at midnight Central, its own PAT piped from Doppler without the value ever being displayed, surveying every repo and merging under its tier and budget rules. Ten &lt;strong&gt;claude.ai scheduled routines&lt;/strong&gt; run the briefs, nags, audits, and the nightly handoff. A 4 AM morning-check worker walks the &lt;code&gt;/health&lt;/code&gt; endpoints. Local tooling grew too: &lt;strong&gt;faster-whisper&lt;/strong&gt; for on-box transcription, and, as of this week, a &lt;strong&gt;Playwright&lt;/strong&gt; E2E smoke suite (desktop and iPhone WebKit profiles, fourteen tests against the live site). I built that suite because a mentor reviewed the site on his own phone, which my desktop testing could never have stood in for, and told me to have my AI write Playwright scripts for automated user testing.&lt;/p&gt;

&lt;p&gt;Read the stages again and the pattern is the point: every layer replaced a specific failure, dated in the history. Nothing was architecture-astronauted into existence. The stack is a fossil record of problems.&lt;/p&gt;

&lt;h2&gt;
  
  
  The pitfall ledger — every one dated, every one now a layer
&lt;/h2&gt;

&lt;p&gt;The full stories come in later installments; here is the catalog, because a list of what actually went wrong is worth more than any best-practices post.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Credentials and auth.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A secret file committed two commits into my first real repo (April 12); the "fix" added a &lt;code&gt;.gitignore&lt;/code&gt; line and scrubbed nothing.&lt;/li&gt;
&lt;li&gt;A live API key pasted into a chat window (May 30); rotated after the assistant flagged it.&lt;/li&gt;
&lt;li&gt;My own session-archiver — built to preserve my learning — committed a live token into two repos (July 4). Unrewritable history; rotation is the only remediation. This is what finally drove Doppler in.&lt;/li&gt;
&lt;li&gt;The day &lt;em&gt;after&lt;/em&gt; Doppler went live, I committed a six-secret JSON file. Ten days exposed. Rotating six values across three surfaces each, under fire, while an unrelated same-morning deploy threw a decoy failure (July 17).&lt;/li&gt;
&lt;li&gt;Two secrets whose values matched &lt;em&gt;by accident&lt;/em&gt; — until a rotation exposed the naming mismatch and a scheduled job started 401ing (July 17).&lt;/li&gt;
&lt;li&gt;An expired PAT named "gatekeeper upload" that mapped to no secret name anyone could recognize, because the chain renames it (&lt;code&gt;GITHUB_&lt;/code&gt; prefix is forbidden in Actions). The publish pipeline failed silently for a day (July 20–21). Zero items lost — the queue retries by design. That part I'd built right.&lt;/li&gt;
&lt;li&gt;Continuous loops of &lt;code&gt;gh&lt;/code&gt; re-auths that succeeded every time and fixed nothing: &lt;strong&gt;four credential stores&lt;/strong&gt; (gh, git-credential-manager, the connector's app install, Actions/Worker secrets), each with separate state, each able to fail while the others were fine. Ended by one command: &lt;code&gt;gh auth setup-git&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;The permanent lesson that cost the most: &lt;strong&gt;a green dry run proves nothing&lt;/strong&gt; — dry mode never exercises the authenticated call. Only a real publish tests a publish token.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Deploys and platform.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Cloudflare's "Connect to Git" auto-deployed the static site &lt;em&gt;over&lt;/em&gt; my real Worker on every push — and wiped the Worker's secrets each time. Every route served HTML shells; days of mystery 401s were actually "the Worker isn't there" (July 14).&lt;/li&gt;
&lt;li&gt;Astro 4→6 removed &lt;code&gt;locals.runtime.env&lt;/code&gt; with a getter that &lt;em&gt;throws&lt;/em&gt;: every API route 500'd for 45 minutes in production. Same migration: Astro 5+'s default CSRF origin check silently 403'd my own Stripe webhook (July 17).&lt;/li&gt;
&lt;li&gt;A CSP &lt;code&gt;form-action&lt;/code&gt; allowlist that Chrome enforces against the POST's &lt;em&gt;redirect target&lt;/em&gt;: checkout buttons died with no visible error.&lt;/li&gt;
&lt;li&gt;CRLF/LF churn making every file show as modified until a &lt;code&gt;.gitattributes&lt;/code&gt; normalization; a tooling bug that appended null bytes to a workflow YAML; an editor mount that truncates multibyte writes — worked around with ASCII-only source and shell heredocs.&lt;/li&gt;
&lt;li&gt;GitHub Actions runner IPs bot-blocked across half the internet — the intel queue sat empty until a residential unlocker went in, with its own trap: raw mode returns empty SPA shells that look like successful fetches.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The pipeline's own sins.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It published a two-year-old acquisition story as news: the prior-year check was nested inside &lt;code&gt;if (category !== 'news')&lt;/code&gt; — the one category that needed it most skipped it. Three-layer fix; the item was unpublished; &lt;a href="https://dev.to/jenatechio/my-auto-publish-pipeline-shipped-a-two-year-old-news-story-heres-the-fix-all-three-layers-of-it-4750"&gt;the postmortem is a published article&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;First live run shipped the same story twice under retitled headlines: subset-matching dedup. Replaced with a token-overlap matcher with a numeric guard.&lt;/li&gt;
&lt;li&gt;A five-day "failure streak" that was three unrelated conditions sharing one red light — including a &lt;code&gt;maxTokens&lt;/code&gt; truncation cutting the judge's JSON mid-object. The redesign gave every signal exactly one meaning.&lt;/li&gt;
&lt;li&gt;21 items "blocked by the judge" that were actually four upstream bugs: HTML stripping ate hrefs before extraction, raw-mode fetches returned shells, no retry on API 529s, no per-URL cache (the same page fetched ~10× per run, billed each time).&lt;/li&gt;
&lt;li&gt;The agent-side gap audit found the miss rate &lt;em&gt;worsening&lt;/em&gt; — and the cause wasn't detection, it was the publish path: the machine caught nearly everything and the queue died downstream. Fix the pipe, not the thesis.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Autonomy's first nights.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The estate agent's first night: six crashes, and each restart counted only still-open PRs — so its nightly caps didn't bound the night, and it blew both budgets. It then diagnosed its own cap-accounting hole, wrote the fix, and declined to ship it off-budget. Trust, earned by restraint.&lt;/li&gt;
&lt;li&gt;Its mystery birth: an empty public repo appearing on my account at 1:45 a.m., flagged by a review as "look at this if it wasn't deliberate." It became the most-asked-about system I run.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The ledger holds successes too, and they count as technical wins, not luck:&lt;/strong&gt; a token outage that lost zero work because the queue design assumed failure; six-value rotations completed same-day with runbooks instead of archaeology; a closed publish loop that has run daily since July 17; an agent that adopts its own queued improvements; a mentor's phone-side review answered within days by a cross-device E2E suite whose first run caught three dead manufacturer links; and a test count — 12 to 88 — where I can tell you which incident bought each test.&lt;/p&gt;

&lt;h2&gt;
  
  
  The scoreboard I'm allowed to show
&lt;/h2&gt;

&lt;p&gt;The numbers here are measured, not promised — that's a house rule. What's real as of this writing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The platform:&lt;/strong&gt; production content site on Cloudflare (Astro), daily closed-loop intel pipeline with LLM judge, geolocation-matched email alerts (honestly framed: metro-matched, not GPS), Stripe live-mode membership with magic-link auth, a bottle-lottery tracking board, and a curated data layer versioned in git.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Client work:&lt;/strong&gt; a live community events board that a venue embeds on its own site — built in days, against an incumbent agency retainer many times what it cost to build. A full site-and-SEO takeover for the same venue is in conversation, not yet signed. The events worker scrapes ten monitored venues before dawn with its own Discord self-check, and its venue-coverage report doubles as an automatically generated sales lead sheet.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Shipping to a codebase I didn't write:&lt;/strong&gt; my first merged fixes on a public fork of an open-source agent orchestrator — a CI repair (a pnpm version pin) and a skills bug. For someone who managed developers without ever being one, merged pull requests on code I hadn't written were a personal line crossed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The graduation evals:&lt;/strong&gt; I formally evaluated the no-code AI app builder everyone recommends for people like me — and found I was already doing everything it offered, at a higher level, with full code ownership. Same verdict when I read up on "loop engineering": I'd been doing it before I knew the term. Both evals produced the same strange, quiet pride: progress measured by a side-by-side verdict, not a feeling.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Writing:&lt;/strong&gt; three published engineering articles — &lt;a href="https://dev.to/jenatechio/a-dead-mans-switch-for-scheduled-jobs-because-silent-failures-rot-data-3nmh"&gt;a dead-man's-switch pattern piece&lt;/a&gt;, &lt;a href="https://dev.to/jenatechio/my-auto-publish-pipeline-shipped-a-two-year-old-news-story-heres-the-fix-all-three-layers-of-it-4750"&gt;the stale-news postmortem&lt;/a&gt; (yes, I publish my own failures), and &lt;a href="https://dev.to/jenatechio/i-gave-an-ai-agent-nightly-merge-rights-to-every-repo-i-own-with-a-budget-and-a-kill-switch-5b6k"&gt;the nightly-merge-rights story&lt;/a&gt;, which &lt;strong&gt;published itself through the scripted rail this week&lt;/strong&gt;: API key from a secrets vault, dupe guard, kill-switch file, one article per run. The pipeline that this article describes published the article. Also: four writer-program applications out, and an essay at the desks of three national outlets — results reported when they're real.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The business:&lt;/strong&gt; Jenatech, LLC — formed in 2026, when the first client conversations turned real. Filing the LLC was the moment this stopped being a pile of experiments and became a company: it made this real. It runs with a declared revenue gate that the briefs measure against every week. Numbers get reported when they're real.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;Start with the one the productivity posts never include: procrastination. Mine was pure ADHD procrastination, despite the best of nags and noise. Friends visited. Trips got planned. Pets had health issues. There were stretches where the morning brief fired at 7:05 into a day that was never going to contain a single one of its actions. And here's what I learned about my own system: the nags failed at their daily purpose and succeeded at something worth more. Every time I came back, I knew exactly where I'd left off and what needed doing, and I found my way back into alignment with my goals. The system isn't built for streaks. It's built to make returning cheap. For a brain like mine, that was a huge success all by itself.&lt;/p&gt;

&lt;p&gt;The ledger has smaller entries too: an unrotated token sat in a settings file longer than I'll admit here, and you'll get the number in the secrets installment. I invented a hard cutoff date around a model transition, panicked, and rushed three comprehensive audits against a deadline that did not exist — they turned out to be the most valuable maintenance passes of the summer, and then I banned invented deadlines. And one "beginner" tool is still in production because I believed it beat its replacement at one specific job — then I finally measured a week of output, and the newer pipeline won decisively. Why I kept the old one is now its own open question, and when I answer that, it'll be a part too. The rest of the ledger becomes the rest of this series.&lt;/p&gt;

&lt;p&gt;The role of a lifetime ended without my consent, and the silence afterward nearly undid me. Seven months later the house isn't quiet: something checks the estate at 4 AM, something briefs me at 7, something merges fixes at midnight and files issues for what it shouldn't touch.&lt;/p&gt;

&lt;p&gt;The one thing I can't handle is quiet. But this is &lt;em&gt;my&lt;/em&gt; noise.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Jennifer Smith is the founder of Jenatech, LLC, a technology practice in St. Louis. The estate described here is real and running; she writes about what breaks at dev.to/jenatechio.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>career</category>
      <category>automation</category>
      <category>devjourney</category>
    </item>
    <item>
      <title>I Gave an AI Agent Nightly Merge Rights to Every Repo I Own (With a Budget and a Kill Switch)</title>
      <dc:creator>Jennifer Smith</dc:creator>
      <pubDate>Sun, 02 Aug 2026 18:41:21 +0000</pubDate>
      <link>https://dev.to/jenatechio/i-gave-an-ai-agent-nightly-merge-rights-to-every-repo-i-own-with-a-budget-and-a-kill-switch-5b6k</link>
      <guid>https://dev.to/jenatechio/i-gave-an-ai-agent-nightly-merge-rights-to-every-repo-i-own-with-a-budget-and-a-kill-switch-5b6k</guid>
      <description>&lt;p&gt;I run about twenty production and support repos by myself — a content site with a curated data feed, a couple of scheduled scrapers, some Cloudflare Workers, and the usual pile of half-dormant tooling that accumulates around all of it. There is no ops team. Maintenance debt on a solo estate doesn't announce itself; it just compounds quietly until the day you need the thing you didn't maintain.&lt;/p&gt;

&lt;p&gt;So I built a robot to do the maintenance, and — this is the part people react to — I gave it merge rights.&lt;/p&gt;

&lt;p&gt;Every night at midnight, a GitHub Actions job wakes up, surveys every repo in my org, and heals what it can: opens pull requests, merges them itself, files issues for anything that needs a human, and writes me a report. I named the repo make-me-better, which tells you what I think of my own discipline.&lt;/p&gt;

&lt;p&gt;Before you close the tab: it operates under a budget, a tier system, and a kill switch. Those three things are the whole article. The agent is the easy part.&lt;/p&gt;

&lt;h2&gt;
  
  
  The rules it lives under
&lt;/h2&gt;

&lt;p&gt;The prompt gives it three tiers of allowed action:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Tier 1 — mechanical.&lt;/strong&gt; Scaffolding missing config, adding a memory directory, boilerplate that has one obviously correct form. It can open and merge these itself.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tier 2 — judgment.&lt;/strong&gt; Anything touching business logic gets an issue, not a PR. The prompt's rule is "never guess at business logic."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tier 3 — self-modification.&lt;/strong&gt; It's allowed to change its own code — observation script, workflow, even its own prompt. One per night, maximum.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;On top of the tiers, hard caps: three heal PRs, three issues, one self-change per night. It never deletes anything and never force-pushes. And the kill switch is deliberately dumb: commit a file named &lt;code&gt;PAUSE&lt;/code&gt; to the repo root and it stops. I didn't want a dashboard or a config flag whose location I'd have to remember at 2 a.m.&lt;/p&gt;

&lt;p&gt;One more design decision that earned its keep immediately: the prompt is report-first. The report gets written and committed even if the run dies partway — the commit step runs &lt;code&gt;if: ${{ !cancelled() }}&lt;/code&gt;. A run that crashes still tells you what it saw before it crashed. When you're debugging a robot that works while you sleep, partial evidence beats a silent grave.&lt;/p&gt;

&lt;h2&gt;
  
  
  Shakedown night: four failures, each one layer deeper
&lt;/h2&gt;

&lt;p&gt;The first night was a tour of my own errors in judgment, peeled off one at a time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Run 1&lt;/strong&gt; died on a shell bug I'd written in the observation script. I had this everywhere:&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;last_commit&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;gh api &lt;span class="s2"&gt;"repos/&lt;/span&gt;&lt;span class="nv"&gt;$OWNER&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="nv"&gt;$r&lt;/span&gt;&lt;span class="s2"&gt;/commits?per_page=1"&lt;/span&gt; &lt;span class="nt"&gt;-q&lt;/span&gt; &lt;span class="s1"&gt;'...'&lt;/span&gt; 2&amp;gt;/dev/null &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;""&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Looks fine. Isn't. If &lt;code&gt;gh api&lt;/code&gt; writes a partial JSON error body before dying — a 409 from an empty repo, say — the &lt;code&gt;|| echo&lt;/code&gt; &lt;em&gt;appends&lt;/em&gt; the fallback to that partial output instead of replacing it. You get &lt;code&gt;{"message":"Git Repository is empty."}&lt;/code&gt; concatenated with your fallback, which is neither valid JSON nor your fallback, and everything downstream chokes. The fix is to assign first and fall back only if the substitution itself failed:&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;last_commit&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;gh api &lt;span class="s2"&gt;"..."&lt;/span&gt; &lt;span class="nt"&gt;-q&lt;/span&gt; &lt;span class="s1"&gt;'...'&lt;/span&gt; 2&amp;gt;/dev/null&lt;span class="si"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nv"&gt;last_commit&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;""&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I have been writing shell scripts on and off since the nineties. I still shipped this. Three separate times, in the same file.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Run 2&lt;/strong&gt; failed on token exchange — the action wanted an OIDC flow I hadn't configured, fixed by passing my PAT explicitly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Runs 3 and 4&lt;/strong&gt; were the interesting ones: the agent ran perfectly, wrote a full report, and then couldn't push it. 403. My fine-grained PAT had read-only access to nearly everything — I'd created it stingy on purpose and then forgotten I'd done that. And here's the error in judgment worth writing down: when the agent later tried to modify its own workflow file, I "knew" from years of classic GitHub tokens that it needed the &lt;code&gt;workflow&lt;/code&gt; scope, and I went looking for one. Fine-grained PATs don't have a workflow scope. They gate workflow-file writes on a repository &lt;em&gt;permission&lt;/em&gt; called Workflows — and editing a fine-grained PAT's permissions keeps the same token value, so nothing needs re-pasting anywhere. Twenty-five years of accumulated knowledge is a real asset, except for the parts that have silently expired.&lt;/p&gt;

&lt;h2&gt;
  
  
  The night it blew its own budget — and the refusal that made me trust it
&lt;/h2&gt;

&lt;p&gt;Night one, the job also crashed into its turn ceiling six times, and GitHub dutifully restarted it. Each restart checked whether it was within its nightly caps the only way it could: by counting &lt;em&gt;open&lt;/em&gt; heal PRs. But merged PRs drop off that list. So each incarnation saw a clean slate, and by morning the night's total was four heal PRs and two self-modifications — over both caps. Nothing it merged was wrong. The budget still failed at its one job, which was bounding a &lt;em&gt;night&lt;/em&gt;, not a &lt;em&gt;run&lt;/em&gt;. Caps that reset on crash aren't caps; they're suggestions.&lt;/p&gt;

&lt;p&gt;What happened next is the reason this article exists. In its own report, the agent diagnosed the cap-tracking hole, wrote up the fix — count the night's PRs cumulatively via search, not the open list — and then &lt;em&gt;declined to implement it&lt;/em&gt;, because its self-modification budget for the night was already spent. It flagged the fix and stopped.&lt;/p&gt;

&lt;p&gt;A system that finds a flaw in its own governance and won't fix it without budget is a system whose budget means something.&lt;/p&gt;

&lt;h2&gt;
  
  
  Steady state: it queues its own work and does it
&lt;/h2&gt;

&lt;p&gt;Three clean nights in, the loop looks like this: one night's report says "worth a future change: skip archived repos, they can't take PRs anyway." The next night's single self-modification slot goes to exactly that fix — validated with &lt;code&gt;bash -n&lt;/code&gt; and merged. The report even tells the next run what to verify: that the four archived repos actually vanish from the survey.&lt;/p&gt;

&lt;p&gt;Along the way it fixed a bug in my code that my own review pass had missed, caught that one of its metrics was silently measuring the wrong thing (the variable said "last memory update"; the API query said otherwise), and corrected its own backlog count downward when it discovered five of its "repos needing work" were archived or deprecated — overcounting it had itself introduced the night before. It flags its own bad data. Most humans I've managed needed coaching to do that.&lt;/p&gt;

&lt;h2&gt;
  
  
  The failure mode nobody warned me about
&lt;/h2&gt;

&lt;p&gt;Within days, eight of my local clones were behind their remotes. Of course they were: the robot commits to GitHub every night and has no idea my laptop exists. Every workflow habit I had assumed &lt;em&gt;I&lt;/em&gt; was the only writer. Now there's a second author who never sleeps, and &lt;code&gt;git pull&lt;/code&gt; before touching anything has gone from hygiene to survival. If you build one of these, tell your team — even if, like me, your team is you.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to steal
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Caps per night, not per run&lt;/strong&gt; — and count them from a source that survives crashes and restarts.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tier the autonomy.&lt;/strong&gt; Mechanical changes get merge rights. Judgment gets an issue. Self-modification gets a budget of one.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Report-first.&lt;/strong&gt; A dead run that left a partial report is debuggable. A dead run that left nothing is a séance.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A kill switch with no moving parts.&lt;/strong&gt; A file named &lt;code&gt;PAUSE&lt;/code&gt;. You will not remember anything cleverer under stress.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Make it write the postmortem.&lt;/strong&gt; The nightly report is where every bug in this article was actually caught — including its own.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The uncomfortable coda
&lt;/h2&gt;

&lt;p&gt;One error in judgment doesn't have a diff attached. While I was building all this, three finished, paid pitches sat in my drafts folder for thirty-five days, ready to send. The automation was genuinely worth building — but I built it partly because polishing a robot is comfortable and asking a human for money is not. The estate now heals itself nightly. Nobody has built the agent that hits send for you, because that one you have to run yourself.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I'm a cloud and infrastructure architect — 25 years of enterprise work, now running a solo consultancy. If your team needs someone who has already seen the enterprise version of your problem, I'm at &lt;a href="mailto:jennifer@jenatech.io"&gt;jennifer@jenatech.io&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>automation</category>
      <category>github</category>
      <category>devops</category>
    </item>
    <item>
      <title>A Dead-Man's Switch for Scheduled Jobs (Because Silent Failures Rot Data)</title>
      <dc:creator>Jennifer Smith</dc:creator>
      <pubDate>Thu, 30 Jul 2026 19:40:21 +0000</pubDate>
      <link>https://dev.to/jenatechio/a-dead-mans-switch-for-scheduled-jobs-because-silent-failures-rot-data-3nmh</link>
      <guid>https://dev.to/jenatechio/a-dead-mans-switch-for-scheduled-jobs-because-silent-failures-rot-data-3nmh</guid>
      <description>&lt;p&gt;A scheduled job has two failure modes. The loud one throws an error and your alerting catches it. The quiet one just stops running, and nothing catches it, because your alerting is wired to the job itself. No job, no error, no alert. The data it maintains gets staler by the day and every downstream system keeps serving it like nothing's wrong.&lt;/p&gt;

&lt;p&gt;I run a handful of production systems by myself. One of them is a niche content site with a curated data feed behind it, plus a release-tracking pipeline on Cloudflare Workers and D1, plus some geolocation alerting. There's no ops team. The quiet failure mode is the one that actually bit me.&lt;/p&gt;

&lt;h2&gt;
  
  
  How I found out
&lt;/h2&gt;

&lt;p&gt;The curation job that refreshes the feed died without a sound. No exception. Nothing in the logs that would make you stop scrolling. The site kept serving the last good copy of the feed, so every health check I had stayed green — the checks were all downstream of the dead job, and downstream was fine.&lt;/p&gt;

&lt;p&gt;I found out days later, reading my own site as a user, thinking huh, this hasn't changed in a while.&lt;/p&gt;

&lt;p&gt;My first instinct was to go add error handling, which was the wrong instinct, because the job didn't error. What I'd actually built was alerting for when things fail. I had nothing for when things stop happening. Took me an embarrassingly long time to see that those are different problems.&lt;/p&gt;

&lt;h2&gt;
  
  
  The pattern
&lt;/h2&gt;

&lt;p&gt;A dead-man's switch flips the alerting relationship around. The job doesn't report failure; it has to keep proving success. Silence is what fires the alarm.&lt;/p&gt;

&lt;p&gt;Two parts, both small.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The job stamps every successful run.&lt;/strong&gt; Last thing before exit, write a timestamp somewhere cheap — a KV key, a D1 row, or in my case a field right in the feed file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"items"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;...&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"generated_at"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-07-30T14:00:11Z"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Only on success. After the output is actually verified. Not in a &lt;code&gt;finally&lt;/code&gt; block — a stamp that gets written on failure is worse than no stamp, and yes, I initially had it in the wrong place.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A separate watcher checks the stamp's age.&lt;/strong&gt; Separate is the load-bearing word. Different runtime, different schedule, different failure domain. Mine is a tiny scheduled Worker on its own cron that reads the stamp and does one comparison:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ageHours&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Date&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="o"&gt;-&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;stamp&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;3600&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ageHours&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;MAX_AGE_HOURS&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;notify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Feed is stale: last success &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;ageHours&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toFixed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;h ago`&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;Job dies, watcher fires. Job hangs, watcher fires. I fat-finger the cron expression in a deploy, watcher fires. It doesn't know why the stamp is old and doesn't need to.&lt;/p&gt;

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

&lt;p&gt;Not the code. The number.&lt;/p&gt;

&lt;p&gt;My instinct was to set the threshold just above the job's schedule — job runs daily, alert at 26 hours, right? That version paged me constantly, because my feed is &lt;em&gt;curated&lt;/em&gt;: the job runs daily but the content only genuinely changes when there's something worth adding, which some weeks is barely at all. So I'd get a stale alert, check it, find nothing wrong, dismiss it. After the third or fourth round of that I'd trained myself to ignore the watcher, at which point it protected nothing.&lt;/p&gt;

&lt;p&gt;The threshold I actually run on that feed now is 720 hours. Thirty days. That number looks absurd next to a daily cron until you accept that the thing being monitored is the &lt;em&gt;content's&lt;/em&gt; rhythm, not the job's. The rule I eventually landed on: set it long enough that an alert is always worth investigating, short enough that the damage window is one you can live with. For a different system of mine — event scraping, where a dead day matters — the threshold is tight. Same pattern, wildly different number.&lt;/p&gt;

&lt;h2&gt;
  
  
  Make the alert say what broke
&lt;/h2&gt;

&lt;p&gt;The notification goes to a Discord channel I actually read. It carries three things: which system, how stale, where to look first.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;⚠️ feed stale: last success 39.2h ago (threshold 24h).
Check: worker cron logs → feed generator → upstream source.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At 7 a.m., "something is wrong somewhere" and "this specific thing is wrong, start here" are very different messages to receive. When you're the whole on-call rotation, the second one is a two-minute fix. The first one is a knot in your stomach until the weekend.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cost
&lt;/h2&gt;

&lt;p&gt;Basically zero. The watcher is a scheduled Worker on the free tier and the stamp is a few bytes. An hour to build the first one, minutes for each system after.&lt;/p&gt;

&lt;p&gt;Since the stamps and watchers went in, no silent failure has made it past one threshold window. The loud failures were never the problem. If your alerting only fires when jobs fail, it's worth checking what happens when one simply stops.&lt;/p&gt;

</description>
      <category>devops</category>
      <category>serverless</category>
      <category>cloudflare</category>
      <category>monitoring</category>
    </item>
    <item>
      <title>My Auto-Publish Pipeline Shipped a Two-Year-Old News Story. Here's the Fix — All Three Layers of It.</title>
      <dc:creator>Jennifer Smith</dc:creator>
      <pubDate>Thu, 30 Jul 2026 19:39:23 +0000</pubDate>
      <link>https://dev.to/jenatechio/my-auto-publish-pipeline-shipped-a-two-year-old-news-story-heres-the-fix-all-three-layers-of-it-4750</link>
      <guid>https://dev.to/jenatechio/my-auto-publish-pipeline-shipped-a-two-year-old-news-story-heres-the-fix-all-three-layers-of-it-4750</guid>
      <description>&lt;p&gt;I run a content pipeline that finds industry news, has an LLM judge score it, and publishes what clears the bar. Fully automated, Cloudflare Workers and D1, one operator: me. It runs daily and the whole point is that I don't babysit it.&lt;/p&gt;

&lt;p&gt;In July it published a news story from 2024 as if it had just happened.&lt;/p&gt;

&lt;p&gt;Nobody was harmed. One item, niche site, caught on a routine quality check within a few days, unpublished. But "the robot posted two-year-old news and nothing stopped it" is the kind of failure that quietly costs a site its credibility, and it deserved a real postmortem instead of a shrug. What went wrong turned out to be more interesting than I expected, and the fix ended up being three layers instead of the one I planned to write.&lt;/p&gt;

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

&lt;p&gt;The pipeline already had two mechanisms that should have caught this.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;There was a recency check.&lt;/strong&gt; But when I built it, the stale-content problem I was seeing was old product releases resurfacing on aggregator sites. So the year-sanity check ran against product items. News items took a different path through the gate and never met it. The check had no bug. It was standing at the wrong door.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;There was an LLM judge.&lt;/strong&gt; It scores every item before publication: relevant? interesting? worth the reader's time? It read the 2024 story and scored it well — because the story &lt;em&gt;was&lt;/em&gt; relevant and interesting. I had never asked the judge whether the story was current. That question wasn't in the rubric, so it wasn't asked. A human editor has a reflexive "hang on, when is this from?" An LLM judge has exactly the reflexes you wrote down and none you didn't.&lt;/p&gt;

&lt;p&gt;Upstream, an aggregator had resurfaced the old story with a fresh feed timestamp. Everything downstream took the feed's word for it. Two guards, both working as designed, both blind to this one.&lt;/p&gt;

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

&lt;p&gt;My first draft of the fix was one line: extend the year check to news items. Done, ship it. I've shipped enough one-line fixes to know that the incident you just had is rarely the exact incident you'll have next, so it became three thinner layers instead. Each one covers a different way the other two fail.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Layer 1: widen the deterministic check.&lt;/strong&gt; The year-sanity check now runs against every content type that passes the gate, not just the type that misbehaved historically. When a deterministic guard misses, check its scope before its logic — in my experience the case almost always walked &lt;em&gt;around&lt;/em&gt; the guard, not through it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Layer 2: put recency in the judge's rubric.&lt;/strong&gt; The judge now explicitly evaluates currency: when did this event happen, does its age undermine its value, penalize items whose event date sits far behind the feed date. This is the layer that catches what rules can't express. It only works for dimensions you remember to name. Rubrics are like tests that way — they encode the failures you've already imagined, and nothing else.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Layer 3: extract the event date from the content itself.&lt;/strong&gt; This one goes at the root cause: trusting upstream metadata. A resurfaced story arrives wearing a fresh timestamp; the packaging lies even when the content doesn't. The pipeline now tries to pull the actual event date out of the item's text and compares it to the feed's claimed freshness. Disagreement gets flagged for me instead of published. It's also the flakiest of the three layers — date extraction from prose fails in fun ways — which is exactly why the two blunter layers stand in front of it.&lt;/p&gt;

&lt;p&gt;Any one of these would have caught July's incident. I've watched each &lt;em&gt;kind&lt;/em&gt; of layer fail before, though: deterministic checks miss on scope, judges miss on rubric gaps, extraction misses on weird input. So it's all three.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd tell anyone running auto-publish
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Your failure modes are public.&lt;/strong&gt; A bad batch job embarrasses you in a log file. A bad publish embarrasses you in front of readers. Budget your quality-gate effort by audience size, not code size.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;LLM judges have no instincts.&lt;/strong&gt; Every implicit check a human editor does for free has to be written into the rubric explicitly. The judge will hand out perfect scores along every axis you specified while the item fails one you didn't.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Don't trust a timestamp you didn't compute.&lt;/strong&gt; Feed metadata describes the feed's behavior, not the content's age. If freshness matters, derive it from the content.&lt;/p&gt;

&lt;p&gt;The item came down, the three layers shipped the same week, and the pipeline went back to running unattended — which is the whole point. One person can run automated publishing. The tax is that every incident has to buy a structural fix, not a patch for the instance. Patch the instance and you've just scheduled the next one.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>automation</category>
      <category>serverless</category>
      <category>postmortem</category>
    </item>
  </channel>
</rss>
