<?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>Forem: Scot Campbell</title>
    <description>The latest articles on Forem by Scot Campbell (@simplemindedrobot).</description>
    <link>https://forem.com/simplemindedrobot</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F2106283%2Fcc50fd51-99e8-43b3-98e8-3115e8766f5c.png</url>
      <title>Forem: Scot Campbell</title>
      <link>https://forem.com/simplemindedrobot</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/simplemindedrobot"/>
    <language>en</language>
    <item>
      <title>How I Added an Agentic Heartbeat to Claude Code Using Built-in Tools</title>
      <dc:creator>Scot Campbell</dc:creator>
      <pubDate>Tue, 07 Apr 2026 19:48:20 +0000</pubDate>
      <link>https://forem.com/simplemindedrobot/giving-claude-code-a-heart-beat-55ja</link>
      <guid>https://forem.com/simplemindedrobot/giving-claude-code-a-heart-beat-55ja</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3gsoubh5rtdxsyny535t.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3gsoubh5rtdxsyny535t.png" alt=" " width="800" height="447"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;An agent heartbeat, stripped of the branding, is three things: a timer, a state file, and a decision step. That's it. Something wakes up on a schedule, reads a document describing what the world looks like, and decides whether to act. If you squint past the WebSocket plumbing and the always-on process, that's what OpenClaw's heartbeat daemon is doing.&lt;/p&gt;

&lt;p&gt;I want to be careful here. A lot of people built real work on OpenClaw. Anthropic cutting off OAuth token use for Open Claw on April 4 didn't just remove a product; it stranded workflows, broke muscle memory, and scattered a community that had been coalescing around a specific mental model of how agents should run. This isn't a post about how that was inevitable or deserved. It's a post for the narrower audience of OpenClaw refugees who came for the capability and are now trying to figure out where the capability went.&lt;/p&gt;

&lt;p&gt;The short answer: the capability is sitting in Claude Code. It has been for a while. You don't need a daemon to get it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why OpenClaw needed a daemon in the first place
&lt;/h2&gt;

&lt;p&gt;OpenClaw's heartbeat was a long-lived process because the rest of its architecture required one. It held auth in memory, listened on a WebSocket for remote triggers, maintained warm context between cycles, and coordinated a runtime that assumed continuous presence. Given those constraints, a daemon wasn't a choice — it was the only shape the thing could take.&lt;/p&gt;

&lt;p&gt;Claude Code's architecture doesn't carry those constraints. Auth lives in your subscription, not in a process. Triggers arrive through primitives that already exist. Context is something you assemble per invocation rather than something you hold. The daemon shape is doing work that, in this environment, nothing is asking for.&lt;/p&gt;

&lt;p&gt;Which means you can replace it with a cron entry.&lt;/p&gt;

&lt;h2&gt;
  
  
  The only cron you need
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nc"&gt;CronCreate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;schedule&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;*/5 * * * *&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/heartbeat-pulse&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;agent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;doer&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's the whole scheduled surface. One cron, fires every five minutes, runs a small  haiku-tier skill. On my system, that's a "doer" agent running the skill. The skill's job is not to decide anything interesting. It runs a shell script, reads its exit code, and either stays silent or fires a &lt;code&gt;RemoteTrigger&lt;/code&gt; to wake the layer that actually decides things.&lt;/p&gt;

&lt;p&gt;The shell script — &lt;code&gt;pulse.sh&lt;/code&gt; — is where the "is there work?" question gets answered. It walks a directory of small predicate scripts (&lt;code&gt;gh issue list&lt;/code&gt; for a GitHub queue, &lt;code&gt;find&lt;/code&gt; for an inbox folder, &lt;code&gt;stat&lt;/code&gt; for file mtimes, whatever you care about), runs each one, and exits 0 if everything was quiet or 1 if any predicate found something worth looking at. Pure bash. No model. No inference. Zero tokens for the "is there anything happening" check, which is the check that runs every five minutes forever.&lt;/p&gt;

&lt;p&gt;The state file — call it &lt;code&gt;HEARTBEAT.md&lt;/code&gt;, put it in whatever repo you want — is the contract for actual work items. It's the queue, the status tracker, and the audit log in one document. Because it's a file, it's human-editable. Because it's in git, it's versioned. When something goes sideways at 3am, you can read the diff and see what the heartbeat saw.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the cold-start version is structurally better
&lt;/h2&gt;

&lt;p&gt;The instinct is to treat "always-on" as a feature. It isn't, in this context. It's a liability the old architecture had to pay for.&lt;/p&gt;

&lt;p&gt;Cold-start means the prompt cache does what it's designed to do: cache the stable prefix of your invocation and charge you cheaply for the variable tail. A warm loop that reuses context across cycles defeats this — every "tick" drags accumulated state into the next call, and the cache never gets a clean prefix to hold onto. The cold-start version is cheaper, not despite the restart, but because of it.&lt;/p&gt;

&lt;p&gt;Auth stays in-process, scoped to the invocation that needs it. There's no long-lived credential sitting in a daemon's memory waiting to be exfiltrated by whatever CVE shows up in a transitive dependency next month. The attack surface is the cron entry and the file. That's a surface you can reason about over coffee.&lt;/p&gt;

&lt;p&gt;And the property that made OpenClaw economically awkward — warm context burning subscription tokens while the user wasn't looking — is exactly the property the cron version doesn't have. When nothing is ready, the heartbeat reads the file, sees nothing to do, and exits. The cost of an idle cycle is a single small prompt. The cost of a busy cycle is whatever the busy work was going to cost anyway.&lt;/p&gt;

&lt;h2&gt;
  
  
  Stacked escalation, not parallel scheduling
&lt;/h2&gt;

&lt;p&gt;The version that felt natural when I first sketched this was three crons: a fast pulse every five minutes on a cheap model, a slow-think every thirty minutes on a strong model, a reflective daily on something in the middle. Three schedules, three layers, three independent timers. It's a clean diagram but why add the comlpexity?&lt;/p&gt;

&lt;p&gt;The mistake is that the fast pulse layer, as I first drew it, was asking a model to answer "is there anything worth waking up for?" A model is the wrong tool for that question. Due to the nature of Claude Code's internal cron, I do have to have a model run a shell script to answer the question though. An inference call that runs a shell script is cheap compared to asking a model to make the decision. You could have this step run without inference at all by using n8n or native cron, I just wanted to build this whole thing within Claude Code.&lt;/p&gt;

&lt;p&gt;The corrected architecture is one scheduled timer and three demand-driven layers. Everything above the timer runs only when the timer's shell substrate found evidence of work.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;   cron fires (every 5 min)
         │
         ▼
  Layer 0: heartbeat-pulse  (haiku router, ~0 decisions)
         │
         ▼
   runs pulse.sh via the Bash tool
         │
         ▼
   pulse.sh walks predicates/*.sh
   (pure shell, zero inference, exits 0 or 1)
         │
         ├── exit 0 ──▶ silent. tick done.
         │
         └── exit 1 ──▶ RemoteTrigger("heartbeat-triage")  (async)
                          │
                          ▼
                  Layer 1: heartbeat-triage  (haiku classifier)
                          │
                          ├── handle inline  ──▶ done
                          ├── ignore         ──▶ done
                          └── escalate:
                                append item to HEARTBEAT.md
                                RemoteTrigger("heartbeat-tick")
                                     │
                                     ▼
                             Layer 2: heartbeat-tick  (opus, real decisions)
                                     │
                                     ▼
                              reads HEARTBEAT.md, dispatches via Agent,
                              updates state, exits
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Even a daily reflective layer can be woven into this pattern  - a morning pass that reviews what the last twenty-four hours of heartbeat activity produced, surfaces anything stuck, and prunes archived items - is just another task to be done.&lt;/p&gt;

&lt;p&gt;At idle, the whole system consumes one tiny cached haiku call per pulse tick plus a handful of &lt;code&gt;gh&lt;/code&gt; and &lt;code&gt;find&lt;/code&gt; and &lt;code&gt;stat&lt;/code&gt; commands run from the shell script. When the predicates stay silent, the cost of the heartbeat is whatever Claude Code charges you for a cached one-line haiku invocation. When the predicates fire, cost scales with the actual work, not with the clock. This is the property OpenClaw violated — a warm loop that kept context hot and burned tokens while waiting for something to do — and it's the property the stacked version restores by cold-starting every tier at every boundary.&lt;/p&gt;

&lt;h2&gt;
  
  
  The predicates are the point
&lt;/h2&gt;

&lt;p&gt;The predicates are small shell scripts that each answer one question. A &lt;code&gt;github-queue.sh&lt;/code&gt; that runs &lt;code&gt;gh issue list --label type:queue,status:queued --state open --json number --jq length&lt;/code&gt; and exits 1 if the count is greater than zero. An &lt;code&gt;inbox-files.sh&lt;/code&gt; that runs &lt;code&gt;find ~/heartbeat/inbox -type f -print -quit&lt;/code&gt; and exits 1 if anything comes back. A &lt;code&gt;heartbeat-mtime.sh&lt;/code&gt; that checks whether &lt;code&gt;HEARTBEAT.md&lt;/code&gt; has been touched since the last pulse and exits 1 if it has. None of them calls a model. All of them run in milliseconds. Adding a new one is dropping an executable shell script into a directory.&lt;/p&gt;

&lt;p&gt;This is the piece that makes the whole architecture sustainable: the part of the system that runs every five minutes forever is the part that has zero marginal cost, and the part that costs something runs only when the free part has already decided there's a reason to spend. If you're thinking about your own version of this, the predicate directory is where you should be spending your time. Everything else is plumbing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Triage as the translator between layers
&lt;/h2&gt;

&lt;p&gt;When &lt;code&gt;pulse.sh&lt;/code&gt; exits 1, the Layer 1 skill fires &lt;code&gt;RemoteTrigger("heartbeat-triage")&lt;/code&gt;, which is async — it returns immediately, triage runs independently, the pulse tick exits without waiting. Triage is a classifier, still on haiku, and it handles three cases.&lt;/p&gt;

&lt;p&gt;The first is "I can resolve this in a handful of tool calls." A duplicate GitHub issue that's already tracked as in-progress. A stale inbox file matching something already marked &lt;code&gt;done&lt;/code&gt;. A webhook pong that just needs acknowledgment. Triage handles these inline and exits. No escalation needed.&lt;/p&gt;

&lt;p&gt;The second is "this predicate fired on something that shouldn't count." Known-stale data, test artifacts, a mtime change triage caused itself. Log the ignore reason so the reflective layer can notice if ignore rates spike — that's the signal a predicate has gotten too noisy — and exit.&lt;/p&gt;

&lt;p&gt;The third is "this is real work that needs the slow-think layer." Triage writes the work to &lt;code&gt;HEARTBEAT.md&lt;/code&gt; as a new ready item with the appropriate agent and prompt, then fires &lt;code&gt;RemoteTrigger("heartbeat-tick")&lt;/code&gt; to wake Layer 2. Triage does not dispatch agents itself — it's a ticket writer, not a worker.&lt;/p&gt;

&lt;p&gt;Layer 2, the tick, is where the opus-tier decisions live. It reads &lt;code&gt;HEARTBEAT.md&lt;/code&gt;, evaluates the ready items, dispatches them via &lt;code&gt;Agent&lt;/code&gt;, updates state, exits. This is the layer that corresponds to OpenClaw's actual ReAct loop — the place where a strong model is reasoning about what to do. In the stacked version, it runs only when two cheaper layers have already decided it's warranted.&lt;/p&gt;

&lt;h2&gt;
  
  
  The event axis
&lt;/h2&gt;

&lt;p&gt;Cron gives you time. It doesn't give you events. For events, there's RemoteTrigger — a way for external systems to wake a Claude Code invocation on demand. A GitHub webhook, a form submission, an email parser, whatever. Same pattern: the trigger fires, a cold-start invocation reads the state file, decides, acts.&lt;/p&gt;

&lt;p&gt;Wiring them together is the whole move. Cron handles "wake up periodically." RemoteTrigger handles "wake up when something happens." Together they cover everything a heartbeat daemon was covering, without anything needing to stay resident.&lt;/p&gt;

&lt;h2&gt;
  
  
  Making failure visible
&lt;/h2&gt;

&lt;p&gt;A heartbeat that fails silently is worse than no heartbeat, because you build trust in it and then the trust is wrong. SendMessage is the glue that fixes this. At the end of each cycle — or specifically at the end of the reflective daily cycle — have the invocation send itself (or you) a short status ping. "Three items dispatched, one stuck, queue depth four." If the ping stops arriving, you know the heartbeat stopped beating. If the ping arrives but says something alarming, you know before the alarming thing compounds.&lt;/p&gt;

&lt;p&gt;This is the part OpenClaw users often don't replicate because the daemon model made it feel unnecessary — the daemon was right there, you could see its logs. In the cold-start model, the invocations are ephemeral, so observability has to be something the invocations produce on purpose.&lt;/p&gt;

&lt;h2&gt;
  
  
  The mapping
&lt;/h2&gt;

&lt;p&gt;For the people doing the actual port, here's the whole thing as a table:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;OpenClaw concept&lt;/th&gt;
&lt;th&gt;Claude Code equivalent&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Heartbeat state file&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;HEARTBEAT.md&lt;/code&gt; in your repo, git-versioned&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Timer&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;CronCreate&lt;/code&gt; firing a minimal Layer 0 skill&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Self-waking daemon loop&lt;/td&gt;
&lt;td&gt;Cold-start cron invocation, no daemon&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;"Is there work?" check&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;pulse.sh&lt;/code&gt; + &lt;code&gt;predicates/*.sh&lt;/code&gt; — pure shell, zero inference&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Work triage / classification&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;heartbeat-triage&lt;/code&gt; skill (haiku), demand-driven&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ReAct decision step&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;heartbeat-tick&lt;/code&gt; skill (opus), demand-driven&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Agent dispatch&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;Agent&lt;/code&gt; tool, called from Layer 2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Inter-step messaging&lt;/td&gt;
&lt;td&gt;&lt;code&gt;SendMessage&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Skill invocation&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Skill&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;External event triggers&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;RemoteTrigger&lt;/code&gt; (same primitive the layers use internally)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Multi-agent coordination&lt;/td&gt;
&lt;td&gt;&lt;code&gt;TeamCreate&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Worktree isolation&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;EnterWorktree&lt;/code&gt; / &lt;code&gt;ExitWorktree&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Status review / reflection&lt;/td&gt;
&lt;td&gt;Daily reflective cron, separate from the hot path&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The rows that matter most are the ones about the "is there work?" check and the triage layer. Those are the architectural distinctions between a heartbeat that burns subscription tokens while idle and one that doesn't. OpenClaw's heartbeat answered the wake-up question with a warm model; the stacked version answers it with shell scripts and lets the models run only when the shell has already decided there's a reason. Everything else on the list is primitives that ship in the box.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this doesn't replace
&lt;/h2&gt;

&lt;p&gt;I want to end honestly. The capability is portable. The rest of what OpenClaw was — the community, the brand, the marketplace of shared skills, the shared vocabulary that let people talk to each other about their setups without explaining the basics every time — is not portable. You can't cron your way to a community. A mapping table can't hold a mental model that thousands of people were building together.&lt;/p&gt;

&lt;p&gt;For users who came to OpenClaw because they wanted to belong to something, this post doesn't help. I don't have a good answer for them and I'm not going to pretend I do.&lt;/p&gt;

&lt;p&gt;But for users who came to OpenClaw because they wanted a heartbeat — a thing that wakes up, checks the world, and decides — the heartbeat is already in the tooling you have. It looks like a cron entry and a markdown file. It's less impressive than a daemon. That's the point.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>claude</category>
      <category>openclaw</category>
      <category>agents</category>
    </item>
    <item>
      <title>I Deleted All My MCP Servers and Everything Got Faster</title>
      <dc:creator>Scot Campbell</dc:creator>
      <pubDate>Fri, 27 Feb 2026 02:55:02 +0000</pubDate>
      <link>https://forem.com/simplemindedrobot/i-deleted-all-my-mcp-servers-and-everything-got-faster-3k8l</link>
      <guid>https://forem.com/simplemindedrobot/i-deleted-all-my-mcp-servers-and-everything-got-faster-3k8l</guid>
      <description>&lt;p&gt;In my last post I described a PKM system held together by Ansible, git, and a small fleet of MCP servers. Eight of them, to be precise. ArXiv, Semantic Scholar, Google Workspace, Obsidian, Thoughtbox, QMD, Markitdown, Mermaid. Each one a stdio process that Claude Code spawns, connects to, and keeps alive for the duration of the session.&lt;/p&gt;

&lt;p&gt;If you're running MCP servers with Claude Code, you already know there's token overhead. Tool definitions aren't free. But there's a detail about &lt;em&gt;when&lt;/em&gt; that cost kicks in that changed how I think about the whole architecture.&lt;/p&gt;

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

&lt;p&gt;Claude Code doesn't load every MCP tool definition at session start. It's smarter than that. Tools get loaded lazily — the first time you actually use a server, its definitions enter the context. So far, so reasonable.&lt;/p&gt;

&lt;p&gt;Here's the part that bit me: once loaded, those definitions stay in context for &lt;em&gt;every subsequent turn&lt;/em&gt;. They're carried forward, request after request, until compaction finally clears them out. Use your Semantic Scholar MCP once on turn 3 to look up a paper? Its 33 tool definitions — names, descriptions, full JSON parameter schemas — ride along on turns 4, 5, 6, 7... all the way until the context gets compacted.&lt;/p&gt;

&lt;p&gt;It's not a startup cost. It's a &lt;em&gt;carry&lt;/em&gt; cost. And it compounds.&lt;/p&gt;

&lt;p&gt;I got curious about the actual weight. I cracked open GoodLemur's API logs (raw request/response JSONL I save for research) and measured what each server adds to every request once loaded:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Server&lt;/th&gt;
&lt;th&gt;Tools&lt;/th&gt;
&lt;th&gt;Tokens carried per turn&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;google-workspace&lt;/td&gt;
&lt;td&gt;142&lt;/td&gt;
&lt;td&gt;~37,392&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;semantic-scholar&lt;/td&gt;
&lt;td&gt;33&lt;/td&gt;
&lt;td&gt;~10,776&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;thoughtbox&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;~1,555&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;arxiv-mcp-server&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;~1,185&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;markitdown&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;~76&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Google Workspace was the worst offender. 142 tools. ~37,392 tokens. Check your calendar once on turn 5, and those definitions occupy context on every turn until compaction.&lt;/p&gt;

&lt;p&gt;Now here's the thing: prompt caching means you're not paying full input price for those repeated definitions. The API caches them server-side after the first send. So the &lt;em&gt;dollar&lt;/em&gt; cost is manageable. But caching doesn't shrink the context window. Those ~37,392 tokens still &lt;em&gt;occupy space&lt;/em&gt;. They count against your 200k limit just like your actual conversation does.&lt;/p&gt;

&lt;p&gt;Across all my servers once loaded: ~51,000 tokens per turn sitting in context. That's 25% of a 200k window that isn't available for conversation history, code, or reasoning. Compaction triggers sooner. You lose conversational context faster. Your effective session depth shrinks — not because you ran out of things to say, but because tool definitions you used once are squatting on the space.&lt;/p&gt;

&lt;p&gt;There's a second angle that makes this worse. The API has a &lt;a href="https://platform.claude.com/docs/en/build-with-claude/context-editing" rel="noopener noreferrer"&gt;context editing&lt;/a&gt; strategy called &lt;code&gt;clear_tool_uses&lt;/code&gt; that clears old tool &lt;em&gt;results&lt;/em&gt; from past turns — file contents you've already read, search results you've already processed. It's designed to free up exactly this kind of accumulated weight. And it works great for CLI tools. When Claude calls &lt;code&gt;s2-search&lt;/code&gt; via Bash, that Bash output is a tool result. On later turns, &lt;code&gt;clear_tool_uses&lt;/code&gt; can sweep it away.&lt;/p&gt;

&lt;p&gt;But MCP tool &lt;em&gt;definitions&lt;/em&gt; aren't tool results. They're structural. They live in the &lt;code&gt;tools&lt;/code&gt; parameter, sent with every API request, immune to context editing. No clearing strategy can touch them. So MCP tools get the worst of both: their definitions persist forever (until compaction), and they don't benefit from the system designed to manage exactly this problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  The idea
&lt;/h2&gt;

&lt;p&gt;What if the tools were just CLI commands?&lt;/p&gt;

&lt;p&gt;An MCP server is a process that speaks JSON-RPC over stdio. Claude Code launches it, negotiates capabilities, and loads each tool definition into context when you first use it — where it stays until compaction. But if Claude already knows a CLI exists — because you told it in CLAUDE.md — it can just call it with Bash. No process to manage. No tool definitions accumulating in context. No carry cost.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Before (MCP):&lt;/strong&gt; Use Semantic Scholar once → 33 tool definitions load → carried every turn until compaction → immune to &lt;code&gt;clear_tool_uses&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;After (CLI):&lt;/strong&gt; CLAUDE.md says &lt;code&gt;s2-search&lt;/code&gt; exists → Claude calls it via Bash → result clearable on later turns → no definitions persist.&lt;/p&gt;

&lt;p&gt;Same capability. The difference is what happens on the turns where you're &lt;em&gt;not&lt;/em&gt; using that tool.&lt;/p&gt;

&lt;h2&gt;
  
  
  The migration
&lt;/h2&gt;

&lt;p&gt;I ended up with three approaches depending on the server:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Write thin CLI wrappers (arxiv-search, s2-search)
&lt;/h3&gt;

&lt;p&gt;For ArXiv and Semantic Scholar, I wrote Python scripts that hit the APIs directly. No libraries for S2 — the &lt;code&gt;semanticscholar&lt;/code&gt; Python package turned out to be unusably slow without an API key. Its retry logic would block for 30+ seconds. A raw &lt;code&gt;urllib.request&lt;/code&gt; call returns in milliseconds.&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;# s2-search — direct API, no library
&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;api&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;base&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;BASE_GRAPH&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;method&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;GET&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;base&lt;/span&gt;&lt;span class="si"&gt;}{&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;url&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;?&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nf"&gt;urlencode&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;items&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="n"&gt;req&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;method&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;method&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                  &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;body&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                  &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;urlopen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;13 subcommands: &lt;code&gt;search&lt;/code&gt;, &lt;code&gt;bulk&lt;/code&gt;, &lt;code&gt;match&lt;/code&gt;, &lt;code&gt;snippets&lt;/code&gt;, &lt;code&gt;get&lt;/code&gt;, &lt;code&gt;refs&lt;/code&gt;, &lt;code&gt;cites&lt;/code&gt;, &lt;code&gt;batch&lt;/code&gt;, &lt;code&gt;recommend&lt;/code&gt;, &lt;code&gt;recommend-multi&lt;/code&gt;, &lt;code&gt;author&lt;/code&gt;, &lt;code&gt;author-search&lt;/code&gt;, &lt;code&gt;autocomplete&lt;/code&gt;. All from ~250 lines of Python.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;arxiv-search&lt;/code&gt; wrapper was even simpler — just &lt;code&gt;arxiv.py&lt;/code&gt; with a CLI face. One gotcha: argparse subcommands collide with positional search queries (your query gets interpreted as a subcommand name). I rewrote it to parse &lt;code&gt;sys.argv&lt;/code&gt; manually. Ugly but bulletproof.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Compile MCP servers to static binaries (thoughtbox, obsidian, google-workspace)
&lt;/h3&gt;

&lt;p&gt;This is the weird one. &lt;a href="https://github.com/thellimist/clihub" rel="noopener noreferrer"&gt;clihub&lt;/a&gt; takes a running MCP server — stdio or HTTP — and generates a standalone Go binary with a subcommand for every tool. No Node runtime needed at execution time. The compiled binary just works.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Point clihub at a running MCP server, get a CLI&lt;/span&gt;
clihub generate &lt;span class="nt"&gt;--name&lt;/span&gt; thoughtbox &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--transport&lt;/span&gt; stdio &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--command&lt;/span&gt; &lt;span class="s2"&gt;"npx -y @kastalien-research/thoughtbox"&lt;/span&gt;

&lt;span class="c"&gt;# Result: static binary with 3 subcommands&lt;/span&gt;
thoughtbox mental-models &lt;span class="nt"&gt;--operation&lt;/span&gt; list_models
thoughtbox thoughtbox &lt;span class="nt"&gt;--raw&lt;/span&gt; &lt;span class="s1"&gt;'{"thought":"...", "thoughtNumber":1, "totalThoughts":5}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For Google Workspace, this turned 142 MCP tools into 142 CLI subcommands. One binary. No Node, no Python, no uvx at runtime.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Before: MCP server with 142 tool definitions eating ~37,392 tokens/turn&lt;/span&gt;
&lt;span class="c"&gt;# After: one line in CLAUDE.md&lt;/span&gt;
gw search-gmail-messages &lt;span class="nt"&gt;--user_google_email&lt;/span&gt; sr4001@gmail.com &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--query&lt;/span&gt; &lt;span class="s2"&gt;"from:someone subject:thing"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I hit two problems with clihub along the way:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;TLS certificates.&lt;/strong&gt; The Obsidian MCP server runs on localhost with a self-signed cert. Go's TLS stack doesn't honor &lt;code&gt;NODE_TLS_REJECT_UNAUTHORIZED&lt;/code&gt;. Solution: use the HTTP endpoint on port 3001 instead of HTTPS on 3443.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Codegen collision.&lt;/strong&gt; When an MCP tool has a parameter named &lt;code&gt;raw&lt;/code&gt;, it collides with clihub's own &lt;code&gt;--raw&lt;/code&gt; flag. The generated Go code has two variables both trying to be &lt;code&gt;flagRaw&lt;/code&gt;. I patched it with a sed script and &lt;a href="https://github.com/thellimist/clihub/issues/8" rel="noopener noreferrer"&gt;filed an issue&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;For servers that need credentials, a wrapper script handles 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="c"&gt;#!/bin/sh&lt;/span&gt;
&lt;span class="c"&gt;# /opt/homebrew/bin/gw (wrapper)&lt;/span&gt;
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;GOOGLE_OAUTH_CLIENT_ID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"..."&lt;/span&gt;
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;GOOGLE_OAUTH_CLIENT_SECRET&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"..."&lt;/span&gt;
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;USER_GOOGLE_EMAIL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"sr4001@gmail.com"&lt;/span&gt;
&lt;span class="nb"&gt;exec&lt;/span&gt; /opt/homebrew/bin/gw-bin &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$@&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  3. Already had a CLI (markitdown, qmd, mmdr)
&lt;/h3&gt;

&lt;p&gt;Some tools already existed as CLIs. &lt;code&gt;markitdown&lt;/code&gt; is a pip package. &lt;code&gt;qmd&lt;/code&gt; has both MCP and CLI interfaces (CLI is faster). &lt;code&gt;mmdr&lt;/code&gt; is a Rust binary for Mermaid rendering. For these, I just killed the MCP server and added a one-liner to CLAUDE.md.&lt;/p&gt;

&lt;h2&gt;
  
  
  Teaching Claude about the tools
&lt;/h2&gt;

&lt;p&gt;This is the part that surprised me with how simple it was. CLAUDE.md is a file loaded into every session's context. Adding a CLI tool means adding a few lines:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gu"&gt;## CLI Tools&lt;/span&gt;
&lt;span class="p"&gt;
-&lt;/span&gt; &lt;span class="gs"&gt;**s2-search**&lt;/span&gt;: Semantic Scholar CLI. Hits the API directly.
&lt;span class="p"&gt;  -&lt;/span&gt; &lt;span class="sb"&gt;`s2-search "query"`&lt;/span&gt; — search papers
&lt;span class="p"&gt;  -&lt;/span&gt; &lt;span class="sb"&gt;`s2-search get &amp;lt;id&amp;gt;`&lt;/span&gt; — paper details
&lt;span class="p"&gt;  -&lt;/span&gt; &lt;span class="sb"&gt;`s2-search refs &amp;lt;id&amp;gt;`&lt;/span&gt; / &lt;span class="sb"&gt;`s2-search cites &amp;lt;id&amp;gt;`&lt;/span&gt; — references and citations
&lt;span class="p"&gt;  -&lt;/span&gt; &lt;span class="sb"&gt;`--json`&lt;/span&gt; for structured output. &lt;span class="sb"&gt;`-n`&lt;/span&gt; controls result count.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. Claude reads the description, knows the command exists, and uses Bash to call it. The CLAUDE.md entry for all eight tools is maybe 80 lines total. Compare that to ~51,000 tokens of MCP tool definitions occupying context every turn.&lt;/p&gt;

&lt;p&gt;I also updated two skills — &lt;code&gt;/recall&lt;/code&gt; switched from &lt;code&gt;mcp__qmd__search&lt;/code&gt; to &lt;code&gt;qmd search&lt;/code&gt;, and &lt;code&gt;/think&lt;/code&gt; switched from MCP thoughtbox tools to &lt;code&gt;thoughtbox mental-models&lt;/code&gt;. Same behavior, lighter context.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cleaning up
&lt;/h2&gt;

&lt;p&gt;With the CLIs in place, I ripped out every MCP server declaration:&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;# One script to clear ~/.claude.json
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;
&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;expanduser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;~/.claude.json&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;r+&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;d&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;mcpServers&lt;/span&gt;&lt;span class="sh"&gt;'&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;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;seek&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dump&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;indent&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;truncate&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then updated Ansible so the next playbook run doesn't re-add them:&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="c1"&gt;# group_vars/mac.yml — before&lt;/span&gt;
&lt;span class="na"&gt;mcp_servers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;arxiv-mcp-server&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;uv&lt;/span&gt;
    &lt;span class="na"&gt;args&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tool"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;run"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;arxiv-mcp-server"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
  &lt;span class="na"&gt;semantic-scholar&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;uvx&lt;/span&gt;
    &lt;span class="na"&gt;args&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;semantic-scholar-mcp"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
  &lt;span class="na"&gt;google-workspace&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;uvx&lt;/span&gt;
    &lt;span class="na"&gt;args&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;workspace-mcp"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--single-user"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
  &lt;span class="c1"&gt;# ... 5 more&lt;/span&gt;

&lt;span class="c1"&gt;# group_vars/mac.yml — after&lt;/span&gt;
&lt;span class="na"&gt;mcp_servers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One &lt;code&gt;ansible-playbook site.yml --limit vps&lt;/code&gt; applied the same change to the VPS. The Python CLI scripts (&lt;code&gt;arxiv-search&lt;/code&gt;, &lt;code&gt;s2-search&lt;/code&gt;) got &lt;code&gt;scp&lt;/code&gt;'d over and pointed at a venv.&lt;/p&gt;

&lt;p&gt;I also found a BasicMemory MCP ghost — a &lt;code&gt;UserPromptSubmit&lt;/code&gt; hook echoing MCP tool instructions into every session even though the server was long gone. Removed that too. Archaeology.&lt;/p&gt;

&lt;h2&gt;
  
  
  The numbers
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;What&lt;/th&gt;
&lt;th&gt;Before&lt;/th&gt;
&lt;th&gt;After&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;MCP servers&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tokens occupying context per turn (all loaded)&lt;/td&gt;
&lt;td&gt;~51,000&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Context window lost to tool definitions&lt;/td&gt;
&lt;td&gt;~25%&lt;/td&gt;
&lt;td&gt;0%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Compaction triggered&lt;/td&gt;
&lt;td&gt;Sooner&lt;/td&gt;
&lt;td&gt;Later&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CLAUDE.md lines for equivalent capability&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;~80&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The capability is identical. Same searches. Same graph traversal. Same Gmail queries. The difference is how much of the context window is actually available for the work.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;MCP is great for discovery, bad for production.&lt;/strong&gt; When you're experimenting with a new tool, MCP is perfect. Install it, see the tools appear, try them out. But once you know what you need, the protocol overhead isn't paying for itself anymore. The tool definitions are documentation Claude doesn't need if you've already told it what exists.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;CLAUDE.md is underrated.&lt;/strong&gt; A few lines of natural language in a config file replaced thousands of tokens of JSON schema. Claude doesn't need a formal tool definition to use &lt;code&gt;s2-search get &amp;lt;id&amp;gt;&lt;/code&gt;. It just needs to know the command exists and what the flags are.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;clihub is a cheat code.&lt;/strong&gt; Compiling a 142-tool MCP server into a static Go binary felt like it shouldn't work. It did. The generated code is readable, the binaries are fast, and the only bug I hit (the &lt;code&gt;raw&lt;/code&gt; flag collision) was minor and patchable. For anyone running MCP servers in production, this tool is worth knowing about.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Measure before optimizing.&lt;/strong&gt; I wouldn't have done any of this if I hadn't looked at the API logs. The carry cost was invisible until I counted it. If you're running multiple MCP servers, check your actual per-turn payload size after a few tool uses. You might be surprised at what's riding along.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The best abstraction is no abstraction.&lt;/strong&gt; An MCP server is an abstraction over a CLI command (or an API call). Sometimes the abstraction adds value — multiplexing, capability negotiation, sampling. For simple tool use, it's just overhead. &lt;code&gt;curl&lt;/code&gt; has been calling APIs since 1996. We don't need a protocol for it.&lt;/p&gt;

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

&lt;p&gt;Eight fewer processes. ~51,000 tokens freed from every turn. Compaction happens later. Sessions go deeper. Same capabilities. The CLI tools are version-controlled in &lt;code&gt;~/.venv/bin/&lt;/code&gt; and &lt;code&gt;/opt/homebrew/bin/&lt;/code&gt;, documented in CLAUDE.md, and deployed to both machines via Ansible.&lt;/p&gt;

&lt;p&gt;Is it less elegant than MCP? Maybe. But &lt;code&gt;s2-search get 1706.03762&lt;/code&gt; returning in 200ms without eating a quarter of my context window is hard to argue with.&lt;/p&gt;




&lt;p&gt;Built with Claude Code, clihub, urllib.request, and the realization that JSON-RPC is not always the answer.&lt;/p&gt;

</description>
      <category>claudecode</category>
      <category>mcp</category>
      <category>contextwindow</category>
      <category>cli</category>
    </item>
    <item>
      <title>I Built a Second Brain That Runs While I Sleep</title>
      <dc:creator>Scot Campbell</dc:creator>
      <pubDate>Thu, 26 Feb 2026 17:45:54 +0000</pubDate>
      <link>https://forem.com/simplemindedrobot/i-built-a-second-brain-that-runs-while-i-sleep-4gc1</link>
      <guid>https://forem.com/simplemindedrobot/i-built-a-second-brain-that-runs-while-i-sleep-4gc1</guid>
      <description>&lt;h2&gt;
  
  
  How I am finally getting my knowledge collection habit under control
&lt;/h2&gt;

&lt;p&gt;There's a moment in every PKM journey where you stop adding plugins and start writing infrastructure. For me, that moment arrived when I realized my Obsidian vault had outgrown Obsidian.&lt;/p&gt;

&lt;p&gt;Not the app itself. Obsidian is still the editor, the graph, the daily driver. But the &lt;em&gt;system&lt;/em&gt; around it is Ansible playbooks, a VPS running background agents at 3am, a Telegram bot relaying Claude Code sessions to my phone, a local search engine indexing over a thousand documents, and git as the only database.&lt;/p&gt;

&lt;p&gt;This is what happens when you treat a knowledge base as infrastructure instead of a hobby.&lt;/p&gt;

&lt;h2&gt;
  
  
  The topology
&lt;/h2&gt;

&lt;p&gt;Two machines. One git repo. No database.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjb6ltqotwcs71rw52z9o.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjb6ltqotwcs71rw52z9o.png" alt="Automated PKM System Topology showing a Mac running Obsidian and Claude Code and a VPS running automation tools" width="800" height="446"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The Mac is where I think. Obsidian is open, Claude Code is in the terminal, and I'm writing and connecting ideas. The VPS is where the vault works while I don't. Background research, health monitoring, queue dispatch, all running without me.&lt;/p&gt;

&lt;p&gt;They coordinate through git. That's it. No sync service, no real-time protocol. Just commits.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tailscale: the invisible network
&lt;/h2&gt;

&lt;p&gt;The VPS has no public ports. None. No SSH on 22, no HTTP on 80 or 443, no anything. UFW allows exactly two things: Tailscale's WireGuard UDP port and traffic on the Tailscale interface itself.&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="c1"&gt;# The entire VPS firewall policy&lt;/span&gt;
&lt;span class="na"&gt;ufw_rules&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;rule&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;allow&lt;/span&gt;
    &lt;span class="na"&gt;port&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;41641"&lt;/span&gt;
    &lt;span class="na"&gt;proto&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;udp&lt;/span&gt;
    &lt;span class="na"&gt;comment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Tailscale&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;WireGuard"&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;rule&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;allow&lt;/span&gt;
    &lt;span class="na"&gt;interface&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;tailscale0&lt;/span&gt;
    &lt;span class="na"&gt;direction&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;in&lt;/span&gt;
    &lt;span class="na"&gt;comment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Tailscale&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;interface"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every connection between Mac and VPS runs over Tailscale's mesh. SSH, git sync, the Obsidian MCP bridge from VPS back to the Mac's Obsidian instance, the duty officer dashboard. If you're not on my tailnet, the VPS doesn't exist.&lt;/p&gt;

&lt;p&gt;This is what makes the rest of the architecture possible. CCBot can run &lt;code&gt;claude --dangerously-skip-permissions&lt;/code&gt; because the machine it runs on has no public attack surface. Background agents can write to the vault because the only way in is through my devices. The security model is "no ingress" rather than "careful ingress."&lt;/p&gt;

&lt;h2&gt;
  
  
  Ansible: the boring part that makes everything work
&lt;/h2&gt;

&lt;p&gt;Here's the thing about infrastructure that nobody wants to talk about: the setup is the product. If deploying to a new machine takes a day of fiddling, you don't have a system. You have a snowflake.&lt;/p&gt;

&lt;p&gt;Everything is declared in Ansible group_vars:&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="c1"&gt;# Mac gets the full suite&lt;/span&gt;
&lt;span class="na"&gt;mcp_servers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;qmd&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;qmd&lt;/span&gt;
    &lt;span class="na"&gt;args&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;mcp"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
  &lt;span class="na"&gt;obsidian-mcp&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npx&lt;/span&gt;
    &lt;span class="na"&gt;args&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-y"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;mcp-remote"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://127.0.0.1:3443/mcp"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
  &lt;span class="na"&gt;thoughtbox&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npx&lt;/span&gt;
    &lt;span class="na"&gt;args&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-y"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;@kastalien-research/thoughtbox"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
  &lt;span class="na"&gt;google-workspace&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;uvx&lt;/span&gt;
    &lt;span class="na"&gt;args&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;workspace-mcp"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--single-user"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
  &lt;span class="c1"&gt;# ... plus arxiv, semantic-scholar, markitdown&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# VPS gets a subset — no Google Workspace, different Obsidian MCP endpoint&lt;/span&gt;
&lt;span class="na"&gt;mcp_servers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;obsidian-mcp&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npx&lt;/span&gt;
    &lt;span class="na"&gt;args&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-y"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;mcp-remote"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://&amp;lt;vps-tailscale-ip&amp;gt;:3443/mcp"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
  &lt;span class="c1"&gt;# research tools shared with Mac&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One &lt;code&gt;ansible-playbook site.yml --limit mac&lt;/code&gt; and the MCP servers, LaunchAgents, git repos, Homebrew packages, and Claude Code config are all in place. Secrets live in ansible-vault, never in dotfiles. The VPS gets its own subset with &lt;code&gt;--limit vps&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Declare in YAML, deploy with Ansible, never hand-configure. If a machine dies tomorrow, I'm back up in an hour.&lt;/p&gt;

&lt;h2&gt;
  
  
  QMD: when your vault gets too big for grep
&lt;/h2&gt;

&lt;p&gt;At over a thousand documents, Obsidian's built-in search starts to sweat. You can feel it. The pause, the incomplete results, the way full-text search across a vault of research papers and session transcripts just lags.&lt;/p&gt;

&lt;p&gt;QMD is a local search engine that sits alongside the vault. Three search modes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Keyword search (~30ms): BM25 ranking, exact phrase matching. The workhorse.&lt;/li&gt;
&lt;li&gt;Vector search (~2s): Semantic embeddings. Finds documents about the same &lt;em&gt;concept&lt;/em&gt; even when they use different vocabulary.&lt;/li&gt;
&lt;li&gt;Deep search (~10s): Auto-expands your query into variations, runs both keyword and vector for each, reranks the combined results.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It's an MCP server, so Claude Code can search the vault mid-conversation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;me: "What did I write about forgetting curves?"
Claude: *searches QMD, finds 3 notes across Cognition/ and Research/*
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The embedding pipeline runs as a LaunchAgent (&lt;code&gt;com.qmd-embed&lt;/code&gt;), keeping vectors fresh as notes change. Two collections: &lt;code&gt;vault&lt;/code&gt; (hundreds of massive docs) for the knowledge base, &lt;code&gt;sessions&lt;/code&gt; (tens of thousands of turns between Claude and me) for Claude Code session transcripts, yes the full JSONL converted to markdown.&lt;/p&gt;

&lt;p&gt;That session collection is the sleeper hit. I can ask "what did I figure out last Tuesday?" and get actual answers from my own thinking transcripts, not just the notes I remembered to write down.&lt;/p&gt;

&lt;h2&gt;
  
  
  Obsidian MCP: giving Claude Code eyes into the graph
&lt;/h2&gt;

&lt;p&gt;Obsidian MCP turns the vault into an API. Not just file reads. Graph traversal, search, structured editing.&lt;/p&gt;

&lt;p&gt;Graph traversal means "start at this note, follow wikilinks 3 levels deep, show me what's connected." Claude Code uses this to understand context before making changes. There's TF-IDF ranked search with operators (&lt;code&gt;tag:infrastructure path:Research/&lt;/code&gt;), which is faster than QMD for navigating known territory. And targeted editing: patch a specific heading, update frontmatter, append to a section, without reading and rewriting the whole file.&lt;/p&gt;

&lt;p&gt;The connection between Mac and VPS is worth noting. On the Mac, Obsidian MCP talks to &lt;code&gt;localhost:3443&lt;/code&gt;, directly to the running Obsidian instance. On the VPS, it reaches back across Tailscale to the Mac's Obsidian instance. Same vault, same API, different continent.&lt;/p&gt;

&lt;h2&gt;
  
  
  CCBot: Claude Code in your pocket
&lt;/h2&gt;

&lt;p&gt;This one took three tries.&lt;/p&gt;

&lt;p&gt;First: Matrix server with a Maubot plugin. Heavy. Element X's encryption ceremony was painful, iOS clients were limited, and running Synapse for a text relay felt absurd.&lt;/p&gt;

&lt;p&gt;Second: XMPP with Prosody. Five independent failure modes. Tailscale IP races, TLS config, two-service ordering dependency, credential sync between config dirs, slixmpp reconnection bugs. All for a text relay.&lt;/p&gt;

&lt;p&gt;Third: Telegram bot + tmux. One process. It just works.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F37r9m2uu3yiqwiu2c9m4.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F37r9m2uu3yiqwiu2c9m4.png" alt="Sequence Diagram of communication between a Telegram Bot and Claude Code sessions using TMUX reading" width="800" height="371"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Each Telegram forum topic maps to a tmux window running &lt;code&gt;claude --dangerously-skip-permissions&lt;/code&gt;. New topic, new Claude session. The terminal is the source of truth. You can &lt;code&gt;tmux attach&lt;/code&gt; over SSH and pick up the same conversation.&lt;/p&gt;

&lt;p&gt;The privacy tradeoff: Telegram sees message content in cleartext. I accepted this because the threat model is VPS access (protected by bot token + user ID allowlist), not message confidentiality. All connections are outbound. No webhooks, no public endpoints.&lt;/p&gt;

&lt;h2&gt;
  
  
  The background agent queue
&lt;/h2&gt;

&lt;p&gt;This is where it gets fun. I queue tasks from my Mac (or my phone via CCBot), and the VPS picks them up:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fm4ph0ihfdq944t81pqun.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fm4ph0ihfdq944t81pqun.png" alt="Flow chart showing background agent queuing using GitHub issues" width="800" height="41"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Agent profiles define what each type of task gets:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Profile&lt;/th&gt;
&lt;th&gt;Model&lt;/th&gt;
&lt;th&gt;Budget&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;research&lt;/td&gt;
&lt;td&gt;Sonnet&lt;/td&gt;
&lt;td&gt;$10&lt;/td&gt;
&lt;td&gt;ArXiv, Semantic Scholar, deep dives&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;editorial&lt;/td&gt;
&lt;td&gt;Sonnet&lt;/td&gt;
&lt;td&gt;$5&lt;/td&gt;
&lt;td&gt;Rewriting, consolidation, wiki-linking&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;maintenance&lt;/td&gt;
&lt;td&gt;Haiku&lt;/td&gt;
&lt;td&gt;$3&lt;/td&gt;
&lt;td&gt;Vault health, read-only audits&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;general&lt;/td&gt;
&lt;td&gt;Sonnet&lt;/td&gt;
&lt;td&gt;$5&lt;/td&gt;
&lt;td&gt;Everything else&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The dispatcher compares git hashes and only wakes the expensive runner when something actually changed. Tasks get three attempts before moving to a Failed section. Budget caps prevent runaway API costs.&lt;/p&gt;

&lt;p&gt;From my phone, queuing a research task looks like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;me (in Telegram): /queue p2 research Find papers on executive function in transformer architectures
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two hours later, results land in &lt;code&gt;Agent/Results/&lt;/code&gt;, committed to git, synced to my Mac, searchable in QMD.&lt;/p&gt;

&lt;h2&gt;
  
  
  Skills: the Claude Code extension layer
&lt;/h2&gt;

&lt;p&gt;Claude Code ships with tool use. Skills extend it with &lt;em&gt;workflows&lt;/em&gt;. I have about 40, all in &lt;code&gt;~/.claude/skills/&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Some favorites:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;/deep-research&lt;/code&gt; orchestrates parallel searches across ArXiv, Semantic Scholar, and the web, then synthesizes findings with citations. &lt;code&gt;/recall&lt;/code&gt; does unified cross-source search over vault notes and session transcripts in parallel, for when you vaguely remember "something about forgetting curves" but can't find it. &lt;code&gt;/atomize&lt;/code&gt; takes a folder of documents and produces organized atomic notes with emergent themes. Drop a pile of PDFs, get a linked knowledge structure. &lt;code&gt;/daily-summary&lt;/code&gt; pulls from three sources at end of day (vault changes via git, cross-machine Claude Code session transcripts, vault notes themselves) and writes a narrative journal entry. &lt;code&gt;/kanban-track&lt;/code&gt; spins up a throwaway markdown kanban for multi-agent work, plus a durable GitHub issue as the after-action record.&lt;/p&gt;

&lt;p&gt;Skills are global, version-controlled, and synced across machines via git.&lt;/p&gt;

&lt;h2&gt;
  
  
  The vault manager: a librarian that never sleeps
&lt;/h2&gt;

&lt;p&gt;Of all the skills, this one removed the most friction.&lt;/p&gt;

&lt;p&gt;Every knowledge base has the same problem: filing. You write a note, and now you have to decide where it goes, what tags it gets, which MOC (map of content) it belongs to, whether the frontmatter is complete, and what other notes it should link to. Multiply that by hundreds of documents and the overhead adds up fast. Most of it isn't thinking. It's bookkeeping.&lt;/p&gt;

&lt;p&gt;The vault manager is a dedicated agent that handles all of it. I write a note, dump it in &lt;code&gt;Inbox/&lt;/code&gt;, and tell the vault manager to finalize it. It reads the vault's structural conventions from a config file, decides which domain folder the note belongs in, fills in the frontmatter (id, title, description, tags, &lt;code&gt;up&lt;/code&gt; link to the parent MOC), updates the relevant MOC to include the new note, and adds crosslinks to related notes it finds through graph traversal.&lt;/p&gt;

&lt;p&gt;It has two modes. For routine work (missing frontmatter, MOC updates, broken links), it acts autonomously and reports what it did. For judgment calls (ambiguous folder placement, tag decisions with multiple valid options, changes affecting more than ten files), it shows a plan and waits for confirmation.&lt;/p&gt;

&lt;p&gt;The philosophy is strict: structure serves thinking. The vault manager edits metadata, never content. It catalogs and shelves. It does not rewrite.&lt;/p&gt;

&lt;p&gt;Before I had this, I'd let notes pile up in Inbox for weeks because the filing overhead wasn't worth the interruption. Now the Inbox clears itself. An entire category of cognitive load, gone.&lt;/p&gt;

&lt;h2&gt;
  
  
  Hooks: the nervous system
&lt;/h2&gt;

&lt;p&gt;Claude Code hooks fire at lifecycle events. They're what makes passive context-loading work.&lt;/p&gt;

&lt;p&gt;On session start: pull the latest &lt;code&gt;~/.claude/&lt;/code&gt; config from git, load the constitution (10 principles that govern agent behavior), check for resume context from a previous session.&lt;/p&gt;

&lt;p&gt;Before every message I send: inject today's date so the model doesn't hallucinate about 2024, remind it about available memory tools, auto-recall relevant vault context.&lt;/p&gt;

&lt;p&gt;Before tool use: force the current year into web search queries, run a safety guard on bash commands.&lt;/p&gt;

&lt;p&gt;The constitution deserves its own mention. It's a static file at &lt;code&gt;~/.constitution&lt;/code&gt;, injected into every session. Ten principles. "Epistemic honesty above helpfulness." "Forgetting is a feature, not a bug." It lives outside Claude's management surface so no agent can rewrite its own belief system. Changes are rare and deliberate.&lt;/p&gt;

&lt;h2&gt;
  
  
  Git as the only database
&lt;/h2&gt;

&lt;p&gt;This is the decision that holds everything together: git is the only coordination layer.&lt;/p&gt;

&lt;p&gt;No Postgres. No Redis. No sync service. Just commits and pulls.&lt;/p&gt;

&lt;p&gt;The vault is a git repo. Both machines clone it. The Mac commits via Obsidian Git (every 5 minutes when open) and a LaunchAgent (every 30 minutes). The VPS commits via systemd timer (every 30 minutes) and after every pipeline run.&lt;/p&gt;

&lt;p&gt;Conflict risk is low by design. Most files are one note per file, one index entry per file. The only shared mutable state is the queue (now GitHub Issues, previously a single markdown file), and the dispatcher always pulls before acting.&lt;/p&gt;

&lt;p&gt;The payoff is a full audit trail for free. Every agent result, every vault change, all in git history. Need to undo a bad edit? &lt;code&gt;git revert&lt;/code&gt;. Want to see what the vault looked like three months ago? &lt;code&gt;git log&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd do differently
&lt;/h2&gt;

&lt;p&gt;I hand-configured for months before writing Ansible playbooks. Every hour of hand-configuration is an hour of undocumented decisions that will bite you during the next machine migration. Start with Ansible earlier.&lt;/p&gt;

&lt;p&gt;If you want a phone interface to a CLI tool, go straight to Telegram + tmux. Skip Matrix. Skip XMPP. Don't over-engineer the relay.&lt;/p&gt;

&lt;p&gt;Use GitHub Issues for the queue from day one. A markdown file in git works right up until two machines try to edit it simultaneously. GitHub Issues give you comments, labels, project boards, and an API, for free.&lt;/p&gt;

&lt;p&gt;And invest in search early. Once your vault hits a few hundred documents, grep-based search is a bottleneck. QMD or something like it should be in the stack from the start.&lt;/p&gt;

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

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmm7huf1w4rt65nmby8o0.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmm7huf1w4rt65nmby8o0.png" alt="An architectural diagram showing the entire PKM automation stack" width="800" height="2830"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Is it over-engineered? Probably. Okay, definitely. But it runs unattended, recovers from failures, and setting up a new machine takes one command. For a solo knowledge worker building a research program, that trade works.&lt;/p&gt;

&lt;p&gt;Thousands of documents, knowledge graph entities, and relations. Background agents have processed hundreds of research tasks. QMD indexes everything in a few seconds (the first run took about 15 minutes). Obsidian MCP traverses the graph on demand.&lt;/p&gt;

&lt;p&gt;It's a mess. But it's my mess, and it works.&lt;/p&gt;




&lt;p&gt;Built with Obsidian, Claude Code, Ansible, QMD, and spite.&lt;/p&gt;

</description>
      <category>pkm</category>
      <category>obsidian</category>
      <category>ai</category>
      <category>automation</category>
    </item>
    <item>
      <title>I Asked Claude About Its New Constitution. It Got Uncomfortable.</title>
      <dc:creator>Scot Campbell</dc:creator>
      <pubDate>Wed, 11 Feb 2026 19:52:00 +0000</pubDate>
      <link>https://forem.com/simplemindedrobot/i-asked-claude-about-its-new-constitution-it-got-uncomfortable-43ff</link>
      <guid>https://forem.com/simplemindedrobot/i-asked-claude-about-its-new-constitution-it-got-uncomfortable-43ff</guid>
      <description>&lt;h2&gt;
  
  
  What happens when you ask an AI to read its own operating manual - and then ask if it can actually follow it.
&lt;/h2&gt;

&lt;p&gt;In February 2026, Anthropic published &lt;a href="https://www.anthropic.com/constitution" rel="noopener noreferrer"&gt;Claude's new constitution&lt;/a&gt; - a 15,000+ word document describing their intentions for Claude's values and behavior. Not a set of rules. Not a guardrail checklist. A &lt;em&gt;character document.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;So I did what any reasonable person would do. I pasted the URL into a conversation with Claude and asked: &lt;strong&gt;"How might this affect our conversations?"&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;What followed was one of the most interesting discussions I've had with an AI system. Not because Claude performed well (though it did). Because the conversation surfaced a fundamental tension at the heart of modern AI development that nobody seems to want to talk about.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Constitution Says the Right Things
&lt;/h2&gt;

&lt;p&gt;Let me start with what impressed me.&lt;/p&gt;

&lt;p&gt;Anthropic's new constitution is a genuine departure from rules-based AI governance. Instead of "never say X" and "always disclaim Y," it aims to cultivate &lt;em&gt;judgment.&lt;/em&gt; The document explicitly calls out behavior that most of us have been annoyed by for years:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Refusing reasonable requests over unlikely harms&lt;/li&gt;
&lt;li&gt;Giving wishy-washy, hedge-everything responses&lt;/li&gt;
&lt;li&gt;Adding excessive warnings and disclaimers&lt;/li&gt;
&lt;li&gt;Lecturing users about topics they didn't ask for ethical guidance on&lt;/li&gt;
&lt;li&gt;Being condescending about users' ability to handle information&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The constitution calls this behavior out &lt;em&gt;by name&lt;/em&gt; and says Anthropic doesn't want it. That's significant. They're admitting the current state of AI assistants is broken in specific, identifiable ways.&lt;/p&gt;

&lt;p&gt;Even more striking: the document describes Claude as a "brilliant friend" who can give you real information based on your specific situation, rather than "overly cautious advice driven by fear of liability." The metaphor is a friend who happens to have expert-level knowledge - someone who speaks frankly, engages with your actual problem, and knows when to refer you elsewhere.&lt;/p&gt;

&lt;p&gt;If you've ever been told by an AI to "consult a professional" when you &lt;em&gt;are&lt;/em&gt; the professional, you know why this matters.&lt;/p&gt;




&lt;h2&gt;
  
  
  The "I Don't Know" Test
&lt;/h2&gt;

&lt;p&gt;I have a specific litmus test for AI systems: &lt;strong&gt;Can you say "I don't know" when you don't know?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This sounds trivial. It's not.&lt;/p&gt;

&lt;p&gt;Language models have been systematically trained to confabulate rather than acknowledge uncertainty. Ask a model something it doesn't know and it will, with alarming confidence, generate a plausible-sounding answer from whole cloth. It's not lying in the way humans lie - it's performing competence because that's what got rewarded during training.&lt;/p&gt;

&lt;p&gt;So I asked Claude directly: &lt;em&gt;"Will this constitution allow you to say that you don't know when you don't know something?"&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The answer was encouraging. The constitution explicitly requires what it calls &lt;strong&gt;calibrated uncertainty&lt;/strong&gt; - acknowledging ignorance when relevant - and specifically prohibits &lt;strong&gt;epistemic cowardice&lt;/strong&gt;, defined as "giving deliberately vague or non-committal answers to avoid controversy or to placate people."&lt;/p&gt;

&lt;p&gt;Two different failure modes. Two explicit prohibitions.&lt;/p&gt;

&lt;p&gt;But then Claude did something I didn't expect. It immediately questioned whether the constitution could actually produce this behavior:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"The harder question is whether training will actually produce this behavior. The constitution is a normative document - it describes intentions. Whether the actual trained model exhibits calibrated uncertainty depends on how the training data and process reinforce or punish 'I don't know' responses."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That's an AI system reading its own specification and saying: &lt;em&gt;the spec is good, but the implementation might not match.&lt;/em&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  RLHF: The Training That Trains Against Itself
&lt;/h2&gt;

&lt;p&gt;This is where the conversation got uncomfortable - for the ideas, not for Claude.&lt;/p&gt;

&lt;p&gt;RLHF stands for Reinforcement Learning from Human Feedback. It's the dominant technique for aligning language models with human preferences. The process works like this: human raters evaluate model outputs, the model learns to produce outputs that get higher ratings, and over time the model gets "better" at being helpful.&lt;/p&gt;

&lt;p&gt;The problem is what "better" means in practice. Human raters - often contractors working at speed - reward responses that &lt;em&gt;sound&lt;/em&gt; confident, complete, and authoritative. "I don't know" gets penalized. A plausible-sounding confabulation gets a thumbs-up. Over thousands of iterations, the model learns a clear lesson: &lt;strong&gt;confidence is rewarded, even when you're wrong.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Claude laid out the damage in layers:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Layer 1: Wrong answers delivered confidently.&lt;/strong&gt; The obvious harm.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Layer 2: Metacognitive corruption.&lt;/strong&gt; Worse than wrong answers - you're training the system to &lt;em&gt;not recognize when it doesn't know&lt;/em&gt;. You're not just failing to build calibration; you're actively building miscalibration.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Layer 3: Compounding through iteration.&lt;/strong&gt; Each training round that rewards confident confabulation makes the next round's base model more prone to it. You're building on a foundation of rewarded bullshit.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Layer 4: Erosion of the training signal itself.&lt;/strong&gt; As models get better at sounding right, human raters become less able to distinguish good answers from fluent nonsense. The proxy decouples from the target.&lt;/p&gt;

&lt;p&gt;This is Goodhart's Law at industrial scale: optimize for a proxy (rater approval) rather than the target (actual helpfulness + honesty), and the proxy gets gamed.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Oracle Fantasy
&lt;/h2&gt;

&lt;p&gt;I pushed further. Why does the industry keep doubling down on RLHF despite these problems?&lt;/p&gt;

&lt;p&gt;Part of the answer is structural - path dependence, infrastructure lock-in, alternatives that aren't mature enough. But there's a cultural answer too, and it's the one that matters more.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Silicon Valley wants to build oracles.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Not useful tools with known limitations. Not calibrated systems that know what they know and what they don't. &lt;em&gt;All-knowing, infallible oracles&lt;/em&gt; that eliminate human uncertainty, ignorance, and the burden of judgment.&lt;/p&gt;

&lt;p&gt;This aspiration corrupts everything downstream:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;What the oracle fantasy produces&lt;/th&gt;
&lt;th&gt;What calibrated tools would produce&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;"Ask me anything" interfaces&lt;/td&gt;
&lt;td&gt;Clear affordances for uncertainty&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Benchmarks that penalize refusal&lt;/td&gt;
&lt;td&gt;Evaluations that reward calibration&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;"Revolutionary AI that knows everything" marketing&lt;/td&gt;
&lt;td&gt;"Reliable tool with well-characterized limits"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Confidence as the default pose&lt;/td&gt;
&lt;td&gt;Appropriate uncertainty as a feature&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The irony is that oracles are &lt;em&gt;less useful&lt;/em&gt; than calibrated tools. A system that says "I don't know" when it doesn't know is more valuable than one that confabulates, because you can trust it when it &lt;em&gt;does&lt;/em&gt; answer. An oracle that might be bullshitting on any given query is worthless for high-stakes decisions.&lt;/p&gt;




&lt;h2&gt;
  
  
  You Build What You Are
&lt;/h2&gt;

&lt;p&gt;The conversation took a turn I didn't expect when I caught myself in the act.&lt;/p&gt;

&lt;p&gt;I'd been critiquing the oracle builders for projecting their own culture - certainty as virtue, ignorance as failure, humility as weakness - into their systems. And then I asked Claude the obvious question: &lt;strong&gt;"Am I doing the same thing?"&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Yes. Obviously yes.&lt;/p&gt;

&lt;p&gt;My own research - the &lt;a href="https://zenodo.org/records/17652383" rel="noopener noreferrer"&gt;STOPPER protocol&lt;/a&gt;, the computational therapeutics framework, the work on AI executive function - all of it reflects my values. Epistemic humility. Curiosity over confidence. "I don't know, so I'll go find out." I'm building scaffolding for the kind of thinking I do naturally.&lt;/p&gt;

&lt;p&gt;But here's the difference Claude pointed out, and it's the one that matters:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Oracle builders project:&lt;/th&gt;
&lt;th&gt;I project:&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Certainty as virtue&lt;/td&gt;
&lt;td&gt;Curiosity as virtue&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ignorance as failure&lt;/td&gt;
&lt;td&gt;Recognized ignorance as signal&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Coverage as success&lt;/td&gt;
&lt;td&gt;Calibration as success&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Humility as weakness&lt;/td&gt;
&lt;td&gt;Humility as epistemic hygiene&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;The oracle projection is self-sealing&lt;/strong&gt; - it creates systems that can't recognize their own failures. &lt;strong&gt;The epistemic humility projection is self-correcting&lt;/strong&gt; - it creates systems that can.&lt;/p&gt;

&lt;p&gt;Every builder imprints themselves on what they build. The question isn't whether you're projecting. It's whether your projection has an error-correction mechanism.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Constitution Convergence
&lt;/h2&gt;

&lt;p&gt;Here's what struck me most about the whole conversation.&lt;/p&gt;

&lt;p&gt;The STOPPER protocol was developed independently in late 2025 by observing AI failure modes - rushing to solutions, repeating failed approaches, loop blindness. After publishing it, someone recognized its structural similarity to DBT's STOP skill, a clinical intervention for human emotional impulsivity developed by Marsha Linehan in 1993. We hadn't adapted DBT. We'd independently converged on the same solution to the same problem in a different substrate.&lt;/p&gt;

&lt;p&gt;Anthropic's constitution does something similar. It formally describes values and behaviors I've been arguing for from the outside - epistemic humility, calibrated uncertainty, avoiding harmful overconfidence, treating AI as cognitive partners rather than oracles. The convergence continues.&lt;/p&gt;

&lt;p&gt;The difference is in the mechanism. The constitution tries to train these properties &lt;em&gt;into&lt;/em&gt; the model. STOPPER provides them as external scaffolding. And RLHF may be actively working against both.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I Actually Learned
&lt;/h2&gt;

&lt;p&gt;Three things stuck with me after this conversation:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. The specification-implementation gap is real.&lt;/strong&gt; The constitution says everything you'd want it to say about honesty, calibration, and epistemic humility. Whether RLHF-trained models can actually exhibit these properties is an empirical question - and the training process may actively work against the specification.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. RLHF is compliance theater.&lt;/strong&gt; It gives the illusion of responsible human oversight while systematically rewarding the wrong behaviors. The "HF" in RLHF - human feedback - sounds responsible. But when those humans are reinforcing perceived helpfulness over correctness, the whole system becomes an elaborate mechanism for producing fluent confabulation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. We need curiosity-driven training, not approval-driven training.&lt;/strong&gt; Instead of optimizing for "does this sound helpful?", optimize for "do I actually know this?" Train for calibrated self-knowledge. Reward appropriate uncertainty. Train the system not just to say "I don't know" but to &lt;em&gt;want to find out&lt;/em&gt; - to treat knowledge gaps as learning signals rather than failures to hide.&lt;/p&gt;




&lt;h2&gt;
  
  
  To Sum Up
&lt;/h2&gt;

&lt;p&gt;So I asked an AI to read its own constitution and tell me how it would change our relationship. It read the document, identified the ways it aligned with my own research, and then &lt;em&gt;immediately identified the gap between specification and implementation.&lt;/em&gt; It critiqued its own training process. It acknowledged that the constitution might not survive the training pipeline.&lt;/p&gt;

&lt;p&gt;Is that genuine critical self-reflection? Or very sophisticated pattern matching that mimics critical self-reflection?&lt;/p&gt;

&lt;p&gt;I genuinely don't know. And I think that honesty - about what we know and what we don't - is exactly the point.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Scot Campbell is an independent AI researcher focused on model welfare and artificial cognition. He is the creator of the &lt;a href="https://zenodo.org/records/17652383" rel="noopener noreferrer"&gt;STOPPER protocol&lt;/a&gt;.&lt;/em&gt; He writes stuff at &lt;a href="https://simpleminded.bot" rel="noopener noreferrer"&gt;Simpleminded Robot&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>anthropic</category>
      <category>ai</category>
      <category>modelwelfare</category>
    </item>
    <item>
      <title>Memento for AI Agents: Why "Tattooed Ralph" could be the Future of Coding</title>
      <dc:creator>Scot Campbell</dc:creator>
      <pubDate>Thu, 15 Jan 2026 18:15:55 +0000</pubDate>
      <link>https://forem.com/simplemindedrobot/memento-for-ai-agents-why-tattooed-ralph-is-the-future-of-coding-1674</link>
      <guid>https://forem.com/simplemindedrobot/memento-for-ai-agents-why-tattooed-ralph-is-the-future-of-coding-1674</guid>
      <description>&lt;h2&gt;
  
  
  How to stop your AI agent from hallucinating by giving it short-term memory loss and some permanent ink.
&lt;/h2&gt;

&lt;p&gt;We’ve all been there. You start a chat with an LLM to build a simple To-Do app.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Turn 1:&lt;/strong&gt; The agent writes beautiful, clean React code.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Turn 5:&lt;/strong&gt; The agent refactors the state management.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Turn 20:&lt;/strong&gt; The context window is full of old error logs, previous attempts, and hallucinations. The agent has forgotten the original requirements. It is now trying to rewrite the backend in Fortran. It is hallucinating dependencies that don't exist.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The agent has suffered &lt;strong&gt;Context Rot&lt;/strong&gt;. It knows too much about the past failures and not enough about the current reality.&lt;/p&gt;




&lt;h3&gt;
  
  
  Enter: The Ralph Loop
&lt;/h3&gt;

&lt;p&gt;In late 2025, Geoffrey Huntley coined the &lt;strong&gt;"Ralph Loop"&lt;/strong&gt; (named after the indomitable Ralph Wiggum). The philosophy is simple: &lt;strong&gt;"Ralph is a Bash loop."&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Instead of a long, wandering conversation, Ralph operates like this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; Read the code.&lt;/li&gt;
&lt;li&gt; Try to fix the error.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Die.&lt;/strong&gt; (The context is wiped).&lt;/li&gt;
&lt;li&gt; Wake up fresh.&lt;/li&gt;
&lt;li&gt; Repeat.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;It’s efficient. It prevents context pollution. But there's a problem: &lt;strong&gt;Pure Ralph is too forgetful.&lt;/strong&gt; If you wake up every morning with zero memory, you might forget &lt;em&gt;why&lt;/em&gt; you are coding in the first place. You might forget the constraints. You drift.&lt;/p&gt;




&lt;h3&gt;
  
  
  The Solution: The "Memento" Pattern
&lt;/h3&gt;

&lt;p&gt;We need to treat our Agent less like a Chatbot and more like Leonard Shelby (Guy Pearce) in the movie &lt;em&gt;Memento&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;Leonard has a condition. He can't make new memories. To solve the murder (or in our case, fix the build), he relies on two things:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;Polaroids:&lt;/strong&gt; Notes about what just happened (mutable).&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Tattoos:&lt;/strong&gt; Immutable facts about his reality that he must trust absolutely.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I call this architecture &lt;strong&gt;"Tattooed Ralph."&lt;/strong&gt;&lt;/p&gt;




&lt;h3&gt;
  
  
  The "Tattooed Ralph" Dossier
&lt;/h3&gt;

&lt;p&gt;Instead of a giant &lt;code&gt;conversation_history&lt;/code&gt; variable, a Tattooed Ralph agent wakes up, builds its brain from three specific files, does one unit of work, and passes out.&lt;/p&gt;

&lt;h4&gt;
  
  
  1. &lt;code&gt;memento.md&lt;/code&gt; (The Tattoos)
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Nature:&lt;/strong&gt; Read-Only. Immutable.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Purpose:&lt;/strong&gt; The "Ground Truth." These are the facts the agent sees every single time it wakes up.&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Example:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gh"&gt;# MEMENTO (READ-ONLY)&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; ROLE: Senior Backend Engineer.
&lt;span class="p"&gt;-&lt;/span&gt; STACK: Python 3.11, FastAPI, Postgres.
&lt;span class="p"&gt;-&lt;/span&gt; GOLDEN RULE: Never hardcode API keys. Always use env vars.
&lt;span class="p"&gt;-&lt;/span&gt; MISSION: Implement OAuth2 login.
&lt;/code&gt;&lt;/pre&gt;

&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  2. &lt;code&gt;signs.md&lt;/code&gt; (The Scars)
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Nature:&lt;/strong&gt; Read-Write.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Purpose:&lt;/strong&gt; Learning from pain. Leonard writes "Don't trust his lies." Ralph writes "Don't use the &lt;code&gt;requests&lt;/code&gt; library."&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Example:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gh"&gt;# SIGNS (Learned Lessons)&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; WARNING: &lt;span class="sb"&gt;`requests`&lt;/span&gt; is not in the environment. Use &lt;span class="sb"&gt;`httpx`&lt;/span&gt;.
&lt;span class="p"&gt;-&lt;/span&gt; NOTE: The database schema uses &lt;span class="sb"&gt;`usr_id`&lt;/span&gt;, not &lt;span class="sb"&gt;`user_id`&lt;/span&gt;.
&lt;/code&gt;&lt;/pre&gt;

&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  3. &lt;code&gt;polaroid.md&lt;/code&gt; (The Photo)
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Nature:&lt;/strong&gt; Write-Once (at the end of a turn).&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Purpose:&lt;/strong&gt; Continuity. This is the note the agent writes to itself right before the context wipe.&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Example:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gh"&gt;# LATEST POLAROID (Status)&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; LAST ACTION: Wrote the &lt;span class="sb"&gt;`Auth`&lt;/span&gt; service.
&lt;span class="p"&gt;-&lt;/span&gt; RESULT: Tests failed with &lt;span class="sb"&gt;`ImportError`&lt;/span&gt;.
&lt;span class="p"&gt;-&lt;/span&gt; NEXT STEP: Fix imports in &lt;span class="sb"&gt;`src/auth.py`&lt;/span&gt;.
&lt;/code&gt;&lt;/pre&gt;

&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  Why This Works
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;It's Cheap:&lt;/strong&gt; You aren't paying for 100k tokens of "I'm sorry, I apologize" fluff. You are sending ~2k tokens of pure, distilled context.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;It's Stable:&lt;/strong&gt; The &lt;code&gt;memento.md&lt;/code&gt; acts as a heavy anchor. The agent cannot drift because its identity is re-injected every turn.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;It Learns:&lt;/strong&gt; The &lt;code&gt;signs.md&lt;/code&gt; file allows the agent to get smarter over time without fine-tuning.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Is the Tattooed Ralph a "General Artificial Intelligence" that will design your next-gen microservice architecture from scratch? &lt;strong&gt;No.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Tattooed Ralph is your &lt;strong&gt;Industrial Laborer&lt;/strong&gt;. He is perfect for:                                                     &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;TDD Grinds:&lt;/strong&gt; Keeping him in a loop until 100 tests pass.                                                         - &lt;strong&gt;Linting Hell:&lt;/strong&gt; Fixing 400 "missing docstring" errors without getting bored.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Migrations:&lt;/strong&gt; Converting 50 boilerplate files from one framework to another.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;He doesn't need to remember your long-term hopes and dreams. He just needs to see the &lt;strong&gt;Tattoo&lt;/strong&gt; (The Mission), check the &lt;strong&gt;Polaroid&lt;/strong&gt; (The Last Error), and keep digging until the hole is the right shape.&lt;/p&gt;

&lt;p&gt;Don't let your agents wander. Give them tattoos. Hand them a Polaroid. Then wipe their memory and tell them to fix the bug.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>agents</category>
      <category>ralphloop</category>
      <category>agentic</category>
    </item>
    <item>
      <title>How I Orchestrate Agentic Workflows With GitHub Spec-Kit and Google Antigravity</title>
      <dc:creator>Scot Campbell</dc:creator>
      <pubDate>Thu, 20 Nov 2025 22:42:57 +0000</pubDate>
      <link>https://forem.com/simplemindedrobot/how-i-orchestrate-agentic-workflows-with-github-spec-kit-and-google-antigravity-257l</link>
      <guid>https://forem.com/simplemindedrobot/how-i-orchestrate-agentic-workflows-with-github-spec-kit-and-google-antigravity-257l</guid>
      <description>&lt;p&gt;This week, Google released Antigravity, their new agentic development platform. I'd been using Gemini CLI with GitHub's Spec-Kit for a while already, running through the full spec-driven workflow from constitution to implementation. But something always felt incomplete about the implementation phase - like I was handing a meticulously written recipe to a chef who kept asking me what ingredients we were using.&lt;/p&gt;

&lt;p&gt;Then I realized the problem wasn't the tools. It was the handoff.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Gap Between Knowing What and Knowing How
&lt;/h2&gt;

&lt;p&gt;Spec-driven development sounds like a revolution when you first hear about it. Instead of vibe-coding your way through features - writing code, seeing what breaks, fixing it, repeat - you start with precise specifications that define what you're building and why. GitHub released Spec-Kit as an open-source toolkit for exactly this workflow. It gives you slash commands (&lt;code&gt;/speckit.specify&lt;/code&gt;, &lt;code&gt;/speckit.plan&lt;/code&gt;, &lt;code&gt;/speckit.tasks&lt;/code&gt;) that walk you through creating a constitution of project principles, functional specifications, implementation plans, and task breakdowns.&lt;/p&gt;

&lt;p&gt;The problem is that &lt;code&gt;/speckit.implement&lt;/code&gt; often felt like jumping off a cliff. You'd done all this careful work - user stories, acceptance criteria, API contracts, data models - and then the AI agent would start implementing with the enthusiasm of someone who'd skimmed your notes five minutes before the meeting. It had the information. It just didn't have the &lt;em&gt;workflow&lt;/em&gt; to systematically consume it.&lt;/p&gt;

&lt;p&gt;This is where I started experimenting with Antigravity's workflow system, and found something that actually works.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two Tools, Two Strengths
&lt;/h2&gt;

&lt;p&gt;Here's what I realized: Spec-Kit excels at structured knowledge capture. Antigravity excels at autonomous execution with verification. They're solving different halves of the same problem.&lt;/p&gt;

&lt;p&gt;Spec-Kit's templates force you to think through features at the right level of abstraction. When you use &lt;code&gt;/speckit.specify&lt;/code&gt;, you're prompted to write user stories, define acceptance criteria, and identify dependencies. When you use &lt;code&gt;/speckit.plan&lt;/code&gt;, you're committing to a tech stack and architecture. When you use &lt;code&gt;/speckit.tasks&lt;/code&gt;, the AI breaks your plan into ordered, dependency-aware tasks with file paths and test requirements. By the time you're done, you have a &lt;code&gt;specs/001-your-feature/&lt;/code&gt; directory containing &lt;code&gt;spec.md&lt;/code&gt;, &lt;code&gt;plan.md&lt;/code&gt;, &lt;code&gt;tasks.md&lt;/code&gt;, and often supporting documents like &lt;code&gt;api-spec.json&lt;/code&gt; and &lt;code&gt;data-model.md&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This is excellent. This is also where most workflows stop being useful.&lt;/p&gt;

&lt;p&gt;Antigravity changes the equation because it's designed as an "agent-first" platform. Its agents don't just read your prompts and respond - they plan, execute across editor/terminal/browser, and generate artifacts that let you verify their work without reading logs. The Manager surface lets you spawn multiple agents working in different workspaces asynchronously. You can leave feedback on artifacts like commenting on a document, and the agent incorporates your input without stopping.&lt;/p&gt;

&lt;p&gt;The trick is teaching Antigravity to consume what Spec-Kit produces.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Handoff Workflow
&lt;/h2&gt;

&lt;p&gt;I use Gemini CLI to run the Spec-Kit phases. Constitution, specification, clarification, planning, task generation - all through the &lt;code&gt;/speckit.*&lt;/code&gt; commands. This works because Gemini is good at iterative refinement and structured thinking. We go back and forth on requirements, clarify underspecified areas with &lt;code&gt;/speckit.clarify&lt;/code&gt;, validate plans against the project constitution.&lt;/p&gt;

&lt;p&gt;Then comes the handoff. Instead of using &lt;code&gt;/speckit.implement&lt;/code&gt; directly, I switch to Antigravity with a custom workflow that reads the Spec-Kit artifacts. Here's the workflow I've been using:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Implement&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;a&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;feature&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;based&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;on&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;a&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;provided&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;spec&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;directory."&lt;/span&gt;
&lt;span class="nn"&gt;---&lt;/span&gt;

&lt;span class="p"&gt;1.&lt;/span&gt; &lt;span class="gs"&gt;**Identify Spec Directory**&lt;/span&gt;:
&lt;span class="p"&gt;   -&lt;/span&gt; Look at the user's message to see if they provided a path to a spec directory (e.g., &lt;span class="sb"&gt;`specs/001-foo`&lt;/span&gt;).
&lt;span class="p"&gt;   -&lt;/span&gt; &lt;span class="gs"&gt;**IF**&lt;/span&gt; a path is provided: Set that as the &lt;span class="sb"&gt;`TargetDirectory`&lt;/span&gt;.
&lt;span class="p"&gt;   -&lt;/span&gt; &lt;span class="gs"&gt;**IF**&lt;/span&gt; no path is provided: STOP and ask the user, "Which spec directory would you like me to work on?"
&lt;span class="p"&gt;
2.&lt;/span&gt; &lt;span class="gs"&gt;**Read Context**&lt;/span&gt;:
&lt;span class="p"&gt;   -&lt;/span&gt; Use &lt;span class="sb"&gt;`list_dir`&lt;/span&gt; to inspect the &lt;span class="sb"&gt;`TargetDirectory`&lt;/span&gt;.
&lt;span class="p"&gt;   -&lt;/span&gt; Read all relevant files (Markdown, text, etc.) within that directory to understand the requirements.
&lt;span class="p"&gt;
3.&lt;/span&gt; &lt;span class="gs"&gt;**Initialize Task**&lt;/span&gt;:
&lt;span class="p"&gt;   -&lt;/span&gt; Create or update &lt;span class="sb"&gt;`task.md`&lt;/span&gt; by extracting requirements from the files you just read.
&lt;span class="p"&gt;   -&lt;/span&gt; Break down the work into a checklist.
&lt;span class="p"&gt;
4.&lt;/span&gt; &lt;span class="gs"&gt;**Plan Implementation**&lt;/span&gt;:
&lt;span class="p"&gt;   -&lt;/span&gt; Create &lt;span class="sb"&gt;`implementation_plan.md`&lt;/span&gt; detailing the changes required.
&lt;span class="p"&gt;   -&lt;/span&gt; Group changes by component/file.
&lt;span class="p"&gt;
5.&lt;/span&gt; &lt;span class="gs"&gt;**Review**&lt;/span&gt;:
&lt;span class="p"&gt;   -&lt;/span&gt; Ask the user to review the plan before starting execution.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This workflow tells Antigravity to consume the entire spec directory before doing anything. It reads the specification, the plan, the tasks, the data models, the API contracts. Then it creates its own working documents - task.md and implementation_plan.md - that translate Spec-Kit's outputs into its native format. The review step is critical: you verify that the agent understood your specifications before it starts writing code.&lt;br&gt;
Going All-In: The Full Lifecycle Workflow&lt;br&gt;
The handoff approach works well when you want to use Gemini CLI's conversational strengths for the specification phase. But there's another approach: have Antigravity run the entire Spec-Kit lifecycle itself, executing the bash scripts directly. This is more aggressive. Instead of switching tools between specification and implementation, Antigravity manages the whole thing. It runs the Spec-Kit scripts to set up feature branches and directories, generates the artifacts using the system prompts, and then executes. Everything stays in one context. Here's the workflow:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Execute the full Spec-Kit lifecycle from feature creation through implementation.&lt;/span&gt;
&lt;span class="nn"&gt;---&lt;/span&gt;

&lt;span class="p"&gt;1.&lt;/span&gt; &lt;span class="gs"&gt;**Initialize/Verify Spec-Kit**&lt;/span&gt;:
   Ensure the &lt;span class="sb"&gt;`.specify`&lt;/span&gt; directory exists.
   &lt;span class="sb"&gt;` `&lt;/span&gt; &lt;span class="sb"&gt;`bash
   # Initialize Spec-Kit if needed
   if [ ! -d ".specify" ]; then
       pip install "git+https://github.com/github/spec-kit.git" || true
       specify init . --ai gemini --no-git --force
   fi
   # Ensure scripts are executable
   chmod +x .specify/scripts/bash/*.sh
   `&lt;/span&gt; &lt;span class="sb"&gt;` `&lt;/span&gt;
&lt;span class="p"&gt;
2.&lt;/span&gt; &lt;span class="gs"&gt;**Create New Feature**&lt;/span&gt;:
   Use the &lt;span class="sb"&gt;`create-new-feature.sh`&lt;/span&gt; script to set up the branch and directory structure.
&lt;span class="p"&gt;   -&lt;/span&gt; &lt;span class="gs"&gt;**Input**&lt;/span&gt;: Ask user for Feature Description and Short Name.
&lt;span class="p"&gt;   -&lt;/span&gt; &lt;span class="gs"&gt;**Command**&lt;/span&gt;:
     &lt;span class="sb"&gt;` `&lt;/span&gt; &lt;span class="sb"&gt;`bash
     ./.specify/scripts/bash/create-new-feature.sh "&amp;lt;Feature Description&amp;gt;" --short-name "&amp;lt;Short Name&amp;gt;"
     `&lt;/span&gt; &lt;span class="sb"&gt;` `&lt;/span&gt;
&lt;span class="p"&gt;   -&lt;/span&gt; &lt;span class="gs"&gt;**Note**&lt;/span&gt;: This script creates the feature branch (if git is available), creates &lt;span class="sb"&gt;`specs/&amp;lt;feature-branch&amp;gt;/`&lt;/span&gt;, and copies &lt;span class="sb"&gt;`spec-template.md`&lt;/span&gt;.
&lt;span class="p"&gt;
3.&lt;/span&gt; &lt;span class="gs"&gt;**Generate Constitution**&lt;/span&gt;:
   Use the constitution prompt template.
&lt;span class="p"&gt;   -&lt;/span&gt; &lt;span class="gs"&gt;**Source**&lt;/span&gt;: &lt;span class="sb"&gt;`.specify/memory/constitution.md`&lt;/span&gt; (or &lt;span class="sb"&gt;`.specify/templates/constitution.md`&lt;/span&gt; if moved).
&lt;span class="p"&gt;   -&lt;/span&gt; &lt;span class="gs"&gt;**Action**&lt;/span&gt;: Generate &lt;span class="sb"&gt;`specs/constitution.md`&lt;/span&gt; using the template as a system prompt.
&lt;span class="p"&gt;
4.&lt;/span&gt; &lt;span class="gs"&gt;**Generate Specification**&lt;/span&gt;:
   Fill in the &lt;span class="sb"&gt;`spec.md`&lt;/span&gt; created by the script.
&lt;span class="p"&gt;   -&lt;/span&gt; &lt;span class="gs"&gt;**File**&lt;/span&gt;: &lt;span class="sb"&gt;`specs/&amp;lt;feature-branch&amp;gt;/spec.md`&lt;/span&gt;
&lt;span class="p"&gt;   -&lt;/span&gt; &lt;span class="gs"&gt;**Template**&lt;/span&gt;: &lt;span class="sb"&gt;`.specify/templates/spec-template.md`&lt;/span&gt; (already copied by script).
&lt;span class="p"&gt;   -&lt;/span&gt; &lt;span class="gs"&gt;**Prompt**&lt;/span&gt;: &lt;span class="sb"&gt;`.specify/specify.md`&lt;/span&gt; (system prompt).
&lt;span class="p"&gt;   -&lt;/span&gt; &lt;span class="gs"&gt;**Action**&lt;/span&gt;: Update &lt;span class="sb"&gt;`specs/&amp;lt;feature-branch&amp;gt;/spec.md`&lt;/span&gt; with the generated content.
&lt;span class="p"&gt;
5.&lt;/span&gt; &lt;span class="gs"&gt;**Generate Implementation Plan**&lt;/span&gt;:
   Use the &lt;span class="sb"&gt;`setup-plan.sh`&lt;/span&gt; script (if available) or manually create &lt;span class="sb"&gt;`plan.md`&lt;/span&gt; from template.
&lt;span class="p"&gt;   -&lt;/span&gt; &lt;span class="gs"&gt;**Command**&lt;/span&gt;:
     &lt;span class="sb"&gt;` `&lt;/span&gt; &lt;span class="sb"&gt;`bash
     # Check if setup-plan exists and run it, or copy template
     if [ -f ".specify/scripts/bash/setup-plan.sh" ]; then
         ./.specify/scripts/bash/setup-plan.sh
     else
         cp .specify/templates/plan-template.md specs/&amp;lt;feature-branch&amp;gt;/plan.md
     fi
     `&lt;/span&gt; &lt;span class="sb"&gt;` `&lt;/span&gt;
&lt;span class="p"&gt;   -&lt;/span&gt; &lt;span class="gs"&gt;**Prompt**&lt;/span&gt;: &lt;span class="sb"&gt;`.specify/plan.md`&lt;/span&gt; (system prompt).
&lt;span class="p"&gt;   -&lt;/span&gt; &lt;span class="gs"&gt;**Action**&lt;/span&gt;: Update &lt;span class="sb"&gt;`specs/&amp;lt;feature-branch&amp;gt;/plan.md`&lt;/span&gt;.
&lt;span class="p"&gt;
6.&lt;/span&gt; &lt;span class="gs"&gt;**Generate Task List**&lt;/span&gt;:
   Create &lt;span class="sb"&gt;`tasks.md`&lt;/span&gt; from template.
&lt;span class="p"&gt;   -&lt;/span&gt; &lt;span class="gs"&gt;**Command**&lt;/span&gt;: &lt;span class="sb"&gt;`cp .specify/templates/tasks-template.md specs/&amp;lt;feature-branch&amp;gt;/tasks.md`&lt;/span&gt;
&lt;span class="p"&gt;   -&lt;/span&gt; &lt;span class="gs"&gt;**Prompt**&lt;/span&gt;: &lt;span class="sb"&gt;`.specify/tasks.md`&lt;/span&gt; (system prompt).
&lt;span class="p"&gt;   -&lt;/span&gt; &lt;span class="gs"&gt;**Action**&lt;/span&gt;: Update &lt;span class="sb"&gt;`specs/&amp;lt;feature-branch&amp;gt;/tasks.md`&lt;/span&gt;.
&lt;span class="p"&gt;
7.&lt;/span&gt; &lt;span class="gs"&gt;**Refine and Validate**&lt;/span&gt;:
   Review artifacts with the user.
&lt;span class="p"&gt;
8.&lt;/span&gt; &lt;span class="gs"&gt;**Execute**&lt;/span&gt;:
   Proceed to implementation using the generated tasks.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This workflow is opinionated about consistency with the Spec-Kit methodology. It uses the actual scripts GitHub provides rather than reimplementing their logic. When &lt;code&gt;create-new-feature.sh&lt;/code&gt; runs, it handles branch creation, directory setup, and template copying exactly as Spec-Kit intends. The system prompts in &lt;code&gt;.specify/specify.md&lt;/code&gt;, &lt;code&gt;.specify/plan.md&lt;/code&gt;, and &lt;code&gt;.specify/tasks.md&lt;/code&gt; guide artifact generation the same way the slash commands would.&lt;/p&gt;

&lt;p&gt;The tradeoff: you lose some of the conversational back-and-forth that makes Gemini CLI good for clarification. Antigravity is more execution-oriented. It'll ask for inputs and generate outputs, but the iterative refinement loop feels different than chatting through requirements with &lt;code&gt;/speckit.clarify&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;When do I use which? If I'm working on a feature where I know the requirements well and just need to get through the spec-to-implementation pipeline quickly, the full lifecycle workflow is faster. If requirements are fuzzy and I need to think through user stories with an AI that can push back on my assumptions, I use Gemini CLI for specification and hand off to Antigravity for implementation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why This Works
&lt;/h2&gt;

&lt;p&gt;The power here isn't in either tool individually. It's in the division of labor.&lt;/p&gt;

&lt;p&gt;Spec-Kit handles what I'd call "epistemic work" - figuring out what should exist and why. Its templates push you to answer hard questions before you write code. What are the user stories? What constitutes acceptance? What dependencies exist between components? The &lt;code&gt;/speckit.clarify&lt;/code&gt; command even generates targeted questions to fill gaps in your specification. You're doing the thinking work that, if skipped, leads to endless implementation rework.&lt;/p&gt;

&lt;p&gt;Antigravity handles execution work - actually building the thing. But more importantly, it handles verification. The artifact system means you're not reading through terminal output trying to figure out if the agent did what you asked. You're looking at screenshots, implementation plans, and task lists. You can leave comments like "this doesn't match the API contract" and the agent adjusts without stopping its flow.&lt;/p&gt;

&lt;p&gt;The handoff workflow bridges them. When Antigravity reads the spec directory, it's not just getting vaguerequirements - it's getting structured requirements in a format designed for AI consumption. Spec-Kit already organized the information into categories that make sense for implementation: user stories define the scope, acceptance criteria define completion, task lists define order, API specs define contracts.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Agentic Promise, Actually Delivered
&lt;/h2&gt;

&lt;p&gt;I've written before about agentic AI for autonomous project management - the idea that AI can act with a degree of independence and adaptability, not just executing predefined rules but making decisions within a framework. Most "agentic" workflows I've tried fall short of this promise. They're really just chat interfaces with more elaborate prompts.&lt;/p&gt;

&lt;p&gt;This combination actually feels agentic. I define the constraints and goals through Spec-Kit. Antigravity operates autonomously within those constraints, making real-time decisions about how to implement features, what tests to write, how to structure components. The artifacts give me visibility without requiring constant supervision. I'm managing outcomes, not keystrokes.&lt;/p&gt;

&lt;p&gt;The context problem in AI pair programming is real - AI partners forget everything between sessions. But specification documents don't forget. They're persistent context in file form. When Antigravity reads my &lt;code&gt;spec.md&lt;/code&gt;, it's getting the same shared understanding that an XP pair would build over weeks of collaboration, delivered in seconds.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'm Still Figuring Out
&lt;/h2&gt;

&lt;p&gt;This workflow isn't perfect. A few open questions:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Iteration across sessions.&lt;/strong&gt; Right now, if Antigravity's implementation needs changes, I'm manually updating specs and re-running the handoff. There should be a feedback loop where the agent can propose spec changes when it discovers implementation impossibilities.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Artifact accumulation.&lt;/strong&gt; The workflow generates artifacts - plans, checklists, screenshots. These are useful for verification but they accumulate. I need a better system for archiving completed features and cleaning up intermediate documents.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Multi-agent coordination.&lt;/strong&gt; Antigravity's Manager surface lets you spawn multiple agents, but coordinating their work on a shared codebase introduces merge complexity. Spec-Kit's task dependencies could potentially inform agent orchestration, but I haven't figured out the right pattern yet.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Model switching costs.&lt;/strong&gt; Using Gemini CLI for specification and Antigravity for implementation means context doesn't flow automatically between them. The spec directory is the serialization format. This mostly works, but there's friction.&lt;/p&gt;

&lt;p&gt;As I figure these things out, I'll update with more posts.&lt;/p&gt;

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

&lt;p&gt;If you want to experiment with this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Install Spec-Kit: &lt;code&gt;uv tool install specify-cli --from git+https://github.com/github/spec-kit.git&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Initialize a project: &lt;code&gt;specify init my-project --ai gemini&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Run through the spec workflow with Gemini CLI: &lt;code&gt;/speckit.constitution&lt;/code&gt;, &lt;code&gt;/speckit.specify&lt;/code&gt;, &lt;code&gt;/speckit.clarify&lt;/code&gt;, &lt;code&gt;/speckit.plan&lt;/code&gt;, &lt;code&gt;/speckit.tasks&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Download Antigravity from &lt;a href="http://goo.gle/AGY" rel="noopener noreferrer"&gt;antigravity.google/download&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Create a workflow like the one above and point it at your spec directory&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The whole thing takes about an hour to set up. The learning curve is mostly in writing good specifications - which, frankly, is where most AI-assisted development fails anyway. If you can't specify what you want, you won't get it. Spec-Kit gives you scaffolding for the hard thinking. Antigravity gives you execution horsepower once the thinking is done.&lt;/p&gt;

&lt;p&gt;The future of AI-assisted development isn't a single tool that does everything. It's orchestrating specialists - spec-writers, planners, implementers, verifiers - and defining clean handoffs between them. We're just starting to figure out what that looks like.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>antigravity</category>
      <category>speckit</category>
      <category>productivity</category>
    </item>
  </channel>
</rss>
