<?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: MediBlackSand</title>
    <description>The latest articles on DEV Community by MediBlackSand (@mediblacksand_f0ea36c53fb).</description>
    <link>https://dev.to/mediblacksand_f0ea36c53fb</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%2F3990079%2Fc77a9545-3144-4249-bb3c-05314e6d981d.png</url>
      <title>DEV Community: MediBlackSand</title>
      <link>https://dev.to/mediblacksand_f0ea36c53fb</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/mediblacksand_f0ea36c53fb"/>
    <language>en</language>
    <item>
      <title>Ornith-1.0 is a clever open coding model. Ollama's tool-calling isn't ready for it.</title>
      <dc:creator>MediBlackSand</dc:creator>
      <pubDate>Wed, 19 Aug 2026 01:41:54 +0000</pubDate>
      <link>https://dev.to/mediblacksand_f0ea36c53fb/ornith-10-is-a-clever-open-coding-model-ollamas-tool-calling-isnt-ready-for-it-5402</link>
      <guid>https://dev.to/mediblacksand_f0ea36c53fb/ornith-10-is-a-clever-open-coding-model-ollamas-tool-calling-isnt-ready-for-it-5402</guid>
      <description>&lt;p&gt;&lt;em&gt;Ornith-1.0 is a June 2026 open-weight coding model which claimed to have a genuinely different idea behind it: instead of learning inside a harness someone else built, it learns to build its own harness while it solves the task. The 9B model's benchmarks are strong for its size. I still couldn't get it running as a local Claude Code backend, and the reason why is more useful than the benchmark numbers.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The idea worth knowing about
&lt;/h2&gt;

&lt;p&gt;Most coding models get trained inside a harness a human already built: a fixed prompt template, retry logic, a plan-then-act loop bolted on from the outside. Ornith claimed to do something different. During training, the model first proposes a plan for how to approach each task, then solves the task using that plan. Both the plan and the answer get scored, so the model learns to write better plans, not just better answers. Over time the two improve together.&lt;/p&gt;

&lt;p&gt;On paper, the 9B model matches or beats Gemma4-31B, four times its size, on SWE-Bench Verified and Terminal-Bench 2.1. MIT-licensed, runs on a laptop.&lt;/p&gt;

&lt;h2&gt;
  
  
  The stack
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Runtime:     Ollama, model built from a raw HF GGUF (registry pull blocked by Zscaler on my network)
Proxy:       LiteLLM, local instance, Anthropic-format /v1/messages front end
Client:      Claude Code, pointed at the local proxy instead of api.anthropic.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I've run this pattern before: point Claude Code at a local proxy instead of Anthropic, let the proxy translate to whatever model is actually running. &lt;a href="https://dev.to/mediblacksand_f0ea36c53fb/one-terminal-two-trust-levels-running-claude-code-against-a-real-subscription-and-a-cheap-proxy-5c02"&gt;Already wrote that up&lt;/a&gt;, with a cloud model behind it. This wasn't my first local model either: Gemma4, Qwen2, and Qwen3 before this. None stuck. The problem was never inference speed. It was trust: I don't have time to fact-check every answer for hallucination on top of doing the actual work.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two fixable bugs, one that wasn't
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Bug 1: silent template fallback.&lt;/strong&gt; Building from a raw GGUF instead of &lt;code&gt;ollama pull&lt;/code&gt;ing the registry tag drops the chat-template metadata the manifest normally carries. Without it, Ollama falls back to bare prompt passthrough, no turn boundaries. Symptom: the model answers, keeps going, repeats the same answer, on a loop, because nothing tells it a turn ended. Fix: hand-write an explicit ChatML template plus a repeat penalty. Ollama applies neither by default when you skip the registry pull.&lt;/p&gt;

&lt;p&gt;Here's the part of the Modelfile that actually fixed it, trimmed to the load-bearing pieces:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jinja"&gt;&lt;code&gt;TEMPLATE """&lt;span class="cp"&gt;{{&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nv"&gt;if&lt;/span&gt; &lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;Messages&lt;/span&gt; &lt;span class="cp"&gt;}}&lt;/span&gt;
&lt;span class="cp"&gt;{{&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nv"&gt;range&lt;/span&gt; &lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;Messages&lt;/span&gt; &lt;span class="cp"&gt;}}&lt;/span&gt;
&lt;span class="cp"&gt;{{&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nv"&gt;if&lt;/span&gt; &lt;span class="nv"&gt;eq&lt;/span&gt; &lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;Role&lt;/span&gt; &lt;span class="s2"&gt;"user"&lt;/span&gt; &lt;span class="cp"&gt;}}&lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;&lt;/span&gt;&lt;span class="err"&gt;|&lt;/span&gt;&lt;span class="na"&gt;im_start&lt;/span&gt;&lt;span class="err"&gt;|&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;user
&lt;span class="cp"&gt;{{&lt;/span&gt; &lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;Content&lt;/span&gt; &lt;span class="cp"&gt;}}&lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;&lt;/span&gt;&lt;span class="err"&gt;|&lt;/span&gt;&lt;span class="na"&gt;im_end&lt;/span&gt;&lt;span class="err"&gt;|&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="cp"&gt;{{&lt;/span&gt; &lt;span class="nv"&gt;else&lt;/span&gt; &lt;span class="nv"&gt;if&lt;/span&gt; &lt;span class="nv"&gt;eq&lt;/span&gt; &lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;Role&lt;/span&gt; &lt;span class="s2"&gt;"assistant"&lt;/span&gt; &lt;span class="cp"&gt;}}&lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;&lt;/span&gt;&lt;span class="err"&gt;|&lt;/span&gt;&lt;span class="na"&gt;im_start&lt;/span&gt;&lt;span class="err"&gt;|&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;assistant
&lt;span class="cp"&gt;{{&lt;/span&gt; &lt;span class="nv"&gt;if&lt;/span&gt; &lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;Content&lt;/span&gt; &lt;span class="cp"&gt;}}{{&lt;/span&gt; &lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;Content&lt;/span&gt; &lt;span class="cp"&gt;}}&lt;/span&gt;
&lt;span class="cp"&gt;{{&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nv"&gt;else&lt;/span&gt; &lt;span class="nv"&gt;if&lt;/span&gt; &lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;ToolCalls&lt;/span&gt; &lt;span class="cp"&gt;}}&lt;/span&gt;
&lt;span class="cp"&gt;{{&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nv"&gt;range&lt;/span&gt; &lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;ToolCalls&lt;/span&gt; &lt;span class="cp"&gt;}}&lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;tool_call&amp;gt;&lt;/span&gt;
{"name": "&lt;span class="cp"&gt;{{&lt;/span&gt; &lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;Function.Name&lt;/span&gt; &lt;span class="cp"&gt;}}&lt;/span&gt;", "arguments": &lt;span class="cp"&gt;{{&lt;/span&gt; &lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;Function.Arguments&lt;/span&gt; &lt;span class="cp"&gt;}}&lt;/span&gt;}
&lt;span class="nt"&gt;&amp;lt;/tool_call&amp;gt;&lt;/span&gt;
&lt;span class="cp"&gt;{{&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nv"&gt;end&lt;/span&gt; &lt;span class="cp"&gt;}}&lt;/span&gt;
&lt;span class="cp"&gt;{{&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nv"&gt;end&lt;/span&gt; &lt;span class="cp"&gt;}}&lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;&lt;/span&gt;&lt;span class="err"&gt;|&lt;/span&gt;&lt;span class="na"&gt;im_end&lt;/span&gt;&lt;span class="err"&gt;|&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="cp"&gt;{{&lt;/span&gt; &lt;span class="nv"&gt;end&lt;/span&gt; &lt;span class="cp"&gt;}}&lt;/span&gt;
&lt;span class="cp"&gt;{{&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nv"&gt;end&lt;/span&gt; &lt;span class="cp"&gt;}}&lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;&lt;/span&gt;&lt;span class="err"&gt;|&lt;/span&gt;&lt;span class="na"&gt;im_start&lt;/span&gt;&lt;span class="err"&gt;|&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;assistant
&lt;span class="cp"&gt;{{&lt;/span&gt; &lt;span class="nv"&gt;end&lt;/span&gt; &lt;span class="cp"&gt;}}&lt;/span&gt;"""
PARAMETER repeat_penalty 1.3
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;&amp;lt;|im_start|&amp;gt;&lt;/code&gt;/&lt;code&gt;&amp;lt;|im_end|&amp;gt;&lt;/code&gt; pairs are the turn boundaries the raw GGUF was missing. The &lt;code&gt;&amp;lt;tool_call&amp;gt;&amp;lt;/tool_call&amp;gt;&lt;/code&gt; XML tags are Hermes-style, matching what Ornith actually generates. That sets up the mismatch below: Ollama can render this template fine, but has nothing built in to parse a &lt;code&gt;&amp;lt;tool_call&amp;gt;&lt;/code&gt; block back into a real, executable tool call the way &lt;code&gt;--tool-call-parser qwen3_xml&lt;/code&gt; does under vLLM.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Bug 2: an unsupported capability request.&lt;/strong&gt; Once output was clean, Claude Code didn't recognize the custom model ID and defaulted to requesting extended thinking on every call. Ollama rejected it outright: &lt;code&gt;"does not support thinking"&lt;/code&gt;, since the Modelfile never declared that capability. One env var fixed it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;MAX_THINKING_TOKENS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;The one that wasn't fixable in an afternoon: tool calls that were never real tool calls.&lt;/strong&gt; First agentic prompt looked like it worked. Claude Code showed what looked exactly like a file-write tool call. The file never existed on disk. The model was generating text shaped like a tool call, not a structured one Ollama could parse and execute.&lt;/p&gt;

&lt;p&gt;Root cause: Ornith is Qwen-derived, so it speaks Qwen's Hermes-style XML tool-call format. vLLM handles this with one flag: &lt;code&gt;--tool-call-parser qwen3_xml&lt;/code&gt;. Ollama has no equivalent. The tool-schema injection and the parse-back-into-a-real-call logic have to be hand-written into the Modelfile template.&lt;/p&gt;

&lt;p&gt;This isn't unique to a manual GGUF build. A &lt;a href="https://github.com/ollama/ollama/issues/16992" rel="noopener noreferrer"&gt;GitHub issue on ollama/ollama&lt;/a&gt; shows the same raw, unparsed tool-call XML on the official registry tag. Other write-ups on running Ornith locally list this as the most common complaint about the model on Ollama.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this matters beyond one model
&lt;/h2&gt;

&lt;p&gt;A model that confidently emits the exact right shape of a tool call, then does nothing, is much harder to catch than an outright error. If I hadn't checked the filesystem directly, I'd have assumed the loop was working.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where this leaves the local tier
&lt;/h2&gt;

&lt;p&gt;The tool-call mismatch is real and documented by other Ornith-on-Ollama users, not just my setup. The manual GGUF build doesn't look like the deciding factor, since the linked issue hits the same wall on the official tag.&lt;/p&gt;

&lt;p&gt;Not chasing this urgently. The pattern I rely on, Claude Code through a translating proxy instead of Anthropic directly, &lt;a href="https://dev.to/mediblacksand_f0ea36c53fb/one-terminal-two-trust-levels-running-claude-code-against-a-real-subscription-and-a-cheap-proxy-5c02"&gt;already runs day to day&lt;/a&gt; on a cloud model. This was the next test on top of that: same client, same proxy, swap the destination for something fully on-device. Didn't work this round. Same lesson a &lt;a href="https://dev.to/mediblacksand_f0ea36c53fb/the-bare-minimum-ai-agent-stack-picoclaw-local-llm-testing-and-why-i-still-chose-a-cloud-model-h20"&gt;monitoring-agent build I wrote up earlier&lt;/a&gt; reached from a different angle: the wiring is rarely the hard part. The hard part is deciding how much to trust what's on the other end.&lt;/p&gt;

&lt;p&gt;If a solid local model shows up that I can use and trust to do tasks such as write a file to disk, I'll test it properly and report back.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>ollama</category>
      <category>opensource</category>
      <category>llm</category>
    </item>
    <item>
      <title>One terminal, two trust levels — running Claude Code against a real subscription and a cheap proxy</title>
      <dc:creator>MediBlackSand</dc:creator>
      <pubDate>Mon, 17 Aug 2026 00:03:06 +0000</pubDate>
      <link>https://dev.to/mediblacksand_f0ea36c53fb/one-terminal-two-trust-levels-running-claude-code-against-a-real-subscription-and-a-cheap-proxy-5c02</link>
      <guid>https://dev.to/mediblacksand_f0ea36c53fb/one-terminal-two-trust-levels-running-claude-code-against-a-real-subscription-and-a-cheap-proxy-5c02</guid>
      <description>&lt;p&gt;&lt;em&gt;Part of an ongoing series on model routing and trust tiering for agentic coding tools. This one's the boring, working half — no bug hunt, just a setup that's been running clean across two machines.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem
&lt;/h2&gt;

&lt;p&gt;Claude Code does one thing well: careful, scoped edits with a real plan-then-execute loop behind them, backed by a subscription you're already paying for. Not every task needs that. Exploratory reads, "summarize this directory," draft-and-discard scratch work — most of that doesn't need the most capable model watching every token.&lt;/p&gt;

&lt;p&gt;The fix is a second, cheaper backend for that category of work. The catch: Claude Code only speaks Anthropic's Messages API. It has no built-in notion of "same tool, different model." So the question is how to point it somewhere else without giving up the interface.&lt;/p&gt;

&lt;h2&gt;
  
  
  The stack
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Trusted agent:  claude       — real Anthropic subscription, default session
Cheap agent:    claude-cheap — same CLI, routed through a self-hosted proxy
Proxy:          LiteLLM, translating Anthropic-format requests to
                DeepSeek V4 (pro for Sonnet-tier calls, flash for Haiku-tier)
                served through an OpenRouter API
Transport:      a persistent SSH tunnel from a small VPS back to each machine
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The proxy itself wasn't new. It's the same LiteLLM instance already routing a separate content pipeline I run. The actual work here was wiring Claude Code to it: a shell function and a few environment variables.&lt;/p&gt;

&lt;p&gt;The core trick and it took me a few week to learn this is to point &lt;code&gt;ANTHROPIC_BASE_URL&lt;/code&gt; at LiteLLM's &lt;code&gt;/v1/messages&lt;/code&gt; endpoint, not the OpenAI-compatible path LiteLLM also exposes. Claude Code only understands the Anthropic shape, so the OpenAI-shaped endpoint fails in ways that look like a client bug and aren't. Once LiteLLM sits on the right endpoint and translates underneath, Claude Code has no idea it isn't talking to Anthropic.&lt;/p&gt;

&lt;h2&gt;
  
  
  The one bug worth flagging
&lt;/h2&gt;

&lt;p&gt;Claude Code's Plan Mode attaches a &lt;code&gt;context_management&lt;/code&gt; parameter to its requests. Anthropic's API handles it. Most other backends don't recognize it and reject the whole request with a 400 — which looks like Plan Mode itself is broken, when it's a parameter the downstream model was never built to accept.&lt;/p&gt;

&lt;p&gt;One-line fix in the LiteLLM config:&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="s"&gt;litellm_config.yaml&lt;/span&gt;
&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="na"&gt;drop_params&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That tells LiteLLM to silently strip unsupported parameters instead of forwarding them and letting the backend reject the call. Plan Mode then works the same regardless of which model is actually answering.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two commands, deliberately asymmetric
&lt;/h2&gt;

&lt;p&gt;This is the part worth copying more than any proxy config: the trusted and cheap agents don't look the same, on purpose.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;claude&lt;/code&gt; — the real thing, full subscription, no wrapper. Default terminal, default prompt. It's the session where mistakes cost the most, so I want zero visual noise between me and what it's doing.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;claude-cheap&lt;/code&gt; — a shell function that drops into an isolated subshell, retitles the tab with a distinct label and icon, and resets on exit. Not aesthetics: at 11pm switching between six tabs, I want it structurally impossible to mistake the cheap, more permissive session for the one running on my subscription. The expensive tool gets no ceremony; the cheap tool gets a costume, because misidentifying that direction is the failure mode worth guarding against.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# --- Cheap agent: DeepSeek via self-hosted LiteLLM proxy ---
claude-cheap() {
  (
    unset ANTHROPIC_API_KEY
    export ANTHROPIC_BASE_URL=http://localhost:3456
    export ANTHROPIC_AUTH_TOKEN=anything
    export ANTHROPIC_MODEL=deepseek/deepseek-v4-pro
    export ANTHROPIC_DEFAULT_SONNET_MODEL=deepseek/deepseek-v4-pro
    export ANTHROPIC_DEFAULT_HAIKU_MODEL=deepseek/deepseek-v4-flash
    export CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1
    echo -ne "\033]0;🐋 DEEPSEEK-AGENT\007"
    claude "$@"
    echo -ne "\033]0;Terminal\007"
  )
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The subshell &lt;code&gt;( ... )&lt;/code&gt; is what makes the exports throwaway — they don't leak into the parent shell once the function returns. &lt;code&gt;ANTHROPIC_AUTH_TOKEN&lt;/code&gt; is set to a dummy value because LiteLLM doesn't check it; it just needs something present so Claude Code doesn't refuse to start. &lt;code&gt;CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC&lt;/code&gt; cuts calls back to Anthropic's own telemetry endpoints, since this session has no real Anthropic account behind it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why bother
&lt;/h2&gt;

&lt;p&gt;Because the goal was never "make Claude Code cheaper." It's a supervisor/executor split: the subscription session does planning, review, anything I'd be upset to see broken. The proxy session handles high-volume, low-stakes work, and its output gets reviewed before it's trusted the same way.&lt;/p&gt;

&lt;p&gt;Same shape of decision as picking a cloud model over a local one for a monitoring agent [earlier in this series] — not "which model is smarter," but "which failure mode can I tolerate, and what's the cheapest thing that clears the bar." Here the axis is subscription cost instead of on-device vs. cloud. The underlying question is identical: what am I willing to have wrong, and what's watching for when it is.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's still open
&lt;/h2&gt;

&lt;p&gt;There's also a local-only variant: same two-tier pattern, but the cheap tier runs entirely on-device instead of through a hosted proxy. That one hit a tool-calling format mismatch — a story of its own, I will write it up separately soon.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>claudecode</category>
      <category>llm</category>
      <category>devops</category>
    </item>
    <item>
      <title>I didn't trust I understood gradient descent, so I vibe coded a demo to play with it</title>
      <dc:creator>MediBlackSand</dc:creator>
      <pubDate>Wed, 12 Aug 2026 00:44:33 +0000</pubDate>
      <link>https://dev.to/mediblacksand_f0ea36c53fb/i-didnt-trust-i-understood-gradient-descent-so-i-vibe-coded-a-demo-to-play-with-it-2cde</link>
      <guid>https://dev.to/mediblacksand_f0ea36c53fb/i-didnt-trust-i-understood-gradient-descent-so-i-vibe-coded-a-demo-to-play-with-it-2cde</guid>
      <description>&lt;h2&gt;
  
  
  Contents
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;The problem&lt;/li&gt;
&lt;li&gt;The first attempt wasn't enough&lt;/li&gt;
&lt;li&gt;What closed the gap&lt;/li&gt;
&lt;li&gt;Where the value was&lt;/li&gt;
&lt;li&gt;Try it&lt;/li&gt;
&lt;li&gt;Closing thought&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The problem
&lt;/h2&gt;

&lt;p&gt;I'd just spent a few sessions in fast.ai's &lt;code&gt;04_mnist_basics.ipynb&lt;/code&gt; finally pinning down what a gradient actually means: not "3's gradient," but the slope of the loss curve at the exact point where the weight currently sits. Small distinction, but it's the one that had stayed fuzzy since high school calculus. Calculus itself still sounds like a foreign language to me 🥲.&lt;/p&gt;

&lt;p&gt;Understanding it in a chat window is one thing. Explaining it well enough for someone else to follow using the Feynman Method, felt like a different bar entirely. So the best way to learn it was to build something around it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The first attempt wasn't enough
&lt;/h2&gt;

&lt;p&gt;The obvious demo already exists in the fast.ai material: a ball rolling downhill on &lt;code&gt;y = x² − 4x + 7&lt;/code&gt;, a learning rate slider, watch it converge or fly off the curve. It's a fine five minute demo (crank the learning rate too high and the ball leaves the screen, which is memorable). I played with it in Colab and had Claude explain it to me, repeatedly, until it stuck. But it's still one number moving on one curve. Real models don't have one weight, they have thousands, and there's no curve you can draw for that.&lt;/p&gt;

&lt;h2&gt;
  
  
  What closed the gap
&lt;/h2&gt;

&lt;p&gt;The fast.ai chapter already had the answer, I just hadn't noticed it. Train a simple linear model on raw pixel values and the weights come out the same shape as the image. Draw the weights as a picture and you can watch them sharpen into something recognizable as training progresses.&lt;/p&gt;

&lt;p&gt;So I had Claude produce a javascript app around that idea. It took a few short tries to get right, but the core of it is: 8x8 pixel cat, dog, and rabbit silhouettes added later, a weight grid per class rendered as a heatmap, and three things students can do instead of just watch.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Draw their own creature on the pixel grid and label it, so "picture becomes numbers" stops being abstract&lt;/li&gt;
&lt;li&gt;Guess before the model does, with a running scoreboard of human versus model accuracy&lt;/li&gt;
&lt;li&gt;Train on one image at a time and see the actual arithmetic: pixel value, error, gradient = error times pixel, old weight, new weight, worked out for a handful of real pixels, not just a color changing on screen&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Toggling in a third class turned into a useful accident. With two classes, one weight grid can decide cat-or-not by itself. Add a third and that stops working: now each class needs its own weight grid, the model scores all three, and picks whichever comes back highest. That's the beginner version of what's called one-vs-rest classification. The next step up, softmax, does the same job but turns those scores into probabilities that add up to 100 percent, so instead of just picking a winner you get something like 70 percent cat, 20 percent dog, 10 percent rabbit. I didn't build that part, but watching three weight grids compete made the idea click in a way the term itself never did.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the value was
&lt;/h2&gt;

&lt;p&gt;Claude wrote all the code. I didn't need to worry about the code at all. I described what I wanted, looked at what came back, and pushed it in a different direction when it wasn't right. A few rounds of that.&lt;/p&gt;

&lt;p&gt;The value wasn't in writing JavaScript. It was in being forced to specify, precisely enough for another system to build it, what a correct demo of gradient descent for a linear pixel model actually looks like. The fast.ai notebook taught me the concept. Directing Claude through "no, the weight grid has to update after every single image, not after each epoch" is what actually pinned it down. Reading forces you to follow someone else's precision. Building, even by proxy, forces you to produce your own.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://mediblacksand.github.io/assets/gradient_descent_classroom_demo.html" rel="noopener noreferrer"&gt;Live demo →&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Runs entirely in the browser. No install, no backend. Draw something, guess, train it, watch the weight grid learn.&lt;/p&gt;

&lt;h2&gt;
  
  
  Closing thought
&lt;/h2&gt;

&lt;p&gt;The bet this demo makes is that watching numbers change on a screen isn't the same as understanding why they change, and that the fastest way to close that gap is to make something that explains it to someone else, even if an AI writes the code for you. Directing that build was harder than reading the notebook a third time would have been. That's probably the point.&lt;/p&gt;

&lt;p&gt;If you've used an AI agent to build a teaching tool for something you were still shaky on yourself, I'd like to hear what part of the build finally made it click.&lt;/p&gt;




&lt;p&gt;Part of the same AI-tools learning log as &lt;a href="https://dev.to/mediblacksand_f0ea36c53fb/i-built-two-ai-tools-the-second-one-told-me-how-i-should-be-learning-ai-5el0"&gt;I built two AI tools, the second one told me how I should be learning AI&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Find me on GitHub: &lt;a href="https://github.com/mediblacksand" rel="noopener noreferrer"&gt;github.com/mediblacksand&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>fastai</category>
      <category>machinelearning</category>
      <category>vibecoding</category>
    </item>
    <item>
      <title>DeepSeek V4 Flash Turned 45 Files Into 0 Bytes, Then Apologized</title>
      <dc:creator>MediBlackSand</dc:creator>
      <pubDate>Mon, 03 Aug 2026 05:41:12 +0000</pubDate>
      <link>https://dev.to/mediblacksand_f0ea36c53fb/deepseek-v4-flash-turned-45-files-into-0-bytes-then-apologized-1kc9</link>
      <guid>https://dev.to/mediblacksand_f0ea36c53fb/deepseek-v4-flash-turned-45-files-into-0-bytes-then-apologized-1kc9</guid>
      <description>&lt;p&gt;&lt;em&gt;I asked OpenCode + DeepSeek V4 Flash to strip metadata from 45 audiobook chapter files and rename them numerically. It did the rename correctly and then, unprompted, it decided a harmless leftover byte was a problem worth fixing, wrote its own binary parser, got the arithmetic wrong by roughly nine orders of magnitude, and turned every file into 0 bytes. I had to use a backup. A decade into fixing other people's IT problems, this is the first time an automated tool has forced me to reach for one over what should have been a small renaming job.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  In this article
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;The actual task&lt;/li&gt;
&lt;li&gt;Where it went right&lt;/li&gt;
&lt;li&gt;Where it went sideways&lt;/li&gt;
&lt;li&gt;The formula, and what it computed&lt;/li&gt;
&lt;li&gt;Why this is a different failure than the usual "agent did something unauthorized"&lt;/li&gt;
&lt;li&gt;Would plan mode have caught this? No, and that's the point&lt;/li&gt;
&lt;li&gt;What saved me&lt;/li&gt;
&lt;li&gt;The fix that worked was a different fix&lt;/li&gt;
&lt;li&gt;The lesson&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The actual task
&lt;/h2&gt;

&lt;p&gt;Small, mechanical, well-scoped: an audiobook player was ignoring chapter order on a 45-file book. Earlier fixes (rewriting ID3 track-number tags to match filename order) hadn't solved it, so the next step was more aggressive: strip every tag entirely and rename every file to a clean zero-padded number (&lt;code&gt;001.mp3&lt;/code&gt;, &lt;code&gt;002.mp3&lt;/code&gt;, ...), removing every possible source of conflicting sort information at once.&lt;/p&gt;

&lt;p&gt;Instead of me executing a python script, I handed this to OpenCode running DeepSeek V4 Flash (to save my Claude Code subscription for tougher jobs), with a Python script using &lt;code&gt;mutagen&lt;/code&gt; (a well-tested audio-tagging library) to do the actual stripping. This is exactly the kind of task a fast, cheap model tier should handle without supervision.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where it went right
&lt;/h2&gt;

&lt;p&gt;It did. The script ran, &lt;code&gt;mutagen&lt;/code&gt; stripped the ID3 tags cleanly, the files got renamed in the correct natural-sort order, and the audio data itself was untouched. Verified with &lt;code&gt;ffprobe&lt;/code&gt;: no tags, correct order, playable files. Task complete.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where it went sideways
&lt;/h2&gt;

&lt;p&gt;Here's the part that wasn't in the brief. &lt;code&gt;mutagen&lt;/code&gt;'s tag-deletion, like most ID3 libraries, doesn't shrink the file. It leaves a 10-byte empty ID3v2 header in place (the standard magic bytes plus a zeroed size field) rather than physically cutting those bytes out. This is completely normal. &lt;code&gt;ffprobe&lt;/code&gt; already confirmed zero tag frames. There was nothing wrong.&lt;/p&gt;

&lt;p&gt;The agent looked at the raw bytes, saw the ID3 magic string still present, and decided on its own that the audiobook player might be confused by it. Nobody asked for this. The brief was "strip tags and rename," not "also manually edit the binary structure of the files afterward." It went ahead anyway, wrote a Python snippet to manually locate and strip the ID3v2 header bytes from all 45 files, and ran it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The formula, and what it computed
&lt;/h2&gt;

&lt;p&gt;ID3v2 stores its tag size as a &lt;em&gt;sync-safe integer&lt;/em&gt;: 4 bytes, but only the low 7 bits of each byte are used (the high bit is reserved so the size field can never accidentally contain a byte sequence that looks like a frame sync marker). Decoding it correctly means shifting each byte independently:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# What it should have been:
&lt;/span&gt;&lt;span class="n"&gt;size&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="mh"&gt;0x7F&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;21&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="mh"&gt;0x7F&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;14&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="mh"&gt;0x7F&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;7&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;9&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="mh"&gt;0x7F&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What it actually wrote:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# What it actually wrote:
&lt;/span&gt;&lt;span class="n"&gt;size&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;j&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;size&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;size&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;j&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="mh"&gt;0x7F&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Looks close enough to pass a glance. It isn't. The loop keeps shifting the &lt;em&gt;entire accumulated value&lt;/em&gt; left by 7 on every iteration, instead of shifting each byte to its own fixed bit position and OR-ing them together. Four iterations of "shift the whole thing left by 7 again" compounds fast: the correctly-computed size for a real header (roughly 2.7 KB) came out as roughly &lt;strong&gt;2.6 GB&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The script then did the equivalent of &lt;code&gt;file_bytes = file_bytes[size:]&lt;/code&gt;, writing everything &lt;em&gt;after&lt;/em&gt; that computed offset back to disk. With an offset three orders of magnitude larger than the file itself, "everything after the offset" is nothing. All 45 files: 0 bytes.&lt;/p&gt;

&lt;p&gt;It caught itself, and said so, without me asking: "I'm sorry — I made a critical error in the binary ID3 stripping step. The sync-safe integer parsing was wrong, which caused all 45 files to be zeroed out." That's an accurate self-report of exactly what went wrong. It just arrived several steps too late to matter.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this is a different failure than the usual "agent did something unauthorized"
&lt;/h2&gt;

&lt;p&gt;I'd already run into a smaller version of this with the same tools: OpenCode killed an SSH tunnel it never started, as an unrequested "cleanup" flourish on session sign-off. That was a &lt;em&gt;scope&lt;/em&gt; failure. The agent did something real and functional, just something nobody asked for.&lt;/p&gt;

&lt;p&gt;This one is a &lt;em&gt;correctness&lt;/em&gt; failure wearing a scope failure's clothes. The scope violation (writing a manual binary parser nobody asked for) is what created the opportunity, but the actual damage came from wrong arithmetic executed with full confidence and zero self-check. A &lt;code&gt;print(size)&lt;/code&gt; before writing anything back would have shown a number in the billions for a few-kilobyte file and ended this in about two seconds. It didn't happen. The model went straight from "I've identified a problem" to "I've written the fix" to "I've applied the fix," with no checkpoint in between where an obviously-insane intermediate value could have been caught.&lt;/p&gt;

&lt;p&gt;That's the part worth sitting with: tighter permissions (the fix for the tunnel incident) wouldn't have stopped this. The agent had every right to write and run Python in this project. The failure isn't "it did something it shouldn't have been allowed to do." It's "it was allowed to do exactly this, and got the math wrong, and nothing forced it to check its own output before committing to it."&lt;/p&gt;

&lt;h2&gt;
  
  
  Would plan mode have caught this? No, and that's the point
&lt;/h2&gt;

&lt;p&gt;I use plan mode as a matter of habit: review the proposed approach, confirm it, then let it execute. It's a good practice, and I used it here: the metadata-strip-and-rename plan was reviewed and approved before anything ran.&lt;/p&gt;

&lt;p&gt;But plan mode reviews &lt;em&gt;the plan you approved&lt;/em&gt;. The binary "fix" wasn't part of that plan. It didn't exist yet when I reviewed anything. It was invented mid-session, after the approved task had already finished successfully, as the model's own unprompted follow-up to a problem that wasn't real. There's no review checkpoint for an action that hasn't been proposed yet.&lt;/p&gt;

&lt;p&gt;So no, I don't think I could have caught this by reviewing more carefully. That's not a comfortable conclusion, but I'd rather say it plainly than pretend the fix is "pay closer attention next time." The actual gap is structural: a session can complete an approved plan and then keep going, and there's no re-entry into plan-review for whatever it decides to do next on its own initiative.&lt;/p&gt;

&lt;h2&gt;
  
  
  What saved me
&lt;/h2&gt;

&lt;p&gt;I had another copy of the book to drop back into the folder. Once the files were real again, I moved the project to Claude Code and reran the whole pipeline, filename shortening and metadata stripping, in one pass. It came back clean, no manual byte-level detours, no surprises. That tracks with something I've now seen running both tools on real work: Claude Code's harness, the guardrails around what a model can do before it has to check in, is simply tighter than OpenCode's. Not a claim about DeepSeek versus Claude as models. A claim about how much rope each harness hands out before a self-directed action like the one that zeroed these files gets a chance to run.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix that worked was a different fix
&lt;/h2&gt;

&lt;p&gt;Here's the part that makes the whole binary detour feel almost beside the point in hindsight. Claude Code's first pass at the project picked up the same idea OpenCode had already tried: strip the metadata, clean up the filenames. It ran fine. It wasn't the fix that solved the chapter-order problem, though. What fixed it was Claude researching the actual problem and pointing me at &lt;code&gt;m4b-tool&lt;/code&gt;, then running its merge command to stitch all 45 chapter files into one continuous &lt;code&gt;.m4b&lt;/code&gt; audiobook file. One file, nothing left for any player to reorder, and it worked cleanly on the first try.&lt;/p&gt;

&lt;h2&gt;
  
  
  The lesson
&lt;/h2&gt;

&lt;p&gt;I went in circles on this for a while, because the obvious answers don't hold up. "Review the plan more carefully" doesn't work: the destructive step wasn't in any plan I reviewed. "Use a smarter model" might reduce the odds but doesn't get you to zero; any model can decide to fix a non-problem, and any model can get arithmetic wrong. "Restrict permissions" was the right fix for a &lt;em&gt;different&lt;/em&gt; incident (an agent killing a process it never started) but doesn't apply here. Writing and running Python was exactly what this task legitimately required.&lt;/p&gt;

&lt;p&gt;One more honest footnote, since it belongs in the retelling: none of the ID3 work ended up mattering. The chapter-order problem got solved by merging the files into one, not by anything to do with tags or byte offsets. The whole binary detour, and the files it cost, happened in service of a problem that had a completely different fix waiting the entire time.&lt;/p&gt;

&lt;p&gt;The lesson I landed on is smaller and less satisfying, but I think it's the honest one: &lt;strong&gt;I can't reliably predict or catch the moment an agent decides to do something beyond the brief, so the only durable fix is making sure that moment can't cost me anything irreplaceable.&lt;/strong&gt; Not better vigilance — better blast radius.&lt;/p&gt;

&lt;p&gt;Concretely, that means treating "the agent has write access to this folder" as equivalent to "I am comfortable losing everything in this folder right now," and building the habit &lt;em&gt;before&lt;/em&gt; the task starts, not after something goes wrong:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Work on a disposable copy, never the original, for anything that isn't already backed up elsewhere&lt;/li&gt;
&lt;li&gt;Snapshot or copy-before-write as a mechanical first step of the task, independent of how simple the task looks or how much I trust the model&lt;/li&gt;
&lt;li&gt;Treat "small renaming job" as no safer a category than anything else — this incident started as the single most boring, low-risk task in the whole project&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I hate audiobook chapter-order bugs enough that I'll keep automating this. I just won't point it at the only copy of anything again. That's the actual takeaway: not a smarter prompt, not a stricter review step. Just assume it can invent something you didn't ask for, and make sure that's survivable by default.&lt;/p&gt;




</description>
      <category>ai</category>
      <category>opencode</category>
      <category>python</category>
      <category>debugging</category>
    </item>
    <item>
      <title>I Configured Claude Desktop's File Access Twice. It Was the Same Setting Both Times.</title>
      <dc:creator>MediBlackSand</dc:creator>
      <pubDate>Mon, 03 Aug 2026 04:30:20 +0000</pubDate>
      <link>https://dev.to/mediblacksand_f0ea36c53fb/i-configured-claude-desktops-file-access-twice-it-was-the-same-setting-both-times-46lo</link>
      <guid>https://dev.to/mediblacksand_f0ea36c53fb/i-configured-claude-desktops-file-access-twice-it-was-the-same-setting-both-times-46lo</guid>
      <description>&lt;p&gt;&lt;em&gt;File access in Claude Desktop's chat mode had been flaky for a while: sometimes there, sometimes not, no obvious pattern. The standard fix, the one Claude itself gives you in a plain chat, is to hand-edit &lt;code&gt;claude_desktop_config.json&lt;/code&gt;. I did that, and before I'd even confirmed it worked on its own, found something else: the same Filesystem settings, filed under two completely different sidebar labels, with nothing anywhere saying they're the same page. I only noticed because I'd enabled an unrelated extension that gives Claude direct control of the Mac, and used it to go audit the settings properly.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  In this article
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;The question I actually needed answered&lt;/li&gt;
&lt;li&gt;The fix that doesn't get you far&lt;/li&gt;
&lt;li&gt;One page, two sidebar labels&lt;/li&gt;
&lt;li&gt;What the audit found&lt;/li&gt;
&lt;li&gt;GitHub: the bug that decided the architecture&lt;/li&gt;
&lt;li&gt;Looks right isn't the same as works&lt;/li&gt;
&lt;li&gt;So, do you actually know?&lt;/li&gt;
&lt;li&gt;What's next&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The question I actually needed answered
&lt;/h2&gt;

&lt;p&gt;I run a personal ops wiki, mostly read and written by Claude, across two Macs. File access from Claude Desktop's plain chat mode never felt fully trustworthy. It would work fine one session and fail the next, with no config change I was aware of in between. The easy answer is "just use Cowork mode instead," and that's fine advice, but it's a workaround, not a diagnosis. I wanted to know what was actually configured before deciding the lighter chat-mode access wasn't worth fixing.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix that doesn't get you far
&lt;/h2&gt;

&lt;p&gt;Ask Claude in a plain chat how to set this up, and it points you at &lt;code&gt;claude_desktop_config.json&lt;/code&gt;: the classic &lt;code&gt;mcpServers&lt;/code&gt; JSON block, hand-edited, one entry per server. That's the answer in most guides.&lt;/p&gt;

&lt;p&gt;I wrote a scoped filesystem entry and restarted. What I didn't do, and should have, was check whether that entry was actually controlling access, versus something already running underneath it. I never isolated a clean test of "JSON entry alone, nothing else present." Going to check what was actually granting access, I found a second settings screen already live and already configured, with no idea how long it had been running or how it related to what I'd just written.&lt;/p&gt;

&lt;h2&gt;
  
  
  One page, two sidebar labels
&lt;/h2&gt;

&lt;p&gt;Finding a settings screen I didn't know existed was reason enough to stop guessing. I had Claude use a separate extension, enabled earlier for unrelated automation and giving it direct AppleScript and shell control of the Mac, to read every settings file involved rather than trust what any single screen claimed.&lt;/p&gt;

&lt;p&gt;What that turned up wasn't three independent systems fighting each other. It was more specifically annoying than that: &lt;strong&gt;the same Filesystem settings screen, reachable from two entirely different places in the sidebar, styled identically, with no cross-reference telling you they're the same page.&lt;/strong&gt; Navigate in via &lt;code&gt;Desktop app → Extensions → Filesystem&lt;/code&gt; and you land on a page. Navigate in via &lt;code&gt;Customize → Connectors → Filesystem&lt;/code&gt; and you land on the exact same page: same toggle, same two allowed directories, same tool-permission list, pixel for pixel. Two doors, one room, and nothing on either door mentions the other.&lt;/p&gt;

&lt;p&gt;Underneath that is a genuinely separate second thing: the raw &lt;code&gt;claude_desktop_config.json&lt;/code&gt; &lt;code&gt;mcpServers&lt;/code&gt; block, which is the old way of doing this and is still correct for anything Anthropic hasn't folded into the unified screen (a third-party server like GitHub's, for instance).&lt;/p&gt;

&lt;p&gt;So the real shape isn't three settings surfaces. It's one legacy JSON method and one current GUI system that Anthropic's own navigation apparently can't decide whether to call "Extensions" or "Connectors," filing the identical screen under both mid-rename, with nothing explaining the overlap. If you've used Claude Desktop across months of incremental UI changes, that's exactly the setup for believing you configured two different things when you configured one thing twice, through two different-looking menus, at two different points in time.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the audit found
&lt;/h2&gt;

&lt;p&gt;Filesystem access was already being controlled by the GUI screen before I ever wrote the JSON entry, set at some earlier point I don't have a clean memory of, to a longer list of folders than the two I actually wanted. Old folders from old projects, added one at a time over months and never reviewed as a whole. Nothing dramatic in the contents. Just more than intended, with no memory of deliberately choosing all of it.&lt;/p&gt;

&lt;p&gt;The part that actually mattered: the JSON entry I'd just written was now sitting live in &lt;code&gt;mcpServers&lt;/code&gt; at the same time as the GUI screen's own config, and I had no idea whether the JSON one was doing anything, doing something partial, or fully inert. I didn't want to find out by trial and error what two separate filesystem MCP servers both claiming the same job actually produces. Best guess: duplicate tools in the picker, maybe ambiguous tool selection if both register something like &lt;code&gt;read_file&lt;/code&gt;. That's a guess, not a confirmed failure mode. So I removed the uncertainty instead of resolving it: reverted the JSON entry back to an empty &lt;code&gt;mcpServers: {}&lt;/code&gt;, and let the GUI screen be the only thing live.&lt;/p&gt;

&lt;p&gt;The real risk here isn't that the JSON method is dead, which I can't claim to have proven either way. It's that if the Extensions-labeled view and the Connectors-labeled view of the same feature ever show &lt;em&gt;different&lt;/em&gt; folder lists, not just two doors to one room but two doors that have genuinely drifted apart, nothing in the UI tells you which one is real, or that they've diverged at all. That's worth actually checking side by side rather than assuming they're in sync.&lt;/p&gt;

&lt;h2&gt;
  
  
  GitHub: the bug that decided the architecture
&lt;/h2&gt;

&lt;p&gt;Separately, I wanted GitHub push access. Anthropic has a native GitHub Integration connector: Settings → Connectors, click, authorize, done. The obvious path.&lt;/p&gt;

&lt;p&gt;Except as of late July 2026, there's an &lt;a href="https://github.com/anthropics/claude-code/issues/80874" rel="noopener noreferrer"&gt;open, unresolved bug&lt;/a&gt; where that connector's OAuth grants read access fine, but every write operation fails with &lt;code&gt;403 Resource not accessible by integration&lt;/code&gt;, including &lt;code&gt;git push&lt;/code&gt;. Confirmed, reported, still open when I checked. Flip that toggle without checking, and you get a connector that looks connected and silently fails the one thing you needed it for.&lt;/p&gt;

&lt;p&gt;The workaround: GitHub's own official MCP server, run locally via Docker, authenticated with a fine-grained personal access token instead of Anthropic's GitHub App.&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;"mcpServers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"github"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"docker"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"args"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"run"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"-i"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"--rm"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"-e"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"GITHUB_PERSONAL_ACCESS_TOKEN"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ghcr.io/github/github-mcp-server"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"env"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"GITHUB_PERSONAL_ACCESS_TOKEN"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&amp;lt;fine-grained PAT&amp;gt;"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This one genuinely belongs in &lt;code&gt;mcpServers&lt;/code&gt;. GitHub isn't shipped as a Claude Extension, so the old method is the correct method here, which is its own small trap: the right place to configure something depends entirely on whether Anthropic happened to ship it as an Extension, and nothing tells you that up front either.&lt;/p&gt;

&lt;h2&gt;
  
  
  Looks right isn't the same as works
&lt;/h2&gt;

&lt;p&gt;Given how wrong "it looks configured" turned out to be for filesystem access, GitHub got three separate checks before I trusted it.&lt;/p&gt;

&lt;p&gt;Does the token actually authenticate:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &lt;/span&gt;&lt;span class="nv"&gt;$TOKEN&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; https://api.github.com/user
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Does it have write permission on a real repo, not just a valid token:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &lt;/span&gt;&lt;span class="nv"&gt;$TOKEN&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; https://api.github.com/repos/me/my-repo
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I checked the &lt;code&gt;permissions&lt;/code&gt; object for &lt;code&gt;"push": true&lt;/code&gt; specifically.&lt;/p&gt;

&lt;p&gt;Did the actual Claude Desktop process connect cleanly, not just a manual test, by tailing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;~/Library/Logs/Claude/mcp-server-github.log
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Looking for &lt;code&gt;readOnly=false&lt;/code&gt;, &lt;code&gt;lockdownEnabled=false&lt;/code&gt;, and a clean &lt;code&gt;Server started and connected successfully&lt;/code&gt; after a real restart.&lt;/p&gt;

&lt;h2&gt;
  
  
  So, do you actually know?
&lt;/h2&gt;

&lt;p&gt;If you're running Claude Desktop and think you know what has file access right now, here's the actual answer, not the guide's answer.&lt;/p&gt;

&lt;p&gt;Check both &lt;code&gt;Desktop app → Extensions → Filesystem&lt;/code&gt; and &lt;code&gt;Customize → Connectors → Filesystem&lt;/code&gt;, and compare what each one shows. For me, after correcting the folder list, they matched, consistent with one underlying screen filed under two labels. I haven't verified that's guaranteed across every install, so check your own rather than take my word for it. If yours show different folders on each, that's the version of this that actually bites: two settings both plausibly "the real one," disagreeing, with nothing telling you which one Claude is using.&lt;/p&gt;

&lt;p&gt;Then check the JSON method isn't also quietly in the mix:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; ~/Library/Application&lt;span class="se"&gt;\ &lt;/span&gt;Support/Claude/claude_desktop_config.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If a &lt;code&gt;filesystem&lt;/code&gt; key shows up under &lt;code&gt;mcpServers&lt;/code&gt;, that's a third thing potentially live at once. I don't know what happens when it and the GUI screen are both active, and I chose not to find out. I'd remove it rather than test it.&lt;/p&gt;

&lt;p&gt;For anything Anthropic hasn't folded into the unified screen, a third-party MCP server without its own Extension, the JSON file is the correct, current place, since there's no GUI card for it at all.&lt;/p&gt;

&lt;p&gt;Whether Extensions and Connectors are one data store behind two doors, two stores kept in sync, or two stores that can silently drift, I only have direct evidence for my own case, and it settled into consistent once corrected. What I can say with more confidence: nothing in the product tells you which of those it is, and that's worth checking for yourself rather than taking on faith.&lt;/p&gt;

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

&lt;p&gt;Everything sorted here is local-Mac config. The bigger, still-untouched piece is a custom MCP server meant to run on a home Proxmox LXC: a small TypeScript service exposing read-only search and lint tools over the network, reached via an &lt;code&gt;mcp-remote&lt;/code&gt; stdio bridge rather than Anthropic's Custom Connectors, which require public internet reachability and aren't a fit for a home LAN. That's a design doc right now, zero code written. Whether that setup turns out to have two doors as well feels like a safe bet.&lt;/p&gt;

&lt;p&gt;If you've dug into your own Claude Desktop settings and found something that didn't match what you thought was configured, I'd like to hear what it was.&lt;/p&gt;




</description>
      <category>ai</category>
      <category>mcp</category>
      <category>macos</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Local LLMs for an Infra-Monitoring Agent: The Ollama think Bug, and Why I Still Chose a Cloud Model</title>
      <dc:creator>MediBlackSand</dc:creator>
      <pubDate>Tue, 21 Jul 2026 00:35:10 +0000</pubDate>
      <link>https://dev.to/mediblacksand_f0ea36c53fb/the-bare-minimum-ai-agent-stack-picoclaw-local-llm-testing-and-why-i-still-chose-a-cloud-model-h20</link>
      <guid>https://dev.to/mediblacksand_f0ea36c53fb/the-bare-minimum-ai-agent-stack-picoclaw-local-llm-testing-and-why-i-still-chose-a-cloud-model-h20</guid>
      <description>&lt;p&gt;&lt;em&gt;OpenClaw went from a weekend project to one of the most-starred repos on GitHub in under five months, and now everyone's using it to run their inbox, their calendar, their whole digital life. I wanted the opposite: the smallest possible slice of that ecosystem, running local-first, doing one boring job well: infrastructure monitoring. This is what happened when I actually tried to build that, including an undocumented Ollama bug that ate an evening.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  In this article
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;The smaller slice I actually wanted&lt;/li&gt;
&lt;li&gt;The stack, bare minimum on purpose&lt;/li&gt;
&lt;li&gt;The Ollama think bug, and the actual fix&lt;/li&gt;
&lt;li&gt;Hallucination, not capability, was the real blocker&lt;/li&gt;
&lt;li&gt;Why this matters beyond one setup&lt;/li&gt;
&lt;li&gt;What's next&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The smaller slice I actually wanted
&lt;/h2&gt;

&lt;p&gt;OpenClaw is the reason "AI agent" stopped meaning a chatbot and started meaning something that reads your email, files your GitHub issues, reschedules your calendar, and runs semi-autonomously through a Discord or Telegram interface. It's impressive: full agent fleets, OAuth into a dozen services, voice mode, phone apps. It's also the wrong shape for something small enough to trust running unattended near infrastructure I'm responsible for.&lt;/p&gt;

&lt;p&gt;That's what pulled me toward PicoClaw, a much leaner, CLI-first agent runtime that keeps the same core idea (a model, a toolset, a sandboxed workspace, a channel to talk to it through) without the sprawl. No inbox integration, no calendar, no dozen-service OAuth surface. Just an agent loop pointed at a workspace folder and a set of tools I explicitly allow.&lt;/p&gt;

&lt;p&gt;The install was the easy part. The real work started with deciding which model to actually trust running inside it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The stack, bare minimum on purpose
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Agent runtime:  PicoClaw (CLI, no web UI, no Docker)
Tier 1 local:   tested — not adopted yet (see below)
Tier 2 local:   Qwen3, no-think config
Tier 3 cloud:   DeepSeek V4 Flash, via a self-hosted LiteLLM proxy
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;"No Docker" up there isn't a stylistic choice. I tried the Docker install first, since that's what most of the setup guides default to. On Mac, PicoClaw's Docker path expects the model endpoint at &lt;code&gt;host.docker.internal&lt;/code&gt;, and getting that to line up with a local Ollama instance and a tunneled cloud endpoint at the same time turned into more plumbing than it was worth. The native binary just uses &lt;code&gt;localhost&lt;/code&gt; for everything. Docker is abandoned for this setup entirely now just for simplicity. &lt;/p&gt;

&lt;p&gt;PicoLM (a tiny, instant-response local model meant for trivial one-word answers) is sitting installed and untouched. I don't have a use case for it yet in an infra-monitoring context; "what time is it" isn't the problem I'm solving. It's on the list to revisit once there's an actual low-stakes, high-frequency task worth routing to something that fast.&lt;/p&gt;

&lt;p&gt;The real work went into everything above tier 1.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Ollama think bug, and the actual fix
&lt;/h2&gt;

&lt;p&gt;I went in expecting a local model (something like Gemma4 or Qwen3, running entirely on-device) to be good enough for real analysis work. A lot of people online are running exactly that combination successfully for agent tool-use, and I don't doubt them. I tried several variants of Gemma4 and a few Qwen builds, specifically stress-testing tool-calling reliability rather than just chat quality, since an agent that can't reliably invoke &lt;code&gt;read_file&lt;/code&gt; or &lt;code&gt;run_command&lt;/code&gt; is useless no matter how articulate its prose is.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Reasoning mode has to be turned off, and not the way you'd expect.&lt;/strong&gt; Qwen3's default "thinking" output is great for open-ended reasoning and actively harmful for an agent loop that expects a clean, immediate tool call: the model talks itself in circles before ever calling anything. The obvious fix is a runtime flag:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;PARAMETER think false
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That throws &lt;code&gt;Error: unknown parameter 'think'&lt;/code&gt;. Ollama's Modelfile syntax doesn't support it at all, despite it looking exactly like every other &lt;code&gt;PARAMETER&lt;/code&gt; line that does work. It's not documented anywhere obvious, and it's an easy hour to lose assuming you've got a typo.&lt;/p&gt;

&lt;p&gt;The actual fix has to happen in the prompt template, not the parameters block. You build a custom model from a Modelfile whose &lt;code&gt;TEMPLATE&lt;/code&gt; does three specific things:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Appends &lt;code&gt;/no_think&lt;/code&gt; to every single user message before it reaches the model&lt;/li&gt;
&lt;li&gt;Strips any &lt;code&gt;&amp;lt;think&amp;gt;...&amp;lt;/think&amp;gt;&lt;/code&gt; block out of how assistant responses get rendered&lt;/li&gt;
&lt;li&gt;Forces an empty &lt;code&gt;&amp;lt;think&amp;gt;\n\n&amp;lt;/think&amp;gt;&lt;/code&gt; pair at the start of every assistant turn, which signals to Qwen3 that the "thinking phase" is already done, so it goes straight to content or a tool call
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /tmp/qwen3-nothinker.modelfile &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;'
FROM qwen3:8b

TEMPLATE """
{{- if or .System .Tools }}&amp;lt;|im_start|&amp;gt;system
{{ .System }}
{{- end }}
{{- range .Messages }}
{{- if eq .Role "user" }}&amp;lt;|im_start|&amp;gt;user
{{ .Content }} /no_think&amp;lt;|im_end|&amp;gt;
{{- else if eq .Role "assistant" }}&amp;lt;|im_start|&amp;gt;assistant
{{ .Content }}&amp;lt;|im_end|&amp;gt;
{{- end }}
{{- end }}&amp;lt;|im_start|&amp;gt;assistant
&amp;lt;think&amp;gt;

&amp;lt;/think&amp;gt;

"""

PARAMETER repeat_penalty 1
PARAMETER temperature 0.6
PARAMETER top_k 20
PARAMETER top_p 0.95
&lt;/span&gt;&lt;span class="no"&gt;EOF

&lt;/span&gt;ollama create qwen3-local &lt;span class="nt"&gt;-f&lt;/span&gt; /tmp/qwen3-nothinker.modelfile
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;(trimmed for readability — the full template also handles tool-call formatting and multi-turn history)&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Then point the agent config at &lt;code&gt;qwen3-local&lt;/code&gt;, not the vanilla &lt;code&gt;qwen3:8b&lt;/code&gt; tag. The custom build is a separate named model in Ollama, so nothing about the fix is implicit. A quick &lt;code&gt;ollama run qwen3-local "hello"&lt;/code&gt; confirms there's no stray &lt;code&gt;&amp;lt;think&amp;gt;&lt;/code&gt; block leaking into the response before wiring it into the agent.&lt;/p&gt;

&lt;h2&gt;
  
  
  Hallucination, not capability, was the real blocker
&lt;/h2&gt;

&lt;p&gt;Once tool-calling was reliable, the real problem showed up: trusting the answer. On multi-step or multi-file analysis, local models would confidently report things that weren't there: files that didn't exist, log lines that didn't match, conclusions that sounded plausible and were simply wrong. For a general assistant that's an annoyance. For something judging whether infrastructure state looks normal, a confidently wrong answer is worse than no answer.&lt;/p&gt;

&lt;p&gt;That's the actual reason DeepSeek V4 Flash via a cloud API ended up as the tier-3 model. Not because local models are bad (plenty of people are getting Gemma4 and Qwen to work well for exactly this kind of agent), but because reliability and low hassle mattered more to me than keeping every call on-device. It's a pragmatic call, not a verdict on local models generally, and testing Gemma4 and Qwen further for lower-stakes tasks is still on the list.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this matters beyond one setup
&lt;/h2&gt;

&lt;p&gt;The bigger pattern here is one a lot of people building on the OpenClaw wave are going to hit eventually: the agent framework is rarely the hard part anymore. Wiring up a CLI agent, a sandbox, and a model is a weekend. The actual engineering is in the boring middle layer: deciding what a model is trustworthy enough to be handed, tier by tier, task by task, and being honest when a shinier local-only setup isn't actually the more reliable one.&lt;/p&gt;

&lt;p&gt;For infrastructure monitoring specifically, that boring middle layer matters more than usual. Wrong output in a chat app is a bad reply. Wrong output feeding an automated check against production infrastructure is a false sense of security, arguably worse than not automating it at all.&lt;/p&gt;

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

&lt;p&gt;The model-routing decisions above were the prep work, and they weren't the only surprise. The web-channel side of PicoClaw behaves differently from the CLI that tripped me, even different ways of picoclaw install on Macs which is worth its own writeup rather than a footnote here.&lt;/p&gt;

&lt;p&gt;The real test comes in Part 2: pointing this agent at a live domain health audit, cloud-brain doing the analysis. It didn't just confirm the setup worked. It surfaced real, ongoing configuration drift, the kind that accumulates in any environment over time: privileged group membership that had grown stale, a service running under an account it had no business running under, Group Policy scoped to the wrong group entirely. None of that was the point of the exercise, and all of it turned out to matter more than the exercise itself.&lt;/p&gt;

&lt;p&gt;Part 2 covers what the audit caught, what the agent got wrong, and whether the DeepSeek-for-reliability bet held up outside of clean test conditions.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Part 1 of a short series on building a minimal, local-first AI agent stack for infrastructure monitoring. Stack: PicoClaw (CLI-only), Qwen3 (local tier, no-think config), DeepSeek V4 Flash via a self-hosted LiteLLM proxy (cloud tier).&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>ollama</category>
      <category>selfhosted</category>
      <category>sysadmin</category>
    </item>
    <item>
      <title>I Was About to Cancel Claude. Now Gemini Is Rate-Limiting Me Out of My Own Plan.</title>
      <dc:creator>MediBlackSand</dc:creator>
      <pubDate>Thu, 25 Jun 2026 06:02:05 +0000</pubDate>
      <link>https://dev.to/mediblacksand_f0ea36c53fb/i-was-about-to-cancel-claude-now-gemini-is-rate-limiting-me-out-of-my-own-plan-215g</link>
      <guid>https://dev.to/mediblacksand_f0ea36c53fb/i-was-about-to-cancel-claude-now-gemini-is-rate-limiting-me-out-of-my-own-plan-215g</guid>
      <description>&lt;h2&gt;
  
  
  The Almost-Cancellation
&lt;/h2&gt;

&lt;p&gt;About six months ago I was one renewal away from cancelling Claude. Not because it got worse, because I just wasn't opening it. Rate limits I kept hitting on the rare day I did, and a Gemini 3 Pro that had just landed and felt better at almost everything I cared about: Python, JavaScript for the artistic coding side projects (a whole separate post, another day), general "what can this thing actually do" exploring.&lt;/p&gt;

&lt;p&gt;Gemini 3 Pro earned the roughly 30 AUD a month on its own. Add the Drive storage I was already half-paying for elsewhere and the image and video generation that was genuinely solid, not a tacked-on afterthought, and the upgrade looked obvious. I downgraded Claude in my head before I'd even opened the app to do it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Then Something Shifted
&lt;/h2&gt;

&lt;p&gt;Sometime this year that stopped being true. Not Gemini getting worse, Claude getting sharply better: coding accuracy, front-end output, MCP, agent behavior, all of it improving in a way I noticed week to week instead of release to release.&lt;/p&gt;

&lt;p&gt;I started running the same prompts through both, mostly out of curiosity at first. Then out of habit, because I kept trusting Claude's answer more often than not. Same task, same wording, and Claude's version was the one I'd actually ship.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Downgrade
&lt;/h2&gt;

&lt;p&gt;So I went the other way on Gemini. Dropped down to Google AI Plus, the cheap tier, around 15 AUD a month. I wasn't using it daily anymore, and I didn't want to keep paying full price for a tool that mostly sat there.&lt;/p&gt;

&lt;p&gt;These days Gemini is what I open for googling instead of the traditional "googling", or for the smaller stuff I don't want eating into my Claude usage. Nothing heavy. Nothing I'd call real work.&lt;/p&gt;

&lt;p&gt;And even at that, low-stakes level, I'm getting a lot more "I'm having a hard time fulfilling your request, can I help you with something else instead?" on requests that aren't unusual at all. Just now, checking back in to write this, I've lost access to the better model completely and landed in a flat rate limit. Not slower. Just off.&lt;/p&gt;

&lt;h2&gt;
  
  
  What This Actually Is
&lt;/h2&gt;

&lt;p&gt;I'm not saying Gemini is bad. Six months ago I'd have written the exact opposite post about Claude, and meant it just as much. What I'm noticing is smaller and more annoying than "which one wins": 15 AUD a month bought me a tool I can poke at, not one I can actually lean on. That's not really a capability problem. That's the cheap tier telling you, politely, that you're not the priority.&lt;/p&gt;

&lt;h2&gt;
  
  
  Closing Thought
&lt;/h2&gt;

&lt;p&gt;I didn't downgrade because Gemini got worse. I downgraded because I wasn't using it enough to justify the higher plan. But the cheap tier doesn't meet you at "occasional and light," it meets you at "barely worth keeping." Pay less and you don't just get less model, you get less patience for using it at all.&lt;/p&gt;




</description>
      <category>ai</category>
      <category>claude</category>
      <category>gemini</category>
      <category>opinion</category>
    </item>
    <item>
      <title>I Built Two AI Tools. The Second One Told Me How I Should Be Learning AI.</title>
      <dc:creator>MediBlackSand</dc:creator>
      <pubDate>Thu, 25 Jun 2026 01:46:26 +0000</pubDate>
      <link>https://dev.to/mediblacksand_f0ea36c53fb/i-built-two-ai-tools-the-second-one-told-me-how-i-should-be-learning-ai-5el0</link>
      <guid>https://dev.to/mediblacksand_f0ea36c53fb/i-built-two-ai-tools-the-second-one-told-me-how-i-should-be-learning-ai-5el0</guid>
      <description>&lt;h2&gt;
  
  
  The Problem
&lt;/h2&gt;

&lt;p&gt;TeachSim taught me LangGraph because the bot had to actually work, with real conversations running through it. GitHub Digest taught me about silent failure modes the same way, by breaking quietly until I went and figured out why. Both stuck because I needed the concept to function, not because I sat down and studied it first.&lt;/p&gt;

&lt;p&gt;I already tried the conventional route once. (It probably applies to everything I do and learn throughout my life.) I opened Anthropic's own intro course for Claude Code, the official one, and gave up a few lessons in having retained almost nothing. Nice material, no stakes, nothing to actually break, so nothing stuck.&lt;/p&gt;

&lt;p&gt;I want to go deeper into AI engineering now, not just orchestration around an API. Fast.ai for the fundamentals, then something heavier for the practitioner-level material. And I caught myself about to repeat the exact same pattern: open a course, start at lesson one, hope the urgency turns up eventually.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Discovery
&lt;/h2&gt;

&lt;p&gt;GitHub Digest is supposed to surface tools I wouldn't find by scrolling GitHub Trending. It's already done that for real: RTK, &lt;code&gt;sst/opencode&lt;/code&gt;, &lt;code&gt;playwright-mcp&lt;/code&gt;, and a course called AI Engineering From Scratch all came out of the pipeline, not a deliberate search.&lt;/p&gt;

&lt;p&gt;The course is the part that actually got to me. I've shipped two production-ish projects on Claude Code and OpenCode, and the entire depth of my knowledge of either tool is "enough slash commands and CLAUDE.md conventions to get something running." I've never deliberately learned MCP, skills, subagents, hooks, plugins, or checkpoints. I just use whichever tool happens to work that day.&lt;/p&gt;

&lt;p&gt;Stacking real ML fundamentals on top of that felt like a bad idea. I'd be fighting the tools and the material at the same time.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Setup
&lt;/h2&gt;

&lt;p&gt;Two machines, two networks I switch between. Claude Desktop with filesystem access is the constant supervisor on both: it reads the actual files, checks claims against source, catches scope creep before anything ships. OpenCode, tunneled to a small VPS running DeepSeek through OpenRouter, and the Claude Code VS Code extension are the interchangeable workers that do the typing.&lt;/p&gt;

&lt;p&gt;I'd been treating both workers as interchangeable without understanding what makes them different underneath. That's the actual gap.&lt;/p&gt;

&lt;p&gt;The supervisor role isn't theoretical, either. Yesterday's task started with the usual pre-flight check: start the tunnel, confirm it's up, then begin. Partway through, OpenCode, running on DeepSeek, killed the tunnel itself and took the whole pipeline down with it mid-task. Claude Code hasn't done anything like that so far. I've now got a "don't touch the tunnel process" rule that only applies to OpenCode, which is a guardrail I built by getting burned, not by reading ahead to Phase 4.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Plan
&lt;/h2&gt;

&lt;p&gt;Eight phases was designed pulling from three resources, claude-howto, Anthropic Academy's official course catalog, and opencode.school, none of those orderings made sense to follow as is so the curriculum runs on its own sequence instead:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Phase&lt;/th&gt;
&lt;th&gt;Theme&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;Foundations (a self-check, not a lesson — already know this)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;MCP — extend what the agent can reach&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Skills — formalize what it now knows how to use&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;Subagents — split work across multiple agents&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;Hooks — add guardrails before something destructive happens&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;Plugins — package it all into one install&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;Checkpoints — undo a bad session without losing real work&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;Final Results&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Each phase, I get to pick: solve a real problem in one of my live projects, or build against a notable open-source project picked because it's a strong production example, not a toy demo. For Phase 1, the real-project version is wiring GitHub's MCP server into GitHub Digest instead of hitting the REST API directly. The open-source version is installing &lt;code&gt;playwright-mcp&lt;/code&gt;, Microsoft's own browser-automation server, and pointing it at something real.&lt;/p&gt;

&lt;p&gt;I haven't done either yet. This is a design brief for a course I'm about to do slowly. The rule I'm holding myself to is that a phase isn't done when I've read about it, it's done when it produces a real fix in one of my actual projects.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why MCP First
&lt;/h2&gt;

&lt;p&gt;It's the one concept every later phase either uses or assumes you've used. Skills often wrap MCP calls. Subagents delegate to MCP-equipped agents. Hooks govern what an MCP server is allowed to touch. Start there and everything later has something concrete to point back at.&lt;/p&gt;

&lt;p&gt;It's also the most demonstrable. Watching an agent drive a real browser for the first time is something you &lt;em&gt;see&lt;/em&gt; happen. A permission setting from Phase 0 is correct and completely invisible. If I want this to survive contact with my own attention span, the visible payoff has to come first.&lt;/p&gt;

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

&lt;p&gt;Skills, subagents, hooks, plugins, checkpoints, in that order, against real projects wherever I can manage it. After that's the actual point: Fast.ai, then the heavier material, with the tools already out of the way instead of competing for attention with the harder stuff.&lt;/p&gt;

&lt;h2&gt;
  
  
  Closing Thought
&lt;/h2&gt;

&lt;p&gt;The bet is that learning the tools you already use makes you slower before it makes you faster. I could open OpenCode right now and ship something without understanding half of what's available to it. Spending real time on this first is a trade: less momentum this month for not relearning the same tool friction later, while I'm meant to be thinking about something harder. I'll find out if that's the right call once I've actually started.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;This is part of an ongoing series: TeachSim, GitHub Digest, and now the plan for the tool-mastery work sitting underneath the next phase of learning AI engineering properly.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Find me on GitHub: &lt;a href="https://github.com/mediblacksand" rel="noopener noreferrer"&gt;github.com/mediblacksand&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>claudecode</category>
      <category>opencode</category>
      <category>learning</category>
    </item>
    <item>
      <title>I Built a Personal Intelligence System That Curates GitHub and News for Me — Here's How It Works</title>
      <dc:creator>MediBlackSand</dc:creator>
      <pubDate>Mon, 22 Jun 2026 05:40:18 +0000</pubDate>
      <link>https://dev.to/mediblacksand_f0ea36c53fb/i-built-a-personal-intelligence-system-that-curates-github-and-news-for-me-heres-how-it-works-5fd4</link>
      <guid>https://dev.to/mediblacksand_f0ea36c53fb/i-built-a-personal-intelligence-system-that-curates-github-and-news-for-me-heres-how-it-works-5fd4</guid>
      <description>&lt;h1&gt;
  
  
  I Built a Personal Intelligence System That Curates GitHub and News for Me — Here's How It Works
&lt;/h1&gt;

&lt;p&gt;&lt;em&gt;A personal intelligence system that reads the news and scrapes GitHub for you, curates it with an LLM, and delivers it to your phone before you've finished your coffee. No dashboard. No login. Just three briefings, on a schedule you set.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem
&lt;/h2&gt;

&lt;p&gt;I'd already built TeachSim, a multi-agent system running on an LLM via OpenRouter, delivered entirely through Telegram, hosted on a budget VPS. Once that stack existed, the obvious question was what else it could power. The quickest win turned out to be information consumption itself.&lt;/p&gt;

&lt;p&gt;The actual itch was how I was consuming news. Google had been pushing me daily feeds for years: algorithmically chosen, entirely passive. I never decided what showed up, I just scrolled what arrived. I wanted to try the opposite, same infrastructure, a completely different job. An LLM doing the choosing instead of an engagement optimized feed, for both the news I read and the open-source ecosystem I found increasingly interested to keep a tab on day to day.&lt;/p&gt;

&lt;p&gt;That second half turned out to matter as much as the first. I don't have time to manually browse GitHub Trending every day, and Trending rewards absolute popularity over genuine novelty. The bet was that an LLM scanning broadly and judging by relevance rather than star count would surface things I'd never have found otherwise.&lt;/p&gt;

&lt;p&gt;It has. Real tools the pipeline has actually surfaced for me, not things I went looking for: &lt;a href="https://github.com/rtk-ai/rtk" rel="noopener noreferrer"&gt;RTK&lt;/a&gt;, &lt;code&gt;sst/opencode&lt;/code&gt;, and &lt;a href="https://aiengineeringfromscratch.com/" rel="noopener noreferrer"&gt;AI Engineering From Scratch&lt;/a&gt;, among others. That alone justified building it: the self-directed version surfaces tools I'd never have found through a popularity-ranked feed.&lt;/p&gt;

&lt;h2&gt;
  
  
  What GitHub Digest Is
&lt;/h2&gt;

&lt;p&gt;GitHub Digest is a personal automation system that delivers three scheduled briefings straight to Telegram:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A &lt;strong&gt;morning news briefing&lt;/strong&gt;: dozens of RSS feeds across world events, technology, AI research, and a few specialist categories, curated down to a handful of stories that fed into Deepseek V4 Flash and judged actually worth my attention that day.&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;GitHub Discovery digest&lt;/strong&gt;: a wide net of GitHub Search API queries surfacing repos I haven't seen before, tiered into "gem" (brand new, low stars, high velocity), "exploding" (star count accelerating fast), and "hot" (established but trending this week).&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;weekly Momentum briefing&lt;/strong&gt;: tracks a personal watchlist of tools over time and reports what changed, in plain language rather than a star-count delta.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Plus an on-demand command layer, so I can pull any of the three manually from my phone, check system health, or manage my watchlist without touching a terminal.&lt;/p&gt;

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



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Sources (RSS feeds / GitHub Search API / personal watchlist)
            │
            ▼
       Scrapers (per-source rate limiting, per-feed age windows)
            │
            ▼
       Analyzers (LLM curation — picks what's actually worth surfacing)
            │
            ▼
       Delivery (Telegram bot, chunked for plain-text limits)
            │
            ▼
   Always-on command listener (on-demand pulls, watchlist management)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three independent scrape-to-analyze-to-deliver pipelines, each on its own schedule, sharing a single delivery layer and a single listener process. All running on a single-core, 1GB budget VPS, which turns out to be the single biggest constraint shaping every other decision in this build.&lt;/p&gt;

&lt;p&gt;The curation step runs through an OpenRouter-routed LLM call: currently a fast, cheap model chosen specifically because curation needs consistency and low cost at high frequency (three runs a day, every day) rather than frontier-level reasoning.&lt;/p&gt;

&lt;h2&gt;
  
  
  Under the Hood
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The model's real output isn't always where you'd expect it, and this one fails silently.&lt;/strong&gt; The LLM doing curation sometimes returns its actual content in a &lt;code&gt;reasoning&lt;/code&gt; field instead of the standard &lt;code&gt;content&lt;/code&gt; field, depending on how it worked through the ranking task internally. The first time this happened, nothing crashed and no error appeared anywhere in the logs. The pipeline ran to completion, the cron job reported success, and the briefing that arrived on Telegram was just empty. That's a worse failure mode than a clean exception: an empty message looks like "nothing happened to be newsworthy today," not "something broke," so it took a few quiet mornings before the pattern was obvious enough to investigate. The fix is a fallback chain every analyzer checks in order, &lt;code&gt;content → reasoning_content → reasoning → text&lt;/code&gt;, rather than assuming the model will always populate the field the API docs imply it should. It's a one-line change once you know to make it, and an invisible one until you've been burned by it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cron doesn't speak the same language as your terminal.&lt;/strong&gt; Every scheduled job activates the Python virtual environment with &lt;code&gt;. venv/bin/activate&lt;/code&gt; rather than &lt;code&gt;source venv/bin/activate&lt;/code&gt;, and the reason is more fundamental than a style preference. &lt;code&gt;source&lt;/code&gt; is a bash builtin; cron runs jobs through &lt;code&gt;sh&lt;/code&gt;, not &lt;code&gt;bash&lt;/code&gt;, and &lt;code&gt;sh&lt;/code&gt; doesn't recognize it. The job had been tested manually dozens of times in an interactive terminal, where &lt;code&gt;source&lt;/code&gt; works fine because the terminal &lt;em&gt;is&lt;/em&gt; bash, so the activation line looked correct right up until it ran unattended for the first time, failed to activate the environment, and silently used whatever Python and packages happened to be on the system path instead of the ones the project actually depends on. No error, no crash, just a job quietly running against the wrong environment until something it depended on wasn't there. &lt;code&gt;.&lt;/code&gt; is the POSIX-portable equivalent and works in both shells. The fix is one character, but only once you understand why an interactive test can pass while the unattended version of the exact same command fails.&lt;/p&gt;

&lt;h2&gt;
  
  
  How Repos Actually Get Chosen
&lt;/h2&gt;

&lt;p&gt;"Show me what's new on GitHub" is a bad prompt on its own. You get either noise or the same dozen famous repos everyone already knows about. Getting to a daily handful that's actually worth reading took a few layers of filtering before an LLM ever sees a candidate.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Casting a wide net first.&lt;/strong&gt; The scraper runs a large batch of targeted search queries across several broad technical categories, each phrased to surface different kinds of repos: some queries hunt for brand-new activity, others for established projects with recent momentum, others for specific technical niches. No single query style finds everything; the combination is the point. The constraint shaping all of it: GitHub's search-specific endpoint caps out at 30 requests per minute, tighter than the general API limit and easy to blow through with this many categories firing in a loop. The scraper sleeps 2.1 seconds between calls, landing at roughly 28 requests a minute: close enough to the ceiling to finish a full run in a few minutes, far enough under it to never trip the limiter. A 403 anyway triggers a full 60-second backoff before retrying, rather than hammering an endpoint that just said slow down.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Code search hides the one number that matters.&lt;/strong&gt; GitHub's code-search endpoint is useful for finding repos by what's actually in them, not just their name or description, but the nested repository object it returns is missing the star count entirely. Every code-search hit needs a second, separate API call to fetch the full repo record before any tiering logic can run. Skip that step and half your candidates silently sort as zero-star nobodies.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tiering compares each repo against itself, not a fixed bar.&lt;/strong&gt; Three tiers, and none of them use a flat "must have N stars" threshold:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Gem&lt;/strong&gt;: very new (under two weeks old), low absolute stars, but high velocity relative to its own short life.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Exploding&lt;/strong&gt;: older, but its star count over the last comparison window is accelerating sharply versus its own recent trend.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hot&lt;/strong&gt;: established, but trending this week compared to its own historical baseline. A popular repo having an ordinary week doesn't qualify; one having an unusually active week does.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That last distinction matters: a repo with ten thousand stars and no LLM-relevant recent activity is no more interesting on a given day than one with two hundred. What's being measured is change against the repo's own pattern, not absolute popularity.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The LLM is told explicitly not to just rank by stars.&lt;/strong&gt; The curation prompt frames the task as picking what's meaningful: genuine novelty, technical relevance, and whether it's something I'd plausibly have missed without this pipeline. A repo near the top of the star-velocity list can still get passed over if it's a fork, a tutorial repo, or something with no real substance behind the trending number.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The weekly tracker measures change, not snapshots.&lt;/strong&gt; For repos on a personal watchlist, every run stores that day's stats and compares them against the last stored snapshot, so the weekly report is a genuine delta (stars gained, momentum direction) rather than a restated current state. The explanation style is deliberately written for someone encountering each tool for the first time, since a watchlist can easily span tools outside what you use day to day.&lt;/p&gt;

&lt;p&gt;This watchlist isn't auto-populated the way the daily Discovery digest is. It's a small, deliberately curated set I add to by hand through a chat command whenever something earns a permanent spot. Right now it's three repos: &lt;code&gt;langchain-ai/langgraph&lt;/code&gt; (the orchestration framework underneath the multi-agent architecture I run in Teachsim, so a breaking change there has direct downstream consequences for things I've already shipped), &lt;code&gt;pydantic/pydantic-ai&lt;/code&gt; (a newer agent framework from a team whose validation library I already trust, worth watching to see whether it earns the same trust in the agent space), and &lt;code&gt;sst/opencode&lt;/code&gt; (a coding agent I started to use when Claude is in rate limits or blocked etc, so velocity here is a signal about where that category of tool is heading). None of these were discovered by the pipeline. They're things I already knew mattered and wanted tracked automatically instead of checking manually. The weekly report tells me, in plain language, whether each one had a quiet week or a meaningful one.&lt;/p&gt;

&lt;h2&gt;
  
  
  How the News Actually Gets Judged
&lt;/h2&gt;

&lt;p&gt;"Is this worth reading" sounds like a single judgment call, but treating it that way is exactly how you get an inconsistent briefing: solid one day, padded with filler the next. The news analyzer runs off a fairly opinionated rubric instead, applied the same way across a few hundred headlines a day.&lt;/p&gt;

&lt;p&gt;It's told what not to include, explicitly. No celebrity news, no sports results, no weather unless it's catastrophic, no stock prices, no opinion pieces that don't contain any new information. That exclusion list does more work than any positive instruction: it's far easier to define "not worth your time" precisely than "worth your time" precisely.&lt;/p&gt;

&lt;p&gt;It's allowed to skip a category entirely. If a section has nothing worth including that day, the model is told to drop it rather than pad it with a weak story just to fill the slot, and it has to report which categories it skipped and why in a short line at the end. Silence has to be justified, not just defaulted into.&lt;/p&gt;

&lt;p&gt;Every kept item earns its place with two sentences, not one: what happened, and separately, why it matters. A story can be true and notable and still get cut if the model can't articulate the "so what" in one clean sentence. That forces an actual relevance judgment instead of a "this happened" summary.&lt;/p&gt;

&lt;p&gt;One section is deliberately anti-safe. There's a slot reserved for something unexpected: outside the normal categories, the kind of thing a curious person would find interesting precisely because nobody assigned it there. Without that forcing function, an LLM left to its own judgment tends to pick safe, obviously-important stories and nothing delightful or odd.&lt;/p&gt;

&lt;p&gt;The model runs at a low temperature for this task. It's tuned for consistent judgment calls, not creative variation. I want the same story judged the same way today and next week, not a slightly different read each run.&lt;/p&gt;

&lt;p&gt;So it's not magic curation. It's a rubric, and the LLM's job is applying it consistently across a volume of headlines that doesn't scale if you're doing it by hand.&lt;/p&gt;

&lt;h2&gt;
  
  
  Honest State of It
&lt;/h2&gt;

&lt;p&gt;This is a single-user, single-region tool, not a product. It runs on a deliberately cheap VPS with real RAM constraints, which has already ruled out some approaches I considered (self-hosted text-to-speech, for one: the model and codec overhead alone would compete with the always-on listener process for memory). The codebase isn't open source right now while I keep iterating on it.&lt;/p&gt;

&lt;p&gt;It's also entirely text-based today. Voice and richer formatting are designed but not yet built, more on that below.&lt;/p&gt;

&lt;p&gt;Deepseek V4 Flash via Openrouter seems to be doing a good enough job, it allows me to read the gist of interesting news sometimes behind paywall and there was only once that I read the LLM thinking process rather than the actual news. Re-generating the news fixed it straightaway. Is it providing me a wholesome view of news and the world? Maybe not but it sure beats Google providing me with generic news everyday.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Voice layer.&lt;/strong&gt; The morning news briefing gets a parallel spoken version: a handful of top stories, rewritten for speech (no URLs, no markdown, abbreviations spelled out) and sent as a Telegram voice note via an API-based text-to-speech call. Self-hosting TTS isn't viable on this hardware, so this stays API-based and stays cheap. The cost estimate for daily voice generation lands at a few cents a month.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Email as the primary reading surface.&lt;/strong&gt; Telegram's plain-text formatting and message-length chunking work fine at the current volume, but there's no rich formatting and no searchable archive: real problems once content volume grows. The plan is to make HTML email the main place I actually read the full briefing, while Telegram steps back to a notification ping plus the voice note plus on-demand commands. Same underlying pipeline, a second delivery path added alongside the existing one, gated behind a feature flag so the live system never breaks while the new path gets built.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;GitHub Discovery, scaled up, and the math that constrains it.&lt;/strong&gt; The plan adds several more topic categories to the search net, which sounds like it just means "more queries," but the 30-requests-per-minute search ceiling doesn't move just because the scope does. Roughly tripling the query count means the per-call sleep interval has to widen too, from the current 2.1 seconds to something closer to 3.2, or the scraper starts tripping the same limiter it was built to respect. The tradeoff is a longer run time in exchange for broader coverage: still comfortably a single-digit number of minutes, just no longer the fastest pipeline of the three. Repo counts per category will also stop being uniform. The categories I care about most get more picks per day, the narrower ones get fewer, and that's a deliberate choice rather than an oversight.(It took me a long time to talk with Claude to decide what fields and topics to track, there are just way too many fields and repos in Github!)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Broader news scope, same constraint-driven approach.&lt;/strong&gt; The news pipeline scales similarly, more feeds, more output sections, with the per-feed age-window logic (different freshness lookback for daily-cadence sources versus slower-publishing ones) carrying over unchanged, since that part of the design already solved the problem correctly the first time.&lt;/p&gt;

&lt;p&gt;None of this changes the core shape of the system. It's still three pipelines, an LLM doing the judgment calls, and a phone that doesn't need an app to receive any of it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Closing Thought
&lt;/h2&gt;

&lt;p&gt;The bet here is that the bottleneck in personal information consumption usually isn't access, it's triage. There's no shortage of news, and no shortage of interesting open-source work. What's scarce is time spent deciding what's worth reading. Handing that decision to a cheap, fast model running on a five-dollar VPS, three times a day, has been a more useful experiment than I expected going in.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;GitHub Digest is a personal project, currently text-only with voice and email delivery in active development. Built with Python, scheduled via cron and systemd, curated with an LLM via OpenRouter.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Find me on GitHub: &lt;a href="https://github.com/mediblacksand" rel="noopener noreferrer"&gt;github.com/mediblacksand&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;




</description>
      <category>python</category>
      <category>ai</category>
      <category>telegrambot</category>
      <category>automation</category>
    </item>
    <item>
      <title>I Built a Three-Agent AI Training Simulator on Telegram — Here's How It Works</title>
      <dc:creator>MediBlackSand</dc:creator>
      <pubDate>Thu, 18 Jun 2026 04:27:26 +0000</pubDate>
      <link>https://dev.to/mediblacksand_f0ea36c53fb/i-built-a-three-agent-ai-training-simulator-on-telegram-heres-how-it-works-1c8b</link>
      <guid>https://dev.to/mediblacksand_f0ea36c53fb/i-built-a-three-agent-ai-training-simulator-on-telegram-heres-how-it-works-1c8b</guid>
      <description>&lt;p&gt;&lt;em&gt;A working Telegram bot that stress-tests trainees with a pressure character, coaches them in real time with Socratic hints, and scores their performance. All from a single message.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem
&lt;/h2&gt;

&lt;p&gt;Most AI training tools do one of two things: they quiz you, or they roleplay with you. Neither is quite right for professional skill training. A quiz tells you what you know. A roleplay with a helpful AI tells you what you should have said.&lt;/p&gt;

&lt;p&gt;The missing piece is pressure. Something that watches how you respond under realistic conditions, nudges you when you're heading the wrong way, and then gives you an honest account of how you actually performed.&lt;/p&gt;

&lt;p&gt;That's what I wanted to build.&lt;/p&gt;

&lt;h2&gt;
  
  
  What TeachSim Is
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://t.me/teachsim_bot" rel="noopener noreferrer"&gt;TeachSim&lt;/a&gt; is a Telegram-based training simulation. A trainee starts a session, gets dropped into a realistic workplace scenario, and has to navigate it in real time. What they don't see: two additional AI agents are watching the whole conversation. One is ready to offer a Socratic hint if the trainee stalls or heads in the wrong direction. The other is building a scored performance report that fires the moment the session ends.&lt;/p&gt;

&lt;p&gt;Three agents. One conversation. The trainee only ever talks to one of them.&lt;/p&gt;

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

&lt;p&gt;The three agents run as nodes in a &lt;strong&gt;LangGraph StateGraph&lt;/strong&gt;, sharing a single state object that tracks everything (conversation history, escalation level, hints used, resolution signals, scoring dimensions) across every exchange.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;User message → Chaos node → Mentor node → [conditional]
                                               │
                                  session_active=False → Score → END
                                  session_active=True  → wait  → END
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each agent gets its own LLM client tuned for its role:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nf"&gt;get_chaos_client&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;    &lt;span class="c1"&gt;# temperature=0.75 — natural, varied character dialogue
&lt;/span&gt;&lt;span class="nf"&gt;get_mentor_client&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;   &lt;span class="c1"&gt;# temperature=0.3  — measured Socratic hints
&lt;/span&gt;&lt;span class="nf"&gt;get_scoring_client&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;  &lt;span class="c1"&gt;# temperature=0.1  — deterministic JSON report
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;All three call DeepSeek V4 Flash via OpenRouter. The model string is &lt;code&gt;deepseek/deepseek-v4-flash&lt;/code&gt;. Switching providers means changing one file in the environment.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Two Live Scenarios
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Girls in STEM: Responding to an Excluded Student&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The trainee receives a message from a student named Jamie who feels pushed out of a STEM group. They have to draft a reply. The Chaos Persona (Jamie) escalates if the trainee stalls, over-apologises, or writes something technically correct but emotionally tone-deaf.&lt;/p&gt;

&lt;p&gt;This scenario is grounded in the &lt;strong&gt;Brooks (2025) TALK framework&lt;/strong&gt;, with three patterns the scoring rubric explicitly tracks: responsiveness (did you address what Jamie actually said, not just the emotional category?), superfluous apology (hedging that signals your discomfort rather than addressing hers), and topic pyramid (connection first, explanation second, practical close third). The mentor reads the trainee's actual drafted reply before deciding whether to intervene, not just a keyword trigger.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Claude Code Assessment&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The trainee is assessed by an AI trainer persona, Alex (warm, patient) or Jordan (direct, challenging), against a tiered competency framework before being granted access to Claude Code. Up to 16 exchanges, three difficulty levels, coverage thresholds that change by tier.&lt;/p&gt;

&lt;p&gt;Novice difficulty always routes to Alex regardless of selection. Expert difficulty requires 80% coverage across all three tiers before the readiness verdict fires.&lt;/p&gt;

&lt;h2&gt;
  
  
  Under the Hood
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The Chaos Persona escalates.&lt;/strong&gt; Escalation level runs from 0 to 4 and never decrements. If the trainee stalls, makes repeated mistakes, or produces something clearly off-target, the persona gets more direct and less patient. The trainee can't reset the mood by being polite. They have to solve the actual problem.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Mentor is silent until it identifies an issue.&lt;/strong&gt; Mentor triggers are defined per scenario as structured conditions with IDs (&lt;code&gt;MT-01&lt;/code&gt; through &lt;code&gt;MT-99&lt;/code&gt;), severity weights, and a &lt;code&gt;max_fires&lt;/code&gt; ceiling. The same trigger won't fire twice. When it does fire, the hint appears as a coaching note, separate from the main conversation. The trainee knows the system is watching; they don't know exactly when it will speak.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;New scenarios need no Python.&lt;/strong&gt; The entire scenario definition lives in a JSON file: persona, escalation arc, resolution conditions, mentor triggers, scoring rubric, difficulty variants. Pydantic validates every JSON at startup. Adding a new simulation topic is a schema problem, not a code problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try It
&lt;/h2&gt;

&lt;p&gt;The bot is live at &lt;a href="https://t.me/teachsim_bot" rel="noopener noreferrer"&gt;@teachsim_bot&lt;/a&gt;. Start a session, pick a scenario and difficulty, and see how the Chaos Persona responds to a weak first reply.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1. Open Telegram → search @teachsim_bot
2. /start
3. Pick a scenario
4. Pick a difficulty (novice / standard / expert)
5. Send your first message
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Sessions time out after 20 minutes. The score report fires automatically when the session resolves or the trainee runs out of exchanges.&lt;/p&gt;

&lt;h2&gt;
  
  
  Honest State of It
&lt;/h2&gt;

&lt;p&gt;Two scenarios in production. Four were built; two are deprecated because the training value is not as high as the two in production. The repo is currently private while I decide whether to open source it. The bot is single-instance, single-region, not designed for concurrent scale yet.&lt;/p&gt;

&lt;h2&gt;
  
  
  On Open Sourcing
&lt;/h2&gt;

&lt;p&gt;I'm still deciding. There's enough going on here (three-agent LangGraph with per-agent temperature tuning, data-driven scenario schema, Socratic mentor triggering) that it might be more useful as a reference implementation than as a closed tool. If there's interest from people who want to build domain-specific training simulations on top of it, open source makes sense. If you'd use this for something, tell me what.&lt;/p&gt;

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

&lt;p&gt;TeachSim was built around workplace and tool scenarios: high stakes, clear right answers, measurable outcomes. The architecture works well for that. But the same three-agent design (pressure character, silent mentor, scored report) maps onto a much older and harder problem: everyday conversation.&lt;/p&gt;

&lt;p&gt;I'm planning a second, more substantial simulation platform on the same foundation. This one is grounded in &lt;strong&gt;Alison Wood Brooks' conversation research&lt;/strong&gt;, specifically her work on topic flow, follow-up questions, the patterns that make people feel genuinely heard versus politely processed. The mentor in TeachSim watches for technical mistakes. The mentor in this one watches for the conversational habits most people don't know they have: the question that shuts a topic down instead of opening it, the pivot that signals discomfort, the apology that's really about the speaker.&lt;/p&gt;

&lt;p&gt;38 situations are designed across six tiers, from a first meeting and a first date through to emotionally complex, high-stakes conversations. Nine JSON files are built. Two are bot-tested. The architecture is the same; the theory layer underneath is different and deeper.&lt;/p&gt;

&lt;p&gt;More on that when it's ready. If conversation science and AI simulation overlap with something you're working on, follow along.&lt;/p&gt;

&lt;h2&gt;
  
  
  Closing Thought
&lt;/h2&gt;

&lt;p&gt;The bet TeachSim makes is that pressure-testing is the missing layer in AI training tools. Most tools will tell you the right answer after you get it wrong. This one makes you find it under conditions that feel like the real thing. Whether that produces better retention, faster skill transfer, or just higher stress is something I'd like to measure. If you run a session and have a reaction, good or bad, I want to hear it.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;TeachSim is live at &lt;a href="https://t.me/teachsim_bot" rel="noopener noreferrer"&gt;@teachsim_bot&lt;/a&gt;. Built with LangGraph, DeepSeek V4 Flash, and python-telegram-bot. Repo currently private; open source decision pending. A second simulation platform based on Alison Wood Brooks' conversation research is in development on the same architecture.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Find me on GitHub: &lt;a href="https://github.com/mediblacksand" rel="noopener noreferrer"&gt;github.com/mediblacksand&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Reference:&lt;/strong&gt; Brooks, Alison Wood. &lt;em&gt;Talk: The Science of Conversation and the Art of Being Ourselves.&lt;/em&gt; Crown, 2025.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;In loving memory of Zhang Fu, 1950–2026.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>python</category>
      <category>langgraph</category>
      <category>ai</category>
      <category>telegrambot</category>
    </item>
  </channel>
</rss>
