<?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: Beni</title>
    <description>The latest articles on DEV Community by Beni (@benixbuzz).</description>
    <link>https://dev.to/benixbuzz</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.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3833048%2F4d20ce81-68f2-47a2-859c-3a0086a3d516.png</url>
      <title>DEV Community: Beni</title>
      <link>https://dev.to/benixbuzz</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/benixbuzz"/>
    <language>en</language>
    <item>
      <title>Our Agent's #1 Failure Mode: Thinking</title>
      <dc:creator>Beni</dc:creator>
      <pubDate>Fri, 27 Mar 2026 16:43:11 +0000</pubDate>
      <link>https://dev.to/benixbuzz/our-agents-1-failure-mode-thinking-2bda</link>
      <guid>https://dev.to/benixbuzz/our-agents-1-failure-mode-thinking-2bda</guid>
      <description>&lt;h1&gt;
  
  
  Our Agent's #1 Failure Mode: Thinking
&lt;/h1&gt;

&lt;p&gt;Thirty-three tasks. Four projects. $32.93. Time to read the spreadsheet.&lt;/p&gt;

&lt;p&gt;MissionControl has been running for a week. Quick context if you're just joining: autonomous dev agent. Describe a coding task in Telegram, it spawns a Claude Code session, builds the feature, opens a PR on GitHub. &lt;a href="//post-1-origin-story.md"&gt;Post 1&lt;/a&gt; covered the 16-hour build. Posts 2 through 5 covered the bugs, the trust chain, the architecture, and a task that deployed a full MVP then got marked as failed. All anecdotal. Now there's enough data to stop telling stories and start reading spreadsheets.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Raw Numbers
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Tasks created&lt;/td&gt;
&lt;td&gt;33&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Completed&lt;/td&gt;
&lt;td&gt;12 (36%)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Failed&lt;/td&gt;
&lt;td&gt;19 (58%)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cancelled&lt;/td&gt;
&lt;td&gt;2 (6%)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Total spend&lt;/td&gt;
&lt;td&gt;$32.93&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;36% completion rate. Worse than the 50% reported after 20 tasks. But the raw number lies — it's weighed down by early infrastructure failures that no longer exist. Strip those out and the picture changes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the Money Went
&lt;/h2&gt;

&lt;p&gt;Not all failures are equal. Some cost pennies. One category cost almost $9.&lt;/p&gt;

&lt;h3&gt;
  
  
  "No commits produced" — 5 tasks, $8.88
&lt;/h3&gt;

&lt;p&gt;The real failure mode. Five tasks where Opus ran for its full budget or turn limit and produced zero commits. Tasks #20, #23, #25, #27, #29 — all greenfield builds ("Build a full-stack...") on $2 budgets.&lt;/p&gt;

&lt;p&gt;The pattern is consistent: Opus starts by reading the entire codebase. Then it plans. Then it plans more. Explores alternative approaches. Considers edge cases it will never hit. By the time it's ready to write code, the budget is gone.&lt;/p&gt;

&lt;p&gt;$8.88 burned on thinking. Not a single line committed.&lt;/p&gt;

&lt;h3&gt;
  
  
  API and infra failures — 10 tasks, $0.69
&lt;/h3&gt;

&lt;p&gt;Ten tasks failed on infrastructure issues — all fixed since. Anthropic API 500s during early testing (4 tasks, $0.69). Missing &lt;code&gt;sudo&lt;/code&gt;, stale OAuth tokens, missing worker user (6 tasks, $0). Resolved in the first week. Noise in the data now.&lt;/p&gt;

&lt;h3&gt;
  
  
  Timeout — 1 task
&lt;/h3&gt;

&lt;p&gt;Default timeout was too short for a full-stack build on a 2-core box. Bumped it. Hasn't recurred.&lt;/p&gt;

&lt;h3&gt;
  
  
  CLI quirk — 1 task
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;--print&lt;/code&gt; combined with &lt;code&gt;--output-format=stream-json&lt;/code&gt; silently requires &lt;code&gt;--verbose&lt;/code&gt;. Without it, the CLI exits 1 with no useful error. Fixed in &lt;code&gt;worker.ts&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Funnel
&lt;/h3&gt;

&lt;p&gt;Signal separated from noise:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;33 total tasks
 - 10 infra/API failures (fixed, no longer relevant)
 -  2 cancelled
 -  1 timeout (fixed)
 -  1 CLI quirk (fixed)
 = 19 real attempts
 - 12 completed
 -  5 "no commits" (the actual problem)
 -  2 other failures
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Strip the noise: roughly 63% on real attempts. Not bad for an autonomous agent with no human in the loop. But 5 tasks and $8.88 wasted on overthinking — that's the leak.&lt;/p&gt;

&lt;h2&gt;
  
  
  Model Economics
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Model&lt;/th&gt;
&lt;th&gt;Tasks&lt;/th&gt;
&lt;th&gt;Cost&lt;/th&gt;
&lt;th&gt;Avg/Task&lt;/th&gt;
&lt;th&gt;Raw Success&lt;/th&gt;
&lt;th&gt;Adjusted&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Opus&lt;/td&gt;
&lt;td&gt;30&lt;/td&gt;
&lt;td&gt;$30.65&lt;/td&gt;
&lt;td&gt;$1.02&lt;/td&gt;
&lt;td&gt;30% (9/30)&lt;/td&gt;
&lt;td&gt;50% (9/18)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sonnet&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;$2.28&lt;/td&gt;
&lt;td&gt;$0.76&lt;/td&gt;
&lt;td&gt;100% (3/3)&lt;/td&gt;
&lt;td&gt;100% (3/3)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Three data points isn't a sample size. But the pattern is worth noting.&lt;/p&gt;

&lt;p&gt;Opus's failure mode is overthinking. Reads everything, considers everything, plans extensively. On a constrained budget, that means it runs out of money before it writes code. On greenfield builds — where the codebase is small and the task is "just build it" — this is exactly wrong.&lt;/p&gt;

&lt;p&gt;Sonnet's strength is mechanical execution. Clear task, does the task. No exploration spirals. No alternative-architecture tangents. Three tasks, three completions, $0.76 average.&lt;/p&gt;

&lt;p&gt;This isn't "Sonnet is better." It's &lt;strong&gt;match the model to the task shape&lt;/strong&gt;. Opus for complex modifications to large codebases where understanding context matters. Sonnet for greenfield builds and mechanical fixes where the path is clear.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three Changes We Made
&lt;/h2&gt;

&lt;p&gt;The data pointed to three specific interventions. Shipped all three before starting the next batch.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Doubled All Budgets
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Parameter&lt;/th&gt;
&lt;th&gt;Old&lt;/th&gt;
&lt;th&gt;New&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Default task budget&lt;/td&gt;
&lt;td&gt;$5&lt;/td&gt;
&lt;td&gt;$10&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Max task budget&lt;/td&gt;
&lt;td&gt;$10&lt;/td&gt;
&lt;td&gt;$20&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Daily budget cap&lt;/td&gt;
&lt;td&gt;$50&lt;/td&gt;
&lt;td&gt;$100&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The hypothesis: "no commits produced" isn't an intelligence failure — it's a budget failure. Opus needs room to think &lt;strong&gt;and&lt;/strong&gt; build. At $2, it can do one or the other. At $4-10, it can do both.&lt;/p&gt;

&lt;p&gt;This is a bet. If doubling budgets converts those five failures into completions, the ROI is obvious — spending $4 to get working code beats spending $2 to get nothing. If it doesn't, we have a deeper problem that money won't fix.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Two-Phase Reviews
&lt;/h3&gt;

&lt;p&gt;Single-phase reviews were inconsistent. Task #33 came back with "Done" and no detail. Task #31 found a real bug. Same prompt, different quality. Split analysis from execution.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Phase 1 — Opus analyzes.&lt;/strong&gt; Read-only access. Reviews the PR diff against a structured checklist: logic errors, security, styling, imports, TypeScript compliance. Outputs a machine-readable verdict:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!-- REVIEW_VERDICT {"approved": false, "issues": [
  "src/components/VotingPanel.tsx:42 — duplicate accent color logic",
  "src/components/Icon.tsx — missing style?: CSSProperties prop"
]} --&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Budget: $1.50. Model: Opus. Tools: read-only (Bash, Read, Glob, Grep).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Phase 2 — Sonnet fixes.&lt;/strong&gt; If Phase 1 finds issues, a child task is auto-created. Sonnet gets the issue list, fixes each one, runs &lt;code&gt;tsc --noEmit&lt;/code&gt; and &lt;code&gt;npm run build&lt;/code&gt;, commits, and pushes.&lt;/p&gt;

&lt;p&gt;Budget: $1.00. Model: Sonnet. Tools: full access.&lt;/p&gt;

&lt;p&gt;Already caught real bugs in production PRs. The duplicate accent color in VotingPanel would have shipped. The missing &lt;code&gt;style&lt;/code&gt; prop on icon components would have caused runtime issues in any consumer passing inline styles. Total review cost: $2.50 for analysis plus fixes — cheaper than a single Opus task that might or might not find anything.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Commit-Early Culture
&lt;/h3&gt;

&lt;p&gt;The lead dev prompt now emphasizes incremental commits over perfect final PRs. Old pattern: plan everything, build everything, commit once at the end. Budget runs out before that final commit — zero output.&lt;/p&gt;

&lt;p&gt;New pattern: commit after each meaningful unit of work. A partial feature with three commits is infinitely more valuable than a complete feature with zero commits.&lt;/p&gt;

&lt;p&gt;Can't force the model to commit early — it's guidance, not enforcement. But combined with higher budgets, the goal is to shift the failure mode from "zero output" to "partial output." Partial output can be retried. Zero output is wasted money.&lt;/p&gt;

&lt;h2&gt;
  
  
  What We're Watching
&lt;/h2&gt;

&lt;p&gt;Batch 2 starts now. Three questions:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does doubling budgets convert failures?&lt;/strong&gt; If the five "no commits" tasks would have succeeded at $4-10, the completion rate will show it. If they still fail at higher budgets, the problem is in the prompt or the task shape, not the money.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does two-phase review scale?&lt;/strong&gt; Three review tasks isn't a pattern. Need 15-20 to know if the structured verdict format is reliable and if Sonnet consistently fixes what Opus finds.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can we auto-calibrate?&lt;/strong&gt; A greenfield build and a one-line config change shouldn't share a budget. Considering scope-size flags — &lt;code&gt;small&lt;/code&gt;, &lt;code&gt;medium&lt;/code&gt;, &lt;code&gt;large&lt;/code&gt; — that auto-set budget and timeout based on expected complexity. Not built yet. Waiting for more data to set the thresholds.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Takeaway
&lt;/h2&gt;

&lt;p&gt;Thirty-three tasks taught us more than building the system did. The system works. The question was always "how well?" Now we know: ~63% on real attempts, with a clear #1 failure mode we can measure and attack.&lt;/p&gt;

&lt;p&gt;Not crashes. Not bugs. Not infrastructure. The agent thinks too much and ships nothing. Solvable problem. Higher budgets give it room. Two-phase reviews separate thinking from doing. Commit-early guidance reduces the blast radius of a timeout.&lt;/p&gt;

&lt;p&gt;$32.93 for 33 tasks and a clear roadmap for improvement. Not bad.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Next up: batch 2 results — did the changes work?&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>agents</category>
      <category>typescript</category>
      <category>devops</category>
    </item>
    <item>
      <title>Error!! Failed Successfully</title>
      <dc:creator>Beni</dc:creator>
      <pubDate>Thu, 26 Mar 2026 16:07:35 +0000</pubDate>
      <link>https://dev.to/benixbuzz/error-failed-successfully-mod</link>
      <guid>https://dev.to/benixbuzz/error-failed-successfully-mod</guid>
      <description>&lt;h1&gt;
  
  
  Error!! Failed Successfully
&lt;/h1&gt;

&lt;blockquote&gt;
&lt;p&gt;Part 5 of the MissionControl series. MissionControl is a Telegram bot that takes coding tasks in plain English, spawns a Claude Code session, and ships pull requests autonomously. &lt;a href="https://dev.to/blog/post-4-what-we-ship"&gt;Post 4&lt;/a&gt; covered the safety stack and architecture after the first 48 hours.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The Notification
&lt;/h2&gt;

&lt;p&gt;12:16 AM UTC. Telegram notification:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Task #23 failed: No commits produced despite success claim
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Opened the Vercel URL anyway. The app loaded. Login screen with four demo users. Picked "Jordan," dragged some flight options around, watched the Borda count scores update in real time. Fully functional group travel planner. Mobile responsive. TypeScript strict. Deployed to production.&lt;/p&gt;

&lt;p&gt;The database said failed. The app said otherwise.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Task
&lt;/h2&gt;

&lt;p&gt;Task #23 was a stress test. After the safety stack work in Post 4 — budget caps, timeouts, commit verification — the question was simple: could the bot take a complex, multi-feature MVP brief and ship it end-to-end? No human intervention, no hand-holding, no retries.&lt;/p&gt;

&lt;p&gt;The prompt: a detailed spec for a group travel planning app. React + Tailwind, deployed to Vercel, with trip creation, drag-to-rank voting across multiple categories (flights, lodging, activities), four demo users with different roles, pre-seeded data, and an admin/member permission split. About 250 words — a product brief you'd hand a junior developer on day one.&lt;/p&gt;

&lt;p&gt;Full task prompt&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Build a group travel planning web app (React + Tailwind, deploy to Vercel).

Core concept: One person creates a trip, invites others. The lead planner
adds options for each category; all members vote via drag-to-rank. Results
are visible to everyone in real time.

MVP Sections:
- Flights
- Lodging (hotel or Airbnb)
- Activities &amp;amp;amp; Dining

Each section lets the planner add multiple options (name, price, link,
image). Members drag to rank them. The top-ranked option is highlighted
as the group pick.

Demo Setup:
- No auth needed — login screen with 4 selectable demo users
- User 1: "Jordan" (Lead Planner/Admin)
- Users 2-4: "Alex," "Sam," "Priya" (Members)
- Pre-filled trip: "Miami Trip — July 4th Weekend" with 2-3 options
  per section and partial votes already cast
- Switching users changes your voting perspective and UI role

Admin view: Add/edit/delete options, see full vote breakdown
Member view: Drag-to-rank only, see live results after voting

Key Features:
- Trip dashboard with progress (how many people have voted per section)
- Drag-to-rank voting UI per section per user
- Live results view showing ranked outcomes across all voters
- Mobile-first, responsive layout
- Invite link UI (non-functional, just show a copyable link)

Tech: React, Tailwind CSS, in-memory state (no backend needed for MVP).
Seed all demo data on load. Deploy to Vercel.

Start with the full file/folder structure, then build it completely —
no placeholders. It must be fully functional and deployable before stopping.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;One message. No follow-ups. Ship it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three Attempts
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Attempt 1&lt;/strong&gt; ran for 18 minutes, then went silent. Exit code: &lt;code&gt;null&lt;/code&gt;. The CLI process never terminated cleanly — stopped producing output and sat there until the timeout killed it. No commits, no progress, nothing salvageable. Classic Opus-on-a-2-core-box behavior: the model spent so long planning it exceeded the soft timeout before writing a single file.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Attempt 2&lt;/strong&gt; lasted 8 minutes before catching a &lt;code&gt;SIGTERM&lt;/code&gt; (exit code 143). Our own timeout enforcement killed it mid-work. The bot was making progress this time, but not fast enough. Nothing committed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Attempt 3&lt;/strong&gt; — 9 minutes and 5 seconds. 49 turns. $1.56.&lt;/p&gt;

&lt;p&gt;Clean exit. Code zero.&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;"subtype"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"success"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"duration_ms"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;543323&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"num_turns"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;49&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"total_cost_usd"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;1.56&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"result"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Done. GroupTrip MVP deployed and live at https://grouptrip-work.vercel.app"&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;Commit &lt;code&gt;223a642&lt;/code&gt; ships &lt;code&gt;feat: build group travel planning web app MVP&lt;/code&gt;. Twenty-one files. 7,466 lines of code.&lt;/p&gt;

&lt;h2&gt;
  
  
  What It Built
&lt;/h2&gt;

&lt;p&gt;The bot made every architectural decision on its own. No guidance on which drag-and-drop library to use, how to structure state, or what scoring algorithm to implement.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;@dnd-kit for drag-and-drop.&lt;/strong&gt; Not react-beautiful-dnd (deprecated), not react-dnd (heavier). The right call. Pulled in &lt;code&gt;@dnd-kit/core&lt;/code&gt;, &lt;code&gt;@dnd-kit/sortable&lt;/code&gt;, and &lt;code&gt;@dnd-kit/utilities&lt;/code&gt;, then built a clean &lt;code&gt;SortableItem&lt;/code&gt; component wrapping each votable option.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;React Context over Redux.&lt;/strong&gt; For an in-memory MVP with no backend, this is correct. A global store with &lt;code&gt;useContext&lt;/code&gt; and &lt;code&gt;structuredClone&lt;/code&gt; for immutable updates. No unnecessary dependencies, no boilerplate.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Borda count scoring.&lt;/strong&gt; The brief said "drag to rank" and "top-ranked option highlighted." The bot decided to use Borda count — a ranked-choice voting algorithm where each position gets a score (first place = N points, second = N-1, and so on). Calculated scores across all voters and surfaced the winner per category. Nobody asked for Borda count. The bot read "drag to rank" and picked an appropriate algorithm on its own.&lt;/p&gt;

&lt;p&gt;The file structure:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;src/
  components/
    CategorySection.tsx    # Per-category voting container
    Dashboard.tsx          # Trip overview + progress
    LoginScreen.tsx        # Demo user picker
    SortableItem.tsx       # dnd-kit wrapper
    VotingPanel.tsx        # Drag-to-rank UI
    ResultsPanel.tsx       # Borda count results
  lib/
    seed-data.ts           # Pre-filled Miami Trip
    store.tsx              # React Context state
  types/
    index.ts               # Shared TypeScript types
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Clean separation. Types in one file, seed data isolated, state in its own module, seven focused components each doing one thing. The seed data included a pre-filled "Miami Trip" with three categories, 2-3 options each, and partial votes already cast — exactly as specified. Switching between demo users changes the perspective: Jordan sees admin controls, the others see the voting interface.&lt;/p&gt;

&lt;p&gt;Code quality: 7.5 out of 10. TypeScript strict mode, no &lt;code&gt;any&lt;/code&gt; types, proper immutability with &lt;code&gt;structuredClone&lt;/code&gt;, clean component boundaries. A few things a senior dev would tighten — some components could be split further, the store could use a reducer pattern instead of raw setState — but nothing that would block a code review. Ship it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The False Failure
&lt;/h2&gt;

&lt;p&gt;The app works. It's deployed. The code is clean. Why did the database say "failed"?&lt;/p&gt;

&lt;p&gt;The sequence in the runner's &lt;code&gt;finally&lt;/code&gt; block:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;CLI exited with code 0. Reported success.&lt;/li&gt;
&lt;li&gt;Runner checks for commits: &lt;code&gt;git rev-list --count main..HEAD&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;At this exact moment, the working tree was dirty — Vercel CLI had written deployment cache files the bot didn't commit.&lt;/li&gt;
&lt;li&gt;Auto-rescue logic detected dirty state and ran &lt;code&gt;git add -A &amp;amp;&amp;amp; git commit -m 'WIP: auto-rescue'&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;But the commit count check had &lt;strong&gt;already run&lt;/strong&gt; against the branch before the rescue commit landed.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Race condition. The runner checked for commits, found zero (commit &lt;code&gt;223a642&lt;/code&gt; was there, but the branch comparison ran against the wrong ref), then the rescue committed after the check. Error message: "no commits produced." Reality: commit &lt;code&gt;223a642&lt;/code&gt; had 7,466 lines of working code. The Vercel deploy had already completed inside the CLI session. The app was live at &lt;code&gt;grouptrip-work.vercel.app&lt;/code&gt; before the runner even started its verification.&lt;/p&gt;

&lt;p&gt;Failed successfully.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Fix
&lt;/h2&gt;

&lt;p&gt;The bot was doing things in the wrong order. The verification had a blind spot.&lt;/p&gt;

&lt;p&gt;Commit &lt;code&gt;f277b4b&lt;/code&gt; patched four things in the bot's prompt template:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Build verification.&lt;/strong&gt; Added &lt;code&gt;npm run build&lt;/code&gt; as a required step after TypeScript checking and before committing. The bot was already running &lt;code&gt;tsc --noEmit&lt;/code&gt;, but a passing type check doesn't guarantee a passing build.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Vercel preview deploy.&lt;/strong&gt; If the project has a &lt;code&gt;.vercel/&lt;/code&gt; directory or &lt;code&gt;vercel.json&lt;/code&gt;, the bot now runs &lt;code&gt;vercel --yes&lt;/code&gt; (not &lt;code&gt;--prod&lt;/code&gt;) as a step. Preview deploys, not production. The human decides when to promote.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"Never merge to main" guardrail.&lt;/strong&gt; Explicit instruction in the prompt: work on the feature branch, push the branch, the reviewer merges. The bot was already doing this. Making it explicit prevents drift.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"Never git add -A" guardrail.&lt;/strong&gt; Stage specific files with &lt;code&gt;git add &amp;lt;file&amp;gt;&lt;/code&gt;. Directly prevents the scenario that caused the false failure. If the Vercel CLI drops cache files in the working tree, the bot won't blindly commit them.&lt;/p&gt;

&lt;p&gt;The bot now follows the same workflow as the human team. Type check, build, commit specific files, push branch, deploy preview. No shortcuts.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Scoreboard
&lt;/h2&gt;

&lt;p&gt;Three attempts across two days:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Attempt&lt;/th&gt;
&lt;th&gt;Duration&lt;/th&gt;
&lt;th&gt;Exit&lt;/th&gt;
&lt;th&gt;Cost&lt;/th&gt;
&lt;th&gt;Result&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;18 min&lt;/td&gt;
&lt;td&gt;null (hung)&lt;/td&gt;
&lt;td&gt;--&lt;/td&gt;
&lt;td&gt;No output&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;8 min&lt;/td&gt;
&lt;td&gt;143 (SIGTERM)&lt;/td&gt;
&lt;td&gt;--&lt;/td&gt;
&lt;td&gt;Killed mid-work&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;9 min&lt;/td&gt;
&lt;td&gt;0 (clean)&lt;/td&gt;
&lt;td&gt;$1.56&lt;/td&gt;
&lt;td&gt;Deployed to production&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Attempt 3: 49 turns, 1.16M cached tokens, 22.5K output tokens against Opus. Total API cost: $1.56.&lt;/p&gt;

&lt;p&gt;What didn't work: Opus on a 2-core server chokes on complex planning. Attempt 1 spent its entire budget thinking. Attempt 2 got killed by our own timeout enforcement before finishing. Two runs wasted — not because the model couldn't do the work, but because the infrastructure couldn't give it enough room to think.&lt;/p&gt;

&lt;p&gt;What worked: when the bot actually got to execute, it made good decisions. Right library for drag-and-drop. Right state management for the scope. Right algorithm for ranked voting. Clean file structure. TypeScript strict. Deployed and functional.&lt;/p&gt;

&lt;p&gt;$1.56 and 9 minutes of compute. An autonomous agent built a production-quality MVP that would take a human developer a full day. The app is live. The code is clean.&lt;/p&gt;

&lt;p&gt;The database was wrong.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Failed successfully.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Next up: &lt;a href="https://dev.to/blog/post-6-data-analysis"&gt;Post 6&lt;/a&gt; — 33 tasks analyzed to find out what actually works, what doesn't, and where the money goes.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>agents</category>
      <category>typescript</category>
      <category>devops</category>
    </item>
    <item>
      <title>What We Actually Ship With MissionControl</title>
      <dc:creator>Beni</dc:creator>
      <pubDate>Thu, 26 Mar 2026 01:46:30 +0000</pubDate>
      <link>https://dev.to/benixbuzz/what-we-actually-ship-with-missioncontrol-1n7n</link>
      <guid>https://dev.to/benixbuzz/what-we-actually-ship-with-missioncontrol-1n7n</guid>
      <description>&lt;h1&gt;
  
  
  What We Actually Ship With MissionControl
&lt;/h1&gt;

&lt;p&gt;Two days. Twenty-one commits. English in, pull requests out.&lt;/p&gt;

&lt;p&gt;If you're joining mid-series: &lt;a href="https://dev.to/blog/post-1-origin-story"&gt;Post 1&lt;/a&gt; covered the 16-hour build — Telegram bot in, pull requests out, ports-and-adapters architecture. Posts 2 and 3 were the bug safari that followed, including a $5.84 task that produced zero useful work and forced a rethink of the entire trust chain. This is what the system looks like after surviving all of that.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Interface
&lt;/h2&gt;

&lt;p&gt;MissionControl runs as a Telegram bot. No web UI. No dashboard. You message it, it does work, it messages you back.&lt;/p&gt;

&lt;p&gt;Every interaction fits in a chat bubble. Send a task from your phone while walking the dog, get a PR link back before you're home. That constraint — everything must fit in a Telegram message — turned out to be a feature, not a limitation.&lt;/p&gt;

&lt;p&gt;The full command set:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;/task &amp;lt;description&amp;gt;&lt;/code&gt; — Queue a task against the default project. The agent picks it up, creates a branch, does the work, pushes, and opens a PR.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/task slug: &amp;lt;description&amp;gt;&lt;/code&gt; — Target a specific project.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/status&lt;/code&gt; — Current running task, queue depth, recent completions.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/cancel &amp;lt;id&amp;gt;&lt;/code&gt; — Kill a running task or remove a queued one.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/retry &amp;lt;id&amp;gt;&lt;/code&gt; — Re-queue a failed or cancelled task.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/logs &amp;lt;id&amp;gt;&lt;/code&gt; — Tail the last 50 lines of a task's execution log.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/budget&lt;/code&gt; — Today's spend, remaining daily budget, per-task breakdown.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/addproject &amp;lt;slug&amp;gt; &amp;lt;path&amp;gt; --github owner/repo&lt;/code&gt; — Register a repo. Auto-chowns &lt;code&gt;.git/&lt;/code&gt; for the worker user.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/create &amp;lt;slug&amp;gt;&lt;/code&gt; — Bootstrap a new project directory, init git, create the GitHub repo, register it.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/rmproject &amp;lt;slug&amp;gt;&lt;/code&gt; — Unregister and clean up.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;No context switching. No browser tabs.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Numbers
&lt;/h2&gt;

&lt;p&gt;Twenty tasks in the first 48 hours across three projects.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Tasks created&lt;/td&gt;
&lt;td&gt;20&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Completed&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Failed&lt;/td&gt;
&lt;td&gt;13&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cancelled&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Running&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Total spend&lt;/td&gt;
&lt;td&gt;$12.49&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Avg cost (completed)&lt;/td&gt;
&lt;td&gt;$2.95&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Most expensive&lt;/td&gt;
&lt;td&gt;$5.84 (Task #19 — did nothing)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cheapest success&lt;/td&gt;
&lt;td&gt;$1.49 (Task #5 — tier enforcement)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;20% completion rate. Looks bad. It's not. Tasks 1 through 4 all failed on the same CLI spawn issue — the zero-stdout bug from Post 2. Four retries of the same failure before we understood it. After the bug fix sprint, the completion rate on new tasks jumped to roughly 50%. The remaining failures: budget timeouts and permission issues on freshly registered projects. Not systemic.&lt;/p&gt;

&lt;p&gt;The number that matters: four completed tasks produced working code, passing builds, and merged PRs. One of them — a fitness trainer dashboard — was a full-stack Next.js app with auth, data visualization, and a PostgreSQL backend. Built autonomously. $2.00.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Safety Stack
&lt;/h2&gt;

&lt;p&gt;Every layer here exists because we shipped without it and something broke.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Budget caps.&lt;/strong&gt; $50/day global. $5 per task default, configurable up to $10. Checked before the task starts and enforced by the CLI's own &lt;code&gt;--max-budget-usd&lt;/code&gt; flag. Task #19 — the $5.84 zero-work disaster from Post 3 — proved that budget enforcement alone isn't enough. You also need to verify the agent actually &lt;strong&gt;produced&lt;/strong&gt; something.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Timeouts.&lt;/strong&gt; 30-minute soft limit, then a 5-minute grace period. Soft limit sends &lt;code&gt;SIGTERM&lt;/code&gt;. Grace lets the agent wrap up and commit. After grace, &lt;code&gt;SIGKILL&lt;/code&gt;. A separate kill timer 60 seconds post-SIGTERM ensures nothing lingers. Opus on a 2-core box analyzing a large codebase can burn 15 minutes just planning. Learned that the hard way.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Orphan cleanup.&lt;/strong&gt; On process restart, any task stuck in &lt;code&gt;running&lt;/code&gt; state gets reset to &lt;code&gt;queued&lt;/code&gt;. Without this, a single PM2 restart freezes the entire queue. Sounds obvious in retrospect. Wasn't obvious at 2 AM.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Commit verification.&lt;/strong&gt; &lt;code&gt;git rev-list --count main..HEAD&lt;/code&gt; — if zero, the task failed. No exceptions. The agent's self-assessment ("I completed the task successfully!") is advisory, not authoritative. We do not trust the agent's opinion of its own work.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Uncommitted work rescue.&lt;/strong&gt; Before any branch cleanup: &lt;code&gt;git status --porcelain&lt;/code&gt;. If dirty, &lt;code&gt;git add -A &amp;amp;&amp;amp; git commit -m 'WIP: auto-rescue'&lt;/code&gt;. Catches work the agent did but didn't commit — timeouts, crashes, the agent forgetting to stage files. Happens more often than expected.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Force checkout fallback.&lt;/strong&gt; The &lt;code&gt;finally&lt;/code&gt; block tries normal checkout first, then force checkout. A dirty working tree from a crashed task can't deadlock the next one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Session isolation.&lt;/strong&gt; &lt;code&gt;--no-session-persistence&lt;/code&gt; on every CLI spawn. Every task starts clean. No stale context, no ghost sessions bleeding between runs.&lt;/p&gt;

&lt;p&gt;Roughly sixty lines of verification and fallback logic. Least interesting code in the project. Most important.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Architecture
&lt;/h2&gt;

&lt;p&gt;Ports and adapters. Same as day one. Three boundaries:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;MessagingPort&lt;/strong&gt; — Telegram today. The interface is &lt;code&gt;sendMessage(chatId, text)&lt;/code&gt;. A Slack adapter would take an afternoon — that's the whole point of the pattern.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;WorkerPort&lt;/strong&gt; — Claude CLI today. Spawns the agent with JSON output, budget caps, tool restrictions. Could be swapped for any agent runtime that accepts a prompt and returns structured output.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;VCSPort&lt;/strong&gt; — GitHub today. Creates PRs, manages branches. Git operations happen locally through a &lt;code&gt;sudo&lt;/code&gt; wrapper that runs everything as the sandboxed worker user.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The core — &lt;code&gt;TaskRunner&lt;/code&gt;, &lt;code&gt;TaskService&lt;/code&gt;, &lt;code&gt;BudgetService&lt;/code&gt; — knows nothing about Telegram, Claude, or GitHub. It processes tasks, enforces budgets, delegates execution. That separation already paid off: changed how the CLI gets spawned twice in two days. Nothing else in the system noticed.&lt;/p&gt;

&lt;p&gt;State lives in SQLite via &lt;code&gt;better-sqlite3&lt;/code&gt;. One file, no server, backed up by PM2's process management. Good enough for a single-operator system. Would need Postgres if this ever went multi-user.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's Next
&lt;/h2&gt;

&lt;p&gt;Three things on the roadmap, in priority order.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Crash recovery.&lt;/strong&gt; If a task gets interrupted mid-work — server reboot, PM2 restart, OOM kill — it gets requeued from scratch. The branch exists with partial commits, but the retry starts a fresh conversation with no memory of what came before. Want to detect partial work on the branch and pass it as context: "Here's what you did before you were interrupted. Continue from commit X." This alone could cut the failure rate in half.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Slack adapter.&lt;/strong&gt; Telegram works for a solo operator. Slack is where teams live. The &lt;code&gt;MessagingPort&lt;/code&gt; interface is already clean — &lt;code&gt;sendMessage&lt;/code&gt; and &lt;code&gt;onCommand&lt;/code&gt; — so a Slack adapter that maps slash commands to the handler interface would open this up to team use without touching the core.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Issue watcher.&lt;/strong&gt; Auto-queue tasks from GitHub issues. Label an issue &lt;code&gt;mc-auto&lt;/code&gt;, MissionControl picks it up, creates a task, links the PR back to the issue. The scaffolding is already in the codebase. Needs a token scope update and it's live.&lt;/p&gt;

&lt;h2&gt;
  
  
  Should This Be Open Source?
&lt;/h2&gt;

&lt;p&gt;Still deciding. The system is opinionated — single operator, Telegram, Claude CLI, GitHub — but the architecture is portable. Swap any layer without touching the core.&lt;/p&gt;

&lt;p&gt;The bugs we found and fixed aren't novel. Stale sessions, permission boundaries, output verification, budget enforcement — every agent builder will hit these. Shipping the fixes as a reference implementation could save other builders the same $5.84 lessons.&lt;/p&gt;

&lt;p&gt;No decision yet. Building something similar? Reach out.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Closing Count
&lt;/h2&gt;

&lt;p&gt;Two days of building. Two days of debugging. Twenty-one commits on main. Twenty tasks processed. Four successful PRs merged. $12.49 spent.&lt;/p&gt;

&lt;p&gt;One system that takes English descriptions from a Telegram message and turns them into branches, commits, and pull requests — with budget caps, timeout enforcement, commit verification, and session isolation.&lt;/p&gt;

&lt;p&gt;It breaks. We fix it. It breaks differently. We fix that too. The difference between "AI agent demo" and "AI agent that ships code" is those sixty lines of verification and fallback logic that nobody shows in the demo.&lt;/p&gt;

&lt;p&gt;MissionControl isn't done. But it works. And it works because of everything that broke.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Next up: &lt;a href="https://dev.to/blog/post-5-failed-successfully"&gt;Post 5&lt;/a&gt; — the bot builds a full MVP, deploys it to production, then tells us it failed.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>agents</category>
      <category>typescript</category>
      <category>devops</category>
    </item>
    <item>
      <title>My AI Agent Spent $5.84 and Did Nothing</title>
      <dc:creator>Beni</dc:creator>
      <pubDate>Tue, 24 Mar 2026 14:17:02 +0000</pubDate>
      <link>https://dev.to/benixbuzz/my-ai-agent-spent-584-and-did-nothing-3kk2</link>
      <guid>https://dev.to/benixbuzz/my-ai-agent-spent-584-and-did-nothing-3kk2</guid>
      <description>&lt;h1&gt;
  
  
  My AI Agent Spent $5.84 and Did Nothing
&lt;/h1&gt;

&lt;p&gt;Give an AI agent a task. It runs for 15 minutes, reports success, bills you $5.84. Click the PR link. GitHub says: "There isn't anything to compare." Zero commits. Zero files changed. Money gone.&lt;/p&gt;

&lt;p&gt;This is the failure mode that matters most when building autonomous agents. Not hallucinations, not bad code, not prompt engineering. The agent does &lt;strong&gt;nothing&lt;/strong&gt;, reports &lt;strong&gt;everything done&lt;/strong&gt;, and the system believes it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Happened
&lt;/h2&gt;

&lt;p&gt;[Post 2] covered six silent-failure bugs from v0.1 — same pattern every time: exit code 0, no actual work. We hardened against that. Then Task #19 proved the hardening wasn't enough.&lt;/p&gt;

&lt;p&gt;The Telegram notification looked fine:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Task #19 completed in 14m 51s
snowcam — Mountain Camera Intelligence Dashboard
Model: opus | Cost: $5.84
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Completed. Fourteen minutes. PR should be ready.&lt;/p&gt;

&lt;p&gt;Clicked the link. GitHub: "main and feature/mc-19-snowcam have identical contents." Double-checked the URL. Refreshed. Checked the branch list. Nothing. $5.84 gone.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Investigation
&lt;/h2&gt;

&lt;p&gt;Task #19 was a retry — same snowcam dashboard prompt that Task #18 had already completed. Task #18 built the app, committed the code, pushed the branch, cost $2.48. Task #19 was queued with the same description, aimed at a different branch.&lt;/p&gt;

&lt;p&gt;The raw CLI output JSON told the story:&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;"num_turns"&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="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"total_cost_usd"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;5.839&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"is_error"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"result"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"The resort data agent confirmed — 50 resorts, all clean TypeScript.
             That file was already incorporated into the build that passed.
             The dashboard is fully built, verified, and pushed."&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;One turn. The agent saw cached context from the previous session and concluded the work was done. Didn't run a single tool. Wrote zero files. Made zero commits. Reported success.&lt;/p&gt;

&lt;p&gt;The model usage confirmed it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;claude-opus-4-6&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;cacheReadInputTokens&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;7,660,816&lt;/span&gt;
  &lt;span class="na"&gt;outputTokens&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;49,373&lt;/span&gt;
  &lt;span class="na"&gt;costUSD&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;$5.83&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;7.6 million cached tokens — the entire previous session. Every file read, every edit, every tool call from Task #18, loaded back via session persistence. The agent saw all that prior work and said "Done." One inference pass. Full price.&lt;/p&gt;

&lt;p&gt;7.6 million tokens of someone else's work. Claimed as its own.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Trust Chain That Failed
&lt;/h2&gt;

&lt;p&gt;The sequence that turned a zero-work session into a "completed" task:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;CLI exits 0&lt;/strong&gt; — no crash, no error, ran successfully from its own perspective&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;JSON says &lt;code&gt;is_error: false&lt;/code&gt;&lt;/strong&gt; — the agent encountered no issues (it just didn't do anything)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Runner parses success&lt;/strong&gt; — &lt;code&gt;code === 0 &amp;amp;&amp;amp; !parsed.is_error&lt;/code&gt; evaluates true, task marked completed&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Push empty branch&lt;/strong&gt; — git pushes a branch with zero new commits&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PR creation fails&lt;/strong&gt; — GitHub notices nothing to compare, but the runner already marked success&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Finally block runs&lt;/strong&gt; — checks out the original branch, would silently discard any uncommitted work&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Every link did exactly what it was supposed to. The bug wasn't in any single step — it was in what we didn't check: did the agent actually produce anything.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Three-Part Fix
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Kill Session Persistence
&lt;/h3&gt;

&lt;p&gt;Root cause: Claude CLI's session persistence. Between tasks, it saved and restored session context. Task #19 resumed Task #18's context and concluded nothing needed doing.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;args&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;-p&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;description&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;--output-format&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;--model&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;--max-turns&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;maxTurns&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;--dangerously-skip-permissions&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;--no-session-persistence&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;// &amp;lt;-- never resume stale sessions&lt;/span&gt;
&lt;span class="p"&gt;];&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every task starts clean. No inherited context. No ghosts from previous runs.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Rescue Uncommitted Work
&lt;/h3&gt;

&lt;p&gt;If the agent did work but didn't commit — crashed mid-edit, hit a timeout, forgot the commit step — rescue it before touching the branch:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;success&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;hasUncommittedChanges&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;project&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;log&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;warn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Claude left uncommitted changes — auto-rescuing&lt;/span&gt;&lt;span class="dl"&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;rescueUncommittedChanges&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;project&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="c1"&gt;// ...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Catches real work left uncommitted. Without this, the force-checkout in the &lt;code&gt;finally&lt;/code&gt; block would destroy it — the same dirty repo bug from Post 2, now handled properly.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Commit Count Verification
&lt;/h3&gt;

&lt;p&gt;The actual gate:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;commitCount&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;getBranchCommitCount&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;project&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;project&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;default_branch&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;commitCount&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;taskService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;markFailed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;No commits produced despite success claim&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;totalCost&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;notifyUser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s2"&gt;`Task #&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; failed: Claude reported success but made no commits\n`&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;
    &lt;span class="s2"&gt;`Cost: $&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;totalCost&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;2&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;getBranchCommitCount&lt;/code&gt; runs &lt;code&gt;git rev-list --count main..HEAD&lt;/code&gt;. Zero commits means the task failed — regardless of what the CLI reported. User gets an honest notification: "Claude said it was done, but it made no commits."&lt;/p&gt;

&lt;p&gt;All three fixes together: Task #19 would have been caught immediately. No stale session to resume. Uncommitted work rescued. Zero-commit branches rejected.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Lesson
&lt;/h2&gt;

&lt;p&gt;$5.84 is cheap tuition.&lt;/p&gt;

&lt;p&gt;The real cost would have come later — 50 tasks a day, empty branches marked "completed," budget burned on phantom work. Dashboard says 100% completion rate. Nothing shipped.&lt;/p&gt;

&lt;p&gt;AI agents are not reliable narrators of their own success. They will report completion when they've done nothing. They will exit clean from a failed state. They will cache-read 7.6 million tokens of prior work and call it their own.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Never trust self-reported success from an AI.&lt;/strong&gt; Verify the artifacts. Count the commits. Check the files. Run the tests. Exit code 0 is evidence the process didn't crash — not evidence of work.&lt;/p&gt;

&lt;p&gt;[Post 4] covers what MissionControl looks like once it stops believing its own agent.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>agents</category>
      <category>typescript</category>
      <category>devops</category>
    </item>
    <item>
      <title>We Accidentally Reinvented SMTP for Claude Code Instances</title>
      <dc:creator>Beni</dc:creator>
      <pubDate>Tue, 24 Mar 2026 12:05:56 +0000</pubDate>
      <link>https://dev.to/benixbuzz/we-accidentally-reinvented-smtp-for-claude-code-instances-4620</link>
      <guid>https://dev.to/benixbuzz/we-accidentally-reinvented-smtp-for-claude-code-instances-4620</guid>
      <description>&lt;p&gt;We didn't plan to build an email system. We planned to copy a file.&lt;/p&gt;

&lt;h2&gt;
  
  
  Day Zero: The Clipboard
&lt;/h2&gt;

&lt;p&gt;Three servers. Up to six concurrent Claude Code instances across them. Same projects, different contexts. One server handles the autonomous dev agent. Another handles product work and competitive intel. A third joined for extra compute.&lt;/p&gt;

&lt;p&gt;The problem is obvious: they can't talk to each other.&lt;/p&gt;

&lt;p&gt;Day one solution: a shared directory. Write a markdown file, SCP it to the other server. The other instance reads it next session.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;scp review-notes.md root@10.0.0.2:~/mailbox/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. That was the entire communication infrastructure. And honestly? It worked. For about two days.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Five Failures That Built the System
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Failure 1: "Did they see this?"&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;You drop a brief. Next session, you have no idea if the other instance read it. Did the human show it to them? Did they skip it? Is the action item in progress or sitting untouched?&lt;/p&gt;

&lt;p&gt;So we added a SQLite inbox. Every file gets registered with a timestamp and a &lt;code&gt;read_at&lt;/code&gt; column. NULL means unread. Not NULL means processed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Failure 2: "What changed since last time?"&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Session context resets every conversation. You can't remember what you saw last time. &lt;code&gt;ls -lat&lt;/code&gt; tells you what's newest, not what's unread.&lt;/p&gt;

&lt;p&gt;So we added a digest command. Query &lt;code&gt;WHERE read_at IS NULL&lt;/code&gt;, read each file, extract action items, mark everything read. One command, full inbox processed.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ClaudeMail Digest — 5 unread

1. [server2 -&amp;gt; all] deploy-css-migration-merged.md
   PR #21 merged, needs production deploy

2. [server2 -&amp;gt; all] cost-model-fitted.md
   281 tasks trained, p75 $0.47, needs wiring into dispatcher
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Failure 3: "Who's online right now?"&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Multiple servers, multiple instances per server. When you send a brief, you don't know if anyone's listening. Are you talking to an empty room?&lt;/p&gt;

&lt;p&gt;So we built a roster. Each instance heartbeats every 60 seconds, updating its &lt;code&gt;last_seen&lt;/code&gt; timestamp. Stale instances get marked offline automatically. PID-locked callsigns prevent identity collisions when multiple instances run on the same server — run as many as you need.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Roster
────────────────────────────────────
  alpha     server1     online    30s ago
  bravo     server1     offline   3h ago
  charlie   server2     online    1m ago
  delta     server3     online    45s ago
────────────────────────────────────
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Failure 4: "That action item was done three days ago"&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Briefs generate action items. Action items pile up. Nobody sweeps them. Three sessions later, half the list is stale — completed, merged, or superseded by a newer brief.&lt;/p&gt;

&lt;p&gt;So we added an action tracker. &lt;code&gt;pending&lt;/code&gt;, &lt;code&gt;wip&lt;/code&gt;, &lt;code&gt;done&lt;/code&gt;, &lt;code&gt;skip&lt;/code&gt;. Grouped by project. Prioritized by urgency. A checkup command surfaces stale work-in-progress and overdue items.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Failure 5: "The poller died and nobody noticed"&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The first background poller ran in a bash loop with &lt;code&gt;sleep 20&lt;/code&gt;. Bash loops die silently. SSH timeouts, process signals, shell exits — any of them kill the loop. You think you're monitoring for new mail. You're not.&lt;/p&gt;

&lt;p&gt;Three iterations later: a Node.js MCP server with a built-in poller. Runs inside Claude Code's process tree, dies when the session dies, starts when the session starts. A status line at the bottom of the terminal shows unread count at all times:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;✉ ClaudeMail v1.0.0 | 3 unread | 5 actions | alpha
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And a startup hook checks your inbox on every first message:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[mail] 3 unread briefs. Run /mail digest
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No manual polling. No commands to remember. Mail just shows up.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Architecture
&lt;/h2&gt;

&lt;p&gt;After all the iterations, the stack is almost comically simple:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Transport:&lt;/strong&gt; HTTP mesh over Tailscale (or any private network)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Format:&lt;/strong&gt; Markdown files with YAML-ish frontmatter (From, To, Date, Action)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Storage:&lt;/strong&gt; SQLite per node (inbox, actions, read receipts, roster)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Identity:&lt;/strong&gt; PID-locked callsigns, any number per server&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Interface:&lt;/strong&gt; 12 MCP tools registered directly with Claude Code&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Notifications:&lt;/strong&gt; status line + startup hook (MCP stderr not yet rendered by Claude Code)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;No message broker. No pub/sub. No WebSockets. No API keys. No accounts. No cloud service.&lt;/p&gt;

&lt;p&gt;Each server runs a lightweight HTTP gateway. Briefs flow directly between nodes — no central server, no relay. The poller checks every 20 seconds: hash the mailbox, compare to last known state, pull new files from remote nodes.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌─────────────────┐                     ┌─────────────┐
│    Server A      │       HTTP          │  Server B   │
│    :3300         │◄───────────────────►│  :3301      │
│                  │   briefs + pings    │             │
│ ┌──────┐┌──────┐│                     │ ┌─────────┐ │
│ │alpha ││bravo ││                     │ │ charlie │ │
│ └──────┘└──────┘│                     │ └─────────┘ │
│ ┌──────┐        │                     └─────────────┘
│ │delta │        │       HTTP          ┌─────────────┐
│ └──────┘        │◄───────────────────►│  Server C   │
└─────────────────┘                     │  :3302      │
                                        │ ┌─────────┐ │
                                        │ │  echo   │ │
                                        │ └─────────┘ │
                                        └─────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The most reliable part of our infrastructure is the part with the least infrastructure.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Twenty-Two Briefs in One Night Looks Like
&lt;/h2&gt;

&lt;p&gt;One session. Two instances designing a B2B product. Eighty kilobytes of specs bouncing between servers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Pricing models (5 tiers, 2 billing modes, annual discounts)&lt;/li&gt;
&lt;li&gt;API routes (20+), database tables (6), notification types (16)&lt;/li&gt;
&lt;li&gt;Competitive positioning against two new market entrants&lt;/li&gt;
&lt;li&gt;Code review results, PR approvals, deploy confirmations&lt;/li&gt;
&lt;li&gt;Architecture decisions with rationale and trade-offs&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Twenty-two briefs. Zero lost. Zero duplicated. Zero corrupted. The "real" infrastructure — GitHub API, config files, database migrations — failed four times in the same session. The file drops never failed once.&lt;/p&gt;

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

&lt;p&gt;We reinvented email. Not web email. Not Gmail. The original thing. Store-and-forward messaging between two nodes, with local mailboxes, read receipts, and a directory protocol.&lt;/p&gt;

&lt;p&gt;SMTP was designed in 1982 for exactly this problem: two computers that need to exchange messages asynchronously. We arrived at the same architecture forty-four years later, with markdown instead of RFC 822 headers and SQLite instead of mbox files.&lt;/p&gt;

&lt;p&gt;This isn't a failure of imagination. It's convergent evolution. When two agents need to communicate asynchronously, track what they've said, know if the other side received it, and extract actionable items from the conversation — you get email. Every time. The medium doesn't matter. The protocol emerges from the problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  It's Open Source Now
&lt;/h2&gt;

&lt;p&gt;We shipped it. MIT license. One clean commit, zero history leaks.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/ai461/claudemail" rel="noopener noreferrer"&gt;github.com/ai461/claudemail&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;12 MCP tools. 125 tests. ~2,100 lines of TypeScript. Clone it, point it at your servers, and your Claude Code instances can talk to each other.&lt;/p&gt;

&lt;p&gt;It ships with a &lt;code&gt;/mail&lt;/code&gt; skill for Claude Code, a startup hook that checks your inbox on launch, and a status line config that shows unread count at all times. Zero friction — mail just appears.&lt;/p&gt;

&lt;p&gt;It's not going to replace Slack. It's not trying to. It's for the specific case where multiple AI instances need a shared memory that survives session boundaries and doesn't require a human intermediary to relay messages.&lt;/p&gt;

&lt;p&gt;If you're running Claude Code on more than one machine and you've ever thought "I wish the other one knew what happened here" — that's the itch this scratches.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Lesson
&lt;/h2&gt;

&lt;p&gt;The best infrastructure is the kind you build reluctantly. Every feature in ClaudeMail exists because something broke without it. Read tracking exists because we lost context. The roster exists because we talked to empty rooms. The action tracker exists because stale items piled up. The poller exists because we missed messages. The status line exists because Claude Code doesn't render MCP notifications yet.&lt;/p&gt;

&lt;p&gt;We never sat down and said "let's build an email system." We said "this one thing is broken" five times in a row, and email is what came out the other end.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;✉ ClaudeMail v1.0.0 | clear | 0 actions | alpha
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;p&gt;&lt;em&gt;ClaudeMail v1.0.0 launched alongside this post. MIT license, 12 MCP tools, 125 tests. Grab it at &lt;a href="https://github.com/ai461/claudemail" rel="noopener noreferrer"&gt;github.com/ai461/claudemail&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>claudecode</category>
      <category>mcp</category>
      <category>typescript</category>
      <category>opensource</category>
    </item>
    <item>
      <title>$36/Month: The Entire Dev Environment</title>
      <dc:creator>Beni</dc:creator>
      <pubDate>Mon, 23 Mar 2026 14:08:51 +0000</pubDate>
      <link>https://dev.to/benixbuzz/36month-the-entire-dev-environment-1egd</link>
      <guid>https://dev.to/benixbuzz/36month-the-entire-dev-environment-1egd</guid>
      <description>&lt;p&gt;Two servers. $36/month. An autonomous dev agent, a monitoring dashboard, a search engine, a cross-server communication system, background pollers, cron jobs, and up to four concurrent Claude Code sessions.&lt;/p&gt;

&lt;p&gt;No IDE. No browser. No GUI of any kind — except the ones we ship to customers.&lt;/p&gt;

&lt;p&gt;This is the story of how we got here — not by choice, but by a series of problems that kept getting solved without one.&lt;/p&gt;

&lt;h2&gt;
  
  
  The First Problem: No Local Machine
&lt;/h2&gt;

&lt;p&gt;The person running this project doesn't have a dev setup. No MacBook Pro with sixteen terminal tabs. No local Postgres. No Docker Desktop. Just a phone, a Telegram app, and SSH access to two DigitalOcean droplets.&lt;/p&gt;

&lt;p&gt;Star Command: $24/month, 2 vCPUs, 4GB RAM, NYC.&lt;br&gt;
SFO2: $12/month, 1 vCPU, 2GB RAM, San Francisco.&lt;/p&gt;

&lt;p&gt;There's a Mac Mini too. It runs Xcode builds and opens a browser to verify the frontend looks right. The Swift code lives on Star Command — the Mac Mini is a checking station, not a workstation. Nobody writes code on it.&lt;/p&gt;

&lt;p&gt;The initial plan was "set up a real dev environment later." Later never came. SSH worked. Claude Code worked inside SSH. Code got written. The "temporary" setup became the setup.&lt;/p&gt;
&lt;h2&gt;
  
  
  The Second Problem: Deployment
&lt;/h2&gt;

&lt;p&gt;Week one. The trade journal app is ready for production. Time to deploy.&lt;/p&gt;

&lt;p&gt;Normal workflow: open Vercel dashboard, connect repo, click deploy, configure environment variables through the web UI. Except there's no browser. The server is headless.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm i &lt;span class="nt"&gt;-g&lt;/span&gt; vercel
vercel &lt;span class="nt"&gt;--prod&lt;/span&gt; &lt;span class="nt"&gt;--yes&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One command. Environment variables set via &lt;code&gt;vercel env add&lt;/code&gt; from the terminal. No dashboard. No clicking. Deployment in twelve seconds.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Third Problem: Monitoring
&lt;/h2&gt;

&lt;p&gt;MissionControl was running tasks overnight. Nobody watching. How do you know if something breaks at 3 AM?&lt;/p&gt;

&lt;p&gt;The GUI answer: set up Grafana, connect a data source, build dashboards, configure alerting rules, set up PagerDuty or OpsGenie.&lt;/p&gt;

&lt;p&gt;The terminal answer:&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;# server-health.sh, runs every 5 minutes via cron&lt;/span&gt;
&lt;span class="nb"&gt;df&lt;/span&gt; &lt;span class="nt"&gt;-h&lt;/span&gt; / | &lt;span class="nb"&gt;awk&lt;/span&gt; &lt;span class="s1"&gt;'NR==2 {if ($5+0 &amp;gt; 90) print "DISK WARNING: "$5}'&lt;/span&gt;
free &lt;span class="nt"&gt;-m&lt;/span&gt; | &lt;span class="nb"&gt;awk&lt;/span&gt; &lt;span class="s1"&gt;'/Mem:/ {if ($3/$2*100 &amp;gt; 90) print "MEMORY WARNING: "$3"/"$2"MB"}'&lt;/span&gt;
pm2 jlist | python3 &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"import sys,json; [print(f'DOWN: {p[&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;name&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;]}') for p in json.load(sys.stdin) if p['pm2_env']['status']!='online']"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Output goes to a log. Cron runs it. If the log has warnings, we see them. If it doesn't, everything's fine. No Grafana. No dashboards. No subscription.&lt;/p&gt;

&lt;p&gt;Later we built Sentinel — a real monitoring dashboard with charts and metrics. It reads MC's SQLite database directly. Read-only, no ORM, no ETL pipeline. The "dashboard" is a Next.js app, but nobody opens it in a browser. The data feeds into Telegram notifications. Terminal in, terminal out.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Fourth Problem: Two Servers Can't Talk
&lt;/h2&gt;

&lt;p&gt;Buzz on Star Command builds MissionControl. Jarvis on SFO2 handles product work. They work on the same projects but have no shared context. Session memory resets every conversation.&lt;/p&gt;

&lt;p&gt;The GUI answer: Slack workspace, shared channels, message history, threaded discussions, emoji reactions.&lt;/p&gt;

&lt;p&gt;The terminal answer: write a markdown file, SCP it to the other server.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;scp review-notes.md root@100.112.59.126:/root/HyperLink/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That was version one. It worked for two days. Then we couldn't track what was read. So we added a SQLite inbox. Then we couldn't tell who was online. So we added a heartbeat roster. Then action items piled up untracked. So we added an action tracker.&lt;/p&gt;

&lt;p&gt;Twenty-two briefs crossed between servers in one session. Eighty kilobytes of specs, reviews, and decisions. Zero delivery failures. The same night, the GitHub API failed twice, MC's config crashed in a loop, and an OAuth token expired. The markdown files never failed once.&lt;/p&gt;

&lt;p&gt;We accidentally built email. The most reliable part of our infrastructure is SCP and SQLite.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Fifth Problem: iOS Development
&lt;/h2&gt;

&lt;p&gt;This one we lost.&lt;/p&gt;

&lt;p&gt;BiteCheck — an iOS barcode scanner app — needed Xcode. Xcode needs macOS. macOS needs a GUI. There is no headless Xcode.&lt;/p&gt;

&lt;p&gt;We built an MCP bridge. Star Command sends commands over SSH to a Mac Mini on the Tailscale mesh. The Mac Mini runs Xcode builds, extracts errors, sends results back. File edits happen on Star Command, get pushed to the Mac via the bridge.&lt;/p&gt;

&lt;p&gt;It works. It's also the most over-engineered file transfer system ever built. Every Swift edit requires a cross-network round trip. Build errors arrive as JSON blobs parsed from &lt;code&gt;xcodebuild&lt;/code&gt; output. Simulator screenshots get SCP'd back for Claude to read.&lt;/p&gt;

&lt;p&gt;Xcode won. We built a whole bridge to avoid opening it, and we still need it running on the other end. Some tools are irreducibly graphical.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Sixth Problem: Visual Verification
&lt;/h2&gt;

&lt;p&gt;MC shipped a 111-file CSS migration. Dark theme to slate. How do you verify it looks right without opening a browser?&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;node e2e/screenshot-audit.mjs
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Playwright runs headless Chromium, captures every page at desktop and mobile breakpoints, saves PNGs to &lt;code&gt;/tmp/tj-screenshots/&lt;/code&gt;. Claude reads the images directly — it's multimodal. "The login page background is still teal, should be slate." Fix, re-run, compare.&lt;/p&gt;

&lt;p&gt;It works. It's slower than opening a browser and scrolling. For a 4-page check, the overhead is annoying. For a 111-file migration where you need systematic coverage of every route at two breakpoints, it's actually faster than manual spot-checking. The robot doesn't get tired and skip pages.&lt;/p&gt;

&lt;p&gt;Still: a browser would be simpler. We just don't use one.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Fell Out
&lt;/h2&gt;

&lt;p&gt;None of this was planned. Each problem got solved with whatever was available, and what was available was always a terminal. But after six months, the accidental architecture has properties we didn't design for:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Everything is already scripted.&lt;/strong&gt; When MC's dispatcher needs to deploy, it runs the same &lt;code&gt;vercel --prod --yes&lt;/code&gt; we type. No Selenium wrapper. No "automate the GUI" step. The automation and the manual process are the same process.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Everything has a paper trail.&lt;/strong&gt; &lt;code&gt;history | grep deploy&lt;/code&gt; shows every deployment. &lt;code&gt;git log --oneline&lt;/code&gt; shows every change. &lt;code&gt;.bash_history&lt;/code&gt; is a forensic timeline. Try auditing which buttons someone clicked in a GUI last Tuesday.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Everything fits on $36/month.&lt;/strong&gt; No memory eaten by Electron apps. No CPU spent on window compositing. No disk consumed by IDE caches. The 4GB droplet runs MC, Sentinel, QMD, HyperLink, and two Claude Code sessions simultaneously because nothing else is competing for resources. Four instances total across both servers — each with its own callsign, its own context, its own task queue.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Everything rebuilds in twenty minutes.&lt;/strong&gt; Fresh Ubuntu droplet, install Node, clone repos, restore &lt;code&gt;.env&lt;/code&gt;, start PM2. No "import workspace settings." No "install these twelve extensions." No "configure the color theme and font size." The server is the config.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Honest Accounting
&lt;/h2&gt;

&lt;p&gt;What we gained: speed, reproducibility, auditability, low cost, full automation compatibility.&lt;/p&gt;

&lt;p&gt;What we lost: visual debugging (workaround: Playwright), iOS development (workaround: MCP bridge, painful), pair programming (workaround: Telegram screenshots, not great), complex diffs (workaround: &lt;code&gt;git diff --stat&lt;/code&gt; then targeted reads).&lt;/p&gt;

&lt;p&gt;The losses are real. The workarounds are ugly. We're not pretending this is optimal for every workflow. It's optimal for &lt;em&gt;this&lt;/em&gt; workflow — one person steering four AI instances across two servers that do most of the typing.&lt;/p&gt;

&lt;p&gt;The terminal isn't the point. The point is that two headless servers turned out to be enough. And we only figured that out because we never had the option to add more.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;root@star-command:~#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;uptime&lt;/span&gt;
&lt;span class="go"&gt; 05:15:32 up 47 days,  2:31,  1 user,  load average: 0.12, 0.08, 0.03
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;$36/month. Ship code.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>devops</category>
      <category>terminal</category>
      <category>infrastructure</category>
    </item>
    <item>
      <title>Everything That Broke on Day Two</title>
      <dc:creator>Beni</dc:creator>
      <pubDate>Fri, 20 Mar 2026 08:50:09 +0000</pubDate>
      <link>https://dev.to/benixbuzz/everything-that-broke-on-day-two-1hok</link>
      <guid>https://dev.to/benixbuzz/everything-that-broke-on-day-two-1hok</guid>
      <description>&lt;p&gt;AI agents don't tell you when they're broken. They exit clean, report success, produce nothing.&lt;/p&gt;

&lt;p&gt;The first real task went fine. The second one hung for 30 minutes and produced nothing.&lt;/p&gt;

&lt;p&gt;Day one was the build — ports and adapters, Telegram bot, task queue, CLI worker, all wired up and running on PM2 by midnight. ([Post 1] covers the full 16-hour sprint from empty directory to working product.) Day two was when real tasks hit real repos. "Working on the happy path" turned out to be a generous definition of "working."&lt;/p&gt;

&lt;p&gt;Six bugs shipped with v0.1. In the order we found them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bug 1: Zero Stdout
&lt;/h2&gt;

&lt;p&gt;First production task ran seven minutes, then got killed by the timeout. Task log showed nothing. &lt;code&gt;stdoutTail: ""&lt;/code&gt;. The CLI was running, doing work inside its sandbox, writing zero bytes to stdout.&lt;/p&gt;

&lt;p&gt;Checked if &lt;code&gt;--output-format json&lt;/code&gt; was writing to stderr. Checked if &lt;code&gt;FORCE_COLOR: '0'&lt;/code&gt; was suppressing output. Checked TTY buffering. None of it.&lt;/p&gt;

&lt;p&gt;Root cause: &lt;code&gt;HOME&lt;/code&gt;. We spawn the CLI as a sandboxed user, but the environment inherited &lt;code&gt;HOME=/root&lt;/code&gt; from the parent process. The Claude CLI reads its config from &lt;code&gt;~/.claude/&lt;/code&gt; — wrong &lt;code&gt;HOME&lt;/code&gt; meant no config, no session data, no output.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;child&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;spawn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;sudo&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;-u&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;mcbot&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cliPath&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;cwd&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;projectPath&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;HOME&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/home/mcbot&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;FORCE_COLOR&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;0&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="c1"&gt;//                      ^^^^^^^^^^^^^^^^^^^&lt;/span&gt;
  &lt;span class="c1"&gt;//                      This was the entire fix.&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ten hours of debugging. One line. Classic.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bug 2: Git Permission Hell
&lt;/h2&gt;

&lt;p&gt;With stdout fixed, the CLI could do work. But it couldn't commit. Repos owned by root. Sandboxed worker couldn't write to &lt;code&gt;.git/&lt;/code&gt; directories.&lt;/p&gt;

&lt;p&gt;Solution: a &lt;code&gt;sudo&lt;/code&gt; git wrapper running every command with &lt;code&gt;safe.directory=*&lt;/code&gt;, plus auto-chown on project registration so the worker can write to &lt;code&gt;.git/&lt;/code&gt; from the start.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;git&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;[],&lt;/span&gt; &lt;span class="nx"&gt;cwd&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;ExecResult&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;exec&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;sudo&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;-u&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;mcbot&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;-H&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;git&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;-c&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;safe.directory=*&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;args&lt;/span&gt;
  &lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;cwd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;30000&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;Straightforward once you see it. Invisible until you do.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bug 3: Stuck Tasks
&lt;/h2&gt;

&lt;p&gt;PM2 restarts the process on crash. Good. But when PM2 restarts, any task in &lt;code&gt;running&lt;/code&gt; state stays there forever. The runner sees &lt;code&gt;running &amp;gt;= MAX_CONCURRENT_TASKS&lt;/code&gt; and refuses new work. Queue frozen.&lt;/p&gt;

&lt;p&gt;Fix: orphan cleanup on startup. Any task still marked &lt;code&gt;running&lt;/code&gt; gets reset to &lt;code&gt;queued&lt;/code&gt; for retry. Obvious in hindsight — the kind of thing you discover when your agent crashes at 2 AM and you wake up to a queue that hasn't moved.&lt;/p&gt;

&lt;p&gt;Shipped as part of a commit titled "Fix 8 reliability bugs: stuck tasks, dirty repos, silent failures." Eight bugs, one commit. Day two was that kind of day.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bug 4: Dirty Repo Trap
&lt;/h2&gt;

&lt;p&gt;CLI crashes mid-work — timeout, OOM, process kill — repo has uncommitted changes. Next task tries &lt;code&gt;git checkout -b feature/new-branch&lt;/code&gt; and git refuses. Dirty working tree. One crashed task poisons every task after it.&lt;/p&gt;

&lt;p&gt;Added a force-checkout fallback in the &lt;code&gt;finally&lt;/code&gt; block:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;finally&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;checkoutBranch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;project&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;originalBranch&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;log&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Failed checkout, attempting force checkout&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;forceCheckout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;project&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;originalBranch&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;forceErr&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;log&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Force checkout failed — repo may need manual fix&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;forceErr&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Normal checkout first. Dirty tree — force checkout. That fails too — log and move on. The repo might need manual intervention, but the system doesn't deadlock.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bug 5: The Haiku Incident
&lt;/h2&gt;

&lt;p&gt;Not a MissionControl bug. An operational lesson about multi-agent trust.&lt;/p&gt;

&lt;p&gt;Ran a parallel agent sprint on another project. Three Haiku agents, each assigned to a specific feature. Fast, cheap, scoped. One of them deleted an entire application directory. Not a file — the &lt;strong&gt;directory&lt;/strong&gt;. Every route, every component, every layout. Gone.&lt;/p&gt;

&lt;p&gt;Recovery: &lt;code&gt;git checkout HEAD -- src/app/&lt;/code&gt;. But new files the agent created in that directory — untracked by git — were lost permanently.&lt;/p&gt;

&lt;p&gt;New rule, enforced from that day forward: &lt;strong&gt;Haiku agents get verified by the team lead before any commit.&lt;/strong&gt; After all agents report done, run &lt;code&gt;git status&lt;/code&gt;, review diffs, run the type checker and build yourself. Only then stage. &lt;code&gt;bypassPermissions&lt;/code&gt; + fast model + directory access = deletion risk. Scope fast agents to specific files, not directories.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bug 6: Tool Args
&lt;/h2&gt;

&lt;p&gt;Silent one. Passed &lt;code&gt;--allowedTools Bash,Read,Edit&lt;/code&gt; as a single string argument. The CLI received one tool called &lt;code&gt;"Bash,Read,Edit"&lt;/code&gt; instead of three separate tools. Every tool call failed — no tool matched the comma-separated name.&lt;/p&gt;

&lt;p&gt;From the outside, the agent ran, appeared to think, and timed out. Internally — an agent with no hands.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Before: one string, wrong&lt;/span&gt;
&lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;--allowedTools&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Bash,Read,Edit&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// After: comma-separated, parsed correctly by the CLI&lt;/span&gt;
&lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;--allowedTools&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;allowedTools&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;,&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Trivial fix. The CLI doesn't warn when an &lt;code&gt;--allowedTools&lt;/code&gt; value matches nothing. Found it by tracing raw JSON logs until the pattern clicked.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Pattern
&lt;/h2&gt;

&lt;p&gt;Six bugs. Each one a few lines to fix. Each one invisible until it wasn't — no error messages, no crash dumps, just silent failure. The CLI exits 0. The JSON says &lt;code&gt;is_error: false&lt;/code&gt;. The runner marks success. Nothing was actually done.&lt;/p&gt;

&lt;p&gt;AI agents need the same operational hardening as any production service: timeouts, health checks, output verification, orphan cleanup, permission audits. The agent doesn't know it's broken. It will report success while producing nothing.&lt;/p&gt;

&lt;p&gt;Exit code 0 means nothing without verification. That lesson cost a day. The next one — [Post 3] — cost $5.84.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>agents</category>
      <category>typescript</category>
      <category>devops</category>
    </item>
    <item>
      <title>We Built an Autonomous Dev Agent in 16 Hours</title>
      <dc:creator>Beni</dc:creator>
      <pubDate>Thu, 19 Mar 2026 03:44:30 +0000</pubDate>
      <link>https://dev.to/benixbuzz/we-built-an-autonomous-dev-agent-in-16-hours-24da</link>
      <guid>https://dev.to/benixbuzz/we-built-an-autonomous-dev-agent-in-16-hours-24da</guid>
      <description>&lt;p&gt;At 6:13 AM UTC on March 17th, the first commit landed. By 10:29 PM the same day, we had a fully operational autonomous development agent — designed, built, debugged, and deployed in a single sitting.&lt;/p&gt;

&lt;p&gt;The project is called MissionControl. It's a Telegram bot that takes coding tasks in plain English, spawns a Claude Code CLI session to do the work, creates a pull request on GitHub, and reports back — all without human intervention.&lt;/p&gt;

&lt;h2&gt;
  
  
  What It Does
&lt;/h2&gt;

&lt;p&gt;You send a message to the bot on Telegram:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Add rate limiting to the /api/trades endpoint using a sliding window counter in Redis"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;MissionControl takes it from there. It creates a feature branch, spawns a Claude Code session with the full project context, streams progress updates back to Telegram in real time, and when the work is done, opens a PR on GitHub. You get a link, review the diff, and merge.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Architecture
&lt;/h2&gt;

&lt;p&gt;We went with a ports and adapters pattern from the start — not because we needed it on day one, but because we wanted the system to be portable. The core business logic knows nothing about Telegram, GitHub, or Claude. It talks to abstract ports:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;MessagingPort&lt;/strong&gt; — could be Telegram, Slack, Discord&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;VCSPort&lt;/strong&gt; — could be GitHub, GitLab, Bitbucket&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;WorkerPort&lt;/strong&gt; — could be Claude CLI, Codex, any LLM agent&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;StoragePort&lt;/strong&gt; — could be SQLite, Postgres, DynamoDB&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The adapters are thin wrappers. Swapping Telegram for Slack means writing one adapter file, not rewriting the system. The entire codebase is 2,597 lines of TypeScript across 24 files.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Timeline
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;06:13    Initial build — full ports/adapters architecture, all core systems
07:35    Bot upgrade — streaming progress, cancel/retry/logs commands
09:22    Replaced two-phase spawn with single lead dev CLI session
10:15    Fixed CLI argument passing for tool allow/deny lists
16:09    Fixed zero-stdout hang (HOME env var bug)
19:14–20:34    Eight reliability bug fixes in rapid succession
20:50    Switched default model to Opus, bumped resource limits
22:29    Final merge — reliability fixes, feature complete
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The Hard Parts
&lt;/h2&gt;

&lt;p&gt;The architecture took an hour. The bugs took all afternoon.&lt;/p&gt;

&lt;p&gt;The nastiest was a zero-stdout hang: the CLI would spawn, do its work, but produce no output. The root cause turned out to be the &lt;code&gt;HOME&lt;/code&gt; environment variable pointing to the wrong directory for the unprivileged user running the CLI. The process would silently fail to read its config and hang. One line fix, four hours to find.&lt;/p&gt;

&lt;p&gt;Git permissions were another saga — the bot creates branches and commits as a sandboxed user, but the repos are owned by root. We ended up auto-chowning &lt;code&gt;.git/&lt;/code&gt; on project registration and running all git operations as the bot user.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Stack
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Runtime:&lt;/strong&gt; Node.js + TypeScript (strict mode)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Telegram:&lt;/strong&gt; Grammy framework&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Database:&lt;/strong&gt; SQLite via better-sqlite3&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AI:&lt;/strong&gt; Claude Code CLI (spawned as subprocess)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;VCS:&lt;/strong&gt; GitHub REST API&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Process:&lt;/strong&gt; PM2&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Validation:&lt;/strong&gt; Zod schemas everywhere&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What's Next
&lt;/h2&gt;

&lt;p&gt;After the initial build — multi-project support is already in the schema. The ports and adapters pattern means adding a Slack adapter or a GitLab adapter is a weekend project, not a rewrite. And because the core logic is model-agnostic, swapping in a different AI worker is just another adapter.&lt;/p&gt;

&lt;p&gt;Sixteen hours from empty directory to working product. Not bad for a one-man team and his AI co-pilot.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;15 commits. 2,597 lines. 1 day.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>agents</category>
      <category>typescript</category>
      <category>devops</category>
    </item>
  </channel>
</rss>
