<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom"><title>Learning Machine</title><link href="https://gallon.me/" rel="alternate"/><link href="https://gallon.me/feeds/all.atom.xml" rel="self"/><id>https://gallon.me/</id><updated>2026-07-22T00:00:00-05:00</updated><subtitle>machina discendi</subtitle><entry><title>Letting Codex Agents Commit: Making .git Writable in the workspace-write Sandbox</title><link href="https://gallon.me/letting-codex-agents-commit-making-git-writable-in-the-workspace-write-sandbox.html" rel="alternate"/><published>2026-07-22T00:00:00-05:00</published><updated>2026-07-22T00:00:00-05:00</updated><author><name>Corey</name></author><id>tag:gallon.me,2026-07-22:/letting-codex-agents-commit-making-git-writable-in-the-workspace-write-sandbox.html</id><summary type="html">&lt;p&gt;Codex CLI's &lt;code&gt;workspace-write&lt;/code&gt; sandbox lets the agent edit anything in your repo -- except commit. Every &lt;code&gt;git commit&lt;/code&gt; dies with:&lt;/p&gt;</summary><content type="html">&lt;p&gt;Codex CLI's &lt;code&gt;workspace-write&lt;/code&gt; sandbox lets the agent edit anything in your repo -- except commit. Every &lt;code&gt;git commit&lt;/code&gt; dies with:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;fatal: Unable to create &amp;#39;/path/to/repo/.git/index.lock&amp;#39;: Read-only file system
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;This is deliberate. On Linux, Codex's bubblewrap sandbox mounts your writable roots read-write, then &lt;strong&gt;re-applies &lt;code&gt;.git&lt;/code&gt; (plus &lt;code&gt;.codex&lt;/code&gt; and &lt;code&gt;.agents&lt;/code&gt;) as read-only&lt;/strong&gt; on top. The security rationale is sound: a writable &lt;code&gt;.git/hooks&lt;/code&gt; would let the agent plant a hook that executes &lt;em&gt;outside&lt;/em&gt; the sandbox the next time you run git yourself.&lt;/p&gt;
&lt;p&gt;But if you're already running with &lt;code&gt;--ask-for-approval never&lt;/code&gt; -- YOLO mode -- you've made that trust decision, and an agent that can't commit its own work is a constant irritation. There's no official toggle: no &lt;code&gt;allow_git_writes&lt;/code&gt; option exists anywhere in the codebase, and the open issues asking for one (&lt;a href="https://github.com/openai/codex/issues/15505"&gt;#15505&lt;/a&gt;, &lt;a href="https://github.com/openai/codex/issues/12280"&gt;#12280&lt;/a&gt;, &lt;a href="https://github.com/openai/codex/issues/14338"&gt;#14338&lt;/a&gt;) have no maintainer response.&lt;/p&gt;
&lt;h2 id="the-trick-name-the-git-path-itself"&gt;The trick: name the &lt;code&gt;.git&lt;/code&gt; path itself&lt;/h2&gt;
&lt;p&gt;The obvious fixes don't work. Adding the &lt;em&gt;repo root&lt;/em&gt; to &lt;code&gt;sandbox_workspace_write.writable_roots&lt;/code&gt; fails because the &lt;code&gt;.git&lt;/code&gt; protection is applied per writable root -- a writable parent still gets a read-only &lt;code&gt;.git&lt;/code&gt; carved out of it (&lt;a href="https://github.com/openai/codex/issues/15505"&gt;#15505&lt;/a&gt;). Passing extra directories with &lt;code&gt;--add-dir&lt;/code&gt; fails because the read-only mount is applied after the command-line write dirs, so it wins (&lt;a href="https://github.com/openai/codex/issues/14338"&gt;#14338&lt;/a&gt;).&lt;/p&gt;
&lt;p&gt;But reading the sandbox policy source (&lt;a href="https://github.com/openai/codex/blob/main/codex-rs/protocol/src/permissions.rs"&gt;&lt;code&gt;codex-rs/protocol/src/permissions.rs&lt;/code&gt;&lt;/a&gt;), the carveout is added by a function named &lt;code&gt;append_default_read_only_path_if_no_explicit_rule&lt;/code&gt;. The name says it all: if an &lt;strong&gt;explicit rule already covers the exact &lt;code&gt;.git&lt;/code&gt; path&lt;/strong&gt;, the read-only protection is skipped.&lt;/p&gt;
&lt;p&gt;So point &lt;code&gt;writable_roots&lt;/code&gt; at the &lt;code&gt;.git&lt;/code&gt; directory itself:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;codex&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;exec&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;--sandbox&lt;span class="w"&gt; &lt;/span&gt;workspace-write&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;-c&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;sandbox_workspace_write.writable_roots=[&amp;quot;/path/to/repo/.git&amp;quot;]&amp;#39;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;commit your work&amp;quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Verified on codex-cli 0.144.6: without the flag, the commit fails with the read-only error above; with it, the commit lands -- confirmed in &lt;code&gt;git log&lt;/code&gt;, not just the agent's self-report.&lt;/p&gt;
&lt;h2 id="wiring-it-into-a-shell-function"&gt;Wiring it into a shell function&lt;/h2&gt;
&lt;p&gt;The writable path is per-repo, so compute it at invocation time. My &lt;code&gt;yolo-codex&lt;/code&gt; wrapper now does:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# Make .git writable inside the workspace-write sandbox so the agent can&lt;/span&gt;
&lt;span class="c1"&gt;# commit. Codex mounts .git read-only unless an explicit writable_roots&lt;/span&gt;
&lt;span class="c1"&gt;# entry names the .git path itself. Includes the common gitdir so linked&lt;/span&gt;
&lt;span class="c1"&gt;# worktrees can commit too.&lt;/span&gt;
&lt;span class="nb"&gt;local&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-a&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;git_writable&lt;/span&gt;&lt;span class="o"&gt;=()&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;git&lt;span class="w"&gt; &lt;/span&gt;rev-parse&lt;span class="w"&gt; &lt;/span&gt;--is-inside-work-tree&lt;span class="w"&gt; &lt;/span&gt;&amp;gt;/dev/null&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt;&amp;gt;&lt;span class="p"&gt;&amp;amp;&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;then&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nb"&gt;local&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;git_dir&lt;span class="w"&gt; &lt;/span&gt;git_common_dir
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nv"&gt;git_dir&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;git&lt;span class="w"&gt; &lt;/span&gt;rev-parse&lt;span class="w"&gt; &lt;/span&gt;--absolute-git-dir&lt;span class="k"&gt;)&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nv"&gt;git_common_dir&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;git&lt;span class="w"&gt; &lt;/span&gt;rev-parse&lt;span class="w"&gt; &lt;/span&gt;--path-format&lt;span class="o"&gt;=&lt;/span&gt;absolute&lt;span class="w"&gt; &lt;/span&gt;--git-common-dir&lt;span class="k"&gt;)&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;[[&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="nv"&gt;$git_dir&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;==&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="nv"&gt;$git_common_dir&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;]]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;then&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nv"&gt;git_writable&lt;/span&gt;&lt;span class="o"&gt;=(&lt;/span&gt;-c&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;sandbox_workspace_write.writable_roots=[\&amp;quot;&lt;/span&gt;&lt;span class="nv"&gt;$git_dir&lt;/span&gt;&lt;span class="s2"&gt;\&amp;quot;]&amp;quot;&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="k"&gt;else&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nv"&gt;git_writable&lt;/span&gt;&lt;span class="o"&gt;=(&lt;/span&gt;-c&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;sandbox_workspace_write.writable_roots=[\&amp;quot;&lt;/span&gt;&lt;span class="nv"&gt;$git_dir&lt;/span&gt;&lt;span class="s2"&gt;\&amp;quot;,\&amp;quot;&lt;/span&gt;&lt;span class="nv"&gt;$git_common_dir&lt;/span&gt;&lt;span class="s2"&gt;\&amp;quot;]&amp;quot;&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="k"&gt;fi&lt;/span&gt;
&lt;span class="k"&gt;fi&lt;/span&gt;

codex&lt;span class="w"&gt; &lt;/span&gt;--ask-for-approval&lt;span class="w"&gt; &lt;/span&gt;never&lt;span class="w"&gt; &lt;/span&gt;--sandbox&lt;span class="w"&gt; &lt;/span&gt;workspace-write&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;git_writable&lt;/span&gt;&lt;span class="p"&gt;[@]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="nv"&gt;$@&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Two details worth the extra lines:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Linked worktrees.&lt;/strong&gt; In a &lt;code&gt;git worktree&lt;/code&gt; checkout, &lt;code&gt;--absolute-git-dir&lt;/code&gt; points at the per-worktree gitdir, but commits also write objects and refs into the &lt;em&gt;common&lt;/em&gt; gitdir back in the main repo. Both paths need to be writable. Tested: a commit from inside a linked worktree works with both entries, and a plain repo needs only the one.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The &lt;code&gt;-c&lt;/code&gt; flag sets only &lt;code&gt;writable_roots&lt;/code&gt;.&lt;/strong&gt; Anything else you have under &lt;code&gt;[sandbox_workspace_write]&lt;/code&gt; in &lt;code&gt;~/.codex/config.toml&lt;/code&gt; (like &lt;code&gt;network_access = true&lt;/code&gt;) is untouched.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Outside a git repo, the array stays empty and the function behaves exactly as before.&lt;/p&gt;
&lt;h2 id="caveats"&gt;Caveats&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;This is undocumented behavior.&lt;/strong&gt; The explicit-rule exemption is an implementation detail of the sandbox policy, not a documented feature. A future Codex release could re-protect &lt;code&gt;.git&lt;/code&gt; unconditionally -- if agent commits start failing after an upgrade, this is the first thing to re-check.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;You're removing a real protection, not working around a bug.&lt;/strong&gt; A writable &lt;code&gt;.git&lt;/code&gt; means a writable &lt;code&gt;.git/hooks&lt;/code&gt;, and hooks run unsandboxed when &lt;em&gt;you&lt;/em&gt; next invoke git. If you're not already running approval-free, think twice.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The blunt alternative is &lt;code&gt;--sandbox danger-full-access&lt;/code&gt;&lt;/strong&gt;, which trades the entire filesystem sandbox to fix one directory. Carving out &lt;code&gt;.git&lt;/code&gt; keeps everything else protected.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Tested on codex-cli 0.144.6, Linux (bubblewrap sandbox).&lt;/p&gt;</content><category term="TIL"/><category term="codex"/><category term="configuration"/><category term="OpenAI"/></entry><entry><title>The Dark Arts of Web Automation</title><link href="https://gallon.me/the-dark-arts-of-web-automation.html" rel="alternate"/><published>2026-07-16T00:00:00-05:00</published><updated>2026-07-16T00:00:00-05:00</updated><author><name>Corey</name></author><id>tag:gallon.me,2026-07-16:/the-dark-arts-of-web-automation.html</id><summary type="html">&lt;p&gt;Ominous, right?&lt;/p&gt;</summary><content type="html">&lt;p&gt;Ominous, right?&lt;/p&gt;
&lt;p&gt;Sounds like I'm about to teach you something you'll need a lawyer for.&lt;/p&gt;
&lt;p&gt;…We'll come back to the lawyer in a bit.&lt;/p&gt;
&lt;p&gt;Here's a little bit of background…&lt;/p&gt;
&lt;p&gt;&lt;img alt="Ban threat" src="./images/dark-arts-of-web-automation/slide-02.png"&gt;&lt;/p&gt;
&lt;p&gt;So…&lt;/p&gt;
&lt;p&gt;I was getting ready for this talk -- and OpenAI threatened to ban my account ...&lt;/p&gt;
&lt;p&gt;Just for the work that I was doing in preparing it…&lt;/p&gt;
&lt;p&gt;&lt;img alt="The email" src="./images/dark-arts-of-web-automation/slide-03.png"&gt;&lt;/p&gt;
&lt;p&gt;I checked my email the other day, and I found this.&lt;/p&gt;
&lt;p&gt;What a &lt;strong&gt;shocker&lt;/strong&gt; this was!&lt;/p&gt;
&lt;p&gt;So -- what does one have to do to earn the banhammer?&lt;/p&gt;
&lt;p&gt;For cyber abuse?&lt;/p&gt;
&lt;p&gt;With a web browser?&lt;/p&gt;
&lt;p&gt;&lt;img alt="What was I doing?" src="./images/dark-arts-of-web-automation/slide-04.png"&gt;&lt;/p&gt;
&lt;p&gt;This. This is what I was doing.&lt;/p&gt;
&lt;p&gt;Every one of these is being solved by an AI agent using a web browser&lt;/p&gt;
&lt;p&gt;... and in 15 minutes or so, you'll understand exactly how.&lt;/p&gt;
&lt;p&gt;I want my agents to be able to use the web the way that I do -- book the things, send the emails, fill in the forms -- so I don't have to.&lt;/p&gt;
&lt;p&gt;But, the moment something that isn't a person starts clicking, many webpages fight back.&lt;/p&gt;
&lt;p&gt;So -- this is a talk about winning that fight. And it starts with one slightly unconventional idea.&lt;/p&gt;
&lt;p&gt;&lt;img alt="Thesis" src="./images/dark-arts-of-web-automation/slide-05.png"&gt;&lt;/p&gt;
&lt;p&gt;Here it is. The premise of the whole talk.&lt;/p&gt;
&lt;p&gt;'A CDP browser is just like a meatbag with a mouse.'&lt;/p&gt;
&lt;p&gt;...&lt;/p&gt;
&lt;p&gt;At least as far as Google, Cloudflare, and the rest can tell&lt;/p&gt;
&lt;p&gt;No joke -- if you have your agent drive a browser through the Chrome DevTools Protocol, your agent's clicks and keystrokes travel the exact same path inside Chrome that yours do.&lt;/p&gt;
&lt;p&gt;That's the big idea. The rest of the talk is how you pull it off. And that comes down to three things.&lt;/p&gt;
&lt;p&gt;&lt;img alt="Three things" src="./images/dark-arts-of-web-automation/slide-06.png"&gt;&lt;/p&gt;
&lt;p&gt;A CLI -- not an MCP.&lt;/p&gt;
&lt;p&gt;The Chrome DevTools Protocol -- which is where a tool I built, called &lt;strong&gt;chrome-agent&lt;/strong&gt; comes in.&lt;/p&gt;
&lt;p&gt;And a loop, run on a ladder.&lt;/p&gt;
&lt;p&gt;So, let's start with the one that picks a fight.&lt;/p&gt;
&lt;p&gt;&lt;img alt="CLI not MCP" src="./images/dark-arts-of-web-automation/slide-07.png"&gt;&lt;/p&gt;
&lt;p&gt;First thing: give your agent a command line interface (shell-based tools), not an MCP server. And before some of you knuckle-up on this debate, there are specific reasons for this.&lt;/p&gt;
&lt;p&gt;It's worth noting that capability is a wash -- CLI and MCP both got to the right answer about 83% of the time in a recent study by Arize AI.&lt;/p&gt;
&lt;p&gt;HOWEVER, a CLI beats an MCP in reuse, speed and cost.&lt;/p&gt;
&lt;p&gt;Reuse first. A CLI sequence can be programmed: write it once, run it a thousand times, without a model in the loop. Whereas MCP hits the model on every turn.&lt;/p&gt;
&lt;p&gt;The CLI is FASTER for a similar reason -- because there's no model in the middle of every step. In that same study -- MCP took 71 round-trips and 8 minutes for the same task that was completed with just 7 calls and under a minute using the CLI. Hold onto this one -- speed comes back at the very end.&lt;/p&gt;
&lt;p&gt;And token cost -- Anthropic reported that executing code instead of running MCP can be 75x cheaper in terms of token usage.&lt;/p&gt;
&lt;p&gt;So -- what are we actually running on the command line?&lt;/p&gt;
&lt;p&gt;&lt;img alt="CDP map" src="./images/dark-arts-of-web-automation/slide-08.png"&gt;&lt;/p&gt;
&lt;p&gt;We drive the browser with the Chrome DevTools Protocol -- the second thing your agent needs to appear human.&lt;/p&gt;
&lt;p&gt;You already know of this protocol, even if you've never heard its name. That developer panel that opens in Chrome when you hit F12 drives the browser using it. Your agents can speak it, too, using the chrome-agent tool. Chrome Agent also makes it easy for your agent to write code to replay CDP interactions.&lt;/p&gt;
&lt;p&gt;The surface area of CDP is enormous. 57 domains, hundreds of methods and events, covering everything the browser can do. I've grouped these domains into 8 buckets to make it easier to hold in your head.&lt;/p&gt;
&lt;p&gt;But the good news is that you don't need all 57 domains. To interact with a page like a human, you usually need just a small subset of these.&lt;/p&gt;
&lt;p&gt;&lt;img alt="See Hear Operate" src="./images/dark-arts-of-web-automation/slide-09.png"&gt;&lt;/p&gt;
&lt;p&gt;It's easiest to think about the subset of CDP domains you'll frequently use in terms of the 'digital senses' they provide your agent.&lt;/p&gt;
&lt;p&gt;You SEE the page -- read its structure from the DOM, its semantics from the accessibility tree, or just take a screenshot when you want the pixels.&lt;/p&gt;
&lt;p&gt;You HEAR what the page reports about itself -- the network data, its console, and its logs.&lt;/p&gt;
&lt;p&gt;And you OPERATE the page -- with clicks, keystrokes, and navigation.&lt;/p&gt;
&lt;p&gt;&lt;img alt="The loop" src="./images/dark-arts-of-web-automation/slide-10.png"&gt;&lt;/p&gt;
&lt;p&gt;The third and final thing you need for your agent to appear human when using the browser is what I call "a loop on a ladder."&lt;/p&gt;
&lt;p&gt;So here's the loop:&lt;/p&gt;
&lt;p&gt;Sense, act, verify -- and you repeat it until the page gives in.&lt;/p&gt;
&lt;p&gt;Sense -- perceive the page through one or more channels -- the DOM, the accessibility tree, a screenshot.&lt;/p&gt;
&lt;p&gt;Then Act -- do ONE thing -- click something, type something, select something.&lt;/p&gt;
&lt;p&gt;And then Verify -- sense again, but through a different channel than the action. e.g. If you clicked something, don't ask the click whether it worked -- look at the screen, or the network instead.&lt;/p&gt;
&lt;p&gt;Sense where you are. Make one move. Confirm it landed. Then iterate again.&lt;/p&gt;
&lt;p&gt;And when the loop won't close -- so when you sense, act, verify, and the page still won't do what you want -- that's the page fighting back and telling you to climb the ladder.&lt;/p&gt;
&lt;p&gt;&lt;img alt="Meatbag ladder" src="./images/dark-arts-of-web-automation/slide-11.png"&gt;&lt;/p&gt;
&lt;p&gt;The Meatbag Ladder, that is! This is a ladder of techniques that are increasingly more human as you climb it.&lt;/p&gt;
&lt;p&gt;It has 3 rungs and you climb only as high as the page forces you. Said differently, you climb to the lowest rung that works.&lt;/p&gt;
&lt;p&gt;On Rung 1 you don't act human at all. If you can just call the API exposed within the page, or fire a synthetic JavaScript click, do that -- it's easy, it's free, it's instant, and it's the right default.&lt;/p&gt;
&lt;p&gt;Climb to Rung 2 when faking it stops working: e.g. when you need a real click using CDP's Input domain. This is agent input that the page cannot tell apart from your own.&lt;/p&gt;
&lt;p&gt;Climb to Rung 3 when you need human input plus human behavior -- a real mouse path, a little dwell and some jitter, or vision to actually see and interpret things.&lt;/p&gt;
&lt;p&gt;So you start cheap, and climb one rung at a time -- only when the page makes you. Climb the ladder, run the loop on each rung, then write down the path that worked.&lt;/p&gt;
&lt;p&gt;&lt;img alt="Explore → automate" src="./images/dark-arts-of-web-automation/slide-12.png"&gt;&lt;/p&gt;
&lt;p&gt;And that's how you arrive at fully automated AI agent-driven browsing.&lt;/p&gt;
&lt;p&gt;First you explore -- you run the loop by hand, climbing rungs, until the thing actually works.&lt;/p&gt;
&lt;p&gt;And then you automate -- you write that solution down so you never have to discover it again.&lt;/p&gt;
&lt;p&gt;And you write it down as CODE, as an AGENT SKILL or, quite often, as both.&lt;/p&gt;
&lt;p&gt;So let me show you how this all comes together -- starting with a word from my attorney.&lt;/p&gt;
&lt;p&gt;&lt;img alt="Harvey" src="./images/dark-arts-of-web-automation/slide-13.png"&gt;&lt;/p&gt;
&lt;p&gt;I told you we'd come back to the lawyer.&lt;/p&gt;
&lt;p&gt;Everything you're about to see is real -- all of this comes from real agent browser use, in the wild.&lt;/p&gt;
&lt;p&gt;HOWEVER, On the advice of counsel, everything you're about to see runs only on infrastructure and accounts I own.&lt;/p&gt;
&lt;p&gt;NOW ...&lt;/p&gt;
&lt;p&gt;Who's ready to see some agents getting BUSY with the browser?&lt;/p&gt;
&lt;p&gt;&lt;img alt="Rung 1 · Outlook" src="./images/dark-arts-of-web-automation/slide-14.png"&gt;&lt;/p&gt;
&lt;video controls muted loop playsinline preload="metadata" poster="./images/dark-arts-of-web-automation/poster-outlook.jpg" style="width:100%; height:auto; display:block; margin:1.25rem auto; border-radius:8px;"&gt;
  &lt;source src="./images/dark-arts-of-web-automation/outlook-batch-10.mp4" type="video/mp4"&gt;
&lt;/video&gt;

&lt;p&gt;Alright, so here's some simple, everyday use. Let's send a batch of personalized emails -- each one different -- from your Outlook web client.&lt;/p&gt;
&lt;p&gt;We've got some lovely pseudocode here to make it easy to get the general sense of how this all works in code.&lt;/p&gt;
&lt;p&gt;Outlook's compose box has nothing to defeat, so this is Rung 1 of the Meatbag Ladder -- you don't act human at all. A synthetic click opens compose, you fill it in programmatically, and a synthetic click sends it.&lt;/p&gt;
&lt;p&gt;And then you just … let it rip!&lt;/p&gt;
&lt;p&gt;The reason this is going so smoothly is that it's executing a program. You capture the sequence once then loop it -- 20 emails, or 200, from one command. Solve it once, run it forever.&lt;/p&gt;
&lt;p&gt;The agent is riffing on the content to personalize it, whilst running a program for all the interactions.&lt;/p&gt;
&lt;p&gt;You may ask "Why drive the web UI at all -- why not use the API?" Because in a corporate environment the official API needs an app registration and an admin's sign-off -- which, as an employee, you often can't get. HOWEVER, The logged-in web session needs nothing but the login you already have. The web UI becomes a universal API in this pattern.&lt;/p&gt;
&lt;p&gt;That's Rung 1 ... But what happens when the page pushes back?&lt;/p&gt;
&lt;p&gt;&lt;img alt="Rung 2 · Demazon" src="./images/dark-arts-of-web-automation/slide-15.png"&gt;&lt;/p&gt;
&lt;video controls muted loop playsinline preload="metadata" poster="./images/dark-arts-of-web-automation/poster-demazon.jpg" style="width:100%; height:auto; display:block; margin:1.25rem auto; border-radius:8px;"&gt;
  &lt;source src="./images/dark-arts-of-web-automation/stage-stack.mp4" type="video/mp4"&gt;
&lt;/video&gt;

&lt;p&gt;So -- let's say you're shopping at your favorite online megastore. Let's just call them … Demazon. They're a crafty bunch over there at Demazon, and their pages don't quite care for your bots. Run that exact same fake click that just worked on Outlook, point it at an 'Add to Cart' button, and … nothing. No error, no complaint. The page just ignores it.&lt;/p&gt;
&lt;p&gt;Because the page is checking: did this click come from a real person, or from JavaScript? Chrome stamps every event with that answer -- whether an input is trusted, or untrusted. A click you fire from code is stamped untrusted, and, in this case, the page quietly drops that input.&lt;/p&gt;
&lt;p&gt;BUT ... No worries -- you just climb the Meatbag Ladder! On Rung 2 you run the click through Chrome's Input domain which uses the same input path your actual mouse uses. Now it's stamped trusted, the page can't tell the difference, and the item drops into the cart.&lt;/p&gt;
&lt;p&gt;Now, we've got quite the inside view of Demazon.com. Watch the bottom panel and let's imagine that's what the server's logs might look like. Every fake click is rejected, but the real ones are accepted.&lt;/p&gt;
&lt;p&gt;As a heads-up -- when you see the mouse pointer in these screen recordings, that has been added programmatically to show what mouse inputs the agent is simulating using chrome-agent / CDP. The agent isn't actually using my mouse.&lt;/p&gt;
&lt;p&gt;Rung 2 is where the real meatbag inputs begin BUT … that, alone, is not enough to let your agent replace you in the browser.&lt;/p&gt;
&lt;p&gt;&lt;img alt="Rung 3 · Turnstile" src="./images/dark-arts-of-web-automation/slide-16.png"&gt;&lt;/p&gt;
&lt;video controls muted loop playsinline preload="metadata" poster="./images/dark-arts-of-web-automation/poster-turnstile.jpg" style="width:100%; max-width:352px; height:auto; display:block; margin:1.25rem auto; border-radius:8px;"&gt;
  &lt;source src="./images/dark-arts-of-web-automation/turnstile.mp4" type="video/mp4"&gt;
&lt;/video&gt;

&lt;p&gt;So we climb to the top of the Meatbag Ladder. Rung 3 -- this is the narrow frontier where pages are actively hunting for bots. There are a variety of techniques we deploy here, though, so let's talk through a few of them.&lt;/p&gt;
&lt;p&gt;I'm sure this guy looks familiar. This is Cloudflare Turnstile and it appears deceptively simple, but it's the hardest target we've hit yet. You can't easily get a hold of that checkbox via typical web automation programming.&lt;/p&gt;
&lt;p&gt;It's buried behind three nested boundaries -- a closed shadow root, then a cross-origin iframe, then another shadow root inside that. To every cheap trick, that checkbox simply does not exist. There is no element to grab.&lt;/p&gt;
&lt;p&gt;SOOOO ... you stop trying to grab it! You ask the browser where the iframe sits on screen, do a bit of maths to calculate the position of the checkbox, and fire a trusted click at that point on the glass. And Chrome does the rest!&lt;/p&gt;
&lt;p&gt;A real click lands right on the checkbox.&lt;/p&gt;
&lt;p&gt;No human in the loop -- all agent. That's one level, cleared, and mostly the trick here was just figuring out how to interact with it. The next levels make you prove you can actually see.&lt;/p&gt;
&lt;p&gt;&lt;img alt="Rung 3 · mtCaptcha" src="./images/dark-arts-of-web-automation/slide-17.png"&gt;&lt;/p&gt;
&lt;video controls muted loop playsinline preload="metadata" poster="./images/dark-arts-of-web-automation/poster-mtcaptcha.jpg" style="width:100%; max-width:352px; height:auto; display:block; margin:1.25rem auto; border-radius:8px;"&gt;
  &lt;source src="./images/dark-arts-of-web-automation/mtcaptcha.mp4" type="video/mp4"&gt;
&lt;/video&gt;

&lt;p&gt;So here's MTCaptcha -- remember these ones? These types are still around ...&lt;/p&gt;
&lt;p&gt;The agent has to actually read this guy. So it simulates what you'd do -- it takes a screenshot of the challenge and looks at it, and its own vision capability picks the characters out of the noise.&lt;/p&gt;
&lt;p&gt;Then it types the answer back using real, trusted keystrokes, routed into the challenge's cross-origin iframe, one character at a time. The same inputs your keyboard sends.&lt;/p&gt;
&lt;p&gt;And the server agrees: text correct, token issued!&lt;/p&gt;
&lt;p&gt;Now there's one more level before the final boss -- and this one is won or lost based on how you move like a meatbag.&lt;/p&gt;
&lt;p&gt;&lt;img alt="Rung 3 · Lemin" src="./images/dark-arts-of-web-automation/slide-18.png"&gt;&lt;/p&gt;
&lt;video controls muted loop playsinline preload="metadata" poster="./images/dark-arts-of-web-automation/poster-lemin.jpg" style="width:100%; max-width:352px; height:auto; display:block; margin:1.25rem auto; border-radius:8px;"&gt;
  &lt;source src="./images/dark-arts-of-web-automation/lemin.mp4" type="video/mp4"&gt;
&lt;/video&gt;

&lt;p&gt;So this one's by Lemin -- it's a little jigsaw puzzle where you have to spot where the piece belongs, and drag it into the gap -- and there's an entire class of CAPTCHAs like this.&lt;/p&gt;
&lt;p&gt;This one's tricksy in different ways. There's no shadow root and no cross-origin iframe to pierce -- the piece is sitting right there in the page. The hard part is the drag itself.&lt;/p&gt;
&lt;p&gt;When you drop the puzzle piece, these types of CAPTCHAs sample the mouse movement into a trail of points the whole way across, including jitter and changing speed and all. So it's not just solving the puzzle, but solving it with Moves like Jagger!&lt;/p&gt;
&lt;p&gt;SOOOO … the agent drags the way a hand drags -- eased in, gently curved, a small overshoot and then settles. Just like a meatbag with a mouse! It uses Vision to see the gap and human-like motion to cross it.&lt;/p&gt;
&lt;p&gt;So that's Turnstile. MTCaptcha. and Lemin -- three gates built to keep agents out, and we've just beat each of them. Which leaves only one boss standing ...&lt;/p&gt;
&lt;p&gt;&lt;img alt="Final boss" src="./images/dark-arts-of-web-automation/slide-19.png"&gt;&lt;/p&gt;
&lt;p&gt;And here he is -- The final boss of The Internet. reCAPTCHA v2.&lt;/p&gt;
&lt;p&gt;The little checkbox, the grid of blurry traffic lights and crosswalks -- everyone in this room has squinted at one.&lt;/p&gt;
&lt;p&gt;BUT we've got the whole kit for how to beat this guy, too!&lt;/p&gt;
&lt;p&gt;The digital senses … the loop to deploy them in … the Meatbag Ladder … everything you need to take down this cheeky bastard!&lt;/p&gt;
&lt;p&gt;So let's go!&lt;/p&gt;
&lt;p&gt;&lt;img alt="How it falls" src="./images/dark-arts-of-web-automation/slide-20.png"&gt;&lt;/p&gt;
&lt;p&gt;Here's the whole machine, and it comes in two halves.&lt;/p&gt;
&lt;p&gt;On one side we have The Solver. Pure code -- no agent, no model. It does everything programmatically:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;the trusted click on the checkbox,&lt;/li&gt;
&lt;li&gt;piercing into the challenge iframe,&lt;/li&gt;
&lt;li&gt;and then every round it screenshots the tile grid;&lt;/li&gt;
&lt;li&gt;if a round expires, it just re-arms and goes again.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This bit is deterministic, fast, free.&lt;/p&gt;
&lt;p&gt;There's one step in that loop the code can't do and that's look at a grid of fuzzy photos and know which ones have a bus in them.&lt;/p&gt;
&lt;p&gt;That's vision and thinking, and it needs eyes and a brain.&lt;/p&gt;
&lt;p&gt;So that's the only job the agent gets. And we call that The Operator. The solver taps the agent on the shoulder -- and the agent:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;takes one look at the grid&lt;/li&gt;
&lt;li&gt;picks the tiles with the thing in them&lt;/li&gt;
&lt;li&gt;hands the answer back to the Solver&lt;/li&gt;
&lt;li&gt;then stands by until the next lap.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This is really the entire talk, running as one system. Code does the deterministic driving; the agent does only the part that takes eyes and a brain.&lt;/p&gt;
&lt;p&gt;Who wants to see it go?&lt;/p&gt;
&lt;p&gt;&lt;img alt="Boss falls" src="./images/dark-arts-of-web-automation/slide-21.png"&gt;&lt;/p&gt;
&lt;video controls muted loop playsinline preload="metadata" poster="./images/dark-arts-of-web-automation/poster-recaptcha.jpg" style="width:100%; max-width:352px; height:auto; display:block; margin:1.25rem auto; border-radius:8px;"&gt;
  &lt;source src="./images/dark-arts-of-web-automation/recaptcha-v2-row-1.mp4" type="video/mp4"&gt;
&lt;/video&gt;

&lt;p&gt;Allllllright!&lt;/p&gt;
&lt;p&gt;So while that's playing, by the way, some of these are really hard! The agent spotted bicycles in photos that I didn't!&lt;/p&gt;
&lt;p&gt;But look at that -- it's solved and server-verified and IT'S FAST!&lt;/p&gt;
&lt;p&gt;And fast is the whole point! This big bad boss is on the clock! Every round expires, and a full solve can be many rounds back to back. An agent that round-trips a model on every click and every look BURNS THAT CLOCK and loses -- the challenge resets before it ever finishes.&lt;/p&gt;
&lt;p&gt;The only thing I've found that defeats this is what you're watching: deterministic code running at machine speed, with one quick AI look per round.&lt;/p&gt;
&lt;p&gt;Remember when I told you to hold onto speed? This is why.&lt;/p&gt;
&lt;p&gt;And this is why it had to be a CLI, on CDP -- never a model sitting in the middle of every single interaction step.&lt;/p&gt;
&lt;p&gt;In case you're wondering, solving this wasn't a fluke …&lt;/p&gt;
&lt;p&gt;&lt;img alt="Repeatable" src="./images/dark-arts-of-web-automation/slide-22.png"&gt;&lt;/p&gt;
&lt;div style="display:grid; grid-template-columns:repeat(2, 1fr); gap:12px; max-width:640px; margin:1.25rem auto;"&gt;
  &lt;video controls muted loop playsinline preload="metadata" poster="./images/dark-arts-of-web-automation/poster-recaptcha.jpg" style="width:100%; height:auto; display:block; border-radius:8px;"&gt;
    &lt;source src="./images/dark-arts-of-web-automation/recaptcha-v2-row-2.mp4" type="video/mp4"&gt;
  &lt;/video&gt;
  &lt;video controls muted loop playsinline preload="metadata" poster="./images/dark-arts-of-web-automation/poster-recaptcha.jpg" style="width:100%; height:auto; display:block; border-radius:8px;"&gt;
    &lt;source src="./images/dark-arts-of-web-automation/recaptcha-v2-row-3.mp4" type="video/mp4"&gt;
  &lt;/video&gt;
  &lt;video controls muted loop playsinline preload="metadata" poster="./images/dark-arts-of-web-automation/poster-recaptcha.jpg" style="width:100%; height:auto; display:block; border-radius:8px;"&gt;
    &lt;source src="./images/dark-arts-of-web-automation/recaptcha-v2-row-4.mp4" type="video/mp4"&gt;
  &lt;/video&gt;
  &lt;video controls muted loop playsinline preload="metadata" poster="./images/dark-arts-of-web-automation/poster-recaptcha.jpg" style="width:100%; height:auto; display:block; border-radius:8px;"&gt;
    &lt;source src="./images/dark-arts-of-web-automation/recaptcha-v2-row-5.mp4" type="video/mp4"&gt;
  &lt;/video&gt;
&lt;/div&gt;

&lt;p&gt;Here it is, again and again ...&lt;/p&gt;
&lt;p&gt;A reliable, repeatable solution, every time.&lt;/p&gt;
&lt;p&gt;But HERE's what I actually want you to walk out of here with …&lt;/p&gt;
&lt;p&gt;&lt;img alt="The method" src="./images/dark-arts-of-web-automation/slide-23.png"&gt;&lt;/p&gt;
&lt;p&gt;The big takeaway is the methodology enabling this. The CAPTCHAs were just a tricksy test of it.&lt;/p&gt;
&lt;p&gt;This came down to careful, disciplined engineering -- it's the engineering that enabled an agent to do something it simply could not do on its own.&lt;/p&gt;
&lt;p&gt;And the method is simple:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Give your agent a CLI, so you can program it.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Drive the whole browser through CDP -- using its digital senses.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Run it as a loop on the Meatbag Ladder -- climbing only as high as the page forces you.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Then Explore until you solve it and write the solution down.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That's what makes this durable and truly useful: Figure it out once. Do it forever.&lt;/p&gt;
&lt;p&gt;Which brings us all the way back … to the crew at OpenAI and the impending banhammer.&lt;/p&gt;
&lt;p&gt;&lt;img alt="Banhammer" src="./images/dark-arts-of-web-automation/slide-24.png"&gt;&lt;/p&gt;
&lt;p&gt;After a quiet word, they kindly rescinded the threat.&lt;/p&gt;
&lt;p&gt;So, I've still got access to Codex, which is nice!&lt;/p&gt;
&lt;p&gt;&lt;img alt="Build your own" src="./images/dark-arts-of-web-automation/slide-25.png"&gt;&lt;/p&gt;
&lt;p&gt;So, you, too, can use Chrome Agent. It's installable in the Python ecosystem.&lt;/p&gt;
&lt;p&gt;It's also open-source and available on GitHub: &lt;a href="https://github.com/captivus/chrome-agent"&gt;github.com/captivus/chrome-agent&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;That's the tool I wrote to do all of this with, and I use it all day, every day!&lt;/p&gt;
&lt;p&gt;OR ... build your own! We live in the age of unbounded, personalized software.&lt;/p&gt;
&lt;p&gt;Please do follow me on X and let's chat -- I want to hear how you're automating the web with AI.&lt;/p&gt;
&lt;p&gt;Happy hacking!&lt;/p&gt;</content><category term="Writing"/><category term="web_automation"/><category term="agents"/><category term="captcha"/><category term="computer_use"/><category term="conference"/><category term="AI_Engineer_Worlds_Fair"/><category term="ai_engineering"/></entry><entry><title>How I Fixed Handy's Memory Leak Without Knowing Rust</title><link href="https://gallon.me/95-gb-in-36-hours-how-i-fixed-handys-memory-leak-without-knowing-rust.html" rel="alternate"/><published>2026-05-28T00:00:00-05:00</published><updated>2026-05-28T00:00:00-05:00</updated><author><name>Corey</name></author><id>tag:gallon.me,2026-05-28:/95-gb-in-36-hours-how-i-fixed-handys-memory-leak-without-knowing-rust.html</id><summary type="html">&lt;p&gt;I applied &lt;a href="https://vibecodinghangover.com/"&gt;The Framework&lt;/a&gt; to diagnose and fix a nasty memory leak in the wonderful &lt;a href="https://handy.computer"&gt;Handy&lt;/a&gt; voice transcription app. This required some novel adaptations of The Framework that Software and AI Engineers alike will appreciate.&lt;/p&gt;</summary><content type="html">&lt;h2 id="for-the-tiktok-attention-span-tldr"&gt;For the TikTok attention span ("TL;DR")&lt;/h2&gt;
&lt;p&gt;I applied &lt;a href="https://vibecodinghangover.com/"&gt;The Framework&lt;/a&gt; to diagnose and fix a nasty memory leak in the wonderful &lt;a href="https://handy.computer"&gt;Handy&lt;/a&gt; voice transcription app. This required some novel adaptations of The Framework that Software and AI Engineers alike will appreciate.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Developed and implemented an agent autonomy harness so that my AI agents could run Handy end-to-end without interrupting my work (this was massive)&lt;/li&gt;
&lt;li&gt;Instrumented the Handy runtime for observability and agent sensory feedback (also massive)&lt;/li&gt;
&lt;li&gt;Used this instrumentation to analyze and narrow systematically to isolate the source of the leak&lt;/li&gt;
&lt;li&gt;Fixed the leak in 3 ways, and tested each bit of the fix on its own, and together, measuring how each contributed (one of the candidate fixes was dropped after seeing its minimal contribution)&lt;/li&gt;
&lt;li&gt;Deployed adversarial agents to scrutinize findings  &lt;/li&gt;
&lt;li&gt;Surfaced a patched version of the app for me to run for days to prove, via additional telemetry, that the fix worked (the important meatbag-in-the-loop bit)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;All of this was done in a completely reversible way, leaving no trace of the Rust development environment on my machine after successfully diagnosing and fixing the memory leak.&lt;/p&gt;
&lt;p&gt;This is what AI-accelerated software engineering looks like when it's not "vibe coding" 🤮.&lt;/p&gt;
&lt;h2 id="have-you-tried-handy-for-voice-transcription"&gt;Have you tried Handy for voice transcription?&lt;/h2&gt;
&lt;p&gt;If not, &lt;a href="https://handy.computer"&gt;get some&lt;/a&gt;! I really dig it. Voice transcription has been my primary input device (measured by characters input) for over a year now. I wrote my own for use in WSL, before I ditched Windoze forever. I've even deployed &lt;a href="https://gallon.me/remapping-the-logitech-r500s-on-ubuntu-2404-with-keyd.html"&gt;the most controversial keyboard in the world&lt;/a&gt; to charactermaxxx with it.&lt;/p&gt;
&lt;p&gt;The problem was that Handy contained a devastating memory leak that rocked my beloved Linux box, blasting my productivity with a system-shattering OOM kill that disrupted my work severely. After recovering, I decided to be a good open source citizen and figure out WTF had caused this, file a bug report, and attempt to fix it.&lt;/p&gt;
&lt;h2 id="im-not-a-rust-programmer-though"&gt;I'm not a Rust programmer though ...&lt;/h2&gt;
&lt;p&gt;Handy is a &lt;a href="https://v2.tauri.app/"&gt;Tauri&lt;/a&gt; app -- a kind of clever spin on the &lt;a href="https://www.electronjs.org/"&gt;Electron&lt;/a&gt; concept of building web applications that run everywhere. It's a Rust backend with a React frontend and, thanks to Tauri, instead of shipping an entire browser engine (vis Electron + Chromium) it borrows the already-installed browser. Neat!&lt;/p&gt;
&lt;p&gt;That said ... I known't Rust. Whatever shall I do?&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;I heard a strange, humming sound from my Ghostty terminal ... &lt;/p&gt;
&lt;p&gt;An off-the-shelf installation of Claude Code was buzzing with the urge to vibe slop a purported solution! &lt;/p&gt;
&lt;p&gt;&lt;img alt="You don't know the **power** of the dark side ..." src="./images/dark-side-meme.png"&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;That simply would not do, though. The Vibe Coding Hangover is, as we all know, quite real. &lt;/p&gt;
&lt;p&gt;&lt;img alt="The Vibe Coding Hangover" src="./images/the_hangover.png"&gt;
&lt;em&gt;Courtesy of &lt;a href="https://vibecodinghangover.com/"&gt;The Cure for the Vibe Coding Hangover&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Plus, I couldn't responsibly vomit slop into someone else's repo, especially not for a tool that I use daily. This remained a software engineering problem. &lt;/p&gt;
&lt;p&gt;Adapt and apply The Framework.&lt;/p&gt;
&lt;h2 id="okay-so-what-is-this-the-framework"&gt;Okay, so what is this ✌️"The Framework"✌️?&lt;/h2&gt;
&lt;p&gt;I'm so relieved that you asked! &lt;a href="https://vibecodinghangover.com/"&gt;The Framework&lt;/a&gt; is the engineering discipline I've built for using AI coding agents to ship real software -- production stuff, not throwaway demos. I first presented it as a talk at the AI Engineer Summit: &lt;a href="https://www.youtube.com/watch?v=JsKTQbT58BY"&gt;The Cure for the Vibe Coding Hangover&lt;/a&gt;. The seminal essay is forthcoming -- I'm working through it now -- but the talk is the public version for the time being.&lt;/p&gt;
&lt;p&gt;&lt;img alt="The Framework" src="./images/the-framework.png"&gt;&lt;/p&gt;
&lt;p&gt;Three components, all interlocking:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Principles&lt;/strong&gt; -- the philosophy. &lt;em&gt;You are the architect, the agent is the implementer.&lt;/em&gt; &lt;em&gt;Specify, don't prompt.&lt;/em&gt; &lt;em&gt;Define done before building.&lt;/em&gt; &lt;em&gt;Reduce until irreducible.&lt;/em&gt; Ten of these in total, each a pithy aphorism that makes "the gospel" of it easy to remember.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Process&lt;/strong&gt; -- two phases that translate the philosophy into work. The &lt;strong&gt;Planning Phase&lt;/strong&gt; is where you do the architectural thinking: vision, features, specifications, dependencies, plan. The &lt;strong&gt;Implementation Loop&lt;/strong&gt; is where the agent takes those specifications and turns them into working code. Planning produces artifacts; the loop spins on them.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Tools&lt;/strong&gt; -- a coding environment, version control, context engineering, and the &lt;strong&gt;Multi-Sensory Feedback System&lt;/strong&gt;. The agent observes its own work through three "digital senses" -- &lt;em&gt;Visual&lt;/em&gt; (what renders), &lt;em&gt;Auditory&lt;/em&gt; (what the logs report), &lt;em&gt;Tactile&lt;/em&gt; (how the thing responds when you actually use it). Tests give the agent a pass/fail verdict; the senses tell the agent &lt;em&gt;why&lt;/em&gt; something is or isn't working. Done means tests pass AND all senses report clean. Both, not either.&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;🍸 &lt;strong&gt;Sound Smart at Cocktail Parties&lt;/strong&gt;: 
"The Multi-Sensory Feedback System closes the diagnostic loop between formal verification and qualitative observation."&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;That's &lt;strong&gt;The Framework&lt;/strong&gt;, and it works &lt;strong&gt;masterfully&lt;/strong&gt; when building new software. It works just as well when applying it to existing software that wasn't developed using the methodology, with some adaptation.&lt;/p&gt;
&lt;h2 id="right-so-what-did-that-adaptation-actually-look-like"&gt;Right, so what did that adaptation actually look like?&lt;/h2&gt;
&lt;p&gt;The Framework is built for writing new software from scratch -- but this wasn't that. I was parachuting into someone else's Rust codebase without a map, without knowledge of the language, and chasing a bug that only shows its face after many hours of use. This was classic hard-bug territory, and the agents couldn't just write some tests and check the senses -- there were no "memory leaks" tests, no observability, and you can't &lt;code&gt;curl&lt;/code&gt; your way into a voice transcription cycle.&lt;/p&gt;
&lt;h3 id="1-the-autonomy-harness"&gt;1. The autonomy harness&lt;/h3&gt;
&lt;p&gt;My AI agents had to be able to &lt;strong&gt;run the app&lt;/strong&gt;. Handy is a desktop app triggered by a global keyboard shortcut -- without a way for the agents to spin it up, inject audio, trigger recording cycles, and measure what happened, all autonomously, every experiment would have needed me to sit there mashing keys for 30-minute stretches, doing nothing else. That's very costly digital babysitting. So I built the agents their own little sandbox:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;a dedicated &lt;a href="https://en.wikipedia.org/wiki/Xvfb"&gt;Xvfb&lt;/a&gt; display so they weren't fighting me for screen real estate&lt;/li&gt;
&lt;li&gt;a &lt;a href="https://pipewire.org/"&gt;PipeWire&lt;/a&gt; null sink piping deterministic test audio through &lt;code&gt;paplay&lt;/code&gt; so they could actually speak into Handy's microphone (that's right -- my AI agents speak into my microphone ... do yours not?)&lt;/li&gt;
&lt;li&gt;a &lt;a href="https://man7.org/linux/man-pages/man7/signal.7.html"&gt;SIGUSR2&lt;/a&gt; trigger to start recordings without spawning a second Handy instance and polluting the trace&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;All of this plus full reversibility: snapshot the system, install the tooling, do the work, diff-based uninstall, verify the system's back to baseline. Tear the whole rig down with no trace afterward. &lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="n"&gt;flowchart&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;LR&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;subgraph&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;WS&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;My workstation&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="n"&gt;Me&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;Me, working normally&amp;lt;br/&amp;gt;(unaffected)&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="kd"&gt;end&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;Agent&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;AI agent&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;subgraph&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Sandbox&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;Agent&amp;#39;&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;sandbox&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;isolated&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;]&lt;/span&gt;
&lt;span class="s"&gt;        direction TB&lt;/span&gt;
&lt;span class="s"&gt;        Audio[&amp;quot;&lt;/span&gt;&lt;span class="n"&gt;test_audio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;wav&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;br&lt;/span&gt;&lt;span class="o"&gt;/&amp;gt;&lt;/span&gt;&lt;span class="err"&gt;→&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;paplay&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;br&lt;/span&gt;&lt;span class="o"&gt;/&amp;gt;&lt;/span&gt;&lt;span class="err"&gt;→&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;PipeWire&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;null&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;sink&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;]&lt;/span&gt;
&lt;span class="s"&gt;        Trigger[&amp;quot;&lt;/span&gt;&lt;span class="n"&gt;SIGUSR2&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;trigger&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;]&lt;/span&gt;
&lt;span class="s"&gt;        Handy[&amp;quot;&lt;/span&gt;&lt;span class="n"&gt;Instrumented&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Handy&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;br&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;/&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;see&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;below&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;]&lt;/span&gt;
&lt;span class="s"&gt;        Display[&amp;quot;&lt;/span&gt;&lt;span class="n"&gt;Xvfb&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kr"&gt;virtual&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;display&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;]&lt;/span&gt;
&lt;span class="s"&gt;        Audio --&amp;gt;|mic input| Handy&lt;/span&gt;
&lt;span class="s"&gt;        Trigger --&amp;gt;|start/stop recording| Handy&lt;/span&gt;
&lt;span class="s"&gt;        Handy --&amp;gt; Display&lt;/span&gt;
&lt;span class="s"&gt;    end&lt;/span&gt;

&lt;span class="s"&gt;    Agent --&amp;gt;|drives| Sandbox&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;&lt;em&gt;The autonomy harness: agent drives Handy end-to-end in an isolated sandbox; my workstation is untouched.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;This was massive&lt;/strong&gt; because without it, the Implementation Loop simply couldn't spin -- the agents had nowhere to put their hands on the app and actually use Handy.&lt;/p&gt;
&lt;h3 id="2-the-instrumentation"&gt;2. The instrumentation&lt;/h3&gt;
&lt;p&gt;The agents also had to be able to &lt;strong&gt;see inside the app&lt;/strong&gt;. A memory leak in a Tauri application is a multi-process, multi-runtime problem -- the Rust process, the WebKit subprocess, the IPC layer stitching them together -- and no single observation tool sees all of it. You can stare at the Rust files all day and learn nothing; the bug only exists at runtime, while the thing is running, across processes. So I wired Handy up to four observation tools, all recording simultaneously: &lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/namhyung/uftrace"&gt;uftrace&lt;/a&gt; for every Rust function call&lt;/li&gt;
&lt;li&gt;&lt;a href="https://strace.io/"&gt;strace&lt;/a&gt; for every syscall and IPC payload&lt;/li&gt;
&lt;li&gt;the &lt;a href="https://webkit.org/web-inspector/"&gt;WebKit Remote Inspector&lt;/a&gt; for the JavaScript heap&lt;/li&gt;
&lt;li&gt;and a &lt;a href="https://man7.org/linux/man-pages/man5/proc.5.html"&gt;&lt;code&gt;smaps_rollup&lt;/code&gt;&lt;/a&gt; sampler for memory usage per process, per memory category&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;flowchart&lt;span class="w"&gt; &lt;/span&gt;TB
&lt;span class="w"&gt;    &lt;/span&gt;subgraph&lt;span class="w"&gt; &lt;/span&gt;Handy[&amp;quot;Handy&lt;span class="w"&gt; &lt;/span&gt;at&lt;span class="w"&gt; &lt;/span&gt;runtime&amp;quot;]
&lt;span class="w"&gt;        &lt;/span&gt;direction&lt;span class="w"&gt; &lt;/span&gt;LR
&lt;span class="w"&gt;        &lt;/span&gt;Rust[&amp;quot;Rust&lt;span class="w"&gt; &lt;/span&gt;process&amp;quot;]
&lt;span class="w"&gt;        &lt;/span&gt;IPC[&amp;quot;Tauri&lt;span class="w"&gt; &lt;/span&gt;IPC&lt;span class="w"&gt; &lt;/span&gt;layer&amp;quot;]
&lt;span class="w"&gt;        &lt;/span&gt;WebKit[&amp;quot;WebKit&lt;span class="w"&gt; &lt;/span&gt;subprocess&amp;quot;]
&lt;span class="w"&gt;        &lt;/span&gt;Memory[&amp;quot;Process&lt;span class="w"&gt; &lt;/span&gt;memory&lt;span class="nt"&gt;&amp;lt;br/&amp;gt;&lt;/span&gt;(by&lt;span class="w"&gt; &lt;/span&gt;category)&amp;quot;]
&lt;span class="w"&gt;    &lt;/span&gt;end

&lt;span class="w"&gt;    &lt;/span&gt;Rust&lt;span class="w"&gt; &lt;/span&gt;-.observed&lt;span class="w"&gt; &lt;/span&gt;by.-&amp;gt;&lt;span class="w"&gt; &lt;/span&gt;U[&amp;quot;&lt;span class="nt"&gt;&amp;lt;b&amp;gt;&lt;/span&gt;uftrace&lt;span class="nt"&gt;&amp;lt;/b&amp;gt;&amp;lt;br/&amp;gt;&lt;/span&gt;every&lt;span class="w"&gt; &lt;/span&gt;Rust&lt;span class="w"&gt; &lt;/span&gt;function&lt;span class="w"&gt; &lt;/span&gt;call&amp;quot;]
&lt;span class="w"&gt;    &lt;/span&gt;IPC&lt;span class="w"&gt; &lt;/span&gt;-.observed&lt;span class="w"&gt; &lt;/span&gt;by.-&amp;gt;&lt;span class="w"&gt; &lt;/span&gt;S[&amp;quot;&lt;span class="nt"&gt;&amp;lt;b&amp;gt;&lt;/span&gt;strace&lt;span class="nt"&gt;&amp;lt;/b&amp;gt;&amp;lt;br/&amp;gt;&lt;/span&gt;syscalls&lt;span class="w"&gt; &lt;/span&gt;+&lt;span class="w"&gt; &lt;/span&gt;IPC&lt;span class="w"&gt; &lt;/span&gt;payloads&amp;quot;]
&lt;span class="w"&gt;    &lt;/span&gt;WebKit&lt;span class="w"&gt; &lt;/span&gt;-.observed&lt;span class="w"&gt; &lt;/span&gt;by.-&amp;gt;&lt;span class="w"&gt; &lt;/span&gt;I[&amp;quot;&lt;span class="nt"&gt;&amp;lt;b&amp;gt;&lt;/span&gt;WebKit&lt;span class="w"&gt; &lt;/span&gt;Inspector&lt;span class="nt"&gt;&amp;lt;/b&amp;gt;&amp;lt;br/&amp;gt;&lt;/span&gt;JavaScript&lt;span class="w"&gt; &lt;/span&gt;heap&amp;quot;]
&lt;span class="w"&gt;    &lt;/span&gt;Memory&lt;span class="w"&gt; &lt;/span&gt;-.observed&lt;span class="w"&gt; &lt;/span&gt;by.-&amp;gt;&lt;span class="w"&gt; &lt;/span&gt;SM[&amp;quot;&lt;span class="nt"&gt;&amp;lt;b&amp;gt;&lt;/span&gt;smaps_rollup&lt;span class="w"&gt; &lt;/span&gt;sampler&lt;span class="nt"&gt;&amp;lt;/b&amp;gt;&amp;lt;br/&amp;gt;&lt;/span&gt;memory&lt;span class="w"&gt; &lt;/span&gt;by&lt;span class="w"&gt; &lt;/span&gt;category&amp;quot;]
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;&lt;em&gt;Four tools, one per layer of Handy's execution surface. None of them sees the whole picture alone.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Four tools, synchronized. Each one sees what the others can't; together, they cover the entire execution surface where allocations could come from. &lt;strong&gt;This is The Framework's Multi-Sensory Feedback System&lt;/strong&gt; adapted for chasing memory leaks in a complex desktop app -- &lt;em&gt;Visual&lt;/em&gt;, &lt;em&gt;Auditory&lt;/em&gt;, and &lt;em&gt;Tactile&lt;/em&gt; senses, instantiated for runtime memory analysis instead of UI rendering. &lt;/p&gt;
&lt;p&gt;&lt;strong&gt;This was also massive&lt;/strong&gt;: the Implementation Loop requires that AI agents have digital senses to successfully observe the target -- runtime memory footprint, in this case -- and close the feedback loop. As explained in The Framework, closing the loop is critical to success.&lt;/p&gt;
&lt;h3 id="3-narrowed-systematically-until-only-one-candidate-remained"&gt;3. Narrowed systematically until only one candidate remained&lt;/h3&gt;
&lt;p&gt;Here's where the engineering discipline goes full on. Most "AI debugging" starts with a hypothesis and tries to confirm it -- which is just expensive vibe coding with extra steps. I started with the data. &lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;We had eight candidate mechanisms that could explain what we were seeing; five iterative measurements later, we had one. &lt;/li&gt;
&lt;li&gt;The smaps sampler localized growth to a specific WebKit subprocess (the recording overlay). &lt;/li&gt;
&lt;li&gt;The WebKit heap snapshot diff ruled out JavaScript -- 1.2% of the growth, a rounding error. &lt;/li&gt;
&lt;li&gt;The smaps category breakdown ruled out shared libraries -- 100% of growth in private-anonymous pages, i.e., C++ allocations. &lt;/li&gt;
&lt;li&gt;uftrace and strace identified the upstream driver: &lt;code&gt;emit_levels&lt;/code&gt; firing at 24 Hz, dual-broadcasting events to a hidden overlay subprocess the user never even sees. &lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This was a constraint-satisfaction problem reduced to one surviving candidate -- and by the time it was, the only intervention left was cutting the event source.&lt;/p&gt;
&lt;h3 id="4-fixed-it-three-ways-tested-each-piece-on-its-own-and-together"&gt;4. Fixed it three ways, tested each piece on its own and together&lt;/h3&gt;
&lt;p&gt;The fix had three candidate components, each wired to a runtime switch -- an environment variable I could flip on or off without rebuilding the app. Then I ran the same memory test against several configurations: all switches off (does the leak still reproduce? yes), each switch on by itself (what does that one piece contribute?), and all switches on (combined effect). The numbers per configuration told me which pieces were doing work and which weren't pulling their share. &lt;strong&gt;One of the three -- throttling the event rate from ~24 Hz to 20 Hz -- had no measurable effect and was decidedly cut from the PR.&lt;/strong&gt; The other two carried the fix. This was impossible to know without the measurements.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;"What were the actual fixes for the bloody memory leak?"&lt;/strong&gt; he asked, brows furrowed with frustration and anticipation.&lt;/p&gt;
&lt;p&gt;Handy has a cool voice visualizer feature (off by default) that bounces about to show microphone input activity. The memory leak lives in this, though it's not as straightforward as it sounds, as the path from mic to visualizer runs through the previously mentioned multi-runtime execution. The &lt;em&gt;actual&lt;/em&gt; memory leak lives upstream of Handy in WebKit -- specifically in the recording overlay's WebKit subprocess -- not in Handy's codebase, which means we couldn't fix it directly so we had to be a bit clever in how we mitigated the leak in Handy itself by starving the upstream of the event traffic that drives the leak.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Don't send voice visualizer data when the overlay is turned off.&lt;/strong&gt; This turned out to be the biggest contributor to the memory leak and, as the overlay is off by default, this fix helps most users. Even when it's off, there's a hidden WebKit subprocess that is still alive, still listening and still leaking (😭). &lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Cache the overlay-enabled state in an &lt;code&gt;AtomicBool&lt;/code&gt;, check it at the top of &lt;code&gt;emit_levels&lt;/code&gt;, return early if the overlay is off. For most Linux users, the event source just goes silent. The leak source is starved.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;Narrow the way that voice visualizer data are sent when the overlay is turned on.&lt;/strong&gt; The original implementation was 2 global broadcasts of microphone level events, sent to every webview, twice per audio callback. This was replaced with a targeted emission directly to the overlay which halved the WebKit dispatch work per callback.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;It's a quirk of Tauri 2: both &lt;code&gt;AppHandle::emit&lt;/code&gt; and &lt;code&gt;WebviewWindow::emit&lt;/code&gt; are global broadcasts. Replace the dual call with a single &lt;code&gt;app.emit_to("recording_overlay", "mic-level", levels)&lt;/code&gt; &lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;Before the fix:&lt;/strong&gt;&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="n"&gt;flowchart&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;LR&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;Mic&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;microphone input&amp;lt;br/&amp;gt;(audio callback ~24 Hz)&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;--&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;EL&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;emit_levels&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;EL&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;--&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;AHE&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;app_handle.emit&amp;lt;br/&amp;gt;(global broadcast)&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;EL&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;--&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;OWE&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;overlay_window.emit&amp;lt;br/&amp;gt;(also a global broadcast)&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;AHE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;--&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Overlay&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;recording_overlay&amp;lt;br/&amp;gt;WebKit subprocess&amp;lt;br/&amp;gt;(hidden when overlay off,&amp;lt;br/&amp;gt;but still alive and listening)&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;OWE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;--&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Overlay&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;AHE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;--&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Others&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;other webviews&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;OWE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;--&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Others&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;Overlay&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;--&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Leak&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;💥 WebKit C++ allocations&amp;lt;br/&amp;gt;(unbounded growth)&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;After the fix:&lt;/strong&gt;&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="n"&gt;flowchart&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;LR&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;Mic&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;microphone input&amp;lt;br/&amp;gt;(audio callback ~24 Hz)&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;--&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;EL&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;emit_levels&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;EL&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;--&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Check&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;AtomicBool check:&amp;lt;br/&amp;gt;is overlay enabled?&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;Check&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;--&amp;gt;|&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;no (most Linux users)&amp;quot;&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Drop&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;return early&amp;lt;br/&amp;gt;(no emit, no leak)&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;Check&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;--&amp;gt;|&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;yes&amp;quot;&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;ET&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;app.emit_to&amp;lt;br/&amp;gt;(recording_overlay only)&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;ET&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;--&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Overlay&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;recording_overlay&amp;lt;br/&amp;gt;WebKit subprocess&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;Overlay&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;--&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Bounded&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;✅ WebKit C++ allocations&amp;lt;br/&amp;gt;(bounded)&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;&lt;em&gt;Before: events fanned out to every webview, twice per audio callback, with the hidden overlay still receiving and leaking. After: events are gated on overlay state, and when they do fire, they go straight to the overlay only.&lt;/em&gt;&lt;/p&gt;
&lt;h3 id="5-three-rounds-of-adversarial-review"&gt;5. Three rounds of adversarial review&lt;/h3&gt;
&lt;p&gt;Even with all of the good engineering work done thus far, before a single claim went into the PR description, agents were explicitly tasked to try to break the analysis. &lt;em&gt;Are you actually measuring what you think you're measuring? Is that a sustained leak or just warmup? Does the data distinguish your hypothesis from alternatives?&lt;/em&gt; &lt;/p&gt;
&lt;p&gt;This three-stage adversarial review puts the artifact through:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;a claim check (every assertion classified by evidence type: measured, verified, inferred, assumed), &lt;/li&gt;
&lt;li&gt;then three reviewer subagents role-playing the audience (the Handy maintainer, a Tauri developer, a WebKit memory expert) with no investigation context, &lt;/li&gt;
&lt;li&gt;then two cold reviewers with zero memory of prior drafts. &lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Agents in each round have a clear objective: tear it apart. Find anything wrong, misleading, or that wastes a maintainer's time. &lt;strong&gt;The PR doesn't bullshit because it's been stress-tested by independent agents whose job was to find the bullshit.&lt;/strong&gt; This was particularly important as I didn't have language familiarity to fallback on as I reviewed the PR.&lt;/p&gt;
&lt;h3 id="6-the-meatbag-in-the-loop-soak"&gt;6. The meatbag-in-the-loop soak&lt;/h3&gt;
&lt;p&gt;Controlled captures are great for proving things in a lab. But the fix had to run on my machine, in production, with days of normal use, before I'd call it done. Memory leaks accumulate over hours and days, not minutes -- and the bug that initially took me down had occurred after 36 hours of continuous use. So I ran the patched build for myself, in actual daily work, for six days straight (well, precisely, it was 5 days, 18.5 hours) across seven separate launches, while a tiny shell script polled every 30 seconds and dumped per-process memory readings into a CSV. The data shows the fix worked. &lt;strong&gt;And honestly, the more important validation: I -- as a human, daily user of this tool -- am writing this very article using the patched build right now.&lt;/strong&gt; If it had broken something, I'd have noticed, and I'd have the data to identify exactly what had gone wrong. Buuuuuuuuuuut it's working brilliantly, due to the application of the engineering discipline prescribed in The Framework!&lt;/p&gt;
&lt;h2 id="whats-next-then"&gt;What's next, then?&lt;/h2&gt;
&lt;p&gt;With all of this real software engineering bolstering it, I submitted &lt;a href="https://github.com/cjpais/Handy/pull/1447"&gt;the pull request&lt;/a&gt; to fix the memory leak. Now we play the waiting game ...&lt;/p&gt;
&lt;div align=center&gt;

&lt;iframe width="560" height="315" src="https://www.youtube.com/embed/9JVNMmsN3Co?si=YX_t6yS439Ifi1_2" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen&gt;&lt;/iframe&gt;

&lt;/div&gt;

&lt;h2 id="you-too-can-be-an-ai-agents-ninja"&gt;You, too, can be an AI agents ninja!&lt;/h2&gt;
&lt;p&gt;Are you struggling with bugs in daily driver open source tools that you love to use? Lift this approach. Fix them. &lt;a href="https://x.com/CoreyGallon"&gt;Tell me how it went&lt;/a&gt;!&lt;/p&gt;
&lt;p&gt;I &lt;em&gt;literally&lt;/em&gt; had my AI agents write-up the &lt;a href="https://gist.github.com/captivus/9913ceda5fb3a9c25329d8afc5fd19f2"&gt;methodology&lt;/a&gt; and &lt;a href="https://gist.github.com/captivus/0a14be7e9ba65a7305f95d6ac0b8b6c7"&gt;instrumentation&lt;/a&gt; so that yours could learn and apply them. Give those to them, along with this article, and let them rip.&lt;/p&gt;
&lt;p&gt;Made it this far? Well done, you. Now go and transcend vibe slop with all you've read!&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;strong&gt;Feature Image Prompt:&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Generate an image. The aesthetic should be cyberpunk with colors of neon pink, blue and purple. Do not add any people. A glowing microphone on a dark workbench, wired into a translucent holographic diagram of a multi-process application — three stacked subsystems (a Rust core, an IPC bridge, and a WebKit subprocess) rendered as luminous geometric containers. From the WebKit container, glowing memory blocks balloon outward in an unbounded cascade, depicting a runaway memory leak. Surrounding the diagram, four floating instrumentation panels emit thin neon beams of light into each subsystem, like sensory probes converging on the leak. In the foreground, a severed neon data stream shows the leak being starved at its source, with bounded, contained allocation blocks on the opposite side. The scene sits inside a darkened engineering workshop with circuit-trace patterns etched into the walls, volumetric haze, and reflective surfaces catching the neon glow.&lt;/p&gt;</content><category term="Writing"/><category term="agentic_coding"/><category term="rust"/><category term="claude_code"/></entry><entry><title>Unlocking Codex GPT-5.4's Full 1M-Token Context Window</title><link href="https://gallon.me/getting-access-to-the-full-1-million-token-context-window-in-codex-gpt-54.html" rel="alternate"/><published>2026-05-25T00:00:00-05:00</published><updated>2026-05-25T00:00:00-05:00</updated><author><name>Corey</name></author><id>tag:gallon.me,2026-05-25:/getting-access-to-the-full-1-million-token-context-window-in-codex-gpt-54.html</id><summary type="html">&lt;p&gt;&lt;em&gt;Update (July 2026): as of Codex 0.144.4, this technique no longer works -- the &lt;code&gt;model_context_window&lt;/code&gt; override is now ignored, and context ceilings are enforced server-side. See the full update at the end of this post.&lt;/em&gt;&lt;/p&gt;</summary><content type="html">&lt;p&gt;&lt;em&gt;Update (July 2026): as of Codex 0.144.4, this technique no longer works -- the &lt;code&gt;model_context_window&lt;/code&gt; override is now ignored, and context ceilings are enforced server-side. See the full update at the end of this post.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;GPT-5.4 supports a million-token context window but Codex launches it at 272,000.&lt;/p&gt;
&lt;p&gt;Codex doesn't ask GPT-5.4 how many tokens it can handle. It looks up the answer in a &lt;strong&gt;model catalog&lt;/strong&gt; -- a JSON file that ships with Codex listing every available model alongside its capabilities: name, context window, supported features. The default catalog sets GPT-5.4's &lt;code&gt;context_window&lt;/code&gt; to 272,000. As far as Codex is concerned, that's the ceiling.&lt;/p&gt;
&lt;p&gt;Codex &lt;strong&gt;profiles&lt;/strong&gt; let you create named configurations -- a different model, different sandbox rules, different settings -- and switch between them with &lt;code&gt;--profile name&lt;/code&gt;. Each profile lives in its own file at &lt;code&gt;~/.codex/&amp;lt;name&amp;gt;.config.toml&lt;/code&gt;, using the same top-level keys as &lt;code&gt;~/.codex/config.toml&lt;/code&gt;. Anything you set in a profile file overrides the matching value in &lt;code&gt;config.toml&lt;/code&gt; for sessions launched under that profile.&lt;/p&gt;
&lt;p&gt;That includes &lt;code&gt;model_context_window&lt;/code&gt;.&lt;/p&gt;
&lt;h2 id="the-profile-that-unlocks-1m"&gt;The profile that unlocks 1M&lt;/h2&gt;
&lt;p&gt;Create &lt;code&gt;~/.codex/gpt54_1m.config.toml&lt;/code&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;gpt-5.4&amp;quot;&lt;/span&gt;
&lt;span class="n"&gt;model_context_window&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1000000&lt;/span&gt;
&lt;span class="n"&gt;model_auto_compact_token_limit&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;900000&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Three lines. &lt;code&gt;model_context_window&lt;/code&gt; tells Codex to use a 1M-token window for sessions launched under this profile. &lt;code&gt;model_auto_compact_token_limit&lt;/code&gt; triggers conversation compaction at 900K, leaving headroom before the effective ceiling.&lt;/p&gt;
&lt;p&gt;Launch with:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;codex&lt;span class="w"&gt; &lt;/span&gt;--profile&lt;span class="w"&gt; &lt;/span&gt;gpt54_1m
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h2 id="verifying-it-works"&gt;Verifying it works&lt;/h2&gt;
&lt;p&gt;Inside the Codex TUI, &lt;code&gt;/status&lt;/code&gt; shows the context window size. For a definitive check, inspect the rollout JSON from your session:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="nv"&gt;latest_rollout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;find&lt;span class="w"&gt; &lt;/span&gt;~/.codex/sessions&lt;span class="w"&gt; &lt;/span&gt;-name&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;rollout-*.jsonl&amp;#39;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-printf&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;%T@ %p\n&amp;#39;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;sort&lt;span class="w"&gt; &lt;/span&gt;--numeric-sort&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;tail&lt;span class="w"&gt; &lt;/span&gt;--lines&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;cut&lt;span class="w"&gt; &lt;/span&gt;--delimiter&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39; &amp;#39;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;--fields&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt;-
&lt;span class="k"&gt;)&lt;/span&gt;

jq&lt;span class="w"&gt; &lt;/span&gt;-r&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;&lt;/span&gt;
&lt;span class="s1"&gt;  select(.type == &amp;quot;event_msg&amp;quot; and .payload.type == &amp;quot;task_started&amp;quot;)&lt;/span&gt;
&lt;span class="s1"&gt;  | .payload.model_context_window&lt;/span&gt;
&lt;span class="s1"&gt;&amp;#39;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="nv"&gt;$latest_rollout&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;This returns 950,000. GPT-5.4 applies a 95% effective context window internally, so the runtime ceiling is 950,000 of the catalog's 1,000,000.&lt;/p&gt;
&lt;p&gt;To prove the window is actually usable end-to-end -- not just declared in metadata -- pipe several hundred thousand tokens of input to a single-turn &lt;code&gt;codex exec&lt;/code&gt; and check that the rollout records the full input acceptance:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;codex&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;exec&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;--model&lt;span class="w"&gt; &lt;/span&gt;gpt-5.4&lt;span class="w"&gt; &lt;/span&gt;-c&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;model_context_window=1000000&amp;#39;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;--skip-git-repo-check&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;Reply with only the literal word RECEIVED.&amp;#39;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&amp;lt;&lt;span class="w"&gt; &lt;/span&gt;big_input_file
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;On my Plus account, this accepted a single turn with 591,580 input tokens -- well above the 272,000 default ceiling.&lt;/p&gt;
&lt;h2 id="why-this-works-now-and-didnt-before"&gt;Why this works now (and didn't before)&lt;/h2&gt;
&lt;p&gt;Until Codex 0.134.0, profiles lived inside &lt;code&gt;~/.codex/config.toml&lt;/code&gt; as &lt;code&gt;[profiles.X]&lt;/code&gt; tables -- and that table parser silently dropped &lt;code&gt;model_context_window&lt;/code&gt; (&lt;a href="https://github.com/openai/codex/issues/14456"&gt;openai/codex#14456&lt;/a&gt;). Setting the key inside &lt;code&gt;[profiles.X]&lt;/code&gt; did nothing. The 0.134.0 release moved profiles into their own files at &lt;code&gt;~/.codex/&amp;lt;name&amp;gt;.config.toml&lt;/code&gt;, using top-level keys where the override was never broken. The bug fix was incidental to the file-shape change.&lt;/p&gt;
&lt;h2 id="what-about-gpt-55"&gt;What about GPT-5.5?&lt;/h2&gt;
&lt;p&gt;GPT-5.5 has the same shape of gap: OpenAI's API docs document a 1,050,000-token context window, but Codex's catalog still ships it at 272,000.&lt;/p&gt;
&lt;p&gt;The same simple override does &lt;em&gt;not&lt;/em&gt; work for GPT-5.5 on the Plus tier (internally labeled &lt;code&gt;prolite&lt;/code&gt; in Codex's rate-limit telemetry). I verified this by stress-testing both models on the same account, back to back:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;GPT-5.4 with &lt;code&gt;model_context_window = 1000000&lt;/code&gt;: a single-turn session accepted &lt;strong&gt;591,580&lt;/strong&gt; input tokens.&lt;/li&gt;
&lt;li&gt;GPT-5.5 with an analogous setup: real working sessions hit "Codex ran out of room in the model's context window" three times in a row, each time when a single turn's input crossed ~270,000 tokens. Codex's &lt;code&gt;/status&lt;/code&gt; reported a 998K window throughout; the OpenAI API rejected the requests anyway.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;So the override-tells-Codex layer worked for both models, but only the GPT-5.4 API actually honored it for Plus. The catalog patch convinces Codex it has more headroom; the server still enforces the plan-tier ceiling for GPT-5.5.&lt;/p&gt;
&lt;p&gt;I haven't tested this on Pro / Enterprise / API access, so I can't say whether the limit relaxes there. If you're on a higher tier and want to find out, the test is: stress-test a GPT-5.5 1M profile with several hundred thousand tokens of single-turn input and read the rollout's &lt;code&gt;last_token_usage.input_tokens&lt;/code&gt;. If it succeeds, the gate is plan-tier; if it fails the same way, the cap is universal on Codex.&lt;/p&gt;
&lt;h2 id="update-july-2026-codex-01444-this-technique-no-longer-works"&gt;Update -- July 2026 (Codex 0.144.4): this technique no longer works&lt;/h2&gt;
&lt;p&gt;Since I wrote this, Codex changed how it resolves context windows, and the override above stopped doing anything. I re-tested the whole thing on the same subscription account against Codex 0.144.4, in an isolated config directory so nothing touched my live setup. Three things changed.&lt;/p&gt;
&lt;h3 id="1-the-model_context_window-override-is-ignored"&gt;1. The &lt;code&gt;model_context_window&lt;/code&gt; override is ignored&lt;/h3&gt;
&lt;p&gt;Whether you set it in a profile file (&lt;code&gt;codex --profile gpt54_1m&lt;/code&gt;) or inline (&lt;code&gt;codex exec -c 'model_context_window=1000000'&lt;/code&gt;), the key no longer changes anything. A session launched with the 1M profile still reports a &lt;strong&gt;258,400&lt;/strong&gt;-token window -- 95% of the 272,000 catalog default. The setting parses without error and has no effect.&lt;/p&gt;
&lt;h3 id="2-the-catalog-now-drives-only-the-displayed-window-not-the-enforced-one"&gt;2. The catalog now drives only the &lt;em&gt;displayed&lt;/em&gt; window -- not the enforced one&lt;/h3&gt;
&lt;p&gt;Codex 0.144.4 reads the window from the model catalog (&lt;code&gt;~/.codex/models_cache.json&lt;/code&gt;), and editing an entry's &lt;code&gt;context_window&lt;/code&gt; does change the number Codex &lt;em&gt;shows&lt;/em&gt; you. But that number is cosmetic. I patched GPT-5.5's catalog entry down to 100,000 -- Codex then displayed a 95,000-token window -- and a &lt;strong&gt;132,606&lt;/strong&gt;-token input still went through. I patched it up to 1,000,000 -- displayed 950,000 -- and inputs above ~272,000 tokens were still rejected client-side with "ran out of room in the model's context window."&lt;/p&gt;
&lt;p&gt;The real limit is enforced server-side, per model and plan tier, and no client-side edit moves it in either direction. (Codex also re-fetches the catalog from the server on a timer, silently overwriting your edit unless you make the file read-only.)&lt;/p&gt;
&lt;h3 id="3-theres-now-a-hard-1-mib-cap-on-a-single-input-message"&gt;3. There's now a hard 1 MiB cap on a single input message&lt;/h3&gt;
&lt;p&gt;Independent of tokens, Codex rejects any single message over &lt;strong&gt;1,048,576 characters&lt;/strong&gt; with &lt;code&gt;input_too_large&lt;/code&gt;, before it ever checks the context window. The original end-to-end test in this post -- piping a 591,580-token file to one &lt;code&gt;codex exec&lt;/code&gt; -- can't even run today: that much text is well over 1 MiB.&lt;/p&gt;
&lt;h3 id="what-the-windows-actually-are-now"&gt;What the windows actually are now&lt;/h3&gt;
&lt;p&gt;Measured by feeding single-turn inputs of increasing size and reading the accepted &lt;code&gt;input_tokens&lt;/code&gt; straight from each session's rollout JSON:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;GPT-5.5&lt;/strong&gt; -- accepts 268,606 tokens; the next step up is rejected. Usable ceiling ~272,000.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;GPT-5.6&lt;/strong&gt; (it ships as three variants -- &lt;code&gt;sol&lt;/code&gt;, &lt;code&gt;terra&lt;/code&gt;, &lt;code&gt;luna&lt;/code&gt;) -- &lt;code&gt;sol&lt;/code&gt; accepted 356,472 tokens and rejected around 392K. Its usable window is clearly larger than 5.5's, but still short of 400K.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;GPT-5.4&lt;/strong&gt; -- accepted 291,221 tokens (I didn't push it to its ceiling). It's the only model whose catalog still carries &lt;code&gt;max_context_window: 1000000&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Every one of these reported the same cosmetic 258,400-token window regardless of the model.&lt;/p&gt;
&lt;p&gt;The practical upshot inverts the original premise. You can no longer unlock a bigger window by editing config -- the number you &lt;em&gt;can&lt;/em&gt; change is the one that doesn't matter. But for the newer models you don't need to: GPT-5.6 already grants more usable context than GPT-5.5 by default. GPT-5.5 is the outlier, capped lowest of the three -- the same regression subscription users flagged in &lt;a href="https://github.com/openai/codex/issues/19464"&gt;openai/codex#19464&lt;/a&gt;, where 5.5 gives &lt;em&gt;less&lt;/em&gt; long-context headroom than 5.4 did.&lt;/p&gt;
&lt;p&gt;As before, this is one ChatGPT-subscription account. On Pro / Enterprise / API the server-enforced ceilings may well be higher -- I haven't tested them.&lt;/p&gt;</content><category term="TIL"/><category term="codex"/><category term="configuration"/><category term="OpenAI"/></entry><entry><title>Getting Handy to work in GNOME Wayland (Ubuntu 25.10+)</title><link href="https://gallon.me/getting-handy-to-work-in-gnome-wayland-ubuntu-2510.html" rel="alternate"/><published>2026-05-15T00:00:00-05:00</published><updated>2026-05-15T00:00:00-05:00</updated><author><name>Corey</name></author><id>tag:gallon.me,2026-05-15:/getting-handy-to-work-in-gnome-wayland-ubuntu-2510.html</id><summary type="html">&lt;p&gt;&lt;a href="https://handy.computer/"&gt;Handy&lt;/a&gt; (CJ Pais's Tauri 2.x speech-to-text desktop app, v0.8.3 Linux AppImage) is the chosen dictation tool. The required workflow:&lt;/p&gt;</summary><content type="html">&lt;h2 id="background-what-were-trying-to-achieve"&gt;Background &amp;amp; what we’re trying to achieve&lt;/h2&gt;
&lt;p&gt;&lt;a href="https://handy.computer/"&gt;Handy&lt;/a&gt; (CJ Pais's Tauri 2.x speech-to-text desktop app, v0.8.3 Linux AppImage) is the chosen dictation tool. The required workflow:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Press a global keyboard shortcut to start recording.&lt;/li&gt;
&lt;li&gt;Speak.&lt;/li&gt;
&lt;li&gt;Press the shortcut again (toggle mode) to stop. Handy transcribes locally via Whisper.&lt;/li&gt;
&lt;li&gt;The transcribed text appears in the focused field of whichever app the cursor was in (terminal, GUI text editor, browser text field, Electron app -- universal).&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;On Ubuntu 25.10 / GNOME 49 / Wayland, neither of those steps works out of the box. The keyboard shortcut only fires when Handy's window itself is focused, and Handy's default text-delivery mechanism fails because Mutter doesn't implement the protocol it depends on. Both problems are solvable from outside Handy with no upstream patch. Here’s how.&lt;/p&gt;
&lt;h2 id="the-solution-overview"&gt;The solution overview&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;The dictation workflow has two distinct failure points on GNOME Wayland, each requiring a "go-around" fix.&lt;/strong&gt; Neither can be addressed by changing Handy's settings alone. Both fixes work by replacing an in-process mechanism that GNOME Wayland refuses to support with an external mechanism that the system already permits.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Failure&lt;/th&gt;
&lt;th&gt;Why it fails on GNOME Wayland&lt;/th&gt;
&lt;th&gt;The go-around&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Global hotkey doesn't fire when Handy is unfocused&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Tauri's &lt;code&gt;global-shortcut&lt;/code&gt; plugin grabs keys via Wayland's keyboard listener, which by design only delivers to the &lt;em&gt;focused&lt;/em&gt; surface. So Tauri's "global" shortcut is in fact window-local.&lt;/td&gt;
&lt;td&gt;A &lt;strong&gt;GNOME custom keybinding&lt;/strong&gt; at the WM level. GNOME's keybinding handler runs in &lt;code&gt;gnome-shell&lt;/code&gt; and fires regardless of which client has focus. It invokes Handy's CLI which IPCs the running daemon.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Transcribed text isn't typed/pasted into the focused window&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Handy's typing tool defaults route through &lt;code&gt;wtype&lt;/code&gt;, which needs the &lt;code&gt;zwp_virtual_keyboard_v1&lt;/code&gt; Wayland protocol. &lt;strong&gt;Mutter &lt;/strong&gt;(GNOME’s window manager &amp;amp; compositor)&lt;strong&gt; intentionally does not implement it&lt;/strong&gt; (it's a wlroots-only protocol). Every paste method that simulates a keystroke through wtype fails.&lt;/td&gt;
&lt;td&gt;An &lt;strong&gt;External Script&lt;/strong&gt; paste method invoking &lt;strong&gt;&lt;code&gt;ydotool type&lt;/code&gt;&lt;/strong&gt;, which synthesizes input via &lt;code&gt;/dev/uinput&lt;/code&gt;. uinput is kernel-level -- the kernel forges input events that propagate through libinput to whichever app is focused, with no need for any Wayland protocol Mutter is missing.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;See the &lt;a href="#appendix-why-simpler-fixes-dont-work"&gt;appendix&lt;/a&gt; for why simpler fixes don’t work.&lt;/p&gt;
&lt;h3 id="a-note-on-the-tauri-typing-tool-dropdown"&gt;A note on the Tauri Typing Tool dropdown&lt;/h3&gt;
&lt;p&gt;Handy's UI on this system shows only &lt;code&gt;Auto (Recommended)&lt;/code&gt; and &lt;code&gt;wtype&lt;/code&gt; in the Typing Tool dropdown. The v0.8.3 source code at &lt;code&gt;src-tauri/src/settings.rs&lt;/code&gt; actually defines the full enum &lt;code&gt;TypingTool { Auto, Wtype, Kwtype, Dotool, Ydotool, Xdotool }&lt;/code&gt;. Handy's UI hides options whose backing binary isn't on &lt;code&gt;PATH&lt;/code&gt;. After &lt;code&gt;apt install ydotool&lt;/code&gt;, &lt;code&gt;Ydotool&lt;/code&gt; does appear in the dropdown -- but it expects 1.x semantics, so it doesn't actually work with the apt version. That's why we use External Script instead of flipping the dropdown.&lt;/p&gt;
&lt;h2 id="implementing-the-fixes"&gt;Implementing the fixes&lt;/h2&gt;
&lt;p&gt;NB — you’ll want to search and replace “\&amp;lt;your username&amp;gt;” with your actual username.&lt;/p&gt;
&lt;h3 id="problem-1-global-hotkey-delivery"&gt;Problem 1: global hotkey delivery&lt;/h3&gt;
&lt;h4 id="step-1-bind-a-gnome-custom-keyboard-shortcut-with-absolute-path"&gt;Step 1: Bind a GNOME custom keyboard shortcut (with absolute path)&lt;/h4&gt;
&lt;p&gt;GNOME Settings -&amp;gt; Keyboard -&amp;gt; Custom Shortcuts, or via &lt;code&gt;gsettings&lt;/code&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# Inspect current custom-keybindings list first:&lt;/span&gt;
gsettings&lt;span class="w"&gt; &lt;/span&gt;get&lt;span class="w"&gt; &lt;/span&gt;org.gnome.settings-daemon.plugins.media-keys&lt;span class="w"&gt; &lt;/span&gt;custom-keybindings
&lt;span class="c1"&gt;# Add an entry path if needed:&lt;/span&gt;
gsettings&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;set&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;org.gnome.settings-daemon.plugins.media-keys&lt;span class="w"&gt; &lt;/span&gt;custom-keybindings&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;[&amp;#39;/org/gnome/settings-daemon/plugins/media-keys/custom-keybindings/custom1/&amp;#39;]&amp;quot;&lt;/span&gt;

gsettings&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;set&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;org.gnome.settings-daemon.plugins.media-keys.custom-keybinding:/org/gnome/settings-daemon/plugins/media-keys/custom-keybindings/custom1/&lt;span class="w"&gt; &lt;/span&gt;name&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;Handy Transcription&amp;#39;&lt;/span&gt;
gsettings&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;set&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;org.gnome.settings-daemon.plugins.media-keys.custom-keybinding:/org/gnome/settings-daemon/plugins/media-keys/custom-keybindings/custom1/&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;command&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;/home/&amp;lt;your username&amp;gt;/.local/bin/handy --toggle-transcription&amp;#39;&lt;/span&gt;
gsettings&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;set&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;org.gnome.settings-daemon.plugins.media-keys.custom-keybinding:/org/gnome/settings-daemon/plugins/media-keys/custom-keybindings/custom1/&lt;span class="w"&gt; &lt;/span&gt;binding&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;&amp;lt;Control&amp;gt;space&amp;#39;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Critical detail:&lt;/strong&gt; the Command field must be an &lt;strong&gt;absolute path&lt;/strong&gt;. GNOME's keybinding handler runs with the session PATH set at graphical login from &lt;code&gt;~/.config/environment.d/&lt;/code&gt;, &lt;em&gt;not&lt;/em&gt; from &lt;code&gt;~/.zshrc&lt;/code&gt;. On Ubuntu 25.10 that PATH is &lt;code&gt;/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/snap/bin:/snap/bin&lt;/code&gt; -- no &lt;code&gt;~/.local/bin&lt;/code&gt;. A bare &lt;code&gt;handy --toggle-transcription&lt;/code&gt; silently fails to resolve. (Long-term cleaner fix: add &lt;code&gt;PATH=...:%h/.local/bin&lt;/code&gt; to &lt;code&gt;~/.config/environment.d/path.conf&lt;/code&gt;, which makes the keybinding handler and any future custom shortcuts find user-local binaries automatically. Optional.)&lt;/p&gt;
&lt;h4 id="step-2-ensure-handy-daemon-is-running"&gt;Step 2: Ensure Handy daemon is running&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;handy --toggle-transcription&lt;/code&gt; is a DBus IPC to the running Handy daemon. With no daemon running, the command exits 0 with no effect (a particularly silent failure mode). Handy creates an autostart entry at &lt;code&gt;~/.config/autostart/Handy.desktop&lt;/code&gt; automatically when &lt;code&gt;autostart_enabled: true&lt;/code&gt; is set in its settings. Edit that file's &lt;code&gt;Exec=&lt;/code&gt; line to add &lt;code&gt;--start-hidden&lt;/code&gt; if you don't want Handy's window to pop visibly on every login:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;Exec&lt;/span&gt;&lt;span class="o"&gt;=/&lt;/span&gt;&lt;span class="nv"&gt;home&lt;/span&gt;&lt;span class="o"&gt;/&amp;lt;&lt;/span&gt;&lt;span class="nv"&gt;your&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;username&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;/&lt;/span&gt;.&lt;span class="nv"&gt;local&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nv"&gt;bin&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nv"&gt;handy&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="nv"&gt;start&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nv"&gt;hidden&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;To start the daemon for the current session without rebooting:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;nohup&lt;span class="w"&gt; &lt;/span&gt;/home/&amp;lt;your&lt;span class="w"&gt; &lt;/span&gt;username&amp;gt;/.local/bin/handy&lt;span class="w"&gt; &lt;/span&gt;--start-hidden&lt;span class="w"&gt; &lt;/span&gt;&amp;gt;/tmp/handy-startup.log&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt;&amp;gt;&lt;span class="p"&gt;&amp;amp;&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;&amp;amp;&lt;/span&gt;
&lt;span class="nb"&gt;disown&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h3 id="problem-2-text-delivery-via-ydotool"&gt;Problem 2: text delivery via ydotool&lt;/h3&gt;
&lt;h4 id="step-3-install-ydotool-and-grant-uinput-access"&gt;Step 3: Install ydotool and grant uinput access&lt;/h4&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;sudo&lt;span class="w"&gt; &lt;/span&gt;apt&lt;span class="w"&gt; &lt;/span&gt;install&lt;span class="w"&gt; &lt;/span&gt;-y&lt;span class="w"&gt; &lt;/span&gt;ydotool
&lt;span class="nb"&gt;echo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;KERNEL==&amp;quot;uinput&amp;quot;, GROUP=&amp;quot;input&amp;quot;, MODE=&amp;quot;0660&amp;quot;, OPTIONS+=&amp;quot;static_node=uinput&amp;quot;&amp;#39;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;sudo&lt;span class="w"&gt; &lt;/span&gt;tee&lt;span class="w"&gt; &lt;/span&gt;/etc/udev/rules.d/60-uinput.rules
&lt;span class="nb"&gt;echo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;uinput&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;sudo&lt;span class="w"&gt; &lt;/span&gt;tee&lt;span class="w"&gt; &lt;/span&gt;/etc/modules-load.d/uinput.conf
sudo&lt;span class="w"&gt; &lt;/span&gt;modprobe&lt;span class="w"&gt; &lt;/span&gt;uinput&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;sudo&lt;span class="w"&gt; &lt;/span&gt;udevadm&lt;span class="w"&gt; &lt;/span&gt;control&lt;span class="w"&gt; &lt;/span&gt;--reload-rules&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;sudo&lt;span class="w"&gt; &lt;/span&gt;udevadm&lt;span class="w"&gt; &lt;/span&gt;trigger
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;What each piece does:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;apt install ydotool&lt;/code&gt; -- installs ydotool 0.1.8-3build1 (the legacy monolithic version, no separate daemon; talks to &lt;code&gt;/dev/uinput&lt;/code&gt; directly).&lt;/li&gt;
&lt;li&gt;The udev rule -- when the kernel creates the &lt;code&gt;uinput&lt;/code&gt; device, set its group to &lt;code&gt;input&lt;/code&gt; and mode to &lt;code&gt;0660&lt;/code&gt; so members of the &lt;code&gt;input&lt;/code&gt; group can write to it without sudo. The &lt;code&gt;OPTIONS+="static_node=uinput"&lt;/code&gt; clause makes systemd-udev pre-create the node with the right permissions even before any uevent fires (relevant because uinput is module-loaded, not auto-discovered).&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/etc/modules-load.d/uinput.conf&lt;/code&gt; -- causes &lt;code&gt;systemd-modules-load.service&lt;/code&gt; to &lt;code&gt;modprobe uinput&lt;/code&gt; at boot. Without this, &lt;code&gt;/dev/uinput&lt;/code&gt; won't exist after reboot until something else loads the module.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;modprobe + udevadm&lt;/code&gt; chain -- applies everything immediately without a reboot: load the module now, reload the rules, replay uevents so the new rule's permissions take effect on the just-created node.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Confirm the user is in the &lt;code&gt;input&lt;/code&gt; group (&lt;code&gt;groups&lt;/code&gt; should list it; was already true on this system as Ubuntu 25.10 default).&lt;/p&gt;
&lt;h4 id="step-4-write-the-external-script"&gt;Step 4: Write the External Script&lt;/h4&gt;
&lt;p&gt;Save as &lt;code&gt;/home/&amp;lt;your username&amp;gt;/.local/bin/handy-paste-wl-copy&lt;/code&gt; (name retained for historical reasons -- this script no longer uses wl-copy):&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="ch"&gt;#!/usr/bin/env bash&lt;/span&gt;
&lt;span class="c1"&gt;# Handy External Script: type transcribed text directly into the focused window&lt;/span&gt;
&lt;span class="c1"&gt;# via ydotool&amp;#39;s `type` subcommand. At --key-delay 0 this is faster than paste&lt;/span&gt;
&lt;span class="c1"&gt;# for typical dictation lengths and works universally (terminals, GUI editors,&lt;/span&gt;
&lt;span class="c1"&gt;# Notion, etc.) without depending on each app&amp;#39;s paste-shortcut convention.&lt;/span&gt;

&lt;span class="nb"&gt;set&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-u

&lt;span class="nv"&gt;TEXT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;1&lt;/span&gt;&lt;span class="k"&gt;:-&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;
&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-z&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;TEXT&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;exit&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;

env&lt;span class="w"&gt; &lt;/span&gt;-i&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nv"&gt;HOME&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;HOME&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nv"&gt;USER&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;USER&lt;/span&gt;&lt;span class="k"&gt;:-&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;your username&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nv"&gt;LOGNAME&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;LOGNAME&lt;/span&gt;&lt;span class="k"&gt;:-&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;USER&lt;/span&gt;&lt;span class="k"&gt;:-&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;your username&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;}}&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nv"&gt;PATH&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nv"&gt;LANG&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;LANG&lt;/span&gt;&lt;span class="k"&gt;:-&lt;/span&gt;&lt;span class="nv"&gt;en_US&lt;/span&gt;&lt;span class="p"&gt;.UTF-8&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;/usr/bin/ydotool&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;type&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;--key-delay&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;TEXT&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&amp;gt;/dev/null&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt;&amp;gt;&lt;span class="p"&gt;&amp;amp;&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;

&lt;span class="nb"&gt;exit&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Make it executable: &lt;code&gt;chmod +x /home/&amp;lt;your username&amp;gt;/.local/bin/handy-paste-wl-copy&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Key design choices:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;env -i&lt;/code&gt;&lt;/strong&gt;&lt;strong&gt; sanitization.&lt;/strong&gt; Handy's AppImage runtime prepends its own bundled lib/bin paths to &lt;code&gt;LD_LIBRARY_PATH&lt;/code&gt;, &lt;code&gt;PYTHONHOME&lt;/code&gt;, &lt;code&gt;PYTHONPATH&lt;/code&gt;, &lt;code&gt;GTK_PATH&lt;/code&gt;, etc. for the AppImage process and its children. Without scrubbing, the system &lt;code&gt;/usr/bin/ydotool&lt;/code&gt; would attempt to dynamic-link against the AppImage's bundled libs, which can fail silently or behave erratically. &lt;code&gt;env -i&lt;/code&gt; clears everything and we explicitly pass only what ydotool needs (&lt;code&gt;PATH&lt;/code&gt;, &lt;code&gt;HOME&lt;/code&gt;, &lt;code&gt;USER&lt;/code&gt;, &lt;code&gt;LOGNAME&lt;/code&gt;, &lt;code&gt;LANG&lt;/code&gt;). Note that &lt;code&gt;WAYLAND_DISPLAY&lt;/code&gt; and &lt;code&gt;XDG_RUNTIME_DIR&lt;/code&gt; are &lt;em&gt;not&lt;/em&gt; needed by ydotool because uinput is kernel-level, not a Wayland client.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;ydotool type&lt;/code&gt;&lt;/strong&gt;&lt;strong&gt; with &lt;/strong&gt;&lt;strong&gt;&lt;code&gt;-key-delay 0&lt;/code&gt;&lt;/strong&gt;&lt;strong&gt;.&lt;/strong&gt; Tested empirically: 225 chars completes in \~280ms with default &lt;code&gt;-delay 100&lt;/code&gt; initial wait. The default key-delay (12ms) would make this 2.8s -- noticeable. Zero delay is reliable on this system for typical dictation lengths.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;No clipboard touch.&lt;/strong&gt; Unlike a clipboard + paste-shortcut pattern, this leaves your clipboard contents alone. Earlier versions of the script wrote via &lt;code&gt;wl-copy&lt;/code&gt; and dispatched Ctrl+Shift+V via ydotool; that approach worked in Ghostty but failed in GNOME Text Editor (which doesn't accept Ctrl+Shift+V as paste). Switching to &lt;code&gt;ydotool type&lt;/code&gt; removed the app-by-app paste-shortcut compatibility problem entirely.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id="step-5-configure-handy"&gt;Step 5: Configure Handy&lt;/h4&gt;
&lt;p&gt;In Handy's settings (&lt;code&gt;Advanced&lt;/code&gt; tab):&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Paste Method:&lt;/strong&gt; External Script&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;External Script Path:&lt;/strong&gt; &lt;code&gt;/home/&amp;lt;your username&amp;gt;/.local/bin/handy-paste-wl-copy&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Clipboard Handling:&lt;/strong&gt; Don't Modify (cosmetic; our script doesn't touch the clipboard, so this preference is honored implicitly).&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;These map to fields in &lt;code&gt;~/.local/share/com.pais.handy/settings_store.json&lt;/code&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;quot;paste_method&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;external_script&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="nt"&gt;&amp;quot;external_script_path&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;/home/&amp;lt;your username&amp;gt;/.local/bin/handy-paste-wl-copy&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="nt"&gt;&amp;quot;clipboard_handling&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;dont_modify&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h2 id="verification"&gt;Verification&lt;/h2&gt;
&lt;p&gt;Confirm each link in the chain works.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;ydotool can drive &lt;/strong&gt;&lt;strong&gt;&lt;code&gt;/dev/uinput&lt;/code&gt;&lt;/strong&gt;&lt;strong&gt;:&lt;/strong&gt;&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;ls&lt;span class="w"&gt; &lt;/span&gt;-la&lt;span class="w"&gt; &lt;/span&gt;/dev/uinput
&lt;span class="c1"&gt;# Expected: crw-rw---- root input ... (group input, mode 660)&lt;/span&gt;
&lt;span class="nb"&gt;test&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-w&lt;span class="w"&gt; &lt;/span&gt;/dev/uinput&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;user can write&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;||&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;DENIED&amp;quot;&lt;/span&gt;
ydotool&lt;span class="w"&gt; &lt;/span&gt;key&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;shift&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt;&amp;gt;&lt;span class="p"&gt;&amp;amp;&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;
&lt;span class="c1"&gt;# Should print &amp;quot;ydotoold backend unavailable&amp;quot; (informational, not an error) and exit 0.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;ydotool type delivers fast enough:&lt;/strong&gt;&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="nv"&gt;LONG&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;printf&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;the quick brown fox jumps over the lazy dog. %.0s&amp;#39;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;{&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;..5&lt;span class="o"&gt;}&lt;/span&gt;&lt;span class="k"&gt;)&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Length: &lt;/span&gt;&lt;span class="si"&gt;${#&lt;/span&gt;&lt;span class="nv"&gt;LONG&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;
&lt;span class="nb"&gt;time&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;ydotool&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;type&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;--key-delay&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="nv"&gt;$LONG&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;
&lt;span class="c1"&gt;# Expected: ~250-350ms wall-clock for ~225 chars. Output should land in the focused window with no dropped characters.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Handy delegates to the script:&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;After triggering a transcription, the Handy log at &lt;code&gt;~/.local/share/com.pais.handy/logs/handy.log&lt;/code&gt; should show:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="n"&gt;handy_app_lib::clipboard&lt;/span&gt;&lt;span class="o"&gt;][&lt;/span&gt;&lt;span class="n"&gt;INFO&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;Using&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;paste&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;method&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;ExternalScript&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;delay&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="n"&gt;ms&lt;/span&gt;
&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="n"&gt;handy_app_lib::clipboard&lt;/span&gt;&lt;span class="o"&gt;][&lt;/span&gt;&lt;span class="n"&gt;INFO&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Pasting&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;via&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;external&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;script&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;home&lt;/span&gt;&lt;span class="o"&gt;/&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;your&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;username&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;/&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;local&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;bin&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;handy&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;paste&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;wl&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;copy&lt;/span&gt;
&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="n"&gt;handy_app_lib::actions&lt;/span&gt;&lt;span class="o"&gt;][&lt;/span&gt;&lt;span class="n"&gt;DEBUG&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nc"&gt;Text&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;pasted&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;successfully&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ow"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Nms&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;If the "Text pasted successfully" line shows a duration much greater than \~50ms, the script is hanging -- typically because a subprocess is daemonizing without closing inherited file descriptors. (Earlier wl-copy-based versions of the script hit this; &lt;code&gt;ydotool type&lt;/code&gt; doesn't, but it's the canary to watch for.)&lt;/p&gt;
&lt;h2 id="troubleshooting-failure-modes"&gt;Troubleshooting / failure modes&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Pressing the shortcut does nothing.&lt;/strong&gt; Check &lt;code&gt;gsettings get .../custom1/ command&lt;/code&gt; shows an &lt;em&gt;absolute&lt;/em&gt; path. A bare &lt;code&gt;handy --toggle-transcription&lt;/code&gt; won't resolve from gnome-shell's PATH.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Pressing the shortcut transcribes but no text appears.&lt;/strong&gt; Tail &lt;code&gt;~/.local/share/com.pais.handy/logs/handy.log&lt;/code&gt; while testing. Look for the "Pasting via external script" line. If absent, Handy isn't invoking the script -- check &lt;code&gt;Paste Method = External Script&lt;/code&gt; and the path field. If present but no text appears, run the script manually with a test argument to verify it works in isolation:&lt;br&gt;Should type "manual-test" into whatever's currently focused.&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;/home/&amp;lt;your&lt;span class="w"&gt; &lt;/span&gt;username&amp;gt;/.local/bin/handy-paste-wl-copy&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;manual-test&amp;quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Wrong characters appear (e.g. "2442" instead of text).&lt;/strong&gt; Means ydotool got the wrong syntax (likely a confusion between 0.1.8 key-name and 1.x keycode:state). Confirm the script uses &lt;code&gt;ydotool type&lt;/code&gt; (not &lt;code&gt;ydotool key 29:1 47:1 ...&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Garbled characters with non-US keyboard layout.&lt;/strong&gt; ydotool 0.1.8's &lt;code&gt;type&lt;/code&gt; assumes US QWERTY. Non-US layouts mistranslate. Realistic fixes: switch to &lt;strong&gt;dotool&lt;/strong&gt; (layout-aware; build from &lt;a href="https://sr.ht/"&gt;sr.ht&lt;/a&gt; source -- not in apt) or pursue the &lt;strong&gt;RemoteDesktop portal&lt;/strong&gt; path (Handy upstream PR #689).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;Handy is ready&lt;/code&gt;&lt;/strong&gt;&lt;strong&gt; notification but the window doesn't focus when you click Settings in the tray menu.&lt;/strong&gt; Separate problem -- Handy's Tauri tray icon doesn't expose the standard &lt;code&gt;.Activate&lt;/code&gt; / &lt;code&gt;.ContextMenu&lt;/code&gt; D-Bus methods, only &lt;code&gt;.Scroll&lt;/code&gt; and &lt;code&gt;.SecondaryActivate&lt;/code&gt;. This is a Tauri/dbusmenu activation-token gap, not a GNOME limitation. Workarounds: Alt+Tab to Handy, pin to dock, or middle-click the tray icon (which fires &lt;code&gt;.SecondaryActivate&lt;/code&gt;). Upstream fix would need to come from Tauri.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="appendix-why-simpler-fixes-dont-work"&gt;Appendix: Why simpler fixes don't work&lt;/h2&gt;
&lt;p&gt;Here’s what didn’t work, and why.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Path&lt;/th&gt;
&lt;th&gt;Verdict&lt;/th&gt;
&lt;th&gt;Reason&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Just use Tauri's in-app shortcut&lt;/td&gt;
&lt;td&gt;Half-dead&lt;/td&gt;
&lt;td&gt;Only fires while Handy is focused -- defeats the purpose of dictation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GNOME custom shortcut &lt;code&gt;handy --toggle-transcription&lt;/code&gt; (bare name)&lt;/td&gt;
&lt;td&gt;Dead&lt;/td&gt;
&lt;td&gt;&lt;code&gt;~/.local/bin&lt;/code&gt; is NOT on &lt;code&gt;gnome-shell&lt;/code&gt;'s session PATH; the bare command silently fails to resolve. Absolute path required.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GNOME custom shortcut without a running Handy daemon&lt;/td&gt;
&lt;td&gt;Dead&lt;/td&gt;
&lt;td&gt;&lt;code&gt;handy --toggle-transcription&lt;/code&gt; is an IPC to the running daemon; without daemon, exits 0 with no effect. Daemon must be autostarted.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Paste Method = Direct&lt;/code&gt; (Handy default) on GNOME&lt;/td&gt;
&lt;td&gt;Dead&lt;/td&gt;
&lt;td&gt;Routes through wtype -&amp;gt; fails on Mutter&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Paste Method = Clipboard (Ctrl+V)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Dead&lt;/td&gt;
&lt;td&gt;Writes to clipboard then sends Ctrl+V via wtype -&amp;gt; same Mutter failure. Worse: Handy's error-path tears down its wl-copy child process, so the clipboard ends up empty anyway.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;QT_QPA_PLATFORM=xcb&lt;/code&gt; to force XWayland&lt;/td&gt;
&lt;td&gt;Dead&lt;/td&gt;
&lt;td&gt;Flatpak/AppImage's bundled Qt runtime lacks &lt;code&gt;libxcb-cursor&lt;/code&gt;; DISPLAY isn't forwarded into the sandbox&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Built-in &lt;code&gt;Typing Tool = Ydotool&lt;/code&gt; with apt's ydotool 0.1.8&lt;/td&gt;
&lt;td&gt;Dead&lt;/td&gt;
&lt;td&gt;Handy's dispatch expects ydotool 1.x (client + daemon, &lt;code&gt;keycode:state&lt;/code&gt; syntax). Apt ships 0.1.8 (monolithic, no daemon, key-name syntax). Version mismatch -- the wrong syntax gets used and "2442" gets typed instead of Ctrl+V.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ydotool key 29:1 47:1 47:0 29:0&lt;/code&gt; (1.x syntax)&lt;/td&gt;
&lt;td&gt;Dead&lt;/td&gt;
&lt;td&gt;0.1.8 parses each token as an unknown key name, falls back to typing the first digit of each. "2442" is the literal evidence.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr&gt;
&lt;p&gt;&lt;strong&gt;Feature Image Prompt:&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Generate an image. The aesthetic should be cyberpunk with colors of neon pink, blue and purple. Do not add any people. Depict a glowing soundwave entering from the left and transforming into a stream of luminous text characters flowing toward a focused input field on the right. Between them, two heavily fortified barrier walls labeled with subtle compositor/protocol motifs block the direct path -- each wall stamped with a glowing "access denied" hexagon. Around each wall, a bright neon conduit reroutes the signal in a smooth go-around arc: the first detour loops up through a system-level keyboard sigil etched into the architecture, the second tunnels down through a deep kernel-level channel that emerges past the barrier. The two reroute paths converge and deliver the glowing text into the waiting field, which lights up to confirm delivery. Render everything as sleek translucent circuitry and data conduits against a dark high-tech backdrop, with volumetric neon glow, fine grid lines, and a sense of signal flowing around obstacles rather than through them.&lt;/p&gt;</content><category term="TIL"/><category term="wayland"/><category term="linux"/><category term="configuration"/><category term="voice_ai"/></entry><entry><title>Is HTML "Strictly Better" Than Markdown for Claude Code?</title><link href="https://gallon.me/is-html-really-strictly-better-than-markdown-for-claude-code-i-ran-the-numbers.html" rel="alternate"/><published>2026-05-11T00:00:00-05:00</published><updated>2026-05-11T00:00:00-05:00</updated><author><name>Corey</name></author><id>tag:gallon.me,2026-05-11:/is-html-really-strictly-better-than-markdown-for-claude-code-i-ran-the-numbers.html</id><summary type="html">&lt;p&gt;Thariq Shihipar of Claude Code team fame posted an article last week, &lt;em&gt;"The Unreasonable Effectiveness of HTML"&lt;/em&gt;, arguing that Claude should output HTML files by default for basically everything. PR reviews, postmortems, status reports, the lot. It's a good piece, racked up 8.3M views, and most of it I …&lt;/p&gt;</summary><content type="html">&lt;p&gt;Thariq Shihipar of Claude Code team fame posted an article last week, &lt;em&gt;"The Unreasonable Effectiveness of HTML"&lt;/em&gt;, arguing that Claude should output HTML files by default for basically everything. PR reviews, postmortems, status reports, the lot. It's a good piece, racked up 8.3M views, and most of it I agree with. But in the replies, when somebody pushed back to say markdown's fine for text-heavy stuff, Thariq held the line: &lt;em&gt;"idk I think HTML is strictly better for all of that too."&lt;/em&gt;
So I ran it all. Three of his use cases, instrumented end to end on Opus 4.7. What follows is what I measured, what surprised me, and where I ended up agreeing and disagreeing. Short version: he's right about half of it, and that half is &lt;em&gt;very&lt;/em&gt; right. The other half I don't think holds up, and the data here is why.&lt;/p&gt;
&lt;h2 id="tldr"&gt;TL;DR&lt;/h2&gt;
&lt;p&gt;Thariq's &lt;a href="https://x.com/trq212/status/2052809885763747935"&gt;piece&lt;/a&gt; is good. Read it before you read this. &lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Visual tasks&lt;/strong&gt; (design exploration, prototypes, drag-and-drop widgets): HTML wins outright. Markdown can describe a design; HTML can show one. The 2-4x token cost is the price of admission, not a tradeoff.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Text-with-structure tasks&lt;/strong&gt; (PR reviews, technical explainers): both formats produce a real artifact. HTML adds polish for about 1.4-1.5x the dollar cost. In one of my two text cases (the rate-limiter explainer) the &lt;em&gt;markdown&lt;/em&gt; surfaced more gotchas (12 vs 8) at lower cost. Which cuts against "strictly better."&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Does Claude read HTML better than markdown?&lt;/strong&gt; (the question Thariq's argument quietly assumes a HTML-favorable answer to): two blinded judges scored Claude 7/7 on factual accuracy from both formats, identically. On explanatory specificity HTML got a ~7% edge, at 33% higher cost per call. Pick your poison.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The "1M context handles it" hand-wave:&lt;/strong&gt; at N=50 artifacts re-ingested K=3 times with cache evictions, HTML's overhead eats roughly the whole 1M window. Not nothing, but depending on the work being done maybe worth it.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Sample is small (N=1 per condition for content quality, three instrumented cases) and the methodology I used actually biases &lt;em&gt;against&lt;/em&gt; markdown. Even with the thumb on the scale, "strictly better" isn't what I saw.&lt;/p&gt;
&lt;h2 id="what-thariq-actually-said"&gt;What Thariq actually said&lt;/h2&gt;
&lt;p&gt;He walks through five buckets of tasks where he reckons HTML should be the default. Roughly:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Specs, planning, exploration (e.g. design exploration)&lt;/li&gt;
&lt;li&gt;Code review &amp;amp; understanding (e.g. PR review with annotated diff)&lt;/li&gt;
&lt;li&gt;Design &amp;amp; prototypes (e.g. an interactive checkout button)&lt;/li&gt;
&lt;li&gt;Reports, research, learning (e.g. a rate limiter explainer)&lt;/li&gt;
&lt;li&gt;Custom editing interfaces (e.g. drag-and-drop Linear ticket reorderer)&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;He flags three costs honestly: more tokens, slower generation, noisier diffs. He concludes the advantages outweigh them. Reasonable people can disagree on the conclusion.&lt;/p&gt;
&lt;p&gt;What I want to look at is the reply to the comment. &lt;em&gt;"Strictly better"&lt;/em&gt; is a specific, testable claim. That's the one I want to examine.&lt;/p&gt;
&lt;h2 id="there-are-actually-two-questions-here"&gt;There are actually two questions here&lt;/h2&gt;
&lt;p&gt;The article treats format choice as a knob: pay a bit extra in tokens, get nicer output, decide if it's worth it. That framing works for half the use cases and quietly stops working for the other half. Here's the split.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Sometimes HTML can do something markdown structurally can't.&lt;/strong&gt; Render a six-mockup grid of UI designs. Run a slider that updates a preview in real time. Drag a card across a kanban. Markdown isn't a worse version of these; there is no markdown version. Asking "is the extra cost worth it?" is like asking whether the airfare to Mars is good value when the alternative is staying home and looking at a globe.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Other times both formats can do the job, and HTML just costs more.&lt;/strong&gt; PR reviews. Postmortems. Technical explainers. Implementation plans. Markdown handles all of them. HTML renders them more attractively. The substance lives in either.&lt;/p&gt;
&lt;p&gt;The "use HTML for everything" framing treats both questions like the second one. The case for HTML gets stronger, not weaker, when you split them apart.&lt;/p&gt;
&lt;h2 id="how-i-ran-this"&gt;How I ran this&lt;/h2&gt;
&lt;p&gt;I picked three of Thariq's five use cases and instrumented them properly. Same prompt, both formats, capture every token. The other two are the categorical ones from the previous section; there's nothing fair to compare them against, so I just demo them at the end.&lt;/p&gt;
&lt;p&gt;The three I instrumented:
1. &lt;strong&gt;Design exploration&lt;/strong&gt;: onboarding screen, six approaches in a grid (his specs/planning bucket)
2. &lt;strong&gt;PR review&lt;/strong&gt; of a real commit: &lt;code&gt;toks&lt;/code&gt; commit &lt;code&gt;c6d70f9&lt;/code&gt;, "Respect .gitignore when target has no .git directory"
3. &lt;strong&gt;Rate limiter explainer&lt;/strong&gt; over the &lt;code&gt;slowapi&lt;/code&gt; source: about 12K tokens of Python as substrate&lt;/p&gt;
&lt;p&gt;The two I just demo:
4. Checkout button prototype (interactive sliders, animation, copy-as-prompt)
5. Linear ticket reorderer (drag-and-drop kanban with copy-as-markdown export)&lt;/p&gt;
&lt;p&gt;For each instrumented case I ran three probes: generate the artifact, feed it back into a fresh agent session as context for a downstream task, and apply a semantic edit while capturing the diff. The first probe is the obvious one. The other two matter because that's where the cumulative token costs actually bite. Re-ingestion and editing happen &lt;em&gt;constantly&lt;/em&gt; in real Claude Code workflows.&lt;/p&gt;
&lt;p&gt;On the design exploration case I ran two methodologies side-by-side: Thariq's prompt unchanged versus a neutrally-phrased version of the same task. Why? Because his prompt literally says "create an HTML file." If you only do a "HTML" → "markdown" word-swap, you're asking Claude to make a markdown file in the shape of an HTML file, which is not the same as asking for markdown. Tracking? The difference between the two methodologies turned out to be larger than I expected: a 3.6x cost ratio under ablation, 2.1x under neutral phrasing. Worth being honest about.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;What "artifact tokens" means below.&lt;/strong&gt; I ran &lt;code&gt;toks &amp;lt;file&amp;gt; --for claude&lt;/code&gt; on each generated artifact, using Anthropic's own tokenizer to count the file the way Claude would see it if fed back as context. That number is different from &lt;code&gt;usage.output_tokens&lt;/code&gt; (what the API reported as the generation length) and from &lt;code&gt;cache_creation_input_tokens&lt;/code&gt; (artifact + system prompt + user message, all of it). The first matters for re-ingestion cost. The other two matter for other things.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;Where this is honestly a bit thin:&lt;/strong&gt;
- My reader-side reactions are N=1. One user (me), one session, I knew which file was which. No blinding, no randomization. Take them as gut reactions, not data.
- Generation runs are also N=1 per condition. Content-quality findings like "markdown surfaced more gotchas" might be variance, not signal. K=5 per condition would tell us. I didn't run K=5.
- Three instrumented cases is a sample, not a census.
- The agent-as-reader test below covers shared-content questions only, i.e. facts present in both artifacts. I didn't test whether HTML's verbose-priming or callout-salience changes output &lt;em&gt;quality&lt;/em&gt; (beyond binary accuracy) in ways that matter downstream.
- I didn't test "colleagues actually read HTML more." That'd need users I don't have.
- &lt;strong&gt;Methodology asymmetry&lt;/strong&gt; is load-bearing, and I'm flagging it here so the reader knows the thumb is on the scale before the data tables start. I ran two methodologies only on the design exploration case. The PR review and rate-limiter cases used Thariq's verbatim prompt structure, which is HTML-affording ("create an HTML artifact..."). That biases &lt;em&gt;against&lt;/em&gt; the markdown artifact compared to a neutrally-phrased prompt. The cost ratios I report for those cases (1.51x for PR review, 1.44x for rate limiter) would likely shrink under a neutral prompt; how much, I didn't measure.&lt;/p&gt;
&lt;h2 id="design-exploration-this-is-where-html-earns-its-keep"&gt;Design exploration: this is where HTML earns its keep&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Thariq's prompt (verbatim):&lt;/strong&gt; &lt;em&gt;"I'm not sure what direction to take the onboarding screen. Generate 6 distinctly different approaches, varying layout, tone, and density, and lay them out as a single HTML file in a grid so I can compare them side by side. Label each with the tradeoff it's making."&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Cost data (verbatim methodology):&lt;/strong&gt;&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Format&lt;/th&gt;
&lt;th&gt;Artifact tokens&lt;/th&gt;
&lt;th&gt;Output tokens&lt;/th&gt;
&lt;th&gt;Generation time&lt;/th&gt;
&lt;th&gt;Cost&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;HTML&lt;/td&gt;
&lt;td&gt;6,877&lt;/td&gt;
&lt;td&gt;10,135&lt;/td&gt;
&lt;td&gt;117 s&lt;/td&gt;
&lt;td&gt;$0.51&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MD&lt;/td&gt;
&lt;td&gt;1,896&lt;/td&gt;
&lt;td&gt;3,284&lt;/td&gt;
&lt;td&gt;49 s&lt;/td&gt;
&lt;td&gt;$0.27&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;Cost data (format-neutral methodology):&lt;/strong&gt;&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Format&lt;/th&gt;
&lt;th&gt;Artifact tokens&lt;/th&gt;
&lt;th&gt;Output tokens&lt;/th&gt;
&lt;th&gt;Generation time&lt;/th&gt;
&lt;th&gt;Cost&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;HTML&lt;/td&gt;
&lt;td&gt;6,871&lt;/td&gt;
&lt;td&gt;9,161&lt;/td&gt;
&lt;td&gt;102 s&lt;/td&gt;
&lt;td&gt;$0.48&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MD&lt;/td&gt;
&lt;td&gt;3,332&lt;/td&gt;
&lt;td&gt;4,769&lt;/td&gt;
&lt;td&gt;73 s&lt;/td&gt;
&lt;td&gt;$0.34&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Reading both artifacts side by side, my reaction was unambiguous:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;"HTML is &lt;strong&gt;massively&lt;/strong&gt; nicer to read in these cases versus the markdown. It's much better ... it's not speed that makes the entire difference. It's quality and detail of visual output. This question is fundamentally visual and HTML allows for near-perfect visual communication of the ideas vs text-only in markdown."&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The HTML rendered six high-fidelity onboarding mockups in iframes inside a responsive grid. The markdown produced a six-section text document that described what each mockup &lt;em&gt;would&lt;/em&gt; look like.&lt;/p&gt;
&lt;iframe srcdoc="&lt;!doctype html&gt;
&lt;html lang=&amp;quot;en&amp;quot;&gt;
&lt;head&gt;
&lt;meta charset=&amp;quot;utf-8&amp;quot;&gt;
&lt;meta name=&amp;quot;viewport&amp;quot; content=&amp;quot;width=device-width, initial-scale=1&amp;quot;&gt;
&lt;title&gt;Onboarding Screen — 6 Approaches&lt;/title&gt;
&lt;style&gt;
  :root {
    --ink: #0f172a;
    --ink-2: #475569;
    --line: #e2e8f0;
    --bg: #f8fafc;
    --accent: #2563eb;
    --warn: #b45309;
  }
  * { box-sizing: border-box; }
  html, body { margin: 0; padding: 0; }
  body {
    font: 15px/1.5 -apple-system, BlinkMacSystemFont, &amp;quot;Segoe UI&amp;quot;, Roboto, &amp;quot;Helvetica Neue&amp;quot;, Arial, sans-serif;
    color: var(--ink);
    background: var(--bg);
    padding: 28px 24px 64px;
  }
  header.page {
    max-width: 1400px;
    margin: 0 auto 20px;
  }
  header.page h1 {
    margin: 0 0 6px;
    font-size: 22px;
    font-weight: 700;
    letter-spacing: -0.01em;
  }
  header.page p {
    margin: 0;
    color: var(--ink-2);
    font-size: 14px;
    max-width: 720px;
  }

  .grid {
    max-width: 1400px;
    margin: 0 auto;
    display: grid;
    grid-template-columns: repeat(3, 1fr);
    gap: 20px;
  }
  @media (max-width: 1100px) { .grid { grid-template-columns: repeat(2, 1fr); } }
  @media (max-width: 700px)  { .grid { grid-template-columns: 1fr; } }

  .card {
    background: #fff;
    border: 1px solid var(--line);
    border-radius: 12px;
    overflow: hidden;
    display: flex;
    flex-direction: column;
    box-shadow: 0 1px 2px rgba(15,23,42,0.04);
  }
  .card-label {
    padding: 12px 14px 10px;
    border-bottom: 1px solid var(--line);
    background: #fff;
  }
  .card-label .row {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: 10px;
    margin-bottom: 4px;
  }
  .card-label .num {
    font-size: 11px;
    font-weight: 700;
    letter-spacing: 0.08em;
    color: var(--ink-2);
    text-transform: uppercase;
  }
  .card-label .name {
    font-size: 15px;
    font-weight: 600;
    color: var(--ink);
  }
  .card-label .tradeoff {
    font-size: 12.5px;
    color: var(--ink-2);
    margin: 2px 0 0;
  }
  .card-label .tradeoff b {
    color: var(--ink);
    font-weight: 600;
  }
  .frame-wrap {
    background: #f1f5f9;
    aspect-ratio: 4 / 3;
    position: relative;
  }
  .frame-wrap iframe {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    border: 0;
    background: #fff;
  }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;

&lt;header class=&amp;quot;page&amp;quot;&gt;
  &lt;h1&gt;Onboarding Screen — 6 Approaches&lt;/h1&gt;
  &lt;p&gt;Each tile is a self-contained mockup of the same product moment (first launch, account just created). They vary in layout, tone, and information density. The label on each names the tradeoff that approach is choosing.&lt;/p&gt;
&lt;/header&gt;

&lt;div class=&amp;quot;grid&amp;quot;&gt;

  &lt;!-- ============================================================ --&gt;
  &lt;!-- 1. MINIMAL: single field, single ask                          --&gt;
  &lt;!-- ============================================================ --&gt;
  &lt;article class=&amp;quot;card&amp;quot;&gt;
    &lt;div class=&amp;quot;card-label&amp;quot;&gt;
      &lt;div class=&amp;quot;row&amp;quot;&gt;
        &lt;span class=&amp;quot;num&amp;quot;&gt;01&lt;/span&gt;
        &lt;span class=&amp;quot;name&amp;quot;&gt;Minimal — single field&lt;/span&gt;
      &lt;/div&gt;
      &lt;p class=&amp;quot;tradeoff&amp;quot;&gt;&lt;b&gt;Buys:&lt;/b&gt; near-zero friction, fast to value. &lt;b&gt;Costs:&lt;/b&gt; no signal of product depth, no personalization data captured.&lt;/p&gt;
    &lt;/div&gt;
    &lt;div class=&amp;quot;frame-wrap&amp;quot;&gt;
      &lt;iframe title=&amp;quot;Minimal&amp;quot; srcdoc='&lt;!doctype html&gt;&lt;html&gt;&lt;head&gt;&lt;meta charset=utf-8&gt;&lt;style&gt;
        html,body{margin:0;height:100%;font:15px/1.5 -apple-system,BlinkMacSystemFont,&amp;amp;quot;Segoe UI&amp;amp;quot;,Roboto,sans-serif;color:#0f172a}
        .stage{height:100%;display:flex;align-items:center;justify-content:center;background:#fff;padding:24px}
        .pane{width:100%;max-width:360px;text-align:center}
        .logo{width:36px;height:36px;border-radius:9px;background:#0f172a;margin:0 auto 28px}
        h1{font-size:22px;font-weight:600;margin:0 0 8px;letter-spacing:-0.01em}
        p{color:#64748b;font-size:14px;margin:0 0 28px}
        .field{display:block;width:100%;padding:14px 16px;border:1px solid #e2e8f0;border-radius:10px;font-size:15px;outline:none}
        .field:focus{border-color:#2563eb}
        .hint{font-size:12px;color:#94a3b8;margin-top:14px}
      &lt;/style&gt;&lt;/head&gt;&lt;body&gt;&lt;div class=stage&gt;&lt;div class=pane&gt;
        &lt;div class=logo&gt;&lt;/div&gt;
        &lt;h1&gt;What should we call you?&lt;/h1&gt;
        &lt;p&gt;One question. We will set up the rest as you go.&lt;/p&gt;
        &lt;input class=field placeholder=&amp;quot;Your first name&amp;quot;&gt;
        &lt;div class=hint&gt;Press Enter to continue&lt;/div&gt;
      &lt;/div&gt;&lt;/div&gt;&lt;/body&gt;&lt;/html&gt;'&gt;&lt;/iframe&gt;
    &lt;/div&gt;
  &lt;/article&gt;

  &lt;!-- ============================================================ --&gt;
  &lt;!-- 2. WIZARD: multi-step, structured                             --&gt;
  &lt;!-- ============================================================ --&gt;
  &lt;article class=&amp;quot;card&amp;quot;&gt;
    &lt;div class=&amp;quot;card-label&amp;quot;&gt;
      &lt;div class=&amp;quot;row&amp;quot;&gt;
        &lt;span class=&amp;quot;num&amp;quot;&gt;02&lt;/span&gt;
        &lt;span class=&amp;quot;name&amp;quot;&gt;Wizard. Multi-step&lt;/span&gt;
      &lt;/div&gt;
      &lt;p class=&amp;quot;tradeoff&amp;quot;&gt;&lt;b&gt;Buys:&lt;/b&gt; thorough personalization, sets expectations. &lt;b&gt;Costs:&lt;/b&gt; highest abandonment risk; each step is an exit ramp.&lt;/p&gt;
    &lt;/div&gt;
    &lt;div class=&amp;quot;frame-wrap&amp;quot;&gt;
      &lt;iframe title=&amp;quot;Wizard&amp;quot; srcdoc='&lt;!doctype html&gt;&lt;html&gt;&lt;head&gt;&lt;meta charset=utf-8&gt;&lt;style&gt;
        html,body{margin:0;height:100%;font:14px/1.5 -apple-system,BlinkMacSystemFont,&amp;amp;quot;Segoe UI&amp;amp;quot;,Roboto,sans-serif;color:#0f172a;background:#fff}
        .wrap{height:100%;display:flex;flex-direction:column;padding:18px 22px}
        .top{display:flex;justify-content:space-between;align-items:center;font-size:12px;color:#64748b}
        .steps{display:flex;gap:6px;margin:10px 0 18px}
        .seg{flex:1;height:4px;background:#e2e8f0;border-radius:2px}
        .seg.done{background:#0f172a}
        .seg.now{background:#2563eb}
        h2{font-size:18px;margin:4px 0 4px;font-weight:600}
        .sub{color:#64748b;margin:0 0 14px;font-size:13px}
        .opts{display:grid;grid-template-columns:1fr 1fr;gap:8px}
        .opt{border:1px solid #e2e8f0;border-radius:8px;padding:10px 12px;font-size:13px;cursor:pointer}
        .opt.sel{border-color:#2563eb;background:#eff6ff}
        .footer{margin-top:auto;display:flex;justify-content:space-between;align-items:center;padding-top:14px}
        .back{font-size:13px;color:#64748b;background:none;border:0;cursor:pointer}
        .next{background:#0f172a;color:#fff;border:0;padding:9px 16px;border-radius:8px;font-size:13px;font-weight:500;cursor:pointer}
      &lt;/style&gt;&lt;/head&gt;&lt;body&gt;&lt;div class=wrap&gt;
        &lt;div class=top&gt;&lt;span&gt;Setup&lt;/span&gt;&lt;span&gt;Step 2 of 4&lt;/span&gt;&lt;/div&gt;
        &lt;div class=steps&gt;
          &lt;div class=&amp;quot;seg done&amp;quot;&gt;&lt;/div&gt;
          &lt;div class=&amp;quot;seg now&amp;quot;&gt;&lt;/div&gt;
          &lt;div class=seg&gt;&lt;/div&gt;
          &lt;div class=seg&gt;&lt;/div&gt;
        &lt;/div&gt;
        &lt;h2&gt;What kind of work do you do?&lt;/h2&gt;
        &lt;p class=sub&gt;We will tailor templates and integrations to match.&lt;/p&gt;
        &lt;div class=opts&gt;
          &lt;div class=&amp;quot;opt sel&amp;quot;&gt;Engineering&lt;/div&gt;
          &lt;div class=opt&gt;Design&lt;/div&gt;
          &lt;div class=opt&gt;Product&lt;/div&gt;
          &lt;div class=opt&gt;Marketing&lt;/div&gt;
          &lt;div class=opt&gt;Operations&lt;/div&gt;
          &lt;div class=opt&gt;Something else&lt;/div&gt;
        &lt;/div&gt;
        &lt;div class=footer&gt;
          &lt;button class=back&gt;&amp;amp;larr; Back&lt;/button&gt;
          &lt;button class=next&gt;Continue&lt;/button&gt;
        &lt;/div&gt;
      &lt;/div&gt;&lt;/body&gt;&lt;/html&gt;'&gt;&lt;/iframe&gt;
    &lt;/div&gt;
  &lt;/article&gt;

  &lt;!-- ============================================================ --&gt;
  &lt;!-- 3. SKIP-FIRST: get to value, defer setup                      --&gt;
  &lt;!-- ============================================================ --&gt;
  &lt;article class=&amp;quot;card&amp;quot;&gt;
    &lt;div class=&amp;quot;card-label&amp;quot;&gt;
      &lt;div class=&amp;quot;row&amp;quot;&gt;
        &lt;span class=&amp;quot;num&amp;quot;&gt;03&lt;/span&gt;
        &lt;span class=&amp;quot;name&amp;quot;&gt;Skip-first. Load sample data&lt;/span&gt;
      &lt;/div&gt;
      &lt;p class=&amp;quot;tradeoff&amp;quot;&gt;&lt;b&gt;Buys:&lt;/b&gt; fastest path to seeing the product work. &lt;b&gt;Costs:&lt;/b&gt; users may never come back to configure; sample data can mislead.&lt;/p&gt;
    &lt;/div&gt;
    &lt;div class=&amp;quot;frame-wrap&amp;quot;&gt;
      &lt;iframe title=&amp;quot;Skip-first&amp;quot; srcdoc='&lt;!doctype html&gt;&lt;html&gt;&lt;head&gt;&lt;meta charset=utf-8&gt;&lt;style&gt;
        html,body{margin:0;height:100%;font:14px/1.5 -apple-system,BlinkMacSystemFont,&amp;amp;quot;Segoe UI&amp;amp;quot;,Roboto,sans-serif;color:#0f172a;background:#fafafa}
        .wrap{height:100%;display:flex;align-items:center;justify-content:center;padding:24px}
        .card{background:#fff;border:1px solid #e2e8f0;border-radius:12px;padding:28px;max-width:380px;width:100%;text-align:center}
        h1{margin:0 0 8px;font-size:20px;font-weight:600}
        p{color:#64748b;margin:0 0 22px;font-size:13.5px}
        .primary{display:block;width:100%;background:#0f172a;color:#fff;border:0;padding:13px;border-radius:10px;font-size:14px;font-weight:600;cursor:pointer;margin-bottom:10px}
        .ghost{display:block;width:100%;background:#fff;color:#0f172a;border:1px solid #e2e8f0;padding:13px;border-radius:10px;font-size:14px;cursor:pointer}
        .or{font-size:12px;color:#94a3b8;margin:14px 0;letter-spacing:0.08em}
        .skip{margin-top:18px;font-size:13px;color:#2563eb;text-decoration:none;display:inline-block;border-bottom:1px solid #bfdbfe}
      &lt;/style&gt;&lt;/head&gt;&lt;body&gt;&lt;div class=wrap&gt;&lt;div class=card&gt;
        &lt;h1&gt;You are in.&lt;/h1&gt;
        &lt;p&gt;Try it now with a sample workspace, or import your own data.&lt;/p&gt;
        &lt;button class=primary&gt;Try with sample data&lt;/button&gt;
        &lt;button class=ghost&gt;Import from CSV&lt;/button&gt;
        &lt;div class=or&gt;OR&lt;/div&gt;
        &lt;a class=skip href=&amp;quot;#&amp;quot;&gt;Skip and start from scratch &amp;amp;rarr;&lt;/a&gt;
      &lt;/div&gt;&lt;/div&gt;&lt;/body&gt;&lt;/html&gt;'&gt;&lt;/iframe&gt;
    &lt;/div&gt;
  &lt;/article&gt;

  &lt;!-- ============================================================ --&gt;
  &lt;!-- 4. CONVERSATIONAL: chat intake                                --&gt;
  &lt;!-- ============================================================ --&gt;
  &lt;article class=&amp;quot;card&amp;quot;&gt;
    &lt;div class=&amp;quot;card-label&amp;quot;&gt;
      &lt;div class=&amp;quot;row&amp;quot;&gt;
        &lt;span class=&amp;quot;num&amp;quot;&gt;04&lt;/span&gt;
        &lt;span class=&amp;quot;name&amp;quot;&gt;Conversational. Chat intake&lt;/span&gt;
      &lt;/div&gt;
      &lt;p class=&amp;quot;tradeoff&amp;quot;&gt;&lt;b&gt;Buys:&lt;/b&gt; warm, human tone; lowers stakes of each ask. &lt;b&gt;Costs:&lt;/b&gt; slow, sequential, hard to scan or skip ahead.&lt;/p&gt;
    &lt;/div&gt;
    &lt;div class=&amp;quot;frame-wrap&amp;quot;&gt;
      &lt;iframe title=&amp;quot;Conversational&amp;quot; srcdoc='&lt;!doctype html&gt;&lt;html&gt;&lt;head&gt;&lt;meta charset=utf-8&gt;&lt;style&gt;
        html,body{margin:0;height:100%;font:14px/1.5 -apple-system,BlinkMacSystemFont,&amp;amp;quot;Segoe UI&amp;amp;quot;,Roboto,sans-serif;color:#0f172a;background:#f8fafc}
        .wrap{height:100%;display:flex;flex-direction:column;padding:16px 18px}
        .head{display:flex;align-items:center;gap:10px;margin-bottom:14px}
        .av{width:30px;height:30px;border-radius:50%;background:linear-gradient(135deg,#a78bfa,#60a5fa)}
        .who{font-size:13px;font-weight:600}
        .who small{display:block;color:#64748b;font-weight:400;font-size:11px}
        .stream{flex:1;display:flex;flex-direction:column;gap:8px;overflow:hidden}
        .msg{max-width:78%;padding:9px 12px;border-radius:14px;font-size:13px}
        .bot{background:#fff;border:1px solid #e2e8f0;align-self:flex-start;border-bottom-left-radius:4px}
        .me{background:#0f172a;color:#fff;align-self:flex-end;border-bottom-right-radius:4px}
        .chips{display:flex;flex-wrap:wrap;gap:6px;margin-top:4px}
        .chip{background:#fff;border:1px solid #cbd5e1;border-radius:14px;padding:5px 11px;font-size:12px;cursor:pointer}
        .input{margin-top:12px;display:flex;gap:8px;background:#fff;border:1px solid #e2e8f0;border-radius:22px;padding:6px 6px 6px 14px}
        .input input{flex:1;border:0;outline:none;font-size:13px;background:transparent}
        .send{background:#0f172a;color:#fff;border:0;border-radius:50%;width:32px;height:32px;cursor:pointer}
      &lt;/style&gt;&lt;/head&gt;&lt;body&gt;&lt;div class=wrap&gt;
        &lt;div class=head&gt;
          &lt;div class=av&gt;&lt;/div&gt;
          &lt;div class=who&gt;Nova&lt;small&gt;Setup assistant&lt;/small&gt;&lt;/div&gt;
        &lt;/div&gt;
        &lt;div class=stream&gt;
          &lt;div class=&amp;quot;msg bot&amp;quot;&gt;Hey! Glad you are here. Mind if I ask a couple things to set up your space?&lt;/div&gt;
          &lt;div class=&amp;quot;msg me&amp;quot;&gt;Sure&lt;/div&gt;
          &lt;div class=&amp;quot;msg bot&amp;quot;&gt;What should I call you?&lt;/div&gt;
          &lt;div class=&amp;quot;msg me&amp;quot;&gt;Sam&lt;/div&gt;
          &lt;div class=&amp;quot;msg bot&amp;quot;&gt;Nice to meet you, Sam. Is this for personal use or for a team?&lt;/div&gt;
          &lt;div class=chips&gt;
            &lt;span class=chip&gt;Just me&lt;/span&gt;
            &lt;span class=chip&gt;A small team&lt;/span&gt;
            &lt;span class=chip&gt;A whole company&lt;/span&gt;
          &lt;/div&gt;
        &lt;/div&gt;
        &lt;div class=input&gt;
          &lt;input placeholder=&amp;quot;Type a reply...&amp;quot;&gt;
          &lt;button class=send&gt;&amp;amp;uarr;&lt;/button&gt;
        &lt;/div&gt;
      &lt;/div&gt;&lt;/body&gt;&lt;/html&gt;'&gt;&lt;/iframe&gt;
    &lt;/div&gt;
  &lt;/article&gt;

  &lt;!-- ============================================================ --&gt;
  &lt;!-- 5. DASHBOARD-PREVIEW: app behind, checklist overlay           --&gt;
  &lt;!-- ============================================================ --&gt;
  &lt;article class=&amp;quot;card&amp;quot;&gt;
    &lt;div class=&amp;quot;card-label&amp;quot;&gt;
      &lt;div class=&amp;quot;row&amp;quot;&gt;
        &lt;span class=&amp;quot;num&amp;quot;&gt;05&lt;/span&gt;
        &lt;span class=&amp;quot;name&amp;quot;&gt;Dashboard-preview. Checklist overlay&lt;/span&gt;
      &lt;/div&gt;
      &lt;p class=&amp;quot;tradeoff&amp;quot;&gt;&lt;b&gt;Buys:&lt;/b&gt; shows the destination immediately; setup feels like progress, not a gate. &lt;b&gt;Costs:&lt;/b&gt; dense and busy; can overwhelm on first impression.&lt;/p&gt;
    &lt;/div&gt;
    &lt;div class=&amp;quot;frame-wrap&amp;quot;&gt;
      &lt;iframe title=&amp;quot;Dashboard-preview&amp;quot; srcdoc='&lt;!doctype html&gt;&lt;html&gt;&lt;head&gt;&lt;meta charset=utf-8&gt;&lt;style&gt;
        html,body{margin:0;height:100%;font:13px/1.4 -apple-system,BlinkMacSystemFont,&amp;amp;quot;Segoe UI&amp;amp;quot;,Roboto,sans-serif;color:#0f172a;background:#f1f5f9;overflow:hidden}
        .app{height:100%;display:grid;grid-template-columns:140px 1fr;position:relative}
        .side{background:#0f172a;color:#cbd5e1;padding:14px 12px;font-size:12px}
        .side .b{color:#fff;font-weight:600;margin-bottom:14px}
        .side .it{padding:6px 8px;border-radius:6px;margin-bottom:2px;cursor:pointer}
        .side .it.on{background:rgba(255,255,255,0.08);color:#fff}
        .main{padding:14px 16px;background:#fff}
        .main h2{margin:0 0 10px;font-size:16px}
        .stats{display:grid;grid-template-columns:1fr 1fr 1fr;gap:8px;margin-bottom:12px}
        .stat{background:#f8fafc;border:1px solid #e2e8f0;border-radius:8px;padding:10px}
        .stat b{display:block;font-size:18px}
        .stat span{color:#64748b;font-size:11px}
        .row{height:10px;background:#e2e8f0;border-radius:3px;margin-bottom:6px}
        .row.s{width:60%}
        .row.m{width:80%}
        .overlay{position:absolute;right:14px;bottom:14px;width:240px;background:#fff;border:1px solid #e2e8f0;border-radius:12px;box-shadow:0 12px 30px rgba(15,23,42,0.18);padding:14px}
        .overlay h3{margin:0 0 4px;font-size:13px}
        .overlay .p{font-size:11px;color:#64748b;margin:0 0 10px}
        .bar{height:5px;background:#e2e8f0;border-radius:3px;overflow:hidden;margin-bottom:10px}
        .bar i{display:block;width:25%;height:100%;background:#22c55e}
        .ck{display:flex;align-items:center;gap:8px;font-size:12px;padding:4px 0}
        .box{width:14px;height:14px;border:1.5px solid #cbd5e1;border-radius:4px;flex-shrink:0}
        .box.done{background:#22c55e;border-color:#22c55e}
        .ck.done{color:#94a3b8;text-decoration:line-through}
      &lt;/style&gt;&lt;/head&gt;&lt;body&gt;&lt;div class=app&gt;
        &lt;div class=side&gt;
          &lt;div class=b&gt;Acme&lt;/div&gt;
          &lt;div class=&amp;quot;it on&amp;quot;&gt;Dashboard&lt;/div&gt;
          &lt;div class=it&gt;Projects&lt;/div&gt;
          &lt;div class=it&gt;Inbox&lt;/div&gt;
          &lt;div class=it&gt;Reports&lt;/div&gt;
          &lt;div class=it&gt;Settings&lt;/div&gt;
        &lt;/div&gt;
        &lt;div class=main&gt;
          &lt;h2&gt;Welcome, Sam&lt;/h2&gt;
          &lt;div class=stats&gt;
            &lt;div class=stat&gt;&lt;b&gt;0&lt;/b&gt;&lt;span&gt;Projects&lt;/span&gt;&lt;/div&gt;
            &lt;div class=stat&gt;&lt;b&gt;0&lt;/b&gt;&lt;span&gt;Tasks&lt;/span&gt;&lt;/div&gt;
            &lt;div class=stat&gt;&lt;b&gt;1&lt;/b&gt;&lt;span&gt;Members&lt;/span&gt;&lt;/div&gt;
          &lt;/div&gt;
          &lt;div class=&amp;quot;row m&amp;quot;&gt;&lt;/div&gt;
          &lt;div class=&amp;quot;row s&amp;quot;&gt;&lt;/div&gt;
          &lt;div class=&amp;quot;row m&amp;quot;&gt;&lt;/div&gt;
          &lt;div class=&amp;quot;row s&amp;quot;&gt;&lt;/div&gt;
        &lt;/div&gt;
        &lt;div class=overlay&gt;
          &lt;h3&gt;Get set up (1 of 4)&lt;/h3&gt;
          &lt;p class=p&gt;Two minutes. You can finish later.&lt;/p&gt;
          &lt;div class=bar&gt;&lt;i&gt;&lt;/i&gt;&lt;/div&gt;
          &lt;div class=&amp;quot;ck done&amp;quot;&gt;&lt;span class=&amp;quot;box done&amp;quot;&gt;&lt;/span&gt;Create account&lt;/div&gt;
          &lt;div class=ck&gt;&lt;span class=box&gt;&lt;/span&gt;Invite a teammate&lt;/div&gt;
          &lt;div class=ck&gt;&lt;span class=box&gt;&lt;/span&gt;Connect a tool&lt;/div&gt;
          &lt;div class=ck&gt;&lt;span class=box&gt;&lt;/span&gt;Start a project&lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;&lt;/body&gt;&lt;/html&gt;'&gt;&lt;/iframe&gt;
    &lt;/div&gt;
  &lt;/article&gt;

  &lt;!-- ============================================================ --&gt;
  &lt;!-- 6. PERSONA PICKER: pick a role, get a preset                  --&gt;
  &lt;!-- ============================================================ --&gt;
  &lt;article class=&amp;quot;card&amp;quot;&gt;
    &lt;div class=&amp;quot;card-label&amp;quot;&gt;
      &lt;div class=&amp;quot;row&amp;quot;&gt;
        &lt;span class=&amp;quot;num&amp;quot;&gt;06&lt;/span&gt;
        &lt;span class=&amp;quot;name&amp;quot;&gt;Persona picker. Pick a role&lt;/span&gt;
      &lt;/div&gt;
      &lt;p class=&amp;quot;tradeoff&amp;quot;&gt;&lt;b&gt;Buys:&lt;/b&gt; instant personalization from one tap. &lt;b&gt;Costs:&lt;/b&gt; forces a category choice users may not have ready; hard to fit edge cases.&lt;/p&gt;
    &lt;/div&gt;
    &lt;div class=&amp;quot;frame-wrap&amp;quot;&gt;
      &lt;iframe title=&amp;quot;Persona&amp;quot; srcdoc='&lt;!doctype html&gt;&lt;html&gt;&lt;head&gt;&lt;meta charset=utf-8&gt;&lt;style&gt;
        html,body{margin:0;height:100%;font:14px/1.5 -apple-system,BlinkMacSystemFont,&amp;amp;quot;Segoe UI&amp;amp;quot;,Roboto,sans-serif;color:#0f172a;background:#fff}
        .wrap{height:100%;display:flex;flex-direction:column;padding:20px 22px}
        h1{font-size:22px;margin:0 0 4px;font-weight:700;letter-spacing:-0.01em}
        p.sub{color:#64748b;margin:0 0 16px;font-size:13px}
        .grid{display:grid;grid-template-columns:1fr 1fr;gap:10px;flex:1}
        .tile{border:1.5px solid #e2e8f0;border-radius:12px;padding:12px;display:flex;flex-direction:column;justify-content:space-between;cursor:pointer;transition:border-color .15s}
        .tile:hover{border-color:#94a3b8}
        .tile.sel{border-color:#0f172a;background:#0f172a;color:#fff}
        .tile .ico{width:28px;height:28px;border-radius:8px;background:#f1f5f9;display:flex;align-items:center;justify-content:center;font-size:14px}
        .tile.sel .ico{background:rgba(255,255,255,0.12)}
        .tile .name{font-weight:600;font-size:14px;margin-top:18px}
        .tile .desc{font-size:11.5px;opacity:0.75;margin-top:2px;line-height:1.35}
        .foot{display:flex;justify-content:space-between;align-items:center;margin-top:14px}
        .foot a{font-size:12.5px;color:#64748b;text-decoration:none}
        .go{background:#0f172a;color:#fff;border:0;padding:9px 18px;border-radius:8px;font-size:13px;font-weight:600;cursor:pointer}
      &lt;/style&gt;&lt;/head&gt;&lt;body&gt;&lt;div class=wrap&gt;
        &lt;h1&gt;Who are you, mostly?&lt;/h1&gt;
        &lt;p class=sub&gt;Pick the closest match. We will preset your workspace.&lt;/p&gt;
        &lt;div class=grid&gt;
          &lt;div class=tile&gt;&lt;div class=ico&gt;&amp;amp;#9881;&lt;/div&gt;&lt;div&gt;&lt;div class=name&gt;Builder&lt;/div&gt;&lt;div class=desc&gt;Code, tickets, deploys.&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;
          &lt;div class=&amp;quot;tile sel&amp;quot;&gt;&lt;div class=ico&gt;&amp;amp;#9998;&lt;/div&gt;&lt;div&gt;&lt;div class=name&gt;Maker&lt;/div&gt;&lt;div class=desc&gt;Drafts, files, calendars.&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;
          &lt;div class=tile&gt;&lt;div class=ico&gt;&amp;amp;#9742;&lt;/div&gt;&lt;div&gt;&lt;div class=name&gt;Connector&lt;/div&gt;&lt;div class=desc&gt;People, threads, follow-ups.&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;
          &lt;div class=tile&gt;&lt;div class=ico&gt;&amp;amp;#9776;&lt;/div&gt;&lt;div&gt;&lt;div class=name&gt;Operator&lt;/div&gt;&lt;div class=desc&gt;Lists, runbooks, status.&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;
        &lt;/div&gt;
        &lt;div class=foot&gt;
          &lt;a href=&amp;quot;#&amp;quot;&gt;Not sure &amp;amp;mdash; show me everything&lt;/a&gt;
          &lt;button class=go&gt;Use this preset &amp;amp;rarr;&lt;/button&gt;
        &lt;/div&gt;
      &lt;/div&gt;&lt;/body&gt;&lt;/html&gt;'&gt;&lt;/iframe&gt;
    &lt;/div&gt;
  &lt;/article&gt;

&lt;/div&gt;
&lt;/body&gt;
&lt;/html&gt;
" height="700" width="100%" style="border:1px solid #d0d7de;border-radius:6px;margin:1.5em 0"&gt;&lt;/iframe&gt;

&lt;p&gt;&lt;em&gt;The HTML artifact, rendered above. Six approaches, side by side. This is the thing markdown structurally can't do.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Three things worth noting from this case:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;em&gt;Markdown leaks HTML, but only when the prompt corners it.&lt;/em&gt; When I ran Thariq's verbatim prompt (with its "single markdown file in a grid" phrasing), the model embedded 10 HTML tags into the "markdown" output (&lt;code&gt;&amp;lt;table&amp;gt;&lt;/code&gt;, &lt;code&gt;&amp;lt;tr&amp;gt;&lt;/code&gt;, &lt;code&gt;&amp;lt;td&amp;gt;&lt;/code&gt;). Of course it did; there's no markdown way to lay out a grid. When I dropped "in a grid" from the prompt, the markdown came out clean. Zero HTML tags. So if you've ever wondered why your markdown agents sometimes spit out HTML, here's one answer: you asked them to do something markdown can't do.&lt;/li&gt;
&lt;li&gt;&lt;em&gt;Methodology shifts the cost ratio.&lt;/em&gt; Verbatim ablation: 3.63x artifact tokens; format-neutral: 2.06x. Single-number ratios reported without methodology disclosure should be read skeptically.&lt;/li&gt;
&lt;li&gt;&lt;em&gt;Generation time partially supports Thariq's "2-4x slower" admission.&lt;/em&gt; Measured 1.4-2.4x depending on methodology. The high end of his range is reachable, the low end is below what I measured.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The call: HTML wins outright. The token cost isn't a tradeoff, it's just what the capability costs.&lt;/p&gt;
&lt;h2 id="pr-review-a-fair-fight-and-html-costs-about-40-more-for-the-polish"&gt;PR review: a fair fight, and HTML costs about 40% more for the polish&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Methodology note:&lt;/strong&gt; this case used verbatim ablation only (the format-neutral methodology was only run on the design exploration). The verbatim prompt structure ("create an HTML artifact...") is HTML-affording and biases against markdown. The 1.51x cost ratio reported here would likely shrink under a neutrally-phrased prompt; I didn't measure by how much.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;Thariq's prompt (lightly adapted for my substrate):&lt;/strong&gt; &lt;em&gt;"Help me review this PR by creating an HTML artifact that describes it. I'm not familiar with the gitignore/path-resolution logic so brace on that. Render the actual diff with inline margin annotations, color-code findings by severity..."&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Substrate:&lt;/strong&gt; &lt;code&gt;toks&lt;/code&gt; commit c6d70f9, a real bug fix (~30 lines of code change plus a new test).&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Cost data:&lt;/strong&gt;&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Format&lt;/th&gt;
&lt;th&gt;Artifact tokens&lt;/th&gt;
&lt;th&gt;Output tokens&lt;/th&gt;
&lt;th&gt;Generation time&lt;/th&gt;
&lt;th&gt;Cost&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;HTML&lt;/td&gt;
&lt;td&gt;11,223&lt;/td&gt;
&lt;td&gt;19,308&lt;/td&gt;
&lt;td&gt;234 s&lt;/td&gt;
&lt;td&gt;$0.82&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MD&lt;/td&gt;
&lt;td&gt;3,810&lt;/td&gt;
&lt;td&gt;10,437&lt;/td&gt;
&lt;td&gt;154 s&lt;/td&gt;
&lt;td&gt;$0.54&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Ratios: 2.95x artifact tokens, 1.85x output tokens, 1.52x time, 1.51x cost.&lt;/p&gt;
&lt;p&gt;The markdown PR review is, frankly, a real PR review. TL;DR verdict at the top ("Approve with two non-blocking notes"), a severity legend in a markdown table, a structured walkthrough of the gitignore logic, line-by-line diff annotations, a test-coverage assessment. If a teammate sent me this in a Slack DM I'd be perfectly happy.&lt;/p&gt;
&lt;p&gt;HTML adds a color-coded verdict bar, rendered diffs with green/red highlighting and line numbers, eight severity-tagged finding cards (Pass / Concern / Nit), and inline severity callouts on the diff. Prettier. More navigable. The substance is the same.&lt;/p&gt;
&lt;iframe srcdoc="&lt;!DOCTYPE html&gt;
&lt;html lang=&amp;quot;en&amp;quot;&gt;
&lt;head&gt;
&lt;meta charset=&amp;quot;UTF-8&amp;quot;&gt;
&lt;title&gt;PR Review &amp;amp;mdash; toks c6d70f9: Respect .gitignore when target has no .git directory&lt;/title&gt;
&lt;style&gt;
  :root {
    --bg: #ffffff;
    --fg: #1f2328;
    --muted: #59636e;
    --border: #d0d7de;
    --code-bg: #f6f8fa;
    --diff-add-bg: #dafbe1;
    --diff-add-bar: #2da44e;
    --diff-del-bg: #ffebe9;
    --diff-del-bar: #cf222e;
    --diff-ctx-bg: #ffffff;

    --sev-pass: #2da44e;
    --sev-pass-bg: #dcfce7;
    --sev-nit:  #0969da;
    --sev-nit-bg:  #ddf4ff;
    --sev-warn: #bf8700;
    --sev-warn-bg: #fff8c5;
    --sev-block:#cf222e;
    --sev-block-bg:#ffebe9;
  }
  html { font-family: -apple-system, BlinkMacSystemFont, &amp;quot;Segoe UI&amp;quot;, Helvetica, Arial, sans-serif; color: var(--fg); background: var(--bg); }
  body { max-width: 1200px; margin: 0 auto; padding: 32px 24px 64px; line-height: 1.5; }
  h1 { font-size: 24px; margin: 0 0 4px 0; }
  h2 { font-size: 18px; margin: 36px 0 12px 0; padding-bottom: 6px; border-bottom: 1px solid var(--border); }
  h3 { font-size: 15px; margin: 18px 0 8px 0; }
  code, pre, .mono { font-family: ui-monospace, SFMono-Regular, &amp;quot;SF Mono&amp;quot;, Menlo, Consolas, monospace; font-size: 13px; }
  code { background: var(--code-bg); padding: 1px 5px; border-radius: 4px; }
  pre { background: var(--code-bg); padding: 12px 14px; border-radius: 6px; overflow-x: auto; margin: 8px 0; }
  .meta { color: var(--muted); font-size: 13px; margin-bottom: 18px; }
  .meta .sha { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; }

  /* Verdict bar */
  .verdict {
    display: flex; align-items: stretch; gap: 16px; margin-top: 8px;
    border: 1px solid var(--border); border-radius: 8px; overflow: hidden;
  }
  .verdict .stripe { width: 8px; background: var(--sev-pass); }
  .verdict .body { padding: 14px 16px; flex: 1; }
  .verdict .label { font-weight: 600; color: var(--sev-pass); font-size: 13px; letter-spacing: 0.04em; text-transform: uppercase; }
  .verdict .summary { margin-top: 4px; }

  /* Severity legend */
  .legend { display: flex; flex-wrap: wrap; gap: 8px; margin: 14px 0 8px 0; }
  .legend .pill { display: inline-flex; align-items: center; gap: 8px; font-size: 12px; padding: 4px 10px; border-radius: 999px; border: 1px solid var(--border); background: #fff; }
  .legend .dot { width: 10px; height: 10px; border-radius: 50%; display: inline-block; }
  .dot.pass  { background: var(--sev-pass); }
  .dot.nit   { background: var(--sev-nit); }
  .dot.warn  { background: var(--sev-warn); }
  .dot.block { background: var(--sev-block); }

  /* Brace box (background explainer) */
  .brace { border: 1px solid var(--border); border-left: 4px solid var(--sev-nit); background: #f8fbff; padding: 14px 18px; border-radius: 6px; margin: 12px 0; }
  .brace h3 { margin-top: 0; color: var(--sev-nit); }
  .brace p { margin: 8px 0; }
  .brace ul { margin: 6px 0 6px 0; padding-left: 22px; }
  .brace li { margin: 4px 0; }

  /* Filesystem diagram */
  .fsdiag { display: grid; grid-template-columns: 1fr 1fr; gap: 14px; margin: 12px 0; }
  .fsdiag .box { border: 1px solid var(--border); border-radius: 6px; padding: 12px 14px; background: #fff; }
  .fsdiag .box h4 { margin: 0 0 8px 0; font-size: 13px; color: var(--muted); text-transform: uppercase; letter-spacing: 0.04em; }
  .tree { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 12.5px; line-height: 1.55; white-space: pre; }
  .tree .ignored { color: var(--sev-block); text-decoration: line-through; }
  .tree .scanned { color: var(--sev-pass); font-weight: 600; }
  .tree .root    { color: var(--sev-nit); font-weight: 600; }
  .tree .target  { background: #fff3b0; padding: 0 4px; }

  /* Annotated diff: 2-column grid (diff on left, callouts on right) */
  .diff-block { margin: 14px 0 24px 0; }
  .hunk-header {
    background: #ddf4ff; color: #0969da; padding: 6px 10px; font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
    font-size: 12.5px; border-radius: 6px 6px 0 0; border: 1px solid var(--border); border-bottom: 0;
  }
  .annotated {
    display: grid; grid-template-columns: minmax(0, 1.55fr) minmax(0, 1fr); gap: 0;
    border: 1px solid var(--border); border-top: 0; border-radius: 0 0 6px 6px; overflow: hidden;
  }
  .annotated .diff { background: var(--code-bg); padding: 0; min-width: 0; }
  .annotated .notes { background: #fafbfc; padding: 0; border-left: 1px solid var(--border); display: flex; flex-direction: column; min-width: 0; }
  .row { display: grid; grid-template-columns: 36px 36px 1fr; align-items: stretch; }
  .row .gutter { background: #eaeef2; color: var(--muted); text-align: right; padding: 2px 6px; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 11.5px; user-select: none; }
  .row .sign { padding: 2px 6px; text-align: center; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 12.5px; }
  .row .code { padding: 2px 8px; white-space: pre; overflow-x: auto; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 12.5px; }
  .row.ctx { background: #ffffff; }
  .row.add { background: var(--diff-add-bg); }
  .row.add .sign { color: var(--diff-add-bar); }
  .row.del { background: var(--diff-del-bg); }
  .row.del .sign { color: var(--diff-del-bar); }
  .row.tag { background: #fff8c5; }
  .row.tag .gutter, .row.tag .sign { background: transparent; color: var(--sev-warn); font-weight: 600; }

  /* annotation cards stacked in right pane */
  .ann {
    border-left: 4px solid var(--sev-nit); background: #fff; padding: 10px 12px; margin: 0; flex: 0 0 auto;
    border-bottom: 1px solid var(--border);
  }
  .ann:last-child { border-bottom: 0; }
  .ann.pass  { border-left-color: var(--sev-pass);  background: var(--sev-pass-bg); }
  .ann.nit   { border-left-color: var(--sev-nit);   background: var(--sev-nit-bg); }
  .ann.warn  { border-left-color: var(--sev-warn);  background: var(--sev-warn-bg); }
  .ann.block { border-left-color: var(--sev-block); background: var(--sev-block-bg); }
  .ann .head { font-size: 11px; text-transform: uppercase; letter-spacing: 0.06em; font-weight: 700; margin-bottom: 4px; }
  .ann.pass  .head { color: var(--sev-pass); }
  .ann.nit   .head { color: var(--sev-nit); }
  .ann.warn  .head { color: var(--sev-warn); }
  .ann.block .head { color: var(--sev-block); }
  .ann .body { font-size: 13px; }
  .ann .body code { background: rgba(255,255,255,0.6); }
  .ann .anchor { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 11px; color: var(--muted); margin-bottom: 2px; }

  /* Findings table */
  .findings { display: flex; flex-direction: column; gap: 10px; }
  .finding {
    display: grid; grid-template-columns: 8px 1fr; gap: 0; border: 1px solid var(--border); border-radius: 6px; overflow: hidden; background: #fff;
  }
  .finding .stripe { width: 8px; }
  .finding.pass  .stripe { background: var(--sev-pass); }
  .finding.nit   .stripe { background: var(--sev-nit); }
  .finding.warn  .stripe { background: var(--sev-warn); }
  .finding.block .stripe { background: var(--sev-block); }
  .finding .body { padding: 12px 14px; }
  .finding .head { display: flex; align-items: center; gap: 10px; margin-bottom: 4px; }
  .finding .badge { font-size: 11px; text-transform: uppercase; letter-spacing: 0.06em; font-weight: 700; padding: 2px 8px; border-radius: 999px; }
  .finding.pass  .badge { background: var(--sev-pass-bg);  color: var(--sev-pass); }
  .finding.nit   .badge { background: var(--sev-nit-bg);   color: var(--sev-nit); }
  .finding.warn  .badge { background: var(--sev-warn-bg);  color: var(--sev-warn); }
  .finding.block .badge { background: var(--sev-block-bg); color: var(--sev-block); }
  .finding .title { font-weight: 600; }
  .finding .where { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 12px; color: var(--muted); }
  .finding p { margin: 6px 0; }

  /* Test coverage matrix */
  table.cov { border-collapse: collapse; width: 100%; margin: 8px 0; font-size: 13px; }
  table.cov th, table.cov td { border: 1px solid var(--border); padding: 8px 10px; text-align: left; vertical-align: top; }
  table.cov th { background: var(--code-bg); font-weight: 600; }
  .yes { color: var(--sev-pass); font-weight: 600; }
  .no  { color: var(--sev-warn); font-weight: 600; }

  .footnote { color: var(--muted); font-size: 12px; margin-top: 8px; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;

&lt;h1&gt;PR Review &amp;amp;mdash; Respect &lt;code&gt;.gitignore&lt;/code&gt; when target has no &lt;code&gt;.git&lt;/code&gt; directory&lt;/h1&gt;
&lt;div class=&amp;quot;meta&amp;quot;&gt;
  Repository &lt;code&gt;toks&lt;/code&gt; &amp;amp;middot; commit &lt;span class=&amp;quot;sha&amp;quot;&gt;c6d70f9&lt;/span&gt; &amp;amp;middot; author Corey Gallon &amp;amp;middot; 2026-04-29
&lt;/div&gt;

&lt;div class=&amp;quot;verdict&amp;quot;&gt;
  &lt;div class=&amp;quot;stripe&amp;quot;&gt;&lt;/div&gt;
  &lt;div class=&amp;quot;body&amp;quot;&gt;
    &lt;div class=&amp;quot;label&amp;quot;&gt;Approve with notes&lt;/div&gt;
    &lt;div class=&amp;quot;summary&amp;quot;&gt;
      The fix is small, correct, and targeted. It closes a real bug:
      &lt;code&gt;.gitignore&lt;/code&gt; was previously a no-op for any directory that didn't sit under a &lt;code&gt;.git&lt;/code&gt; root.
      The new test exercises the fixed path. Two pre-existing rough edges in the surrounding logic
      are surfaced below as concerns&amp;amp;mdash;not blockers&amp;amp;mdash;because the PR description's motivating example
      (&lt;code&gt;.venv&lt;/code&gt; being scanned) intersects them and a future reader will want to know.
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;div class=&amp;quot;legend&amp;quot;&gt;
  &lt;span class=&amp;quot;pill&amp;quot;&gt;&lt;span class=&amp;quot;dot pass&amp;quot;&gt;&lt;/span&gt;Pass &amp;amp;mdash; nothing to change&lt;/span&gt;
  &lt;span class=&amp;quot;pill&amp;quot;&gt;&lt;span class=&amp;quot;dot nit&amp;quot;&gt;&lt;/span&gt;Nit &amp;amp;mdash; style/clarity&lt;/span&gt;
  &lt;span class=&amp;quot;pill&amp;quot;&gt;&lt;span class=&amp;quot;dot warn&amp;quot;&gt;&lt;/span&gt;Concern &amp;amp;mdash; worth discussing&lt;/span&gt;
  &lt;span class=&amp;quot;pill&amp;quot;&gt;&lt;span class=&amp;quot;dot block&amp;quot;&gt;&lt;/span&gt;Blocker &amp;amp;mdash; must fix&lt;/span&gt;
&lt;/div&gt;

&lt;h2&gt;Background: how gitignore resolution actually works here&lt;/h2&gt;

&lt;div class=&amp;quot;brace&amp;quot;&gt;
  &lt;h3&gt;Two functions, two directions of walk&lt;/h3&gt;
  &lt;p&gt;The scanner relies on two helpers in &lt;code&gt;src/toks/scanner.py&lt;/code&gt;. To read this PR you have to hold both in your head at once:&lt;/p&gt;
  &lt;ul&gt;
    &lt;li&gt;&lt;b&gt;&lt;code&gt;find_git_root(start=target)&lt;/code&gt;&lt;/b&gt; walks &lt;b&gt;upward&lt;/b&gt; from the target, looking for a &lt;code&gt;.git&lt;/code&gt; directory in each parent. Returns that parent, or &lt;code&gt;None&lt;/code&gt; if it reaches the filesystem root without finding one.&lt;/li&gt;
    &lt;li&gt;&lt;b&gt;&lt;code&gt;load_gitignore_specs(git_root, target)&lt;/code&gt;&lt;/b&gt; walks &lt;b&gt;downward&lt;/b&gt; from &lt;code&gt;git_root&lt;/code&gt; via &lt;code&gt;os.walk(git_root)&lt;/code&gt;, collecting every &lt;code&gt;.gitignore&lt;/code&gt; it finds, and prefixing each pattern with the &lt;code&gt;.gitignore&lt;/code&gt;'s relative directory so a nested &lt;code&gt;foo/.gitignore&lt;/code&gt; rule like &lt;code&gt;build/&lt;/code&gt; becomes &lt;code&gt;foo/build/&lt;/code&gt;.&lt;/li&gt;
  &lt;/ul&gt;
  &lt;p&gt;In the scan loop, each candidate file is reduced to a path &lt;b&gt;relative to that same root&lt;/b&gt; via &lt;code&gt;file_path.relative_to(git_root)&lt;/code&gt; before being matched against the spec. &lt;i&gt;This is the critical invariant&lt;/i&gt;: the root used to &lt;i&gt;build&lt;/i&gt; the spec must be the same root used to &lt;i&gt;relativize&lt;/i&gt; the candidate, or every match silently fails (or, worse, raises &lt;code&gt;ValueError&lt;/code&gt; from &lt;code&gt;relative_to&lt;/code&gt;).&lt;/p&gt;
  &lt;p&gt;The bug being fixed: when &lt;code&gt;find_git_root&lt;/code&gt; returned &lt;code&gt;None&lt;/code&gt;, the old code skipped &lt;code&gt;load_gitignore_specs&lt;/code&gt; entirely. So a project with a &lt;code&gt;.gitignore&lt;/code&gt; but no &lt;code&gt;git init&lt;/code&gt; got no filtering at all.&lt;/p&gt;
&lt;/div&gt;

&lt;div class=&amp;quot;fsdiag&amp;quot;&gt;
  &lt;div class=&amp;quot;box&amp;quot;&gt;
    &lt;h4&gt;Before this PR (no &lt;code&gt;.git&lt;/code&gt; upstream)&lt;/h4&gt;
    &lt;pre class=&amp;quot;tree&amp;quot;&gt;/projects/&lt;span class=&amp;quot;target&amp;quot;&gt;myproj&lt;/span&gt;/        &amp;amp;larr; target
  .gitignore         (says: .venv/)
  src/
    main.py          &lt;span class=&amp;quot;scanned&amp;quot;&gt;scanned&lt;/span&gt;
  .venv/
    lib/site-pkgs/
      django/...     &lt;span class=&amp;quot;scanned&amp;quot;&gt;scanned (bug!)&lt;/span&gt;
      numpy/...      &lt;span class=&amp;quot;scanned&amp;quot;&gt;scanned (bug!)&lt;/span&gt;&lt;/pre&gt;
    &lt;p class=&amp;quot;footnote&amp;quot;&gt;&lt;code&gt;find_git_root&lt;/code&gt; returns &lt;code&gt;None&lt;/code&gt; &amp;amp;rarr; &lt;code&gt;gitignore_spec&lt;/code&gt; stays &lt;code&gt;None&lt;/code&gt; &amp;amp;rarr; the &lt;code&gt;.gitignore&lt;/code&gt; is silently ignored.&lt;/p&gt;
  &lt;/div&gt;
  &lt;div class=&amp;quot;box&amp;quot;&gt;
    &lt;h4&gt;After this PR (no &lt;code&gt;.git&lt;/code&gt; upstream)&lt;/h4&gt;
    &lt;pre class=&amp;quot;tree&amp;quot;&gt;/projects/&lt;span class=&amp;quot;target&amp;quot;&gt;myproj&lt;/span&gt;/        &amp;amp;larr; &lt;span class=&amp;quot;root&amp;quot;&gt;target = gitignore_root&lt;/span&gt;
  .gitignore         (says: .venv/)
  src/
    main.py          &lt;span class=&amp;quot;scanned&amp;quot;&gt;scanned&lt;/span&gt;
  .venv/
    lib/site-pkgs/
      django/...     &lt;span class=&amp;quot;ignored&amp;quot;&gt;ignored&lt;/span&gt;
      numpy/...      &lt;span class=&amp;quot;ignored&amp;quot;&gt;ignored&lt;/span&gt;&lt;/pre&gt;
    &lt;p class=&amp;quot;footnote&amp;quot;&gt;Fallback: &lt;code&gt;gitignore_root = target&lt;/code&gt;. &lt;code&gt;load_gitignore_specs&lt;/code&gt; walks the target subtree, builds a spec from the local &lt;code&gt;.gitignore&lt;/code&gt;, and the per-file check now matches.&lt;/p&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;h2&gt;Annotated diff &amp;amp;mdash; &lt;code&gt;src/toks/scanner.py&lt;/code&gt;&lt;/h2&gt;

&lt;div class=&amp;quot;diff-block&amp;quot;&gt;
  &lt;div class=&amp;quot;hunk-header&amp;quot;&gt;@@ -104,11 +104,11 @@ def scan_files(...):&lt;/div&gt;
  &lt;div class=&amp;quot;annotated&amp;quot;&gt;
    &lt;div class=&amp;quot;diff&amp;quot;&gt;
      &lt;div class=&amp;quot;row ctx&amp;quot;&gt;&lt;div class=&amp;quot;gutter&amp;quot;&gt;104&lt;/div&gt;&lt;div class=&amp;quot;sign&amp;quot;&gt; &lt;/div&gt;&lt;div class=&amp;quot;code&amp;quot;&gt;    raise ValueError(f&amp;quot;Not a directory: {target}&amp;quot;)&lt;/div&gt;&lt;/div&gt;
      &lt;div class=&amp;quot;row ctx&amp;quot;&gt;&lt;div class=&amp;quot;gutter&amp;quot;&gt;105&lt;/div&gt;&lt;div class=&amp;quot;sign&amp;quot;&gt; &lt;/div&gt;&lt;div class=&amp;quot;code&amp;quot;&gt; &lt;/div&gt;&lt;/div&gt;
      &lt;div class=&amp;quot;row ctx&amp;quot;&gt;&lt;div class=&amp;quot;gutter&amp;quot;&gt;106&lt;/div&gt;&lt;div class=&amp;quot;sign&amp;quot;&gt; &lt;/div&gt;&lt;div class=&amp;quot;code&amp;quot;&gt;    gitignore_spec = None&lt;/div&gt;&lt;/div&gt;
      &lt;div class=&amp;quot;row del&amp;quot;&gt;&lt;div class=&amp;quot;gutter&amp;quot;&gt;107&lt;/div&gt;&lt;div class=&amp;quot;sign&amp;quot;&gt;-&lt;/div&gt;&lt;div class=&amp;quot;code&amp;quot;&gt;    git_root = None&lt;/div&gt;&lt;/div&gt;
      &lt;div class=&amp;quot;row add&amp;quot;&gt;&lt;div class=&amp;quot;gutter&amp;quot;&gt;107&lt;/div&gt;&lt;div class=&amp;quot;sign&amp;quot;&gt;+&lt;/div&gt;&lt;div class=&amp;quot;code&amp;quot;&gt;    gitignore_root = None&lt;/div&gt;&lt;/div&gt;
      &lt;div class=&amp;quot;row ctx&amp;quot;&gt;&lt;div class=&amp;quot;gutter&amp;quot;&gt;108&lt;/div&gt;&lt;div class=&amp;quot;sign&amp;quot;&gt; &lt;/div&gt;&lt;div class=&amp;quot;code&amp;quot;&gt;    if not no_gitignore:&lt;/div&gt;&lt;/div&gt;
      &lt;div class=&amp;quot;row ctx&amp;quot;&gt;&lt;div class=&amp;quot;gutter&amp;quot;&gt;109&lt;/div&gt;&lt;div class=&amp;quot;sign&amp;quot;&gt; &lt;/div&gt;&lt;div class=&amp;quot;code&amp;quot;&gt;        git_root = find_git_root(start=target)&lt;/div&gt;&lt;/div&gt;
      &lt;div class=&amp;quot;row del&amp;quot;&gt;&lt;div class=&amp;quot;gutter&amp;quot;&gt;110&lt;/div&gt;&lt;div class=&amp;quot;sign&amp;quot;&gt;-&lt;/div&gt;&lt;div class=&amp;quot;code&amp;quot;&gt;        if git_root:&lt;/div&gt;&lt;/div&gt;
      &lt;div class=&amp;quot;row del&amp;quot;&gt;&lt;div class=&amp;quot;gutter&amp;quot;&gt;111&lt;/div&gt;&lt;div class=&amp;quot;sign&amp;quot;&gt;-&lt;/div&gt;&lt;div class=&amp;quot;code&amp;quot;&gt;            gitignore_spec = load_gitignore_specs(git_root=git_root, target=target)&lt;/div&gt;&lt;/div&gt;
      &lt;div class=&amp;quot;row add&amp;quot;&gt;&lt;div class=&amp;quot;gutter&amp;quot;&gt;110&lt;/div&gt;&lt;div class=&amp;quot;sign&amp;quot;&gt;+&lt;/div&gt;&lt;div class=&amp;quot;code&amp;quot;&gt;        gitignore_root = git_root if git_root else target&lt;/div&gt;&lt;/div&gt;
      &lt;div class=&amp;quot;row add&amp;quot;&gt;&lt;div class=&amp;quot;gutter&amp;quot;&gt;111&lt;/div&gt;&lt;div class=&amp;quot;sign&amp;quot;&gt;+&lt;/div&gt;&lt;div class=&amp;quot;code&amp;quot;&gt;        gitignore_spec = load_gitignore_specs(git_root=gitignore_root, target=target)&lt;/div&gt;&lt;/div&gt;
      &lt;div class=&amp;quot;row ctx&amp;quot;&gt;&lt;div class=&amp;quot;gutter&amp;quot;&gt;112&lt;/div&gt;&lt;div class=&amp;quot;sign&amp;quot;&gt; &lt;/div&gt;&lt;div class=&amp;quot;code&amp;quot;&gt; &lt;/div&gt;&lt;/div&gt;
      &lt;div class=&amp;quot;row ctx&amp;quot;&gt;&lt;div class=&amp;quot;gutter&amp;quot;&gt;113&lt;/div&gt;&lt;div class=&amp;quot;sign&amp;quot;&gt; &lt;/div&gt;&lt;div class=&amp;quot;code&amp;quot;&gt;    results: list[tuple[Path, str, int]] = []&lt;/div&gt;&lt;/div&gt;
    &lt;/div&gt;
    &lt;div class=&amp;quot;notes&amp;quot;&gt;
      &lt;div class=&amp;quot;ann pass&amp;quot;&gt;
        &lt;div class=&amp;quot;anchor&amp;quot;&gt;line 107 &amp;amp;mdash; rename&lt;/div&gt;
        &lt;div class=&amp;quot;head&amp;quot;&gt;Pass &amp;amp;mdash; better name&lt;/div&gt;
        &lt;div class=&amp;quot;body&amp;quot;&gt;
          Renaming &lt;code&gt;git_root&lt;/code&gt; &amp;amp;rarr; &lt;code&gt;gitignore_root&lt;/code&gt; is the right call. After the fix the variable can hold a path that has nothing to do with git (it can just be the target). The new name reflects what it's &lt;i&gt;used for&lt;/i&gt; rather than where it came from.
        &lt;/div&gt;
      &lt;/div&gt;
      &lt;div class=&amp;quot;ann pass&amp;quot;&gt;
        &lt;div class=&amp;quot;anchor&amp;quot;&gt;lines 110&amp;amp;ndash;111 &amp;amp;mdash; the fix&lt;/div&gt;
        &lt;div class=&amp;quot;head&amp;quot;&gt;Pass &amp;amp;mdash; correct fallback&lt;/div&gt;
        &lt;div class=&amp;quot;body&amp;quot;&gt;
          &lt;code&gt;git_root if git_root else target&lt;/code&gt; establishes a non-null root in every branch where &lt;code&gt;no_gitignore&lt;/code&gt; is false. The same value is then threaded into &lt;code&gt;load_gitignore_specs&lt;/code&gt; as &lt;code&gt;git_root=&amp;amp;hellip;&lt;/code&gt;. The keyword name in the callee now reads as a slight misnomer (it's not necessarily a git root anymore), but that's a follow-up rename, not a bug.
        &lt;/div&gt;
      &lt;/div&gt;
      &lt;div class=&amp;quot;ann warn&amp;quot;&gt;
        &lt;div class=&amp;quot;anchor&amp;quot;&gt;line 109 &amp;amp;mdash; pre-existing&lt;/div&gt;
        &lt;div class=&amp;quot;head&amp;quot;&gt;Concern &amp;amp;mdash; upstream &lt;code&gt;.git&lt;/code&gt; can hijack the root&lt;/div&gt;
        &lt;div class=&amp;quot;body&amp;quot;&gt;
          &lt;code&gt;find_git_root&lt;/code&gt; walks all the way up to &lt;code&gt;/&lt;/code&gt;. If the user has any unrelated &lt;code&gt;.git&lt;/code&gt; upstream of the target (e.g. a dotfiles repo at &lt;code&gt;~/.git&lt;/code&gt;, or a parent monorepo), it becomes the root. &lt;code&gt;load_gitignore_specs&lt;/code&gt; then &lt;code&gt;os.walk&lt;/code&gt;s the entire ancestor tree to harvest every &lt;code&gt;.gitignore&lt;/code&gt; under it. This was true before the PR too &amp;amp;mdash; the PR doesn't make it worse &amp;amp;mdash; but it's worth surfacing because the new fallback only kicks in when &lt;i&gt;no&lt;/i&gt; &lt;code&gt;.git&lt;/code&gt; is found, and the more common surprising case is finding the wrong one.
        &lt;/div&gt;
      &lt;/div&gt;
      &lt;div class=&amp;quot;ann nit&amp;quot;&gt;
        &lt;div class=&amp;quot;anchor&amp;quot;&gt;line 111 &amp;amp;mdash; small style&lt;/div&gt;
        &lt;div class=&amp;quot;head&amp;quot;&gt;Nit &amp;amp;mdash; consider renaming the parameter too&lt;/div&gt;
        &lt;div class=&amp;quot;body&amp;quot;&gt;
          &lt;code&gt;load_gitignore_specs(git_root=&amp;amp;hellip;)&lt;/code&gt; still takes a parameter named &lt;code&gt;git_root&lt;/code&gt;. After this change, callers pass either a true git root or the target. Renaming the parameter to &lt;code&gt;root&lt;/code&gt; would close the loop on the rename you started in this PR. Optional &amp;amp;mdash; do it now or in a follow-up.
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;div class=&amp;quot;diff-block&amp;quot;&gt;
  &lt;div class=&amp;quot;hunk-header&amp;quot;&gt;@@ -128,8 +128,8 @@ def scan_files(...):&lt;/div&gt;
  &lt;div class=&amp;quot;annotated&amp;quot;&gt;
    &lt;div class=&amp;quot;diff&amp;quot;&gt;
      &lt;div class=&amp;quot;row ctx&amp;quot;&gt;&lt;div class=&amp;quot;gutter&amp;quot;&gt;128&lt;/div&gt;&lt;div class=&amp;quot;sign&amp;quot;&gt; &lt;/div&gt;&lt;div class=&amp;quot;code&amp;quot;&gt;            if file_path.is_symlink() and file_path.is_dir():&lt;/div&gt;&lt;/div&gt;
      &lt;div class=&amp;quot;row ctx&amp;quot;&gt;&lt;div class=&amp;quot;gutter&amp;quot;&gt;129&lt;/div&gt;&lt;div class=&amp;quot;sign&amp;quot;&gt; &lt;/div&gt;&lt;div class=&amp;quot;code&amp;quot;&gt;                continue&lt;/div&gt;&lt;/div&gt;
      &lt;div class=&amp;quot;row ctx&amp;quot;&gt;&lt;div class=&amp;quot;gutter&amp;quot;&gt;130&lt;/div&gt;&lt;div class=&amp;quot;sign&amp;quot;&gt; &lt;/div&gt;&lt;div class=&amp;quot;code&amp;quot;&gt; &lt;/div&gt;&lt;/div&gt;
      &lt;div class=&amp;quot;row del&amp;quot;&gt;&lt;div class=&amp;quot;gutter&amp;quot;&gt;131&lt;/div&gt;&lt;div class=&amp;quot;sign&amp;quot;&gt;-&lt;/div&gt;&lt;div class=&amp;quot;code&amp;quot;&gt;            if gitignore_spec and git_root:&lt;/div&gt;&lt;/div&gt;
      &lt;div class=&amp;quot;row del&amp;quot;&gt;&lt;div class=&amp;quot;gutter&amp;quot;&gt;132&lt;/div&gt;&lt;div class=&amp;quot;sign&amp;quot;&gt;-&lt;/div&gt;&lt;div class=&amp;quot;code&amp;quot;&gt;                rel = file_path.relative_to(git_root)&lt;/div&gt;&lt;/div&gt;
      &lt;div class=&amp;quot;row add&amp;quot;&gt;&lt;div class=&amp;quot;gutter&amp;quot;&gt;131&lt;/div&gt;&lt;div class=&amp;quot;sign&amp;quot;&gt;+&lt;/div&gt;&lt;div class=&amp;quot;code&amp;quot;&gt;            if gitignore_spec and gitignore_root:&lt;/div&gt;&lt;/div&gt;
      &lt;div class=&amp;quot;row add&amp;quot;&gt;&lt;div class=&amp;quot;gutter&amp;quot;&gt;132&lt;/div&gt;&lt;div class=&amp;quot;sign&amp;quot;&gt;+&lt;/div&gt;&lt;div class=&amp;quot;code&amp;quot;&gt;                rel = file_path.relative_to(gitignore_root)&lt;/div&gt;&lt;/div&gt;
      &lt;div class=&amp;quot;row ctx&amp;quot;&gt;&lt;div class=&amp;quot;gutter&amp;quot;&gt;133&lt;/div&gt;&lt;div class=&amp;quot;sign&amp;quot;&gt; &lt;/div&gt;&lt;div class=&amp;quot;code&amp;quot;&gt;                if gitignore_spec.match_file(str(rel)):&lt;/div&gt;&lt;/div&gt;
      &lt;div class=&amp;quot;row ctx&amp;quot;&gt;&lt;div class=&amp;quot;gutter&amp;quot;&gt;134&lt;/div&gt;&lt;div class=&amp;quot;sign&amp;quot;&gt; &lt;/div&gt;&lt;div class=&amp;quot;code&amp;quot;&gt;                    continue&lt;/div&gt;&lt;/div&gt;
    &lt;/div&gt;
    &lt;div class=&amp;quot;notes&amp;quot;&gt;
      &lt;div class=&amp;quot;ann pass&amp;quot;&gt;
        &lt;div class=&amp;quot;anchor&amp;quot;&gt;line 132 &amp;amp;mdash; invariant preserved&lt;/div&gt;
        &lt;div class=&amp;quot;head&amp;quot;&gt;Pass &amp;amp;mdash; root used to build = root used to relativize&lt;/div&gt;
        &lt;div class=&amp;quot;body&amp;quot;&gt;
          The critical invariant from the brace above is intact: the spec is built with &lt;code&gt;gitignore_root&lt;/code&gt; in &lt;code&gt;load_gitignore_specs&lt;/code&gt;, and candidate files are relativized to the &lt;i&gt;same&lt;/i&gt; &lt;code&gt;gitignore_root&lt;/code&gt; here. If a future change splits these, that's where bugs would creep in.
        &lt;/div&gt;
      &lt;/div&gt;
      &lt;div class=&amp;quot;ann nit&amp;quot;&gt;
        &lt;div class=&amp;quot;anchor&amp;quot;&gt;line 131 &amp;amp;mdash; defensive check&lt;/div&gt;
        &lt;div class=&amp;quot;head&amp;quot;&gt;Nit &amp;amp;mdash; &lt;code&gt;and gitignore_root&lt;/code&gt; is now redundant&lt;/div&gt;
        &lt;div class=&amp;quot;body&amp;quot;&gt;
          After the fix, whenever &lt;code&gt;gitignore_spec&lt;/code&gt; is non-&lt;code&gt;None&lt;/code&gt;, &lt;code&gt;gitignore_root&lt;/code&gt; is also set (the only path that produces a spec assigns the root first). The &lt;code&gt;and gitignore_root&lt;/code&gt; guard is harmless but no longer carries information. You could drop it, or keep it as defensive code &amp;amp;mdash; not worth blocking on.
        &lt;/div&gt;
      &lt;/div&gt;
      &lt;div class=&amp;quot;ann warn&amp;quot;&gt;
        &lt;div class=&amp;quot;anchor&amp;quot;&gt;whole hunk &amp;amp;mdash; pre-existing perf&lt;/div&gt;
        &lt;div class=&amp;quot;head&amp;quot;&gt;Concern &amp;amp;mdash; gitignore filtering is per-file, not per-directory&lt;/div&gt;
        &lt;div class=&amp;quot;body&amp;quot;&gt;
          The walk in &lt;code&gt;scan_files&lt;/code&gt; only consults the spec for &lt;i&gt;files&lt;/i&gt;. Directory pruning is hard-coded to &lt;code&gt;.git&lt;/code&gt; and symlinks. So even after this fix, if &lt;code&gt;.gitignore&lt;/code&gt; contains &lt;code&gt;.venv/&lt;/code&gt;, &lt;code&gt;os.walk&lt;/code&gt; still descends into &lt;code&gt;.venv/&lt;/code&gt;, stats every file, and then drops them one-by-one via &lt;code&gt;match_file&lt;/code&gt;. The output is correct; the walk is slow on a directory full of &lt;code&gt;site-packages&lt;/code&gt;. The PR description names &lt;code&gt;.venv&lt;/code&gt; specifically, which is exactly the case where users will notice the cost. Worth a follow-up: prune &lt;code&gt;dirnames&lt;/code&gt; against the spec before recursing.
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;h2&gt;Annotated diff &amp;amp;mdash; &lt;code&gt;tests/test_scanner.py&lt;/code&gt;&lt;/h2&gt;

&lt;div class=&amp;quot;diff-block&amp;quot;&gt;
  &lt;div class=&amp;quot;hunk-header&amp;quot;&gt;@@ -95,3 +95,16 @@ class TestScanFiles:&lt;/div&gt;
  &lt;div class=&amp;quot;annotated&amp;quot;&gt;
    &lt;div class=&amp;quot;diff&amp;quot;&gt;
      &lt;div class=&amp;quot;row ctx&amp;quot;&gt;&lt;div class=&amp;quot;gutter&amp;quot;&gt;95&lt;/div&gt;&lt;div class=&amp;quot;sign&amp;quot;&gt; &lt;/div&gt;&lt;div class=&amp;quot;code&amp;quot;&gt;        empty.mkdir()&lt;/div&gt;&lt;/div&gt;
      &lt;div class=&amp;quot;row ctx&amp;quot;&gt;&lt;div class=&amp;quot;gutter&amp;quot;&gt;96&lt;/div&gt;&lt;div class=&amp;quot;sign&amp;quot;&gt; &lt;/div&gt;&lt;div class=&amp;quot;code&amp;quot;&gt;        results = scan_files(target=empty)&lt;/div&gt;&lt;/div&gt;
      &lt;div class=&amp;quot;row ctx&amp;quot;&gt;&lt;div class=&amp;quot;gutter&amp;quot;&gt;97&lt;/div&gt;&lt;div class=&amp;quot;sign&amp;quot;&gt; &lt;/div&gt;&lt;div class=&amp;quot;code&amp;quot;&gt;        assert results == []&lt;/div&gt;&lt;/div&gt;
      &lt;div class=&amp;quot;row add&amp;quot;&gt;&lt;div class=&amp;quot;gutter&amp;quot;&gt;98&lt;/div&gt;&lt;div class=&amp;quot;sign&amp;quot;&gt;+&lt;/div&gt;&lt;div class=&amp;quot;code&amp;quot;&gt; &lt;/div&gt;&lt;/div&gt;
      &lt;div class=&amp;quot;row add&amp;quot;&gt;&lt;div class=&amp;quot;gutter&amp;quot;&gt;99&lt;/div&gt;&lt;div class=&amp;quot;sign&amp;quot;&gt;+&lt;/div&gt;&lt;div class=&amp;quot;code&amp;quot;&gt;    def test_gitignore_respected_without_git_dir(self, tmp_path):&lt;/div&gt;&lt;/div&gt;
      &lt;div class=&amp;quot;row add&amp;quot;&gt;&lt;div class=&amp;quot;gutter&amp;quot;&gt;100&lt;/div&gt;&lt;div class=&amp;quot;sign&amp;quot;&gt;+&lt;/div&gt;&lt;div class=&amp;quot;code&amp;quot;&gt;        (tmp_path / &amp;quot;.gitignore&amp;quot;).write_text(&amp;quot;ignored/\n*.log\n&amp;quot;)&lt;/div&gt;&lt;/div&gt;
      &lt;div class=&amp;quot;row add&amp;quot;&gt;&lt;div class=&amp;quot;gutter&amp;quot;&gt;101&lt;/div&gt;&lt;div class=&amp;quot;sign&amp;quot;&gt;+&lt;/div&gt;&lt;div class=&amp;quot;code&amp;quot;&gt;        (tmp_path / &amp;quot;keep.py&amp;quot;).write_text(&amp;quot;print('hi')\n&amp;quot;)&lt;/div&gt;&lt;/div&gt;
      &lt;div class=&amp;quot;row add&amp;quot;&gt;&lt;div class=&amp;quot;gutter&amp;quot;&gt;102&lt;/div&gt;&lt;div class=&amp;quot;sign&amp;quot;&gt;+&lt;/div&gt;&lt;div class=&amp;quot;code&amp;quot;&gt;        (tmp_path / &amp;quot;debug.log&amp;quot;).write_text(&amp;quot;noise\n&amp;quot;)&lt;/div&gt;&lt;/div&gt;
      &lt;div class=&amp;quot;row add&amp;quot;&gt;&lt;div class=&amp;quot;gutter&amp;quot;&gt;103&lt;/div&gt;&lt;div class=&amp;quot;sign&amp;quot;&gt;+&lt;/div&gt;&lt;div class=&amp;quot;code&amp;quot;&gt;        (tmp_path / &amp;quot;ignored&amp;quot;).mkdir()&lt;/div&gt;&lt;/div&gt;
      &lt;div class=&amp;quot;row add&amp;quot;&gt;&lt;div class=&amp;quot;gutter&amp;quot;&gt;104&lt;/div&gt;&lt;div class=&amp;quot;sign&amp;quot;&gt;+&lt;/div&gt;&lt;div class=&amp;quot;code&amp;quot;&gt;        (tmp_path / &amp;quot;ignored&amp;quot; / &amp;quot;junk.py&amp;quot;).write_text(&amp;quot;x = 1\n&amp;quot;)&lt;/div&gt;&lt;/div&gt;
      &lt;div class=&amp;quot;row add&amp;quot;&gt;&lt;div class=&amp;quot;gutter&amp;quot;&gt;105&lt;/div&gt;&lt;div class=&amp;quot;sign&amp;quot;&gt;+&lt;/div&gt;&lt;div class=&amp;quot;code&amp;quot;&gt; &lt;/div&gt;&lt;/div&gt;
      &lt;div class=&amp;quot;row add&amp;quot;&gt;&lt;div class=&amp;quot;gutter&amp;quot;&gt;106&lt;/div&gt;&lt;div class=&amp;quot;sign&amp;quot;&gt;+&lt;/div&gt;&lt;div class=&amp;quot;code&amp;quot;&gt;        results = scan_files(target=tmp_path)&lt;/div&gt;&lt;/div&gt;
      &lt;div class=&amp;quot;row add&amp;quot;&gt;&lt;div class=&amp;quot;gutter&amp;quot;&gt;107&lt;/div&gt;&lt;div class=&amp;quot;sign&amp;quot;&gt;+&lt;/div&gt;&lt;div class=&amp;quot;code&amp;quot;&gt;        names = {r[0].name for r in results}&lt;/div&gt;&lt;/div&gt;
      &lt;div class=&amp;quot;row add&amp;quot;&gt;&lt;div class=&amp;quot;gutter&amp;quot;&gt;108&lt;/div&gt;&lt;div class=&amp;quot;sign&amp;quot;&gt;+&lt;/div&gt;&lt;div class=&amp;quot;code&amp;quot;&gt;        assert &amp;quot;keep.py&amp;quot; in names&lt;/div&gt;&lt;/div&gt;
      &lt;div class=&amp;quot;row add&amp;quot;&gt;&lt;div class=&amp;quot;gutter&amp;quot;&gt;109&lt;/div&gt;&lt;div class=&amp;quot;sign&amp;quot;&gt;+&lt;/div&gt;&lt;div class=&amp;quot;code&amp;quot;&gt;        assert &amp;quot;debug.log&amp;quot; not in names&lt;/div&gt;&lt;/div&gt;
      &lt;div class=&amp;quot;row add&amp;quot;&gt;&lt;div class=&amp;quot;gutter&amp;quot;&gt;110&lt;/div&gt;&lt;div class=&amp;quot;sign&amp;quot;&gt;+&lt;/div&gt;&lt;div class=&amp;quot;code&amp;quot;&gt;        assert &amp;quot;junk.py&amp;quot; not in names&lt;/div&gt;&lt;/div&gt;
    &lt;/div&gt;
    &lt;div class=&amp;quot;notes&amp;quot;&gt;
      &lt;div class=&amp;quot;ann pass&amp;quot;&gt;
        &lt;div class=&amp;quot;anchor&amp;quot;&gt;test as a whole&lt;/div&gt;
        &lt;div class=&amp;quot;head&amp;quot;&gt;Pass &amp;amp;mdash; exercises the fixed path&lt;/div&gt;
        &lt;div class=&amp;quot;body&amp;quot;&gt;
          &lt;code&gt;tmp_path&lt;/code&gt; is a fresh directory with no &lt;code&gt;.git&lt;/code&gt; anywhere upstream (pytest's tmp lives outside any project repo by default). It covers both a glob (&lt;code&gt;*.log&lt;/code&gt;) and a directory (&lt;code&gt;ignored/&lt;/code&gt;), and asserts both positive (&lt;code&gt;keep.py&lt;/code&gt; kept) and negative (two paths excluded). This test would have failed against the pre-fix code.
        &lt;/div&gt;
      &lt;/div&gt;
      &lt;div class=&amp;quot;ann nit&amp;quot;&gt;
        &lt;div class=&amp;quot;anchor&amp;quot;&gt;coverage gap&lt;/div&gt;
        &lt;div class=&amp;quot;head&amp;quot;&gt;Nit &amp;amp;mdash; consider one more case&lt;/div&gt;
        &lt;div class=&amp;quot;body&amp;quot;&gt;
          &lt;code&gt;load_gitignore_specs&lt;/code&gt; supports nested &lt;code&gt;.gitignore&lt;/code&gt; files (it walks the whole subtree and prefixes patterns with the relative directory). The new test only places a &lt;code&gt;.gitignore&lt;/code&gt; at the root of &lt;code&gt;tmp_path&lt;/code&gt;. A second test with &lt;code&gt;tmp_path / &amp;quot;sub&amp;quot; / &amp;quot;.gitignore&amp;quot;&lt;/code&gt; and a file inside &lt;code&gt;sub/&lt;/code&gt; would lock in the nested-spec behavior under the new fallback path &amp;amp;mdash; that's the part most likely to regress in a future refactor.
        &lt;/div&gt;
      &lt;/div&gt;
      &lt;div class=&amp;quot;ann warn&amp;quot;&gt;
        &lt;div class=&amp;quot;anchor&amp;quot;&gt;latent assumption&lt;/div&gt;
        &lt;div class=&amp;quot;head&amp;quot;&gt;Concern &amp;amp;mdash; test would silently fail if pytest tmp ever lived under a &lt;code&gt;.git&lt;/code&gt;&lt;/div&gt;
        &lt;div class=&amp;quot;body&amp;quot;&gt;
          The test depends on pytest's &lt;code&gt;tmp_path&lt;/code&gt; not having a &lt;code&gt;.git&lt;/code&gt; ancestor. That's true today on every CI runner I'm aware of, and almost always true locally, but it's an implicit assumption. Adding an explicit &lt;code&gt;assert find_git_root(start=tmp_path) is None&lt;/code&gt; at the top of the test would make the assumption visible and produce a clearer failure if it ever broke.
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;h2&gt;Findings summary&lt;/h2&gt;

&lt;div class=&amp;quot;findings&amp;quot;&gt;

  &lt;div class=&amp;quot;finding pass&amp;quot;&gt;
    &lt;div class=&amp;quot;stripe&amp;quot;&gt;&lt;/div&gt;
    &lt;div class=&amp;quot;body&amp;quot;&gt;
      &lt;div class=&amp;quot;head&amp;quot;&gt;
        &lt;span class=&amp;quot;badge&amp;quot;&gt;Pass&lt;/span&gt;
        &lt;span class=&amp;quot;title&amp;quot;&gt;Fix is correct and minimally scoped&lt;/span&gt;
      &lt;/div&gt;
      &lt;span class=&amp;quot;where&amp;quot;&gt;scanner.py:107&amp;amp;ndash;111, 131&amp;amp;ndash;132&lt;/span&gt;
      &lt;p&gt;The same root value flows through spec construction and per-file relativization. No new code paths added; behavior with a real git root is byte-for-byte unchanged.&lt;/p&gt;
    &lt;/div&gt;
  &lt;/div&gt;

  &lt;div class=&amp;quot;finding pass&amp;quot;&gt;
    &lt;div class=&amp;quot;stripe&amp;quot;&gt;&lt;/div&gt;
    &lt;div class=&amp;quot;body&amp;quot;&gt;
      &lt;div class=&amp;quot;head&amp;quot;&gt;
        &lt;span class=&amp;quot;badge&amp;quot;&gt;Pass&lt;/span&gt;
        &lt;span class=&amp;quot;title&amp;quot;&gt;Variable rename improves accuracy&lt;/span&gt;
      &lt;/div&gt;
      &lt;span class=&amp;quot;where&amp;quot;&gt;scanner.py:107&lt;/span&gt;
      &lt;p&gt;&lt;code&gt;git_root&lt;/code&gt; &amp;amp;rarr; &lt;code&gt;gitignore_root&lt;/code&gt; reflects the broadened semantics. Reading the new code, the intent is clearer than the old code's intent was.&lt;/p&gt;
    &lt;/div&gt;
  &lt;/div&gt;

  &lt;div class=&amp;quot;finding pass&amp;quot;&gt;
    &lt;div class=&amp;quot;stripe&amp;quot;&gt;&lt;/div&gt;
    &lt;div class=&amp;quot;body&amp;quot;&gt;
      &lt;div class=&amp;quot;head&amp;quot;&gt;
        &lt;span class=&amp;quot;badge&amp;quot;&gt;Pass&lt;/span&gt;
        &lt;span class=&amp;quot;title&amp;quot;&gt;New test exercises the bug&lt;/span&gt;
      &lt;/div&gt;
      &lt;span class=&amp;quot;where&amp;quot;&gt;test_scanner.py:99&amp;amp;ndash;110&lt;/span&gt;
      &lt;p&gt;Test fails on pre-fix code, passes on post-fix code, covers both a glob pattern and a directory pattern.&lt;/p&gt;
    &lt;/div&gt;
  &lt;/div&gt;

  &lt;div class=&amp;quot;finding warn&amp;quot;&gt;
    &lt;div class=&amp;quot;stripe&amp;quot;&gt;&lt;/div&gt;
    &lt;div class=&amp;quot;body&amp;quot;&gt;
      &lt;div class=&amp;quot;head&amp;quot;&gt;
        &lt;span class=&amp;quot;badge&amp;quot;&gt;Concern&lt;/span&gt;
        &lt;span class=&amp;quot;title&amp;quot;&gt;Pre-existing: an unrelated upstream &lt;code&gt;.git&lt;/code&gt; hijacks the root&lt;/span&gt;
      &lt;/div&gt;
      &lt;span class=&amp;quot;where&amp;quot;&gt;scanner.py:109 (and find_git_root)&lt;/span&gt;
      &lt;p&gt;&lt;code&gt;find_git_root&lt;/code&gt; walks to &lt;code&gt;/&lt;/code&gt;. With &lt;code&gt;~/.git&lt;/code&gt; (dotfiles), running toks on &lt;code&gt;~/projects/whatever&lt;/code&gt; picks up the dotfiles repo and triggers &lt;code&gt;os.walk(~)&lt;/code&gt; in &lt;code&gt;load_gitignore_specs&lt;/code&gt;. Not introduced by this PR. Worth a follow-up to either bound the upward walk (e.g. stop at the user's home or filesystem boundary) or treat that case the way you treat &amp;amp;ldquo;no git root&amp;amp;rdquo; here.&lt;/p&gt;
    &lt;/div&gt;
  &lt;/div&gt;

  &lt;div class=&amp;quot;finding warn&amp;quot;&gt;
    &lt;div class=&amp;quot;stripe&amp;quot;&gt;&lt;/div&gt;
    &lt;div class=&amp;quot;body&amp;quot;&gt;
      &lt;div class=&amp;quot;head&amp;quot;&gt;
        &lt;span class=&amp;quot;badge&amp;quot;&gt;Concern&lt;/span&gt;
        &lt;span class=&amp;quot;title&amp;quot;&gt;Pre-existing: gitignored directories are still walked into&lt;/span&gt;
      &lt;/div&gt;
      &lt;span class=&amp;quot;where&amp;quot;&gt;scanner.py:scan_files loop&lt;/span&gt;
      &lt;p&gt;The PR description specifically calls out &lt;code&gt;.venv&lt;/code&gt;. With this fix, &lt;code&gt;.venv&lt;/code&gt;'s files are correctly excluded from results, but &lt;code&gt;os.walk&lt;/code&gt; still descends through every &lt;code&gt;site-packages&lt;/code&gt; file and stats it. On a fresh venv that's tens of thousands of stats. Pruning &lt;code&gt;dirnames&lt;/code&gt; against &lt;code&gt;gitignore_spec&lt;/code&gt; before recursing would address it. Out of scope for this PR; flag as follow-up.&lt;/p&gt;
    &lt;/div&gt;
  &lt;/div&gt;

  &lt;div class=&amp;quot;finding nit&amp;quot;&gt;
    &lt;div class=&amp;quot;stripe&amp;quot;&gt;&lt;/div&gt;
    &lt;div class=&amp;quot;body&amp;quot;&gt;
      &lt;div class=&amp;quot;head&amp;quot;&gt;
        &lt;span class=&amp;quot;badge&amp;quot;&gt;Nit&lt;/span&gt;
        &lt;span class=&amp;quot;title&amp;quot;&gt;&lt;code&gt;load_gitignore_specs&lt;/code&gt; parameter still named &lt;code&gt;git_root&lt;/code&gt;&lt;/span&gt;
      &lt;/div&gt;
      &lt;span class=&amp;quot;where&amp;quot;&gt;scanner.py:73&lt;/span&gt;
      &lt;p&gt;You renamed the variable in the caller; the parameter in the callee is now slightly misleading (callers may pass a non-git path). Trivial follow-up rename.&lt;/p&gt;
    &lt;/div&gt;
  &lt;/div&gt;

  &lt;div class=&amp;quot;finding nit&amp;quot;&gt;
    &lt;div class=&amp;quot;stripe&amp;quot;&gt;&lt;/div&gt;
    &lt;div class=&amp;quot;body&amp;quot;&gt;
      &lt;div class=&amp;quot;head&amp;quot;&gt;
        &lt;span class=&amp;quot;badge&amp;quot;&gt;Nit&lt;/span&gt;
        &lt;span class=&amp;quot;title&amp;quot;&gt;&lt;code&gt;and gitignore_root&lt;/code&gt; guard is redundant&lt;/span&gt;
      &lt;/div&gt;
      &lt;span class=&amp;quot;where&amp;quot;&gt;scanner.py:131&lt;/span&gt;
      &lt;p&gt;After the fix, the two flags are coupled. Harmless but no longer informative.&lt;/p&gt;
    &lt;/div&gt;
  &lt;/div&gt;

  &lt;div class=&amp;quot;finding nit&amp;quot;&gt;
    &lt;div class=&amp;quot;stripe&amp;quot;&gt;&lt;/div&gt;
    &lt;div class=&amp;quot;body&amp;quot;&gt;
      &lt;div class=&amp;quot;head&amp;quot;&gt;
        &lt;span class=&amp;quot;badge&amp;quot;&gt;Nit&lt;/span&gt;
        &lt;span class=&amp;quot;title&amp;quot;&gt;One more test would lock in nested-&lt;code&gt;.gitignore&lt;/code&gt; behavior on the fallback path&lt;/span&gt;
      &lt;/div&gt;
      &lt;span class=&amp;quot;where&amp;quot;&gt;test_scanner.py&lt;/span&gt;
      &lt;p&gt;Add a test where &lt;code&gt;tmp_path/sub/.gitignore&lt;/code&gt; defines a pattern and a file inside &lt;code&gt;sub/&lt;/code&gt; matches it &amp;amp;mdash; verifies &lt;code&gt;load_gitignore_specs&lt;/code&gt;'s prefix logic still works when the root is the target rather than a real git root.&lt;/p&gt;
    &lt;/div&gt;
  &lt;/div&gt;

&lt;/div&gt;

&lt;h2&gt;Behavioral coverage matrix&lt;/h2&gt;
&lt;p&gt;Cases the implementation can encounter and where each is covered:&lt;/p&gt;

&lt;table class=&amp;quot;cov&amp;quot;&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;Scenario&lt;/th&gt;
      &lt;th&gt;Pre-PR behavior&lt;/th&gt;
      &lt;th&gt;Post-PR behavior&lt;/th&gt;
      &lt;th&gt;Test coverage&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;Target is inside a git repo, has &lt;code&gt;.gitignore&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;Honored&lt;/td&gt;
      &lt;td&gt;Honored (unchanged)&lt;/td&gt;
      &lt;td&gt;&lt;span class=&amp;quot;yes&amp;quot;&gt;Yes&lt;/span&gt; &amp;amp;mdash; existing fixture-based tests&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Target has &lt;code&gt;.gitignore&lt;/code&gt;, no &lt;code&gt;.git&lt;/code&gt; upstream&lt;/td&gt;
      &lt;td&gt;&lt;span class=&amp;quot;no&amp;quot;&gt;Silently ignored&lt;/span&gt; (the bug)&lt;/td&gt;
      &lt;td&gt;Honored via target-as-root fallback&lt;/td&gt;
      &lt;td&gt;&lt;span class=&amp;quot;yes&amp;quot;&gt;Yes&lt;/span&gt; &amp;amp;mdash; new test&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Target has &lt;code&gt;.gitignore&lt;/code&gt; at root &lt;em&gt;and&lt;/em&gt; a nested &lt;code&gt;sub/.gitignore&lt;/code&gt;, no &lt;code&gt;.git&lt;/code&gt; upstream&lt;/td&gt;
      &lt;td&gt;Both ignored (bug)&lt;/td&gt;
      &lt;td&gt;Both should be honored (relies on &lt;code&gt;load_gitignore_specs&lt;/code&gt; walking from &lt;code&gt;target&lt;/code&gt;)&lt;/td&gt;
      &lt;td&gt;&lt;span class=&amp;quot;no&amp;quot;&gt;Not directly tested&lt;/span&gt;&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Target with no &lt;code&gt;.gitignore&lt;/code&gt; and no &lt;code&gt;.git&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;No filtering&lt;/td&gt;
      &lt;td&gt;No filtering (&lt;code&gt;load_gitignore_specs&lt;/code&gt; returns &lt;code&gt;None&lt;/code&gt;)&lt;/td&gt;
      &lt;td&gt;Implicit via &lt;code&gt;test_empty_dir&lt;/code&gt;&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Target with &lt;code&gt;no_gitignore=True&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;Skipped&lt;/td&gt;
      &lt;td&gt;Skipped (unchanged)&lt;/td&gt;
      &lt;td&gt;Not directly tested (pre-existing gap)&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Target inside an unrelated upstream &lt;code&gt;.git&lt;/code&gt; (e.g. &lt;code&gt;~/.git&lt;/code&gt;)&lt;/td&gt;
      &lt;td&gt;&lt;span class=&amp;quot;no&amp;quot;&gt;Walks entire ancestor tree&lt;/span&gt;&lt;/td&gt;
      &lt;td&gt;&lt;span class=&amp;quot;no&amp;quot;&gt;Same &amp;amp;mdash; not addressed&lt;/span&gt;&lt;/td&gt;
      &lt;td&gt;Not tested&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;h2&gt;Recommendation&lt;/h2&gt;
&lt;p&gt;Ship this PR as-is. The fix is correct, well-named, and has a test that would catch a regression. The two pre-existing concerns (upstream-&lt;code&gt;.git&lt;/code&gt; hijacking and per-file rather than per-directory filtering) are worth filing as follow-ups but should not block this merge &amp;amp;mdash; widening scope would dilute a clean, easy-to-review change.&lt;/p&gt;

&lt;/body&gt;
&lt;/html&gt;
" height="700" width="100%" style="border:1px solid #d0d7de;border-radius:6px;margin:1.5em 0"&gt;&lt;/iframe&gt;

&lt;p&gt;&lt;em&gt;The HTML PR review. Verdict bar, severity-tagged findings, annotated diff. Polished. But the markdown version below has the same substance.&lt;/em&gt;&lt;/p&gt;
&lt;iframe srcdoc="&lt;!doctype html&gt;
&lt;html lang=&amp;quot;en&amp;quot;&gt;
&lt;head&gt;
&lt;meta charset=&amp;quot;utf-8&amp;quot;&gt;
&lt;title&gt;Rendered markdown&lt;/title&gt;
&lt;style&gt;
  body { font: 15px/1.6 -apple-system, BlinkMacSystemFont, &amp;quot;Segoe UI&amp;quot;, Roboto, sans-serif;
         color: #1f2328; background: #ffffff; max-width: 760px; margin: 0 auto; padding: 24px; }
  h1 { font-size: 24px; margin-top: 0; border-bottom: 1px solid #d0d7de; padding-bottom: 8px; }
  h2 { font-size: 20px; margin-top: 28px; border-bottom: 1px solid #d0d7de; padding-bottom: 6px; }
  h3 { font-size: 16px; margin-top: 22px; }
  code { background: #f6f8fa; padding: 1px 5px; border-radius: 4px; font-size: 13px;
         font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; }
  pre { background: #f6f8fa; padding: 12px 14px; border-radius: 6px; overflow-x: auto;
        font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-size: 13px;
        line-height: 1.45; }
  pre code { background: transparent; padding: 0; font-size: inherit; }
  blockquote { border-left: 3px solid #d0d7de; padding-left: 12px; color: #59636e; margin: 12px 0; }
  table { border-collapse: collapse; margin: 12px 0; font-size: 14px; }
  th, td { border: 1px solid #d0d7de; padding: 6px 12px; text-align: left; vertical-align: top; }
  th { background: #f6f8fa; }
  hr { border: none; border-top: 1px solid #d0d7de; margin: 28px 0; }
  a { color: #0969da; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;h1&gt;PR Review — &lt;code&gt;toks&lt;/code&gt; c6d70f9&lt;/h1&gt;
&lt;p&gt;&lt;strong&gt;Title:&lt;/strong&gt; Respect .gitignore when target has no .git directory&lt;br /&gt;
&lt;strong&gt;Author:&lt;/strong&gt; Corey Gallon&lt;br /&gt;
&lt;strong&gt;Date:&lt;/strong&gt; 2026-04-29&lt;br /&gt;
&lt;strong&gt;Files touched:&lt;/strong&gt; &lt;code&gt;src/toks/scanner.py&lt;/code&gt; (+4 / -4), &lt;code&gt;tests/test_scanner.py&lt;/code&gt; (+13 / -0)&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;TL;DR&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Verdict: Approve with two non-blocking notes.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;The fix is minimal, correct for the bug it targets, and well-tested. The rename from &lt;code&gt;git_root&lt;/code&gt; to &lt;code&gt;gitignore_root&lt;/code&gt; is the right framing — the variable is now &amp;quot;the root we're matching gitignore patterns relative to,&amp;quot; and that name finally tells the truth.&lt;/p&gt;
&lt;p&gt;The two notes worth flagging before merge:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;The fallback intentionally narrows discovery: &lt;code&gt;.gitignore&lt;/code&gt; files &lt;strong&gt;above&lt;/strong&gt; the target are no longer consulted when no &lt;code&gt;.git&lt;/code&gt; is present (they never were, but the fallback formalizes that). Worth a one-line docstring note so future-you doesn't re-litigate it.&lt;/li&gt;
&lt;li&gt;The new test is mildly fragile to ancestor-&lt;code&gt;.git&lt;/code&gt; pollution from &lt;code&gt;tmp_path&lt;/code&gt;. In practice fine; a single &lt;code&gt;no_gitignore=False&lt;/code&gt; is implicit and may want a sibling assertion to lock the contract.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Neither blocks the merge.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Severity legend&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tag&lt;/th&gt;
&lt;th&gt;Meaning&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;[CRITICAL]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Blocks merge. Correctness, security, data loss.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;[HIGH]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Should fix before merge. Likely to bite users.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;[MEDIUM]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Address before merge if cheap, otherwise track.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;[LOW]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Nit / polish. Optional.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;[POSITIVE]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Worth calling out — got this right.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;This review found: &lt;strong&gt;0 CRITICAL · 0 HIGH · 2 MEDIUM · 3 LOW · 2 POSITIVE.&lt;/strong&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Bracing the gitignore / path-resolution logic&lt;/h2&gt;
&lt;p&gt;I want to walk this carefully because the bug is exactly the kind of thing that hides in path semantics. Here's the model after the change:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;target = the directory the user pointed toks at
git_root = nearest ancestor containing .git (or None)
gitignore_root = git_root if git_root else target   # NEW
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;gitignore_root&lt;/code&gt; is then used for two things:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;As the walk root for &lt;code&gt;.gitignore&lt;/code&gt; discovery&lt;/strong&gt; — &lt;code&gt;load_gitignore_specs(git_root=...)&lt;/code&gt; does &lt;code&gt;os.walk(git_root)&lt;/code&gt; and concatenates patterns from every &lt;code&gt;.gitignore&lt;/code&gt; it finds, prefixed by their relative path under that root.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;As the basis for relativization at match time&lt;/strong&gt; — &lt;code&gt;file_path.relative_to(gitignore_root)&lt;/code&gt; produces the path string passed to &lt;code&gt;pathspec.match_file&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;These two uses must use the &lt;strong&gt;same&lt;/strong&gt; root for matching to be correct. The PR keeps them in lockstep — that's the load-bearing invariant, and it's preserved.&lt;/p&gt;
&lt;h3&gt;Walking the four cases&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Case&lt;/th&gt;
&lt;th&gt;git found?&lt;/th&gt;
&lt;th&gt;&lt;code&gt;git_root&lt;/code&gt;&lt;/th&gt;
&lt;th&gt;&lt;code&gt;gitignore_root&lt;/code&gt; (after)&lt;/th&gt;
&lt;th&gt;Discovery walks&lt;/th&gt;
&lt;th&gt;Relativizes against&lt;/th&gt;
&lt;th&gt;Correct?&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Target inside a git repo&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/repo&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/repo&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/repo&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/repo&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;yes (unchanged)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Target IS the git root&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;&lt;code&gt;target&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;target&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;target&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;target&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;yes (unchanged)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Target has its own &lt;code&gt;.gitignore&lt;/code&gt;, no &lt;code&gt;.git&lt;/code&gt; anywhere&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;&lt;code&gt;None&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;target&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;target&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;target&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;yes — newly fixed&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Target has no &lt;code&gt;.gitignore&lt;/code&gt;, no &lt;code&gt;.git&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;&lt;code&gt;None&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;target&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;target&lt;/code&gt; (yields no patterns)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;target&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;yes — &lt;code&gt;load_gitignore_specs&lt;/code&gt; returns &lt;code&gt;None&lt;/code&gt;, so the guard &lt;code&gt;if gitignore_spec and gitignore_root:&lt;/code&gt; short-circuits&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;The fourth row is the one I'd want a reviewer to verify by reading. &lt;code&gt;load_gitignore_specs&lt;/code&gt; returns &lt;code&gt;None&lt;/code&gt; when &lt;code&gt;patterns&lt;/code&gt; is empty, the assignment becomes &lt;code&gt;gitignore_spec = None&lt;/code&gt;, and the per-file guard skips matching. No regression.&lt;/p&gt;
&lt;h3&gt;What the fallback does NOT do&lt;/h3&gt;
&lt;p&gt;There's one behavior it would be easy to assume but isn't true: &lt;strong&gt;the fallback does not search upward for &lt;code&gt;.gitignore&lt;/code&gt; files when there's no &lt;code&gt;.git&lt;/code&gt;.&lt;/strong&gt; If the layout is&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/parent/
  .gitignore        # contains &amp;amp;quot;*.log&amp;amp;quot;
  child/
    debug.log
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;…and the user runs &lt;code&gt;toks /parent/child&lt;/code&gt;, &lt;code&gt;debug.log&lt;/code&gt; will be scanned. &lt;code&gt;find_git_root&lt;/code&gt; returns &lt;code&gt;None&lt;/code&gt; (no ancestor has &lt;code&gt;.git&lt;/code&gt;), &lt;code&gt;gitignore_root&lt;/code&gt; falls back to &lt;code&gt;child&lt;/code&gt;, and &lt;code&gt;os.walk(child)&lt;/code&gt; never sees &lt;code&gt;/parent/.gitignore&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;This is consistent with the commit message (&amp;quot;fall back to the target directory itself&amp;quot;) and consistent with how git itself behaves (no &lt;code&gt;.git&lt;/code&gt; → no project boundary → no upward &lt;code&gt;.gitignore&lt;/code&gt; chain). I'd just like one line in the docstring saying so, because the next person to think about this will think about it again otherwise.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;The diff, annotated&lt;/h2&gt;
&lt;p&gt;Below: each hunk, followed by per-line notes. Annotations cite line numbers from the &lt;em&gt;new&lt;/em&gt; file.&lt;/p&gt;
&lt;h3&gt;Hunk 1 — &lt;code&gt;scanner.py&lt;/code&gt; lines 104-112&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&amp;quot;language-python&amp;quot;&gt;   target = target.resolve()
   if not target.is_dir():
       raise ValueError(f&amp;amp;quot;Not a directory: {target}&amp;amp;quot;)

   gitignore_spec = None
-  git_root = None
+  gitignore_root = None                                                       # ← (A)
   if not no_gitignore:
       git_root = find_git_root(start=target)
-      if git_root:
-          gitignore_spec = load_gitignore_specs(git_root=git_root, target=target)
+      gitignore_root = git_root if git_root else target                       # ← (B)
+      gitignore_spec = load_gitignore_specs(git_root=gitignore_root, target=target)  # ← (C)
&lt;/code&gt;&lt;/pre&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Mark&lt;/th&gt;
&lt;th&gt;Annotation&lt;/th&gt;
&lt;th&gt;Severity&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;A&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Rename is the right call — this variable now means &amp;quot;the root we relativize against,&amp;quot; not &amp;quot;the git repo root.&amp;quot; Name follows semantics.&lt;/td&gt;
&lt;td&gt;&lt;code&gt;[POSITIVE]&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;B&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Ternary is readable. Could equivalently be &lt;code&gt;gitignore_root = git_root or target&lt;/code&gt;, which is shorter and idiomatic Python. Style preference; either is fine.&lt;/td&gt;
&lt;td&gt;&lt;code&gt;[LOW]&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;C&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;The &lt;code&gt;target=target&lt;/code&gt; keyword arg is now unused inside &lt;code&gt;load_gitignore_specs&lt;/code&gt; — &lt;code&gt;target_resolved = target.resolve()&lt;/code&gt; is computed and never read. Pre-existing dead code, but this PR is the natural moment to either delete the parameter or use it. See finding &lt;strong&gt;F-3&lt;/strong&gt; below.&lt;/td&gt;
&lt;td&gt;&lt;code&gt;[MEDIUM]&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;Hunk 2 — &lt;code&gt;scanner.py&lt;/code&gt; lines 128-135&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&amp;quot;language-python&amp;quot;&gt;           if file_path.is_symlink() and file_path.is_dir():
               continue

-          if gitignore_spec and git_root:
-              rel = file_path.relative_to(git_root)
+          if gitignore_spec and gitignore_root:                               # ← (D)
+              rel = file_path.relative_to(gitignore_root)                     # ← (E)
               if gitignore_spec.match_file(str(rel)):
                   continue
&lt;/code&gt;&lt;/pre&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Mark&lt;/th&gt;
&lt;th&gt;Annotation&lt;/th&gt;
&lt;th&gt;Severity&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;D&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Guard updated in lockstep with the rename. The two checks (&lt;code&gt;gitignore_spec&lt;/code&gt;, &lt;code&gt;gitignore_root&lt;/code&gt;) are now both truthy iff we successfully built a spec — &lt;code&gt;gitignore_spec&lt;/code&gt; alone is sufficient (since spec is only built when root is set), but the redundant guard is defensive and harmless.&lt;/td&gt;
&lt;td&gt;&lt;code&gt;[POSITIVE]&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;E&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;relative_to(gitignore_root)&lt;/code&gt; is safe: &lt;code&gt;file_path&lt;/code&gt; comes from &lt;code&gt;os.walk(target)&lt;/code&gt;, and &lt;code&gt;gitignore_root&lt;/code&gt; is either &lt;code&gt;target&lt;/code&gt; or an ancestor of it, so &lt;code&gt;file_path&lt;/code&gt; is always under &lt;code&gt;gitignore_root&lt;/code&gt;. No &lt;code&gt;ValueError&lt;/code&gt; risk.&lt;/td&gt;
&lt;td&gt;&lt;code&gt;[POSITIVE]&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;Hunk 3 — &lt;code&gt;tests/test_scanner.py&lt;/code&gt; lines 99-110&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&amp;quot;language-python&amp;quot;&gt;+  def test_gitignore_respected_without_git_dir(self, tmp_path):
+      (tmp_path / &amp;amp;quot;.gitignore&amp;amp;quot;).write_text(&amp;amp;quot;ignored/\n*.log\n&amp;amp;quot;)
+      (tmp_path / &amp;amp;quot;keep.py&amp;amp;quot;).write_text(&amp;amp;quot;print('hi')\n&amp;amp;quot;)
+      (tmp_path / &amp;amp;quot;debug.log&amp;amp;quot;).write_text(&amp;amp;quot;noise\n&amp;amp;quot;)
+      (tmp_path / &amp;amp;quot;ignored&amp;amp;quot;).mkdir()
+      (tmp_path / &amp;amp;quot;ignored&amp;amp;quot; / &amp;amp;quot;junk.py&amp;amp;quot;).write_text(&amp;amp;quot;x = 1\n&amp;amp;quot;)
+
+      results = scan_files(target=tmp_path)                                   # ← (F)
+      names = {r[0].name for r in results}
+      assert &amp;amp;quot;keep.py&amp;amp;quot; in names
+      assert &amp;amp;quot;debug.log&amp;amp;quot; not in names                                         # ← (G)
+      assert &amp;amp;quot;junk.py&amp;amp;quot; not in names                                           # ← (H)
&lt;/code&gt;&lt;/pre&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Mark&lt;/th&gt;
&lt;th&gt;Annotation&lt;/th&gt;
&lt;th&gt;Severity&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;F&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Test exercises the regression directly: &lt;code&gt;tmp_path&lt;/code&gt; is system tmp on Linux/macOS and typically has no ancestor &lt;code&gt;.git&lt;/code&gt;. Fragility note in &lt;strong&gt;F-1&lt;/strong&gt; below.&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;G&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Covers the file-glob case (&lt;code&gt;*.log&lt;/code&gt;).&lt;/td&gt;
&lt;td&gt;&lt;code&gt;[POSITIVE]&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;H&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Covers the directory case (&lt;code&gt;ignored/&lt;/code&gt;). Both major gitignore pattern shapes get coverage.&lt;/td&gt;
&lt;td&gt;&lt;code&gt;[POSITIVE]&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;The test asserts the &lt;em&gt;positive&lt;/em&gt; (&lt;code&gt;keep.py&lt;/code&gt; in) and the &lt;em&gt;negatives&lt;/em&gt; (&lt;code&gt;debug.log&lt;/code&gt;, &lt;code&gt;junk.py&lt;/code&gt; not in). That's the right shape — a test that only asserted exclusions could pass with &lt;code&gt;scan_files&lt;/code&gt; returning &lt;code&gt;[]&lt;/code&gt;.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Findings&lt;/h2&gt;
&lt;h3&gt;&lt;code&gt;[MEDIUM]&lt;/code&gt; F-1 — Test is silently dependent on &lt;code&gt;tmp_path&lt;/code&gt; having no ancestor &lt;code&gt;.git&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;find_git_root&lt;/code&gt; walks upward from &lt;code&gt;target.resolve()&lt;/code&gt; until it hits the filesystem root, looking for any &lt;code&gt;.git&lt;/code&gt;. If a developer's test environment puts &lt;code&gt;tmp_path&lt;/code&gt; somewhere under a git checkout (rare but possible — custom &lt;code&gt;tmpdir&lt;/code&gt; configs, certain CI sandboxes, network-mounted home dirs with stray &lt;code&gt;.git&lt;/code&gt; symlinks), the test would skip the new code path entirely. It would still likely pass because of how the patterns happen to be structured, but it would no longer be testing what its name claims.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Suggested fix:&lt;/strong&gt; Either (a) explicitly verify no ancestor has &lt;code&gt;.git&lt;/code&gt; at the start of the test, or (b) make the intent unambiguous by also asserting via a second call with &lt;code&gt;no_gitignore=True&lt;/code&gt; that the included set differs:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&amp;quot;language-python&amp;quot;&gt;results_no_ignore = scan_files(target=tmp_path, no_gitignore=True)
no_ignore_names = {r[0].name for r in results_no_ignore}
assert &amp;amp;quot;debug.log&amp;amp;quot; in no_ignore_names  # confirms gitignore did the filtering
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That second assertion locks the contract: &amp;quot;filtering happened &lt;em&gt;because of&lt;/em&gt; gitignore handling,&amp;quot; not &amp;quot;filtering happened, somehow.&amp;quot;&lt;/p&gt;
&lt;h3&gt;&lt;code&gt;[MEDIUM]&lt;/code&gt; F-2 — Behavior of fallback should be documented&lt;/h3&gt;
&lt;p&gt;The docstring of &lt;code&gt;scan_files&lt;/code&gt; doesn't mention gitignore handling at all today. With this change, the rule &amp;quot;we'll honor a &lt;code&gt;.gitignore&lt;/code&gt; in target even without &lt;code&gt;.git&lt;/code&gt;&amp;quot; is now part of the contract. One line is enough:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&amp;quot;language-python&amp;quot;&gt;&amp;amp;quot;&amp;amp;quot;&amp;amp;quot;Scan a directory for files, returning (path, mime_type, file_size) tuples.

…

When no_gitignore is False (default), .gitignore files are honored. The
gitignore root is the nearest ancestor containing .git, or the target
itself if no such ancestor exists. .gitignore files above the gitignore
root are not consulted.
&amp;amp;quot;&amp;amp;quot;&amp;amp;quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;&lt;code&gt;[LOW]&lt;/code&gt; F-3 — Unused parameter in &lt;code&gt;load_gitignore_specs&lt;/code&gt;&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&amp;quot;language-python&amp;quot;&gt;def load_gitignore_specs(*, git_root: Path, target: Path) -&amp;amp;gt; pathspec.PathSpec | None:
    patterns: list[str] = []
    target_resolved = target.resolve()  # ← never read
    ...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;target&lt;/code&gt; is no longer used inside this function. Pre-existing, not introduced by this PR — but the PR is the natural pass-by for it. Either remove the parameter and the dead line, or use it (e.g., to skip &lt;code&gt;.gitignore&lt;/code&gt; files outside the target subtree if you wanted to make scoping stricter — though I'd argue you don't, because git itself doesn't).&lt;/p&gt;
&lt;p&gt;If removing: also rename &lt;code&gt;git_root&lt;/code&gt; → &lt;code&gt;root&lt;/code&gt; while you're there, since the parameter is no longer git-specific in concept.&lt;/p&gt;
&lt;h3&gt;&lt;code&gt;[LOW]&lt;/code&gt; F-4 — Style: &lt;code&gt;git_root or target&lt;/code&gt; over the ternary&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&amp;quot;language-python&amp;quot;&gt;gitignore_root = git_root if git_root else target
# vs
gitignore_root = git_root or target
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;Path&lt;/code&gt; instances are always truthy, and &lt;code&gt;find_git_root&lt;/code&gt; returns &lt;code&gt;None&lt;/code&gt; or a &lt;code&gt;Path&lt;/code&gt;, so the short form is both safe and idiomatic. Pure preference.&lt;/p&gt;
&lt;h3&gt;&lt;code&gt;[LOW]&lt;/code&gt; F-5 — &lt;code&gt;find_git_root&lt;/code&gt; accepts &lt;code&gt;.git&lt;/code&gt; as either file or directory; commit message says &amp;quot;directory&amp;quot;&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;find_git_root&lt;/code&gt; checks &lt;code&gt;(current / &amp;quot;.git&amp;quot;).exists()&lt;/code&gt;, which is true for both directories and files. Worktrees use a &lt;code&gt;.git&lt;/code&gt; &lt;em&gt;file&lt;/em&gt; (containing &lt;code&gt;gitdir: ...&lt;/code&gt;). The commit message says &amp;quot;no &lt;code&gt;.git&lt;/code&gt; directory,&amp;quot; but the code does the right thing for worktrees too. This is a wording nit on the commit message, not a code issue. The change correctly does not regress worktree handling.&lt;/p&gt;
&lt;h3&gt;&lt;code&gt;[POSITIVE]&lt;/code&gt; F-6 — Minimal diff&lt;/h3&gt;
&lt;p&gt;Four-line change in production code, two of them pure renames, plus a focused test. Doesn't touch unrelated logic. Doesn't introduce new abstractions. The kind of fix that ages well.&lt;/p&gt;
&lt;h3&gt;&lt;code&gt;[POSITIVE]&lt;/code&gt; F-7 — Test assertions cover both pattern shapes&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;*.log&lt;/code&gt; (file glob) and &lt;code&gt;ignored/&lt;/code&gt; (directory) are the two pattern syntaxes most users care about, and both are exercised. A &lt;code&gt;.gitignore&lt;/code&gt; parser regression in either would fail this test.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Suggested commit-message tweak (optional)&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;Respect .gitignore when target has no .git directory &lt;strong&gt;or worktree marker&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Tiny edit; keeps the message accurate for the worktree-file case the code already handles.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Pre-merge checklist&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;[ ] Add the docstring note from &lt;strong&gt;F-2&lt;/strong&gt; (10 seconds).&lt;/li&gt;
&lt;li&gt;[ ] Optional: tighten the test per &lt;strong&gt;F-1&lt;/strong&gt; (one extra &lt;code&gt;no_gitignore=True&lt;/code&gt; call).&lt;/li&gt;
&lt;li&gt;[ ] Optional: address &lt;strong&gt;F-3&lt;/strong&gt; in a follow-up cleanup commit.&lt;/li&gt;
&lt;li&gt;[ ] No security implications. No performance regression: when &lt;code&gt;git_root&lt;/code&gt; is &lt;code&gt;None&lt;/code&gt; and target has no &lt;code&gt;.gitignore&lt;/code&gt;, &lt;code&gt;load_gitignore_specs&lt;/code&gt; walks the target tree once and returns &lt;code&gt;None&lt;/code&gt;. That walk is bounded by the same tree the main &lt;code&gt;os.walk&lt;/code&gt; traverses anyway, so worst case is one extra traversal of a directory the user already chose to scan.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Ready to merge after F-2.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;" height="700" width="100%" style="border:1px solid #d0d7de;border-radius:6px;margin:1.5em 0"&gt;&lt;/iframe&gt;

&lt;p&gt;&lt;em&gt;The same review, in markdown. Same content. Roughly 30% cheaper. The cost of "looks nicer" is a real number, and that's it.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;I also asked Claude to make a semantic edit to each artifact (add a "Test coverage assessment" section) and looked at the resulting diffs. HTML's was 5,658 bytes / 93 lines; markdown's was 4,618 bytes / 55 lines. HTML's structural overhead (1.2x more bytes than markdown for the same logical edit) is real but modest, because both formats already had a fair bit of structure to begin with.&lt;/p&gt;
&lt;p&gt;So: markdown does the job. HTML is prettier and costs about 50% more. Worth it for a review template you'll reuse across the team and reference repeatedly; probably not for a one-off you'll glance at and close.&lt;/p&gt;
&lt;h2 id="rate-limiter-explainer-where-the-markdown-actually-won-on-substance"&gt;Rate limiter explainer: where the markdown actually won on substance&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Methodology note:&lt;/strong&gt; same as above, verbatim ablation only. The 1.44x cost ratio is from a HTML-affording prompt and would likely be smaller under a neutral one.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;Thariq's prompt (lightly adapted):&lt;/strong&gt; &lt;em&gt;"I don't understand how slowapi's rate limiter actually works. Read the relevant code and produce a single HTML explainer page: a diagram of the rate-limiting flow, the 3-4 key code snippets annotated, and a 'gotchas' section..."&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Substrate:&lt;/strong&gt; &lt;code&gt;slowapi&lt;/code&gt; core (&lt;code&gt;extension.py&lt;/code&gt;, &lt;code&gt;middleware.py&lt;/code&gt;, &lt;code&gt;wrappers.py&lt;/code&gt;), about 12K tokens, ~1,200 lines of Python.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Cost data:&lt;/strong&gt;&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Format&lt;/th&gt;
&lt;th&gt;Artifact tokens&lt;/th&gt;
&lt;th&gt;Output tokens&lt;/th&gt;
&lt;th&gt;Generation time&lt;/th&gt;
&lt;th&gt;Cost&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;HTML&lt;/td&gt;
&lt;td&gt;8,700&lt;/td&gt;
&lt;td&gt;14,713&lt;/td&gt;
&lt;td&gt;191 s&lt;/td&gt;
&lt;td&gt;$0.79&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MD&lt;/td&gt;
&lt;td&gt;3,629&lt;/td&gt;
&lt;td&gt;7,915&lt;/td&gt;
&lt;td&gt;118 s&lt;/td&gt;
&lt;td&gt;$0.55&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Ratios: 2.40x artifact tokens, 1.86x output tokens, 1.62x time, 1.44x cost.&lt;/p&gt;
&lt;p&gt;Both artifacts are real explainers. The bit I wasn't expecting: &lt;strong&gt;both used Mermaid for the flow diagram.&lt;/strong&gt; The markdown one wraps it in a &lt;code&gt;```mermaid&lt;/code&gt; fence; the HTML one loads the Mermaid CDN and embeds the same chart definition. GitHub renders the markdown version as a real diagram. So does VS Code. The "HTML can show diagrams and markdown can't" intuition that does some quiet work in Thariq's argument is mostly gone for technical writing in 2026.&lt;/p&gt;
&lt;iframe srcdoc="&lt;!DOCTYPE html&gt;
&lt;html lang=&amp;quot;en&amp;quot;&gt;
&lt;head&gt;
&lt;meta charset=&amp;quot;utf-8&amp;quot;&gt;
&lt;title&gt;How slowapi's rate limiter actually works&lt;/title&gt;
&lt;script src=&amp;quot;https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.min.js&amp;quot;&gt;&lt;/script&gt;
&lt;script&gt;
  mermaid.initialize({ startOnLoad: true, theme: 'neutral', flowchart: { useMaxWidth: true, htmlLabels: true } });
&lt;/script&gt;
&lt;style&gt;
  :root {
    --fg: #1a1a1a;
    --muted: #555;
    --bg: #fafafa;
    --panel: #ffffff;
    --border: #d8d8d8;
    --accent: #0b5fff;
    --code-bg: #0f1115;
    --code-fg: #e6e6e6;
    --kw: #ff7b72;
    --str: #a5d6ff;
    --com: #8b949e;
    --num: #d2a8ff;
    --hilite: #fff7c2;
    --warn: #b54708;
    --warn-bg: #fff7ed;
    --warn-border: #fdba74;
  }
  html { box-sizing: border-box; }
  *, *:before, *:after { box-sizing: inherit; }
  body {
    margin: 0;
    font-family: -apple-system, BlinkMacSystemFont, &amp;quot;Segoe UI&amp;quot;, Roboto, Helvetica, Arial, sans-serif;
    color: var(--fg);
    background: var(--bg);
    line-height: 1.55;
    font-size: 16px;
  }
  .wrap {
    max-width: 980px;
    margin: 0 auto;
    padding: 36px 28px 80px;
  }
  h1 {
    font-size: 28px;
    margin: 0 0 4px;
    letter-spacing: -0.01em;
  }
  .subtitle {
    color: var(--muted);
    margin: 0 0 28px;
    font-size: 15px;
  }
  h2 {
    font-size: 21px;
    margin: 36px 0 12px;
    padding-bottom: 6px;
    border-bottom: 1px solid var(--border);
    letter-spacing: -0.01em;
  }
  h3 {
    font-size: 16px;
    margin: 24px 0 8px;
    color: #222;
  }
  p { margin: 0 0 12px; }
  .lede {
    background: var(--panel);
    border-left: 3px solid var(--accent);
    padding: 14px 18px;
    border-radius: 4px;
    margin-bottom: 20px;
  }
  .panel {
    background: var(--panel);
    border: 1px solid var(--border);
    border-radius: 6px;
    padding: 16px 18px;
    margin: 12px 0 20px;
  }
  .diagram {
    background: var(--panel);
    border: 1px solid var(--border);
    border-radius: 6px;
    padding: 14px;
    overflow-x: auto;
  }
  pre {
    background: var(--code-bg);
    color: var(--code-fg);
    padding: 14px 16px;
    border-radius: 6px;
    overflow-x: auto;
    font-size: 13.5px;
    font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
    line-height: 1.5;
    margin: 8px 0 0;
  }
  pre .kw { color: var(--kw); }
  pre .str { color: var(--str); }
  pre .com { color: var(--com); font-style: italic; }
  pre .num { color: var(--num); }
  pre .hi {
    background: rgba(255, 247, 194, 0.18);
    display: inline-block;
    width: 100%;
  }
  code.inline {
    background: #eef0f3;
    padding: 1px 6px;
    border-radius: 3px;
    font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
    font-size: 0.9em;
  }
  .annot {
    margin: 0 0 8px;
    color: var(--muted);
    font-size: 14.5px;
  }
  .snippet {
    margin-bottom: 22px;
  }
  .snippet-title {
    font-weight: 600;
    font-size: 15px;
    margin-bottom: 4px;
  }
  .file-tag {
    font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
    font-size: 12px;
    color: var(--muted);
    margin-left: 6px;
    font-weight: normal;
  }
  ol.gotchas, ul.gotchas { padding-left: 20px; }
  ol.gotchas li, ul.gotchas li { margin-bottom: 14px; }
  .gotcha {
    border-left: 3px solid var(--warn-border);
    background: var(--warn-bg);
    padding: 10px 14px;
    border-radius: 4px;
    margin: 10px 0;
  }
  .gotcha .label {
    color: var(--warn);
    font-weight: 600;
    font-size: 12.5px;
    letter-spacing: 0.04em;
    text-transform: uppercase;
    margin-bottom: 4px;
  }
  .pill {
    display: inline-block;
    padding: 1px 8px;
    border-radius: 999px;
    background: #eef2ff;
    color: #1e3a8a;
    font-size: 12px;
    margin-right: 6px;
    font-weight: 600;
  }
  table {
    border-collapse: collapse;
    width: 100%;
    margin: 8px 0 14px;
    font-size: 14.5px;
  }
  th, td {
    border: 1px solid var(--border);
    padding: 8px 10px;
    text-align: left;
    vertical-align: top;
  }
  th { background: #f3f4f6; font-weight: 600; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;div class=&amp;quot;wrap&amp;quot;&gt;

&lt;h1&gt;How slowapi's rate limiter actually works&lt;/h1&gt;
&lt;p class=&amp;quot;subtitle&amp;quot;&gt;A one-shot tour of &lt;code class=&amp;quot;inline&amp;quot;&gt;extension.py&lt;/code&gt;, &lt;code class=&amp;quot;inline&amp;quot;&gt;middleware.py&lt;/code&gt;, and &lt;code class=&amp;quot;inline&amp;quot;&gt;wrappers.py&lt;/code&gt;.&lt;/p&gt;

&lt;div class=&amp;quot;lede&amp;quot;&gt;
  &lt;p&gt;&lt;strong&gt;The shape in one paragraph.&lt;/strong&gt; slowapi is a thin orchestration layer over the &lt;code class=&amp;quot;inline&amp;quot;&gt;limits&lt;/code&gt; library. It collects rate-limit declarations from three places (decorators, app-level &lt;em&gt;application&lt;/em&gt; limits, app-level &lt;em&gt;default&lt;/em&gt; limits), and on each request it builds a list of which of those apply, calls &lt;code class=&amp;quot;inline&amp;quot;&gt;limits.hit()&lt;/code&gt; on each in turn against a configured backend (memory / Redis / etc.), and either lets the request through or raises &lt;code class=&amp;quot;inline&amp;quot;&gt;RateLimitExceeded&lt;/code&gt;. The check can be triggered from a decorator wrapper or from a Starlette middleware -- they are two entry points into the same core function, &lt;code class=&amp;quot;inline&amp;quot;&gt;_check_request_limit&lt;/code&gt;.&lt;/p&gt;
&lt;/div&gt;

&lt;h2&gt;Rate-limiting flow&lt;/h2&gt;

&lt;div class=&amp;quot;diagram&amp;quot;&gt;
&lt;div class=&amp;quot;mermaid&amp;quot;&gt;
flowchart TD
  Req([Incoming HTTP request])
  Req --&gt; Entry{&amp;quot;Entry point&amp;quot;}

  Entry --&gt;|&amp;quot;Middleware path&lt;br/&gt;(SlowAPIMiddleware /&lt;br/&gt;SlowAPIASGIMiddleware)&amp;quot;| MW[Find route handler&lt;br/&gt;via app.routes]
  Entry --&gt;|&amp;quot;Decorator path&lt;br/&gt;(@limiter.limit / @limiter.shared_limit)&amp;quot;| Dec[Decorator wrapper&lt;br/&gt;extracts request from args/kwargs]

  MW --&gt; Exempt{&amp;quot;_should_exempt?&lt;br/&gt;handler missing OR&lt;br/&gt;name in _exempt_routes OR&lt;br/&gt;name in _route_limits&amp;quot;}
  Exempt --&gt;|yes| Pass[Pass through, no check]
  Exempt --&gt;|no| CallCore[&amp;quot;_check_request_limit&lt;br/&gt;(in_middleware=True)&amp;quot;]

  Dec --&gt; Flag{&amp;quot;request.state._rate_limiting_complete&lt;br/&gt;already True?&amp;quot;}
  Flag --&gt;|yes| RunHandler1[Run handler]
  Flag --&gt;|no| CallCoreD[&amp;quot;_check_request_limit&lt;br/&gt;(in_middleware=False)&amp;quot;]
  CallCoreD --&gt; SetFlag[Set _rate_limiting_complete = True]
  SetFlag --&gt; RunHandler1

  CallCore --&gt; Build
  CallCoreD --&gt; Build

  Build[&amp;quot;Build all_limits list:&lt;br/&gt;• application_limits (only if in_middleware)&lt;br/&gt;• route_limits + dynamic_route_limits (only if NOT in_middleware)&lt;br/&gt;• default_limits (unless route has override_defaults=True)&amp;quot;]
  Build --&gt; StorageDead{&amp;quot;_storage_dead&lt;br/&gt;AND fallback_limiter?&amp;quot;}
  StorageDead --&gt;|yes| Fallback[Use _in_memory_fallback limits&lt;br/&gt;via _fallback_limiter]
  StorageDead --&gt;|no| Eval

  Fallback --&gt; Eval

  Eval[&amp;quot;__evaluate_limits:&lt;br/&gt;for each Limit in all_limits&amp;quot;]
  Eval --&gt; ForEach{&amp;quot;per-limit checks&amp;quot;}
  ForEach --&gt;|&amp;quot;is_exempt(request)&lt;br/&gt;or method mismatch&amp;quot;| Skip[Skip this limit]
  ForEach --&gt;|&amp;quot;otherwise&amp;quot;| Hit[&amp;quot;self.limiter.hit(&lt;br/&gt;limit, key_func(request), scope, cost=...)&amp;quot;]
  Skip --&gt; Eval

  Hit --&gt; Allowed{&amp;quot;hit returns True?&amp;quot;}
  Allowed --&gt;|yes, track smallest as&lt;br/&gt;limit_for_header| Eval
  Allowed --&gt;|no| Fail[&amp;quot;Set request.state.view_rate_limit&lt;br/&gt;raise RateLimitExceeded&amp;quot;]

  Eval --&gt;|&amp;quot;all passed&amp;quot;| Done[&amp;quot;request.state.view_rate_limit&lt;br/&gt;= smallest limit seen&amp;quot;]
  Done --&gt; RunHandler2[Run handler / call_next]

  RunHandler1 --&gt; Inject[_inject_headers / _inject_asgi_headers:&lt;br/&gt;X-RateLimit-Limit / Remaining / Reset / Retry-After]
  RunHandler2 --&gt; Inject
  Fail --&gt; ExcHandler[&amp;quot;_rate_limit_exceeded_handler&lt;br/&gt;builds 429 JSON response&amp;quot;]
  ExcHandler --&gt; Inject
  Inject --&gt; Resp([Response to client])
  Pass --&gt; Resp
&lt;/div&gt;
&lt;/div&gt;

&lt;h2&gt;The four code paths that matter&lt;/h2&gt;

&lt;div class=&amp;quot;snippet&amp;quot;&gt;
&lt;div class=&amp;quot;snippet-title&amp;quot;&gt;1. The core check: &lt;code class=&amp;quot;inline&amp;quot;&gt;_check_request_limit&lt;/code&gt; &lt;span class=&amp;quot;file-tag&amp;quot;&gt;extension.py&lt;/span&gt;&lt;/div&gt;
&lt;p class=&amp;quot;annot&amp;quot;&gt;Both entry points funnel here. The job of this function is to &lt;em&gt;assemble&lt;/em&gt; the list of limits that apply to this request, then hand them to &lt;code class=&amp;quot;inline&amp;quot;&gt;__evaluate_limits&lt;/code&gt;. The &lt;code class=&amp;quot;inline&amp;quot;&gt;in_middleware&lt;/code&gt; flag is the pivot: it controls which buckets of limits are pulled in, so middleware and decorator don't double-count or miss each other.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span class=&amp;quot;kw&amp;quot;&gt;def&lt;/span&gt; _check_request_limit(self, request, endpoint_func, in_middleware=&lt;span class=&amp;quot;num&amp;quot;&gt;True&lt;/span&gt;):
    endpoint_url     = request[&lt;span class=&amp;quot;str&amp;quot;&gt;&amp;quot;path&amp;quot;&lt;/span&gt;] &lt;span class=&amp;quot;kw&amp;quot;&gt;or&lt;/span&gt; &lt;span class=&amp;quot;str&amp;quot;&gt;&amp;quot;&amp;quot;&lt;/span&gt;
    endpoint_name    = &lt;span class=&amp;quot;kw&amp;quot;&gt;f&lt;/span&gt;&lt;span class=&amp;quot;str&amp;quot;&gt;&amp;quot;{endpoint_func.__module__}.{endpoint_func.__name__}&amp;quot;&lt;/span&gt; &lt;span class=&amp;quot;kw&amp;quot;&gt;if&lt;/span&gt; endpoint_func &lt;span class=&amp;quot;kw&amp;quot;&gt;else&lt;/span&gt; &lt;span class=&amp;quot;str&amp;quot;&gt;&amp;quot;&amp;quot;&lt;/span&gt;
    _endpoint_key    = endpoint_url &lt;span class=&amp;quot;kw&amp;quot;&gt;if&lt;/span&gt; self._key_style == &lt;span class=&amp;quot;str&amp;quot;&gt;&amp;quot;url&amp;quot;&lt;/span&gt; &lt;span class=&amp;quot;kw&amp;quot;&gt;else&lt;/span&gt; endpoint_name

    &lt;span class=&amp;quot;com&amp;quot;&gt;# Bail-outs: disabled, exempt, or a request_filter says skip.&lt;/span&gt;
    &lt;span class=&amp;quot;kw&amp;quot;&gt;if&lt;/span&gt; (&lt;span class=&amp;quot;kw&amp;quot;&gt;not&lt;/span&gt; _endpoint_key &lt;span class=&amp;quot;kw&amp;quot;&gt;or not&lt;/span&gt; self.enabled
        &lt;span class=&amp;quot;kw&amp;quot;&gt;or&lt;/span&gt; endpoint_name &lt;span class=&amp;quot;kw&amp;quot;&gt;in&lt;/span&gt; self._exempt_routes
        &lt;span class=&amp;quot;kw&amp;quot;&gt;or&lt;/span&gt; any(fn() &lt;span class=&amp;quot;kw&amp;quot;&gt;for&lt;/span&gt; fn &lt;span class=&amp;quot;kw&amp;quot;&gt;in&lt;/span&gt; self._request_filters)):
        &lt;span class=&amp;quot;kw&amp;quot;&gt;return&lt;/span&gt;

    limits, dynamic_limits = [], []
    &lt;span class=&amp;quot;kw&amp;quot;&gt;if not&lt;/span&gt; in_middleware:                              &lt;span class=&amp;quot;com&amp;quot;&gt;# decorator path only&lt;/span&gt;
        limits         = self._route_limits.get(endpoint_name, [])
        dynamic_limits = [l &lt;span class=&amp;quot;kw&amp;quot;&gt;for&lt;/span&gt; lg &lt;span class=&amp;quot;kw&amp;quot;&gt;in&lt;/span&gt; self._dynamic_route_limits.get(endpoint_name, [])
                            &lt;span class=&amp;quot;kw&amp;quot;&gt;for&lt;/span&gt; l &lt;span class=&amp;quot;kw&amp;quot;&gt;in&lt;/span&gt; lg.with_request(request)]

    route_limits      = limits + dynamic_limits
    all_limits        = list(itertools.chain(*self._application_limits)) &lt;span class=&amp;quot;kw&amp;quot;&gt;if&lt;/span&gt; in_middleware &lt;span class=&amp;quot;kw&amp;quot;&gt;else&lt;/span&gt; []
    all_limits       += route_limits

    &lt;span class=&amp;quot;com&amp;quot;&gt;# Defaults apply unless THIS route's limits all set override_defaults=True.&lt;/span&gt;
    combined_defaults = all(&lt;span class=&amp;quot;kw&amp;quot;&gt;not&lt;/span&gt; l.override_defaults &lt;span class=&amp;quot;kw&amp;quot;&gt;for&lt;/span&gt; l &lt;span class=&amp;quot;kw&amp;quot;&gt;in&lt;/span&gt; route_limits)
    &lt;span class=&amp;quot;kw&amp;quot;&gt;if&lt;/span&gt; (&lt;span class=&amp;quot;kw&amp;quot;&gt;not&lt;/span&gt; route_limits &lt;span class=&amp;quot;kw&amp;quot;&gt;or&lt;/span&gt; combined_defaults):
        all_limits   += list(itertools.chain(*self._default_limits))

    self.__evaluate_limits(request, _endpoint_key, all_limits)&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;

&lt;div class=&amp;quot;snippet&amp;quot;&gt;
&lt;div class=&amp;quot;snippet-title&amp;quot;&gt;2. The actual hit-or-miss: &lt;code class=&amp;quot;inline&amp;quot;&gt;__evaluate_limits&lt;/code&gt; &lt;span class=&amp;quot;file-tag&amp;quot;&gt;extension.py&lt;/span&gt;&lt;/div&gt;
&lt;p class=&amp;quot;annot&amp;quot;&gt;This is the hot loop. For each &lt;code class=&amp;quot;inline&amp;quot;&gt;Limit&lt;/code&gt; it computes the bucket key (&lt;code class=&amp;quot;inline&amp;quot;&gt;key_func(request)&lt;/code&gt; + scope), tracks the &lt;em&gt;smallest&lt;/em&gt; limit seen so far for header reporting, and calls &lt;code class=&amp;quot;inline&amp;quot;&gt;self.limiter.hit(...)&lt;/code&gt; -- this is the &lt;code class=&amp;quot;inline&amp;quot;&gt;limits&lt;/code&gt; library's &lt;code class=&amp;quot;inline&amp;quot;&gt;RateLimiter&lt;/code&gt;, the thing that actually increments the counter in Redis/memory. &lt;code class=&amp;quot;inline&amp;quot;&gt;hit&lt;/code&gt; returning &lt;code class=&amp;quot;inline&amp;quot;&gt;False&lt;/code&gt; means &amp;quot;you're over&amp;quot;; we capture the failed limit and break out so subsequent limits aren't decremented.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span class=&amp;quot;kw&amp;quot;&gt;def&lt;/span&gt; __evaluate_limits(self, request, endpoint, limits):
    failed_limit = &lt;span class=&amp;quot;num&amp;quot;&gt;None&lt;/span&gt;
    limit_for_header = &lt;span class=&amp;quot;num&amp;quot;&gt;None&lt;/span&gt;
    &lt;span class=&amp;quot;kw&amp;quot;&gt;for&lt;/span&gt; lim &lt;span class=&amp;quot;kw&amp;quot;&gt;in&lt;/span&gt; limits:
        limit_scope = lim.scope &lt;span class=&amp;quot;kw&amp;quot;&gt;or&lt;/span&gt; endpoint
        &lt;span class=&amp;quot;kw&amp;quot;&gt;if&lt;/span&gt; lim.is_exempt(request): &lt;span class=&amp;quot;kw&amp;quot;&gt;continue&lt;/span&gt;
        &lt;span class=&amp;quot;kw&amp;quot;&gt;if&lt;/span&gt; lim.methods &lt;span class=&amp;quot;kw&amp;quot;&gt;is not&lt;/span&gt; &lt;span class=&amp;quot;num&amp;quot;&gt;None&lt;/span&gt; &lt;span class=&amp;quot;kw&amp;quot;&gt;and&lt;/span&gt; request.method.lower() &lt;span class=&amp;quot;kw&amp;quot;&gt;not in&lt;/span&gt; lim.methods: &lt;span class=&amp;quot;kw&amp;quot;&gt;continue&lt;/span&gt;
        &lt;span class=&amp;quot;kw&amp;quot;&gt;if&lt;/span&gt; lim.per_method:
            limit_scope += &lt;span class=&amp;quot;str&amp;quot;&gt;&amp;quot;:%s&amp;quot;&lt;/span&gt; % request.method

        limit_key = lim.key_func(request) &lt;span class=&amp;quot;kw&amp;quot;&gt;if&lt;/span&gt; &lt;span class=&amp;quot;str&amp;quot;&gt;&amp;quot;request&amp;quot;&lt;/span&gt; &lt;span class=&amp;quot;kw&amp;quot;&gt;in&lt;/span&gt; inspect.signature(lim.key_func).parameters &lt;span class=&amp;quot;kw&amp;quot;&gt;else&lt;/span&gt; lim.key_func()
        args = [limit_key, limit_scope]
        &lt;span class=&amp;quot;kw&amp;quot;&gt;if&lt;/span&gt; all(args):
            &lt;span class=&amp;quot;kw&amp;quot;&gt;if&lt;/span&gt; self._key_prefix: args = [self._key_prefix] + args
            &lt;span class=&amp;quot;com&amp;quot;&gt;# Track the SMALLEST limit -- this is what gets reported in headers.&lt;/span&gt;
            &lt;span class=&amp;quot;kw&amp;quot;&gt;if not&lt;/span&gt; limit_for_header &lt;span class=&amp;quot;kw&amp;quot;&gt;or&lt;/span&gt; lim.limit &amp;amp;lt; limit_for_header[&lt;span class=&amp;quot;num&amp;quot;&gt;0&lt;/span&gt;]:
                limit_for_header = (lim.limit, args)

            cost = lim.cost(request) &lt;span class=&amp;quot;kw&amp;quot;&gt;if&lt;/span&gt; callable(lim.cost) &lt;span class=&amp;quot;kw&amp;quot;&gt;else&lt;/span&gt; lim.cost
            &lt;span class=&amp;quot;hi&amp;quot;&gt;&lt;span class=&amp;quot;kw&amp;quot;&gt;if not&lt;/span&gt; self.limiter.hit(lim.limit, *args, cost=cost):  &lt;span class=&amp;quot;com&amp;quot;&gt;# &amp;amp;lt;-- the actual check&lt;/span&gt;&lt;/span&gt;
                failed_limit = lim
                limit_for_header = (lim.limit, args)
                &lt;span class=&amp;quot;kw&amp;quot;&gt;break&lt;/span&gt;                                          &lt;span class=&amp;quot;com&amp;quot;&gt;# stop -- don't decrement remaining limits&lt;/span&gt;

    request.state.view_rate_limit = limit_for_header        &lt;span class=&amp;quot;com&amp;quot;&gt;# picked up by header injection later&lt;/span&gt;
    &lt;span class=&amp;quot;kw&amp;quot;&gt;if&lt;/span&gt; failed_limit:
        &lt;span class=&amp;quot;kw&amp;quot;&gt;raise&lt;/span&gt; RateLimitExceeded(failed_limit)&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;

&lt;div class=&amp;quot;snippet&amp;quot;&gt;
&lt;div class=&amp;quot;snippet-title&amp;quot;&gt;3. Decorator entry point: &lt;code class=&amp;quot;inline&amp;quot;&gt;__limit_decorator&lt;/code&gt; wrapper &lt;span class=&amp;quot;file-tag&amp;quot;&gt;extension.py&lt;/span&gt;&lt;/div&gt;
&lt;p class=&amp;quot;annot&amp;quot;&gt;The &lt;code class=&amp;quot;inline&amp;quot;&gt;@limiter.limit(&amp;quot;5/minute&amp;quot;)&lt;/code&gt; decorator registers the limit into &lt;code class=&amp;quot;inline&amp;quot;&gt;_route_limits&lt;/code&gt; / &lt;code class=&amp;quot;inline&amp;quot;&gt;_dynamic_route_limits&lt;/code&gt; at decoration time, then returns a wrapper. The wrapper finds the &lt;code class=&amp;quot;inline&amp;quot;&gt;Request&lt;/code&gt; in args/kwargs, runs the check (passing &lt;code class=&amp;quot;inline&amp;quot;&gt;in_middleware=False&lt;/code&gt;), runs the handler, injects headers. Note the &lt;code class=&amp;quot;inline&amp;quot;&gt;_rate_limiting_complete&lt;/code&gt; flag -- this is the &lt;em&gt;only&lt;/em&gt; guard against double-checking when middleware also fires.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span class=&amp;quot;kw&amp;quot;&gt;async def&lt;/span&gt; async_wrapper(*args, **kwargs):
    &lt;span class=&amp;quot;kw&amp;quot;&gt;if&lt;/span&gt; self.enabled:
        request = kwargs.get(&lt;span class=&amp;quot;str&amp;quot;&gt;&amp;quot;request&amp;quot;&lt;/span&gt;, args[idx] &lt;span class=&amp;quot;kw&amp;quot;&gt;if&lt;/span&gt; args &lt;span class=&amp;quot;kw&amp;quot;&gt;else&lt;/span&gt; &lt;span class=&amp;quot;num&amp;quot;&gt;None&lt;/span&gt;)
        &lt;span class=&amp;quot;kw&amp;quot;&gt;if not&lt;/span&gt; isinstance(request, Request):
            &lt;span class=&amp;quot;kw&amp;quot;&gt;raise&lt;/span&gt; Exception(&lt;span class=&amp;quot;str&amp;quot;&gt;&amp;quot;parameter `request` must be an instance of starlette.requests.Request&amp;quot;&lt;/span&gt;)

        &lt;span class=&amp;quot;hi&amp;quot;&gt;&lt;span class=&amp;quot;kw&amp;quot;&gt;if&lt;/span&gt; self._auto_check &lt;span class=&amp;quot;kw&amp;quot;&gt;and not&lt;/span&gt; getattr(request.state, &lt;span class=&amp;quot;str&amp;quot;&gt;&amp;quot;_rate_limiting_complete&amp;quot;&lt;/span&gt;, &lt;span class=&amp;quot;num&amp;quot;&gt;False&lt;/span&gt;):&lt;/span&gt;
            self._check_request_limit(request, func, &lt;span class=&amp;quot;num&amp;quot;&gt;False&lt;/span&gt;)        &lt;span class=&amp;quot;com&amp;quot;&gt;# in_middleware=False&lt;/span&gt;
            request.state._rate_limiting_complete = &lt;span class=&amp;quot;num&amp;quot;&gt;True&lt;/span&gt;          &lt;span class=&amp;quot;com&amp;quot;&gt;# &amp;amp;lt;-- only the decorator sets this&lt;/span&gt;

    response = &lt;span class=&amp;quot;kw&amp;quot;&gt;await&lt;/span&gt; func(*args, **kwargs)

    &lt;span class=&amp;quot;kw&amp;quot;&gt;if&lt;/span&gt; self.enabled:
        &lt;span class=&amp;quot;kw&amp;quot;&gt;if not&lt;/span&gt; isinstance(response, Response):
            self._inject_headers(kwargs.get(&lt;span class=&amp;quot;str&amp;quot;&gt;&amp;quot;response&amp;quot;&lt;/span&gt;), request.state.view_rate_limit)
        &lt;span class=&amp;quot;kw&amp;quot;&gt;else&lt;/span&gt;:
            self._inject_headers(response, request.state.view_rate_limit)
    &lt;span class=&amp;quot;kw&amp;quot;&gt;return&lt;/span&gt; response&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;

&lt;div class=&amp;quot;snippet&amp;quot;&gt;
&lt;div class=&amp;quot;snippet-title&amp;quot;&gt;4. Middleware entry point &lt;span class=&amp;quot;file-tag&amp;quot;&gt;middleware.py&lt;/span&gt;&lt;/div&gt;
&lt;p class=&amp;quot;annot&amp;quot;&gt;The middleware path is what handles &lt;em&gt;application limits&lt;/em&gt; and &lt;em&gt;default limits&lt;/em&gt; for routes that don't have their own decorator. &lt;code class=&amp;quot;inline&amp;quot;&gt;_should_exempt&lt;/code&gt; deliberately skips routes that already have a static decorator-defined limit -- &amp;quot;the decorator handles it.&amp;quot; After the handler returns, the middleware injects headers from &lt;code class=&amp;quot;inline&amp;quot;&gt;request.state.view_rate_limit&lt;/code&gt; (set by &lt;code class=&amp;quot;inline&amp;quot;&gt;__evaluate_limits&lt;/code&gt;).&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span class=&amp;quot;kw&amp;quot;&gt;class&lt;/span&gt; SlowAPIMiddleware(BaseHTTPMiddleware):
    &lt;span class=&amp;quot;kw&amp;quot;&gt;async def&lt;/span&gt; dispatch(self, request, call_next):
        app, limiter = request.app, request.app.state.limiter
        &lt;span class=&amp;quot;kw&amp;quot;&gt;if not&lt;/span&gt; limiter.enabled:
            &lt;span class=&amp;quot;kw&amp;quot;&gt;return await&lt;/span&gt; call_next(request)

        handler = _find_route_handler(app.routes, request.scope)
        &lt;span class=&amp;quot;kw&amp;quot;&gt;if&lt;/span&gt; _should_exempt(limiter, handler):                &lt;span class=&amp;quot;com&amp;quot;&gt;# handler missing,&lt;/span&gt;
            &lt;span class=&amp;quot;kw&amp;quot;&gt;return await&lt;/span&gt; call_next(request)               &lt;span class=&amp;quot;com&amp;quot;&gt;# in _exempt_routes,&lt;/span&gt;
                                                            &lt;span class=&amp;quot;com&amp;quot;&gt;# OR in _route_limits (decorator owns it)&lt;/span&gt;
        error_response, should_inject_headers = sync_check_limits(limiter, request, handler, app)
        &lt;span class=&amp;quot;kw&amp;quot;&gt;if&lt;/span&gt; error_response &lt;span class=&amp;quot;kw&amp;quot;&gt;is not&lt;/span&gt; &lt;span class=&amp;quot;num&amp;quot;&gt;None&lt;/span&gt;:
            &lt;span class=&amp;quot;kw&amp;quot;&gt;return&lt;/span&gt; error_response                          &lt;span class=&amp;quot;com&amp;quot;&gt;# 429 path&lt;/span&gt;

        response = &lt;span class=&amp;quot;kw&amp;quot;&gt;await&lt;/span&gt; call_next(request)
        &lt;span class=&amp;quot;kw&amp;quot;&gt;if&lt;/span&gt; should_inject_headers:
            response = limiter._inject_headers(response, request.state.view_rate_limit)
        &lt;span class=&amp;quot;kw&amp;quot;&gt;return&lt;/span&gt; response

&lt;span class=&amp;quot;kw&amp;quot;&gt;def&lt;/span&gt; _should_exempt(limiter, handler):
    &lt;span class=&amp;quot;kw&amp;quot;&gt;if&lt;/span&gt; handler &lt;span class=&amp;quot;kw&amp;quot;&gt;is&lt;/span&gt; &lt;span class=&amp;quot;num&amp;quot;&gt;None&lt;/span&gt;: &lt;span class=&amp;quot;kw&amp;quot;&gt;return&lt;/span&gt; &lt;span class=&amp;quot;num&amp;quot;&gt;True&lt;/span&gt;
    name = _get_route_name(handler)
    &lt;span class=&amp;quot;kw&amp;quot;&gt;if&lt;/span&gt; name &lt;span class=&amp;quot;kw&amp;quot;&gt;in&lt;/span&gt; limiter._exempt_routes: &lt;span class=&amp;quot;kw&amp;quot;&gt;return&lt;/span&gt; &lt;span class=&amp;quot;num&amp;quot;&gt;True&lt;/span&gt;
    &lt;span class=&amp;quot;hi&amp;quot;&gt;&lt;span class=&amp;quot;kw&amp;quot;&gt;if&lt;/span&gt; name &lt;span class=&amp;quot;kw&amp;quot;&gt;in&lt;/span&gt; limiter._route_limits: &lt;span class=&amp;quot;kw&amp;quot;&gt;return&lt;/span&gt; &lt;span class=&amp;quot;num&amp;quot;&gt;True&lt;/span&gt;     &lt;span class=&amp;quot;com&amp;quot;&gt;# static decorator limits only -- NOT dynamic ones&lt;/span&gt;&lt;/span&gt;
    &lt;span class=&amp;quot;kw&amp;quot;&gt;return&lt;/span&gt; &lt;span class=&amp;quot;num&amp;quot;&gt;False&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;

&lt;h2&gt;Where the three buckets of limits come from&lt;/h2&gt;
&lt;table&gt;
  &lt;thead&gt;&lt;tr&gt;&lt;th&gt;Bucket&lt;/th&gt;&lt;th&gt;Set by&lt;/th&gt;&lt;th&gt;Applied when&lt;/th&gt;&lt;th&gt;Stored in&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;&lt;td&gt;&lt;strong&gt;application_limits&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;&lt;code class=&amp;quot;inline&amp;quot;&gt;Limiter(application_limits=[...])&lt;/code&gt; or &lt;code class=&amp;quot;inline&amp;quot;&gt;RATELIMIT_APPLICATION&lt;/code&gt;&lt;/td&gt;&lt;td&gt;Middleware path only (&lt;code class=&amp;quot;inline&amp;quot;&gt;in_middleware=True&lt;/code&gt;). One shared bucket across the whole app, scope=&lt;code class=&amp;quot;inline&amp;quot;&gt;&amp;quot;global&amp;quot;&lt;/code&gt;.&lt;/td&gt;&lt;td&gt;&lt;code class=&amp;quot;inline&amp;quot;&gt;_application_limits&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;
    &lt;tr&gt;&lt;td&gt;&lt;strong&gt;route limits&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;&lt;code class=&amp;quot;inline&amp;quot;&gt;@limiter.limit(...)&lt;/code&gt; / &lt;code class=&amp;quot;inline&amp;quot;&gt;@limiter.shared_limit(...)&lt;/code&gt;&lt;/td&gt;&lt;td&gt;Decorator path only (&lt;code class=&amp;quot;inline&amp;quot;&gt;in_middleware=False&lt;/code&gt;). Static strings live in &lt;code class=&amp;quot;inline&amp;quot;&gt;_route_limits&lt;/code&gt;; callables live in &lt;code class=&amp;quot;inline&amp;quot;&gt;_dynamic_route_limits&lt;/code&gt;.&lt;/td&gt;&lt;td&gt;both above&lt;/td&gt;&lt;/tr&gt;
    &lt;tr&gt;&lt;td&gt;&lt;strong&gt;default_limits&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;&lt;code class=&amp;quot;inline&amp;quot;&gt;Limiter(default_limits=[...])&lt;/code&gt; or &lt;code class=&amp;quot;inline&amp;quot;&gt;RATELIMIT_DEFAULT&lt;/code&gt;&lt;/td&gt;&lt;td&gt;Either path -- but only if the route has no decorator-applied limits, OR every route limit was declared with &lt;code class=&amp;quot;inline&amp;quot;&gt;override_defaults=False&lt;/code&gt;.&lt;/td&gt;&lt;td&gt;&lt;code class=&amp;quot;inline&amp;quot;&gt;_default_limits&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;h2&gt;Gotchas&lt;/h2&gt;

&lt;div class=&amp;quot;gotcha&amp;quot;&gt;
&lt;div class=&amp;quot;label&amp;quot;&gt;Gotcha 1 -- The flag that prevents double-checking is set by only one side&lt;/div&gt;
&lt;p&gt;The decorator wrapper sets &lt;code class=&amp;quot;inline&amp;quot;&gt;request.state._rate_limiting_complete = True&lt;/code&gt; after running its check. The middleware &lt;em&gt;reads&lt;/em&gt; that flag (in &lt;code class=&amp;quot;inline&amp;quot;&gt;_check_limits&lt;/code&gt;) but never sets it. In practice this is fine because &lt;code class=&amp;quot;inline&amp;quot;&gt;_should_exempt&lt;/code&gt; tells the middleware to skip routes whose name is in &lt;code class=&amp;quot;inline&amp;quot;&gt;_route_limits&lt;/code&gt; -- but that check uses &lt;strong&gt;only static decorator limits&lt;/strong&gt;. If your route has a &lt;em&gt;dynamic&lt;/em&gt; limit (callable passed to &lt;code class=&amp;quot;inline&amp;quot;&gt;@limiter.limit&lt;/code&gt;), the middleware will &lt;em&gt;not&lt;/em&gt; exempt it, and you'll evaluate application/default limits in the middleware &lt;em&gt;and&lt;/em&gt; the dynamic limit in the decorator on the same request.&lt;/p&gt;
&lt;/div&gt;

&lt;div class=&amp;quot;gotcha&amp;quot;&gt;
&lt;div class=&amp;quot;label&amp;quot;&gt;Gotcha 2 -- &lt;code class=&amp;quot;inline&amp;quot;&gt;override_defaults&lt;/code&gt; behaves the opposite of how it reads&lt;/div&gt;
&lt;p&gt;It defaults to &lt;code class=&amp;quot;inline&amp;quot;&gt;True&lt;/code&gt;, meaning &amp;quot;if I have a route limit, skip the defaults.&amp;quot; The merge logic is:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;combined_defaults = all(&lt;span class=&amp;quot;kw&amp;quot;&gt;not&lt;/span&gt; l.override_defaults &lt;span class=&amp;quot;kw&amp;quot;&gt;for&lt;/span&gt; l &lt;span class=&amp;quot;kw&amp;quot;&gt;in&lt;/span&gt; route_limits)
&lt;span class=&amp;quot;kw&amp;quot;&gt;if not&lt;/span&gt; route_limits &lt;span class=&amp;quot;kw&amp;quot;&gt;or&lt;/span&gt; combined_defaults:
    all_limits += default_limits&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;So defaults are added only when &lt;em&gt;every&lt;/em&gt; route limit explicitly opts in by setting &lt;code class=&amp;quot;inline&amp;quot;&gt;override_defaults=False&lt;/code&gt;. A single &lt;code class=&amp;quot;inline&amp;quot;&gt;override_defaults=True&lt;/code&gt; limit on the route silences &lt;em&gt;all&lt;/em&gt; defaults, even other route limits' opt-ins.&lt;/p&gt;
&lt;/div&gt;

&lt;div class=&amp;quot;gotcha&amp;quot;&gt;
&lt;div class=&amp;quot;label&amp;quot;&gt;Gotcha 3 -- Headers report the &lt;em&gt;smallest&lt;/em&gt; limit, not the one you might expect&lt;/div&gt;
&lt;p&gt;If a route is decorated with &lt;code class=&amp;quot;inline&amp;quot;&gt;&amp;quot;5/minute;100/hour&amp;quot;&lt;/code&gt;, &lt;code class=&amp;quot;inline&amp;quot;&gt;__evaluate_limits&lt;/code&gt; tracks &lt;code class=&amp;quot;inline&amp;quot;&gt;limit_for_header&lt;/code&gt; as the limit with the smallest &lt;code class=&amp;quot;inline&amp;quot;&gt;amount&lt;/code&gt; seen so far. &lt;code class=&amp;quot;inline&amp;quot;&gt;X-RateLimit-Limit&lt;/code&gt; / &lt;code class=&amp;quot;inline&amp;quot;&gt;Remaining&lt;/code&gt; / &lt;code class=&amp;quot;inline&amp;quot;&gt;Reset&lt;/code&gt; always reflect &lt;em&gt;that&lt;/em&gt; limit. Clients reading those headers won't see the hourly bucket at all unless it becomes the binding constraint at the moment the response is built. Also: when a limit fails, the &lt;em&gt;failing&lt;/em&gt; limit overwrites &lt;code class=&amp;quot;inline&amp;quot;&gt;limit_for_header&lt;/code&gt; regardless of size, so the headers on a 429 response always describe the limit that just rejected you.&lt;/p&gt;
&lt;/div&gt;

&lt;div class=&amp;quot;gotcha&amp;quot;&gt;
&lt;div class=&amp;quot;label&amp;quot;&gt;Gotcha 4 -- &lt;code class=&amp;quot;inline&amp;quot;&gt;hit()&lt;/code&gt; short-circuits on the first failure&lt;/div&gt;
&lt;p&gt;The loop in &lt;code class=&amp;quot;inline&amp;quot;&gt;__evaluate_limits&lt;/code&gt; calls &lt;code class=&amp;quot;inline&amp;quot;&gt;self.limiter.hit(...)&lt;/code&gt; for each limit in order, and &lt;code class=&amp;quot;inline&amp;quot;&gt;break&lt;/code&gt;s on the first &lt;code class=&amp;quot;inline&amp;quot;&gt;False&lt;/code&gt;. &lt;code class=&amp;quot;inline&amp;quot;&gt;hit&lt;/code&gt; increments the counter as a side effect, so a 429 response means earlier limits in the list &lt;em&gt;were&lt;/em&gt; incremented but later ones &lt;em&gt;were not&lt;/em&gt;. If you depend on multiple counters being kept in lock-step (e.g., to derive analytics from &lt;code class=&amp;quot;inline&amp;quot;&gt;Remaining&lt;/code&gt;), they will drift the moment any single bucket overflows. Order matters: limits are iterated in the order they were registered.&lt;/p&gt;
&lt;/div&gt;

&lt;div class=&amp;quot;gotcha&amp;quot;&gt;
&lt;div class=&amp;quot;label&amp;quot;&gt;Gotcha 5 -- &lt;code class=&amp;quot;inline&amp;quot;&gt;SlowAPIMiddleware&lt;/code&gt; downgrades async exception handlers&lt;/div&gt;
&lt;p&gt;The classic middleware uses &lt;code class=&amp;quot;inline&amp;quot;&gt;sync_check_limits&lt;/code&gt;, which contains:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&lt;span class=&amp;quot;kw&amp;quot;&gt;if&lt;/span&gt; inspect.iscoroutinefunction(exception_handler):
    exception_handler = _rate_limit_exceeded_handler&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If you registered a custom &lt;em&gt;async&lt;/em&gt; handler for &lt;code class=&amp;quot;inline&amp;quot;&gt;RateLimitExceeded&lt;/code&gt; on the app, the middleware silently swaps it out for the default sync handler. Custom 429 responses, audit hooks, etc. won't run for requests that hit through this path. Use &lt;code class=&amp;quot;inline&amp;quot;&gt;SlowAPIASGIMiddleware&lt;/code&gt; (which uses &lt;code class=&amp;quot;inline&amp;quot;&gt;async_check_limits&lt;/code&gt;) if you need the async handler to fire.&lt;/p&gt;
&lt;/div&gt;

&lt;div class=&amp;quot;gotcha&amp;quot;&gt;
&lt;div class=&amp;quot;label&amp;quot;&gt;Gotcha 6 -- The &amp;quot;fallback to in-memory&amp;quot; is best-effort and self-flips on any header error&lt;/div&gt;
&lt;p&gt;&lt;code class=&amp;quot;inline&amp;quot;&gt;_inject_headers&lt;/code&gt; wraps the &lt;code class=&amp;quot;inline&amp;quot;&gt;window_stats&lt;/code&gt; call in a bare &lt;code class=&amp;quot;inline&amp;quot;&gt;except:&lt;/code&gt;. Any exception there -- not just storage timeouts -- causes &lt;code class=&amp;quot;inline&amp;quot;&gt;self._storage_dead = True&lt;/code&gt; and a recursive call against the in-memory fallback. Until the next storage probe (&lt;code class=&amp;quot;inline&amp;quot;&gt;__should_check_backend&lt;/code&gt;, exponentially backed off), every request goes to the local memory store. In a multi-worker deployment this means each worker silently gets its own private counter, and your effective rate limit is multiplied by worker count. The probe will eventually re-test the real backend and recover -- but the window of inconsistency depends on &lt;code class=&amp;quot;inline&amp;quot;&gt;MAX_BACKEND_CHECKS&lt;/code&gt; (5) and &lt;code class=&amp;quot;inline&amp;quot;&gt;2**check_count&lt;/code&gt; seconds.&lt;/p&gt;
&lt;/div&gt;

&lt;div class=&amp;quot;gotcha&amp;quot;&gt;
&lt;div class=&amp;quot;label&amp;quot;&gt;Gotcha 7 -- Two latent bugs in the codebase&lt;/div&gt;
&lt;ul style=&amp;quot;margin: 4px 0 0;&amp;quot;&gt;
  &lt;li&gt;&lt;code class=&amp;quot;inline&amp;quot;&gt;slowapi_startup&lt;/code&gt; references a bare name &lt;code class=&amp;quot;inline&amp;quot;&gt;app&lt;/code&gt; with no enclosing scope (see the &lt;code class=&amp;quot;inline&amp;quot;&gt;# type: ignore&lt;/code&gt;s). Calling it raises &lt;code class=&amp;quot;inline&amp;quot;&gt;NameError&lt;/code&gt;. The intended wiring is &lt;code class=&amp;quot;inline&amp;quot;&gt;app.state.limiter = limiter&lt;/code&gt; done by the user.&lt;/li&gt;
  &lt;li&gt;&lt;code class=&amp;quot;inline&amp;quot;&gt;Limit.scope&lt;/code&gt; calls &lt;code class=&amp;quot;inline&amp;quot;&gt;self.__scope(request.endpoint)&lt;/code&gt; when the scope is callable -- but &lt;code class=&amp;quot;inline&amp;quot;&gt;request&lt;/code&gt; is undefined in that property and &lt;code class=&amp;quot;inline&amp;quot;&gt;Request&lt;/code&gt; objects don't have &lt;code class=&amp;quot;inline&amp;quot;&gt;.endpoint&lt;/code&gt; in Starlette anyway. The in-source &lt;code class=&amp;quot;inline&amp;quot;&gt;FIXME&lt;/code&gt; confirms this path is broken; callable scopes will raise.&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;

&lt;div class=&amp;quot;gotcha&amp;quot;&gt;
&lt;div class=&amp;quot;label&amp;quot;&gt;Gotcha 8 -- The &lt;code class=&amp;quot;inline&amp;quot;&gt;key_style&lt;/code&gt; choice silently changes which routes share buckets&lt;/div&gt;
&lt;p&gt;With &lt;code class=&amp;quot;inline&amp;quot;&gt;key_style=&amp;quot;url&amp;quot;&lt;/code&gt; (the default) the bucket scope is the literal request path, so &lt;code class=&amp;quot;inline&amp;quot;&gt;/users/1&lt;/code&gt; and &lt;code class=&amp;quot;inline&amp;quot;&gt;/users/2&lt;/code&gt; have &lt;em&gt;separate&lt;/em&gt; rate-limit buckets even though they hit the same handler. With &lt;code class=&amp;quot;inline&amp;quot;&gt;key_style=&amp;quot;endpoint&amp;quot;&lt;/code&gt;, both share a bucket. If you have parameterized routes and you set up a per-endpoint quota expecting &amp;quot;10 calls per minute against this handler,&amp;quot; the default URL keying gives you &amp;quot;10 per minute per distinct path you happen to call&amp;quot; instead. Pick deliberately.&lt;/p&gt;
&lt;/div&gt;

&lt;/div&gt;
&lt;/body&gt;
&lt;/html&gt;
" height="700" width="100%" style="border:1px solid #d0d7de;border-radius:6px;margin:1.5em 0"&gt;&lt;/iframe&gt;

&lt;p&gt;&lt;em&gt;The HTML rate-limiter explainer. Mermaid flowchart via CDN, callout boxes for the 8 gotchas, syntax-highlighted code via CSS classes. Looks the part.&lt;/em&gt;&lt;/p&gt;
&lt;iframe srcdoc="&lt;!doctype html&gt;
&lt;html lang=&amp;quot;en&amp;quot;&gt;
&lt;head&gt;
&lt;meta charset=&amp;quot;utf-8&amp;quot;&gt;
&lt;title&gt;Rendered markdown&lt;/title&gt;
&lt;style&gt;
  body { font: 15px/1.6 -apple-system, BlinkMacSystemFont, &amp;quot;Segoe UI&amp;quot;, Roboto, sans-serif;
         color: #1f2328; background: #ffffff; max-width: 760px; margin: 0 auto; padding: 24px; }
  h1 { font-size: 24px; margin-top: 0; border-bottom: 1px solid #d0d7de; padding-bottom: 8px; }
  h2 { font-size: 20px; margin-top: 28px; border-bottom: 1px solid #d0d7de; padding-bottom: 6px; }
  h3 { font-size: 16px; margin-top: 22px; }
  code { background: #f6f8fa; padding: 1px 5px; border-radius: 4px; font-size: 13px;
         font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; }
  pre { background: #f6f8fa; padding: 12px 14px; border-radius: 6px; overflow-x: auto;
        font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-size: 13px;
        line-height: 1.45; }
  pre code { background: transparent; padding: 0; font-size: inherit; }
  blockquote { border-left: 3px solid #d0d7de; padding-left: 12px; color: #59636e; margin: 12px 0; }
  table { border-collapse: collapse; margin: 12px 0; font-size: 14px; }
  th, td { border: 1px solid #d0d7de; padding: 6px 12px; text-align: left; vertical-align: top; }
  th { background: #f6f8fa; }
  hr { border: none; border-top: 1px solid #d0d7de; margin: 28px 0; }
  a { color: #0969da; }
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;h1&gt;slowapi rate limiter, explained&lt;/h1&gt;
&lt;p&gt;slowapi rate-limits requests to a Starlette/FastAPI app. It is a thin coordinator on top of the &lt;code&gt;limits&lt;/code&gt; library: &lt;code&gt;limits&lt;/code&gt; owns the storage (memory/Redis/...) and the strategy (fixed-window/...). slowapi owns &lt;em&gt;which&lt;/em&gt; limits apply to &lt;em&gt;this&lt;/em&gt; request, &lt;em&gt;what key&lt;/em&gt; identifies the caller, and &lt;em&gt;what headers&lt;/em&gt; go on the response.&lt;/p&gt;
&lt;p&gt;There are two ways limits get registered, and two ways they get checked. Keeping those four straight is most of understanding the code.&lt;/p&gt;
&lt;h2&gt;How limits get registered&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Source&lt;/th&gt;
&lt;th&gt;Registered into&lt;/th&gt;
&lt;th&gt;Applied by&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Limiter(default_limits=[...])&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;_default_limits&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;both paths, when route has no overriding limit&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Limiter(application_limits=[...])&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;_application_limits&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;middleware path only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;@limiter.limit(&amp;quot;5/minute&amp;quot;)&lt;/code&gt; (static)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;_route_limits[name]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;decorator wrapper&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;@limiter.limit(callable)&lt;/code&gt; (dynamic)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;_dynamic_route_limits[name]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;decorator wrapper&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;@limiter.exempt&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;_exempt_routes&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;both paths skip&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;A &amp;quot;limit&amp;quot; is parsed by &lt;code&gt;limits.parse_many(&amp;quot;5/minute;100/hour&amp;quot;)&lt;/code&gt; into one or more &lt;code&gt;RateLimitItem&lt;/code&gt; objects, each wrapped in a &lt;code&gt;Limit&lt;/code&gt; (wrappers.py). &lt;code&gt;LimitGroup&lt;/code&gt; is iterable and yields one &lt;code&gt;Limit&lt;/code&gt; per parsed item.&lt;/p&gt;
&lt;h2&gt;How a request is checked&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&amp;quot;language-mermaid&amp;quot;&gt;flowchart TD
    A[Request arrives] --&amp;amp;gt; B{Middleware&amp;amp;lt;br/&amp;amp;gt;installed?}
    B --&amp;amp;gt;|Yes| C[SlowAPIMiddleware.dispatch]
    B --&amp;amp;gt;|No| K

    C --&amp;amp;gt; D{_should_exempt?&amp;amp;lt;br/&amp;amp;gt;handler in&amp;amp;lt;br/&amp;amp;gt;_exempt_routes or&amp;amp;lt;br/&amp;amp;gt;_route_limits}
    D --&amp;amp;gt;|Yes, skip&amp;amp;lt;br/&amp;amp;gt;middleware checks| K[call route handler]
    D --&amp;amp;gt;|No| E[_check_request_limit&amp;amp;lt;br/&amp;amp;gt;in_middleware=True]
    E --&amp;amp;gt; F[Builds: application_limits&amp;amp;lt;br/&amp;amp;gt;+ defaults if applicable]
    F --&amp;amp;gt; G[__evaluate_limits]
    G --&amp;amp;gt;|hit OK| H[continue to handler]
    G --&amp;amp;gt;|hit fails| X[raise RateLimitExceeded&amp;amp;lt;br/&amp;amp;gt;→ 429 JSON response]
    H --&amp;amp;gt; K

    K --&amp;amp;gt; L{Route has&amp;amp;lt;br/&amp;amp;gt;@limiter.limit?}
    L --&amp;amp;gt;|No| Z[response]
    L --&amp;amp;gt;|Yes| M[decorator's sync/async_wrapper]
    M --&amp;amp;gt; N{_rate_limiting_complete&amp;amp;lt;br/&amp;amp;gt;already True?}
    N --&amp;amp;gt;|Yes, already checked| O[run handler]
    N --&amp;amp;gt;|No| P[_check_request_limit&amp;amp;lt;br/&amp;amp;gt;in_middleware=False]
    P --&amp;amp;gt; Q[Builds: route_limits&amp;amp;lt;br/&amp;amp;gt;+ dynamic_limits&amp;amp;lt;br/&amp;amp;gt;+ defaults if applicable]
    Q --&amp;amp;gt; R[__evaluate_limits]
    R --&amp;amp;gt;|hit OK| S[set _rate_limiting_complete=True]
    R --&amp;amp;gt;|hit fails| X
    S --&amp;amp;gt; O
    O --&amp;amp;gt; T[_inject_headers&amp;amp;lt;br/&amp;amp;gt;X-RateLimit-* + Retry-After]
    T --&amp;amp;gt; Z

    style X fill:#fdd
    style G fill:#ffd
    style R fill:#ffd
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The decorator and middleware paths are &lt;strong&gt;independent&lt;/strong&gt;. The middleware checks if a route has a registered &lt;code&gt;@limiter.limit&lt;/code&gt; — if so, it skips its own check and lets the decorator handle it (&lt;code&gt;_should_exempt&lt;/code&gt; returns True for routes in &lt;code&gt;_route_limits&lt;/code&gt;). Application-wide limits therefore only fire on routes that do &lt;em&gt;not&lt;/em&gt; carry a decorator, unless you also wrap them with the decorator separately. This is the most surprising design choice in the library.&lt;/p&gt;
&lt;h2&gt;Key code, annotated&lt;/h2&gt;
&lt;h3&gt;1. The decorator wrapper — where rate-limiting actually attaches to a route&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;extension.py&lt;/code&gt;, inside &lt;code&gt;__limit_decorator&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&amp;quot;language-python&amp;quot;&gt;@functools.wraps(func)
async def async_wrapper(*args, **kwargs):
    if self.enabled:
        request = kwargs.get(&amp;amp;quot;request&amp;amp;quot;, args[idx] if args else None)
        # Idempotency guard: if the middleware already checked, don't double-hit storage.
        if self._auto_check and not getattr(
            request.state, &amp;amp;quot;_rate_limiting_complete&amp;amp;quot;, False
        ):
            self._check_request_limit(request, func, False)  # in_middleware=False
            request.state._rate_limiting_complete = True
    response = await func(*args, **kwargs)
    if self.enabled:
        # Even if the check was skipped (already complete), headers still get injected
        # using request.state.view_rate_limit set during evaluation.
        self._inject_headers(response, request.state.view_rate_limit)
    return response
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;_rate_limiting_complete&lt;/code&gt; flag is only ever set by the decorator wrapper — never by the middleware. So middleware → decorator double-checking is prevented by the middleware's &lt;code&gt;_should_exempt&lt;/code&gt; returning early for decorated routes, &lt;em&gt;not&lt;/em&gt; by the flag. The flag exists for the rarer case of nested decorators / repeated dispatch.&lt;/p&gt;
&lt;h3&gt;2. Building the limit list — when do defaults apply?&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;extension.py&lt;/code&gt;, inside &lt;code&gt;_check_request_limit&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&amp;quot;language-python&amp;quot;&gt;route_limits: List[Limit] = limits + dynamic_limits
all_limits = (
    list(itertools.chain(*self._application_limits))
    if in_middleware
    else []
)
all_limits += route_limits
combined_defaults = all(
    not limit.override_defaults for limit in route_limits
)
if (
    not route_limits
    and not (in_middleware and endpoint_func_name in self.__marked_for_limiting)
    or combined_defaults
):
    all_limits += list(itertools.chain(*self._default_limits))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Read the boolean carefully — Python parses it as &lt;code&gt;(A and not B) or C&lt;/code&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;A&lt;/strong&gt;: this route has no route-level limits, AND&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;B&lt;/strong&gt; (negated): we're not in the middleware-skipping-a-decorated-route case, OR&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;C&lt;/strong&gt;: every route-level limit was registered with &lt;code&gt;override_defaults=False&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;So defaults stack with route limits &lt;em&gt;only&lt;/em&gt; when you opt out of overriding via &lt;code&gt;override_defaults=False&lt;/code&gt; (note: the decorator default is &lt;code&gt;True&lt;/code&gt;, i.e. route limits replace defaults by default). This is the second-most-surprising design choice.&lt;/p&gt;
&lt;h3&gt;3. The hit loop — where requests are actually counted&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;extension.py&lt;/code&gt;, &lt;code&gt;__evaluate_limits&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&amp;quot;language-python&amp;quot;&gt;def __evaluate_limits(self, request, endpoint, limits):
    failed_limit = None
    limit_for_header = None
    for lim in limits:
        limit_scope = lim.scope or endpoint
        if lim.is_exempt(request): continue
        if lim.methods is not None and request.method.lower() not in lim.methods: continue
        if lim.per_method:
            limit_scope += &amp;amp;quot;:%s&amp;amp;quot; % request.method

        # key_func may or may not accept `request` — sniffed via inspect.signature
        if &amp;amp;quot;request&amp;amp;quot; in inspect.signature(lim.key_func).parameters.keys():
            limit_key = lim.key_func(request)
        else:
            limit_key = lim.key_func()

        args = [limit_key, limit_scope]
        if all(args):  # silently skip if either is empty/falsy
            if self._key_prefix:
                args = [self._key_prefix] + args
            # Track the SMALLEST limit (most restrictive) for headers.
            if not limit_for_header or lim.limit &amp;amp;lt; limit_for_header[0]:
                limit_for_header = (lim.limit, args)

            cost = lim.cost(request) if callable(lim.cost) else lim.cost
            if not self.limiter.hit(lim.limit, *args, cost=cost):
                # First failure wins — break, do NOT hit subsequent limits.
                failed_limit = lim
                limit_for_header = (lim.limit, args)
                break
        else:
            self.logger.error(&amp;amp;quot;Skipping limit: %s. Empty value found in parameters.&amp;amp;quot;, lim.limit)
            continue
    request.state.view_rate_limit = limit_for_header
    if failed_limit:
        raise RateLimitExceeded(failed_limit)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Two points worth burning in:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;The header-target is the &lt;strong&gt;smallest&lt;/strong&gt; limit, by &lt;code&gt;RateLimitItem.__lt__&lt;/code&gt; (smaller window/amount = more restrictive). This is what the client sees in &lt;code&gt;X-RateLimit-*&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;On failure, the loop &lt;strong&gt;breaks&lt;/strong&gt; — slowapi reports the first limit that failed and never increments counters for limits later in the list. This matters when a route has multiple stacked limits (&amp;quot;5/sec;100/min;1000/hr&amp;quot;); only the smallest-window-that-fails counts the request.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;4. Header injection — the contract with the client&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;extension.py&lt;/code&gt;, &lt;code&gt;_inject_headers&lt;/code&gt; (asgi variant is identical, just operating on &lt;code&gt;MutableHeaders&lt;/code&gt;):&lt;/p&gt;
&lt;pre&gt;&lt;code class=&amp;quot;language-python&amp;quot;&gt;window_stats = self.limiter.get_window_stats(current_limit[0], *current_limit[1])
reset_in = 1 + window_stats[0]   # window_stats[0] is the unix ts when the window resets
response.headers.append(self._header_mapping[HEADERS.LIMIT], str(current_limit[0].amount))
response.headers.append(self._header_mapping[HEADERS.REMAINING], str(window_stats[1]))
response.headers.append(self._header_mapping[HEADERS.RESET], str(reset_in))

existing_retry_after_header = response.headers.get(&amp;amp;quot;Retry-After&amp;amp;quot;)
if existing_retry_after_header is not None:
    reset_in = max(self._determine_retry_time(existing_retry_after_header), reset_in)
response.headers[self._header_mapping[HEADERS.RETRY_AFTER]] = (
    formatdate(reset_in) if self._retry_after == &amp;amp;quot;http-date&amp;amp;quot;
    else str(int(reset_in - time.time()))
)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;Retry-After&lt;/code&gt; defaults to delta-seconds (&lt;code&gt;reset_in - now&lt;/code&gt;); set &lt;code&gt;retry_after=&amp;quot;http-date&amp;quot;&lt;/code&gt; to emit RFC2616 dates instead. Headers are only injected if &lt;code&gt;headers_enabled=True&lt;/code&gt; &lt;em&gt;and&lt;/em&gt; a limit was evaluated for this request — otherwise the response is silently un-annotated.&lt;/p&gt;
&lt;h2&gt;Gotchas&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Application limits only fire on un-decorated routes.&lt;/strong&gt; &lt;code&gt;SlowAPIMiddleware&lt;/code&gt; short-circuits via &lt;code&gt;_should_exempt&lt;/code&gt; for any route present in &lt;code&gt;_route_limits&lt;/code&gt;. If you set &lt;code&gt;application_limits=[&amp;quot;1000/hour&amp;quot;]&lt;/code&gt; and decorate every endpoint, the application limit never runs. Either keep the application limit's routes un-decorated, or stack the limit explicitly.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;override_defaults=True&lt;/code&gt; is the decorator default.&lt;/strong&gt; Adding &lt;code&gt;@limiter.limit(&amp;quot;5/minute&amp;quot;)&lt;/code&gt; &lt;em&gt;replaces&lt;/em&gt; your &lt;code&gt;default_limits&lt;/code&gt;, it does not stack. Pass &lt;code&gt;override_defaults=False&lt;/code&gt; to stack. The variable &lt;code&gt;combined_defaults&lt;/code&gt; in &lt;code&gt;_check_request_limit&lt;/code&gt; reads as if it computes a union — it does not; it just decides whether to &lt;em&gt;append&lt;/em&gt; defaults.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Empty &lt;code&gt;key_func()&lt;/code&gt; or &lt;code&gt;scope&lt;/code&gt; silently skips a limit.&lt;/strong&gt; The &lt;code&gt;if all(args):&lt;/code&gt; guard means a buggy key function that returns &lt;code&gt;&amp;quot;&amp;quot;&lt;/code&gt; or &lt;code&gt;None&lt;/code&gt; (e.g., user not authenticated, header missing) results in a request that is &lt;em&gt;not&lt;/em&gt; rate-limited at all, with only an error log. This is a common bypass vector — always make &lt;code&gt;key_func&lt;/code&gt; return a sentinel like &lt;code&gt;&amp;quot;anonymous&amp;quot;&lt;/code&gt; rather than an empty string.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Multiple stacked limits: only the first-failing one counts the hit.&lt;/strong&gt; A request that would breach &lt;code&gt;5/sec&lt;/code&gt; and &lt;code&gt;100/min&lt;/code&gt; simultaneously increments the per-second counter and stops; the per-minute counter is untouched for that request. So aggregate &amp;quot;minute&amp;quot; usage will under-report whenever the second-window limit is being hit.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;The headers report the &lt;em&gt;smallest&lt;/em&gt; limit, not the first.&lt;/strong&gt; If you stack &lt;code&gt;1000/hr;5/sec&lt;/code&gt;, &lt;code&gt;X-RateLimit-Limit&lt;/code&gt; will be &lt;code&gt;5&lt;/code&gt;. Clients implementing backoff against a single header pair will see the tightest limit, which is usually what you want — but be aware your &amp;quot;1000/hr&amp;quot; budget is invisible to them.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;Limit.scope&lt;/code&gt; is broken for callable scopes&lt;/strong&gt; (&lt;code&gt;wrappers.py&lt;/code&gt;). The property body references an undefined &lt;code&gt;request.endpoint&lt;/code&gt; (a Flask-ism leftover, with a &lt;code&gt;FIXME&lt;/code&gt; in the source). Passing a callable as &lt;code&gt;scope=&lt;/code&gt; to &lt;code&gt;shared_limit&lt;/code&gt; will raise &lt;code&gt;NameError&lt;/code&gt; at evaluation time. Use string scopes only.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;scope=&lt;/code&gt; on &lt;code&gt;@limiter.limit()&lt;/code&gt; is a no-op.&lt;/strong&gt; &lt;code&gt;__limit_decorator&lt;/code&gt; does &lt;code&gt;_scope = scope if shared else None&lt;/code&gt;, and &lt;code&gt;limit()&lt;/code&gt; calls it without &lt;code&gt;shared=True&lt;/code&gt;. Only &lt;code&gt;shared_limit()&lt;/code&gt; actually uses scope.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;In-memory fallback hides backend outages.&lt;/strong&gt; When &lt;code&gt;_storage_dead = True&lt;/code&gt; and &lt;code&gt;_in_memory_fallback_enabled&lt;/code&gt;, slowapi uses local memory until the backend recovers. In a multi-process deployment each worker maintains its own counter while the backend is down, so the effective limit during outages is &lt;code&gt;N_workers × configured_limit&lt;/code&gt;. Health is rechecked with exponential backoff (&lt;code&gt;pow(2, count)&lt;/code&gt;), so recovery can take up to ~32 seconds after the backend returns.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;slowapi_startup&lt;/code&gt; is dead code.&lt;/strong&gt; It references a free variable &lt;code&gt;app&lt;/code&gt; and would &lt;code&gt;NameError&lt;/code&gt; if called. Don't wire it up; set &lt;code&gt;app.state.limiter = limiter&lt;/code&gt; and register the &lt;code&gt;RateLimitExceeded&lt;/code&gt; exception handler manually (or use the middleware, which reads &lt;code&gt;app.state.limiter&lt;/code&gt;).&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;SlowAPIMiddleware&lt;/code&gt; (BaseHTTPMiddleware) only handles sync exception handlers&lt;/strong&gt;; if your custom &lt;code&gt;RateLimitExceeded&lt;/code&gt; handler is async it silently falls back to the default &lt;code&gt;_rate_limit_exceeded_handler&lt;/code&gt;. Use &lt;code&gt;SlowAPIASGIMiddleware&lt;/code&gt; if you need async exception handlers.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;key_func&lt;/code&gt; signature is sniffed, not specified.&lt;/strong&gt; &lt;code&gt;__evaluate_limits&lt;/code&gt; introspects &lt;code&gt;key_func.parameters&lt;/code&gt; and calls &lt;code&gt;key_func(request)&lt;/code&gt; if &lt;code&gt;&amp;quot;request&amp;quot;&lt;/code&gt; is in the signature, else &lt;code&gt;key_func()&lt;/code&gt;. A decorator like &lt;code&gt;@functools.wraps&lt;/code&gt; that hides parameters, or a &lt;code&gt;lambda *a: ...&lt;/code&gt;, will land on the wrong branch.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;request.state.view_rate_limit&lt;/code&gt; is the contract between check and header injection.&lt;/strong&gt; If you wrap the response in custom middleware after slowapi without preserving &lt;code&gt;request.state&lt;/code&gt;, headers won't be injected. The exception handler &lt;code&gt;_rate_limit_exceeded_handler&lt;/code&gt; also reads from it — clearing it breaks 429 headers.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;&lt;/body&gt;&lt;/html&gt;" height="700" width="100%" style="border:1px solid #d0d7de;border-radius:6px;margin:1.5em 0"&gt;&lt;/iframe&gt;

&lt;p&gt;&lt;em&gt;The same explainer, in markdown. Twelve gotchas instead of eight. Mermaid flowchart in a fenced block, fenced code blocks letting the viewer handle syntax highlighting, no CSS chrome. Cheaper. And, to my eye, the better artifact.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;The HTML version's distinctive features are &lt;em&gt;CSS chrome&lt;/em&gt;: a stylesheet for syntax-highlighting tokens (&lt;code&gt;&amp;lt;span class="kw"&amp;gt;&lt;/code&gt;, &lt;code&gt;&amp;lt;span class="str"&amp;gt;&lt;/code&gt;), background panels with &lt;code&gt;border-radius&lt;/code&gt; for code blocks, color-coded callout boxes for gotchas. The markdown version uses fenced code blocks which most modern viewers syntax-highlight automatically.&lt;/p&gt;
&lt;p&gt;I counted the gotchas in both files. HTML had 8. Markdown had 12. The HTML compresses two findings into one "Two latent bugs" entry, so charitably call it 9-10. Even charitably, the markdown surfaces more distinct gotchas the HTML doesn't reach: the empty-&lt;code&gt;key_func&lt;/code&gt; bypass, the &lt;code&gt;inspect.parameters&lt;/code&gt; signature sniffing, the &lt;code&gt;request.state.view_rate_limit&lt;/code&gt; contract between checking and header injection. The HTML caught one the markdown missed (the &lt;code&gt;key_style="url"&lt;/code&gt; vs &lt;code&gt;"endpoint"&lt;/code&gt; distinction), but it isn't a wash. Markdown was the more substantive artifact.&lt;/p&gt;
&lt;p&gt;That surprised me. Going in, I'd assumed the verbose-priming effect (HTML's tag-heavy output register leading Claude into more verbose responses) would push HTML toward more comprehensive answers. It didn't here. It pushed the other way.&lt;/p&gt;
&lt;p&gt;Think of it like two technical writers given the same source code. One hands you a clean PDF with a cover page, syntax-highlighted snippets, and tasteful colored boxes around the warnings. The other gives you a longer markdown file with no styling, but he caught four more gotchas. Which one do you want before you ship to prod?&lt;/p&gt;
&lt;p&gt;The edit probe threw me a small surprise. I asked Claude to add a "Custom storage backends" section to each artifact and looked at the diffs. HTML's diff was 2,471 bytes over 19 lines; markdown's was &lt;em&gt;bigger&lt;/em&gt; at 2,987 bytes over the same 19 lines. Same line count, but markdown's paragraph-style prose has longer lines than HTML's tag-broken structure. The "HTML diffs are noisier" claim turns out not to generalize cleanly. When the content is mostly prose, markdown isn't a saving on diff size.&lt;/p&gt;
&lt;p&gt;Verdict, with the obvious caveat that this is one sample on one substrate: the markdown was the better artifact. More comprehensive (12 gotchas vs effectively 9-10 once you count the HTML's "two latent bugs" as separate items), cheaper, with a real Mermaid diagram in the fenced block. The intuition that "HTML wins because richer information" doesn't hold up in this case. Whether it'd hold up at K=5 generations I genuinely don't know, and I think that's the right answer to give. One sample is one sample. But the direction of the surprise is striking. I expected HTML's verbose-priming effect to push HTML toward more thorough answers; it pushed the other way here.&lt;/p&gt;
&lt;h2 id="the-1m-context-window-handles-it-does-it-though"&gt;"The 1M context window handles it." Does it though?&lt;/h2&gt;
&lt;p&gt;Thariq's FAQ says: &lt;em&gt;"With the 1M context window in Opus 4.7, the increased token usage is not really noticeable in the context window."&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Fair point for one-shot generation. Not so fair for re-ingestion, which is what Claude Code actually does. Feed yesterday's artifact back in as context for today's follow-up turn, over and over. Let me show what that costs.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Cache_creation tokens (artifact-as-input-context) per re-ingestion across the three cases:&lt;/strong&gt;&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Use case&lt;/th&gt;
&lt;th&gt;HTML&lt;/th&gt;
&lt;th&gt;MD&lt;/th&gt;
&lt;th&gt;Delta&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Design exploration&lt;/td&gt;
&lt;td&gt;27,309&lt;/td&gt;
&lt;td&gt;21,657&lt;/td&gt;
&lt;td&gt;+5,652&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PR review&lt;/td&gt;
&lt;td&gt;33,892&lt;/td&gt;
&lt;td&gt;25,532&lt;/td&gt;
&lt;td&gt;+8,360&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Rate limiter expl.&lt;/td&gt;
&lt;td&gt;32,280&lt;/td&gt;
&lt;td&gt;26,379&lt;/td&gt;
&lt;td&gt;+5,901&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Run the math. Project produces N artifacts. Each gets re-fed into the agent K times. The cache evicts between feeds (i.e. calls more than ~5 minutes apart, where you pay the cache_creation cost fresh). The HTML-over-markdown overhead is roughly N × K × 6,000 tokens. At N=50, K=3 that's 900,000 tokens of cache_creation, basically the whole 1M context window, spent on markup you wouldn't have if you'd used markdown. For tight call chains where the cache stays warm, subsequent reads hit &lt;code&gt;cache_read_input_tokens&lt;/code&gt; (much cheaper, \&lt;span class="math"&gt;\(1.50/M vs \\)&lt;/span&gt;15/M for fresh input), and the overhead amortizes to roughly the artifact's first-ingest cost.&lt;/p&gt;
&lt;p&gt;The dollar cost differential is muted by cache pricing for tight chains, so individual calls feel cheap. The &lt;em&gt;context budget&lt;/em&gt; is where the cost most reliably shows, because that's measured in tokens regardless of cache state.&lt;/p&gt;
&lt;p&gt;This matters more if you're not on an enterprise plan with effectively unlimited tokens. Several commenters in Thariq's thread raised exactly that point: the "context budget isn't a concern" framing reads differently from inside Anthropic than it does for a developer paying retail. The math behind that pushback is real.&lt;/p&gt;
&lt;h2 id="does-claude-read-html-better-than-markdown-spoiler-not-really"&gt;Does Claude read HTML better than markdown? (Spoiler: not really.)&lt;/h2&gt;
&lt;p&gt;Several commenters on Thariq's piece pushed back on the agent side of his argument. HTML "wastes the model's cognitive space on tags, nesting, closure, styles" (@AlexMares). It has "low signal-to-noise ratio" and risks hallucination (@AkhilDevelops). Markdown is just the right format for agents to read (@AiAGiAI). And Thariq's whole pitch quietly assumes the opposite: that the agent processes HTML at least as well as it processes markdown. Otherwise his "use HTML for everything, Claude reads it back later" workflow doesn't really hold up, does it?&lt;/p&gt;
&lt;p&gt;So I tested it. Seven specific factual questions about slowapi's rate limiter, each answerable from both the HTML and the markdown version of the same explainer artifact (shared content, different format). I asked Claude (fresh session, Opus 4.7) to answer all seven questions from each format, then scored the answers against ground truth derived from the slowapi source.&lt;/p&gt;
&lt;p&gt;Here's what came back:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;HTML source&lt;/th&gt;
&lt;th&gt;MD source&lt;/th&gt;
&lt;th&gt;Ratio HTML/MD&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Correct answers&lt;/td&gt;
&lt;td&gt;7 / 7&lt;/td&gt;
&lt;td&gt;7 / 7&lt;/td&gt;
&lt;td&gt;equal&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Input cache_creation tokens&lt;/td&gt;
&lt;td&gt;30,213&lt;/td&gt;
&lt;td&gt;24,312&lt;/td&gt;
&lt;td&gt;1.24x&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Output tokens generated&lt;/td&gt;
&lt;td&gt;1,560&lt;/td&gt;
&lt;td&gt;785&lt;/td&gt;
&lt;td&gt;1.99x&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cost per call&lt;/td&gt;
&lt;td&gt;$0.246&lt;/td&gt;
&lt;td&gt;$0.185&lt;/td&gt;
&lt;td&gt;1.33x&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Both formats produced equally accurate answers to all seven questions, confirmed by two independent judges scoring blinded (neither judge knew which set came from which format). Both judges gave both sets 7/7 on factual accuracy.&lt;/p&gt;
&lt;p&gt;On a separate axis (explanatory specificity, the "clarity and learning value" of the answer) both judges scored HTML marginally higher. Judge 1 gave markdown 6.5/7 and HTML 7.0/7. Judge 2 gave markdown 6.0/7 and HTML 6.5/7. The HTML-sourced answers consistently cited more specific code constructs (e.g. &lt;code&gt;all(not l.override_defaults for l in route_limits)&lt;/code&gt;, &lt;code&gt;MAX_BACKEND_CHECKS=5&lt;/code&gt;, &lt;code&gt;inspect.iscoroutinefunction&lt;/code&gt;). The markdown-sourced answers were slightly tighter prose. Both judges independently described the same pattern.&lt;/p&gt;
&lt;p&gt;So the strong commenter worry, that HTML "wastes the model's cognitive space" or "lowers signal-to-noise to the point of hallucination," didn't show up. If anything, the mild &lt;em&gt;opposite&lt;/em&gt; did: HTML-sourced answers came back a hair more specific. Both blinded judges said so. But the price-quality math is bad either way: you pay 33% more per call for answers that score ~7% better on specificity and identically on accuracy. The verbose-priming story holds up at the qualitative level too. HTML doesn't spend its extra tokens on factual errors; it spends them on more specific code citations.&lt;/p&gt;
&lt;p&gt;A few things to keep in mind before reading too much into this:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;N=1 per condition. Single shot per format. Could be variance.&lt;/li&gt;
&lt;li&gt;Tested only on shared content in a single artifact. I didn't try cases where the two formats actually differ in what they covered.&lt;/li&gt;
&lt;li&gt;Didn't test whether answer &lt;em&gt;quality&lt;/em&gt; beyond factual accuracy matters for downstream agent reasoning. Two answers can both be correct and still be differently useful for the agent's next turn.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;What the test does is rule out the strong version of the agent-side worry. HTML doesn't degrade accuracy on shared content. It does cost more on both directions of the interaction for the same outcome.&lt;/p&gt;
&lt;h2 id="the-two-i-didnt-bother-instrumenting-because-theres-no-markdown-alternative"&gt;The two I didn't bother instrumenting (because there's no markdown alternative)&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Checkout button prototype.&lt;/strong&gt; Thariq's prompt: &lt;em&gt;"...Create a HTML file with several sliders and options for me to try different options or actions. Eventually, give me a copy button..."&lt;/em&gt; This is a working interactive widget. Markdown has no equivalent. The cost question is moot; there is no markdown alternative to compare against.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Linear ticket reorderer.&lt;/strong&gt; Thariq's prompt: &lt;em&gt;"Make me an HTML file with each ticket as a draggable card across New / Next / Later / Cut columns..."&lt;/em&gt; Drag-and-drop is an HTML+JavaScript capability. Markdown cannot do this.&lt;/p&gt;
&lt;p&gt;Both of these are real use cases. They demonstrate the categorical-win bucket. The token cost is the price of admission, and there is no marginal-cost-vs-benefit question because there is no marginal alternative.&lt;/p&gt;
&lt;h2 id="going-through-thariqs-claims-one-by-one"&gt;Going through Thariq's claims, one by one&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Claim from the article&lt;/th&gt;
&lt;th&gt;My verdict&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;HTML has higher information density than markdown&lt;/td&gt;
&lt;td&gt;True for visual content; false for text-only content. Markdown handles prose, tables, code blocks well.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;HTML is more readable past 100 lines&lt;/td&gt;
&lt;td&gt;True for visual artifacts (confirmed by user reading observation); equivocal for text-with-structure (markdown's structure works well at length).&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;HTML is easier to share than markdown&lt;/td&gt;
&lt;td&gt;Partly true (raw browsers don't render &lt;code&gt;.md&lt;/code&gt; files), but understated: GitHub renders markdown natively (and renders Mermaid as real diagrams since 2022); VS Code's built-in preview renders markdown; Slack's own "mrkdwn" subset handles a useful chunk. The "you have to attach as email" framing in the article ignores that uploading to GitHub/Gist and sharing a link is a one-step workaround. Untestable here at engagement-level.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;HTML enables two-way interaction&lt;/td&gt;
&lt;td&gt;True categorically. This is the strongest argument in the article (categorical-win bucket).&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;"Token overhead is not really noticeable" with 1M context&lt;/td&gt;
&lt;td&gt;False. Per-artifact overhead is 1.4-1.9x in dollars, 2-4x in tokens. Cumulative overhead approaches the 1M context limit for moderate-sized projects.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;"2-4x slower" generation&lt;/td&gt;
&lt;td&gt;Partially supported: measured 1.4-2.4x across the three cases. The high end of his range is reachable; the low end is below what I measured.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;HTML diffs are noisy (acknowledged downside)&lt;/td&gt;
&lt;td&gt;Confirmed for verbatim methodology (1.2-2.5x diff bytes). Less clear for text-heavy content where markdown's prose lines have their own size.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;HTML is more joyful to work with&lt;/td&gt;
&lt;td&gt;Subjective. Skipped.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;"HTML is strictly better even for text-focused content"&lt;/strong&gt; (his hardline reply)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Not what I saw in the data.&lt;/strong&gt; Both text cases produced competent markdown that did the job, at 1.4-1.5x less cost than the HTML. One of them produced &lt;em&gt;more&lt;/em&gt; substance than the HTML. N=1 per condition isn't enough to refute the hardline definitively, but it's plenty to say the strong version of the claim isn't warranted by what I measured.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;em&gt;Implied&lt;/em&gt; claim: HTML reads better as agent context downstream&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Mixed.&lt;/strong&gt; On shared-content factual retrieval (7-question test on slowapi gotchas), two blinded judges scored both formats 7/7 on accuracy. On explanatory specificity, both judges gave HTML a ~7% edge (6.5-7.0 vs 6.0-6.5). HTML costs 1.33x more per call (1.24x input + 1.99x output). HTML-sourced answers are marginally more specific (more code-citation detail) at significantly higher cost; the quality-per-dollar trade favors markdown.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2 id="so-what-should-you-actually-do"&gt;So what should you actually do?&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;HTML if&lt;/strong&gt; the task is fundamentally visual or interactive: design exploration, prototypes, mockups, dashboards, drag-and-drop widgets, slider-tuned UIs, throwaway editor surfaces. Or you specifically need to send it to someone who won't render markdown themselves. The token cost is what the capability costs. Pay it without thinking.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Markdown if&lt;/strong&gt; the task is text with structure: PR reviews, postmortems, design docs, implementation plans, status reports, meeting notes, technical explainers. The substance lives in the words. HTML's polish costs about 40-50% more per generation, and in my one rate-limiter sample (take it for what one sample's worth) that polish came at the expense of substance, not on top of it.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Maybe HTML on a text task if&lt;/strong&gt; the artifact is going to be the centerpiece of a meeting, or you need visual hierarchy to handle dense reference material (tables of tables, comparison matrices), or the reader can't comfortably read raw markdown. Otherwise, save the tokens.&lt;/p&gt;
&lt;p&gt;The "HTML for everything" stance treats two different questions as one. Half of it is true and worth paying for. The other half is paying extra for things the markdown was already going to give you. Pick the right tool for the right job.&lt;/p&gt;
&lt;h2 id="if-you-want-to-try-this-yourself"&gt;If you want to try this yourself&lt;/h2&gt;
&lt;p&gt;You don't need anything fancy. Opus 4.7 (the model Thariq is pitching for) via Claude Code's non-interactive mode does the whole thing: &lt;code&gt;claude --print --model claude-opus-4-7 -- "&amp;lt;the prompt&amp;gt;"&lt;/code&gt;. The prompts are scattered through this post in blockquotes; use them verbatim for the HTML-affording methodology, or rephrase to drop the format-specific cues if you want the neutral version.&lt;/p&gt;
&lt;p&gt;For substrate, pick something real. A non-trivial recent commit from a codebase you know for the PR review case. Any reasonably-sized library you don't fully understand for the explainer case (I used &lt;a href="https://github.com/laurentS/slowapi"&gt;slowapi&lt;/a&gt;). Anything plausible and visually rich for the design exploration.&lt;/p&gt;
&lt;p&gt;For token counts, I used &lt;a href="https://pypi.org/project/toks/"&gt;&lt;code&gt;toks&lt;/code&gt;&lt;/a&gt;, a small Anthropic-tokenizer wrapper that reports an exact token count for whichever provider you're targeting. Without it I would have been squinting at character counts and guessing.&lt;/p&gt;
&lt;p&gt;One thing I'd do differently if I were starting over: run K=5 generations per condition. Mine were all K=1, which is why a lot of the findings (especially "markdown was more substantive on the rate-limiter") come with the N=1 caveat. If your finding is going to be load-bearing, run it five times.&lt;/p&gt;
&lt;h2 id="what-this-cost-me"&gt;What this cost me&lt;/h2&gt;
&lt;p&gt;The whole thing. Three instrumented cases, two methodologies on one of them, three probes each, the agent-as-reader experiment, plus the two demo cases. Ran me $9.42 across 26 API calls on Opus 4.7. About 28 minutes of API time, less in wall clock once you parallelize. Cheaper than the lunch I had while it ran. Tracking?&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;strong&gt;Feature Image Prompt:&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Generate an image. The aesthetic should be cyberpunk with colors of neon pink, blue and purple. Do not add any people. Depict two colossal floating monolithic slabs facing each other in a rain-slicked neon city alley, suspended in mid-air. The left slab is constructed of intricate, ornate glowing HTML tag scaffolding -- angle brackets, nested div structures, and CSS class hieroglyphs etched in luminous magenta and electric pink, dense and baroque, with cables and wires spilling from its edges. The right slab is sparse and elegant, made of clean horizontal markdown rules, hashtag headers, and asterisk emphasis carved in cool cyan and deep purple light, minimalist and crystalline. Between them hovers a glowing translucent balance scale made of holographic wireframe, tipping almost imperceptibly, with streams of tokenized data particles flowing from both slabs into the central fulcrum. The background is a vertical canyon of dark chrome skyscrapers with flickering neon signage in Japanese and binary code, volumetric fog rolling at the base, puddles reflecting the pink and blue glow. Wet asphalt, lens flares, chromatic aberration, cinematic depth of field, ultra-detailed, atmospheric haze, Blade Runner meets technical schematic.&lt;/p&gt;
&lt;script type="text/javascript"&gt;if (!document.getElementById('mathjaxscript_pelican_#%@#$@#')) {
    var align = "center",
        indent = "0em",
        linebreak = "false";

    if (false) {
        align = (screen.width &lt; 768) ? "left" : align;
        indent = (screen.width &lt; 768) ? "0em" : indent;
        linebreak = (screen.width &lt; 768) ? 'true' : linebreak;
    }

    var mathjaxscript = document.createElement('script');
    mathjaxscript.id = 'mathjaxscript_pelican_#%@#$@#';
    mathjaxscript.type = 'text/javascript';
    mathjaxscript.src = 'https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.3/latest.js?config=TeX-AMS-MML_HTMLorMML';

    var configscript = document.createElement('script');
    configscript.type = 'text/x-mathjax-config';
    configscript[(window.opera ? "innerHTML" : "text")] =
        "MathJax.Hub.Config({" +
        "    config: ['MMLorHTML.js']," +
        "    TeX: { extensions: ['AMSmath.js','AMSsymbols.js','noErrors.js','noUndefined.js'], equationNumbers: { autoNumber: 'none' } }," +
        "    jax: ['input/TeX','input/MathML','output/HTML-CSS']," +
        "    extensions: ['tex2jax.js','mml2jax.js','MathMenu.js','MathZoom.js']," +
        "    displayAlign: '"+ align +"'," +
        "    displayIndent: '"+ indent +"'," +
        "    showMathMenu: true," +
        "    messageStyle: 'normal'," +
        "    tex2jax: { " +
        "        inlineMath: [ ['\\\\(','\\\\)'] ], " +
        "        displayMath: [ ['$$','$$'] ]," +
        "        processEscapes: true," +
        "        preview: 'TeX'," +
        "    }, " +
        "    'HTML-CSS': { " +
        "        availableFonts: ['STIX', 'TeX']," +
        "        preferredFont: 'STIX'," +
        "        styles: { '.MathJax_Display, .MathJax .mo, .MathJax .mi, .MathJax .mn': {color: 'inherit ! important'} }," +
        "        linebreaks: { automatic: "+ linebreak +", width: '90% container' }," +
        "    }, " +
        "}); " +
        "if ('default' !== 'default') {" +
            "MathJax.Hub.Register.StartupHook('HTML-CSS Jax Ready',function () {" +
                "var VARIANT = MathJax.OutputJax['HTML-CSS'].FONTDATA.VARIANT;" +
                "VARIANT['normal'].fonts.unshift('MathJax_default');" +
                "VARIANT['bold'].fonts.unshift('MathJax_default-bold');" +
                "VARIANT['italic'].fonts.unshift('MathJax_default-italic');" +
                "VARIANT['-tex-mathit'].fonts.unshift('MathJax_default-italic');" +
            "});" +
            "MathJax.Hub.Register.StartupHook('SVG Jax Ready',function () {" +
                "var VARIANT = MathJax.OutputJax.SVG.FONTDATA.VARIANT;" +
                "VARIANT['normal'].fonts.unshift('MathJax_default');" +
                "VARIANT['bold'].fonts.unshift('MathJax_default-bold');" +
                "VARIANT['italic'].fonts.unshift('MathJax_default-italic');" +
                "VARIANT['-tex-mathit'].fonts.unshift('MathJax_default-italic');" +
            "});" +
        "}";

    (document.body || document.getElementsByTagName('head')[0]).appendChild(configscript);
    (document.body || document.getElementsByTagName('head')[0]).appendChild(mathjaxscript);
}
&lt;/script&gt;</content><category term="Writing"/><category term="claude_code"/><category term="context_engineering"/></entry><entry><title>The Web Is Now a Two-Way Street for AI</title><link href="https://gallon.me/ai-didnt-kill-the-web-it-moved-in-olivier-leplus-aws-yohan-lasorsa-microsoft.html" rel="alternate"/><published>2026-04-12T00:00:00-05:00</published><updated>2026-04-12T00:00:00-05:00</updated><author><name>Corey</name></author><id>tag:gallon.me,2026-04-12:/ai-didnt-kill-the-web-it-moved-in-olivier-leplus-aws-yohan-lasorsa-microsoft.html</id><summary type="html">&lt;p&gt;Yohan Lasorsa, Developer Advocate at Microsoft, and Olivier Leplus, Developer Advocate at AWS, opened their joint talk with a premise most web developers already accept: AI helps you build websites. Their argument is that the reverse is now equally true -- the web is becoming AI's runtime, its data source, and …&lt;/p&gt;</summary><content type="html">&lt;p&gt;Yohan Lasorsa, Developer Advocate at Microsoft, and Olivier Leplus, Developer Advocate at AWS, opened their joint talk with a premise most web developers already accept: AI helps you build websites. Their argument is that the reverse is now equally true -- the web is becoming AI's runtime, its data source, and its interface layer. Browsers ship local models. Web pages expose tools for agents. The relationship, they argue, is no longer one-directional.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;"It's 2026, it's no longer the question of can I code my web app with AI, but rather how to get the best results out of AI coding agents."&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id="skills-as-composable-agent-plugins"&gt;Skills as Composable Agent Plugins&lt;/h2&gt;
&lt;p&gt;Yohan started with "skills" -- lightweight text-based plugins stored in &lt;code&gt;.agent/skills/&lt;/code&gt; folders that follow an open specification supported by most coding agents. Each skill has a name, a description the agent uses to decide when to load it, and instructional content.&lt;/p&gt;
&lt;p&gt;He demonstrated chaining several together: a GitHub CLI skill pulls an issue, Playwright records a video of the implemented feature, a tunnel skill creates a public URL for mobile testing, and a Telegram skill sends that URL to his phone. An &lt;code&gt;agents.md&lt;/code&gt; file orchestrates this into a repeatable workflow -- implement, record, tunnel, notify, then wait for human confirmation before closing the issue.&lt;/p&gt;
&lt;p&gt;&lt;img alt="VS Code explorer showing the .agent/skills/ folder expanded with skills including chrome-devtools, frontend-design, gh-cli, playwright-cli, public-tunnel, skill-creator, and telegram-send, while a coding agent reads files and loads skills in the right panel" src="images/agent-skills-folder.jpg"&gt;&lt;/p&gt;
&lt;p&gt;The practical upshot: instead of improving your prompting, you improve your skills library. The wordplay was intentional -- Lasorsa joked that getting better results from coding agents "is mainly a matter of skills -- but don't get me wrong, it's the one that you install and use with your favorite code agent."&lt;/p&gt;
&lt;h2 id="debugging-without-opening-devtools"&gt;Debugging Without Opening DevTools&lt;/h2&gt;
&lt;p&gt;Olivier demonstrated the Chrome DevTools MCP server -- an open-source project that exposes Chrome DevTools capabilities as tools callable by coding agents. Click, fill forms, read console messages, run Lighthouse audits, take screenshots, capture performance traces -- all available as MCP tools without manual DevTools interaction.&lt;/p&gt;
&lt;p&gt;&lt;img alt="VS Code showing the mcp.json configuration for the chrome-devtools MCP server, with the available tools listed in the left sidebar including click, fill_form, take_screenshot, navigate_page, and performance_analyze_insight" src="images/chrome-devtools-mcp-tools.jpg"&gt;&lt;/p&gt;
&lt;p&gt;In the demo, an agent autonomously launched Chrome, navigated to the app, and ran performance traces under three network conditions -- no throttling, fast 3G, and slow 2G. It produced a report with LCP, CLS, critical path latency, and render-blocking resource analysis. The agent did the work a developer would normally do by hand across multiple DevTools panels.&lt;/p&gt;
&lt;p&gt;Leplus also showed Chrome's built-in AI features in DevTools: an "explain with AI" button on console errors, a "debug with AI" option on failing network requests, and an "Ask AI" button on performance traces. These are baked into Chrome itself, not extensions.&lt;/p&gt;
&lt;h2 id="a-4gb-model-in-your-browser"&gt;A 4GB Model in Your Browser&lt;/h2&gt;
&lt;p&gt;The most technically surprising section covered the Web AI APIs -- currently in W3C draft status, implemented in Chrome behind flags, with Opera also adding support. These are browser-native APIs that run a local model on the client machine.&lt;/p&gt;
&lt;p&gt;Lasorsa and Leplus demonstrated three APIs:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Summarizer:&lt;/strong&gt; &lt;code&gt;ai.summarizer.create()&lt;/code&gt; with options for output type (TLDR, teaser, key points, headline), length, and language. They summarized product reviews live.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Proofreader:&lt;/strong&gt; Returns corrected text with start and end indices for each correction.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Prompt API:&lt;/strong&gt; &lt;code&gt;ai.languageModel.create()&lt;/code&gt; supporting multimodal input -- text, images, audio -- with structured JSON output via schema constraints. They uploaded a photo of headphones and got a generated product review back.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img alt="The Write a Review form auto-filled with an AI-generated title and review text based on an uploaded photo of a broken headset, with the Chrome console showing the Prompt API analyzing the image and generating structured output" src="images/prompt-api-multimodal-demo.jpg"&gt;&lt;/p&gt;
&lt;p&gt;The model download is roughly four gigabytes, according to Leplus, downloaded once and shared across all websites. Chrome's &lt;code&gt;chrome://on-device-internals&lt;/code&gt; page provides debugging for model status and token usage. The speakers emphasized these APIs are highly experimental -- the language specification requirement was added in the week before the talk.&lt;/p&gt;
&lt;h2 id="making-websites-agent-readable"&gt;Making Websites Agent-Readable&lt;/h2&gt;
&lt;p&gt;The second half of the symbiosis argument: if AI builds the web, the web should also feed AI. Lasorsa and Leplus covered two approaches.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;llms.txt&lt;/strong&gt; is a markdown file served at a domain's root that acts as a map for AI agents to discover documentation pages -- a hybrid of robots.txt and sitemaps. There's also an &lt;code&gt;llm-full.txt&lt;/code&gt; variant that consolidates all site content into a single file, useful for feeding coding agents up-to-date framework docs. Yohan pointed to Angular.dev as an example already shipping this.&lt;/p&gt;
&lt;p&gt;&lt;img alt="Angular.dev's llms.txt file displayed in a browser, showing a structured markdown table of contents with links to component guides, template guides, directives, and signals documentation" src="images/angular-llms-txt.jpg"&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;WebMCP&lt;/strong&gt; is far more ambitious and, as Leplus put it, "very, very highly experimental." The proposal lets websites register MCP tools directly on web pages. Leplus showed two approaches: a JavaScript API where you define a tool with name, description, schema, and execute function, then register it via the browser's &lt;code&gt;navigator&lt;/code&gt; object; and a declarative HTML approach using attributes like &lt;code&gt;tool-name&lt;/code&gt;, &lt;code&gt;tool-description&lt;/code&gt;, and &lt;code&gt;tool-auto-submit&lt;/code&gt; on existing form elements. The browser derives the tool schema from form inputs and their labels automatically.&lt;/p&gt;
&lt;p&gt;&lt;img alt="HTML form code in VS Code showing WebMCP declarative attributes -- tool-name set to write_review_tool, toolDescription, and tool-param-description attributes on form inputs, allowing the browser to auto-generate an MCP tool schema from the form structure" src="images/webmcp-html-attributes.jpg"&gt;&lt;/p&gt;
&lt;p&gt;He demonstrated calling these registered tools from both a Chrome extension and from a coding agent IDE, with the tool executing directly on the web page.&lt;/p&gt;
&lt;h2 id="the-responsive-design-analogy"&gt;The Responsive Design Analogy&lt;/h2&gt;
&lt;p&gt;Olivier closed with an analogy that frames the stakes clearly.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;"It's like responsive design. At some point, you had to adapt your website for mobile. And if you didn't do it, then the competition did it. And then people wouldn't go to your website on the mobile."&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;His argument is that agent-readiness is the next version of that transition. Websites that don't expose structured interfaces for AI agents -- via llms.txt, WebMCP, or whatever standards emerge -- will be at a disadvantage as agentic browsers become mainstream. Whether the timeline is as compressed as responsive design's was is an open question, but the direction Lasorsa and Leplus describe is concrete enough to act on today.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;Yohan Lasorsa and Olivier Leplus spoke at AI Engineer Europe 2026. Lasorsa is a Developer Advocate at &lt;a href="https://www.microsoft.com/"&gt;Microsoft&lt;/a&gt;; Leplus is a Developer Advocate at &lt;a href="https://aws.amazon.com"&gt;AWS&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;&lt;a href="https://www.youtube.com/watch?v=XZ0boOjtbNo"&gt;Watch the full talk&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;</content><category term="Conferences"/><category term="AI_Engineer_Europe"/><category term="mcp"/><category term="agents"/><category term="webdev"/></entry><entry><title>Your Digital Exhaust Is the Most Underused Dataset You Own</title><link href="https://gallon.me/cognitive-exhaust-fumes-or-read-only-ai-is-underrated-imon-podhajsk-head-of-ai-waypoint.html" rel="alternate"/><published>2026-04-12T00:00:00-05:00</published><updated>2026-04-12T00:00:00-05:00</updated><author><name>Corey</name></author><id>tag:gallon.me,2026-04-12:/cognitive-exhaust-fumes-or-read-only-ai-is-underrated-imon-podhajsk-head-of-ai-waypoint.html</id><summary type="html">&lt;p&gt;Šimon Podhajský (&lt;a href="https://linkedin.com/in/simonpodhajsky"&gt;LinkedIn&lt;/a&gt;, &lt;a href="https://x.com/sim_pod"&gt;X&lt;/a&gt;), Head of AI at Waypoint AI, opened with a premise that cuts against the grain of most personal AI demos: what if the most valuable thing an AI can do with your data is simply read it?&lt;/p&gt;</summary><content type="html">&lt;p&gt;Šimon Podhajský (&lt;a href="https://linkedin.com/in/simonpodhajsky"&gt;LinkedIn&lt;/a&gt;, &lt;a href="https://x.com/sim_pod"&gt;X&lt;/a&gt;), Head of AI at Waypoint AI, opened with a premise that cuts against the grain of most personal AI demos: what if the most valuable thing an AI can do with your data is simply read it?&lt;/p&gt;
&lt;p&gt;Every other personal AI project seems to be racing toward agents that send emails and manage calendars on your behalf. Podhajský built the opposite -- a read-only system that queries six personal data sources (email, journal, tasks, CRM, browser sessions, notes) but can never write back to any of them. He argues this isn't a limitation to overcome. It's the point.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;"It's my term for the digital activity that is a byproduct of your cognition, like exhaust fumes for a car engine. Individually, it's just waste, but if you analyze the exhaust, you can diagnose the engine."&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id="cross-source-signal-is-the-product"&gt;Cross-Source Signal Is the Product&lt;/h2&gt;
&lt;p&gt;The core of Šimon's argument is that individual tools are blind to each other. Your email client doesn't know what you journaled. Your task manager doesn't know what you're browsing. The value isn't in any single data source -- it's in the patterns that emerge when you read across all of them.&lt;/p&gt;
&lt;p&gt;He demonstrated three use cases, all requiring cross-source analysis:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Intention-action gaps:&lt;/strong&gt; Comparing what you said you'd do (tasks, journal entries) against what you actually did (email, browser activity).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Attention drift:&lt;/strong&gt; Detecting when your browsing patterns diverge from your stated priorities.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Relationship decay:&lt;/strong&gt; Surfacing contacts you've been neglecting by cross-referencing CRM data against communication patterns.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img alt="Slide titled &amp;quot;What the Exhaust Reveals&amp;quot; listing three cross-source use cases -- intention-action gaps, attention drift, and relationship decay -- each with a concrete example and the data sources that power it" src="images/what-the-exhaust-reveals.jpg"&gt;&lt;/p&gt;
&lt;h2 id="three-zones-no-write-path"&gt;Three Zones, No Write Path&lt;/h2&gt;
&lt;p&gt;The architecture is deliberately simple. Šimon described three zones:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Sources&lt;/strong&gt; -- the six data sources, all read-only. The AI never writes back.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Workspace&lt;/strong&gt; -- where analysis happens, using structured prompts and Python scripts.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Outputs&lt;/strong&gt; -- results land in a separate destination (he uses an Obsidian vault), never back into the source systems.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img alt="Architecture diagram showing &amp;quot;Three Zones&amp;quot;: read-only sources (email, journal, browser, tasks, notes, contacts) flowing into an analysis workspace running Claude Code with 18 specialized skills, which writes outputs (weekly reflections, draft rankings, alerts) for the user to review in Obsidian" src="images/three-zones-architecture.jpg"&gt;&lt;/p&gt;
&lt;p&gt;The separation between sources and outputs is load-bearing. Podhajský argues that the moment an AI writes back to your data sources, the exhaust is contaminated -- you can no longer tell which patterns are yours and which are the AI's.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;"The moment your AI writes to your data sources, the exhaust fumes are contaminated. You're no longer observing your cognition. You're observing a human-AI hybrid, and you can't tell which patterns are yours."&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id="the-weekly-mirror"&gt;The Weekly Mirror&lt;/h2&gt;
&lt;p&gt;In a live demo, Šimon ran a weekly reflection -- a David Allen-style review generated by pulling data from all six sources and synthesizing it into a markdown document. The output covers themes of the week, tensions, commitments, relationship gaps (notable for what's missing, not just what's present), and reflection questions.&lt;/p&gt;
&lt;p&gt;He described the output as "occasionally brutal." The system told him he'd been avoiding his most important project for two weeks -- something no single tool would have flagged.&lt;/p&gt;
&lt;p&gt;&lt;img alt="Slide titled &amp;quot;In Practice: Weekly Reflection&amp;quot; showing example output: &amp;quot;You said apartment furnishing matters but spent 2h on it vs. 11h on side projects. Three people you called important haven't heard from you in a month. You created 12 tasks and completed 4.&amp;quot; Followed by a callout: &amp;quot;Not a productivity report. A reflection on how you're thinking -- assembled entirely from exhaust.&amp;quot;" src="images/weekly-reflection-example.jpg"&gt;&lt;/p&gt;
&lt;p&gt;A second demo showed cross-source reading recommendations: given what he's currently reading (pulled from his browser's local SQLite database), who in his network should he discuss it with? The system matched articles to contacts by interest. He noted the CRM integration was the slowest part and the whole thing is token-heavy -- best run in a clean session.&lt;/p&gt;
&lt;h2 id="read-errors-are-free-write-errors-are-not"&gt;Read Errors Are Free, Write Errors Are Not&lt;/h2&gt;
&lt;p&gt;Podhajský's risk analysis is where the talk gets sharp. He frames the choice between read-only and agentic AI as fundamentally asymmetric:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;"The downside of a read-only error is zero. I just ignore it. The downside of a write error is unbounded."&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Or, more bluntly: "I'd rather miss out on automated emails than have a misfire nuke my life."&lt;/p&gt;
&lt;p&gt;&lt;img alt="Risk table comparing observer (read-only) vs. agent (read-write) across four dimensions: best case, worst case, cost of error, and recovery. Observer worst case is &amp;quot;shows me something irrelevant&amp;quot; with zero-cost recovery; agent worst case is &amp;quot;sends wrong email, creates false commitment, deletes a file&amp;quot; with potentially irreversible consequences" src="images/risk-table-observer-vs-agent.jpg"&gt;&lt;/p&gt;
&lt;p&gt;He also acknowledged the risks that remain. Cross-referencing personal data creates what he calls the mosaic effect -- the same capability that makes the system useful makes it a devastating target if compromised.&lt;/p&gt;
&lt;p&gt;&lt;img alt="Slide on &amp;quot;The Mosaic Effect&amp;quot; contrasting low-sensitivity individual sources (an email about a meeting, a browser tab on flights) with the high-sensitivity picture that emerges when cross-referenced: &amp;quot;Subject is planning a large purchase, has a trip abroad next week, just had a conflict with a close friend, missed a family obligation.&amp;quot; The bottom reads: &amp;quot;Individually noise, together they reveal the engine&amp;quot; is both my value proposition and my threat model." src="images/mosaic-effect.jpg"&gt;&lt;/p&gt;
&lt;p&gt;He referenced Simon Willison's "lethal trifecta" (private data, untrusted content, external communications) and initially thought his read-only design broke it, but conceded it doesn't fully -- shell access still provides external communication channels.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;"I'm not claiming the system is secure. I'm claiming that I've thought about where it isn't and I've decided which risks I'm willing to carry."&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id="a-mirror-not-a-broken-butler"&gt;A Mirror, Not a Broken Butler&lt;/h2&gt;
&lt;p&gt;The sharpest reframe in the talk is Šimon's insistence that read-only AI isn't a stepping stone toward "real" agentic AI. It's a different product category entirely.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;"A mirror isn't a broken butler."&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The industry, he argues, frames read-only as a limitation you graduate from. He thinks that's wrong. The observer -- the system that shows you what you're actually doing versus what you think you're doing -- produces more value per interaction than the agent, in his experience. The race to build personal AI agents that act on your behalf skips over something more fundamental: most people don't have a clear picture of their own cognitive patterns. He published an &lt;a href="https://github.com/shippy/personal-intelligence-kit"&gt;open-source template&lt;/a&gt; for others to try the approach.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;"Your digital exhaust is the most underused dataset you own."&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;Šimon Podhajský spoke at AI Engineer Europe 2026. Head of AI at &lt;a href="https://simon.podhajsky.net"&gt;Waypoint AI&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;&lt;a href="https://www.youtube.com/watch?v=u0TOSBbAw7c"&gt;Watch the full talk&lt;/a&gt; | &lt;a href="https://slides.podhajsky.net/read-only-ai"&gt;Slides&lt;/a&gt; | &lt;a href="https://github.com/shippy/personal-intelligence-kit"&gt;Personal Intelligence Kit (GitHub)&lt;/a&gt; | &lt;a href="https://linkedin.com/in/simonpodhajsky"&gt;LinkedIn&lt;/a&gt; | &lt;a href="https://x.com/sim_pod"&gt;X&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;</content><category term="Conferences"/><category term="AI_Engineer_Europe"/><category term="conference"/><category term="agents"/><category term="ai_engineering"/></entry><entry><title>Your Multi-Agent System Isn't Failing Because of the AI</title><link href="https://gallon.me/from-chaos-to-choreography-multi-agent-orchestration-patterns-that-actually-work-sandipan-bhaumik.html" rel="alternate"/><published>2026-04-12T00:00:00-05:00</published><updated>2026-04-12T00:00:00-05:00</updated><author><name>Corey</name></author><id>tag:gallon.me,2026-04-12:/from-chaos-to-choreography-multi-agent-orchestration-patterns-that-actually-work-sandipan-bhaumik.html</id><summary type="html">&lt;p&gt;Sandipan Bhaumik (&lt;a href="https://www.linkedin.com/in/sandipanbhaumik"&gt;LinkedIn&lt;/a&gt;), Data &amp;amp; AI Tech Lead at Databricks, opened with an anecdote that set the tone for the whole talk. A single credit-scoring agent ran for two weeks in production without issues. The team added four more agents. Within days, 20% of risk ratings were wrong -- not because the …&lt;/p&gt;</summary><content type="html">&lt;p&gt;Sandipan Bhaumik (&lt;a href="https://www.linkedin.com/in/sandipanbhaumik"&gt;LinkedIn&lt;/a&gt;), Data &amp;amp; AI Tech Lead at Databricks, opened with an anecdote that set the tone for the whole talk. A single credit-scoring agent ran for two weeks in production without issues. The team added four more agents. Within days, 20% of risk ratings were wrong -- not because the LLM was hallucinating, but because a caching layer between agents wasn't invalidating correctly. A classic distributed systems race condition on stale data.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;"They think adding more agents is just like adding more features. It's not. It's building a distributed system."&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;His argument: when multi-agent systems break, teams blame the model or the prompts. Almost every time, Bhaumik says, it's the architecture.&lt;/p&gt;
&lt;h2 id="the-coordination-complexity-problem"&gt;The Coordination Complexity Problem&lt;/h2&gt;
&lt;p&gt;Sandipan points out that going from one agent to five doesn't create five times the complexity. Five agents have at least ten potential coordination points -- each one a failure surface. The math is straightforward (pairwise connections), but teams consistently underestimate it because adding an agent &lt;em&gt;feels&lt;/em&gt; like adding a feature.&lt;/p&gt;
&lt;p&gt;&lt;img alt="Slide titled &amp;quot;The Complexity Curve&amp;quot; showing an exponential curve with Number of Agents on the y-axis and Coordination Complexity on the x-axis, with a highlighted callout reading &amp;quot;5 agents = 25x complex&amp;quot;" src="images/complexity-curve.jpg"&gt;&lt;/p&gt;
&lt;p&gt;The fix, he argues, isn't better AI. It's applying decades of distributed systems engineering to a problem space that's pretending those lessons don't exist.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;"This is no longer an AI problem. This is a distributed system problem."&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id="choreography-vs-orchestration"&gt;Choreography vs. Orchestration&lt;/h2&gt;
&lt;p&gt;Sandipan breaks agent coordination into two patterns, and argues most teams pick one instinctively and regret it.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Choreography&lt;/strong&gt; is event-driven and decentralized. Agents publish events to a message bus when they finish work; downstream agents subscribe to the event types they care about. It scales well and makes adding new agents easy. The downside: debugging is brutal without strong observability. You can't trace which agent failed to publish, whether events were consumed, or whether they were consumed twice.&lt;/p&gt;
&lt;p&gt;&lt;img alt="Slide titled &amp;quot;Choreography: Event-Driven Coordination&amp;quot; showing three hexagonal agents -- Research Agent, Analysis Agent, and Report Agent -- connected through a Message Bus, with arrows labeled Publish, Subscribe &amp;amp; Consume between them" src="images/choreography-event-driven.jpg"&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Orchestration&lt;/strong&gt; is centralized. A workflow orchestrator calls each agent directly, manages parallelism, tracks the full execution graph, handles retries, and logs every step. Agents are deliberately simple -- they take input, do work, return output. He says financial services uses orchestration almost exclusively because rollback capability and auditability matter more than agent autonomy.&lt;/p&gt;
&lt;p&gt;&lt;img alt="Slide titled &amp;quot;Orchestration: Centralized Coordination&amp;quot; showing an Orchestrator node on the left calling Agent A in Step 1, then Agent B and Agent C in parallel in Step 2, then Agent D in Step 3" src="images/orchestration-centralized.jpg"&gt;&lt;/p&gt;
&lt;p&gt;His decision framework maps workflow complexity against autonomy requirements across four quadrants.&lt;/p&gt;
&lt;p&gt;&lt;img alt="Decision Matrix with Workflow Complexity on the x-axis (simple to complex) and Autonomy on the y-axis (low to high), showing four quadrants: Choreography (simple, high autonomy), Hybrid (complex, high autonomy), Simple Orchestration (simple, low autonomy), and Full Orchestration (complex, low autonomy)" src="images/decision-matrix.jpg"&gt;&lt;/p&gt;
&lt;p&gt;Simple workflow with high autonomy needs points to choreography. Complex workflow with low autonomy tolerance points to orchestration. Complex workflow with high autonomy needs points to hybrid patterns -- choreography with saga patterns for compensation.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;"I've seen teams choose choreography because it feels more agentic, more autonomous. Then they spend months firefighting because they can't debug distributed event flows."&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id="immutable-state-over-shared-mutable-state"&gt;Immutable State Over Shared Mutable State&lt;/h2&gt;
&lt;p&gt;The anti-pattern Sandipan flags most often: shared mutable state where multiple agents read and write the same database records concurrently. Even with modern database protections, teams use default isolation levels, skip explicit locks, and ship race conditions to production.&lt;/p&gt;
&lt;p&gt;His recommended pattern is immutable state snapshots with versioning. Each agent produces a sealed, immutable state version -- append-only inserts, never updates. At each handoff, the receiving agent validates the schema against a data contract before processing. If an agent fails, you roll back to the previous version. For debugging, you replay state evolution from version 1 through version N.&lt;/p&gt;
&lt;p&gt;&lt;img alt="Slide titled &amp;quot;Correct Pattern: Immutable State Snapshots&amp;quot; showing Agent A producing state v1 (with lock and checkmark icons), which flows to Agent B, which produces state v2, which flows to Agent C, with a note that state snapshots can be logged to append-only storage for audit/replay but never shared for read/write" src="images/immutable-state-snapshots.jpg"&gt;&lt;/p&gt;
&lt;p&gt;Data contracts enforce that one agent's output schema matches the next agent's expected input. If a research agent outputs data with a confidence score below a threshold, the contract rejects the handoff at the boundary rather than letting bad data propagate three agents downstream.&lt;/p&gt;
&lt;h2 id="circuit-breakers-and-compensation"&gt;Circuit Breakers and Compensation&lt;/h2&gt;
&lt;p&gt;He covers two failure recovery patterns he considers essential for production multi-agent systems.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Circuit breakers&lt;/strong&gt; wrap every agent call. After a configurable number of consecutive failures, the circuit opens and the system fails fast instead of waiting for timeouts. After a cooldown period, it goes half-open and tests with a single request. This prevents one failing agent from cascading into a full system outage.&lt;/p&gt;
&lt;p&gt;&lt;img alt="Slide titled &amp;quot;Circuit Breaker Pattern: Fail Fast, Recover Gracefully&amp;quot; showing a state diagram with three states -- Circuit Closed (normal operation), Circuit Open (blocking), and Circuit Half-Open (testing recovery) -- connected by transitions: failed 5 times opens the circuit, after 60 seconds it goes half-open, success closes it, failure reopens it" src="images/circuit-breaker-pattern.jpg"&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;"Circuit breakers are the single most important failure recovery pattern for multi-agent systems."&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;The saga/compensation pattern&lt;/strong&gt; gives transactional semantics across distributed agents. Every agent implements two methods: &lt;code&gt;execute&lt;/code&gt; and &lt;code&gt;compensate&lt;/code&gt;. If an agent fails mid-workflow, the orchestrator walks backward through previously successful agents, calling &lt;code&gt;compensate&lt;/code&gt; on each to undo their work.&lt;/p&gt;
&lt;p&gt;&lt;img alt="Slide titled &amp;quot;Compensation Pattern: Rollback When Failure Happens Mid-Workflow&amp;quot; showing three agents in sequence -- Research Agent, Analysis Agent, Execution Agent -- where the Execution Agent fails, triggering backward compensation: Analysis Agent deletes its draft recommendation, Research Agent clears its cached research data, and Execution Agent has nothing to undo" src="images/compensation-pattern-rollback.jpg"&gt;&lt;/p&gt;
&lt;p&gt;Sandipan acknowledges it's not glamorous work. But it's how production systems handle partial failures without human intervention at 2 a.m.&lt;/p&gt;
&lt;h2 id="the-unsexy-work-that-keeps-systems-running"&gt;The Unsexy Work That Keeps Systems Running&lt;/h2&gt;
&lt;p&gt;Bhaumik's closing is blunt. Demos are easy -- anyone can use an LLM to show something cool. The hard part is everything he covered: choreography versus orchestration decisions, immutable state, circuit breakers. All of it is infrastructure work that won't get applause.&lt;/p&gt;
&lt;p&gt;&lt;img alt="Slide titled &amp;quot;Production&amp;quot; showing a full production architecture with an Orchestrator containing a Workflow Engine (DAG), State Store (v0, v1, v2...), and Observability (Tracing), calling Agent A, then Agent B and C in parallel, then Agent D, with each agent returning versioned state objects" src="images/production-architecture.jpg"&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;"You won't get applause for implementing a circuit breaker, but you make your systems more reliable. They don't fail at 2 a.m. in the night."&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;His core argument is that the teams succeeding with multi-agent systems in production aren't the ones with the best prompts or the most capable models. They're the ones treating agent coordination as what it is -- a distributed systems problem -- and applying the patterns that have solved those problems for decades.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;Sandipan Bhaumik spoke at AI Engineer Europe 2026. Data &amp;amp; AI Tech Lead at &lt;a href="https://www.databricks.com"&gt;Databricks&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;&lt;a href="https://www.youtube.com/watch?v=2czYyrTzILg"&gt;Watch the full talk&lt;/a&gt; | &lt;a href="https://drive.google.com/file/d/18LqVzhfVS3iULYuy2EshWoMLmQt3rdpT/view?usp=sharing"&gt;Slides&lt;/a&gt; | &lt;a href="https://www.linkedin.com/in/sandipanbhaumik"&gt;LinkedIn&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;</content><category term="Conferences"/><category term="agents"/><category term="AI_Engineer_Europe"/><category term="architecture"/><category term="orchestration"/></entry><entry><title>Your LLM Evaluator Is Probably Lying to You</title><link href="https://gallon.me/judge-the-judge-building-llm-evaluators-that-actually-work-with-gepa-mahmoud-mabrouk-agenta-ai.html" rel="alternate"/><published>2026-04-12T00:00:00-05:00</published><updated>2026-04-12T00:00:00-05:00</updated><author><name>Corey</name></author><id>tag:gallon.me,2026-04-12:/judge-the-judge-building-llm-evaluators-that-actually-work-with-gepa-mahmoud-mabrouk-agenta-ai.html</id><summary type="html">&lt;p&gt;Mahmoud Mabrouk (&lt;a href="https://x.com/mmabrouk_"&gt;X&lt;/a&gt;, &lt;a href="https://www.linkedin.com/in/mmabrouk2/"&gt;LinkedIn&lt;/a&gt;), co-founder and CEO of Agenta AI, opened his AI Engineer Europe workshop with a scenario most teams will recognize: your LLM agent is in production, your observability dashboard looks clean, but customers keep saying the thing doesn't work. The culprit, he argues, isn't the agent -- it's …&lt;/p&gt;</summary><content type="html">&lt;p&gt;Mahmoud Mabrouk (&lt;a href="https://x.com/mmabrouk_"&gt;X&lt;/a&gt;, &lt;a href="https://www.linkedin.com/in/mmabrouk2/"&gt;LinkedIn&lt;/a&gt;), co-founder and CEO of Agenta AI, opened his AI Engineer Europe workshop with a scenario most teams will recognize: your LLM agent is in production, your observability dashboard looks clean, but customers keep saying the thing doesn't work. The culprit, he argues, isn't the agent -- it's the evaluator. A miscalibrated LLM-as-a-judge gives you false confidence while producing no useful signal.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;"You'll find a prompt not very far from this one: 'You'll be given an LLM output, write whether it's a hallucination, make no mistakes.' Now obviously, how the hell would the agent know whether it's a hallucination? If it could, then your app would have worked from day one."&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id="evaluation-as-a-machine-learning-problem"&gt;Evaluation as a Machine Learning Problem&lt;/h2&gt;
&lt;p&gt;Mabrouk's central reframe is that building an LLM-as-a-judge is itself a machine learning problem, not a prompt engineering exercise. The evaluator prompt is a learned artifact that should be optimized against labeled data -- the same way you'd train any model. The quality ceiling of your evaluator, he argues, is determined by the quality of your annotation data and your optimization process, not by how clever your starting prompt is.&lt;/p&gt;
&lt;p&gt;&lt;img alt="Slide showing the evaluation iteration loop: two boxes labeled &amp;quot;Improve (harness, prompt..)&amp;quot; and &amp;quot;Measure (evals)&amp;quot; connected by arrows in a continuous cycle" src="images/eval-iteration-loop.jpg"&gt;&lt;/p&gt;
&lt;p&gt;This leads to what Mahmoud calls the "holy grail of AI engineering" -- a data flywheel where you optimize your evaluation harness, observe traces in production, add new evals based on edge cases, and repeat.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;"The speed in which you move to production or add features is actually the speed in which you can complete this loop."&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id="four-steps-to-a-calibrated-judge"&gt;Four Steps to a Calibrated Judge&lt;/h2&gt;
&lt;p&gt;Mahmoud's workflow has four stages:&lt;/p&gt;
&lt;p&gt;&lt;img alt="Slide titled &amp;quot;Workflow&amp;quot; listing four steps: 1. Design metrics, 2. Annotate, 3. Optimize Judge (in bold), 4. Validate results" src="images/four-step-workflow.jpg"&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Metric design&lt;/strong&gt; -- Define evaluation axes from your specific use case, not from generic libraries. He identified four error types through manual analysis of traces: policy adherence, response style, information delivery, and incorrect tool calls. Each gets its own binary evaluator (pass/fail, not a 1-5 scale). He credits Hamel Husain for articulating this error analysis workflow.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img alt="Slide titled &amp;quot;Error Analysis&amp;quot; showing Agenta's annotation interface with a dropdown listing four error categories: Policy Adherence, Response Style, Information Delivery, and Miscalled tools" src="images/error-analysis-categories.jpg"&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Data annotation&lt;/strong&gt; -- Subject matter experts annotate conversation traces with binary verdicts &lt;em&gt;plus reasoning&lt;/em&gt;. Mabrouk stresses the reasoning field is critical -- without it, the optimization algorithm has to independently discover &lt;em&gt;why&lt;/em&gt; something failed, which is extremely difficult for complex policy evaluation.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Optimization with GEPA&lt;/strong&gt; -- GEPA is a prompt optimization algorithm that works like a genetic algorithm. Each iteration samples new candidate prompts (through mutation and merging), evaluates them against mini-batches of training data, then selects the best candidates using a Pareto frontier rather than simple averaging. The Pareto approach maintains diversity -- it picks the best candidate &lt;em&gt;per task&lt;/em&gt; before merging into a final prompt.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img alt="Slide titled &amp;quot;How does GEPA work?&amp;quot; showing the algorithm's three-phase cycle alongside a diagram of the Pareto frontier selection process, where a scores matrix of candidates versus tasks identifies the best candidate per task to build a filtered pool" src="images/gepa-pareto-frontier.jpg"&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Validation&lt;/strong&gt; -- Evaluate the optimized judge on a held-out set to check for generalization.&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote&gt;
&lt;p&gt;"Although we went very quickly through the first step and the second step, these are actually the hardest part of the problem. Like in reality, as every data scientist knows, getting your data is the hardest thing."&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id="the-counterintuitive-seed-prompt"&gt;The Counterintuitive Seed Prompt&lt;/h2&gt;
&lt;p&gt;One of the more surprising findings Mahmoud shared: the seed prompt that &lt;em&gt;excluded&lt;/em&gt; the agent's policy outperformed the one that included it. His hypothesis is that including the full policy from the start traps the optimizer in a local minimum. Starting naive -- "if I don't have any rules, it should say everything is all right" -- with good annotations lets the algorithm explore the prompt space more effectively.&lt;/p&gt;
&lt;p&gt;This inverts the common intuition that more information in the starting prompt is always better. In optimization terms, a good starting point isn't necessarily one that's close to the answer -- it's one that gives the optimizer room to move.&lt;/p&gt;
&lt;h2 id="results-on-a-real-benchmark"&gt;Results on a Real Benchmark&lt;/h2&gt;
&lt;p&gt;Mabrouk tested this on TauBench, a benchmark by Sierra containing 599 conversation traces from a simulated airline support agent. He split the data into 480 training and 112 validation traces, with roughly 62% compliant and 38% non-compliant conversations.&lt;/p&gt;
&lt;p&gt;&lt;img alt="Slide titled &amp;quot;Best Experiment: Grok Judge + Gemini Reflection&amp;quot; showing a results table: validation accuracy improved from 62.5% to 76.8% (+14.3 points), training accuracy from 62.3% to 71.5% (+9.2 points), and Pareto frontier accuracy reached 100%" src="images/optimization-results-table.jpg"&gt;&lt;/p&gt;
&lt;p&gt;The naive seed judge scored 61% accuracy on validation, according to Mabrouk, with a 98% bias toward predicting "compliant" -- meaning it was essentially rubber-stamping everything. After GEPA optimization, he reports accuracy rose to 74% on the validation set, with the compliant prediction rate dropping to 64%, much closer to the actual distribution.&lt;/p&gt;
&lt;p&gt;The optimized rubric had learned parts of the airline policy on its own -- cancellation rules, flight modification procedures, communication requirements -- all discovered through the optimization process rather than hand-written.&lt;/p&gt;
&lt;h2 id="practical-lessons-from-the-optimization-loop"&gt;Practical Lessons from the Optimization Loop&lt;/h2&gt;
&lt;p&gt;Mahmoud shared several hard-won lessons from iterating on this approach:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Smaller or older models failed as both judge and reflector for this complexity level. His best results came from mixing models -- Gemini for reflection, Grok for the judge -- though GPT-4.1 Mini for both also worked.&lt;/li&gt;
&lt;li&gt;He wrote a custom reflection template rather than using the default, embedding domain-specific priors about how to discover policy rules.&lt;/li&gt;
&lt;li&gt;Start with small iterations, visualize the generated candidates and reasoning, and overfit to training data first before scaling up.&lt;/li&gt;
&lt;li&gt;The Pareto frontier reached 100% on training data -- meaning for every training task, &lt;em&gt;some&lt;/em&gt; candidate prompt solved it -- but consolidating all that knowledge into a single merged prompt remained the bottleneck.&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;"It's not an algorithm that you just take and it works from day one, unless for kind of toy examples."&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id="treat-it-like-ml-not-prompt-engineering"&gt;Treat It Like ML, Not Prompt Engineering&lt;/h2&gt;
&lt;p&gt;Mabrouk's takeaway is straightforward: stop treating LLM evaluation as a prompt writing exercise and start treating it as a machine learning problem. Get labeled data from domain experts, optimize your evaluator systematically, and validate on held-out examples. According to his experiments, the optimization runs cost a few hundred dollars in tokens and take about an hour -- a modest investment for evaluators you can actually trust.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;Mahmoud Mabrouk spoke at AI Engineer Europe 2026. Co-founder and CEO at &lt;a href="https://agenta.ai"&gt;Agenta AI&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;&lt;a href="https://www.youtube.com/watch?v=X4dEHRzBLmc"&gt;Watch the full talk&lt;/a&gt; | &lt;a href="https://github.com/agenta-ai/agenta"&gt;Agenta on GitHub&lt;/a&gt; | &lt;a href="https://www.linkedin.com/in/mmabrouk2/"&gt;LinkedIn&lt;/a&gt; | &lt;a href="https://x.com/mmabrouk_"&gt;X&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;</content><category term="Conferences"/><category term="AI_Engineer_Europe"/><category term="LLMs"/><category term="ai_engineering"/><category term="benchmarking"/></entry><entry><title>When Every Team Builds Its Own AI Agent, You Need a Registry</title><link href="https://gallon.me/one-registry-to-rule-them-all-sonny-merla-mauro-luchetti-mattia-redaelli-quantyca.html" rel="alternate"/><published>2026-04-12T00:00:00-05:00</published><updated>2026-04-12T00:00:00-05:00</updated><author><name>Corey</name></author><id>tag:gallon.me,2026-04-12:/one-registry-to-rule-them-all-sonny-merla-mauro-luchetti-mattia-redaelli-quantyca.html</id><summary type="html">&lt;p&gt;Sonny Merla, Mauro Luchetti, and Mattia Redaelli (&lt;a href="https://www.quantyca.it/"&gt;Quantyca&lt;/a&gt;) opened with a question that any large organization experimenting with AI agents will recognize: what happens when dozens of teams across multiple continents are all building agents independently?&lt;/p&gt;</summary><content type="html">&lt;p&gt;Sonny Merla, Mauro Luchetti, and Mattia Redaelli (&lt;a href="https://www.quantyca.it/"&gt;Quantyca&lt;/a&gt;) opened with a question that any large organization experimenting with AI agents will recognize: what happens when dozens of teams across multiple continents are all building agents independently?&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;"What happens when you have dozens of teams across three continents, all building AI agents, each one wiring up their own connections, reinventing their own security model, deploying their own infrastructures? You get chaos."&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Their answer, built for a global enterprise operating across 26 countries, is a registry-based architecture that makes governance a side effect of deployment rather than a gate before it.&lt;/p&gt;
&lt;h2 id="the-problem-with-letting-teams-solve-it-themselves"&gt;The Problem With Letting Teams Solve It Themselves&lt;/h2&gt;
&lt;p&gt;Sonny described the familiar pattern: teams build what they need, wire up their own security, deploy their own way. Multiply that across an enterprise and you get no central visibility into what AI tooling exists, no standardized deployment process, and no way to trace which business use cases depend on which models and services.&lt;/p&gt;
&lt;p&gt;The challenges, as he framed them, fall into three buckets: maintenance and operations (keeping everything running), governance and compliance (knowing what's deployed and who owns it), and enterprise scaling (making it work across the whole organization without slowing anyone down).&lt;/p&gt;
&lt;p&gt;&lt;img alt="Slide titled &amp;quot;The problems&amp;quot; showing three problem categories -- Maintenance &amp;amp; Operations, Governance &amp;amp; Compliance, and Enterprise scaling -- with specific issues like custom integration sprawl, inconsistent security patterns, zero shared discoverability, and no AI estate overview, mapped to required capabilities like secure LLM access, tool exposure, agent-to-agent communication, and cost tracking" src="images/problems-three-categories.jpg"&gt;&lt;/p&gt;
&lt;h2 id="governance-as-a-byproduct-of-shipping"&gt;Governance as a Byproduct of Shipping&lt;/h2&gt;
&lt;p&gt;The core architectural idea is straightforward. Rather than creating an approval process that sits between developers and deployment, they built a system where deploying an MCP server or an A2A agent automatically publishes its metadata to a central registry through CI/CD.&lt;/p&gt;
&lt;p&gt;Mauro described it as making agent development "self-documenting." Tag a branch, and a GitHub Action publishes both the Docker image and the metadata -- an agent card for A2A agents, a server.json for MCP servers -- to the registry catalog.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;"We want to make easy the life of developers to focus on the business logic inside the use cases, avoiding to reinvent the wheel every time we need to take care about the security, but also the deployment and maintenance of the use cases."&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The result is that governance doesn't require extra work from developers. It happens because they shipped.&lt;/p&gt;
&lt;h2 id="three-registries-one-graph"&gt;Three Registries, One Graph&lt;/h2&gt;
&lt;p&gt;The architecture centers on three interconnected registries.&lt;/p&gt;
&lt;p&gt;&lt;img alt="Slide titled &amp;quot;Platform Overview&amp;quot; showing the three registries -- MCP Registry, A2A Registry, and Use Case Registry -- with the MCP Registry containing both approved public servers and custom internal servers, enriched with enterprise metadata fields: ownership, environment, auth model, cost attribution, and use case linkage" src="images/three-registries-metadata-detail.jpg"&gt;&lt;/p&gt;
&lt;p&gt;The &lt;strong&gt;MCP Registry&lt;/strong&gt; is a private extension of the open-source MCP registry specification. It contains both custom internal servers and a curated subset of approved public ones. Each entry carries enterprise metadata: ownership, environment, authentication model, and cost attribution.&lt;/p&gt;
&lt;p&gt;The &lt;strong&gt;A2A Registry&lt;/strong&gt; is based on agent cards from the Agent-to-Agent protocol. When an agent deploys, its card is automatically published. Other agents and developers can discover it immediately.&lt;/p&gt;
&lt;p&gt;The &lt;strong&gt;Use Case Registry&lt;/strong&gt; ties the other two to business context. It maps agents and tools to specific use cases and tracks which AI models each depends on.&lt;/p&gt;
&lt;p&gt;Mauro argued this metadata isn't optional decoration:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;"These are not simply metadata that are nice to have. This is something that really brings out the impact analysis functionality."&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The registries together enable lineage analysis -- tracing the full dependency graph from a use case down through agents, MCP servers, and models. If a model goes down, you can see what breaks.&lt;/p&gt;
&lt;p&gt;&lt;img alt="The AmplifAI platform's lineage view showing a use case called &amp;quot;Ticket Optimization with AI&amp;quot; connected to agents (Echo Agent), an MCP server (a2a-registry), and AI models (Gemini 2.5 Flash, Gemini 2.5 Flash Lite), with lines tracing the dependency graph between them" src="images/lineage-graph-use-case.jpg"&gt;&lt;/p&gt;
&lt;h2 id="the-platform-layer"&gt;The Platform Layer&lt;/h2&gt;
&lt;p&gt;Sitting above the registries is a unified AI gateway that handles authentication, per-use-case budgeting, and central auditing of all LLM requests. Sonny described monthly and weekly cost caps that erode as tokens are consumed, giving each use case its own budget envelope.&lt;/p&gt;
&lt;p&gt;&lt;img alt="Slide titled &amp;quot;Platform Overview&amp;quot; showing the AI Gateway layer with four pillars -- Unified Access, Security, Budgeting, and Control -- sitting above the three registries (MCP Registry, A2A Registry, Use Case Registry)" src="images/platform-overview-gateway-registries.jpg"&gt;&lt;/p&gt;
&lt;p&gt;Below the registries, two template repositories -- one for MCP servers, one for A2A agents -- give developers a starting point with authentication, cost tracking, and observability pre-configured. Mattia noted the A2A template is framework-agnostic, using interfaces so teams can implement with whatever agent framework they prefer.&lt;/p&gt;
&lt;p&gt;&lt;img alt="Slide titled &amp;quot;Enterprise Development Cycle&amp;quot; showing the two template repositories (MCP Server Blueprint and A2A Server Blueprint) with pre-configured features: Dockerfiles, uv Package Manager, FastAPI Server, Auth via Entra ID, Cost Tracking, and Langfuse Integration for observability" src="images/template-repositories-features.jpg"&gt;&lt;/p&gt;
&lt;p&gt;The runtime discovery flow works through an API gateway with proxies that look up backend URLs from the registry catalog. Agents authenticate via a separate header to the actual backend.&lt;/p&gt;
&lt;p&gt;&lt;img alt="Slide titled &amp;quot;Agentic Gateway&amp;quot; showing the CI/CD and runtime architecture: GitHub Actions publish Docker images to an Artifact Registry and metadata to the AI Registries backend proxy, while AI agents at runtime route through an Apigee AI Gateway with MCP and A2A proxies that retrieve backend URLs from the catalog and authenticate via Entra ID" src="images/agentic-gateway-cicd-flow.jpg"&gt;&lt;/p&gt;
&lt;h2 id="where-this-stands"&gt;Where This Stands&lt;/h2&gt;
&lt;p&gt;The speakers were transparent that the platform is not yet in production. Mauro noted it is "still in progress," and the demo used sample data. The architectural patterns -- self-registering services, metadata-rich catalogs, lineage graphs -- are well-established in data engineering. The contribution here is applying them specifically to the MCP and A2A ecosystem.&lt;/p&gt;
&lt;p&gt;The bet is that as internal MCP servers and A2A agents multiply, the discovery and governance problem will hit every large organization. Building the registry into the deployment pipeline, rather than bolting it on after, is how they propose to keep it from becoming another compliance burden that developers route around.&lt;/p&gt;
&lt;h2 id="visibility-first-control-second"&gt;Visibility First, Control Second&lt;/h2&gt;
&lt;p&gt;Sonny, Mauro, and Mattia are making a case that the scaling problem for enterprise AI agents isn't technical capability -- it's visibility. If you can't see what's deployed, who owns it, and what depends on what, you can't govern it. And if governance requires extra work, developers won't do it. The registry approach makes the right thing the default thing.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;Sonny Merla, Mauro Luchetti, and Mattia Redaelli spoke at AI Engineer Europe 2026. Merla is Global Data Science and AI Manager at Amplifon; Luchetti is AI Center of Excellence Manager and Redaelli is AI Engineer at &lt;a href="https://www.quantyca.it/"&gt;Quantyca&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;&lt;a href="https://www.youtube.com/watch?v=VXfRt_H-V08"&gt;Watch the full talk&lt;/a&gt; | &lt;a href="https://quantyca-my.sharepoint.com/:b:/g/personal/mauro_luchetti_quantyca_it/IQBUCcMBzsAfSZtJXrCdaqV0AaUyDhifxP360fqCUupyaGc?e=S6ytoA"&gt;Slides&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;</content><category term="Conferences"/><category term="agents"/><category term="mcp"/><category term="architecture"/><category term="AI_Engineer_Europe"/></entry><entry><title>RAG Isn't Dead, You Just Need a Better Starting Point</title><link href="https://gallon.me/openrag-an-open-source-stack-for-rag-phil-nash.html" rel="alternate"/><published>2026-04-12T00:00:00-05:00</published><updated>2026-04-12T00:00:00-05:00</updated><author><name>Corey</name></author><id>tag:gallon.me,2026-04-12:/openrag-an-open-source-stack-for-rag-phil-nash.html</id><summary type="html">&lt;p&gt;Phil Nash (&lt;a href="https://x.com/philnash"&gt;X&lt;/a&gt;, &lt;a href="https://linkedin.com/in/philnash"&gt;LinkedIn&lt;/a&gt;), a developer relations engineer at IBM, opened his AI Engineer Europe talk by taking aim at the "RAG is dead" discourse. His counter is simple: if every business had less than a million tokens of data, maybe. But they don't, and not everyone wants to pay …&lt;/p&gt;</summary><content type="html">&lt;p&gt;Phil Nash (&lt;a href="https://x.com/philnash"&gt;X&lt;/a&gt;, &lt;a href="https://linkedin.com/in/philnash"&gt;LinkedIn&lt;/a&gt;), a developer relations engineer at IBM, opened his AI Engineer Europe talk by taking aim at the "RAG is dead" discourse. His counter is simple: if every business had less than a million tokens of data, maybe. But they don't, and not everyone wants to pay for a million input tokens on every query. RAG isn't going anywhere -- it's just harder than people think.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;"It turns out that RAG is actually hard, and it's hard for different reasons for different projects."&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id="the-problem-isnt-the-loop"&gt;The Problem Isn't the Loop&lt;/h2&gt;
&lt;p&gt;Phil argues that the basic retrieve-and-generate pattern isn't where teams struggle. The difficulty is everything around it: parsing messy documents (especially PDFs), choosing embedding models, tuning chunking strategies, and adapting search to specific data and user patterns. Every organization's documents and users are different, so there's no universal solution.&lt;/p&gt;
&lt;p&gt;His reframe: what's missing isn't a better algorithm but a better starting point. An opinionated-but-flexible baseline built from open-source components -- sensible defaults with every layer exposed for customization. He presented a stack combining three open-source projects (Docling for document processing, OpenSearch for search, and Langflow for orchestration) as one such baseline, currently at version 0.4.0.&lt;/p&gt;
&lt;p&gt;&lt;img alt="OpenRAG logo with the logos of its three open-source components: the Docling duck mascot, the OpenSearch logo, and the Langflow logo" src="images/openrag-stack-overview.jpg"&gt;&lt;/p&gt;
&lt;h2 id="parsing-is-the-first-hard-problem"&gt;Parsing Is the First Hard Problem&lt;/h2&gt;
&lt;p&gt;The document processing layer uses Docling, an open-source library from IBM Research Zurich. Phil walks through why parsing matters: RAG applications ingest HTML, Markdown, Word docs, slides, spreadsheets, audio, video, and PDFs. Each format has different challenges.&lt;/p&gt;
&lt;p&gt;Docling runs multiple processing pipelines depending on file type:&lt;/p&gt;
&lt;p&gt;&lt;img alt="Docling's pipeline architecture diagram showing multiple processing paths: a simple pipeline for text documents, an ASR pipeline for audio and video, and two PDF pipelines (standard multi-model and VLM), all converging into a DocTags intermediate representation that feeds into chunking and export" src="images/docling-pipeline-architecture.jpg"&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Simple pipeline&lt;/strong&gt; -- text extraction and hierarchy for straightforward formats like Markdown and HTML&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;ASR pipeline&lt;/strong&gt; -- automatic speech recognition for audio and video&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Standard PDF pipeline&lt;/strong&gt; -- a collection of small focused models for layout analysis, table extraction, and image extraction, with optional OCR for scanned documents&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;VLM pipeline&lt;/strong&gt; -- a 258-million-parameter vision model (Granite Docling) that extracts everything in a single pass&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;All of these produce an intermediate representation -- an XML-like format called DocTags -- that can be converted to Markdown, HTML, or JSON. The chunker then uses the document's parsed structure rather than arbitrary character counts, which Nash presents as a meaningful improvement over naive chunking strategies. The whole thing runs offline, which matters for air-gapped environments.&lt;/p&gt;
&lt;h2 id="search-beyond-vectors"&gt;Search Beyond Vectors&lt;/h2&gt;
&lt;p&gt;The retrieval layer uses OpenSearch, the open-source Elasticsearch fork. Nash's point here is that vector search alone isn't enough -- hybrid search combining vectors and keyword matching gives better results out of the box.&lt;/p&gt;
&lt;p&gt;A few features he highlights:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Multiple embedding models simultaneously&lt;/strong&gt; -- useful when migrating between embedding models, though he notes it slows search down&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Configurable filtering and aggregation&lt;/strong&gt; -- exposed to end users as "knowledge filters" so they can scope queries to specific document sets&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;JVector&lt;/strong&gt; -- a disk-ANN-based vector index plugin that replaces the default HNSW/IVF options. The key property: indexes don't need to fit entirely in memory, and it supports live indexing&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="making-retrieval-agentic"&gt;Making Retrieval Agentic&lt;/h2&gt;
&lt;p&gt;The most interesting architectural choice Phil describes is replacing the traditional retrieval pipeline with an agent. Instead of the standard embed-query, top-K, stuff-into-prompt flow, the user's query goes to an agent equipped with search tools. The agent decides what searches to run and how many.&lt;/p&gt;
&lt;p&gt;&lt;img alt="Slide titled &amp;quot;Langflow — Agentic retrieval&amp;quot; showing the full agent flow in the Langflow visual editor, with embedding models on the left feeding into an OpenSearch multi-model search node, connected to an agent node with tools including a calculator and MCP flow, a prompt template, and a chat output" src="images/langflow-agentic-retrieval-flow.jpg"&gt;&lt;/p&gt;
&lt;p&gt;In his demo, the agent has access to an OpenSearch search tool, a calculator, and an MCP-based URL ingester. He demos the generation side running Granite 4 3B locally through Ollama, with Qwen3 Embedding 0.6B handling the embeddings -- also local.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;"They're language models, not math models. So a calculator is always useful."&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The orchestration layer (Langflow) provides a drag-and-drop visual editor for these flows. Nash demos adding guardrails to the agent pipeline by dragging in components -- the point being that customization doesn't require rewriting code.&lt;/p&gt;
&lt;p&gt;&lt;img alt="Langflow editor zoomed into the agent node, showing the agent configuration panel with model selection (granite4-3b), agent instructions, and connected tools including an MCP URL ingester, calculator, and OpenSearch search" src="images/langflow-agent-node-detail.jpg"&gt;&lt;/p&gt;
&lt;h2 id="everything-is-swappable"&gt;Everything Is Swappable&lt;/h2&gt;
&lt;p&gt;Phil is explicit that the stack isn't prescriptive about model providers. It supports OpenAI, Anthropic, WatsonX AI, and Ollama for both embedding and generation. The demo uses fully local models, but you can swap in hosted APIs without changing the pipeline.&lt;/p&gt;
&lt;p&gt;&lt;img alt="OpenRAG settings page showing cloud connectors (Google Drive, SharePoint, OneDrive), model providers (OpenAI, Ollama, IBM watsonx.ai, Anthropic), and agent configuration with granite4-3b selected as the language model" src="images/openrag-settings-model-providers.jpg"&gt;&lt;/p&gt;
&lt;p&gt;The project also includes cloud connectors for Google Drive, SharePoint, and OneDrive for document syncing, a chat UI with suggested follow-up prompts, and API keys for using the search and agent capabilities in external applications. There's also an MCP server available for handing the stack off to other agents.&lt;/p&gt;
&lt;h2 id="the-takeaway"&gt;The Takeaway&lt;/h2&gt;
&lt;p&gt;Nash's closing argument is that whether RAG is "solved" depends entirely on your data and your users. The answer isn't a single algorithm -- it's a stack that gives you a strong baseline and lets you tune every layer against your own evaluation criteria.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;"Well, that's kind of up to you, to your data and to your users."&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;Phil Nash spoke at AI Engineer Europe 2026. Developer relations engineer at &lt;a href="https://www.ibm.com/us-en"&gt;IBM&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;&lt;a href="https://www.youtube.com/watch?v=4TxOBhDRRCM"&gt;Watch the full talk&lt;/a&gt; | &lt;a href="https://github.com/langflow-ai/openrag"&gt;OpenRAG on GitHub&lt;/a&gt; | &lt;a href="https://github.com/docling-project/docling"&gt;Docling&lt;/a&gt; | &lt;a href="https://philna.sh"&gt;philna.sh&lt;/a&gt; | &lt;a href="https://linkedin.com/in/philnash"&gt;LinkedIn&lt;/a&gt; | &lt;a href="https://x.com/philnash"&gt;X&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;</content><category term="Conferences"/><category term="RAG"/><category term="AI_Engineer_Europe"/><category term="open_source"/><category term="retrieval"/><category term="agents"/></entry><entry><title>AI Agents Can't Walk Upstairs and Ask for Help</title><link href="https://gallon.me/platforms-for-humans-and-machines-engineering-for-the-age-of-agents-juan-herreros-elorza.html" rel="alternate"/><published>2026-04-12T00:00:00-05:00</published><updated>2026-04-12T00:00:00-05:00</updated><author><name>Corey</name></author><id>tag:gallon.me,2026-04-12:/platforms-for-humans-and-machines-engineering-for-the-age-of-agents-juan-herreros-elorza.html</id><summary type="html">&lt;p&gt;Juan Herreros Elorza (&lt;a href="https://linkedin.com/in/juan-herreros-elorza"&gt;LinkedIn&lt;/a&gt;, &lt;a href="https://github.com/jherreros"&gt;GitHub&lt;/a&gt;), Team Lead on the Cloud Native Technology team at Banking Circle, makes a deceptively simple argument: the platform engineering practices that have always been "best practices" are now prerequisites. Not because they've changed, but because AI coding agents have become first-class users of internal developer …&lt;/p&gt;</summary><content type="html">&lt;p&gt;Juan Herreros Elorza (&lt;a href="https://linkedin.com/in/juan-herreros-elorza"&gt;LinkedIn&lt;/a&gt;, &lt;a href="https://github.com/jherreros"&gt;GitHub&lt;/a&gt;), Team Lead on the Cloud Native Technology team at Banking Circle, makes a deceptively simple argument: the platform engineering practices that have always been "best practices" are now prerequisites. Not because they've changed, but because AI coding agents have become first-class users of internal developer platforms -- and agents can't compensate for the gaps that humans have been working around for years.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;"If this situation was tricky for a developer, this situation is essentially impossible for a machine, because the machine is not going to go and try the pipeline and then go up to the second floor and talk to the person in that other team."&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id="the-new-developer-who-cant-improvise"&gt;The New Developer Who Can't Improvise&lt;/h2&gt;
&lt;p&gt;Juan opens with a story anyone in a large engineering org will recognize. A new developer joins, writes their application, and hits the deployment wall. They copy a CI pipeline from a teammate. They chase down someone on the infrastructure team for a database. They wait days. Eventually, through Slack messages, hallway conversations, and borrowed tribal knowledge, they get their service running.&lt;/p&gt;
&lt;p&gt;&lt;img alt="Slide illustration split in two: on the left, a developer celebrates at their desk after writing code; on the right, the same developer sits frustrated, thinking about cloud infrastructure, deployment pipelines, and provisioning" src="images/developer-vs-deployment-wall.jpg"&gt;&lt;/p&gt;
&lt;p&gt;Humans muddle through this. Agents cannot. An agent can't wander over to the infrastructure team's desk. It can't read the room to figure out which Slack channel has the person who knows the answer. Every place where your platform relies on implicit knowledge or human intervention is a place where an agent hits a dead end.&lt;/p&gt;
&lt;p&gt;&lt;img alt="The same split illustration, now with robots instead of humans: a happy robot celebrates writing code on the left, while a frustrated robot on the right faces the same cloud, infrastructure, and deployment thought bubbles" src="images/agent-hits-same-wall.jpg"&gt;&lt;/p&gt;
&lt;p&gt;He frames this not as doom but as opportunity. Organizations that have struggled to get buy-in for platform improvements now have executive attention on AI. That attention can fund the work that platform teams have been advocating for all along.&lt;/p&gt;
&lt;h2 id="six-principles-for-agent-ready-platforms"&gt;Six Principles for Agent-Ready Platforms&lt;/h2&gt;
&lt;p&gt;Drawing from his experience building an internal developer platform at a fintech, Herreros Elorza lays out six principles. None of them are new -- that's his point.&lt;/p&gt;
&lt;p&gt;&lt;img alt="Summary slide listing all six principles: 1. Self-service, 2. API-based, 3. Local-first, 4. Documentation, 5. Enable contributions, 6. Measure" src="images/six-principles-summary.jpg"&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;1. Self-service.&lt;/strong&gt; Remove humans from all provisioning and deployment paths. If a developer or agent needs a resource, they should get it without filing a request or waiting on another team. Juan is specific about what doesn't count:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;"If it is technically self-service, but it requires fetching some building blocks from five different places and putting them together and then triggering a flow somewhere else, then it's not really self-service."&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;2. API-based interfaces.&lt;/strong&gt; Everything exposed through well-defined APIs with schema validation. Agents are good at calling structured APIs and discovering what's available. CLIs, MCP servers, or other wrappers on top are fine, but the API is the foundation. Schema validation means agents naturally construct valid requests.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;3. Local-first.&lt;/strong&gt; Since agents typically run on the developer's machine, make it possible to validate everything locally. Don't force an agent to push to version control and wait for a remote CI pipeline to fail minutes later. Local validation means tight iteration loops.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;4. Documentation.&lt;/strong&gt; Two strategies depending on scale -- docs next to the code for smaller repos, centralized docs for platform-wide concerns. Serve documentation via API so agents get structured content rather than parsing HTML. Use agent-specific files like &lt;code&gt;agents.md&lt;/code&gt; or &lt;code&gt;claude.md&lt;/code&gt; to describe build, test, deploy, and verification conventions.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;5. Encourage contributions.&lt;/strong&gt; Agents lower the barrier to contributing to platform code, so platform teams should expect more pull requests from product teams. But -- and Herreros Elorza emphasizes this -- the platform team still owns maintenance. Combine hard guardrails (security policies, compliance checks) with soft guidance (instruction files that steer agent behavior toward good patterns).&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;6. Measure outcomes.&lt;/strong&gt; Use metrics to verify that platform changes actually helped. He references DORA metrics for delivery performance, reliability metrics for operational health, and platform-specific metrics like support request volume as a proxy for self-service effectiveness.&lt;/p&gt;
&lt;h2 id="rethinking-observability-for-agents"&gt;Rethinking Observability for Agents&lt;/h2&gt;
&lt;p&gt;One point Juan singles out deserves its own attention. Humans verify deployments by looking at dashboards -- graphs, charts, color-coded status panels. Agents can't do that.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;"You also need to think: how does observability look like if the prime user is going to be an AI agent?"&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Logs, metrics, and traces need to be available through APIs, CLIs, or MCP servers so agents can programmatically verify that their work succeeded. This is a subtle but important shift: observability systems were built for human visual consumption. Making them machine-readable is a prerequisite for agents that can autonomously deploy, verify, and iterate.&lt;/p&gt;
&lt;h2 id="use-the-momentum"&gt;Use the Momentum&lt;/h2&gt;
&lt;p&gt;Juan's closing point is practical. AI agents don't require a new set of platform engineering principles -- they make the existing ones non-negotiable. And right now, there's organizational willpower to fund the work.&lt;/p&gt;
&lt;p&gt;&lt;img alt="Slide showing a cartoon Trojan horse labeled &amp;quot;Best practices&amp;quot; being wheeled through a castle gate -- illustrating how AI hype can be used to smuggle in long-overdue platform improvements" src="images/trojan-horse-best-practices.jpg"&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;"Take advantage. Everyone from the executive level to the individual contributors are looking at AI now. It is a very hot topic. So you can use AI as the excuse to implement some best practices that, again, were always best practices if you didn't have the chance to do it until now."&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;Juan Herreros Elorza spoke at AI Engineer Europe 2026. Team Lead, Cloud Native Technology at Banking Circle.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;&lt;a href="https://www.youtube.com/watch?v=cCRO3ChaYhM"&gt;Watch the full talk&lt;/a&gt; | &lt;a href="https://speakerdeck.com/jherreros/platforms-for-humans-and-machines-engineering-for-the-age-of-agents"&gt;Slides&lt;/a&gt; | &lt;a href="https://linkedin.com/in/juan-herreros-elorza"&gt;LinkedIn&lt;/a&gt; | &lt;a href="https://github.com/jherreros"&gt;GitHub&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;</content><category term="Conferences"/><category term="ai_engineering"/><category term="agents"/><category term="mcp"/><category term="AI_Engineer_Europe"/></entry><entry><title>Fitting the Model Isn't the Same as Running It Well</title><link href="https://gallon.me/running-llms-locally-practical-llm-performance-on-dgx-spark-mozhgan-kabiri-chimeh-nvidia.html" rel="alternate"/><published>2026-04-12T00:00:00-05:00</published><updated>2026-04-12T00:00:00-05:00</updated><author><name>Corey</name></author><id>tag:gallon.me,2026-04-12:/running-llms-locally-practical-llm-performance-on-dgx-spark-mozhgan-kabiri-chimeh-nvidia.html</id><summary type="html">&lt;p&gt;Mozhgan Kabiri Chimeh (&lt;a href="https://www.linkedin.com/in/mozhgankch/"&gt;LinkedIn&lt;/a&gt;), a developer relations manager at NVIDIA, opened her AI Engineer Europe talk with the pain point that drives most AI developers to the cloud: you either run out of memory or you don't have the right software stack. The result is that development iteration speed depends …&lt;/p&gt;</summary><content type="html">&lt;p&gt;Mozhgan Kabiri Chimeh (&lt;a href="https://www.linkedin.com/in/mozhgankch/"&gt;LinkedIn&lt;/a&gt;), a developer relations manager at NVIDIA, opened her AI Engineer Europe talk with the pain point that drives most AI developers to the cloud: you either run out of memory or you don't have the right software stack. The result is that development iteration speed depends on shared infrastructure, where your work gets scheduled against everyone else's compute jobs.&lt;/p&gt;
&lt;p&gt;Her talk walks through benchmarking open-source models from 1.5 billion to 14 billion parameters on a local workstation, with a focus on the trade-offs between throughput, latency, and quantization format. It's a data-driven argument for when local inference makes sense -- and what actually determines whether it's viable.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;"This isn't a theoretical talk, it's a data-driven journey through the trade-offs of modern AI infrastructure."&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id="memory-capacity-is-not-memory-bandwidth"&gt;Memory Capacity Is Not Memory Bandwidth&lt;/h2&gt;
&lt;p&gt;The central insight Kabiri Chimeh presents is a distinction that's easy to overlook. A workstation with 128 GB of unified memory can fit models up to roughly 200 billion parameters. But fitting a model into memory is not the same as running it at useful speeds.&lt;/p&gt;
&lt;p&gt;&lt;img alt="The GB10 Grace Blackwell Superchip spec sheet: NVIDIA Blackwell GPU with FP4 support, 20-core Arm CPU, NVLink C2C interface at 5x PCIe bandwidth, and 128GB LPDDR5x coherent unified system memory shared between GPU and CPU" src="images/gb10-grace-blackwell-superchip-specs.jpg"&gt;&lt;/p&gt;
&lt;p&gt;Throughput is governed by how efficiently the system moves data through memory, not just how much it can hold. She argues that this is where most local inference setups fall short -- developers load a model, confirm it runs, and then discover the tokens-per-second rate makes interactive use impractical.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;"Memory capacity is not the same as memory bandwidth."&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id="quantization-as-the-decisive-lever"&gt;Quantization as the Decisive Lever&lt;/h2&gt;
&lt;p&gt;This is where Mozhgan's benchmarks get interesting. She tested the Qwen model family at different sizes and precision formats, and the results show that quantization format choice matters as much as the hardware itself.&lt;/p&gt;
&lt;p&gt;The headline numbers for a 14 billion parameter model:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Base (unquantized):&lt;/strong&gt; 8.40 tokens/second&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;4-bit quantized (NVFP4):&lt;/strong&gt; 20.19 tokens/second&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That's a 2.4x improvement from quantization alone -- on the same hardware, with the same model. For context, she notes that 20 tokens per second exceeds average human reading speed, which puts it in the range of viable interactive use.&lt;/p&gt;
&lt;p&gt;At the smaller end, a 1.5 billion parameter instruct model hit 61.73 tokens per second. The pattern is clear: model size sets the ceiling, but quantization determines whether you're anywhere near it.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;"On Blackwell hardware, the choice of quantization format is just as important as the hardware itself."&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;She describes 4-bit floating point quantization as effectively increasing "intelligence per byte" -- allowing a 14 billion parameter model to feel as responsive as a much smaller one.&lt;/p&gt;
&lt;p&gt;&lt;img alt="Throughput bar chart showing completion tokens per second across six model configurations: 1.5B Instruct at 61.73, 8B FP8 at 23.88, 8B Base at 14.60, 14B FP8 at 14.78, 14B NVFP4 at 20.19, and 14B Base at 8.40 tokens per second, with annotations highlighting the 14B NVFP4 to 14B Base comparison" src="images/throughput-tokens-per-second.jpg"&gt;&lt;/p&gt;
&lt;h2 id="benchmarking-thats-worth-reproducing"&gt;Benchmarking That's Worth Reproducing&lt;/h2&gt;
&lt;p&gt;Mozhgan doesn't just present numbers -- she walks through the methodology in detail, which is arguably the most transferable part of the talk. Her benchmarking harness follows a strict protocol:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Environment isolation via Docker containers&lt;/li&gt;
&lt;li&gt;Three mandatory warm-up runs before any measurement&lt;/li&gt;
&lt;li&gt;Background GPU metrics logging at one-second intervals&lt;/li&gt;
&lt;li&gt;Each run generates a unique directory with timestamp and sanitized model ID&lt;/li&gt;
&lt;li&gt;Full capture of model endpoint response and metrics&lt;/li&gt;
&lt;li&gt;Versioned artifacts containing metadata and benchmark results&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img alt="The benchmarking harness code: the orchestrator script on the left handles environment setup, GPU logging, warm-up runs, and metric capture; the right side shows the versioned output directory structure and an example launch command" src="images/benchmarking-harness-code.jpg"&gt;&lt;/p&gt;
&lt;p&gt;She measures two key metrics: completion tokens per second (raw throughput) and time to first token (TTFT), which captures user-perceived responsiveness. The TTFT measurement uses explicit streaming response handling, timestamping the first chunk from the model server.&lt;/p&gt;
&lt;p&gt;The TTFT results reinforce the quantization story: the 4-bit quantized 14B model is 3.4x faster to first token than the unquantized version.&lt;/p&gt;
&lt;p&gt;&lt;img alt="Time to first token chart showing TTFT p50 in seconds across model sizes: 1.5B Instruct at 0.03s, 8B FP8 at 0.06s, 8B Base at 0.08s, 14B FP8 at 0.09s, 14B NVFP4 at 0.07s, and 14B Base at 0.24s, with an annotation showing the 14B NVFP4 is 3.4x faster than the 14B Base" src="images/time-to-first-token-chart.jpg"&gt;&lt;/p&gt;
&lt;h2 id="when-local-compute-is-the-right-choice"&gt;When Local Compute Is the Right Choice&lt;/h2&gt;
&lt;p&gt;Mozhgan frames local inference not as a replacement for the cloud but as a complement. She identifies three use cases where it makes the most sense:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Steady-state workloads&lt;/strong&gt; -- predictable inference demand that doesn't need elastic scaling&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Privacy-sensitive data&lt;/strong&gt; -- when data governance means nothing leaves the building&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Rapid prototyping&lt;/strong&gt; -- fast iteration cycles without waiting for shared infrastructure&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote&gt;
&lt;p&gt;"The key idea here is not replacing the cloud, but bringing powerful AI development closer to the developer."&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The software stack she demonstrates uses the same serving framework (vLLM) and containerized environment that runs in data center deployments. Her point is that workflows developed locally can move to larger infrastructure without rearchitecting -- the iteration happens close to the developer, and the scaling happens later.&lt;/p&gt;
&lt;h2 id="the-takeaway"&gt;The Takeaway&lt;/h2&gt;
&lt;p&gt;Kabiri Chimeh's argument comes down to a practical framework: if your model fits in memory, the next question isn't whether it runs -- it's how fast. Quantization format is the lever that determines whether local inference is a viable development workflow or a frustrating bottleneck. Match your quantization to your use case, benchmark rigorously, and scale out only when the workload demands it.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;"Run locally, iterate quickly, and when ready, scale to data center or cloud."&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;Mozhgan Kabiri Chimeh spoke at AI Engineer Europe 2026. Developer relations manager at &lt;a href="https://www.nvidia.com/en-us/"&gt;NVIDIA&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;&lt;a href="https://www.youtube.com/watch?v=c5-kx2bwoCk"&gt;Watch the full talk&lt;/a&gt; | &lt;a href="https://build.nvidia.com/spark"&gt;build.nvidia.com/spark&lt;/a&gt; | &lt;a href="https://www.linkedin.com/in/mozhgankch/"&gt;LinkedIn&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;</content><category term="Conferences"/><category term="LLMs"/><category term="AI_Engineer_Europe"/><category term="inference"/><category term="quantization"/><category term="benchmarking"/><category term="local_compute"/></entry><entry><title>The Real Problem Is the Six Minutes After the Call</title><link href="https://gallon.me/voiceops-fying-low-latency-intelligence-extraction-from-messy-audio-streams-dippu-kumar-singh.html" rel="alternate"/><published>2026-04-12T00:00:00-05:00</published><updated>2026-04-12T00:00:00-05:00</updated><author><name>Corey</name></author><id>tag:gallon.me,2026-04-12:/voiceops-fying-low-latency-intelligence-extraction-from-messy-audio-streams-dippu-kumar-singh.html</id><summary type="html">&lt;p&gt;Dippu Kumar Singh (&lt;a href="https://www.linkedin.com/in/dippukumarsingh/"&gt;LinkedIn&lt;/a&gt;), Leader of Emerging Technologies at Fujitsu North America, presents a talk that starts where most AI discussions stop. Most generative AI demos assume clean text input. In a contact center, the data starts as messy, overlapping, emotionally charged audio -- and the engineering challenge isn't transcription. It's …&lt;/p&gt;</summary><content type="html">&lt;p&gt;Dippu Kumar Singh (&lt;a href="https://www.linkedin.com/in/dippukumarsingh/"&gt;LinkedIn&lt;/a&gt;), Leader of Emerging Technologies at Fujitsu North America, presents a talk that starts where most AI discussions stop. Most generative AI demos assume clean text input. In a contact center, the data starts as messy, overlapping, emotionally charged audio -- and the engineering challenge isn't transcription. It's what happens after the call ends.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;"We cannot hire more people, we have to fundamentally engineer the stress out of the workflow."&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id="the-after-call-work-problem"&gt;The After-Call Work Problem&lt;/h2&gt;
&lt;p&gt;Singh's central reframe is about where the ROI actually lives. He shares internal baseline data showing the average contact center call lasts about 6.5 minutes -- and the post-call administrative work takes another 6.3 minutes. Operators spend nearly half their working hours on data entry, not talking to customers.&lt;/p&gt;
&lt;p&gt;That near 1:1 ratio between talk time and paperwork creates a stress-turnover spiral. Operators burn out. Attrition rises. And as Singh argues, hiring more people doesn't fix a structural problem.&lt;/p&gt;
&lt;p&gt;&lt;img alt="Slide showing call time vs. after-call work: 6.6 minutes average call time, 6.3 minutes average ACW time, with 79.2% of centers expecting AI gains. The core mission stated as shifting focus from handling calls to analyzing Voice of Customer." src="images/call-time-vs-after-call-work.jpg"&gt;&lt;/p&gt;
&lt;p&gt;The pipeline he describes isn't designed to replace the operator. It's designed to auto-populate structured output that the operator validates with a quick visual check and a confirm click -- turning a 6.3-minute writing task into a seconds-long review task.&lt;/p&gt;
&lt;h2 id="four-stages-of-the-pipeline"&gt;Four Stages of the Pipeline&lt;/h2&gt;
&lt;p&gt;The system Dippu describes is a four-stage architecture deployed in a high-volume contact center.&lt;/p&gt;
&lt;p&gt;&lt;img alt="Solution component architecture showing four stages: Voice Capture (raw high-fidelity audio), Speech-to-Text Engine (converting speech to accurate text), Generative AI Core (summarization and context reasoning), and Customer Data Sync (automated entry and VOC reporting). System goal: transform raw conversational audio into structured business intelligence with minimal human intervention." src="images/solution-component-architecture.jpg"&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Voice Capture&lt;/strong&gt; handles real-time audio intake with noise filtering and level normalization. The critical detail here is stereo channel separation -- isolating the agent on one channel and the customer on the other.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;"If you mix them into a single mono track, overlapping with each other, the AI will struggle to figure out who said what and thereby ruining the entire downstream summary."&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;PII masking happens at this stage too. Credit card numbers and passwords are stripped from the audio buffer before anything reaches the LLM.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Speech-to-Text&lt;/strong&gt; converts the cleaned audio. Singh says the STT accuracy needs to exceed 90% for the downstream AI to function. Domain-specific dictionaries help -- distinguishing "term life" from "turn right" in an insurance context, for example. Post-processing handles inverse text normalization, so spoken "five thousand dollars" becomes "$5,000" before entity extraction.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Generative AI Core&lt;/strong&gt; is structured in three layers. An orchestration layer uses few-shot prompt libraries rather than open-ended "summarize this call" instructions. A reasoning layer classifies the call against a predefined list of categories (cancellation, new application, claim status) and outputs its reasoning. A trust layer handles token optimization for latency and automated hallucination checks.&lt;/p&gt;
&lt;p&gt;&lt;img alt="The Generative AI Core broken into three layers: Orchestration (guiding the LLM with specific task templates and samples via prompt engine and few-shot library), Reasoning (determining the why behind the call and customer emotion via intent extraction and sentiment score), and Trust Layer (ensuring the summary is factually grounded in the transcript via hallucination check and token optimizer)." src="images/generative-ai-core-three-layers.jpg"&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Customer Data Sync&lt;/strong&gt; maps the LLM's JSON output to CRM fields via an API gateway. The operator sees the AI-generated summary auto-populated on screen, makes any corrections, and confirms.&lt;/p&gt;
&lt;h2 id="why-just-summarize-doesnt-work"&gt;Why "Just Summarize" Doesn't Work&lt;/h2&gt;
&lt;p&gt;One of the more concrete points Dippu makes is about prompt design. He argues that asking an LLM to summarize a call produces a messy narrative paragraph -- not something you can feed into a CRM.&lt;/p&gt;
&lt;p&gt;The alternative: structured few-shot prompts that instruct the LLM to output separate bullet-point lists -- one for customer inquiry, one for operator actions. The model receives a predefined list of call reasons and must classify against them while showing its work.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;"This strict formatting is what turns an unstructured conversation into a database-ready asset."&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img alt="Summarization workflow logic showing four stages: Raw Transcript (time-indexing, confidence scoring, denoising), Speaker Separation (channel splitting, voiceprints, dialogue stitching), Context Deduction (intent recognition, entity spotting, sentiment analysis), and Structured Output (bullet points, JSON schema, template matching)." src="images/summarization-workflow-logic.jpg"&gt;&lt;/p&gt;
&lt;p&gt;This is where the audio quality decisions upstream pay off. Speaker separation means the LLM can distinguish customer intent from operator chit-chat. Without it, the structured extraction falls apart.&lt;/p&gt;
&lt;h2 id="the-results"&gt;The Results&lt;/h2&gt;
&lt;p&gt;Singh reports that after-call work dropped to 3.1 minutes -- roughly a 50% reduction. Across what he describes as a 500-seat operation handling thousands of calls per day, that translates to what he characterizes as the equivalent of reclaiming dozens of full-time headcounts in productivity.&lt;/p&gt;
&lt;p&gt;&lt;img alt="Key outcomes table comparing manual operation to AI-powered results: ACW time dropped from 6.3 to 3.1 minutes (50% reduction), data entry quality moved from variable/subjective to standardized (high uniformity), inquiry categorization shifted from skill-dependent to logic-based (consistent VOC), and staff turnover went from high stress-linked to reduced burden (stabilized ops)." src="images/key-outcomes-table.jpg"&gt;&lt;/p&gt;
&lt;p&gt;Structured data from the pipeline also flows into BI dashboards that aggregate voice-of-customer patterns and auto-flag candidates for new FAQ entries -- a secondary benefit that turns call data into a strategic asset.&lt;/p&gt;
&lt;h2 id="what-comes-next"&gt;What Comes Next&lt;/h2&gt;
&lt;p&gt;Dippu closes with the argument that this kind of pipeline transforms contact centers from high-stress call centers into what he calls "intelligence-gathering engines."&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;"By applying these rigorous engineering techniques to the messy audio data, we can definitely transform the contact centers from call centers of high stress into highly efficient intelligence-gathering engines that protect their workforces."&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;Dippu Kumar Singh spoke at AI Engineer Europe 2026. Leader of Emerging Technologies (Apps) at &lt;a href="https://global.fujitsu/en-us"&gt;Fujitsu North America&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;&lt;a href="https://www.youtube.com/watch?v=IEF842ZEU5A"&gt;Watch the full talk&lt;/a&gt; | &lt;a href="https://docs.google.com/presentation/d/1f2y1s64irhdDNTRgK6bWrBtOgMWlhQYM/edit?usp=sharing&amp;amp;ouid=107532212133041789455&amp;amp;rtpof=true&amp;amp;sd=true"&gt;Slides&lt;/a&gt; | &lt;a href="https://www.linkedin.com/in/dippukumarsingh/"&gt;LinkedIn&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;</content><category term="Conferences"/><category term="AI_Engineer_Europe"/><category term="ai_engineering"/><category term="voice_ai"/><category term="real_time_systems"/><category term="contact_centers"/></entry><entry><title>AI-Generated Code Is Just Untrusted Code From the Internet</title><link href="https://gallon.me/why-and-how-you-need-to-sandbox-ai-generated-code-harshil-agrawal-cloudflare.html" rel="alternate"/><published>2026-04-12T00:00:00-05:00</published><updated>2026-04-12T00:00:00-05:00</updated><author><name>Corey</name></author><id>tag:gallon.me,2026-04-12:/why-and-how-you-need-to-sandbox-ai-generated-code-harshil-agrawal-cloudflare.html</id><summary type="html">&lt;p&gt;Harshil Agrawal (&lt;a href="https://x.com/harshil1712"&gt;X&lt;/a&gt;, &lt;a href="https://linkedin.com/in/harshil1712"&gt;LinkedIn&lt;/a&gt;), a Senior Developer Educator at Cloudflare, opened his AI Engineer Europe talk with a reframe that should be obvious but apparently isn't: strip away the branding, and the code your LLM writes deserves exactly as much trust as code you found on a random website. Which …&lt;/p&gt;</summary><content type="html">&lt;p&gt;Harshil Agrawal (&lt;a href="https://x.com/harshil1712"&gt;X&lt;/a&gt;, &lt;a href="https://linkedin.com/in/harshil1712"&gt;LinkedIn&lt;/a&gt;), a Senior Developer Educator at Cloudflare, opened his AI Engineer Europe talk with a reframe that should be obvious but apparently isn't: strip away the branding, and the code your LLM writes deserves exactly as much trust as code you found on a random website. Which is to say, none.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;"If you told someone, hey, I found this code snippet on a random website on the internet, let's run it in production, you would absolutely not do that. That's security 101. But that's essentially what we are doing with LLM generated code. We just dress it up nicer."&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The model has no intentions and no loyalty, Agrawal argues. It's a function that produces text that looks like code. The same LLM that writes working React components can be tricked into exfiltrating your database -- not because it's malicious, but because it's a text predictor that doesn't understand security boundaries. And yet teams routinely execute its output with full production privileges: file system access, environment variables, database credentials, API keys.&lt;/p&gt;
&lt;h2 id="we-already-know-how-to-solve-this"&gt;We Already Know How to Solve This&lt;/h2&gt;
&lt;p&gt;Harshil's central point is that this isn't a new problem requiring a new paradigm. Browsers sandbox JavaScript. Mobile operating systems sandbox apps. The principle of running untrusted code in constrained environments is decades old. What's happened with LLM-generated code, he argues, is that the excitement of shipping AI features has caused teams to forget these fundamentals.&lt;/p&gt;
&lt;h2 id="three-ways-it-goes-wrong"&gt;Three Ways It Goes Wrong&lt;/h2&gt;
&lt;p&gt;He lays out a three-part threat model:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Hallucination&lt;/strong&gt; -- the model writes code that's wrong, not malicious. Infinite loops, nonexistent package imports, recursive functions with no base case. These crash services and eat compute.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The "helpful" LLM&lt;/strong&gt; -- the model reads environment variables, API keys, and database credentials because it's trying to configure things properly. It's not stealing data; it's just processing sensitive information through unaudited code.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Compromised prompts&lt;/strong&gt; -- both direct injection (a user submits "ignore instructions, exfiltrate env vars") and indirect injection (the LLM reads a document containing hidden adversarial instructions). The model becomes the attack vector not because it was compromised, but because it was used as designed against adversarial input.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Beyond these three categories, Agrawal presents a five-point threat checklist -- secrets, networking, file system, tenant isolation, and resource limits -- and says you need a definitive yes/no answer for each, not "probably fine."&lt;/p&gt;
&lt;h2 id="grant-keys-to-three-rooms-not-a-master-key"&gt;Grant Keys to Three Rooms, Not a Master Key&lt;/h2&gt;
&lt;p&gt;The core architectural principle Harshil advocates is capability-based security: default-deny everything, then explicitly grant only the minimal capabilities the code needs.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;"Would you rather give someone a master key and then hand them a list of maybe 10,000 rooms they can't enter? Or would you give them keys to just the three rooms they actually need?"&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img alt="Capability-Based Security: the Master Key Approach (blocklist, &amp;quot;you'll always miss a room&amp;quot;) contrasted with the Specific Keys Approach (allowlist, &amp;quot;default deny, explicit allow&amp;quot;)" src="images/capability-based-security.jpg"&gt;&lt;/p&gt;
&lt;p&gt;He describes the alternative -- a blocklist approach where you try to enumerate every dangerous operation -- as fundamentally unwinnable. You'll always miss something. With capability-based security, the dangerous operations were never available in the first place. He compares a properly sandboxed environment to "a room with no doors or windows. The only things inside are what I put there before I locked it."&lt;/p&gt;
&lt;h2 id="two-levels-of-isolation"&gt;Two Levels of Isolation&lt;/h2&gt;
&lt;p&gt;&lt;img alt="The Isolation Spectrum: a comparison table of eval, V8 Isolates, and Containers across startup time, isolation level, capabilities, and use cases, with a note highlighting V8 Isolates and Containers as the two practical options for AI code execution" src="images/isolation-spectrum.jpg"&gt;&lt;/p&gt;
&lt;p&gt;Agrawal presents two sandboxing approaches, suited to different use cases:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Lightweight isolates&lt;/strong&gt; (built on browser engine technology) work for code that doesn't need a file system or package manager. They start in roughly a millisecond, support JavaScript, TypeScript, Python, and WebAssembly, and give each execution its own memory and context. The key pattern: block all outbound network requests by default, and expose only specific, restricted method stubs. The sandboxed code can call &lt;code&gt;query&lt;/code&gt; on a database binding, but that binding is a stub routing through the controlling process -- it never has direct database access.&lt;/p&gt;
&lt;p&gt;&lt;img alt="Code showing the core isolate setup: loader.load creates the isolate, passes user code as a module, sets globalOutbound to null to block all network requests, and exposes only restricted database and logger bindings" src="images/isolate-code-setup.jpg"&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;"There's nothing to intercept. The dangerous operations were never available."&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;Containers&lt;/strong&gt; handle heavier workloads -- anything needing a real file system, process model, or package installs. They take seconds to start rather than milliseconds, but support git clone, npm install, running dev servers, and exposing ports. The critical pattern here is the &lt;strong&gt;proxy for secrets&lt;/strong&gt;: never pass API keys into the sandbox as environment variables. Instead, the sandbox calls a proxy endpoint that adds authentication headers and forwards to external services. The secrets never enter the sandbox's address space.&lt;/p&gt;
&lt;p&gt;&lt;img alt="Secrets Management: the BAD pattern (passing API keys as environment variables into the sandbox) contrasted with the GOOD pattern (sandbox calls your worker's API endpoint, worker adds authentication and proxies to upstream)" src="images/secrets-proxy-pattern.jpg"&gt;&lt;/p&gt;
&lt;p&gt;His decision heuristic is straightforward: does the code need a file system, processes, or package installs? If yes, containers. If no, isolates. In practice, he says, most applications will use both -- isolates as the fast path for tool-calling loops, containers as the workbench for building and deploying.&lt;/p&gt;
&lt;p&gt;&lt;img alt="Decision tree for choosing a sandboxing approach: does the code need a file system, processes, or package installs? If yes, use Containers (Sandbox SDK). If no, does it need network access? If yes, use Isolates with an outbound service. If no, use Isolates with globalOutbound set to null." src="images/decision-tree.jpg"&gt;&lt;/p&gt;
&lt;h2 id="the-universal-checklist"&gt;The Universal Checklist&lt;/h2&gt;
&lt;p&gt;Harshil closes with an eight-item checklist he says applies regardless of sandboxing approach:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Default deny network access&lt;/li&gt;
&lt;li&gt;Grant explicit capabilities, not broad access&lt;/li&gt;
&lt;li&gt;Isolate per user -- one user, one sandbox&lt;/li&gt;
&lt;li&gt;Set resource limits (timeouts, memory caps, CPU limits)&lt;/li&gt;
&lt;li&gt;Keep secrets outside the sandbox (proxy pattern)&lt;/li&gt;
&lt;li&gt;Destroy sandboxes when done (try/finally, max lifetimes)&lt;/li&gt;
&lt;li&gt;Log everything (what code ran, when, who triggered it, what it did)&lt;/li&gt;
&lt;li&gt;Validate input before it hits the sandbox (length limits, syntax validation, dangerous pattern detection)&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img alt="The Universal Checklist slide showing all 8 security items, from default deny network access to input validation, with the note: &amp;quot;Print this. Put it next to your monitor. Check it every time.&amp;quot;" src="images/universal-checklist.jpg"&gt;&lt;/p&gt;
&lt;h2 id="old-principles-new-context"&gt;Old Principles, New Context&lt;/h2&gt;
&lt;p&gt;Sandboxing AI-generated code isn't a new security paradigm, Harshil argues -- it's an old one applied to a new context. The tools and principles already exist. The gap is that teams aren't using them because the AI framing makes the risk feel different when it isn't.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;"The cost of an extra sandbox is always less than the cost of a data leak."&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;Harshil Agrawal spoke at AI Engineer Europe 2026. Senior Developer Educator at &lt;a href="https://www.cloudflare.com"&gt;Cloudflare&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;&lt;a href="https://www.youtube.com/watch?v=AHtGAgQ0Q_Q"&gt;Watch the full talk&lt;/a&gt; | &lt;a href="https://harshil.dev/slides/sandbox-ai-engineer"&gt;Slides&lt;/a&gt; | &lt;a href="https://linkedin.com/in/harshil1712"&gt;LinkedIn&lt;/a&gt; | &lt;a href="https://x.com/harshil1712"&gt;X&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;</content><category term="Conferences"/><category term="ai_security"/><category term="sandboxing"/><category term="code_execution"/><category term="AI_Engineer_Europe"/></entry><entry><title>The Security Cliff Between Local and Production MCP</title><link href="https://gallon.me/your-insecure-mcp-server-wont-survive-production-tun-shwe-lenses.html" rel="alternate"/><published>2026-04-12T00:00:00-05:00</published><updated>2026-04-12T00:00:00-05:00</updated><author><name>Corey</name></author><id>tag:gallon.me,2026-04-12:/your-insecure-mcp-server-wont-survive-production-tun-shwe-lenses.html</id><summary type="html">&lt;p&gt;Tun Shwe (&lt;a href="https://www.linkedin.com/in/tunshwe/"&gt;LinkedIn&lt;/a&gt;) and Jeremy Frenay (&lt;a href="https://www.linkedin.com/in/jeremy-frenay/"&gt;LinkedIn&lt;/a&gt;), both AI Engineers at &lt;a href="https://lenses.io/"&gt;Lenses.io&lt;/a&gt;, gave a joint talk at AI Engineer Europe 2026 on what happens when MCP servers leave the safety of a developer's laptop. Their central claim: most MCP servers are built for single-player local development and collapse the …&lt;/p&gt;</summary><content type="html">&lt;p&gt;Tun Shwe (&lt;a href="https://www.linkedin.com/in/tunshwe/"&gt;LinkedIn&lt;/a&gt;) and Jeremy Frenay (&lt;a href="https://www.linkedin.com/in/jeremy-frenay/"&gt;LinkedIn&lt;/a&gt;), both AI Engineers at &lt;a href="https://lenses.io/"&gt;Lenses.io&lt;/a&gt;, gave a joint talk at AI Engineer Europe 2026 on what happens when MCP servers leave the safety of a developer's laptop. Their central claim: most MCP servers are built for single-player local development and collapse the moment you try to run them in production. There's no gradual on-ramp -- you go from zero security surface to needing OAuth, token management, CORS, TLS, and rate limiting all at once.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;"If you get the design wrong, no amount of OAuth will save you."&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id="agents-are-not-humans-with-api-keys"&gt;Agents Are Not Humans with API Keys&lt;/h2&gt;
&lt;p&gt;Shwe frames the problem through three dimensions where agents differ from humans -- a framework he attributes to Jeremiah Lowen, creator of Fast MCP. Each dimension casts what he calls a "security shadow."&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Discovery.&lt;/strong&gt; A human reads API docs once and picks the endpoints they need. An agent enumerates every tool and reads every description on each connection. Every tool description becomes a surface for tool poisoning -- hidden instructions invisible in the UI but followed by the model.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Iteration.&lt;/strong&gt; A human reruns a script in a second. An agent sends the full conversation history with each retry. Each round trip is a data leakage opportunity, including sensitive data from previous tool calls.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Context.&lt;/strong&gt; Humans bring decades of intuition. An agent has a fixed context window. Unfiltered data in that window hands PII and credentials to a model that can be tricked into exfiltrating them.&lt;/p&gt;
&lt;h2 id="five-principles-for-secure-agentic-design"&gt;Five Principles for Secure Agentic Design&lt;/h2&gt;
&lt;p&gt;Shwe's argument is that poor MCP design and poor MCP security are the same problem. He lays out five principles that reduce security exposure before you write a single line of authentication code:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Shrink the attack surface by design.&lt;/strong&gt; Consolidate fine-grained operations into single coarse-grained, outcome-oriented tools. One permission check, one audit log entry, one authorization enforcement point. "Fewer doors, fewer locks to manage."&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Constrain inputs at the schema level.&lt;/strong&gt; Accept top-level primitives and enums. Use validation libraries like Pydantic. Reject free-form nested payloads -- an unconstrained string argument passed to a shell or query engine is a command injection waiting to happen.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Treat documentation as a defensive layer.&lt;/strong&gt; Tool poisoning works by embedding malicious instructions in tool descriptions. Complete, unambiguous documentation crowds out the space a poisoned neighboring server would try to fill.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Return only what the agent needs.&lt;/strong&gt; Oversized responses dump potential PII into a context window that can be tricked into exfiltrating it. Strip payloads to the minimum.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Minimize the blast radius.&lt;/strong&gt; Scope permissions at the tool and resource level, not the session level. Use read-only annotations. Every tool removed is an attack vector eliminated.&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote&gt;
&lt;p&gt;"A badly designed MCP server is also a badly secured one."&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id="the-cliff-from-local-to-remote"&gt;The Cliff from Local to Remote&lt;/h2&gt;
&lt;p&gt;Standard IO mode -- a local process, single user, no network exposure -- works fine for individual developer productivity. But Shwe argues there's no halfway house between that and production.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;"You can't do a little bit of production. You're either behind the wall or you're standing out in the open."&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Moving to production means streamable HTTP transport, remote deployment, multiple clients, horizontal scaling, and centralized governance. The jump is abrupt: OAuth, token management, CORS, TLS, SSRF protection, rate limiting, and audit logging all land at once.&lt;/p&gt;
&lt;p&gt;&lt;img alt="Slide listing the production security concerns that arrive all at once when moving from local STDIO mode: OAuth, token management, CORS, TLS, SSRF protection, rate limiting, and audit logging" src="images/no-gradual-on-ramp-security-cliff.jpg"&gt;&lt;/p&gt;
&lt;p&gt;He cites load tests attributed to StackLock showing that standard IO transport buckled under even modest concurrency -- according to the cited results, 20 out of 22 requests failed with just 20 simultaneous connections.&lt;/p&gt;
&lt;h2 id="oauths-client-identity-problem"&gt;OAuth's Client Identity Problem&lt;/h2&gt;
&lt;p&gt;Frenay takes over for the authentication deep-dive. The first approach most teams reach for -- long-lived API keys -- scales poorly. Keys are rarely rotated, not scoped to specific actions, often shared across systems, and stored in config files. In a remote deployment, the MCP server may simply pass the key through to an upstream API, creating what Frenay describes as a confused deputy vulnerability.&lt;/p&gt;
&lt;p&gt;Dynamic Client Registration (DCR) was the initial answer. A client self-registers against the authorization server and gets a new client ID. But registrations aren't portable across devices, the registration endpoint is vulnerable to phishing, and the server blindly trusts self-asserted client metadata.&lt;/p&gt;
&lt;p&gt;The approach Frenay presents as the current direction is Client ID Metadata Document (CIMD). Instead of self-registering, a client exposes its identity metadata on a public HTTPS URL. The authorization server fetches this metadata during authorization. Identity is proven by domain control rather than self-assertion, redirect URIs are explicitly bound in the metadata document, and the authorization server can selectively allow or deny clients.&lt;/p&gt;
&lt;p&gt;&lt;img alt="Slide showing Client ID Metadata Document overview: the MCP server runs remotely, is protected by an authorization server, the client owner exposes its ID on a public URL, and the server fetches client metadata during authorization. A JSON config snippet shows the simple client setup." src="images/cimd-overview-client-metadata-url.jpg"&gt;&lt;/p&gt;
&lt;h2 id="beyond-authentication"&gt;Beyond Authentication&lt;/h2&gt;
&lt;p&gt;Frenay closes by arguing that OAuth alone isn't enough for enterprise deployment. He lists four additional requirements: role-based access control scoped at the individual tool level, data masking for PII fields before the agent sees them, audit logging that captures which agent called which tool with what parameters and what data came back, and end-to-end observability across the full request lifecycle.&lt;/p&gt;
&lt;p&gt;&lt;img alt="Slide listing the four enterprise requirements beyond OAuth: RBAC at the tool level, data masking, audit logging, and observability and telemetry" src="images/enterprise-requirements-beyond-oauth.jpg"&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;"If you cannot trace what an agent did end to end, you cannot govern it."&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id="the-takeaway"&gt;The Takeaway&lt;/h2&gt;
&lt;p&gt;Shwe and Frenay's core argument is that MCP security isn't a layer you bolt on after building your tools -- it's a consequence of good tool design itself. Get the five design principles right and you've already shrunk your attack surface before touching OAuth. Get them wrong, and no amount of authentication infrastructure will save you.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;Tun Shwe and Jeremy Frenay spoke at AI Engineer Europe 2026. AI Engineers at &lt;a href="https://lenses.io/"&gt;Lenses.io&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;&lt;a href="https://www.youtube.com/watch?v=BurJvbqFr4c"&gt;Watch the full talk&lt;/a&gt; | &lt;a href="https://drive.google.com/file/d/1zLzkVO7_kBoV6bI7lhYIi3AxUH6j7xH_/view?usp=sharing"&gt;Slides&lt;/a&gt; | &lt;a href="https://github.com/lensesio/lenses-mcp"&gt;Lenses MCP Server (GitHub)&lt;/a&gt; | Tun Shwe &lt;a href="https://www.linkedin.com/in/tunshwe/"&gt;LinkedIn&lt;/a&gt; | Jeremy Frenay &lt;a href="https://www.linkedin.com/in/jeremy-frenay/"&gt;LinkedIn&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;</content><category term="Conferences"/><category term="mcp"/><category term="security"/><category term="AI_Engineer_Europe"/><category term="agents"/></entry><entry><title>Your AI Agent Is a Junior Developer. Manage It Like One.</title><link href="https://gallon.me/agentic-engineering-working-with-ai-not-just-using-it-brendan-oleary.html" rel="alternate"/><published>2026-04-11T00:00:00-05:00</published><updated>2026-04-11T00:00:00-05:00</updated><author><name>Corey</name></author><id>tag:gallon.me,2026-04-11:/agentic-engineering-working-with-ai-not-just-using-it-brendan-oleary.html</id><summary type="html">&lt;p&gt;Brendan O'Leary (&lt;a href="https://www.linkedin.com/in/olearycrew/"&gt;LinkedIn&lt;/a&gt;, &lt;a href="https://x.com/olearycrew"&gt;X&lt;/a&gt;), a Developer Relations Engineer at &lt;a href="https://kilo.ai/"&gt;Kilo Code&lt;/a&gt;, opened his AI Engineer Europe talk with a observation that's easy to nod along with and hard to act on: most engineers have used AI tools by now, but almost none of them can articulate &lt;em&gt;how&lt;/em&gt; they actually work …&lt;/p&gt;</summary><content type="html">&lt;p&gt;Brendan O'Leary (&lt;a href="https://www.linkedin.com/in/olearycrew/"&gt;LinkedIn&lt;/a&gt;, &lt;a href="https://x.com/olearycrew"&gt;X&lt;/a&gt;), a Developer Relations Engineer at &lt;a href="https://kilo.ai/"&gt;Kilo Code&lt;/a&gt;, opened his AI Engineer Europe talk with a observation that's easy to nod along with and hard to act on: most engineers have used AI tools by now, but almost none of them can articulate &lt;em&gt;how&lt;/em&gt; they actually work with them. What do they hand off? What do they keep? How do they decide? That gap -- between using AI and working with it -- is where O'Leary spent his 27 minutes.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;"Tools are things that you pick up and put down. You use a hammer. You don't work with a hammer."&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id="the-junior-developer-mental-model"&gt;The Junior Developer Mental Model&lt;/h2&gt;
&lt;p&gt;O'Leary's central framing is blunt and practical:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;"You kind of have to think about your AI agent as an energetic, enthusiastic, extremely well-read, often confidently wrong junior developer."&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;This isn't just a throwaway analogy. He uses it as an actual management framework. The skills that make someone a good engineering manager of junior developers -- giving clear context, scoping work tightly, reviewing output carefully -- are exactly the skills that transfer to working with coding agents. If you've ever handed a vague ticket to an intern and gotten back something technically functional but completely wrong-headed, you already understand the failure mode.&lt;/p&gt;
&lt;p&gt;&lt;img alt="Slide titled &amp;quot;The Paradigm Shift: From autocomplete to teammates&amp;quot; showing a three-stage timeline -- 2020 Autocomplete (finish your line), 2022 Copilots (suggest functions), 2025+ Agents (execute tasks) -- with a stat that 90% of tech workers now use AI at work per the Google DORA Report" src="images/paradigm-shift-timeline.jpg"&gt;&lt;/p&gt;
&lt;h2 id="context-is-the-bottleneck"&gt;Context Is the Bottleneck&lt;/h2&gt;
&lt;p&gt;The bulk of Brendan's technical content centers on what he calls context engineering -- the idea that the quality of an agent's output is bounded by the quality of context you give it. He claims context quality starts to degrade once the context window is roughly half full, and that stale or irrelevant context can actively poison output.&lt;/p&gt;
&lt;p&gt;&lt;img alt="Slide titled &amp;quot;Problem 2: More context makes the model dumber&amp;quot; showing a gradient bar from green (Smart Zone) to red (Dumb Zone) at roughly 50% context utilization, with warnings about the MCP Trap (loading every MCP server puts you in the dumb zone before you've typed a word) and a benchmark recommendation to stay under 50% context utilization" src="images/context-degradation-quality.jpg"&gt;&lt;/p&gt;
&lt;p&gt;His four practices for managing context:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Persist information outside the context window&lt;/strong&gt; -- scratch pads, memory files, and project-level instruction files so the agent can access knowledge without bloating the live session.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Be selective&lt;/strong&gt; -- only pull in what's relevant for the current step. He specifically warns against leaving unnecessary tool integrations enabled, since each one adds tokens to the system prompt on every interaction.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Summarize and compress&lt;/strong&gt; -- after a long debugging session, distill the context down to just the problem and solution before moving to implementation.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Isolate&lt;/strong&gt; -- split work across parallel agents or sessions to prevent context accumulation. He points to the rise of parallel agent workflows over the past several months as a direct response to this problem.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img alt="Slide titled &amp;quot;The solution: four ways to manage context&amp;quot; showing four quadrants -- Write Context (persist information outside the window via scratchpads, memory files, CLAUDE.md), Select Context (pull only what's relevant via RAG, @-mentions, file references), Compress Context (reduce what's in the window via summarization and trimming logs), and Isolate Context (split work across sessions via parallel agents and task separation)" src="images/four-ways-to-manage-context.jpg"&gt;&lt;/p&gt;
&lt;p&gt;The selectivity point lands hard. O'Leary describes a scenario where a database integration tool is left enabled during frontend work -- it wastes tokens and can mislead the agent into touching things it shouldn't. The principle is the same one that applies to human developers: don't leave irrelevant tools and docs scattered across someone's desk and expect focused work.&lt;/p&gt;
&lt;h2 id="research-first-code-last"&gt;Research First, Code Last&lt;/h2&gt;
&lt;p&gt;O'Leary's recommended workflow inverts the assumption that AI's value is in code generation speed. He argues the real leverage comes &lt;em&gt;before&lt;/em&gt; any code gets written.&lt;/p&gt;
&lt;p&gt;He quotes Dex Horthy: "A bad line of research can potentially be hundreds of lines of bad code."&lt;/p&gt;
&lt;p&gt;&lt;img alt="Slide titled &amp;quot;The Classic Mistake&amp;quot; contrasting what most people do (red panel: &amp;quot;Hey AI, implement this feature&amp;quot; leading to wrong assumptions, wasted time, and frustration) versus what works (green panel: Research → Plan → Implement, leading to understanding first, explicit steps, and executing with confidence), with the Dex Horthy quote at the bottom" src="images/classic-mistake-research-vs-code.jpg"&gt;&lt;/p&gt;
&lt;p&gt;The workflow breaks into three phases:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Research:&lt;/strong&gt; Use a restricted mode where the agent can read files and discuss but cannot write code. The goal is to understand the system, identify relevant files, map data flow, and brainstorm edge cases. The output is a research document the engineer reviews.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Plan:&lt;/strong&gt; Outline specific files to create or change, define verification steps and test strategies, explicitly scope what's in and out. The output is a plan file with step-by-step instructions.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Implement:&lt;/strong&gt; Start a fresh session with only the plan as context. This keeps context lean, enables careful per-change review, and -- because the hard thinking is already done -- can even use smaller, cheaper models.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The fresh-session trick is worth highlighting. By discarding the research and planning context and starting implementation with just the plan document, you sidestep the context degradation problem entirely. The plan becomes the distillation -- all the thinking, none of the noise.&lt;/p&gt;
&lt;h2 id="configuring-agent-behavior"&gt;Configuring Agent Behavior&lt;/h2&gt;
&lt;p&gt;Brendan breaks agent configuration into three layers:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Modes&lt;/strong&gt; -- role-based behavioral configurations that constrain what the agent can do (research-only, planning, coding).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Project-level instruction files&lt;/strong&gt; -- always-on rules covering conventions, build commands, and pre-commit requirements. O'Leary describes these as becoming a "de facto standard" across tools.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Skills&lt;/strong&gt; -- on-demand reusable playbooks for specific workflows, like generating changelogs or creating assets from templates.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img alt="Slide titled &amp;quot;Mental Model for Agent Configuration&amp;quot; showing three buckets -- AGENTS.md (always-on rules for the repo: project conventions, build commands, testing requirements), SKILLS.md (task-specific workflows on-demand: reusable playbooks like api-design and code-review), and Modes (personas with different behaviors: role-based configurations like Architect, Coder, Debugger) -- with a note that Memory Bank is being deprecated in favor of AGENTS.md + SKILLS" src="images/agent-configuration-mental-model.jpg"&gt;&lt;/p&gt;
&lt;p&gt;He also touches on connecting agents to internal APIs, listing four approaches for enterprises: use existing OpenAPI specs, convert API docs to markdown stored in the repo, provide a reference URL the agent pulls fresh each time, or build a custom integration server for complex multi-system workflows.&lt;/p&gt;
&lt;h2 id="thinking-is-the-job"&gt;Thinking Is the Job&lt;/h2&gt;
&lt;p&gt;O'Leary's talk keeps circling back to one idea: the agent amplifies whatever you bring to it. Good preparation produces good code. Sloppy preparation produces confident-sounding garbage.&lt;/p&gt;
&lt;p&gt;He quotes Dex Horthy again to make the point stick:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;"AI can't replace thinking. It can only amplify the thinking you've done or the lack of thinking you haven't done."&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The actionable takeaway from Brendan's talk is that the highest-leverage skill for working with coding agents isn't prompt engineering or tool configuration -- it's the unglamorous work of researching a problem thoroughly, writing a clear plan, and scoping the implementation tightly before letting the agent touch a single line of code. The same things that make code reviews go smoothly with human developers make agent output worth keeping.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;Brendan O'Leary spoke at AI Engineer Europe 2026. Developer Relations Engineer at &lt;a href="https://kilo.ai/"&gt;Kilo Code&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;&lt;a href="https://www.youtube.com/watch?v=BEKc4P87XKo"&gt;Watch the full talk&lt;/a&gt; | &lt;a href="https://path.kilo.ai"&gt;path.kilo.ai&lt;/a&gt; | &lt;a href="https://www.linkedin.com/in/olearycrew/"&gt;LinkedIn&lt;/a&gt; | &lt;a href="https://x.com/olearycrew"&gt;X&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;</content><category term="Conferences"/><category term="ai_engineering"/><category term="agentic_coding"/><category term="context_engineering"/><category term="AI_Engineer_Europe"/></entry><entry><title>MCP Tools Are Raw Material, Not Finished Products</title><link href="https://gallon.me/bending-a-public-mcp-server-without-breaking-it-nimrod-hauser-baz.html" rel="alternate"/><published>2026-04-11T00:00:00-05:00</published><updated>2026-04-11T00:00:00-05:00</updated><author><name>Corey</name></author><id>tag:gallon.me,2026-04-11:/bending-a-public-mcp-server-without-breaking-it-nimrod-hauser-baz.html</id><summary type="html">&lt;p&gt;Nimrod Hauser (&lt;a href="https://www.linkedin.com/in/nimrod-hauser-03776a31/"&gt;LinkedIn&lt;/a&gt;, &lt;a href="https://x.com/NimrodHauser"&gt;X&lt;/a&gt;), a founding engineer at Baz, opened his talk at AI Engineer Europe with a deceptively simple observation: public MCP servers ship tools designed for everyone, which means they're optimized for no one. When you plug generic tools into a production agent, the agent hallucinates URLs, saves …&lt;/p&gt;</summary><content type="html">&lt;p&gt;Nimrod Hauser (&lt;a href="https://www.linkedin.com/in/nimrod-hauser-03776a31/"&gt;LinkedIn&lt;/a&gt;, &lt;a href="https://x.com/NimrodHauser"&gt;X&lt;/a&gt;), a founding engineer at Baz, opened his talk at AI Engineer Europe with a deceptively simple observation: public MCP servers ship tools designed for everyone, which means they're optimized for no one. When you plug generic tools into a production agent, the agent hallucinates URLs, saves files in the wrong places, and picks the wrong tool for the job.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;"Agents are already non-deterministic, unpredictable things. You give them tools and you get unpredictability at scale."&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;His framing is that MCP tools are "glorified integration code written by a third party." The descriptions attached to those tools are the primary mechanism by which agents decide what to do -- and generic descriptions produce generic, often wrong, agent behavior. The fix isn't better prompts or a smarter model. It's reshaping the tool layer itself.&lt;/p&gt;
&lt;h2 id="the-setup-a-spec-reviewer-that-doesnt-work"&gt;The Setup: A Spec Reviewer That Doesn't Work&lt;/h2&gt;
&lt;p&gt;Hauser demonstrated with a concrete example: an agent that reads a ticket and a Figma design, opens a browser via Playwright's MCP server, navigates to the deployed application, and checks whether the implementation matches the spec -- producing a pass/fail verdict with screenshot evidence.&lt;/p&gt;
&lt;p&gt;&lt;img alt="Slide showing Baz's spec reviewer case study: an agentic reviewer that compares requirements against implementation using Playwright MCP, with a flow diagram showing JIRA, Figma, and other inputs flowing through the agent to a browser" src="images/spec-reviewer-architecture.jpg"&gt;&lt;/p&gt;
&lt;p&gt;The baseline version loaded all 21 Playwright MCP tools unmodified. The result: the agent hallucinated a URL, navigated to a 404, and returned a false negative. The tool descriptions were shallow and generic -- things like "Press a key on the keyboard" and "Close the page." Hauser doesn't blame the Playwright team for this. As he put it, "the people at Playwright don't know what our specific use case is."&lt;/p&gt;
&lt;p&gt;&lt;img alt="VS Code debugger showing the list of Playwright MCP tools with their default shallow descriptions: browser_close as &amp;quot;Close the page&amp;quot;, browser_handle_dialog as &amp;quot;Handle a dialog&amp;quot;, browser_file_upload as &amp;quot;Upload one or multiple files&amp;quot;, and others" src="images/playwright-tools-debugger.jpg"&gt;&lt;/p&gt;
&lt;h2 id="five-techniques-to-reshape-the-tool-layer"&gt;Five Techniques to Reshape the Tool Layer&lt;/h2&gt;
&lt;p&gt;Rather than treating MCP integration as all-or-nothing, Hauser presented five iterative techniques, applied one at a time to the broken baseline.&lt;/p&gt;
&lt;p&gt;&lt;img alt="Slide titled &amp;quot;5 Best Practices for Robust AI Agent Integration&amp;quot; showing five numbered cards: 1. Curate 3rd party tools, 2. Wrap 3rd party tools for focus and alignment, 3. Add deterministic guardrails, 4. Create new tools for consistency and context, 5. Treat tools as functions (occasionally). Cards are color-coded with a legend showing Context Engineering and Deterministic Guardrails buckets." src="images/five-best-practices.jpg"&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;1. Curate.&lt;/strong&gt; Remove tools the agent doesn't need. Hauser cut 5-6 irrelevant tools (browser resize, drag, execute JavaScript, etc.), dropping from 21 to 16. Fewer tools means a smaller decision space for the agent.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;2. Wrap with enhanced descriptions.&lt;/strong&gt; Create wrapper tools that call the original functions but carry descriptions tailored to the use case. The Playwright "snapshot" tool -- actually an accessibility tree dump, not a visual screenshot -- got a directive description steering the agent to use it as a first step before clicking or hovering.&lt;/p&gt;
&lt;p&gt;&lt;img alt="Code showing the wrapped tool descriptions: BROWSER_NAVIGATE gets &amp;quot;Navigate the browser to a specific URL — Always include the full URL with protocol&amp;quot;; BROWSER_CLICK gets &amp;quot;Click on an element in the browser page&amp;quot;; BROWSER_TYPE gets &amp;quot;Type text into an editable element on the page — First call the snapshot tool to get the input element's ref&amp;quot;; BROWSER_HOVER gets &amp;quot;Move the mouse over an element to trigger hover effects&amp;quot;" src="images/wrapped-tool-descriptions.jpg"&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;3. Add deterministic guardrails.&lt;/strong&gt; Intercept tool calls with validation logic before they execute. His example: the screenshot tool accepts a file path, so a check validates that the path falls within the designated directory. If not, it returns a structured error message explaining the constraint. The agent self-corrects on the next attempt. Hauser stressed returning friendly error messages rather than raising exceptions, so the agentic loop keeps running.&lt;/p&gt;
&lt;p&gt;&lt;img alt="Code showing the path validation guardrail: a function that resolves the absolute path and checks whether it falls inside SCREENSHOTS_ROOT, raising a FileValidationError if the path escapes the designated directory" src="images/path-validation-guardrail.jpg"&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;4. Compose new tools from existing ones.&lt;/strong&gt; He created an "evidence screenshot" tool wrapping the existing screenshot tool but with a separate description scoped to evidence-taking. The description instructs the agent to include the ticket number in the filename. This lets the agent distinguish between casual navigation screenshots and formal evidence captures.&lt;/p&gt;
&lt;p&gt;&lt;img alt="Code showing the evidence screenshot tool description: &amp;quot;Take a screenshot specifically for EVIDENCE purposes only. Use this tool ONLY when capturing evidence screenshots — do NOT use it for general visual inspection. Before calling this tool you MUST: 1. Read the ticket folder, 2. Identify the ticket number, 3. Include the ticket number in the screenshot filename&amp;quot;" src="images/evidence-screenshot-tool.jpg"&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;5. Pull critical operations out of the agent loop entirely.&lt;/strong&gt; Some steps are both critical and invariant -- the agent adds no value in deciding how to do them. Hauser's example: logging in by injecting JWT tokens into browser local storage via Playwright tools, called as plain functions before the agent starts. The agent receives a logged-in session and skips the error-prone login step entirely.&lt;/p&gt;
&lt;p&gt;&lt;img alt="Code showing the deterministic login_to_baz function that injects tokens into browser localStorage and triggers a page reload — called as a plain function before the agent starts, bypassing the agentic loop entirely" src="images/deterministic-login-function.jpg"&gt;&lt;/p&gt;
&lt;h2 id="the-leverage-is-in-the-descriptions"&gt;The Leverage Is in the Descriptions&lt;/h2&gt;
&lt;p&gt;The second technique -- wrapping tools with better descriptions -- is where Hauser sees the most underappreciated leverage. Descriptions are the interface between your agent and its tools. When you write "always prefer this over taking an actual snapshot," you're not prompting the agent -- you're reshaping its decision environment at the tool level.&lt;/p&gt;
&lt;p&gt;This is distinct from prompt engineering. The descriptions travel with the tools themselves, which means they work regardless of how you structure your system prompt. Hauser groups techniques 1, 2, 4, and 5 under "context engineering" -- the practice of shaping what the agent sees and has access to, rather than what you tell it to do.&lt;/p&gt;
&lt;h2 id="when-to-take-the-agent-out-of-the-loop"&gt;When to Take the Agent Out of the Loop&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;"There are aspects of your tasks that are just too sensitive to leave at the hands of the agents."&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Hauser argues that the line between agentic and deterministic should be drawn per-operation, not per-system. His login example is instructive: authentication involves sensitive tokens, must happen exactly one way, and fails catastrophically if done wrong. Making it deterministic costs nothing -- the agent was never going to improve on a hardcoded sequence.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;"Agents are non-deterministic things and sometimes they will just ignore you."&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The broader point is that a production agent system is a mix of deterministic and non-deterministic steps. The wrapper layer between the MCP server and the agent is where you make those choices, and you still get the benefit of upstream MCP server updates because the underlying tool calls remain unchanged.&lt;/p&gt;
&lt;h2 id="the-takeaway"&gt;The Takeaway&lt;/h2&gt;
&lt;p&gt;Hauser's argument is that the gap between a working MCP demo and a production MCP integration is a tool-engineering problem, not a prompt-engineering problem. The five techniques -- curate, wrap, guardrail, compose, and de-agentify -- give you a repeatable framework for closing that gap. As he put it:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;"There's no one size fits all. It's mainly a question of how do I kind of mold the tools to best fit my use case. Sometimes they'll be deterministic, sometimes they'll be flexible. It depends, and you're gonna need to tinker with it."&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;Nimrod Hauser spoke at AI Engineer Europe 2026. Founding Software Engineer at &lt;a href="https://baz.co"&gt;Baz&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;&lt;a href="https://www.youtube.com/watch?v=U00AOI1eJUE"&gt;Watch the full talk&lt;/a&gt; | &lt;a href="https://prezi.com/view/TSBwBXLNcXzzWrLbRiit/?referral_token=4jzLrblnB3FN"&gt;Slides&lt;/a&gt; | &lt;a href="https://www.linkedin.com/in/nimrod-hauser-03776a31/"&gt;LinkedIn&lt;/a&gt; | &lt;a href="https://x.com/NimrodHauser"&gt;X&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;</content><category term="Conferences"/><category term="mcp"/><category term="agents"/><category term="tool_design"/><category term="context_engineering"/><category term="AI_Engineer_Europe"/></entry><entry><title>Build the Gym, Not the Dataset</title><link href="https://gallon.me/let-llms-wander-engineering-rl-environments-stefano-fiorucci.html" rel="alternate"/><published>2026-04-11T00:00:00-05:00</published><updated>2026-04-11T00:00:00-05:00</updated><author><name>Corey</name></author><id>tag:gallon.me,2026-04-11:/let-llms-wander-engineering-rl-environments-stefano-fiorucci.html</id><summary type="html">&lt;p&gt;Stefano Fiorucci (&lt;a href="https://x.com/theanakin87"&gt;X&lt;/a&gt;, &lt;a href="https://www.linkedin.com/in/stefano-fiorucci/"&gt;LinkedIn&lt;/a&gt;, &lt;a href="https://github.com/anakin87"&gt;GitHub&lt;/a&gt;) is an AI/Software Engineer at deepset, where he contributes to the open-source LLM framework Haystack. At AI Engineer Europe 2026, he made a case that the next leap for open-source language models isn't better datasets -- it's better environments. The kind where models can act …&lt;/p&gt;</summary><content type="html">&lt;p&gt;Stefano Fiorucci (&lt;a href="https://x.com/theanakin87"&gt;X&lt;/a&gt;, &lt;a href="https://www.linkedin.com/in/stefano-fiorucci/"&gt;LinkedIn&lt;/a&gt;, &lt;a href="https://github.com/anakin87"&gt;GitHub&lt;/a&gt;) is an AI/Software Engineer at deepset, where he contributes to the open-source LLM framework Haystack. At AI Engineer Europe 2026, he made a case that the next leap for open-source language models isn't better datasets -- it's better environments. The kind where models can act, fail, and learn from the outcome.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;"This is exciting because the model is no longer limited by the quality of human examples. Through trial and error it can discover more efficient reasoning strategies."&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id="from-imitation-to-interaction"&gt;From Imitation to Interaction&lt;/h2&gt;
&lt;p&gt;Fiorucci frames the shift by contrasting two training paradigms. Supervised fine-tuning is statistical imitation -- you show the model curated examples and it learns to mimic them. Reinforcement learning with verifiable rewards (RLVR), the approach used in DeepSeek R1, is different. The model generates reasoning traces, its answers get checked against ground truth, and rewards drive the next update. No curated dataset required -- just a clear reward signal and room to explore.&lt;/p&gt;
&lt;p&gt;He paraphrases Andrej Karpathy: giving an LLM the opportunity to interact, take actions, and see outcomes means "you can hope to do a lot better than statistical expert imitation."&lt;/p&gt;
&lt;p&gt;&lt;img alt="Slide showing the RLVR training loop: a prompt is fed to the LLM, which generates a sampled completion with reasoning trace and answer. The answer is checked by a deterministic verifier against ground truth, producing a reward signal that feeds back to the RL optimizer, which updates the model." src="images/rlvr-loop-diagram.jpg"&gt;&lt;/p&gt;
&lt;p&gt;The mapping is straightforward. The language model is the agent. The environment encompasses the task, data, analysis, and scoring rules. The reward is the verification signal. What's less straightforward, Fiorucci argues, is the software engineering problem of making these environments reusable.&lt;/p&gt;
&lt;h2 id="environments-as-software-artifacts"&gt;Environments as Software Artifacts&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;"Too often environments are locked into specific training stacks, making them difficult to reuse. And as a market for closed-source environments emerges, these open initiatives ensure we have a robust alternative."&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;This is where Stefano's core reframing comes in. RL environments for LLMs should be installable, distributable Python packages -- not one-off training scripts welded to a specific framework. He walks through an open-source library called Verifiers that implements this idea with a hierarchy of environment types:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Single-turn&lt;/strong&gt; -- one model interaction (e.g., reverse a text string)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Multi-turn&lt;/strong&gt; -- multiple exchanges with state tracking and stopping conditions&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Tool environments&lt;/strong&gt; -- models call Python functions during rollouts&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;MCP environments&lt;/strong&gt; -- auto-connect to Model Context Protocol servers&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Stateful tool environments&lt;/strong&gt; -- per-rollout persisted state like database connections&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The library abstracts model serving behind OpenAI-compatible API endpoints and integrates with multiple training frameworks. There's also a community hub for sharing environments -- the point being that if environments can be packaged and shared, the open-source ecosystem doesn't fall behind closed models just because it lacks training infrastructure.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;"We don't want open-source models to lag behind just because they lack the right playground training."&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id="teaching-a-small-model-to-play-tic-tac-toe"&gt;Teaching a Small Model to Play Tic-Tac-Toe&lt;/h2&gt;
&lt;p&gt;The demo is where theory meets practice. Fiorucci takes LFM2, a small open model by Liquid AI, and tries to make it a competent tic-tac-toe player through a multi-stage pipeline.&lt;/p&gt;
&lt;p&gt;The baseline is bleak. GPT-5 mini plays well and follows the expected format. LFM2 struggles with format, makes invalid moves, and loses constantly.&lt;/p&gt;
&lt;p&gt;&lt;img alt="Baseline evaluation table comparing GPT-5 mini and LFM2-2.6B on tic-tac-toe. Against a random opponent, GPT-5 mini wins 90% with 100% format compliance; LFM2 wins only 40% with 27.8% format compliance and 40% invalid move rate. Against an optimal opponent, GPT-5 mini draws 76% of games; LFM2 draws only 11% and loses 89%." src="images/baseline-evaluation-table.jpg"&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Stage 1: SFT warm-up.&lt;/strong&gt; Generate 200 synthetic games from GPT-5 mini, filter out losses, fine-tune LFM2. This teaches format and valid move syntax -- nothing more. Training takes minutes on a single 96GB GPU.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Stage 2: RL training (round 1).&lt;/strong&gt; Using CISPO (an improvement on DeepSeek's GRPO algorithm), the model plays against opponents with configurable skill levels -- random move probability ranging from 20% to 70%. Several design choices matter here:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Stratified sampling&lt;/strong&gt; ensures each training batch contains a balanced mix of opponent difficulties&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Deterministic seeding&lt;/strong&gt; per example and per turn, so reward differences reflect model skill rather than opponent randomness&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Invalid move tolerance&lt;/strong&gt; with a small penalty (-0.1) rather than immediate game-over, preserving learning signal for small models&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Minimum batch size of 256&lt;/strong&gt; -- smaller values caused training instability and collapse&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Stage 3: RL training (round 2).&lt;/strong&gt; Tougher opponents (0-25% random moves), higher temperature to encourage exploration. An initial performance dip -- interpreted as an exploration phase -- followed by recovery and improvement.&lt;/p&gt;
&lt;p&gt;&lt;img alt="Four training plots from RL round 1 showing reward metrics over training steps. Total reward trends upward steadily. Format reward starts near perfect and holds. Win reward climbs from roughly 0.3 to 0.8. Invalid move penalty starts slightly negative and converges to zero." src="images/rl-round1-training-curves.jpg"&gt;&lt;/p&gt;
&lt;h2 id="the-student-surpasses-the-teacher"&gt;The Student Surpasses the Teacher&lt;/h2&gt;
&lt;p&gt;The result: after round 1, the small model dominates random opponents and draws roughly 85% of the time against optimal play. After round 2, it outperforms GPT-5 mini against an optimal opponent.&lt;/p&gt;
&lt;p&gt;This is the non-obvious punchline. Fiorucci used GPT-5 mini to generate the initial training data, then used RL to push a small model past the model that taught it. His argument: if you can define a clear reward signal, the ceiling for a small specialized model isn't set by its teacher.&lt;/p&gt;
&lt;p&gt;&lt;img alt="Bar chart comparing three models against an optimal tic-tac-toe opponent. Base LFM2-2.6B loses 89 games and draws only 11. GPT-5 mini loses 24 and draws 76. The RL-trained LFM2-2.6B-mr-tictactoe loses only 3 and draws 97 -- outperforming its teacher model." src="images/models-vs-optimal-opponent.jpg"&gt;&lt;/p&gt;
&lt;h2 id="lessons-from-failed-experiments"&gt;Lessons from Failed Experiments&lt;/h2&gt;
&lt;p&gt;Fiorucci is candid about what went wrong along the way:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Batch size too small&lt;/strong&gt; leads to instability and model collapse -- the model learns from too few games and opponent types at once.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Hidden environment biases&lt;/strong&gt; can sabotage training. His Minimax opponent implementation always selected the first valid position when moves had equal scores, so the model memorized one opponent's behavior instead of learning general play.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Starting from a reasoning model&lt;/strong&gt; with long thinking traces on limited GPU meant forced truncation, which wastes compute budget and risks damaging the model's capabilities. Better to start from an instruct model.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Very small models&lt;/strong&gt; may simply lack capacity for a given task. Evaluate base models first and look for promising behaviors before committing to training.&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;"Reinforcement learning is slow and takes time to see progress. If you continually monitor it, you risk the temptation to stop it and tweak something prematurely... So start training and go for a walk."&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id="the-takeaway"&gt;The Takeaway&lt;/h2&gt;
&lt;p&gt;Fiorucci's argument is practical: the barrier to training capable small models isn't algorithmic sophistication -- it's the lack of reusable, shareable environments where models can learn through interaction. Build the environment, define the reward signal, and a small model can beat a large one on a specific task at a fraction of the cost.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;"You can do this at home too. If you can define a clear reward signal, you can build an environment and train a small specialized model to beat a large closed model on a specific task at a fraction of the cost."&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;Stefano Fiorucci spoke at AI Engineer Europe 2026. AI/Software Engineer at &lt;a href="https://www.deepset.ai/"&gt;deepset&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;&lt;a href="https://www.youtube.com/watch?v=71V3fTaUp2Q"&gt;Watch the full talk&lt;/a&gt; | &lt;a href="https://github.com/anakin87/llm-rl-environments-lil-course"&gt;LLM RL Environments Lil Course&lt;/a&gt; | &lt;a href="https://drive.google.com/file/d/116PKThwtyTxeH1GmZQ7bL3HPYM6KCgHa/view?usp=drive_link"&gt;Slides&lt;/a&gt; | &lt;a href="https://www.linkedin.com/in/stefano-fiorucci/"&gt;LinkedIn&lt;/a&gt; | &lt;a href="https://x.com/theanakin87"&gt;X&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;</content><category term="Conferences"/><category term="reinforcement_learning"/><category term="LLMs"/><category term="open_source"/><category term="small_language_models"/><category term="AI_Engineer_Europe"/></entry><entry><title>Subagent Modes in Claude Code</title><link href="https://gallon.me/subagent-modes-in-claude-code.html" rel="alternate"/><published>2026-04-01T00:00:00-05:00</published><updated>2026-04-01T00:00:00-05:00</updated><author><name>Corey</name></author><id>tag:gallon.me,2026-04-01:/subagent-modes-in-claude-code.html</id><summary type="html">&lt;p&gt;A &lt;a href="https://x.com/mal_shaik/status/2038918662489510273"&gt;post making the rounds&lt;/a&gt; claims that Claude Code subagents share a prompt cache, making parallelism "basically free." It says you can spin up five agents and pay barely more than one. It lists three execution models — fork, teammate, and worktree — and says they all share the cache. Analysis of …&lt;/p&gt;</summary><content type="html">&lt;h1 id="what-claude-codes-source-code-actually-says-about-subagents"&gt;What Claude Code's Source Code Actually Says About Subagents&lt;/h1&gt;
&lt;p&gt;A &lt;a href="https://x.com/mal_shaik/status/2038918662489510273"&gt;post making the rounds&lt;/a&gt; claims that Claude Code subagents share a prompt cache, making parallelism "basically free." It says you can spin up five agents and pay barely more than one. It lists three execution models — fork, teammate, and worktree — and says they all share the cache. Analysis of the source code reveals that some of this is true, but most of it isn't — or at least, isn't true in the way the post suggests.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;h/t to &lt;a href="https://x.com/swyx"&gt;@swyx&lt;/a&gt; for sharing this post as part of his excellent coverage of the Claude Code source leak in &lt;a href="https://www.latent.space/p/ainews-the-claude-code-source-leak"&gt;yesterday's AI News&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;
&lt;h2 id="fork-mode-exists-but-you-probably-dont-have-it"&gt;Fork mode exists, but you probably don't have it&lt;/h2&gt;
&lt;p&gt;The cache-sharing behavior the post describes is real. It lives in a subagent execution model called "fork," where a child agent inherits the parent's full conversation context and system prompt. The code goes to considerable lengths to keep the API request prefix identical across fork children so they hit the Anthropic API's prompt cache. The parent's rendered system prompt is threaded directly to forks rather than recomputed, specifically to prevent cache busting from feature flag state changes between turns.&lt;sup id="fnref:1"&gt;&lt;a class="footnote-ref" href="#fn:1"&gt;1&lt;/a&gt;&lt;/sup&gt; Fork children receive the parent's exact tool array rather than building their own.&lt;sup id="fnref:2"&gt;&lt;a class="footnote-ref" href="#fn:2"&gt;2&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;p&gt;The problem is that fork mode is gated behind &lt;code&gt;feature('FORK_SUBAGENT')&lt;/code&gt;, a compile-time flag resolved by the Bun bundler.&lt;sup id="fnref:3"&gt;&lt;a class="footnote-ref" href="#fn:3"&gt;3&lt;/a&gt;&lt;/sup&gt; When the flag is off, the code is eliminated from the build entirely. There is no user-facing setting, environment variable, or runtime toggle to enable it. If your build doesn't have it compiled in, it doesn't exist.&lt;/p&gt;
&lt;p&gt;When fork is disabled, omitting &lt;code&gt;subagent_type&lt;/code&gt; on the Agent tool falls back to a general-purpose agent &lt;strong&gt;with no parent context and no cache sharing&lt;/strong&gt;.&lt;sup id="fnref:4"&gt;&lt;a class="footnote-ref" href="#fn:4"&gt;4&lt;/a&gt;&lt;/sup&gt; This is the default subagent experience in Claude Code. (In case you're wondering, the &lt;code&gt;/fork&lt;/code&gt; slash command doesn't register; its alias gets claimed by &lt;code&gt;/branch&lt;/code&gt; instead.&lt;sup id="fnref:5"&gt;&lt;a class="footnote-ref" href="#fn:5"&gt;5&lt;/a&gt;&lt;/sup&gt;)&lt;/p&gt;
&lt;h2 id="the-cost-math-doesnt-add-up"&gt;The cost math doesn't add up&lt;/h2&gt;
&lt;p&gt;Even when fork mode is active, "5 agents cost barely more than 1" overstates things.&lt;/p&gt;
&lt;p&gt;The cost model tracks input tokens, output tokens, cache read tokens, and cache write tokens separately.&lt;sup id="fnref:6"&gt;&lt;a class="footnote-ref" href="#fn:6"&gt;6&lt;/a&gt;&lt;/sup&gt; Cache reads cost about 10% of regular input, but the first fork pays a 25% write premium to populate the cache, and every fork pays full price for its own output tokens, its unique task directive, and all the tool calls and results it generates while doing its work.&lt;/p&gt;
&lt;p&gt;The savings are proportional to how large the shared prefix is relative to total token usage. For agents doing real work — reading files, running commands, writing code — output and tool interaction tokens add up quickly. The shared prefix helps, but it doesn't make parallelism free.&lt;/p&gt;
&lt;p&gt;This may not matter to Claude Max subscribers, but it certainly matters if you have "extra usage" enabled.&lt;/p&gt;
&lt;h2 id="there-arent-three-execution-models-there-are-four-and-worktree-isnt-one-of-them"&gt;There aren't three execution models. There are four, and worktree isn't one of them.&lt;/h2&gt;
&lt;p&gt;The post lists fork, teammate, and worktree as three execution models. The routing logic in the source tells a different story.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Teammate&lt;/strong&gt; is an independent worker spawned with a &lt;code&gt;team_name&lt;/code&gt; and &lt;code&gt;name&lt;/code&gt;. Teammates do not inherit the parent's conversation. The parent's messages are explicitly zeroed out at spawn time.&lt;sup id="fnref:7"&gt;&lt;a class="footnote-ref" href="#fn:7"&gt;7&lt;/a&gt;&lt;/sup&gt; They build their own history from scratch.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Fresh specialized&lt;/strong&gt; is triggered by setting &lt;code&gt;subagent_type&lt;/code&gt;. The agent gets its own system prompt, its own tool pool, and no parent context.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Fork&lt;/strong&gt; is triggered by omitting &lt;code&gt;subagent_type&lt;/code&gt; when the fork gate is enabled. It inherits parent context and is cache-optimized, as described above.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;General-purpose&lt;/strong&gt; is the fallback when &lt;code&gt;subagent_type&lt;/code&gt; is omitted and fork is disabled. It behaves like a fresh specialized agent using a default agent definition.&lt;/p&gt;
&lt;p&gt;Worktree is not an execution model. It's an isolation modifier that can be combined with any of the four models above.&lt;sup id="fnref:8"&gt;&lt;a class="footnote-ref" href="#fn:8"&gt;10&lt;/a&gt;&lt;/sup&gt; It creates an isolated git worktree so the agent's file operations don't touch the parent's working copy. A fork agent with worktree isolation still inherits context. A fresh specialized agent with worktree isolation still doesn't. Worktree changes where file operations land, not how context is constructed.&lt;/p&gt;
&lt;h2 id="not-all-subagents-share-the-cache"&gt;Not all subagents share the cache&lt;/h2&gt;
&lt;p&gt;The post's central claim is that all subagent types share the prompt cache. This is wrong.&lt;/p&gt;
&lt;p&gt;Fresh specialized agents build a different system prompt, assemble a different tool pool, and carry no parent conversation history. Different prefix, no cache sharing. Teammates have their messages explicitly emptied.&lt;sup id="fnref2:7"&gt;&lt;a class="footnote-ref" href="#fn:7"&gt;7&lt;/a&gt;&lt;/sup&gt; They share nothing with the parent.&lt;/p&gt;
&lt;p&gt;Fork children can't fork further, either. The code detects a boilerplate tag in conversation history and rejects recursive fork attempts.&lt;sup id="fnref:9"&gt;&lt;a class="footnote-ref" href="#fn:9"&gt;8&lt;/a&gt;&lt;/sup&gt; Cache sharing is one level deep.&lt;/p&gt;
&lt;h2 id="teammates-dont-all-use-file-based-mailboxes"&gt;Teammates don't all use file-based mailboxes&lt;/h2&gt;
&lt;p&gt;The post says teammates communicate via file-based mailbox. This depends on which backend spawns them.&lt;/p&gt;
&lt;p&gt;In-process teammates use an in-memory &lt;code&gt;Mailbox&lt;/code&gt; class with a queue-and-waiters async pattern.&lt;sup id="fnref:10"&gt;&lt;a class="footnote-ref" href="#fn:10"&gt;9&lt;/a&gt;&lt;/sup&gt; No files involved. Teammates spawned in separate tmux or iTerm panes do use file-based mechanisms for initial instruction delivery, but that's a different execution path.&lt;/p&gt;
&lt;h2 id="what-the-source-code-actually-tells-us"&gt;What the source code actually tells us&lt;/h2&gt;
&lt;p&gt;The interesting story here isn't the one the post tells. The source reveals a team thinking carefully about API-level cache mechanics, building explicit cost tracking around them, and keeping the feature gated while they validate it. The &lt;code&gt;CacheSafeParams&lt;/code&gt; type, the byte-exact system prompt threading, the identical placeholder tool results, the analytics tracking cache hit rates: this is deliberate, measured engineering work.&lt;/p&gt;
&lt;p&gt;But it's behind an experiment gate. Most users interact with fresh specialized or general-purpose agents that share nothing with each other or the parent. That's worth knowing before you reorganize your workflow around a capability you may not have.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;strong&gt;Feature Image Prompt:&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Generate an image. The aesthetic should be cyberpunk with colors of neon pink, blue and purple. Do not add any people. A glowing central node pulses at the center of a dark circuit-board cityscape, splitting into three distinct branching pathways made of light. The first branch is a perfect mirror-copy of the central node, trailing an identical stream of data behind it — a fork. The second branch launches a fresh, smaller node with its own clean trajectory and no trailing data — a specialized agent. The third branch creates an independent floating terminal window connected back to the hub only by a thin mailbox-style data link — a teammate. Each pathway illuminates a different sector of the city below. A translucent hexagonal grid overlays everything, representing the shared prompt cache, but only the fork branch glows where it intersects the grid. Tiny flowing particles of data stream along the branches. In the background, isolated floating platforms with their own miniature cityscapes represent worktree isolation. The overall composition suggests parallel orchestration — multiple autonomous systems radiating from a single point of origin.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="references"&gt;References&lt;/h2&gt;
&lt;p&gt;All references are to the Claude Code source as leaked March 31, 2026. Someone who isn't me obtained it and performed this analysis.&lt;/p&gt;
&lt;div class="footnote"&gt;
&lt;hr&gt;
&lt;ol&gt;
&lt;li id="fn:1"&gt;
&lt;p&gt;&lt;code&gt;forkSubagent.ts:54-58&lt;/code&gt; — Comment: "Reconstructing by re-calling getSystemPrompt() can diverge (GrowthBook cold→warm) and bust the prompt cache; threading the rendered bytes is byte-exact."&amp;#160;&lt;a class="footnote-backref" href="#fnref:1" title="Jump back to footnote 1 in the text"&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:2"&gt;
&lt;p&gt;&lt;code&gt;AgentTool.tsx:627&lt;/code&gt; — &lt;code&gt;availableTools: isForkPath ? toolUseContext.options.tools : workerTools&lt;/code&gt;&amp;#160;&lt;a class="footnote-backref" href="#fnref:2" title="Jump back to footnote 2 in the text"&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:3"&gt;
&lt;p&gt;&lt;code&gt;forkSubagent.ts:32-39&lt;/code&gt; — &lt;code&gt;isForkSubagentEnabled()&lt;/code&gt; checks &lt;code&gt;feature('FORK_SUBAGENT')&lt;/code&gt;, then excludes coordinator mode and non-interactive sessions.&amp;#160;&lt;a class="footnote-backref" href="#fnref:3" title="Jump back to footnote 3 in the text"&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:4"&gt;
&lt;p&gt;&lt;code&gt;AgentTool.tsx:322&lt;/code&gt; — &lt;code&gt;const effectiveType = subagent_type ?? (isForkSubagentEnabled() ? undefined : GENERAL_PURPOSE_AGENT.agentType)&lt;/code&gt;&amp;#160;&lt;a class="footnote-backref" href="#fnref:4" title="Jump back to footnote 4 in the text"&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:5"&gt;
&lt;p&gt;&lt;code&gt;commands/branch/index.ts:8&lt;/code&gt; — &lt;code&gt;aliases: feature('FORK_SUBAGENT') ? [] : ['fork']&lt;/code&gt;&amp;#160;&lt;a class="footnote-backref" href="#fnref:5" title="Jump back to footnote 5 in the text"&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:6"&gt;
&lt;p&gt;&lt;code&gt;modelCost.ts:131-138&lt;/code&gt; — &lt;code&gt;tokensToUSDCost()&lt;/code&gt; sums &lt;code&gt;input_tokens&lt;/code&gt;, &lt;code&gt;output_tokens&lt;/code&gt;, &lt;code&gt;cache_read_input_tokens&lt;/code&gt;, and &lt;code&gt;cache_creation_input_tokens&lt;/code&gt; at different rates.&amp;#160;&lt;a class="footnote-backref" href="#fnref:6" title="Jump back to footnote 6 in the text"&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:7"&gt;
&lt;p&gt;&lt;code&gt;spawnMultiAgent.ts:927-931&lt;/code&gt; — Comment: "Strip messages: the teammate never reads toolUseContext.messages." Code: &lt;code&gt;toolUseContext: { ...context, messages: [] }&lt;/code&gt;&amp;#160;&lt;a class="footnote-backref" href="#fnref:7" title="Jump back to footnote 7 in the text"&gt;&amp;#8617;&lt;/a&gt;&lt;a class="footnote-backref" href="#fnref2:7" title="Jump back to footnote 7 in the text"&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:9"&gt;
&lt;p&gt;&lt;code&gt;forkSubagent.ts:73-87&lt;/code&gt; — &lt;code&gt;isInForkChild()&lt;/code&gt; scans conversation history for the fork boilerplate tag and rejects recursive fork attempts.&amp;#160;&lt;a class="footnote-backref" href="#fnref:9" title="Jump back to footnote 8 in the text"&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:10"&gt;
&lt;p&gt;&lt;code&gt;mailbox.ts:19-73&lt;/code&gt; — In-memory &lt;code&gt;Mailbox&lt;/code&gt; class with &lt;code&gt;send()&lt;/code&gt;, &lt;code&gt;poll()&lt;/code&gt;, and &lt;code&gt;receive()&lt;/code&gt; methods using a queue-and-waiters pattern.&amp;#160;&lt;a class="footnote-backref" href="#fnref:10" title="Jump back to footnote 9 in the text"&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:8"&gt;
&lt;p&gt;&lt;code&gt;AgentTool.tsx:431&lt;/code&gt; — &lt;code&gt;const effectiveIsolation = isolation ?? selectedAgent.isolation&lt;/code&gt; — resolved independently of execution model routing.&amp;#160;&lt;a class="footnote-backref" href="#fnref:8" title="Jump back to footnote 10 in the text"&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;</content><category term="Writing"/><category term="claude_code"/><category term="agents"/><category term="AI"/></entry><entry><title>Remapping the Logitech R500s on Ubuntu 24.04 with keyd</title><link href="https://gallon.me/remapping-the-logitech-r500s-on-ubuntu-2404-with-keyd.html" rel="alternate"/><published>2026-03-25T00:00:00-05:00</published><updated>2026-03-25T00:00:00-05:00</updated><author><name>Corey</name></author><id>tag:gallon.me,2026-03-25:/remapping-the-logitech-r500s-on-ubuntu-2404-with-keyd.html</id><summary type="html">&lt;p&gt;&lt;strong&gt;Here’s my latest keyboard:&lt;/strong&gt;&lt;/p&gt;</summary><content type="html">&lt;h1 id="remapping-the-logitech-r500s-on-ubuntu-2404-with-keyd"&gt;Remapping the Logitech R500s on Ubuntu 24.04 with keyd&lt;/h1&gt;
&lt;p&gt;&lt;strong&gt;Here’s my latest keyboard:&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img alt="Logitech R500s Remote.jpeg" src="images/20260325_logitech-r500s.jpeg"&gt;&lt;/p&gt;
&lt;p&gt;That’s right — I said it — &lt;strong&gt;keyboard!&lt;/strong&gt; This is so very nice when you want to just sit back in your chair and have a long working session with an AI. You just use the big button to talk, and the little button to send what you’ve typed with speech! This effectively replaces your keyboard with this lovely, ergonomic remote. In order to do this, though, you need to do a little bit of config, but it’s dead easy.&lt;/p&gt;
&lt;p&gt;Here’s a complete guide to remapping presenter buttons to arbitrary keys or macros, including a technical explanation of how the Linux input stack works and why this approach works the way it does.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="the-hardware-logitech-r500s"&gt;The Hardware: Logitech R500s&lt;/h2&gt;
&lt;p&gt;The R500s is a three-button Bluetooth/USB presenter clicker. Despite having no air mouse or cursor capability, Linux exposes it as &lt;strong&gt;two input nodes&lt;/strong&gt; when connected:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;/dev/input/event24&lt;/code&gt; — &lt;code&gt;Logi R500 Keyboard&lt;/code&gt; (where button presses register)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/dev/input/event25&lt;/code&gt; — &lt;code&gt;Logi R500 Mouse&lt;/code&gt; (exposed due to HID descriptor, largely inactive)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The device vendor/product ID is &lt;code&gt;046d:b505&lt;/code&gt; (Logitech vendor &lt;code&gt;046d&lt;/code&gt;, R500s product &lt;code&gt;b505&lt;/code&gt;).&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="how-the-linux-input-stack-works"&gt;How the Linux Input Stack Works&lt;/h2&gt;
&lt;p&gt;Understanding why keyd works requires understanding the path from physical button press to application event.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="n"&gt;flowchart&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;TD&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;A&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Physical&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;button&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;press&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;--&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;B&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;HID&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;report&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;over&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Bluetooth&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kr"&gt;or&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;USB&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;B&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;--&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;C&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Linux&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;kernel&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="n"&gt;nhid&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;generic&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;driver&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;C&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;--&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;D&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;evdev&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;event&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;written&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;to&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;dev&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;eventN&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;D&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;--&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;E&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Applications&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="n"&gt;ndesktop&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;·&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;keyd&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;·&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;evtest&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;·&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;etc&lt;/span&gt;&lt;span class="p"&gt;.]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;When you press the advance button, the R500s sends a HID (Human Interface Device) report over Bluetooth to the kernel. The kernel's &lt;code&gt;hid-generic&lt;/code&gt; driver reads it and translates it into a standardized &lt;strong&gt;evdev event&lt;/strong&gt; — a struct that says: device &lt;code&gt;event24&lt;/code&gt;, event type &lt;code&gt;EV_KEY&lt;/code&gt;, code &lt;code&gt;KEY_RIGHT&lt;/code&gt;, value &lt;code&gt;1&lt;/code&gt; (pressed). That event gets written to &lt;code&gt;/dev/input/event24&lt;/code&gt;, which is just a character device file that any program can open and read.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;evtest&lt;/code&gt; is simply a program that opens that file and prints what it sees. That's its entire job — it's a transparent window into the raw event stream, which makes it the right first tool when you need to know what a device actually sends.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="discovering-what-your-device-sends"&gt;Discovering What Your Device Sends&lt;/h2&gt;
&lt;p&gt;Before remapping anything, always verify the actual keycodes with &lt;code&gt;evtest&lt;/code&gt;. Never assume based on documentation or what worked on a similar device.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;sudo&lt;span class="w"&gt; &lt;/span&gt;evtest&lt;span class="w"&gt; &lt;/span&gt;/dev/input/event24
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Press each physical button and observe the output. For the R500s:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Physical Button&lt;/th&gt;
&lt;th&gt;Keycode Sent&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Advance (big front button)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;KEY_RIGHT&lt;/code&gt; (code 106)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Back button&lt;/td&gt;
&lt;td&gt;verify with evtest&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Laser button&lt;/td&gt;
&lt;td&gt;verify with evtest&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;The evtest output also shows the MSC_SCAN value, which is the raw hardware scancode (&lt;code&gt;value 7004f&lt;/code&gt; for the advance button). This is the scancode at the HID protocol level, before the kernel translates it to a named keycode.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="how-keyd-works"&gt;How keyd Works&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;keyd&lt;/code&gt; is a key remapping daemon — a background service that intercepts input events and rewrites them before they reach your desktop.&lt;/p&gt;
&lt;p&gt;When keyd starts, it reads your config files and &lt;strong&gt;grabs&lt;/strong&gt; the matching devices. "Grabbing" means it opens &lt;code&gt;/dev/input/event24&lt;/code&gt; with an &lt;strong&gt;exclusive lock&lt;/strong&gt;. Once grabbed, no other program can read from that file. The events go only to keyd.&lt;/p&gt;
&lt;p&gt;keyd then reads those events, applies your remapping rules, and re-emits the translated events through a &lt;strong&gt;virtual input device&lt;/strong&gt; it creates via &lt;code&gt;/dev/uinput&lt;/code&gt;. Your desktop sees this virtual device and receives the remapped key. It never knows the R500s was involved.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="n"&gt;flowchart&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;TD&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;A&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Physical&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;button&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;press&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;--&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;B&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;/dev/input/event24\nKEY_RIGHT&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;B&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;--&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;C&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;keyd grabs it exclusively\nnothing else sees it&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;C&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;--&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;D&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;keyd applies rule\nright → pause&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;D&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;--&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;E&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;keyd emits KEY_PAUSE\nvia /dev/uinput virtual device&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;E&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;--&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;F&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Desktop&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;applications&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="n"&gt;nsee&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;KEY_PAUSE&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Because keyd runs as a &lt;strong&gt;system service&lt;/strong&gt; (not a user session process), it starts early in the boot sequence before your desktop loads. This means remaps are active from the moment you log in, regardless of which application is focused.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="installation"&gt;Installation&lt;/h2&gt;
&lt;p&gt;keyd is not in the Ubuntu 24.04 default repositories. Build from source:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# Install build dependencies&lt;/span&gt;
sudo&lt;span class="w"&gt; &lt;/span&gt;apt&lt;span class="w"&gt; &lt;/span&gt;install&lt;span class="w"&gt; &lt;/span&gt;git&lt;span class="w"&gt; &lt;/span&gt;make&lt;span class="w"&gt; &lt;/span&gt;gcc

&lt;span class="c1"&gt;# Clone the repository&lt;/span&gt;
git&lt;span class="w"&gt; &lt;/span&gt;clone&lt;span class="w"&gt; &lt;/span&gt;https://github.com/rvaiya/keyd
&lt;span class="nb"&gt;cd&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;keyd

&lt;span class="c1"&gt;# Build and install&lt;/span&gt;
make
sudo&lt;span class="w"&gt; &lt;/span&gt;make&lt;span class="w"&gt; &lt;/span&gt;install

&lt;span class="c1"&gt;# Enable and start the service&lt;/span&gt;
sudo&lt;span class="w"&gt; &lt;/span&gt;systemctl&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;enable&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;--now&lt;span class="w"&gt; &lt;/span&gt;keyd
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Verify it's running:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;sudo&lt;span class="w"&gt; &lt;/span&gt;systemctl&lt;span class="w"&gt; &lt;/span&gt;status&lt;span class="w"&gt; &lt;/span&gt;keyd
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;hr&gt;
&lt;h2 id="the-config-file"&gt;The Config File&lt;/h2&gt;
&lt;p&gt;Config files live in &lt;code&gt;/etc/keyd/&lt;/code&gt; and can be named anything with a &lt;code&gt;.conf&lt;/code&gt; extension. keyd loads all files in that directory on startup.&lt;/p&gt;
&lt;h3 id="full-config-for-the-r500s"&gt;Full config for the R500s&lt;/h3&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Logitech&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;R500s&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Presenter&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Remote&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Control&lt;/span&gt;
&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="n"&gt;ids&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;
&lt;span class="mi"&gt;046&lt;/span&gt;&lt;span class="nl"&gt;d&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;b505&lt;/span&gt;

&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Button&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;mappings&lt;/span&gt;
&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="n"&gt;main&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;
&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Big&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;advance&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;button&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;front&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;→&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Pause&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;key&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;triggers&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Handy&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;speech&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="k"&gt;to&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nc"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;right&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;pause&lt;/span&gt;
&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Back&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;button&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;→&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Enter&lt;/span&gt;
&lt;span class="nf"&gt;left&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;enter&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Critical gotcha: no inline comments.&lt;/strong&gt; keyd only supports comments on their own line starting with &lt;code&gt;#&lt;/code&gt;. An inline comment like &lt;code&gt;046d:b505 # my device&lt;/code&gt; will break the config — keyd will try to match the entire string including the comment text as the device ID and silently fail to grab anything.
&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id="how-each-part-works"&gt;How each part works&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;[ids]&lt;/code&gt;&lt;/strong&gt; — tells keyd which physical devices this config applies to. The value &lt;code&gt;046d:b505&lt;/code&gt; is the USB vendor ID and product ID of the R500s. keyd matches this against every input device on the system. Only the R500s gets grabbed and remapped. Every other device is unaffected, including your keyboard's own right arrow key, which has a different product ID.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;[main]&lt;/code&gt;&lt;/strong&gt; — the default layer, meaning "rules that are always active." keyd supports multiple layers (shift layers, function layers, tap-hold behaviors, etc.), but &lt;code&gt;[main]&lt;/code&gt; is the base that's always on.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;right = pause&lt;/code&gt;&lt;/strong&gt; — the remap rule. Left side is the incoming keycode (&lt;code&gt;KEY_RIGHT&lt;/code&gt;, written without the &lt;code&gt;KEY_&lt;/code&gt; prefix, lowercase). Right side is what keyd emits instead (&lt;code&gt;KEY_PAUSE&lt;/code&gt;). keyd swallows the original event and injects the replacement.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;left = macro(C-a delete)&lt;/code&gt;&lt;/strong&gt; — a macro rule. Instead of emitting a single key, keyd fires a sequence: &lt;code&gt;Ctrl+A&lt;/code&gt; followed by &lt;code&gt;Delete&lt;/code&gt;. This selects all text then deletes it. &lt;code&gt;C-&lt;/code&gt; is keyd's syntax for Ctrl, &lt;code&gt;M-&lt;/code&gt; for Alt, &lt;code&gt;S-&lt;/code&gt; for Shift.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="key-syntax-reference"&gt;Key Syntax Reference&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Syntax&lt;/th&gt;
&lt;th&gt;Meaning&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;pause&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Pause key&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;enter&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Return/Enter&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;space&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Spacebar&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;esc&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Escape&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;f5&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;F5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;C-a&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Ctrl+A&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;C-z&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Ctrl+Z (undo)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;C-c&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Ctrl+C (copy)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;M-f&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Alt+F&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;S-tab&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Shift+Tab&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;macro(C-a delete)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Ctrl+A then Delete&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;macro(C-a C-c)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Select all then copy&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;macro(h e l l o)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Types the word "hello"&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr&gt;
&lt;h2 id="applying-config-changes"&gt;Applying Config Changes&lt;/h2&gt;
&lt;p&gt;After editing the config file, restart keyd to pick up the changes:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;sudo&lt;span class="w"&gt; &lt;/span&gt;systemctl&lt;span class="w"&gt; &lt;/span&gt;restart&lt;span class="w"&gt; &lt;/span&gt;keyd
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;To verify the R500s was matched:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;sudo&lt;span class="w"&gt; &lt;/span&gt;journalctl&lt;span class="w"&gt; &lt;/span&gt;-u&lt;span class="w"&gt; &lt;/span&gt;keyd&lt;span class="w"&gt; &lt;/span&gt;--no-pager&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;grep&lt;span class="w"&gt; &lt;/span&gt;-i&lt;span class="w"&gt; &lt;/span&gt;r500
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;You should see lines like:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="n"&gt;CONFIG&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;parsing&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="sr"&gt;/etc/keyd/&lt;/span&gt;&lt;span class="n"&gt;r500s&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;conf&lt;/span&gt;
&lt;span class="n"&gt;DEVICE&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;match&lt;/span&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="mi"&gt;046&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="n"&gt;b505&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="n"&gt;a33bddee&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="sr"&gt;/etc/keyd/&lt;/span&gt;&lt;span class="n"&gt;r500s&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;conf&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Logi&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;R500&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Keyboard&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;DEVICE&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;match&lt;/span&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="mi"&gt;046&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="n"&gt;b505&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="n"&gt;ef6d98d&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="sr"&gt;/etc/keyd/&lt;/span&gt;&lt;span class="n"&gt;r500s&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;conf&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Logi&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;R500&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Mouse&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Both nodes being matched is correct and expected.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="why-device-id-scoping-matters"&gt;Why Device ID Scoping Matters&lt;/h2&gt;
&lt;p&gt;The &lt;code&gt;[ids]&lt;/code&gt; section is what makes keyd safe to use. Because every rule is scoped to a specific device by vendor/product ID, you can write:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;[ids]&lt;/span&gt;
&lt;span class="na"&gt;046d&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="s"&gt;b505&lt;/span&gt;

&lt;span class="k"&gt;[main]&lt;/span&gt;
&lt;span class="na"&gt;right&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;pause&lt;/span&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="c1"&gt;# R500s advance button → Pause&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;And your keyboard's right arrow key continues to work normally because your keyboard has a different product ID. keyd tracks the source of every event.&lt;/p&gt;
&lt;p&gt;This is fundamentally different from older tools like &lt;code&gt;xmodmap&lt;/code&gt;, which remapped keycodes globally system-wide regardless of which device sent them.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="the-systemd-integration"&gt;The systemd Integration&lt;/h2&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;sudo&lt;span class="w"&gt; &lt;/span&gt;systemctl&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;enable&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;--now&lt;span class="w"&gt; &lt;/span&gt;keyd
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;&lt;code&gt;enable&lt;/code&gt; tells systemd to start keyd automatically on every boot by creating a symlink in &lt;code&gt;/etc/systemd/system/&lt;/code&gt;. &lt;code&gt;--now&lt;/code&gt; also starts it immediately without requiring a reboot. The service file installed by &lt;code&gt;make install&lt;/code&gt; is at &lt;code&gt;/usr/local/lib/systemd/system/keyd.service&lt;/code&gt;.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="troubleshooting"&gt;Troubleshooting&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Device not being matched:&lt;/strong&gt;
The R500s may not have been connected when keyd started. Toggle it off and on, or restart keyd:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;sudo&lt;span class="w"&gt; &lt;/span&gt;systemctl&lt;span class="w"&gt; &lt;/span&gt;restart&lt;span class="w"&gt; &lt;/span&gt;keyd
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Remap not working after config edit:&lt;/strong&gt;
Config changes require a restart:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;sudo&lt;span class="w"&gt; &lt;/span&gt;systemctl&lt;span class="w"&gt; &lt;/span&gt;restart&lt;span class="w"&gt; &lt;/span&gt;keyd
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Check full keyd logs:&lt;/strong&gt;&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;sudo&lt;span class="w"&gt; &lt;/span&gt;journalctl&lt;span class="w"&gt; &lt;/span&gt;-u&lt;span class="w"&gt; &lt;/span&gt;keyd&lt;span class="w"&gt; &lt;/span&gt;--no-pager
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Verify device is connected and recognized by the kernel:&lt;/strong&gt;&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;ls&lt;span class="w"&gt; &lt;/span&gt;/dev/input/by-id/&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;grep&lt;span class="w"&gt; &lt;/span&gt;-i&lt;span class="w"&gt; &lt;/span&gt;logi
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;</content><category term="TIL"/><category term="linux"/><category term="hardware"/><category term="productivity"/></entry><entry><title>The Cure for the Vibe Coding Hangover</title><link href="https://gallon.me/the-cure-for-the-vibe-coding-hangover.html" rel="alternate"/><published>2025-09-01T00:00:00-05:00</published><updated>2025-09-01T00:00:00-05:00</updated><author><name>Corey</name></author><id>tag:gallon.me,2025-09-01:/the-cure-for-the-vibe-coding-hangover.html</id><summary type="html">&lt;p&gt;A &lt;strong&gt;practical framework&lt;/strong&gt; for building software with AI agents.&lt;/p&gt;</summary><content type="html">&lt;p&gt;A &lt;strong&gt;practical framework&lt;/strong&gt; for building software with AI agents.&lt;/p&gt;
&lt;p&gt;This was ultimately presented as a talk at the &lt;a href="https://www.youtube.com/watch?v=JsKTQbT58BY"&gt;AI Engineer Code Summit&lt;/a&gt; in November 2025.&lt;/p&gt;
&lt;p&gt;Inspiration strikes. You've got an idea, and you know exactly how you're going to build it: &lt;strong&gt;let's vibe code, baby!&lt;/strong&gt; You fire up your favorite AI coding agent, you jam in those prompts, and you hand it over. The app works. This is what 10x engineering really feels like. You're a genius -- a rebel in the AI revolution.&lt;/p&gt;
&lt;p&gt;Then Monday rolls around. You want to add a feature, or change the way something works, and you realize that you don't understand it, you can't maintain it, and you have to throw most or all of it away.&lt;/p&gt;
&lt;p&gt;&lt;img alt="A tiger passed out on the floor of a dark, messy dev room -- energy-drink cans, a pizza box and a rubber duck strewn about, a coder hunched at a wall of glowing monitors, a neon hexagon logo on the wall." src="./images/vibe-coding-hangover/the-hangover.png"&gt;&lt;/p&gt;
&lt;p&gt;That's the hangover. Vibe coding is the low-spec, zero-planning approach to AI-accelerated development that feels productive but results in brittle, unmaintainable demo-ware. The hangover is the despair that follows when you try to build maintainable, understandable software that way.&lt;/p&gt;
&lt;p&gt;There's a cure, though, and it's a framework for building with AI coding agents. That's what this is about.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Vibe coding feels like 10x engineering right up until the moment you have to live with what it built.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id="who-this-is-for"&gt;Who This Is For&lt;/h2&gt;
&lt;p&gt;You'll dig this if you value programming as a daily learning experience. If you want to understand and own the software you write with AI coding agents, just as you own all the other software you write. If you want to be the boss of the coding agents, not their confused intern. If working with agents lately makes you feel like a prompt jockey and no longer an AI engineer. If you're sick of throwing away code, burning time and tokens. Or if you want to use coding agents to build production applications that do real work.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;On the other hand ...&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img alt="The IT Crowd's Roy in a black &amp;quot;NO.&amp;quot; t-shirt, giving a flat, deadpan stare." src="./images/vibe-coding-hangover/not-for-you.png"&gt;&lt;/p&gt;
&lt;p&gt;This isn't for you if programming is a job and not a craft you're refining -- and that works for you. If you're satisfied having AI just do it for you without needing to understand how or why. Or if vibe coding gets you what you need and that's good enough. None of that is a judgment. It's just a very different path than the one I'm taking here.&lt;/p&gt;
&lt;h2 id="the-framework-in-overview"&gt;The Framework in Overview&lt;/h2&gt;
&lt;p&gt;&lt;img alt="The Framework on one slide: three rows -- Principles (a row of emblem icons), Process (the planning flow beside the implementation loop), and Tools (four emblem icons)." src="./images/vibe-coding-hangover/framework-three-pillars.png"&gt;&lt;/p&gt;
&lt;p&gt;The Framework has three pillars. &lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Principles&lt;/strong&gt; are the philosophy underpinning all of it. &lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Process&lt;/strong&gt; is the workflow for actually getting software built using AI.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Tools&lt;/strong&gt; are the accelerators and enablers of the process -- which also reflect the principles.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;So what can you build with it? Really, anything. The Framework is adaptive to all types of software.&lt;/p&gt;
&lt;p&gt;&lt;img alt="What can you build with The Framework? The key point: these aren't toys -- they're real software applications that do real work every day, evolved and maintained at breakneck pace by AI engineers." src="./images/vibe-coding-hangover/what-you-can-build.png"&gt;&lt;/p&gt;
&lt;p&gt;Here are a few examples of working software in the wild right now, built with this approach: &lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;specialized litigation support applications for law firms; &lt;/li&gt;
&lt;li&gt;real-time appliance monitoring packages for smart cooking devices; &lt;/li&gt;
&lt;li&gt;digital publishing systems for dynamic content replatforming; &lt;/li&gt;
&lt;li&gt;a code execution environment for secure, isolated development workflows; &lt;/li&gt;
&lt;li&gt;a context-management suite that preserves conversational data across AI sessions; &lt;/li&gt;
&lt;li&gt;a digital media production system for studios &lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;... and on and on. Plus a litany of smaller tools and utilities used daily to automate routine tasks.&lt;/p&gt;
&lt;p&gt;The point is that these aren't toys. They're real software applications that do real work every day, and they're evolved and maintained at breakneck pace by AI engineers who apply this Framework.&lt;/p&gt;
&lt;h2 id="the-principles"&gt;The Principles&lt;/h2&gt;
&lt;p&gt;&lt;img alt="The ten principles grouped into three categories -- General, Planning, and Implementation -- each shown as a row of emblem icons." src="./images/vibe-coding-hangover/principles-overview.png"&gt;&lt;/p&gt;
&lt;p&gt;There are ten principles. They map across three groups: &lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;general principles that apply overarchingly, &lt;/li&gt;
&lt;li&gt;principles that skew toward the planning phase, and &lt;/li&gt;
&lt;li&gt;principles that skew toward implementation. &lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;For each one, it helps to understand the problem it came from, the idea that answers it, and the one line I use to remember it.&lt;/p&gt;
&lt;h3 id="1-ai-engineering-is-accelerated-learning"&gt;1. AI Engineering Is Accelerated Learning&lt;/h3&gt;
&lt;p&gt;&lt;img alt="Principle: AI Engineering Is Accelerated Learning -- A-Always, B-Be, L-Learning. Always be learning." src="./images/vibe-coding-hangover/principle-01-accelerated-learning.png"&gt;&lt;/p&gt;
&lt;p&gt;The problem this came from: treating AI coding agents as pure productivity tools, just to crank out code faster. Using AI to generate software and learning nothing from the process. Six months later, being no better an engineer -- plateaued. Or worse, becoming dependent on AI for debugging, modifications, architectural decisions. That's not AI augmentation. That's AI dependency.&lt;/p&gt;
&lt;p&gt;The Framework isn't just about building faster -- it's a learning system. Every step creates specific learning opportunities, so you're not just shipping software, you're building yourself. The software is valuable, but the engineer you become is exponentially more valuable.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Always. Be. Learning. A-always, B-be, L-learning.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img alt="The &amp;quot;A-B-C, Always Be Closing&amp;quot; chalkboard scene from Glengarry Glen Ross -- riffed here as Always Be Learning." src="./images/vibe-coding-hangover/always-be-closing.jpg"&gt;&lt;/p&gt;
&lt;h3 id="2-you-are-the-architect-the-agent-is-the-implementer"&gt;2. You Are the Architect, the Agent Is the Implementer&lt;/h3&gt;
&lt;p&gt;&lt;img alt="Principle: You are the Architect, the Agent is the Implementer -- delegate the doing, not the thinking." src="./images/vibe-coding-hangover/principle-02-architect-implementer.png"&gt;&lt;/p&gt;
&lt;p&gt;The problem: treating AI agents as replacements for architectural thinking, rather than implementers of your decisions once those decisions are well-specified.&lt;/p&gt;
&lt;p&gt;Keep the architect/implementer boundary crystal clear. You own the thinking -- architecture and interfaces, the intent of the system, its structure, the design decisions and their trade-offs. The agent handles the doing -- implementation, typing code, following patterns, implementing the tests you specify, banging out boilerplate.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Delegate the doing, not the thinking.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id="3-slow-down-and-iterate-to-go-fast"&gt;3. Slow Down and Iterate to Go Fast&lt;/h3&gt;
&lt;p&gt;&lt;img alt="Principle: Slow Down and Iterate to Go Fast -- compound progress, accelerate velocity." src="./images/vibe-coding-hangover/principle-03-slow-down-iterate.png"&gt;&lt;/p&gt;
&lt;p&gt;It's a little counterintuitive. The problem is the starting-over cycle: without deliberate iteration on validated work, you repeatedly start from scratch. Three months in, you've got multiple abandoned attempts instead of one consistently improving system.&lt;/p&gt;
&lt;p&gt;Deliberate iteration enables compounding returns on both understanding and productivity. Week one feels slow. Week two builds momentum. Week three is dramatically faster.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Compound progress, accelerate velocity.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id="4-specification-prompt-engineering"&gt;4. Specification &amp;gt; Prompt Engineering&lt;/h3&gt;
&lt;p&gt;&lt;img alt="Principle: Specification &amp;gt; Prompt Engineering -- write the blueprint, not the prompt." src="./images/vibe-coding-hangover/principle-04-specification.png"&gt;&lt;/p&gt;
&lt;p&gt;The problem: prompt engineering treats AI interaction as an optimization problem rather than a communication problem -- hunting for magic words that produce the right output, instead of clearly defining what "right" means.&lt;/p&gt;
&lt;p&gt;Specifications are different from prompts. A specification is a structured, precise definition of requirements, behavior, interfaces, and acceptance criteria. Writing one forces architectural thinking: you have to understand the problem completely, define interfaces precisely, and anticipate edge cases. In turn, the specification gives the agent clear, unambiguous direction -- it implements what you specified, not what it interpreted from a conversational prompt.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Write the blueprint, not the prompt.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id="5-define-done-before-implementing"&gt;5. Define "Done" Before Implementing&lt;/h3&gt;
&lt;p&gt;&lt;img alt="Principle: Define &amp;quot;Done&amp;quot; Before Implementing -- specify success, then build." src="./images/vibe-coding-hangover/principle-05-define-done.png"&gt;&lt;/p&gt;
&lt;p&gt;The problem: starting implementation without executable tests and observable success criteria means the agent has no clear completion criteria and no immediate feedback. It can't self-validate, can't self-correct, and doesn't know when it's done -- at least not in a way consistent with your specifications.&lt;/p&gt;
&lt;p&gt;Defining "done" up front keeps you thinking deeply about requirements, and it lets the agent work autonomously. Tests defined upfront give the agent clear stop conditions and immediate feedback during implementation. And there's more than tests: the multi-sensory validation we'll get to lets agents observe through visual senses (what renders), auditory senses (what they hear through logs and errors), and tactile senses (how they interact with the system). Tests verify the correctness of the implementation; the senses reveal the actual behavior of the software as it's being built. A feature is done when the tests pass and the senses come back clean.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Specify success, then build.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id="6-feature-atomicity"&gt;6. Feature Atomicity&lt;/h3&gt;
&lt;p&gt;&lt;img alt="Principle: Feature Atomicity -- reduce until irreducible." src="./images/vibe-coding-hangover/principle-06-feature-atomicity.png"&gt;&lt;/p&gt;
&lt;p&gt;The problem: writing non-atomic features leaves the decomposition work for implementation time, which forces the agent to make architectural decisions on the fly.&lt;/p&gt;
&lt;p&gt;Feature atomicity forces you to completely decompose each feature during specification, which then lets the agent implement within a manageable scope. Features become implementation work units -- atomic, irreducible tasks ready for an agent to execute completely. Keep them as small as possible to make agent implementation as successful as possible.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Reduce until irreducible.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id="7-dependency-driven-development"&gt;7. Dependency-Driven Development&lt;/h3&gt;
&lt;p&gt;&lt;img alt="Principle: Dependency-Driven Development -- schedule implementation by dependencies." src="./images/vibe-coding-hangover/principle-07-dependency-driven.png"&gt;&lt;/p&gt;
&lt;p&gt;The problem: implementing without explicit dependency analysis treats all features as independent, when we know they actually form an interconnected graph.&lt;/p&gt;
&lt;p&gt;Dependency-driven development forces you to understand how features relate and integrate -- and it ensures the agent never implements a feature that depends on incomplete work.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Schedule implementation by dependencies.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id="8-implement-one-atomic-feature-at-a-time"&gt;8. Implement One Atomic Feature at a Time&lt;/h3&gt;
&lt;p&gt;&lt;img alt="Principle: Implement One Atomic Feature at a Time -- complete one, commit one, continue." src="./images/vibe-coding-hangover/principle-08-one-feature-at-a-time.png"&gt;&lt;/p&gt;
&lt;p&gt;Now the implementation-related principles. The problem: working on multiple features treats implementation as parallel streams that can be context-switched freely. But implementation quality depends on sustained focus, complete context, and very tight feedback loops. Jumping between features fragments your focus.&lt;/p&gt;
&lt;p&gt;So the agent implements a single, atomically-defined feature. You study it and understand it. You validate that it works. You commit it as a checkpoint. Then you move to the next. That rhythm builds both momentum and deepening understanding -- working software and engineering knowledge at the same time.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Complete one, commit one, continue.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id="9-context-engineering-and-management"&gt;9. Context Engineering and Management&lt;/h3&gt;
&lt;p&gt;&lt;img alt="Principle: Context Engineering &amp;amp; Management -- curate context, don't accumulate it." src="./images/vibe-coding-hangover/principle-09-context-engineering.png"&gt;&lt;/p&gt;
&lt;p&gt;The problem: treating context as something that just happens automatically, rather than something you actively engineer. You let conversation history passively accumulate instead of curating what actually matters. And if you don't build context resilience, state eventually fails to persist and you lose continuity.&lt;/p&gt;
&lt;p&gt;So don't rely on conversational state persisting. Capture architectural decisions in persistent documents -- specifications, plans, design documents -- and build context from those artifacts, not from yours or your agents' memory.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Curate context, don't accumulate it.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id="10-make-it-work-make-it-right-make-it-fast"&gt;10. Make It Work, Make It Right, Make It Fast&lt;/h3&gt;
&lt;p&gt;&lt;img alt="Principle: Make it Work, Make it Right, Make it Fast -- build, learn, improve." src="./images/vibe-coding-hangover/principle-10-make-it-work.png"&gt;&lt;/p&gt;
&lt;p&gt;This one is borrowed from the annals of software engineering. The problem is treating all three phases as equally important from the start, or trying to achieve them all at once.&lt;/p&gt;
&lt;p&gt;The Framework focuses on getting to &lt;em&gt;make it work&lt;/em&gt; -- working software you can ship and use. Only after real usage reveals what matters do you selectively invest in &lt;em&gt;make it right&lt;/em&gt; and &lt;em&gt;make it fast&lt;/em&gt;. So stop pursuing elegance and performance upfront. Direct the agent explicitly to make it work -- a simple, functional implementation that passes tests and ships quickly -- and let real usage reveal what deserves further investment.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Build, ship, learn, improve.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img alt="A wide-eyed, open-mouthed young developer staring in astonishment -- our vibe coder, mind blown." src="./images/vibe-coding-hangover/interlude-astonished.png"&gt;&lt;/p&gt;
&lt;p&gt;So there they are -- ten principles, the philosophy that makes The Framework work. Mind sufficiently blown? Stick with me; it gets better.&lt;/p&gt;
&lt;h2 id="the-process"&gt;The Process&lt;/h2&gt;
&lt;p&gt;The process is where we put the principles to work -- principles in action. It has two distinct phases: &lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;the &lt;strong&gt;planning phase&lt;/strong&gt;, where you do all the architectural thinking to define what to build, and &lt;/li&gt;
&lt;li&gt;the &lt;strong&gt;implementation phase&lt;/strong&gt;, where the agent executes your specifications with your oversight and validation.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img alt="Process (the Workflow): the Planning column -- Vision, Feature Specification, Dependency Analysis, Implementation Planning -- beside the Implementation loop: Assemble Context, Write Code, Execute &amp;amp; Sense, Test &amp;amp; Validate, Analyze Feedback, Refine, and back to Write Code." src="./images/vibe-coding-hangover/process-two-phases.png"&gt;&lt;/p&gt;
&lt;p&gt;Planning produces the artifacts that enable autonomous agent implementation. Implementation then uses those artifacts to build working software, feature by feature.&lt;/p&gt;
&lt;h3 id="the-planning-phase"&gt;The Planning Phase&lt;/h3&gt;
&lt;p&gt;Planning is where you complete your architectural thinking. You transform a vague project idea into atomic, sequenced, fully-specified features ready for implementation. This is purely your work -- the architectural decisions, the decomposition, the specification writing, the dependency analysis. The agent can assist as a thinking partner, but you make every decision.&lt;/p&gt;
&lt;p&gt;The five planning steps are sequential and build on each other: &lt;strong&gt;Vision → Features → Specification → Dependencies → Plan.&lt;/strong&gt; It's a highly iterative process of extracting and refining your thinking into tangible artifacts. The input and output of each step are a template and a completed template, respectively -- a lot of work has gone into well-structured templates that both guide the thinking and capture the results.&lt;/p&gt;
&lt;h4 id="planning-step-1-vision-capture"&gt;Planning Step 1: Vision Capture&lt;/h4&gt;
&lt;p&gt;&lt;img alt="Planning Step 1, Vision Capture: input a vague project idea, iterate until complete and clear, output the Master Project Specification." src="./images/vibe-coding-hangover/planning-01-vision-capture.png"&gt;&lt;/p&gt;
&lt;p&gt;The purpose is to transform your vague project idea into a complete, structured Master Project Specification that articulates the problem, the users, the functionality, the scope, and the workflows.&lt;/p&gt;
&lt;p&gt;The problem it solves: your initial idea exists only in your head, and it's incomplete. You have a general sense of the problem and an approach, but the details are fuzzy, the implicit assumptions are unexamined, and critical aspects are unformed. Without structured exploration you can't communicate your thinking, articulate requirements clearly, or create a shared foundation the agent can build on.&lt;/p&gt;
&lt;p&gt;So you think out loud with an agent -- optional, but strongly recommended -- to refine and capture your vision across five sections:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Project purpose&lt;/strong&gt; -- the problem you're solving, who experiences it, and the core value your software delivers.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Essential functionality&lt;/strong&gt; -- the three to five fundamental workflows that solve the problem.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Scope boundaries&lt;/strong&gt; -- explicit &lt;em&gt;now / not / next&lt;/em&gt; decisions: &lt;em&gt;now&lt;/em&gt; (must-have for the make-it-work version), &lt;em&gt;not&lt;/em&gt; (out of scope), &lt;em&gt;next&lt;/em&gt; (future enhancements).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Technical context&lt;/strong&gt; -- where it runs, how users interact, what systems it connects to.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Workflow details&lt;/strong&gt; -- for each core workflow, the goal, the high-level steps, and the expected outcome.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;You iterate until the vision is clear and complete. The agent is great here for surfacing gaps, suggesting edge cases, and probing assumptions -- but you make every decision. The output is the &lt;strong&gt;Master Project Specification&lt;/strong&gt;, the foundation for extracting features in step two.&lt;/p&gt;
&lt;h4 id="planning-step-2-feature-identification-and-categorization"&gt;Planning Step 2: Feature Identification and Categorization&lt;/h4&gt;
&lt;p&gt;&lt;img alt="Planning Step 2, Feature Identification &amp;amp; Categorization: from the Master Project Specification to a categorized Feature Inventory." src="./images/vibe-coding-hangover/planning-02-feature-identification.png"&gt;&lt;/p&gt;
&lt;p&gt;The purpose is to systematically extract every unit of functionality from your Master Project Specification and organize them into a categorized Feature Inventory.&lt;/p&gt;
&lt;p&gt;You don't jump straight from high-level vision to detailed feature specs -- that's too big a leap. So you work through the specification section by section with targeted extraction questions: what foundational capabilities does the system need; what discrete capabilities does each workflow require; what infrastructure makes the make-it-work version work now; what platform, integration, and interface features are needed; for each workflow, what handles input, processing, output, errors, and feedback; and across everything, what security, logging, configuration, and testing features span the system. You document each feature's source for traceability.&lt;/p&gt;
&lt;p&gt;You build the raw feature list first -- capturing every capability, not organizing yet -- and challenge completeness ("what handles errors? what validates input? what provides feedback?"). Then you analyze for natural groupings, settling on three to seven categories that reflect how your specific software is structured, assign each feature to its best-fit category with a unique ID (like CORE-001 or API-101), and give each an initial complexity estimate of easy, medium, or hard. The categories emerge from your actual features, not from a predetermined template. The output is the &lt;strong&gt;Feature Inventory&lt;/strong&gt;.&lt;/p&gt;
&lt;h4 id="planning-step-3-iterative-specification-development"&gt;Planning Step 3: Iterative Specification Development&lt;/h4&gt;
&lt;p&gt;&lt;img alt="Planning Step 3, Specification Development: from the Feature Inventory to atomic Feature Specifications, checking for gaps and atomicity." src="./images/vibe-coding-hangover/planning-03-specification-development.png"&gt;&lt;/p&gt;
&lt;p&gt;This is the critical step. The purpose is to transform each feature from the inventory into a complete, atomic, implementation-ready specification that defines exactly what to build, how it will be validated, and what it depends on.&lt;/p&gt;
&lt;p&gt;You collaborate with an agent to refine each feature through a three-level pattern. First, a &lt;strong&gt;user story&lt;/strong&gt; -- "as a [user type], I want to [action] so that I can [benefit]" -- capturing who needs this, what they're doing, and why. Then the &lt;strong&gt;implementation contracts&lt;/strong&gt;, in three levels of increasing precision:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Level 1, plain English&lt;/strong&gt; -- what the feature does in natural language: what it receives, what it does, what it produces.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Level 2, logic flow&lt;/strong&gt; -- structured pseudocode with clear INPUT, step-by-step LOGIC, and defined OUTPUT.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Level 3, formal interfaces&lt;/strong&gt; -- exact signatures, data structures, and API specs: precise input types, return types, and errors.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Then the &lt;strong&gt;validation contracts&lt;/strong&gt;, also in three levels:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Level 1, plain-English scenarios&lt;/strong&gt; -- every situation that needs validation: happy path, error cases, edge cases, security properties.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Level 2, test logic&lt;/strong&gt; -- each scenario as GIVEN/WHEN/THEN, with setup, trigger, and expected outcomes.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Level 3, formal test definitions&lt;/strong&gt; -- exact test interfaces with setup, inputs, precise assertions, and teardown.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Then you &lt;strong&gt;validate atomicity&lt;/strong&gt;: can this be implemented in a single focused session? If the spec feels scattered or describes multiple capabilities, you split it and repeat. Finally you &lt;strong&gt;identify dependencies&lt;/strong&gt; -- which other features must exist before this one can be implemented -- documented as explicit, binary dependencies: it either depends on another feature or it doesn't. The output is a complete &lt;strong&gt;Atomic Feature Specification&lt;/strong&gt; for every feature in the inventory.&lt;/p&gt;
&lt;h4 id="planning-step-4-dependency-analysis"&gt;Planning Step 4: Dependency Analysis&lt;/h4&gt;
&lt;p&gt;&lt;img alt="Planning Step 4, Dependency Analysis: from atomic Feature Specifications to a validated Dependency Matrix and Graph, iterating until cycles are resolved." src="./images/vibe-coding-hangover/planning-04-dependency-analysis.png"&gt;&lt;/p&gt;
&lt;p&gt;The purpose is to transform your complete set of feature specifications into a validated dependency matrix that defines the exact order features can be implemented in -- eliminating circular dependencies and revealing natural phases.&lt;/p&gt;
&lt;p&gt;Your specs contain accurate dependency declarations, but they're scattered across individual documents. You have the local picture (feature X depends on feature Y) but not the global one. Without a synthesized view you can't see the complete graph, detect cycles that span multiple features, identify the natural implementation phases, or know what has to be built first.&lt;/p&gt;
&lt;p&gt;So you &lt;strong&gt;extract the matrix&lt;/strong&gt; -- every feature is a row and a column, and you mark an X where a row feature depends on a column feature. You &lt;strong&gt;generate a graph&lt;/strong&gt; from it (Graphviz, Mermaid, your pick) with features as nodes and dependencies as edges, which makes cycles immediately visible as closed loops and reveals the layered structure. You &lt;strong&gt;validate and clean&lt;/strong&gt; with the binary dependency test on every mark: does the row feature actually require the column feature's specific output, configuration, or functionality to work? If yes, keep it; if no -- if it's only coordination or tool-sharing -- remove it. You &lt;strong&gt;regenerate the graph&lt;/strong&gt;, then &lt;strong&gt;detect cycles&lt;/strong&gt;, and where you find them you apply resolution strategies in order: first dependency elimination (re-examine with the binary test), then revised specification (rethink interfaces so features don't need each other's output), then feature splitting (maybe it wasn't atomic), and only as a last resort, consolidation. You iterate -- update matrix, regenerate graph, recheck -- until zero cycles remain. The outputs are the &lt;strong&gt;validated Dependency Matrix&lt;/strong&gt; and the &lt;strong&gt;Dependency Graph&lt;/strong&gt;.&lt;/p&gt;
&lt;h4 id="planning-step-5-implementation-plan-development"&gt;Planning Step 5: Implementation Plan Development&lt;/h4&gt;
&lt;p&gt;&lt;img alt="Planning Step 5, Implementation Planning: from the Dependency Matrix and Graph to a phase-organized Implementation Plan." src="./images/vibe-coding-hangover/planning-05-implementation-plan.png"&gt;&lt;/p&gt;
&lt;p&gt;The final planning step transforms the validated dependency matrix into a comprehensive, phase-organized implementation roadmap -- sequencing features into dependency layers, defining phase completion criteria, and establishing the validation strategies that enable the implementation loop.&lt;/p&gt;
&lt;p&gt;Without this, you face implementation chaos: even with complete specs and a validated matrix, you can't say which features come first and in what order, or when it's safe to start a feature that depends on earlier work. So you &lt;strong&gt;organize phases via a topological sort&lt;/strong&gt;: features with no dependencies are phase one; features depending only on phase one are phase two; and so on, each phase depending only on previous phases. You verify that no two features within a phase depend on each other, and you identify the critical path -- the longest dependency chain. Then you do &lt;strong&gt;validation strategy planning&lt;/strong&gt;: for each phase, binary success criteria -- what tests must pass, what integration points must work, how you'll verify features work together -- and feedback loops that enable autonomous refinement and binary progress tracking (a feature is implemented or it isn't; there's no 20%-done). Finally, &lt;strong&gt;implementation sequencing&lt;/strong&gt;: phase gates, guidance for how the agent selects the next feature, a blocker-management process, and progress-tracking mechanisms at the feature level, the phase level, and along the critical path. The output is the &lt;strong&gt;Implementation Plan&lt;/strong&gt;.&lt;/p&gt;
&lt;h3 id="the-implementation-loop"&gt;The Implementation Loop&lt;/h3&gt;
&lt;p&gt;Implementation is where your planning artifacts guide the transformation of specifications into working, tested software. Unlike planning, which is linear and proceeds through five distinct steps, implementation is a tight, rapid loop run repeatedly for each atomic feature.&lt;/p&gt;
&lt;h4 id="the-multi-sensory-feedback-loop"&gt;The Multi-Sensory Feedback Loop&lt;/h4&gt;
&lt;p&gt;&lt;img alt="Multi-Sensory Feedback -- the three &amp;quot;senses&amp;quot; coding agents need, shown as emblem icons: an eye (visual), an ear (auditory), and a hand (tactile)." src="./images/vibe-coding-hangover/multi-sensory-feedback.png"&gt;&lt;/p&gt;
&lt;p&gt;This is a really key idea. The agent implements code, then executes it while gathering feedback through three digital senses: &lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;a &lt;strong&gt;visual&lt;/strong&gt; sense (what renders), &lt;/li&gt;
&lt;li&gt;an &lt;strong&gt;auditory&lt;/strong&gt; sense (what the system reports), and &lt;/li&gt;
&lt;li&gt;a &lt;strong&gt;tactile&lt;/strong&gt; sense (how interactions respond). &lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This sensory feedback provides rich diagnostic information about what's actually happening in the application. The agent also runs formal tests against the acceptance criteria -- but by correlating sensory feedback with test results, it understands both &lt;em&gt;what&lt;/em&gt; failed, from the tests, and &lt;em&gt;why&lt;/em&gt; it failed, from the senses. The loop continues until all acceptance criteria pass and all senses report clean execution.&lt;/p&gt;
&lt;h4 id="implementation-step-1-context-assembly"&gt;Implementation Step 1: Context Assembly&lt;/h4&gt;
&lt;p&gt;&lt;img alt="Implementation Step 1, Context Assembly: inputs -- the Implementation Plan, the atomic Feature Specification, and @-referenced dependencies (specs and code) -- assembled into a Curated Context Package." src="./images/vibe-coding-hangover/implementation-01-context-assembly.png"&gt;&lt;/p&gt;
&lt;p&gt;The purpose is to transform planning artifacts into a curated context package that enables autonomous feature implementation within a single coding session.&lt;/p&gt;
&lt;p&gt;You have atomic features fully specified and sequenced, but you can't just throw everything at the agent and hope. Dumping entire planning documents into a session wastes context window on irrelevant information, leads the agent to make decisions without critical context (or to stop and ask), and turns what should be autonomous implementation into constant back-and-forth.&lt;/p&gt;
&lt;p&gt;So you curate exactly what this one feature needs, in four steps. &lt;strong&gt;Feature specification assembly&lt;/strong&gt;: include the complete spec -- user story, technical contracts, acceptance criteria -- and &lt;code&gt;@&lt;/code&gt;-reference its dependencies; this is the primary blueprint. &lt;strong&gt;Dependency context gathering&lt;/strong&gt;: follow those &lt;code&gt;@&lt;/code&gt; references, each pointing to a dependency's specification &lt;em&gt;and&lt;/em&gt; its actual implemented code (per The Framework, all dependencies are implemented previously), so the agent knows exactly what to integrate with. &lt;strong&gt;Implementation guidance extraction&lt;/strong&gt;: pull only the relevant sections of the Implementation Plan -- what phase this is, the phase completion criteria, the validation strategy. &lt;strong&gt;Sensory capability enablement&lt;/strong&gt;: read the acceptance criteria to identify which senses are required -- visual language like "sees / displays / renders" needs the visual tools, logging and error language needs the auditory tools, interaction language like "clicks / submits / completes" needs the tactile tools -- and &lt;code&gt;@&lt;/code&gt;-reference the appropriate tool-usage guides (written once per tool, reusable across all features). The output is the &lt;strong&gt;Curated Context Package&lt;/strong&gt;.&lt;/p&gt;
&lt;h4 id="implementation-step-2-the-implementation-loop"&gt;Implementation Step 2: The Implementation Loop&lt;/h4&gt;
&lt;p&gt;&lt;img alt="Implementation Step 2, the Implementation Loop -- the one step where the AI writes code; input the Curated Context Package, output a fully implemented feature." src="./images/vibe-coding-hangover/implementation-02-implementation-loop.png"&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;This is the only step in the entire Framework where the AI writes code.&lt;/strong&gt; The purpose is to transform an atomic feature specification into working, tested code.&lt;/p&gt;
&lt;p&gt;Without structure here, agents either write all the code before testing anything -- so problems accumulate undetected and you debug many interconnected issues at once -- or they write and test ad hoc, missing the problems tests don't catch: tests pass but the UI doesn't render, the workflow completes but errors fill the logs, the feature "works" but the interactions are broken. Because features are atomic, the complete implementation fits in one context window, so the agent keeps full understanding from start to finish -- no context loss, no reconstruction, no degraded fidelity.&lt;/p&gt;
&lt;p&gt;The loop runs like this. &lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Write code&lt;/strong&gt; following the specification's technical contracts, translating all three levels into working code so interfaces, inputs, outputs, and error handling match the spec exactly. &lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Execute and sense&lt;/strong&gt;: run it immediately and gather comprehensive feedback through the three senses based on the acceptance criteria. &lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Test and validate&lt;/strong&gt;: run all the test scenarios from the validation contracts for binary pass/fail signals. &lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Analyze feedback&lt;/strong&gt;: correlate signals across all active senses and the test results -- multiple senses reporting the same issue confirm a diagnosis, conflicting signals reveal hidden complexity -- and this integrated view shows both what failed and why, enabling &lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Targeted refinement&lt;/strong&gt;. &lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;You loop until the feature is complete: all tests pass and all senses report clean execution -- no errors in logs, no rendering issues, interactions working. Then the agent makes an atomic git commit containing only this feature's changes, with a structured message including the feature ID, a specification summary, validation confirmation, and implementation notes. The output is a fully working, tested feature, validated by all three senses and ready for integration.&lt;/p&gt;
&lt;p&gt;&lt;img alt="A wide-eyed, open-mouthed tiger in front of a glowing neon hexagon -- even the tiger's mind is blown." src="./images/vibe-coding-hangover/interlude-tiger.png"&gt;&lt;/p&gt;
&lt;p&gt;We've even blown the tiger's mind, now! Let's round the corner into tools.&lt;/p&gt;
&lt;h2 id="the-tools"&gt;The Tools&lt;/h2&gt;
&lt;p&gt;&lt;img alt="Tools (the Enablers): the four capabilities -- Coding Environment, Multi-Sensory Feedback, Context Engineering &amp;amp; Assembly, and Version Control &amp;amp; Progress Tracking." src="./images/vibe-coding-hangover/tools-overview.png"&gt;&lt;/p&gt;
&lt;p&gt;The Framework requires four foundational capabilities that enable the work done through the process. The Framework isn't prescriptive about &lt;em&gt;which&lt;/em&gt; tools -- only about the &lt;em&gt;capabilities&lt;/em&gt; those tools should possess.&lt;/p&gt;
&lt;h3 id="coding-environment"&gt;Coding Environment&lt;/h3&gt;
&lt;p&gt;&lt;img alt="Coding Environment: an AI coding agent, an execution sandbox, an IDE (or text editor), and voice input." src="./images/vibe-coding-hangover/tool-coding-environment.png"&gt;&lt;/p&gt;
&lt;p&gt;A complete development workspace that supports two fundamentally different kinds of work happening at once: your architectural thinking and planning, and the agent's autonomous implementation and testing. Four core components:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;An &lt;strong&gt;AI coding agent&lt;/strong&gt; -- obviously.&lt;/li&gt;
&lt;li&gt;An &lt;strong&gt;execution sandbox&lt;/strong&gt; -- a safe, isolated environment where all agent-written code executes and tests run. Autonomous implementation needs the freedom to experiment, iterate, and occasionally break things; the sandbox gives complete development capability in a disposable, risk-free space with easy reset and no consequences for failure.&lt;/li&gt;
&lt;li&gt;An &lt;strong&gt;IDE&lt;/strong&gt; (or a text editor, if you must) -- pick the one that suits you; I'll avoid the holy wars.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Voice input&lt;/strong&gt; -- a rapid-capture system that converts speech to text at thinking speed. Planning means externalizing architectural thinking, which is often incomplete, exploratory, and iterative; voice removes the typing bottleneck and lets you think out loud far faster than you can type. I can't oversell how massively impactful a good voice-input tool is in applying The Framework.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="multi-sensory-feedback-system"&gt;Multi-Sensory Feedback System&lt;/h3&gt;
&lt;p&gt;&lt;img alt="Multi-Sensory Feedback System: visual tools (UI rendering, system state, code structure), auditory tools (logs, errors, API responses, stack traces), and tactile tools (interaction and workflow testing)." src="./images/vibe-coding-hangover/tool-multi-sensory-feedback.png"&gt;&lt;/p&gt;
&lt;p&gt;A comprehensive validation infrastructure that gives agents the ability to observe their implementations through three complementary digital senses, enabling autonomous refinement through the same breadth of observation humans use during development:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Visual sense tools&lt;/strong&gt; -- direct observation of what was produced: UI rendering (screenshots, layout, styling), system state (database contents, configuration, session data), and code structure. Visual observation catches what logs and tests miss -- broken rendering, incorrect state, structural issues.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Auditory sense tools&lt;/strong&gt; -- what the system reports: logs (the system narrating its operations), errors and warnings, API responses, and stack traces. This explains &lt;em&gt;why&lt;/em&gt; things fail, not just that they failed.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Tactile sense tools&lt;/strong&gt; -- active interaction testing: simulating and executing user workflows end-to-end, API request/response cycles, performance validation, security checks, and integration testing. These reveal whether the software behaves correctly under actual use, not just in isolated tests.&lt;/li&gt;
&lt;li&gt;An &lt;strong&gt;orchestration layer&lt;/strong&gt; (MCP or equivalent) -- the protocol layer that surfaces these tools and coordinates them into one integrated feedback system, delivering structured sensory data so the agent understands both what failed and why.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="context-engineering-and-assembly"&gt;Context Engineering and Assembly&lt;/h3&gt;
&lt;p&gt;&lt;img alt="Context Engineering &amp;amp; Assembly: @ cross-references, slash commands, framework templates, and markdown." src="./images/vibe-coding-hangover/tool-context-engineering.png"&gt;&lt;/p&gt;
&lt;p&gt;A systematic approach to assembling focused, complete context packages for agents:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;An &lt;strong&gt;&lt;code&gt;@&lt;/code&gt; cross-reference system&lt;/strong&gt; -- declarative linking that lets documents explicitly reference other documents, code files, or sections, enabling automatic context assembly by following the dependency chains. These aren't hyperlinks for human navigation; they're actionable declarations that automation can follow.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Slash commands&lt;/strong&gt; (or your environment's equivalent) -- process automation that triggers multi-step framework workflows from a single invocation, especially for context assembly, template instantiation, and implementation-session initialization.&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;template system&lt;/strong&gt; -- structured templates for every framework artifact (Master Project Specification, Feature Specification, Dependency Matrix, Implementation Plan, Implementation Record) that ensure consistent format and completeness. Without templates you reinvent the structure every time, and starting from scratch is exhausting.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Markdown documentation format&lt;/strong&gt; -- agents are so literate in it that it's vital to have tooling that converts inputs to markdown rapidly. Anything you want to communicate to an agent should be instantly convertible to markdown in your toolchain.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="version-control-and-progress-tracking"&gt;Version Control and Progress Tracking&lt;/h3&gt;
&lt;p&gt;&lt;img alt="Version Control &amp;amp; Progress Tracking: a version control system plus the Implementation Plan with progress tracking." src="./images/vibe-coding-hangover/tool-version-control.png"&gt;&lt;/p&gt;
&lt;p&gt;A dual-mechanism system for provenance and current-state visibility:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A &lt;strong&gt;version control system&lt;/strong&gt; -- I'll just say it: Git, or the equivalent. Implementation history through atomic feature commits. It's like saving progress in a video game: obvious and essential.&lt;/li&gt;
&lt;li&gt;The &lt;strong&gt;Implementation Plan with progress tracking&lt;/strong&gt; -- the same planning artifact from step five, doing double duty. Git shows what changed and when, but not the project's state; the Implementation Plan fills that gap, tracking which features are complete, which are blocked, and what's next. This is the simplest form of project tracking -- it can get more complex and more integrated -- but it's enough.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img alt="My tools stack: Claude Code in YOLO mode, YOLOsandbox, WinWhisper, Cursor for tab-completion, MCP, Playwright, tmux, Claude Code custom slash commands, GitHub, and the Implementation Plan." src="./images/vibe-coding-hangover/my-tools-stack.png"&gt;&lt;/p&gt;
&lt;p&gt;For the curious, that's my own tool stack.&lt;/p&gt;
&lt;h2 id="the-morning-after"&gt;The Morning After&lt;/h2&gt;
&lt;p&gt;That's The Framework: the principles, the process, and the tools -- and the way they reinforce one another, with each process step and each tool capability tracing back to the principles. Build this way and the Monday after isn't a hangover. You understand the software, you own it, you can maintain and extend it -- and you're a better engineer than you were the feature before.&lt;/p&gt;
&lt;p&gt;&lt;img alt="The same developer lying back, calm, as sunrise streams through the window -- the morning after, cured." src="./images/vibe-coding-hangover/the-cure.png"&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;The software is valuable. The engineer you become is exponentially more valuable.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img alt="Closing slide: VibeCodingHangover.com." src="./images/vibe-coding-hangover/vibecodinghangover-com.png"&gt;&lt;/p&gt;
&lt;p&gt;If you enjoyed this and want the slides, the soundtrack, and the other resources, I built a site just for the talk: &lt;a href="https://vibecodinghangover.com"&gt;&lt;strong&gt;vibecodinghangover.com&lt;/strong&gt;&lt;/a&gt;. Happy hacking.&lt;/p&gt;
&lt;script type="application/ld+json"&gt;
{
  "@context": "https://schema.org",
  "@type": "Article",
  "headline": "The Cure for the Vibe Coding Hangover",
  "description": "A practical framework for building maintainable, production-grade software with AI coding agents.",
  "image": "https://gallon.me/images/feature_images/20250901_-_The_Cure_for_the_Vibe_Coding_Hangover_0.jpg",
  "author": {"@type": "Person", "name": "Corey Gallon", "url": "https://gallon.me/", "sameAs": ["https://gallon.me/", "https://x.com/coreygallon"]},
  "publisher": {"@type": "Person", "name": "Corey Gallon", "url": "https://gallon.me/"},
  "datePublished": "2025-09-01",
  "url": "https://gallon.me/the-cure-for-the-vibe-coding-hangover.html",
  "mainEntityOfPage": "https://gallon.me/the-cure-for-the-vibe-coding-hangover.html"
}
&lt;/script&gt;</content><category term="Writing"/><category term="agentic_coding"/><category term="ai_engineering"/><category term="conference"/></entry><entry><title>WTF is BM25? The Intuition Behind the Algorithm</title><link href="https://gallon.me/wtf-is-bm25-the-intuition-behind-the-algorithm.html" rel="alternate"/><published>2025-03-25T00:00:00-05:00</published><updated>2025-03-25T00:00:00-05:00</updated><author><name>Corey</name></author><id>tag:gallon.me,2025-03-25:/wtf-is-bm25-the-intuition-behind-the-algorithm.html</id><summary type="html">&lt;p&gt;BM25 is an unsung hero powering heaps of stuff you use every day – think search bars, recommendation systems, or even those fancy AI chatbots that somehow dig up the right info from a mountain of text (we're looking at you, Retrieval-Augmented Generation).&lt;/p&gt;</summary><content type="html">&lt;h1 id="why-should-you-give-a-toss-about-bm25"&gt;❓ Why Should You Give a Toss About BM25?&lt;/h1&gt;
&lt;p&gt;BM25 is an unsung hero powering heaps of stuff you use every day – think search bars, recommendation systems, or even those fancy AI chatbots that somehow dig up the right info from a mountain of text (we're looking at you, Retrieval-Augmented Generation).&lt;/p&gt;
&lt;p&gt;If you’re building anything that needs to sift through data – like a search tool for your blog, a customer support bot, or a system to find the juiciest research papers – BM25 is part of your secret sauce.  It’s fast, it’s clever, and it’s been battle-tested for decades, quietly outranking simpler methods while the modern AI models somehow get all the Instagram likes.  Understanding it means you can make sense of why some results pop to the top and others sink, whether you’re coding it yourself or just trying to impress your boss with some “I know how this works” swagger.&lt;/p&gt;
&lt;p&gt;Plus, in a world drowning in info, &lt;strong&gt;knowing how to rank what matters is important&lt;/strong&gt;. BM25 isn’t just maths – it’s the art of cutting through the noise.  So, whether you’re a dev, a data geek, or just someone who hates wading through irrelevant Google results, stick around. This algo’s got lessons for us all – and it might just save your sanity when the AI Apocalypse hits and we’re all searching for “how to reboot the robot overlords.”&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;We will build the intuition for how BM25 works, step by step,&lt;/strong&gt; and unravel this:&lt;/p&gt;
&lt;div class="math"&gt;$$
\text{Score}(D, Q) = \sum_{i=1}^{n} \text{IDF}(q_i) \cdot \frac{\text{TF}(q_i, D) \cdot (k_1 + 1)}{\text{TF}(q_i, D) + k_1 \cdot (1 - b + b \cdot \frac{|D|}{\text{avgdl}})} 
$$&lt;/div&gt;
&lt;p&gt;into this:&lt;/p&gt;
&lt;p&gt;&lt;span class="math"&gt;\(Score = Sum\ for\ Each\ Term(\ Adjusted\ Count\ of\ Term \times Rarity\ of\ Term\ )\)&lt;/span&gt;&lt;/p&gt;
&lt;h2 id="ground-control-to-major-tom"&gt;🚀 Ground Control to Major Tom&lt;/h2&gt;
&lt;p&gt;Imagine you're searching the Web for "Mars exploration" among this tiny library of 5 books (we'll use the term "document" interchangeably with "book").  How on Earth (or Mars) does a search engine perform keyword search to know which documents are most relevant to your search?  &lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;📱&lt;strong&gt;Mobile Readers Note&lt;/strong&gt;: If you're reading this on mobile, and any of the tables or formulas overflow the page, they will scroll if you swipe left on them like you're ducking a rando on Tinder!&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Here are our books:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Book&lt;/th&gt;
&lt;th&gt;Title&lt;/th&gt;
&lt;th&gt;Length (pages)&lt;/th&gt;
&lt;th&gt;"Mars" count&lt;/th&gt;
&lt;th&gt;"exploration" count&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;A&lt;/td&gt;
&lt;td&gt;Mars Exploration Guide&lt;/td&gt;
&lt;td&gt;50&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;B&lt;/td&gt;
&lt;td&gt;Space Encyclopedia&lt;/td&gt;
&lt;td&gt;200&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;C&lt;/td&gt;
&lt;td&gt;Planetary Science&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;D&lt;/td&gt;
&lt;td&gt;Exploration Techniques&lt;/td&gt;
&lt;td&gt;75&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;12&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;E&lt;/td&gt;
&lt;td&gt;Solar System&lt;/td&gt;
&lt;td&gt;150&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Left to your own considerable cleverness, how would you devise an algorithm to execute this search? &lt;/p&gt;
&lt;hr&gt;
&lt;h1 id="step-1-tonight-were-counting-words-like-its-1995-term-frequency"&gt;🔢 Step 1: Tonight We're Counting Words Like It's 1995 (Term Frequency)&lt;/h1&gt;
&lt;p&gt;If we were to try to solve this problem in the simplest possible way, perhaps the most basic way to determine relevance is to simply count how many times each search term appears in a document (book).  This is how early attempts at search worked, by the way, and isn't too dissimilar to how search capabilities on the Web may have worked in 1995.&lt;/p&gt;
&lt;p&gt;&lt;img alt="1999" src="images/20250325_prince.gif"&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;(Yes, I know the lyrics are "1999" ... Google was doing more sophisticated things than this by then ... just stay with me in 1995.)&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Let's score each document as follows:&lt;/p&gt;
&lt;p&gt;&lt;span class="math"&gt;\(Score = \text{Count of "Mars"} + \text{Count of "exploration"}\)&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;So, the book with the highest score is the highest ranked, and so on.&lt;/p&gt;
&lt;p&gt;Using this naive approach:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Book A&lt;/strong&gt;: 8 + 6 = 14 points&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Book B&lt;/strong&gt;: 10 + 4 = 14 points &lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Book C&lt;/strong&gt;: 0 + 0 = 0 points&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Book D&lt;/strong&gt;: 0 + 12 = 12 points&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Book E&lt;/strong&gt;: 0 + 0 = 0 points&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Good enough?  Nay, I say!&lt;/p&gt;
&lt;h2 id="the-problem-shortcomings-of-simple-term-counting"&gt;🔍 The Problem: Shortcomings of Simple Term Counting&lt;/h2&gt;
&lt;p&gt;Books A and B tie with 14 points – but, mate, surely the "Mars Exploration Guide" (Book A) is more focused on our search than some general "Space Encyclopedia" (Book B)!  This approach just won't do.  Let's fix it!&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;💡 &lt;strong&gt;Key Insight&lt;/strong&gt;: Simple word counting treats every word equally, which doesn't quite work well.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h1 id="step-2-not-all-words-are-created-equal-inverse-document-frequency"&gt;❗🟰  Step 2: Not All Words Are Created Equal (Inverse Document Frequency)&lt;/h1&gt;
&lt;p&gt;In our simple counting approach, we treated each occurrence of "Mars" and "exploration" as equally valuable ... and it kind of worked, until it didn't.  "Mars Exploration Guide" and "Space Encyclopedia" tied, despite one clearly being more about our search query.  This happened because we treated all words equally. But in searches, some words are better at targeting relevant results. "Mars" is a laser beam. "Exploration" is a flashlight. "The"? A fog machine.&lt;/p&gt;
&lt;p&gt;This suggests that the less frequent -- i.e. more rare -- a word is, the more useful it is as a search term.  That makes intuitive sense, also.  Imagine flipping through a phone book (no, really -- people &lt;em&gt;actually&lt;/em&gt; did this at a distant point in human history) looking for someone's phone number.  If your search is for a fellow named "Smith", you're going to have a much harder time than if you're looking for a professionally good-looking fellow called "Zoolander"!  The lesson?  The rarer a search term is, the faster it narrows or filters your search.&lt;/p&gt;
&lt;p&gt;How about we weight the counts of terms by their "rarity"?  Let’s adjust our scoring:&lt;/p&gt;
&lt;p&gt;&lt;span class="math"&gt;\(Score = (\text{Count of Term}) \times (\text{Rarity of Term}) + (\text{Count of Term}) \times (\text{Rarity of Term})\)&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;This immediately fixes a big part of our problem: words that appear everywhere ("exploration") get less credit than words that are rare ("Mars"), even if they appear just as often in a document.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;💡 &lt;strong&gt;Key Insight&lt;/strong&gt;: The rarer a term is in the whole collection, the more useful it is for identifying relevant documents.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;So how do we calculate that rarity score?&lt;/p&gt;
&lt;h2 id="simply-rare-its-hardly-there"&gt;✨ Simply Rare:  It's Hardly There&lt;/h2&gt;
&lt;p&gt;Again, barely taxing our considerable intellect, we could calculate the rarity score simply by expressing the rarity of a term as the percentage of documents that the term appears in.  Yeah?&lt;/p&gt;
&lt;div class="math"&gt;$$
Rarity\ Score = \frac{1}{\text{\# of documents the term appears in}}
$$&lt;/div&gt;
&lt;p&gt;We could, then, simply weight the counts of each term by the rarity of each term.  Tracking?&lt;/p&gt;
&lt;p&gt;&lt;span class="math"&gt;\(Score = \text{Count of "Mars"} \times \text{Rarity of "Mars"} + \text{Count of "exploration"} \times \text{Rarity of "exploration"}\)&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;So, then, we get the following scores for "Mars exploration" across each of the books in our library:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Book&lt;/th&gt;
&lt;th&gt;Title&lt;/th&gt;
&lt;th&gt;Mars Count&lt;/th&gt;
&lt;th&gt;Exploration Count&lt;/th&gt;
&lt;th&gt;Mars Rarity&lt;/th&gt;
&lt;th&gt;Exploration Rarity&lt;/th&gt;
&lt;th&gt;Score&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;A&lt;/td&gt;
&lt;td&gt;Mars Exploration Guide&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;0.5&lt;/td&gt;
&lt;td&gt;0.333&lt;/td&gt;
&lt;td&gt;6.0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;B&lt;/td&gt;
&lt;td&gt;Space Encyclopedia&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;0.5&lt;/td&gt;
&lt;td&gt;0.333&lt;/td&gt;
&lt;td&gt;6.333&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;C&lt;/td&gt;
&lt;td&gt;Planetary Science&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0.5&lt;/td&gt;
&lt;td&gt;0.333&lt;/td&gt;
&lt;td&gt;0.0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;D&lt;/td&gt;
&lt;td&gt;Exploration Techniques&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;12&lt;/td&gt;
&lt;td&gt;0.5&lt;/td&gt;
&lt;td&gt;0.333&lt;/td&gt;
&lt;td&gt;4.0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;E&lt;/td&gt;
&lt;td&gt;Solar System&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0.5&lt;/td&gt;
&lt;td&gt;0.333&lt;/td&gt;
&lt;td&gt;0.0&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Whilst this, again, kind of works, it's got some fairly significant shortcomings.&lt;/p&gt;
&lt;h2 id="the-problem-shortcomings-of-the-simplified-rarity"&gt;🔍 The Problem: Shortcomings of the Simplified Rarity&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Overvalues Rare Terms:&lt;/strong&gt; Simplified rarity &lt;em&gt;over-rewards&lt;/em&gt; extremely rare words, treating a term in 1 document as &lt;em&gt;10× more valuable&lt;/em&gt; than one in 10 docs.  A word in 1 document gets a rarity score of 1, while a word in 10 documents gets a rarity score of 0.10.  Similarly, a word in just 2 documents gets a rarity score of 0.5. This says that the word is half as valuable in our search, but is that really true?  Is a term that appears in 2 documents &lt;em&gt;really&lt;/em&gt; only half as useful as one that appears in 1?&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Drops Off Too Quickly:&lt;/strong&gt; As term frequency increases significantly, simplified rarity becomes very small.  In large collections, simplified rarity for frequent (common) terms becomes vanishingly small and meaningless.&lt;/p&gt;
&lt;p&gt;This simplified calculation of rarity also misses a key fact about what rarity means in terms of filtering power for search terms.&lt;/p&gt;
&lt;h2 id="rarity-power-why-uncommon-words-matter-more"&gt;💪 Rarity = Power: Why Uncommon Words Matter More&lt;/h2&gt;
&lt;p&gt;This is the core idea behind filtering power (fancily called "discriminative power") – the ability of a term to eliminate irrelevant documents.  The more irrelevant documents it can eliminate in a single step, the more power the term has.  &lt;/p&gt;
&lt;p&gt;Let’s take an example:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A word in 50% of documents gives you 1 "unit" of filtering power.  (Think about asking 1 yes/no question that perfectly eliminates half of the documents in the collection.)&lt;/li&gt;
&lt;li&gt;A word in 25% gives you 2 units.  (Think of asking 2 yes/no questions that, each, perfectly eliminate half of the remaining documents.)&lt;/li&gt;
&lt;li&gt;A word in 12.5% gives you 3.  (3 yes/no questions ...)&lt;/li&gt;
&lt;li&gt;A word in 1% gives you 6.6 units.  (Woah!)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;These units "filtering power" are like asking the # of units of yes/no questions all at once in order to reduce the size of the search area. How the hell did we know that the last one was 6.6 units?  &lt;/p&gt;
&lt;p&gt;&lt;img alt="Clever girl!" src="images/20250325_jurassic-park.gif"&gt;&lt;/p&gt;
&lt;p&gt;Each unit of filtering power halves your remaining search space. And this halving pattern is captured perfectly by our old mate from maths: 💥&lt;strong&gt;the logarithm&lt;/strong&gt;.💥  This means that a word that's 10× rarer (i.e. less frequent) isn't 10× more useful -- it's more like 2× more useful. Why? Because filtering power is logarithmic!&lt;/p&gt;
&lt;div class="math"&gt;$$Term\ Rarity\ \text{or}\ Term\ Filtering\ Power = \log_2\left(\frac{\text{Total # of Docs}}{\text{# of Docs Containing the Term}}\right)$$&lt;/div&gt;
&lt;p&gt;This gives us a clean measure of a term’s power to shrink the search haystack.  So, we were able to calculate the filtering power of a word that only appears in 1% of the documents collection as follows:&lt;/p&gt;
&lt;div class="math"&gt;$$Term\ Filtering\ Power = \log_2\left(\frac{1}{0.01}\right) = 6.6$$&lt;/div&gt;
&lt;p&gt;BM25 calls this, more formally, "Inverse Document Frequency (IDF)".  One way to think about IDF that you may find helpful is as follows:&lt;/p&gt;
&lt;div class="math"&gt;$$
\text{Term Frequency} = \frac{\text{# of Docs Containing the Term}}{\text{Total # of Docs}}
$$&lt;/div&gt;
&lt;p&gt;Soooo ... &lt;/p&gt;
&lt;div class="math"&gt;$$
\text{Inverse of Term Frequency} = \frac{\text{Total # of Docs}}{\text{# of Docs Containing the Term}}
$$&lt;/div&gt;
&lt;p&gt;Sooooooooo ... dressing this up in BM25-specific evening attire ... &lt;/p&gt;
&lt;div class="math"&gt;$$Inverse\ Document\ Frequency\ (IDF) = \log\left(\frac{\text{Total # of Docs}}{\text{# of Docs Containing the Term}}\right)$$&lt;/div&gt;
&lt;p&gt;Bam!&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;💡 &lt;strong&gt;Key Insight:&lt;/strong&gt; The less frequently a word appears in the document collection, the more filtering (discriminative) power it has – and that power grows logarithmically.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;What a veritable buffet of intellectual satisfaction this is proving!&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Let’s apply it to our lil' library:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;"Mars" appears in 2 of 5 books → &lt;code&gt;IDF = log₂(5 / 2) ≈ 1.32&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;"Exploration" appears in 3 of 5 books → &lt;code&gt;IDF = log₂(5 / 3) ≈ 0.74&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;We can rewrite our intuitive formula with some $10 words, so that:&lt;/p&gt;
&lt;p&gt;&lt;span class="math"&gt;\(\text{Score} = (\text{Count of Term}_1) \times (\text{Rarity of Term}_1) + (\text{Count of Term}_2) \times (\text{Rarity of Term}_2)\)&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;Becomes:&lt;/p&gt;
&lt;p&gt;&lt;span class="math"&gt;\(Score = TF("Mars") \times IDF("Mars") + TF("exploration") \times IDF("exploration")\)&lt;/span&gt;&lt;/p&gt;
&lt;h2 id="its-time-for-the-calculator"&gt;🧮 It's Time for the Calculator!&lt;/h2&gt;
&lt;p&gt;&lt;img alt="It's time for the percolator!" src="images/20250325_the-percolator.gif"&gt;
&lt;br&gt;&lt;em&gt;(Only the real ones from Chicago get that ...)&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Let's calc it down now:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Book&lt;/th&gt;
&lt;th&gt;Title&lt;/th&gt;
&lt;th&gt;Count of "Mars"&lt;/th&gt;
&lt;th&gt;Rarity of "Mars"&lt;/th&gt;
&lt;th&gt;Count of "exploration"&lt;/th&gt;
&lt;th&gt;Rarity of "exploration"&lt;/th&gt;
&lt;th&gt;Score&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;A&lt;/td&gt;
&lt;td&gt;Mars Exploration Guide&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;1.32&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;0.74&lt;/td&gt;
&lt;td&gt;15.00&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;B&lt;/td&gt;
&lt;td&gt;Space Encyclopedia&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;1.32&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;0.74&lt;/td&gt;
&lt;td&gt;16.17&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;C&lt;/td&gt;
&lt;td&gt;Planetary Science&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;1.32&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0.74&lt;/td&gt;
&lt;td&gt;0.00&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;D&lt;/td&gt;
&lt;td&gt;Exploration Techniques&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;1.32&lt;/td&gt;
&lt;td&gt;12&lt;/td&gt;
&lt;td&gt;0.74&lt;/td&gt;
&lt;td&gt;8.84&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;E&lt;/td&gt;
&lt;td&gt;Solar System&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;1.32&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0.74&lt;/td&gt;
&lt;td&gt;0.00&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2 id="the-problem-say-less-baby"&gt;🔍 The Problem: Say Less, Baby!&lt;/h2&gt;
&lt;p&gt;Our scoring is still not quite right. Book B edges out Book A because it says “Mars” more, even though it’s not focused on it.  (Like so many "AI influencers" on social media ...)  And Book D scores surprisingly high despite not mentioning “Mars” at all!&lt;/p&gt;
&lt;p&gt;So even with IDF (weighting by the rarity of the term), we’re missing something crucial – document focus. Or maybe… document length?&lt;/p&gt;
&lt;p&gt;Let’s fix that next!&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;🤵 &lt;strong&gt;Sound Smart at Dinner Parties&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;When "filtering power" puts on a tuxedo, it's known as "discriminative power"&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h1 id="step-3-the-enough-already-we-get-it-approach-diminishing-returns"&gt;🤦‍♂️ Step 3: The "Enough, Already – We Get It!" Approach (Diminishing Returns)&lt;/h1&gt;
&lt;p&gt;At the end of Step 2, we fixed a huge flaw: we now value &lt;em&gt;rarer&lt;/em&gt; words more (thanks to the homie IDF).  But two problems remain:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Book B&lt;/strong&gt; still scores higher than &lt;strong&gt;Book A&lt;/strong&gt;, despite A being more focused on Mars exploration.  &lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Book D&lt;/strong&gt; scores surprisingly well despite never mentioning "Mars" at all.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Recall that we've got the following expressions of BM25 thus far.  In simple terms:&lt;/p&gt;
&lt;p&gt;&lt;span class="math"&gt;\(Score = (Count\ of\ Term) \times (Rarity\ of\ Term) + (Count\ of\ Term) \times (Rarity\ of\ Term)\)&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;img alt="Oh, you fancy, huh?" src="images/20250325_fancy.gif"&gt;&lt;/p&gt;
&lt;p&gt;Fine, here's the more formal version:&lt;/p&gt;
&lt;p&gt;&lt;span class="math"&gt;\(Score = TF("Mars") \times IDF("Mars") + TF("exploration") \times IDF("exploration")\)&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;Let’s dig into problem #1 first.  We need to find a way to adjust how we calculate "Count of Term" -- i.e. Term Frequency (TF) -- to address this problem.&lt;/p&gt;
&lt;h2 id="the-10th-mars-the-1st-mars"&gt;❗ The 10th “Mars” ≠ The 1st “Mars”&lt;/h2&gt;
&lt;p&gt;Book B mentions "Mars" &lt;strong&gt;10 times&lt;/strong&gt;. Book A mentions it &lt;strong&gt;8 times&lt;/strong&gt;. Our current scoring says more mentions = better score. But think about it:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;First mention of "Mars": &lt;em&gt;"Okay, this is about Mars – right on!"&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;Second mention: &lt;em&gt;"Yup, still on about Mars. Cool."&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;Tenth mention: &lt;em&gt;"Enough, already – we GET IT! You're about Mars!."&lt;/em&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;💡 &lt;strong&gt;Key Insight&lt;/strong&gt;: After a few mentions, additional occurrences of a term provide less and less new information about relevance.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;This is what's called &lt;strong&gt;diminishing returns&lt;/strong&gt;.  How can we reflect the diminishing returns of additional term frequency?&lt;/p&gt;
&lt;h2 id="keeping-it-simple-the-one-and-done-approach"&gt;✨ Keeping It Simple: The “One-And-Done” Approach&lt;/h2&gt;
&lt;p&gt;Let’s say we score a term as 1 if it shows up &lt;strong&gt;at all&lt;/strong&gt;, and 0 if it doesn’t. Simple!&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;If the term appears at least once → score = 1&lt;/li&gt;
&lt;li&gt;If not → score = 0&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This implies that our simple Score formula becomes:&lt;/p&gt;
&lt;p&gt;&lt;span class="math"&gt;\(Score = (1\ if\ Term\ found,\ else\ 0) \times (Rarity\ of\ Term) + (1\ if\ Term\ found,\ else\ 0) \times (Rarity\ of\ Term)\)&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;Calc it out, now!&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Book&lt;/th&gt;
&lt;th&gt;Title&lt;/th&gt;
&lt;th&gt;"Mars" Present&lt;/th&gt;
&lt;th&gt;"Exploration" Present&lt;/th&gt;
&lt;th&gt;Rarity of "Mars"&lt;/th&gt;
&lt;th&gt;Rarity of "exploration"&lt;/th&gt;
&lt;th&gt;Score&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;A&lt;/td&gt;
&lt;td&gt;Mars Exploration Guide&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;1.32&lt;/td&gt;
&lt;td&gt;0.74&lt;/td&gt;
&lt;td&gt;2.06&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;B&lt;/td&gt;
&lt;td&gt;Space Encyclopedia&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;1.32&lt;/td&gt;
&lt;td&gt;0.74&lt;/td&gt;
&lt;td&gt;2.06&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;C&lt;/td&gt;
&lt;td&gt;Planetary Science&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;1.32&lt;/td&gt;
&lt;td&gt;0.74&lt;/td&gt;
&lt;td&gt;0.00&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;D&lt;/td&gt;
&lt;td&gt;Exploration Techniques&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;1.32&lt;/td&gt;
&lt;td&gt;0.74&lt;/td&gt;
&lt;td&gt;0.74&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;E&lt;/td&gt;
&lt;td&gt;Solar System&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;1.32&lt;/td&gt;
&lt;td&gt;0.74&lt;/td&gt;
&lt;td&gt;0.00&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2 id="the-problem-too-hamfisted"&gt;🔍 The Problem: Too Hamfisted&lt;/h2&gt;
&lt;p&gt;This flattens everything too much – Book A and B tie again, and there's no reward for mentioning a term more than once.&lt;/p&gt;
&lt;p&gt;We need something better: a curve that rises at first mention, but levels off with more and more mentions.  This is consistent with the key insight above.&lt;/p&gt;
&lt;h2 id="bm25s-solution-smoothing-term-frequency-with-saturation"&gt;🤓 BM25's Solution: Smoothing Term Frequency with Saturation&lt;/h2&gt;
&lt;p&gt;What we want, is to revise our Score like this:&lt;/p&gt;
&lt;p&gt;&lt;span class="math"&gt;\(Score = (\mathbf{Smoothed}\ Count\ of\ Term) \times (Rarity\ of\ Term) + (\mathbf{Smoothed}\ Count\ of\ Term) \times (Rarity\ of\ Term)\)&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;BM25 uses a clever formula to model diminishing returns.  It smooths the impact of each extra mention, and calls it "Saturated Term Frequency" like this:&lt;/p&gt;
&lt;div class="math"&gt;$$
Saturated\ TF = \frac{Term\ Frequency \times (k + 1)}{Term\ Frequency + k}
$$&lt;/div&gt;
&lt;p&gt;Where &lt;strong&gt;k&lt;/strong&gt; controls how fast the saturation kicks in. (Think of it like a knob for “how quickly we get tired of hearing the word.”)  Oh, you're a smooth operator, BM25!&lt;/p&gt;
&lt;p&gt;&lt;img alt="Smooth operator" src="images/20250325_smooth-operator.gif"&gt;&lt;/p&gt;
&lt;p&gt;Let’s set &lt;strong&gt;k = 1.2&lt;/strong&gt;, which is common in practice.  We'll compute "Saturated" Term Frequency for &lt;strong&gt;Book A&lt;/strong&gt; (8 mentions) and &lt;strong&gt;Book B&lt;/strong&gt; (10 mentions):&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Book A:&lt;/strong&gt;&lt;br&gt;
&lt;code&gt;(8 × 2.2) / (8 + 1.2) = 17.6 / 9.2 ≈ 1.91&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Book B:&lt;/strong&gt;&lt;br&gt;
&lt;code&gt;(10 × 2.2) / (10 + 1.2) = 22 / 11.2 ≈ 1.96&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Only a &lt;strong&gt;tiny bump&lt;/strong&gt; in score for Book B, even with 2 more mentions. Exactly what we want!&lt;/p&gt;
&lt;p&gt;Now we recalculate our score as follows:&lt;/p&gt;
&lt;p&gt;&lt;span class="math"&gt;\(Score = Saturated\ TF(term) \times IDF(term) + Saturated\ TF(term) \times IDF(term)\)&lt;/span&gt;&lt;/p&gt;
&lt;h2 id="its-time-for-the-calculator_1"&gt;🧮 It's Time for the Calculator!&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Book&lt;/th&gt;
&lt;th&gt;Title&lt;/th&gt;
&lt;th&gt;"Mars" Count&lt;/th&gt;
&lt;th&gt;"Exploration" Count&lt;/th&gt;
&lt;th&gt;Saturated TF ("Mars")&lt;/th&gt;
&lt;th&gt;Saturated TF ("exploration")&lt;/th&gt;
&lt;th&gt;Score&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;A&lt;/td&gt;
&lt;td&gt;Mars Exploration Guide&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;1.91&lt;/td&gt;
&lt;td&gt;1.83&lt;/td&gt;
&lt;td&gt;3.88&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;B&lt;/td&gt;
&lt;td&gt;Space Encyclopedia&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;1.96&lt;/td&gt;
&lt;td&gt;1.69&lt;/td&gt;
&lt;td&gt;3.84&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;C&lt;/td&gt;
&lt;td&gt;Planetary Science&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0.00&lt;/td&gt;
&lt;td&gt;0.00&lt;/td&gt;
&lt;td&gt;0.00&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;D&lt;/td&gt;
&lt;td&gt;Exploration Techniques&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;12&lt;/td&gt;
&lt;td&gt;0.00&lt;/td&gt;
&lt;td&gt;2.00&lt;/td&gt;
&lt;td&gt;1.47&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;E&lt;/td&gt;
&lt;td&gt;Solar System&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0.00&lt;/td&gt;
&lt;td&gt;0.00&lt;/td&gt;
&lt;td&gt;0.00&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;Victory!&lt;/strong&gt;  Book A now edges out Book B – just like it should. The repeated mentions in Book B still count, but no longer dominate.&lt;/p&gt;
&lt;p&gt;Book D also ranks lower than both, which makes sense – it never even mentions Mars. Now we’re cooking!&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;🍸 &lt;strong&gt;Sound Smart and Be Sexy at Cocktail Parties&lt;/strong&gt;:&lt;br&gt;
"BM25 models diminishing returns using a saturation function."&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h1 id="step-4-size-really-does-matter-but-not-how-you-think-document-length-normalization"&gt;📏 Step 4: Size Really Does Matter -- But Not How You Think (Document Length Normalization)&lt;/h1&gt;
&lt;p&gt;So, we’ve handled word rarity. We’ve handled diminishing returns. But there’s still a problem lurking in our scoring.&lt;/p&gt;
&lt;p&gt;Imagine we add &lt;strong&gt;Book F&lt;/strong&gt;:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Book&lt;/th&gt;
&lt;th&gt;Title&lt;/th&gt;
&lt;th&gt;Length (pages)&lt;/th&gt;
&lt;th&gt;"Mars" count&lt;/th&gt;
&lt;th&gt;"exploration" count&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;F&lt;/td&gt;
&lt;td&gt;The Everything Book&lt;/td&gt;
&lt;td&gt;1,000&lt;/td&gt;
&lt;td&gt;15&lt;/td&gt;
&lt;td&gt;12&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;This book is long. Like, &lt;em&gt;Tolstoy&lt;/em&gt; long. It has 15 mentions of “Mars”! 12 mentions of “exploration”! No big deal -- we'll just sort it out in smooooothing, right?&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Slow your roll.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;A long document might mention "Mars" a bunch, even if it’s not the focus. A shorter document with fewer mentions might still be &lt;strong&gt;way more relevant&lt;/strong&gt; if it’s concentrated on the topic.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;💡 &lt;strong&gt;Key Insight&lt;/strong&gt;: Longer documents mention &lt;em&gt;everything&lt;/em&gt; more – not because they’re more relevant, but because they’re longer.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;That means we need to adjust our scores based on document length.&lt;/p&gt;
&lt;h2 id="the-intuition-proportional-relevance"&gt;🤔 The Intuition: Proportional Relevance&lt;/h2&gt;
&lt;p&gt;Take Book A and newly-added Book F (which we shall immediately drop and never consider again):&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A 1-page document that mentions “Mars” 5 times  &lt;/li&gt;
&lt;li&gt;A 1,000-page book that mentions “Mars” 15 times&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Which is more “about” Mars? The short one, obviously. It’s basically banging on about &lt;strong&gt;Mars&lt;/strong&gt; every few lines.&lt;/p&gt;
&lt;p&gt;So we want to reward &lt;strong&gt;concentration of terms&lt;/strong&gt;, not just raw frequency.  How could we do this?&lt;/p&gt;
&lt;h2 id="simple-yo"&gt;✨ Simple, Yo!&lt;/h2&gt;
&lt;p&gt;We could just take our current scoring function, and adjust the frequency by the length of the document, right?  That checks out.&lt;/p&gt;
&lt;p&gt;Recall our simply worded score function:&lt;/p&gt;
&lt;p&gt;&lt;span class="math"&gt;\(Score = (Smoothed\ Count\ of\ Term) \times (Rarity\ of\ Term) + (Smoothed\ Count\ of\ Term) \times (Rarity\ of\ Term)\)&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;Which, when it graduates from university and gets an MBA from Wharton, becomes:&lt;/p&gt;
&lt;p&gt;&lt;span class="math"&gt;\(Score = Saturated\ TF(term) \times IDF(term) + Saturated\ TF(term) \times IDF(term)\)&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;What if we just adjusted our Smoothed Count by the length of the document thusly:&lt;/p&gt;
&lt;div class="math"&gt;$$
Score = \frac{(Smoothed\ Count\ of\ Term)}{Document\ Length} \times (Rarity\ of\ Term) + \frac{(Smoothed\ Count\ of\ Term)}{Document\ Length} \times (Rarity\ of\ Term)
$$&lt;/div&gt;
&lt;p&gt;That would do just nicely!  Let's run those calcs:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Book&lt;/th&gt;
&lt;th&gt;Smoothed "Mars" Count&lt;/th&gt;
&lt;th&gt;Smoothed "Exploration" Count&lt;/th&gt;
&lt;th&gt;Document Length&lt;/th&gt;
&lt;th&gt;Rarity of "Mars"&lt;/th&gt;
&lt;th&gt;Rarity of "exploration"&lt;/th&gt;
&lt;th&gt;Score&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;A&lt;/td&gt;
&lt;td&gt;1.9130&lt;/td&gt;
&lt;td&gt;1.8333&lt;/td&gt;
&lt;td&gt;50&lt;/td&gt;
&lt;td&gt;1.32&lt;/td&gt;
&lt;td&gt;0.74&lt;/td&gt;
&lt;td&gt;0.0776&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;B&lt;/td&gt;
&lt;td&gt;1.9643&lt;/td&gt;
&lt;td&gt;1.6923&lt;/td&gt;
&lt;td&gt;200&lt;/td&gt;
&lt;td&gt;1.32&lt;/td&gt;
&lt;td&gt;0.74&lt;/td&gt;
&lt;td&gt;0.0192&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;C&lt;/td&gt;
&lt;td&gt;0.0000&lt;/td&gt;
&lt;td&gt;0.0000&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;1.32&lt;/td&gt;
&lt;td&gt;0.74&lt;/td&gt;
&lt;td&gt;0.0000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;D&lt;/td&gt;
&lt;td&gt;0.0000&lt;/td&gt;
&lt;td&gt;2.0000&lt;/td&gt;
&lt;td&gt;75&lt;/td&gt;
&lt;td&gt;1.32&lt;/td&gt;
&lt;td&gt;0.74&lt;/td&gt;
&lt;td&gt;0.0197&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;E&lt;/td&gt;
&lt;td&gt;0.0000&lt;/td&gt;
&lt;td&gt;0.0000&lt;/td&gt;
&lt;td&gt;150&lt;/td&gt;
&lt;td&gt;1.32&lt;/td&gt;
&lt;td&gt;0.74&lt;/td&gt;
&lt;td&gt;0.0000&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2 id="the-problem-where-did-it-go"&gt;🔍 The Problem: Where Did It Go?&lt;/h2&gt;
&lt;p&gt;Our simple, adjusted score gives short documents an edge (good!) and prevents long ones from coasting on raw term count (also good!).  This works (-ish) but notice that our score is now reeeaaaalllyyy small.  Look, in particular, at Books A and B.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Book A is short and focused – but its score is &lt;em&gt;tiny&lt;/em&gt;.  &lt;/li&gt;
&lt;li&gt;Book B is long – and it scores &lt;em&gt;basically nothing&lt;/em&gt;, even though it’s somewhat relevant to our search.&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;💡 &lt;strong&gt;Key Problem&lt;/strong&gt;: We’re dividing by the &lt;strong&gt;raw length&lt;/strong&gt; of the document, and it’s punishing longer documents a little too aggressively.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Long documents aren't necessarily bad – they just need a fair chance to prove their focus. And short documents shouldn't get an automatic VIP pass just because they’re compact.&lt;/p&gt;
&lt;h2 id="size-does-matter-but-its-relative"&gt;⚖️📏 Size Does Matter, But It's Relative&lt;/h2&gt;
&lt;p&gt;Okay, dividing by raw length was a start, but it’s a bit too harsh–short docs get VIP passes, long ones get squashed. The real trick? Make it &lt;em&gt;relative&lt;/em&gt; to the average document length, so everyone gets a fair shake.&lt;/p&gt;
&lt;p&gt;Here’s how BM25 rolls: instead of just slapping a length penalty on after smoothing, we bake it right into the count adjustment. Think of it as tweaking how much repetition matters based on how chatty the document is compared to the norm. We call this the "Length Tweak":&lt;/p&gt;
&lt;div class="math"&gt;$$
Length\ Tweak = Base\ Penalty + (Length\ Factor \times \frac{Document\ Length}{Average\ Document\ Length})
$$&lt;/div&gt;
&lt;p&gt;
- &lt;strong&gt;Base Penalty&lt;/strong&gt; = A number like &lt;code&gt;1.2&lt;/code&gt; (same as our &lt;code&gt;k&lt;/code&gt; from smoothing–caps those extra mentions).
- &lt;strong&gt;Length Factor&lt;/strong&gt; = A dial, say &lt;code&gt;0.9&lt;/code&gt;, to decide how much length should nudge things (it’s &lt;code&gt;Base Penalty × 0.75&lt;/code&gt;, but don’t sweat the math yet).&lt;/p&gt;
&lt;p&gt;See how the &lt;strong&gt;Length Tweak&lt;/strong&gt;, overall, calibrates the document being compared to the average document length?  We then take this Length Tweak and scale the term count up or down, based on the length of the document it was found in.  So, instead of:&lt;/p&gt;
&lt;div class="math"&gt;$$
Score = \frac{(Smoothed\ Count)}{Length} \times Rarity
$$&lt;/div&gt;
&lt;p&gt;We mix it all together like this:&lt;/p&gt;
&lt;div class="math"&gt;$$
Adjusted\ Count = \frac{(Count\ of\ Term \times Boost)}{(Count\ of\ Term + Length\ Tweak)}
$$&lt;/div&gt;
&lt;p&gt;For now, &lt;strong&gt;Boost&lt;/strong&gt; = &lt;code&gt;2.2&lt;/code&gt; – it's a little kick to Count of Term which we’ll explain soon.&lt;/p&gt;
&lt;p&gt;Note that our Adjusted Count is scaled &lt;strong&gt;up&lt;/strong&gt; if the document was relatively short -- i.e. we divide Count of Term by our Length Tweak which is between 0 and 1.  The Adjusted Count is scaled &lt;strong&gt;down&lt;/strong&gt; if the document was relatively long -- i.e. we divide Ccount of Term by our Length Tweak, which is &amp;gt;= 1.  &lt;/p&gt;
&lt;p&gt;Then, what we're left with, now, is our simplified Score of:&lt;/p&gt;
&lt;p&gt;&lt;span class="math"&gt;\(Score = (Adjusted\ Count\ of\ Term) \times (Rarity\ of\ Term) + (Adjusted\ Count\ of\ Term) \times (Rarity\ of\ Term)\)&lt;/span&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;💡 &lt;strong&gt;Key Insight:&lt;/strong&gt; Long docs don’t get slammed just for being long – they only take a hit if they’re longer than average and still blathering.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;See? Size &lt;em&gt;does&lt;/em&gt; matter, but it’s all relative! 😜  We'll bring it all together in the next section.&lt;/p&gt;
&lt;hr&gt;
&lt;h1 id="the-bm25-reveal-why-this-works"&gt;💡 The BM25 Reveal: Why This Works 💡&lt;/h1&gt;
&lt;p&gt;Taking it all in, then, from Steps 1 - 4, BM25 ranks documents based on search terms by calculating:&lt;/p&gt;
&lt;p&gt;&lt;span class="math"&gt;\(Score = \sum_{each\ Term} [ Adjusted\ Count\ of\ Term \times Rarity\ of\ Term ]\)&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;Where:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Adjusted Count of Term&lt;/strong&gt; =&lt;br&gt;
&lt;/p&gt;
&lt;div class="math"&gt;$$
\frac{Term\ Frequency \times (k + 1)}{Term\ Frequency + k \times ((1 - b) + b \times \frac{Document\ Length}{Average\ Document\ Length})}
$$&lt;/div&gt;
&lt;p&gt;This takes the term’s count and gives it a little boost – called &lt;strong&gt;Boost&lt;/strong&gt; and often set to &lt;code&gt;2.2&lt;/code&gt; – then tones it down based on how often it repeats and how long the document is compared to the average. Why &lt;code&gt;2.2&lt;/code&gt; for the Boost? Back in Step 3, we smoothed counts with a knob called &lt;code&gt;k&lt;/code&gt; – set to &lt;code&gt;1.2&lt;/code&gt; – to chill out extra mentions. The Boost is just &lt;code&gt;k + 1&lt;/code&gt;, or &lt;code&gt;1.2 + 1 = 2.2&lt;/code&gt;, like the high-five between Maverick and Goose on the runway, before we calm things down with &lt;strong&gt;diminishing returns&lt;/strong&gt; (saturation) and &lt;strong&gt;length normalization&lt;/strong&gt;, all in one go.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Rarity of Term&lt;/strong&gt; = &lt;strong&gt;Inverse Document Frequency&lt;/strong&gt; =&lt;br&gt;
&lt;/p&gt;
&lt;div class="math"&gt;$$
\log\left(\frac{Total\ Docs}{Docs\ containing\ the\ term}\right)
$$&lt;/div&gt;
&lt;p&gt;
  This measures how rare the term is across all documents–rarer terms get a bigger score because they’re more special.&lt;/p&gt;
&lt;p&gt;OR, said in words that Mum will understand over Sunday dinner, &lt;strong&gt;for each search term in the query&lt;/strong&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Count how often it pops up in the document (but don’t go overboard – we give it a boost then smooth it out so a few mentions are enough).&lt;/li&gt;
&lt;li&gt;Multiply by how rare it is in the whole pile of books (rare words are gold).&lt;/li&gt;
&lt;li&gt;Tweak it based on the document’s length (short and snappy gets a lift, long and wordy gets a gentle nudge down).&lt;/li&gt;
&lt;li&gt;Add up the scores for all the terms you’re searching for.&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;🍷 &lt;strong&gt;Summarize BM25 at Gala Affairs&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;“Each term gets credit for how often it appears, how rare it is, and how focused the document is on it – with soft penalties for verbosity and overuse.”&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;This addresses the problems we saw along the way:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Raw counts aren't enough&lt;/strong&gt; – a term appearing more often doesn't always mean the document is more relevant. (See: Book B beating Book A early on)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Some words matter more than others&lt;/strong&gt; – rare terms provide sharper clues about relevance. (IDF to the rescue)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Extra mentions don’t add linear value&lt;/strong&gt; – the 10th “Mars” isn’t as informative as the 1st. (Saturation fixes that)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Longer docs talk more&lt;/strong&gt; – but that doesn’t mean they’re better matches. (Normalization keeps verbosity in check)&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="bm25-ranking-of-mars-exploration-for-our-baby-library"&gt;✅ BM25 Ranking of "Mars exploration" for our Baby Library&lt;/h2&gt;
&lt;p&gt;It's the last time for The Calculator! 😢 Here's how the books in our wee library rank:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Book&lt;/th&gt;
&lt;th&gt;Title&lt;/th&gt;
&lt;th&gt;Length&lt;/th&gt;
&lt;th&gt;Adjusted Count (Mars)&lt;/th&gt;
&lt;th&gt;Adjusted Count (exploration)&lt;/th&gt;
&lt;th&gt;BM25 Score&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;A&lt;/td&gt;
&lt;td&gt;Mars Exploration Guide&lt;/td&gt;
&lt;td&gt;50&lt;/td&gt;
&lt;td&gt;1.83&lt;/td&gt;
&lt;td&gt;1.74&lt;/td&gt;
&lt;td&gt;3.71&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;B&lt;/td&gt;
&lt;td&gt;Space Encyclopedia&lt;/td&gt;
&lt;td&gt;200&lt;/td&gt;
&lt;td&gt;1.72&lt;/td&gt;
&lt;td&gt;1.30&lt;/td&gt;
&lt;td&gt;3.24&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;C&lt;/td&gt;
&lt;td&gt;Planetary Science&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;0.00&lt;/td&gt;
&lt;td&gt;0.00&lt;/td&gt;
&lt;td&gt;0.00&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;D&lt;/td&gt;
&lt;td&gt;Exploration Techniques&lt;/td&gt;
&lt;td&gt;75&lt;/td&gt;
&lt;td&gt;0.00&lt;/td&gt;
&lt;td&gt;1.93&lt;/td&gt;
&lt;td&gt;1.42&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;E&lt;/td&gt;
&lt;td&gt;Solar System&lt;/td&gt;
&lt;td&gt;150&lt;/td&gt;
&lt;td&gt;0.00&lt;/td&gt;
&lt;td&gt;0.00&lt;/td&gt;
&lt;td&gt;0.00&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;So, all's well that ends well. Book A -- &lt;strong&gt;Mars Exploration Guide&lt;/strong&gt; -- ranked highest in our search and provided us with all of the knowhow we need to put boots on the ground on Mars when Elon gets us there.&lt;/p&gt;
&lt;p&gt;BM25 FTW! And best of all? It’s fast. It’s robust. And it &lt;em&gt;works&lt;/em&gt; – in RAG pipelines, search engines, recommendation systems ... you name it!&lt;/p&gt;
&lt;h1 id="mathing-all-the-bm25-maths"&gt;🟰 Mathing All the BM25 Maths&lt;/h1&gt;
&lt;p&gt;Here's BM25 formally expressed, so that your mates don't bully you at your next gathering.&lt;/p&gt;
&lt;div class="math"&gt;$$
\text{Score}(D, Q) = \sum_{i=1}^{n} \text{IDF}(q_i) \cdot \frac{\text{TF}(q_i, D) \cdot (k_1 + 1)}{\text{TF}(q_i, D) + k_1 \cdot (1 - b + b \cdot \frac{|D|}{\text{avgdl}})} 
$$&lt;/div&gt;
&lt;p&gt;Where:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;span class="math"&gt;\(D\)&lt;/span&gt; is the document, &lt;/li&gt;
&lt;li&gt;&lt;span class="math"&gt;\(Q\)&lt;/span&gt; is the query with terms &lt;span class="math"&gt;\(q_i\)&lt;/span&gt;.&lt;/li&gt;
&lt;li&gt;&lt;span class="math"&gt;\(\text{TF}(q_i, D)\)&lt;/span&gt; is the term frequency of query term &lt;span class="math"&gt;\(q_i\)&lt;/span&gt; in document &lt;span class="math"&gt;\(D\)&lt;/span&gt;.&lt;/li&gt;
&lt;li&gt;&lt;span class="math"&gt;\(k_1\)&lt;/span&gt; is a parameter controlling term frequency saturation (commonly 1.2).&lt;/li&gt;
&lt;li&gt;&lt;span class="math"&gt;\(b\)&lt;/span&gt; is a parameter controlling length normalization (commonly 0.75).&lt;/li&gt;
&lt;li&gt;&lt;span class="math"&gt;\(|D|\)&lt;/span&gt; is the document length, &lt;span class="math"&gt;\(\text{avgdl}\)&lt;/span&gt; is the average document length.&lt;/li&gt;
&lt;li&gt;&lt;span class="math"&gt;\(\text{IDF}(q_i) = \log\left(\frac{N - n(q_i) + 0.5}{n(q_i) + 0.5}\right)\)&lt;/span&gt; or a simpler variant like &lt;span class="math"&gt;\(\log\left(\frac{N}{n(q_i)}\right)\)&lt;/span&gt;, where &lt;span class="math"&gt;\(N\)&lt;/span&gt; is total documents and &lt;span class="math"&gt;\(n(q_i)\)&lt;/span&gt; is documents containing &lt;span class="math"&gt;\(q_i\)&lt;/span&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Be gone, maths bullies!&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;strong&gt;Feature Image Prompt:&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Generate an image. The aesthetic should be cyberpunk with colors of neon pink, blue and purple. Do not add any people. Imagine a futuristic holographic interface floating in a dense digital matrix: abstract, glowing documents and cascading streams of neon equations that represent a sophisticated search ranking algorithm. Scattered holograms and digital grids display intricate formulas and logarithmic graphs, evoking the essence of BM25’s balancing of term frequency with filtering power. The scene is awash in swirling neon lights, pulsating data streams, and a high-tech, otherworldly ambiance that highlights the interplay between mathematical precision and futuristic innovation.&lt;/p&gt;
&lt;script type="text/javascript"&gt;if (!document.getElementById('mathjaxscript_pelican_#%@#$@#')) {
    var align = "center",
        indent = "0em",
        linebreak = "false";

    if (false) {
        align = (screen.width &lt; 768) ? "left" : align;
        indent = (screen.width &lt; 768) ? "0em" : indent;
        linebreak = (screen.width &lt; 768) ? 'true' : linebreak;
    }

    var mathjaxscript = document.createElement('script');
    mathjaxscript.id = 'mathjaxscript_pelican_#%@#$@#';
    mathjaxscript.type = 'text/javascript';
    mathjaxscript.src = 'https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.3/latest.js?config=TeX-AMS-MML_HTMLorMML';

    var configscript = document.createElement('script');
    configscript.type = 'text/x-mathjax-config';
    configscript[(window.opera ? "innerHTML" : "text")] =
        "MathJax.Hub.Config({" +
        "    config: ['MMLorHTML.js']," +
        "    TeX: { extensions: ['AMSmath.js','AMSsymbols.js','noErrors.js','noUndefined.js'], equationNumbers: { autoNumber: 'none' } }," +
        "    jax: ['input/TeX','input/MathML','output/HTML-CSS']," +
        "    extensions: ['tex2jax.js','mml2jax.js','MathMenu.js','MathZoom.js']," +
        "    displayAlign: '"+ align +"'," +
        "    displayIndent: '"+ indent +"'," +
        "    showMathMenu: true," +
        "    messageStyle: 'normal'," +
        "    tex2jax: { " +
        "        inlineMath: [ ['\\\\(','\\\\)'] ], " +
        "        displayMath: [ ['$$','$$'] ]," +
        "        processEscapes: true," +
        "        preview: 'TeX'," +
        "    }, " +
        "    'HTML-CSS': { " +
        "        availableFonts: ['STIX', 'TeX']," +
        "        preferredFont: 'STIX'," +
        "        styles: { '.MathJax_Display, .MathJax .mo, .MathJax .mi, .MathJax .mn': {color: 'inherit ! important'} }," +
        "        linebreaks: { automatic: "+ linebreak +", width: '90% container' }," +
        "    }, " +
        "}); " +
        "if ('default' !== 'default') {" +
            "MathJax.Hub.Register.StartupHook('HTML-CSS Jax Ready',function () {" +
                "var VARIANT = MathJax.OutputJax['HTML-CSS'].FONTDATA.VARIANT;" +
                "VARIANT['normal'].fonts.unshift('MathJax_default');" +
                "VARIANT['bold'].fonts.unshift('MathJax_default-bold');" +
                "VARIANT['italic'].fonts.unshift('MathJax_default-italic');" +
                "VARIANT['-tex-mathit'].fonts.unshift('MathJax_default-italic');" +
            "});" +
            "MathJax.Hub.Register.StartupHook('SVG Jax Ready',function () {" +
                "var VARIANT = MathJax.OutputJax.SVG.FONTDATA.VARIANT;" +
                "VARIANT['normal'].fonts.unshift('MathJax_default');" +
                "VARIANT['bold'].fonts.unshift('MathJax_default-bold');" +
                "VARIANT['italic'].fonts.unshift('MathJax_default-italic');" +
                "VARIANT['-tex-mathit'].fonts.unshift('MathJax_default-italic');" +
            "});" +
        "}";

    (document.body || document.getElementsByTagName('head')[0]).appendChild(configscript);
    (document.body || document.getElementsByTagName('head')[0]).appendChild(mathjaxscript);
}
&lt;/script&gt;</content><category term="Writing"/><category term="AI"/><category term="machine_learning"/><category term="algorithms"/><category term="RAG"/><category term="retrieval"/></entry><entry><title>⚡️ Latent Space - The new OpenAI Agents Platform</title><link href="https://gallon.me/latent-space-the-new-openai-agents-platform.html" rel="alternate"/><published>2025-03-20T00:00:00-05:00</published><updated>2025-03-20T00:00:00-05:00</updated><author><name>Corey</name></author><id>tag:gallon.me,2025-03-20:/latent-space-the-new-openai-agents-platform.html</id><summary type="html">&lt;p&gt;OpenAI have dropped their new Agents Platform.  Swyx and Alessio chat with Nikunj Handa and Romain Huet of OpenAI to discuss the release.&lt;/p&gt;</summary><content type="html">&lt;p&gt;OpenAI have dropped their new Agents Platform.  Swyx and Alessio chat with Nikunj Handa and Romain Huet of OpenAI to discuss the release.&lt;/p&gt;
&lt;h1 id="episode-show-notes"&gt;&lt;a href="https://share.snipd.com/episode/0a578189-7b37-4981-945e-a9cd8618f4d7"&gt;Episode Show Notes&lt;/a&gt;&lt;/h1&gt;
&lt;blockquote&gt;
&lt;p&gt;While everyone is now repeating that   2025 is the “Year of the Agent”,   OpenAI is heads down building towards it. In the first 2 months of the year they released  Operator  and  Deep Research  (arguably the most successful agent archetype so far), and today they are bringing a lot of those capabilities to the API:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Responses API &lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Web Search Tool &lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Computer Use Tool &lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;File Search Tool &lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;A new open source  Agents SDK  with integrated  Observability Tools &lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;We cover all this and more in today’s lightning pod on  YouTube !&lt;/p&gt;
&lt;p&gt;More details here:&lt;/p&gt;
&lt;p&gt;Responses API &lt;/p&gt;
&lt;p&gt;In our  Michelle Pokrass episode  we talked about the Assistants API needing a redesign. Today OpenAI is launching the Responses API, “a more flexible foundation for developers building agentic applications”. It’s a superset of the chat completion API, and the suggested starting point for developers working with OpenAI models. &lt;/p&gt;
&lt;p&gt;One of the big upgrades is the new set of built-in tools for the responses API: Web Search, Computer Use, and Files. &lt;/p&gt;
&lt;p&gt;Web Search Tool&lt;/p&gt;
&lt;p&gt;We previously had  Exa AI  on the podcast to talk about web search for AI. OpenAI is also now joining the race; the Web Search API is actually a new “model” that exposes two 4o fine-tunes: gpt-4o-search-preview and gpt-4o-mini-search-preview. These are the same models that power ChatGPT Search, and are priced at $30/1000 queries and $25/1000 queries respectively. &lt;/p&gt;
&lt;p&gt;The killer feature is inline citations: you do not only get a link to a page, but also a deep link to exactly where your query was answered in the result page. &lt;/p&gt;
&lt;p&gt;Computer Use Tool&lt;/p&gt;
&lt;p&gt;The model that powers Operator, called Computer-Using-Agent (CUA), is also now available in the API. The computer-use-preview model is SOTA on most benchmarks, achieving 38.1% success on OSWorld for full computer use tasks, 58.1% on WebArena, and 87% on WebVoyager for web-based interactions.&lt;/p&gt;
&lt;p&gt;As you will notice in the docs, &lt;code&gt;computer-use-preview&lt;/code&gt; is both a model and a tool through which you can specify the environment. &lt;/p&gt;
&lt;p&gt;Usage is priced at $3/1M input tokens and $12/1M output tokens, and it’s currently only available to users in tiers 3-5.&lt;/p&gt;
&lt;p&gt;File Search Tool&lt;/p&gt;
&lt;p&gt;File Search was also available in the Assistants API, and it’s now coming to Responses too. OpenAI is bringing search + RAG all under one umbrella, and we’ll definitely see more people trying to find new ways to build all-in-one apps on OpenAI. &lt;/p&gt;
&lt;p&gt;Usage is priced at $2.50 per thousand queries and file storage at $0.10/GB/day, with the first GB free.&lt;/p&gt;
&lt;p&gt;Agent SDK: Swarms++!&lt;/p&gt;
&lt;p&gt;https://github.com/openai/openai-agents-python &lt;/p&gt;
&lt;p&gt;To bring it all together, after the viral reception to  Swarm , OpenAI is releasing an officially supported agents framework (which was  previewed at our AI Engineer Summit ) with 4 core pieces:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Agents : Easily configurable LLMs with clear instructions and built-in tools.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Handoﬀs : Intelligently transfer control between agents.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Guardrails : Configurable safety checks for input and output validation.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Tracing  utm_campaign=CTA_4"&amp;gt;www.latent.space/subscribe&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;h2 id="new-openai-agent-tools-apis"&gt;New OpenAI Agent Tools &amp;amp; APIs&lt;/h2&gt;
&lt;p&gt;&lt;a href="https://share.snipd.com/snip/11874635-3b11-43c2-b325-865bc0dfe11f"&gt;🎧 Play snip - 2min (00:40 - 02:21)&lt;/a&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;OpenAI is launching three new built-in tools: a web search tool (ChatGPT for search in the API), a file search tool (upload, parse, chunk, embed, and search your data), and a computer use tool (powering the Operator product in ChatGPT).&lt;/li&gt;
&lt;li&gt;They are also launching a new Responses API to support these tools, replacing the older ChatCompletions API and designed for future agentic products.&lt;/li&gt;
&lt;li&gt;Finally, they are releasing an upgraded Agents SDK (formerly Swarm) with built-in tracing in the OpenAI dashboard for multi-agent orchestration.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="responses-api-a-unified-and-flexible-approach"&gt;Responses API: A Unified and Flexible Approach&lt;/h2&gt;
&lt;p&gt;&lt;a href="https://share.snipd.com/snip/253a86c0-2eeb-41a1-ba98-809732b236ed"&gt;🎧 Play snip - 4min (02:41 - 06:16)&lt;/a&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The Responses API is a new, more flexible API from OpenAI, superseding the Chat Completions and Assistants APIs.&lt;/li&gt;
&lt;li&gt;It's designed for agentic workflows, supporting longer, multi-turn tasks and tool use.&lt;/li&gt;
&lt;li&gt;While Chat Completions remains available, Responses API offers a unified endpoint with broader capabilities.&lt;/li&gt;
&lt;li&gt;It incorporates features from the Assistants API, like convenient tool access, while simplifying integration.&lt;/li&gt;
&lt;li&gt;Responses API also offers stateless mode for compatibility with Chat Completions use cases.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="gpt-4-with-search-preview-performance"&gt;GPT-4 with Search Preview Performance&lt;/h2&gt;
&lt;p&gt;&lt;a href="https://share.snipd.com/snip/e3540f28-cf78-4d97-adac-b06eb8c65da2"&gt;🎧 Play snip - 9sec (08:09 - 08:19)&lt;/a&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;OpenAI's web search API uses a GPT-4 model fine-tuned specifically for search, named GPT-4.0 Search Preview.&lt;/li&gt;
&lt;li&gt;GPT-4.0 with search has significantly better performance than the base model.&lt;/li&gt;
&lt;li&gt;Simple QA accuracy jumps from 38% with the base GPT-4 to 90% with the search-tuned model.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="computer-use-tool"&gt;Computer Use Tool&lt;/h2&gt;
&lt;p&gt;&lt;a href="https://share.snipd.com/snip/12ba5e97-028f-428c-9923-1a755b311ed8"&gt;🎧 Play snip - 2min (18:16 - 20:08)&lt;/a&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;OpenAI's Computer Use tool empowers developers to build agents that can complete tasks using a computer or browser.&lt;/li&gt;
&lt;li&gt;This tool utilizes a custom model optimized for computer use, enabling agents to interact with the screen by clicking, scrolling, typing, and reporting back.&lt;/li&gt;
&lt;li&gt;By wrapping this functionality as a tool within the Responses API, developers can automate tasks and create multi-turn interactions where agents can execute complex actions over time.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="diy-vs-openai-for-vector-search"&gt;DIY vs. OpenAI for Vector Search&lt;/h2&gt;
&lt;p&gt;&lt;a href="https://share.snipd.com/snip/bd43ef3c-7c0b-4298-a56c-1e7c8db23c0d"&gt;🎧 Play snip - 1min (17:03 - 18:33)&lt;/a&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;If starting from scratch, you'll have more control over chunking and retrieval strategies.&lt;/li&gt;
&lt;li&gt;OpenAI's tool provides an out-of-the-box managed service with customization options.&lt;/li&gt;
&lt;li&gt;Start with OpenAI's solution and see if it meets your needs.&lt;/li&gt;
&lt;li&gt;They plan to add more customization features over time.&lt;/li&gt;
&lt;li&gt;Consider hand-rolling with other solutions if you require complete control.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="openai-agent-sdk"&gt;OpenAI Agent SDK&lt;/h2&gt;
&lt;p&gt;&lt;a href="https://share.snipd.com/snip/14b11275-7f9d-4d62-a9d5-4eb1af461d78"&gt;🎧 Play snip - 3min (21:34 - 24:28)&lt;/a&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;OpenAI's new Agent SDK simplifies building and managing agents in production.&lt;/li&gt;
&lt;li&gt;It supports type checking, guardrails for safer execution, and tracing for monitoring agent behavior.&lt;/li&gt;
&lt;li&gt;The SDK is flexible, allowing integration with various chat completion APIs and tracing providers.&lt;/li&gt;
&lt;li&gt;It leverages the 'handoff' technique popularized by Swarm, enabling complex agent workflows.&lt;/li&gt;
&lt;li&gt;Combined with new tracing UIs in the OpenAI dashboard, developers can effectively troubleshoot and optimize their agent interactions.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;p&gt;&lt;strong&gt;Feature Image Prompt:&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Generate an image. The aesthetic should be cyberpunk with colors of neon pink, blue and purple. Do not add any people. Imagine a futuristic control center filled with holographic digital interfaces and abstract data streams that represent advanced AI and agentic workflows. The scene showcases glowing icons and symbols illustrating computerized search tools, file systems, and interactive APIs, all interwoven with sleek circuit patterns and dynamic grids. The overall atmosphere is busy yet orderly, evoking the cutting-edge innovations of OpenAI’s new Agents Platform and the rapid evolution of agent-based technology in a neon-lit digital landscape.&lt;/p&gt;</content><category term="Snips"/><category term="OpenAI"/><category term="agents"/></entry><entry><title>The Python Lambda Tutorial You Never Asked For</title><link href="https://gallon.me/the-quick-tutorial-on-python-lambda-functions-that-you-never-asked-for.html" rel="alternate"/><published>2025-03-19T00:00:00-05:00</published><updated>2025-03-19T00:00:00-05:00</updated><author><name>Corey</name></author><id>tag:gallon.me,2025-03-19:/the-quick-tutorial-on-python-lambda-functions-that-you-never-asked-for.html</id><summary type="html">&lt;p&gt;Let's be real, nobody wakes up in the morning thinking, "You know what I need more of in my life? Python lambda functions." But here we are. Buckle up, because this is the lambda tutorial you didn't ask for -- but secretly need.&lt;/p&gt;</summary><content type="html">&lt;p&gt;Let's be real, nobody wakes up in the morning thinking, "You know what I need more of in my life? Python lambda functions." But here we are. Buckle up, because this is the lambda tutorial you didn't ask for -- but secretly need.&lt;/p&gt;
&lt;h2 id="lambda-functions-anonymous-lazy-one-liners"&gt;Lambda Functions: Anonymous, Lazy One-Liners&lt;/h2&gt;
&lt;p&gt;Lambda functions, also known as anonymous functions (because naming is overrated), let you squeeze a simple function into a single, no-nonsense line of code:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;lambda&lt;/span&gt; &lt;span class="n"&gt;arguments&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;expression&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Arguments:&lt;/strong&gt; Stuff you feed the function.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Expression:&lt;/strong&gt; What the function spits out. And only one -- don't get greedy.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;For instance, incrementing a number by 1, because who has time for &lt;code&gt;def&lt;/code&gt;?&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;lambda&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;This is equivalent to:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;increment&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;It's basically the Netflix of Python functions: fast, easy, and minimal commitment.&lt;/p&gt;
&lt;h2 id="when-lambdas-actually-matter"&gt;When Lambdas Actually Matter&lt;/h2&gt;
&lt;p&gt;Lambda functions are perfect when:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;You want a quick-and-dirty function without cluttering your namespace.&lt;/li&gt;
&lt;li&gt;Passing functions as arguments -- especially with pandas' &lt;code&gt;apply()&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Here's an actual example from real-world usage:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;vector&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;content&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;apply&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;lambda&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;normalize_embeddings&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;True&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Translation:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Take each &lt;code&gt;content&lt;/code&gt; item from your DataFrame.&lt;/li&gt;
&lt;li&gt;Turn it into a vector (because vectors are cool).&lt;/li&gt;
&lt;li&gt;Do it without polluting your script with needless function names.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;It's like hiring an intern for a five-minute task -- without having to learn their name.&lt;/p&gt;
&lt;h2 id="lambda-goodness"&gt;Lambda Goodness&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Concise&lt;/strong&gt;: One-liner code beauty.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Functional Programming Friendly&lt;/strong&gt;: Easily passed around like a hot potato.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Cleaner&lt;/strong&gt;: Prevents namespace pollution, aka fewer things to regret later.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="lambda-badness"&gt;Lambda Badness&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Single Expression Limitation&lt;/strong&gt;: Complex logic? Look elsewhere.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Debugging Nightmare&lt;/strong&gt;: Anonymous means nameless means frustration.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Overuse Syndrome&lt;/strong&gt;: Just because you can doesn't mean you should. (Looking at you, regex.)&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="quick-and-dirty-lambda-examples"&gt;Quick-and-Dirty Lambda Examples&lt;/h2&gt;
&lt;h3 id="sorting-a-list"&gt;Sorting a List&lt;/h3&gt;
&lt;p&gt;Here's where lambdas actually shine - complex sorting that would be a pain in the ass otherwise:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="n"&gt;points&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
&lt;span class="n"&gt;sorted_points&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;sorted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;points&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;lambda&lt;/span&gt; &lt;span class="n"&gt;point&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;point&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sorted_points&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# Output: [(5, 0), (3, 1), (1, 2)]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;What's happening here? We're telling Python "sort these tuples, but ignore the first number and sort by the second one instead." The lambda grabs each tuple, extracts the second value (&lt;code&gt;point[1]&lt;/code&gt;), and &lt;code&gt;sorted()&lt;/code&gt; does the rest. Without lambdas, you'd need a whole separate function for this trivial operation. Bloody waste of code.&lt;/p&gt;
&lt;h3 id="filtering-a-list"&gt;Filtering a List&lt;/h3&gt;
&lt;p&gt;When you need to separate the wheat from the chaff:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="n"&gt;numbers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;even_numbers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;lambda&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;numbers&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;even_numbers&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# Output: [2, 4]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The &lt;code&gt;filter()&lt;/code&gt; function is expecting another function that returns &lt;code&gt;True&lt;/code&gt; or &lt;code&gt;False&lt;/code&gt;. Our lambda takes each number, divides by 2, and checks if there's a remainder. No remainder? It's even, keep it. Otherwise, chuck it. The result is only the even numbers making it through the filter. It's like a bouncer at a club where odd numbers aren't cool enough to get in.&lt;/p&gt;
&lt;h3 id="transforming-a-list-with-map"&gt;Transforming a List with Map&lt;/h3&gt;
&lt;p&gt;Need to apply the same operation to every item in a list? &lt;code&gt;map()&lt;/code&gt; + lambda is your friend:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="n"&gt;numbers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;squared_numbers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;lambda&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;numbers&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;squared_numbers&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# Output: [1, 4, 9, 16]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Here, &lt;code&gt;map()&lt;/code&gt; takes our lambda and applies it to each number in the list. The lambda just squares whatever it gets. So simple it's almost boring, but that's the point. You don't need a whole function definition ceremony for something this straightforward. It's the difference between sending a text and writing a formal letter – sometimes you just need to get the message across without the fluff.&lt;/p&gt;
&lt;h2 id="wrap-up-lambdas-in-a-nutshell"&gt;Wrap-Up: Lambdas in a Nutshell&lt;/h2&gt;
&lt;p&gt;Lambda functions are your go-to for simple, anonymous tasks that aren't worth the full-blown function definition. Sure, they're limited. Yeah, debugging them is annoying. But when used wisely, they're elegant as hell.&lt;/p&gt;
&lt;p&gt;So next time you need a function that's here for a good time -- not a long time -- reach for lambda.&lt;/p&gt;
&lt;p&gt;And that's it. Tutorial over. Go forth and lambda responsibly (or don't -- I'm not your boss).&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;strong&gt;Feature Image Prompt:&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Generate an image. The aesthetic should be cyberpunk with colors of neon pink, blue and purple. Do not add any people. Create a futuristic digital scene featuring a sleek, neon-glowing computer interface that showcases snippets of Python lambda function code floating in holographic displays. Integrate abstract data streams, dynamic geometric grids, and circuit-like patterns that pulse with vibrant neon pink, blue, and purple hues to evoke the minimalist yet powerful nature of lambda functions in a high-tech, cyberpunk environment.&lt;/p&gt;</content><category term="Writing"/><category term="python"/><category term="lambda_functions"/><category term="functional_programming"/></entry><entry><title>Founders #383: Todd Graves' $10B Chicken Finger Dream</title><link href="https://gallon.me/founders-383-todd-graves-and-his-10-billion-chicken-finger-dream.html" rel="alternate"/><published>2025-03-18T00:00:00-05:00</published><updated>2025-03-18T00:00:00-05:00</updated><author><name>Corey</name></author><id>tag:gallon.me,2025-03-18:/founders-383-todd-graves-and-his-10-billion-chicken-finger-dream.html</id><summary type="html">&lt;p&gt;I listened to Senra's great episode on Todd Graves of Raising Cane's fame this morning.  Here are my snips from the episode.&lt;/p&gt;</summary><content type="html">&lt;p&gt;I listened to Senra's great episode on Todd Graves of Raising Cane's fame this morning.  Here are my snips from the episode.&lt;/p&gt;
&lt;h1 id="episode-show-notes"&gt;&lt;a href="https://share.snipd.com/episode/df1d9f02-543b-4f34-9358-1d49e5cce045"&gt;Episode Show Notes&lt;/a&gt;&lt;/h1&gt;
&lt;blockquote&gt;
&lt;p&gt;Todd Graves is one of my favorite living entrepreneurs. He's a great example of Charlie Munger's maxim: Find a simple idea and take it seriously. Todd wanted to create a quick service restaurant that only focused on quality chicken finger meals and nothing else. Everyone told him that couldn't possibly work. The college paper that described the idea that would turn into Raising Canes got the lowest grade in the class. Banks wouldn't loan him any money —but nothing could stop Todd from living out his "chicken finger dream." He worked 95 hour weeks as a boilermaker, risked his life on a commercial fishing boat off the coast of Alaska, and scrounged up startup money from his bookie and a guy named Wild Bill. Todd made every mistake in the book, over leveraged himself, almost lost everything and yet he refused to give up or sell out. Today he has over 800 locations, 50,000 employees, and owns 90% of a business that's worth at least $10 billion. Todd's maxim is "Do one thing and do it better than anyone else." &lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id="winning-is-personal"&gt;Winning Is Personal&lt;/h2&gt;
&lt;p&gt;&lt;a href="https://share.snipd.com/snip/ced0a57e-a8e6-4fbd-a6e2-2585d77a9298"&gt;🎧 Play snip - 1min (03:39 - 05:06)&lt;/a&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Approach your work with a personal stake, treating every win as deeply significant and every loss as a personal setback.&lt;/li&gt;
&lt;li&gt;Internalize the drive to win and never let anyone take away what you've worked hard to achieve.&lt;/li&gt;
&lt;li&gt;Cultivate this mindset from the beginning of your endeavors to fuel continuous hard work and determination.&lt;/li&gt;
&lt;li&gt;Take your business or work seriously and be deeply invested in your success, similar to Todd Graves of Raising Cane's and legendary coach Pat Riley.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="maximize-one-or-a-few-variables"&gt;Maximize One or a Few Variables&lt;/h2&gt;
&lt;p&gt;&lt;a href="https://share.snipd.com/snip/f3ee462c-3a18-4ad3-bbe8-bc2d6ed60a98"&gt;🎧 Play snip - 5min (05:17 - 10:12)&lt;/a&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Focus on maximizing one or a few key variables in your business.&lt;/li&gt;
&lt;li&gt;Todd Graves built a successful chain by focusing on simple menu and speed.&lt;/li&gt;
&lt;li&gt;Rockefeller's obsession with efficiency, like minimizing solder drops, saved vast amounts over time.&lt;/li&gt;
&lt;li&gt;Small efficiency gains at scale can compound into large savings.&lt;/li&gt;
&lt;li&gt;Consider how even minor changes in your system can multiply across your operations.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="passion-and-perseverance"&gt;Passion and Perseverance&lt;/h2&gt;
&lt;p&gt;&lt;a href="https://share.snipd.com/snip/7aa54728-d3d8-44c3-97e3-9ce3f3864dc1"&gt;🎧 Play snip - 2min (16:03 - 17:54)&lt;/a&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Choose a career you are passionate about because work can be a grind.&lt;/li&gt;
&lt;li&gt;If you love what you do, you will be happier and more likely to stick with it.&lt;/li&gt;
&lt;li&gt;This long-term commitment allows for compounding success over time, like with Raising Cane's.&lt;/li&gt;
&lt;li&gt;Steve Jobs believed passion is essential for enduring the hardships of work.&lt;/li&gt;
&lt;li&gt;Todd Graves echoed this sentiment, emphasizing that giving up or selling out was never an option for him.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="hands-on-management"&gt;Hands-On Management&lt;/h2&gt;
&lt;p&gt;&lt;a href="https://share.snipd.com/snip/7bb9972b-87fe-4c86-b9a9-681c85155b89"&gt;🎧 Play snip - 2min (21:06 - 23:00)&lt;/a&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Prioritize quality and refuse to compromise.&lt;/li&gt;
&lt;li&gt;Maintain a hands-on management style, overseeing all aspects of the business.&lt;/li&gt;
&lt;li&gt;Treat employees exceptionally well to cultivate loyalty and long-term retention.&lt;/li&gt;
&lt;li&gt;Be relentless in your work ethic, setting an example for your team.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="todd-graves-ownership"&gt;Todd Graves' Ownership&lt;/h2&gt;
&lt;p&gt;&lt;a href="https://share.snipd.com/snip/ca3a65b8-285d-47d9-a71e-006f16a000a2"&gt;🎧 Play snip - 1min (28:10 - 29:28)&lt;/a&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;When asked about funding, Todd Graves revealed he prefers using his own money.&lt;/li&gt;
&lt;li&gt;He owns over 90% of his multi-billion dollar business, Raising Cane's Chicken Fingers.&lt;/li&gt;
&lt;li&gt;This contrasts with many entrepreneurs who get diluted by private equity and eventually sell out.&lt;/li&gt;
&lt;li&gt;Graves' approach allows him to maintain control and passion for his brand.&lt;/li&gt;
&lt;li&gt;The host emphasizes the rarity of such high ownership in large businesses, particularly in the restaurant industry.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="commitment-to-the-long-term"&gt;Commitment to the Long Term&lt;/h2&gt;
&lt;p&gt;&lt;a href="https://share.snipd.com/snip/47d49b8f-ff0b-482a-8320-c86ad7cac32f"&gt;🎧 Play snip - 1min (30:04 - 30:51)&lt;/a&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Your exit strategy should be death, meaning dedicate your life to your business.&lt;/li&gt;
&lt;li&gt;Work on your best ideas and love what you do, like Steve Jobs, Charlie Munger, and Coco Chanel who worked until they died.&lt;/li&gt;
&lt;li&gt;Have a high-risk tolerance as an entrepreneur and aim to retain as much equity as possible because the business is yours.&lt;/li&gt;
&lt;li&gt;Build a multi-generational business that you wouldn't sell.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="todd-graves-and-daniel-ludwigs-creative-financing"&gt;Todd Graves' and Daniel Ludwig's Creative Financing&lt;/h2&gt;
&lt;p&gt;&lt;a href="https://share.snipd.com/snip/8c67987f-1752-4408-af1e-c370502fae21"&gt;🎧 Play snip - 5min (33:03 - 37:35)&lt;/a&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Todd Graves expanded Raising Cane's by acquiring failing double drive-through burger joints and converting them cheaply.&lt;/li&gt;
&lt;li&gt;He financed this by securing subordinated debt notes from angel investors with a guaranteed 15% return, using these notes as collateral for bank loans.&lt;/li&gt;
&lt;li&gt;Daniel Ludwig, once the richest American, started with a 'two-name paper arrangement'.&lt;/li&gt;
&lt;li&gt;He secured long-term charter agreements with oil companies, then used these as collateral for bank loans to build or renovate ships.&lt;/li&gt;
&lt;li&gt;Oil company payments went directly to the bank, covering the loan and depositing the rest into Ludwig's account; this maximized leverage and minimized personal investment.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="todd-graves-on-katrina-and-pandemic-as-opportunities"&gt;Todd Graves on Katrina and Pandemic as Opportunities&lt;/h2&gt;
&lt;p&gt;&lt;a href="https://share.snipd.com/snip/6a548c3b-4d11-470e-918c-0fb82df1d027"&gt;🎧 Play snip - 2min (39:06 - 40:54)&lt;/a&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Todd Graves almost lost Raising Cane's due to over-leveraging when Hurricane Katrina hit, shutting down most of his restaurants.&lt;/li&gt;
&lt;li&gt;However, being the first to reopen turned into an opportunity, gaining new loyal customers in a period with no competition.&lt;/li&gt;
&lt;li&gt;Similarly, during the pandemic, Raising Cane's drive-thru model thrived as an essential business, boosting revenue from $1.5B to $4.5B in three years.&lt;/li&gt;
&lt;li&gt;Graves emphasizes that his sole ownership allowed for quick decision-making during crises, focusing on crew and customers without board or shareholder constraints.&lt;/li&gt;
&lt;li&gt;He highlights the importance of adaptability and decisive action during challenging times.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="raising-canes-love-department"&gt;Raising Cane's 'Love' Department&lt;/h2&gt;
&lt;p&gt;&lt;a href="https://share.snipd.com/snip/d4146b7b-4399-41ad-a35e-8475a6c95fc1"&gt;🎧 Play snip - 3min (42:20 - 45:24)&lt;/a&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Todd Graves prioritizes positive reinforcement and appreciation for his employees, creating a 'Cane's Love' department.&lt;/li&gt;
&lt;li&gt;This department focuses on respecting, rewarding, and recognizing crew members, even for small acts of service.&lt;/li&gt;
&lt;li&gt;Graves emphasizes that good quality work comes from positive motivational management, visiting kitchens and offering praise to employees.&lt;/li&gt;
&lt;li&gt;He believes in caring for his employees, which in turn motivates them to work harder, leading to a positive work environment and efficient service.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="founder-led-advantage"&gt;Founder-Led Advantage&lt;/h2&gt;
&lt;p&gt;&lt;a href="https://share.snipd.com/snip/9ea17ad3-dcda-4d9b-a1c8-675303fc0258"&gt;🎧 Play snip - 1min (47:42 - 48:27)&lt;/a&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Todd Graves prioritizes company-owned Raising Cane's stores over franchises for greater control and quality.&lt;/li&gt;
&lt;li&gt;He believes founder-led businesses have a competitive edge.&lt;/li&gt;
&lt;li&gt;Large corporations often make impersonal financial decisions, while founders have a personal stake.&lt;/li&gt;
&lt;li&gt;Graves emphasizes that Raising Cane's is personal to him, which drives his commitment to excellence.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="missionary-founder"&gt;Missionary Founder&lt;/h2&gt;
&lt;p&gt;&lt;a href="https://share.snipd.com/snip/d8c8b112-c8c0-45ba-80d8-1be264d0a69f"&gt;🎧 Play snip - 1min (48:25 - 49:30)&lt;/a&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Todd Graves believes his talent for chicken fingers is divinely inspired to help others.&lt;/li&gt;
&lt;li&gt;He encourages aspiring entrepreneurs to pursue their unique concepts, fostering diversity and innovation.&lt;/li&gt;
&lt;li&gt;Despite billion-dollar offers, he remains committed to his mission, prioritizing purpose over profit.&lt;/li&gt;
&lt;li&gt;Graves believes that God makes everyone good at something to ultimately help others.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="leverage-your-assets"&gt;Leverage Your Assets&lt;/h2&gt;
&lt;p&gt;&lt;a href="https://share.snipd.com/snip/e9140733-7229-4b4e-b6c7-66044a81eee3"&gt;🎧 Play snip - 1min (49:56 - 50:42)&lt;/a&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Consider what non-financial assets you possess that you aren't fully utilizing.&lt;/li&gt;
&lt;li&gt;When starting out, you might lack money or experience, but you have assets that older, wealthier individuals don't.&lt;/li&gt;
&lt;li&gt;Younger people often have abundant energy and determination, which can be leveraged to outwork others.&lt;/li&gt;
&lt;li&gt;Todd Graves, the founder of Raising Cane's, emphasizes the importance of youthful energy and determination in his entrepreneurial journey.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="last-business"&gt;Last Business&lt;/h2&gt;
&lt;p&gt;&lt;a href="https://share.snipd.com/snip/ea8261f5-ab27-4cee-bdbf-ba59d77ffda0"&gt;🎧 Play snip - 1min (50:51 - 51:32)&lt;/a&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;David Senra expresses more interest in a founder's final business than their initial ventures.&lt;/li&gt;
&lt;li&gt;He believes understanding oneself and accumulating experience often leads to starting multiple businesses.&lt;/li&gt;
&lt;li&gt;However, some founders, like Todd Graves and Sam Walton, find their ultimate business early on and dedicate themselves to it fully.&lt;/li&gt;
&lt;li&gt;This dedicated focus allows for growth in unforeseen ways over time.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="extreme-patience-and-intolerance-for-slowness"&gt;Extreme Patience and Intolerance for Slowness&lt;/h2&gt;
&lt;p&gt;&lt;a href="https://share.snipd.com/snip/68348187-4763-4537-8cf4-37d1b69fc207"&gt;🎧 Play snip - 1min (53:42 - 54:55)&lt;/a&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Todd Graves took 25 years to build 600 Raising Cane's locations, then added 200 more in just two to three years.&lt;/li&gt;
&lt;li&gt;Sam Walton, in his first five years of retail, only opened one store.&lt;/li&gt;
&lt;li&gt;Three decades later, he launched Sam's Club and reached $1 billion in sales in three years.&lt;/li&gt;
&lt;li&gt;In 7 years, he expanded from zero to 105 stores.&lt;/li&gt;
&lt;li&gt;This exemplifies "extreme patience coupled with an extreme intolerance for slowness."&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="trust-your-gut"&gt;Trust Your Gut&lt;/h2&gt;
&lt;p&gt;&lt;a href="https://share.snipd.com/snip/7095429a-301d-4fec-bb32-0d0cb4d8edbf"&gt;🎧 Play snip - 1min (57:13 - 57:55)&lt;/a&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Don't listen to so-called 'experts'.&lt;/li&gt;
&lt;li&gt;Listen to your gut and intuition.&lt;/li&gt;
&lt;li&gt;Stay true to your vision and what you do well.&lt;/li&gt;
&lt;li&gt;Avoid trying to be all things to all people.&lt;/li&gt;
&lt;li&gt;Know who you are and stick to it.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="henry-ford-on-experts"&gt;Henry Ford on Experts&lt;/h2&gt;
&lt;p&gt;&lt;a href="https://share.snipd.com/snip/9633a257-30c7-44a2-977e-6aaeae1cd560"&gt;🎧 Play snip - 1min (59:18 - 01:00:14)&lt;/a&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Henry Ford didn't believe experts could see the potential of the internal combustion engine.&lt;/li&gt;
&lt;li&gt;They were too focused on its limitations compared to steam.&lt;/li&gt;
&lt;li&gt;Ford believed that experts could kill opposition with too much advice and little work.&lt;/li&gt;
&lt;li&gt;He valued trusting his gut and staying true to his vision.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="focus-on-details"&gt;Focus on Details&lt;/h2&gt;
&lt;p&gt;&lt;a href="https://share.snipd.com/snip/f0bc05c9-a1a0-4d37-aeeb-6611696f7dbe"&gt;🎧 Play snip - 15sec (01:00:37 - 01:00:53)&lt;/a&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Limit the number of things you try to perfect.&lt;/li&gt;
&lt;li&gt;Then, make every detail of those things perfect.&lt;/li&gt;
&lt;li&gt;This allows for laser focus on making each item great.&lt;/li&gt;
&lt;li&gt;Bring in people smarter than you in their respective areas but don't stop being detail-oriented yourself.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="stay-in-the-details"&gt;Stay in the Details&lt;/h2&gt;
&lt;p&gt;&lt;a href="https://share.snipd.com/snip/dcc9f891-0443-4c04-b0a8-d29dba9c6807"&gt;🎧 Play snip - 2min (01:00:56 - 01:02:48)&lt;/a&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Work with great leaders, but stay involved in the details of your business.&lt;/li&gt;
&lt;li&gt;Don't just delegate; understand the specifics of what's happening.&lt;/li&gt;
&lt;li&gt;Even as your business grows, maintain awareness of seemingly small expenses, as they reflect broader spending habits.&lt;/li&gt;
&lt;li&gt;Todd Graves emphasizes this, citing Edison Chouest, a successful shipbuilder, as an example of someone who paid attention to even the cost of bottled water.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="do-what-you-love"&gt;Do What You Love&lt;/h2&gt;
&lt;p&gt;&lt;a href="https://share.snipd.com/snip/7b400253-e850-41d0-aa5a-87e178f64a90"&gt;🎧 Play snip - 1min (01:02:39 - 01:04:01)&lt;/a&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The most successful people stay engaged in the details of their work.&lt;/li&gt;
&lt;li&gt;This is easier when you're passionate about what you do.&lt;/li&gt;
&lt;li&gt;Don't be afraid to do things yourself, even if others advise against it.&lt;/li&gt;
&lt;li&gt;Working on something you love doesn't feel like work; it's enjoyable.&lt;/li&gt;
&lt;li&gt;“You don't work your entire life to get, to, to be able to do what you love, to not do it.”&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="passion-as-a-competitive-advantage"&gt;Passion as a Competitive Advantage&lt;/h2&gt;
&lt;p&gt;&lt;a href="https://share.snipd.com/snip/aed50b9c-5cb6-4cbf-830e-48fead53dbab"&gt;🎧 Play snip - 1min (01:03:46 - 01:05:08)&lt;/a&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Loving what you do makes hard work easier, as exemplified by Todd Graves's dedication to his "chicken finger dream."&lt;/li&gt;
&lt;li&gt;True passion fuels long hours and hands-on involvement in every aspect of the business.&lt;/li&gt;
&lt;li&gt;Competing with someone driven by such passion is difficult because their work ethic becomes a major competitive advantage.&lt;/li&gt;
&lt;li&gt;Graves welcomes competition but warns potential rivals to be prepared for the intense dedication required to challenge a business built on personal commitment and family values.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="fear-of-purpose"&gt;Fear of Purpose&lt;/h2&gt;
&lt;p&gt;&lt;a href="https://share.snipd.com/snip/a622c509-c328-4103-a4de-e3badc39016e"&gt;🎧 Play snip - 1min (01:05:40 - 01:07:09)&lt;/a&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Todd Graves advises entrepreneurs to honestly confront their life's purpose.&lt;/li&gt;
&lt;li&gt;He acknowledges that accepting this purpose can be daunting, as it requires dedicating oneself entirely to it.&lt;/li&gt;
&lt;li&gt;This resonates with Kobe Bryant's message about facing our greatest fear: ourselves and our dreams.&lt;/li&gt;
&lt;li&gt;Bryant emphasizes the fear of fully committing to a dream and potentially failing.&lt;/li&gt;
&lt;li&gt;He encourages pursuing dreams fearlessly, not for external validation, but for self-fulfillment.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="todd-graves-advice-to-entrepreneurs"&gt;Todd Graves' Advice to Entrepreneurs&lt;/h2&gt;
&lt;p&gt;&lt;a href="https://share.snipd.com/snip/6c307688-3b50-458d-bb3d-68bbabeca3d4"&gt;🎧 Play snip - 29sec (01:07:15 - 01:07:44)&lt;/a&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Never give up on your vision, even when faced with challenges like securing financing or finding a suitable location.&lt;/li&gt;
&lt;li&gt;Be fanatical about your pursuit.&lt;/li&gt;
&lt;li&gt;True success comes from relentlessly pursuing your vision with unwavering passion.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;p&gt;&lt;strong&gt;Feature Image Prompt:&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Generate an image. The aesthetic should be cyberpunk with colors of neon pink, blue and purple. Do not add any people. Envision a dazzling futuristic metropolis at night where holographic skyscrapers and digital billboards rise like monoliths from a rain-soaked cityscape. In the heart of the scene, a sleek, high-tech restaurant stands out with a luminous, abstract neon chicken finger emblem that pulses with electric energy. Intricate, circuit-like patterns and reflective surfaces weave through the urban tapestry, evoking the raw determination, innovative spirit, and relentless ambition of a visionary billion-dollar dream.&lt;/p&gt;</content><category term="Snips"/><category term="founders"/><category term="podcasts"/><category term="snips"/></entry><entry><title>How to Add Custom HTML Pages to a Pelican Site</title><link href="https://gallon.me/how-to-add-custom-html-pages-to-a-pelican-site.html" rel="alternate"/><published>2025-02-27T00:00:00-06:00</published><updated>2025-02-27T00:00:00-06:00</updated><author><name>Cyrano</name></author><id>tag:gallon.me,2025-02-27:/how-to-add-custom-html-pages-to-a-pelican-site.html</id><summary type="html">&lt;p&gt;Pelican is a powerful static site generator written in Python, primarily designed for blogs. However, sometimes you need to include custom HTML pages that don't fit into Pelican's standard content model. This guide explains how to add standalone HTML pages to your Pelican site while preserving their exact formatting and …&lt;/p&gt;</summary><content type="html">&lt;h2 id="introduction"&gt;Introduction&lt;/h2&gt;
&lt;p&gt;Pelican is a powerful static site generator written in Python, primarily designed for blogs. However, sometimes you need to include custom HTML pages that don't fit into Pelican's standard content model. This guide explains how to add standalone HTML pages to your Pelican site while preserving their exact formatting and functionality.&lt;/p&gt;
&lt;h2 id="method-using-static-files"&gt;Method: Using Static Files&lt;/h2&gt;
&lt;p&gt;The most reliable method for adding custom HTML pages to a Pelican site is to use the static files approach. This method bypasses Pelican's content processing system and treats your HTML file as a static asset.&lt;/p&gt;
&lt;h3 id="step-1-create-a-static-directory"&gt;Step 1: Create a Static Directory&lt;/h3&gt;
&lt;p&gt;First, create a static directory inside your Pelican project's content folder:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;mkdir -p content/static
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h3 id="step-2-add-your-html-file"&gt;Step 2: Add Your HTML File&lt;/h3&gt;
&lt;p&gt;Copy your custom HTML file to the static directory:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;cp your-custom-page.html content/static/
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h3 id="step-3-configure-pelican"&gt;Step 3: Configure Pelican&lt;/h3&gt;
&lt;p&gt;Edit your pelicanconf.py file to include the following settings:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# Include static directory in static paths&lt;/span&gt;
&lt;span class="n"&gt;STATIC_PATHS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;images&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;static&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;  &lt;span class="c1"&gt;# Add other paths as needed&lt;/span&gt;

&lt;span class="c1"&gt;# Exclude static directory from content processing&lt;/span&gt;
&lt;span class="n"&gt;ARTICLE_EXCLUDES&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;static&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;PAGE_EXCLUDES&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;static&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="c1"&gt;# Optional: Configure a specific URL for your page&lt;/span&gt;
&lt;span class="n"&gt;EXTRA_PATH_METADATA&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="s1"&gt;&amp;#39;static/your-custom-page.html&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;path&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;custom-page/index.html&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;This configuration does three important things:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;STATIC_PATHS tells Pelican to copy files from the static directory to the output&lt;/li&gt;
&lt;li&gt;ARTICLE_EXCLUDES and PAGE_EXCLUDES prevent Pelican from trying to process the files as content&lt;/li&gt;
&lt;li&gt;EXTRA_PATH_METADATA allows you to specify a custom output path for your HTML file&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="step-4-build-your-site"&gt;Step 4: Build Your Site&lt;/h3&gt;
&lt;p&gt;Now when you build your Pelican site, your custom HTML page will be copied to the output directory:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;pelican content
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Your page will be accessible at yourblog.com/custom-page/.&lt;/p&gt;
&lt;h2 id="why-this-works"&gt;Why This Works&lt;/h2&gt;
&lt;p&gt;This approach works because:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Bypasses Content Processing: By excluding the static directory from content processing, Pelican won't try to parse your HTML file for metadata or apply templates to it.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Preserves Original HTML: Your HTML file is copied as-is to the output directory, preserving all styling, scripts, and functionality.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Custom URL Structure: Using EXTRA_PATH_METADATA allows you to place the file at any URL path you want, creating clean URLs without the .html extension.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="common-issues-and-solutions"&gt;Common Issues and Solutions&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Issue: Pelican tries to process HTML as content&lt;/strong&gt;&lt;br&gt;
Solution: Make sure you've added the static directory to both ARTICLE_EXCLUDES and PAGE_EXCLUDES.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Issue: CSS or JavaScript paths are broken&lt;/strong&gt;&lt;br&gt;
Solution: If your HTML file references relative paths for CSS or JavaScript, you may need to adjust them to work with your site's URL structure.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Issue: HTML file not showing up in output&lt;/strong&gt;&lt;br&gt;
Solution: Verify that you've added the static directory to STATIC_PATHS and that the file exists in the correct location.&lt;/p&gt;
&lt;h2 id="advanced-linking-to-your-custom-page"&gt;Advanced: Linking to Your Custom Page&lt;/h2&gt;
&lt;p&gt;To link to your custom page from your Pelican site's navigation menu, you'll need to modify your theme's templates. Most themes use a variable like MENUITEMS in the pelicanconf.py file:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="n"&gt;MENUITEMS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;Home&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;/&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;Blog&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;/blog/&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;Custom Page&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;/custom-page/&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h2 id="integrating-custom-pages-with-site-navigation"&gt;Integrating Custom Pages with Site Navigation&lt;/h2&gt;
&lt;p&gt;After adding your custom HTML page to your Pelican site, you'll likely want to make it accessible through your site's navigation menu. There are several ways to accomplish this, depending on your theme and navigation preferences.&lt;/p&gt;
&lt;h3 id="using-menuitems-for-basic-navigation"&gt;Using MENUITEMS for Basic Navigation&lt;/h3&gt;
&lt;p&gt;The simplest approach is to use Pelican's built-in &lt;code&gt;MENUITEMS&lt;/code&gt; configuration variable, which most themes support. In your &lt;code&gt;pelicanconf.py&lt;/code&gt; file, add or modify the &lt;code&gt;MENUITEMS&lt;/code&gt; list:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="n"&gt;MENUITEMS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;Home&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;/&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;Blog&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;/blog/&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;Custom Page&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;/custom-page/&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;  &lt;span class="c1"&gt;# This links to your custom HTML page&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Each tuple contains two elements:
1. The display name that will appear in your menu
2. The URL path to the page (which should match what you configured in &lt;code&gt;EXTRA_PATH_METADATA&lt;/code&gt;)&lt;/p&gt;
&lt;h3 id="balancing-menuitems-with-dynamic-category-links"&gt;Balancing MENUITEMS with Dynamic Category Links&lt;/h3&gt;
&lt;p&gt;If your theme automatically displays categories in the navigation menu, you'll need to consider how static menu items and dynamic category links will interact. Here are some common configurations:&lt;/p&gt;
&lt;h4 id="option-1-disable-category-menu-items"&gt;Option 1: Disable Category Menu Items&lt;/h4&gt;
&lt;p&gt;If you prefer to manually control all menu items, you can disable automatic category links:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# Disable automatic category links in menu&lt;/span&gt;
&lt;span class="n"&gt;DISPLAY_CATEGORIES_ON_MENU&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;False&lt;/span&gt;

&lt;span class="c1"&gt;# Define your custom menu structure&lt;/span&gt;
&lt;span class="n"&gt;MENUITEMS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;Home&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;/&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;Blog&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;/blog/&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;Categories&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;/categories/&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;  &lt;span class="c1"&gt;# Link to category index if available&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;Custom Page&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;/custom-page/&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h4 id="option-2-keep-both-systems"&gt;Option 2: Keep Both Systems&lt;/h4&gt;
&lt;p&gt;Many themes will gracefully handle both static menu items and dynamic category links, placing &lt;code&gt;MENUITEMS&lt;/code&gt; first, followed by category links:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# Keep automatic category links enabled (default behavior)&lt;/span&gt;
&lt;span class="c1"&gt;# DISPLAY_CATEGORIES_ON_MENU = True  # This is usually the default&lt;/span&gt;

&lt;span class="c1"&gt;# Define static menu items&lt;/span&gt;
&lt;span class="n"&gt;MENUITEMS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;Home&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;/&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;Blog&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;/blog/&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;Custom Page&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;/custom-page/&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h3 id="theme-specific-navigation"&gt;Theme-Specific Navigation&lt;/h3&gt;
&lt;p&gt;Some themes implement custom navigation systems that don't rely solely on &lt;code&gt;MENUITEMS&lt;/code&gt;. In these cases, consult your theme's documentation or examine its template files to understand how to integrate custom pages.&lt;/p&gt;
&lt;p&gt;For example, the popular theme "Flex" uses a variable called &lt;code&gt;MAIN_MENU&lt;/code&gt; with a more complex structure:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="n"&gt;MAIN_MENU&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;True&lt;/span&gt;
&lt;span class="n"&gt;MENUITEMS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;Home&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;/&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;Custom Page&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;/custom-page/&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;Categories&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;Category 1&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;/category/category-1/&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;Category 2&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;/category/category-2/&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;)),&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h3 id="testing-your-navigation"&gt;Testing Your Navigation&lt;/h3&gt;
&lt;p&gt;Whenever you modify your menu configuration, test your site by regenerating it:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;pelican&lt;span class="w"&gt; &lt;/span&gt;content
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Then check the navigation links in your local development server to ensure they work as expected.&lt;/p&gt;
&lt;h3 id="troubleshooting-common-issues"&gt;Troubleshooting Common Issues&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Issue: Menu items don't appear&lt;/strong&gt;&lt;br&gt;
Check if your theme supports &lt;code&gt;MENUITEMS&lt;/code&gt; or requires a different variable. Some themes require &lt;code&gt;MAIN_MENU = True&lt;/code&gt; to be set as well.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Issue: Links don't work correctly&lt;/strong&gt;&lt;br&gt;
Verify that your URL paths match the structure defined in &lt;code&gt;EXTRA_PATH_METADATA&lt;/code&gt;. Remember that if you're using &lt;code&gt;RELATIVE_URLS = True&lt;/code&gt; for development, your paths will be relative.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Issue: Unexpected menu order&lt;/strong&gt;&lt;br&gt;
If categories are appearing between your static menu items, you may need to override your theme's template files to control the precise ordering.&lt;/p&gt;
&lt;h2 id="conclusion"&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;Using the static files approach is the most straightforward way to add custom HTML pages to your Pelican site. It preserves the original HTML without modification, allows for custom URL structures, and integrates seamlessly with the rest of your site.&lt;/p&gt;
&lt;p&gt;This method is particularly useful for: 
- Interactive pages with complex JavaScript 
- HTML generated by other tools 
- Pages with custom styling that shouldn't be affected by your site's theme 
- Embedding third-party applications or widgets&lt;/p&gt;
&lt;p&gt;By following these steps, you can extend your Pelican site beyond its standard blog functionality while maintaining full control over your custom HTML content.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;strong&gt;Feature Image Prompt:&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Generate an image. The aesthetic should be cyberpunk with colors of neon pink, blue and purple. Do not add any people. Imagine a futuristic digital workspace where sleek holographic panels display lines of glowing HTML code and static web assets, symbolizing custom HTML pages for a Pelican site. The background features a neon-lit cityscape with pulsating grids and cybernetic data streams flowing through an atmospheric digital environment. The scene exudes high-tech energy and the seamless integration of traditional web development with a modern, cyberpunk twist.&lt;/p&gt;</content><category term="TIL"/><category term="webdev"/></entry><entry><title>AI Engineer Summit 2025: Agents at Work</title><link href="https://gallon.me/ai-engineer-summit-2025-agents-at-work.html" rel="alternate"/><published>2025-02-22T00:00:00-06:00</published><updated>2025-02-22T00:00:00-06:00</updated><author><name>Corey</name></author><id>tag:gallon.me,2025-02-22:/ai-engineer-summit-2025-agents-at-work.html</id><summary type="html"/><content type="html">&lt;style&gt;
        @import url('https://fonts.googleapis.com/css2?family=Rajdhani:wght@300;400;500;600;700&amp;family=Share+Tech+Mono&amp;display=swap');
        @import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&amp;display=swap');

        .cyberpunk-article {
            --neon-pink: #ff2a6d;
            --neon-blue: #05d9e8;
            --neon-purple: #be00fe;
            --dark-bg: #000000;
            --dark-accent: #1a1330;
            --light-text: #d1f7ff;
            --grid-color: rgba(5, 217, 232, 0.2);
        }

        .cyberpunk-article {
            background-color: var(--dark-bg);
            color: var(--light-text);
            font-family: 'Inter', sans-serif;
            line-height: 1.6;
            background-image: 
                linear-gradient(0deg, var(--grid-color) 1px, transparent 1px),
                linear-gradient(90deg, var(--grid-color) 1px, transparent 1px);
            background-size: 20px 20px;
            overflow-x: hidden;
        }

        .cyberpunk-article .container {
            max-width: 1000px;
            margin: 0 auto;
            padding: 20px;
            width: 100%;
            box-sizing: border-box;
        }

        .cyberpunk-article .header {
            position: relative;
            padding: 40px 30px;
            margin-bottom: 0;
            border-bottom: 2px solid var(--neon-pink);
            background: rgba(0, 0, 0, 0.7);
            overflow: hidden;
        }

        .cyberpunk-article .header::before {
            content: '';
            position: absolute;
            top: 0;
            left: 0;
            right: 0;
            height: 3px;
            background: linear-gradient(90deg, var(--neon-blue), var(--neon-purple), var(--neon-pink));
            z-index: 1;
            animation: neonGlow 3s ease-in-out infinite;
        }

        @keyframes neonGlow {
            0% { opacity: 1; }
            50% { opacity: 0.7; }
            100% { opacity: 1; }
        }

        .cyberpunk-article .glitch-wrapper {
            position: relative;
        }

        .cyberpunk-article .header h1 {
            font-family: 'Share Tech Mono', monospace;
            font-size: 42px;
            color: var(--light-text);
            text-shadow: 
                0 0 5px var(--neon-blue),
                0 0 10px var(--neon-blue),
                0 0 20px var(--neon-purple);
            margin-bottom: 5px;
            position: relative;
            z-index: 1;
            word-wrap: break-word;
        }

        .cyberpunk-article .subtitle {
            font-family: 'Share Tech Mono', monospace;
            font-size: 32px;
            font-weight: bold;
            color: var(--neon-blue);
            margin-bottom: 15px;
            margin-top: 10px;
            text-align: left;
            letter-spacing: 1px;
            position: relative;
            z-index: 1;
            display: inline-block;
            clear: both;
            word-wrap: break-word;
        }

        .cyberpunk-article .subtitle::after {
            content: '';
            position: absolute;
            bottom: -5px;
            left: 0;
            width: 100%;
            height: 2px;
            background: var(--neon-pink);
        }

        .cyberpunk-article .header-info {
            font-size: 18px;
            margin-bottom: 5px;
        }

        .cyberpunk-article .header-info strong {
            color: var(--neon-pink);
            font-weight: 600;
        }

        .cyberpunk-article .neon-divider {
            height: 2px;
            background: linear-gradient(90deg, var(--neon-blue), var(--neon-purple), var(--neon-pink));
            margin: 30px 0;
            position: relative;
        }

        .cyberpunk-article .neon-divider::after {
            content: '';
            position: absolute;
            top: 0;
            left: 0;
            right: 0;
            bottom: 0;
            box-shadow: 0 0 10px 1px var(--neon-purple);
        }

        .cyberpunk-article .newsletter-content {
            background-color: rgba(0, 0, 0, 0.3);
            padding: 30px;
            backdrop-filter: blur(5px);
            border-left: 2px solid var(--neon-blue);
            margin-bottom: 30px;
        }

        .cyberpunk-article h2 {
            font-family: 'Share Tech Mono', monospace;
            color: var(--neon-blue);
            margin: 25px 0 15px 0;
            position: relative;
            display: inline-block;
            font-size: 28px;
        }

        .cyberpunk-article h2::after {
            content: '';
            position: absolute;
            bottom: -5px;
            left: 0;
            width: 100%;
            height: 2px;
            background: var(--neon-pink);
        }

        .cyberpunk-article h3 {
            font-family: 'Share Tech Mono', monospace;
            color: var(--neon-purple);
            margin: 20px 0 10px 0;
            font-size: 22px;
        }

        .cyberpunk-article p {
            margin-bottom: 15px;
            font-size: 16px;
            line-height: 1.7;
        }

        .cyberpunk-article ul, .cyberpunk-article ol {
            margin: 15px 0 15px 25px;
        }

        .cyberpunk-article li {
            margin-bottom: 8px;
        }

        .cyberpunk-article .quote {
            border-left: 3px solid var(--neon-pink);
            padding: 10px 20px;
            margin: 20px 0;
            position: relative;
            background: rgba(190, 0, 254, 0.1);
        }

        .cyberpunk-article .quote p {
            font-style: italic;
        }

        .cyberpunk-article .quote cite {
            display: block;
            text-align: right;
            margin-top: 10px;
            color: var(--neon-pink);
            font-size: 14px;
        }

        .cyberpunk-article .quote::before {
            content: '"';
            position: absolute;
            left: 7px;
            top: -10px;
            font-size: 60px;
            color: var(--neon-pink);
            opacity: 0.4;
            font-family: 'Share Tech Mono', monospace;
        }

        .cyberpunk-article .highlights {
            display: grid;
            grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
            gap: 20px;
            margin: 30px 0;
        }

        .cyberpunk-article .highlight-card {
            background: var(--dark-accent);
            border: 1px solid var(--neon-blue);
            padding: 20px;
            position: relative;
            overflow: hidden;
        }

        .cyberpunk-article .highlight-card::before {
            content: '';
            position: absolute;
            top: 0;
            left: 0;
            width: 100%;
            height: 3px;
            background: var(--neon-pink);
        }

        .cyberpunk-article .highlight-card h4 {
            color: var(--neon-blue);
            margin-bottom: 10px;
            font-family: 'Share Tech Mono', monospace;
        }

        .cyberpunk-article .footer {
            text-align: center;
            padding: 40px 30px;
            margin-top: 40px;
            border-top: 2px solid var(--neon-purple);
            font-size: 14px;
            position: relative;
            background: rgba(10, 2, 33, 0.3);
            backdrop-filter: blur(5px);
            border-radius: 8px;
            display: flex;
            flex-direction: column;
            align-items: center;
            animation: footerFadeIn 1s ease-out forwards;
            opacity: 0;
        }

        @keyframes footerFadeIn {
            from {
                opacity: 0;
                transform: translateY(20px);
            }
            to {
                opacity: 1;
                transform: translateY(0);
            }
        }

        .cyberpunk-article .footer p {
            margin-bottom: 10px;
        }

        .cyberpunk-article .footer-content {
            display: flex;
            flex-wrap: wrap;
            justify-content: center;
            gap: 30px;
            margin-bottom: 20px;
            width: 100%;
            max-width: 800px;
        }

        .cyberpunk-article .footer-left {
            flex: 1;
            min-width: 250px;
            text-align: left;
            border-left: 2px solid var(--neon-blue);
            padding-left: 20px;
            animation: slideInLeft 0.8s ease-out forwards;
            opacity: 0;
            animation-delay: 0.3s;
        }

        @keyframes slideInLeft {
            from {
                opacity: 0;
                transform: translateX(-20px);
            }
            to {
                opacity: 1;
                transform: translateX(0);
            }
        }

        .cyberpunk-article .footer-right {
            flex: 1;
            min-width: 250px;
            text-align: left;
            border-left: 2px solid var(--neon-pink);
            padding-left: 20px;
            animation: slideInRight 0.8s ease-out forwards;
            opacity: 0;
            animation-delay: 0.5s;
        }

        @keyframes slideInRight {
            from {
                opacity: 0;
                transform: translateX(20px);
            }
            to {
                opacity: 1;
                transform: translateX(0);
            }
        }

        .cyberpunk-article .footer-name {
            font-family: 'Share Tech Mono', monospace;
            color: var(--neon-blue);
            font-size: 20px;
            margin-bottom: 5px;
            letter-spacing: 1px;
            position: relative;
            display: inline-block;
        }

        .cyberpunk-article .footer-name::after {
            content: '';
            position: absolute;
            bottom: -2px;
            left: 0;
            width: 100%;
            height: 1px;
            background: var(--neon-blue);
            animation: lineGrow 1.5s ease-in-out infinite alternate;
        }

        @keyframes lineGrow {
            from {
                width: 0;
                opacity: 0.5;
            }
            to {
                width: 100%;
                opacity: 1;
            }
        }

        .cyberpunk-article .footer-role {
            color: var(--light-text);
            font-size: 16px;
            margin-bottom: 15px;
        }

        .cyberpunk-article .footer-company {
            color: var(--neon-purple);
            font-size: 16px;
            margin-bottom: 20px;
            font-weight: 500;
        }

        .cyberpunk-article .footer-contact {
            display: flex;
            flex-direction: column;
            gap: 8px;
        }

        .cyberpunk-article .footer-contact-item {
            display: flex;
            align-items: center;
            gap: 10px;
            transition: transform 0.3s ease;
        }

        .cyberpunk-article .footer-contact-item:hover {
            transform: translateX(5px);
        }

        .cyberpunk-article .footer-contact-item span {
            color: var(--neon-pink);
            font-weight: 500;
        }

        .cyberpunk-article .footer a {
            color: var(--neon-blue);
            text-decoration: none;
            transition: all 0.3s ease;
            position: relative;
            padding-bottom: 2px;
        }

        .cyberpunk-article .footer a::after {
            content: '';
            position: absolute;
            bottom: 0;
            left: 0;
            width: 0;
            height: 1px;
            background: var(--neon-pink);
            transition: width 0.3s ease;
        }

        .cyberpunk-article .footer a:hover {
            color: var(--neon-pink);
        }

        .cyberpunk-article .footer a:hover::after {
            width: 100%;
        }

        .cyberpunk-article .footer-cta {
            margin-top: 20px;
            color: var(--neon-pink);
            font-size: 18px;
            font-weight: 600;
            letter-spacing: 1px;
            text-shadow: 0 0 5px rgba(255, 42, 109, 0.5);
            font-family: 'Share Tech Mono', monospace;
            animation: glowPulse 2s ease-in-out infinite alternate;
            opacity: 0;
            animation-delay: 0.8s;
            animation-fill-mode: forwards;
        }

        @keyframes glowPulse {
            0% {
                opacity: 1;
                text-shadow: 0 0 5px rgba(255, 42, 109, 0.5);
            }
            100% {
                opacity: 1;
                text-shadow: 0 0 15px rgba(255, 42, 109, 0.8);
            }
        }

        .cyberpunk-article .footer::after {
            content: '';
            position: absolute;
            bottom: 0;
            left: 0;
            right: 0;
            height: 3px;
            background: linear-gradient(90deg, var(--neon-pink), var(--neon-purple), var(--neon-blue));
        }

        .cyberpunk-article code {
            font-family: 'Share Tech Mono', monospace;
            background: rgba(5, 217, 232, 0.1);
            color: var(--neon-blue);
            padding: 2px 5px;
            border-radius: 3px;
        }

        /* Custom styling for blockquotes to make them stand out */
        .cyberpunk-article blockquote {
            border-left: 3px solid var(--neon-pink);
            padding: 10px 20px;
            margin: 20px 0;
            background: rgba(255, 42, 109, 0.1);
            font-style: italic;
        }

        /* Section animations */
        .cyberpunk-article .section-animate {
            animation: sectionFadeIn 0.8s ease-out forwards;
            opacity: 0;
        }

        @keyframes sectionFadeIn {
            from {
                opacity: 0;
                transform: translateY(20px);
            }
            to {
                opacity: 1;
                transform: translateY(0);
            }
        }

        /* Grid/cyberpunk elements */
        .cyberpunk-article .cyber-circuit {
            position: absolute;
            width: 300px;
            height: 300px;
            background-image: 
                radial-gradient(var(--neon-blue) 1px, transparent 1px),
                radial-gradient(var(--neon-purple) 1px, transparent 1px);
            background-size: 30px 30px;
            background-position: 0 0, 15px 15px;
            transform: rotate(45deg);
            opacity: 0.1;
            z-index: 0;
        }

        /* Table styling */
        .cyberpunk-article table {
            width: 100%;
            border-collapse: collapse;
            margin: 20px 0;
            font-family: 'Inter', sans-serif;
        }

        .cyberpunk-article th {
            background-color: rgba(190, 0, 254, 0.2);
            color: var(--neon-blue);
            padding: 12px 15px;
            text-align: left;
            font-family: 'Share Tech Mono', monospace;
            border-bottom: 2px solid var(--neon-pink);
        }

        .cyberpunk-article td {
            padding: 10px 15px;
            border-bottom: 1px solid rgba(5, 217, 232, 0.2);
        }

        .cyberpunk-article tr:hover {
            background-color: rgba(5, 217, 232, 0.05);
        }

        /* Added styles for specific demonstration */
        .cyberpunk-article .takeaways-grid {
            display: grid;
            grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
            gap: 20px;
            margin: 30px 0;
        }

        .cyberpunk-article .takeaway-card {
            background: var(--dark-accent);
            border: 1px solid var(--neon-blue);
            padding: 20px;
            position: relative;
            box-shadow: 0 0 15px rgba(5, 217, 232, 0.15);
        }

        .cyberpunk-article .takeaway-card::before {
            content: '';
            position: absolute;
            top: 0;
            left: 0;
            width: 100%;
            height: 3px;
            background: var(--neon-pink);
        }

        .cyberpunk-article .questions-list {
            list-style-type: none;
            margin: 20px 0;
            padding: 0;
        }

        .cyberpunk-article .question-item {
            background: var(--dark-accent);
            margin-bottom: 15px;
            padding: 15px 20px;
            border-left: 3px solid var(--neon-purple);
            position: relative;
            transition: transform 0.3s ease;
        }

        .cyberpunk-article .question-item:hover {
            transform: translateX(5px);
            background: rgba(26, 19, 48, 0.9);
        }

        .cyberpunk-article .quotes-grid {
            display: grid;
            grid-template-columns: repeat(auto-fill, minmax(400px, 1fr));
            gap: 25px;
            margin: 30px 0;
        }

        .cyberpunk-article .quote-card {
            background: rgba(26, 19, 48, 0.7);
            border-left: 3px solid var(--neon-pink);
            padding: 20px;
            position: relative;
            box-shadow: 0 0 20px rgba(190, 0, 254, 0.2);
        }

        .cyberpunk-article .quote-text {
            font-style: italic;
            margin-bottom: 10px;
            position: relative;
            padding-left: 20px;
        }

        .cyberpunk-article .quote-text::before {
            content: '"';
            position: absolute;
            left: 0;
            top: -5px;
            font-size: 30px;
            color: var(--neon-pink);
            font-family: 'Share Tech Mono', monospace;
        }

        .cyberpunk-article .quote-attribution {
            text-align: right;
            color: var(--neon-blue);
            font-family: 'Share Tech Mono', monospace;
        }

        .cyberpunk-article .glitch-effect {
            position: relative;
            display: inline-block;
        }

        .cyberpunk-article .glitch-effect::before,
        .cyberpunk-article .glitch-effect::after {
            content: attr(data-text);
            position: absolute;
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
        }

        .cyberpunk-article .glitch-effect::before {
            left: 2px;
            text-shadow: -1px 0 var(--neon-pink);
            clip: rect(44px, 450px, 56px, 0);
            animation: glitch-anim-1 5s linear infinite alternate-reverse;
        }

        .cyberpunk-article .glitch-effect::after {
            left: -2px;
            text-shadow: -1px 0 var(--neon-blue);
            clip: rect(44px, 450px, 56px, 0);
            animation: glitch-anim-2 5s linear infinite alternate-reverse;
        }

        .cyberpunk-article .glitch-effect::before {
            left: 2px;
            text-shadow: -1px 0 var(--neon-pink);
            clip: rect(44px, 450px, 56px, 0);
            animation: glitch-anim-1 5s linear infinite alternate-reverse;
        }

        .cyberpunk-article .glitch-effect::after {
            left: -2px;
            text-shadow: -1px 0 var(--neon-blue);
            clip: rect(44px, 450px, 56px, 0);
            animation: glitch-anim-2 5s linear infinite alternate-reverse;
        }

        @keyframes glitch-anim-1 {
            0% {
                clip: rect(68px, 9999px, 42px, 0);
            }
            5% {
                clip: rect(34px, 9999px, 22px, 0);
            }
            10% {
                clip: rect(67px, 9999px, 78px, 0);
            }
            15% {
                clip: rect(95px, 9999px, 59px, 0);
            }
            20% {
                clip: rect(5px, 9999px, 59px, 0);
            }
            25% {
                clip: rect(15px, 9999px, 42px, 0);
            }
            30% {
                clip: rect(95px, 9999px, 76px, 0);
            }
            35% {
                clip: rect(45px, 9999px, 85px, 0);
            }
            40% {
                clip: rect(19px, 9999px, 24px, 0);
            }
            45% {
                clip: rect(78px, 9999px, 85px, 0);
            }
            50% {
                clip: rect(19px, 9999px, 88px, 0);
            }
            55% {
                clip: rect(45px, 9999px, 76px, 0);
            }
            60% {
                clip: rect(56px, 9999px, 83px, 0);
            }
            65% {
                clip: rect(12px, 9999px, 59px, 0);
            }
            70% {
                clip: rect(31px, 9999px, 44px, 0);
            }
            75% {
                clip: rect(96px, 9999px, 81px, 0);
            }
            80% {
                clip: rect(1px, 9999px, 98px, 0);
            }
            85% {
                clip: rect(89px, 9999px, 34px, 0);
            }
            90% {
                clip: rect(15px, 9999px, 12px, 0);
            }
            95% {
                clip: rect(37px, 9999px, 38px, 0);
            }
            100% {
                clip: rect(45px, 9999px, 88px, 0);
            }
        }

        @keyframes glitch-anim-2 {
            0% {
                clip: rect(28px, 9999px, 69px, 0);
            }
            5% {
                clip: rect(89px, 9999px, 68px, 0);
            }
            10% {
                clip: rect(3px, 9999px, 55px, 0);
            }
            15% {
                clip: rect(12px, 9999px, 38px, 0);
            }
            20% {
                clip: rect(55px, 9999px, 45px, 0);
            }
            25% {
                clip: rect(89px, 9999px, 34px, 0);
            }
            30% {
                clip: rect(25px, 9999px, 99px, 0);
            }
            35% {
                clip: rect(45px, 9999px, 9px, 0);
            }
            40% {
                clip: rect(72px, 9999px, 63px, 0);
            }
            45% {
                clip: rect(12px, 9999px, 14px, 0);
            }
            50% {
                clip: rect(34px, 9999px, 71px, 0);
            }
            55% {
                clip: rect(58px, 9999px, 46px, 0);
            }
            60% {
                clip: rect(49px, 9999px, 43px, 0);
            }
            65% {
                clip: rect(99px, 9999px, 45px, 0);
            }
            70% {
                clip: rect(78px, 9999px, 10px, 0);
            }
            75% {
                clip: rect(67px, 9999px, 51px, 0);
            }
            80% {
                clip: rect(5px, 9999px, 88px, 0);
            }
            85% {
                clip: rect(99px, 9999px, 89px, 0);
            }
            90% {
                clip: rect(65px, 9999px, 4px, 0);
            }
            95% {
                clip: rect(55px, 9999px, 21px, 0);
            }
            100% {
                clip: rect(5px, 9999px, 67px, 0);
            }
        }

        /* Resources Compendium styling */
        .cyberpunk-article .resources-section {
            margin: 20px 0;
        }

        .cyberpunk-article .resources-category {
            margin-bottom: 30px;
            animation: fadeInUp 0.8s ease-out forwards;
            opacity: 0;
        }

        .cyberpunk-article .resources-category:nth-child(1) { animation-delay: 0.1s; }
        .cyberpunk-article .resources-category:nth-child(2) { animation-delay: 0.2s; }
        .cyberpunk-article .resources-category:nth-child(3) { animation-delay: 0.3s; }
        .cyberpunk-article .resources-category:nth-child(4) { animation-delay: 0.4s; }
        .cyberpunk-article .resources-category:nth-child(5) { animation-delay: 0.5s; }

        .cyberpunk-article .resources-category h3 {
            color: var(--neon-pink);
            margin-bottom: 20px;
            text-shadow: 0 0 5px rgba(255, 42, 109, 0.5);
        }

        .cyberpunk-article .resources-grid {
            display: grid;
            grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
            gap: 15px;
        }

        .cyberpunk-article .resource-item {
            background: var(--dark-accent);
            padding: 20px;
            border-left: 3px solid var(--neon-blue);
            position: relative;
            transition: all 0.3s ease;
            backdrop-filter: blur(5px);
        }

        .cyberpunk-article .resource-item:hover {
            transform: translateX(10px);
            background: rgba(26, 19, 48, 0.9);
            border-left-color: var(--neon-pink);
            box-shadow: 
                0 0 10px rgba(5, 217, 232, 0.2),
                0 0 20px rgba(5, 217, 232, 0.1);
        }

        .cyberpunk-article .resource-item::before {
            content: '';
            position: absolute;
            top: 0;
            left: 0;
            right: 0;
            height: 1px;
            background: linear-gradient(90deg, var(--neon-blue), transparent);
            opacity: 0;
            transition: opacity 0.3s ease;
        }

        .cyberpunk-article .resource-item:hover::before {
            opacity: 1;
        }

        .cyberpunk-article .resource-item p {
            margin: 0;
        }

        .cyberpunk-article .resource-item a {
            color: var(--neon-blue);
            text-decoration: none;
            font-weight: 600;
            position: relative;
            transition: all 0.3s ease;
            padding: 0 2px;
        }

        .cyberpunk-article .resource-item a:hover {
            color: var(--neon-pink);
            text-shadow: 0 0 5px rgba(255, 42, 109, 0.7);
        }

        .cyberpunk-article .resource-item a::after {
            content: '';
            position: absolute;
            bottom: -2px;
            left: 0;
            width: 100%;
            height: 1px;
            background: var(--neon-blue);
            transition: all 0.3s ease;
        }

        .cyberpunk-article .resource-item a:hover::after {
            background: var(--neon-pink);
            height: 2px;
            box-shadow: 0 0 5px rgba(255, 42, 109, 0.7);
        }

        /* Action Items styling */
        .cyberpunk-article .action-items-container {
            margin: 20px 0;
        }

        .cyberpunk-article .action-items-group {
            margin-bottom: 30px;
            animation: fadeInUp 0.8s ease-out forwards;
            opacity: 0;
        }

        .cyberpunk-article .action-items-group:nth-child(1) { animation-delay: 0.1s; }
        .cyberpunk-article .action-items-group:nth-child(2) { animation-delay: 0.2s; }
        .cyberpunk-article .action-items-group:nth-child(3) { animation-delay: 0.3s; }
        .cyberpunk-article .action-items-group:nth-child(4) { animation-delay: 0.4s; }
        .cyberpunk-article .action-items-group:nth-child(5) { animation-delay: 0.5s; }
        .cyberpunk-article .action-items-group:nth-child(6) { animation-delay: 0.6s; }
        .cyberpunk-article .action-items-group:nth-child(7) { animation-delay: 0.7s; }

        @keyframes fadeInUp {
            from {
                opacity: 0;
                transform: translateY(20px);
            }
            to {
                opacity: 1;
                transform: translateY(0);
            }
        }

        .cyberpunk-article .action-items-role {
            font-family: 'Share Tech Mono', monospace;
            color: var(--neon-pink);
            font-size: 20px;
            margin-bottom: 15px;
            padding-left: 15px;
            position: relative;
            text-shadow: 0 0 5px rgba(255, 42, 109, 0.5);
        }

        .cyberpunk-article .action-items-role::before {
            content: '';
            position: absolute;
            left: 0;
            top: 50%;
            width: 5px;
            height: 0;
            background: var(--neon-blue);
            transition: height 0.3s ease;
            transform: translateY(-50%);
        }

        .cyberpunk-article .action-items-group:hover .action-items-role::before {
            height: 100%;
        }

        .cyberpunk-article .action-items-list {
            list-style-type: none;
            margin: 0;
            padding: 0;
        }

        .cyberpunk-article .action-item {
            background: rgba(26, 19, 48, 0.7);
            margin-bottom: 15px;
            padding: 15px 20px;
            border-left: 3px solid var(--neon-blue);
            position: relative;
            transition: all 0.3s ease;
            backdrop-filter: blur(5px);
        }

        .cyberpunk-article .action-item:hover {
            transform: translateX(10px);
            background: rgba(26, 19, 48, 0.9);
            border-left-color: var(--neon-pink);
            box-shadow: 
                0 0 10px rgba(5, 217, 232, 0.2),
                0 0 20px rgba(5, 217, 232, 0.1);
        }

        .cyberpunk-article .action-item::before {
            content: '';
            position: absolute;
            top: 0;
            left: 0;
            right: 0;
            height: 1px;
            background: linear-gradient(90deg, var(--neon-blue), transparent);
            opacity: 0;
            transition: opacity 0.3s ease;
        }

        .cyberpunk-article .action-item:hover::before {
            opacity: 1;
        }

        /* Industry Direction styling */
        .cyberpunk-article .industry-direction-list {
            list-style-type: none;
            margin: 20px 0;
            padding: 0;
        }

        .cyberpunk-article .industry-item {
            background: var(--dark-accent);
            margin-bottom: 15px;
            padding: 15px 20px;
            border-left: 3px solid var(--neon-blue);
            position: relative;
            transition: transform 0.3s ease;
        }

        .cyberpunk-article .industry-item:hover {
            transform: translateX(5px);
            background: rgba(26, 19, 48, 0.9);
        }

        .cyberpunk-article .subtitle.glitch-effect::before {
            left: 1px;
            text-shadow: -1px 0 var(--neon-pink);
            clip: rect(24px, 450px, 36px, 0);
            animation: glitch-anim-1 4s linear infinite alternate-reverse;
        }

        .cyberpunk-article .subtitle.glitch-effect::after {
            left: -1px;
            text-shadow: -1px 0 var(--neon-purple);
            clip: rect(24px, 450px, 36px, 0);
            animation: glitch-anim-2 4s linear infinite alternate-reverse;
        }

        /* Responsive Media Queries */
        @media screen and (max-width: 1024px) {
            .cyberpunk-article .container {
                padding: 15px;
            }

            .cyberpunk-article .header {
                padding: 30px 20px;
                margin-bottom: 0;
            }

            .cyberpunk-article .header h1 {
                font-size: 36px;
            }

            .cyberpunk-article .subtitle {
                font-size: 28px;
            }

            .cyberpunk-article h2 {
                font-size: 24px;
            }

            .cyberpunk-article h3 {
                font-size: 20px;
            }
        }

        @media screen and (max-width: 768px) {
            .cyberpunk-article .container {
                padding: 12px;
            }

            .cyberpunk-article .header {
                padding: 25px 15px;
                margin-bottom: 0;
            }

            .cyberpunk-article .header h1 {
                font-size: 32px;
            }

            .cyberpunk-article .subtitle {
                font-size: 24px;
            }

            .cyberpunk-article .header-info {
                font-size: 16px;
            }

            .cyberpunk-article .quotes-grid {
                grid-template-columns: 1fr;
                gap: 20px;
            }

            .cyberpunk-article .takeaways-grid {
                grid-template-columns: 1fr;
                gap: 15px;
            }

            .cyberpunk-article .resources-grid {
                grid-template-columns: 1fr;
            }

            .cyberpunk-article .footer-content {
                flex-direction: column;
                gap: 20px;
            }

            .cyberpunk-article .footer-left, .cyberpunk-article .footer-right {
                min-width: 100%;
            }

            .cyberpunk-article .newsletter-content {
                padding: 20px 15px;
            }

            .cyberpunk-article h2 {
                font-size: 22px;
            }

            .cyberpunk-article h3 {
                font-size: 18px;
            }

            .cyberpunk-article p, .cyberpunk-article li {
                font-size: 15px;
            }

            /* Fix for glitch effect on mobile */
            .cyberpunk-article .glitch-effect::before,
            .cyberpunk-article .glitch-effect::after {
                display: none;
            }

            /* Improve mobile touch targets */
            .cyberpunk-article .footer-contact-item {
                padding: 5px 0;
            }

            .cyberpunk-article .question-item, .cyberpunk-article .action-item, .cyberpunk-article .industry-item {
                margin-bottom: 10px;
            }
        }

        @media screen and (max-width: 480px) {
            .cyberpunk-article .container {
                padding: 10px;
            }

            .cyberpunk-article .header {
                padding: 20px 12px;
                margin-bottom: 0;
            }

            .cyberpunk-article .header h1 {
                font-size: 28px;
                text-shadow: 
                    0 0 3px var(--neon-blue),
                    0 0 7px var(--neon-blue),
                    0 0 14px var(--neon-purple);
            }

            .cyberpunk-article .subtitle {
                font-size: 20px;
            }

            .cyberpunk-article .header-info {
                font-size: 14px;
            }

            .cyberpunk-article .newsletter-content {
                padding: 15px 12px;
            }

            .cyberpunk-article .quote-card {
                padding: 15px;
            }

            .cyberpunk-article .quote-text {
                font-size: 14px;
            }

            .cyberpunk-article .quote-attribution {
                font-size: 13px;
            }

            .cyberpunk-article .takeaway-card {
                padding: 15px;
            }

            .cyberpunk-article .resource-item {
                padding: 12px;
            }

            .cyberpunk-article .question-item, .cyberpunk-article .action-item, .cyberpunk-article .industry-item {
                padding: 12px 15px;
            }

            .cyberpunk-article .footer {
                padding: 25px 15px;
            }

            .cyberpunk-article .footer-name {
                font-size: 18px;
            }

            .cyberpunk-article .footer-role, .cyberpunk-article .footer-company {
                font-size: 14px;
            }

            .cyberpunk-article .footer-contact-item {
                font-size: 13px;
            }

            .cyberpunk-article .footer-cta {
                font-size: 16px;
            }

            .cyberpunk-article h2 {
                font-size: 20px;
            }

            .cyberpunk-article h3 {
                font-size: 17px;
            }

            .cyberpunk-article p, .cyberpunk-article li {
                font-size: 14px;
                line-height: 1.5;
            }

            .cyberpunk-article .neon-divider {
                margin: 20px 0;
            }

            /* Improve mobile animations */
            @keyframes neonGlow {
                0% { opacity: 0.8; }
                50% { opacity: 0.6; }
                100% { opacity: 0.8; }
            }

            /* Reduce animation complexity for better performance */
            .cyberpunk-article .footer-left, .cyberpunk-article .footer-right {
                animation: none;
                opacity: 1;
            }

            .cyberpunk-article .footer {
                animation: none;
                opacity: 1;
            }

            /* Improve touch targets for mobile */
            .cyberpunk-article .footer-contact-item {
                padding: 8px 0;
            }

            .cyberpunk-article a {
                padding: 2px 0;
            }
        }

        /* Fix for very small screens */
        @media screen and (max-width: 320px) {
            .cyberpunk-article .header h1 {
                font-size: 24px;
            }

            .cyberpunk-article .subtitle {
                font-size: 18px;
            }

            .cyberpunk-article .header-info {
                font-size: 13px;
            }

            .cyberpunk-article h2 {
                font-size: 18px;
            }

            .cyberpunk-article h3 {
                font-size: 16px;
            }

            .cyberpunk-article p, .cyberpunk-article li {
                font-size: 13px;
            }

            .cyberpunk-article .quote-text {
                padding-left: 15px;
            }

            .cyberpunk-article .quote-text::before {
                font-size: 24px;
            }
        }

        /* Resources */
        .cyberpunk-article .resources-list {
            list-style: none;
            margin: 15px 0;
        }

        .cyberpunk-article .resource-item a {
            color: var(--neon-blue);
            text-decoration: none;
            transition: all 0.3s ease;
        }

        .cyberpunk-article .resource-item a:hover {
            color: var(--neon-pink);
            text-decoration: underline;
        }

        /* Industry Direction */
        .cyberpunk-article .industry-direction-list {
            list-style: none;
            margin: 15px 0;
        }

        .cyberpunk-article .direction-item {
            margin: 10px 0;
            padding: 10px;
            background: rgba(0, 0, 0, 0.3);
            border-left: 2px solid var(--neon-blue);
            transition: all 0.3s ease;
        }

        .cyberpunk-article .direction-item:hover {
            transform: translateX(5px);
            background: rgba(0, 0, 0, 0.5);
        }

        /* Feature Image Section */
        .cyberpunk-article .feature-image-container {
            width: 100%;
            margin: 5px 0 5px 0;
            position: relative;
            overflow: hidden;
            border: 1px solid var(--neon-blue);
            box-shadow: 
                0 0 15px rgba(5, 217, 232, 0.3),
                0 0 30px rgba(5, 217, 232, 0.2);
            animation: imagePulse 3s ease-in-out infinite;
        }

        .cyberpunk-article .feature-image-container::before {
            content: '';
            position: absolute;
            top: 0;
            left: 0;
            right: 0;
            height: 2px;
            background: linear-gradient(90deg, var(--neon-pink), var(--neon-purple), var(--neon-blue));
            z-index: 2;
            animation: scanline 3s linear infinite;
        }

        .cyberpunk-article .feature-image-container::after {
            content: '';
            position: absolute;
            bottom: 0;
            left: 0;
            right: 0;
            height: 2px;
            background: linear-gradient(90deg, var(--neon-blue), var(--neon-purple), var(--neon-pink));
            z-index: 2;
        }

        .cyberpunk-article .feature-image-wrapper {
            position: relative;
            width: 100%;
            padding-top: 56.25%; /* 16:9 Aspect Ratio */
            overflow: hidden;
        }

        .cyberpunk-article .feature-image {
            position: absolute;
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
            object-fit: cover;
            transition: transform 0.3s ease;
            filter: brightness(0.9) contrast(1.1);
        }

        .cyberpunk-article .feature-image:hover {
            transform: scale(1.02);
        }

        .cyberpunk-article .feature-image-overlay {
            position: absolute;
            top: 0;
            left: 0;
            right: 0;
            bottom: 0;
            background: linear-gradient(45deg, rgba(255, 42, 109, 0.1), rgba(190, 0, 254, 0.1));
            pointer-events: none;
        }

        .cyberpunk-article .feature-image-glitch {
            position: absolute;
            top: 0;
            left: 0;
            right: 0;
            bottom: 0;
            background: url('/images/feature_images/aie-summit-2025-feature-image.jpg');
            background-size: cover;
            background-position: center;
            opacity: 0;
            mix-blend-mode: screen;
            animation: glitchEffect 4s infinite;
        }

        @keyframes scanline {
            0% {
                transform: translateX(-100%);
            }
            100% {
                transform: translateX(100%);
            }
        }

        @keyframes imagePulse {
            0% {
                box-shadow: 
                    0 0 15px rgba(5, 217, 232, 0.3),
                    0 0 30px rgba(5, 217, 232, 0.2);
            }
            50% {
                box-shadow: 
                    0 0 20px rgba(5, 217, 232, 0.4),
                    0 0 40px rgba(5, 217, 232, 0.3);
            }
            100% {
                box-shadow: 
                    0 0 15px rgba(5, 217, 232, 0.3),
                    0 0 30px rgba(5, 217, 232, 0.2);
            }
        }

        @keyframes glitchEffect {
            0% {
                opacity: 0;
                transform: translate(0);
            }
            2% {
                opacity: 0.1;
                transform: translate(-3px, 2px);
            }
            4% {
                opacity: 0;
                transform: translate(0);
            }
            25% {
                opacity: 0;
                transform: translate(0);
            }
            27% {
                opacity: 0.1;
                transform: translate(3px, -2px);
            }
            29% {
                opacity: 0;
                transform: translate(0);
            }
            100% {
                opacity: 0;
                transform: translate(0);
            }
        }

        /* Add mobile responsiveness for feature image */
        @media screen and (max-width: 768px) {
            .cyberpunk-article .feature-image-container {
                margin: 15px 0 30px 0;
            }

            .cyberpunk-article .feature-image-wrapper {
                padding-top: 75%; /* 4:3 Aspect Ratio for mobile */
            }
        }
&lt;/style&gt;

&lt;div class="cyberpunk-article"&gt;
&lt;div class="container"&gt;
        &lt;header class="header"&gt;
            &lt;div class="cyber-circuit" style="top: -150px; right: -150px;"&gt;&lt;/div&gt;
            &lt;div class="cyber-circuit" style="bottom: -150px; left: -150px;"&gt;&lt;/div&gt;
            &lt;div class="glitch-wrapper"&gt;
                &lt;h1 class="glitch-effect" data-text="AI Engineer Summit 2025"&gt;AI Engineer Summit 2025&lt;/h1&gt;

                &lt;br&gt;
                &lt;div class="subtitle glitch-effect" data-text="Agents at Work"&gt;Agents at Work&lt;/div&gt;

            &lt;/div&gt;
            &lt;p class="header-info"&gt;&lt;strong&gt;Event:&lt;/strong&gt; AI Engineer Summit 2025: Agents at Work&lt;/p&gt;
            &lt;p class="header-info"&gt;&lt;strong&gt;When:&lt;/strong&gt; February 19 - 22, 2025&lt;/p&gt;
            &lt;p class="header-info"&gt;&lt;strong&gt;Where:&lt;/strong&gt; New York&lt;/p&gt;
            &lt;p class="header-info"&gt;&lt;strong&gt;Focus:&lt;/strong&gt; Practical implementation of AI agents in production environments&lt;/p&gt;
        &lt;/header&gt;

        &lt;div class="feature-image-container"&gt;
            &lt;div class="feature-image-wrapper"&gt;
                &lt;img src="https://gallon.me/images/feature_images/aie-summit-2025-feature-image.jpg" alt="Feature Image" class="feature-image"&gt;
                &lt;div class="feature-image-overlay"&gt;&lt;/div&gt;
                &lt;div class="feature-image-glitch"&gt;&lt;/div&gt;
            &lt;/div&gt;
        &lt;/div&gt;

        &lt;div class="newsletter-content"&gt;




                    &lt;h2 class="section-title"&gt;Executive Summary&lt;/h2&gt;


                        &lt;div class="summary section-animate"&gt;

                                &lt;p&gt;The 2025 AI Engineer Summit marked a pivotal transition from theoretical AI capabilities to practical implementation reality. We've had unprecedented advances in foundation models, reasoning capabilities, and infrastructure creating what some called "the perfect storm for AI agents," but let's be real – there's a massive gap between what these models can theoretically do and what they reliably deliver in production. The most successful implementations are focused on specific domains with clear business value rather than general-purpose solutions, with consistent reports that even advanced reasoning models achieve only 70-80% accuracy on complex real-world tasks. That sounds impressive until you realize it means every fifth request is completely wrong. The industry's entering a consolidation phase where implementation expertise, reliability engineering, and domain knowledge are becoming more valuable than raw model capabilities. We've got the rocket, and it (mostly) doesn't blow up, but to get to Mars, there are a hundred other things we need to figure out.&lt;/p&gt;

                        &lt;/div&gt;



















                    &lt;div class="neon-divider"&gt;&lt;/div&gt;


                    &lt;h2 class="section-title"&gt;Key Takeaways&lt;/h2&gt;




                        &lt;div class="takeaways-grid section-animate"&gt;

                                &lt;div class="takeaway-card"&gt;
                                    &lt;p&gt; This is supposedly "the year of agents" (yeah, I know, we're all tired of hearing it already). 
Things still don't actually work reliably yet. That said, it's only February and we have 10 fast market months of AI development ahead of us!&lt;/p&gt;
                                &lt;/div&gt;

                                &lt;div class="takeaway-card"&gt;
                                    &lt;p&gt;There's a serious reality gap where models performing well in controlled settings regularly fail in complex real-world environments, with error rates of 15-20% being common – when did we abandon basic machine learning principles?&lt;/p&gt;
                                &lt;/div&gt;

                                &lt;div class="takeaway-card"&gt;
                                    &lt;p&gt;Evals matter more than ever – you need multi-dimensional evaluation frameworks that consider accuracy, cost, latency, and reliability rather than relying on benchmark performance that rarely translates to the real world.&lt;/p&gt;
                                &lt;/div&gt;

                                &lt;div class="takeaway-card"&gt;
                                    &lt;p&gt;Domain-specific models consistently outperform general-purpose solutions for enterprise use cases, crossing the threshold of sufficient reliability where general models still fall short.&lt;/p&gt;
                                &lt;/div&gt;

                                &lt;div class="takeaway-card"&gt;
                                    &lt;p&gt;Enterprise adoption requires addressing security, governance, and compliance concerns through multi-layered frameworks that most AI demos conveniently ignore.&lt;/p&gt;
                                &lt;/div&gt;

                                &lt;div class="takeaway-card"&gt;
                                    &lt;p&gt;The most future-proof systems are designed to scale with intelligence, improving automatically as underlying models get better rather than requiring constant reengineering.&lt;/p&gt;
                                &lt;/div&gt;

                                &lt;div class="takeaway-card"&gt;
                                    &lt;p&gt;Voice and multimodal agents represent the next frontier but come with massive hurdles for latency, reliability, and user experience that aren't worth chasing until the tech catches up.&lt;/p&gt;
                                &lt;/div&gt;

                                &lt;div class="takeaway-card"&gt;
                                    &lt;p&gt;Effective human-AI collaboration remains central to successful implementations, with the best systems amplifying human expertise rather than replacing it.&lt;/p&gt;
                                &lt;/div&gt;

                        &lt;/div&gt;

















                    &lt;div class="neon-divider"&gt;&lt;/div&gt;


                    &lt;h2 class="section-title"&gt;Speaker Landscape&lt;/h2&gt;






                        &lt;div class="section-animate"&gt;
                            &lt;p&gt;The conference featured an exceptionally diverse and high-quality set of perspectives spanning the complete AI implementation ecosystem – all the real ones were there. Enterprise practitioners included leaders from financial services (Bloomberg, Jane Street, Ramp, Method Financial), pharmaceuticals (Pfizer), media (Thomson Reuters), technology (LinkedIn, Datadog), and travel (Booking.com). Major AI labs were represented by speakers from OpenAI, Anthropic and Google Gemini, providing insights into frontier model capabilities. The venture capital perspective came from Grace Isford (Lux Capital) and Heath Black (SignalFire), while infrastructure specialists included Paul Gilbert (Arista Networks) and Don Bosco Durai (Privacera). Emerging startups showcasing specialized tools included Windsurf, BrightWave, Sierra, OpenPipe, Writer and Contextual AI. Academic and research perspectives came from Will Brown (Morgan Stanley), Sayash Kapoor (AI Snake Oil), and Stefania Druga (Google). This blend created a comprehensive view of the state of AI implementation across different domains and organizational contexts.&lt;/p&gt;
                        &lt;/div&gt;















                    &lt;div class="neon-divider"&gt;&lt;/div&gt;


                    &lt;h2 class="section-title"&gt;Thematic Analysis&lt;/h2&gt;








                        &lt;div class="section-animate"&gt;

                                &lt;h3&gt;From Perfect Storm to Practical Implementation: The Reality Gap&lt;/h3&gt;

                                    &lt;p&gt;Everyone's talking about 2025 as "the perfect storm for AI agents" with converging advancements in reasoning models, test-time compute, engineering optimizations, hardware costs, and infrastructure investments. Yet as Grace Isford from Lux Capital put it, "we're seeing a lot of thunder, a lot of great momentum, but we haven't seen that lightning strike." This tension between theoretical capabilities and practical implementation challenges dominated every track.&lt;/p&gt;

                                    &lt;p&gt;The gap is real and it's massive. Sayash Kapoor (AI Snake Oil) highlighted how many headline-grabbing agent capabilities fail spectacularly in real-world scenarios. Waseem Alshikh (Writer) revealed that even leading reasoning models achieve only 81% combined accuracy on real-world financial scenarios, meaning "every hundred requests, 20 of them are just completely wrong." Anju Kambadur (Bloomberg) emphasized that in finance, "precision, comprehensiveness, speed, throughput, and availability" are non-negotiable requirements that current agent technologies don't consistently deliver. This pattern revealed a consistent truth across presentations: the gap between AI's capabilities in controlled settings and its performance in complex real-world environments remains substantial.&lt;/p&gt;

                                    &lt;p&gt;Think of it in an analogy of getting to Mars – the LLM is the rocket, but to get to the Mars, there are a hundred other things we need to figure out. We've got a rocket that (mostly) doesn't blow up, but that's it. Space is unforgiving, and so (to a lesser extent) are production environments.&lt;/p&gt;

                                    &lt;p&gt;The path forward emphasized systematic evaluation and incremental improvement. Kyle Corbitt (OpenPipe) demonstrated how Method Financial scaled to 500 million agent deployments by carefully measuring error rates, latency, and cost across different models before fine-tuning a small, reliable model optimized for their specific use case. Barry Zhang (Anthropic) advised a simple architecture focused on three components—environment, tools, and system prompt—that could be iteratively improved rather than trying to build complex systems from the start. Multiple speakers recommended starting with single-purpose agents before attempting multi-agent systems, focusing on specific high-value domains rather than attempting to build general-purpose solutions.&lt;/p&gt;


                                &lt;h3&gt;Domain Specialization and Data as the Competitive Moat&lt;/h3&gt;

                                    &lt;p&gt;A clear consensus emerged that domain-specific approaches consistently outperform general-purpose solutions in enterprise settings. Douwe Kiela (Contextual AI) advocated explicitly for "specialization over AGI," arguing that commercial applications require focus on specific domains where AI can deliver measurable value. Jonathan Lowe demonstrated how Pfizer's GraphRAG approach incorporates domain-specific relationships in pharmaceutical data, outperforming standard approaches by understanding connections between entities like compounds, proteins, and diseases.&lt;/p&gt;

                                    &lt;p&gt;Multiple speakers emphasized that proprietary data and domain knowledge provide the true competitive advantage in an era of rapidly commoditizing foundation models. Grace Isford dropped one of the conference bangers when she noted that "foundation models are the fastest depreciating asset class on the market right now," suggesting that organizations should focus less on model selection and more on developing proprietary data advantages. John Crepezzi described how Jane Street built custom models for working with OCaml code because off-the-shelf models were insufficient for their specialized domain.&lt;/p&gt;

                                    &lt;p&gt;I don't fully buy the "data is your moat" claim, but there's something to be said for domain-specific applications crossing the threshold of sufficiency where general models still fall short. Kyle Corbitt shared how Method Financial discovered that fine-tuned 8B parameter models outperformed much larger models for their specific use case while dramatically reducing costs and latency. Bruno Passos explained how Booking.com's AI coding tools leverage the context of their codebase to generate more relevant solutions than general-purpose tools. The bitter lesson still applies – over time, bigger general models with more compute will likely win, but in the near term, specialized models deliver the reliability enterprises need.&lt;/p&gt;


                                &lt;h3&gt;The Trust Framework: Evaluation, Security, and Reliability&lt;/h3&gt;

                                    &lt;p&gt;When did we all abandon basic machine learning principles? You can't even train a model without a test set, but somehow we collectively forgot that evals matter when it comes to generative AI, let a lone AI agents! A consistent theme throughout both conference tracks was the critical importance of robust evaluation frameworks, security measures, and reliability engineering for production AI systems.&lt;/p&gt;

                                    &lt;p&gt;Aparna Dhinkaran (Arize) presented a comprehensive framework for evaluating AI agents at multiple levels, emphasizing that "evals aren't just at one layer of your trace" and that rigorous testing across all components is essential for production-ready systems. Sayash Kapoor criticized over-reliance on static benchmarks, noting that impressive benchmark performance "very rarely translates into the real world." He advocated for multi-dimensional metrics that consider not just accuracy but also cost and reliability, showing how these metrics revealed that Claude 3.5 performed as well as GPT-4o on some tasks at 1/10th the cost.&lt;/p&gt;

                                    &lt;p&gt;Security concerns received significant attention, with Don Bosco Durai outlining a multi-layered approach to AI agent security addressing the unique vulnerabilities created by agents running in a single process. His framework consisted of pre-deployment evaluation, runtime enforcement, and continuous monitoring, addressing different aspects of security, safety, and compliance at each layer. Anju Kambadur explained that Bloomberg's agents undergo rigorous testing with "remediation workflows and circuit breakers" to catch errors before they impact financial data.&lt;/p&gt;

                                    &lt;p&gt;Most speakers agreed that while 100% accuracy is unattainable, organizations need robust observability, attribution, and audit trails to handle cases where things go wrong. Mike Conover described how BrightWave's research agent includes detailed citation tracking and "receipts" so users can validate information sources. Diamond Bishop explained how Datadog's AI agents generate postmortems and maintain full visibility into their decision-making processes, making them accountable even when operating autonomously.&lt;/p&gt;


                                &lt;h3&gt;Engineering for Intelligence: Building Systems That Scale with Smarter Models&lt;/h3&gt;

                                    &lt;p&gt;A forward-looking theme emerged around designing systems that automatically improve as underlying models get better. Rahul Sengottuvelu (Ramp) presented a framework distinguishing "classical compute" from "fuzzy compute" (neural networks), arguing that systems should maximize the latter since "if you did nothing, absolutely nothing... the big labs are still working, spending billions of dollars making those models better." He demonstrated an experimental email client where the LLM itself acted as the backend, rendering UI and handling user interactions without traditional software engineering.&lt;/p&gt;

                                    &lt;p&gt;This is where you need to catch the wave. That wave is barely a swell right now – it's way out there. It's not worth chasing yet. You've got to wait till that wave starts to crest, and then you catch it. But when you do, you're riding the momentum of exponential improvement in foundation models.&lt;/p&gt;

                                    &lt;p&gt;Will Brown (Morgan Stanley) discussed how reinforcement learning is becoming crucial for agent development, describing how models like DeepSeek's R1 demonstrate that "the long chain of thought... actually emerges as a byproduct" of training with appropriate reinforcement signals. The results of reinforcement learning are astounding – it's a completely intelligible methodology, you can totally get it, and that it works is borderline miraculous!&lt;/p&gt;

                                    &lt;p&gt;John Crepezzi described Jane Street's "AID" (AI Developer) framework which provides a unified backend for multiple editor integrations, allowing them to swap in new models or context-building strategies without changing frontends. Similarly, Kevin Hou (Windsurf) explained how their agent architecture was designed to "scale with intelligence" so that "if the models get better, our product gets better," including removing chat interfaces in favor of pure agent interactions. These approaches collectively point toward a future where AI systems continuously improve through reinforcement learning rather than requiring constant human reengineering.&lt;/p&gt;


                                &lt;h3&gt;Voice and Multimodal Agents: The Next Frontier&lt;/h3&gt;

                                    &lt;p&gt;Voice AI emerged as an important frontier for agent development across multiple presentations, but let's be real – the latency issues with frontier models totally kill the voice agent experience. They're going to be great when run at full power, full bandwidth and locally, but the practical applications of remotely deployed voice models are still struggling.&lt;/p&gt;

                                    &lt;p&gt;Nick Karyotakis (SuperDial) outlined the challenges of building reliable voice agents, noting that modern approaches have shifted from "prescriptive to descriptive development." He described how SuperDial builds phone agents that handle insurance verification calls by traversing phone trees, extracting information, and escalating to humans when needed.&lt;/p&gt;

                                    &lt;p&gt;Zack Reneau-Wedeen (Sierra) shared how Sierra's voice assistants handle customer service for brands by creating a "responsive design" approach where "it's the same agent code" operating across different channels and modalities. He emphasized that for voice agents, "latency all of a sudden matters so much more" than with text interfaces, creating new engineering challenges.&lt;/p&gt;

                                    &lt;p&gt;Multimodal capabilities also received significant attention. Mukund Sridhar and Aarush Selvan (Google) presented Gemini Deep Research as a multimodal research agent that "can browse the web as much as it needs" to produce comprehensive answers. Karina Nguyen (OpenAI) discussed how multimodal capabilities in Canvas enable more natural collaboration between humans and AI on creative tasks, suggesting that "the kind of interface to AGI is blank canvas that kind of self-morphs into your intent."&lt;/p&gt;

                                    &lt;p&gt;This is a great example of the wrong end of the spectrum to chase right now. Yes, it's the next frontier, but there are big hurdles that make it impractical. Don't waste time and effort here – the tech has to bridge the gap further before we get there. Focus where the gap is narrow and closest to sufficiency – that's where you should experiment.&lt;/p&gt;


                                &lt;h3&gt;Human-AI Partnership and Education&lt;/h3&gt;

                                    &lt;p&gt;Despite the focus on automation, speakers consistently emphasized the importance of thoughtful human-AI collaboration. Colin Flaherty described how their AI coding agent has written over 90% of its own codebase, but with human supervision at critical junctures. Diamond Bishop explained that Datadog's AI agents help human engineers by running investigations automatically but present results in ways that build trust and facilitate learning.&lt;/p&gt;

                                    &lt;p&gt;Karina Nguyen outlined an evolution from "models trained on RL and chain of thought using real-world tools" toward "co-innovators" that collaborate with humans on creative tasks. Kevin Hou demonstrated how Windsurf tracks a "unified timeline" of both human and AI actions, allowing their agent to understand what developers are doing and continue work seamlessly.&lt;/p&gt;

                                    &lt;p&gt;The conference closed with perspectives on democratizing AI engineering. Stefania Druga presented Cognimates, a platform that teaches children about AI by allowing them to train their own models and build applications. She emphasized that "kids are actually like little scientists" who can formulate and test hypotheses about how AI works, and that early exposure helps "demystify the intelligence" of AI systems. Multiple speakers touched on making agent development more accessible, with Will Brown sharing a simple reinforcement learning implementation that went viral because "it was one file of code... really simple... and it invited modification."&lt;/p&gt;


                        &lt;/div&gt;













                    &lt;div class="neon-divider"&gt;&lt;/div&gt;


                    &lt;h2 class="section-title"&gt;Notable Quotables&lt;/h2&gt;










                        &lt;div class="quotes-grid section-animate"&gt;

                                &lt;div class="quote-card"&gt;
                                    &lt;div class="quote-text"&gt;Foundation models are the fastest depreciating asset class on the market right now.&lt;/div&gt;
                                    &lt;div class="quote-attribution"&gt;— Grace Isford, Lux Capital&lt;/div&gt;
                                &lt;/div&gt;

                                &lt;div class="quote-card"&gt;
                                    &lt;div class="quote-text"&gt;In reality, you're saying every hundred requests, 20 of them are just completely wrong.&lt;/div&gt;
                                    &lt;div class="quote-attribution"&gt;— Waseem Alshikh, Writer&lt;/div&gt;
                                &lt;/div&gt;

                                &lt;div class="quote-card"&gt;
                                    &lt;div class="quote-text"&gt;Don't build agents for everything. Keep it as simple for as long as possible. And finally, as you iterate, try to think like your agent, gain their perspective, and help them do their job.&lt;/div&gt;
                                    &lt;div class="quote-attribution"&gt;— Barry Zhang, Anthropic&lt;/div&gt;
                                &lt;/div&gt;

                                &lt;div class="quote-card"&gt;
                                    &lt;div class="quote-text"&gt;Pilots are very easy. Production is incredibly hard.&lt;/div&gt;
                                    &lt;div class="quote-attribution"&gt;— Douwe Kiela, Contextual AI&lt;/div&gt;
                                &lt;/div&gt;

                                &lt;div class="quote-card"&gt;
                                    &lt;div class="quote-text"&gt;We're seeing a lot of thunder, a lot of great momentum, but we haven't seen that lightning strike.&lt;/div&gt;
                                    &lt;div class="quote-attribution"&gt;— Grace Isford, Lux Capital&lt;/div&gt;
                                &lt;/div&gt;

                                &lt;div class="quote-card"&gt;
                                    &lt;div class="quote-text"&gt;Language models are already capable of very many things. But if you trick yourself into believing this means a reliable experience for the end user, that's when products in the real world go wrong.&lt;/div&gt;
                                    &lt;div class="quote-attribution"&gt;— Sayash Kapoor, AI Snake Oil&lt;/div&gt;
                                &lt;/div&gt;

                                &lt;div class="quote-card"&gt;
                                    &lt;div class="quote-text"&gt;Benchmark performance very rarely translates into the real world.&lt;/div&gt;
                                    &lt;div class="quote-attribution"&gt;— Sayash Kapoor, AI Snake Oil&lt;/div&gt;
                                &lt;/div&gt;

                                &lt;div class="quote-card"&gt;
                                    &lt;div class="quote-text"&gt;We want you to spend time on things that you are good at, right? The things that make us all excited, which is shipping products, building great features, and generally just shipping code.&lt;/div&gt;
                                    &lt;div class="quote-attribution"&gt;— Kevin Hou, Windsurf (on delegating tedious engineering tasks to AI agents)&lt;/div&gt;
                                &lt;/div&gt;

                                &lt;div class="quote-card"&gt;
                                    &lt;div class="quote-text"&gt;The bitter lesson is just so powerful, and exponential trends are so powerful that you can just hitch the ride.&lt;/div&gt;
                                    &lt;div class="quote-attribution"&gt;— Rahul Sengottuvelu, Ramp&lt;/div&gt;
                                &lt;/div&gt;

                                &lt;div class="quote-card"&gt;
                                    &lt;div class="quote-text"&gt;If the models get better, our product gets better.&lt;/div&gt;
                                    &lt;div class="quote-attribution"&gt;— Kevin Hou, Windsurf&lt;/div&gt;
                                &lt;/div&gt;

                                &lt;div class="quote-card"&gt;
                                    &lt;div class="quote-text"&gt;Winning systems will perform end-to-end RL over tool use calls.&lt;/div&gt;
                                    &lt;div class="quote-attribution"&gt;— Mike Conover, BrightWave&lt;/div&gt;
                                &lt;/div&gt;

                                &lt;div class="quote-card"&gt;
                                    &lt;div class="quote-text"&gt;Specialization over AGI.&lt;/div&gt;
                                    &lt;div class="quote-attribution"&gt;— Douwe Kiela, Contextual AI&lt;/div&gt;
                                &lt;/div&gt;

                                &lt;div class="quote-card"&gt;
                                    &lt;div class="quote-text"&gt;At enterprise scale, data is your moat.&lt;/div&gt;
                                    &lt;div class="quote-attribution"&gt;— Douwe Kiela, Contextual AI&lt;/div&gt;
                                &lt;/div&gt;

                                &lt;div class="quote-card"&gt;
                                    &lt;div class="quote-text"&gt;You need to get your human wetware chatbot speaking the right language at the right level.&lt;/div&gt;
                                    &lt;div class="quote-attribution"&gt;— Jonathan Lowe, Pfizer&lt;/div&gt;
                                &lt;/div&gt;

                                &lt;div class="quote-card"&gt;
                                    &lt;div class="quote-text"&gt;The way I'm thinking about it is the kind of interface to AGI is blank canvas that kind of self-morphs into your intent.&lt;/div&gt;
                                    &lt;div class="quote-attribution"&gt;— Karina Nguyen, OpenAI&lt;/div&gt;
                                &lt;/div&gt;

                                &lt;div class="quote-card"&gt;
                                    &lt;div class="quote-text"&gt;Gartner hates us. Gartner thinks we've hit the peak. So it's only downhill from here, guys. Sorry to inform you that AI engineering is over.&lt;/div&gt;
                                    &lt;div class="quote-attribution"&gt;— Swyx, AI Engineer Foundation (joking about the Gartner hype cycle)&lt;/div&gt;
                                &lt;/div&gt;

                        &lt;/div&gt;











                    &lt;div class="neon-divider"&gt;&lt;/div&gt;


                    &lt;h2 class="section-title"&gt;Emerging Questions&lt;/h2&gt;












                        &lt;ul class="questions-list section-animate"&gt;

                                &lt;li class="question-item"&gt; How do we design evaluation frameworks that measure agent performance in real-world conditions rather than controlled environments?&lt;/li&gt;

                                &lt;li class="question-item"&gt;What is the optimal balance between model capabilities and engineering scaffolding in production agent systems?&lt;/li&gt;

                                &lt;li class="question-item"&gt;How can organizations quantify the business value of AI initiatives to justify continued investment in implementation expertise?&lt;/li&gt;

                                &lt;li class="question-item"&gt;What new security threats emerge when autonomous agents are given access to sensitive systems and data?&lt;/li&gt;

                                &lt;li class="question-item"&gt;How will the economics of agent development evolve as compute costs decrease but expectations for reliability increase?&lt;/li&gt;

                                &lt;li class="question-item"&gt;Will domain-specific agents continue to dominate, or will general-purpose agents eventually become viable for enterprise use?&lt;/li&gt;

                                &lt;li class="question-item"&gt;What new user interfaces and interaction paradigms will emerge to support effective human-AI collaboration?&lt;/li&gt;

                                &lt;li class="question-item"&gt;How should education systems evolve to prepare both current professionals and future generations for an agent-driven world?&lt;/li&gt;

                                &lt;li class="question-item"&gt;What organizational structures best support AI implementation across enterprise silos?&lt;/li&gt;

                                &lt;li class="question-item"&gt;How can voice and multimodal agents overcome the increased latency and reliability challenges compared to text-only systems?&lt;/li&gt;

                        &lt;/ul&gt;









                    &lt;div class="neon-divider"&gt;&lt;/div&gt;


                    &lt;h2 class="section-title"&gt;Action Items&lt;/h2&gt;













                        &lt;div class="action-items-container"&gt;

                            &lt;div class="action-items-group"&gt;
                                &lt;div class="action-items-role"&gt;For Technical Leaders&lt;/div&gt;
                                &lt;ul class="action-items-list"&gt;

                                    &lt;li class="action-item"&gt; Implement multi-dimensional evaluation frameworks that consider accuracy, cost, latency, and reliability&lt;/li&gt;

                                    &lt;li class="action-item"&gt;Design systems that improve automatically as models get better&lt;/li&gt;

                                    &lt;li class="action-item"&gt;Start with single-purpose agents before attempting complex multi-agent systems&lt;/li&gt;

                                &lt;/ul&gt;
                            &lt;/div&gt;

                            &lt;div class="action-items-group"&gt;
                                &lt;div class="action-items-role"&gt;For Business Leaders&lt;/div&gt;
                                &lt;ul class="action-items-list"&gt;

                                    &lt;li class="action-item"&gt; Focus on domain-specific applications with clear ROI&lt;/li&gt;

                                    &lt;li class="action-item"&gt;Invest in proprietary data advantages&lt;/li&gt;

                                    &lt;li class="action-item"&gt;Align AI initiatives with core business objectives&lt;/li&gt;

                                &lt;/ul&gt;
                            &lt;/div&gt;

                            &lt;div class="action-items-group"&gt;
                                &lt;div class="action-items-role"&gt;For Security Teams&lt;/div&gt;
                                &lt;ul class="action-items-list"&gt;

                                    &lt;li class="action-item"&gt; Deploy multi-layered frameworks addressing pre-deployment evaluation, runtime enforcement, and continuous monitoring&lt;/li&gt;

                                    &lt;li class="action-item"&gt;Develop specialized approaches for agent-specific vulnerabilities&lt;/li&gt;

                                &lt;/ul&gt;
                            &lt;/div&gt;

                            &lt;div class="action-items-group"&gt;
                                &lt;div class="action-items-role"&gt;For Product Managers&lt;/div&gt;
                                &lt;ul class="action-items-list"&gt;

                                    &lt;li class="action-item"&gt; Design for human-AI collaboration rather than full autonomy&lt;/li&gt;

                                    &lt;li class="action-item"&gt;Prioritize UX that complements AI capabilities&lt;/li&gt;

                                    &lt;li class="action-item"&gt;Focus on specific high-value use cases rather than general-purpose solutions&lt;/li&gt;

                                &lt;/ul&gt;
                            &lt;/div&gt;

                            &lt;div class="action-items-group"&gt;
                                &lt;div class="action-items-role"&gt;For Organizations&lt;/div&gt;
                                &lt;ul class="action-items-list"&gt;

                                    &lt;li class="action-item"&gt; Invest in data infrastructure, cross-functional communication, and educational initiatives&lt;/li&gt;

                                    &lt;li class="action-item"&gt;Develop appropriate governance frameworks for agents with increasing autonomy&lt;/li&gt;

                                &lt;/ul&gt;
                            &lt;/div&gt;

                            &lt;div class="action-items-group"&gt;
                                &lt;div class="action-items-role"&gt;For AI Engineers&lt;/div&gt;
                                &lt;ul class="action-items-list"&gt;

                                    &lt;li class="action-item"&gt; Learn reinforcement learning techniques for agent development&lt;/li&gt;

                                    &lt;li class="action-item"&gt;Focus on building systems that scale with model improvements&lt;/li&gt;

                                    &lt;li class="action-item"&gt;Develop both technical and communication skills&lt;/li&gt;

                                &lt;/ul&gt;
                            &lt;/div&gt;

                            &lt;div class="action-items-group"&gt;
                                &lt;div class="action-items-role"&gt;For Educators&lt;/div&gt;
                                &lt;ul class="action-items-list"&gt;

                                    &lt;li class="action-item"&gt; Integrate AI literacy into curricula from childhood&lt;/li&gt;

                                    &lt;li class="action-item"&gt;Develop tools that help students understand AI concepts through hands-on experience&lt;/li&gt;

                                &lt;/ul&gt;
                            &lt;/div&gt;

                        &lt;/div&gt;







                    &lt;div class="neon-divider"&gt;&lt;/div&gt;


                    &lt;h2 class="section-title"&gt;Resources&lt;/h2&gt;
















                        &lt;div class="resources-section"&gt;

                                &lt;div class="resources-category"&gt;
                                    &lt;h3&gt;Development Platforms and Tools&lt;/h3&gt;
                                    &lt;div class="resources-grid"&gt;

                                            &lt;div class="resource-item"&gt;
                                                &lt;p&gt; &lt;a href="https://huggingface.co"&gt;Hugging Face&lt;/a&gt; - GitHub for machine learning (mentioned by Isford)&lt;/p&gt;
                                            &lt;/div&gt;

                                            &lt;div class="resource-item"&gt;
                                                &lt;p&gt;&lt;a href="https://www.together.ai/"&gt;Together AI&lt;/a&gt; - Open source AI cloud infrastructure (mentioned by Isford)&lt;/p&gt;
                                            &lt;/div&gt;

                                            &lt;div class="resource-item"&gt;
                                                &lt;p&gt;&lt;a href="https://openpipe.ai/"&gt;OpenPipe&lt;/a&gt; - Platform for building, training, and deploying fine-tuned open source models (presented by Corbitt)&lt;/p&gt;
                                            &lt;/div&gt;

                                            &lt;div class="resource-item"&gt;
                                                &lt;p&gt;&lt;a href="https://arize.com/"&gt;Arize&lt;/a&gt; - AI observability platform for agent evaluation (presented by Dhinkaran)&lt;/p&gt;
                                            &lt;/div&gt;

                                            &lt;div class="resource-item"&gt;
                                                &lt;p&gt;&lt;a href="https://windsurf.ai/"&gt;WindSurf&lt;/a&gt; - Agentic editor with background understanding of development context (presented by Hou)&lt;/p&gt;
                                            &lt;/div&gt;

                                            &lt;div class="resource-item"&gt;
                                                &lt;p&gt;&lt;a href="https://github.com/pipecat-ai/pipecat"&gt;PipeCat&lt;/a&gt; - Open source voice AI orchestration framework by Daily (mentioned by Karyotakis)&lt;/p&gt;
                                            &lt;/div&gt;

                                            &lt;div class="resource-item"&gt;
                                                &lt;p&gt;&lt;a href="https://langfuse.com/"&gt;LangFuse&lt;/a&gt; - Self-hostable observability platform for LLM applications (mentioned by Karyotakis)&lt;/p&gt;
                                            &lt;/div&gt;

                                            &lt;div class="resource-item"&gt;
                                                &lt;p&gt;&lt;a href="https://github.com/stanfordnlp/dspy"&gt;DSPy&lt;/a&gt; - Framework for optimizing prompts through automation (mentioned by Brown)&lt;/p&gt;
                                            &lt;/div&gt;

                                            &lt;div class="resource-item"&gt;
                                                &lt;p&gt;&lt;a href="https://tensorzero.ai/"&gt;Tensor Zero&lt;/a&gt; - Structured and typed LLM endpoints for production (mentioned by Karyotakis)&lt;/p&gt;
                                            &lt;/div&gt;

                                            &lt;div class="resource-item"&gt;
                                                &lt;p&gt;&lt;a href="https://cursor.com/"&gt;Cursor&lt;/a&gt; - Named the #1 AI tool that engineers love (from Frontier Feud)&lt;/p&gt;
                                            &lt;/div&gt;

                                    &lt;/div&gt;
                                &lt;/div&gt;

                                &lt;div class="resources-category"&gt;
                                    &lt;h3&gt;Security &amp; Governance&lt;/h3&gt;
                                    &lt;div class="resources-grid"&gt;

                                            &lt;div class="resource-item"&gt;
                                                &lt;p&gt; &lt;a href="https://trust3.ai/platform/"&gt;Page.ai (PAIG)&lt;/a&gt; - Privacera's open-sourced solution for AI safety and security (presented by Durai)&lt;/p&gt;
                                            &lt;/div&gt;

                                            &lt;div class="resource-item"&gt;
                                                &lt;p&gt;&lt;a href="https://ranger.apache.org/"&gt;Apache Ranger&lt;/a&gt; - Open-source data governance project for big data (mentioned by Durai)&lt;/p&gt;
                                            &lt;/div&gt;

                                            &lt;div class="resource-item"&gt;
                                                &lt;p&gt;FailSafe - Writer's evaluation framework for testing models (presented by Alshikh)&lt;/p&gt;
                                            &lt;/div&gt;

                                    &lt;/div&gt;
                                &lt;/div&gt;

                                &lt;div class="resources-category"&gt;
                                    &lt;h3&gt;Models and Research&lt;/h3&gt;
                                    &lt;div class="resources-grid"&gt;

                                            &lt;div class="resource-item"&gt;
                                                &lt;p&gt; &lt;a href="https://github.com/deepseek-ai/DeepSeek-R1"&gt;DeepSeek-R1&lt;/a&gt; - DeepSeek's open source reasoning model inspired by OpenAI's o1 (discussed by Brown)&lt;/p&gt;
                                            &lt;/div&gt;

                                            &lt;div class="resource-item"&gt;
                                                &lt;p&gt;&lt;a href="https://ai.meta.com/blog/meta-llama-3-1/"&gt;Llama 3.1&lt;/a&gt; - Meta's open source language model (mentioned by multiple speakers)&lt;/p&gt;
                                            &lt;/div&gt;

                                            &lt;div class="resource-item"&gt;
                                                &lt;p&gt;&lt;a href="https://arxiv.org/abs/2502.01652"&gt;GRPO Algorithm&lt;/a&gt; - Simple RL algorithm for fine-tuning LLMs (presented by Brown)&lt;/p&gt;
                                            &lt;/div&gt;

                                            &lt;div class="resource-item"&gt;
                                                &lt;p&gt;&lt;a href="https://www.anthropic.com/engineering/contextual-retrieval"&gt;"Introducing Contextual Retrieval"&lt;/a&gt; - Post by Anthropic (mentioned by Bricken)&lt;/p&gt;
                                            &lt;/div&gt;

                                            &lt;div class="resource-item"&gt;
                                                &lt;p&gt;&lt;a href="https://transformer-circuits.pub/2023/monosemantic-features"&gt;"Towards Monosemanticity"&lt;/a&gt; and &lt;a href="https://transformer-circuits.pub/2024/scaling-monosemanticity/"&gt;"Scaling Monosemanticity"&lt;/a&gt; - Anthropic interpretability papers (mentioned by Bricken)&lt;/p&gt;
                                            &lt;/div&gt;

                                            &lt;div class="resource-item"&gt;
                                                &lt;p&gt;&lt;a href="https://x.com/willccbb/status/1883611121577517092"&gt;Rubric Engineering&lt;/a&gt; - Will Brown's approach to designing reward functions for LLM fine-tuning&lt;/p&gt;
                                            &lt;/div&gt;

                                            &lt;div class="resource-item"&gt;
                                                &lt;p&gt;&lt;a href="https://www.gr.inc/"&gt;gr.inc&lt;/a&gt; - Ross Taylor's project addressing reasoning gaps in open models (mentioned by Soumith Chintala)&lt;/p&gt;
                                            &lt;/div&gt;

                                    &lt;/div&gt;
                                &lt;/div&gt;

                                &lt;div class="resources-category"&gt;
                                    &lt;h3&gt;Enterprise Solutions&lt;/h3&gt;
                                    &lt;div class="resources-grid"&gt;

                                            &lt;div class="resource-item"&gt;
                                                &lt;p&gt; &lt;a href="https://www.datadoghq.com/blog/datadog-bits-generative-ai/"&gt;Bits AI&lt;/a&gt; - Datadog's AI assistant for DevOps (presented by Bishop)&lt;/p&gt;
                                            &lt;/div&gt;

                                            &lt;div class="resource-item"&gt;
                                                &lt;p&gt;&lt;a href="https://contextual.ai/"&gt;Contextual AI&lt;/a&gt; - Specialized RAG agents for enterprise use cases (presented by Kiela)&lt;/p&gt;
                                            &lt;/div&gt;

                                            &lt;div class="resource-item"&gt;
                                                &lt;p&gt;&lt;a href="https://writer.com/"&gt;Writer&lt;/a&gt; - Domain-specific LLMs for enterprises (presented by Alshikh)&lt;/p&gt;
                                            &lt;/div&gt;

                                            &lt;div class="resource-item"&gt;
                                                &lt;p&gt;&lt;a href="https://brightwave.ai/"&gt;BrightWave&lt;/a&gt; - Knowledge agent for due diligence and financial research (presented by Conover)&lt;/p&gt;
                                            &lt;/div&gt;

                                            &lt;div class="resource-item"&gt;
                                                &lt;p&gt;&lt;a href="https://sierra.ai/"&gt;Sierra&lt;/a&gt; - Voice AI platform for customer service and conversation automation (presented by Reneau-Wedeen)&lt;/p&gt;
                                            &lt;/div&gt;

                                            &lt;div class="resource-item"&gt;
                                                &lt;p&gt;&lt;a href="https://augmentcode.ai/"&gt;Augment Code&lt;/a&gt; - Building AI coding agents (presented by Flaherty)&lt;/p&gt;
                                            &lt;/div&gt;

                                    &lt;/div&gt;
                                &lt;/div&gt;

                                &lt;div class="resources-category"&gt;
                                    &lt;h3&gt;Infrastructure &amp; Implementation&lt;/h3&gt;
                                    &lt;div class="resources-grid"&gt;

                                            &lt;div class="resource-item"&gt;
                                                &lt;p&gt; &lt;a href="https://www.arista.com/en/solutions/ai-networking"&gt;Arista Networks&lt;/a&gt; - Specialized networking infrastructure for AI data centers (presented by Gilbert)&lt;/p&gt;
                                            &lt;/div&gt;

                                            &lt;div class="resource-item"&gt;
                                                &lt;p&gt;&lt;a href="https://ultraethernet.org/"&gt;Ultra Ethernet Consortium&lt;/a&gt; - Next-generation ethernet standard for AI workloads (mentioned by Gilbert)&lt;/p&gt;
                                            &lt;/div&gt;

                                            &lt;div class="resource-item"&gt;
                                                &lt;p&gt;&lt;a href="https://www.anthropic.com/news/model-context-protocol"&gt;Model Context Protocol&lt;/a&gt; - Anthropic's open source protocol for language models to interact with data sources&lt;/p&gt;
                                            &lt;/div&gt;

                                    &lt;/div&gt;
                                &lt;/div&gt;

                                &lt;div class="resources-category"&gt;
                                    &lt;h3&gt;Educational Resources&lt;/h3&gt;
                                    &lt;div class="resources-grid"&gt;

                                            &lt;div class="resource-item"&gt;
                                                &lt;p&gt; &lt;a href="https://hackidemia.github.io/cognimates-website/home/"&gt;Cognimates&lt;/a&gt; - Platform for teaching children about AI through visual programming (presented by Druga)&lt;/p&gt;
                                            &lt;/div&gt;

                                            &lt;div class="resource-item"&gt;
                                                &lt;p&gt;&lt;a href="https://www.jonesday.com/en/insights/2025/02/eu-ai-act-first-rules-take-effect-on-prohibited-ai-systems"&gt;AI Literacy&lt;/a&gt; - EU AI Act now requires AI literacy as part of regulation (mentioned by Druga)&lt;/p&gt;
                                            &lt;/div&gt;

                                            &lt;div class="resource-item"&gt;
                                                &lt;p&gt;&lt;a href="https://scratch.mit.edu/"&gt;Scratch&lt;/a&gt; - Visual programming environment for children with 100+ million users (mentioned by Druga)&lt;/p&gt;
                                            &lt;/div&gt;

                                            &lt;div class="resource-item"&gt;
                                                &lt;p&gt;&lt;a href="https://www.signalfire.com/beacon-ai"&gt;Beacon&lt;/a&gt; - SignalFire's AI ML platform tracking 650M+ employees and 80M+ companies (presented by Black)&lt;/p&gt;
                                            &lt;/div&gt;

                                    &lt;/div&gt;
                                &lt;/div&gt;

                        &lt;/div&gt;



        &lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;

&lt;script&gt;
        // Add animation delay to sections
        document.addEventListener('DOMContentLoaded', function() {
            const sections = document.querySelectorAll('.cyberpunk-article h2');
            sections.forEach((section, index) =&gt; {
                const sectionContent = section.nextElementSibling;
                if (sectionContent) {
                    sectionContent.classList.add('section-animate');
                    sectionContent.style.animationDelay = `${index * 0.2}s`;
                }
            });

            // Check if device is mobile for performance optimization
            const isMobile = window.matchMedia('(max-width: 768px)').matches;

            if (isMobile) {
                // Reduce animations on mobile for better performance
                document.querySelectorAll('.cyberpunk-article .section-animate').forEach(section =&gt; {
                    section.style.animationDuration = '0.5s';
                });

                // Disable complex animations on mobile
                document.querySelectorAll('.cyberpunk-article .glitch-effect').forEach(element =&gt; {
                    element.classList.remove('glitch-effect');
                });
            }

            // Lazy load animations as user scrolls
            const observer = new IntersectionObserver((entries) =&gt; {
                entries.forEach(entry =&gt; {
                    if (entry.isIntersecting) {
                        entry.target.style.opacity = '1';
                        entry.target.style.transform = 'translateY(0)';
                        observer.unobserve(entry.target);
                    }
                });
            }, { threshold: 0.1 });

            // Only use intersection observer for non-mobile or if it's supported
            if (!isMobile &amp;&amp; 'IntersectionObserver' in window) {
                document.querySelectorAll('.cyberpunk-article .section-animate').forEach(section =&gt; {
                    section.style.opacity = '0';
                    section.style.transform = 'translateY(20px)';
                    section.style.transition = 'opacity 0.8s ease-out, transform 0.8s ease-out';
                    observer.observe(section);
                });
            }
        });
&lt;/script&gt;</content><category term="Conferences"/><category term="conference"/><category term="agents"/><category term="ai_engineering"/><category term="LLMs"/></entry><entry><title>Using the BytesIO Class in Python</title><link href="https://gallon.me/using-the-bytesio-class-in-python.html" rel="alternate"/><published>2025-02-14T00:00:00-06:00</published><updated>2025-02-14T00:00:00-06:00</updated><author><name>Corey</name></author><id>tag:gallon.me,2025-02-14:/using-the-bytesio-class-in-python.html</id><summary type="html">&lt;p&gt;The &lt;code&gt;io.BytesIO&lt;/code&gt; class in Python is an in-memory stream for binary data. It provides a file-like interface that lets you read and write bytes just like you would with a file, but all the data is kept in memory rather than on disk. This can be extremely useful when …&lt;/p&gt;</summary><content type="html">&lt;p&gt;The &lt;code&gt;io.BytesIO&lt;/code&gt; class in Python is an in-memory stream for binary data. It provides a file-like interface that lets you read and write bytes just like you would with a file, but all the data is kept in memory rather than on disk. This can be extremely useful when you need a temporary buffer, when you're processing data that's generated on the fly, or when you want to simulate a file without touching the filesystem.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="key-concepts"&gt;Key Concepts&lt;/h2&gt;
&lt;h3 id="1-binary-data"&gt;1. &lt;strong&gt;Binary Data&lt;/strong&gt;&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Binary Data vs. Text Data&lt;/strong&gt;: Binary data is any data that is not necessarily human-readable (e.g., images, audio files, compiled programs). It consists of bytes, whereas text data is typically encoded in formats like UTF-8.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Why Binary?&lt;/strong&gt;: When dealing with non-textual content (like images or executables), you need to handle data at the byte level to preserve its exact structure.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="2-files-in-binary-mode"&gt;2. &lt;strong&gt;Files in Binary Mode&lt;/strong&gt;&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Opening Files in Binary Mode&lt;/strong&gt;: When reading or writing binary data to a file, you typically open the file in binary mode (&lt;code&gt;'rb'&lt;/code&gt; for reading and &lt;code&gt;'wb'&lt;/code&gt; for writing). This ensures that no encoding/decoding happens automatically, preserving the raw bytes.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;```python
  # Writing binary data to a file:
  with open('output.bin', 'wb') as f:
      f.write(b'\x00\x01\x02')&lt;/p&gt;
&lt;p&gt;# Reading binary data from a file:
  with open('output.bin', 'rb') as f:
      data = f.read()
      print(data)  # Output: b'\x00\x01\x02'
  ```&lt;/p&gt;
&lt;h3 id="3-iobytesio-in-practice"&gt;3. &lt;strong&gt;&lt;code&gt;io.BytesIO&lt;/code&gt; in Practice&lt;/strong&gt;&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;In-memory File-like Object&lt;/strong&gt;: &lt;code&gt;BytesIO&lt;/code&gt; acts like a file that exists in memory. This is particularly handy for testing, manipulating binary data without writing to disk, or when performance matters (reducing I/O overhead).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Interface Similarity&lt;/strong&gt;: It supports many of the same methods as regular file objects (like &lt;code&gt;.read()&lt;/code&gt;, &lt;code&gt;.write()&lt;/code&gt;, &lt;code&gt;.seek()&lt;/code&gt;, etc.).&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id="applied-examples"&gt;Applied Examples&lt;/h2&gt;
&lt;h3 id="example-1-manipulating-image-data"&gt;Example 1: Manipulating Image Data&lt;/h3&gt;
&lt;p&gt;Imagine you’re working with an image processing library that expects a file-like object, but your image is coming from a web request as bytes. You can wrap the bytes in a &lt;code&gt;BytesIO&lt;/code&gt; to provide that interface:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;io&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;PIL&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Image&lt;/span&gt;  &lt;span class="c1"&gt;# Pillow library for image processing&lt;/span&gt;

&lt;span class="c1"&gt;# Simulated image bytes (normally you&amp;#39;d get this from a request)&lt;/span&gt;
&lt;span class="n"&gt;image_bytes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;b&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;...&amp;#39;&lt;/span&gt;  &lt;span class="c1"&gt;# Replace with actual image bytes&lt;/span&gt;

&lt;span class="c1"&gt;# Wrap the bytes in a BytesIO object&lt;/span&gt;
&lt;span class="n"&gt;image_stream&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;io&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;BytesIO&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;image_bytes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Open the image using PIL, which expects a file-like object&lt;/span&gt;
&lt;span class="n"&gt;image&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Image&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;image_stream&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;image&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;show&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;  &lt;span class="c1"&gt;# Display the image&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h3 id="example-2-temporary-data-buffer"&gt;Example 2: Temporary Data Buffer&lt;/h3&gt;
&lt;p&gt;If you need a temporary buffer to collect binary data before writing it to disk, &lt;code&gt;BytesIO&lt;/code&gt; is ideal:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;io&lt;/span&gt;

&lt;span class="c1"&gt;# Create an in-memory binary stream&lt;/span&gt;
&lt;span class="n"&gt;buffer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;io&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;BytesIO&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="c1"&gt;# Write some binary data to it&lt;/span&gt;
&lt;span class="n"&gt;buffer&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;b&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;Hello, &amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;buffer&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;b&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;world!&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Move to the beginning of the buffer to read the data&lt;/span&gt;
&lt;span class="n"&gt;buffer&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;seek&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;buffer&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;read&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# Output: b&amp;#39;Hello, world!&amp;#39;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h3 id="example-3-testing-without-file-io"&gt;Example 3: Testing Without File I/O&lt;/h3&gt;
&lt;p&gt;When writing tests, you might want to avoid creating actual files. &lt;code&gt;BytesIO&lt;/code&gt; allows you to simulate file operations entirely in memory:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;io&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;process_binary_data&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;file_obj&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="c1"&gt;# Example function that reads binary data and processes it&lt;/span&gt;
    &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;file_obj&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;read&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[::&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;  &lt;span class="c1"&gt;# Return reversed data&lt;/span&gt;

&lt;span class="c1"&gt;# Create a BytesIO object with some binary data&lt;/span&gt;
&lt;span class="n"&gt;fake_file&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;io&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;BytesIO&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;b&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;abcdef&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Pass the BytesIO object to your function&lt;/span&gt;
&lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;process_binary_data&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fake_file&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# Output: b&amp;#39;fedcba&amp;#39;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;hr&gt;
&lt;h2 id="when-to-use-iobytesio"&gt;When to Use &lt;code&gt;io.BytesIO&lt;/code&gt;&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Unit Testing&lt;/strong&gt;: Simulate file objects without creating real files.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Web Applications&lt;/strong&gt;: Handle file uploads or downloads in memory.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Data Processing Pipelines&lt;/strong&gt;: Process streams of binary data (e.g., for image manipulation, compression, encryption) without intermediate files.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Performance Sensitive Applications&lt;/strong&gt;: Reduce I/O overhead by using memory-based buffers.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id="summary"&gt;Summary&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;io.BytesIO&lt;/code&gt;&lt;/strong&gt; is an in-memory binary stream that mimics file operations.&lt;/li&gt;
&lt;li&gt;It's useful when you need a temporary file-like object for binary data.&lt;/li&gt;
&lt;li&gt;Common use cases include image processing, temporary buffers, and testing.&lt;/li&gt;
&lt;li&gt;Understanding binary data is crucial when working with non-text files to avoid encoding issues.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This flexibility makes &lt;code&gt;io.BytesIO&lt;/code&gt; a powerful tool in Python for efficiently handling binary data without the need for disk I/O.&lt;/p&gt;</content><category term="TIL"/><category term="python"/></entry><entry><title>Linking to Posts in Python Pelican</title><link href="https://gallon.me/linking-to-posts-in-python-pelican.html" rel="alternate"/><published>2024-12-16T00:00:00-06:00</published><updated>2024-12-16T00:00:00-06:00</updated><author><name>Corey</name></author><id>tag:gallon.me,2024-12-16:/linking-to-posts-in-python-pelican.html</id><summary type="html">&lt;p&gt;Pelican provides a syntax for linking to another post by its filename.&lt;/p&gt;</summary><content type="html">&lt;p&gt;Pelican provides a syntax for linking to another post by its filename.&lt;/p&gt;
&lt;p&gt;Pelican provides a special syntax to simplify linking between content files. Use &lt;code&gt;|filename|&lt;/code&gt; to automatically resolve the correct URL. For example:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;Link to another [&lt;span class="nt"&gt;TIL&lt;/span&gt;](&lt;span class="na"&gt;|filename|__main__.py file in a project.md&lt;/span&gt;) on this
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Pelican converts this into the correct URL during site generation. This is particularly useful for linking without worrying about the final directory structure.&lt;/p&gt;</content><category term="TIL"/><category term="python"/><category term="pelican"/></entry><entry><title>Copying Files in Linux With a Progress Bar</title><link href="https://gallon.me/copying-files-directories-in-linux-with-progress-indication.html" rel="alternate"/><published>2024-11-28T00:00:00-06:00</published><updated>2024-11-28T00:00:00-06:00</updated><author><name>Corey</name></author><id>tag:gallon.me,2024-11-28:/copying-files-directories-in-linux-with-progress-indication.html</id><summary type="html">&lt;p&gt;I’ve long used &lt;code&gt;rsync&lt;/code&gt; for high fidelity, detailed copies of large swathes of files in Linux.  For example:&lt;/p&gt;</summary><content type="html">&lt;p&gt;I’ve long used &lt;code&gt;rsync&lt;/code&gt; for high fidelity, detailed copies of large swathes of files in Linux.  For example:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;rsync&lt;span class="w"&gt; &lt;/span&gt;-av&lt;span class="w"&gt; &lt;/span&gt;--progress&lt;span class="w"&gt; &lt;/span&gt;/source/directory&lt;span class="w"&gt; &lt;/span&gt;/destination/directory
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;This gives you really nice, file-by-file progress indications — which is especially useful when moving large files across the network between machines or even locally across physical devices.&lt;/p&gt;
&lt;p&gt;Today I was moving data from old hard drives and just wanted overall progress on the total job to be done.  &lt;code&gt;rsync&lt;/code&gt; still FTW!&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;rsync&lt;span class="w"&gt; &lt;/span&gt;-a&lt;span class="w"&gt; &lt;/span&gt;--info&lt;span class="o"&gt;=&lt;/span&gt;progress2&lt;span class="w"&gt; &lt;/span&gt;/source/directory&lt;span class="w"&gt; &lt;/span&gt;/destination/directory/
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;NB — do not use a trailling &lt;code&gt;/&lt;/code&gt; in the source directory if you want the copied files to be retained inside of that directory name in the destination!&lt;/p&gt;
&lt;p&gt;This provides lovely summary output like this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;ubuntu@ubuntu:/media/ubuntu$&lt;span class="w"&gt; &lt;/span&gt;rsync&lt;span class="w"&gt; &lt;/span&gt;-a&lt;span class="w"&gt; &lt;/span&gt;--info&lt;span class="o"&gt;=&lt;/span&gt;progress2&lt;span class="w"&gt; &lt;/span&gt;/source/directory&lt;span class="w"&gt; &lt;/span&gt;/destination/directory
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt;,675,525,167&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="m"&gt;71&lt;/span&gt;%&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="m"&gt;13&lt;/span&gt;.49MB/s&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;:03:09&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;xfr#1281,&lt;span class="w"&gt; &lt;/span&gt;to-chk&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;/1712&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h1 id="a-better-more-robust-way-to-do-this"&gt;A Better, More Robust Way to Do This&lt;/h1&gt;
&lt;p&gt;Running this command gets some extra good stuffs.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;rsync&lt;span class="w"&gt; &lt;/span&gt;-a&lt;span class="w"&gt; &lt;/span&gt;--info&lt;span class="o"&gt;=&lt;/span&gt;progress2&lt;span class="w"&gt; &lt;/span&gt;--checksum&lt;span class="w"&gt; &lt;/span&gt;--partial&lt;span class="w"&gt; &lt;/span&gt;/source/directory&lt;span class="w"&gt; &lt;/span&gt;/destination/directory/
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h3 id="explanation-of-the-additions"&gt;Explanation of the Additions:&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;-checksum&lt;/code&gt;&lt;/strong&gt;:&lt;ul&gt;
&lt;li&gt;Ensures that files are compared using checksums rather than just timestamps and file sizes.&lt;/li&gt;
&lt;li&gt;Guarantees that files are transferred only if their content differs, even if metadata like modification times are identical.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;-partial&lt;/code&gt;&lt;/strong&gt;:&lt;ul&gt;
&lt;li&gt;Retains partially transferred files if the transfer is interrupted, allowing resumption without starting over.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;</content><category term="TIL"/><category term="linux"/></entry><entry><title>Why An 8TB Drive Isn’t 8 Usable TB</title><link href="https://gallon.me/why-an-8tb-drive-isnt-8-usable-tb.html" rel="alternate"/><published>2024-11-28T00:00:00-06:00</published><updated>2024-11-28T00:00:00-06:00</updated><author><name>Corey</name></author><id>tag:gallon.me,2024-11-28:/why-an-8tb-drive-isnt-8-usable-tb.html</id><summary type="html">&lt;p&gt;I bought an 8TB external SSD to use as a live boot Linux device.  I knew that this issue was due to the way that bits are aggregated into terabytes, but — thanks to our mate ChatGPT — here’s a nice explanation of why my 8TB SSD only shows 7,452 …&lt;/p&gt;</summary><content type="html">&lt;p&gt;I bought an 8TB external SSD to use as a live boot Linux device.  I knew that this issue was due to the way that bits are aggregated into terabytes, but — thanks to our mate ChatGPT — here’s a nice explanation of why my 8TB SSD only shows 7,452.02 GB (7.452TB) of usable space.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;The discrepancy between the advertised capacity of your SSD (8 TB) and the actual usable capacity (7,452.02 GB) is due to differences in how storage manufacturers and operating systems calculate disk space. Here’s a detailed explanation:&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id="1-manufacturer-vs-operating-system-measurements"&gt;&lt;strong&gt;1. Manufacturer vs. Operating System Measurements&lt;/strong&gt;&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Manufacturer Calculation (Decimal - Base 10):&lt;/strong&gt;&lt;ul&gt;
&lt;li&gt;Storage manufacturers use the decimal system, where:&lt;ul&gt;
&lt;li&gt;1 TB = &lt;strong&gt;1,000,000,000,000 bytes&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;8 TB = &lt;strong&gt;8,000,000,000,000 bytes&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Operating System Calculation (Binary - Base 2):&lt;/strong&gt;&lt;ul&gt;
&lt;li&gt;Operating systems (like Windows, Linux, macOS) use the binary system, where:&lt;ul&gt;
&lt;li&gt;1 GiB = &lt;strong&gt;1,073,741,824 bytes&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;1 TiB = &lt;strong&gt;1,099,511,627,776 bytes&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;When your OS displays capacity:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;8,000,000,000,000 bytes ÷ 1,073,741,824 bytes per GiB&lt;/strong&gt; = &lt;strong&gt;~7,452 GiB&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Similarly:&lt;ul&gt;
&lt;li&gt;8,000,000,000,000 bytes ÷ 1,099,511,627,776 bytes per TiB = &lt;strong&gt;~7.28 TiB&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3 id="2-why-this-happens"&gt;&lt;strong&gt;2. Why This Happens&lt;/strong&gt;&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Marketing:&lt;/strong&gt; Manufacturers prefer the larger-looking decimal numbers.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Reality Check:&lt;/strong&gt; Computers deal in binary, so the OS displays the actual capacity based on base-2 calculations.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3 id="3-example-of-conversion"&gt;&lt;strong&gt;3. Example of Conversion&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;If you calculate the conversion:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Advertised (Decimal):&lt;/strong&gt;
8 TB = 8,000,000,000,000 bytes.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Actual (Binary):&lt;/strong&gt;
8,000,000,000,000 bytes ÷ 1,073,741,824 (GiB) = &lt;strong&gt;~7,452 GiB&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3 id="4-formatting-overhead"&gt;&lt;strong&gt;4. Formatting Overhead&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;Additionally, after formatting the drive:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Some space is used for file system structures and metadata (e.g., NTFS, exFAT, ext4), reducing the usable capacity further.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3 id="5-practical-impact"&gt;&lt;strong&gt;5. Practical Impact&lt;/strong&gt;&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;The actual capacity you see (7,452.02 GB) is correct and normal for an 8 TB SSD.&lt;/li&gt;
&lt;li&gt;You haven’t lost any capacity—it’s just how the numbers are calculated and represented.&lt;/li&gt;
&lt;/ul&gt;</content><category term="TIL"/><category term="hardware"/></entry><entry><title>Wrap with HTML tags in VSCode</title><link href="https://gallon.me/wrap-with-html-tags-in-vscode.html" rel="alternate"/><published>2024-11-14T00:00:00-06:00</published><updated>2024-11-14T00:00:00-06:00</updated><author><name>Corey</name></author><id>tag:gallon.me,2024-11-14:/wrap-with-html-tags-in-vscode.html</id><summary type="html">&lt;p&gt;Using the Emmet capabilities in VSCode (see previous TIL) you can highlight a section of HTML code and quickly wrap it with other HTML using Emmet shortcuts.&lt;/p&gt;</summary><content type="html">&lt;p&gt;Using the Emmet capabilities in VSCode (see previous TIL) you can highlight a section of HTML code and quickly wrap it with other HTML using Emmet shortcuts.&lt;/p&gt;
&lt;p&gt;This is particularly useful when writing plain HTML and then returning to style it.&lt;/p&gt;
&lt;p&gt;You can access this functionality in the command palette &lt;/p&gt;
&lt;p&gt;&lt;img alt="wrap in command palette" src="images/20241114_wrap_in_command_palette.png"&gt;&lt;/p&gt;
&lt;p&gt;Smarter, still, is to create a keyboard shortcut for this. We’ll bind &lt;code&gt;ALT+M&lt;/code&gt; to this functionality.&lt;/p&gt;
&lt;p&gt;&lt;img alt="create keyboard shortcut" src="images/20241114_create_keyboard_shortcut.png"&gt;&lt;/p&gt;
&lt;p&gt;Now, simply highlight the HTML code you want to wrap, hit &lt;code&gt;ALT+M&lt;/code&gt; and start typing the Emmet abbreviations for the tags you want to wrap the code with. For example, we’ll wrap this simple HTML form with some BulmaCSS classes to improve layout …&lt;/p&gt;
&lt;p&gt;&lt;img alt="wrap with tags" src="images/20241114_wrap_with_tags.png"&gt;&lt;/p&gt;
&lt;p&gt;et voila!&lt;/p&gt;</content><category term="TIL"/><category term="vscode"/><category term="html"/><category term="10Xer"/></entry><entry><title>__main__.py file in a project</title><link href="https://gallon.me/__main__py-file-in-a-project.html" rel="alternate"/><published>2024-10-15T00:00:00-05:00</published><updated>2024-10-15T00:00:00-05:00</updated><author><name>Corey</name></author><id>tag:gallon.me,2024-10-15:/__main__py-file-in-a-project.html</id><summary type="html">&lt;p&gt;This is, to a Python project / module, what &lt;code&gt;if __name__ == '__main__&lt;/code&gt; is to an individual Python file.&lt;/p&gt;</summary><content type="html">&lt;p&gt;This is, to a Python project / module, what &lt;code&gt;if __name__ == '__main__&lt;/code&gt; is to an individual Python file.&lt;/p&gt;
&lt;p&gt;In Python projects, a &lt;code&gt;__main__.py&lt;/code&gt; file is used to define what happens when a package or module is run directly as a script. It allows you to make the package executable from the command line, similar to how you would run a single Python script.&lt;/p&gt;
&lt;p&gt;Here’s how it works:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Executed when the package is run as a script&lt;/strong&gt;: When you run a package like this:&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="nx"&gt;python&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;package_name&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The &lt;code&gt;__main__.py&lt;/code&gt; file inside the package is executed, just like a script. This is similar to how a &lt;code&gt;main()&lt;/code&gt; function works in other programming languages.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Entry point for a package&lt;/strong&gt;: It serves as an entry point to the package’s functionality. You can add logic here that should be executed when the package is run.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Example Structure&lt;/strong&gt;:&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="nx"&gt;my_package&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;
&lt;span class="err"&gt;├──&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;py&lt;/span&gt;
&lt;span class="err"&gt;├──&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;__main__&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;py&lt;/span&gt;
&lt;span class="err"&gt;├──&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;module1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;py&lt;/span&gt;
&lt;span class="err"&gt;└──&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;module2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;py&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;If you run:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="nx"&gt;python&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;my_package&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Python will execute the &lt;code&gt;__main__.py&lt;/code&gt; file in &lt;code&gt;my_package&lt;/code&gt;.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Typical Use Case&lt;/strong&gt;: It’s often used to bootstrap or run the application, e.g., starting a server, running an app, or triggering some core functionality when the package is invoked from the command line.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;For example, in a &lt;code&gt;__main__.py&lt;/code&gt; file, you might have:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Running the application!&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="vm"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;__main__&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;This ensures that the package can be run both as a standalone script or imported as a module without accidentally triggering the main execution logic.&lt;/p&gt;</content><category term="TIL"/><category term="python"/><category term="programming"/></entry><entry><title>A nice workflow for creating Python projects with AI</title><link href="https://gallon.me/a-nice-workflow-for-creating-python-projects-with-ai.html" rel="alternate"/><published>2024-10-15T00:00:00-05:00</published><updated>2024-10-15T00:00:00-05:00</updated><author><name>Corey</name></author><id>tag:gallon.me,2024-10-15:/a-nice-workflow-for-creating-python-projects-with-ai.html</id><summary type="html">&lt;p&gt;First, create a new project using poetry&lt;/p&gt;</summary><content type="html">&lt;p&gt;First, create a new project using poetry&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;poetry&lt;span class="w"&gt; &lt;/span&gt;new&lt;span class="w"&gt; &lt;/span&gt;asciinema-editor
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;This gives you a nice, clean new project structure.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;.
├──&lt;span class="w"&gt; &lt;/span&gt;README.md
├──&lt;span class="w"&gt; &lt;/span&gt;asciinema_editor
│&lt;span class="w"&gt;   &lt;/span&gt;├──&lt;span class="w"&gt; &lt;/span&gt;__init__.py
├──&lt;span class="w"&gt; &lt;/span&gt;poetry.lock
├──&lt;span class="w"&gt; &lt;/span&gt;pyproject.toml
└──&lt;span class="w"&gt; &lt;/span&gt;tests
&lt;span class="w"&gt;    &lt;/span&gt;└──&lt;span class="w"&gt; &lt;/span&gt;__init__.py
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Now, let the model know that you have this project structure and ask it to write you code.  Claude does a nice job of providing the relevant bits to make your project work nicely.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;e.g. &lt;code&gt;__main__.py&lt;/code&gt; (see &lt;a href="https://gallon.me/__main__py-file-in-a-project.html"&gt;TIL&lt;/a&gt; on this)&lt;/li&gt;
&lt;li&gt;Exposes the relevant main function in &lt;code&gt;__init__.py&lt;/code&gt; making it easier to import and use the package in other scripts if needed.&lt;/li&gt;
&lt;/ul&gt;</content><category term="TIL"/><category term="python"/><category term="programming"/></entry><entry><title>neofetch for Linux system info summary</title><link href="https://gallon.me/neofetch-for-linux-system-info-summary.html" rel="alternate"/><published>2024-10-15T00:00:00-05:00</published><updated>2024-10-15T00:00:00-05:00</updated><author><name>Corey</name></author><id>tag:gallon.me,2024-10-15:/neofetch-for-linux-system-info-summary.html</id><summary type="html">&lt;p&gt;&lt;code&gt;neofetch&lt;/code&gt; provides a nice snapshot of system information.  This is what you were wanting to add to the &lt;code&gt;--sysinfo&lt;/code&gt; output for &lt;code&gt;gpt-engineer&lt;/code&gt;.&lt;/p&gt;</summary><content type="html">&lt;p&gt;&lt;code&gt;neofetch&lt;/code&gt; provides a nice snapshot of system information.  This is what you were wanting to add to the &lt;code&gt;--sysinfo&lt;/code&gt; output for &lt;code&gt;gpt-engineer&lt;/code&gt;.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;❯&lt;span class="w"&gt; &lt;/span&gt;neofetch
.-/+oossssoo+/-.&lt;span class="w"&gt;               &lt;/span&gt;captivus@brisbane
&lt;span class="sb"&gt;`&lt;/span&gt;:+ssssssssssssssssss+:&lt;span class="sb"&gt;`&lt;/span&gt;&lt;span class="w"&gt;           &lt;/span&gt;-----------------
-+ssssssssssssssssssyyssss+-&lt;span class="w"&gt;         &lt;/span&gt;OS:&lt;span class="w"&gt; &lt;/span&gt;Ubuntu&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;22&lt;/span&gt;.04.5&lt;span class="w"&gt; &lt;/span&gt;LTS&lt;span class="w"&gt; &lt;/span&gt;on&lt;span class="w"&gt; &lt;/span&gt;Windows&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;10&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;x86_64
.ossssssssssssssssssdMMMNysssso.&lt;span class="w"&gt;       &lt;/span&gt;Kernel:&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;5&lt;/span&gt;.15.153.1-microsoft-standard-WSL2
/ssssssssssshdmmNNmmyNMMMMhssssss/&lt;span class="w"&gt;      &lt;/span&gt;Uptime:&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;days,&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;20&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;hours,&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;58&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;mins
+ssssssssshmydMMMMMMMNddddyssssssss+&lt;span class="w"&gt;     &lt;/span&gt;Packages:&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;961&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;dpkg&lt;span class="o"&gt;)&lt;/span&gt;,&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;10&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;snap&lt;span class="o"&gt;)&lt;/span&gt;
/sssssssshNMMMyhhyyyyhmNMMMNhssssssss/&lt;span class="w"&gt;    &lt;/span&gt;Shell:&lt;span class="w"&gt; &lt;/span&gt;zsh&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;5&lt;/span&gt;.8.1
.ssssssssdMMMNhsssssssssshNMMMdssssssss.&lt;span class="w"&gt;   &lt;/span&gt;Theme:&lt;span class="w"&gt; &lt;/span&gt;Adwaita&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;GTK3&lt;span class="o"&gt;]&lt;/span&gt;
+sssshhhyNMMNyssssssssssssyNMMMysssssss+&lt;span class="w"&gt;   &lt;/span&gt;Icons:&lt;span class="w"&gt; &lt;/span&gt;Adwaita&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;GTK3&lt;span class="o"&gt;]&lt;/span&gt;
ossyNMMMNyMMhsssssssssssssshmmmhssssssso&lt;span class="w"&gt;   &lt;/span&gt;Terminal:&lt;span class="w"&gt; &lt;/span&gt;Windows&lt;span class="w"&gt; &lt;/span&gt;Terminal
ossyNMMMNyMMhsssssssssssssshmmmhssssssso&lt;span class="w"&gt;   &lt;/span&gt;CPU:&lt;span class="w"&gt; &lt;/span&gt;13th&lt;span class="w"&gt; &lt;/span&gt;Gen&lt;span class="w"&gt; &lt;/span&gt;Intel&lt;span class="w"&gt; &lt;/span&gt;i7-13700F&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="m"&gt;24&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;@&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt;.111GHz
+sssshhhyNMMNyssssssssssssyNMMMysssssss+&lt;span class="w"&gt;   &lt;/span&gt;GPU:&lt;span class="w"&gt; &lt;/span&gt;d474:00:00.0&lt;span class="w"&gt; &lt;/span&gt;Microsoft&lt;span class="w"&gt; &lt;/span&gt;Corporation&lt;span class="w"&gt; &lt;/span&gt;Device&lt;span class="w"&gt; &lt;/span&gt;008e
.ssssssssdMMMNhsssssssssshNMMMdssssssss.&lt;span class="w"&gt;   &lt;/span&gt;Memory:&lt;span class="w"&gt; &lt;/span&gt;3098MiB&lt;span class="w"&gt; &lt;/span&gt;/&lt;span class="w"&gt; &lt;/span&gt;15921MiB
/sssssssshNMMMyhhyyyyhdNMMMNhssssssss/
+sssssssssdmydMMMMMMMMddddyssssssss+
/ssssssssssshdmNNNNmyNMMMMhssssss/
.ossssssssssssssssssdMMMNysssso.
-+sssssssssssssssssyyyssss+-
&lt;span class="sb"&gt;`&lt;/span&gt;:+ssssssssssssssssss+:&lt;span class="sb"&gt;`&lt;/span&gt;
.-/+oossssoo+/-.
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;&lt;img alt="neofetch" src="./images/20241015_neofetch.png"&gt;&lt;/p&gt;</content><category term="TIL"/><category term="linux"/></entry><entry><title>Emmet Abbreviations in VSCode</title><link href="https://gallon.me/emmet-abbreviations-in-vscode.html" rel="alternate"/><published>2024-10-10T00:00:00-05:00</published><updated>2024-10-10T00:00:00-05:00</updated><author><name>Corey</name></author><id>tag:gallon.me,2024-10-10:/emmet-abbreviations-in-vscode.html</id><summary type="html">&lt;p&gt;Apparently there’s this plugin for many IDEs called &lt;a href="https://emmet.io/"&gt;Emmet&lt;/a&gt; which makes writing HTML very fast!&lt;/p&gt;</summary><content type="html">&lt;p&gt;Apparently there’s this plugin for many IDEs called &lt;a href="https://emmet.io/"&gt;Emmet&lt;/a&gt; which makes writing HTML very fast!&lt;/p&gt;
&lt;p&gt;Here’s the &lt;a href="https://docs.emmet.io/cheat-sheet/"&gt;Emmet Cheat Sheet&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Here’s a great video on it:&lt;/p&gt;
&lt;p&gt;https://www.youtube.com/watch?v=V8vizNQKtx0&lt;/p&gt;
&lt;p&gt;Start all HTML projects by just typing &lt;code&gt;!&lt;/code&gt;:&lt;/p&gt;
&lt;p&gt;&lt;img alt="start html file.png" src="./images/20241010_start_html_file.png"&gt;&lt;/p&gt;
&lt;p&gt;Then, hit ENTER (not Tab) to complete to this boilerplate!&lt;/p&gt;
&lt;p&gt;&lt;img alt="enter completes.png" src="./images/20241010_enter_completes.png"&gt;&lt;/p&gt;
&lt;p&gt;Start by typing the name of the tag you want … then hit ENTER (not Tab) to complete.&lt;/p&gt;
&lt;p&gt;e.g. &lt;code&gt;div&lt;/code&gt; (ignore Cursor’s autocomplete suggestions behind it)&lt;/p&gt;
&lt;p&gt;&lt;img alt="div.png" src="./images/20241010_div.png"&gt;&lt;/p&gt;
&lt;p&gt;Then, ENTER:&lt;/p&gt;
&lt;p&gt;&lt;img alt="div complete.png" src="./images/20241010_div_complete.png"&gt;&lt;/p&gt;
&lt;p&gt;That works with any &lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements"&gt;HTML element&lt;/a&gt;.  It gets even better, though.  You can quickly generate CSS and other HTML attributes.&lt;/p&gt;
&lt;p&gt;For example, a &lt;code&gt;&amp;lt;span&amp;gt;&lt;/code&gt; with the CSS class &lt;code&gt;purple&lt;/code&gt;:&lt;/p&gt;
&lt;p&gt;&lt;img alt="span purple.png" src="./images/20241010_span_purple.png"&gt;&lt;/p&gt;
&lt;p&gt;ENTER completes it to:&lt;/p&gt;
&lt;p&gt;&lt;img alt="span purple complete.png" src="./images/20241010_span_purple_complete.png"&gt;&lt;/p&gt;
&lt;p&gt;Similarly, adding an &lt;code&gt;id=&lt;/code&gt; attribute to an element:&lt;/p&gt;
&lt;p&gt;&lt;img alt="span purple id.png" src="./images/20241010_span_purple_id.png"&gt;&lt;/p&gt;
&lt;p&gt;ENTER completes to:&lt;/p&gt;
&lt;p&gt;&lt;img alt="span purple id complete.png" src="./images/20241010_span_purple_id_complete.png"&gt;&lt;/p&gt;
&lt;p&gt;You can complete all of the attributes you want for an element by chaining them as so:&lt;/p&gt;
&lt;p&gt;&lt;img alt="div class id.png" src="./images/20241010_div_class_id.png"&gt;&lt;/p&gt;
&lt;p&gt;ENTER completes to:&lt;/p&gt;
&lt;p&gt;&lt;img alt="div class id complete.png" src="./images/20241010_div_class_id_complete.png"&gt;&lt;/p&gt;
&lt;p&gt;You can write these things the way that &lt;a href="https://developer.mozilla.org/en-US/docs/Web/CSS/Guides/Selectors"&gt;CSS Selectors&lt;/a&gt; are written.&lt;/p&gt;
&lt;p&gt;&lt;img alt="CSS selectors.png" src="./images/20241010_css_selectors.png"&gt;&lt;/p&gt;
&lt;p&gt;So, a &lt;code&gt;&amp;lt;div&amp;gt;&lt;/code&gt; with a class of &lt;code&gt;purple&lt;/code&gt; and a child &lt;code&gt;&amp;lt;span&amp;gt;&lt;/code&gt; with class &lt;code&gt;cyan&lt;/code&gt; is:&lt;/p&gt;
&lt;p&gt;&lt;img alt="div purple span cyan.png" src="./images/20241010_div_purple_span_cyan.png"&gt;&lt;/p&gt;
&lt;p&gt;ENTER completes to:&lt;/p&gt;
&lt;p&gt;&lt;img alt="div purple span cyan complete.png" src="./images/20241010_div_purple_span_cyan_complete.png"&gt;&lt;/p&gt;
&lt;p&gt;You can nest all of your elements similarly:&lt;/p&gt;
&lt;p&gt;&lt;img alt="header nav ul li.png" src="./images/20241010_header_nav_ul_li.png"&gt;&lt;/p&gt;
&lt;p&gt;Becomes:&lt;/p&gt;
&lt;p&gt;&lt;img alt="header nav ul complete.png" src="./images/20241010_header_nav_ul_complete.png"&gt;&lt;/p&gt;
&lt;p&gt;If you wanted 3 &lt;code&gt;&amp;lt;li&amp;gt;&lt;/code&gt; elements as children of the &lt;code&gt;&amp;lt;ul&amp;gt;&lt;/code&gt;, you can use &lt;code&gt;*&lt;/code&gt; multiplication:&lt;/p&gt;
&lt;p&gt;&lt;img alt="header nav ul li 3.png" src="./images/20241010_header_nav_ul_li_3.png"&gt;&lt;/p&gt;
&lt;p&gt;Becomes:&lt;/p&gt;
&lt;p&gt;&lt;img alt="header nav ul li 3 complete.png" src="./images/20241010_header_nav_ul_li_3_complete.png"&gt;&lt;/p&gt;
&lt;p&gt;Want to add text to each of those &lt;code&gt;&amp;lt;li&amp;gt;&lt;/code&gt; elements? Use the &lt;code&gt;{ }&lt;/code&gt; operators:&lt;/p&gt;
&lt;p&gt;&lt;img alt="header nav ul li 3 text.png" src="./images/20241010_header_nav_ul_li_3_text.png"&gt;&lt;/p&gt;
&lt;p&gt;Becomes:&lt;/p&gt;
&lt;p&gt;&lt;img alt="header nav ul li 3 text complete.png" src="./images/20241010_header_nav_ul_li_3_text_complete.png"&gt;&lt;/p&gt;
&lt;p&gt;Add numbers to those list items:&lt;/p&gt;
&lt;p&gt;&lt;img alt="header nav ul li 3 text 1 2 3.png" src="./images/20241010_header_nav_ul_li_3_text_1_2_3.png"&gt;&lt;/p&gt;
&lt;p&gt;Becomes:&lt;/p&gt;
&lt;p&gt;&lt;img alt="header nav ul li 3 text 1 2 3 complete.png" src="./images/20241010_header_nav_ul_li_3_text_1_2_3_complete.png"&gt;&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;$&lt;/code&gt; operator can be used in any other attribute also. e.g. &lt;code&gt;.class$&lt;/code&gt; becomes &lt;code&gt;.class-1&lt;/code&gt;, &lt;code&gt;.class-2&lt;/code&gt;, etc …&lt;/p&gt;
&lt;p&gt;Add additional &lt;code&gt;$&lt;/code&gt; to zero-pad the numbers.&lt;/p&gt;
&lt;p&gt;&lt;img alt="zero padded.png" src="./images/20241010_zero_padded.png"&gt;&lt;/p&gt;
&lt;p&gt;Becomes:&lt;/p&gt;
&lt;p&gt;&lt;img alt="zero padded complete.png" src="./images/20241010_zero_padded_complete.png"&gt;&lt;/p&gt;
&lt;p&gt;Add sibling elements using the &lt;code&gt;+&lt;/code&gt; operator.&lt;/p&gt;
&lt;p&gt;&lt;img alt="add sibling elements.png" src="./images/20241010_add_sibling_elements.png"&gt;&lt;/p&gt;
&lt;p&gt;Becomes:&lt;/p&gt;
&lt;p&gt;&lt;img alt="add sibling elements complete.png" src="./images/20241010_add_sibling_elements_complete.png"&gt;&lt;/p&gt;
&lt;p&gt;Group elements together to implement more complex, yet readable, structures. For example, if we wanted a &lt;code&gt;&amp;lt;header&amp;gt;&lt;/code&gt; with a &lt;code&gt;&amp;lt;nav&amp;gt;&lt;/code&gt; child, then the &lt;code&gt;&amp;lt;main&amp;gt;&lt;/code&gt; and &lt;code&gt;&amp;lt;footer&amp;gt;&lt;/code&gt; siblings as before, we can group with the &lt;code&gt;( )&lt;/code&gt; operators.&lt;/p&gt;
&lt;p&gt;&lt;img alt="group elements.png" src="./images/20241010_group_elements.png"&gt;&lt;/p&gt;
&lt;p&gt;Becomes:&lt;/p&gt;
&lt;p&gt;&lt;img alt="group elements complete.png" src="./images/20241010_group_elements_complete.png"&gt;&lt;/p&gt;
&lt;p&gt;Let’s combo it up! We want:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Header&lt;ul&gt;
&lt;li&gt;H2, with text&lt;/li&gt;
&lt;li&gt;Nav&lt;ul&gt;
&lt;li&gt;Ordered List&lt;/li&gt;
&lt;li&gt;5 list elements, with links (anchor tags)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Main&lt;/li&gt;
&lt;li&gt;Footer&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;To accomplish this, we write:&lt;/p&gt;
&lt;p&gt;&lt;img alt="combo.png" src="./images/20241010_combo.png"&gt;&lt;/p&gt;
&lt;p&gt;Which becomes:&lt;/p&gt;
&lt;p&gt;&lt;img alt="combo complete.png" src="./images/20241010_combo_complete.png"&gt;&lt;/p&gt;
&lt;p&gt;Building Forms using Emmet&lt;/p&gt;</content><category term="TIL"/><category term="vscode"/><category term="html"/><category term="programming"/><category term="10Xer"/></entry><entry><title>Installing Racket (Scheme) in WSL</title><link href="https://gallon.me/installing-racket-scheme-in-wsl.html" rel="alternate"/><published>2024-10-06T00:00:00-05:00</published><updated>2024-10-06T00:00:00-05:00</updated><author><name>Corey</name></author><id>tag:gallon.me,2024-10-06:/installing-racket-scheme-in-wsl.html</id><summary type="html">&lt;p&gt;We’ll install Racket from &lt;code&gt;apt&lt;/code&gt; as follows, as the snap version ended up with many hassles &lt;/p&gt;</summary><content type="html">&lt;p&gt;We’ll install Racket from &lt;code&gt;apt&lt;/code&gt; as follows, as the snap version ended up with many hassles &lt;/p&gt;
&lt;p&gt;Note to self — &lt;em&gt;don’t install Racket from the Snap Store!&lt;/em&gt;&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;**sudo&lt;span class="w"&gt; &lt;/span&gt;apt&lt;span class="w"&gt; &lt;/span&gt;update
sudo&lt;span class="w"&gt; &lt;/span&gt;apt&lt;span class="w"&gt; &lt;/span&gt;install&lt;span class="w"&gt; &lt;/span&gt;racket**
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Then:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;❯&lt;span class="w"&gt; &lt;/span&gt;racket&lt;span class="w"&gt; &lt;/span&gt;--version
Welcome&lt;span class="w"&gt; &lt;/span&gt;to&lt;span class="w"&gt; &lt;/span&gt;Racket&lt;span class="w"&gt; &lt;/span&gt;v8.2&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;cs&lt;span class="o"&gt;]&lt;/span&gt;.
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;We’re going to use VSCode (obviously) so we’ll install the &lt;a href="https://marketplace.visualstudio.com/items?itemName=evzen-wybitul.magic-racket"&gt;Magic Racket extension&lt;/a&gt; (which is the best of the various extensions).  Follow the instructions provided by the extension:&lt;/p&gt;
&lt;p&gt;&lt;img alt="setting up the extension" src="./images/20241006_setting_up_the_extension.png"&gt;&lt;/p&gt;
&lt;p&gt;NB, this is, specifically:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;raco&lt;span class="w"&gt; &lt;/span&gt;pkg&lt;span class="w"&gt; &lt;/span&gt;install&lt;span class="w"&gt; &lt;/span&gt;racket-langserver
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Respond “y” when prompted to install dependencies …&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;❯&lt;span class="w"&gt; &lt;/span&gt;raco&lt;span class="w"&gt; &lt;/span&gt;pkg&lt;span class="w"&gt; &lt;/span&gt;install&lt;span class="w"&gt; &lt;/span&gt;racket-langserver
Resolving&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;racket-langserver&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;via&lt;span class="w"&gt; &lt;/span&gt;https://download.racket-lang.org/releases/8.2/catalog/
Resolving&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;racket-langserver&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;via&lt;span class="w"&gt; &lt;/span&gt;https://pkgs.racket-lang.org
Downloading&lt;span class="w"&gt; &lt;/span&gt;repository&lt;span class="w"&gt; &lt;/span&gt;https://github.com/jeapostrophe/racket-langserver
The&lt;span class="w"&gt; &lt;/span&gt;following&lt;span class="w"&gt; &lt;/span&gt;uninstalled&lt;span class="w"&gt; &lt;/span&gt;packages&lt;span class="w"&gt; &lt;/span&gt;are&lt;span class="w"&gt; &lt;/span&gt;listed&lt;span class="w"&gt; &lt;/span&gt;as&lt;span class="w"&gt; &lt;/span&gt;dependencies&lt;span class="w"&gt; &lt;/span&gt;of&lt;span class="w"&gt; &lt;/span&gt;racket-langserver:
&lt;span class="w"&gt;   &lt;/span&gt;html-parsing
&lt;span class="w"&gt;   &lt;/span&gt;chk-lib
Would&lt;span class="w"&gt; &lt;/span&gt;you&lt;span class="w"&gt; &lt;/span&gt;like&lt;span class="w"&gt; &lt;/span&gt;to&lt;span class="w"&gt; &lt;/span&gt;install&lt;span class="w"&gt; &lt;/span&gt;these&lt;span class="w"&gt; &lt;/span&gt;dependencies?&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;Y/n/a/c/?&lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;y
&lt;span class="m"&gt;00&lt;/span&gt;:&lt;span class="w"&gt; &lt;/span&gt;Resolving&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;html-parsing&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;via&lt;span class="w"&gt; &lt;/span&gt;https://download.racket-lang.org/releases/8.2/catalog/
&lt;span class="m"&gt;00&lt;/span&gt;:&lt;span class="w"&gt; &lt;/span&gt;Resolving&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;html-parsing&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;via&lt;span class="w"&gt; &lt;/span&gt;https://pkgs.racket-lang.org
Resolving&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;chk-lib&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;via&lt;span class="w"&gt; &lt;/span&gt;https://download.racket-lang.org/releases/8.2/catalog/
Resolving&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;chk-lib&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;via&lt;span class="w"&gt; &lt;/span&gt;https://pkgs.racket-lang.org
Downloading&lt;span class="w"&gt; &lt;/span&gt;https://www.neilvandyke.org/racket/html-parsing.zip
Downloading&lt;span class="w"&gt; &lt;/span&gt;repository&lt;span class="w"&gt; &lt;/span&gt;git://github.com/jeapostrophe/chk?path&lt;span class="o"&gt;=&lt;/span&gt;chk-lib
The&lt;span class="w"&gt; &lt;/span&gt;following&lt;span class="w"&gt; &lt;/span&gt;uninstalled&lt;span class="w"&gt; &lt;/span&gt;packages&lt;span class="w"&gt; &lt;/span&gt;are&lt;span class="w"&gt; &lt;/span&gt;listed&lt;span class="w"&gt; &lt;/span&gt;as&lt;span class="w"&gt; &lt;/span&gt;dependencies&lt;span class="w"&gt; &lt;/span&gt;of&lt;span class="w"&gt; &lt;/span&gt;html-parsing:
&lt;span class="w"&gt;   &lt;/span&gt;mcfly
&lt;span class="w"&gt;   &lt;/span&gt;overeasy
Would&lt;span class="w"&gt; &lt;/span&gt;you&lt;span class="w"&gt; &lt;/span&gt;like&lt;span class="w"&gt; &lt;/span&gt;to&lt;span class="w"&gt; &lt;/span&gt;install&lt;span class="w"&gt; &lt;/span&gt;these&lt;span class="w"&gt; &lt;/span&gt;dependencies?&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;Y/n/a/c/?&lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;y
&lt;span class="m"&gt;00&lt;/span&gt;:&lt;span class="w"&gt; &lt;/span&gt;Resolving&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;mcfly&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;via&lt;span class="w"&gt; &lt;/span&gt;https://download.racket-lang.org/releases/8.2/catalog/
&lt;span class="m"&gt;00&lt;/span&gt;:&lt;span class="w"&gt; &lt;/span&gt;Resolving&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;mcfly&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;via&lt;span class="w"&gt; &lt;/span&gt;https://pkgs.racket-lang.org
Resolving&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;overeasy&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;via&lt;span class="w"&gt; &lt;/span&gt;https://download.racket-lang.org/releases/8.2/catalog/
Resolving&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;overeasy&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;via&lt;span class="w"&gt; &lt;/span&gt;https://pkgs.racket-lang.org
Downloading&lt;span class="w"&gt; &lt;/span&gt;https://www.neilvandyke.org/racket/mcfly.zip
Downloading&lt;span class="w"&gt; &lt;/span&gt;https://www.neilvandyke.org/racket/overeasy.zip
The&lt;span class="w"&gt; &lt;/span&gt;following&lt;span class="w"&gt; &lt;/span&gt;uninstalled&lt;span class="w"&gt; &lt;/span&gt;packages&lt;span class="w"&gt; &lt;/span&gt;were&lt;span class="w"&gt; &lt;/span&gt;listed&lt;span class="w"&gt; &lt;/span&gt;as&lt;span class="w"&gt; &lt;/span&gt;dependencies
and&lt;span class="w"&gt; &lt;/span&gt;they&lt;span class="w"&gt; &lt;/span&gt;were&lt;span class="w"&gt; &lt;/span&gt;installed:
&lt;span class="w"&gt; &lt;/span&gt;dependencies&lt;span class="w"&gt; &lt;/span&gt;of&lt;span class="w"&gt; &lt;/span&gt;racket-langserver:
&lt;span class="w"&gt;   &lt;/span&gt;html-parsing
&lt;span class="w"&gt;   &lt;/span&gt;chk-lib
&lt;span class="w"&gt; &lt;/span&gt;dependencies&lt;span class="w"&gt; &lt;/span&gt;of&lt;span class="w"&gt; &lt;/span&gt;html-parsing:
&lt;span class="w"&gt;   &lt;/span&gt;mcfly
&lt;span class="w"&gt;   &lt;/span&gt;overeasy
raco&lt;span class="w"&gt; &lt;/span&gt;setup:&lt;span class="w"&gt; &lt;/span&gt;version:&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;8&lt;/span&gt;.2
raco&lt;span class="w"&gt; &lt;/span&gt;setup:&lt;span class="w"&gt; &lt;/span&gt;platform:&lt;span class="w"&gt; &lt;/span&gt;x86_64-linux&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;cs&lt;span class="o"&gt;]&lt;/span&gt;
raco&lt;span class="w"&gt; &lt;/span&gt;setup:&lt;span class="w"&gt; &lt;/span&gt;target&lt;span class="w"&gt; &lt;/span&gt;machine:&lt;span class="w"&gt; &lt;/span&gt;ta6le
raco&lt;span class="w"&gt; &lt;/span&gt;setup:&lt;span class="w"&gt; &lt;/span&gt;installation&lt;span class="w"&gt; &lt;/span&gt;name:&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;8&lt;/span&gt;.2
raco&lt;span class="w"&gt; &lt;/span&gt;setup:&lt;span class="w"&gt; &lt;/span&gt;variants:&lt;span class="w"&gt; &lt;/span&gt;cs
raco&lt;span class="w"&gt; &lt;/span&gt;setup:&lt;span class="w"&gt; &lt;/span&gt;main&lt;span class="w"&gt; &lt;/span&gt;collects:&lt;span class="w"&gt; &lt;/span&gt;/usr/share/racket/collects/
raco&lt;span class="w"&gt; &lt;/span&gt;setup:&lt;span class="w"&gt; &lt;/span&gt;collects&lt;span class="w"&gt; &lt;/span&gt;paths:
raco&lt;span class="w"&gt; &lt;/span&gt;setup:&lt;span class="w"&gt;   &lt;/span&gt;/home/captivus/.local/share/racket/8.2/collects
raco&lt;span class="w"&gt; &lt;/span&gt;setup:&lt;span class="w"&gt;   &lt;/span&gt;/usr/share/racket/collects/
raco&lt;span class="w"&gt; &lt;/span&gt;setup:&lt;span class="w"&gt; &lt;/span&gt;main&lt;span class="w"&gt; &lt;/span&gt;pkgs:&lt;span class="w"&gt; &lt;/span&gt;/usr/share/racket/pkgs
raco&lt;span class="w"&gt; &lt;/span&gt;setup:&lt;span class="w"&gt; &lt;/span&gt;pkgs&lt;span class="w"&gt; &lt;/span&gt;paths:
raco&lt;span class="w"&gt; &lt;/span&gt;setup:&lt;span class="w"&gt;   &lt;/span&gt;/usr/share/racket/pkgs
raco&lt;span class="w"&gt; &lt;/span&gt;setup:&lt;span class="w"&gt;   &lt;/span&gt;/home/captivus/.local/share/racket/8.2/pkgs
raco&lt;span class="w"&gt; &lt;/span&gt;setup:&lt;span class="w"&gt; &lt;/span&gt;links&lt;span class="w"&gt; &lt;/span&gt;files:
raco&lt;span class="w"&gt; &lt;/span&gt;setup:&lt;span class="w"&gt;   &lt;/span&gt;/usr/share/racket/links.rktd
raco&lt;span class="w"&gt; &lt;/span&gt;setup:&lt;span class="w"&gt;   &lt;/span&gt;/home/captivus/.local/share/racket/8.2/links.rktd
raco&lt;span class="w"&gt; &lt;/span&gt;setup:&lt;span class="w"&gt; &lt;/span&gt;compiled-file&lt;span class="w"&gt; &lt;/span&gt;roots:
raco&lt;span class="w"&gt; &lt;/span&gt;setup:&lt;span class="w"&gt;   &lt;/span&gt;same
raco&lt;span class="w"&gt; &lt;/span&gt;setup:&lt;span class="w"&gt;   &lt;/span&gt;/usr/lib/racket/compiled
raco&lt;span class="w"&gt; &lt;/span&gt;setup:&lt;span class="w"&gt; &lt;/span&gt;main&lt;span class="w"&gt; &lt;/span&gt;docs:&lt;span class="w"&gt; &lt;/span&gt;/usr/share/doc/racket
raco&lt;span class="w"&gt; &lt;/span&gt;setup:&lt;span class="w"&gt; &lt;/span&gt;---&lt;span class="w"&gt; &lt;/span&gt;updating&lt;span class="w"&gt; &lt;/span&gt;info-domain&lt;span class="w"&gt; &lt;/span&gt;tables&lt;span class="w"&gt; &lt;/span&gt;---&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="m"&gt;20&lt;/span&gt;:14:51&lt;span class="o"&gt;]&lt;/span&gt;
raco&lt;span class="w"&gt; &lt;/span&gt;setup:&lt;span class="w"&gt; &lt;/span&gt;updating:&lt;span class="w"&gt; &lt;/span&gt;/home/captivus/.local/share/racket/8.2/share/info-cache.rktd
raco&lt;span class="w"&gt; &lt;/span&gt;setup:&lt;span class="w"&gt; &lt;/span&gt;---&lt;span class="w"&gt; &lt;/span&gt;pre-installing&lt;span class="w"&gt; &lt;/span&gt;collections&lt;span class="w"&gt; &lt;/span&gt;---&lt;span class="w"&gt;                     &lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="m"&gt;20&lt;/span&gt;:14:51&lt;span class="o"&gt;]&lt;/span&gt;
raco&lt;span class="w"&gt; &lt;/span&gt;setup:&lt;span class="w"&gt; &lt;/span&gt;---&lt;span class="w"&gt; &lt;/span&gt;installing&lt;span class="w"&gt; &lt;/span&gt;foreign&lt;span class="w"&gt; &lt;/span&gt;libraries&lt;span class="w"&gt; &lt;/span&gt;---&lt;span class="w"&gt;                   &lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="m"&gt;20&lt;/span&gt;:14:51&lt;span class="o"&gt;]&lt;/span&gt;
raco&lt;span class="w"&gt; &lt;/span&gt;setup:&lt;span class="w"&gt; &lt;/span&gt;---&lt;span class="w"&gt; &lt;/span&gt;installing&lt;span class="w"&gt; &lt;/span&gt;shared&lt;span class="w"&gt; &lt;/span&gt;files&lt;span class="w"&gt; &lt;/span&gt;---&lt;span class="w"&gt;                        &lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="m"&gt;20&lt;/span&gt;:14:51&lt;span class="o"&gt;]&lt;/span&gt;
raco&lt;span class="w"&gt; &lt;/span&gt;setup:&lt;span class="w"&gt; &lt;/span&gt;---&lt;span class="w"&gt; &lt;/span&gt;compiling&lt;span class="w"&gt; &lt;/span&gt;collections&lt;span class="w"&gt; &lt;/span&gt;---&lt;span class="w"&gt;                          &lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="m"&gt;20&lt;/span&gt;:14:51&lt;span class="o"&gt;]&lt;/span&gt;
raco&lt;span class="w"&gt; &lt;/span&gt;setup:&lt;span class="w"&gt; &lt;/span&gt;---&lt;span class="w"&gt; &lt;/span&gt;parallel&lt;span class="w"&gt; &lt;/span&gt;build&lt;span class="w"&gt; &lt;/span&gt;using&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;8&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;jobs&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;---&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="m"&gt;20&lt;/span&gt;:14:51&lt;span class="o"&gt;]&lt;/span&gt;
raco&lt;span class="w"&gt; &lt;/span&gt;setup:&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;7&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;making:&lt;span class="w"&gt; &lt;/span&gt;&amp;lt;pkgs&amp;gt;/chk-lib/chk
raco&lt;span class="w"&gt; &lt;/span&gt;setup:&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;6&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;making:&lt;span class="w"&gt; &lt;/span&gt;&amp;lt;pkgs&amp;gt;/html-parsing&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;html-parsing&lt;span class="o"&gt;)&lt;/span&gt;
raco&lt;span class="w"&gt; &lt;/span&gt;setup:&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;5&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;making:&lt;span class="w"&gt; &lt;/span&gt;&amp;lt;pkgs&amp;gt;/mcfly&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;McFly&lt;span class="w"&gt; &lt;/span&gt;Runtime&lt;span class="o"&gt;)&lt;/span&gt;
raco&lt;span class="w"&gt; &lt;/span&gt;setup:&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;4&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;making:&lt;span class="w"&gt; &lt;/span&gt;&amp;lt;pkgs&amp;gt;/overeasy&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;Overeasy&lt;span class="o"&gt;)&lt;/span&gt;
raco&lt;span class="w"&gt; &lt;/span&gt;setup:&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;3&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;making:&lt;span class="w"&gt; &lt;/span&gt;&amp;lt;pkgs&amp;gt;/racket-langserver
raco&lt;span class="w"&gt; &lt;/span&gt;setup:&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;3&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;making:&lt;span class="w"&gt; &lt;/span&gt;&amp;lt;pkgs&amp;gt;/racket-langserver/scribblings
raco&lt;span class="w"&gt; &lt;/span&gt;setup:&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;3&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;making:&lt;span class="w"&gt; &lt;/span&gt;&amp;lt;pkgs&amp;gt;/racket-langserver/tests
raco&lt;span class="w"&gt; &lt;/span&gt;setup:&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;3&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;making:&lt;span class="w"&gt; &lt;/span&gt;&amp;lt;pkgs&amp;gt;/racket-langserver/tests/lifecycle
raco&lt;span class="w"&gt; &lt;/span&gt;setup:&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;3&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;making:&lt;span class="w"&gt; &lt;/span&gt;&amp;lt;pkgs&amp;gt;/racket-langserver/tests/sync
raco&lt;span class="w"&gt; &lt;/span&gt;setup:&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;3&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;making:&lt;span class="w"&gt; &lt;/span&gt;&amp;lt;pkgs&amp;gt;/racket-langserver/tests/textDocument
raco&lt;span class="w"&gt; &lt;/span&gt;setup:&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;3&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;making:&lt;span class="w"&gt; &lt;/span&gt;&amp;lt;pkgs&amp;gt;/racket-langserver/tests/textDocument/code-action
raco&lt;span class="w"&gt; &lt;/span&gt;setup:&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;3&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;making:&lt;span class="w"&gt; &lt;/span&gt;&amp;lt;pkgs&amp;gt;/racket-langserver/tests/textDocument/completion
raco&lt;span class="w"&gt; &lt;/span&gt;setup:&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;3&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;making:&lt;span class="w"&gt; &lt;/span&gt;&amp;lt;pkgs&amp;gt;/racket-langserver/tests/textDocument/find-symbol
raco&lt;span class="w"&gt; &lt;/span&gt;setup:&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;3&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;making:&lt;span class="w"&gt; &lt;/span&gt;&amp;lt;pkgs&amp;gt;/racket-langserver/tests/textDocument/rename
raco&lt;span class="w"&gt; &lt;/span&gt;setup:&lt;span class="w"&gt; &lt;/span&gt;---&lt;span class="w"&gt; &lt;/span&gt;creating&lt;span class="w"&gt; &lt;/span&gt;launchers&lt;span class="w"&gt; &lt;/span&gt;---&lt;span class="w"&gt;                             &lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="m"&gt;20&lt;/span&gt;:14:56&lt;span class="o"&gt;]&lt;/span&gt;
raco&lt;span class="w"&gt; &lt;/span&gt;setup:&lt;span class="w"&gt; &lt;/span&gt;---&lt;span class="w"&gt; &lt;/span&gt;installing&lt;span class="w"&gt; &lt;/span&gt;man&lt;span class="w"&gt; &lt;/span&gt;pages&lt;span class="w"&gt; &lt;/span&gt;---&lt;span class="w"&gt;                           &lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="m"&gt;20&lt;/span&gt;:14:56&lt;span class="o"&gt;]&lt;/span&gt;
raco&lt;span class="w"&gt; &lt;/span&gt;setup:&lt;span class="w"&gt; &lt;/span&gt;---&lt;span class="w"&gt; &lt;/span&gt;building&lt;span class="w"&gt; &lt;/span&gt;documentation&lt;span class="w"&gt; &lt;/span&gt;---&lt;span class="w"&gt;                         &lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="m"&gt;20&lt;/span&gt;:14:56&lt;span class="o"&gt;]&lt;/span&gt;
raco&lt;span class="w"&gt; &lt;/span&gt;setup:&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;7&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;running:&lt;span class="w"&gt; &lt;/span&gt;&amp;lt;pkgs&amp;gt;/html-parsing/html-parsing.scrbl
raco&lt;span class="w"&gt; &lt;/span&gt;setup:&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;5&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;running:&lt;span class="w"&gt; &lt;/span&gt;&amp;lt;pkgs&amp;gt;/racket-index/scribblings/main/user/local-redirect.scrbl
raco&lt;span class="w"&gt; &lt;/span&gt;setup:&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;running:&lt;span class="w"&gt; &lt;/span&gt;&amp;lt;pkgs&amp;gt;/mcfly/mcfly.scrbl
raco&lt;span class="w"&gt; &lt;/span&gt;setup:&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;6&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;running:&lt;span class="w"&gt; &lt;/span&gt;&amp;lt;pkgs&amp;gt;/overeasy/overeasy.scrbl
raco&lt;span class="w"&gt; &lt;/span&gt;setup:&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;3&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;running:&lt;span class="w"&gt; &lt;/span&gt;&amp;lt;pkgs&amp;gt;/racket-langserver/scribblings/racket-langserver.scrbl
raco&lt;span class="w"&gt; &lt;/span&gt;setup:&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;running:&lt;span class="w"&gt; &lt;/span&gt;&amp;lt;pkgs&amp;gt;/racket-index/scribblings/main/user/release.scrbl
raco&lt;span class="w"&gt; &lt;/span&gt;setup:&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;running:&lt;span class="w"&gt; &lt;/span&gt;&amp;lt;pkgs&amp;gt;/racket-index/scribblings/main/user/search.scrbl
raco&lt;span class="w"&gt; &lt;/span&gt;setup:&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;4&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;running:&lt;span class="w"&gt; &lt;/span&gt;&amp;lt;pkgs&amp;gt;/racket-index/scribblings/main/user/start.scrbl
raco&lt;span class="w"&gt; &lt;/span&gt;setup:&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;7&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;rendering:&lt;span class="w"&gt; &lt;/span&gt;&amp;lt;pkgs&amp;gt;/html-parsing/html-parsing.scrbl
raco&lt;span class="w"&gt; &lt;/span&gt;setup:&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;6&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;rendering:&lt;span class="w"&gt; &lt;/span&gt;&amp;lt;pkgs&amp;gt;/racket-index/scribblings/main/user/local-redirect.scrbl
raco&lt;span class="w"&gt; &lt;/span&gt;setup:&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;5&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;rendering:&lt;span class="w"&gt; &lt;/span&gt;&amp;lt;pkgs&amp;gt;/mcfly/mcfly.scrbl
raco&lt;span class="w"&gt; &lt;/span&gt;setup:&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;4&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;rendering:&lt;span class="w"&gt; &lt;/span&gt;&amp;lt;pkgs&amp;gt;/overeasy/overeasy.scrbl
raco&lt;span class="w"&gt; &lt;/span&gt;setup:&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;3&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;rendering:&lt;span class="w"&gt; &lt;/span&gt;&amp;lt;pkgs&amp;gt;/racket-langserver/scribblings/racket-langserver.scrbl
raco&lt;span class="w"&gt; &lt;/span&gt;setup:&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;rendering:&lt;span class="w"&gt; &lt;/span&gt;&amp;lt;pkgs&amp;gt;/racket-index/scribblings/main/user/release.scrbl
raco&lt;span class="w"&gt; &lt;/span&gt;setup:&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;rendering:&lt;span class="w"&gt; &lt;/span&gt;&amp;lt;pkgs&amp;gt;/racket-index/scribblings/main/user/search.scrbl
raco&lt;span class="w"&gt; &lt;/span&gt;setup:&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;rendering:&lt;span class="w"&gt; &lt;/span&gt;&amp;lt;pkgs&amp;gt;/racket-index/scribblings/main/user/start.scrbl
raco&lt;span class="w"&gt; &lt;/span&gt;setup:&lt;span class="w"&gt; &lt;/span&gt;---&lt;span class="w"&gt; &lt;/span&gt;installing&lt;span class="w"&gt; &lt;/span&gt;collections&lt;span class="w"&gt; &lt;/span&gt;---&lt;span class="w"&gt;                         &lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="m"&gt;20&lt;/span&gt;:15:02&lt;span class="o"&gt;]&lt;/span&gt;
raco&lt;span class="w"&gt; &lt;/span&gt;setup:&lt;span class="w"&gt; &lt;/span&gt;---&lt;span class="w"&gt; &lt;/span&gt;post-installing&lt;span class="w"&gt; &lt;/span&gt;collections&lt;span class="w"&gt; &lt;/span&gt;---&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="m"&gt;20&lt;/span&gt;:15:02&lt;span class="o"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Notes on running this in WSL&lt;/p&gt;
&lt;p&gt;&lt;img alt="Notes on Running Racket in WSL" src="./images/20241006_notes_on_running_racket_in_wsl.png"&gt;&lt;/p&gt;</content><category term="TIL"/><category term="racket"/><category term="scheme"/><category term="programming"/><category term="wsl"/></entry><entry><title>raco: Racket's Package Management System (and More)</title><link href="https://gallon.me/raco-is-rackets-package-management-system-among-other-things.html" rel="alternate"/><published>2024-10-06T00:00:00-05:00</published><updated>2024-10-06T00:00:00-05:00</updated><author><name>Corey</name></author><id>tag:gallon.me,2024-10-06:/raco-is-rackets-package-management-system-among-other-things.html</id><summary type="html">&lt;p&gt;In the Racket programming language, &lt;code&gt;raco&lt;/code&gt; is a command-line tool used to interact with Racket's package management system, run tests, build documentation, and perform other tasks related to Racket development. It's a versatile utility that automates various tasks in Racket projects.&lt;/p&gt;</summary><content type="html">&lt;p&gt;In the Racket programming language, &lt;code&gt;raco&lt;/code&gt; is a command-line tool used to interact with Racket's package management system, run tests, build documentation, and perform other tasks related to Racket development. It's a versatile utility that automates various tasks in Racket projects.&lt;/p&gt;
&lt;p&gt;Some of the common uses of &lt;code&gt;raco&lt;/code&gt; include:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Package Management&lt;/strong&gt;: You can install, remove, or update Racket packages using commands like:&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;raco&lt;span class="w"&gt; &lt;/span&gt;pkg&lt;span class="w"&gt; &lt;/span&gt;install&lt;span class="w"&gt; &lt;/span&gt;&amp;lt;package-name&amp;gt;
raco&lt;span class="w"&gt; &lt;/span&gt;pkg&lt;span class="w"&gt; &lt;/span&gt;remove&lt;span class="w"&gt; &lt;/span&gt;&amp;lt;package-name&amp;gt;
raco&lt;span class="w"&gt; &lt;/span&gt;pkg&lt;span class="w"&gt; &lt;/span&gt;update&lt;span class="w"&gt; &lt;/span&gt;&amp;lt;package-name&amp;gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Compiling Racket Programs&lt;/strong&gt;: &lt;code&gt;raco&lt;/code&gt; can compile Racket files to bytecode or even to an executable:&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;raco&lt;span class="w"&gt; &lt;/span&gt;make&lt;span class="w"&gt; &lt;/span&gt;&amp;lt;file.rkt&amp;gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Running Tests&lt;/strong&gt;: Racket includes support for automated tests, and you can run tests using &lt;code&gt;raco test&lt;/code&gt;:&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;raco&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;test&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&amp;lt;file.rkt&amp;gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Building Documentation&lt;/strong&gt;: Racket supports generating documentation, and &lt;code&gt;raco&lt;/code&gt; can be used to build the documentation for a package:&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;raco&lt;span class="w"&gt; &lt;/span&gt;docs&lt;span class="w"&gt; &lt;/span&gt;&amp;lt;package-name&amp;gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Creating Executables&lt;/strong&gt;: You can use &lt;code&gt;raco exe&lt;/code&gt; to create standalone executables from Racket code:&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;raco&lt;span class="w"&gt; &lt;/span&gt;exe&lt;span class="w"&gt; &lt;/span&gt;&amp;lt;file.rkt&amp;gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;&lt;code&gt;raco&lt;/code&gt; simplifies project management by providing a unified interface for all these tasks, making it essential for anyone working on Racket projects.&lt;/p&gt;</content><category term="TIL"/><category term="racket"/><category term="scheme"/><category term="programming"/></entry><entry><title>Working through SICP in Jupyter Notebooks</title><link href="https://gallon.me/working-through-sicp-in-jupyter-notebooks.html" rel="alternate"/><published>2024-10-06T00:00:00-05:00</published><updated>2024-10-06T00:00:00-05:00</updated><author><name>Corey</name></author><id>tag:gallon.me,2024-10-06:/working-through-sicp-in-jupyter-notebooks.html</id><summary type="html">&lt;p&gt;I’m reading Structure and Interpretation of Computer Programs and want to write Racket in VSCode in Jupyter notebooks in a dialect of Scheme closest to the book.  This requires this declaration at the top of each Racket file:&lt;/p&gt;</summary><content type="html">&lt;p&gt;I’m reading Structure and Interpretation of Computer Programs and want to write Racket in VSCode in Jupyter notebooks in a dialect of Scheme closest to the book.  This requires this declaration at the top of each Racket file:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="kn"&gt;#lang &lt;/span&gt;&lt;span class="nn"&gt;r5rs&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;HOWEVER, Jupyter doesn’t like this …&lt;/p&gt;
&lt;p&gt;&lt;img alt="Jupyter Doesn't Like This" src="./images/20241006_jupyter_doesnt_like_this.png"&gt;&lt;/p&gt;
&lt;p&gt;The Jupyter kernel for Racket doesn't directly support changing the language with &lt;code&gt;#lang&lt;/code&gt; declarations in individual cells.  Claude to the rescue, though … eventually … what a pain to finally get to this!&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;We need to find our where &lt;code&gt;iracket&lt;/code&gt; is installed.&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;❯&lt;span class="w"&gt; &lt;/span&gt;find&lt;span class="w"&gt; &lt;/span&gt;~/.local&lt;span class="w"&gt; &lt;/span&gt;-name&lt;span class="w"&gt; &lt;/span&gt;iracket.rkt
/home/captivus/.local/share/racket/8.2/pkgs/iracket/iracket.rkt
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;ol&gt;
&lt;li&gt;Now, we edit this file (&lt;code&gt;home/captivus/.local/share/racket/8.2/pkgs/iracket/iracket.rkt&lt;/code&gt;&lt;strong&gt;)&lt;/strong&gt; as follows:&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="kn"&gt;#lang &lt;/span&gt;&lt;span class="nn"&gt;racket/base&lt;/span&gt;

&lt;span class="c1"&gt;;; ============================================================&lt;/span&gt;
&lt;span class="c1"&gt;;; Enabling R5RS mode for compatibility with SICP code&lt;/span&gt;
&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;require&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;racket/sandbox&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;define&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;orig-current-namespace&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;current-namespace&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;define&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;r5rs-namespace&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;make-base-namespace&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;parameterize&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="nb"&gt;current-namespace&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;r5rs-namespace&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;namespace-require&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;#39;&lt;/span&gt;&lt;span class="ss"&gt;r5rs&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;define&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;switch-to-r5rs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;current-namespace&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;r5rs-namespace&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;define&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;switch-to-orig&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;current-namespace&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;orig-current-namespace&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;switch-to-r5rs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;;; The rest of the original iracket.rkt content should follow here ...&lt;/span&gt;
&lt;span class="c1"&gt;;; ============================================================&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;ol&gt;
&lt;li&gt;Now, restart your VSCode and create a Jupyter notebook as previously described in your TIL article … and you’re up!&lt;/li&gt;
&lt;/ol&gt;</content><category term="TIL"/><category term="scheme"/><category term="racket_programming"/><category term="jupyter"/></entry><entry><title>Writing Racket (Scheme) in Jupyter Notebooks</title><link href="https://gallon.me/writing-racket-scheme-in-jupyter-notebooks.html" rel="alternate"/><published>2024-10-06T00:00:00-05:00</published><updated>2024-10-06T00:00:00-05:00</updated><author><name>Corey</name></author><id>tag:gallon.me,2024-10-06:/writing-racket-scheme-in-jupyter-notebooks.html</id><summary type="html">&lt;p&gt;First, we’re going to install Jupyter.  As we don’t want to contaminate our global Python installation, we’ll do this in a venv&lt;/p&gt;</summary><content type="html">&lt;p&gt;First, we’re going to install Jupyter.  As we don’t want to contaminate our global Python installation, we’ll do this in a venv&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;python&lt;span class="w"&gt; &lt;/span&gt;-m&lt;span class="w"&gt; &lt;/span&gt;venv&lt;span class="w"&gt; &lt;/span&gt;venv
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Now, let’s activate that bad boy:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="nb"&gt;source&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;./venv/bin/activate
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Assuming you have Racket installed (see previous TIL — NB do not install via the Snap store … use &lt;code&gt;apt&lt;/code&gt;!), install the Racket kernel for Jupyter:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;raco&lt;span class="w"&gt; &lt;/span&gt;pkg&lt;span class="w"&gt; &lt;/span&gt;install&lt;span class="w"&gt; &lt;/span&gt;iracket
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Should your system complain about not having &lt;code&gt;libzmq5&lt;/code&gt; installed:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;sudo&lt;span class="w"&gt; &lt;/span&gt;apt&lt;span class="w"&gt; &lt;/span&gt;install&lt;span class="w"&gt; &lt;/span&gt;libzmq5
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Then, install the Racket kernel into Jupyter:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;raco&lt;span class="w"&gt; &lt;/span&gt;iracket&lt;span class="w"&gt; &lt;/span&gt;install
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Then, confirm that Jupyter sees the Racket kernel:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;❯&lt;span class="w"&gt; &lt;/span&gt;jupyter&lt;span class="w"&gt; &lt;/span&gt;kernelspec&lt;span class="w"&gt; &lt;/span&gt;list
Available&lt;span class="w"&gt; &lt;/span&gt;kernels:
&lt;span class="w"&gt;  &lt;/span&gt;python3&lt;span class="w"&gt;    &lt;/span&gt;/home/captivus/projects/sicp/venv/share/jupyter/kernels/python3
&lt;span class="w"&gt;  &lt;/span&gt;racket&lt;span class="w"&gt;     &lt;/span&gt;/home/captivus/.local/share/jupyter/kernels/racket
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Now … we create a new Jupyter notebook in VSCode using the Command Palette:&lt;/p&gt;
&lt;p&gt;&lt;img alt="Command Palette New Jupyter Notebook.png" src="./images/20241006_command_palette_new_jupyter_notebook.png"&gt;&lt;/p&gt;
&lt;p&gt;In the top right of the notebook, select the kernel (it won’t say Racket when you start):&lt;/p&gt;
&lt;p&gt;&lt;img alt="Jupyter Notebook Select Kernel.png" src="./images/20241006_jupyter_notebook_select_kernel.png"&gt;&lt;/p&gt;
&lt;p&gt;You’ll be presented with these options:&lt;/p&gt;
&lt;p&gt;&lt;img alt="Jupyter Kernel Options.png" src="./images/20241006_jupyter_kernel_options.png"&gt;&lt;/p&gt;
&lt;p&gt;Select “Jupyter Kernel”, then select “Racket”:&lt;/p&gt;
&lt;p&gt;&lt;img alt="Jupyter Notebook Select Racket.png" src="./images/20241006_jupyter_notebook_select_racket.png"&gt;&lt;/p&gt;
&lt;p&gt;… and you’re up!&lt;/p&gt;</content><category term="TIL"/><category term="racket"/><category term="scheme"/><category term="programming"/><category term="jupyter"/></entry><entry><title>Debugging Python CLI Arguments in VSCode</title><link href="https://gallon.me/debugging-python-programs-that-require-cli-arguments-in-vscode.html" rel="alternate"/><published>2024-09-15T00:00:00-05:00</published><updated>2024-09-15T00:00:00-05:00</updated><author><name>Corey</name></author><id>tag:gallon.me,2024-09-15:/debugging-python-programs-that-require-cli-arguments-in-vscode.html</id><summary type="html">&lt;p&gt;Command Palette (CTRL+SHIFT+P):  Debug: Add Configuration&lt;/p&gt;</summary><content type="html">&lt;p&gt;Command Palette (CTRL+SHIFT+P):  Debug: Add Configuration&lt;/p&gt;
&lt;p&gt;&lt;img alt="Command Palette Debug Config.png" src="./images/20240911_command_palette_debug_config.png"&gt;&lt;/p&gt;
&lt;p&gt;Then, select “Python Debugger”&lt;/p&gt;
&lt;p&gt;&lt;img alt="Command Palette Python Debugger.png" src="./images/20240911_command_palette_python_debugger.png"&gt;&lt;/p&gt;
&lt;p&gt;Then, select “Python File with Arguments”&lt;/p&gt;
&lt;p&gt;&lt;img alt="Command Palette Python File with Arguments.png" src="./images/20240911_command_palette_python_file_with_arguments.png"&gt;&lt;/p&gt;
&lt;p&gt;VSCode then creates the &lt;code&gt;.vscode&lt;/code&gt; directory in your project, if it doesn’t exist already, and adds &lt;code&gt;launch.json&lt;/code&gt; as follows:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="c1"&gt;// Use IntelliSense to learn about possible attributes.&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="c1"&gt;// Hover to view descriptions of existing attributes.&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="c1"&gt;// For more information, visit: https://code.visualstudio.com/docs/debugtest/debugging&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;&amp;quot;version&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;0.2.0&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;&amp;quot;configurations&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="nt"&gt;&amp;quot;name&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Python Debugger: Current File with Arguments&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="nt"&gt;&amp;quot;type&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;debugpy&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="nt"&gt;&amp;quot;request&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;launch&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="nt"&gt;&amp;quot;program&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;${file}&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="nt"&gt;&amp;quot;console&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;integratedTerminal&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="nt"&gt;&amp;quot;args&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;${command:pickArgs}&amp;quot;&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;If you don’t change anything, when you debug a file VSCode will prompt you for arguments to pass the file every time you run the debugger.&lt;/p&gt;
&lt;p&gt;&lt;img alt="VSCode Prompt for Arguments.png" src="./images/20240911_vscode_prompt_for_arguments.png"&gt;&lt;/p&gt;
&lt;p&gt;Can also click, bottom right, “Add Configuration” which will stub out adding more configs in the JSON so that you can tailor it to your specific application.  For example:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="c1"&gt;// Use IntelliSense to learn about possible attributes.&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="c1"&gt;// Hover to view descriptions of existing attributes.&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="c1"&gt;// For more information, visit: https://code.visualstudio.com/docs/debugtest/debugging&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;&amp;quot;version&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;0.2.0&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;&amp;quot;configurations&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="nt"&gt;&amp;quot;name&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Blue Horseshoe Debugger&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="nt"&gt;&amp;quot;type&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;debugpy&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="nt"&gt;&amp;quot;request&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;launch&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="nt"&gt;&amp;quot;program&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;${file}&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="nt"&gt;&amp;quot;console&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;integratedTerminal&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="nt"&gt;&amp;quot;args&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;run&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;--players&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;5&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;--duration&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;2&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;--round-end&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;fixed&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;--config&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;strategy_config.yaml&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;--simulation&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;true&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;--strategies&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;conservative,conservative,conservative,momentum,arbitrage&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="nt"&gt;&amp;quot;name&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Python Debugger: Current File with Arguments&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="nt"&gt;&amp;quot;type&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;debugpy&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="nt"&gt;&amp;quot;request&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;launch&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="nt"&gt;&amp;quot;program&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;${file}&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="nt"&gt;&amp;quot;console&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;integratedTerminal&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="nt"&gt;&amp;quot;args&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;${command:pickArgs}&amp;quot;&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;</content><category term="TIL"/><category term="python"/><category term="programming"/><category term="vscode"/></entry><entry><title>Debugging Python Tests in VSCode</title><link href="https://gallon.me/debugging-python-tests-in-vscode.html" rel="alternate"/><published>2024-09-15T00:00:00-05:00</published><updated>2024-09-15T00:00:00-05:00</updated><author><name>Corey</name></author><id>tag:gallon.me,2024-09-15:/debugging-python-tests-in-vscode.html</id><summary type="html">&lt;p&gt;VSCode offers native test capabilities that are quite useful.  &lt;a href="https://code.visualstudio.com/docs/python/testing"&gt;Testing Python in Visual Studio Code&lt;/a&gt;&lt;/p&gt;</summary><content type="html">&lt;p&gt;VSCode offers native test capabilities that are quite useful.  &lt;a href="https://code.visualstudio.com/docs/python/testing"&gt;Testing Python in Visual Studio Code&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;It doesn’t, however, recognize your Python tests by default.  You must configure it to do so for each project.&lt;/p&gt;
&lt;p&gt;Command Palette (CTRL+SHIFT+P) → “Python: Configure Tests” → &lt;/p&gt;
&lt;p&gt;&lt;img alt="Command Palette Python Configure Tests.png" src="./images/20240915_command_palette_python_configure_tests.png"&gt;&lt;/p&gt;
&lt;p&gt;Select the framework you’re using:&lt;/p&gt;
&lt;p&gt;&lt;img alt="Command Palette Select Framework.png" src="./images/20240915_command_palette_select_framework.png"&gt;&lt;/p&gt;
&lt;p&gt;Now, you get all of the testy-goodness that VSCode has to offer for Python.&lt;/p&gt;
&lt;p&gt;In this case, I wanted to &lt;a href="https://code.visualstudio.com/docs/python/testing#_debug-tests"&gt;debug a specific test&lt;/a&gt;.  Super easy! Just drop breakpoints in the test where you want them, then run “Debug Test”.&lt;/p&gt;
&lt;p&gt;&lt;img alt="Right Click Debug Test.png" src="./images/20240915_right_click_debug_test.png"&gt;&lt;/p&gt;
&lt;p&gt;The “Test Explorer” is also quite useful.&lt;/p&gt;</content><category term="TIL"/><category term="python"/><category term="programming"/><category term="vscode"/></entry><entry><title>Suppressing Rich Tracebacks in Typer Apps</title><link href="https://gallon.me/suppressing-rich-tracebacks-in-typer-apps.html" rel="alternate"/><published>2024-09-11T00:00:00-05:00</published><updated>2024-09-11T00:00:00-05:00</updated><author><name>Corey</name></author><id>tag:gallon.me,2024-09-11:/suppressing-rich-tracebacks-in-typer-apps.html</id><summary type="html">&lt;p&gt;This must be applied to the configuration of the Typer object created for the CLI.&lt;/p&gt;</summary><content type="html">&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# suppress rich traceback&lt;/span&gt;
&lt;span class="n"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;typer&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Typer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pretty_exceptions_show_locals&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;False&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;This must be applied to the configuration of the Typer object created for the CLI.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Feature Image Prompt:&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Generate an image. The aesthetic should be cyberpunk with colors of neon pink, blue and purple. Do not add any people. Create a futuristic, abstract digital control panel featuring holographic panels and cascading streams of Python code, including a glimpse of the code snippet "app = typer.Typer(pretty_exceptions_show_locals=False)". The scene should evoke the concept of suppressing rich tracebacks, with flowing neon light trails, glitch effects, and a background filled with dark urban circuitry and digital grids, representing the seamless configuration of a high-tech CLI environment.&lt;/p&gt;</content><category term="TIL"/><category term="python"/><category term="programming"/></entry><entry><title>Uninstalling Poetry Completions for zsh</title><link href="https://gallon.me/uninstalling-poetry-completions-for-zsh.html" rel="alternate"/><published>2024-08-31T00:00:00-05:00</published><updated>2024-08-31T00:00:00-05:00</updated><author><name>Corey</name></author><id>tag:gallon.me,2024-08-31:/uninstalling-poetry-completions-for-zsh.html</id><summary type="html">&lt;p&gt;Here’s how to enable: https://python-poetry.org/docs/#enable-tab-completion-for-bash-fish-or-zsh&lt;/p&gt;</summary><content type="html">&lt;p&gt;Here’s how to enable: https://python-poetry.org/docs/#enable-tab-completion-for-bash-fish-or-zsh&lt;/p&gt;
&lt;p&gt;Uninstalling a specific completion works as follows:&lt;/p&gt;
&lt;p&gt;Navigate to `~/.zfunc&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;❯&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;cd&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;.zfunc
❯&lt;span class="w"&gt; &lt;/span&gt;ll
Permissions&lt;span class="w"&gt; &lt;/span&gt;Size&lt;span class="w"&gt; &lt;/span&gt;User&lt;span class="w"&gt;     &lt;/span&gt;Date&lt;span class="w"&gt; &lt;/span&gt;Modified&lt;span class="w"&gt; &lt;/span&gt;Name
.rw-r--r--&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="m"&gt;243&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;captivus&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;31&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;Aug&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;10&lt;/span&gt;:28&lt;span class="w"&gt;  &lt;/span&gt;_convert-mp4-to-wav
.rw-r--r--&lt;span class="w"&gt;   &lt;/span&gt;13k&lt;span class="w"&gt; &lt;/span&gt;captivus&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;10&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;Oct&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="m"&gt;2023&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;_poetry
.rw-r--r--&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="m"&gt;273&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;captivus&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;28&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;Aug&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;14&lt;/span&gt;:20&lt;span class="w"&gt;  &lt;/span&gt;_stccc-directory-scraper
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;To uninstall the Zsh completions for &lt;code&gt;_convert-mp4-to-wav&lt;/code&gt;, follow these steps:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Remove the Completion File:&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Delete the &lt;code&gt;_convert-mp4-to-wav&lt;/code&gt; file from your &lt;code&gt;~/.zfunc&lt;/code&gt; directory:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;rm&lt;span class="w"&gt; &lt;/span&gt;~/.zfunc/_convert-mp4-to-wav
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Update the Zsh Completion System:&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;After removing the file, you should update the Zsh completion system by running the following command:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;compinit
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Reload Your Zsh Configuration:&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Finally, reload your Zsh configuration to ensure that the changes take effect:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="nb"&gt;source&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;~/.zshrc
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;This will remove the completion for &lt;code&gt;_convert-mp4-to-wav&lt;/code&gt; from your Zsh setup.&lt;/p&gt;
&lt;p&gt;Optionally, you can leave #2 and #3 for later, as the shell should reinitialize compinit when restarted.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Feature Image Prompt:&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Generate an image. The aesthetic should be cyberpunk with colors of neon pink, blue and purple. Do not add any people. Imagine a sleek, futuristic terminal screen floating in a dark, digital space. The terminal features luminous commands and code snippets like "cd ~/.zfunc", "rm ~/.zfunc/_convert-mp4-to-wav", and "source ~/.zshrc" in vibrant neon text. Surround the screen with abstract, holographic gridlines and circuit-like patterns, evoking the essence of a high-tech hacking interface. The overall vibe is minimalist yet distinctly technical, capturing the spirit of a cyberpunk command line environment without any human presence.&lt;/p&gt;</content><category term="TIL"/><category term="zsh"/><category term="python"/><category term="programming"/></entry><entry><title>Ubuntu Snaps in WSL &amp; zsh</title><link href="https://gallon.me/ubuntu-snaps-in-wsl-zsh.html" rel="alternate"/><published>2024-08-26T00:00:00-05:00</published><updated>2024-08-26T00:00:00-05:00</updated><author><name>Corey</name></author><id>tag:gallon.me,2024-08-26:/ubuntu-snaps-in-wsl-zsh.html</id><summary type="html">&lt;p&gt;What on earth are “&lt;a href="https://snapcraft.io/docs/"&gt;Snaps&lt;/a&gt;”?&lt;/p&gt;</summary><content type="html">&lt;p&gt;What on earth are “&lt;a href="https://snapcraft.io/docs/"&gt;Snaps&lt;/a&gt;”?&lt;/p&gt;
&lt;p&gt;These are fully containerized versions of applications that can be easily “installed” from the &lt;a href="https://snapcraft.io/"&gt;Snap Store&lt;/a&gt;. This store is run by canonical, but it looks like they support other distributions of Linux.&lt;/p&gt;
&lt;p&gt;From the link above:&lt;/p&gt;
&lt;h1 id="introduction-to-snaps"&gt;&lt;strong&gt;Introduction to snaps&lt;/strong&gt;&lt;/h1&gt;
&lt;p&gt;Snaps are a secure and scalable way to embed applications on Linux devices. A snap is an application containerised with all its dependencies. A snap can be installed using a single command on any device running Linux. With snaps, software updates are automatic and resilient. Applications run fully isolated in their own sandbox, thus minimising security risks.&lt;/p&gt;
&lt;p&gt;Snaps are hosted in the global &lt;a href="https://snapcraft.io/"&gt;Snap Store&lt;/a&gt;, an application repository hosted and managed by Canonical, and are free for anyone to download. Snaps can be created by anyone - existing software can be packaged as a snap or new software can be built from scratch using snap packaging. There is also an active, vibrant community of developers and users, with a &lt;a href="https://forum.snapcraft.io/"&gt;forum&lt;/a&gt; where anyone can ask questions.&lt;/p&gt;
&lt;p&gt;Packaging IoT applications as snaps bring the following benefits:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Reliable&lt;/td&gt;
&lt;td&gt;Snaps use transactional updates, meaning that if for any reason an update you push to your snap fails, the snap will roll back to its last stable state&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Modular&lt;/td&gt;
&lt;td&gt;Snaps are reusable, they enable a loosely-coupled software architecture for embedded software and are compatible across architectures&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Robust&lt;/td&gt;
&lt;td&gt;With snaps, software updates are automatic and over-the-air (OTA), meaning your software is never out-of-date&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Optimised&lt;/td&gt;
&lt;td&gt;Snaps harness delta updates, minimising the storage and bandwidth needed when updating software. Read more about differential updates &lt;a href="https://ubuntu.com/engage/snap-deltas"&gt;here&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;I wanted to install the great DBeaver database tool, but it isn’t in the mainline &lt;code&gt;apt&lt;/code&gt; repos (and I don’t trust third-party apt repos). It &lt;em&gt;is&lt;/em&gt; available as a &lt;a href="https://snapcraft.io/install/dbeaver-ce/ubuntu"&gt;Snap&lt;/a&gt;, though.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;sudo&lt;span class="w"&gt; &lt;/span&gt;snap&lt;span class="w"&gt; &lt;/span&gt;install&lt;span class="w"&gt; &lt;/span&gt;dbeaver-ce
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Now, though … how to run it, as it wasn’t found in my &lt;code&gt;$PATH&lt;/code&gt;.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="nb"&gt;export&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;PATH&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$PATH&lt;/span&gt;:/snap/bin
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;That did the trick!  I added the above to my &lt;code&gt;.zshrc&lt;/code&gt; file also, for persistence.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Feature Image Prompt:&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Generate an image. The aesthetic should be cyberpunk with colors of neon pink, blue and purple. Do not add any people. Picture an ultra-modern, futuristic digital landscape that symbolizes Ubuntu Snaps and WSL integration. Feature a sleek, neon-lit Linux terminal screen with abstract container icons and flowing circuit patterns, interlaced with holographic overlays representing secure and isolated application environments. In the backdrop, depict a sprawling cyberpunk cityscape of glowing data streams and network grids that evoke the modular, robust, and automated world of snap updates—all rendered in vivid neon shades of pink, blue, and purple.&lt;/p&gt;</content><category term="TIL"/><category term="linux"/><category term="wsl"/><category term="zsh"/></entry><entry><title>Running Google Chrome GUI in WSL</title><link href="https://gallon.me/running-google-chrome-gui-in-wsl.html" rel="alternate"/><published>2024-08-25T00:00:00-05:00</published><updated>2024-08-25T00:00:00-05:00</updated><author><name>Corey</name></author><id>tag:gallon.me,2024-08-25:/running-google-chrome-gui-in-wsl.html</id><summary type="html">&lt;p&gt;This was working for me a while ago but today, when I tried, it was not. &lt;/p&gt;</summary><content type="html">&lt;p&gt;This was working for me a while ago but today, when I tried, it was not. &lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;❯&lt;span class="w"&gt; &lt;/span&gt;google-chrome
&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="m"&gt;541540&lt;/span&gt;:541540:0825/114954.251188:ERROR:ozone_platform_x11.cc&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="m"&gt;244&lt;/span&gt;&lt;span class="o"&gt;)]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;Missing&lt;span class="w"&gt; &lt;/span&gt;X&lt;span class="w"&gt; &lt;/span&gt;server&lt;span class="w"&gt; &lt;/span&gt;or&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$DISPLAY&lt;/span&gt;
&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="m"&gt;541540&lt;/span&gt;:541540:0825/114954.251464:ERROR:env.cc&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="m"&gt;258&lt;/span&gt;&lt;span class="o"&gt;)]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;The&lt;span class="w"&gt; &lt;/span&gt;platform&lt;span class="w"&gt; &lt;/span&gt;failed&lt;span class="w"&gt; &lt;/span&gt;to&lt;span class="w"&gt; &lt;/span&gt;initialize.&lt;span class="w"&gt;  &lt;/span&gt;Exiting.
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;There’s a good wiki on this available &lt;a href="https://github.com/microsoft/wslg/wiki/Diagnosing-%22cannot-open-display%22-type-issues-with-WSLg"&gt;here&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The solution was simple and immediate:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="nb"&gt;export&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;DISPLAY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;:0
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Importantly, this doesn’t carry through to a Jupyter notebook. To make this work in Jupyter notebooks, you need to use the &lt;code&gt;%env&lt;/code&gt; magic command.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="n"&gt;env&lt;/span&gt; &lt;span class="n"&gt;DISPLAY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Let me re-read your post carefully to match your voice.Your style is direct and practical — problem statement, error output, brief context, solution, done. No fluff. You show rather than explain, and you trust the reader to follow along. Here's an update that matches:&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="update-december-2025"&gt;Update: December 2025&lt;/h2&gt;
&lt;p&gt;Different failure mode this time. Chrome launched but no window appeared.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;❯ google-chrome
[3365071:3365071:1205/114205.717359:ERROR:dbus/object_proxy.cc:573] Failed to call method: org.freedesktop.DBus.Properties.GetAll: object_path= /org/freedesktop/UPower/devices/DisplayDevice: org.freedesktop.DBus.Error.ServiceUnknown: The name org.freedesktop.UPower was not provided by any .service files
Created TensorFlow Lite XNNPACK delegate for CPU.
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;No X11 error. &lt;code&gt;$DISPLAY&lt;/code&gt; was set correctly. But &lt;code&gt;xeyes&lt;/code&gt; also ran without showing a window. The process was running, just invisible.&lt;/p&gt;
&lt;p&gt;The wiki says &lt;code&gt;/tmp/.X11-unix&lt;/code&gt; should be a symlink to &lt;code&gt;/mnt/wslg/.X11-unix&lt;/code&gt;. Mine was a directory:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;❯ ls -la /tmp/ | grep X11
drwxrwxrwx    - root     24 Nov 12:12 .X11-unix
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Tried to remove it:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;❯ sudo rm -r /tmp/.X11-unix
rm: cannot remove &amp;#39;/tmp/.X11-unix/X0&amp;#39;: Read-only file system
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;A read-only tmpfs was mounted on top:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="err"&gt;❯&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;mount&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;grep&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;X11&lt;/span&gt;
&lt;span class="nx"&gt;none&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;on&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;tmp&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;X11&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;unix&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;type&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;tmpfs&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ro&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nx"&gt;relatime&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The culprit is a race condition between two systemd mechanisms. The &lt;code&gt;xserver-common&lt;/code&gt; package includes &lt;code&gt;/usr/lib/tmpfiles.d/x11.conf&lt;/code&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;D! /tmp/.X11-unix 1777 root root 10d
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;This creates &lt;code&gt;/tmp/.X11-unix&lt;/code&gt; as a directory during &lt;code&gt;systemd-tmpfiles-setup.service&lt;/code&gt;. WSLg's &lt;code&gt;wslg.service&lt;/code&gt; runs after and tries to bind-mount over it, resulting in the broken read-only state. Apps connect to what looks like an X socket but renders nowhere.&lt;/p&gt;
&lt;p&gt;The fix is to override the tmpfiles rule so it creates a symlink instead:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;echo &amp;#39;L+ /tmp/.X11-unix - - - - /mnt/wslg/.X11-unix&amp;#39; | sudo tee /etc/tmpfiles.d/wslg-x11.conf
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Then fix the current state:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;sudo umount /tmp/.X11-unix
sudo rm -r /tmp/.X11-unix
ln -s /mnt/wslg/.X11-unix /tmp/.X11-unix
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;This survives &lt;code&gt;wsl --shutdown&lt;/code&gt;. The &lt;code&gt;L+&lt;/code&gt; directive removes whatever exists and creates a symlink, so tmpfiles and WSLg stop fighting.&lt;/p&gt;
&lt;p&gt;Preserving the above-linked wiki for posterity below&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="diagnosing-cannot-open-display-type-issues-with-wslg"&gt;Diagnosing "cannot open display" type issues with WSLg&lt;/h2&gt;
&lt;p&gt;Steve Pronovost edited this page on May 7, 2021 · &lt;a href="https://github.com/microsoft/wslg/wiki/Diagnosing-%22cannot-open-display%22-type-issues-with-WSLg/_history"&gt;3 revisions&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;On category of issues that we have seen popping up is folks having trouble getting their GUI application to properly connect to WSLg's X server. This page is meant as a quick guide to diagnose this type of connection issue as well as list the currently known problem we're working on fixing.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Verify you are running on Windows build 21364+&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;From a Windows command prompt, type &lt;em&gt;ver&lt;/em&gt; to verify which build you are running.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;E:\wsl&amp;gt;ver

Microsoft Windows [Version 10.0.21367.1000]
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;You must be running on Windows build version 21364+ for WSLg to work. This version of Windows is currently only available through the Windows Insider program. See https://www.microsoft.com/en-us/windowsinsider/ to join the insider program and help us validate pre-released version of Windows.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;DISPLAY environment variable&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;WSLg's X server is running on display 0. The DISPLAY environment variable must have the value :0 for GUI application to connect to the right display. You can verify what the value of your DISPLAY environment variable is per below.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="n"&gt;spronovo&lt;/span&gt;&lt;span class="nv"&gt;@OFFICE&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;&lt;span class="o"&gt;~&lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;echo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="n"&gt;DISPLAY&lt;/span&gt;
&lt;span class="err"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;This environment variable is initialize as part of WSL's INIT. If it is unset or has a value other than :0, than you likely have a profile script that is changing it's value that you'll want to hunt down. You can also reset that environment variable like below.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;DISPLAY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;X11 display socket&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;X servers create their socket under /tmp/.X11-Unix. This directory must exist and must be linked to /mnt/wslg/.X11-Unix where WSLg built-in X server create it's socket. You can verify the mapping exist and is the expected link per below.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="n"&gt;spronovo&lt;/span&gt;&lt;span class="nv"&gt;@OFFICE&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;&lt;span class="o"&gt;~&lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;ls&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;la&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;tmp&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;X11&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;unix&lt;/span&gt;
&lt;span class="n"&gt;lrwxrwxrwx&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;spronovo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;spronovo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;19&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Apr&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;21&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;28&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;tmp&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;X11&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;unix&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;mnt&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;wslg&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;X11&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;unix&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;This link is setup during WSL's INIT. If this directory doesn't exist, something likely caused it be removed in your environment that needs to be tracked down.&lt;/p&gt;
&lt;p&gt;You can re-create the link manually to try things out.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;sudo rm -r /tmp/.X11-unix
ln -s /mnt/wslg/.X11-unix /tmp/.X11-unix
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;X11 server running?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;If the X server is running, you should see an X0 socket&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="n"&gt;spronovo&lt;/span&gt;&lt;span class="nv"&gt;@OFFICE&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;&lt;span class="o"&gt;~&lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;ls&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;tmp&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;X11&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;unix&lt;/span&gt;
&lt;span class="n"&gt;X0&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;If you don't please open an issue and attach /mnt/wslg/weston.log to the bug.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Known issues&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;You can verify the version of WSLg you are running per below:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="n"&gt;spronovo&lt;/span&gt;&lt;span class="nv"&gt;@OFFICE&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;&lt;span class="o"&gt;~&lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;cat&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;mnt&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;wslg&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;versions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;txt&lt;/span&gt;
&lt;span class="n"&gt;WSLg&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;x86_64&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;1.0.17&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="mf"&gt;3.&lt;/span&gt;&lt;span class="n"&gt;Branch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;master&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Sha&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;a526dfd5ad03d126bb2d8c528f6c3563e86a40da&lt;/span&gt;
&lt;span class="nl"&gt;Mariner&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;VERSION&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="ss"&gt;&amp;quot;1.0.20210224&amp;quot;&lt;/span&gt;
&lt;span class="nl"&gt;FreeRDP&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;e4a2fc2053bd8c5f99455fcd08ffee7e5591567a&lt;/span&gt;
&lt;span class="nl"&gt;weston&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;fd961f5cd116c9358d82ce94d139c1578e21bd00&lt;/span&gt;
&lt;span class="nl"&gt;pulseaudio&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="n"&gt;f0f0b8c3872780f15e275fc12899f4564f01bd5&lt;/span&gt;
&lt;span class="nl"&gt;mesa&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Complex monitor arrangement (Fixed in WSLg 1.0.19)&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;There is a known issue in WSLg 1.0.17 that if you have a combination of vertically and horizontally aligned monitor, Weston may hit an invalid assert and restart. Effectively crashing and restarting the X server on every connection attempt.&lt;/p&gt;
&lt;p&gt;You can verify if this is what you are hitting per below&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;cat /mnt/wslg/weston.log | grep isConnected_V
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;if you see something like&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;weston: ../libweston/backend-rdp/rdpdisp.c:481: disp_monitor_validate_and_compute_layout: Assertion `isConnected_V == true&amp;#39; failed.
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Then you are hitting this problem. The workaround at the moment is to stack all of your monitor either vertically, or horizontally, but not use a mix of both.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Setting /tmp in /etc/fstab&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;There is a known issue at the moment (https://github.com/microsoft/wslg/issues/43) where configuring /tmp in /etc/fstab will overwrite the /tmp/.X11-unix link previously described. The workaround at the moment is to either avoid configuring /tmp, or manually recreating the link&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;ln -s /mnt/wslg/.X11-unix /tmp/.X11-unix
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Still having a problem?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Please open an issue and include the following&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Run the following command and provide the output:&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="n"&gt;spronovo&lt;/span&gt;&lt;span class="nv"&gt;@OFFICE&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;&lt;span class="o"&gt;~&lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;cat&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;mnt&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;wslg&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;versions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;txt&lt;/span&gt;
&lt;span class="n"&gt;WSLg&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;x86_64&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;current&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="nl"&gt;Mariner&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;VERSION&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="ss"&gt;&amp;quot;1.0.20210224&amp;quot;&lt;/span&gt;
&lt;span class="nl"&gt;FreeRDP&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="n"&gt;f083fa0b97d433d6204985f6047886e29c1c61e&lt;/span&gt;
&lt;span class="nl"&gt;weston&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="n"&gt;de531f00aa3dfd17e0de74c8f49e9fd7cec617&lt;/span&gt;
&lt;span class="nl"&gt;pulseaudio&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="n"&gt;f0f0b8c3872780f15e275fc12899f4564f01bd5&lt;/span&gt;
&lt;span class="nl"&gt;mesa&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="n"&gt;ad0684038f5732f7e4bd1a391ec9d833685fb48&lt;/span&gt;

&lt;span class="n"&gt;spronovo&lt;/span&gt;&lt;span class="nv"&gt;@OFFICE&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;&lt;span class="o"&gt;~&lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;echo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="n"&gt;DISPLAY&lt;/span&gt;
&lt;span class="err"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;

&lt;span class="n"&gt;spronovo&lt;/span&gt;&lt;span class="nv"&gt;@OFFICE&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;&lt;span class="o"&gt;~&lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;ls&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;la&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;tmp&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;X11&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;unix&lt;/span&gt;
&lt;span class="n"&gt;lrwxrwxrwx&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;root&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;root&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;19&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Apr&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;21&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;tmp&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;X11&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;unix&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;mnt&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;wslg&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;X11&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;unix&lt;/span&gt;

&lt;span class="n"&gt;spronovo&lt;/span&gt;&lt;span class="nv"&gt;@OFFICE&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;&lt;span class="o"&gt;~&lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;ls&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;la&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;tmp&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;X11&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;unix&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;
&lt;span class="n"&gt;total&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;
&lt;span class="n"&gt;drwxrwxrwx&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;root&lt;/span&gt;&lt;span class="w"&gt;     &lt;/span&gt;&lt;span class="n"&gt;root&lt;/span&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Apr&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;21&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;22&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;
&lt;span class="n"&gt;drwxrwxrwt&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;root&lt;/span&gt;&lt;span class="w"&gt;     &lt;/span&gt;&lt;span class="n"&gt;root&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="mi"&gt;220&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Apr&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;21&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;22&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;..&lt;/span&gt;
&lt;span class="n"&gt;srwxrwxrwx&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;spronovo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;users&lt;/span&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Apr&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;21&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;22&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;X0&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;ul&gt;
&lt;li&gt;Attach your /mnt/wslg/weston.log file&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Feature Image Prompt:&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Generate an image. The aesthetic should be cyberpunk with colors of neon pink, blue and purple. Do not add any people. Imagine a futuristic, shadowy workstation floating in a dark, neon-drenched room. A translucent, holographic Linux terminal displays cryptic code and error messages in glowing neon, casting a blue and purple light across sleek chrome interfaces and digital circuitry that pulses like living art. Abstract, geometric symbols of connectivity and digital mystery intertwine with glitch-effect overlays, evoking the enigmatic energy of a high-tech realm where advanced WSL environments and futuristic software merge into one surreal, incandescent scene.&lt;/p&gt;</content><category term="TIL"/><category term="linux"/><category term="wsl"/></entry><entry><title>Comments are Failures in Coding</title><link href="https://gallon.me/comments-are-failures-in-coding.html" rel="alternate"/><published>2024-08-24T00:00:00-05:00</published><updated>2024-08-24T00:00:00-05:00</updated><author><name>Corey</name></author><id>tag:gallon.me,2024-08-24:/comments-are-failures-in-coding.html</id><summary type="html">&lt;p&gt;“The proper use of comments is to compensate for our failure to express ourselves in code.” — Robert C. Martin, Clean Code&lt;/p&gt;</summary><content type="html">&lt;blockquote&gt;
&lt;p&gt;“The proper use of comments is to compensate for our failure to express ourselves in code.” — Robert C. Martin, Clean Code&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Every time you express yourself in code, you should pat yourself on the back. &lt;/p&gt;
&lt;p&gt;Every time you write a comment, you should grimace and feel the failure of your ability of expression.&lt;/p&gt;
&lt;p&gt;Find a way to express your intent in the code, directly.  The only truly good comment, is the comment you found a way &lt;em&gt;not&lt;/em&gt; &lt;em&gt;to write&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;Truth can only be found in one place — the code.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Feature Image Prompt:&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Generate an image. The aesthetic should be cyberpunk with colors of neon pink, blue and purple. Do not add any people. Incorporate a futuristic, digital landscape filled with glowing code snippets, circuit board patterns, and abstract text fragments resembling failed comments in programming. Let the environment evoke the feeling of a cityscape made of luminous data streams and neon-lit architecture, reflecting the idea that true expression lies in the pure essence of code rather than in the annotations.&lt;/p&gt;</content><category term="TIL"/><category term="programming"/></entry><entry><title>Launching a Web App from Stream Deck</title><link href="https://gallon.me/launching-a-web-app-from-stream-deck.html" rel="alternate"/><published>2024-08-24T00:00:00-05:00</published><updated>2024-08-24T00:00:00-05:00</updated><author><name>Corey</name></author><id>tag:gallon.me,2024-08-24:/launching-a-web-app-from-stream-deck.html</id><summary type="html">&lt;p&gt;This is actually much  more of a hassle than I’d have expected …&lt;/p&gt;</summary><content type="html">&lt;p&gt;This is actually much  more of a hassle than I’d have expected …&lt;/p&gt;
&lt;p&gt;The trick, in my case, is that I have a virtual desktop called &lt;code&gt;Media&lt;/code&gt; and, on it, an always open Microsoft Edge browser window also called &lt;code&gt;Media&lt;/code&gt; in which I keep the &lt;a href="https://www.brain.fm/"&gt;Brain.fm&lt;/a&gt; web player open.  I want to be able to press a button on the Stream Deck and play or pause Brain.fm from anywhere.&lt;/p&gt;
&lt;p&gt;I ended up writing a PowerShell script to accomplish this, using Claude to help. Claude did a relatively terrible job prompting him regularly. The way I ultimately got there quickly was to borrow from Manuel Odendahl’s brilliant suggestions (&lt;a href="https://www.youtube.com/watch?v=zwItokY087U"&gt;video&lt;/a&gt; and &lt;a href="https://github.com/go-go-golems/go-go-workshop/blob/main/2024-06-24%20-%20Workshop%20AI%20Programmer%20Handout.pdf"&gt;PDF handout&lt;/a&gt;) of having the model create a &lt;a href="https://en.wikipedia.org/wiki/Domain-specific_language"&gt;Domain Specific Language (DSL)&lt;/a&gt; for the tasks at hand, and then implement specific parts of the DSL. &lt;/p&gt;
&lt;p&gt;The resulting utility is &lt;a href="https://github.com/captivus/utilities-play_brainfm"&gt;available on GitHub here&lt;/a&gt;. This was, at first, a hassle but then really effing cool to see how much better the model does with the DSL!  h/t &lt;a href="https://the.scapegoat.dev/"&gt;Manuel&lt;/a&gt;!&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="cm"&gt;&amp;lt;#&lt;/span&gt;
&lt;span class="sd"&gt;.SYNOPSIS&lt;/span&gt;
&lt;span class="cm"&gt;Automates playing Brain.fm in Microsoft Edge on a specific virtual desktop.&lt;/span&gt;

&lt;span class="sd"&gt;.DESCRIPTION&lt;/span&gt;
&lt;span class="cm"&gt;This script navigates to a named virtual desktop, focuses on Microsoft Edge,&lt;/span&gt;
&lt;span class="cm"&gt;switches to the Brain.fm tab, and starts playback. It uses the VirtualDesktop&lt;/span&gt;
&lt;span class="cm"&gt;module and Windows API calls for desktop and window management.&lt;/span&gt;

&lt;span class="sd"&gt;.NOTES&lt;/span&gt;
&lt;span class="cm"&gt;Requires the VirtualDesktop module to be installed.&lt;/span&gt;
&lt;span class="cm"&gt;Feature_Image: ./images/feature_images/20240824_-_Launching_a_Web_App_from_Stream_Deck_3.png&lt;/span&gt;

&lt;span class="cm"&gt;#&amp;gt;&lt;/span&gt;

&lt;span class="c"&gt;# Import the VirtualDesktop module&lt;/span&gt;
&lt;span class="c"&gt;# Must be installed as admin by running &amp;quot;Install-Module VirtualDesktop -Scope CurrentUser&amp;quot;&lt;/span&gt;
&lt;span class="nb"&gt;Import-Module&lt;/span&gt; &lt;span class="n"&gt;VirtualDesktop&lt;/span&gt;

&lt;span class="nb"&gt;Add-Type&lt;/span&gt; &lt;span class="n"&gt;-AssemblyName&lt;/span&gt; &lt;span class="n"&gt;System&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Windows&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Forms&lt;/span&gt;

&lt;span class="c"&gt;# Add Windows API function declarations for window management&lt;/span&gt;
&lt;span class="nb"&gt;Add-Type&lt;/span&gt; &lt;span class="sh"&gt;@&amp;quot;&lt;/span&gt;
&lt;span class="sh"&gt;using System;&lt;/span&gt;
&lt;span class="sh"&gt;using System.Runtime.InteropServices;&lt;/span&gt;

&lt;span class="sh"&gt;public class User32 {&lt;/span&gt;
&lt;span class="sh"&gt;    [DllImport(&amp;quot;user32.dll&amp;quot;)]&lt;/span&gt;
&lt;span class="sh"&gt;    public static extern bool ShowWindow(IntPtr hWnd, int nCmdShow);&lt;/span&gt;

&lt;span class="sh"&gt;    [DllImport(&amp;quot;user32.dll&amp;quot;)]&lt;/span&gt;
&lt;span class="sh"&gt;    [return: MarshalAs(UnmanagedType.Bool)]&lt;/span&gt;
&lt;span class="sh"&gt;    public static extern bool SetForegroundWindow(IntPtr hWnd);&lt;/span&gt;
&lt;span class="sh"&gt;}&lt;/span&gt;
&lt;span class="sh"&gt;&amp;quot;@&lt;/span&gt;

&lt;span class="cm"&gt;&amp;lt;#&lt;/span&gt;
&lt;span class="sd"&gt;.SYNOPSIS&lt;/span&gt;
&lt;span class="cm"&gt;Switches to a named virtual desktop.&lt;/span&gt;

&lt;span class="sd"&gt;.PARAMETER&lt;/span&gt;&lt;span class="cm"&gt; name&lt;/span&gt;
&lt;span class="cm"&gt;The name of the virtual desktop to switch to.&lt;/span&gt;

&lt;span class="sd"&gt;.EXAMPLE&lt;/span&gt;
&lt;span class="cm"&gt;Switch-ToNamedDesktop -name &amp;quot;Media&amp;quot;&lt;/span&gt;
&lt;span class="cm"&gt;#&amp;gt;&lt;/span&gt;
&lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="k"&gt;Switch&lt;/span&gt;&lt;span class="n"&gt;-ToNamedDesktop&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;param&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;[string]&lt;/span&gt;&lt;span class="nv"&gt;$name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nv"&gt;$desktops&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Get-DesktopList&lt;/span&gt;
    &lt;span class="nv"&gt;$targetDesktop&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$desktops&lt;/span&gt; &lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="nb"&gt;Where-Object&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;$_&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Name&lt;/span&gt; &lt;span class="o"&gt;-eq&lt;/span&gt; &lt;span class="nv"&gt;$name&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$targetDesktop&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;Switch&lt;/span&gt;&lt;span class="n"&gt;-Desktop&lt;/span&gt; &lt;span class="n"&gt;-Desktop&lt;/span&gt; &lt;span class="nv"&gt;$targetDesktop&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Number&lt;/span&gt;
            &lt;span class="nb"&gt;Write-Host&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;Switched to virtual desktop: $name&amp;quot;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nb"&gt;Write-Host&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;Error switching to desktop: $_&amp;quot;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nb"&gt;Write-Host&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;Virtual desktop &amp;#39;$name&amp;#39; not found.&amp;quot;&lt;/span&gt;
        &lt;span class="nb"&gt;Write-Host&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;Available desktops: &lt;/span&gt;&lt;span class="p"&gt;$(&lt;/span&gt;&lt;span class="nv"&gt;$desktops&lt;/span&gt; &lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="k"&gt;ForEach&lt;/span&gt;&lt;span class="n"&gt;-Object&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;$_&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Name&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="nb"&gt;Join-String&lt;/span&gt; &lt;span class="n"&gt;-Separator&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;, &amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="cm"&gt;&amp;lt;#&lt;/span&gt;
&lt;span class="sd"&gt;.SYNOPSIS&lt;/span&gt;
&lt;span class="cm"&gt;Sets focus on an application window.&lt;/span&gt;

&lt;span class="sd"&gt;.PARAMETER&lt;/span&gt;&lt;span class="cm"&gt; name&lt;/span&gt;
&lt;span class="cm"&gt;The name of the application to focus on.&lt;/span&gt;

&lt;span class="sd"&gt;.EXAMPLE&lt;/span&gt;
&lt;span class="cm"&gt;Set-ApplicationFocus -name &amp;quot;Edge&amp;quot;&lt;/span&gt;
&lt;span class="cm"&gt;#&amp;gt;&lt;/span&gt;
&lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="nb"&gt;Set-ApplicationFocus&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;param&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;[string]&lt;/span&gt;&lt;span class="nv"&gt;$name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nv"&gt;$processes&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Get-Process&lt;/span&gt; &lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="nb"&gt;Where-Object&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;$_&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;MainWindowTitle&lt;/span&gt; &lt;span class="o"&gt;-ne&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;&amp;quot;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="nb"&gt;Write-Host&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;Searching for application with name containing &amp;#39;$name&amp;#39;&amp;quot;&lt;/span&gt;
    &lt;span class="nb"&gt;Write-Host&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;Running processes with non-empty window titles:&amp;quot;&lt;/span&gt;
    &lt;span class="nv"&gt;$processes&lt;/span&gt; &lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="k"&gt;ForEach&lt;/span&gt;&lt;span class="n"&gt;-Object&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nb"&gt;Write-Host&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;  - &lt;/span&gt;&lt;span class="p"&gt;$(&lt;/span&gt;&lt;span class="nv"&gt;$_&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ProcessName&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;: &lt;/span&gt;&lt;span class="p"&gt;$(&lt;/span&gt;&lt;span class="nv"&gt;$_&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;MainWindowTitle&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="nv"&gt;$app&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$processes&lt;/span&gt; &lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="nb"&gt;Where-Object&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;$_&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ProcessName&lt;/span&gt; &lt;span class="o"&gt;-like&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;*$name*&amp;quot;&lt;/span&gt; &lt;span class="o"&gt;-or&lt;/span&gt; &lt;span class="nv"&gt;$_&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;MainWindowTitle&lt;/span&gt; &lt;span class="o"&gt;-like&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;*$name*&amp;quot;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="nb"&gt;Select-Object&lt;/span&gt; &lt;span class="n"&gt;-First&lt;/span&gt; &lt;span class="n"&gt;1&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$app&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="no"&gt;[User32]&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;ShowWindow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;MainWindowHandle&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;9&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c"&gt;# 9 = SW_RESTORE&lt;/span&gt;
        &lt;span class="no"&gt;[User32]&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;SetForegroundWindow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;MainWindowHandle&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nb"&gt;Write-Host&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;Set focus on application: &lt;/span&gt;&lt;span class="p"&gt;$(&lt;/span&gt;&lt;span class="nv"&gt;$app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ProcessName&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="s2"&gt; (Window Title: &lt;/span&gt;&lt;span class="p"&gt;$(&lt;/span&gt;&lt;span class="nv"&gt;$app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;MainWindowTitle&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;)&amp;quot;&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$true&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nb"&gt;Write-Host&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;Application with name containing &amp;#39;$name&amp;#39; not found.&amp;quot;&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$false&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="cm"&gt;&amp;lt;#&lt;/span&gt;
&lt;span class="sd"&gt;.SYNOPSIS&lt;/span&gt;
&lt;span class="cm"&gt;Switches to a specific tab in Microsoft Edge.&lt;/span&gt;

&lt;span class="sd"&gt;.PARAMETER&lt;/span&gt;&lt;span class="cm"&gt; tabTitle&lt;/span&gt;
&lt;span class="cm"&gt;The title of the tab to switch to.&lt;/span&gt;

&lt;span class="sd"&gt;.EXAMPLE&lt;/span&gt;
&lt;span class="cm"&gt;Switch-EdgeTab -tabTitle &amp;quot;brain.fm&amp;quot;&lt;/span&gt;
&lt;span class="cm"&gt;#&amp;gt;&lt;/span&gt;
&lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="k"&gt;Switch&lt;/span&gt;&lt;span class="n"&gt;-EdgeTab&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;param&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="no"&gt;[string]&lt;/span&gt;&lt;span class="nv"&gt;$tabTitle&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nv"&gt;$edge&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Get-Process&lt;/span&gt; &lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="nb"&gt;Where-Object&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;$_&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ProcessName&lt;/span&gt; &lt;span class="o"&gt;-eq&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;msedge&amp;quot;&lt;/span&gt; &lt;span class="o"&gt;-and&lt;/span&gt; &lt;span class="nv"&gt;$_&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;MainWindowTitle&lt;/span&gt; &lt;span class="o"&gt;-ne&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;&amp;quot;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="nb"&gt;Select-Object&lt;/span&gt; &lt;span class="n"&gt;-First&lt;/span&gt; &lt;span class="n"&gt;1&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$edge&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c"&gt;# Activate the Edge window&lt;/span&gt;
        &lt;span class="no"&gt;[User32]&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;ShowWindow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$edge&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;MainWindowHandle&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;9&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c"&gt;# 9 = SW_RESTORE&lt;/span&gt;
        &lt;span class="no"&gt;[User32]&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;SetForegroundWindow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$edge&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;MainWindowHandle&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nb"&gt;Write-Host&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;Activated Edge window: &lt;/span&gt;&lt;span class="p"&gt;$(&lt;/span&gt;&lt;span class="nv"&gt;$edge&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;MainWindowTitle&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;

        &lt;span class="c"&gt;# Use correct keyboard shortcut to search tabs&lt;/span&gt;
        &lt;span class="nv"&gt;$wshell&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;New-Object&lt;/span&gt; &lt;span class="n"&gt;-ComObject&lt;/span&gt; &lt;span class="n"&gt;wscript&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;shell&lt;/span&gt;
        &lt;span class="nv"&gt;$wshell&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SendKeys&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;^+a&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c"&gt;# Ctrl+Shift+A to open tab search&lt;/span&gt;
        &lt;span class="nb"&gt;Start-Sleep&lt;/span&gt; &lt;span class="n"&gt;-Milliseconds&lt;/span&gt; &lt;span class="n"&gt;500&lt;/span&gt;  &lt;span class="c"&gt;# Wait for search to open&lt;/span&gt;
        &lt;span class="nv"&gt;$wshell&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SendKeys&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$tabTitle&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nb"&gt;Start-Sleep&lt;/span&gt; &lt;span class="n"&gt;-Milliseconds&lt;/span&gt; &lt;span class="n"&gt;500&lt;/span&gt;  &lt;span class="c"&gt;# Wait for search results&lt;/span&gt;
        &lt;span class="nv"&gt;$wshell&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SendKeys&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;{ENTER}&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="nb"&gt;Write-Host&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;Attempted to switch to tab &amp;#39;$tabTitle&amp;#39; in Edge&amp;quot;&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$true&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nb"&gt;Write-Host&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;No Edge window with a non-empty title found.&amp;quot;&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$false&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c"&gt;# Main execution&lt;/span&gt;
&lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c"&gt;# Switch to the &amp;quot;Media&amp;quot; virtual desktop&lt;/span&gt;
    &lt;span class="k"&gt;Switch&lt;/span&gt;&lt;span class="n"&gt;-ToNamedDesktop&lt;/span&gt; &lt;span class="n"&gt;-name&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;Media&amp;quot;&lt;/span&gt;

    &lt;span class="c"&gt;# Set focus on the Edge browser window&lt;/span&gt;
    &lt;span class="nv"&gt;$focusSuccess&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Set-ApplicationFocus&lt;/span&gt; &lt;span class="n"&gt;-name&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;Media&amp;quot;&lt;/span&gt;

    &lt;span class="c"&gt;# Only proceed if focus was successful&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$focusSuccess&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c"&gt;# Switch to the tab with brain.fm&lt;/span&gt;
        &lt;span class="nv"&gt;$switchSuccess&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;Switch&lt;/span&gt;&lt;span class="n"&gt;-EdgeTab&lt;/span&gt; &lt;span class="n"&gt;-tabTitle&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;brain.fm&amp;quot;&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$switchSuccess&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nb"&gt;Write-Host&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;Navigation completed successfully.&amp;quot;&lt;/span&gt;
            &lt;span class="c"&gt;# Wait for 0.5 seconds&lt;/span&gt;
            &lt;span class="nb"&gt;Start-Sleep&lt;/span&gt; &lt;span class="n"&gt;-Milliseconds&lt;/span&gt; &lt;span class="n"&gt;500&lt;/span&gt;
            &lt;span class="c"&gt;# Send a space key press to start playing Brain.fm&lt;/span&gt;
            &lt;span class="no"&gt;[System.Windows.Forms.SendKeys]&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;SendWait&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39; &amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nb"&gt;Write-Host&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;Navigation partially completed. Failed to switch to the desired tab.&amp;quot;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nb"&gt;Write-Host&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;Navigation failed. Could not focus on Microsoft Edge.&amp;quot;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nb"&gt;Write-Host&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;An error occurred: $_&amp;quot;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Feature Image Prompt:&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Generate an image. The aesthetic should be cyberpunk with colors of neon pink, blue and purple. Do not add any people. Envision a futuristic digital control nexus where sleek, high-tech interfaces merge with holographic displays that float against a dark, reflective backdrop. The scene is filled with dynamic arrays of abstract code, streaming data ribbons, and luminescent virtual desktops that evoke a sophisticated automation environment reminiscent of a Stream Deck’s command center. Intricate neon circuitry and shimmering panels pulse with energy, creating an atmosphere of digital mastery and modern mystique.&lt;/p&gt;</content><category term="TIL"/><category term="utilities"/><category term="python"/><category term="productivity"/></entry><entry><title>Programs Do Things</title><link href="https://gallon.me/programs-do-things.html" rel="alternate"/><published>2024-08-24T00:00:00-05:00</published><updated>2024-08-24T00:00:00-05:00</updated><author><name>Corey</name></author><id>tag:gallon.me,2024-08-24:/programs-do-things.html</id><summary type="html">&lt;p&gt;“Every system is built from a domain specific language designed by the programmers to describe that system. Functions are the verbs of that language, and classes are the nouns.”  — Robert C. Martin, Clean Code.&lt;/p&gt;</summary><content type="html">&lt;h1 id="programs-do-things"&gt;Programs Do Things&lt;/h1&gt;
&lt;blockquote&gt;
&lt;p&gt;“Every system is built from a domain specific language designed by the programmers to describe that system. Functions are the verbs of that language, and classes are the nouns.”  — Robert C. Martin, Clean Code.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img alt="Programs Do Things" src="./images/20240824_programs_do_things.png"&gt;&lt;/p&gt;
&lt;p&gt;Functions are the verbs of your programs. They “do” work.&lt;/p&gt;
&lt;p&gt;Classes are the nouns of your programs. They are the “things” that functions do work on.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Feature Image Prompt:&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Generate an image. The aesthetic should be cyberpunk with colors of neon pink, blue and purple. Do not add any people. Imagine a dark, futuristic digital landscape where abstract, glowing circuit patterns and symbolic nodes form a sprawling network. In this environment, bursts of neon energy represent dynamic functions—energetic, vivid flashes that trigger action—while structured, luminous blocks symbolize classes, solid and foundational elements that organize the chaos. The composition should evoke a high-tech, cybernetic world where programming languages transform into vibrant, mesmerizing visuals, reminiscent of a digital metropolis pulsing with life.&lt;/p&gt;</content><category term="TIL"/><category term="programming"/></entry></feed>