<?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: Benny</title>
    <description>The latest articles on DEV Community by Benny (@bencoy09).</description>
    <link>https://dev.to/bencoy09</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4157578%2F60155284-79e7-4a9a-88a2-e1ea8c994361.jpg</url>
      <title>DEV Community: Benny</title>
      <link>https://dev.to/bencoy09</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/bencoy09"/>
    <language>en</language>
    <item>
      <title>CaptionBot: Say What You’re Selling. Get a WhatsApp Caption. Hacktoberfest Weekend Challenge — Build for a Friend</title>
      <dc:creator>Benny</dc:creator>
      <pubDate>Sat, 03 Oct 2026 17:22:48 +0000</pubDate>
      <link>https://dev.to/bencoy09/captionbot-say-what-youre-selling-get-a-whatsapp-caption-hacktoberfest-weekend-challenge--3a5k</link>
      <guid>https://dev.to/bencoy09/captionbot-say-what-youre-selling-get-a-whatsapp-caption-hacktoberfest-weekend-challenge--3a5k</guid>
      <description>&lt;p&gt;The Problem&lt;/p&gt;

&lt;p&gt;A lot of small clothing sellers in Nigeria sell through WhatsApp Status.&lt;/p&gt;

&lt;p&gt;The workflow is simple: take a picture, write a caption, post it, repeat.&lt;/p&gt;

&lt;p&gt;The pictures are easy.&lt;/p&gt;

&lt;p&gt;Writing a fresh caption for every item is the annoying part.&lt;/p&gt;

&lt;p&gt;I wanted to make that part almost disappear.&lt;/p&gt;

&lt;p&gt;What I Built&lt;/p&gt;

&lt;p&gt;CaptionBot lets a seller describe what they’re selling by typing or speaking naturally.&lt;/p&gt;

&lt;p&gt;For example:&lt;/p&gt;

&lt;p&gt;“I get two Ankara gowns, size 12 and 14, 18k each, Lagos delivery.”&lt;/p&gt;

&lt;p&gt;CaptionBot turns that into a clean, ready-to-post WhatsApp caption.&lt;/p&gt;

&lt;p&gt;Then the seller can Copy it or Send to WhatsApp.&lt;/p&gt;

&lt;p&gt;The idea is deliberately small. I am not trying to build another giant AI writing assistant. I wanted to solve one repetitive task for a specific type of person.&lt;/p&gt;

&lt;p&gt;I built it for sellers like the ones already in my own contacts.&lt;/p&gt;

&lt;p&gt;I have not put it in a seller’s hands yet, so I am not claiming user validation that I don’t have. I tested the workflow myself and recorded what actually broke.&lt;/p&gt;

&lt;p&gt;Demo&lt;/p&gt;

&lt;p&gt;&lt;a href="https://youtu.be/qfmQImkSp24?is=twic1RjefQTXimIy" rel="noopener noreferrer"&gt;https://youtu.be/qfmQImkSp24?is=twic1RjefQTXimIy&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Code&lt;/p&gt;

&lt;p&gt;CaptionBot GitHub Repository⁠￼&lt;/p&gt;

&lt;p&gt;How It Works&lt;/p&gt;

&lt;p&gt;The entire application is intentionally lightweight.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Python runs the application.&lt;/li&gt;
&lt;li&gt;Ollama runs Gemma 3 1B locally.&lt;/li&gt;
&lt;li&gt;Chrome voice typing lets the seller speak instead of type.&lt;/li&gt;
&lt;li&gt;A small preprocessing step fixes common transcription mistakes.&lt;/li&gt;
&lt;li&gt;Gemma generates the caption.&lt;/li&gt;
&lt;li&gt;Code checks the result before showing it to the user.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I chose Gemma 3 1B because this project does not need a huge model.&lt;/p&gt;

&lt;p&gt;The model download is about 815 MB. In a market where mobile data is not something I want a seller constantly spending money on, I wanted the actual AI generation to happen locally on an ordinary laptop.&lt;/p&gt;

&lt;p&gt;That led to a simple design:&lt;/p&gt;

&lt;p&gt;Gemma handles the language.&lt;br&gt;
My code handles the rules.&lt;/p&gt;

&lt;p&gt;The Part That Didn’t Work&lt;/p&gt;

&lt;p&gt;My first version had a bug.&lt;/p&gt;

&lt;p&gt;I gave CaptionBot a gown with no price.&lt;/p&gt;

&lt;p&gt;It generated a caption containing 40k.&lt;/p&gt;

&lt;p&gt;I never gave it 40k.&lt;/p&gt;

&lt;p&gt;The reason was embarrassing but useful: one of my prompt examples contained a 40k lace gown. The small model copied the example into a completely different product.&lt;/p&gt;

&lt;p&gt;That became one of the most important lessons from building CaptionBot:&lt;/p&gt;

&lt;p&gt;A small model can produce a very convincing answer and still invent a detail that was never provided.&lt;/p&gt;

&lt;p&gt;There were other problems too.&lt;/p&gt;

&lt;p&gt;Chrome’s voice typing heard:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;“lace” as “list”&lt;/li&gt;
&lt;li&gt;“12k” as “12 key”&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It also struggled with Nigerian Pidgin in some cases. Typed Pidgin worked much better.&lt;/p&gt;

&lt;p&gt;So I fixed the system instead of hiding the failure.&lt;/p&gt;

&lt;p&gt;I:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Removed the misleading examples.&lt;/li&gt;
&lt;li&gt;Added a stronger instruction not to invent product details.&lt;/li&gt;
&lt;li&gt;Added a code-level price check.&lt;/li&gt;
&lt;li&gt;Made the system reject a generated price if that price was never provided.&lt;/li&gt;
&lt;li&gt;Added retries, up to three attempts, when the generated caption fails validation.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The repository contains the fixed version.&lt;/p&gt;

&lt;p&gt;The demo video still shows the original failure because I wanted to show what actually happened during development.&lt;/p&gt;

&lt;p&gt;Why I Used Gemma&lt;/p&gt;

&lt;p&gt;Caption generation is a small task.&lt;/p&gt;

&lt;p&gt;The seller is not asking the model to research the internet, write an essay, or solve a complicated problem. They are giving it a handful of product details and asking it to turn them into natural language.&lt;/p&gt;

&lt;p&gt;Gemma 3 1B was enough for that.&lt;/p&gt;

&lt;p&gt;More importantly, running it locally gave me something I would not get from simply calling a hosted API:&lt;/p&gt;

&lt;p&gt;I could see exactly where the model ended and my application began.&lt;/p&gt;

&lt;p&gt;When I discovered that the model could invent a price, I could put the guardrail directly in my own code.&lt;/p&gt;

&lt;p&gt;The model generates.&lt;/p&gt;

&lt;p&gt;The application verifies.&lt;/p&gt;

&lt;p&gt;Why Open Innovation Matters&lt;/p&gt;

&lt;p&gt;For a small seller, paying an API every time they need a caption adds unnecessary cost to a very small task.&lt;/p&gt;

&lt;p&gt;CaptionBot can run the model locally, so there is no per-caption AI bill.&lt;/p&gt;

&lt;p&gt;But the bigger point for me is control.&lt;/p&gt;

&lt;p&gt;Because the model and application pipeline are accessible, I can experiment with the prompt, change the examples, add validation, and test different approaches myself.&lt;/p&gt;

&lt;p&gt;The 40k bug is a good example.&lt;/p&gt;

&lt;p&gt;I didn’t need to wait for a model provider to fix it.&lt;/p&gt;

&lt;p&gt;I found the failure, understood why it happened, changed the prompt, and added a code-level check so the same mistake would be harder to repeat.&lt;/p&gt;

&lt;p&gt;That is what open innovation means to me here:&lt;/p&gt;

&lt;p&gt;not just using an open model, but being able to build around it, break it, understand it, and improve it.&lt;/p&gt;

&lt;p&gt;There is one limitation worth being clear about: the AI inference is local, but Chrome’s voice-typing feature uses Google’s servers. So the entire input-to-output pipeline is not fully local.&lt;/p&gt;

&lt;p&gt;What I Would Test Next&lt;/p&gt;

&lt;p&gt;The next step is not adding more features.&lt;/p&gt;

&lt;p&gt;It is putting CaptionBot in front of actual sellers.&lt;/p&gt;

&lt;p&gt;I would want to learn:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Whether sellers prefer speaking or typing.&lt;/li&gt;
&lt;li&gt;How well it handles Nigerian Pidgin and local product names.&lt;/li&gt;
&lt;li&gt;What happens with incomplete product information.&lt;/li&gt;
&lt;li&gt;Whether sellers actually use the WhatsApp send button.&lt;/li&gt;
&lt;li&gt;Which types of clothing descriptions need the most correction.&lt;/li&gt;
&lt;li&gt;What other repetitive parts of their selling workflow could be automated locally.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That would turn CaptionBot from something I built for sellers into something shaped by actual sellers.&lt;/p&gt;

&lt;p&gt;Prize Category&lt;/p&gt;

&lt;p&gt;Best Use of Gemma&lt;/p&gt;

&lt;p&gt;CaptionBot uses Gemma 3 1B as a local caption-generation model, while application-level validation handles important factual constraints such as prices and sizes.&lt;/p&gt;

&lt;p&gt;The result is a small, local-first tool built around one practical problem:&lt;/p&gt;

&lt;p&gt;A seller says what they’re selling. CaptionBot makes it ready to post.&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>weekendchallenge</category>
      <category>hf26challenge</category>
      <category>gemma</category>
    </item>
    <item>
      <title>[Boost]</title>
      <dc:creator>Benny</dc:creator>
      <pubDate>Fri, 02 Oct 2026 22:32:06 +0000</pubDate>
      <link>https://dev.to/bencoy09/-31ao</link>
      <guid>https://dev.to/bencoy09/-31ao</guid>
      <description>&lt;div class="ltag__link--embedded"&gt;
  &lt;div class="crayons-story "&gt;
  &lt;a href="https://dev.to/bencoy09/my-server-files-its-own-tickets-then-waits-for-my-signature-to-heal-itself-2dhf" class="crayons-story__hidden-navigation-link"&gt;My Server Files Its Own Tickets, Then Waits for My Signature to Heal Itself&lt;/a&gt;


  &lt;div class="crayons-story__body crayons-story__body-full_post"&gt;
      &lt;a href="https://dev.to/bencoy09/my-server-files-its-own-tickets-then-waits-for-my-signature-to-heal-itself-2dhf" class="crayons-article__context-note crayons-article__context-note__feed"&gt;&lt;p&gt;Sanity Challenge Path Two Submission&lt;/p&gt;

&lt;/a&gt;
    &lt;div class="crayons-story__top"&gt;
      &lt;div class="crayons-story__meta"&gt;
        &lt;div class="crayons-story__author-pic"&gt;

          &lt;a href="/bencoy09" class="crayons-avatar  crayons-avatar--l  "&gt;
            &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4157578%2F60155284-79e7-4a9a-88a2-e1ea8c994361.jpg" alt="bencoy09 profile" class="crayons-avatar__image" width="460" height="460"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
        &lt;div&gt;
          &lt;div&gt;
            &lt;a href="/bencoy09" class="crayons-story__secondary fw-medium m:hidden"&gt;
              Benny
            &lt;/a&gt;
            &lt;div class="profile-preview-card relative mb-4 s:mb-0 fw-medium hidden m:inline-block"&gt;
              
                Benny
                
                
              
              &lt;div id="story-author-preview-content-4789295" class="profile-preview-card__content crayons-dropdown branded-7 p-4 pt-0"&gt;
                &lt;div class="gap-4 grid"&gt;
                  &lt;div class="-mt-4"&gt;
                    &lt;a href="/bencoy09" class="flex"&gt;
                      &lt;span class="crayons-avatar crayons-avatar--xl mr-2 shrink-0"&gt;
                        &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4157578%2F60155284-79e7-4a9a-88a2-e1ea8c994361.jpg" class="crayons-avatar__image" alt="" width="460" height="460"&gt;
                      &lt;/span&gt;
                      &lt;span class="crayons-link crayons-subtitle-2 mt-5"&gt;Benny&lt;/span&gt;
                    &lt;/a&gt;
                  &lt;/div&gt;
                  &lt;div class="print-hidden"&gt;
                    
                      Follow
                    
                  &lt;/div&gt;
                  &lt;div class="author-preview-metadata-container"&gt;&lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            &lt;/div&gt;

          &lt;/div&gt;
          &lt;a href="https://dev.to/bencoy09/my-server-files-its-own-tickets-then-waits-for-my-signature-to-heal-itself-2dhf" class="crayons-story__tertiary fs-xs"&gt;&lt;time&gt;Oct 2&lt;/time&gt;&lt;span class="time-ago-indicator-initial-placeholder"&gt;&lt;/span&gt;&lt;/a&gt;
        &lt;/div&gt;
      &lt;/div&gt;

    &lt;/div&gt;

    &lt;div class="crayons-story__indention"&gt;
      &lt;h2 class="crayons-story__title crayons-story__title-full_post"&gt;
        &lt;a href="https://dev.to/bencoy09/my-server-files-its-own-tickets-then-waits-for-my-signature-to-heal-itself-2dhf" id="article-link-4789295"&gt;
          My Server Files Its Own Tickets, Then Waits for My Signature to Heal Itself
        &lt;/a&gt;
      &lt;/h2&gt;
        &lt;div class="crayons-story__tags"&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/devchallenge"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;devchallenge&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/sanitychallenge"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;sanitychallenge&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/sanity"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;sanity&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/ai"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;ai&lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="crayons-story__bottom"&gt;
        &lt;div class="crayons-story__details"&gt;
          &lt;a href="https://dev.to/bencoy09/my-server-files-its-own-tickets-then-waits-for-my-signature-to-heal-itself-2dhf" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left"&gt;
            &lt;div class="multiple_reactions_aggregate"&gt;
              &lt;span class="multiple_reactions_icons_container"&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/sparkle-heart-5f9bee3767e18deb1bb725290cb151c25234768a0e9a2bd39370c382d02920cf.svg" width="24" height="24"&gt;
                  &lt;/span&gt;
              &lt;/span&gt;
              &lt;span class="aggregate_reactions_counter"&gt;1&lt;span class="hidden s:inline"&gt;&amp;nbsp;reaction&lt;/span&gt;&lt;/span&gt;
            &lt;/div&gt;
          &lt;/a&gt;
            &lt;a href="https://dev.to/bencoy09/my-server-files-its-own-tickets-then-waits-for-my-signature-to-heal-itself-2dhf#comments" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left flex items-center"&gt;
              

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

&lt;/div&gt;


</description>
      <category>automation</category>
      <category>devops</category>
      <category>linux</category>
    </item>
    <item>
      <title>My Server Files Its Own Tickets, Then Waits for My Signature to Heal Itself</title>
      <dc:creator>Benny</dc:creator>
      <pubDate>Fri, 02 Oct 2026 22:31:22 +0000</pubDate>
      <link>https://dev.to/bencoy09/my-server-files-its-own-tickets-then-waits-for-my-signature-to-heal-itself-2dhf</link>
      <guid>https://dev.to/bencoy09/my-server-files-its-own-tickets-then-waits-for-my-signature-to-heal-itself-2dhf</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/sanity-2026-09-16"&gt;Sanity Challenge, Path Two: Vibe-Code Something Strange&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

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

&lt;p&gt;Servers crash at 3 AM. A human wakes up, SSHs in, types &lt;code&gt;systemctl restart&lt;/code&gt;, and goes back to sleep. I wondered if the &lt;em&gt;ticket&lt;/em&gt; could do that work instead.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Ops Brain&lt;/strong&gt; is a server that files its own incident reports in Sanity and heals itself, but only after a human approves.&lt;/p&gt;

&lt;p&gt;The loop:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;A small Python agent watches my Ubuntu machine.&lt;/li&gt;
&lt;li&gt;nginx dies. The agent grabs the recent logs and creates an &lt;code&gt;incident&lt;/code&gt; document in Sanity with a title, service, diagnosis, proposed action, risk level and log excerpt.&lt;/li&gt;
&lt;li&gt;I open Sanity Studio and find a ticket from a server that reported its own death.&lt;/li&gt;
&lt;li&gt;I flip the status to &lt;code&gt;approved&lt;/code&gt; and hit Publish.&lt;/li&gt;
&lt;li&gt;The agent sees my approval, restarts nginx, checks that it's actually healthy, and stamps the ticket &lt;code&gt;verified&lt;/code&gt; with a resolved time.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The server files the ticket, I sign it, and the server fixes itself.&lt;/p&gt;

&lt;h2&gt;
  
  
  Demo
&lt;/h2&gt;

&lt;p&gt;{&lt;a href="https://drive.google.com/file/d/1JbS2bEXw86HEF122E8LWS6lPCNezKAyf/view?usp=drivesdk" rel="noopener noreferrer"&gt;https://drive.google.com/file/d/1JbS2bEXw86HEF122E8LWS6lPCNezKAyf/view?usp=drivesdk&lt;/a&gt;}&lt;/p&gt;

&lt;p&gt;Live Studio: &lt;a href="https://opsbrain-benny.sanity.studio/" rel="noopener noreferrer"&gt;https://opsbrain-benny.sanity.studio/&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Code
&lt;/h2&gt;

&lt;p&gt;{&lt;a href="https://github.com/Bencoy09/ops-brain" rel="noopener noreferrer"&gt;https://github.com/Bencoy09/ops-brain&lt;/a&gt; }&lt;/p&gt;

&lt;h2&gt;
  
  
  How I Used Sanity
&lt;/h2&gt;

&lt;p&gt;The whole project rests on one decision: &lt;strong&gt;the workflow is data, not code.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;An incident is a document that moves through states: &lt;code&gt;awaiting_approval → approved → verified&lt;/code&gt; (or &lt;code&gt;failed&lt;/code&gt;). The agent and a human push the same document forward. There's no separate approvals app and no webhook maze. The document &lt;em&gt;is&lt;/em&gt; the process.&lt;/p&gt;

&lt;p&gt;The schema is a single &lt;code&gt;incident&lt;/code&gt; type:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;service&lt;/code&gt;, &lt;code&gt;title&lt;/code&gt;, &lt;code&gt;logExcerpt&lt;/code&gt;: what broke, plus the evidence&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;diagnosis&lt;/code&gt;, &lt;code&gt;action&lt;/code&gt;, &lt;code&gt;risk&lt;/code&gt;: what the agent proposes&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;status&lt;/code&gt;: the workflow state, shown as radio buttons in Studio so approving takes one click&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;detectedAt&lt;/code&gt;, &lt;code&gt;resolvedAt&lt;/code&gt;: a built-in timeline of how long each outage lasted&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Because incidents are structured content, I can ask GROQ questions like "show me everything that failed this week" without grepping a single log file.&lt;/p&gt;

&lt;h2&gt;
  
  
  My Build Process
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The one rule I wouldn't bend:&lt;/strong&gt; the agent never runs arbitrary commands. It executes only actions from a hard-coded allowlist (&lt;code&gt;restart_service&lt;/code&gt; on explicitly watched services). Anything else is blocked and marked &lt;code&gt;failed&lt;/code&gt;. An agent that obeys whatever text lands in a database is a remote-code-execution bug waiting to happen, so the allowlist is part of the design.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What went wrong (the honest part):&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;npm kept timing out.&lt;/strong&gt; My connection dropped &lt;code&gt;create sanity&lt;/code&gt; mid-install. The project and dataset had already been created, so I reran only &lt;code&gt;npm install&lt;/code&gt; with &lt;code&gt;--maxsockets=3&lt;/code&gt; and it went through.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A half-written package broke &lt;code&gt;sanity deploy&lt;/code&gt;.&lt;/strong&gt; An interrupted install left &lt;code&gt;zod&lt;/code&gt; corrupted. Deleting it and reinstalling that one package fixed it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;I pasted a shell command into my Python file.&lt;/strong&gt; Line 1 was &lt;code&gt;cat &amp;gt; ...&lt;/code&gt;, and Python was not amused. I deleted the stray line.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The classic gotcha:&lt;/strong&gt; the agent only sees &lt;em&gt;published&lt;/em&gt; documents. I approved an incident, nothing happened, and the cause was that I hadn't clicked Publish.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;I leaked my own token while testing.&lt;/strong&gt; I typed it on the command line, so I revoked it and switched to a hidden prompt (&lt;code&gt;read -s&lt;/code&gt;) for the new one.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Tools:&lt;/strong&gt; I used Claude to plan the architecture and debug each error as it appeared. The rest is deliberately plain: WSL Ubuntu, nginx, Python's standard library and Sanity Studio.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What I'd build next:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;An &lt;strong&gt;Approve&lt;/strong&gt; button inside Studio using the App SDK, instead of changing a dropdown.&lt;/li&gt;
&lt;li&gt;A learning step where, after a successful fix, the agent drafts a new runbook entry for me to approve.&lt;/li&gt;
&lt;li&gt;More watched services, with risk levels that decide what needs a human and what doesn't.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Sanity Project Details
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Project ID: &lt;code&gt;hhoxq5y4&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Dataset: &lt;code&gt;production&lt;/code&gt; (public)&lt;/li&gt;
&lt;li&gt;Query the incidents yourself: &lt;a href="https://hhoxq5y4.api.sanity.io/v2025-02-19/data/query/production?query=*%5B_type==%22incident%22%5D" rel="noopener noreferrer"&gt;https://hhoxq5y4.api.sanity.io/v2025-02-19/data/query/production?query=*[_type=="incident"]&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>devchallenge</category>
      <category>sanitychallenge</category>
      <category>sanity</category>
      <category>ai</category>
    </item>
  </channel>
</rss>
