<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>Ai-Agents on Krishnatejaswi S</title>
    <link>https://KTS-o7.github.io/bear/tags/ai-agents/</link>
    <description>Recent content in Ai-Agents on Krishnatejaswi S</description>
    <generator>Hugo -- gohugo.io</generator>
    <language>en-us</language>
    <managingEditor>shentharkrishnatejaswi@gmail.com (Krishnatejaswi S)</managingEditor>
    <webMaster>shentharkrishnatejaswi@gmail.com (Krishnatejaswi S)</webMaster>
    <copyright>Krishnatejaswi S</copyright>
    <lastBuildDate>Thu, 16 Apr 2026 00:00:00 +0530</lastBuildDate>
    <atom:link href="https://KTS-o7.github.io/bear/tags/ai-agents/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>Fixing Silent $0 Streaming Costs in AgentBudget: An OSS Contribution</title>
      <link>https://KTS-o7.github.io/bear/notes/agentbudget_ulimit_for_ai_agents/</link>
      <pubDate>Thu, 16 Apr 2026 00:00:00 +0530</pubDate><author>shentharkrishnatejaswi@gmail.com (Krishnatejaswi S)</author>
      <guid>https://KTS-o7.github.io/bear/notes/agentbudget_ulimit_for_ai_agents/</guid>
      <description>&lt;p&gt;AgentBudget patches the OpenAI and Anthropic SDKs to track the cost of every LLM call. Set a budget, get a hard limit. Simple premise.&lt;/p&gt;&#xA;&lt;p&gt;Except there was a gap. Every streaming call cost $0.00 in the ledger. Silently. No error, no warning &amp;ndash; just wrong numbers.&lt;/p&gt;&#xA;&lt;p&gt;I found the gap, fixed it, and got the PR merged. This is the story of how streaming cost tracking actually works.&lt;/p&gt;&#xA;&lt;h2 id=&#34;what-agentbudget-does&#34;&gt;What AgentBudget does&lt;/h2&gt;&#xA;&lt;p&gt;AgentBudget is an open-source SDK that puts a hard dollar limit on AI agent sessions. Two lines, no infrastructure:&lt;/p&gt;</description>
      <content:encoded><![CDATA[<p>AgentBudget patches the OpenAI and Anthropic SDKs to track the cost of every LLM call. Set a budget, get a hard limit. Simple premise.</p>
<p>Except there was a gap. Every streaming call cost $0.00 in the ledger. Silently. No error, no warning &ndash; just wrong numbers.</p>
<p>I found the gap, fixed it, and got the PR merged. This is the story of how streaming cost tracking actually works.</p>
<h2 id="what-agentbudget-does">What AgentBudget does</h2>
<p>AgentBudget is an open-source SDK that puts a hard dollar limit on AI agent sessions. Two lines, no infrastructure:</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln">1</span><span class="cl"><span class="kn">import</span> <span class="nn">agentbudget</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="n">agentbudget</span><span class="o">.</span><span class="n">init</span><span class="p">(</span><span class="s2">&#34;$5.00&#34;</span><span class="p">)</span></span></span></code></pre></div><p>It works by monkey-patching <code>Completions.create</code> (OpenAI) and <code>Messages.create</code> (Anthropic) at init time. Every call goes through a wrapper that looks up the model cost, accumulates spend, and raises <code>BudgetExhausted</code> when the limit is hit.</p>
<p>The non-streaming path worked fine. The streaming path did not.</p>
<h2 id="the-bug">The bug</h2>
<p>When <code>stream=True</code>, the patched method receives a <code>Stream</code> object back from the SDK &ndash; not a completed response with token counts. The original code had no handling for this case. It silently passed the stream through untracked.</p>
<p>Every streaming call registered as $0.00 in the ledger. Any agent using streaming could blow past its budget with zero resistance. The circuit breaker was blind to half its traffic.</p>
<h2 id="why-streaming-is-harder-to-cost">Why streaming is harder to cost</h2>
<p>Non-streaming is straightforward: the response comes back with a <code>usage</code> object containing <code>prompt_tokens</code> and <code>completion_tokens</code>. Multiply by the per-token rate, done.</p>
<p>Streaming returns a sequence of chunks. Token counts are not available upfront &ndash; they only appear after the stream is exhausted. For OpenAI specifically, usage data on the final chunk is opt-in: you need <code>stream_options={&quot;include_usage&quot;: True}</code> in the request, otherwise the final chunk carries no usage data and there is nothing to cost.</p>
<p>Anthropic handles it differently. Usage data is always present, split across two events: <code>message_start</code> carries input tokens, <code>message_delta</code> carries output tokens at the end.</p>
<h2 id="the-fix-transparent-wrapper-classes">The fix: transparent wrapper classes</h2>
<p>The solution is four wrapper classes that intercept the stream, yield every chunk unchanged, and record cost after the iterator is exhausted:</p>
<table>
  <thead>
      <tr>
          <th>Class</th>
          <th>Wraps</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>_OpenAIStreamWrapper</code></td>
          <td><code>openai.Stream[ChatCompletionChunk]</code></td>
      </tr>
      <tr>
          <td><code>_AsyncOpenAIStreamWrapper</code></td>
          <td><code>openai.AsyncStream[ChatCompletionChunk]</code></td>
      </tr>
      <tr>
          <td><code>_AnthropicStreamWrapper</code></td>
          <td><code>anthropic.Stream[RawMessageStreamEvent]</code></td>
      </tr>
      <tr>
          <td><code>_AsyncAnthropicStreamWrapper</code></td>
          <td><code>anthropic.AsyncStream[RawMessageStreamEvent]</code></td>
      </tr>
  </tbody>
</table>
<p>Each wrapper is fully transparent to the caller. The existing code pattern &ndash; <code>for chunk in stream:</code> or <code>async for chunk in stream:</code> or <code>with client.stream(...) as stream:</code> &ndash; continues to work without any changes.</p>
<p>The wrapper accumulates token counts as chunks arrive, then records the cost in the session ledger once the stream ends.</p>
<p>For OpenAI:</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="k">class</span> <span class="nc">_OpenAIStreamWrapper</span><span class="p">:</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">    <span class="k">def</span> <span class="fm">__init__</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">stream</span><span class="p">,</span> <span class="n">session</span><span class="p">,</span> <span class="n">model</span><span class="p">):</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">_stream</span> <span class="o">=</span> <span class="n">stream</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">_session</span> <span class="o">=</span> <span class="n">session</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">_model</span> <span class="o">=</span> <span class="n">model</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">    <span class="k">def</span> <span class="fm">__iter__</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">        <span class="k">for</span> <span class="n">chunk</span> <span class="ow">in</span> <span class="bp">self</span><span class="o">.</span><span class="n">_stream</span><span class="p">:</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">            <span class="c1"># Track usage from the final chunk</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">            <span class="k">if</span> <span class="n">chunk</span><span class="o">.</span><span class="n">usage</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">                <span class="n">cost</span> <span class="o">=</span> <span class="n">price</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">_model</span><span class="p">,</span> <span class="n">chunk</span><span class="o">.</span><span class="n">usage</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">                <span class="bp">self</span><span class="o">.</span><span class="n">_session</span><span class="o">.</span><span class="n">record</span><span class="p">(</span><span class="n">cost</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">            <span class="k">yield</span> <span class="n">chunk</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">
</span></span><span class="line"><span class="ln">15</span><span class="cl">    <span class="c1"># Full protocol: __enter__, __exit__, close()</span></span></span></code></pre></div><p>For Anthropic, the accumulation spans two event types:</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="k">class</span> <span class="nc">_AnthropicStreamWrapper</span><span class="p">:</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">    <span class="k">def</span> <span class="fm">__iter__</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">        <span class="n">input_tokens</span> <span class="o">=</span> <span class="mi">0</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">        <span class="n">output_tokens</span> <span class="o">=</span> <span class="mi">0</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">        <span class="k">for</span> <span class="n">event</span> <span class="ow">in</span> <span class="bp">self</span><span class="o">.</span><span class="n">_stream</span><span class="p">:</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">            <span class="k">if</span> <span class="n">event</span><span class="o">.</span><span class="n">type</span> <span class="o">==</span> <span class="s2">&#34;message_start&#34;</span><span class="p">:</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">                <span class="n">input_tokens</span> <span class="o">=</span> <span class="n">event</span><span class="o">.</span><span class="n">message</span><span class="o">.</span><span class="n">usage</span><span class="o">.</span><span class="n">input_tokens</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">            <span class="k">elif</span> <span class="n">event</span><span class="o">.</span><span class="n">type</span> <span class="o">==</span> <span class="s2">&#34;message_delta&#34;</span><span class="p">:</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">                <span class="n">output_tokens</span> <span class="o">=</span> <span class="n">event</span><span class="o">.</span><span class="n">usage</span><span class="o">.</span><span class="n">output_tokens</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">            <span class="k">yield</span> <span class="n">event</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">        <span class="n">cost</span> <span class="o">=</span> <span class="n">price</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">_model</span><span class="p">,</span> <span class="n">input_tokens</span><span class="p">,</span> <span class="n">output_tokens</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">_session</span><span class="o">.</span><span class="n">record</span><span class="p">(</span><span class="n">cost</span><span class="p">)</span></span></span></code></pre></div><p>The detection of whether a response is a stream happens via <code>isinstance</code> checks guarded by <code>ImportError</code>, so the wrapper degrades gracefully if only one SDK is installed.</p>
<h2 id="implementing-the-full-protocol">Implementing the full protocol</h2>
<p>The tricky part is not the cost tracking logic &ndash; it is making the wrapper implement the full stream protocol correctly.</p>
<p>Python streams support multiple usage patterns: iterator (<code>for chunk in stream</code>), context manager (<code>with stream</code>), async iterator, async context manager, and explicit <code>close()</code>/<code>aclose()</code>. Missing any of these means certain callers will break with an <code>AttributeError</code> or a resource leak.</p>
<p>The wrapper needs to implement all of them:</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1"># Sync: iterator + context manager + close</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="k">def</span> <span class="fm">__iter__</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span> <span class="o">...</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="k">def</span> <span class="fm">__enter__</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span> <span class="k">return</span> <span class="bp">self</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="k">def</span> <span class="fm">__exit__</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="o">*</span><span class="n">args</span><span class="p">):</span> <span class="bp">self</span><span class="o">.</span><span class="n">close</span><span class="p">()</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="k">def</span> <span class="nf">close</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span> <span class="bp">self</span><span class="o">.</span><span class="n">_stream</span><span class="o">.</span><span class="n">close</span><span class="p">()</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">
</span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="c1"># Async: async iterator + async context manager + aclose</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="k">async</span> <span class="k">def</span> <span class="fm">__aiter__</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span> <span class="o">...</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="k">async</span> <span class="k">def</span> <span class="fm">__aenter__</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span> <span class="k">return</span> <span class="bp">self</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="k">async</span> <span class="k">def</span> <span class="fm">__aexit__</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="o">*</span><span class="n">args</span><span class="p">):</span> <span class="k">await</span> <span class="bp">self</span><span class="o">.</span><span class="n">aclose</span><span class="p">()</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="k">async</span> <span class="k">def</span> <span class="nf">aclose</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span> <span class="k">await</span> <span class="bp">self</span><span class="o">.</span><span class="n">_stream</span><span class="o">.</span><span class="n">aclose</span><span class="p">()</span></span></span></code></pre></div><p>Getting this wrong is invisible in unit tests with mocks but breaks immediately against real SDK objects.</p>
<h2 id="tests">Tests</h2>
<p>The PR added 19 tests covering:</p>
<ul>
<li>Cost recorded correctly after stream exhaustion</li>
<li>Chunks with no usage data skipped without error</li>
<li>Passthrough identity &ndash; every chunk the caller sees is unchanged</li>
<li><code>BudgetExhausted</code> propagates correctly mid-stream</li>
<li>Both <code>for chunk in stream:</code> and <code>with client.stream() as stream:</code> patterns</li>
<li><code>close()</code> and <code>aclose()</code> delegation to the underlying stream</li>
<li>Drop-in mode (<code>agentbudget.init()</code>) wrapping end-to-end</li>
<li>No-session noop &ndash; if no budget session is active, streaming is fully transparent</li>
</ul>
<p>All 129 pre-existing tests continued to pass. 148 total passing at merge.</p>
<h2 id="the-known-limitation">The known limitation</h2>
<p>OpenAI streaming requires the caller to pass <code>stream_options={&quot;include_usage&quot;: True}</code>. Without it, the final chunk carries no usage data, and the cost is silently $0.</p>
<p>This is OpenAI&rsquo;s API behaviour &ndash; usage data on streams is opt-in. The limitation is documented in the module docstring. A future improvement could auto-inject the option at the patch layer, but that requires modifying the outbound request, which is a bigger change than this PR intended to be.</p>
<p>Anthropic has no equivalent limitation &ndash; usage is always present.</p>
<h2 id="what-i-learned">What I learned</h2>
<p><strong>Transparent wrappers need the full protocol.</strong> It is tempting to implement just <code>__iter__</code> and call it done. But real callers use context managers, explicit close, and async variants. Implementing the minimum breaks production code in ways that are hard to debug.</p>
<p><strong>Streaming costs are provider-specific.</strong> OpenAI and Anthropic have different conventions for where usage data appears in the stream. Any SDK that claims to support both needs to handle both separately.</p>
<p><strong>Test against real SDK objects, not just mocks.</strong> The <code>isinstance</code> detection and chunk structure only behave correctly against live SDK classes. Mock-based tests passed for the wrong reasons until I verified against an actual installed SDK.</p>
<h2 id="links">Links</h2>
<ul>
<li><a href="https://github.com/AgentBudget/agentbudget/pull/6">PR #6 &ndash; feat: track costs for streaming OpenAI and Anthropic responses</a></li>
<li><a href="https://github.com/AgentBudget/agentbudget">AgentBudget on GitHub</a></li>
<li><a href="https://pypi.org/project/agentbudget/">PyPI</a></li>
</ul>
]]></content:encoded>
    </item>
    <item>
      <title>Building a Local AI Stack: Hermes, Bifrost, Telegram, and Camoufox</title>
      <link>https://KTS-o7.github.io/bear/notes/hermes_local_ai_stack/</link>
      <pubDate>Wed, 15 Apr 2026 00:00:00 +0530</pubDate><author>shentharkrishnatejaswi@gmail.com (Krishnatejaswi S)</author>
      <guid>https://KTS-o7.github.io/bear/notes/hermes_local_ai_stack/</guid>
      <description>&lt;p&gt;I wanted a persistent AI agent that lives on my machine, reaches cloud models through a single gateway, and talks back to me on Telegram. Not a SaaS. Not a wrapper. Something I own end-to-end.&lt;/p&gt;&#xA;&lt;p&gt;This is the story of setting that up in one session, and the parts that went sideways.&lt;/p&gt;&#xA;&lt;h2 id=&#34;the-stack&#34;&gt;The Stack&lt;/h2&gt;&#xA;&lt;p&gt;Four moving parts:&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;&lt;strong&gt;Bifrost&lt;/strong&gt; &amp;ndash; LLM gateway already running on localhost:24242, routing to Ollama cloud models&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;Hermes Agent&lt;/strong&gt; &amp;ndash; open-source AI agent framework by Nous Research, pointed at Bifrost&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;Ollama&lt;/strong&gt; &amp;ndash; cloud relay for gemma4 and qwen3.5 cloud models via ollama.com&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;Camoufox&lt;/strong&gt; &amp;ndash; Firefox fork with TLS fingerprint spoofing for scraping tasks&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;The goal was to have Hermes run as a Telegram bot so I could assign it tasks from my phone, with all model traffic flowing through Bifrost so routing stays centralized.&lt;/p&gt;</description>
      <content:encoded><![CDATA[<p>I wanted a persistent AI agent that lives on my machine, reaches cloud models through a single gateway, and talks back to me on Telegram. Not a SaaS. Not a wrapper. Something I own end-to-end.</p>
<p>This is the story of setting that up in one session, and the parts that went sideways.</p>
<h2 id="the-stack">The Stack</h2>
<p>Four moving parts:</p>
<ul>
<li><strong>Bifrost</strong> &ndash; LLM gateway already running on localhost:24242, routing to Ollama cloud models</li>
<li><strong>Hermes Agent</strong> &ndash; open-source AI agent framework by Nous Research, pointed at Bifrost</li>
<li><strong>Ollama</strong> &ndash; cloud relay for gemma4 and qwen3.5 cloud models via ollama.com</li>
<li><strong>Camoufox</strong> &ndash; Firefox fork with TLS fingerprint spoofing for scraping tasks</li>
</ul>
<p>The goal was to have Hermes run as a Telegram bot so I could assign it tasks from my phone, with all model traffic flowing through Bifrost so routing stays centralized.</p>
<h2 id="bifrost-first">Bifrost First</h2>
<p>Bifrost was already up on port 24242 with Ollama configured. The only addition needed was an <code>ollama-cloud</code> provider pointing at <code>https://ollama.com</code> rather than localhost. This is a non-obvious distinction.</p>
<p>The trap: Ollama&rsquo;s OpenAI-compatible endpoint (<code>/v1/chat/completions</code>) does not work for cloud models. It hangs. Cloud models only work through Ollama&rsquo;s native <code>/api/chat</code> endpoint. Pointing Bifrost directly at <code>https://ollama.com</code> instead of <code>localhost:11434</code> bypasses the broken compat layer and hits the right path.</p>
<p>Once the provider was registered and <code>OLLAMA_API_KEY</code> was set in <code>.zshrc</code>, both cloud models confirmed working:</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">ollama pull gemma4:31b-cloud
</span></span><span class="line"><span class="ln">2</span><span class="cl">ollama pull qwen3.5:cloud</span></span></code></pre></div><h2 id="installing-hermes">Installing Hermes</h2>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">curl -fsSL https://raw.githubusercontent.com/NousResearch/hermes-agent/main/scripts/install.sh <span class="p">|</span> bash</span></span></code></pre></div><p>That puts everything under <code>~/.hermes/</code>. The setup wizard runs next:</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">hermes setup</span></span></code></pre></div><p>It walks through provider selection interactively. I pointed it at the Bifrost custom endpoint (<code>http://localhost:24242/v1</code>) and picked <code>ollama-cloud/gemma4:31b-cloud</code> as the default model.</p>
<p>Key config paths:</p>





<pre tabindex="0"><code>~/.hermes/config.yaml    -- main settings
~/.hermes/.env           -- secrets and API keys
~/.hermes/skills/        -- reusable agent procedures
~/.hermes/sessions/      -- full conversation history (searchable)</code></pre><p>After setup, a quick smoke test confirmed the agent was up and routing through Bifrost correctly:</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">hermes chat -q <span class="s2">&#34;what model are you using and where does traffic go&#34;</span></span></span></code></pre></div><h2 id="context-window">Context Window</h2>
<p>The default context length was under 20k tokens. Ollama cloud models support much larger windows. One config set:</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">hermes config <span class="nb">set</span> model.context_length <span class="m">200000</span></span></span></code></pre></div><p>Takes effect on the next session restart.</p>
<h2 id="telegram-bot">Telegram Bot</h2>
<p>This is where Hermes gets genuinely useful. The gateway turns it into a bot that accepts tasks from any platform you connect.</p>
<p>Create a bot via BotFather, get the token, then add it to <code>~/.hermes/.env</code>:</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl"><span class="nv">TELEGRAM_BOT_TOKEN</span><span class="o">=</span>&lt;your-bot-token&gt;
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="nv">TELEGRAM_ALLOWED_USERS</span><span class="o">=</span>&lt;your-numeric-telegram-id&gt;</span></span></code></pre></div><p><code>TELEGRAM_ALLOWED_USERS</code> takes the numeric user ID, not the username. Lock it down. Anyone who finds the bot URL can send it commands if this is not set.</p>
<p>Start the gateway:</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">hermes gateway run</span></span></code></pre></div><p>For persistent background operation, install it as a service:</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">hermes gateway install
</span></span><span class="line"><span class="ln">2</span><span class="cl">hermes gateway start</span></span></code></pre></div><p>A set of aliases in <code>.zshrc</code> makes it easier to manage day to day:</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl"><span class="nb">alias</span> hermes-gateway-start<span class="o">=</span><span class="s2">&#34;hermes gateway start&#34;</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="nb">alias</span> hermes-gateway-stop<span class="o">=</span><span class="s2">&#34;hermes gateway stop&#34;</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="nb">alias</span> hermes-gateway-log<span class="o">=</span><span class="s2">&#34;tail -f ~/.hermes/logs/gateway.log&#34;</span></span></span></code></pre></div><p>First run hit a missing Python dependency and failed silently. The fix was running <code>hermes gateway run</code> in the foreground once to see the real error, installing the missing package, then moving back to the service.</p>
<h2 id="camoufox">Camoufox</h2>
<p>Camoufox is a Firefox fork built for scraping. It spoofs TLS fingerprints so sites see a legitimate browser rather than a Playwright or CDP signature. It exposes a CDP endpoint that Hermes&rsquo;s browser tool connects to.</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">git clone https://github.com/daijro/camoufox
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="nb">cd</span> camoufox <span class="o">&amp;&amp;</span> npm install
</span></span><span class="line"><span class="ln">3</span><span class="cl">python -m camoufox fetch   <span class="c1"># ~300MB download</span></span></span></code></pre></div><p>The <code>camoufox fetch</code> step downloads the actual browser binary and does not run automatically via postinstall. If you skip it, the first start fails silently. Run it manually and wait for the download to finish before starting the server.</p>
<p>Start it on port 9377:</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">python -m camoufox server --port <span class="m">9377</span> <span class="p">&amp;</span></span></span></code></pre></div><p>Smoke test via Hermes:</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">hermes chat -q <span class="s2">&#34;use the browser to fetch the title of example.com&#34;</span></span></span></code></pre></div><p>Aliases for day-to-day:</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl"><span class="nb">alias</span> camofox-start<span class="o">=</span><span class="s2">&#34;python -m camoufox server --port 9377 &amp;&#34;</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="nb">alias</span> camofox-stop<span class="o">=</span><span class="s2">&#34;pkill -f &#39;camoufox server&#39;&#34;</span></span></span></code></pre></div><h2 id="what-actually-went-wrong">What Actually Went Wrong</h2>
<p><strong>Ollama native vs compat endpoint.</strong> Already covered above, but it cost a couple of hours. If a cloud model hangs at the provider level, check whether your gateway is talking to the right API path.</p>
<p><strong>Camoufox silent failure.</strong> The binary fetch is not in postinstall. No error, just no browser. Always run <code>python -m camoufox fetch</code> explicitly before the first start.</p>
<p><strong>Hermes gateway dependency.</strong> First start printed nothing useful. Run in foreground before daemonizing so you can see the actual error.</p>
<p><strong>Bifrost and empty API keys.</strong> Ollama initially rejected non-empty dummy keys in the Authorization header. Setting the key to an empty string before switching to the real <code>OLLAMA_API_KEY</code> got things unstuck. The order mattered.</p>
<p><strong>Session corruption in OpenCode.</strong> An OpenCode session used to orchestrate this setup got a zombie assistant message from an interrupted stream. The message had an empty string for content. Bifrost converted that empty string to null before forwarding, and the upstream provider rejected the entire next request with <code>messages.315.member.content must not be null</code>. Every retry included the same zombie in history, so the error was permanent until manually fixed.</p>
<p>The fix is to delete the zombie row directly from the OpenCode SQLite database:</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">sqlite3 ~/.local/share/opencode/opencode.db <span class="se">\
</span></span></span><span class="line"><span class="ln">2</span><span class="cl">  <span class="s2">&#34;DELETE FROM message WHERE id = &#39;&lt;zombie-message-id&gt;&#39; AND session_id = &#39;&lt;session-id&gt;&#39;;&#34;</span></span></span></code></pre></div><p>There is no built-in repair command. Back up the database before editing it.</p>
<h2 id="final-state">Final State</h2>
<p>Services running when done:</p>





<pre tabindex="0"><code>bifrost    :24242  -- Ollama cloud routes
ollama     :11434  -- cloud relay (gemma4, qwen3.5)
camoufox   :9377   -- CDP browser endpoint
hermes     gateway -- Telegram bot (restricted to single user)</code></pre><p>The agent receives messages on Telegram, runs tool calls locally (terminal, browser, file ops, web search), and routes all model traffic through Bifrost.</p>
<h2 id="whats-left">What&rsquo;s Left</h2>
<p>The original plan included running a local Qwen3.5-27B model via llama.cpp with Metal acceleration. That is a 13.5GB download and a build-from-source step, so it was deferred to the next session. The cloud routing setup works fine in the meantime.</p>
<h2 id="references">References</h2>
<ul>
<li>Hermes Agent: <a href="https://github.com/NousResearch/hermes-agent">https://github.com/NousResearch/hermes-agent</a></li>
<li>Hermes docs: <a href="https://hermes-agent.nousresearch.com/docs/">https://hermes-agent.nousresearch.com/docs/</a></li>
<li>Bifrost: <a href="https://github.com/maximhq/bifrost">https://github.com/maximhq/bifrost</a></li>
<li>Camoufox: <a href="https://github.com/daijro/camoufox">https://github.com/daijro/camoufox</a></li>
<li>OpenCode: <a href="https://opencode.ai">https://opencode.ai</a></li>
</ul>
]]></content:encoded>
    </item>
    <item>
      <title>Many Small Tasks, One Deadline: Running Parallel AI Agents with Git Worktrees</title>
      <link>https://KTS-o7.github.io/bear/notes/codex_worktrees_adventure/</link>
      <pubDate>Thu, 19 Feb 2026 00:00:00 +0530</pubDate><author>shentharkrishnatejaswi@gmail.com (Krishnatejaswi S)</author>
      <guid>https://KTS-o7.github.io/bear/notes/codex_worktrees_adventure/</guid>
      <description>&lt;p&gt;I thought this would be an easy day.&lt;/p&gt;&#xA;&lt;p&gt;The tasks were small. The backlog was not. This is the worst possible combo for a normal workflow because the fixed overhead dominates: branch setup, dependency sync, commits, reviewable diffs, and PR management. Doing it serially feels safe, but it is slow in a way that doesn&amp;rsquo;t show up in estimates.&lt;/p&gt;&#xA;&lt;p&gt;So I tried the obvious cheat: run multiple AI agents in parallel.&lt;/p&gt;</description>
      <content:encoded><![CDATA[<p>I thought this would be an easy day.</p>
<p>The tasks were small. The backlog was not. This is the worst possible combo for a normal workflow because the fixed overhead dominates: branch setup, dependency sync, commits, reviewable diffs, and PR management. Doing it serially feels safe, but it is slow in a way that doesn&rsquo;t show up in estimates.</p>
<p>So I tried the obvious cheat: run multiple AI agents in parallel.</p>
<p>It worked, but only after I tripped over three problems that are easy to reproduce if you try this without thinking through git state, shells, and sandbox boundaries.</p>
<h2 id="what-the-system-looked-like-so-you-dont-think-this-is-a-demo-problem">What the system looked like (so you don&rsquo;t think this is a demo problem)</h2>
<p>I ended up writing a small orchestration script with three phases:</p>
<ol>
<li>Create (or reuse) one worktree per task.</li>
<li>Install dependencies in parallel.</li>
<li>Run <code>codex exec</code> in batched parallel, then push branches and open draft PRs.</li>
</ol>
<p>The key detail is that batching wasn&rsquo;t “run everything at once”. It was “parallel within a batch, sequential across batches”, grouped by expected file overlap.</p>
<p>Here is the redacted shape of the script (this is not pseudocode; it&rsquo;s a sanitized skeleton of what I ran):</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-zsh" data-lang="zsh"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="cp">#!/usr/bin/env zsh
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="nb">set</span> -euo pipefail
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">
</span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="nv">WORKTREE_DIR</span><span class="o">=</span><span class="s2">&#34;.worktrees&#34;</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="nv">BASE_BRANCH</span><span class="o">=</span><span class="s2">&#34;&lt;base-branch&gt;&#34;</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="nv">LOG_DIR</span><span class="o">=</span><span class="s2">&#34;</span><span class="si">${</span><span class="nv">WORKTREE_DIR</span><span class="si">}</span><span class="s2">/_logs&#34;</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">mkdir -p <span class="s2">&#34;</span><span class="si">${</span><span class="nv">WORKTREE_DIR</span><span class="si">}</span><span class="s2">&#34;</span> <span class="s2">&#34;</span><span class="si">${</span><span class="nv">LOG_DIR</span><span class="si">}</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">
</span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="c1"># Phase 1: worktrees + deps (parallel installs)</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">hdr <span class="s2">&#34;Phase 1: Setting up worktrees&#34;</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">git worktree add -b <span class="s2">&#34;&lt;feature-branch&gt;&#34;</span> <span class="s2">&#34;</span><span class="si">${</span><span class="nv">WORKTREE_DIR</span><span class="si">}</span><span class="s2">/&lt;lane&gt;&#34;</span> <span class="s2">&#34;</span><span class="si">${</span><span class="nv">BASE_BRANCH</span><span class="si">}</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="o">(</span><span class="nb">cd</span> <span class="s2">&#34;</span><span class="si">${</span><span class="nv">WORKTREE_DIR</span><span class="si">}</span><span class="s2">/&lt;lane&gt;/frontend&#34;</span> <span class="o">&amp;&amp;</span> bun install --frozen-lockfile<span class="o">)</span> <span class="p">&amp;</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="o">(</span><span class="nb">cd</span> <span class="s2">&#34;</span><span class="si">${</span><span class="nv">WORKTREE_DIR</span><span class="si">}</span><span class="s2">/&lt;lane&gt;/backend&#34;</span>  <span class="o">&amp;&amp;</span> uv sync<span class="o">)</span> <span class="p">&amp;</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="nb">wait</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl">
</span></span><span class="line"><span class="ln">17</span><span class="cl"><span class="c1"># Phase 2: batched parallel codex exec (one log per lane)</span>
</span></span><span class="line"><span class="ln">18</span><span class="cl">hdr <span class="s2">&#34;Phase 2: Running Codex&#34;</span>
</span></span><span class="line"><span class="ln">19</span><span class="cl">codex <span class="nb">exec</span> --dangerously-bypass-approvals-and-sandbox -C <span class="s2">&#34;</span><span class="si">${</span><span class="nv">WORKTREE_DIR</span><span class="si">}</span><span class="s2">/&lt;lane&gt;&#34;</span> -m <span class="s2">&#34;</span><span class="si">${</span><span class="nv">CODEX_MODEL</span><span class="si">}</span><span class="s2">&#34;</span> <span class="s2">&#34;&lt;prompt&gt;&#34;</span> &gt; <span class="s2">&#34;</span><span class="si">${</span><span class="nv">LOG_DIR</span><span class="si">}</span><span class="s2">/&lt;lane&gt;.log&#34;</span> 2&gt;<span class="p">&amp;</span><span class="m">1</span> <span class="p">&amp;</span>
</span></span><span class="line"><span class="ln">20</span><span class="cl"><span class="nb">wait</span>
</span></span><span class="line"><span class="ln">21</span><span class="cl">
</span></span><span class="line"><span class="ln">22</span><span class="cl"><span class="c1"># Phase 3: push + PRs</span>
</span></span><span class="line"><span class="ln">23</span><span class="cl">hdr <span class="s2">&#34;Phase 3: PRs to </span><span class="si">${</span><span class="nv">BASE_BRANCH</span><span class="si">}</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="ln">24</span><span class="cl">git -C <span class="s2">&#34;</span><span class="si">${</span><span class="nv">WORKTREE_DIR</span><span class="si">}</span><span class="s2">/&lt;lane&gt;&#34;</span> push -u origin <span class="s2">&#34;&lt;feature-branch&gt;&#34;</span>
</span></span><span class="line"><span class="ln">25</span><span class="cl">gh pr create --base <span class="s2">&#34;</span><span class="si">${</span><span class="nv">BASE_BRANCH</span><span class="si">}</span><span class="s2">&#34;</span> --head <span class="s2">&#34;&lt;feature-branch&gt;&#34;</span> --draft --body <span class="s2">&#34;&lt;templated body&gt;&#34;</span></span></span></code></pre></div><p>Notice what this gives you immediately: one directory per lane, one log file per lane, and a clean boundary between “make changes” and “publish changes”.</p>
<p>Now for the parts that went wrong.</p>
<h2 id="failure-1-the-shell-lied-about-what-bash-means">Failure #1: The shell lied about what “Bash” means</h2>
<p>The first failure happened before any parallelism even started. The orchestration was written with associative arrays, and the system shell rejected it with:</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln">1</span><span class="cl">declare: -A: invalid option</span></span></code></pre></div><p>This is the kind of bug that looks embarrassing until you realize it is inevitable. macOS ships an older Bash; other environments may default to <code>sh</code>; CI runners may use different interpreters. If your control-plane script can&rsquo;t build the lane-to-branch mapping reliably, your entire run becomes a dice roll.</p>
<p>The fix was not “make it work on my machine”. The fix was pinning the interpreter and failing fast (<code>set -euo pipefail</code>). Once the script ran under the interpreter it was written for, this class of failure disappeared.</p>
<h2 id="failure-2-the-sandbox-was-fine-until-it-touched-git">Failure #2: The sandbox was fine until it touched <code>.git</code></h2>
<p>The second failure was more instructive: everything looked successful until commit/push time, and then git failed when trying to write worktree metadata:</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln">1</span><span class="cl">fatal: Unable to create &#39;.git/worktrees/&lt;lane&gt;/index.lock&#39;: Operation not permitted</span></span></code></pre></div><p>The important mental model is: “can edit files” is not equivalent to “can finish delivery”. Git history mutation needs to create locks and update metadata under <code>.git/</code>. Sandboxed agents often allow workspace file edits while denying <code>.git</code> writes by design.</p>
<p>The fix is to treat permissions as architecture. In my runbook now, implementation and version-control mutation are distinct phases. I keep guardrails on while generating and editing code, and only elevate privileges for <code>commit</code>, <code>rebase</code>, <code>push</code>, and PR creation. That sounds procedural, but it is the difference between parallelism and chaos.</p>
<h2 id="failure-3-the-pr-diff-was-computed-against-the-wrong-reality">Failure #3: The PR diff was computed against the wrong reality</h2>
<p>The third failure was the “ghost diff” class: the PR contained changes that “weren&rsquo;t part of the task” even though the worktree looked clean locally. This is not a GitHub bug. It is a merge-base bug.</p>
<p>PR diff is computed from:</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln">1</span><span class="cl">merge_base(feature, base) .. feature</span></span></code></pre></div><p>If your base ref is stale when the feature branch is compared, <code>merge_base</code> resolves behind your intended base, and commits that should be “already in base” appear as if they were introduced by the feature.</p>
<p>The only reliable approach is to verify ancestry before review and rebase if needed:</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">git merge-base --is-ancestor origin/&lt;base-branch&gt; HEAD <span class="o">||</span> <span class="nb">echo</span> REBASE_REQUIRED
</span></span><span class="line"><span class="ln">2</span><span class="cl">git diff --name-only origin/&lt;base-branch&gt;...HEAD</span></span></code></pre></div><p>This takes seconds and saves you from burning reviewer attention on diff noise.</p>
<h2 id="what-changed-once-i-actually-read-the-codex-cli-docs">What changed once I actually read the Codex CLI docs</h2>
<p>My first attempt treated Codex like a generic shell command that prints code. The docs made the control surface explicit, and that changed how I designed the run.</p>
<p>Approval and sandbox mode became part of the plan instead of an afterthought. Configuration precedence became something I could reason about when a lane behaved differently. Non-interactive <code>codex exec</code> became the default because it is scriptable and produces logs you can archive. And built-in review flows (like <code>/review</code>) became a lane-local quality gate before humans ever see the PR.</p>
<p>I also stopped mixing two different kinds of parallelism. Worktrees are for isolating tasks across lanes. Multi-agent mode is for decomposing one task inside a lane. When you treat them as separate tools, your system stays debuggable.</p>
<h2 id="the-boring-checklist">The boring checklist</h2>
<p>At this point the process is intentionally boring, which is exactly what you want near a deadline. My checklist is short:</p>
<ul>
<li>If the lane can&rsquo;t commit/push, it doesn&rsquo;t “count” as done. Fix the permission boundary first.</li>
<li>If the PR diff scope isn&rsquo;t what you expect, stop and fix ancestry before asking for review.</li>
<li>If a lane doesn&rsquo;t have a log file, assume it didn&rsquo;t run deterministically.</li>
</ul>
<h2 id="appendix-redacted-script-skeleton">Appendix: Redacted Script Skeleton</h2>
<p>Below is a condensed, anonymized skeleton of the script I used. It preserves the mechanics (worktrees, batching, logs, PR creation) but removes identity details.</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-zsh" data-lang="zsh"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="cp">#!/usr/bin/env zsh
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="nb">set</span> -euo pipefail
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">
</span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="nv">BASE_BRANCH</span><span class="o">=</span><span class="s2">&#34;&lt;base-branch&gt;&#34;</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="nv">WORKTREE_DIR</span><span class="o">=</span><span class="s2">&#34;.worktrees&#34;</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="nv">LOG_DIR</span><span class="o">=</span><span class="s2">&#34;</span><span class="si">${</span><span class="nv">WORKTREE_DIR</span><span class="si">}</span><span class="s2">/_logs&#34;</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">mkdir -p <span class="s2">&#34;</span><span class="si">${</span><span class="nv">WORKTREE_DIR</span><span class="si">}</span><span class="s2">&#34;</span> <span class="s2">&#34;</span><span class="si">${</span><span class="nv">LOG_DIR</span><span class="si">}</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">
</span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="nb">typeset</span> -A TASK_BRANCH TASK_TITLE TASK_PLAN TASK_LABEL
</span></span><span class="line"><span class="ln">11</span><span class="cl">TASK_BRANCH<span class="o">[</span>001<span class="o">]=</span><span class="s2">&#34;feature/lane-001&#34;</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">TASK_TITLE<span class="o">[</span>001<span class="o">]=</span><span class="s2">&#34;Lane 001: &lt;title&gt;&#34;</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">TASK_PLAN<span class="o">[</span>001<span class="o">]=</span><span class="s2">&#34;docs/plans/&lt;plan&gt;.md&#34;</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">TASK_LABEL<span class="o">[</span>001<span class="o">]=</span><span class="s2">&#34;&lt;label&gt;&#34;</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl">
</span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="nv">BATCH_1</span><span class="o">=(</span><span class="m">001</span> 002<span class="o">)</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl"><span class="nv">BATCH_2</span><span class="o">=(</span>003<span class="o">)</span>
</span></span><span class="line"><span class="ln">18</span><span class="cl">
</span></span><span class="line"><span class="ln">19</span><span class="cl">setup_lane<span class="o">()</span> <span class="o">{</span>
</span></span><span class="line"><span class="ln">20</span><span class="cl">  <span class="nb">local</span> <span class="nv">id</span><span class="o">=</span><span class="s2">&#34;</span><span class="nv">$1</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="ln">21</span><span class="cl">  <span class="nb">local</span> <span class="nv">branch</span><span class="o">=</span><span class="s2">&#34;</span><span class="si">${</span><span class="nv">TASK_BRANCH</span><span class="p">[</span><span class="nv">$id</span><span class="p">]</span><span class="si">}</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="ln">22</span><span class="cl">  <span class="nb">local</span> <span class="nv">worktree</span><span class="o">=</span><span class="s2">&#34;</span><span class="si">${</span><span class="nv">WORKTREE_DIR</span><span class="si">}</span><span class="s2">/lane-</span><span class="si">${</span><span class="nv">id</span><span class="si">}</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="ln">23</span><span class="cl">
</span></span><span class="line"><span class="ln">24</span><span class="cl">  <span class="k">if</span> <span class="o">[[</span> -d <span class="s2">&#34;</span><span class="si">${</span><span class="nv">worktree</span><span class="si">}</span><span class="s2">&#34;</span> <span class="o">]]</span><span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="ln">25</span><span class="cl">    <span class="nb">echo</span> <span class="s2">&#34;Reusing </span><span class="si">${</span><span class="nv">worktree</span><span class="si">}</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="ln">26</span><span class="cl">  <span class="k">else</span>
</span></span><span class="line"><span class="ln">27</span><span class="cl">    git worktree add -b <span class="s2">&#34;</span><span class="si">${</span><span class="nv">branch</span><span class="si">}</span><span class="s2">&#34;</span> <span class="s2">&#34;</span><span class="si">${</span><span class="nv">worktree</span><span class="si">}</span><span class="s2">&#34;</span> <span class="s2">&#34;</span><span class="si">${</span><span class="nv">BASE_BRANCH</span><span class="si">}</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="ln">28</span><span class="cl">  <span class="k">fi</span>
</span></span><span class="line"><span class="ln">29</span><span class="cl">
</span></span><span class="line"><span class="ln">30</span><span class="cl">  <span class="o">(</span><span class="nb">cd</span> <span class="s2">&#34;</span><span class="si">${</span><span class="nv">worktree</span><span class="si">}</span><span class="s2">/frontend&#34;</span> <span class="o">&amp;&amp;</span> bun install --frozen-lockfile<span class="o">)</span> <span class="p">&amp;</span>
</span></span><span class="line"><span class="ln">31</span><span class="cl">  <span class="o">(</span><span class="nb">cd</span> <span class="s2">&#34;</span><span class="si">${</span><span class="nv">worktree</span><span class="si">}</span><span class="s2">/backend&#34;</span>  <span class="o">&amp;&amp;</span> uv sync<span class="o">)</span> <span class="p">&amp;</span>
</span></span><span class="line"><span class="ln">32</span><span class="cl">  <span class="nb">wait</span>
</span></span><span class="line"><span class="ln">33</span><span class="cl"><span class="o">}</span>
</span></span><span class="line"><span class="ln">34</span><span class="cl">
</span></span><span class="line"><span class="ln">35</span><span class="cl">run_lane<span class="o">()</span> <span class="o">{</span>
</span></span><span class="line"><span class="ln">36</span><span class="cl">  <span class="nb">local</span> <span class="nv">id</span><span class="o">=</span><span class="s2">&#34;</span><span class="nv">$1</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="ln">37</span><span class="cl">  <span class="nb">local</span> <span class="nv">worktree</span><span class="o">=</span><span class="s2">&#34;</span><span class="si">${</span><span class="nv">WORKTREE_DIR</span><span class="si">}</span><span class="s2">/lane-</span><span class="si">${</span><span class="nv">id</span><span class="si">}</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="ln">38</span><span class="cl">  <span class="nb">local</span> <span class="nv">log</span><span class="o">=</span><span class="s2">&#34;</span><span class="si">${</span><span class="nv">LOG_DIR</span><span class="si">}</span><span class="s2">/lane-</span><span class="si">${</span><span class="nv">id</span><span class="si">}</span><span class="s2">.log&#34;</span>
</span></span><span class="line"><span class="ln">39</span><span class="cl">  <span class="nb">local</span> <span class="nv">prompt</span><span class="o">=</span><span class="s2">&#34;&lt;redacted prompt built from plan + conventions&gt;&#34;</span>
</span></span><span class="line"><span class="ln">40</span><span class="cl">
</span></span><span class="line"><span class="ln">41</span><span class="cl">  codex <span class="nb">exec</span> --dangerously-bypass-approvals-and-sandbox -C <span class="s2">&#34;</span><span class="si">${</span><span class="nv">worktree</span><span class="si">}</span><span class="s2">&#34;</span> <span class="s2">&#34;</span><span class="si">${</span><span class="nv">prompt</span><span class="si">}</span><span class="s2">&#34;</span> &gt; <span class="s2">&#34;</span><span class="si">${</span><span class="nv">log</span><span class="si">}</span><span class="s2">&#34;</span> 2&gt;<span class="p">&amp;</span><span class="m">1</span>
</span></span><span class="line"><span class="ln">42</span><span class="cl"><span class="o">}</span>
</span></span><span class="line"><span class="ln">43</span><span class="cl">
</span></span><span class="line"><span class="ln">44</span><span class="cl">create_pr<span class="o">()</span> <span class="o">{</span>
</span></span><span class="line"><span class="ln">45</span><span class="cl">  <span class="nb">local</span> <span class="nv">id</span><span class="o">=</span><span class="s2">&#34;</span><span class="nv">$1</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="ln">46</span><span class="cl">  <span class="nb">local</span> <span class="nv">branch</span><span class="o">=</span><span class="s2">&#34;</span><span class="si">${</span><span class="nv">TASK_BRANCH</span><span class="p">[</span><span class="nv">$id</span><span class="p">]</span><span class="si">}</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="ln">47</span><span class="cl">  <span class="nb">local</span> <span class="nv">title</span><span class="o">=</span><span class="s2">&#34;</span><span class="si">${</span><span class="nv">TASK_TITLE</span><span class="p">[</span><span class="nv">$id</span><span class="p">]</span><span class="si">}</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="ln">48</span><span class="cl">  <span class="nb">local</span> <span class="nv">worktree</span><span class="o">=</span><span class="s2">&#34;</span><span class="si">${</span><span class="nv">WORKTREE_DIR</span><span class="si">}</span><span class="s2">/lane-</span><span class="si">${</span><span class="nv">id</span><span class="si">}</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="ln">49</span><span class="cl">
</span></span><span class="line"><span class="ln">50</span><span class="cl">  git -C <span class="s2">&#34;</span><span class="si">${</span><span class="nv">worktree</span><span class="si">}</span><span class="s2">&#34;</span> push -u origin <span class="s2">&#34;</span><span class="si">${</span><span class="nv">branch</span><span class="si">}</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="ln">51</span><span class="cl">  gh pr create --base <span class="s2">&#34;</span><span class="si">${</span><span class="nv">BASE_BRANCH</span><span class="si">}</span><span class="s2">&#34;</span> --head <span class="s2">&#34;</span><span class="si">${</span><span class="nv">branch</span><span class="si">}</span><span class="s2">&#34;</span> --draft --title <span class="s2">&#34;</span><span class="si">${</span><span class="nv">title</span><span class="si">}</span><span class="s2">&#34;</span> --body <span class="s2">&#34;&lt;templated body&gt;&#34;</span>
</span></span><span class="line"><span class="ln">52</span><span class="cl"><span class="o">}</span></span></span></code></pre></div><h2 id="references">References</h2>
<ul>
<li>Codex docs hub: <a href="https://developers.openai.com/codex">https://developers.openai.com/codex</a></li>
<li>Codex CLI overview: <a href="https://developers.openai.com/codex/cli">https://developers.openai.com/codex/cli</a></li>
<li>Codex CLI features: <a href="https://developers.openai.com/codex/cli/features">https://developers.openai.com/codex/cli/features</a></li>
<li>Codex multi-agent docs: <a href="https://developers.openai.com/codex/multi-agent">https://developers.openai.com/codex/multi-agent</a></li>
<li>Codex security/sandbox docs: <a href="https://developers.openai.com/codex/security">https://developers.openai.com/codex/security</a></li>
<li>Codex config basics: <a href="https://developers.openai.com/codex/config-basic">https://developers.openai.com/codex/config-basic</a></li>
<li>Codex config reference: <a href="https://developers.openai.com/codex/config-reference">https://developers.openai.com/codex/config-reference</a></li>
<li>Codex CLI GitHub README: <a href="https://github.com/openai/codex/blob/main/README.md">https://github.com/openai/codex/blob/main/README.md</a></li>
<li>Ralph loop agent (Vercel Labs): <a href="https://github.com/vercel-labs/ralph-loop-agent">https://github.com/vercel-labs/ralph-loop-agent</a></li>
<li>Ralph from first principles (Geoffrey Huntley): <a href="https://www.youtube.com/watch?v=4Nna09dG_c0">https://www.youtube.com/watch?v=4Nna09dG_c0</a></li>
<li>Git worktree docs: <a href="https://git-scm.com/docs/git-worktree">https://git-scm.com/docs/git-worktree</a></li>
</ul>
]]></content:encoded>
    </item>
  </channel>
</rss>
