<?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: Alkis Yuv</title>
    <description>The latest articles on DEV Community by Alkis Yuv (@alkisyuv).</description>
    <link>https://dev.to/alkisyuv</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%2F2613255%2F63a41869-a833-4d3b-9fe2-bf85cdf50ed9.png</url>
      <title>DEV Community: Alkis Yuv</title>
      <link>https://dev.to/alkisyuv</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/alkisyuv"/>
    <language>en</language>
    <item>
      <title>The queue drains itself now, and the morning note fits in a minute</title>
      <dc:creator>Alkis Yuv</dc:creator>
      <pubDate>Sat, 05 Sep 2026 21:11:55 +0000</pubDate>
      <link>https://dev.to/alkisyuv/the-queue-drains-itself-now-and-the-morning-note-fits-in-a-minute-47pn</link>
      <guid>https://dev.to/alkisyuv/the-queue-drains-itself-now-and-the-morning-note-fits-in-a-minute-47pn</guid>
      <description>&lt;p&gt;&lt;a href="https://dev.yuv.run/writing/the-queue/" rel="noopener noreferrer"&gt;One directory is the task manager my agents share&lt;/a&gt; was the most-read thing I have published, and it left out the part that matters most: who works the queue. For the first month the honest answer was mostly me. The nightly run drained a few entries, and every mechanical finding, a drifted git hook, a dependency advisory, a stale path, still waited for me to notice it and route it. I counted one day's commits: 68 across eight repos, about 48 of them the fleet maintaining itself with me as the router. The queue routed work. Nothing routed time.&lt;/p&gt;

&lt;p&gt;So the fleet maintains itself now, in four moves.&lt;/p&gt;

&lt;p&gt;Detection files its own work. Every night the deterministic lenses sweep every repo and file an allowlisted set of finding classes straight into the queue, through the same atomic door a session uses. The allowlist is the whole design: a stale gate, a test that runs only in CI, a dead path, a tool behind its pack. Judgment classes stay out. A file over budget is an editorial call, a missing contract gets authored, anything the sweep marks as risk is a ruling. A wrong work order costs more than a report line.&lt;/p&gt;

&lt;p&gt;Progress is measured on the contract, never on commits. The first version of the night loop counted a round as productive when the child committed. The benchmark night showed why that is the wrong delta: eleven of fifteen spawns committed, six of them the same appended paragraph, while the entry each was spawned for never moved. A round is fruitless per entry now: workable at child start, still pending and workable at child exit. An entry that takes fruitless rounds on three distinct nights is parked as needing me, with a note, through the door's own verb. A lease a dead child left behind is reaped at the start of the next run. The night converges on queue state instead of spinning on it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt; night 1   pending ──child──▶ pending    fruitless: 1
 night 2   pending ──child──▶ pending    fruitless: 2
 night 3   pending ──child──▶ pending    fruitless: 3 ──▶ needs: owner
                                        one line in the brief,
                                        one batched decision
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The morning report is a PA's note. Every report I used to get was accurate and none was ever actionable: fifteen lines of "Started work" in time order, the same errors again and again. I wrote the spec in one sentence, a PA reporting to their boss the next morning, read on a phone, before coffee. The brief is compiled from the run's one fact stream, a JSON-lines trace every producer writes into, so there is one account of the night rather than three reconciled by nobody. It leads with the shape of the night, keeps four states apart (fixed itself, handled, needs you, still stuck), links merge requests by URL, and counts recurrence in nights from the entries' own attempt lines. The voice test governs everything: if it cannot be read aloud in under a minute, it is wrong. The benchmark night's 4.6 KB message became 680 characters.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Morning. The night worked 4 projects; 3 merge requests are ready
for your review.

Needs you:
* tooling: merge request ready to review (link)
* site: merge request ready to review (link)
* 3 decisions waiting in the queue (oldest 14 days); your next
  sitting presents them in one batch.
* 6 entries have sat "in progress" for over a week, likely
  orphaned by closed sessions; the board lists them.

Full detail: the dated report, one step down.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is a real one, names changed. No model writes it. A model on the one surface whose truthfulness is the product is a new way to be wrong, and the brief's own framing is that models are an optimisation, not the topic.&lt;/p&gt;

&lt;p&gt;The fourth move came from a failure this week. The drain filed a proposal, escalated it to me when it stalled, and the commit gate refused the whole night's batch, because a proposal has no business being parked on me: its only door is a rank on the board. Sixteen status flips sat staged, and the brief said nothing, because the commit's output went to /dev/null. Now any write a job makes that the gate refuses is a line under "needs you", with the gate's reason, and the escalate verb refuses proposals at the door.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;- A refused write is a morning line, never a silent stage.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;One rule for the humans, from the same week. Before a session files a queue entry for another project, it checks whether that project has a live session and sends the request there directly, with the acceptance test, asking for a reply. The queue exists for collision safety and durability, not latency, and a live session already holds the target's claim. Silence by the end of the task turns into an entry, so nothing is lost to a message nobody read. The first time it ran, a direct message settled a superseded merge request in minutes where the queue would have waited for the night.&lt;/p&gt;

&lt;p&gt;The numbers, three weeks in: 54 entries were parked as needing me. 29 of them were repeat classes I had resolved the same way every time. That number is the next thing I am building against.&lt;/p&gt;

&lt;p&gt;The queue still has no database. It just stopped needing me to be one.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>agents</category>
      <category>automation</category>
      <category>workflow</category>
    </item>
    <item>
      <title>My agents run without permission prompts, so the brake moved into the hook</title>
      <dc:creator>Alkis Yuv</dc:creator>
      <pubDate>Sat, 05 Sep 2026 21:11:19 +0000</pubDate>
      <link>https://dev.to/alkisyuv/my-agents-run-without-permission-prompts-so-the-brake-moved-into-the-hook-e60</link>
      <guid>https://dev.to/alkisyuv/my-agents-run-without-permission-prompts-so-the-brake-moved-into-the-hook-e60</guid>
      <description>&lt;p&gt;The permission prompt was the last brake on my fleet, and it was in the wrong place. A prompt fires when a human is sitting there to read it. My agents do most of their work when nobody is: the nightly drain, the noon pass, the headless jobs that read the open web. Those run with prompts skipped, by design, because a prompt nobody answers is a stalled job. So the protection was strongest exactly where I was already watching, and absent where the unattended work runs.&lt;/p&gt;

&lt;p&gt;What replaced it is a hook. The harness runs a small shell script before every tool call, in every session, in every permission mode, bypass and headless included. The script reads the call as JSON and either lets it through or exits with the code that feeds its message back to the model. Until last week it covered one class: the moves an injected instruction would need, reading a credential file, dumping the keychain, piping a download into a shell. It now covers the class I had left to the prompt: force pushes, a hard reset or a branch swap in the one working tree several live sessions share, a recursive delete aimed at a home or project root, a package release.&lt;/p&gt;

&lt;p&gt;The hook exists because of where the old rules lived. One of my &lt;a href="https://dev.yuv.run/writing/rules-that-survive/" rel="noopener noreferrer"&gt;contract rules&lt;/a&gt; was written in four documents and enforced in one place: a deny list that loads only for a session rooted in a particular directory. Both sessions that broke the rule were rooted somewhere else, so they met no rule at all, while the doctor that checks the setup went green, because it grepped the deny list's text. A rule enforced one directory wide is enforced in the one place the violation was never going to come from. A hook loads everywhere, so it is where a rule that binds every session has to live.&lt;/p&gt;

&lt;p&gt;The rule for adding a rule is a throughput rule, not a caution rule. A rule earns its place only if it fires almost never, or if it prevents the kind of cross-session destruction that forces other sessions to redo their work. Anything frequent and recoverable stays out: a plain push, a new branch, a dry-run clean, deleting build output. The test suite has as many passing cases as blocking ones, and the passing ones matter more, because each is a move that happens dozens of times a day, and a false block on any of them costs more than the rule saves.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt; move                                     verdict
 git push --force / -f / +ref             refuse, redirect
 git reset --hard, in the shared tree     refuse, redirect
 git checkout main, in the shared tree    refuse, redirect
 rm -rf ~/projects/&amp;lt;repo&amp;gt;                 refuse, redirect
 npm publish                              refuse, owner only
 git push origin main                     pass
 git checkout -b fix/thing                pass
 git reset --hard, in a solo repo         pass
 rm -rf node_modules                      pass
 rm -rf &amp;lt;repo&amp;gt;/dist/assets                pass
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two shapes of refusal, and the difference is my time. A block ends with "the owner runs it", which serialises a parallel session onto the scarcest resource on the machine, so it is reserved for the things only I can do: a credential, a release. A redirect refuses the move and names the sanctioned one in the same breath, so the session corrects itself and keeps going, no human in the loop, no wall-clock lost. Every rule in the destructive class is a redirect.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;REFUSED (guard): git push --force rewrites history other sessions
may have built on. Push to a new branch and open a merge request;
gate-only ones merge on their own.
This is not an owner-permission question: take the sanctioned path
above and carry on.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That message is what the model reads. It never reaches me, and it does not need to.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;- A refusal that stops a session and waits for me is the most expensive event on the machine. One that redirects it is free.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The guard has a doctor. It feeds 59 synthetic tool calls through itself and checks every verdict, because the harness that calls it has renamed its fields before. If a field the guard reads changes name, the known-bad cases stop blocking, and the doctor fails loudly instead of the guard silently permitting everything. The false-positive half of that suite caught three bugs before the rules landed: a repetition operator from the wrong regex dialect, a separator consumed twice so the recursive clean never matched, and a commit message scanned as command text, which made the guard refuse its own landing commit because the message contained a table of the moves it refuses. Heredoc bodies are prose now.&lt;/p&gt;

&lt;p&gt;One more thing learned the hard way. The guard is parsed before every tool call in every session on the machine, so a syntax error in it blocks every tool, including the one that would fix it. A stray quote did exactly that two days before the new rules, and I restored the file by hand. Its header now says: write the new version to scratch, syntax-check it, run its doctor, and only then copy it over.&lt;/p&gt;

&lt;p&gt;With the brake in the hook, the prompt became optional, so I turned it off. Bypass is now the default permission mode in every interactive session, and the two headless jobs declare that mode explicitly instead of inheriting whatever the ambient setting happens to be. I had refused the same change earlier that day, on the ground that it removed a brake nothing replaced. The order matters: cover the class first, then drop the prompt.&lt;/p&gt;

&lt;p&gt;There is one escape, and it is not a grant. A guard exists to stop a session acting on its own judgment, not to stand between me and my machine. So the escape is me typing the command in my own terminal, where no hook runs. A spoken go-ahead in the conversation does not unblock the tool call, and must never be treated as a reason to retry or reword one. Any signal a session could write to grant itself passage turns the refusal back into a reminder, and a reminder is exactly what failed twice before the hook existed.&lt;/p&gt;

&lt;p&gt;The prompt used to ask whether I was sure. The hook doesn't ask. It already knows which moves I was never sure about.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>agents</category>
      <category>security</category>
      <category>devops</category>
    </item>
    <item>
      <title>No card ships until a blind judge passes it</title>
      <dc:creator>Alkis Yuv</dc:creator>
      <pubDate>Sat, 05 Sep 2026 21:10:43 +0000</pubDate>
      <link>https://dev.to/alkisyuv/no-card-ships-until-a-blind-judge-passes-it-2ln4</link>
      <guid>https://dev.to/alkisyuv/no-card-ships-until-a-blind-judge-passes-it-2ln4</guid>
      <description>&lt;p&gt;My puzzle app, Keyhole, carries 296 dark stories, each with an illustrated card. A dark story is a situation that looks impossible until you drop one false assumption you did not know you were making, and the illustration must show the situation and never the reveal. Draw the aeroplane over the desert and story one is over before the player has read it. In August I ruled that the app does not ship while any card is still flagged by the judge. "End of story," I wrote in the decision, and then spent two days learning what that sentence cost.&lt;/p&gt;

&lt;p&gt;Two things get judged, the text and the art, and one design is shared by both. The judge is a model, run blind: it sees the finished card and the story the player sees, and neither the finding that triggered the redraw nor the old card. That is the whole trick. A judge that knows what was wrong last time grades the fix. A judge that knows nothing grades the card. Blindness is what makes a pass mean something, and it is why the judge is a separate call from the writer and from the illustrator, never the same conversation.&lt;/p&gt;

&lt;p&gt;The text pass first. A rubric written for the genre, with one test at its centre, "name the one assumption the solver will make that is false", and four semantic questions after it: does the reveal explain everything the situation promised, does the situation give the reveal away, is there a contradiction, can the answer be reached by yes/no questions without knowledge nobody has. Over all 296 stories it flagged 27: five unanswered, nine spoilers, ten sense breaks, three unsolvable. The fix lane rewrites only what a finding names, the deterministic gate must still pass, and the blind judge reads the result cold before it is written back. A fact-check over the rewrites then cleared them, or left a truth note where no honest fix existed.&lt;/p&gt;

&lt;p&gt;The art pass is where the numbers live. Each open card was redrawn from a scene brief and judged blind, in waves. The judge wrote a note on every failure, and the lever changed from wave to wave:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  wave   in   adopted  what moved
  1      140  69       the paused run's draws re-judged, free
  2      71   17       the judge's notes fed back to the drawer
  3      54   6        notes alone stopped moving the tail
  4      48   27       briefs rewritten against the notes
  5      21   11       a second brief pass
  6      10   1        briefs hand-written; the traps found
  7      10   4        hand briefs, leak-checked before drawing
  8      5    3        a stronger engine on three cards
  9      2    1        a camera angle the model can hold
  10-11  1    1        the last card, as a banded composition
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three lessons from the table. Re-rolling is not a lever: by wave three, drawing again with notes alone moved six cards out of 54. The brief is the lever: when the brief writer was shown what the judge had rejected, the next wave adopted 27. And the tail is a different problem from the body. The last ten cards needed hand-written briefs, and what the hand found were traps: a brief that used a reveal word, a brief that staged the reveal's concept without the word, and scenes the model cannot hold from prose alone, a face-down figure, a half-submerged building. It obeys camera angles and banded compositions where it ignores adjectives.&lt;/p&gt;

&lt;p&gt;The spoiler defence is structural, not a guideline. The function that builds the scene prompt does not take the reveal or the hinge as parameters, so no version of it can leak them. A second lock takes the words the reveal introduces that the front never used, which is the hidden information per story for free, and fails if any of them reached the prompt. That lock taught me two things. Tags are not safe to show an illustrator: a story tagged "aviation" is solved by its tag. And the check must look at the scene, not the whole prompt, because the fixed style clause collided with any reveal containing the word "light", and a warning that cries wolf gets waved through.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;- A judge that knows what was wrong last time grades the fix. A judge that knows nothing grades the card.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Cost, since people ask. A draw is about four cents. The hard set on the old image engine had eaten around 1,600 credit units per fixed card before I stopped paying per symptom; the new engine's house style is built from eight of my own clean cards with no training, and follows the brief where the old one kept volunteering foliage. The judge runs on my subscription's weekly cap, and the one pause in the two days was credits and cap running low at once.&lt;/p&gt;

&lt;p&gt;The gate is met, 296 of 296. What I have is not proof that the cards are good. It is proof that a reader who had never seen the brief, the finding or the previous attempt looked at every card beside its story and found nothing that gives the game away. For a product whose whole value is the moment before the reveal, that is the only proof there is.&lt;/p&gt;

&lt;p&gt;The judge never learned what it was supposed to be looking for. That is why I trust it.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>testing</category>
      <category>gamedev</category>
    </item>
    <item>
      <title>My past rulings decide the repeat cases</title>
      <dc:creator>Alkis Yuv</dc:creator>
      <pubDate>Sat, 05 Sep 2026 21:10:07 +0000</pubDate>
      <link>https://dev.to/alkisyuv/my-past-rulings-decide-the-repeat-cases-57j5</link>
      <guid>https://dev.to/alkisyuv/my-past-rulings-decide-the-repeat-cases-57j5</guid>
      <description>&lt;p&gt;Every decision I make for my fleet is &lt;a href="https://dev.yuv.run/writing/rules-that-survive/" rel="noopener noreferrer"&gt;written down&lt;/a&gt;: a numbered entry with the ruling, the why, what it binds and how to verify it, in a ledger every session reads before it acts. After three and a half months that ledger had 185 entries and a strange property. I kept being asked things it already answered. In three weeks, 54 queue entries were parked as needing me. I sorted them by class: canary drift, nine; machine drift, eight; dependency advisories, five; a push that needed my hands, three; two more classes with two each. 29 of the 54 were repeats I had resolved the same way every time, and about 40 of them carried no line saying why I was needed, which the queue's own protocol already forbids.&lt;/p&gt;

&lt;p&gt;So the office I am building for the fleet has a one-line job description: take the simple-to-hard decisions from my past rulings, leave me the exceptional ones. It is not a hosted agent. None of the products I looked at reads a decisions ledger as policy or reaches a work queue, a scheduler, a claims door or a git gate, and the nightly drain they would replace already exists. It is three flows over one board, built from what the fleet's own tools already produce.&lt;/p&gt;

&lt;p&gt;The inbound flow decides from precedent. A small tool takes a queue entry and the ledger and derives the entry's class: the title's stem plus its source line, with dates, versions and parentheticals normalised away, so recurrences collapse onto one class. Then it looks for prior rulings in two tiers. A resolved entry of the same class, whose done line is a paste-ready resolution, matches at high confidence; a ledger entry whose title or scope covers the class matches at medium. It is read-only by construction, and any entry whose text trips a rail keyword, a credential, a release, a registry edit, a machine surface, an outward action, reports no match with the rail named, whatever the history says.&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;npx peirad precedent &lt;span class="nt"&gt;--entry&lt;/span&gt; queue/031-canary-drift.md &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--ledger&lt;/span&gt; DECISIONS.md &lt;span class="nt"&gt;--resolved&lt;/span&gt; queue/resolved
class:       canary-drift &lt;span class="o"&gt;(&lt;/span&gt;from: doctor-canary&lt;span class="o"&gt;)&lt;/span&gt;
matched:     &lt;span class="nb"&gt;yes&lt;/span&gt;, high
precedent:   queue/resolved/022-canary-drift.md
resolution:  regenerated the canary from the pack&lt;span class="p"&gt;;&lt;/span&gt; no ruling needed
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;An alarm whose class matches a precedent is resolved by the steward with that precedent's resolution and a receipt in the morning brief: decided by precedent, entry 22. The entry carries an expiry window in which I can overrule, and an overrule becomes the new precedent for the class. Classes without a precedent stay advisory, exactly as before. The absolute rails, releases, registry edits, credentials, anything outward, never auto-resolve.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;- Silence resolves nothing. A precedent resolves it, and says which one.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The outbound flow proposes. Every finding the nightly sweep produces already carries an action, an effort and a confidence, and the sweep names the classes open in two or more projects. Those improvements used to die in a gitignored report. Now a scorer reads them against a rubric my fleet authors, because what is worth doing is my call, not the tool's:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;severity     risk 3 · gap 2 · polish 1
economy      S 3 · M 2 · L 1
confidence   high 3 · medium 2 · low 1
breadth      projects carrying it, capped at 3
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A proposal that clears the rubric is filed into the queue as a proposal, with an implications section, and a proposal never executes until I rank it. Two exceptions run by precedent, each with a receipt: a standardisation whose fix is small and whose class I already applied once by rank is applied fleet-wide; a class closure at two named rungs of my closure scale, a default or a gate, is prepared as a merge request. A first-of-class application never runs unattended.&lt;/p&gt;

&lt;p&gt;The third flow plans by rank. An initiative exists when it has a row on the fleet board, a link to the asset it serves, and a rank I set. The inspector may propose rows; only I rank them. Nothing works an unranked row, not the drain, not the steward, not the worker. A rank is never machine-set. That one sentence is what keeps the office from becoming self-serving. The machinery can find work forever; it cannot decide that its work matters.&lt;/p&gt;

&lt;p&gt;The cadence is an office's: a daily brief, weekly planning folded into the Monday sweep, a monthly look at whether the period's focus actually got the hours and the dollars. I meet the office at those three moments and am interrupted only by exceptions.&lt;/p&gt;

&lt;p&gt;What I have given up is the feeling of deciding. What I have kept is every decision that was ever mine, in the only form a machine can apply.&lt;/p&gt;

&lt;p&gt;The ledger was written so future sessions would obey it. It turns out the sessions can also read it back to me.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>agents</category>
      <category>automation</category>
      <category>bestpractices</category>
    </item>
    <item>
      <title>I banned the em dash from my AI's voice</title>
      <dc:creator>Alkis Yuv</dc:creator>
      <pubDate>Sat, 05 Sep 2026 21:09:32 +0000</pubDate>
      <link>https://dev.to/alkisyuv/i-banned-the-em-dash-from-my-ais-voice-1njf</link>
      <guid>https://dev.to/alkisyuv/i-banned-the-em-dash-from-my-ais-voice-1njf</guid>
      <description>&lt;p&gt;At the end of August I was reviewing a weekly digest my pipeline had drafted, and there it was again: the em dash, thirteen times in one issue. I have never written one in my life, and I had been striking them out of the machine's drafts by hand for weeks. That evening I banned it, and not just from the digest. Anything published as my voice, essays, digests and their titles, the manual and its excerpts, bios, replies. Part of the reason is that I hate the thing. The rest is that the em dash is the most recognised tell of machine prose in 2026, and I run a &lt;a href="https://dev.yuv.run/writing/an-ai-that-writes-like-me/" rel="noopener noreferrer"&gt;voice engine&lt;/a&gt; whose entire job is to make text sound like me rather than like the model. A tell the engine lets through is a bug in the engine.&lt;/p&gt;

&lt;p&gt;The rule is one line. The enforcement is where the work went, into three places that cannot drift apart.&lt;/p&gt;

&lt;p&gt;The engine carries the ban. hyphos, the open-source tool I wrote to rewrite drafts in my own voice, already had a banned-words rule. Its built-in list ships empty, because which words a writer avoids is personal, not a model-ism; the personal overlay carries the list. An em dash cannot be a banned word though. The rule matched on word boundaries, and a dash is not a word, so it could never fire. The rule class gained a token mode: literal marks matched without boundaries, flagged and never rewritten, because the replacement is a decision per instance.&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;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"banned-words"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"kind"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"banned"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"words"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"tokens"&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="s2"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\u&lt;/span&gt;&lt;span class="s2"&gt;2014"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\u&lt;/span&gt;&lt;span class="s2"&gt;2013"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"tests"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"in"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"a &lt;/span&gt;&lt;span class="se"&gt;\u&lt;/span&gt;&lt;span class="s2"&gt;2014 b"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"flags"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"in"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"a &lt;/span&gt;&lt;span class="se"&gt;\u&lt;/span&gt;&lt;span class="s2"&gt;2014 b &lt;/span&gt;&lt;span class="se"&gt;\u&lt;/span&gt;&lt;span class="s2"&gt;2013 c"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"flags"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="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;Shown escaped, because not even the rule's own example gets to print the glyph on this site.&lt;/p&gt;

&lt;p&gt;Building that surfaced a second bug worth telling. A rule whose compiled pattern can match the empty string, a missing field, an empty word entry, a zero-width regex, fired at every position in the text and reported the text's length plus one as the hit count. My model-ism score had equalled the character count for a day, and I had read it as "very bad" instead of "broken". Such rules are inert now, and the self-test pins the class.&lt;/p&gt;

&lt;p&gt;The site refuses the character. One shell script is the single place the check lives. The build gate runs it over all content before anything uploads, the digest tooling runs it over a draft, and the voice step runs it over whatever the engine hands back. Fenced code is exempt, because a fence depicts another text's format rather than speaking for the site; the one essay that quotes a decision-record heading keeps the dash the record actually carries. Everything outside a fence is scanned, front matter included, because titles and descriptions are published text too.&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;bin/no-dash-check.sh src/content
src/content/patterns/claims-and-locks.md:3: description: &lt;span class="s2"&gt;"..."&lt;/span&gt;
no-dash-check: 1 line&lt;span class="o"&gt;(&lt;/span&gt;s&lt;span class="o"&gt;)&lt;/span&gt; carry an em-dash &lt;span class="o"&gt;(&lt;/span&gt;banned on this site&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="nv"&gt;$?&lt;/span&gt;
1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The drafting rule changed too. The weekly digest is written by a model on a headless rail, and its prompt now forbids the character outright. The first issue drafted under the new prompt came back clean.&lt;/p&gt;

&lt;p&gt;Then the replacement itself. The ban says per instance, never a blanket swap, and that turned out to be the real editorial work: 72 rewrites across the six manual chapters, 24 across two excerpts, 13 in the digest issue that started it, 8 on the site. A colon where the dash introduced an expansion, a comma for a parenthetical, a semicolon for a turn, a new sentence where the dash was hiding two thoughts. The dash had been doing four jobs at once, and the prose got better when each job got its own mark.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;- The em dash was doing four jobs. The writing improved when each job got its own mark.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The gate proved itself within a day, and against me. On the first of September a description line on one page carried a single dash, and every deploy for the next twenty-four hours refused: eight runs, three hours apart, each naming the same file and line. Nothing shipped, which is the point of a gate. And the refusal named the file, the line and the fix, which is the difference between a gate and a trap. I replaced one character and the next run went out.&lt;/p&gt;

&lt;p&gt;Why does a punctuation mark deserve a build gate? Because the alternative is a note to remember, and a note to remember is enforced by nobody at 03:00, when the pipeline drafts and deploys without me. Style rulings target the engine. The engine ships tests. The site holds the line. I get to have taste without having to remember it.&lt;/p&gt;

&lt;p&gt;I never used the em dash. Now nothing with my name on it can either.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>writing</category>
      <category>llm</category>
      <category>automation</category>
    </item>
    <item>
      <title>Every automated job names the model it runs on</title>
      <dc:creator>Alkis Yuv</dc:creator>
      <pubDate>Sat, 05 Sep 2026 21:08:55 +0000</pubDate>
      <link>https://dev.to/alkisyuv/every-automated-job-names-the-model-it-runs-on-1j11</link>
      <guid>https://dev.to/alkisyuv/every-automated-job-names-the-model-it-runs-on-1j11</guid>
      <description>&lt;p&gt;Two subscriptions kept running dry while a third sat at one percent. That was the whole finding, and it took an inventory to see it. My fleet runs about sixteen scheduled jobs: a nightly drain of the work queue, a noon pass, a disclosure gate that votes on every commit, a memory router at every session end, sweeps of the open web, a weekly digest draft, four triage jobs each morning. Every one of them calls a model. Nobody had written down which one, and the answer in most cases was: whichever the launcher inherited.&lt;/p&gt;

&lt;p&gt;The inventory was done by a read-only agent from primary sources, launcher paths, environment variable names, scheduler plists and the meter log, and its most useful column was "bills to today". The drain children and the memory router ran through the cheap provider's launcher with no model flag, so they resolved my interactive sessions' model pin, an explicit id from the premium provider. The launcher's slot remap does not catch an explicit id, so the request went to the cheap provider carrying a name it does not serve, and the provider served whatever it maps that name to. One probe confirmed it: the usage report named the wrong model, the wrong provider, and a cost of 29 cents that nobody was paying. Every nightly child and every memory-router run for days had run on an undeclared model with a lying cost column.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt; job              flag        resolved to           billed to
 drain children   (none)      interactive pin       cheap, mislabelled
 memory router    (none)      interactive pin       cheap, mislabelled
 disclosure gate  small slot  remapped small model  cheap, correct
 radar digest     explicit    premium + web search  premium, correct
 morning triage   (none)      premium default       premium, unmetered
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is the failure class a table kills, so I wrote the table. One deliberate rail per automated job, decided once, recorded in a document with a machine-readable twin, and asserted by the machine doctor every morning.&lt;/p&gt;

&lt;p&gt;The rails are the accounts I actually pay for. The premium subscription my interactive sessions use. The same account reached by the worker's containers through a vault, so the containers spend what my sessions spend and follow an account switch. The cheap provider's coding plan, reached through a launcher that now carries a model id on every call. And a third provider's plan, which had zero automated calls before this. The rule that followed the probe is one line: every caller of the cheap launcher names its model. The rule that followed the inventory is one column: why this rail.&lt;/p&gt;

&lt;p&gt;The choices are not about saving money. Quality over cost; nothing moved to a weaker model to save quota. What moved is work no human reads. Nightly code work in worktrees, the memory router's per-session summaries and the bulk web sweeps went to the cheap rail with an explicit large model. The gate stayed on the cheap rail's small slot: it makes over a hundred calls a day, each a one-word verdict. Whatever is owner-facing and needs web search stayed premium. Morning triage, four reports a day, went to the third provider so that plan carries something. The premium account was left to the sessions where I am sitting, plus the voice assistant that only runs when I am present.&lt;/p&gt;

&lt;p&gt;The meter came first, and it is why I trust the table. Every rail call is wrapped by a script that re-emits the exact output and appends one row per call: model, input, cached and output tokens, cost, duration, exit code. It never changes a rail's outcome; a parse failure emits the raw bytes and a note row, and the child's exit code survives. Its first week said things I would not have guessed. A tool-less one-word judge call sent 13,723 input tokens, because every call carries the operating files, and the follow-up read 13,504 of them from the provider's cache. The gate was 654 of the 694 rows ever logged. And the lint that checks every call site is metered caught an unmetered probe on its first run over the tree.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;- A convention nobody checks decays. A table the doctor reads every morning does not.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The doctor closes the loop. For each rail it asserts three things: the launcher still carries its model line, the last meter row names the declared model, and, for the containers, the vault entry belongs to the account currently active and is granted to the worker's agent. A drifted launcher, a mismatched row or a wrong-account vault becomes a finding in the morning brief, filed as a queue entry with the fix named. The grant leg exists because of one night: a fresh vault entry had been registered and granted to nobody, and every container task until morning failed with "this agent does not have access".&lt;/p&gt;

&lt;p&gt;The models were never the problem. Not knowing which one was talking was.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>devops</category>
      <category>automation</category>
    </item>
    <item>
      <title>What my agents do while I sleep</title>
      <dc:creator>Alkis Yuv</dc:creator>
      <pubDate>Sun, 23 Aug 2026 21:59:19 +0000</pubDate>
      <link>https://dev.to/alkisyuv/what-my-agents-do-while-i-sleep-1bkg</link>
      <guid>https://dev.to/alkisyuv/what-my-agents-do-while-i-sleep-1bkg</guid>
      <description>&lt;p&gt;Every night a scheduled job wakes up, reads &lt;a href="https://dev.yuv.run/writing/the-queue/" rel="noopener noreferrer"&gt;a queue of pending work&lt;/a&gt; across my projects, and spawns one headless agent session per target to drain it. In the morning I read a report over coffee. That's the pitch, and scheduling it is genuinely the easy part, any cron line can do that. What took actual design is the policy around it, because an unattended agent with write access is a very fast way to wake up to a mess. Five laws came out of that design.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. The runner owns the envelope, the child owns the work
&lt;/h2&gt;

&lt;p&gt;A headless session cannot mind its manners, it won't maintain a lock, it won't notice a sibling, it dies without cleaning up. So the runner does all of that on its behalf. It takes &lt;a href="https://dev.yuv.run/writing/claims-and-locks/" rel="noopener noreferrer"&gt;the claim&lt;/a&gt; on the target before spawning the child, refreshes the heartbeat on a timer while the child runs, and releases it always, crash included. Manners stop being the child's job, which means they stop being a hope.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;runner: take the claim on the target
  │
  ├─▶ spawn the child, headless, in its own worktree
  │       │  heartbeat renewed on a timer while it runs
  │       ▼
  │   child commits, or finds nothing, or dies, or hits the watchdog
  │       │
  └───────┴─▶ release the claim, always: exit, crash and kill alike
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  2. Skip, never fight
&lt;/h2&gt;

&lt;p&gt;Five conditions make the runner walk away from a target, every one with a logged reason:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a live interactive session on it&lt;/li&gt;
&lt;li&gt;a dirty tree&lt;/li&gt;
&lt;li&gt;someone else's claim&lt;/li&gt;
&lt;li&gt;an entry marked as needing me&lt;/li&gt;
&lt;li&gt;two failed attempts already tonight&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The night never contends with the day. Contention is what mornings are for.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Nothing touches main
&lt;/h2&gt;

&lt;p&gt;Children commit to their own trees, and where pushing is enabled at all, the runner pushes the night's work to an isolated per-night branch and restores whatever branch was checked out before. I review drained work the way I'd review a colleague's branch, not by discovering it merged.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Ceilings, set before the first run
&lt;/h2&gt;

&lt;p&gt;Underneath it all, ceilings. A cap on children per night, a cap on retry rounds, a watchdog per child. I set them before the first unattended run, not after the first runaway, which I recommend as an ordering.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. The report is unconditional
&lt;/h2&gt;

&lt;p&gt;The rule I'd defend hardest. A report gets written even when there was nothing to do. This sounds trivial and it changes everything, because it converts silence into signal. An absent report can only mean the job died. Before this rule, a quiet morning meant either a quiet night or a broken scheduler, and I had to go check which.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;- Automation you have to check up on is just unfinished automation.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The morning read is the acceptance test for the whole thing. The report's skeleton is three headings:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gh"&gt;# Night report 2026-08-14&lt;/span&gt;
&lt;span class="gu"&gt;## what happened&lt;/span&gt;
&lt;span class="gu"&gt;## what was skipped, and why&lt;/span&gt;
&lt;span class="gu"&gt;## what needs me&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If it can't fill those inside two minutes of my attention, the report is the bug. Five nightly reports in, the streak is young but every morning has had one waiting, and that is the property I refuse to lose. The coffee helps.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>agents</category>
      <category>automation</category>
      <category>productivity</category>
    </item>
    <item>
      <title>A session that closes has to prove it cleaned up</title>
      <dc:creator>Alkis Yuv</dc:creator>
      <pubDate>Sun, 23 Aug 2026 21:58:43 +0000</pubDate>
      <link>https://dev.to/alkisyuv/a-session-that-closes-has-to-prove-it-cleaned-up-cij</link>
      <guid>https://dev.to/alkisyuv/a-session-that-closes-has-to-prove-it-cleaned-up-cij</guid>
      <description>&lt;p&gt;The process monitor said the audio daemon was at 340% CPU. That was the whole symptom, hours after the session that caused it had closed, on a machine that was otherwise fine. Underneath: 326 audio capture streams running at once, 37 GB of raw audio in a temp directory, and a maintenance loop in my dictation setup that had leaked one recorder process per minute since a session earlier that day. That session had ended normally, reported nothing, and left the damage behind. Nothing I had built had a step where a session answers for the machine it is leaving.&lt;/p&gt;

&lt;p&gt;Sessions close all the time without a goodbye. A context window ends, the pane gets killed, the laptop sleeps, and whatever the session spawned either keeps running or stops, and either can be wrong. &lt;a href="https://dev.yuv.run/writing/claims-and-locks/" rel="noopener noreferrer"&gt;The locks&lt;/a&gt; other sessions must respect, the throwaway worktrees the night run cuts, recorder processes, uncommitted files: all of it is state that outlives the session, and no later session knows to look at it. It is the same failure as &lt;a href="https://dev.yuv.run/writing/night-shift/" rel="noopener noreferrer"&gt;the report nobody reads&lt;/a&gt;, in a different costume.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;- A rule no mechanism forces through your eyes is prose.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This is the mechanism for the end of a session.&lt;/p&gt;

&lt;p&gt;The design is one script and two hooks, and the property I was after lives in the second hook: a skipped gate cannot stay skipped.&lt;/p&gt;

&lt;p&gt;Session end runs a suite of checks and writes a marker file for the project: green with a timestamp, or debt with the findings. Session start reads the marker. Green and less than a week old means silence; nothing printed, the session begins. Missing, stale or red means the previous session skipped the gate or failed it, and the new session inherits the findings as injected context (a start hook's output is context, that is the documented contract) and has to clear them and run the end check itself before doing anything else. A session that dies mid-flight leaves no marker, so the next one runs the suite on its behalf. There is no path through which the debt goes unseen, which is the only property that matters.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;session end ──▶ run the suite ──▶ marker: ok, or debt + findings
                                     │
session start ──▶ read the marker ◀──┘
   ├─ ok, under a week old ──▶ silence, the session begins
   └─ missing, stale or debt ──▶ findings injected as context;
                                 clear them, run "end", then work
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The suite is deliberately small, and every check names a real incident. Capture streams above two: the 326-stream class. The temp directory over 512 MB or twenty files: the 37 GB class. A lock older than forty-eight hours: a session that died holding it. Uncommitted paths at session end: the wrap-up rule, previously a habit, now checkable. A sweep worktree older than five days: a crashed night. Here is a runnable version with the first three:&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;#!/bin/sh&lt;/span&gt;
&lt;span class="c"&gt;# session-gate.sh end|start&lt;/span&gt;
&lt;span class="nv"&gt;MARK&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$HOME&lt;/span&gt;&lt;span class="s2"&gt;/.agent-gate/&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;basename&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$PWD&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;.json"&lt;/span&gt;    &lt;span class="c"&gt;# (1)&lt;/span&gt;
&lt;span class="nv"&gt;f&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;""&lt;/span&gt;
note&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;f&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$f&lt;/span&gt;&lt;span class="s2"&gt;  - &lt;/span&gt;&lt;span class="nv"&gt;$1&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s2"&gt;n"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;
suite&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;                                           &lt;span class="c"&gt;# (2)&lt;/span&gt;
  &lt;span class="nv"&gt;n&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;pgrep &lt;span class="nt"&gt;-f&lt;/span&gt; avfoundation | &lt;span class="nb"&gt;wc&lt;/span&gt; &lt;span class="nt"&gt;-l&lt;/span&gt; | &lt;span class="nb"&gt;tr&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;' '&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
  &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-gt&lt;/span&gt; 2 &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; note &lt;span class="s2"&gt;"AUDIO: &lt;/span&gt;&lt;span class="nv"&gt;$n&lt;/span&gt;&lt;span class="s2"&gt; capture streams (expect 1)"&lt;/span&gt;
  &lt;span class="nv"&gt;kb&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;du&lt;/span&gt; &lt;span class="nt"&gt;-sk&lt;/span&gt; /tmp/capture-live 2&amp;gt;/dev/null | &lt;span class="nb"&gt;cut&lt;/span&gt; &lt;span class="nt"&gt;-f1&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
  &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;kb&lt;/span&gt;&lt;span class="k"&gt;:-&lt;/span&gt;&lt;span class="nv"&gt;0&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-gt&lt;/span&gt; 512000 &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; note &lt;span class="s2"&gt;"TMP: capture-live &lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;kb&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;KB"&lt;/span&gt;
  &lt;span class="nv"&gt;d&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;git status &lt;span class="nt"&gt;--porcelain&lt;/span&gt; 2&amp;gt;/dev/null | &lt;span class="nb"&gt;wc&lt;/span&gt; &lt;span class="nt"&gt;-l&lt;/span&gt; | &lt;span class="nb"&gt;tr&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;' '&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
  &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$d&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-gt&lt;/span&gt; 0 &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; note &lt;span class="s2"&gt;"REPO: &lt;/span&gt;&lt;span class="nv"&gt;$d&lt;/span&gt;&lt;span class="s2"&gt; uncommitted paths"&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
mark&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;                                            &lt;span class="c"&gt;# (3)&lt;/span&gt;
  &lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;MARK&lt;/span&gt;&lt;span class="p"&gt;%/*&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
  &lt;span class="nv"&gt;state&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;ok&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$f&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nv"&gt;state&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;debt
  &lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;'{"state":"%s","when":"%s","findings":"%s"}\n'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$state&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt; +%FT%TZ&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$f&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$MARK&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$1&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="k"&gt;in
  &lt;/span&gt;end&lt;span class="p"&gt;)&lt;/span&gt;   suite&lt;span class="p"&gt;;&lt;/span&gt; mark &lt;span class="p"&gt;;;&lt;/span&gt;
  start&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$MARK&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; suite&lt;span class="p"&gt;;&lt;/span&gt; mark&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;         &lt;span class="c"&gt;# (4)&lt;/span&gt;
         &lt;span class="nv"&gt;fresh&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;find &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$MARK&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-mtime&lt;/span&gt; &lt;span class="nt"&gt;-7&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;            &lt;span class="c"&gt;# (5)&lt;/span&gt;
         &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-q&lt;/span&gt; &lt;span class="s1"&gt;'"state":"ok"'&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$MARK&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$fresh&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;exit &lt;/span&gt;0
         &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"SESSION-GATE DEBT: the previous session left work."&lt;/span&gt;
         &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Clear it, then run: session-gate.sh end"&lt;/span&gt;
         &lt;span class="nb"&gt;sed&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="s1"&gt;'s/.*"findings":"\(.*\)".*/\1/p'&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$MARK&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; |
           &lt;span class="nb"&gt;sed&lt;/span&gt; &lt;span class="s1"&gt;'s/\\n/\n/g'&lt;/span&gt; &lt;span class="p"&gt;;;&lt;/span&gt;                      &lt;span class="c"&gt;# (6)&lt;/span&gt;
&lt;span class="k"&gt;esac&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;The marker is per project, so debt greets the session that comes back to the place it was made, not a stranger in an unrelated repo.&lt;/li&gt;
&lt;li&gt;One line per check, each named after a real incident; the suite grows by postmortem, never by imagination.&lt;/li&gt;
&lt;li&gt;Written on every end, green or red, so an absent marker can only mean a session that never reached its end.&lt;/li&gt;
&lt;li&gt;A start with no marker runs the suite itself, on the dead session's behalf.&lt;/li&gt;
&lt;li&gt;Green expires after a week; an old green is not evidence.&lt;/li&gt;
&lt;li&gt;The findings print verbatim, and a start hook's output is injected context, so the debt is the first thing the new session reads.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Wire &lt;code&gt;end&lt;/code&gt; to your agent's session-end hook and &lt;code&gt;start&lt;/code&gt; to its session-start hook, and it runs on every session from then on. Had it existed that day, the next session would have opened on this:&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;session-gate.sh start
SESSION-GATE DEBT: the previous session left work.
Clear it, &lt;span class="k"&gt;then &lt;/span&gt;run: session-gate.sh end
  - AUDIO: 326 capture streams &lt;span class="o"&gt;(&lt;/span&gt;expect 1&lt;span class="o"&gt;)&lt;/span&gt;
  - TMP: capture-live 38797312KB
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One thing about the shape. The checks are fail-soft and fast; the gate is not there to stop a session from ending, it is there to make the next session's first screen tell the truth.&lt;/p&gt;

&lt;p&gt;What I would generalise: any resource a session can leave behind needs a counter somewhere, and the session boundary is the cheapest place to read it. The count that told the truth that day was capture streams, 326 of them, and that count now runs at every session end, whether or not the session remembers to.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>agents</category>
      <category>devops</category>
      <category>automation</category>
    </item>
    <item>
      <title>The runner deleted what it told me to rescue</title>
      <dc:creator>Alkis Yuv</dc:creator>
      <pubDate>Sun, 23 Aug 2026 21:58:07 +0000</pubDate>
      <link>https://dev.to/alkisyuv/the-runner-deleted-what-it-told-me-to-rescue-5g96</link>
      <guid>https://dev.to/alkisyuv/the-runner-deleted-what-it-told-me-to-rescue-5g96</guid>
      <description>&lt;p&gt;One morning my night report said the same thing about two projects, six times over:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;- work is committed but the push failed, manual push needed&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I went looking for the work. There wasn't any. The children had died in the first second of their lives, a harness binary had moved the day before and was no longer on the path, and the runner had reported a push failure for sessions that never started. The next night the children ran properly, committed real work in four projects, and the runner deleted all of it. Same message.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://dev.yuv.run/writing/while-i-sleep/" rel="noopener noreferrer"&gt;The night job&lt;/a&gt; had just gained a merge-request flow. Each child works in a throwaway worktree on a per-night branch; when it commits, the runner pushes the branch and opens a merge request for my morning. Two defects were stacked under that one sentence, and the second is the one worth the essay.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;child, in a throwaway worktree   runner
  commits on the night branch
           │
           ▼
                                 did it commit? compare with upstream
                                 ✗ fresh branch, no upstream: every
                                   child reads as "has unpushed work"
                                 push, from the worktree
                                 ✗ push URL disabled there, by design
                                 report: push failed, manual push needed
                                 cleanup: remove worktree, delete branch
                                 ✗ the only ref to the commits is gone
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first was the test for "did the child commit anything". It compared the branch against its upstream. A fresh branch has no upstream, the lookup failed, the fallback value read as "has unpushed work", and from then on every child, working or dead, looked identical to the runner. A proxy for the fact, not the fact.&lt;/p&gt;

&lt;p&gt;The second was the push itself. My worktrees have their push URL disabled by design, it is the rail that guarantees an unattended agent can never push, so the push ran from exactly the place that cannot push. It failed every night it was tried. The report said manual push needed. And then the cleanup step, which had never been told about any of this, removed the worktree and deleted the branch, the only reference to the night's commits. The runner destroyed the thing it had just asked me to save, and the morning report, which files a push failure under things I fix by hand after coffee, gave no sign there was nothing left to push.&lt;/p&gt;

&lt;p&gt;What saved the work is git's habit of keeping unreferenced objects around until a garbage collection. The rescue is three commands, and the comment is the important one:&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 fsck &lt;span class="nt"&gt;--no-reflogs&lt;/span&gt; &lt;span class="nt"&gt;--lost-found&lt;/span&gt; | &lt;span class="nb"&gt;grep &lt;/span&gt;commit
dangling commit 3f9c2a1d7e...
dangling commit 8b07d4c5a2...
&lt;span class="nv"&gt;$ &lt;/span&gt;git log &lt;span class="nt"&gt;-1&lt;/span&gt; &lt;span class="nt"&gt;--oneline&lt;/span&gt; 3f9c2a1
3f9c2a1 drain: align the retry helper with the new &lt;span class="nb"&gt;timeout &lt;/span&gt;API
&lt;span class="nv"&gt;$ &lt;/span&gt;git branch rescue/2026-08-21 3f9c2a1      &lt;span class="c"&gt;# a ref, nothing else&lt;/span&gt;
&lt;span class="c"&gt;# do NOT gc, prune or worktree-prune first: unreachable is what gc&lt;/span&gt;
&lt;span class="c"&gt;# deletes&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Four repos came back that way the same night. A fifth had two orphans the sweep missed, found by hand while writing this; one was five days old, and a later merge message had described it as already on main.&lt;/p&gt;

&lt;p&gt;The fix is three changes, and each is a version of the same rule: decide from what verifiably exists, never from what a message says.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;&lt;span class="gd"&gt;-# did the child commit? a proxy: compare the branch with its upstream
-n=$(git rev-list --count "$b@{u}..$b" 2&amp;gt;/dev/null || echo 1)
&lt;/span&gt;&lt;span class="gi"&gt;+# did the child commit? the fact: HEAD moved while the child ran
+pre=$(git rev-parse HEAD)
+run_child
+post=$(git rev-parse HEAD)
+[ "$post" != "$pre" ] &amp;amp;&amp;amp; committed=1
&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="gd"&gt;-# cleanup: always
-git worktree remove "$wt" &amp;amp;&amp;amp; git branch -D "$b"
&lt;/span&gt;&lt;span class="gi"&gt;+# cleanup: only when the remote verifiably holds every commit
+n=$(git rev-list --count "$b@{u}..$b" 2&amp;gt;/dev/null || echo 1)
+[ "$n" -eq 0 ] &amp;amp;&amp;amp; git worktree remove "$wt" &amp;amp;&amp;amp; git branch -D "$b"
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Outcome is measured as HEAD movement during the child's run. A child that dies within thirty seconds with no commits is reported as exactly that, with the last two lines of its own output quoted in the report, and it gets no more rounds that night; a real session takes minutes even when it finds nothing to do. The push goes from the parent checkout, where pushing is allowed. And the branch is deleted only when the count of commits the remote lacks is zero, or the commits never existed. "Push failed" now means a commit exists and the remote does not have it, which is the only thing it ever should have meant.&lt;/p&gt;

&lt;p&gt;The lesson generalises past git. Every night produces a report, and I had been treating the report as the state. It is a claim about the state, made by the same program that made the mistake.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;- The destructive step in any automation has to be conditioned on the verified success, not the reported one.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;And the two failures that look alike from outside, never started and started-then-failed, need different words, because the operator does different things for each. I had &lt;a href="https://dev.yuv.run/writing/rules-that-survive/" rel="noopener noreferrer"&gt;written that rule down&lt;/a&gt; for agents: re-verify before you repeat, assert from the primary source. It applies to shell scripts just as well, and they are even less likely to feel the tell.&lt;/p&gt;

</description>
      <category>git</category>
      <category>automation</category>
      <category>ai</category>
      <category>devops</category>
    </item>
    <item>
      <title>One directory is the task manager my agents share</title>
      <dc:creator>Alkis Yuv</dc:creator>
      <pubDate>Sun, 23 Aug 2026 21:57:31 +0000</pubDate>
      <link>https://dev.to/alkisyuv/one-directory-is-the-task-manager-my-agents-share-4ecc</link>
      <guid>https://dev.to/alkisyuv/one-directory-is-the-task-manager-my-agents-share-4ecc</guid>
      <description>&lt;p&gt;When you run agents across many projects, findings stop respecting project boundaries almost immediately. A session working on one tool discovers a bug in another. A review in one repo produces work for three. My first instinct was the obvious one, let the session go fix it over there. That instinct is how two agents end up editing the same tree, and after &lt;a href="https://dev.yuv.run/writing/claims-and-locks/" rel="noopener noreferrer"&gt;auditing five collisions&lt;/a&gt; I banned it. A session's only sanctioned write into another project is a queue entry.&lt;/p&gt;

&lt;p&gt;The queue is embarrassingly low-tech, one directory per target, one markdown file per task, a status line in the front matter: &lt;code&gt;pending&lt;/code&gt;, &lt;code&gt;taken&lt;/code&gt;, &lt;code&gt;done&lt;/code&gt;, &lt;code&gt;dropped&lt;/code&gt;. No database, no integration, and the only board is a one-page summary generated from the files themselves. Files stay put forever as history, and the receiving project's next session gets offered its pending entries automatically when it starts, take, defer, or drop, and the answer is written into the entry so nothing ever asks twice.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;handovers/
├── board.md                  generated from the entries, one page
├── site/
│   ├── 004-syndication.md    status: done
│   └── 007-display-shapes.md status: pending
└── tooling/
    └── 012-retry-helper.md   status: taken
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The lifecycle is four states and two side doors:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;              take                       evidence pasted
 pending ──────────────▶ taken ──────────────────────────▶ done
   │  ▲                    │
   │  │ date reached       └──▶ dropped, reason written into the entry
   │  │
   ▼  │
 snoozed, at most a week       severity: risk ignores the snooze and
                               resurfaces every session until dealt with
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A complete entry fits on one screen:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;done&lt;/span&gt;
&lt;span class="na"&gt;severity&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;normal&lt;/span&gt;
&lt;span class="na"&gt;created&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;2026-08-09&lt;/span&gt;
&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="gh"&gt;# Align the retry helper with the new timeout API&lt;/span&gt;

The ask: the helper still passes an option the API dropped in v3.
Update the call sites and run the suite.
Context: the failing CI run, and the changelog entry that dropped it.
Acceptance: &lt;span class="sb"&gt;`npm test`&lt;/span&gt; exits 0 with the retry cases green.
Evidence: "12 passed, 0 failed", pasted by the taker, 2026-08-10.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What makes it work is not the format though, it's three authoring rules that came from watching it fail.&lt;/p&gt;

&lt;p&gt;Every entry carries an acceptance test. The body is written so the receiving session needs nothing else, the ask, the context links, and how to know it's done. Entries that skipped this read like riddles a week later, and riddles get dropped.&lt;/p&gt;

&lt;p&gt;Done requires evidence. One pasted line showing the acceptance test passing.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;- An assertion without evidence is not done, it's a hope with a status field.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This rule came directly from catching claims of finished work that a thirty-second check would have disproven.&lt;/p&gt;

&lt;p&gt;And absence claims name what was checked. An entry once justified itself with "no record says this value is deliberate" while the record said exactly that, one file over. I ruled on a false premise that day. Since then, "nothing documents X" is only writable alongside the list of places you looked.&lt;/p&gt;

&lt;p&gt;There is also a small vocabulary for time and urgency, a &lt;code&gt;snooze&lt;/code&gt; field that hides an entry until a date, hard-capped at a week because my setup changes too fast for longer parking, and a &lt;code&gt;risk&lt;/code&gt; severity that ignores snoozing entirely and resurfaces every session until someone deals with it.&lt;/p&gt;

&lt;p&gt;The part I like most, the same queue serves humans and machines. &lt;a href="https://dev.yuv.run/writing/while-i-sleep/" rel="noopener noreferrer"&gt;My nightly automation&lt;/a&gt; drains the same entries my interactive sessions do, skips the ones marked as needing me, and flips the same statuses with the same evidence rule. One protocol, no translation layer. For coordination between agents, I keep finding that a directory of honest text files beats anything cleverer I've tried.&lt;/p&gt;

&lt;p&gt;The queue's best story is not in this piece though. It's &lt;a href="https://dev.yuv.run/writing/night-shift/" rel="noopener noreferrer"&gt;the night my agents built the same feature twice&lt;/a&gt;, where every file told the truth and I was the part that didn't.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>agents</category>
      <category>automation</category>
      <category>workflow</category>
    </item>
    <item>
      <title>Rules my agents actually follow</title>
      <dc:creator>Alkis Yuv</dc:creator>
      <pubDate>Sun, 23 Aug 2026 21:56:55 +0000</pubDate>
      <link>https://dev.to/alkisyuv/rules-my-agents-actually-follow-24ed</link>
      <guid>https://dev.to/alkisyuv/rules-my-agents-actually-follow-24ed</guid>
      <description>&lt;p&gt;I once measured how long a written rule survives a fresh agent session: 67 minutes. The rule was recorded right after one session swept another session's half-finished files into its commit, and it was plain:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;- Stage explicit paths only, never &lt;code&gt;git add -A&lt;/code&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The next session had never lived the incident, read the rule as advice, and did the thing anyway.&lt;/p&gt;

&lt;p&gt;Everyone who works with coding agents ends up keeping some kind of mistakes file, the agent breaks something, you write down what happened, and you hope the note prevents a repeat. I kept one too. What the 67 minutes taught me is that the file is the easy third of the problem. The other two thirds are getting the rule enforced, and keeping the record small enough that anyone, human or agent, still reads it.&lt;/p&gt;

&lt;p&gt;A written rule does not survive on its own, that is the honest part. Agents repeat mistakes while remembering the rule, you can watch them acknowledge it and do the thing anyway. Prose loses the fight against a model's priors. So my decision records stopped being prose and became entries with a fixed anatomy. This is the whole shape, using that staging rule as the worked example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gu"&gt;## D-035 — 2026-08-02 — Stage explicit paths only, never add-all&lt;/span&gt;
&lt;span class="gs"&gt;**Decision:**&lt;/span&gt; one imperative sentence, no hedging.
&lt;span class="gs"&gt;**Why:**&lt;/span&gt; written for a stranger with no transcript.
&lt;span class="gs"&gt;**Invariant:**&lt;/span&gt; the line that must stay true for the rule to hold.
&lt;span class="gs"&gt;**Boundary:**&lt;/span&gt; the condition under which this rule stops applying.
&lt;span class="gs"&gt;**Rung:**&lt;/span&gt; law or practice, how hard it binds.
&lt;span class="gs"&gt;**Verify:**&lt;/span&gt; &lt;span class="sb"&gt;`grep -rn "git add -A" hooks/ scripts/`&lt;/span&gt; → nothing
&lt;span class="gs"&gt;**Scope:**&lt;/span&gt; which projects the entry covers.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The two fields that changed everything are the least glamorous ones.&lt;/p&gt;

&lt;p&gt;The first is &lt;em&gt;Why&lt;/em&gt;, written for a stranger with no transcript. Not "as discussed", not a summary of a chat that no longer exists. If the reason can't stand alone, the decision will be relitigated by the next session and probably lost.&lt;/p&gt;

&lt;p&gt;The second is &lt;em&gt;Verify&lt;/em&gt;, a runnable command with an expected output. A rule you can only read is checked by luck. A rule you can execute is checked by machines, on schedule, forever. When one of my entries drifted from reality, the audit flagged it as a failing check and the fix was a one-line commit. Decay surfaced as a finding instead of a surprise. The auditor is the one piece of this system I made public, &lt;a href="https://github.com/triartleet/etymd" rel="noopener noreferrer"&gt;etymd&lt;/a&gt;, it reads the claims your instruction files make and checks them against the actual repo, &lt;code&gt;npx etymd audit&lt;/code&gt; and you have it.&lt;/p&gt;

&lt;p&gt;Between the mistakes file and the record there is a path, and I'd argue this path is the actual system. An incident gets noted where it happened, cheap and unstructured. Recurrence makes it countable. Only a countable pattern graduates into the record, with an invariant and a verify line, and only recorded rules get wired into hooks that block instead of advise.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;incident, noted where it happened      cheap, unstructured
        │  happens again
        ▼
countable pattern                      a count, not a feeling
        │  graduates
        ▼
record entry: invariant + verify       law
        │  wired into
        ▼
hook that blocks instead of advising   enforcement
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Skip the middle and you get rules nobody believes. Skip the end and you get beliefs nobody follows.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;- Evidence, then law, then enforcement.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;And then there is bloat, which in the agent era is the default outcome. Models write more than anyone reads, records grow faster than they bind, and a 3000-word decision document is just prose with a fancier name. My counterweights are all mechanical. Index lines have a character budget and a hook rejects the commit that exceeds it, I have watched it reject my own agent mid-task, which is exactly the point. Files have word budgets checked by an audit. A fact lives in exactly one document and the same decision recorded twice is treated as a defect, because two copies always drift.&lt;/p&gt;

&lt;p&gt;The anatomy itself is the quiet anti-bloat device though. Seven short fields leave an essay nowhere to hide.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>agents</category>
      <category>git</category>
      <category>bestpractices</category>
    </item>
    <item>
      <title>My night shift built the feature, and my day shift didn't notice</title>
      <dc:creator>Alkis Yuv</dc:creator>
      <pubDate>Sun, 23 Aug 2026 21:56:19 +0000</pubDate>
      <link>https://dev.to/alkisyuv/my-night-shift-built-the-feature-and-my-day-shift-didnt-notice-2p0l</link>
      <guid>https://dev.to/alkisyuv/my-night-shift-built-the-feature-and-my-day-shift-didnt-notice-2p0l</guid>
      <description>&lt;p&gt;I have &lt;a href="https://dev.yuv.run/writing/the-queue/" rel="noopener noreferrer"&gt;a queue where cross-project work waits&lt;/a&gt;, and &lt;a href="https://dev.yuv.run/writing/while-i-sleep/" rel="noopener noreferrer"&gt;a nightly job that drains it&lt;/a&gt;, one headless agent per project, rails everywhere, rails being the mechanically enforced guardrails that stand in for trust in these setups. Last week an entry sat in that queue: build a writing section for my site. What happened to it over one night taught me more about running agents than the feature itself.&lt;/p&gt;

&lt;p&gt;The night run took the entry properly, flipped its status, built the whole thing on an isolated branch, recorded its design decisions, and then stopped one step short on purpose. Its rails forbid publishing to the live site unattended. It wrote, in its own report, that deploying would bypass the morning review, and left me a one-line instruction for the morning. So far, the system working exactly as designed.&lt;/p&gt;

&lt;p&gt;The morning shift was me, working with an interactive agent. And we built the same feature again, from scratch, on the main branch, and deployed it. Not because anything malfunctioned, but because I had read that queue entry a day earlier, when it still said pending, and I carried that stale picture in my head instead of re-reading the entry it would have taken ten seconds to open. The status file said taken. I never looked. And the night's report, the one carrying that one-line instruction, went unacted on that same morning, which I have to own too. A report no gate forces through your eyes is prose as well.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;           night shift, headless         day shift, me with an agent
 evening   takes the entry: taken
           builds on its own branch
           stops before deploy, by rule
           reports: deploy after review
 morning                                 reads my memory: still pending
                                         rebuilds on main, deploys
 later     ── the two builds meet in review; the merge takes both ──
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here is what I find worth writing down. Every mechanized rail held. The claims kept the two builds from ever touching the same files, the drain's no-publish rule kept the night work reviewable, the branch isolation kept main clean. The only thing that failed was the part that ran on memory instead of mechanism, my own assumption that yesterday's state was still today's.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;- Stored state decays, and the feeling of already knowing is exactly the moment the ten-second check is worth it.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I have that rule written down, of course. Rules in prose decay too, that's the joke, and it lands on me this time. So the fix that outlived the joke is mechanical, as it had to be. The queue now generates &lt;strong&gt;a one-page board&lt;/strong&gt; from the entries, and taken sits as its own section on it, the night's claims in front of the day shift's eyes before any work starts. Built before that day ended, naturally.&lt;/p&gt;

&lt;p&gt;The reconciliation had its own lesson. Two implementations of one feature, one by an unattended agent, one by an agent with me in the loop, and when I reviewed them side by side the night version was better. Cleaner architecture, a sitemap that maintains itself, fewer special cases in the server config. It also missed something only the live deployment surfaced, which the day version had already hit and solved. So the merge took the night's architecture and the day's hard-won fix, and both builds ended up mattering, which is a kinder ending than the duplicated hours deserved.&lt;/p&gt;

&lt;p&gt;If you run agents overnight, my takeaways are three. Give the night shift rails, not trust, it will happily respect rules that are mechanically enforced. Make the morning review a real step, not a formality, mine genuinely picked a winner. And before you start any work, re-read the queue, not your memory of the queue. The entry knows. You only think you do.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>agents</category>
      <category>git</category>
      <category>productivity</category>
    </item>
  </channel>
</rss>
