<?xml version="1.0" encoding="utf-8" standalone="yes"?><?xml-stylesheet type="text/xsl" href="https://perrotta.dev/rss.xsl"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    <title>Coding on ¬ just serendipity 🍀</title>
    <link>https://perrotta.dev/</link>
    <description>Recent content in Coding on ¬ just serendipity 🍀</description>
    <generator>Hugo</generator>
    <language>en-us</language>
    <managingEditor>serendipity@perrotta.dev (Thiago Perrotta)</managingEditor>
    <webMaster>serendipity@perrotta.dev (Thiago Perrotta)</webMaster>
    <copyright>© 2013 - 2026 Thiago Perrotta ·
  a fork of [hugo ʕ•ᴥ•ʔ bear](https://github.com/janraasch/hugo-bearblog/)
</copyright>
    <lastBuildDate>Mon, 27 Jul 2026 18:25:23 +0200</lastBuildDate>
    <atom:link href="https://perrotta.dev/tags/coding/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>pi: asynchronous exit summaries
      </title>
      <link>https://perrotta.dev/2026/07/pi-asynchronous-exit-summaries/</link>
      <pubDate>Mon, 27 Jul 2026 18:18:38 +0200</pubDate><author>serendipity@perrotta.dev (Thiago Perrotta)</author>
      <category>ai</category>
      <category>coding</category>
      <category>dev</category>
      <category>pkm</category>
      <guid>https://perrotta.dev/2026/07/pi-asynchronous-exit-summaries/</guid>
      <description>&lt;p&gt;♠ &lt;strong&gt;Problem statement&lt;/strong&gt;: &lt;code&gt;ctrl+d&lt;/code&gt; in &lt;a href=&#34;https://github.com/earendil-works/pi&#34;&gt;pi&lt;/a&gt;&#xA;took several seconds to quit.&lt;/p&gt;&#xA;&lt;p&gt;The culprit was &lt;a href=&#34;https://www.npmjs.com/package/pi-memory&#34;&gt;&lt;code&gt;pi-memory&lt;/code&gt;&lt;/a&gt;&amp;rsquo;s&#xA;&lt;code&gt;session_shutdown&lt;/code&gt; handler. It sent one last LLM request to summarize the&#xA;session, wrote its answer to the daily log, then ran &lt;code&gt;qmd update&lt;/code&gt;. Pi awaits&#xA;all shutdown handlers, correctly, so quitting waited for everything:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-diff&#34;&gt;pi.on(&amp;#34;session_shutdown&amp;#34;, async (event, ctx) =&amp;gt; {&#xA;  const result = await generateExitSummary(ctx);&#xA;  fs.writeFileSync(filePath, entry, &amp;#34;utf-8&amp;#34;);&#xA;  await runQmdUpdateNow();&#xA;});&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;Making that promise fire-and-forget would not work: Pi exits immediately after&#xA;shutdown, and either the in-flight request keeps Node alive or gets killed&#xA;before it can write anything.&lt;/p&gt;&#xA;&lt;p&gt;Instead, I made the shutdown path write a durable job, synchronously:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-typescript&#34;&gt;const job: ExitSummaryJob = {&#xA;  version: 1,&#xA;  id: randomUUID(),&#xA;  createdAt: new Date().toISOString(),&#xA;  date: todayStr(),&#xA;  reason,&#xA;  sessionId: shortSessionId(ctx.sessionManager.getSessionId()),&#xA;  conversationText: truncated.text,&#xA;  truncated: truncated.truncated,&#xA;  totalChars: truncated.totalChars,&#xA;};&#xA;&#xA;const jobPath = path.join(&#xA;  EXIT_SUMMARY_JOBS_DIR,&#xA;  `${job.createdAt.replace(/[:.]/g, &amp;#34;-&amp;#34;)}-${job.id}.json`,&#xA;);&#xA;fs.writeFileSync(jobPath, `${JSON.stringify(job)}\n`, { encoding: &amp;#34;utf-8&amp;#34;, flag: &amp;#34;wx&amp;#34; });&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;The next interactive session starts an unref&amp;rsquo;ed worker. It atomically renames&#xA;one job to claim it, generates summary with active model, writes it back to&#xA;original day&amp;rsquo;s daily log, and deletes job. Failed jobs return to queue. A&#xA;stale claimed job retries after ten minutes.&lt;/p&gt;&#xA;&lt;p&gt;I kept upstream package intact by replacing it with a local package, effectively&#xA;forking it:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;% pi remove npm:pi-memory&#xA;Removed npm:pi-memory&#xA;% pi install ~/.pi/agent/local/pi-memory-async&#xA;Installed /Users/thiago.perrotta/.pi/agent/local/pi-memory-async&#xA;% pi list&#xA;User packages:&#xA;  local/pi-memory-async&#xA;    /Users/thiago.perrotta/.pi/agent/local/pi-memory-async&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;Jobs live in &lt;code&gt;~/.pi/agent/memory/exit-summary-jobs/&lt;/code&gt;. &lt;code&gt;ctrl+d&lt;/code&gt; only writes one&#xA;JSON file now; summary happens during next session, without blocking exits.&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;Source code&lt;/strong&gt;:&#xA;&lt;a href=&#34;https://github.com/thiagowfx/.dotfiles/tree/1f175b77a960a460b4e5cc677d6d6390e0cc0530/pi/.pi/agent/local/pi-memory-async&#34;&gt;pi-memory-async&lt;/a&gt;.&lt;/p&gt;&#xA;&lt;hr&gt;&#xA;&lt;p&gt;🤖 &lt;em&gt;Drafted with &lt;code&gt;/bloggify&lt;/code&gt;.&lt;/em&gt;&lt;/p&gt;&#xA;&lt;p&gt;— § —&lt;/p&gt;&lt;p&gt;Reply via &lt;a href=&#34;mailto:serendipity@perrotta.dev?subject=Reply to: pi: asynchronous exit summaries&#34;&gt;email&lt;/a&gt;&lt;/p&gt;&lt;p&gt;&lt;a href=&#34;https://perrotta.dev/tags/ai/&#34;&gt;#ai&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/coding/&#34;&gt;#coding&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/dev/&#34;&gt;#dev&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/pkm/&#34;&gt;#pkm&lt;/a&gt;&lt;/p&gt;</description>
    </item>
    <item>
      <title>pi: update pinned npm packages
      </title>
      <link>https://perrotta.dev/2026/07/pi-update-pinned-npm-packages/</link>
      <pubDate>Mon, 27 Jul 2026 14:28:59 +0200</pubDate><author>serendipity@perrotta.dev (Thiago Perrotta)</author>
      <category>ai</category>
      <category>coding</category>
      <category>dev</category>
      <category>security</category>
      <guid>https://perrotta.dev/2026/07/pi-update-pinned-npm-packages/</guid>
      <description>&lt;p&gt;♠ &lt;strong&gt;Problem statement&lt;/strong&gt;: pin &lt;a href=&#34;https://pi.dev/&#34;&gt;pi&lt;/a&gt; npm packages without giving up&#xA;convenient updates.&lt;/p&gt;&#xA;&lt;p&gt;My&#xA;&lt;a href=&#34;https://github.com/thiagowfx/.dotfiles/blob/master/pi/.pi/agent/settings.json&#34;&gt;&lt;code&gt;settings.json&lt;/code&gt;&lt;/a&gt;&#xA;used to leave some package versions unpinned, until now:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-diff&#34;&gt;     &amp;#34;npm:@heyhuynhgiabuu/pi-diff@0.7.6&amp;#34;,&#xA;-    &amp;#34;npm:@tifan/pi-recap&amp;#34;,&#xA;-    &amp;#34;npm:@tmustier/pi-tab-status&amp;#34;,&#xA;-    &amp;#34;npm:@tintinweb/pi-subagents&amp;#34;&#xA;&amp;#43;    &amp;#34;npm:@tifan/pi-recap@0.4.4&amp;#34;,&#xA;&amp;#43;    &amp;#34;npm:@tmustier/pi-tab-status@0.1.4&amp;#34;,&#xA;&amp;#43;    &amp;#34;npm:@tintinweb/pi-subagents@0.14.3&amp;#34;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;Exact versions make package installation reproducible (&amp;amp; safer), but Pi then&#xA;does not automatically pull newer packages.&lt;/p&gt;&#xA;&lt;p&gt;To tackle that, I added a &lt;code&gt;just update-pi&lt;/code&gt; recipe that asks npm for each&#xA;package&amp;rsquo;s current &lt;code&gt;latest&lt;/code&gt; version and writes back an exact pin:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-just&#34;&gt;[doc(&amp;#39;Update pinned Pi npm packages&amp;#39;)]&#xA;[group(&amp;#39;update&amp;#39;)]&#xA;update-pi:&#xA;    #!/usr/bin/env bash&#xA;    set -euo pipefail&#xA;&#xA;    settings=&amp;#34;pi/.pi/agent/settings.json&amp;#34;&#xA;    updates=&amp;#34;{}&amp;#34;&#xA;    while IFS= read -r spec; do&#xA;        if [[ &amp;#34;$spec&amp;#34; =~ ^npm:((@[^/]&amp;#43;/[^@]&amp;#43;)|([^@]&amp;#43;))(@[^@]&amp;#43;)?$ ]]; then&#xA;            package=&amp;#34;${BASH_REMATCH[1]}&amp;#34;&#xA;        else&#xA;            echo &amp;#34;Invalid npm package spec: $spec&amp;#34; &amp;gt;&amp;amp;2&#xA;            exit 1&#xA;        fi&#xA;&#xA;        latest=&amp;#34;$(npm view &amp;#34;$package@latest&amp;#34; version)&amp;#34;&#xA;        pinned=&amp;#34;npm:$package@$latest&amp;#34;&#xA;        updates=&amp;#34;$(jq -cn \&#xA;            --argjson updates &amp;#34;$updates&amp;#34; \&#xA;            --arg spec &amp;#34;$spec&amp;#34; \&#xA;            --arg pinned &amp;#34;$pinned&amp;#34; \&#xA;            &amp;#39;$updates &amp;#43; {($spec): $pinned}&amp;#39;)&amp;#34;&#xA;        echo &amp;#34;✓ $pinned&amp;#34;&#xA;    done &amp;lt; &amp;lt;(jq -r &amp;#39;.packages[] | select(type == &amp;#34;string&amp;#34; and startswith(&amp;#34;npm:&amp;#34;))&amp;#39; &amp;#34;$settings&amp;#34;)&#xA;&#xA;    updated_settings=&amp;#34;$(jq --argjson updates &amp;#34;$updates&amp;#34; \&#xA;        &amp;#39;.packages |= map($updates[.] // .)&amp;#39; &amp;#34;$settings&amp;#34;)&amp;#34;&#xA;    printf &amp;#39;%s\n&amp;#39; &amp;#34;$updated_settings&amp;#34; &amp;gt; &amp;#34;$settings&amp;#34;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;It handles every &lt;code&gt;npm:&lt;/code&gt; entry, including scoped packages:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;% just update-pi&#xA;✓ npm:@heyhuynhgiabuu/pi-diff@0.7.6&#xA;✓ npm:@heyhuynhgiabuu/pi-pretty@0.6.20&#xA;✓ npm:@tifan/pi-recap@0.4.4&#xA;✓ npm:@tmustier/pi-tab-status@0.1.4&#xA;✓ npm:@tintinweb/pi-subagents@0.14.3&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;The aggregate &lt;code&gt;just update&lt;/code&gt; recipe invokes it alongside submodule, prek hook,&#xA;and vendored-file updates.&lt;/p&gt;&#xA;&lt;hr&gt;&#xA;&lt;p&gt;🤖 &lt;em&gt;Drafted with &lt;code&gt;/bloggify&lt;/code&gt;.&lt;/em&gt;&lt;/p&gt;&#xA;&lt;p&gt;— § —&lt;/p&gt;&lt;p&gt;Reply via &lt;a href=&#34;mailto:serendipity@perrotta.dev?subject=Reply to: pi: update pinned npm packages&#34;&gt;email&lt;/a&gt;&lt;/p&gt;&lt;p&gt;&lt;a href=&#34;https://perrotta.dev/tags/ai/&#34;&gt;#ai&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/coding/&#34;&gt;#coding&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/dev/&#34;&gt;#dev&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/security/&#34;&gt;#security&lt;/a&gt;&lt;/p&gt;</description>
    </item>
    <item>
      <title>pi: /btw side-chat
      </title>
      <link>https://perrotta.dev/2026/07/pi-/btw-side-chat/</link>
      <pubDate>Wed, 22 Jul 2026 14:07:54 +0200</pubDate><author>serendipity@perrotta.dev (Thiago Perrotta)</author>
      <category>ai</category>
      <category>coding</category>
      <category>dev</category>
      <guid>https://perrotta.dev/2026/07/pi-/btw-side-chat/</guid>
      <description>&lt;p&gt;♠ &lt;a href=&#34;https://perrotta.dev/2026/07/pi-auto-retry-stalled-requests/&#34;&gt;Previously&lt;/a&gt;.&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;Problem statement&lt;/strong&gt;: mid-task, I want to ask the LLM agent a quick tangential&#xA;question — &amp;ldquo;will this change cause downtime?&amp;rdquo; — without polluting the main&#xA;agent thread.&lt;/p&gt;&#xA;&lt;p&gt;Claude Code has &lt;code&gt;/btw&lt;/code&gt;&lt;sup id=&#34;fnref:1&#34;&gt;&lt;a href=&#34;https://perrotta.dev/2026/07/pi-/btw-side-chat/#fn:1&#34; class=&#34;footnote-ref&#34; role=&#34;doc-noteref&#34;&gt;1&lt;/a&gt;&lt;/sup&gt; for exactly this.&#xA;&lt;a href=&#34;https://github.com/earendil-works/pi&#34;&gt;pi&lt;/a&gt; doesn&amp;rsquo;t, but&#xA;&lt;a href=&#34;https://github.com/patriceckhart/pi-btw&#34;&gt;patriceckhart/pi-btw&lt;/a&gt; does, as an&#xA;extension. It opens a side-chat overlay: the LLM gets to see the full main&#xA;thread context and answers, but nothing is written back to the conversation&#xA;history.&lt;/p&gt;&#xA;&lt;p&gt;The upstream extension imports the pre-fork &lt;code&gt;@mariozechner/*&lt;/code&gt; package names,&#xA;which are now deprecated. As such, I use&#xA;&lt;a href=&#34;https://github.com/earendil-works/pi&#34;&gt;earendil-works/pi&lt;/a&gt;. Rather than trust a&#xA;diff I hadn&amp;rsquo;t read, I reimplemented it against the current API, and&#xA;cross-checked against the bundled &lt;code&gt;examples/extensions/summarize.ts&lt;/code&gt; (the&#xA;canonical example for calling the model from an extension):&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-typescript&#34;&gt;import { complete } from &amp;#34;@earendil-works/pi-ai/compat&amp;#34;;&#xA;import type { ExtensionAPI, SessionEntry } from &amp;#34;@earendil-works/pi-coding-agent&amp;#34;;&#xA;import { convertToLlm, getMarkdownTheme } from &amp;#34;@earendil-works/pi-coding-agent&amp;#34;;&#xA;&#xA;// ...inside the /btw command handler, once at entry:&#xA;const branch = ctx.sessionManager.getBranch();&#xA;const mainMessages = branch&#xA;  .filter((entry): entry is SessionEntry &amp;amp; { type: &amp;#34;message&amp;#34; } =&amp;gt; entry.type === &amp;#34;message&amp;#34;)&#xA;  .map((entry) =&amp;gt; entry.message);&#xA;const mainLlmMessages = convertToLlm(mainMessages);   // frozen snapshot&#xA;const systemPrompt = ctx.getSystemPrompt();&#xA;&#xA;// ...per question, the side-chat calls the model directly:&#xA;const response = await complete(&#xA;  ctx.model!,&#xA;  { systemPrompt, messages: [...mainLlmMessages, ...btwLlmMessages] },&#xA;  { apiKey: auth.apiKey, headers: auth.headers, env: auth.env, signal },&#xA;);&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;The key move is what&amp;rsquo;s &lt;em&gt;missing&lt;/em&gt;: the handler never calls &lt;code&gt;pi.sendMessage()&lt;/code&gt;&#xA;or appends anything. It reads the branch once, talks to the model in an&#xA;&lt;a href=&#34;https://github.com/earendil-works/pi/blob/main/packages/coding-agent/docs/extensions.md#custom-ui&#34;&gt;&lt;code&gt;ctx.ui.custom()&lt;/code&gt;&lt;/a&gt;&#xA;overlay, and returns. The main session is untouched, so the next real prompt&#xA;doesn&amp;rsquo;t pay for any of it.&lt;/p&gt;&#xA;&lt;p&gt;Added to &lt;a href=&#34;https://github.com/thiagowfx/.dotfiles/blob/1f42eb6f6632f946c23d1278e7e3f18da78d5de8/pi/.pi/agent/extensions/btw.ts&#34;&gt;&lt;code&gt;~/.pi/agent/extensions/btw.ts&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;&#xA;&lt;hr&gt;&#xA;&lt;p&gt;🤖 &lt;em&gt;Drafted with &lt;code&gt;/bloggify&lt;/code&gt;.&lt;/em&gt;&lt;/p&gt;&#xA;&lt;div class=&#34;footnotes&#34; role=&#34;doc-endnotes&#34;&gt;&#xA;&lt;hr&gt;&#xA;&lt;ol&gt;&#xA;&lt;li id=&#34;fn:1&#34;&gt;&#xA;&lt;p&gt;I am no longer using &lt;a href=&#34;https://archlinux.org/&#34;&gt;Arch&lt;/a&gt;, btw (sad!)&amp;#160;&lt;a href=&#34;https://perrotta.dev/2026/07/pi-/btw-side-chat/#fnref:1&#34; class=&#34;footnote-backref&#34; role=&#34;doc-backlink&#34;&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;&#xA;&lt;/li&gt;&#xA;&lt;/ol&gt;&#xA;&lt;/div&gt;&#xA;&lt;p&gt;— § —&lt;/p&gt;&lt;p&gt;Reply via &lt;a href=&#34;mailto:serendipity@perrotta.dev?subject=Reply to: pi: /btw side-chat&#34;&gt;email&lt;/a&gt;&lt;/p&gt;&lt;p&gt;&lt;a href=&#34;https://perrotta.dev/tags/ai/&#34;&gt;#ai&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/coding/&#34;&gt;#coding&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/dev/&#34;&gt;#dev&lt;/a&gt;&lt;/p&gt;</description>
    </item>
    <item>
      <title>pi: auto-retry stalled requests
      </title>
      <link>https://perrotta.dev/2026/07/pi-auto-retry-stalled-requests/</link>
      <pubDate>Wed, 22 Jul 2026 13:45:41 +0200</pubDate><author>serendipity@perrotta.dev (Thiago Perrotta)</author>
      <category>ai</category>
      <category>coding</category>
      <category>dev</category>
      <guid>https://perrotta.dev/2026/07/pi-auto-retry-stalled-requests/</guid>
      <description>&lt;p&gt;♠ &lt;a href=&#34;https://perrotta.dev/2026/07/pi-cycle-models-backward-with-ctrl-n/&#34;&gt;Previously&lt;/a&gt;.&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;Problem statement&lt;/strong&gt;: sometimes a prompt in &lt;a href=&#34;https://github.com/earendil-works/pi&#34;&gt;pi&lt;/a&gt;&#xA;just hangs, as if the network dropped. It never recovers on its own — I have to&#xA;hit &lt;code&gt;Esc&lt;/code&gt; and re-prompt.&lt;/p&gt;&#xA;&lt;p&gt;It&amp;rsquo;s 2026, this symptom should be self-recoverable.&lt;/p&gt;&#xA;&lt;p&gt;Pi already has agent-level retry on transient errors (&lt;code&gt;retry.enabled&lt;/code&gt; defaults&#xA;to &lt;code&gt;true&lt;/code&gt;). The catch is &lt;em&gt;when&lt;/em&gt; it fires: a stalled connection isn&amp;rsquo;t an error&#xA;until the HTTP idle timeout trips, and that defaults to five minutes:&lt;/p&gt;&#xA;&lt;blockquote&gt;&#xA;&lt;p&gt;&lt;code&gt;httpIdleTimeoutMs&lt;/code&gt; | number | &lt;code&gt;300000&lt;/code&gt; | HTTP header/body idle timeout in&#xA;milliseconds [&amp;hellip;] Set to &lt;code&gt;0&lt;/code&gt; to disable.&lt;/p&gt;&#xA;&lt;/blockquote&gt;&#xA;&lt;p&gt;So pi would have &lt;em&gt;eventually&lt;/em&gt; recovered. I was just quitting long before the&#xA;five-minute clock ran out. Who has patience to wait for &lt;strong&gt;5&lt;/strong&gt; minutes?!&lt;/p&gt;&#xA;&lt;p&gt;:∴ Lower the idle timeout to O(seconds) instead:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-json&#34;&gt;{&#xA;  &amp;#34;httpIdleTimeoutMs&amp;#34;: 30000,&#xA;  &amp;#34;retry&amp;#34;: {&#xA;    &amp;#34;enabled&amp;#34;: true,&#xA;    &amp;#34;maxRetries&amp;#34;: 5,&#xA;    &amp;#34;baseDelayMs&amp;#34;: 2000&#xA;  }&#xA;}&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;Dropped into &lt;a href=&#34;https://github.com/thiagowfx/.dotfiles/commit/aae17ba8d7546901820d7474136a628b16d5feee&#34;&gt;&lt;code&gt;~/.pi/agent/settings.json&lt;/code&gt;&lt;/a&gt;, documented in&#xA;&lt;a href=&#34;https://github.com/earendil-works/pi/blob/main/packages/coding-agent/docs/settings.md&#34;&gt;&lt;code&gt;docs/settings.md&lt;/code&gt;&lt;/a&gt;.&#xA;Now a stall errors out after 30s and &lt;code&gt;pi&lt;/code&gt; retries automatically with exponential&#xA;backoff (2s, 4s, 8s…), up to five attempts. There&amp;rsquo;s no need for &lt;code&gt;Esc&lt;/code&gt; nor&#xA;re-prompting.&lt;/p&gt;&#xA;&lt;p&gt;The idle timer resets on every byte received, so long thinking or a slow stream&#xA;won&amp;rsquo;t trip it — only a genuinely dead connection will. Restart &lt;code&gt;pi&lt;/code&gt; for the&#xA;change to take effect.&lt;/p&gt;&#xA;&lt;hr&gt;&#xA;&lt;p&gt;🤖 &lt;em&gt;Drafted with &lt;code&gt;/bloggify&lt;/code&gt;.&lt;/em&gt;&lt;/p&gt;&#xA;&lt;p&gt;— § —&lt;/p&gt;&lt;p&gt;Reply via &lt;a href=&#34;mailto:serendipity@perrotta.dev?subject=Reply to: pi: auto-retry stalled requests&#34;&gt;email&lt;/a&gt;&lt;/p&gt;&lt;p&gt;&lt;a href=&#34;https://perrotta.dev/tags/ai/&#34;&gt;#ai&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/coding/&#34;&gt;#coding&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/dev/&#34;&gt;#dev&lt;/a&gt;&lt;/p&gt;</description>
    </item>
    <item>
      <title>pi: cycle models backward with ctrl&#43;n
      </title>
      <link>https://perrotta.dev/2026/07/pi-cycle-models-backward-with-ctrl-n/</link>
      <pubDate>Wed, 22 Jul 2026 13:23:53 +0200</pubDate><author>serendipity@perrotta.dev (Thiago Perrotta)</author>
      <category>ai</category>
      <category>coding</category>
      <category>dev</category>
      <guid>https://perrotta.dev/2026/07/pi-cycle-models-backward-with-ctrl-n/</guid>
      <description>&lt;p&gt;♠ &lt;a href=&#34;https://perrotta.dev/2026/07/pi-caveman-mode/&#34;&gt;Previously&lt;/a&gt;.&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;Problem statement&lt;/strong&gt;: &lt;a href=&#34;https://github.com/earendil-works/pi&#34;&gt;pi&lt;/a&gt; binds &lt;code&gt;ctrl+p&lt;/code&gt;&#xA;to &lt;code&gt;app.model.cycleForward&lt;/code&gt;&lt;sup id=&#34;fnref:1&#34;&gt;&lt;a href=&#34;https://perrotta.dev/2026/07/pi-cycle-models-backward-with-ctrl-n/#fn:1&#34; class=&#34;footnote-ref&#34; role=&#34;doc-noteref&#34;&gt;1&lt;/a&gt;&lt;/sup&gt; by default, but has no default binding to cycle&#xA;backward other than &lt;code&gt;shift+ctrl+p&lt;/code&gt;, which is arguably difficult to remember.&lt;/p&gt;&#xA;&lt;p&gt;Every keybinding in pi is namespaced and overridable via&#xA;&lt;a href=&#34;https://github.com/thiagowfx/.dotfiles/blob/1f42eb6f6632f946c23d1278e7e3f18da78d5de8/pi/.pi/agent/keybindings.json&#34;&gt;&lt;code&gt;~/.pi/agent/keybindings.json&lt;/code&gt;&lt;/a&gt;, documented in&#xA;&lt;a href=&#34;https://github.com/earendil-works/pi/blob/main/packages/coding-agent/docs/keybindings.md&#34;&gt;&lt;code&gt;docs/keybindings.md&lt;/code&gt;&lt;/a&gt;:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-json&#34;&gt;{&#xA;  &amp;#34;app.model.cycleBackward&amp;#34;: [&#xA;    &amp;#34;shift&amp;#43;ctrl&amp;#43;p&amp;#34;,&#xA;    &amp;#34;ctrl&amp;#43;n&amp;#34;&#xA;  ]&#xA;}&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;&lt;code&gt;ctrl+n&lt;/code&gt; is also the default for &lt;code&gt;app.session.toggleNamedFilter&lt;/code&gt;, but that&#xA;only fires inside the session picker, not while a model selector or the main&#xA;editor has focus.&lt;/p&gt;&#xA;&lt;p&gt;Run &lt;code&gt;/reload&lt;/code&gt; in pi to pick up the keybinding addition without restarting the&#xA;session.&lt;/p&gt;&#xA;&lt;p&gt;∴ &lt;code&gt;ctrl+p&lt;/code&gt;/&lt;code&gt;ctrl+n&lt;/code&gt; now cycle models forward/backward, matching the readline&#xA;(emacs!) history bindings I already have muscle memory for.&lt;/p&gt;&#xA;&lt;hr&gt;&#xA;&lt;p&gt;🤖 &lt;em&gt;Drafted with &lt;code&gt;/bloggify&lt;/code&gt;.&lt;/em&gt;&lt;/p&gt;&#xA;&lt;div class=&#34;footnotes&#34; role=&#34;doc-endnotes&#34;&gt;&#xA;&lt;hr&gt;&#xA;&lt;ol&gt;&#xA;&lt;li id=&#34;fn:1&#34;&gt;&#xA;&lt;p&gt;It cycles through models selected in &lt;code&gt;/scoped-models&lt;/code&gt;.&amp;#160;&lt;a href=&#34;https://perrotta.dev/2026/07/pi-cycle-models-backward-with-ctrl-n/#fnref:1&#34; class=&#34;footnote-backref&#34; role=&#34;doc-backlink&#34;&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;&#xA;&lt;/li&gt;&#xA;&lt;/ol&gt;&#xA;&lt;/div&gt;&#xA;&lt;p&gt;— § —&lt;/p&gt;&lt;p&gt;Reply via &lt;a href=&#34;mailto:serendipity@perrotta.dev?subject=Reply to: pi: cycle models backward with ctrl+n&#34;&gt;email&lt;/a&gt;&lt;/p&gt;&lt;p&gt;&lt;a href=&#34;https://perrotta.dev/tags/ai/&#34;&gt;#ai&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/coding/&#34;&gt;#coding&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/dev/&#34;&gt;#dev&lt;/a&gt;&lt;/p&gt;</description>
    </item>
    <item>
      <title>pi: caveman mode
      </title>
      <link>https://perrotta.dev/2026/07/pi-caveman-mode/</link>
      <pubDate>Wed, 22 Jul 2026 12:14:47 +0200</pubDate><author>serendipity@perrotta.dev (Thiago Perrotta)</author>
      <category>ai</category>
      <category>coding</category>
      <category>dev</category>
      <guid>https://perrotta.dev/2026/07/pi-caveman-mode/</guid>
      <description>&lt;p&gt;♠ &lt;a href=&#34;https://perrotta.dev/2025/12/pi/&#34;&gt;Previously&lt;/a&gt;,&#xA;&lt;a href=&#34;https://perrotta.dev/2026/06/caveman-talk-like-caveman-save-tokens/&#34;&gt;previously&lt;/a&gt;.&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;Problem statement&lt;/strong&gt;: adopt &lt;a href=&#34;https://github.com/JuliusBrussee/caveman&#34;&gt;caveman&lt;/a&gt;&#xA;in &lt;a href=&#34;https://github.com/earendil-works/pi&#34;&gt;pi&lt;/a&gt;.&lt;/p&gt;&#xA;&lt;p&gt;Pi isn&amp;rsquo;t one of the 30+ agents caveman auto-detects. The install matrix&#xA;(&lt;code&gt;INSTALL.md&lt;/code&gt;) lists agent detection rules one by one — &lt;code&gt;claude&lt;/code&gt;, &lt;code&gt;cursor&lt;/code&gt;,&#xA;&lt;code&gt;windsurf&lt;/code&gt;, &lt;code&gt;opencode&lt;/code&gt;, &lt;code&gt;openclaw&lt;/code&gt;, etc — and pi isn&amp;rsquo;t in it. Running the&#xA;one-liner installer here would just skip everything silently. Booo!&lt;/p&gt;&#xA;&lt;p&gt;Rather than run someone else&amp;rsquo;s &lt;code&gt;curl | bash&lt;/code&gt; against an agent it wasn&amp;rsquo;t built&#xA;for, &amp;ldquo;&amp;ldquo;&amp;ldquo;I&amp;rdquo;&amp;rdquo;&amp;rdquo; reimplemented the terse-speak part natively, since &lt;code&gt;pi&lt;/code&gt; already ships the&#xA;two primitives needed: &lt;a href=&#34;https://agentskills.io/specification&#34;&gt;skills&lt;/a&gt; and&#xA;&lt;a href=&#34;https://github.com/earendil-works/pi/blob/main/packages/coding-agent/docs/extensions.md&#34;&gt;extensions&lt;/a&gt;.&lt;/p&gt;&#xA;&lt;p&gt;A&#xA;&lt;a href=&#34;https://github.com/thiagowfx/.dotfiles/blob/7c495489141fd6036e4be70de9a065c53f7a8394/pi/.pi/agent/extensions/caveman.ts&#34;&gt;skill&lt;/a&gt;&#xA;for &lt;code&gt;/skill:caveman&lt;/code&gt; documentation, and an extension that rewrites the system&#xA;prompt on every turn via &lt;code&gt;pi.on(&amp;quot;before_agent_start&amp;quot;, (event) =&amp;gt; ...)&lt;/code&gt;:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-typescript&#34;&gt;const INSTRUCTIONS = `Caveman mode: on. Respond terse. Drop articles (a/an/the), filler (just/really/basically), pleasantries, hedging. Fragments OK. Short synonyms. Technical terms exact. Pattern: [thing] [action] [reason]. [next step]. Example — not: &amp;#34;Sure! I&amp;#39;d be happy to help you with that.&amp;#34; yes: &amp;#34;Bug in auth middleware. Fix:&amp;#34;&#xA;Boundaries, always: code blocks, commit messages, and PR descriptions stay normal full verbosity, never compressed. Drop caveman style for security warnings, irreversible/destructive actions, or when the user seems confused — resume caveman after.`;&#xA;&#xA;export default function (pi: ExtensionAPI) {&#xA;  let on = true; // auto-on by default, every session&#xA;&#xA;  const applyStatus = (ctx: { ui: { setStatus: (k: string, v?: string) =&amp;gt; void } }) =&amp;gt; {&#xA;    ctx.ui.setStatus(&amp;#34;caveman&amp;#34;, on ? &amp;#34;[caveman]&amp;#34; : undefined);&#xA;  };&#xA;&#xA;  pi.on(&amp;#34;session_start&amp;#34;, async (_event, ctx) =&amp;gt; applyStatus(ctx));&#xA;&#xA;  pi.on(&amp;#34;before_agent_start&amp;#34;, (event) =&amp;gt; {&#xA;    if (!on) return;&#xA;    return { systemPrompt: `${event.systemPrompt}\n\n${INSTRUCTIONS}` };&#xA;  });&#xA;&#xA;  pi.registerCommand(&amp;#34;caveman&amp;#34;, {&#xA;    description: &amp;#34;Toggle caveman terse-response mode: on|off (default: on)&amp;#34;,&#xA;    handler: async (args, ctx) =&amp;gt; {&#xA;      const arg = args.trim().toLowerCase();&#xA;      if (arg === &amp;#34;off&amp;#34; || arg === &amp;#34;stop&amp;#34; || arg === &amp;#34;normal&amp;#34;) on = false;&#xA;      else if (arg === &amp;#34;on&amp;#34; || arg === &amp;#34;&amp;#34;) on = true;&#xA;      else return ctx.ui.notify(`Unknown arg &amp;#34;${arg}&amp;#34;. Use: on | off`, &amp;#34;error&amp;#34;);&#xA;      applyStatus(ctx);&#xA;      ctx.ui.notify(on ? &amp;#34;Caveman mode on.&amp;#34; : &amp;#34;Caveman mode off.&amp;#34;, &amp;#34;info&amp;#34;);&#xA;    },&#xA;  });&#xA;}&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;&lt;a href=&#34;https://github.com/earendil-works/pi/blob/main/packages/coding-agent/docs/extensions.md#before_agent_start&#34;&gt;&lt;code&gt;before_agent_start&lt;/code&gt;&lt;/a&gt;&#xA;fires on &lt;strong&gt;every prompt&lt;/strong&gt;, before the agent loop starts, so&#xA;the instructions are re-appended each turn instead of relying on one message&#xA;that could get pruned from context. Dropped in&#xA;&lt;code&gt;~/.pi/agent/extensions/caveman.ts&lt;/code&gt;, auto-discovered, no &lt;code&gt;/reload&lt;/code&gt; needed after&#xA;the first load.&lt;/p&gt;&#xA;&lt;p&gt;Unlike the Claude Code plugin, I skipped &lt;code&gt;lite&lt;/code&gt;/&lt;code&gt;full&lt;/code&gt;/&lt;code&gt;ultra&lt;/code&gt;/&lt;code&gt;wenyan&lt;/code&gt; — I&#xA;called that bloat back in June, and I&amp;rsquo;m not adding it to my own version&#xA;either. On or off is enough.&lt;/p&gt;&#xA;&lt;p&gt;The result, &lt;em&gt;unprompted&lt;/em&gt;:&lt;/p&gt;&#xA;&lt;blockquote&gt;&#xA;&lt;p&gt;&lt;strong&gt;user&lt;/strong&gt;: caveman on or off now?&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;assistant&lt;/strong&gt;: Caveman: on.&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;user&lt;/strong&gt;: how to turn it off?&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;assistant&lt;/strong&gt;: &lt;code&gt;/caveman off&lt;/code&gt;&lt;/p&gt;&#xA;&lt;/blockquote&gt;&#xA;&lt;p&gt;I also skipped the parts that don&amp;rsquo;t have a pi equivalent: the statusline&#xA;savings counter, &lt;code&gt;/caveman-stats&lt;/code&gt;, the &lt;code&gt;caveman-shrink&lt;/code&gt; MCP proxy that rewrites&#xA;tool descriptions (a proxy that rewrites what an agent sees is exactly the&#xA;kind of thing worth reading before installing). The 40-line extension above&#xA;covers what I actually wanted.&lt;/p&gt;&#xA;&lt;p&gt;One rule baked into both the skill and the extension: code, commits, and PRs&#xA;stay full verbosity, never compressed.&lt;/p&gt;&#xA;&lt;p&gt;This experience was an eye-opener for me to realize how bloated the upstream&#xA;caveman project became.&lt;/p&gt;&#xA;&lt;hr&gt;&#xA;&lt;p&gt;🤖 &lt;em&gt;Drafted with &lt;code&gt;/bloggify&lt;/code&gt;.&lt;/em&gt;&lt;/p&gt;&#xA;&lt;p&gt;— § —&lt;/p&gt;&lt;p&gt;Reply via &lt;a href=&#34;mailto:serendipity@perrotta.dev?subject=Reply to: pi: caveman mode&#34;&gt;email&lt;/a&gt;&lt;/p&gt;&lt;p&gt;&lt;a href=&#34;https://perrotta.dev/tags/ai/&#34;&gt;#ai&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/coding/&#34;&gt;#coding&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/dev/&#34;&gt;#dev&lt;/a&gt;&lt;/p&gt;</description>
    </item>
    <item>
      <title>myrepos: a wip action to find unfinished work
      </title>
      <link>https://perrotta.dev/2026/07/myrepos-a-wip-action-to-find-unfinished-work/</link>
      <pubDate>Fri, 17 Jul 2026 12:23:10 +0200</pubDate><author>serendipity@perrotta.dev (Thiago Perrotta)</author>
      <category>coding</category>
      <category>dev</category>
      <category>git</category>
      <guid>https://perrotta.dev/2026/07/myrepos-a-wip-action-to-find-unfinished-work/</guid>
      <description>&lt;p&gt;♠ &lt;a href=&#34;https://perrotta.dev/2026/03/mr-update/&#34;&gt;Previously&lt;/a&gt;.&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;Problem statement&lt;/strong&gt;: I manage a few dozen repos via&#xA;&lt;a href=&#34;https://myrepos.branchable.com/&#34;&gt;myrepos&lt;/a&gt; (&lt;code&gt;mr&lt;/code&gt;), and I wanted a single command&#xA;to show which ones have work in progress (&amp;ldquo;WIP&amp;rdquo;) — uncommitted changes, dirty&#xA;staging area, stray branches and alike.&lt;/p&gt;&#xA;&lt;p&gt;I want essentially a filtered version of &lt;code&gt;mr ls&lt;/code&gt;.&lt;/p&gt;&#xA;&lt;p&gt;So I defined my own command. A repo is &lt;em&gt;not&lt;/em&gt; WIP when all three hold:&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;the default branch is checked out;&lt;/li&gt;&#xA;&lt;li&gt;no other local branches exist;&lt;/li&gt;&#xA;&lt;li&gt;and the working tree is fully clean.&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;Anything else gets flagged with a reason.&lt;/p&gt;&#xA;&lt;p&gt;&lt;a href=&#34;https://github.com/thiagowfx/.dotfiles/blob/28376040a292a9898cca59f8ce27de81482d1a26/mr/.mrconfig.defaults#L14-L23&#34;&gt;The definition&lt;/a&gt;:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-ini&#34;&gt;# List repos with work in progress: not on default branch, extra local branches, or dirty tree.&#xA;# Run as `mr -m wip`: the -m (minimal) flag drops mr&amp;#39;s per-repo &amp;#34;mr wip:&amp;#34; header and blank&#xA;# lines, leaving only the flagged repos. This cannot be defaulted per-action (mr only honors&#xA;# -m/-q as CLI flags, not via config).&#xA;wip =&#xA; reasons=&amp;#34;&amp;#34;&#xA; cur=$(git symbolic-ref --quiet --short HEAD 2&amp;gt;/dev/null || echo &amp;#34;(detached)&amp;#34;)&#xA; def=$(git symbolic-ref --quiet --short refs/remotes/origin/HEAD 2&amp;gt;/dev/null | sed &amp;#39;s|^origin/||&amp;#39;)&#xA; if [ -z &amp;#34;$def&amp;#34; ]; then if git show-ref --verify --quiet refs/heads/main; then def=main; elif git show-ref --verify --quiet refs/heads/master; then def=master; fi; fi&#xA; if [ -n &amp;#34;$def&amp;#34; ] &amp;amp;&amp;amp; [ &amp;#34;$cur&amp;#34; != &amp;#34;$def&amp;#34; ]; then reasons=&amp;#34;$reasons branch:$cur&amp;#34;; fi&#xA; n=$(git for-each-ref --format=&amp;#39;%(refname)&amp;#39; refs/heads | wc -l | tr -d &amp;#39; &amp;#39;)&#xA; if [ &amp;#34;$n&amp;#34; -gt 1 ]; then reasons=&amp;#34;$reasons &amp;#43;$((n-1))_branches&amp;#34;; fi&#xA; if [ -n &amp;#34;$(git status --porcelain)&amp;#34; ]; then reasons=&amp;#34;$reasons dirty&amp;#34;; fi&#xA; if [ -n &amp;#34;$reasons&amp;#34; ]; then printf &amp;#39;%s [%s]\n&amp;#39; &amp;#34;$MR_REPO&amp;#34; &amp;#34;${reasons# }&amp;#34;; fi&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;Naturally, the snippet above is art from the LLM, but it accomplishes exactly&#xA;what I want:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;thiago.perrotta ~&#xA;% mr -m wip&#xA;mr wip: /Users/thiago.perrotta/Corp/gitops&#xA;/Users/thiago.perrotta/Corp/gitops [branch:thiagowfx/acme &amp;#43;1_branches]&#xA;&#xA;mr wip: /Users/thiago.perrotta/Corp/terraform&#xA;/Users/thiago.perrotta/Corp/terraform [&amp;#43;1_branches]&#xA;&#xA;mr wip: /Users/thiago.perrotta/workspace/perrotta.dev&#xA;/Users/thiago.perrotta/workspace/perrotta.dev [&amp;#43;3_branches dirty]&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;Clean repos stay silent. I get to see all repos with unfinished work, and I can&#xA;see exactly why each one is dirty.&lt;/p&gt;&#xA;&lt;p&gt;&lt;code&gt;mr wip&lt;/code&gt; works too but it&amp;rsquo;s noisier. I need to add &lt;code&gt;mr -m wip&lt;/code&gt; to my muscle&#xA;memory&lt;sup id=&#34;fnref:1&#34;&gt;&lt;a href=&#34;https://perrotta.dev/2026/07/myrepos-a-wip-action-to-find-unfinished-work/#fn:1&#34; class=&#34;footnote-ref&#34; role=&#34;doc-noteref&#34;&gt;1&lt;/a&gt;&lt;/sup&gt;. Without the &lt;code&gt;-m&lt;/code&gt; flag, &lt;code&gt;mr&lt;/code&gt; prepends a &lt;code&gt;mr wip:&lt;/code&gt; header and a blank&#xA;line for &lt;em&gt;every&lt;/em&gt; repo, WIP or not.&lt;/p&gt;&#xA;&lt;p&gt;&lt;code&gt;-m&lt;/code&gt; can&amp;rsquo;t be baked into the action definition — &lt;code&gt;mr&lt;/code&gt; only honors it as a CLI&#xA;flag — so the comment above the action documents it.&lt;/p&gt;&#xA;&lt;p&gt;Previously I&amp;rsquo;d run &lt;code&gt;mr xl&lt;/code&gt; (an alias for &lt;code&gt;git branch -vv&lt;/code&gt; across every repo) and&#xA;eyeball the result — but that&amp;rsquo;s dozens of lines of branch tips for tens of&#xA;repos, and I still have to scan for the ones that are actually behind or dirty.&#xA;&lt;code&gt;wip&lt;/code&gt; does the scanning: it only prints the repos that need attention, with the&#xA;reason attached. Much more ergonomic than visually diffing a wall of green.&lt;/p&gt;&#xA;&lt;p&gt;Runs read-only, so it&amp;rsquo;s safe to run twice.&lt;/p&gt;&#xA;&lt;hr&gt;&#xA;&lt;p&gt;🤖 &lt;em&gt;Drafted with &lt;code&gt;/bloggify&lt;/code&gt;.&lt;/em&gt;&lt;/p&gt;&#xA;&lt;div class=&#34;footnotes&#34; role=&#34;doc-endnotes&#34;&gt;&#xA;&lt;hr&gt;&#xA;&lt;ol&gt;&#xA;&lt;li id=&#34;fn:1&#34;&gt;&#xA;&lt;p&gt;Should I rely on this workflow more often, I could leverage &lt;a href=&#34;https://perrotta.dev/2025/05/espanso-hello-world/&#34;&gt;Espanso&lt;/a&gt; or a&#xA;shell alias/function to easen its recall.&amp;#160;&lt;a href=&#34;https://perrotta.dev/2026/07/myrepos-a-wip-action-to-find-unfinished-work/#fnref:1&#34; class=&#34;footnote-backref&#34; role=&#34;doc-backlink&#34;&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;&#xA;&lt;/li&gt;&#xA;&lt;/ol&gt;&#xA;&lt;/div&gt;&#xA;&lt;p&gt;— § —&lt;/p&gt;&lt;p&gt;Reply via &lt;a href=&#34;mailto:serendipity@perrotta.dev?subject=Reply to: myrepos: a wip action to find unfinished work&#34;&gt;email&lt;/a&gt;&lt;/p&gt;&lt;p&gt;&lt;a href=&#34;https://perrotta.dev/tags/coding/&#34;&gt;#coding&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/dev/&#34;&gt;#dev&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/git/&#34;&gt;#git&lt;/a&gt;&lt;/p&gt;</description>
    </item>
    <item>
      <title>claude code: replace a hand-rolled loop with /goal
      </title>
      <link>https://perrotta.dev/2026/07/claude-code-replace-a-hand-rolled-loop-with-/goal/</link>
      <pubDate>Thu, 16 Jul 2026 13:44:25 +0200</pubDate><author>serendipity@perrotta.dev (Thiago Perrotta)</author>
      <category>ai</category>
      <category>coding</category>
      <category>dev</category>
      <guid>https://perrotta.dev/2026/07/claude-code-replace-a-hand-rolled-loop-with-/goal/</guid>
      <description>&lt;p&gt;♠ &lt;a href=&#34;https://perrotta.dev/2026/06/claude-code-ship-your-skills-as-a-plugin-marketplace/&#34;&gt;Previously&lt;/a&gt;.&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;Problem statement&lt;/strong&gt;: one of my skills, &lt;code&gt;/pr-pass&lt;/code&gt;, was a hand-rolled loop —&#xA;push, poll CI, fix a failure, re-push, repeat, bail after five iterations.&lt;/p&gt;&#xA;&lt;p&gt;It works quite well, and it&amp;rsquo;s one of my most used skills.&lt;/p&gt;&#xA;&lt;p&gt;That said, recently Claude Code shipped&#xA;&lt;a href=&#34;https://code.claude.com/docs/en/goal&#34;&gt;&lt;code&gt;/goal&lt;/code&gt;&lt;/a&gt; (in v2.1.139), which is exactly&#xA;that loop, packaging it as a primitive. So the skill was reimplementing something&#xA;the harness &lt;em&gt;now&lt;/em&gt; does natively.&lt;/p&gt;&#xA;&lt;p&gt;In fact,&#xA;&lt;a href=&#34;https://newsletter.pragmaticengineer.com/p/what-is-loop-engineering&#34;&gt;most&lt;/a&gt;&#xA;harnesses ship &lt;code&gt;/goal&lt;/code&gt; natively these days.&lt;/p&gt;&#xA;&lt;p&gt;We shall&#xA;&lt;a href=&#34;https://github.com/thiagowfx/skills/commit/322d4523f10c8ab1fa3cf8eeb95d776c6debf17c&#34;&gt;refactor&lt;/a&gt;!&lt;/p&gt;&#xA;&lt;p&gt;&lt;code&gt;/goal&lt;/code&gt; sets a completion condition. After each turn a fast model checks whether&#xA;it holds; if not, Claude starts another turn on its own. It auto-clears once the&#xA;condition is met. From the docs:&lt;/p&gt;&#xA;&lt;blockquote&gt;&#xA;&lt;p&gt;The &lt;code&gt;/goal&lt;/code&gt; command sets a completion condition and Claude keeps working&#xA;toward it without you prompting each step. After each turn, a small fast model&#xA;checks whether the condition holds. If not, Claude starts another turn instead&#xA;of returning control to you.&lt;/p&gt;&#xA;&lt;/blockquote&gt;&#xA;&lt;p&gt;That deletes three things the old skill hand-built: the &lt;code&gt;Monitor&lt;/code&gt; poll waiting&#xA;on CI state, the &amp;ldquo;go back to Step 1&amp;rdquo; loop, and the max-5-iterations guard. The&#xA;guard becomes a clause in the condition itself:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-plaintext&#34;&gt;/goal every check on this PR has passed (gh pr checks shows no FAILURE&#xA;and no PENDING), or stop after 5 turns&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;One catch worth knowing before reaching for &lt;code&gt;/goal&lt;/code&gt;: the evaluator (tester?) is&#xA;a fresh model that &lt;strong&gt;only reads the transcript&lt;/strong&gt;. It does not run &lt;code&gt;gh&lt;/code&gt; or read&#xA;files.&lt;/p&gt;&#xA;&lt;blockquote&gt;&#xA;&lt;p&gt;It does not call tools, so it can only judge what Claude has already surfaced&#xA;in the conversation.&lt;/p&gt;&#xA;&lt;/blockquote&gt;&#xA;&lt;p&gt;So the condition has to be something Claude&amp;rsquo;s own output demonstrates. The skill&#xA;now says to paste the &lt;code&gt;gh pr checks&lt;/code&gt; result into the reply every turn, so the&#xA;evaluator has a CI state to judge against. Otherwise the goal would have never&#xA;resolved thus it would have looped forever.&lt;/p&gt;&#xA;&lt;p&gt;Another gotcha: &lt;code&gt;/goal&lt;/code&gt; doesn&amp;rsquo;t touch permissions. Each turn&amp;rsquo;s &lt;code&gt;git push&lt;/code&gt;&#xA;still prompts unless paired it with &lt;a href=&#34;https://code.claude.com/docs/en/auto-mode-config&#34;&gt;auto&#xA;mode&lt;/a&gt;.&lt;/p&gt;&#xA;&lt;p&gt;The lesson generalizes past this one skill: whenever the harness grows a&#xA;primitive, the custom code that predates it becomes technical debt. &lt;code&gt;/loop&lt;/code&gt;,&#xA;Stop hooks, and &lt;code&gt;/goal&lt;/code&gt; now cover most of the &amp;ldquo;keep going until X&amp;rdquo; constructs I&#xA;used previously.&lt;/p&gt;&#xA;&lt;hr&gt;&#xA;&lt;p&gt;🤖 &lt;em&gt;Drafted with &lt;code&gt;/bloggify&lt;/code&gt;.&lt;/em&gt;&lt;/p&gt;&#xA;&lt;p&gt;— § —&lt;/p&gt;&lt;p&gt;Reply via &lt;a href=&#34;mailto:serendipity@perrotta.dev?subject=Reply to: claude code: replace a hand-rolled loop with /goal&#34;&gt;email&lt;/a&gt;&lt;/p&gt;&lt;p&gt;&lt;a href=&#34;https://perrotta.dev/tags/ai/&#34;&gt;#ai&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/coding/&#34;&gt;#coding&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/dev/&#34;&gt;#dev&lt;/a&gt;&lt;/p&gt;</description>
    </item>
    <item>
      <title>★ claude code: ship your skills as a plugin marketplace
      </title>
      <link>https://perrotta.dev/2026/06/claude-code-ship-your-skills-as-a-plugin-marketplace/</link>
      <pubDate>Thu, 25 Jun 2026 20:40:02 +0200</pubDate><author>serendipity@perrotta.dev (Thiago Perrotta)</author>
      <category>ai</category>
      <category>bestof</category>
      <category>coding</category>
      <category>dev</category>
      <guid>https://perrotta.dev/2026/06/claude-code-ship-your-skills-as-a-plugin-marketplace/</guid>
      <description>&lt;p&gt;♠ &lt;strong&gt;Problem statement&lt;/strong&gt;: my Claude Code&#xA;&lt;a href=&#34;https://code.claude.com/docs/en/skills&#34;&gt;skills&lt;/a&gt; lived in my&#xA;&lt;a href=&#34;https://github.com/thiagowfx/.dotfiles&#34;&gt;.dotfiles&lt;/a&gt;, usable only on the machine&#xA;that cloned them. I wanted them to be installable anywhere with one command, so&#xA;that (for example) my teammates could easily adopt them.&lt;/p&gt;&#xA;&lt;p&gt;Claude Code reads skills from a marketplace, which is essentially a git repo&#xA;with a manifest. So I turned &lt;code&gt;github.com/thiagowfx/skills&lt;/code&gt; into one. The layout:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-plaintext&#34;&gt;.claude-plugin/marketplace.json    # marketplace manifest&#xA;plugins/thiagowfx/                 # one plugin&#xA;  .claude-plugin/plugin.json       # plugin manifest&#xA;  skills/&amp;lt;name&amp;gt;/SKILL.md           # one dir per skill (auto-discovered)&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;The marketplace lists plugins; a plugin bundles skills. I have one plugin&#xA;holding all ten. I do not find it necessary to have multiple plug-ins at this&#xA;point. &lt;code&gt;marketplace.json&lt;/code&gt; points at it with a relative &lt;code&gt;source&lt;/code&gt;:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-json&#34;&gt;{&#xA;  &amp;#34;name&amp;#34;: &amp;#34;thiagowfx&amp;#34;,&#xA;  &amp;#34;owner&amp;#34;: { &amp;#34;name&amp;#34;: &amp;#34;Thiago Perrotta&amp;#34;, &amp;#34;url&amp;#34;: &amp;#34;https://github.com/thiagowfx&amp;#34; },&#xA;  &amp;#34;plugins&amp;#34;: [&#xA;    {&#xA;      &amp;#34;name&amp;#34;: &amp;#34;thiagowfx&amp;#34;,&#xA;      &amp;#34;source&amp;#34;: &amp;#34;./plugins/thiagowfx&amp;#34;,&#xA;      &amp;#34;description&amp;#34;: &amp;#34;Thiago&amp;#39;s personal Gen-AI / Claude Code skills.&amp;#34;&#xA;    }&#xA;  ]&#xA;}&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;The plugin manifest is just metadata — there&amp;rsquo;s no need to enumerate skills.&#xA;Anything under &lt;code&gt;skills/&amp;lt;name&amp;gt;/SKILL.md&lt;/code&gt; is discovered automatically:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-json&#34;&gt;{&#xA;  &amp;#34;name&amp;#34;: &amp;#34;thiagowfx&amp;#34;,&#xA;  &amp;#34;version&amp;#34;: &amp;#34;0.2.2&amp;#34;,&#xA;  &amp;#34;license&amp;#34;: &amp;#34;MIT&amp;#34;&#xA;}&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;The docs are explicit about why I keep that &lt;code&gt;version&lt;/code&gt;:&lt;/p&gt;&#xA;&lt;blockquote&gt;&#xA;&lt;p&gt;Setting &lt;code&gt;version&lt;/code&gt; means users only receive updates when you change this field, so&#xA;bump it on every release. If you omit &lt;code&gt;version&lt;/code&gt; and host this marketplace in git,&#xA;every commit automatically counts as a new version.&lt;/p&gt;&#xA;&lt;/blockquote&gt;&#xA;&lt;p&gt;I&amp;rsquo;ll go with version pinning for now.&lt;/p&gt;&#xA;&lt;p&gt;Install from any machine:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;/plugin marketplace add thiagowfx/skills&#xA;/plugin install thiagowfx@thiagowfx&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;&lt;code&gt;/plugin marketplace update thiagowfx&lt;/code&gt; pulls future changes.&lt;/p&gt;&#xA;&lt;p&gt;Outside Claude:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;claude plugins update thiagowfx@thiagowfx&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;The whole thing — manifests, nine seeded skills, README, license — was&#xA;scaffolded by Claude Code itself, which felt appropriately recursive: one of the&#xA;skills I was packaging interviews me about a plan before any code gets written,&#xA;so it &lt;a href=&#34;https://github.com/thiagowfx/skills/blob/bb5c0d6ef60b7873d8df907a668db833674a0849/plugins/thiagowfx/skills/grill-me/SKILL.md&#34;&gt;grilled&#xA;me&lt;/a&gt;&#xA;about how to package the skills. Quite meta, eh?&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;Next up&lt;/strong&gt;: point the dotfiles at the repo so there&amp;rsquo;s a single source of truth.&lt;/p&gt;&#xA;&lt;hr&gt;&#xA;&lt;p&gt;🤖 &lt;em&gt;Drafted with &lt;code&gt;/bloggify&lt;/code&gt;.&lt;/em&gt;&lt;/p&gt;&#xA;&lt;p&gt;— § —&lt;/p&gt;&lt;p&gt;Reply via &lt;a href=&#34;mailto:serendipity@perrotta.dev?subject=Reply to: claude code: ship your skills as a plugin marketplace&#34;&gt;email&lt;/a&gt;&lt;/p&gt;&lt;p&gt;&lt;a href=&#34;https://perrotta.dev/tags/ai/&#34;&gt;#ai&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/bestof/&#34;&gt;#bestof&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/coding/&#34;&gt;#coding&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/dev/&#34;&gt;#dev&lt;/a&gt;&lt;/p&gt;</description>
    </item>
  </channel>
</rss>
