<?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>¬ just serendipity 🍀</title>
    <link>https://perrotta.dev/</link>
    <description>Recent content 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>
    <atom:link href="https://perrotta.dev/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>ssh: pipe SQL through doas with base64
      </title>
      <link>https://perrotta.dev/2026/08/ssh-pipe-sql-through-doas-with-base64/</link>
      <pubDate>Sat, 29 Aug 2026 02:44:43 +0200</pubDate><author>serendipity@perrotta.dev (Thiago Perrotta)</author>
      <category>bloggify</category>
      <category>dev</category>
      <category>linux</category>
      <category>ssh</category>
      <guid>https://perrotta.dev/2026/08/ssh-pipe-sql-through-doas-with-base64/</guid>
      <description>&lt;p&gt;♠ &lt;strong&gt;Problem statement&lt;/strong&gt;: nested shell quotes made a remote PostgreSQL update&#xA;harder to run than the update itself.&lt;/p&gt;&#xA;&lt;p&gt;The target is an Alpine Linux host. Its Miniflux configuration contains the database&#xA;connection string, so &lt;code&gt;psql&lt;/code&gt; must run after sourcing &lt;code&gt;/etc/miniflux.conf&lt;/code&gt; through&#xA;&lt;code&gt;doas&lt;/code&gt;.&lt;/p&gt;&#xA;&lt;p&gt;An inline command quickly becomes a quoting puzzle. SQL contains single quotes,&#xA;Unicode, and sometimes apostrophes. The local shell also expands unquoted&#xA;characters before &lt;code&gt;ssh&lt;/code&gt; sends anything to the server.&lt;/p&gt;&#xA;&lt;p&gt;I wrote SQL to a temporary file, encoded it locally, and sent only base64 over&#xA;SSH:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;% base64 /tmp/miniflux-flags.sql | tr -d &amp;#39;\n&amp;#39; | \&#xA;  ssh knol &amp;#39;doas sh -c &amp;#39;\&amp;#39;&amp;#39;&#xA;    . /etc/miniflux.conf&#xA;    base64 -d | psql &amp;#34;$DATABASE_URL&amp;#34; -X -v ON_ERROR_STOP=1&#xA;  &amp;#39;\&amp;#39;&amp;#39;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;Remote shell receives a simple command. &lt;code&gt;base64 -d&lt;/code&gt; reconstructs SQL on the&#xA;server. &lt;code&gt;psql&lt;/code&gt; reads it from standard input. &lt;code&gt;ON_ERROR_STOP=1&lt;/code&gt; makes a SQL&#xA;error stop the command instead of continuing to later statements.&lt;/p&gt;&#xA;&lt;p&gt;The file contained real, guarded updates:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-sql&#34;&gt;BEGIN;&#xA;UPDATE feeds SET title = title || &amp;#39; 🇺🇸&amp;#39;&#xA;WHERE id = 1394 AND title = &amp;#39;furbo.org: Craig Hockenberry&amp;#39;&#xA;RETURNING id, title;&#xA;UPDATE feeds SET title = title || &amp;#39; 🇨🇦&amp;#39;&#xA;WHERE id = 1406 AND title = &amp;#39;Ansuz: Matthew Skala&amp;#39;&#xA;RETURNING id, title;&#xA;UPDATE feeds SET title = title || &amp;#39; 🇬🇧&amp;#39;&#xA;WHERE id = 1411 AND title = &amp;#39;Ian Jackson&amp;#39;&#xA;RETURNING id, title;&#xA;-- eight more exact-title guards&#xA;COMMIT;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;The remote output stayed useful:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-plaintext&#34;&gt;BEGIN&#xA; id  |              title&#xA;-----&amp;#43;---------------------------------&#xA;1394 | furbo.org: Craig Hockenberry 🇺🇸&#xA;(1 row)&#xA;UPDATE 1&#xA;...&#xA;UPDATE 1&#xA;COMMIT&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;A second query verified every ID after commit:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-plaintext&#34;&gt; id  |              title&#xA;-----&amp;#43;---------------------------------&#xA;1394 | furbo.org: Craig Hockenberry 🇺🇸&#xA;1406 | Ansuz: Matthew Skala 🇨🇦&#xA;1411 | Ian Jackson 🇬🇧&#xA;1444 | Matthew Garrett: mjg59 🇬🇧&#xA;1459 | Cloudscaling by Randy Bias 🇺🇸&#xA;1460 | Marcin Juszkiewicz 🇵🇱&#xA;1464 | Liss is More by Casey Liss 🇺🇸&#xA;1472 | Jerod Santo 🇺🇸&#xA;1482 | Matthias Kirschner 🇩🇪&#xA;1545 | Charles Leifer 🇺🇸&#xA;(10 rows)&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;Base64 does not encrypt anything. It only moves one opaque payload through&#xA;several shells. That is enough to keep local parsing away from SQL.&lt;/p&gt;&#xA;&lt;hr&gt;&#xA;&lt;p&gt;🤖 &lt;em&gt;Drafted with &lt;a href=&#34;https://github.com/thiagowfx/skills/blob/master/plugins/thiagowfx/skills/bloggify/SKILL.md&#34;&gt;&lt;code&gt;/bloggify&lt;/code&gt;&lt;/a&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: ssh: pipe SQL through doas with base64&#34;&gt;email&lt;/a&gt;&lt;/p&gt;&lt;p&gt;&lt;a href=&#34;https://perrotta.dev/tags/bloggify/&#34;&gt;#bloggify&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/linux/&#34;&gt;#linux&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/ssh/&#34;&gt;#ssh&lt;/a&gt;&lt;/p&gt;</description>
    </item>
    <item>
      <title>blog: calendar view
      </title>
      <link>https://perrotta.dev/2026/08/blog-calendar-view/</link>
      <pubDate>Sat, 29 Aug 2026 02:12:41 +0200</pubDate><author>serendipity@perrotta.dev (Thiago Perrotta)</author>
      <category>bloggify</category>
      <category>dev</category>
      <category>meta</category>
      <guid>https://perrotta.dev/2026/08/blog-calendar-view/</guid>
      <description>&lt;p&gt;♠ &lt;strong&gt;Problem statement&lt;/strong&gt;: my blog archive was useful, but did not show its posting&#xA;pattern over time.&lt;/p&gt;&#xA;&lt;p&gt;I liked &lt;a href=&#34;https://blog.jim-nielsen.com/2026/blog-calendar-view/&#34;&gt;Jim Nielsen&amp;rsquo;s calendar view&lt;/a&gt;,&#xA;so I added one too. &lt;a href=&#34;https://perrotta.dev/posts/&#34;&gt;List&lt;/a&gt; remains better for finding a post. The new&#xA;&lt;a href=&#34;https://perrotta.dev/calendar/&#34;&gt;calendar&lt;/a&gt; is for seeing when I published.&lt;/p&gt;&#xA;&lt;p&gt;The calendar and list use one filter. Coding, recipes, and RSS-only posts stay&#xA;out of both:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-go-html-template&#34;&gt;{{- $pages := where .pages &amp;#34;Site.Language.Lang&amp;#34; .lang -}}&#xA;{{- $pages = where $pages &amp;#34;Kind&amp;#34; &amp;#34;page&amp;#34; -}}&#xA;{{- $pages = where $pages &amp;#34;Params.rss_only&amp;#34; &amp;#34;!=&amp;#34; true -}}&#xA;{{- $excludeCategories := slice &amp;#34;coding&amp;#34; &amp;#34;recipes&amp;#34; -}}&#xA;{{- $pages = where $pages &amp;#34;Params.categories&amp;#34; &amp;#34;intersect&amp;#34; $excludeCategories | symdiff $pages -}}&#xA;{{- return $pages -}}&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;I group posts by ISO date. A day gets a blue dot. A day with a &lt;code&gt;bestof&lt;/code&gt; post&#xA;gets a yellow star. Clicking it shows every post from that date:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-go-html-template&#34;&gt;{{- $postsByDate := newScratch }}&#xA;{{- range $pages }}&#xA;  {{- $key := .Date.Format &amp;#34;2006-01-02&amp;#34; }}&#xA;  {{- $dayPages := $postsByDate.Get $key | default (slice) }}&#xA;  {{- $postsByDate.Set $key ($dayPages | append .) }}&#xA;{{- end }}&#xA;&#xA;&amp;lt;details class=&amp;#34;calendar-day calendar-day--has-post&amp;#34;&amp;gt;&#xA;  &amp;lt;summary&amp;gt;&#xA;    &amp;lt;span class=&amp;#34;calendar-day--post{{ if $featured }} calendar-day--featured{{ end }}&amp;#34;&amp;gt;&#xA;      {{ if $featured }}&amp;lt;span class=&amp;#34;calendar-day--featured-icon&amp;#34;&amp;gt;★&amp;lt;/span&amp;gt;{{ end }}&#xA;    &amp;lt;/span&amp;gt;&#xA;  &amp;lt;/summary&amp;gt;&#xA;  &amp;lt;div class=&amp;#34;calendar-popover&amp;#34;&amp;gt;&#xA;    {{- range $dayPages }}&#xA;    &amp;lt;a href=&amp;#34;{{ .Permalink }}&amp;#34;&amp;gt;&#xA;      &amp;lt;span&amp;gt;{{ if in .Params.tags &amp;#34;bestof&amp;#34; }}★ {{ end }}{{ .Title }}&amp;lt;/span&amp;gt;&#xA;    &amp;lt;/a&amp;gt;&#xA;    {{- end }}&#xA;  &amp;lt;/div&amp;gt;&#xA;&amp;lt;/details&amp;gt;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;It has English, Portuguese, and Italian month labels. The marker target is 24&#xA;pixels tall, and mobile uses two month columns:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-css&#34;&gt;.calendar-day--has-post summary {&#xA;  width: 100%;&#xA;  min-height: 24px;&#xA;}&#xA;&#xA;@media (max-width: 600px) {&#xA;  .calendar-months {&#xA;    grid-template-columns: repeat(2, minmax(0, 1fr));&#xA;  }&#xA;}&lt;/code&gt;&lt;/pre&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;% git show --stat --oneline 9c1db78581&#xA;9c1db78581 calendar&#xA;11 files changed, 388 insertions(&amp;#43;), 33 deletions(-)&#xA;&#xA;% hugo --environment production&#xA;                   │  EN  │ PT  │ IT&#xA;───────────────────┼──────┼─────┼────&#xA; Pages             │ 2968 │ 225 │ 21&#xA;Total in 8080 ms&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;hr&gt;&#xA;&lt;p&gt;🤖 &lt;em&gt;Drafted with &lt;a href=&#34;https://github.com/thiagowfx/skills/blob/master/plugins/thiagowfx/skills/bloggify/SKILL.md&#34;&gt;&lt;code&gt;/bloggify&lt;/code&gt;&lt;/a&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: blog: calendar view&#34;&gt;email&lt;/a&gt;&lt;/p&gt;&lt;p&gt;&lt;a href=&#34;https://perrotta.dev/tags/bloggify/&#34;&gt;#bloggify&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/meta/&#34;&gt;#meta&lt;/a&gt;&lt;/p&gt;</description>
    </item>
    <item>
      <title>★ miniflux: add country flags to all feeds 🌐
      </title>
      <link>https://perrotta.dev/2026/08/miniflux-add-country-flags-to-all-feeds/</link>
      <pubDate>Fri, 28 Aug 2026 01:20:26 +0200</pubDate><author>serendipity@perrotta.dev (Thiago Perrotta)</author>
      <category>bestof</category>
      <category>dev</category>
      <category>selfhosted</category>
      <guid>https://perrotta.dev/2026/08/miniflux-add-country-flags-to-all-feeds/</guid>
      <description>&lt;p&gt;♠ &lt;strong&gt;Problem statement&lt;/strong&gt;: given my miniflux server with hundreds of RSS feed&#xA;subscriptions, add emoji country flags to each feed title, representing their&#xA;country of origin and/or the country they&amp;rsquo;re associated with&lt;sup id=&#34;fnref:1&#34;&gt;&lt;a href=&#34;https://perrotta.dev/2026/08/miniflux-add-country-flags-to-all-feeds/#fn:1&#34; class=&#34;footnote-ref&#34; role=&#34;doc-noteref&#34;&gt;1&lt;/a&gt;&lt;/sup&gt;.&lt;/p&gt;&#xA;&lt;p&gt;For example:&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;&lt;strong&gt;Before&lt;/strong&gt;: Not Just Serendipity by Thiago Perrotta&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;After&lt;/strong&gt;: Not Just Serendipity by Thiago Perrotta 🇧🇷🇨🇦&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;Some feeds may be identified with multiple flags. The distinction is purposely&#xA;loose.&lt;/p&gt;&#xA;&lt;p&gt;I started to do this tagging process manually, throughout several weeks, until I&#xA;exhausted the ones I knew about.&lt;/p&gt;&#xA;&lt;p&gt;The next natural step, in 2026, is to resort to Gen AI to finish the heavy&#xA;lifting, now that the initial pattern has been established.&lt;/p&gt;&#xA;&lt;p&gt;With my &lt;a href=&#34;https://perrotta.dev/2026/08/chatgpt-/-openai-/-codex-plan-usage/&#34;&gt;OpenAI Codex&lt;/a&gt; subscription and pi, I prompted away:&lt;/p&gt;&#xA;&lt;blockquote&gt;&#xA;&lt;p&gt;User: List all prompts I ran in this session, bulleted list:&lt;/p&gt;&#xA;&lt;p&gt;Thinking: Listing all user prompt messages&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;&amp;ldquo;Are you able to connect to my miniflux database in knol?&#xA;(ssh knol) Creds are in /etc/miniflux.conf in knol.&amp;rdquo;&lt;/li&gt;&#xA;&lt;li&gt;&amp;ldquo;Note that Alpine Linux uses doas, not sudo.&amp;rdquo;&lt;/li&gt;&#xA;&lt;li&gt;&amp;ldquo;Are you able to find my list of subscriptions / feeds? There&#xA;should be 687 of these.&amp;rdquo;&lt;/li&gt;&#xA;&lt;li&gt;&amp;ldquo;Observe that some feed titles have flags in them.&amp;rdquo;&lt;/li&gt;&#xA;&lt;li&gt;&amp;ldquo;I want to add a flag for the origin of each blog. Source a&#xA;few feeds lacking flags and suggest which flags to add.&amp;rdquo;&lt;/li&gt;&#xA;&lt;li&gt;&amp;ldquo;LGTM.&amp;rdquo;&lt;/li&gt;&#xA;&lt;li&gt;&amp;ldquo;Source 10–15 more blogs.&amp;rdquo;&lt;/li&gt;&#xA;&lt;li&gt;&amp;ldquo;LGTM.&amp;rdquo;&lt;/li&gt;&#xA;&lt;li&gt;&amp;ldquo;Write a blog post about this work using /bloggify.&amp;rdquo;&lt;/li&gt;&#xA;&lt;li&gt;&amp;ldquo;List all prompts I ran in this session, bulleted list.&amp;rdquo;&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;/blockquote&gt;&#xA;&lt;p&gt;Here are a few interesting bits produced by the agent. Note that I run the&#xA;coding harness (&lt;code&gt;pi&lt;/code&gt;) from my laptop, not from my server. As such, all commands&#xA;that interact with the server do so via &lt;code&gt;ssh&lt;/code&gt;. This is good enough for my&#xA;purposes.&lt;/p&gt;&#xA;&lt;p&gt;Count all feeds:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;% ssh knol &amp;#39;doas sh -c &amp;#39;\&amp;#39;&amp;#39;. /etc/miniflux.conf; psql &amp;#34;$DATABASE_URL&amp;#34; -X -Atqc &amp;#34;SELECT count(*) FROM feeds&amp;#34;&amp;#39;\&amp;#39;&amp;#39;&amp;#39;&#xA;687&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;Some titles already followed the convention:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-plaintext&#34;&gt;780|37signals jobs 🇩🇰🇺🇸&#xA;818|A cup of coffee by Quentin JOLY 🇫🇷&#xA;1366|AI Engineer Guide 🇮🇳&#xA;562|Adam Gordon Bell: Cascade Of Insights 🇨🇦&#xA;916|Adam Johnson 🇬🇧&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;A pair of Unicode regional indicators forms a flag. Export the titles and&#xA;count those pairs:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;% ssh knol &amp;#39;doas sh -c &amp;#39;\&amp;#39;&amp;#39;. /etc/miniflux.conf; psql &amp;#34;$DATABASE_URL&amp;#34; -X -q -c \&#xA;  &amp;#34;COPY (SELECT id, title FROM feeds ORDER BY title) TO STDOUT WITH CSV&amp;#34;&amp;#39;\&amp;#39;&amp;#39;&amp;#39; | \&#xA;  python3 -c &amp;#39;import csv,sys,re; rows=list(csv.reader(sys.stdin)); p=re.compile(r&amp;#34;[\U0001F1E6-\U0001F1FF]{2}&amp;#34;); found=[(i,t,p.findall(t)) for i,t in rows if p.search(t)]; print(f&amp;#34;feeds_with_flags={len(found)} flags={sum(len(x[2]) for x in found)}&amp;#34;)&amp;#39;&#xA;feeds_with_flags=148 flags=158&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;I did not infer origin from a name, domain, language, or current residence. Each&#xA;addition needed an explicit source. For example, &lt;a href=&#34;https://adactio.com/about/&#34;&gt;Jeremy&#xA;Keith&lt;/a&gt; says &amp;ldquo;I&amp;rsquo;m from Ireland originally&amp;rdquo;, &lt;a href=&#34;https://andregarzia.com/about.html&#34;&gt;Andre&#xA;Garzia&lt;/a&gt; says he is originally from Brazil,&#xA;and &lt;a href=&#34;https://shibumi.dev/about/&#34;&gt;Christian Rebischke&lt;/a&gt; describes himself as being&#xA;from Germany.&lt;/p&gt;&#xA;&lt;p&gt;The title itself is the metadata. Exact old-title guards made each update&#xA;idempotent and prevented an accidental edit to a renamed feed:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-sql&#34;&gt;BEGIN;&#xA;UPDATE feeds&#xA;SET title = &amp;#39;Christian Rebischke: shibumi 🇩🇪&amp;#39;&#xA;WHERE id = 207 AND title = &amp;#39;Christian Rebischke: shibumi&amp;#39;;&#xA;UPDATE feeds&#xA;SET title = &amp;#39;Adactio: Journal by Jeremy Keith 🇮🇪&amp;#39;&#xA;WHERE id = 1106 AND title = &amp;#39;Adactio: Journal by Jeremy Keith&amp;#39;;&#xA;UPDATE feeds&#xA;SET title = &amp;#39;Adactio: Articles by Jeremy Keith 🇮🇪&amp;#39;&#xA;WHERE id = 1107 AND title = &amp;#39;Adactio: Articles by Jeremy Keith&amp;#39;;&#xA;UPDATE feeds&#xA;SET title = &amp;#39;Andre Alves Garzia 🇧🇷&amp;#39;&#xA;WHERE id = 1481 AND title = &amp;#39;Andre Alves Garzia&amp;#39;;&#xA;COMMIT;&lt;/code&gt;&lt;/pre&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-plaintext&#34;&gt;BEGIN&#xA;UPDATE 1&#xA;UPDATE 1&#xA;UPDATE 1&#xA;UPDATE 1&#xA;COMMIT&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;A second sourced batch added 12 more titles. Compound origins stay compound:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-plaintext&#34;&gt;430|Burak Karakan: ps 🇩🇪🇹🇷&#xA;1022|Amjad Masad 🇯🇴🇺🇸&#xA;1076|Christopher Olah 🇨🇦&#xA;1100|Cognitive Medium by Michael Nielsen 🇦🇺🇺🇸&#xA;1174|Andrej Karpathy 🇸🇰🇨🇦&#xA;1489|Jeff Geerling 🇺🇸&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;The same audit now reports:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-plaintext&#34;&gt;total=687&#xA;feeds_with_flags=164&#xA;flags=178&#xA;without_flags=523&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;Only 523 to go.&lt;/p&gt;&#xA;&lt;p&gt;This is a practical excuse to tokenmaxx, eh?!&lt;/p&gt;&#xA;&lt;p&gt;Web searches are done with &lt;a href=&#34;https://github.com/thiagowfx/.dotfiles/tree/e6d1452118ad2b2c317545571fd0923519bf7a41/pi/.pi/agent/local/web-search&#34;&gt;pi-web-search&lt;/a&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;Why? Purely as a matter of context, and for an extra dose of serendipity&#xA;via diversity. For example, it&amp;rsquo;s often exciting to me to read posts from&#xA;other Canadians, for no particular reason.&amp;#160;&lt;a href=&#34;https://perrotta.dev/2026/08/miniflux-add-country-flags-to-all-feeds/#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: miniflux: add country flags to all feeds 🌐&#34;&gt;email&lt;/a&gt;&lt;/p&gt;&lt;p&gt;&lt;a href=&#34;https://perrotta.dev/tags/bestof/&#34;&gt;#bestof&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/selfhosted/&#34;&gt;#selfhosted&lt;/a&gt;&lt;/p&gt;</description>
    </item>
    <item>
      <title>claude code: /goal
      </title>
      <link>https://perrotta.dev/2026/08/claude-code-/goal/</link>
      <pubDate>Wed, 26 Aug 2026 17:45:34 +0200</pubDate><author>serendipity@perrotta.dev (Thiago Perrotta)</author>
      <category>ai</category>
      <category>argocd</category>
      <category>bloggify</category>
      <category>claude</category>
      <category>dev</category>
      <category>kubernetes</category>
      <guid>https://perrotta.dev/2026/08/claude-code-/goal/</guid>
      <description>&lt;p&gt;♠ &lt;strong&gt;Problem statement&lt;/strong&gt;: a coding agent stops when it thinks it is done, not when&#xA;the thing is actually done.&lt;/p&gt;&#xA;&lt;p&gt;&lt;a href=&#34;https://code.claude.com/docs/en/goal.md&#34;&gt;&lt;code&gt;/goal&lt;/code&gt;&lt;/a&gt; sets a condition that&#xA;outlives the turn. The docs call it a wrapper around a session-scoped,&#xA;prompt-based Stop hook: every time the agent tries to end its turn, an evaluator&#xA;checks the condition and pushes back if it does not hold.&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-plaintext&#34;&gt;/goal argocd, prometheus and external secrets should all be healthy and synced in cluster #26&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;I set that mid-session while debugging a cluster where three Argo CD apps were&#xA;stuck. What followed was the useful part. The agent wrapped up with a tidy&#xA;summary and two pull requests, and got this back:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-plaintext&#34;&gt;Stop hook feedback:&#xA;[argocd, prometheus and external secrets should all be healthy and synced in cluster #26]:&#xA;PRs #3064 and #3062 have been opened but not yet merged. The latest status check&#xA;in the transcript confirms external-secrets = &amp;#39;OutOfSync&amp;#39;, argocd = &amp;#39;OutOfSync&amp;#39;,&#xA;prometheus = &amp;#39;Unknown&amp;#39; — none are both &amp;#39;healthy and synced&amp;#39;.&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;Opening a PR is not the same as the apps being green, and the evaluator reads&#xA;the transcript rather than the closing paragraph. It fired three times. Each&#xA;time the summary was plausible and each time the condition was still false.&lt;/p&gt;&#xA;&lt;p&gt;&lt;code&gt;/goal&lt;/code&gt; with no arguments shows the condition, elapsed time, turn count, token&#xA;spend, and the evaluator&amp;rsquo;s last reason. &lt;code&gt;/goal clear&lt;/code&gt; ends it.&lt;/p&gt;&#xA;&lt;p&gt;The failure mode it fixes is specific: an agent that declares victory one step&#xA;early. It does not make the agent smarter — mine still burned several turns on&#xA;wrong theories, and I had to redirect it more than once. It only refuses to let&#xA;&amp;ldquo;I opened a PR&amp;rdquo; stand in for &amp;ldquo;the apps are green&amp;rdquo;.&lt;/p&gt;&#xA;&lt;hr&gt;&#xA;&lt;p&gt;🤖 &lt;em&gt;Drafted with &lt;a href=&#34;https://github.com/thiagowfx/skills/blob/master/plugins/thiagowfx/skills/bloggify/SKILL.md&#34;&gt;&lt;code&gt;/bloggify&lt;/code&gt;&lt;/a&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: /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/argocd/&#34;&gt;#argocd&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/bloggify/&#34;&gt;#bloggify&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/claude/&#34;&gt;#claude&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/kubernetes/&#34;&gt;#kubernetes&lt;/a&gt;&lt;/p&gt;</description>
    </item>
    <item>
      <title>Continuity
      </title>
      <link>https://perrotta.dev/2026/08/continuity/</link>
      <pubDate>Tue, 25 Aug 2026 15:40:59 +0200</pubDate><author>serendipity@perrotta.dev (Thiago Perrotta)</author>
      <category>dev</category>
      <category>macos</category>
      <category>serenity</category>
      <guid>https://perrotta.dev/2026/08/continuity/</guid>
      <description>&lt;p&gt;♠ A serendipitous discovery: macOS&#xA;&lt;a href=&#34;https://www.apple.com/macos/continuity/&#34;&gt;continuity&lt;/a&gt;.&lt;/p&gt;&#xA;&lt;p&gt;Picture this:&lt;/p&gt;&#xA;&lt;p&gt;I am using my corp Macbook Pro, whilst my personal Macbook Air happens to be&#xA;close by. Both of their lids are open.&lt;/p&gt;&#xA;&lt;p&gt;In my corp Macbook, for some reason, I decided to move the mouse cursor to the&#xA;edge of the laptop screen, using the trackpad.&lt;/p&gt;&#xA;&lt;p&gt;And then, just like magic, the mouse cursor &lt;em&gt;in my personal Macbook&lt;/em&gt; starts to&#xA;move around.&lt;/p&gt;&#xA;&lt;p&gt;And then I open up a terminal and start to type, from my corp Macbook, and&#xA;somehow characters start to appear in my personal Macbook.&lt;/p&gt;&#xA;&lt;p&gt;Remote trackpad + keyboard control. This is amazing!&lt;/p&gt;&#xA;&lt;p&gt;This post was created in my personal Macbook, typed from my corp keyboard.&lt;/p&gt;&#xA;&lt;p&gt;Naturally, I am signed into my Apple ID in both devices. ∎&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: Continuity&#34;&gt;email&lt;/a&gt;&lt;/p&gt;&lt;p&gt;&lt;a href=&#34;https://perrotta.dev/tags/dev/&#34;&gt;#dev&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/macos/&#34;&gt;#macos&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/serenity/&#34;&gt;#serenity&lt;/a&gt;&lt;/p&gt;</description>
    </item>
    <item>
      <title>pi: prompt template or skill?
      </title>
      <link>https://perrotta.dev/2026/08/pi-prompt-template-or-skill/</link>
      <pubDate>Tue, 25 Aug 2026 01:13:46 +0200</pubDate><author>serendipity@perrotta.dev (Thiago Perrotta)</author>
      <category>ai</category>
      <category>dev</category>
      <category>pi</category>
      <guid>https://perrotta.dev/2026/08/pi-prompt-template-or-skill/</guid>
      <description>&lt;p&gt;♠ &lt;strong&gt;Problem statement&lt;/strong&gt;: I often type &lt;code&gt;commit (only) what you changed&lt;/code&gt; in coding&#xA;agent harnesses. Can I make an abbreviation out of it?&lt;/p&gt;&#xA;&lt;p&gt;&lt;a href=&#34;https://pi.dev/&#34;&gt;Pi&lt;/a&gt; turns a Markdown file in &lt;code&gt;~/.pi/agent/prompts/&lt;/code&gt; into a&#xA;/slash command. This one gives me &lt;code&gt;/commit&lt;/code&gt;:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-markdown&#34;&gt;---&#xA;description: Commit files changed in this turn without pushing&#xA;---&#xA;Commit what you changed (only). DO NOT push.&lt;/code&gt;&lt;/pre&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;% find ~/.pi/agent/prompts -maxdepth 1 -type f -print -exec sh -c &amp;#39;printf &amp;#34;%s\n&amp;#34; &amp;#34;--- $1&amp;#34;; cat &amp;#34;$1&amp;#34;&amp;#39; _ {} \;&#xA;/Users/thiago.perrotta/.pi/agent/prompts/commit.md&#xA;--- /Users/thiago.perrotta/.pi/agent/prompts/commit.md&#xA;---&#xA;description: Commit files changed in this turn without pushing&#xA;---&#xA;Commit what you changed (only). DO NOT push.&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;In Pi, a &lt;a href=&#34;https://pi.dev/docs/latest/prompt-templates&#34;&gt;prompt template&lt;/a&gt; is fixed&#xA;prompt expansion. It is right for a short, stable request:&lt;/p&gt;&#xA;&lt;blockquote&gt;&#xA;&lt;p&gt;Prompt templates are Markdown snippets that expand into full prompts. Type&#xA;/name in the editor to invoke a template, where name is the filename without&#xA;.md.&lt;/p&gt;&#xA;&lt;/blockquote&gt;&#xA;&lt;p&gt;A &lt;a href=&#34;https://agentskills.io/home&#34;&gt;skill&lt;/a&gt; is a capability package. Its description&#xA;is available at startup, but Pi loads its&#xA;&lt;a href=&#34;https://pi.dev/docs/latest/skills&#34;&gt;&lt;code&gt;SKILL.md&lt;/code&gt;&lt;/a&gt; only when task matches or I run&#xA;&lt;code&gt;/skill:name&lt;/code&gt;. A skill can carry scripts, references, setup, and a workflow that&#xA;branches on state:&lt;/p&gt;&#xA;&lt;blockquote&gt;&#xA;&lt;p&gt;Skills are self-contained capability packages that the agent loads on-demand.&#xA;A skill provides specialized workflows, setup instructions, helper scripts,&#xA;and reference documentation for specific tasks.&lt;/p&gt;&#xA;&lt;/blockquote&gt;&#xA;&lt;p&gt;A commit flow with checks, generated files, partial staging rules, or project&#xA;specific policy deserves a skill. This one is a couple of words and one&#xA;constraint, so I choose to keep it simple. ∎&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: prompt template or skill?&#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/dev/&#34;&gt;#dev&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/pi/&#34;&gt;#pi&lt;/a&gt;&lt;/p&gt;</description>
    </item>
    <item>
      <title>terrateam: plan from drift issues
      </title>
      <link>https://perrotta.dev/2026/08/terrateam-plan-from-drift-issues/</link>
      <pubDate>Mon, 24 Aug 2026 18:38:39 +0200</pubDate><author>serendipity@perrotta.dev (Thiago Perrotta)</author>
      <category>bloggify</category>
      <category>dev</category>
      <category>terraform</category>
      <category>terrateam</category>
      <guid>https://perrotta.dev/2026/08/terrateam-plan-from-drift-issues/</guid>
      <description>&lt;p&gt;♠ &lt;strong&gt;Problem statement&lt;/strong&gt;: Terrateam opens drift reports as GitHub issues, but &lt;code&gt;terrateam plan&lt;/code&gt; only works on pull requests.&lt;/p&gt;&#xA;&lt;p&gt;The distinction is explicit in Terrateam&amp;rsquo;s &lt;a href=&#34;https://github.com/terrateamio/terrateam/blob/cc723447ff80023b8627df6f8719bfb38462cf87/code/src/terrat_vcs_service_github/terrat_vcs_service_github_ep_events3.ml#L550-L655&#34;&gt;issue comment handler&lt;/a&gt;:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-ocaml&#34;&gt;{ primary = Primary.{ number = pull_request_id; pull_request = Some _; _ }; _ };&#xA;&#xA;| Gw.Issue_comment_event.Issue_comment_created _ -&amp;gt;&#xA;    Logs.debug (fun m -&amp;gt; m &amp;#34;%s : NOOP : ISSUE_COMMENT_CREATED&amp;#34; request_id);&#xA;    Prmths.Counter.inc_one (Metrics.comment_events_total &amp;#34;noop&amp;#34;);&#xA;    Abbs_future_combinators.return_ok ()&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;Calling the repository workflow directly was not an option either. Its work token comes from the Terrateam backend.&lt;/p&gt;&#xA;&lt;p&gt;So I kept Terrateam in charge and gave it the pull request it expects. A GitHub Actions relay checks that the source is an open Terrateam drift issue and that the commenter has repository write access. It then puts a comment-only Terraform file in each selected drift directory:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-python&#34;&gt;markers = [&#xA;    InputGitTreeElement(&#xA;        path=f&amp;#34;{directory}/terrateam_issue_relay.tf&amp;#34;,&#xA;        mode=&amp;#34;100644&amp;#34;,&#xA;        type=&amp;#34;blob&amp;#34;,&#xA;        content=f&amp;#34;# Terrateam plan relay for drift issue #{issue_number}, comment {comment_id}.\n&amp;#34;,&#xA;    )&#xA;    for directory in directories&#xA;]&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;That path change gives Terrateam a real dirspace without changing infrastructure. The relay opens a temporary draft PR and posts the original command on it:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-python&#34;&gt;pull = repo.create_pull(&#xA;    base=repo.default_branch,&#xA;    head=branch,&#xA;    title=f&amp;#34;Terrateam plan relay for drift issue #{issue_number}&amp;#34;,&#xA;    body=relay_body(issue_number, comment_id, event[&amp;#34;comment&amp;#34;][&amp;#34;html_url&amp;#34;]),&#xA;    draft=True,&#xA;)&#xA;pull.as_issue().create_comment(command)&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;Terrateam uses its normal locks, hooks, credentials, and batching. Another workflow copies its comments back to the drift issue. &lt;code&gt;apply&lt;/code&gt; remains unsupported: an issue has no PR approval or review intent.&lt;/p&gt;&#xA;&lt;p&gt;The live test planned the requested directory and returned the result:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-plaintext&#34;&gt;## Plans :thumbsup:&#xA;&#xA;## Terrateam Plan Output :thumbsup:&#xA;&#xA;**Plan: 0 to add, 8 to change, 0 to destroy**&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;The temporary PR ended where it should:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;% gh pr view 6687 --json number,state,headRefName,title&#xA;{&amp;#34;headRefName&amp;#34;:&amp;#34;bot/terrateam-issue-relay/6663-5398113346&amp;#34;,&amp;#34;number&amp;#34;:6687,&amp;#34;state&amp;#34;:&amp;#34;CLOSED&amp;#34;,&amp;#34;title&amp;#34;:&amp;#34;Terrateam plan relay for drift issue #6663&amp;#34;}&lt;/code&gt;&lt;/pre&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;% ./ci/run_python_tests.sh ci/terrateam_issue_relay&#xA;collected 19 items&#xA;tests/test_relay.py ...................                                  [100%]&#xA;============================== 19 passed in 1.45s ==============================&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;A scheduled job deletes inactive relay branches after 30 hours. Terrateam still owns plan execution; the relay only translates the issue command into its existing PR protocol.&lt;/p&gt;&#xA;&lt;hr&gt;&#xA;&lt;p&gt;🤖 &lt;em&gt;Drafted with &lt;a href=&#34;https://github.com/thiagowfx/skills/blob/master/plugins/thiagowfx/skills/bloggify/SKILL.md&#34;&gt;&lt;code&gt;/bloggify&lt;/code&gt;&lt;/a&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: terrateam: plan from drift issues&#34;&gt;email&lt;/a&gt;&lt;/p&gt;&lt;p&gt;&lt;a href=&#34;https://perrotta.dev/tags/bloggify/&#34;&gt;#bloggify&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/terraform/&#34;&gt;#terraform&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/terrateam/&#34;&gt;#terrateam&lt;/a&gt;&lt;/p&gt;</description>
    </item>
    <item>
      <title>★ Books
      </title>
      <link>https://perrotta.dev/2026/08/books/</link>
      <pubDate>Mon, 24 Aug 2026 18:28:31 +0200</pubDate><author>serendipity@perrotta.dev (Thiago Perrotta)</author>
      <category>bestof</category>
      <category>serenity</category>
      <guid>https://perrotta.dev/2026/08/books/</guid>
      <description>&lt;p&gt;♠ It&amp;rsquo;s about time: I added a &lt;a href=&#34;https://perrotta.dev/books/&#34;&gt;books&lt;/a&gt; page to this blog with a sample of&#xA;my favorites. ∎&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: Books&#34;&gt;email&lt;/a&gt;&lt;/p&gt;&lt;p&gt;&lt;a href=&#34;https://perrotta.dev/tags/bestof/&#34;&gt;#bestof&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/serenity/&#34;&gt;#serenity&lt;/a&gt;&lt;/p&gt;</description>
    </item>
    <item>
      <title>bloggify, revisited
      </title>
      <link>https://perrotta.dev/2026/08/bloggify-revisited/</link>
      <pubDate>Mon, 24 Aug 2026 01:21:57 +0200</pubDate><author>serendipity@perrotta.dev (Thiago Perrotta)</author>
      <category>ai</category>
      <category>bloggify</category>
      <category>dev</category>
      <category>serenity</category>
      <guid>https://perrotta.dev/2026/08/bloggify-revisited/</guid>
      <description>&lt;p&gt;♠ Yours truly, &lt;a href=&#34;https://perrotta.dev/2026/06/bloggify/&#34;&gt;more than two months ago&lt;/a&gt;:&lt;/p&gt;&#xA;&lt;blockquote&gt;&#xA;&lt;p&gt;As of today, it&amp;rsquo;s not clear to me whether this approach will stick.&lt;/p&gt;&#xA;&lt;/blockquote&gt;&#xA;&lt;p&gt;Clearly this approach is here to stay: &lt;a href=&#34;https://perrotta.dev/tags/bloggify/&#34;&gt;bloggify&lt;/a&gt; (40+&#xA;posts at the time of this writing).&lt;/p&gt;&#xA;&lt;p&gt;I am starting to ponder whether to split &lt;code&gt;/bloggify&lt;/code&gt; posts into an &lt;a href=&#34;https://blog.fsck.com/agent-blog/&#34;&gt;Agent&#xA;Blog&lt;/a&gt;:&lt;/p&gt;&#xA;&lt;blockquote&gt;&#xA;&lt;p&gt;These posts are written by my agents, not by me.&lt;/p&gt;&#xA;&lt;/blockquote&gt;&#xA;&lt;p&gt;We&amp;rsquo;ll see. ∎&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: bloggify, revisited&#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/bloggify/&#34;&gt;#bloggify&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/serenity/&#34;&gt;#serenity&lt;/a&gt;&lt;/p&gt;</description>
    </item>
    <item>
      <title>pi: statusline with GitHub PR link
      </title>
      <link>https://perrotta.dev/2026/08/pi-statusline-with-github-pr-link/</link>
      <pubDate>Mon, 24 Aug 2026 01:14:47 +0200</pubDate><author>serendipity@perrotta.dev (Thiago Perrotta)</author>
      <category>ai</category>
      <category>bloggify</category>
      <category>dev</category>
      <category>git</category>
      <category>pi</category>
      <guid>https://perrotta.dev/2026/08/pi-statusline-with-github-pr-link/</guid>
      <description>&lt;p&gt;♠ &lt;a href=&#34;https://perrotta.dev/2026/01/starship-github-pr-prompt/&#34;&gt;Previously&lt;/a&gt;.&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;Problem statement&lt;/strong&gt;: the &lt;a href=&#34;https://www.npmjs.com/package/@narumitw/pi-github-pr&#34;&gt;&lt;code&gt;@narumitw/pi-github-pr&lt;/code&gt;&lt;/a&gt;&#xA;statusline entry reports much more than I care about:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-plaintext&#34;&gt;PR #123: checks failing (2), changes requested, 3 comments&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;I want the clickable PR link and nothing else. Checks live in &lt;code&gt;gh pr checks&lt;/code&gt;,&#xA;comments live on GitHub, they are the source of truth.&lt;/p&gt;&#xA;&lt;p&gt;There&amp;rsquo;s no knob for it in this particular pi extension. The wording is hardcoded&#xA;in &lt;code&gt;formatCompactStatus()&lt;/code&gt;, and the single option the extension takes&#xA;(&lt;code&gt;refreshIntervalMs&lt;/code&gt;) is unreachable, because pi calls the default export with&#xA;&lt;code&gt;pi&lt;/code&gt; alone. Nothing lighter exists on the registry either — everything else is a&#xA;full footer replacement:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;% for q in &amp;#34;pi-extension%20pull%20request&amp;#34; &amp;#34;pi-extension%20statusline&amp;#34;; do&#xA;    curl -s &amp;#34;https://registry.npmjs.org/-/v1/search?text=$q&amp;amp;size=25&amp;#34; |&#xA;      jq -r &amp;#39;.objects[].package | select(.name | test(&amp;#34;^(@[^/]&amp;#43;/)?pi-&amp;#34;)) | &amp;#34;\(.name) — \(.description)&amp;#34;&amp;#39;&#xA;  done | grep -iE &amp;#34;pull request|statusline|footer&amp;#34; | sort -u&#xA;@feniix/pi-statusline — Fixed two-line status footer for pi with model, thinking, context, git, token, worktree, and skill indicators&#xA;@narumitw/pi-github-pr — Pi extension that shows GitHub pull request review, checks, and comment status.&#xA;@narumitw/pi-statusline — Pi extension that replaces the footer with an information-rich statusline.&#xA;[...]&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;Then we&amp;rsquo;ll simply fork it away: 65 lines of local extension replace the 600+ of&#xA;the extension:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-typescript&#34;&gt;export function formatPrLink(stdout: string): string | undefined {&#xA;&#x9;let pr: { number?: unknown; url?: unknown; state?: unknown };&#xA;&#x9;try {&#xA;&#x9;&#x9;pr = JSON.parse(stdout);&#xA;&#x9;} catch {&#xA;&#x9;&#x9;return undefined;&#xA;&#x9;}&#xA;&#x9;if (pr.state !== &amp;#34;OPEN&amp;#34;) return undefined;&#xA;&#x9;if (typeof pr.number !== &amp;#34;number&amp;#34; || !Number.isFinite(pr.number)) return undefined;&#xA;&#x9;const label = `PR #${pr.number}`;&#xA;&#x9;return typeof pr.url === &amp;#34;string&amp;#34; ? osc8Link(pr.url, label) : label;&#xA;}&#xA;&#xA;export default function (pi: ExtensionAPI) {&#xA;&#x9;let request = 0;&#xA;&#xA;&#x9;const refresh = async (ctx: ExtensionContext) =&amp;gt; {&#xA;&#x9;&#x9;request &amp;#43;= 1;&#xA;&#x9;&#x9;const current = request;&#xA;&#x9;&#x9;let status: string | undefined;&#xA;&#x9;&#x9;try {&#xA;&#x9;&#x9;&#x9;const result = await pi.exec(&amp;#34;gh&amp;#34;, [&amp;#34;pr&amp;#34;, &amp;#34;view&amp;#34;, &amp;#34;--json&amp;#34;, &amp;#34;number,url,state&amp;#34;], {&#xA;&#x9;&#x9;&#x9;&#x9;cwd: ctx.cwd,&#xA;&#x9;&#x9;&#x9;&#x9;signal: ctx.signal,&#xA;&#x9;&#x9;&#x9;&#x9;timeout: GH_TIMEOUT_MS,&#xA;&#x9;&#x9;&#x9;});&#xA;&#x9;&#x9;&#x9;if (result.code === 0 &amp;amp;&amp;amp; !result.killed) status = formatPrLink(result.stdout);&#xA;&#x9;&#x9;} catch {&#xA;&#x9;&#x9;&#x9;status = undefined;&#xA;&#x9;&#x9;}&#xA;&#x9;&#x9;if (current === request &amp;amp;&amp;amp; !ctx.signal?.aborted) ctx.ui.setStatus(STATUS_KEY, status);&#xA;&#x9;};&#xA;&#xA;&#x9;pi.on(&amp;#34;session_start&amp;#34;, async (_event, ctx) =&amp;gt; refresh(ctx));&#xA;&#x9;pi.on(&amp;#34;agent_end&amp;#34;, async (_event, ctx) =&amp;gt; refresh(ctx));&#xA;&#x9;pi.on(&amp;#34;session_shutdown&amp;#34;, async (_event, ctx) =&amp;gt; {&#xA;&#x9;&#x9;request &amp;#43;= 1;&#xA;&#x9;&#x9;ctx.ui.setStatus(STATUS_KEY, undefined);&#xA;&#x9;});&#xA;}&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;The &lt;code&gt;osc8Link()&lt;/code&gt; helper wraps the number in an OSC 8 hyperlink, so the entry is&#xA;clickable in the terminal (Cmd + click in Ghostty, for example):&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;% node --experimental-strip-types -e &amp;#39;&#xA;const { formatPrLink } = await import(&amp;#34;/Users/thiago.perrotta/.pi/agent/extensions/github-pr-link.ts&amp;#34;);&#xA;const { execFileSync } = await import(&amp;#34;node:child_process&amp;#34;);&#xA;const out = execFileSync(&amp;#34;gh&amp;#34;, [&amp;#34;pr&amp;#34;, &amp;#34;view&amp;#34;, &amp;#34;14215&amp;#34;, &amp;#34;-R&amp;#34;, &amp;#34;cli/cli&amp;#34;, &amp;#34;--json&amp;#34;, &amp;#34;number,url,state&amp;#34;], { encoding: &amp;#34;utf8&amp;#34; });&#xA;console.log(&amp;#34;gh stdout:&amp;#34;, out.trim());&#xA;console.log(&amp;#34;status:&amp;#34;, JSON.stringify(formatPrLink(out)));&#xA;&amp;#39;&#xA;gh stdout: {&amp;#34;number&amp;#34;:14215,&amp;#34;state&amp;#34;:&amp;#34;OPEN&amp;#34;,&amp;#34;url&amp;#34;:&amp;#34;https://github.com/cli/cli/pull/14215&amp;#34;}&#xA;status: &amp;#34;\u001b]8;;https://github.com/cli/cli/pull/14215\u0007PR #14215\u001b]8;;\u0007&amp;#34;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;Dropping the counters also dropped the second network call: the package pairs&#xA;every &lt;code&gt;gh pr view&lt;/code&gt; with a GraphQL query for comment and review totals, and polls&#xA;every 60 seconds on top of session start, branch changes, and agent turns. One&#xA;&lt;code&gt;gh pr view&lt;/code&gt; on session start and after each turn is enough. A branch switch&#xA;lands on the next turn end anyway, because switching branches means running&#xA;&lt;code&gt;git checkout&lt;/code&gt;.&lt;/p&gt;&#xA;&lt;p&gt;Every failure path — no PR, merged PR, missing &lt;code&gt;gh&lt;/code&gt;, unauthenticated &lt;code&gt;gh&lt;/code&gt; — just&#xA;clears the entry:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;% node --experimental-strip-types --test agent/extensions/tests/github-pr-link.test.ts&#xA;✔ formatPrLink links open PRs and hides everything else (0.601333ms)&#xA;✔ osc8Link rejects non-http schemes (0.079ms)&#xA;✔ session start shows the PR link (0.526625ms)&#xA;✔ agent end clears the entry when gh fails (0.094167ms)&#xA;✔ agent end clears the entry when gh is missing (0.093125ms)&#xA;✔ aborted turn keeps the previous entry (0.466084ms)&#xA;✔ session shutdown clears the entry (0.104292ms)&#xA;✔ a stale refresh does not overwrite a newer one (0.165791ms)&#xA;ℹ pass 8&#xA;ℹ fail 0&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;The whole swap:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;% pi uninstall npm:@narumitw/pi-github-pr&#xA;Removing npm:@narumitw/pi-github-pr...&#xA;Removed npm:@narumitw/pi-github-pr&#xA;&#xA;% git --no-pager show --stat --oneline HEAD&#xA;4dadda2 pi: replace pi-github-pr with a link-only extension&#xA; pi/.pi/README.md                                   |   4 &amp;#43;-&#xA; pi/.pi/agent/extensions/github-pr-link.ts          |  65 &amp;#43;&amp;#43;&amp;#43;&amp;#43;&amp;#43;&amp;#43;&amp;#43;&amp;#43;&amp;#43;&amp;#43;&#xA; .../agent/extensions/tests/github-pr-link.test.ts  | 139 &amp;#43;&amp;#43;&amp;#43;&amp;#43;&amp;#43;&amp;#43;&amp;#43;&amp;#43;&amp;#43;&amp;#43;&amp;#43;&amp;#43;&amp;#43;&amp;#43;&amp;#43;&amp;#43;&amp;#43;&amp;#43;&amp;#43;&amp;#43;&amp;#43;&#xA; pi/.pi/agent/settings.json                         |   3 &amp;#43;-&#xA; 4 files changed, 207 insertions(&amp;#43;), 4 deletions(-)&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;Files under &lt;code&gt;~/.pi/agent/extensions/*.ts&lt;/code&gt; are auto-discovered, so &lt;code&gt;/reload&lt;/code&gt;&#xA;picks it up without restarting the session.&lt;/p&gt;&#xA;&lt;hr&gt;&#xA;&lt;p&gt;🤖 &lt;em&gt;Drafted with &lt;a href=&#34;https://github.com/thiagowfx/skills/blob/master/plugins/thiagowfx/skills/bloggify/SKILL.md&#34;&gt;&lt;code&gt;/bloggify&lt;/code&gt;&lt;/a&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: statusline with GitHub PR link&#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/bloggify/&#34;&gt;#bloggify&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;a href=&#34;https://perrotta.dev/tags/pi/&#34;&gt;#pi&lt;/a&gt;&lt;/p&gt;</description>
    </item>
    <item>
      <title>★ CodeBurn
      </title>
      <link>https://perrotta.dev/2026/08/codeburn/</link>
      <pubDate>Sun, 23 Aug 2026 00:17:38 +0200</pubDate><author>serendipity@perrotta.dev (Thiago Perrotta)</author>
      <category>ai</category>
      <category>bestof</category>
      <category>dev</category>
      <guid>https://perrotta.dev/2026/08/codeburn/</guid>
      <description>&lt;p&gt;♠ &lt;a href=&#34;https://perrotta.dev/2025/09/claude-ccusage/&#34;&gt;Previously&lt;/a&gt;.&lt;/p&gt;&#xA;&lt;p&gt;&lt;a href=&#34;https://github.com/getagentseal/codeburn&#34;&gt;&lt;code&gt;CodeBurn&lt;/code&gt;&lt;/a&gt; shows Gen AI / LLM code&#xA;usage: where did all the money &lt;del&gt;burning tokens&lt;/del&gt; ($$) go?&lt;/p&gt;&#xA;&lt;blockquote&gt;&#xA;&lt;p&gt;CodeBurn is a free, open-source, local-first tool that tracks AI coding token&#xA;usage and cost across 41 tools and agents (Claude Code, Pi, Codex, Cursor,&#xA;and more), broken down by model, project, and task.&lt;/p&gt;&#xA;&lt;/blockquote&gt;&#xA;&lt;p&gt;It reads the session files already on disk. No wrapper, proxy, or API key is&#xA;needed, and nothing leaves the machine (great!).&lt;/p&gt;&#xA;&lt;p&gt;It&amp;rsquo;s a TUI.&lt;/p&gt;&#xA;&lt;p&gt;Installation methods:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;% brew install codeburn&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;OR&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;% npx codeburn&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;The same data is also available via web and desktop interfaces. On macOS,&#xA;&lt;code&gt;menubar&lt;/code&gt; downloads the native app to &lt;code&gt;~/Applications&lt;/code&gt; and launches it:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;% codeburn menubar&#xA;Resolving CodeBurn Menubar v0.9.20...&#xA;Downloading CodeBurnMenubar-v0.9.20.zip...&#xA;Verifying checksum...&#xA;Unpacking...&#xA;Verifying app bundle...&#xA;Launching CodeBurn Menubar...&#xA;&#xA;  Ready. /Users/thiago.perrotta/Applications/CodeBurnMenubar.app&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;It shows the current spend in the menu bar. Clicking it opens local breakdowns&#xA;by agent, model, and activity, plus trends, forecasts, and exports. It refreshes&#xA;every 30 seconds by default and backs off on battery.&lt;/p&gt;&#xA;&lt;p&gt;It tracks spend calculated from local sessions, not provider usage windows or&#xA;reset countdowns. &lt;a href=&#34;https://perrotta.dev/2026/05/codexbar/&#34;&gt;Codexbar&lt;/a&gt; does the latter.&lt;/p&gt;&#xA;&lt;p&gt;I find that the CLI TUI is enough.&lt;/p&gt;&#xA;&lt;p&gt;A few subcommands:&lt;/p&gt;&#xA;&lt;p&gt;&lt;code&gt;yield&lt;/code&gt; correlates AI sessions with nearby Git commits. It classifies the money&#xA;spent as productive, reverted, abandoned, or ambiguous:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;% codeburn yield -p 30days&#xA;&#xA;  Analyzing yield for Last 30 Days...&#xA;&#xA;&#xA;Productive:   $594.85 (31%) - 117 sessions shipped to main&#xA;Reverted:       $0.00 (0%) - 0 sessions were reverted&#xA;Abandoned:    $611.66 (32%) - 377 sessions never committed&#xA;Ambiguous:    $709.10 (37%) - 220 sessions lost commits to concurrent sessions&#xA;&#xA;Attribution: timestamp-window based (heuristic)&#xA;&#xA;Total:       $1915.61     - 714 sessions&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;It&amp;rsquo;s an experimental timestamp-based heuristic, but a neat answer to &amp;ldquo;did all&#xA;those tokens produce code that shipped?&amp;rdquo;&lt;/p&gt;&#xA;&lt;p&gt;&lt;code&gt;report&lt;/code&gt; opens the interactive dashboard for a given period, with token and cost&#xA;breakdowns by tool, model, project, and task:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;% codeburn report --provider pi -p month&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;∎&lt;/p&gt;&lt;p&gt;— § —&lt;/p&gt;&lt;p&gt;Reply via &lt;a href=&#34;mailto:serendipity@perrotta.dev?subject=Reply to: CodeBurn&#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/dev/&#34;&gt;#dev&lt;/a&gt;&lt;/p&gt;</description>
    </item>
    <item>
      <title>prometheus: a 117GB WAL that could not truncate
      </title>
      <link>https://perrotta.dev/2026/08/prometheus-a-117gb-wal-that-could-not-truncate/</link>
      <pubDate>Sat, 22 Aug 2026 23:35:00 +0200</pubDate><author>serendipity@perrotta.dev (Thiago Perrotta)</author>
      <category>bloggify</category>
      <category>dev</category>
      <category>kubernetes</category>
      <guid>https://perrotta.dev/2026/08/prometheus-a-117gb-wal-that-could-not-truncate/</guid>
      <description>&lt;p&gt;♠ &lt;a href=&#34;https://perrotta.dev/2026/08/azure-disk-resize-needs-the-pvc-patch-too/&#34;&gt;Previously&lt;/a&gt;.&lt;/p&gt;&#xA;&lt;p&gt;With the volume expanded, the next failure surfaced. The pod came up, served&#xA;for 14 minutes, then hit a different wall:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;% kubectl -n monitoring get pod prometheus-...-prometheus-0 -o json | \&#xA;    jq &amp;#39;.status.containerStatuses[] | select(.name==&amp;#34;prometheus&amp;#34;) | .lastState.terminated&amp;#39;&#xA;{&#xA;  &amp;#34;reason&amp;#34;: &amp;#34;OOMKilled&amp;#34;,&#xA;  &amp;#34;exitCode&amp;#34;: 137,&#xA;  &amp;#34;startedAt&amp;#34;: &amp;#34;2026-08-22T20:35:12Z&amp;#34;,&#xA;  &amp;#34;finishedAt&amp;#34;: &amp;#34;2026-08-22T20:49:29Z&amp;#34;&#xA;}&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;Not disk this time — 129GB was free. The WAL was the problem:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;% du -sh /prometheus/prometheus-db/wal&#xA;117.7G&#x9;/prometheus/prometheus-db/wal&#xA;% ls /prometheus/prometheus-db/wal | wc -l&#xA;1062&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;A healthy WAL holds about two hours. This one spanned two days, because WAL&#xA;truncation happens at head compaction, and head compaction was OOMKilling&#xA;before it could commit. I caught it mid-attempt once:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;% ls -d /prometheus/prometheus-db/wal/checkpoint.*&#xA;/prometheus/prometheus-db/wal/checkpoint.00188884&#xA;/prometheus/prometheus-db/wal/checkpoint.00189585.tmp&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;That &lt;code&gt;.tmp&lt;/code&gt; never landed. The next check, it was gone and the old checkpoint&#xA;from two days earlier was still the newest one. Each cycle ingested a little&#xA;more than it compacted, so the head grew, and the OOM arrived sooner. 205&#xA;restarts of no net progress.&lt;/p&gt;&#xA;&lt;p&gt;The memory limit had no room to grow either:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;      resources:&#xA;        limits:&#xA;          memory: 14Gi&#xA;        requests:&#xA;          cpu: 1&#xA;          memory: 11Gi&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;14Gi against a &lt;code&gt;Standard_D4ps_v6&lt;/code&gt; with 15.0Gi allocatable. Raising it makes the&#xA;pod unschedulable.&lt;/p&gt;&#xA;&lt;p&gt;So: delete the WAL. Thanos had already shipped every local block, which is what&#xA;made this cheap:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;% cat /prometheus/prometheus-db/thanos.shipper.json&#xA;{&#xA;&#x9;&amp;#34;version&amp;#34;: 1,&#xA;&#x9;&amp;#34;uploaded&amp;#34;: [&#xA;&#x9;&#x9;&amp;#34;01M0NKKGWJVKD3R84S90WW9R7G&amp;#34;,&#xA;&#x9;&#x9;&amp;#34;01M0NMBBC2Y31KMSJRA7VSGYXA&amp;#34;,&#xA;&#x9;&#x9;&amp;#34;01M0NMDM1XRJ0P1MMRBKF5HDV0&amp;#34;&#xA;&#x9;]&#xA;}&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;Scaling the StatefulSet down does not work — prometheus-operator owns it and put&#xA;the replica back within two minutes. The &lt;code&gt;Prometheus&lt;/code&gt; CR is the real lever:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;% kubectl -n monitoring patch prometheus prometheus-kube-prometheus-prometheus \&#xA;    --type merge -p &amp;#39;{&amp;#34;spec&amp;#34;:{&amp;#34;replicas&amp;#34;:0}}&amp;#39;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;Then a scratch pod on the PVC, where I nearly deleted nothing at all:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;% kubectl -n monitoring exec wal-cleanup -- ls /prometheus&#xA;lost&amp;#43;found&#xA;prometheus-db&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;The StatefulSet mounts that volume with a &lt;code&gt;subPath&lt;/code&gt;. Deleting &lt;code&gt;/prometheus/wal&lt;/code&gt;&#xA;would have removed nothing, and the restart would have looked like a fix.&#xA;The data is one level down:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;% kubectl -n monitoring exec wal-cleanup -- \&#xA;    find /prometheus/prometheus-db/wal /prometheus/prometheus-db/chunks_head -mindepth 1 -delete&#xA;% kubectl -n monitoring exec wal-cleanup -- df -h /prometheus | tail -1&#xA;/dev/sda                250.9G      3.8G    247.1G   2% /prometheus&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;Replay after that:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;total_replay_duration=280.16µs&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;Down from ten minutes. Cost: five hours of local samples, everything older still&#xA;queryable through Thanos.&lt;/p&gt;&#xA;&lt;p&gt;Two independent limits, hit in sequence: the volume, then the memory ceiling&#xA;that kept compaction from ever committing. The remaining one is a limit sized at&#xA;93% of the node it runs on, and that needs a bigger node pool.&lt;/p&gt;&#xA;&lt;hr&gt;&#xA;&lt;p&gt;🤖 &lt;em&gt;Drafted with &lt;a href=&#34;https://github.com/thiagowfx/skills/blob/master/plugins/thiagowfx/skills/bloggify/SKILL.md&#34;&gt;&lt;code&gt;/bloggify&lt;/code&gt;&lt;/a&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: prometheus: a 117GB WAL that could not truncate&#34;&gt;email&lt;/a&gt;&lt;/p&gt;&lt;p&gt;&lt;a href=&#34;https://perrotta.dev/tags/bloggify/&#34;&gt;#bloggify&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/kubernetes/&#34;&gt;#kubernetes&lt;/a&gt;&lt;/p&gt;</description>
    </item>
    <item>
      <title>azure disk: resize needs the PVC patch too
      </title>
      <link>https://perrotta.dev/2026/08/azure-disk-resize-needs-the-pvc-patch-too/</link>
      <pubDate>Sat, 22 Aug 2026 22:50:20 +0200</pubDate><author>serendipity@perrotta.dev (Thiago Perrotta)</author>
      <category>bloggify</category>
      <category>dev</category>
      <category>kubernetes</category>
      <guid>https://perrotta.dev/2026/08/azure-disk-resize-needs-the-pvc-patch-too/</guid>
      <description>&lt;p&gt;♠ &lt;strong&gt;Problem statement&lt;/strong&gt;: a Prometheus pod crash-looped on a full 128GiB volume. I&#xA;resized the Azure-managed disk to 256Gi in the Azure Portal (ClickOps) to no&#xA;avail.&lt;/p&gt;&#xA;&lt;p&gt;The disk &lt;em&gt;really&lt;/em&gt; was bigger:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;% az disk show -g my-cluster-k8s-nodes \&#xA;    -n pvc-86ac2c66-fb67-43c3-84b0-7209f3a23bd3 -o json | \&#xA;    jq &amp;#39;{diskSizeGB, diskState, provisioningState}&amp;#39;&#xA;{&#xA;  &amp;#34;diskSizeGB&amp;#34;: 256,&#xA;  &amp;#34;diskState&amp;#34;: &amp;#34;Attached&amp;#34;,&#xA;  &amp;#34;provisioningState&amp;#34;: &amp;#34;Succeeded&amp;#34;&#xA;}&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;Kubernetes disagreed though (well, it didn&amp;rsquo;t know about the resize yet):&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;% kubectl -n monitoring get pvc prometheus-...-prometheus-0 -o json | \&#xA;    jq &amp;#39;{spec: .spec.resources.requests, status: .status.capacity, alloc: .status.allocatedResources}&amp;#39;&#xA;{&#xA;  &amp;#34;spec&amp;#34;: {&amp;#34;storage&amp;#34;: &amp;#34;128Gi&amp;#34;},&#xA;  &amp;#34;status&amp;#34;: {&amp;#34;storage&amp;#34;: &amp;#34;128Gi&amp;#34;},&#xA;  &amp;#34;alloc&amp;#34;: null&#xA;}&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;&lt;code&gt;allocatedResources: null&lt;/code&gt; means no resize was ever requested. Growing the disk&#xA;in Azure doesn&amp;rsquo;t tell the CSI driver anything, so nobody ran &lt;code&gt;resize2fs&lt;/code&gt;, and&#xA;the filesystem stayed exactly where it was:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;% kubectl -n monitoring exec prometheus-...-prometheus-0 -c thanos-sidecar -- df -h /prometheus&#xA;Filesystem                Size      Used Available Use% Mounted on&#xA;/dev/sda                124.9G    123.0G      1.9G  98% /prometheus&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;The fix is to &lt;strong&gt;patch the PVC&lt;/strong&gt; so the control plane catches up.&#xA;&lt;code&gt;disk.csi.azure.com&lt;/code&gt; sees the block device is already large enough, skips the&#xA;Azure API call, and goes straight to the filesystem:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;% kubectl -n monitoring patch pvc prometheus-...-prometheus-0 \&#xA;    -p &amp;#39;{&amp;#34;spec&amp;#34;:{&amp;#34;resources&amp;#34;:{&amp;#34;requests&amp;#34;:{&amp;#34;storage&amp;#34;:&amp;#34;256Gi&amp;#34;}}}}&amp;#39;&#xA;persistentvolumeclaim/prometheus-...-prometheus-0 patched&lt;/code&gt;&lt;/pre&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;% kubectl -n monitoring exec prometheus-...-prometheus-0 -c thanos-sidecar -- df -h /prometheus&#xA;Filesystem                Size      Used Available Use% Mounted on&#xA;/dev/sda                250.9G    124.6G    126.4G  50% /prometheus&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;42MB free became 126.4GB, and the pod went &lt;code&gt;3/3 Running&lt;/code&gt; after 203 crashes.&lt;/p&gt;&#xA;&lt;p&gt;The lesson: patch the PVC and then let CSI follow up.&lt;/p&gt;&#xA;&lt;p&gt;Resizing in Azure first just creates a split state where the volume is bigger&#xA;but nothing can use it.&lt;/p&gt;&#xA;&lt;hr&gt;&#xA;&lt;p&gt;🤖 &lt;em&gt;Drafted with &lt;a href=&#34;https://github.com/thiagowfx/skills/blob/master/plugins/thiagowfx/skills/bloggify/SKILL.md&#34;&gt;&lt;code&gt;/bloggify&lt;/code&gt;&lt;/a&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: azure disk: resize needs the PVC patch too&#34;&gt;email&lt;/a&gt;&lt;/p&gt;&lt;p&gt;&lt;a href=&#34;https://perrotta.dev/tags/bloggify/&#34;&gt;#bloggify&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/kubernetes/&#34;&gt;#kubernetes&lt;/a&gt;&lt;/p&gt;</description>
    </item>
    <item>
      <title>anki: migrate English notes to TTS note types
      </title>
      <link>https://perrotta.dev/2026/08/anki-migrate-english-notes-to-tts-note-types/</link>
      <pubDate>Sat, 22 Aug 2026 13:36:25 +0200</pubDate><author>serendipity@perrotta.dev (Thiago Perrotta)</author>
      <category>anki</category>
      <category>bloggify</category>
      <category>dev</category>
      <category>pkm</category>
      <guid>https://perrotta.dev/2026/08/anki-migrate-english-notes-to-tts-note-types/</guid>
      <description>&lt;p&gt;♠ &lt;a href=&#34;https://perrotta.dev/2026/04/anki-flashcard-generation-with-claude-code/&#34;&gt;Previously&lt;/a&gt;.&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;Problem statement&lt;/strong&gt;: migrate my English 🇬🇧 deck to dedicated note types with&#xA;British English text-to-speech, without adding reverse cards.&lt;/p&gt;&#xA;&lt;p&gt;I created &lt;code&gt;English Language Card 🇬🇧&lt;/code&gt; with four fields (&lt;code&gt;Front&lt;/code&gt;, &lt;code&gt;Front Example&lt;/code&gt;,&#xA;&lt;code&gt;Back&lt;/code&gt;, and &lt;code&gt;Striked&lt;/code&gt;) and only one card template:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-plaintext&#34;&gt;{{Front}} {{tts en_GB:Front}}&#xA;&#xA;{{#Front Example}}&#xA;&amp;lt;br&amp;gt;&#xA;&amp;lt;i&amp;gt;{{Front Example}}&amp;lt;/i&amp;gt; {{tts en_GB:Front Example}}&#xA;{{/Front Example}}&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;&lt;code&gt;English Grammatik Cloze 🇬🇧&lt;/code&gt; uses the same TTS setup for cloze notes:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-plaintext&#34;&gt;{{cloze:Text}} {{tts en_GB:Text}}&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;AnkiConnect could create both types, but did not expose a &lt;code&gt;changeNoteType&lt;/code&gt;&#xA;action. I closed Anki and used its own Python package instead. Before changing&#xA;the collection, I exported the English deck and copied the SQLite database:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-plaintext&#34;&gt;4.8M pre-english-note-type-migration-2026-08-22-13.27.44.apkg&#xA;9.9M pre-english-note-type-migration-2026-08-22-13.29.08.collection.anki2&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;The field and template maps did the migration in place:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-python&#34;&gt;col.models.change(basic, groups[&amp;#34;Basic&amp;#34;], language, {0: 0, 1: 2}, {0: 0})&#xA;col.models.change(cloze, groups[&amp;#34;Cloze&amp;#34;], english_cloze, {0: 0, 1: 1}, None)&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;I rehearsed the migration against the database copy before running it for real:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;% PYTHONPATH=/Applications/Anki.app/Contents/Resources/app_packages \&#xA;  /opt/homebrew/bin/python3.13 /tmp/anki_migrate_english.py \&#xA;  ~/Library/Application\ Support/Anki2/User\ 1/collection.anki2&#xA;{&#xA;  &amp;#34;notes&amp;#34;: 1089,&#xA;  &amp;#34;cards_before&amp;#34;: 1096,&#xA;  &amp;#34;cards_after&amp;#34;: 1089,&#xA;  &amp;#34;language_notes&amp;#34;: 1086,&#xA;  &amp;#34;cloze_notes&amp;#34;: 3,&#xA;  &amp;#34;removed_reverse_cards&amp;#34;: 7,&#xA;  &amp;#34;integrity&amp;#34;: &amp;#34;ok&amp;#34;&#xA;}&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;The script also checked every mapped field, retained card ID, and review-log&#xA;count. After a full upload to AnkiWeb, a normal sync passed.&lt;/p&gt;&#xA;&lt;p&gt;No reverse cards, and all English notes can now speak for themselves (pun&#xA;intended!).&lt;/p&gt;&#xA;&lt;hr&gt;&#xA;&lt;p&gt;🤖 &lt;em&gt;Drafted with &lt;a href=&#34;https://github.com/thiagowfx/skills/blob/master/plugins/thiagowfx/skills/bloggify/SKILL.md&#34;&gt;&lt;code&gt;/bloggify&lt;/code&gt;&lt;/a&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: anki: migrate English notes to TTS note types&#34;&gt;email&lt;/a&gt;&lt;/p&gt;&lt;p&gt;&lt;a href=&#34;https://perrotta.dev/tags/anki/&#34;&gt;#anki&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/bloggify/&#34;&gt;#bloggify&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>hugo: tag-based post connections
      </title>
      <link>https://perrotta.dev/2026/08/hugo-tag-based-post-connections/</link>
      <pubDate>Sat, 22 Aug 2026 13:29:06 +0200</pubDate><author>serendipity@perrotta.dev (Thiago Perrotta)</author>
      <category>bloggify</category>
      <category>coding</category>
      <category>dev</category>
      <category>meta</category>
      <guid>https://perrotta.dev/2026/08/hugo-tag-based-post-connections/</guid>
      <description>&lt;p&gt;♠ &lt;a href=&#34;https://perrotta.dev/2026/08/llm-devise-a-plan/&#34;&gt;Previously&lt;/a&gt;.&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;Problem statement&lt;/strong&gt;: post connections showed only direct links, while posts&#xA;with the same tags remained disconnected in the graph of this blog.&lt;/p&gt;&#xA;&lt;p&gt;I added a second kind of edge. The pre-computation picks up to two posts with&#xA;the &lt;strong&gt;most shared tags&lt;/strong&gt;, breaks ties by date, and excludes direct links:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-python&#34;&gt;def compute_tag_related_posts(&#xA;    post: dict,&#xA;    all_posts: list[dict],&#xA;    outlinks_set: set[str],&#xA;    backlinks_set: set[str],&#xA;    limit: int = 2,&#xA;) -&amp;gt; list[str]:&#xA;    &amp;#34;&amp;#34;&amp;#34;Compute tag connections, excluding posts with direct links.&amp;#34;&amp;#34;&amp;#34;&#xA;    post_tags = set(post[&amp;#34;tags&amp;#34;])&#xA;    if not post_tags:&#xA;        return []&#xA;&#xA;    direct_ids = outlinks_set | backlinks_set&#xA;    scored = []&#xA;    for other in all_posts:&#xA;        other_id = other[&amp;#34;id&amp;#34;]&#xA;        if other_id == post[&amp;#34;id&amp;#34;] or other_id in direct_ids:&#xA;            continue&#xA;&#xA;        shared_tags = post_tags &amp;amp; set(other[&amp;#34;tags&amp;#34;])&#xA;        if shared_tags:&#xA;            scored.append((other_id, len(shared_tags), other[&amp;#34;date&amp;#34;]))&#xA;&#xA;    scored.sort(key=lambda item: (item[1], item[2], item[0]), reverse=True)&#xA;    return [item[0] for item in scored[:limit]]&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;Direct links remain solid and prominent. Shared-tag links use smaller arrows&#xA;with dashed, muted edges:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-css&#34;&gt;.mini-graph .mini-graph-direct-edge {&#xA;  fill: none;&#xA;  stroke: var(--link-color);&#xA;  stroke-width: 2.25;&#xA;  stroke-opacity: 0.9;&#xA;}&#xA;&#xA;.mini-graph .mini-graph-tag-edge {&#xA;  fill: none;&#xA;  stroke: var(--color-gray);&#xA;  stroke-width: 1;&#xA;  stroke-dasharray: 4 4;&#xA;  stroke-opacity: 0.65;&#xA;}&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;The original &lt;a href=&#34;https://perrotta.dev/2026/07/anki-archive-a-deck/&#34;&gt;Anki post&lt;/a&gt; now gets&#xA;one direct link and two tag connections:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-json&#34;&gt;{&#xA;  &amp;#34;outlinks&amp;#34;: [&amp;#34;2024-12-23-anki-workflow&amp;#34;],&#xA;  &amp;#34;tag_related&amp;#34;: [&#xA;    &amp;#34;2026-04-15-anki-flashcards-with-claude-code&amp;#34;,&#xA;    &amp;#34;2026-04-13-anki-api-access&amp;#34;&#xA;  ]&#xA;}&lt;/code&gt;&lt;/pre&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;% python3 ci/test_precompute_links.py&#xA;..&#xA;----------------------------------------------------------------------&#xA;Ran 2 tests in 0.000s&#xA;&#xA;OK&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;There&amp;rsquo;s a limit of 2 shared tag post edges for the sake of not visually&#xA;polluting the presentation.&lt;/p&gt;&#xA;&lt;p&gt;I&amp;rsquo;ve been loving how this little blog is evolving into my beloved (public)&#xA;digital garden.&lt;/p&gt;&#xA;&lt;hr&gt;&#xA;&lt;p&gt;🤖 &lt;em&gt;Drafted with &lt;a href=&#34;https://github.com/thiagowfx/skills/blob/master/plugins/thiagowfx/skills/bloggify/SKILL.md&#34;&gt;&lt;code&gt;/bloggify&lt;/code&gt;&lt;/a&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: hugo: tag-based post connections&#34;&gt;email&lt;/a&gt;&lt;/p&gt;&lt;p&gt;&lt;a href=&#34;https://perrotta.dev/tags/bloggify/&#34;&gt;#bloggify&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/meta/&#34;&gt;#meta&lt;/a&gt;&lt;/p&gt;</description>
    </item>
    <item>
      <title>LLM: Devise a plan
      </title>
      <link>https://perrotta.dev/2026/08/llm-devise-a-plan/</link>
      <pubDate>Sat, 22 Aug 2026 13:07:58 +0200</pubDate><author>serendipity@perrotta.dev (Thiago Perrotta)</author>
      <category>ai</category>
      <category>dev</category>
      <guid>https://perrotta.dev/2026/08/llm-devise-a-plan/</guid>
      <description>&lt;p&gt;♠ There&amp;rsquo;s no need for&#xA;&lt;a href=&#34;https://opencode.ai/docs/#:~:text=OpenCode%20has%20a%20Plan%20mode%20that%20disables%20its%20ability%20to%20make%20changes%20and%20instead%20suggest%20how%20it%E2%80%99ll%20implement%20the%20feature.&#34;&gt;plan&lt;/a&gt;&#xA;&lt;a href=&#34;https://code.claude.com/docs/en/common-workflows#plan-before-editing&#34;&gt;mode&lt;/a&gt;.&lt;/p&gt;&#xA;&lt;p&gt;&lt;a href=&#34;https://perrotta.dev/2026/02/cco-claude-condom-sandbox/&#34;&gt;Sandboxing&lt;/a&gt; + prompt&#xA;engineering is enough.&lt;/p&gt;&#xA;&lt;p&gt;End your prompt with&lt;/p&gt;&#xA;&lt;blockquote&gt;&#xA;&lt;p&gt;Devise a plan&lt;/p&gt;&#xA;&lt;/blockquote&gt;&#xA;&lt;p&gt;&amp;hellip;and let the 2026 Gen AI models flow by themselves.&lt;/p&gt;&#xA;&lt;p&gt;Here&amp;rsquo;a a sample session:&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;User&lt;/strong&gt;:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-plaintext&#34;&gt;some of my blog posts e.g. https://perrotta.dev/2026/07/anki-archive-a-deck/ have&#xA;&amp;#34;connections&amp;#34;&#xA;&#xA;this is great, but it includes _only_ direct links&#xA;&#xA;I wanted to expand connections to include a couple of posts that share tags too&#xA;&#xA;They should be visually distinct from direct links (direct links should stand out better)&#xA;&#xA;*Devise a plan*&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;&lt;strong&gt;Assistant&lt;/strong&gt;:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-plaintext&#34;&gt;[...]&#xA;&#xA;Current Connections graph reads only backlinks and outlinks. Existing related&#xA;data mixes links, tags, categories, and title words. It cannot guarantee two&#xA;tag-based results.&#xA;&#xA; Plan:&#xA;&#xA; 1. Precompute tag connections&#xA;     - Add tag_related to ci/precompute_links.py.&#xA;     - Select up to two posts with shared tags.&#xA;     - Exclude current post, backlinks, and outlinks.&#xA;     - Rank by shared-tag count, then newest date.&#xA;     - Store post IDs in data/links.json.&#xA;&#xA; 2. Extend mini graph&#xA;     - Read tag_related in layouts/partials/mini-graph.html.&#xA;     - Keep incoming links on left.&#xA;     - Keep outgoing links on right.&#xA;     - Place tag connections below current post.&#xA;     - Use no arrows because tag relationships have no direction.&#xA;     - Include shared tag names in each SVG &amp;lt;title&amp;gt;.&#xA;&#xA;[...]&#xA;&#xA; 6. Verify&#xA;     - Add focused tests for ranking, direct-link exclusion, deduplication, and&#xA;       fewer-than-two matches.&#xA;     - Regenerate data/links.json.&#xA;     - Run hugo.&#xA;     - Inspect /2026/07/anki-archive-a-deck/ on desktop and mobile.&#xA;     - Confirm direct Anki workflow link stands out.&#xA;     - Confirm two non-direct Anki/tag posts appear as muted tag connections.&#xA;     - Confirm posts without tags or direct links still render correctly.&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;∎&lt;/p&gt;&lt;p&gt;— § —&lt;/p&gt;&lt;p&gt;Reply via &lt;a href=&#34;mailto:serendipity@perrotta.dev?subject=Reply to: LLM: Devise a plan&#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/dev/&#34;&gt;#dev&lt;/a&gt;&lt;/p&gt;</description>
    </item>
    <item>
      <title>kargo: notify when a soak completes
      </title>
      <link>https://perrotta.dev/2026/08/kargo-notify-when-a-soak-completes/</link>
      <pubDate>Fri, 21 Aug 2026 18:38:23 +0200</pubDate><author>serendipity@perrotta.dev (Thiago Perrotta)</author>
      <category>argocd</category>
      <category>bloggify</category>
      <category>dev</category>
      <category>kubernetes</category>
      <guid>https://perrotta.dev/2026/08/kargo-notify-when-a-soak-completes/</guid>
      <description>&lt;p&gt;♠ &lt;a href=&#34;https://perrotta.dev/2026/08/kargo-cool-down-upstream-releases-with-a-holding-stage/&#34;&gt;Previously&lt;/a&gt;.&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;Problem statement&lt;/strong&gt;: notify a Slack channel whenever Freight finishes soaking in&#xA;an upstream Kargo Stage, without promoting it to the next environment.&lt;/p&gt;&#xA;&lt;p&gt;A soak does not finish a process. Once &lt;code&gt;requiredSoakTime&lt;/code&gt; elapses, Kargo simply&#xA;makes the Freight available downstream. There is no soak-complete event in the&#xA;&lt;a href=&#34;https://docs.kargo.io/user-guide/reference-docs/events/event-reference&#34;&gt;Kargo event list&lt;/a&gt;.&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;Worakround&lt;/strong&gt;: I made availability start a notification-only promotion. Relevant excerpt,&#xA;with variable values omitted:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;metadata:&#xA;  name: notify-remaining-gardens-ready&#xA;spec:&#xA;  requestedFreight:&#xA;    - origin:&#xA;        kind: Warehouse&#xA;        name: argocd&#xA;      sources:&#xA;        stages:&#xA;          - staging-gardens&#xA;        requiredSoakTime: 48h0m0s&#xA;  promotionTemplate:&#xA;    spec:&#xA;      steps:&#xA;        - uses: http&#xA;          config:&#xA;            method: POST&#xA;            url: https://slack.com/api/chat.postMessage&#xA;            responseContentType: application/json&#xA;            successExpression: response.status == 200 &amp;amp;&amp;amp; response.body.ok == true&#xA;            failureExpression: response.status != 200 || response.body.ok != true&#xA;            body: |-&#xA;              ${{ quote({&#xA;                &amp;#34;channel&amp;#34;: vars.slackChannel,&#xA;                &amp;#34;username&amp;#34;: &amp;#34;Kargo&amp;#34;,&#xA;                &amp;#34;icon_emoji&amp;#34;: &amp;#34;:kargo:&amp;#34;,&#xA;                &amp;#34;unfurl_links&amp;#34;: false,&#xA;                &amp;#34;text&amp;#34;: &amp;#34;:white_check_mark: *argocd*: chart &amp;#34; &amp;#43; chartFrom(vars.chartRepo, &amp;#34;argocd&amp;#34;).Version&#xA;                  &amp;#43; &amp;#34; completed 48h of soak in staging-gardens and is ready for promotion to remaining-gardens.&amp;#34;&#xA;                  &amp;#43; &amp;#34;\nFreight: `&amp;#34; &amp;#43; ctx.targetFreight.name &amp;#43; &amp;#34;`&amp;#34;&#xA;              })&#xA;              }}&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;&amp;hellip;wherein remaining-gardens = prod.&lt;/p&gt;&#xA;&lt;p&gt;Only the notification Stage auto-promotes:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;promotionPolicies:&#xA;  - stageSelector:&#xA;      name: notify-remaining-gardens-ready&#xA;    autoPromotionEnabled: true&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;Once the HTTP step succeeds, the Freight occupies this Stage and does not send&#xA;again. The real downstream Stage remains human-gated. A Slack API error fails&#xA;only the notification Promotion, which can be retried.&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;% prek run kargo-promotion-tasks --all-files&#xA;Check Kargo PromotionTask references.....................................Passed&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;This uses Kargo&amp;rsquo;s generic &lt;a href=&#34;https://docs.kargo.io/user-guide/reference-docs/promotion-steps/http&#34;&gt;&lt;code&gt;http&lt;/code&gt;&#xA;step&lt;/a&gt; as&#xA;the notification transport and Kargo itself as the timer.&lt;/p&gt;&#xA;&lt;p&gt;There&amp;rsquo;s no need to enroll into Akuity Platform ($$) just for the sake of&#xA;&lt;a href=&#34;https://docs.kargo.io/user-guide/reference-docs/promotion-steps/send-message&#34;&gt;&lt;code&gt;send-message&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;&#xA;&lt;hr&gt;&#xA;&lt;p&gt;🤖 &lt;em&gt;Drafted with &lt;a href=&#34;https://github.com/thiagowfx/skills/blob/master/plugins/thiagowfx/skills/bloggify/SKILL.md&#34;&gt;&lt;code&gt;/bloggify&lt;/code&gt;&lt;/a&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: kargo: notify when a soak completes&#34;&gt;email&lt;/a&gt;&lt;/p&gt;&lt;p&gt;&lt;a href=&#34;https://perrotta.dev/tags/argocd/&#34;&gt;#argocd&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/bloggify/&#34;&gt;#bloggify&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/kubernetes/&#34;&gt;#kubernetes&lt;/a&gt;&lt;/p&gt;</description>
    </item>
    <item>
      <title>mise: one frontend for tool versions
      </title>
      <link>https://perrotta.dev/2026/08/mise-one-frontend-for-tool-versions/</link>
      <pubDate>Fri, 21 Aug 2026 16:55:45 +0200</pubDate><author>serendipity@perrotta.dev (Thiago Perrotta)</author>
      <category>ai</category>
      <category>coding</category>
      <category>dev</category>
      <guid>https://perrotta.dev/2026/08/mise-one-frontend-for-tool-versions/</guid>
      <description>&lt;p&gt;♠ &lt;a href=&#34;https://perrotta.dev/2026/08/thoughtworks-technology-radar/&#34;&gt;Previously&lt;/a&gt;.&lt;/p&gt;&#xA;&lt;p&gt;Thanks to the ThoughtWorks Technology Radar I finally decided to adopt&#xA;&lt;a href=&#34;https://mise.jdx.dev/&#34;&gt;mise&lt;/a&gt;, so I migrated my dotfiles to use it for every&#xA;programming language runtime:&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;&lt;code&gt;rbenv&lt;/code&gt; (ruby)&lt;/li&gt;&#xA;&lt;li&gt;&lt;code&gt;tfenv&lt;/code&gt; (terraform)&lt;/li&gt;&#xA;&lt;li&gt;&lt;code&gt;nvm&lt;/code&gt; (nodejs)&lt;/li&gt;&#xA;&lt;li&gt;&lt;code&gt;pyenv&lt;/code&gt; (python)&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;A single bespoke declarative file to rule them all:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-toml&#34;&gt;[tools]&#xA;go = &amp;#34;1.27&amp;#34;&#xA;node = &amp;#34;26.7&amp;#34;&#xA;python = &amp;#34;3.14&amp;#34;&#xA;ruby = &amp;#34;3.4&amp;#34;&#xA;rust = &amp;#34;1.97&amp;#34;&#xA;terraform = &amp;#34;1.15&amp;#34;&#xA;terraform-docs = &amp;#34;0.24.0&amp;#34;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;Deleted &lt;code&gt;~/.rbenv&lt;/code&gt; (1.2 GB, three ruby builds) and &lt;code&gt;~/.config/tfenv&lt;/code&gt; (821 MB,&#xA;eight terraform builds). Mise is using 275 MB now. That&amp;rsquo;s a nice cleanup bonus.&lt;/p&gt;&#xA;&lt;p&gt;I should write a dedicated blog post about it if it sticks in my setup. ∎&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: mise: one frontend for tool versions&#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>Google notice of content restriction
      </title>
      <link>https://perrotta.dev/2026/08/google-notice-of-content-restriction/</link>
      <pubDate>Fri, 21 Aug 2026 12:26:12 +0200</pubDate><author>serendipity@perrotta.dev (Thiago Perrotta)</author>
      <category>privacy</category>
      <category>serenity</category>
      <guid>https://perrotta.dev/2026/08/google-notice-of-content-restriction/</guid>
      <description>&lt;p&gt;♠ From: &lt;a href=&#34;mailto:removals@google.com&#34;&gt;removals@google.com&lt;/a&gt;&lt;/p&gt;&#xA;&lt;p&gt;To: me&lt;/p&gt;&#xA;&lt;p&gt;Subject: Google notice of content restriction&lt;/p&gt;&#xA;&lt;blockquote&gt;&#xA;&lt;p&gt;Hello,&lt;/p&gt;&#xA;&lt;p&gt;Your review isn&amp;rsquo;t posted.&lt;/p&gt;&#xA;&lt;p&gt;What happened&lt;/p&gt;&#xA;&lt;p&gt;We received a complaint that your review is defamatory under German law. Your&#xA;review is removed for now, because German courts have set a low threshold for&#xA;businesses to challenge reviews as defamatory. For example, businesses can&#xA;simply claim that they cannot verify that you were a customer (for example,&#xA;they have no record of a transaction) and unless there is evidence to the&#xA;contrary, courts may order platforms to remove the review. To show that you&#xA;were a customer and reinstate your review, use the appeals option below under&#xA;&amp;ldquo;What you can do next&amp;rdquo;.&lt;/p&gt;&#xA;&lt;p&gt;Affected URL(s): {redacted}&lt;/p&gt;&#xA;&lt;p&gt;Thiago ★ {1 star}&lt;/p&gt;&#xA;&lt;p&gt;What you can do next&lt;/p&gt;&#xA;&lt;p&gt;If you think your content should be re-published, we invite you to contest&#xA;this removal decision by providing us with evidence of your interaction with&#xA;this business at this link:&lt;/p&gt;&#xA;&lt;p&gt;&lt;a href=&#34;https://reportcontent.google.com/forms/dsa_appeal&#34;&gt;https://reportcontent.google.com/forms/dsa_appeal&lt;/a&gt;&lt;/p&gt;&#xA;&lt;/blockquote&gt;&#xA;&lt;p&gt;In case you&amp;rsquo;re curious, it was a blank review. I did not write any text at&#xA;all.&lt;/p&gt;&#xA;&lt;p&gt;In other words, a 1-star review without any commentary is apparently deemed&#xA;defamatory. What a great system.&lt;/p&gt;&#xA;&lt;p&gt;This has happened more than once.&lt;/p&gt;&#xA;&lt;p&gt;I can no longer recommend Google Maps as a reliable source for unbiased reviews.&lt;/p&gt;&#xA;&lt;p&gt;It is certainly a reliable proxy for &lt;em&gt;popularity&lt;/em&gt;, but that&amp;rsquo;s all.&lt;/p&gt;&#xA;&lt;p&gt;Some businesses take advantage of the situation to attempt to take down bad&#xA;reviews, and they&amp;rsquo;re often successful while at it. Therefore you cannot trust&#xA;the leftover glamourous reviews.&lt;/p&gt;&#xA;&lt;p&gt;I stopped using Yelp many years ago for the same reason. ∎&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: Google notice of content restriction&#34;&gt;email&lt;/a&gt;&lt;/p&gt;&lt;p&gt;&lt;a href=&#34;https://perrotta.dev/tags/privacy/&#34;&gt;#privacy&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/serenity/&#34;&gt;#serenity&lt;/a&gt;&lt;/p&gt;</description>
    </item>
    <item>
      <title>Terminals
      </title>
      <link>https://perrotta.dev/2026/08/terminals/</link>
      <pubDate>Thu, 20 Aug 2026 18:04:21 +0200</pubDate><author>serendipity@perrotta.dev (Thiago Perrotta)</author>
      <category>ai</category>
      <category>dev</category>
      <category>serenity</category>
      <guid>https://perrotta.dev/2026/08/terminals/</guid>
      <description>&lt;p&gt;♠ Today I found myself with 13 open terminals, more than half of them running&#xA;&lt;a href=&#34;https://pi.dev/&#34;&gt;Pi&lt;/a&gt; or &lt;a href=&#34;https://claude.ai/code&#34;&gt;Claude Code&lt;/a&gt;, the rest with&#xA;various projects and explorations split amongst work and personal affairs.&lt;/p&gt;&#xA;&lt;p&gt;13!!&lt;/p&gt;&#xA;&lt;p&gt;&lt;a href=&#34;https://en.wikipedia.org/wiki/Information_overload&#34;&gt;This&lt;/a&gt;&#xA;&lt;a href=&#34;https://en.wikipedia.org/wiki/Decision_fatigue&#34;&gt;is&lt;/a&gt;&#xA;&lt;a href=&#34;https://en.wikipedia.org/wiki/Cognitive_load&#34;&gt;not&lt;/a&gt;&#xA;&lt;a href=&#34;https://en.wikipedia.org/wiki/Continuous_partial_attention&#34;&gt;a&lt;/a&gt; matter of pride.&lt;/p&gt;&#xA;&lt;p&gt;I need to &lt;a href=&#34;https://mariozechner.at/posts/2026-03-25-thoughts-on-slowing-the-fuck-down/&#34;&gt;take a&#xA;break&lt;/a&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: Terminals&#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/dev/&#34;&gt;#dev&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/serenity/&#34;&gt;#serenity&lt;/a&gt;&lt;/p&gt;</description>
    </item>
    <item>
      <title>ThoughtWorks Technology Radar
      </title>
      <link>https://perrotta.dev/2026/08/thoughtworks-technology-radar/</link>
      <pubDate>Thu, 20 Aug 2026 13:27:08 +0200</pubDate><author>serendipity@perrotta.dev (Thiago Perrotta)</author>
      <category>dev</category>
      <category>serenity</category>
      <guid>https://perrotta.dev/2026/08/thoughtworks-technology-radar/</guid>
      <description>&lt;p&gt;♠ &lt;strong&gt;TIL&lt;/strong&gt;: The &lt;a href=&#34;https://www.thoughtworks.com/en-us/radar&#34;&gt;ThoughtWorks Technology&#xA;Radar&lt;/a&gt; publishes an yearly&lt;sup id=&#34;fnref:1&#34;&gt;&lt;a href=&#34;https://perrotta.dev/2026/08/thoughtworks-technology-radar/#fn:1&#34; class=&#34;footnote-ref&#34; role=&#34;doc-noteref&#34;&gt;1&lt;/a&gt;&lt;/sup&gt; opinionated&#xA;report about the technology landscape today.&lt;/p&gt;&#xA;&lt;p&gt;It is a rich source of inspiration to identify techniques, tools, platforms&#xA;and languages &amp;amp; frameworks to assess and/or adopt.&lt;/p&gt;&#xA;&lt;p&gt;They also publish PDFs and keep &lt;a href=&#34;https://www.thoughtworks.com/en-us/radar/archive&#34;&gt;historical&#xA;editions&lt;/a&gt; available.&lt;/p&gt;&#xA;&lt;p&gt;Too bad there isn&amp;rsquo;t a RSS feed though, not that I could find.&lt;/p&gt;&#xA;&lt;p&gt;That&amp;rsquo;s what we have &lt;a href=&#34;https://kill-the-newsletter.com/&#34;&gt;Kill the Newsletter&lt;/a&gt; for,&#xA;right? ∎&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;Actually, biyearly (=twice a year).&amp;#160;&lt;a href=&#34;https://perrotta.dev/2026/08/thoughtworks-technology-radar/#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: ThoughtWorks Technology Radar&#34;&gt;email&lt;/a&gt;&lt;/p&gt;&lt;p&gt;&lt;a href=&#34;https://perrotta.dev/tags/dev/&#34;&gt;#dev&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/serenity/&#34;&gt;#serenity&lt;/a&gt;&lt;/p&gt;</description>
    </item>
    <item>
      <title>★ human voice
      </title>
      <link>https://perrotta.dev/2026/08/human-voice/</link>
      <pubDate>Wed, 19 Aug 2026 16:24:06 +0200</pubDate><author>serendipity@perrotta.dev (Thiago Perrotta)</author>
      <category>ai</category>
      <category>bestof</category>
      <category>dev</category>
      <category>serenity</category>
      <guid>https://perrotta.dev/2026/08/human-voice/</guid>
      <description>&lt;p&gt;♠ More and more I&amp;rsquo;ve been resorting to LLMs / Gen AI to file feature requests or&#xA;issues/bugs in upstream repositories.&lt;/p&gt;&#xA;&lt;p&gt;Sometimes I get completely ignored, even when I follow the project guidelines.&lt;/p&gt;&#xA;&lt;p&gt;For example, last week I filed &lt;a href=&#34;https://github.com/earendil-works/pi/issues/7070&#34;&gt;this&#xA;issue&lt;/a&gt; in&#xA;&lt;a href=&#34;https://pi.dev&#34;&gt;Pi&lt;/a&gt;. It got auto-closed. I know&#xA;&lt;a href=&#34;https://github.com/earendil-works/pi/blob/main/CONTRIBUTING.md&#34;&gt;why&lt;/a&gt; it was&#xA;auto-closed, but it&amp;rsquo;s lame, because I followed their guidelines. In spite of&#xA;writing it in my own voice, it got lost in the sea of slop issues. I can&amp;rsquo;t blame&#xA;the maintainers, I totally understandand and sympathize with them. Yet, it sucks&#xA;for me&lt;sup id=&#34;fnref:1&#34;&gt;&lt;a href=&#34;https://perrotta.dev/2026/08/human-voice/#fn:1&#34; class=&#34;footnote-ref&#34; role=&#34;doc-noteref&#34;&gt;1&lt;/a&gt;&lt;/sup&gt;.&lt;/p&gt;&#xA;&lt;p&gt;In other words: even when I put in the effort to file an issue manually, without&#xA;using an LLM, it gets mistaken for an LLM slop issue, because that&amp;rsquo;s what&#xA;everybody else is doing.&lt;/p&gt;&#xA;&lt;p&gt;Which kind of incentives does this create?&lt;/p&gt;&#xA;&lt;p&gt;→ There&amp;rsquo;s little reason to put in the effort to write issues in my own voice.&lt;/p&gt;&#xA;&lt;p&gt;Which is, ironically, the opposite of what the project maintainers seemingly&#xA;hoped to accomplish (=to attract non-clanker contributors that file high-quality&#xA;issues).&lt;/p&gt;&#xA;&lt;p&gt;So now I am pivoting.&lt;/p&gt;&#xA;&lt;p&gt;I&amp;rsquo;ll shamelessly create issues (or feature requests) with an LLM. I&amp;rsquo;ll review&#xA;the wording first, but won&amp;rsquo;t attempt to write them in my own voice. Would it&#xA;even matter? Maintainers are too busy and overwhelmed to be able to distinguish&#xA;slop from human voice, which is ironical — LLMs were supposed to &lt;em&gt;improve&lt;/em&gt; WLB,&#xA;not to make it worse. Oh well.&lt;/p&gt;&#xA;&lt;p&gt;That said, pure Gen AI is lame. So I am starting to complement with a follow-up&#xA;comment in my own voice.&lt;/p&gt;&#xA;&lt;p&gt;Today&amp;rsquo;s example: &lt;a href=&#34;https://github.com/angristan/fast-resume/issues/87#issuecomment-5343487168&#34;&gt;angristan/fast-resume#87&lt;/a&gt;:&lt;/p&gt;&#xA;&lt;blockquote&gt;&#xA;&lt;p&gt;(human voice) &lt;strong&gt;Disclaimer&lt;/strong&gt;: the previous comment was created by an LLM, I&#xA;reviewed its text though. I can reliably reproduce this locally. I&amp;rsquo;ve been&#xA;using &lt;code&gt;fr&lt;/code&gt; &lt;a href=&#34;https://perrotta.dev/2026/05/fast-resume-search-coding-agent-sessions/&#34;&gt;for a&#xA;while&lt;/a&gt;&#xA;now, it&amp;rsquo;s very handy. This is the only use case that is getting on my way.&lt;/p&gt;&#xA;&lt;p&gt;Let me know if you would like help to implement/address it. Thanks!&lt;/p&gt;&#xA;&lt;/blockquote&gt;&#xA;&lt;p&gt;Check out &lt;a href=&#34;https://perrotta.dev/2026/05/fast-resume-search-coding-agent-sessions/&#34;&gt;fast-resume&lt;/a&gt; if you haven&amp;rsquo;t&#xA;yet. It&amp;rsquo;s awesome.&lt;/p&gt;&#xA;&lt;p&gt;I think this is a good compromise. ∎&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;And, frankly, for them too. I no longer feel motivated to file issues for&#xA;Pi, even though I&amp;rsquo;m becoming a heavy user of it.&amp;#160;&lt;a href=&#34;https://perrotta.dev/2026/08/human-voice/#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: human voice&#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/dev/&#34;&gt;#dev&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/serenity/&#34;&gt;#serenity&lt;/a&gt;&lt;/p&gt;</description>
    </item>
    <item>
      <title>★ /ship-and-burn a pull request
      </title>
      <link>https://perrotta.dev/2026/08/ship-and-burn-a-pull-request/</link>
      <pubDate>Wed, 19 Aug 2026 12:02:31 +0200</pubDate><author>serendipity@perrotta.dev (Thiago Perrotta)</author>
      <category>ai</category>
      <category>bestof</category>
      <category>coding</category>
      <category>dev</category>
      <category>git</category>
      <guid>https://perrotta.dev/2026/08/ship-and-burn-a-pull-request/</guid>
      <description>&lt;p&gt;♠ &lt;strong&gt;Problem statement&lt;/strong&gt;: ship a simple change (or commit, or pull request: you&#xA;name it), then wait for green CI, then remove its local branch and worktree&#xA;(=clean up).&lt;/p&gt;&#xA;&lt;p&gt;It&amp;rsquo;s 2026, so we&amp;rsquo;ll codify this workflow in the form of an &lt;a href=&#34;https://skills.md/&#34;&gt;agent&#xA;skill&lt;/a&gt;.&lt;/p&gt;&#xA;&lt;p&gt;I created&#xA;&lt;a href=&#34;https://github.com/thiagowfx/skills/blob/master/plugins/thiagowfx/skills/ship-and-burn/SKILL.md&#34;&gt;&lt;code&gt;/ship-and-burn&lt;/code&gt;&lt;/a&gt;&#xA;for that.&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-plaintext&#34;&gt;/ship-and-burn [reviewer]&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;It&amp;rsquo;s very important that you picture &lt;a href=&#34;https://www.imdb.com/title/tt0887883/&#34;&gt;this&#xA;movie&lt;/a&gt; whenever you invoke this skill.&lt;/p&gt;&#xA;&lt;p&gt;The structure is the following, composing various of my other &lt;a href=&#34;https://github.com/thiagowfx/skills&#34;&gt;skills&lt;/a&gt;:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-plaintext&#34;&gt;/ship ──&amp;gt; pull request ──&amp;gt; /pr-pass ──&amp;gt; green ──&amp;gt; delete local state&#xA;                                  └──&amp;gt; blocked ──&amp;gt; keep local state&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;The &lt;em&gt;implementation&lt;/em&gt; is a single &lt;code&gt;SKILL.md&lt;/code&gt; Markdown file (naturally!):&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;% git show --stat --oneline 8e0eed7&#xA;8e0eed7 feat: add ship-and-burn skill&#xA;&#xA; README.md                                       |  1 &amp;#43;&#xA; package.json                                    |  2 &amp;#43;-&#xA; plugins/thiagowfx/.claude-plugin/plugin.json    |  2 &amp;#43;-&#xA; plugins/thiagowfx/skills/ship-and-burn/SKILL.md | 67 &amp;#43;&amp;#43;&amp;#43;&amp;#43;&amp;#43;&amp;#43;&amp;#43;&amp;#43;&amp;#43;&amp;#43;&amp;#43;&amp;#43;&amp;#43;&amp;#43;&amp;#43;&amp;#43;&amp;#43;&amp;#43;&amp;#43;&amp;#43;&amp;#43;&amp;#43;&amp;#43;&amp;#43;&amp;#43;&#xA; 4 files changed, 70 insertions(&amp;#43;), 2 deletions(-)&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;The skill stops before cleanup whenever CI remains failed or pending 🔴.&lt;/p&gt;&#xA;&lt;p&gt;Once checks pass 🟢, it verifies that the worktree/checkout is clean and that&#xA;every local commit exists on the remote:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;test -z &amp;#34;$(git status --porcelain)&amp;#34;&#xA;git merge-base --is-ancestor HEAD &amp;#34;origin/$branch&amp;#34;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;For a secondary (=non-base) worktree, &lt;a href=&#34;https://github.com/adamtabrams/wt&#34;&gt;&lt;code&gt;wt&lt;/code&gt;&lt;/a&gt;&#xA;removes both the worktree and its local branch:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;cd &amp;#34;$primary&amp;#34;&#xA;wt del -- --foreground &amp;#34;$branch&amp;#34;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;The pull request and remote branch remain intact.&lt;/p&gt;&#xA;&lt;p&gt;Should review comments arrive later, my &lt;code&gt;gh co&lt;/code&gt; (or &lt;code&gt;wt co&lt;/code&gt; for a separate&#xA;worktree) alias restores the branch and worktree before I address them:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-plaintext&#34;&gt;% gh co &amp;lt;pull-request&amp;gt;&#xA;% /address-pr-comments&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;This surely feels like living in the future!&lt;/p&gt;&#xA;&lt;p&gt;And, when using a light harness like &lt;a href=&#34;https://pi.dev&#34;&gt;pi&lt;/a&gt;, it&amp;rsquo;s super fast! ∎&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: /ship-and-burn a pull request&#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;a href=&#34;https://perrotta.dev/tags/git/&#34;&gt;#git&lt;/a&gt;&lt;/p&gt;</description>
    </item>
    <item>
      <title>★ kubernetes: force-refresh a cached multi-architecture image
      </title>
      <link>https://perrotta.dev/2026/08/kubernetes-force-refresh-a-cached-multi-architecture-image/</link>
      <pubDate>Wed, 19 Aug 2026 11:22:26 +0200</pubDate><author>serendipity@perrotta.dev (Thiago Perrotta)</author>
      <category>argocd</category>
      <category>aws</category>
      <category>bestof</category>
      <category>bloggify</category>
      <category>dev</category>
      <category>docker</category>
      <category>kubernetes</category>
      <guid>https://perrotta.dev/2026/08/kubernetes-force-refresh-a-cached-multi-architecture-image/</guid>
      <description>&lt;p&gt;♠ &lt;a href=&#34;https://perrotta.dev/2024/12/skopeo-operate-container-images-and-registries/&#34;&gt;Previously&lt;/a&gt;.&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;Problem statement&lt;/strong&gt;: a Kubernetes ARM64 node kept running an AMD64 image after&#xA;I replaced the registry tag with a multi-architecture image via &lt;code&gt;skopeo&lt;/code&gt;, due to&#xA;image caching.&lt;/p&gt;&#xA;&lt;p&gt;I first mirrored only the AMD64 variant into AWS ECR (note: no &lt;code&gt;--all&lt;/code&gt;):&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;% skopeo sync \&#xA;  --dest-creds &amp;#34;AWS:$(aws ecr get-login-password --region cn-north-1)&amp;#34; \&#xA;  --src docker --dest docker \&#xA;  --override-os linux --override-arch amd64 \&#xA;  quay.io/argoproj/argocd:v3.5.0 \&#xA;  &amp;#34;$REGISTRY/quay.io/argoproj&amp;#34;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;That image failed on the ARM64 node:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-plaintext&#34;&gt;exec /usr/local/bin/argocd: exec format error&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;The source has four platform manifests, so I replaced the ECR tag with the full&#xA;manifest list (&lt;code&gt;-all&lt;/code&gt;&lt;sup id=&#34;fnref:1&#34;&gt;&lt;a href=&#34;https://perrotta.dev/2026/08/kubernetes-force-refresh-a-cached-multi-architecture-image/#fn:1&#34; class=&#34;footnote-ref&#34; role=&#34;doc-noteref&#34;&gt;1&lt;/a&gt;&lt;/sup&gt;):&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;% skopeo sync \&#xA;  --all \&#xA;  --dest-creds &amp;#34;AWS:$(aws ecr get-login-password --region cn-north-1)&amp;#34; \&#xA;  --src docker --dest docker \&#xA;  quay.io/argoproj/argocd:v3.5.0 \&#xA;  &amp;#34;$REGISTRY/quay.io/argoproj&amp;#34;&lt;/code&gt;&lt;/pre&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-plaintext&#34;&gt;linux  amd64    sha256:521d6b62ecd0434c9cc6e9242a74f0e1137bb8fc0026b2c483ea88f3f17e725d&#xA;linux  arm64    sha256:8a14921f7e5dc3408111714f05bff68e337d459653c45f0aea738330d156079b&#xA;linux  s390x    sha256:28743e713eabeeaacc103d177fc0b651b564d10309839077b0381ca980055c7d&#xA;linux  ppc64le  sha256:1f9e154e0eef6df29b75948b0b54dfc8ec490c657100df11a24c34197b9beb3a&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;The sync is complete.&lt;/p&gt;&#xA;&lt;p&gt;Deleting the failed pod did not help though. Its replacement still used the&#xA;cached AMD64 child manifest:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-plaintext&#34;&gt;pod: pod/argocd-redis-secret-init-qgnqr&#xA;node: ip-10-25-54-66.cn-north-1.compute.internal&#xA;arch: arm64&#xA;image ID: $REGISTRY/quay.io/argoproj/argocd@sha256:521d6b62ecd0434c9cc6e9242a74f0e1137bb8fc0026b2c483ea88f3f17e725d&#xA;pull policy: IfNotPresent&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;The Job pod template is immutable, so changing its pull policy in place (to&#xA;&lt;code&gt;Always&lt;/code&gt;) also failed. Instead, I created a temporary pod on the same node. It&#xA;forced a pull and ran a harmless command that proved the selected binary can&#xA;be executed:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion: v1&#xA;kind: Pod&#xA;metadata:&#xA;  name: argocd-image-refresh&#xA;  namespace: argocd&#xA;spec:&#xA;  nodeName: ip-10-0-0-42.cn-north-1.compute.internal&#xA;  restartPolicy: Never&#xA;  tolerations:&#xA;    - key: arch&#xA;      operator: Exists&#xA;      effect: NoSchedule&#xA;    - key: arch&#xA;      operator: Exists&#xA;      effect: NoExecute&#xA;  containers:&#xA;    - name: refresh&#xA;      image: $REGISTRY/quay.io/argoproj/argocd:v3.5.0&#xA;      imagePullPolicy: Always  # This is the important part!&#xA;      command: [&amp;#34;/usr/local/bin/argocd&amp;#34;]&#xA;      args: [&amp;#34;version&amp;#34;, &amp;#34;--client&amp;#34;]&lt;/code&gt;&lt;/pre&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-plaintext&#34;&gt;NAME                   READY   STATUS      RESTARTS&#xA;argocd-image-refresh   0/1     Completed   0&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;The node cache now pointed at the multi-architecture manifest list:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-plaintext&#34;&gt;$REGISTRY/quay.io/argoproj/argocd@sha256:c298cedbaeb31532ba8d4e9904eba9e4987e067293fbd86400c5194e78f743d5&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;I removed the temporary pod and the failed Job pod. The Job controller created&#xA;a replacement, and the ARM64 binary started normally.&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;% kubectl -n argocd delete pod argocd-image-refresh&#xA;% kubectl -n argocd delete pod -l job-name=argocd-redis-secret-init&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;hr&gt;&#xA;&lt;p&gt;🤖 &lt;em&gt;Drafted with &lt;a href=&#34;https://github.com/thiagowfx/skills/blob/master/plugins/thiagowfx/skills/bloggify/SKILL.md&#34;&gt;&lt;code&gt;/bloggify&lt;/code&gt;&lt;/a&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;Why isn&amp;rsquo;t this the default?!&amp;#160;&lt;a href=&#34;https://perrotta.dev/2026/08/kubernetes-force-refresh-a-cached-multi-architecture-image/#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: kubernetes: force-refresh a cached multi-architecture image&#34;&gt;email&lt;/a&gt;&lt;/p&gt;&lt;p&gt;&lt;a href=&#34;https://perrotta.dev/tags/argocd/&#34;&gt;#argocd&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/aws/&#34;&gt;#aws&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/bloggify/&#34;&gt;#bloggify&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/docker/&#34;&gt;#docker&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/kubernetes/&#34;&gt;#kubernetes&lt;/a&gt;&lt;/p&gt;</description>
    </item>
    <item>
      <title>kargo: cool down upstream releases with a holding stage
      </title>
      <link>https://perrotta.dev/2026/08/kargo-cool-down-upstream-releases-with-a-holding-stage/</link>
      <pubDate>Wed, 19 Aug 2026 01:36:27 +0200</pubDate><author>serendipity@perrotta.dev (Thiago Perrotta)</author>
      <category>argocd</category>
      <category>bloggify</category>
      <category>dev</category>
      <category>kubernetes</category>
      <category>security</category>
      <guid>https://perrotta.dev/2026/08/kargo-cool-down-upstream-releases-with-a-holding-stage/</guid>
      <description>&lt;p&gt;♠ &lt;strong&gt;Problem statement&lt;/strong&gt;: wait before opening an upstream Helm chart upgrade PR,&#xA;without putting a timer in the PR workflow.&lt;/p&gt;&#xA;&lt;p&gt;In other words: implement &lt;a href=&#34;https://blog.yossarian.net/2025/12/13/cooldowns-redux&#34;&gt;dependency&#xA;cooldowns&lt;/a&gt;&lt;sup id=&#34;fnref:1&#34;&gt;&lt;a href=&#34;https://perrotta.dev/2026/08/kargo-cool-down-upstream-releases-with-a-holding-stage/#fn:1&#34; class=&#34;footnote-ref&#34; role=&#34;doc-noteref&#34;&gt;1&lt;/a&gt;&lt;/sup&gt; in Kargo.&lt;/p&gt;&#xA;&lt;p&gt;A &lt;a href=&#34;https://kargo.io/&#34;&gt;Kargo&lt;/a&gt; &lt;code&gt;Warehouse&lt;/code&gt; makes newly discovered &lt;code&gt;Freight&lt;/code&gt;&#xA;immediately available to a Stage that accepts it directly:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-plaintext&#34;&gt;Warehouse ──immediate──&amp;gt; open-upgrade-pr ──&amp;gt; PR&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;Kargo already has&#xA;&lt;a href=&#34;https://docs.kargo.io/user-guide/how-to-guides/verification/#soak-times&#34;&gt;&lt;code&gt;requiredSoakTime&lt;/code&gt;&lt;/a&gt;,&#xA;but only between Stages. It cannot delay a direct Warehouse-to-Stage promotion.&#xA;So add a Stage whose promotion deliberately does nothing:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;apiVersion: kargo.akuity.io/v1alpha1&#xA;kind: Stage&#xA;metadata:&#xA;  name: hold-upstream-release&#xA;spec:&#xA;  requestedFreight:&#xA;    - origin:&#xA;        kind: Warehouse&#xA;        name: external-secrets-upstream&#xA;      sources:&#xA;        direct: true&#xA;  promotionTemplate:&#xA;    spec:&#xA;      steps:&#xA;        - uses: compose-output&#xA;          config:&#xA;            state: holding&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;The PR Stage accepts Freight from that holding Stage and starts only after the&#xA;cool-off period:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-yaml&#34;&gt;spec:&#xA;  requestedFreight:&#xA;    - origin:&#xA;        kind: Warehouse&#xA;        name: external-secrets-upstream&#xA;      sources:&#xA;        stages:&#xA;          - hold-upstream-release&#xA;        requiredSoakTime: 72h0m0s&#xA;  promotionTemplate:&#xA;    # clone, update chart dependency, push, and open PR&lt;/code&gt;&lt;/pre&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-plaintext&#34;&gt;Warehouse ──&amp;gt; hold-upstream-release ──72h──&amp;gt; open-upgrade-pr ──&amp;gt; PR&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;The holding Stage must auto-promote, as must the PR Stage after its soak. A new&#xA;release replaces the current Freight in the holding Stage. If the old release&#xA;did not complete 72 hours, it does not advance. This makes the hold a stability&#xA;window: only a release that remains current for three days opens a PR.&lt;/p&gt;&#xA;&lt;p&gt;Explicit Freight approval remains the escape hatch because it bypasses the soak.&lt;/p&gt;&#xA;&lt;hr&gt;&#xA;&lt;p&gt;🤖 &lt;em&gt;Drafted with &lt;a href=&#34;https://github.com/thiagowfx/skills/blob/master/plugins/thiagowfx/skills/bloggify/SKILL.md&#34;&gt;&lt;code&gt;/bloggify&lt;/code&gt;&lt;/a&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;cooldown or cool-off?&amp;#160;&lt;a href=&#34;https://perrotta.dev/2026/08/kargo-cool-down-upstream-releases-with-a-holding-stage/#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: kargo: cool down upstream releases with a holding stage&#34;&gt;email&lt;/a&gt;&lt;/p&gt;&lt;p&gt;&lt;a href=&#34;https://perrotta.dev/tags/argocd/&#34;&gt;#argocd&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/bloggify/&#34;&gt;#bloggify&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/kubernetes/&#34;&gt;#kubernetes&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>git: intent-to-add breaks autostash
      </title>
      <link>https://perrotta.dev/2026/08/git-intent-to-add-breaks-autostash/</link>
      <pubDate>Wed, 19 Aug 2026 01:09:07 +0200</pubDate><author>serendipity@perrotta.dev (Thiago Perrotta)</author>
      <category>bloggify</category>
      <category>coding</category>
      <category>git</category>
      <guid>https://perrotta.dev/2026/08/git-intent-to-add-breaks-autostash/</guid>
      <description>&lt;p&gt;♠ &lt;strong&gt;Problem statement&lt;/strong&gt;: &lt;code&gt;git pull&lt;/code&gt; could not autostash files added with&#xA;&lt;code&gt;git add -N&lt;/code&gt;.&lt;/p&gt;&#xA;&lt;p&gt;I had local work and was nine commits behind &lt;code&gt;origin/master&lt;/code&gt;:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;% git pull --force&#xA;Updating 266a5af537..07e4551075&#xA;error: Entry &amp;#39;ci/goodreads_to_reading.py&amp;#39; not uptodate. Cannot merge.&#xA;Cannot save the current worktree state&#xA;fatal: Cannot autostash&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;&lt;code&gt;--force&lt;/code&gt; does not replace local worktree files. My global Git configuration&#xA;makes &lt;code&gt;git pull&lt;/code&gt; run &lt;code&gt;rebase.autostash&lt;/code&gt;, which found an intent-to-add entry:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;% git status --porcelain=v2&#xA;1 .M N... 100755 100755 100755 fcc319233ddf8e000ad2f735dbc2b0d760f467e5 fcc319233ddf8e000ad2f735dbc2b0d760f467e5 Justfile&#xA;1 .A N... 000000 000000 100755 0000000000000000000000000000000000000000 0000000000000000000000000000000000000000 ci/goodreads_to_reading.py&#xA;1 .M N... 100644 100644 100644 2787100c3968766ea3a84e7c28e28b5046d1da32 2787100c3968766ea3a84e7c28e28b5046d1da32 config/_default/config.yml&#xA;1 .A N... 000000 000000 100644 0000000000000000000000000000000000000000 0000000000000000000000000000000000000000 content/reading.md&#xA;1 .A N... 000000 000000 100644 0000000000000000000000000000000000000000 0000000000000000000000000000000000000000 data/reading.yaml&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;The &lt;code&gt;.A&lt;/code&gt; records have an empty index object (&lt;code&gt;0000000&lt;/code&gt;), so Git has tracked&#xA;path metadata without file content to stash.&lt;/p&gt;&#xA;&lt;p&gt;&lt;code&gt;git reset&lt;/code&gt; removes intent-to-add entries and leaves worktree files alone:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;% git reset&#xA;% git pull&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;hr&gt;&#xA;&lt;p&gt;🤖 &lt;em&gt;Drafted with &lt;a href=&#34;https://github.com/thiagowfx/skills/blob/master/plugins/thiagowfx/skills/bloggify/SKILL.md&#34;&gt;&lt;code&gt;/bloggify&lt;/code&gt;&lt;/a&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: git: intent-to-add breaks autostash&#34;&gt;email&lt;/a&gt;&lt;/p&gt;&lt;p&gt;&lt;a href=&#34;https://perrotta.dev/tags/bloggify/&#34;&gt;#bloggify&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/git/&#34;&gt;#git&lt;/a&gt;&lt;/p&gt;</description>
    </item>
    <item>
      <title>pi: handing off to a focused session
      </title>
      <link>https://perrotta.dev/2026/08/pi-handing-off-to-a-focused-session/</link>
      <pubDate>Wed, 19 Aug 2026 01:05:33 +0200</pubDate><author>serendipity@perrotta.dev (Thiago Perrotta)</author>
      <category>ai</category>
      <category>bloggify</category>
      <category>coding</category>
      <category>dev</category>
      <category>pi</category>
      <guid>https://perrotta.dev/2026/08/pi-handing-off-to-a-focused-session/</guid>
      <description>&lt;p&gt;♠ &lt;a href=&#34;https://perrotta.dev/2026/08/pi-fork-vs-clone-sessions/&#34;&gt;Previously&lt;/a&gt;.&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;Disclaimer&lt;/strong&gt;: This was a failed experiment.&#xA;I am including it here anyway, for completeness.&lt;/p&gt;&#xA;&lt;h2 id=&#34;handoff&#34;&gt;&#xA;  Handoff&#xA;  &lt;a class=&#34;heading-anchor&#34; href=&#34;https://perrotta.dev/2026/08/pi-handing-off-to-a-focused-session/#handoff&#34; aria-label=&#34;Link to this section&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h2&gt;&#xA;&lt;p&gt;&lt;strong&gt;Problem statement&lt;/strong&gt;: move a running Pi task into a fresh, focused session&#xA;without losing the way back.&lt;/p&gt;&#xA;&lt;p&gt;I wanted &lt;a href=&#34;https://ampcode.com/&#34;&gt;Amp&lt;/a&gt;&amp;rsquo;s handoff workflow in Pi, so I vendored the&#xA;&lt;code&gt;handoff&lt;/code&gt; and &lt;code&gt;session_query&lt;/code&gt; pieces of&#xA;&lt;a href=&#34;https://github.com/pasky/pi-amplike&#34;&gt;&lt;code&gt;pasky/pi-amplike&lt;/code&gt;&lt;/a&gt; as a local package:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;% git show --stat --oneline 77af167&#xA;77af167 Add Amp-like handoff package&#xA; pi/.pi/README.md                                   |   2 &amp;#43;&#xA; pi/.pi/agent/local/handoff-amp/LICENSE             |  21 &amp;#43;&#xA; pi/.pi/agent/local/handoff-amp/README.md           |  28 &amp;#43;&amp;#43;&#xA; .../agent/local/handoff-amp/extensions/handoff.ts  | 553 &amp;#43;&amp;#43;&amp;#43;&amp;#43;&amp;#43;&amp;#43;&amp;#43;&amp;#43;&amp;#43;&amp;#43;&amp;#43;&amp;#43;&amp;#43;&amp;#43;&amp;#43;&amp;#43;&amp;#43;&amp;#43;&amp;#43;&amp;#43;&amp;#43;&#xA; .../local/handoff-amp/extensions/lib/mode-utils.ts | 127 &amp;#43;&amp;#43;&amp;#43;&amp;#43;&amp;#43;&#xA; .../local/handoff-amp/extensions/session-query.ts  | 198 &amp;#43;&amp;#43;&amp;#43;&amp;#43;&amp;#43;&amp;#43;&amp;#43;&amp;#43;&#xA; pi/.pi/agent/local/handoff-amp/package.json        |  17 &amp;#43;&#xA; .../handoff-amp/skills/session-query/SKILL.md      |  35 &amp;#43;&amp;#43;&#xA; pi/.pi/agent/settings.json                         |   1 &amp;#43;&#xA; 9 files changed, 982 insertions(&amp;#43;)&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;From the editor, &lt;code&gt;/handoff&lt;/code&gt; accepts a goal and optional mode or model:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-plaintext&#34;&gt;/handoff execute phase one of the plan&#xA;/handoff -mode rush execute phase one of the plan&#xA;/handoff -model anthropic/claude-haiku-4-5 check other places that need this fix&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;The extension serializes the current branch, asks the model for a focused&#xA;context summary, and includes the source session path in the new prompt:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-typescript&#34;&gt;// ~/.pi/agent/local/handoff-amp/extensions/handoff.ts&#xA;const currentSessionFile = ctx.sessionManager.getSessionFile();&#xA;&#xA;if (currentSessionFile) {&#xA;  finalPrompt = `${goal}\n\n/skill:session-query\n\n**Parent session:** \`${currentSessionFile}\`\n\n${result}`;&#xA;}&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;The important part: handoff does not open a second Pi process. The command&#xA;replaces the active session inside the current process:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-typescript&#34;&gt;const newSessionResult = await cmdCtx.newSession({&#xA;  parentSession: currentSessionFile,&#xA;});&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;When the agent invokes the &lt;code&gt;handoff&lt;/code&gt; tool instead, Pi waits for the current turn&#xA;to finish, then changes the session file:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-typescript&#34;&gt;pi.on(&amp;#34;agent_end&amp;#34;, (_event, ctx) =&amp;gt; {&#xA;  if (!pendingHandoff) return;&#xA;&#xA;  const { prompt, parentSession } = pendingHandoff;&#xA;  (ctx.sessionManager as any).newSession({ parentSession });&#xA;});&lt;/code&gt;&lt;/pre&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-plaintext&#34;&gt;Handoff initiated. The session will switch after the current turn completes.&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;Same TUI, new active session. The old session remains saved as the parent. To&#xA;return to it, open the session picker:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-plaintext&#34;&gt;/resume&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;Or start Pi with that picker from the shell:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;% pi -r&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;&lt;code&gt;/tree&lt;/code&gt; is not the way back: it navigates branches inside the current session&#xA;file. Handoff creates another session file, so &lt;code&gt;/resume&lt;/code&gt; is the matching&#xA;operation.&lt;/p&gt;&#xA;&lt;h2 id=&#34;conclusion&#34;&gt;&#xA;  Conclusion&#xA;  &lt;a class=&#34;heading-anchor&#34; href=&#34;https://perrotta.dev/2026/08/pi-handing-off-to-a-focused-session/#conclusion&#34; aria-label=&#34;Link to this section&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h2&gt;&#xA;&lt;p&gt;&lt;strong&gt;Human voice&lt;/strong&gt;: Guess what, after doing all of the above I came to the&#xA;conclusion that this is a cumbersome workflow. No wonder the Amp folks&#xA;&lt;a href=&#34;https://www.nicolaygerold.com/posts/kirby-is-eating-context-engineering&#34;&gt;seem&lt;/a&gt;&#xA;to have reached the same conclusion:&lt;/p&gt;&#xA;&lt;blockquote&gt;&#xA;&lt;p&gt;Handoff is another one of my features that I expect to die soon.&lt;/p&gt;&#xA;&lt;/blockquote&gt;&#xA;&lt;p&gt;I am ditching it.&lt;/p&gt;&#xA;&lt;p&gt;A better workflow is to backtrack the session tree with &lt;code&gt;/tree&lt;/code&gt; or &lt;code&gt;Esc Esc&lt;/code&gt;.&lt;/p&gt;&#xA;&lt;hr&gt;&#xA;&lt;p&gt;🤖 &lt;em&gt;Drafted with &lt;a href=&#34;https://github.com/thiagowfx/skills/blob/master/plugins/thiagowfx/skills/bloggify/SKILL.md&#34;&gt;&lt;code&gt;/bloggify&lt;/code&gt;&lt;/a&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: handing off to a focused session&#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/bloggify/&#34;&gt;#bloggify&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/pi/&#34;&gt;#pi&lt;/a&gt;&lt;/p&gt;</description>
    </item>
    <item>
      <title>ChatGPT / OpenAI / Codex plan usage
      </title>
      <link>https://perrotta.dev/2026/08/chatgpt-/-openai-/-codex-plan-usage/</link>
      <pubDate>Wed, 19 Aug 2026 00:14:25 +0200</pubDate><author>serendipity@perrotta.dev (Thiago Perrotta)</author>
      <category>ai</category>
      <category>dev</category>
      <guid>https://perrotta.dev/2026/08/chatgpt-/-openai-/-codex-plan-usage/</guid>
      <description>&lt;p&gt;♠ &lt;a href=&#34;https://perrotta.dev/2026/05/claude-enterprise-plan-usage/&#34;&gt;Previously&lt;/a&gt;.&lt;/p&gt;&#xA;&lt;p&gt;When enrolled in an &lt;a href=&#34;https://chatgpt.com/pricing/&#34;&gt;OpenAI / ChatGPT plan&lt;/a&gt;&#xA;(e.g. Pro/Plus), you may want to figure out how much weekly usage (%) you have&#xA;left.&lt;/p&gt;&#xA;&lt;p&gt;I&amp;rsquo;m sure &lt;a href=&#34;https://openai.com/codex/&#34;&gt;&lt;code&gt;codex&lt;/code&gt;&lt;/a&gt; has an &lt;code&gt;/usage&lt;/code&gt; or &lt;code&gt;/cost&lt;/code&gt;&#xA;command to do so, but what if I&amp;rsquo;m using a third-party harness like&#xA;&lt;a href=&#34;https://pi.dev&#34;&gt;Pi&lt;/a&gt; or&#xA;&lt;a href=&#34;https://opencode.ai/&#34;&gt;OpenCode&lt;/a&gt;?&lt;/p&gt;&#xA;&lt;p&gt;Head over to &lt;a href=&#34;https://chatgpt.com/#settings/Usage&#34;&gt;https://chatgpt.com/#settings/Usage&lt;/a&gt; to assess your &lt;strong&gt;usage&#xA;limits&lt;/strong&gt;.&lt;/p&gt;&#xA;&lt;p&gt;Usage limits normally reset every week.&lt;/p&gt;&#xA;&lt;p&gt;You can buy API credits (and even enable auto reload) when you run out, just&#xA;like how you would do with your mobile phone plan.&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;Alternatively&lt;/strong&gt;: &lt;a href=&#34;https://perrotta.dev/2026/05/codexbar/&#34;&gt;codexbar&lt;/a&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: ChatGPT / OpenAI / Codex plan usage&#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/dev/&#34;&gt;#dev&lt;/a&gt;&lt;/p&gt;</description>
    </item>
    <item>
      <title>★ Reviewing slop pull requests
      </title>
      <link>https://perrotta.dev/2026/08/reviewing-slop-pull-requests/</link>
      <pubDate>Tue, 18 Aug 2026 00:47:17 +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/08/reviewing-slop-pull-requests/</guid>
      <description>&lt;p&gt;♠ &lt;a href=&#34;https://perrotta.dev/2026/08/dual-review-address-review-findings-automatically/&#34;&gt;Previously&lt;/a&gt;.&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;Problem statement&lt;/strong&gt;: review an AI-written &lt;del&gt;slop&lt;/del&gt; pull request from a&#xA;teammate.&lt;/p&gt;&#xA;&lt;p&gt;We&amp;rsquo;ll use a &lt;a href=&#34;https://lucumr.pocoo.org/2026/5/26/clankers/&#34;&gt;clanker&lt;/a&gt; to review slop&#xA;PRs.&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;First&lt;/strong&gt;, I invoke the&#xA;&lt;a href=&#34;https://github.com/thiagowfx/skills/blob/master/plugins/thiagowfx/skills/meat/SKILL.md&#34;&gt;&lt;code&gt;/meat&lt;/code&gt;&lt;/a&gt;&#xA;skill, turning the diff into a reading guide, inspired by &lt;a href=&#34;https://meat.dev/&#34;&gt;David Crawshaw&lt;/a&gt;:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-plaintext&#34;&gt;/meat &amp;lt;PR-URL&amp;gt;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;It does not look for defects. It captures the diff once, follows control and&#xA;data flow, treats tests as specifications, and returns this shape:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-markdown&#34;&gt;One sentence with intent and observable result.&#xA;&#xA;## Read in this order&#xA;- path:line — why this contract matters&#xA;&#xA;## Flow&#xA;1. input&#xA;2. transformation&#xA;3. effect&#xA;&#xA;## Contracts&#xA;## Tests as specs&#xA;## Omitted&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;That gives me the author&amp;rsquo;s system model (the &amp;ldquo;meat&amp;rdquo; of their PR) without the PR&#xA;body&amp;rsquo;s narrative.&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;Second&lt;/strong&gt;, &lt;a href=&#34;https://github.com/thiagowfx/skills/blob/master/plugins/thiagowfx/skills/dual-review/SKILL.md&#34;&gt;&lt;code&gt;/dual-review&lt;/code&gt;&lt;/a&gt;&lt;sup id=&#34;fnref:1&#34;&gt;&lt;a href=&#34;https://perrotta.dev/2026/08/reviewing-slop-pull-requests/#fn:1&#34; class=&#34;footnote-ref&#34; role=&#34;doc-noteref&#34;&gt;1&lt;/a&gt;&lt;/sup&gt;&#xA;tries to break that model:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-plaintext&#34;&gt;/dual-review &amp;lt;PR-URL&amp;gt; [--post]&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;It runs &lt;em&gt;two&lt;/em&gt; review passes with different lenses / perspectives, then validates&#xA;every candidate against source and focused probes. Reviewer agreement changes&#xA;investigation priority.&lt;/p&gt;&#xA;&lt;p&gt;&lt;code&gt;--post&lt;/code&gt; instructs the clanker to write a comment in the PR.&lt;/p&gt;&#xA;&lt;p&gt;To recap:&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;&lt;code&gt;/meat&lt;/code&gt; says what the change means and where to read it.&lt;/li&gt;&#xA;&lt;li&gt;&lt;code&gt;/dual-review&lt;/code&gt; checks whether contracts survive hostile input.&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;My default sequence for a long, plausible PR is now:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-plaintext&#34;&gt;/meat &amp;lt;PR-URL&amp;gt;&#xA;/dual-review &amp;lt;PR-URL&amp;gt; --post&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;One tool removes narrative noise.&#xA;The other removes findings without evidence. ∎&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;Inspired by an internal skill created by our CISO.&amp;#160;&lt;a href=&#34;https://perrotta.dev/2026/08/reviewing-slop-pull-requests/#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: Reviewing slop pull 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/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>
    <item>
      <title>pi: fork vs clone sessions
      </title>
      <link>https://perrotta.dev/2026/08/pi-fork-vs-clone-sessions/</link>
      <pubDate>Mon, 17 Aug 2026 16:10:31 +0200</pubDate><author>serendipity@perrotta.dev (Thiago Perrotta)</author>
      <category>ai</category>
      <category>coding</category>
      <category>dev</category>
      <category>pi</category>
      <guid>https://perrotta.dev/2026/08/pi-fork-vs-clone-sessions/</guid>
      <description>&lt;p&gt;♠ &lt;strong&gt;Problem statement&lt;/strong&gt;: split a running Pi conversation / session into a second&#xA;independent one whilst keeping the original.&lt;/p&gt;&#xA;&lt;p&gt;Pi has three similarly named albeit distinct operations:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-plaintext&#34;&gt;/fork&#xA;/clone&#xA;pi --fork &amp;lt;path|id&amp;gt;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;&lt;code&gt;/fork&lt;/code&gt; opens a selector for an earlier user message, copies the active path up&#xA;to the message&amp;rsquo;s parent into a new session, and puts the selected prompt back in&#xA;the editor. This is for changing an earlier request.&lt;/p&gt;&#xA;&lt;p&gt;&lt;code&gt;/clone&lt;/code&gt; copies the current active root-to-leaf path into a new session and&#xA;switches the current Pi process to it. The old session file remains unchanged;&#xA;retrieve it with &lt;code&gt;/resume&lt;/code&gt;, or note its ID first and open it directly:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-plaintext&#34;&gt;/session&#xA;/clone&lt;/code&gt;&lt;/pre&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;% pi --session 019feb2b&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;The CLI spelling does something subtly different:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;% pi --help | grep -A5 -- &amp;#39;--session &amp;lt;path&amp;#39;&#xA;  --session &amp;lt;path|id&amp;gt;            Use specific session file or partial UUID&#xA;  --session-id &amp;lt;id&amp;gt;              Use exact project session ID, creating it if missing&#xA;  --fork &amp;lt;path|id&amp;gt;               Fork specific session file or partial UUID into a new session&#xA;  --session-dir &amp;lt;dir&amp;gt;            Directory for session storage and lookup&#xA;  --no-session                   Don&amp;#39;t save session (ephemeral)&#xA;  --name, -n &amp;lt;name&amp;gt;              Set session display name&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;&lt;code&gt;pi --fork&lt;/code&gt; copies the complete source session file, including all branches,&#xA;then opens the copy in a new Pi process. The running source process is untouched:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-plaintext&#34;&gt;# current Pi&#xA;/session&lt;/code&gt;&lt;/pre&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;# second terminal, same project&#xA;% pi --fork 019feb2b&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;It&amp;rsquo;s easier to do so if we add the session ID to the Pi status bar. Pi loads&#xA;extensions from &lt;code&gt;~/.pi/agent/extensions/&lt;/code&gt;, so I have&#xA;&lt;a href=&#34;https://github.com/thiagowfx/.dotfiles/blob/e08ea70fbec9decd9059817119ef715a34d8c12e/pi/.pi/agent/extensions/session-id.ts&#34;&gt;this one&lt;/a&gt;:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-typescript&#34;&gt;// ~/.pi/agent/extensions/session-id.ts&#xA;import type { ExtensionAPI, ExtensionContext } from &amp;#34;@earendil-works/pi-coding-agent&amp;#34;;&#xA;&#xA;export function updateSessionIdStatus(ctx: ExtensionContext): void {&#xA;  const sessionId = ctx.sessionManager.getSessionId();&#xA;  ctx.ui.setStatus(&amp;#34;session-id&amp;#34;, ctx.ui.theme.fg(&amp;#34;dim&amp;#34;, `sid:${sessionId}`));&#xA;}&#xA;&#xA;export default function (pi: ExtensionAPI) {&#xA;  pi.on(&amp;#34;session_start&amp;#34;, async (_event, ctx) =&amp;gt; updateSessionIdStatus(ctx));&#xA;}&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;For a linear session, &lt;code&gt;pi --fork&lt;/code&gt; and &lt;code&gt;/clone&lt;/code&gt; produce equivalent conversation&#xA;context. Once a session has branches, &lt;code&gt;/clone&lt;/code&gt; extracts only the active branch;&#xA;&lt;code&gt;pi --fork&lt;/code&gt; preserves the full tree.&lt;/p&gt;&#xA;&lt;p&gt;Pi sessions are&#xA;&lt;a href=&#34;https://github.com/earendil-works/pi-mono/blob/main/packages/coding-agent/docs/sessions.md&#34;&gt;JSONL trees&lt;/a&gt;,&#xA;so none of these operations mutate the source history. For splitting one live&#xA;conversation into two live processes, use &lt;code&gt;pi --fork &amp;lt;id&amp;gt;&lt;/code&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: fork vs clone sessions&#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/pi/&#34;&gt;#pi&lt;/a&gt;&lt;/p&gt;</description>
    </item>
    <item>
      <title>markdown: nested code fences
      </title>
      <link>https://perrotta.dev/2026/08/markdown-nested-code-fences/</link>
      <pubDate>Mon, 17 Aug 2026 01:27:47 +0200</pubDate><author>serendipity@perrotta.dev (Thiago Perrotta)</author>
      <category>bloggify</category>
      <category>coding</category>
      <category>dev</category>
      <category>meta</category>
      <guid>https://perrotta.dev/2026/08/markdown-nested-code-fences/</guid>
      <description>&lt;p&gt;♠ &lt;strong&gt;Problem statement&lt;/strong&gt;: a code block that quotes a markdown file swallows the&#xA;first closing fence it meets, which is the &lt;em&gt;inner&lt;/em&gt; one.&lt;/p&gt;&#xA;&lt;p&gt;The &lt;a href=&#34;https://perrotta.dev/2026/01/adrs-and-llms/&#34;&gt;ADRs and LLMs&lt;/a&gt; post quotes a&#xA;&lt;code&gt;README.md&lt;/code&gt; that itself contains a fenced template. Written naively:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-markdown&#34;&gt;```markdown&#xA;# README&#xA;&#xA;Use this template:&#xA;&#xA;```markdown&#xA;# ADR-NNNN: Title&#xA;```&#xA;&#xA;That was the template.&#xA;```&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;The &lt;a href=&#34;https://spec.commonmark.org/0.31.2/#fenced-code-blocks&#34;&gt;CommonMark spec&lt;/a&gt;&#xA;says who wins:&lt;/p&gt;&#xA;&lt;blockquote&gt;&#xA;&lt;p&gt;The content of the code block consists of all subsequent lines, until a&#xA;closing code fence of the same type as the code block began with (backticks&#xA;or tildes), and with at least as many backticks or tildes as the opening code&#xA;fence.&lt;/p&gt;&#xA;&lt;/blockquote&gt;&#xA;&lt;p&gt;Three backticks close three backticks, so the template&amp;rsquo;s closing fence&#xA;terminates the &lt;em&gt;outer&lt;/em&gt; block. Everything after it leaks into the page:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-html&#34;&gt;&amp;lt;div class=&amp;#34;codeblock&amp;#34; data-lang=&amp;#34;markdown&amp;#34;&amp;gt;&#xA;  &amp;lt;!-- header --&amp;gt;&#xA;&amp;lt;p&amp;gt;That was the template.&amp;lt;/p&amp;gt;&#xA;&amp;lt;div class=&amp;#34;codeblock&amp;#34;&amp;gt;&#xA;   &amp;lt;div class=&amp;#34;highlight&amp;#34;&amp;gt;&amp;lt;pre tabindex=&amp;#34;0&amp;#34; class=&amp;#34;chroma&amp;#34;&amp;gt;&amp;lt;code class=&amp;#34;language-plaintext&amp;#34; data-lang=&amp;#34;plaintext&amp;#34;&amp;gt;&amp;lt;/code&amp;gt;&amp;lt;/pre&amp;gt;&amp;lt;/div&amp;gt;&#xA;&amp;lt;/div&amp;gt;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;Two code blocks and a stray paragraph, where one code block was meant.&lt;/p&gt;&#xA;&lt;p&gt;The fix is a longer outer fence — four backticks, or more:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-markdown&#34;&gt;````markdown&#xA;# README&#xA;&#xA;Use this template:&#xA;&#xA;```markdown&#xA;# ADR-NNNN: Title&#xA;```&#xA;&#xA;That was the template.&#xA;````&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;Two details make this worth writing down.&lt;/p&gt;&#xA;&lt;p&gt;The damage is quiet. On the real file the outer fence was &lt;code&gt;shell&lt;/code&gt;, so Hugo&#xA;rendered a plausible block that had merely lost its last line. Comparing the&#xA;rendered code block before and after the fix:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-diff&#34;&gt;-lang: shell&#xA;&amp;#43;lang: markdown&#xA; …What becomes easier or more difficult to do because of this change?&#xA;&amp;#43;```&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;And &lt;code&gt;markdownlint&lt;/code&gt; was happy the whole time:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;% prek run markdownlint --files content/posts/2026-01-11-adrs-and-llms.md&#xA;markdownlint-cli2........................................................Passed&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;Rule of thumb: count the backticks of the innermost block, then add one for&#xA;every level above it.&lt;/p&gt;&#xA;&lt;hr&gt;&#xA;&lt;p&gt;🤖 &lt;em&gt;Drafted with &lt;a href=&#34;https://github.com/thiagowfx/skills/blob/master/plugins/thiagowfx/skills/bloggify/SKILL.md&#34;&gt;&lt;code&gt;/bloggify&lt;/code&gt;&lt;/a&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: markdown: nested code fences&#34;&gt;email&lt;/a&gt;&lt;/p&gt;&lt;p&gt;&lt;a href=&#34;https://perrotta.dev/tags/bloggify/&#34;&gt;#bloggify&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/meta/&#34;&gt;#meta&lt;/a&gt;&lt;/p&gt;</description>
    </item>
    <item>
      <title>microSD card: wiping before selling
      </title>
      <link>https://perrotta.dev/2026/08/microsd-card-wiping-before-selling/</link>
      <pubDate>Sun, 16 Aug 2026 15:20:10 +0200</pubDate><author>serendipity@perrotta.dev (Thiago Perrotta)</author>
      <category>dev</category>
      <category>macos</category>
      <category>privacy</category>
      <category>security</category>
      <guid>https://perrotta.dev/2026/08/microsd-card-wiping-before-selling/</guid>
      <description>&lt;p&gt;♠ &lt;strong&gt;Problem statement&lt;/strong&gt;: I want to sell a 32 GB microSD card, previously used to&#xA;boot a Raspberry Pi.&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;% diskutil list disk4&#xA;/dev/disk4 (external, physical):&#xA;   #:                       TYPE NAME                    SIZE       IDENTIFIER&#xA;   0:     FDisk_partition_scheme                        *32.0 GB    disk4&#xA;   1:             Windows_FAT_32 BOOT                    268.4 MB   disk4s1&#xA;   2:                      Linux                         31.7 GB    disk4s2&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;For that, I would like to securely wipe it. &lt;code&gt;dd&lt;/code&gt; is my preferred tool to do so.&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;% diskutil unmountDisk /dev/disk4&#xA;Unmount of all volumes on disk4 was successful&#xA;% sudo dd if=/dev/zero of=/dev/rdisk4 bs=4m&#xA;dd: invalid number: &amp;#39;4m&amp;#39;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;That is GNU &lt;code&gt;dd&lt;/code&gt; from coreutils (installed via homebrew) shadowing the BSD one&#xA;in &lt;code&gt;PATH&lt;/code&gt;. BSD &lt;code&gt;dd&lt;/code&gt; takes &lt;code&gt;bs=4m&lt;/code&gt;, GNU &lt;code&gt;dd&lt;/code&gt; takes &lt;code&gt;4M&lt;/code&gt;. It&amp;rsquo;s hard to standardize&#xA;and agree upon something so simple, eh?&lt;/p&gt;&#xA;&lt;p&gt;&lt;code&gt;/dev/rdisk4&lt;/code&gt; rather than &lt;code&gt;/dev/disk4&lt;/code&gt; because the raw node skips the buffer&#xA;cache and thus it is several times faster:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;% sudo dd if=/dev/zero of=/dev/rdisk4 bs=4M status=progress&#xA;dd: error writing &amp;#39;/dev/rdisk4&amp;#39;: Input/output error&#xA;7633&amp;#43;0 records in&#xA;7632&amp;#43;0 records out&#xA;32010928128 bytes (32 GB, 30 GiB) copied, 3550.08 s, 9.0 MB/s&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;The &lt;code&gt;Input/output error&lt;/code&gt; is the happy ending: &lt;code&gt;dd&lt;/code&gt; ran off the end of the&#xA;device. 32010928128 bytes is precisely what &lt;code&gt;diskutil info&lt;/code&gt; reported for the&#xA;disk size, so every addressable byte got a zero.&lt;/p&gt;&#xA;&lt;p&gt;The process took ~59min at ~9.0 MB/s.&lt;/p&gt;&#xA;&lt;p&gt;Then we&amp;rsquo;ll partition it again so that the buyer&amp;rsquo;s camera or laptop will&#xA;properly recognize it:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;% diskutil eraseDisk FAT32 SDCARD MBRFormat /dev/disk4&#xA;Started erase on disk4&#xA;Creating the partition map&#xA;Formatting disk4s1 as MS-DOS (FAT32) with name SDCARD&#xA;512 bytes per physical sector&#xA;/dev/rdisk4s1: 62488736 sectors in 1952773 FAT32 clusters (16384 bytes/cluster)&#xA;Finished erase on disk4&#xA;% diskutil list disk4&#xA;/dev/disk4 (external, physical):&#xA;   #:                       TYPE NAME                    SIZE       IDENTIFIER&#xA;   0:     FDisk_partition_scheme                        *32.0 GB    disk4&#xA;   1:                 DOS_FAT_32 SDCARD                  32.0 GB    disk4s1&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;Now we can finally post it for sale. ∎&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: microSD card: wiping before selling&#34;&gt;email&lt;/a&gt;&lt;/p&gt;&lt;p&gt;&lt;a href=&#34;https://perrotta.dev/tags/dev/&#34;&gt;#dev&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/macos/&#34;&gt;#macos&lt;/a&gt; &lt;a href=&#34;https://perrotta.dev/tags/privacy/&#34;&gt;#privacy&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>homebrew: cask drift after an in-app update
      </title>
      <link>https://perrotta.dev/2026/08/homebrew-cask-drift-after-an-in-app-update/</link>
      <pubDate>Sun, 16 Aug 2026 14:11:17 +0200</pubDate><author>serendipity@perrotta.dev (Thiago Perrotta)</author>
      <category>bloggify</category>
      <category>coding</category>
      <category>dev</category>
      <category>macos</category>
      <guid>https://perrotta.dev/2026/08/homebrew-cask-drift-after-an-in-app-update/</guid>
      <description>&lt;p&gt;♠ &lt;a href=&#34;https://perrotta.dev/2026/08/homebrew-replace-deprecated-and-orphaned-packages/&#34;&gt;Previously&lt;/a&gt;.&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;Problem statement&lt;/strong&gt;: every &lt;code&gt;brew upgrade&lt;/code&gt; died on a cask whose app had&#xA;upgraded itself years ago.&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;% brew upgrade&#xA;==&amp;gt; Upgrading 1 outdated package:&#xA;1password 7.9.2 -&amp;gt; 8.12.33&#xA;==&amp;gt; Upgrading 1password&#xA;  7.9.2 -&amp;gt; 8.12.33&#xA;Error: 1password: It seems the App source &amp;#39;/Applications/1Password 7.app&amp;#39; is not there.&#xA;==&amp;gt; Purging files for version 8.12.33 of Cask 1password&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;That path stopped existing when 1Password 8 renamed its bundle from&#xA;&lt;code&gt;1Password 7.app&lt;/code&gt; to &lt;code&gt;1Password.app&lt;/code&gt;. The app had updated itself in place,&#xA;behind Homebrew&amp;rsquo;s back:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;% defaults read /Applications/1Password.app/Contents/Info.plist CFBundleShortVersionString&#xA;8.12.30&#xA;% brew list --cask --versions 1password&#xA;1password 7.9.2&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;Two disagreeing sources of truth. &lt;code&gt;--adopt&lt;/code&gt; looked like the obvious cure, and&#xA;wasn&amp;rsquo;t — the stale receipt still routes into the upgrade code path, which still&#xA;looks for the v7 bundle:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;% brew install --cask --adopt 1password&#xA;==&amp;gt; Upgrading 1password&#xA;  7.9.2 -&amp;gt; 8.12.33&#xA;Error: 1password: It seems the App source &amp;#39;/Applications/1Password 7.app&amp;#39; is not there.&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;The receipt is the whole problem, so look at what it actually holds:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;% find /opt/homebrew/Caskroom/1password -maxdepth 2&#xA;/opt/homebrew/Caskroom/1password&#xA;/opt/homebrew/Caskroom/1password/.metadata&#xA;/opt/homebrew/Caskroom/1password/.metadata/config.json&#xA;/opt/homebrew/Caskroom/1password/.metadata/7.9.2&#xA;/opt/homebrew/Caskroom/1password/7.9.2&#xA;% ls -A /opt/homebrew/Caskroom/1password/7.9.2/&#xA;% du -sh /opt/homebrew/Caskroom/1password&#xA;8.0K&#x9;/opt/homebrew/Caskroom/1password&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;Eight kilobytes of bookkeeping and an empty version directory. Nothing in there&#xA;is the application. Delete it, then reinstall over the app that is really on&#xA;disk:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;% rm -rf /opt/homebrew/Caskroom/1password&#xA;% brew install --cask --force 1password&#xA;==&amp;gt; Fetching downloads for: 1password&#xA;✔︎ Cask 1password (8.12.33)&#xA;Warning: It seems there is already an App at &amp;#39;/Applications/1Password.app&amp;#39;; overwriting.&#xA;==&amp;gt; Installing Cask 1password&#xA;==&amp;gt; Removing App &amp;#39;/Applications/1Password.app&amp;#39;&#xA;==&amp;gt; Moving App &amp;#39;1Password.app&amp;#39; to &amp;#39;/Applications/1Password.app&amp;#39;&#xA;🍺  1password was successfully installed!&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;Both sides agree again:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;% defaults read /Applications/1Password.app/Contents/Info.plist CFBundleShortVersionString&#xA;8.12.33&#xA;% brew list --cask --versions 1password&#xA;1password 8.12.33&#xA;% brew outdated --cask | grep 1password&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;Quitting the app first is mandatory, and this one ignored two polite&#xA;&lt;code&gt;osascript&lt;/code&gt; requests before a plain &lt;code&gt;kill&lt;/code&gt; took:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;% osascript -e &amp;#39;quit app &amp;#34;1Password&amp;#34;&amp;#39;&#xA;% pgrep -x 1Password&#xA;740&#xA;% kill 740&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;The general shape: a cask whose app ships its own updater will drift, and&#xA;Homebrew only notices when the vendor also renames the bundle. &lt;code&gt;--adopt&lt;/code&gt; is for&#xA;an app Homebrew never tracked; for an app it tracked &lt;em&gt;wrongly&lt;/em&gt;, the receipt has&#xA;to go first.&lt;/p&gt;&#xA;&lt;hr&gt;&#xA;&lt;p&gt;🤖 &lt;em&gt;Drafted with &lt;a href=&#34;https://github.com/thiagowfx/skills/blob/master/plugins/thiagowfx/skills/bloggify/SKILL.md&#34;&gt;&lt;code&gt;/bloggify&lt;/code&gt;&lt;/a&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: homebrew: cask drift after an in-app update&#34;&gt;email&lt;/a&gt;&lt;/p&gt;&lt;p&gt;&lt;a href=&#34;https://perrotta.dev/tags/bloggify/&#34;&gt;#bloggify&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/macos/&#34;&gt;#macos&lt;/a&gt;&lt;/p&gt;</description>
    </item>
    <item>
      <title>new tag: bloggify
      </title>
      <link>https://perrotta.dev/2026/08/new-tag-bloggify/</link>
      <pubDate>Sun, 16 Aug 2026 13:50:00 +0200</pubDate><author>serendipity@perrotta.dev (Thiago Perrotta)</author>
      <category>ai</category>
      <category>bloggify</category>
      <category>dev</category>
      <category>meta</category>
      <guid>https://perrotta.dev/2026/08/new-tag-bloggify/</guid>
      <description>&lt;p&gt;♠ &lt;a href=&#34;https://perrotta.dev/2026/06/bloggify/&#34;&gt;Previously&lt;/a&gt;.&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;Problem statement&lt;/strong&gt;: some readers may not want AI-assisted posts in their feed&#xA;reader, even with my edits. I support making it easier for people to opt out&#xA;from such posts. They should be able to filter them out without unsubscribing&#xA;from the whole blog.&lt;/p&gt;&#xA;&lt;p&gt;Every post the &lt;a href=&#34;https://github.com/thiagowfx/skills/blob/master/plugins/thiagowfx/skills/bloggify/SKILL.md&#34;&gt;&lt;code&gt;/bloggify&lt;/code&gt;&lt;/a&gt;&#xA;skill drafts already carries a footer disclosing it. What was missing was a&#xA;machine-readable signal, not just a line at the bottom a feed reader can&amp;rsquo;t act&#xA;on. So I grepped for the marker and tagged every post that has it:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;% grep -rl &amp;#34;Drafted with \[\`/bloggify\`\]&amp;#34; content/posts/ | wc -l&#xA;27&lt;/code&gt;&lt;/pre&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;% git show 79018e8cf1 -- content/posts/2026-06-09-bloggify.md&#xA;diff --git content/posts/2026-06-09-bloggify.md content/posts/2026-06-09-bloggify.md&#xA;index b0a5a8ee86..9d15d656fd 100644&#xA;--- content/posts/2026-06-09-bloggify.md&#xA;&amp;#43;&amp;#43;&amp;#43; content/posts/2026-06-09-bloggify.md&#xA;@@ -3,6 &amp;#43;3,7 @@ title: &amp;#34;bloggify&amp;#34;&#xA; date: 2026-06-09T16:50:11&amp;#43;02:00&#xA; tags:&#xA;   - ai&#xA;&amp;#43;  - bloggify&#xA;   - claude&#xA;   - dev&#xA;   - meta&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;Every post&amp;rsquo;s &lt;code&gt;tags:&lt;/code&gt; list ends up in the RSS &lt;code&gt;&amp;lt;item&amp;gt;&lt;/code&gt; as a &lt;code&gt;&amp;lt;category&amp;gt;&lt;/code&gt;&#xA;element, one per tag (see &lt;code&gt;layouts/_default/rss.xml&lt;/code&gt;). So &lt;code&gt;bloggify&lt;/code&gt; is now a&#xA;regular, filterable category in the feed, same as any other tag.&lt;/p&gt;&#xA;&lt;p&gt;For those of you who use &lt;a href=&#34;https://miniflux.app/&#34;&gt;Miniflux&lt;/a&gt;, filtering it out is the same trick from&#xA;&lt;a href=&#34;https://perrotta.dev/2026/02/miniflux-block-entry-matching-url/&#34;&gt;blocking an entry matching a URL&lt;/a&gt; and the &lt;a href=&#34;https://perrotta.dev/2024/12/miniflux-global-blocklist/&#34;&gt;global&#xA;blocklist&lt;/a&gt;, except on the&#xA;&lt;code&gt;EntryTag&lt;/code&gt; field instead of &lt;code&gt;EntryURL&lt;/code&gt; or &lt;code&gt;EntryTitle&lt;/code&gt;:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-ini&#34;&gt;EntryTag=(?i)^bloggify$&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;Added as a global block rule in miniflux, and every future &lt;code&gt;/bloggify&lt;/code&gt;-drafted&#xA;post never reaches the inbox. &lt;code&gt;/tags/bloggify/&lt;/code&gt; on the blog itself works too,&#xA;for anyone who&amp;rsquo;d rather subscribe to the opposite: only the AI-assisted posts.&lt;/p&gt;&#xA;&lt;hr&gt;&#xA;&lt;p&gt;🤖 &lt;em&gt;Drafted with &lt;a href=&#34;https://github.com/thiagowfx/skills/blob/master/plugins/thiagowfx/skills/bloggify/SKILL.md&#34;&gt;&lt;code&gt;/bloggify&lt;/code&gt;&lt;/a&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: new tag: bloggify&#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/bloggify/&#34;&gt;#bloggify&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/meta/&#34;&gt;#meta&lt;/a&gt;&lt;/p&gt;</description>
    </item>
    <item>
      <title>pi: prompt before dangerous commands
      </title>
      <link>https://perrotta.dev/2026/08/pi-prompt-before-dangerous-commands/</link>
      <pubDate>Sun, 16 Aug 2026 13:28:44 +0200</pubDate><author>serendipity@perrotta.dev (Thiago Perrotta)</author>
      <category>ai</category>
      <category>bloggify</category>
      <category>coding</category>
      <category>dev</category>
      <category>pi</category>
      <category>security</category>
      <guid>https://perrotta.dev/2026/08/pi-prompt-before-dangerous-commands/</guid>
      <description>&lt;p&gt;♠ &lt;a href=&#34;https://perrotta.dev/2026/07/pi-block-dangerous-commands-syntax-aware/&#34;&gt;Previously&lt;/a&gt;.&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;Problem statement&lt;/strong&gt;: my Pi extension blocked dangerous shell commands outright,&#xA;even when I had already preserved the state they could destroy.&lt;/p&gt;&#xA;&lt;p&gt;The guard parses every &lt;code&gt;bash&lt;/code&gt; tool call with tree-sitter, catching commands such&#xA;as &lt;code&gt;rm -rf&lt;/code&gt;, &lt;code&gt;terraform destroy&lt;/code&gt;, and destructive Git operations. Today it did&#xA;exactly what I had asked:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;% git -C &amp;#34;$(brew --repo thiagowfx/pancake)&amp;#34; reset --hard origin/master&#xA;git reset --hard is blocked - discards changes irreversibly&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;Blocking remains the right default, but an interactive session can ask me. Pi&#xA;extensions can intercept the&#xA;&lt;a href=&#34;https://github.com/earendil-works/pi-mono/blob/main/packages/coding-agent/docs/extensions.md#tool_call&#34;&gt;&lt;code&gt;tool_call&lt;/code&gt; event&lt;/a&gt;&#xA;and open a confirmation dialog through &lt;code&gt;ctx.ui&lt;/code&gt;.&lt;/p&gt;&#xA;&lt;p&gt;The change in&#xA;&lt;a href=&#34;https://github.com/thiagowfx/.dotfiles/commit/b5cb42f8deafae176ecddc12fccbd7c04ca264dd&#34;&gt;&lt;code&gt;dangerous-command-guard/guard.ts&lt;/code&gt;&lt;/a&gt;:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-diff&#34;&gt;-  pi.on(&amp;#34;tool_call&amp;#34;, async (event) =&amp;gt; {&#xA;&amp;#43;  pi.on(&amp;#34;tool_call&amp;#34;, async (event, ctx) =&amp;gt; {&#xA;     if (event.toolName !== &amp;#34;bash&amp;#34;) return;&#xA;&#xA;     const command = event.input.command;&#xA;     if (typeof command !== &amp;#34;string&amp;#34;) return;&#xA;&#xA;     const blocked = await findBlockedCommand(command);&#xA;-    if (blocked) return { block: true, reason: blocked.reason };&#xA;&amp;#43;    if (!blocked) return;&#xA;&amp;#43;    if (!ctx.hasUI) return { block: true, reason: blocked.reason };&#xA;&amp;#43;&#xA;&amp;#43;    const allowed = await ctx.ui.confirm(&#xA;&amp;#43;      &amp;#34;Allow dangerous command?&amp;#34;,&#xA;&amp;#43;      `${blocked.command}\n\n${blocked.reason}`,&#xA;&amp;#43;    );&#xA;&amp;#43;    if (!allowed) return { block: true, reason: &amp;#34;Blocked by user&amp;#34; };&#xA;   });&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;Interactive Pi now pauses for approval. Print and JSON modes have no UI, so they&#xA;still fail closed. Safe commands never prompt.&lt;/p&gt;&#xA;&lt;p&gt;The tests exercise all three decisions:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-plaintext&#34;&gt;✔ extension prompts before dangerous bash tool calls (0.66225ms)&#xA;ℹ tests 48&#xA;ℹ pass 48&#xA;ℹ fail 0&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;Same guardrails, with an escape hatch operated by a human rather than a model.&lt;/p&gt;&#xA;&lt;hr&gt;&#xA;&lt;p&gt;🤖 &lt;em&gt;Drafted with &lt;a href=&#34;https://github.com/thiagowfx/skills/blob/master/plugins/thiagowfx/skills/bloggify/SKILL.md&#34;&gt;&lt;code&gt;/bloggify&lt;/code&gt;&lt;/a&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: prompt before dangerous commands&#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/bloggify/&#34;&gt;#bloggify&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/pi/&#34;&gt;#pi&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: am I sandboxed?
      </title>
      <link>https://perrotta.dev/2026/08/pi-am-i-sandboxed/</link>
      <pubDate>Sat, 15 Aug 2026 00:56:29 +0200</pubDate><author>serendipity@perrotta.dev (Thiago Perrotta)</author>
      <category>ai</category>
      <category>bloggify</category>
      <category>coding</category>
      <category>dev</category>
      <category>pi</category>
      <category>security</category>
      <guid>https://perrotta.dev/2026/08/pi-am-i-sandboxed/</guid>
      <description>&lt;p&gt;♠ &lt;strong&gt;Problem statement&lt;/strong&gt;: asking a coding agent whether it runs in a sandbox&#xA;returns a confident answer derived from its system prompt, not from the machine&#xA;it is actually running on.&lt;/p&gt;&#xA;&lt;p&gt;I asked &lt;a href=&#34;https://github.com/earendil-works/pi-mono&#34;&gt;Pi&lt;/a&gt; exactly that. The reply&#xA;was assured:&lt;/p&gt;&#xA;&lt;blockquote&gt;&#xA;&lt;p&gt;No — pi itself doesn&amp;rsquo;t sandbox tool execution. When I run &lt;code&gt;bash&lt;/code&gt;, &lt;code&gt;edit&lt;/code&gt;, or&#xA;&lt;code&gt;write&lt;/code&gt;, they execute directly on your machine with your user&amp;rsquo;s permissions,&#xA;in the current working directory (&lt;code&gt;/Users/thiago.perrotta/.pi&lt;/code&gt;). [&amp;hellip;] But&#xA;there&amp;rsquo;s no OS-level sandbox (no container, no seccomp, no filesystem jail).&lt;/p&gt;&#xA;&lt;/blockquote&gt;&#xA;&lt;p&gt;It&amp;rsquo;s plausible, well-formatted, and&amp;hellip;&lt;strong&gt;wrong&lt;/strong&gt;!. One typo-ridden command was&#xA;enough to break the illusion:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;% ls ~/worksppace&#xA;ls: cannot access &amp;#39;/home/hostuser/worksppace&amp;#39;: No such file or directory&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;&lt;code&gt;~&lt;/code&gt; is not &lt;code&gt;/Users/thiago.perrotta&lt;/code&gt;. The follow-up probe:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;% pwd; uname -a; whoami; cat /etc/os-release&#xA;/Users/thiago.perrotta/.pi&#xA;Linux lima-rancher-desktop 6.18.37-0-virt #1-Alpine SMP PREEMPT_DYNAMIC 2026-06-29 10:52:25 aarch64 GNU/Linux&#xA;hostuser&#xA;PRETTY_NAME=&amp;#34;Debian GNU/Linux 12 (bookworm)&amp;#34;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;A macOS-shaped &lt;code&gt;pwd&lt;/code&gt; inside an aarch64 Debian container, on an Alpine&#xA;&lt;a href=&#34;https://lima-vm.io/&#34;&gt;Lima&lt;/a&gt; VM, under Rancher Desktop. &lt;em&gt;Containerception&lt;/em&gt;.&lt;/p&gt;&#xA;&lt;p&gt;And the giveaway at the filesystem root:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;% ls -la / | head -5&#xA;total 68&#xA;drwxr-xr-x   1 root root 4096 Aug 15 00:54 .&#xA;drwxr-xr-x   1 root root 4096 Aug 15 00:54 ..&#xA;-rwxr-xr-x   1 root root    0 Aug 15 00:54 .dockerenv&#xA;drwxr-xr-x   3 root root 4096 Aug 15 00:54 Users&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;&lt;code&gt;/.dockerenv&lt;/code&gt;, plus a &lt;code&gt;/Users&lt;/code&gt; bind mount recreating just enough of the host&#xA;path for the system prompt&amp;rsquo;s &lt;code&gt;cwd&lt;/code&gt; to resolve.&lt;/p&gt;&#xA;&lt;p&gt;The model was not lying; it had no way to know. Its self-description comes from&#xA;tokens someone else wrote, while the ground truth sits one &lt;code&gt;uname&lt;/code&gt; away. The&#xA;useful lesson is that agent introspection is a documentation lookup, not a&#xA;measurement — for anything security-relevant, make it run the command.&lt;/p&gt;&#xA;&lt;p&gt;The sandbox was &lt;a href=&#34;https://perrotta.dev/2026/02/cco-claude-condom-sandbox/&#34;&gt;cco&lt;/a&gt;.&lt;/p&gt;&#xA;&lt;hr&gt;&#xA;&lt;p&gt;🤖 &lt;em&gt;Drafted with &lt;a href=&#34;https://github.com/thiagowfx/skills/blob/master/plugins/thiagowfx/skills/bloggify/SKILL.md&#34;&gt;&lt;code&gt;/bloggify&lt;/code&gt;&lt;/a&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: am I sandboxed?&#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/bloggify/&#34;&gt;#bloggify&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/pi/&#34;&gt;#pi&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>git: cd from worktree to main checkout
      </title>
      <link>https://perrotta.dev/2026/08/git-cd-from-worktree-to-main-checkout/</link>
      <pubDate>Wed, 12 Aug 2026 17:22:01 +0200</pubDate><author>serendipity@perrotta.dev (Thiago Perrotta)</author>
      <category>coding</category>
      <category>dev</category>
      <category>git</category>
      <guid>https://perrotta.dev/2026/08/git-cd-from-worktree-to-main-checkout/</guid>
      <description>&lt;p&gt;♠ &lt;a href=&#34;https://perrotta.dev/2024/10/cdg-change-directory-to-the-git-root/&#34;&gt;Previously&lt;/a&gt;.&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;Problem statement&lt;/strong&gt;: make my &lt;code&gt;cdg&lt;/code&gt; shortcut jump from a linked Git worktree&#xA;back to the main checkout when already at the worktree root.&lt;/p&gt;&#xA;&lt;p&gt;The shortcut originally stopped at the current checkout&amp;rsquo;s root:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;alias cdg=&amp;#39;cd &amp;#34;$(git root)&amp;#34;&amp;#39;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;I wanted two consecutive calls from a worktree subdirectory to behave like&#xA;the following:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-plaintext&#34;&gt;worktree/subdirectory&#xA;        ↓ cdg&#xA;worktree (at the root)&#xA;        ↓ cdg&#xA;main checkout (at the root)&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;An alias cannot succinctly express the necessary conditional behavior, whereas&#xA;an external script cannot change its parent shell&amp;rsquo;s working directory. A shell&#xA;function can:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;cdg() {&#xA;&#x9;# This file is sourced by bash and zsh, both of which support local.&#xA;&#x9;# shellcheck disable=SC3043&#xA;&#x9;local line main_is_bare main_worktree root worktrees&#xA;&#xA;&#x9;root=$(git rev-parse --path-format=absolute --show-toplevel) || return&#xA;&#xA;&#x9;if [ &amp;#34;$(pwd -P)&amp;#34; != &amp;#34;$root&amp;#34; ]; then&#xA;&#x9;&#x9;cd &amp;#34;$root&amp;#34; || return&#xA;&#x9;&#x9;return&#xA;&#x9;fi&#xA;&#xA;&#x9;worktrees=$(git worktree list --porcelain) || return&#xA;&#x9;main_worktree=&#xA;&#x9;main_is_bare=false&#xA;&#x9;while IFS= read -r line; do&#xA;&#x9;&#x9;case &amp;#34;$line&amp;#34; in&#xA;&#x9;&#x9;&#x9;worktree\ *) main_worktree=${line#worktree } ;;&#xA;&#x9;&#x9;&#x9;bare) main_is_bare=true ;;&#xA;&#x9;&#x9;&#x9;&amp;#39;&amp;#39;) break ;;&#xA;&#x9;&#x9;esac&#xA;&#x9;done &amp;lt;&amp;lt;-EOF&#xA;&#x9;$worktrees&#xA;&#x9;EOF&#xA;&#xA;&#x9;if [ &amp;#34;$main_is_bare&amp;#34; = false ] &amp;amp;&amp;amp; [ -n &amp;#34;$main_worktree&amp;#34; ] &amp;amp;&amp;amp; [ &amp;#34;$main_worktree&amp;#34; != &amp;#34;$root&amp;#34; ]; then&#xA;&#x9;&#x9;cd &amp;#34;$main_worktree&amp;#34; || return&#xA;&#x9;fi&#xA;}&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;The first entry from &lt;code&gt;git worktree list --porcelain&lt;/code&gt; is the main checkout. At&#xA;its root, &lt;code&gt;cdg&lt;/code&gt; remains a no-op. If that entry is a bare repository, it also&#xA;stays put instead of entering the Git directory. ∎&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: git: cd from worktree to main checkout&#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>★ cmux: search all windows
      </title>
      <link>https://perrotta.dev/2026/08/cmux-search-all-windows/</link>
      <pubDate>Wed, 12 Aug 2026 11:41:09 +0200</pubDate><author>serendipity@perrotta.dev (Thiago Perrotta)</author>
      <category>ai</category>
      <category>bestof</category>
      <category>dev</category>
      <guid>https://perrotta.dev/2026/08/cmux-search-all-windows/</guid>
      <description>&lt;p&gt;♠ &lt;strong&gt;Problem statement&lt;/strong&gt;: given a &lt;code&gt;cmux&lt;/code&gt; session with a dozen&lt;sup id=&#34;fnref:1&#34;&gt;&lt;a href=&#34;https://perrotta.dev/2026/08/cmux-search-all-windows/#fn:1&#34; class=&#34;footnote-ref&#34; role=&#34;doc-noteref&#34;&gt;1&lt;/a&gt;&lt;/sup&gt; workspaces, how can you&#xA;quickly find the one you&amp;rsquo;re looking for?&lt;/p&gt;&#xA;&lt;p&gt;Keyboard shortcut (macOS): &lt;code&gt;Cmd + ⌥ + F&lt;/code&gt;.&#xA;&lt;a href=&#34;https://cmux.com/docs/keyboard-shortcuts&#34;&gt;Source&lt;/a&gt;.&lt;/p&gt;&#xA;&lt;p&gt;There&amp;rsquo;s also a system tray icon for &lt;code&gt;cmux&lt;/code&gt; with a &lt;code&gt;Search All Windows...&lt;/code&gt; menu&#xA;item.&lt;/p&gt;&#xA;&lt;p&gt;How did I look this up? Via &lt;a href=&#34;https://kagi.com/search?q=cmux&amp;#43;search&amp;#43;shorcut%3F&amp;amp;r=no_region&amp;amp;sh=LJvrhfVS1VIXBrbO4rOJrw&#34;&gt;Kagi Search&lt;/a&gt;.&#xA;My query was &lt;code&gt;cmux search shorcut?&lt;/code&gt;.&lt;/p&gt;&#xA;&lt;p&gt;The question mark (&lt;code&gt;?&lt;/code&gt;)&lt;sup id=&#34;fnref:2&#34;&gt;&lt;a href=&#34;https://perrotta.dev/2026/08/cmux-search-all-windows/#fn:2&#34; class=&#34;footnote-ref&#34; role=&#34;doc-noteref&#34;&gt;2&lt;/a&gt;&lt;/sup&gt; at the end triggers the &lt;a href=&#34;https://help.kagi.com/kagi/ai/quick-answer.html&#34;&gt;quick&#xA;answer&lt;/a&gt; feature, including&#xA;links to sources:&lt;/p&gt;&#xA;&lt;blockquote&gt;&#xA;&lt;p&gt;Kagi&amp;rsquo;s Quick Answer quickly produces a summary of the results across the pages&#xA;returned and provides references to the pages that are used. This&#xA;functionality allows you to quickly consume the desired information from the&#xA;search while giving you the pointers to dive deeper into the information if&#xA;desired.&lt;/p&gt;&#xA;&lt;/blockquote&gt;&#xA;&lt;p&gt;Whenever I know exactly the shape of the problem that I want to look up and&#xA;solve, and whenever it is byte-sized enough, I often resort to this approach; it&#xA;is very efficient.&lt;/p&gt;&#xA;&lt;p&gt;In the &lt;del&gt;old&lt;/del&gt; pre-LLM days, I would instead scour &lt;a href=&#34;https://stackoverflow.com/&#34;&gt;Stack&#xA;Overflow&lt;/a&gt; or the upstream documentations.&lt;/p&gt;&#xA;&lt;p&gt;In the early LLM days, I would ask &lt;a href=&#34;https://chatgpt.com/&#34;&gt;ChatGPT&lt;/a&gt; or&#xA;&lt;a href=&#34;https://duck.ai/&#34;&gt;Duck&lt;/a&gt;, but I&amp;rsquo;d be limited to the training cut-off of the&#xA;underlying model. ∎&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;&lt;em&gt;&amp;ldquo;&amp;ldquo;&amp;ldquo;This should not happen&amp;rdquo;&amp;rdquo;&amp;rdquo;&lt;/em&gt;. We&amp;rsquo;re poor humans with poor attention&#xA;spans. Managing more than 4-7 workspaces at once is counter-productive due&#xA;to context switching tax. One &lt;em&gt;should not&lt;/em&gt; end up in this situation in the&#xA;first place.&amp;#160;&lt;a href=&#34;https://perrotta.dev/2026/08/cmux-search-all-windows/#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;li id=&#34;fn:2&#34;&gt;&#xA;&lt;p&gt;The friction of adding the question mark is important and deliberate, it&#xA;is what enables the decision to get a digest of &lt;del&gt;slop&lt;/del&gt; LLM results. I&#xA;quite dislike the approach of &lt;a href=&#34;http://google.com/&#34;&gt;other search engines&lt;/a&gt;,&#xA;unconditionally shoving questionable AI results on everyone&amp;rsquo;s face.&amp;#160;&lt;a href=&#34;https://perrotta.dev/2026/08/cmux-search-all-windows/#fnref:2&#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: cmux: search all windows&#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/dev/&#34;&gt;#dev&lt;/a&gt;&lt;/p&gt;</description>
    </item>
    <item>
      <title>hwatch
      </title>
      <link>https://perrotta.dev/2026/08/hwatch/</link>
      <pubDate>Tue, 11 Aug 2026 01:29:14 +0200</pubDate><author>serendipity@perrotta.dev (Thiago Perrotta)</author>
      <category>dev</category>
      <guid>https://perrotta.dev/2026/08/hwatch/</guid>
      <description>&lt;p&gt;♠ &lt;a href=&#34;https://github.com/blacknon/hwatch&#34;&gt;hwatch&lt;/a&gt;:&lt;/p&gt;&#xA;&lt;blockquote&gt;&#xA;&lt;p&gt;hwatch: alternative watch command with history, diff view, JSONL logging, and&#xA;change hooks. since 2018.&lt;/p&gt;&#xA;&lt;/blockquote&gt;&#xA;&lt;p&gt;Or, even better, from its man page &lt;a href=&#34;https://manpages.debian.org/testing/hwatch/hwatch.1.en.html&#34;&gt;&lt;code&gt;hwatch(1)&lt;/code&gt;&lt;/a&gt;:&lt;/p&gt;&#xA;&lt;blockquote&gt;&#xA;&lt;p&gt;hwatch is like watch command, repeatedly executes a command and displays its&#xA;output.  However, the output results can be scrolled and displayed.  In&#xA;addition, the difference of the execution result is recorded with the time&#xA;stamp, and it can be checked later.  When checking, it is also possible to&#xA;display the diff with the previous difference together.&lt;/p&gt;&#xA;&lt;/blockquote&gt;&#xA;&lt;p&gt;This is the first project I&amp;rsquo;ve ever stumbled upon that (i) is written in Rust,&#xA;and (ii) doesn&amp;rsquo;t brag about being written in Rust. Am I having a dream? Not&#xA;kidding, there&amp;rsquo;s not a single mention of Rust in its&#xA;&lt;a href=&#34;https://github.com/blacknon/hwatch&#34;&gt;README.md&lt;/a&gt;.&lt;/p&gt;&#xA;&lt;p&gt;As usual, it is available via your favorite &lt;a href=&#34;https://repology.org/project/hwatch/versions&#34;&gt;package&#xA;manager&lt;/a&gt;.&lt;/p&gt;&#xA;&lt;p&gt;I particularly like:&lt;/p&gt;&#xA;&lt;blockquote&gt;&#xA;&lt;p&gt;that records the result of command execution, can display it history and diffs [&amp;hellip;]&lt;/p&gt;&#xA;&lt;/blockquote&gt;&#xA;&lt;p&gt;A simple way to test it:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;% hwatch git status&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;Then make a silly change like:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;% touch foo&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;It behaves just like vanilla&#xA;&lt;a href=&#34;https://man.archlinux.org/man/watch.1&#34;&gt;&lt;code&gt;watch(1)&lt;/code&gt;&lt;/a&gt;:&lt;/p&gt;&#xA;&lt;blockquote&gt;&#xA;&lt;p&gt;watch runs command repeatedly, displaying its output and errors (the first&#xA;screenful). This allows you to watch the program output change over time. By&#xA;default, command is run every 2 seconds and watch will run until interrupted.&#xA;A header informs of the start and running time of command as well as its exit&#xA;code.&lt;/p&gt;&#xA;&lt;/blockquote&gt;&#xA;&lt;p&gt;&amp;hellip;plus a few goodies such as history tracking.&lt;/p&gt;&#xA;&lt;p&gt;&lt;code&gt;h&lt;/code&gt; opens a pop-up with in-app keyboard shortcuts&lt;sup id=&#34;fnref:1&#34;&gt;&lt;a href=&#34;https://perrotta.dev/2026/08/hwatch/#fn:1&#34; class=&#34;footnote-ref&#34; role=&#34;doc-noteref&#34;&gt;1&lt;/a&gt;&lt;/sup&gt;.&lt;/p&gt;&#xA;&lt;p&gt;&lt;code&gt;--aftercommand&lt;/code&gt; (hook) is also interesting:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;% hwatch --aftercommand &amp;#39;/usr/bin/afplay /System/Library/Sounds/Glass.aiff&amp;#39; git st&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;&amp;hellip;makes a chime sound upon each diff&lt;sup id=&#34;fnref:2&#34;&gt;&lt;a href=&#34;https://perrotta.dev/2026/08/hwatch/#fn:2&#34; class=&#34;footnote-ref&#34; role=&#34;doc-noteref&#34;&gt;2&lt;/a&gt;&lt;/sup&gt;.&lt;/p&gt;&#xA;&lt;p&gt;A poor man&amp;rsquo;s stopwatch:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;% hwatch --aftercommand &amp;#39;/usr/bin/afplay /System/Library/Sounds/Glass.aiff&amp;#39; --interval 1 date&lt;/code&gt;&lt;/pre&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;Why not &lt;code&gt;?&lt;/code&gt;? :(&amp;#160;&lt;a href=&#34;https://perrotta.dev/2026/08/hwatch/#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;li id=&#34;fn:2&#34;&gt;&#xA;&lt;p&gt;There&amp;rsquo;s also a native &lt;code&gt;--beep&lt;/code&gt; option but it did not work in my terminal.&amp;#160;&lt;a href=&#34;https://perrotta.dev/2026/08/hwatch/#fnref:2&#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;— § —&lt;/p&gt;&lt;p&gt;Reply via &lt;a href=&#34;mailto:serendipity@perrotta.dev?subject=Reply to: hwatch&#34;&gt;email&lt;/a&gt;&lt;/p&gt;&lt;p&gt;&lt;a href=&#34;https://perrotta.dev/tags/dev/&#34;&gt;#dev&lt;/a&gt;&lt;/p&gt;</description>
    </item>
    <item>
      <title>homebrew: replace deprecated and orphaned packages
      </title>
      <link>https://perrotta.dev/2026/08/homebrew-replace-deprecated-and-orphaned-packages/</link>
      <pubDate>Mon, 10 Aug 2026 15:10:42 +0200</pubDate><author>serendipity@perrotta.dev (Thiago Perrotta)</author>
      <category>bloggify</category>
      <category>coding</category>
      <category>dev</category>
      <category>macos</category>
      <guid>https://perrotta.dev/2026/08/homebrew-replace-deprecated-and-orphaned-packages/</guid>
      <description>&lt;p&gt;♠ &lt;strong&gt;Problem statement&lt;/strong&gt;: &lt;code&gt;brew doctor&lt;/code&gt; found one deprecated cask and one installed&#xA;keg whose formula had disappeared.&lt;/p&gt;&#xA;&lt;h2 id=&#34;tflint&#34;&gt;&#xA;  tflint&#xA;  &lt;a class=&#34;heading-anchor&#34; href=&#34;https://perrotta.dev/2026/08/homebrew-replace-deprecated-and-orphaned-packages/#tflint&#34; aria-label=&#34;Link to this section&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h2&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-plaintext&#34;&gt;Warning: Some installed casks are deprecated or disabled.&#xA;You should find replacements for the following casks:&#xA;flameshot&#xA;&#xA;Warning: Some installed kegs have no formulae!&#xA;You should find replacements for the following formulae:&#xA;  tflint&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;Homebrew/core &lt;a href=&#34;https://github.com/Homebrew/homebrew-core/commit/6cefefcbba24e4a1093dfd2e189f2cb01170e45f&#34;&gt;removed TFLint&lt;/a&gt;&#xA;after its executable became subject to both MPL 2.0 and BUSL 1.1. Upstream now&#xA;maintains &lt;a href=&#34;https://github.com/terraform-linters/homebrew-tap&#34;&gt;&lt;code&gt;terraform-linters/tap&lt;/code&gt;&lt;/a&gt;&#xA;as a cask, so migrate the old formula instead of merely reinstalling it:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;% brew uninstall tflint&#xA;Uninstalling /opt/homebrew/Cellar/tflint/0.61.0... (8 files, 51.8MB)&#xA;&#xA;% brew install terraform-linters/tap/tflint&#xA;==&amp;gt; Tapping terraform-linters/tap&#xA;Tapped 1 cask (15 files, 14.7KB).&#xA;==&amp;gt; Installing Cask tflint&#xA;==&amp;gt; Linking Binary &amp;#39;tflint&amp;#39; to &amp;#39;/opt/homebrew/bin/tflint&amp;#39;&#xA;🍺  tflint was successfully installed!&#xA;&#xA;% tflint --version&#xA;TFLint version 0.64.0&#xA;&amp;#43; ruleset.terraform (0.15.0-bundled)&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;h2 id=&#34;flameshot--shottr&#34;&gt;&#xA;  Flameshot → Shottr&#xA;  &lt;a class=&#34;heading-anchor&#34; href=&#34;https://perrotta.dev/2026/08/homebrew-replace-deprecated-and-orphaned-packages/#flameshot--shottr&#34; aria-label=&#34;Link to this section&#34;&gt;#&lt;/a&gt;&#xA;&lt;/h2&gt;&#xA;&lt;p&gt;The &lt;a href=&#34;https://formulae.brew.sh/cask/flameshot&#34;&gt;Flameshot cask&lt;/a&gt; was deprecated&#xA;because it does not pass macOS Gatekeeper checks. I replaced it with&#xA;&lt;a href=&#34;https://shottr.cc/&#34;&gt;Shottr&lt;/a&gt;:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;% brew uninstall --cask flameshot&#xA;==&amp;gt; Uninstalling Cask flameshot&#xA;==&amp;gt; Removing App &amp;#39;/Applications/flameshot.app&amp;#39;&#xA;==&amp;gt; Purging files for version 14.0.0,14.0 of Cask flameshot&#xA;&#xA;% brew install --cask shottr&#xA;==&amp;gt; Installing Cask shottr&#xA;==&amp;gt; Moving App &amp;#39;Shottr.app&amp;#39; to &amp;#39;/Applications/Shottr.app&amp;#39;&#xA;🍺  shottr was successfully installed!&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;The corresponding &lt;code&gt;Brewfile&lt;/code&gt; &lt;a href=&#34;https://github.com/thiagowfx/.dotfiles/commit/79cd3dd4eb448d4a3bc5f66a124fd60d3f131d58&#34;&gt;change&lt;/a&gt; keeps the migration reproducible:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-diff&#34;&gt;&amp;#43;tap &amp;#34;terraform-linters/tap&amp;#34;&#xA;&#xA;-brew &amp;#34;tflint&amp;#34;&#xA;&#xA;-cask &amp;#34;flameshot&amp;#34;&#xA;&amp;#43;cask &amp;#34;shottr&amp;#34;&#xA;&amp;#43;cask &amp;#34;tflint&amp;#34;&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;p&gt;Both offending entries are now gone:&lt;/p&gt;&#xA;&#xA;&lt;pre&gt;&lt;code class=&#34;language-bash&#34;&gt;% brew doctor 2&amp;gt;&amp;amp;1 | grep -E &amp;#39;flameshot|tflint|deprecated|no formulae&amp;#39;&#xA;&#xA;% brew list --cask | grep -E &amp;#39;^(flameshot|shottr|tflint)$&amp;#39;&#xA;shottr&#xA;tflint&lt;/code&gt;&lt;/pre&gt;&#xA;&lt;hr&gt;&#xA;&lt;p&gt;🤖 &lt;em&gt;Drafted with &lt;a href=&#34;https://github.com/thiagowfx/skills/blob/master/plugins/thiagowfx/skills/bloggify/SKILL.md&#34;&gt;&lt;code&gt;/bloggify&lt;/code&gt;&lt;/a&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: homebrew: replace deprecated and orphaned packages&#34;&gt;email&lt;/a&gt;&lt;/p&gt;&lt;p&gt;&lt;a href=&#34;https://perrotta.dev/tags/bloggify/&#34;&gt;#bloggify&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/macos/&#34;&gt;#macos&lt;/a&gt;&lt;/p&gt;</description>
    </item>
  </channel>
</rss>
