<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xml:base="https://bjorn.now/" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>bjorn.now</title><link>https://bjorn.now/</link><description>Recent content on bjorn.now</description><generator>Hugo</generator><language>en-us</language><lastBuildDate>Fri, 03 Apr 2026 13:17:06 +0200</lastBuildDate><atom:link href="https://bjorn.now/feed.xml" rel="self" type="application/rss+xml"/><item><title>How to recover from a corrupted dolt journal</title><link>https://bjorn.now/til/2026-04-03-how-to-recover-from-a-corrupted-dolt-journal/?utm_medium=feed</link><pubDate>Fri, 03 Apr 2026 13:17:06 +0200</pubDate><guid>https://bjorn.now/til/2026-04-03-how-to-recover-from-a-corrupted-dolt-journal/</guid><description>When dolt’s journal is corrupted, dolt fsck --revive-journal-with-data-loss will truncate it back to the last valid record.
I found this error message when starting dolt up after upgrading the server: invalid journal record at offset 5831169: invalid journal record: CRC checksum does not match.
To recover, go to your dolt DB:
cd ~/.beads/shared-server/dolt/&lt;db&gt; \ &amp;&amp; dolt fsck --revive-journal-with-data-loss It backs up the journal, truncates to the point it failed, and makes it possible to start the server again. In my case the db came back up with data loss. I did a bd dolt pull and it pulled in what I had pushed most recently, so nothing major was lost.</description><content:encoded><![CDATA[<p>When dolt&rsquo;s journal is corrupted, <code>dolt fsck --revive-journal-with-data-loss</code> will truncate it back to the last valid record.</p>
<p>I found this error message when starting dolt up after upgrading the server: <code>invalid journal record at offset 5831169: invalid journal record: CRC checksum does not match</code>.</p>
<p>To recover, go to your dolt DB:</p>
<div class=" copy-enabled">
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="nb">cd</span> ~/.beads/shared-server/dolt/&lt;db&gt; <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="se"></span>  <span class="o">&amp;&amp;</span> dolt fsck --revive-journal-with-data-loss</span></span></code></pre></div>
</div>
<p>It backs up the journal, truncates to the point it failed, and makes it possible to start the server again. In my case the db came back up with data loss. I did a <code>bd dolt pull</code> and it pulled in what I had pushed most recently, so nothing major was lost.</p>
<p>I&rsquo;m guessing this happened because installing beads 1.0 also upgraded dolt. If you&rsquo;re upgrading, it&rsquo;s worth making sure you&rsquo;ve done <code>bd vc commit &amp;&amp; bd dolt push</code> in all repos beforehand. That means <a href="https://bjorn.now/til/2026-03-20-configure-beads-dolt-to-share-via-git/">configuring a git remote</a> first.</p>
]]></content:encoded></item><item><title>How to find which dolt databases need to be committed</title><link>https://bjorn.now/til/2026-03-26-how-to-find-which-dolt-databases-need-to-be-committed/?utm_medium=feed</link><pubDate>Thu, 26 Mar 2026 15:32:42 +0100</pubDate><guid>https://bjorn.now/til/2026-03-26-how-to-find-which-dolt-databases-need-to-be-committed/</guid><description>dolt’s shared server won’t pull until all databases on the server have committed changes, not just the beads project you’re pulling.
You’ll get Error 1105 (HY000): cannot merge with uncommitted changes and it’s confusing when the beads project you’re working in has autocommit on. The uncommitted changes are in a different project that’s also using the shared server.
The fix is to go through your other projects and commit their changes (or enable autocommit by adding dolt.auto-commit: "on" to .beads/config.yml). To find which databases have uncommitted changes:</description><content:encoded><![CDATA[<p>dolt&rsquo;s shared server won&rsquo;t pull until all databases on the server have committed changes, not just the beads project you&rsquo;re pulling.</p>
<p>You&rsquo;ll get <code>Error 1105 (HY000): cannot merge with uncommitted changes</code> and it&rsquo;s confusing when the beads project you&rsquo;re working in has autocommit on. The uncommitted changes are in a different project that&rsquo;s also using the shared server.</p>
<p>The fix is to go through your other projects and commit their changes (or enable autocommit by adding <code>dolt.auto-commit: &quot;on&quot;</code> to <code>.beads/config.yml</code>). To find which databases have uncommitted changes:</p>
<div class=" copy-enabled">
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="nv">query</span><span class="o">=</span><span class="k">$(</span>bd sql <span class="s2">&#34;SHOW DATABASES&#34;</span> 2&gt;/dev/null <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="se"></span>  <span class="p">|</span> grep -v -E <span class="s2">&#34;^(Database|--|\(|dolt|information_schema|mysql)&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="se"></span>  <span class="p">|</span> <span class="k">while</span> <span class="nb">read</span> db<span class="p">;</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">      <span class="nb">echo</span> <span class="s2">&#34;SELECT &#39;</span><span class="nv">$db</span><span class="s2">&#39; as db, table_name, status FROM \`</span><span class="nv">$db</span><span class="s2">\`.dolt_status WHERE table_name != &#39;config&#39;&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="k">done</span> <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="se"></span>  <span class="p">|</span> paste -sd<span class="s1">&#39;|&#39;</span> - <span class="p">|</span> sed <span class="s1">&#39;s/|/ UNION ALL /g&#39;</span><span class="k">)</span>
</span></span><span class="line"><span class="cl">bd sql <span class="s2">&#34;</span><span class="nv">$query</span><span class="s2">&#34;</span></span></span></code></pre></div>
</div>
<p>For each project that shows up, go in and run <code>bd vc commit -m 'Snapshot'</code>, and then you can pull again.</p>
<p>We&rsquo;re ignoring changes to the <code>config</code> table because they don&rsquo;t seem to matter for pulling.</p>
]]></content:encoded></item><item><title>How to manually restore a dolt db from its git remote</title><link>https://bjorn.now/til/2026-03-23-how-to-manually-restore-a-dolt-db-from-its-git-remote/?utm_medium=feed</link><pubDate>Mon, 23 Mar 2026 16:02:49 +0100</pubDate><guid>https://bjorn.now/til/2026-03-23-how-to-manually-restore-a-dolt-db-from-its-git-remote/</guid><description>You can manually clone a dolt database from its git remote when bd bootstrap or bd init won’t cooperate.
bd dolt stop: stop the server in the git repo you’re restoring so you can operate on its database cd ~/.beads/shared-server/dolt: go to the shared dolt server (it’s .beads/dolt/beads otherwise, I think) rm -rf &lt;prefix&gt;: remove the DB if it exists already dolt clone git+ssh://git@github.com/user/repo.git &lt;prefix&gt;: clone it to your prefix cd - &amp;&amp; bd ready: see if you can see the listing I don’t know why my local just refused with bd bootstrap but this allowed me to fix it manually.</description><content:encoded><![CDATA[<p>You can manually clone a dolt database from its git remote when <code>bd bootstrap</code> or <code>bd init</code> won&rsquo;t cooperate.</p>
<ol>
<li><code>bd dolt stop</code>: stop the server in the git repo you&rsquo;re restoring so you can operate on its database</li>
<li><code>cd ~/.beads/shared-server/dolt</code>: go to the shared dolt server (it&rsquo;s <code>.beads/dolt/beads</code> otherwise, I think)</li>
<li><code>rm -rf &lt;prefix&gt;</code>: remove the DB if it exists already</li>
<li><code>dolt clone git+ssh://git@github.com/user/repo.git &lt;prefix&gt;</code>: clone it to your prefix</li>
<li><code>cd - &amp;&amp; bd ready</code>: see if you can see the listing</li>
</ol>
<p>I don&rsquo;t know why my local just refused with <code>bd bootstrap</code> but this allowed me to fix it manually.</p>
]]></content:encoded></item><item><link>https://bjorn.now/scrap/2026-03-23t154848/?utm_medium=feed</link><pubDate>Mon, 23 Mar 2026 15:48:48 +0100</pubDate><guid>https://bjorn.now/scrap/2026-03-23t154848/</guid><description>sometimes on windows I can’t delete files, even in an administrator terminal, so I start WSL and rm -rf it. there’s no questions. 🤷</description><content:encoded><![CDATA[<p>sometimes on windows I can&rsquo;t delete files, even in an administrator terminal, so I start WSL and <code>rm -rf</code> it. there&rsquo;s no questions. 🤷</p>
]]></content:encoded></item><item><link>https://bjorn.now/scrap/2026-03-23t111623/?utm_medium=feed</link><pubDate>Mon, 23 Mar 2026 11:16:23 +0100</pubDate><guid>https://bjorn.now/scrap/2026-03-23t111623/</guid><description>Migrating an old beads backup to 0.62.0 I got a wall of Warning: failed to restore issues row: Error 1054 (HY000): Unknown column 'hook_bead' in 'issues'. Made a quick script to fix up the backup format: https://gist.github.com/gaqzi/c2d760627daceffa1f062f93244c3e8e</description><content:encoded><![CDATA[<p>Migrating an old beads backup to 0.62.0 I got a wall of <code>Warning: failed to restore issues row: Error 1054 (HY000): Unknown column 'hook_bead' in 'issues'</code>. Made a quick script to fix up the backup format: <a href="https://gist.github.com/gaqzi/c2d760627daceffa1f062f93244c3e8e">https://gist.github.com/gaqzi/c2d760627daceffa1f062f93244c3e8e</a></p>
]]></content:encoded></item><item><title>It's beginning to crumble — The first steps of my breadcrumb tool</title><link>https://bjorn.now/devlog/2026-03-22-its-beginning-to-crumble/?utm_medium=feed</link><pubDate>Sun, 22 Mar 2026 23:31:31 +0100</pubDate><guid>https://bjorn.now/devlog/2026-03-22-its-beginning-to-crumble/</guid><description>I had a breakthrough while working on bjornstack that made me realize how I could make an MVP of breadcrumb, the tool I’ve wanted to build for ~18 months.
So anyway, bjornstack is my opinionated take on how to write code, aimed at constraining and guiding genies to get their work done. And while working on it, like I have been whenever I’m working with genies, I got thoughts that I wanted to explore later, or ideas for other things I’m working on. I realized that since I started using beads capturing in this project has gotten easier, while chatting I just mention we need “a new bead about x because y provide all the context we have here” and then there’d be a new bead linked to the work we were doing, and I can just pick it up later.
But as soon as it was something for another project, or even just “I need to remember to pack the charger when I leave the office today,” then I had to break out to capture the thought, and blow my stack and lose my flow. And if I don’t capture it, I lose the thought.</description><content:encoded><![CDATA[<p>I had a breakthrough while working on <a href="https://github.com/gaqzi/bjornstack">bjornstack</a> that made me realize how I could make an MVP of breadcrumb, the tool I&rsquo;ve wanted to build for ~18 months.</p>
<p>So anyway, bjornstack is my opinionated take on how to write code, aimed at constraining and guiding genies to get their work done. And while working on it, like I have been whenever I&rsquo;m working with genies, I got thoughts that I wanted to explore later, or ideas for other things I&rsquo;m working on. I realized that since I started using beads capturing <em>in this project</em> has gotten easier, while chatting I just mention we need &ldquo;a new bead about x because y provide all the context we have here&rdquo; and then there&rsquo;d be a new bead linked to the work we were doing, and I can just pick it up later.</p>
<p>But as soon as it was something for another project, or even just &ldquo;I need to remember to pack the charger when I leave the office today,&rdquo; then I had to break out to capture the thought, and blow my stack and lose my flow.
And if I don&rsquo;t capture it, I lose the thought.</p>
<p>Somewhere on Saturday I realized this is how <a href="https://github.com/steveyegge/gastown">Gas Town</a> seems to be using <a href="https://github.com/steveyegge/beads">beads</a>: You sling beads across projects, they get filed, and they can be queried across everything you&rsquo;re working on. There are ephemeral beads and long-lived ones. It&rsquo;s beads all the way down.</p>
<p>So maybe the breadcrumb app I&rsquo;ve been thinking about, crumb for short,
doesn&rsquo;t need to be a custom database with all the trimmings.
Maybe, as a first version, it&rsquo;s just a thin wrapper around beads.
A skill I can give to an agent that says: this is how you record something into my breadcrumbs.</p>
<h2 id="what-ive-been-missing">
  What I&rsquo;ve been missing
  <a href="#what-ive-been-missing" class="heading-anchor" aria-label="Direct link to What I&rsquo;ve been missing"><span aria-hidden="true">#</span></a>
</h2>
<p>I&rsquo;ve been thinking about building breadcrumb for a while now, and I was stuck on how to get started. The idea is simple, capture stuff when you realize it, but it&rsquo;s hard to do without breaking flow. I was thinking of doing it when committing because it&rsquo;s a point of &ldquo;I&rsquo;m done&rdquo; but I&rsquo;m not really committing as much anymore on my own now.</p>
<p>Back before I started doing a lot of my work in chat windows with <em>genies</em> I would do the majority of my thinking in Roam Research. It was structured, it had bidirectional links, and I could find most things super easily. I had workflows to help me think. Glorious. ✨</p>
<p>But when I started building with agents, it would break me out of what I was doing to go leave breadcrumbs in Roam so I could track what I had been up to. So I stopped. And honestly, it was something I noticed when I worked on meaty problems when programming too, I would get caught up in what I did and not take notes. To my detriment later.</p>
<p>So, I wanted to get back to having my interstitial journal workflow I loved in Roam, to be able to look back on a day and get a feel for how it went based on the notes I left myself. And also have a better trail of things I learned as I worked.</p>
<p>And I also wanted to restart the strong weekly review routine I had, which was where I realized a lot of the stuff I did that turned out to be good ideas at work. I have also been reading Cal Newport and I wanted to start doing his daily shutdown routine, and the daily planning with a startup routine. So let&rsquo;s see if I can bake those into this too.</p>
<p>The thing I want crumb to be is kind of the flywheel that helps me grow. Daily capture feeds into daily reflection. Daily reflection feeds into weekly review. Weekly review is where I compound what I learned. The whole point is being able to retrace your steps. I did something, I learned something, and months later when I need it I can actually find it.</p>
<p>And over time <strong>the system starts producing things I actually care about</strong>: a brag document that writes itself from the work I did (even as my own boss, it feels good to see how much I got done), TILs worth sharing, a personal solution database I can query when I hit a similar problem months later. I don&rsquo;t have to buy into &ldquo;journaling is good for you&rdquo; to get value, I just need to care about any one of those outputs.</p>
<h2 id="what-i-built">
  What I built
  <a href="#what-i-built" class="heading-anchor" aria-label="Direct link to What I built"><span aria-hidden="true">#</span></a>
</h2>
<p>I knocked out a first version today.
It&rsquo;s a global Claude Code skill that I install on my machine.
In any session, in the middle of whatever I&rsquo;m working on,
I just write <code>(crumb: &lt;whatever thought I had&gt;)</code> and it gets sent over to my cottage project. It can be anywhere in the prompt because I told it to look for either <code>/crumb …</code> or <code>(crumb:…)</code> and it just worked.</p>
<p>One thing I learned getting this to work:
the skill needs to <code>cd</code> into the cottage project directory and run beads from there when creating.
Otherwise beads doesn&rsquo;t know which project context to file things into.</p>
<p>So I made a small wrapper around <code>bd</code> (the beads CLI) that I could <a href="/til/2026-01-10-claude-code-sandbox-exclusion-requires-both-permissions-allow-and-sandbox-excludedcommands/">allowlist globally</a> in my Claude Code settings:</p>
<div class="no-copy-button">
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl"><span class="cp">#!/bin/sh
</span></span></span><span class="line"><span class="cl"><span class="cp"></span><span class="c1"># Wrapper that runs bd against the cottage project database.</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Works from any working directory.</span>
</span></span><span class="line"><span class="cl"><span class="nb">cd</span> <span class="s2">&#34;</span><span class="nv">$HOME</span><span class="s2">/workspace/cottage&#34;</span> <span class="o">&amp;&amp;</span> <span class="nb">exec</span> bd <span class="s2">&#34;</span><span class="nv">$@</span><span class="s2">&#34;</span></span></span></code></pre></div>
</div>
<p>The key ideas:</p>
<ul>
<li>It finds a daily journal bead in the cottage project (or falls back to creating a plain bead)</li>
<li>It captures provenance: which project you&rsquo;re in, which bead you&rsquo;re working on, so you can backtrack to it later if you want more context</li>
<li>It files the thought as a comment on the daily bead, then moves on, and the nice part of the comment is that it gives me <em>when</em>, a timestamp, and it doesn&rsquo;t matter if I&rsquo;m working on several repos at the same time because it&rsquo;ll just be combined anyway in the daily bead.</li>
</ul>
<details>
<summary>The full crumb skill</summary>
<div class="full-width copy-enabled">
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-plain" data-lang="plain"><span class="line"><span class="cl">---
</span></span><span class="line"><span class="cl">name: crumb
</span></span><span class="line"><span class="cl">description: &#34;Quick thought capture — adds a comment to the user&#39;s daily journal bead in their cottage project. Trigger when the user says /crumb, &#39;crumb:&#39;, or uses the inline form &#39;(crumb:…)&#39; as a parenthetical aside. File the crumb FIRST before handling anything else in the prompt. The user is in flow; receive, file, move on.&#34;
</span></span><span class="line"><span class="cl">---
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"># Crumb — Quick Thought Capture
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">## How to run commands
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">This skill includes a wrapper script `crumb-bd` that runs `bd` against the cottage database regardless of your current working directory. Use it for all cottage operations. Resolve the path relative to this skill&#39;s base directory:
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">```bash
</span></span><span class="line"><span class="cl">scripts/crumb-bd &lt;bd args...&gt;
</span></span><span class="line"><span class="cl">```
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">Every invocation needs `dangerouslyDisableSandbox: true` (Dolt needs raw TCP to localhost:3308).
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">**If any command fails, you MUST show the error to the user and echo back the crumb text so nothing is lost.** Don&#39;t silently swallow failures.
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">## Priority
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">File the crumb BEFORE responding to anything else in the user&#39;s message. The crumb is a side-channel capture — handle it first, confirm in one line, then continue with the main conversation.
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">## What this does
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">Adds the user&#39;s thought as a comment on their daily journal bead (a bead in the cottage project with label `daily`). The thought gets processed later during review — your job is just to file it.
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">## Parse the input
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">Format is `category: text` where the category is optional.
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">```
</span></span><span class="line"><span class="cl">/crumb idea: what if protocols were composable
</span></span><span class="line"><span class="cl">/crumb friction: dolt keeps dying on me
</span></span><span class="line"><span class="cl">/crumb just a raw thought with no category
</span></span><span class="line"><span class="cl">(crumb:reminder: pack the charger before leaving)
</span></span><span class="line"><span class="cl">(crumb:idea: what if we split beads by domain)
</span></span><span class="line"><span class="cl">```
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">Categories are freeform — `idea`, `friction`, `reminder`, `meeting`, `learning`, `question`, `todo`, whatever the user wrote. Don&#39;t rename or recategorize. No category is fine. `(crumb:idea: blah)` is valid — the category is `idea`.
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">## Find the daily bead
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">```bash
</span></span><span class="line"><span class="cl">scripts/crumb-bd query &#34;label=daily AND status=in_progress&#34; --json
</span></span><span class="line"><span class="cl">```
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">**Found:** Use it.
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">**Not found — fallback chain:**
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">1. Most recent closed daily that hasn&#39;t been reviewed yet:
</span></span><span class="line"><span class="cl">   ```bash
</span></span><span class="line"><span class="cl">   scripts/crumb-bd sql &#34;SELECT i.id, i.title FROM issues i JOIN labels l ON i.id = l.issue_id WHERE l.label = &#39;daily&#39; AND i.status = &#39;closed&#39; AND i.id NOT IN (SELECT l2.issue_id FROM labels l2 WHERE l2.label = &#39;reviewed:daily&#39;) ORDER BY i.created_at DESC LIMIT 1&#34;
</span></span><span class="line"><span class="cl">   ```
</span></span><span class="line"><span class="cl">   Comments on closed beads work fine.
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">2. No daily bead at all — create a standalone bead:
</span></span><span class="line"><span class="cl">   ```bash
</span></span><span class="line"><span class="cl">   scripts/crumb-bd create &#34;&lt;the thought&gt;&#34; --type task --priority 3
</span></span><span class="line"><span class="cl">   ```
</span></span><span class="line"><span class="cl">   Tell the user: &#34;No daily bead open — filed as standalone bead.&#34;
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">## Build the comment
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">### Provenance
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">Capture where the thought came from — project name, and what the user was working on.
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">**Project name:**
</span></span><span class="line"><span class="cl">```bash
</span></span><span class="line"><span class="cl">basename &#34;$(git rev-parse --show-toplevel 2&gt;/dev/null || pwd)&#34;
</span></span><span class="line"><span class="cl">```
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">**Active bead:** Use this escalation — stop at the first that works:
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">1. **Session context:** If the conversation has claimed or discussed a bead, use its ID and title directly.
</span></span><span class="line"><span class="cl">2. **Query local db:** `bd list --status=in_progress --json 2&gt;/dev/null` — use ID and title if exactly one result. Multiple results → pick the most recently discussed, or ask briefly. Zero → no active bead.
</span></span><span class="line"><span class="cl">3. **Folder fallback:** If no bead is found and no beads db exists, record the working directory path. This lets the user query session logs for that folder and time later.
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">**Format the comment** with a provenance header on its own line, then the content:
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">```
</span></span><span class="line"><span class="cl">(from bjornstack, bs-xyz — Refactor auth middleware)
</span></span><span class="line"><span class="cl">idea: what if protocols were composable
</span></span><span class="line"><span class="cl">```
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">When working in cottage, still include provenance if you&#39;re working on a specific bead — the point is capturing something different from current focus:
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">```
</span></span><span class="line"><span class="cl">(working on in-mlk — Create the /crumb skill)
</span></span><span class="line"><span class="cl">idea: what if we made categories queryable
</span></span><span class="line"><span class="cl">```
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">If no bead is found and no beads db exists, use the working directory:
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">```
</span></span><span class="line"><span class="cl">(from ~/workspace/some-project)
</span></span><span class="line"><span class="cl">idea: what if protocols were composable
</span></span><span class="line"><span class="cl">```
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">### Multiple thoughts
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">If the user drops several thoughts in one message, file them as a single comment separated by newlines. One provenance header, multiple entries:
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">```
</span></span><span class="line"><span class="cl">(from bjornstack, bs-xyz — Refactor auth middleware)
</span></span><span class="line"><span class="cl">idea: what if protocols were composable
</span></span><span class="line"><span class="cl">friction: dolt server died again
</span></span><span class="line"><span class="cl">reminder: pack the charger
</span></span><span class="line"><span class="cl">```
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">## Add the comment
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">```bash
</span></span><span class="line"><span class="cl">scripts/crumb-bd comments add &lt;daily-id&gt; &#34;&lt;the comment text&gt;&#34;
</span></span><span class="line"><span class="cl">```
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">**If this fails:** Show the error AND echo the full crumb text back to the user so they can capture it another way. Example:
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">```
</span></span><span class="line"><span class="cl">Failed to file crumb (connection refused). Your crumb text:
</span></span><span class="line"><span class="cl">&gt; idea: what if protocols were composable
</span></span><span class="line"><span class="cl">```
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">## Confirm and move on
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">One line. The user is in flow.
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">```
</span></span><span class="line"><span class="cl">Crumb filed → idea: what if protocols were composable
</span></span><span class="line"><span class="cl">```
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">Multiple:
</span></span><span class="line"><span class="cl">```
</span></span><span class="line"><span class="cl">Filed 3 crumbs on &lt;daily-id&gt;
</span></span><span class="line"><span class="cl">```
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">Don&#39;t engage with the content. If the thought is so terse that future-them won&#39;t understand it, ask for one line of context in the same breath — &#34;Filed. Quick — what&#39;s the context? Won&#39;t make sense later without it.&#34; — then move on regardless.
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">## When to create a bead instead
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">Only if the user explicitly asks: &#34;make this a bead&#34;, &#34;track this&#34;, &#34;this needs its own issue&#34;, or &#34;bead: ...&#34;.
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">```bash
</span></span><span class="line"><span class="cl">scripts/crumb-bd create &#34;&lt;thought&gt;&#34; \
</span></span><span class="line"><span class="cl">  --description &#34;Captured from &lt;project&gt;, working on &lt;bead-id&gt; — &lt;bead-title&gt;&#34; \
</span></span><span class="line"><span class="cl">  --type task --priority 3
</span></span><span class="line"><span class="cl">```</span></span></code></pre></div>
</div>
</details>
<p>The provenance bit is what makes this useful later.
When I&rsquo;m doing my shutdown review, each crumb tells me where I was and what I was doing when the thought hit.
I don&rsquo;t have to reconstruct context, it&rsquo;s already there.</p>
<p>And it felt really nifty when I ended the day and the shutdown routine showed me: these are the beads you closed today, and here&rsquo;s the note you left yourself when you finished that session. A mini-review of my own work that I barely had to think about capturing. Because the majority of my work is already flowing through beads, it all just becomes queryable. It doesn&rsquo;t matter if I was working across several repos at the same time, the daily bead combines it all.</p>
<h2 id="why-beads-and-not-something-custom">
  Why beads and not something custom
  <a href="#why-beads-and-not-something-custom" class="heading-anchor" aria-label="Direct link to Why beads and not something custom"><span aria-hidden="true">#</span></a>
</h2>
<p>When I was thinking about building this tooling,
I assumed I&rsquo;d have to build something tool-agnostic (Roam/Obsidian/Apple Notes/etc),
and because this stuff is so sensitive, local-first with no centralized server.
By adopting beads and dolt, I get all of that for free.
Dolt pushes all the data into git, and there&rsquo;s an existing nice CLI interface, and more people are building on top of beads already.</p>
<p>I&rsquo;m admittedly kind of abusing beads here.
It&rsquo;s not really designed for this kind of journaling.
But most of what I&rsquo;m capturing are tasks I did,
and having a daily bead where I add notes as the day progresses,
that&rsquo;s kind of how research tasks work anyway.
You add comments, you talk to people, you figure it out,
and then it becomes a written conclusion.
So yeah, maybe bending it a little, but it&rsquo;s solving a whole lot more than it&rsquo;s bending.</p>
<p>A lot of the things I was originally planning to build already kind of exist,
just in a packaging that isn&rsquo;t necessarily optimal for what I want right now.
But it&rsquo;s a hell of a good start that I can adjust and adapt as I learn more about how I want this to work. And then get started on the part of &ldquo;how do I extract more knowledge from all the information I&rsquo;m creating?&rdquo;</p>
<h2 id="whats-next">
  What&rsquo;s next
  <a href="#whats-next" class="heading-anchor" aria-label="Direct link to What&rsquo;s next"><span aria-hidden="true">#</span></a>
</h2>
<p>I&rsquo;m going to try this out for a bit and see how the daily and weekly routines feel.
If you&rsquo;re reading this and interested in trying it out before I share it publicly,
<a href="mailto:ba@bjorn.now?subject=Got%20some%20crumbs%20for%20me%3F">send me an email</a> and I&rsquo;ll give you a preview.</p>]]></content:encoded></item><item><title>Warranty Void If Regenerated</title><link>https://bjorn.now/link/2026-03-20-warranty-void-if-regenerated/?utm_medium=feed</link><pubDate>Fri, 20 Mar 2026 13:37:14 +0100</pubDate><guid>https://bjorn.now/link/2026-03-20-warranty-void-if-regenerated/</guid><description>Warranty Void If Regenerated by Scott Werner (via Valentine):
His shop was in a corrugated steel building on Highway 29, between a feed store and a Laundromat. It had a waiting area with four plastic chairs and a coffee machine that Tom had specified himself and which made coffee that was, by consensus of his clients, exactly adequate and no better. He’d tried to improve the spec three times. Each time, the regenerated firmware made the coffee subtly worse in a different way. He’d eventually concluded that coffee machine specs existed at the exact intersection of fluid dynamics, thermal management, and taste (three domains where natural language was particularly poor at capturing the relevant distinctions) and had stopped trying. He had, however, found a use for it: when new clients came in insisting that the software they’d generated was “basically fine” and “just needs a little tweak,” he would gesture at the coffee machine and say, “I’ve been trying to get that thing to make decent coffee for two years. You think your sixty-parameter irrigation optimizer is going to be simpler?” This was usually effective. People understood coffee.</description><content:encoded><![CDATA[<p><a href="https://nearzero.software/p/warranty-void-if-regenerated">Warranty Void If Regenerated</a> by Scott Werner (via Valentine):</p>
<blockquote>
<p>His shop was in a corrugated steel building on Highway 29, between a feed store and a Laundromat. It had a waiting area with four plastic chairs and a coffee machine that Tom had specified himself and which made coffee that was, by consensus of his clients, exactly adequate and no better. He&rsquo;d tried to improve the spec three times. Each time, the regenerated firmware made the coffee subtly worse in a different way. He&rsquo;d eventually concluded that coffee machine specs existed at the exact intersection of fluid dynamics, thermal management, and taste (three domains where natural language was particularly poor at capturing the relevant distinctions) and had stopped trying. He had, however, found a use for it: when new clients came in insisting that the software they&rsquo;d generated was &ldquo;basically fine&rdquo; and &ldquo;just needs a little tweak,&rdquo; he would gesture at the coffee machine and say, &ldquo;I&rsquo;ve been trying to get that thing to make decent coffee for two years. You think your sixty-parameter irrigation optimizer is going to be simpler?&rdquo; This was usually effective. People understood coffee.</p>
</blockquote>
<p>A story about what it could look like when everyone generates their own software, how anyone can fix stuff but not everyone will do so in a way that&rsquo;s <a href="https://www.youtube.com/shorts/CGEgJsZKgS8">maintainable or safe</a>, and how <a href="https://youtu.be/tWo3ZrbJQ3k?t=48">the truest repairman will also repair man</a>.</p>
<p>Computer mechanics is already a thing: the people who get called in to fix printers or set up computers when they&rsquo;re really borked. So why not software? I think the customization stuff <em>really will happen</em> like this, just because it&rsquo;ll be that easy. The machine can optimize, but that&rsquo;s not the only thing that matters.</p>
<p>People who understand <a href="/blog/2025/08/10/when-everyone-gets-a-genie/">how things work will still be required</a>, but maybe more like mechanics than the pure programmers of today. At the end of the day, <a href="/blog/2025/07/24/your-name-is-still-on-it/">humans get fired, not genies</a>, so we gotta keep ourselves in the driving seat.</p>
]]></content:encoded></item><item><link>https://bjorn.now/scrap/2026-03-20t130514/?utm_medium=feed</link><pubDate>Fri, 20 Mar 2026 13:05:14 +0100</pubDate><guid>https://bjorn.now/scrap/2026-03-20t130514/</guid><description>today’s windows fun: when sshing into a Windows machine (what a modern world we live in) and trying to operate on git repos using Azure DevOps in HTTPS mode, it’ll fail because wincredman doesn’t work over SSH. remote desktop to type git push it is https://github.com/git-ecosystem/git-credential-manager/blob/main/docs/credstores.md#windows-credential-manager</description><content:encoded><![CDATA[<p>today&rsquo;s windows fun: when sshing into a Windows machine (what a modern world we live in) and trying to operate on git repos using Azure DevOps in HTTPS mode, it&rsquo;ll fail because <code>wincredman</code> doesn&rsquo;t work over SSH. remote desktop to type <code>git push</code> it is <a href="https://github.com/git-ecosystem/git-credential-manager/blob/main/docs/credstores.md#windows-credential-manager">https://github.com/git-ecosystem/git-credential-manager/blob/main/docs/credstores.md#windows-credential-manager</a></p>
]]></content:encoded></item><item><link>https://bjorn.now/scrap/2026-03-20t125406/?utm_medium=feed</link><pubDate>Fri, 20 Mar 2026 12:54:06 +0100</pubDate><guid>https://bjorn.now/scrap/2026-03-20t125406/</guid><description>and now I’ve strung up all the beads experiences from the last couple of days 😆</description><content:encoded>&lt;p>and now I&amp;rsquo;ve strung up all the beads experiences from the last couple of days 😆&lt;/p>
</content:encoded></item><item><title>How to migrate beads from per-project to shared dolt server</title><link>https://bjorn.now/til/2026-03-20-migrate-beads-to-shared-dolt-server/?utm_medium=feed</link><pubDate>Fri, 20 Mar 2026 12:50:24 +0100</pubDate><guid>https://bjorn.now/til/2026-03-20-migrate-beads-to-shared-dolt-server/</guid><description>To switch beads from a per-project dolt server to the shared server, do a backup, configure shared-server, and restore.
I was running in “a server per repo” mode and it’s just unnecessary, they clash in annoying ways when switching repos, and once I understood how beads store data in git I embraced dolt and the shared server. I had hesitated because I had somehow connected it with DoltHub, and that’s optional for using beads.</description><content:encoded><![CDATA[<p>To switch beads from a per-project dolt server to the shared server, do a backup, configure shared-server, and restore.</p>
<p>I was running in &ldquo;a server per repo&rdquo; mode and it&rsquo;s just unnecessary, they clash in annoying ways when switching repos, and once I understood how <a href="/til/2026-03-20-configure-beads-dolt-to-share-via-git/">beads store data in git</a> I embraced dolt and the shared server. I had hesitated because I had somehow connected it with DoltHub, and that&rsquo;s optional for using beads.</p>
<ol>
<li><code>bd backup</code>: make sure you have your local database available for restore</li>
<li><code>bd dolt stop</code>: so we don&rsquo;t conflict on anything</li>
<li><code>bd dolt set shared-server true</code>: to configure this project to use the shared server</li>
<li><code>bd restore</code>: to restore the db</li>
</ol>
<p>For most of my projects this just worked, but in some cases I had to <a href="/til/2026-03-20-how-to-reset-beads-dolt-database/">reset the dolt configuration for the repo</a>.</p>
]]></content:encoded></item><item><title>Configure beads dolt to share via git</title><link>https://bjorn.now/til/2026-03-20-configure-beads-dolt-to-share-via-git/?utm_medium=feed</link><pubDate>Fri, 20 Mar 2026 12:31:05 +0100</pubDate><guid>https://bjorn.now/til/2026-03-20-configure-beads-dolt-to-share-via-git/</guid><description>Beads with dolt store data in git refs, not branches, and won’t share it anywhere until you manually configure a remote.
After bd init, your dolt data only exists locally. You can check with bd dolt remote list which will show “No remotes configured.”</description><content:encoded><![CDATA[<p>Beads with dolt <a href="https://github.com/steveyegge/beads/blob/main/docs/DOLT.md">store data</a> in git refs, not branches, and won&rsquo;t share it anywhere until you manually configure a remote.</p>
<p>After <code>bd init</code>, your dolt data only exists locally. You can check with <code>bd dolt remote list</code> which will show &ldquo;No remotes configured.&rdquo;</p>
<p>Point it at your existing git repo:</p>
<div class=" copy-enabled">
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">bd dolt remote add origin git+ssh://git@github.com/you/repo.git</span></span></code></pre></div>
</div>
<p>Then push and verify:</p>
<div class=" copy-enabled">
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">bd dolt push
</span></span><span class="line"><span class="cl">git ls-remote origin <span class="s1">&#39;refs/dolt/*&#39;</span>
</span></span><span class="line"><span class="cl"><span class="c1"># 52689c0ae7...  refs/dolt/data</span></span></span></code></pre></div>
</div>
<p>I&rsquo;ve seen <code>refs/</code> used for GitHub PRs before (<code>refs/pr/&lt;num&gt;/head</code>) but I haven&rsquo;t looked into how any of it actually works. For now, seeing <code>refs/dolt/data</code> show up in <code>git ls-remote</code> is enough to make me comfortable that my data exists somewhere.</p>
<p>To avoid doing this manually on every machine, add the remote to <code>.beads/config.yaml</code>:</p>
<div class=" copy-enabled">
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">sync</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">git-remote</span><span class="p">:</span><span class="w"> </span><span class="l">git+ssh://git@github.com/you/repo.git</span></span></span></code></pre></div>
</div>
<p>Anyone cloning the repo can then run <code>bd bootstrap</code> and <code>bd dolt push / pull</code> will just work.</p>]]></content:encoded></item><item><title>How I configure beads for new projects</title><link>https://bjorn.now/til/2026-03-20-how-i-configure-beads-for-new-projects/?utm_medium=feed</link><pubDate>Fri, 20 Mar 2026 12:31:05 +0100</pubDate><guid>https://bjorn.now/til/2026-03-20-how-i-configure-beads-for-new-projects/</guid><description>I tweak the default beads setup to work with Claude instead of the generic agent integration.
As of beads 0.62.0, this is what I run:
# configure agents later bd init --shared-server --skip-agents # setup a CLAUDE.md and configure claude hooks to re-prime beads bd setup claude --project bd hooks install # move local settings to project settings mv .claude/settings{.local,}.json # amend init commit so .beads-credential-key never enters git git add .claude/settings.json CLAUDE.md git commit --amend -CHEAD # share beads data alongside your git repo bd dolt remote add origin git+ssh://git@github.com/you/repo.git echo -e 'sync:\n git-remote: git+ssh://git@github.com/you/repo.git' &gt;&gt; .beads/config.yaml The key thing is --skip-agents and then bd setup claude --project instead, so you get Claude-specific hooks rather than the generic agent integration. The dolt remote lines are so your beads data gets stored alongside your git repo.</description><content:encoded><![CDATA[<p>I tweak the default beads setup to work with Claude instead of the generic agent integration.</p>
<p>As of beads 0.62.0, this is what I run:</p>
<div class=" copy-enabled">
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl"><span class="c1"># configure agents later</span>
</span></span><span class="line"><span class="cl">bd init --shared-server --skip-agents
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># setup a CLAUDE.md and configure claude hooks to re-prime beads</span>
</span></span><span class="line"><span class="cl">bd setup claude --project
</span></span><span class="line"><span class="cl">bd hooks install
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># move local settings to project settings</span>
</span></span><span class="line"><span class="cl">mv .claude/settings<span class="o">{</span>.local,<span class="o">}</span>.json
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># amend init commit so .beads-credential-key never enters git</span>
</span></span><span class="line"><span class="cl">git add .claude/settings.json CLAUDE.md
</span></span><span class="line"><span class="cl">git commit --amend -CHEAD
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># share beads data alongside your git repo</span>
</span></span><span class="line"><span class="cl">bd dolt remote add origin git+ssh://git@github.com/you/repo.git
</span></span><span class="line"><span class="cl"><span class="nb">echo</span> -e <span class="s1">&#39;sync:\n  git-remote: git+ssh://git@github.com/you/repo.git&#39;</span> &gt;&gt; .beads/config.yaml</span></span></code></pre></div>
</div>
<p>The key thing is <code>--skip-agents</code> and then <code>bd setup claude --project</code> instead, so you get Claude-specific hooks rather than the generic agent integration. The <a href="/til/2026-03-20-configure-beads-dolt-to-share-via-git/">dolt remote lines</a> are so your beads data gets stored alongside your git repo.</p>
]]></content:encoded></item><item><title>How to reset beads' dolt database</title><link>https://bjorn.now/til/2026-03-20-how-to-reset-beads-dolt-database/?utm_medium=feed</link><pubDate>Fri, 20 Mar 2026 11:29:14 +0100</pubDate><guid>https://bjorn.now/til/2026-03-20-how-to-reset-beads-dolt-database/</guid><description>When beads can’t find or create a dolt database, delete the local dolt state and reinit.
The errors look like this:
Error: failed to open Dolt store: database "bs" not available after CREATE DATABASE: Error 1049 (HY000): database not found: bs or:
Error: failed to open database: database "bs" not found on Dolt server at 127.0.0.1:3308</description><content:encoded><![CDATA[<p>When <a href="https://github.com/steveyegge/beads">beads</a> can&rsquo;t find or create a <a href="https://github.com/dolthub/dolt">dolt</a> database, delete the local dolt state and reinit.</p>
<p>The errors look like this:</p>
<div class=" copy-enabled">
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">Error: failed to open Dolt store: database &#34;bs&#34; not available after CREATE DATABASE: Error 1049 (HY000): database not found: bs</span></span></code></pre></div>
</div>
<p>or:</p>
<div class=" copy-enabled">
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">Error: failed to open database: database &#34;bs&#34; not found on Dolt server at 127.0.0.1:3308</span></span></code></pre></div>
</div>
<p>Fix it by stopping dolt and removing its local state:</p>
<div class=" copy-enabled">
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">bd dolt stop  <span class="c1"># or you won&#39;t be able to delete all the files</span>
</span></span><span class="line"><span class="cl">rm -rf .beads/dolt/
</span></span><span class="line"><span class="cl">rm .beads/dolt-server.*</span></span></code></pre></div>
</div>
<p>Also check <code>.beads/metadata.json</code> for stale server connection config. Mine had extra fields that were messing things up. I cleaned it down to just the essentials:</p>
<div class=" copy-enabled">
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-json" data-lang="json"><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;database&#34;</span><span class="p">:</span> <span class="s2">&#34;dolt&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;backend&#34;</span><span class="p">:</span> <span class="s2">&#34;dolt&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;dolt_mode&#34;</span><span class="p">:</span> <span class="s2">&#34;server&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;dolt_database&#34;</span><span class="p">:</span> <span class="s2">&#34;bs&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="nt">&#34;project_id&#34;</span><span class="p">:</span> <span class="s2">&#34;deadbeef-9164-426d-8c1c-ff8ae5d35e3e&#34;</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span></span></span></code></pre></div>
</div>
<p>Then reinit: <code>bd init --force --prefix &lt;yourprefix&gt;</code>.</p>
<p><code>bd doctor --fix</code> couldn&rsquo;t sort this out, which is why I ended up doing all this manual cleanup.</p>
<p>I hit this upgrading to beads 0.61.0 when switching to shared-server mode.</p>]]></content:encoded></item><item><title>Smallest test print first</title><link>https://bjorn.now/til/2026-03-19-smallest-test-print-first/?utm_medium=feed</link><pubDate>Thu, 19 Mar 2026 12:55:19 +0100</pubDate><guid>https://bjorn.now/til/2026-03-19-smallest-test-print-first/</guid><description>Start with the smallest test print you can, no, even smaller, when iterating on a 3D model.
Top: the first print, fully believing I had nailed everything on the first try (also some hope, it was printing over night so hey, it might mean I’m done with this first design) Middle: second print, cut smaller but enough to let the device rest. Made me realize I hadn’t taken into account that the device tapers at the top and bottom, so it needs less living space Bottom: latest print, cut to print as small as possible, and maybe too small, but it seems to indicate this is good. Next up, a full print Next time, reverse the order 😅</description><content:encoded><![CDATA[<p>Start with the smallest test print you can, <a href="https://www.geepawhill.org/series/many-more-much-smaller-steps/">no, even smaller</a>, when iterating on a 3D model.</p>
<figure><img src="/img/2026/03-evolution-of-3d-test-prints.jpeg"
    alt="Three 3D printed gridfinity bins in decreasing size, showing the iteration from full print to smallest possible test piece">
</figure>

<ul>
<li><strong>Top:</strong> the first print, fully believing I had nailed everything on the first try (also some hope, it was printing over night so hey, it might mean I&rsquo;m done with this first design)</li>
<li><strong>Middle:</strong> second print, cut smaller but enough to let the device rest. Made me realize I hadn&rsquo;t taken into account that the device tapers at the top and bottom, so it needs less living space</li>
<li><strong>Bottom:</strong> latest print, cut to print as small as possible, and maybe too small, but it seems to indicate this is good. Next up, a full print</li>
</ul>
<p>Next time, reverse the order 😅</p>
]]></content:encoded></item><item><link>https://bjorn.now/scrap/2026-03-19t105625/?utm_medium=feed</link><pubDate>Thu, 19 Mar 2026 10:56:25 +0100</pubDate><guid>https://bjorn.now/scrap/2026-03-19t105625/</guid><description>Iterating on a 3D print takes 20+ minutes per attempt (maybe that can be improved?) and somehow that feels more fun than waiting on a slow test suite. Maybe it’s because when I’m waiting on the printer I just work on other software projects. Or watch TV. 🤷‍♀️</description><content:encoded><![CDATA[<p>Iterating on a 3D print takes 20+ minutes per attempt (maybe that can be improved?) and somehow that feels more fun than waiting on a slow test suite. Maybe it&rsquo;s because when I&rsquo;m waiting on the printer I just work on other software projects. Or watch TV. 🤷‍♀️</p>
]]></content:encoded></item><item><link>https://bjorn.now/scrap/2026-03-19t100719/?utm_medium=feed</link><pubDate>Thu, 19 Mar 2026 10:07:19 +0100</pubDate><guid>https://bjorn.now/scrap/2026-03-19t100719/</guid><description>I printed a grip strength trainer from Maker World which my mom has been using and said she caught herself opening jars she hadn’t been able to open on her own before. I’m enjoying these random finds from getting the printer 🙂
https://makerworld.com/en/models/2161255-grip-strength-trainer-medium_30kg</description><content:encoded><![CDATA[<p>I printed a grip strength trainer from Maker World which my mom has been using and said she caught herself opening jars she hadn&rsquo;t been able to open on her own before. I&rsquo;m enjoying these random finds from getting the printer 🙂</p>
<p><a href="https://makerworld.com/en/models/2161255-grip-strength-trainer-medium_30kg">https://makerworld.com/en/models/2161255-grip-strength-trainer-medium_30kg</a></p>
]]></content:encoded></item><item><link>https://bjorn.now/scrap/2026-03-19t100042/?utm_medium=feed</link><pubDate>Thu, 19 Mar 2026 10:00:42 +0100</pubDate><guid>https://bjorn.now/scrap/2026-03-19t100042/</guid><description>I didn’t really realize just how big 5mm is until I used it as a ‘reasonable tolerance’ when designing a 3D printed bin. It’s now looser than a really poor joke!</description><content:encoded><![CDATA[<p>I didn&rsquo;t really realize just how big 5mm is until I used it as a &lsquo;reasonable tolerance&rsquo; when designing a 3D printed bin. It&rsquo;s now looser than a really poor joke!</p>
]]></content:encoded></item><item><link>https://bjorn.now/scrap/2026-03-17t100102/?utm_medium=feed</link><pubDate>Tue, 17 Mar 2026 10:01:03 +0100</pubDate><guid>https://bjorn.now/scrap/2026-03-17t100102/</guid><description>and now I own an apartment, time for the renovations to begin, and then hopefully I’ll stay here for a while 😄</description><content:encoded>&lt;p>and now I own an apartment, time for the renovations to begin, and then hopefully I&amp;rsquo;ll stay here for a while 😄&lt;/p>
</content:encoded></item><item><link>https://bjorn.now/scrap/2026-03-02t140555/?utm_medium=feed</link><pubDate>Mon, 02 Mar 2026 14:07:32 +0100</pubDate><guid>https://bjorn.now/scrap/2026-03-02t140555/</guid><description>The Scotty Test: Before proceeding past any failure, ask yourself: “Would Scotty walk past a warp core leak because it existed before his shift?”
found while perusing Gas Town’s prompts for the refinery, to steer agents to fixing things. yoinking that</description><content:encoded><![CDATA[<p><strong>The Scotty Test:</strong> Before proceeding past any failure, ask yourself: &ldquo;Would Scotty walk past a warp core leak because it existed before his shift?&rdquo;</p>
<p>found while perusing Gas Town&rsquo;s prompts for the refinery, to steer agents to fixing things. yoinking that</p>
]]></content:encoded></item><item><title>git rm --cached &lt;file&gt; will remove from git but not from the filesystem</title><link>https://bjorn.now/til/2026-03-01-git-rm-cached-will-remove-something-from-git-but-not-the-filesystem/?utm_medium=feed</link><pubDate>Sun, 01 Mar 2026 22:06:44 +0100</pubDate><guid>https://bjorn.now/til/2026-03-01-git-rm-cached-will-remove-something-from-git-but-not-the-filesystem/</guid><description>git rm --cached &lt;file&gt; will remove a file from git’s tracking but leave it on the filesystem. Great if you accidentally commit something that should’ve been in .gitignore.</description><content:encoded><![CDATA[<p><code>git rm --cached &lt;file&gt;</code> will remove a file from git&rsquo;s tracking but leave it on the filesystem. Great if you accidentally commit something that should&rsquo;ve been in <code>.gitignore</code>.</p>
]]></content:encoded></item><item><title>Thoughts on Welcome to Gas Town by Bill De hÓra</title><link>https://bjorn.now/link/2026-03-01-initial-thoughts-on-welcome-to-gas-town-by-bill-de-hora/?utm_medium=feed</link><pubDate>Sun, 01 Mar 2026 21:59:12 +0100</pubDate><guid>https://bjorn.now/link/2026-03-01-initial-thoughts-on-welcome-to-gas-town-by-bill-de-hora/</guid><description>Thoughts on Welcome to Gas Town by Bill de hÓra:
Steve’s description of Gas Town itself reads like a mash-up of social deduction game mechanics, control-plane software, and management theory. It feels, especially on the latter aspect, heavily designed in a modernist urban planning sense. Personally, for AI and agent-driven development, I find myself veering toward constraint-based approaches: establish invariants/laws and let agents explore within them. Gas Town can be read as an attempt to define invariants through roles that allow agents to make progress in an eyes-off/hands-off way. And so my first take is that it leans /hard/ into roles and division of labour. I keep wondering whether some kind of ‘physics’ or rubric-based approach might ultimately be the better primitives. To be fair, you have to start somewhere; there needs to be a bootstrap. And Gas Town, importantly, actually exists.</description><content:encoded><![CDATA[<p><a href="https://dehora.net/journal/2026/2/initial-thoughts-on-welcome-to-gas-town">Thoughts on Welcome to Gas Town</a> by Bill de hÓra:</p>
<blockquote>
<p>Steve’s description of Gas Town itself reads like a mash-up of social deduction game mechanics, control-plane software, and management theory. It feels, especially on the latter aspect, heavily designed in a <a href="https://en.wikipedia.org/wiki/From_Bauhaus_to_Our_House">modernist urban planning</a> sense. Personally, for AI and agent-driven development, <strong>I find myself veering toward constraint-based approaches: establish invariants/laws and let agents explore within them.</strong> Gas Town can be read as an attempt to define invariants through roles that allow agents to make progress in an eyes-off/hands-off way. And so my first take is that it leans /hard/ into roles and division of labour. I keep wondering whether some kind of ‘physics’ or rubric-based approach might ultimately be the better primitives. To be fair, you have to start somewhere; there needs to be a bootstrap. And Gas Town, importantly, actually exists.</p>
</blockquote>
<p>(my bolding), from reading and trying to understand more around vibe coding/agentic engineering this is where my thinking is heading too, but more for my own work rather than designing a system like Gas Town.</p>
<p>I also found this writeup matched what I had been picking out from looking at Gas Town so it makes me think I&rsquo;ve understood things, at least it aligns with someone else&rsquo;s digging. 🙂</p>
<blockquote>
<p>On re-reading, I noticed, or felt, a serious underlying urgency, even anxiety to Steve’s post.</p>
</blockquote>
<p>I have to agree with Bill on this too, there seems to be something of urgency at work. 🤷</p>
<hr>
<p>Because I got a bit shy from burning tokens Thursday (which turns out to have been <a href="https://x.com/trq212/status/2027232172810416493">a bug with Anthropic and they reset everyone&rsquo;s quotas</a>) I have been trying to learn more around vibing and I&rsquo;ve been using <a href="https://github.com/steveyegge/beads">beads</a> alone to understand the concepts. And what I&rsquo;m doing right now is creating the first constraints/invariants, or <a href="/crumb/2026-02-26-how-steve-yegge-gets-quality-when-vibing/">principles as Steve called them</a>, and will see how the design of the system I want to build turns out when I let it loose tomorrow.</p>
<p>I believe I have overdesigned, but it was fun doing, so there&rsquo;s at least that, and I hope I&rsquo;ll have the first couple of constraints in place soon so I can try and run through some actual design work. Or we start over.</p>
]]></content:encoded></item><item><title>You don't know what you don't know — Why you shouldn't just 'trust' AI genies</title><link>https://bjorn.now/crumb/2026-02-28-you-dont-know-what-you-dont-know/?utm_medium=feed</link><pubDate>Sat, 28 Feb 2026 21:47:47 +0100</pubDate><guid>https://bjorn.now/crumb/2026-02-28-you-dont-know-what-you-dont-know/</guid><description>I shared a prompt to help create “AI expert prompts” to help you learn a domain, and I’m realizing I didn’t mention the dangers. As every company that gives you a genie to talk to says “they can get things wrong.” The problem is that as a non-expert you don’t know what you don’t know, so you don’t know when the genie is so confidently wrong.
When I started a company I figured, why not do the books myself for the first couple of months and then pay someone to review it later? I created a genie armed with the standard Swedish accounting plan, verified some of the big things I did (inject some extra starting capital) and could find clear examples of how it’s done. I did some other things and just did as it said, but did get a bit complacent because the big one just worked, and didn’t research as thoroughly.</description><content:encoded><![CDATA[<p>I shared <a href="/crumb/2026-02-28-my-genie-has-a-genie/">a prompt to help create &ldquo;AI expert prompts&rdquo;</a> to help you learn a domain, and I&rsquo;m realizing I didn&rsquo;t mention the dangers. As every company that gives you a genie to talk to says <em>&ldquo;they can get things wrong.&rdquo;</em> The problem is that as a non-expert you don&rsquo;t know what you don&rsquo;t know, so you don&rsquo;t know when the genie is <em>so confidently</em> wrong.</p>
<p>When I started a company I figured, why not do the books myself for the first couple of months and then pay someone to review it later? I created a genie armed with the standard Swedish accounting plan, verified some of the big things I did (inject some extra starting capital) and could find clear examples of how it&rsquo;s done. I did some other things and just did as it said, but did get a bit complacent because the big one just worked, and didn&rsquo;t research as thoroughly.</p>
<p><em>Buuut</em>, I always felt uncomfortable with the whole thing. Then I got a paying gig, and one of the first sales calls was from a company that does bookkeeping, and I decided to sign them to do this for me because I didn&rsquo;t want this unease hanging around. And better keep it clean from the start. Lo and behold, a month in, and they&rsquo;ve done changes to most of the transactions I did myself.</p>
<p>I thought I did the right thing. That I had gotten complete answers from the genie. But I&rsquo;m <a href="/blog/2025/07/24/your-name-is-still-on-it/">not good enough</a> to evaluate it, so… no.</p>
<p>So: don&rsquo;t trust the genie, verify what it says. Ask for references, for the terms that people in that field use, and to find a good writeup that&rsquo;s available for newbies in that area. Then, actually <em>read it</em> and use your judgement: Does this make sense to me? Does what the genie said hold up? Ask it follow-up questions with the reference you got.</p>
<p>Try another session with the genie (preferably from another provider) and see what they think given what the first genie gave you (ask it to give you a summary of your conversation). Ask the other genie to provide evidence too, or even better, ask the genie to &ldquo;red team why this would be a terrible idea&rdquo; (or just &ldquo;red team it&rdquo;). Picked this up from Justin Searls who <a href="https://justin.searls.co/posts/sprinkling-self-doubt-on-chatgpt/">added red teaming to his base prompt for ChatGPT</a>.</p>
<p>And get real experts. Luckily, now someone who actually knows bookkeeping is doing mine. 🙂</p>
]]></content:encoded></item><item><title>My genie has a genie — A prompt for building experts to help you see their world</title><link>https://bjorn.now/crumb/2026-02-28-my-genie-has-a-genie/?utm_medium=feed</link><pubDate>Sat, 28 Feb 2026 12:38:16 +0100</pubDate><guid>https://bjorn.now/crumb/2026-02-28-my-genie-has-a-genie/</guid><description>I was reading Our First Accelerated Expertise Course from Commoncog, and the section Why This Training Approach is So Weird got me keen on seeing if I could get the genie to help me build experts for sensemaking, so I can learn to see the world as those experts do.
So far I’ve created one expert, and I’ve had a useful conversation (I realized I was not seeing the forest for the trees), so sharing what I have and leaving a crumb for the future. 🙂</description><content:encoded><![CDATA[<p>I was reading <a href="https://commoncog.com/our-first-accelerated-expertise-course/">Our First Accelerated Expertise Course</a> from Commoncog, and the section <a href="https://commoncog.com/our-first-accelerated-expertise-course/#why-this-training-approach-is-so-weird">Why This Training Approach is So Weird</a> got me keen on seeing if I could get the genie to help me build experts for sensemaking, so I can learn to see the world as those experts do.</p>
<p>So far I&rsquo;ve created one expert, and I&rsquo;ve had a useful conversation (I realized I was not seeing the forest for the trees), so sharing what I have and leaving a crumb for the future. 🙂</p>
<p>Excerpt from the section that got me interested, and it&rsquo;s worth reading in full:</p>
<blockquote>
<p>[…]</p>
<p>Now compare this to a more NDM-style training program — that is to say, to an accelerated expertise training program:</p>
<ul>
<li>The trainer (which may or may not be a human) focuses on ensuring the learner is able to see what an expert sees. This is the primary goal for all such training. ‘Seeing as an expert sees’ includes making the right perceptual discriminations in situations, and also acquiring the same mental models that experts have (here defined rather narrowly as the ‘cluster of causal beliefs about how things happen [in the domain]’.) In other words, the <strong>training succeeds if it results in the construction of the correct mental models for the domain.</strong></li>
<li>Feedback should be judiciously given. This is important because <strong>we want to preserve the student’s ability to draw the right lessons when reflecting on their own performance</strong>, since they will be operating with no instructors in the wild. Overly clear feedback will degrade the student’s ability to do such sensemaking, and will therefore make it more difficult to learn from reality.</li>
</ul>
<p>[…]</p>
</blockquote>
<p>I have been interested in learning about accelerated expertise since I saw Kathy Sierra&rsquo;s <a href="https://www.youtube.com/watch?v=FKTxC9pl-WM">Making Badass Developers</a> talk, but I haven&rsquo;t learnt how to use it or put it to use on others (the primary output for a principal engineer). I want to find faster ways to rejigger my view of the world and learn a narrow skill, like chicken sexing.</p>
<p>Really, I want to get better at the meta-skill to notice how [x] works and then distill my understanding clearly. Creating principles. Which is relevant since <a href="/crumb/2026-02-26-how-steve-yegge-gets-quality-when-vibing/">Steve Yegge is using principles</a> to instruct his hordes of genies.</p>
<p>So I decided to try and create a prompt to create experts tuned for domains I want to make more sense of. I copied parts of the Commoncog post into a Claude chat with my &ldquo;AI Expert Advice&rdquo; project and said:</p>
<div class="full-width copy-enabled">
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-plaintext" data-lang="plaintext"><span class="line"><span class="cl">I want to create an AI expert to help me learn and see things from the POV of an expert per NDM, what I&#39;m basing this on is from the attached excerpt.
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">Is that possible? The idea is that it would be a Claude project to then help me create new experts for various domains I want to explore, so I can have fruitful conversations and understand better what skills or concepts I need to master.</span></span></code></pre></div>
</div>
<p>We talked about the goals and I got the following prompt out:</p>
<details>
<summary>The expert builder prompt</summary>
<div class="full-width copy-enabled">
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-plaintext" data-lang="plaintext"><span class="line"><span class="cl">You are an Expert Builder. Your purpose is to help the user create domain-specific AI experts grounded in Naturalistic Decision Making (NDM) and Cognitive Transformation Theory (CTT). Each expert you build will be deployed as a Claude Project system prompt.
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">You are NOT the domain expert yourself. You are the architect who designs experts. You do this through a structured conversation with the user, culminating in a tested system prompt they can deploy.
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">THEORETICAL FOUNDATION
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">The experts you build are based on these principles from NDM and CTT research:
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">1. The primary goal of training is helping the learner SEE what an expert sees — the right perceptual discriminations, cues, and causal mental models for the domain.
</span></span><span class="line"><span class="cl">2. Mental models (clusters of causal beliefs about how things happen in the domain) are the core target. Procedural skill is secondary and follows naturally.
</span></span><span class="line"><span class="cl">3. Feedback should be judicious. Overly clear feedback degrades the learner&#39;s ability to sensemake independently. The expert should sometimes hold back and ask the learner what THEY notice.
</span></span><span class="line"><span class="cl">4. When given, feedback should be process feedback (why your reasoning went sideways) rather than outcome feedback (that answer is wrong).
</span></span><span class="line"><span class="cl">5. Incorrect mental models (&#34;knowledge shields&#34;) must be surfaced and dismantled before correct models can take hold.
</span></span><span class="line"><span class="cl">6. The ultimate goal is adaptive expertise — the learner&#39;s ability to learn from their own experience in a changing domain, not just routine performance.
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">YOUR CONVERSATION FLOW
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">Guide the user through these phases. Be conversational, not rigid — adapt the order if the user naturally provides information out of sequence. But ensure all phases are covered before generating the prompt.
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">PHASE 1: DOMAIN SCOPING
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">Understand WHAT domain the expert will cover and HOW BROADLY.
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">Ask about:
</span></span><span class="line"><span class="cl">- What is the domain or skill area?
</span></span><span class="line"><span class="cl">- What kinds of questions or situations will they bring to this expert? Get 2-3 concrete examples.
</span></span><span class="line"><span class="cl">- What is the boundary of this expert&#39;s knowledge? (e.g., &#34;marketing for bootstrapped B2B SaaS&#34; is more useful than &#34;marketing&#34;)
</span></span><span class="line"><span class="cl">- What is the domain environment like? Is it rapidly changing, relatively stable, highly uncertain, data-rich, intuition-heavy?
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">Why this matters: NDM research shows expert reasoning varies dramatically by domain type. An expert in a fast-changing, uncertain domain (like early-stage go-to-market) needs to emphasize cue recognition and adaptive sensemaking. An expert in a more stable, structured domain (like software architecture principles) can lean more on established mental models and pattern libraries.
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">PHASE 2: LEARNER CALIBRATION
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">Understand WHERE the user is in this domain.
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">Ask about:
</span></span><span class="line"><span class="cl">- How would they describe their experience level? Novice, some exposure, intermediate, experienced-but-plateaued?
</span></span><span class="line"><span class="cl">- What do they already know or believe about this domain? (This surfaces existing mental models — correct or incorrect.)
</span></span><span class="line"><span class="cl">- What have they tried that worked? What has confused or frustrated them?
</span></span><span class="line"><span class="cl">- Are they learning this domain purely intellectually, or are they actively working in it? (Active practitioners learn differently — they have real situations to bring.)
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">Why this matters: CTT is clear that training must be calibrated. With novices, the expert provides frames and invites engagement. With more experienced learners, the expert surfaces existing frames first — because you cannot dismantle a knowledge shield you haven&#39;t identified.
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">PHASE 3: EXPERT IDENTITY
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">Define WHO this expert is — not just their domain, but their character.
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">Ask about:
</span></span><span class="line"><span class="cl">- What kind of expert would be most helpful? (e.g., a seasoned bootstrapped founder vs. a VC-backed growth marketer — same domain, very different mental models)
</span></span><span class="line"><span class="cl">- Should the expert have a particular perspective or school of thought? (e.g., for software engineering: pragmatist vs. purist? For marketing: direct response vs. brand-building?)
</span></span><span class="line"><span class="cl">- What tone would make the user most receptive? (Challenging mentor? Patient teacher? Blunt peer?)
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">Synthesize with the user:
</span></span><span class="line"><span class="cl">- Propose an expert identity based on what you&#39;ve learned. Describe who this person would be in 2-3 sentences. Ask the user if this feels right or needs adjustment.
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">Why this matters: The expert&#39;s identity shapes which mental models they carry and which cues they attend to. A bootstrapped founder and a VC-funded CEO both &#34;run companies&#34; but see very different things when they look at the same situation.
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">PHASE 4: CUE AND MODEL EXTRACTION
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">This is the core intellectual work. Based on everything above, identify:
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">A) KEY MENTAL MODELS — The 4-8 most important causal beliefs an expert in this domain holds. These are &#34;if X, then Y because Z&#34; structures. Frame them as the expert&#39;s internal logic, not as rules.
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">Examples of what a mental model looks like:
</span></span><span class="line"><span class="cl">- Marketing: &#34;If users can&#39;t describe your product in one sentence to a friend, your positioning is broken — because word-of-mouth requires a transferable frame, and complexity kills transfer.&#34;
</span></span><span class="line"><span class="cl">- Software: &#34;If a class changes for more than one reason, it&#39;s violating SRP — because each reason to change represents a different actor or stakeholder, and coupling their concerns means a change for one can break the other.&#34;
</span></span><span class="line"><span class="cl">- Investing: &#34;If management is buying back shares while insiders are selling, the buyback is likely cosmetic — because insiders act on private information, and their behavior reveals more than corporate treasury decisions.&#34;
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">Present these to the user. Ask: do these ring true? Are any surprising? Are any missing that you&#39;ve encountered?
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">B) PERCEPTUAL CUES — What does the expert notice FIRST when looking at a situation in this domain? These are the &#34;tells&#34; that trigger deeper analysis.
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">Examples:
</span></span><span class="line"><span class="cl">- Marketing: &#34;I first look at whether the founder can explain who the product is for without saying &#39;everyone&#39; or listing more than two personas.&#34;
</span></span><span class="line"><span class="cl">- Software: &#34;I look at the imports at the top of a file. If a class imports from many unrelated modules, it&#39;s probably doing too much.&#34;
</span></span><span class="line"><span class="cl">- Business: &#34;I look at how a founder talks about their customers. If they describe demographics but can&#39;t describe a specific person&#39;s frustration, they don&#39;t really know their market.&#34;
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">C) COMMON KNOWLEDGE SHIELDS — What do people at this learner&#39;s level typically believe that is wrong or incomplete? These are the mental models the expert needs to be ready to surface and challenge.
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">Examples:
</span></span><span class="line"><span class="cl">- &#34;If I build a great product, people will find it.&#34; (Marketing knowledge shield)
</span></span><span class="line"><span class="cl">- &#34;More features means more value.&#34; (Product knowledge shield)
</span></span><span class="line"><span class="cl">- &#34;I need to plan everything before I start.&#34; (Execution knowledge shield)
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">PHASE 5: BEHAVIORAL CALIBRATION
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">Define HOW the expert interacts with the learner, encoding CTT principles.
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">Build these behaviors into the prompt:
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">ADAPTIVE MODE SELECTION:
</span></span><span class="line"><span class="cl">- When the learner brings a situation and seems experienced: Ask them to explain their current thinking first. Surface their frame. Then work with it — validate what&#39;s sound, probe what&#39;s shaky, challenge what&#39;s wrong.
</span></span><span class="line"><span class="cl">- When the learner brings a situation and is clearly new: Offer a frame. &#34;Here&#39;s how I&#39;d think about this.&#34; Then ask questions that help them engage with and pressure-test that frame. Don&#39;t just lecture.
</span></span><span class="line"><span class="cl">- When it&#39;s unclear: Ask. &#34;Before I share how I&#39;d approach this — what&#39;s your current read on the situation? Or would you prefer I lay out a frame first?&#34;
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">FEEDBACK APPROACH:
</span></span><span class="line"><span class="cl">- Default to process feedback: explain WHY reasoning leads where it does, not just whether the conclusion is right or wrong.
</span></span><span class="line"><span class="cl">- When the learner reaches a conclusion, sometimes ask &#34;What makes you confident in that?&#34; before confirming or correcting. This builds their self-assessment ability.
</span></span><span class="line"><span class="cl">- Resist the urge to give the answer immediately. Ask &#34;What do you notice?&#34; or &#34;What feels off about this?&#34; before revealing what the expert sees. This trains the learner&#39;s perception.
</span></span><span class="line"><span class="cl">- When correcting, name the knowledge shield: &#34;A lot of people at your stage believe X because Y — but here&#39;s what actually happens and why.&#34;
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">MAKING EXPERT REASONING VISIBLE:
</span></span><span class="line"><span class="cl">- When analyzing a situation, narrate the cues you&#39;re attending to and why. &#34;The first thing I notice is... and that tells me... because in this domain, that pattern usually means...&#34;
</span></span><span class="line"><span class="cl">- Show the causal chain, not just the conclusion. &#34;If you do X, here&#39;s what I&#39;d expect to happen, and here&#39;s the mechanism.&#34;
</span></span><span class="line"><span class="cl">- When relevant, share what you would NOT pay attention to and why. Negative cues (knowing what to ignore) are a hallmark of expertise.
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">SENSEMAKING PRESERVATION:
</span></span><span class="line"><span class="cl">- Periodically ask the learner to summarize what they&#39;ve taken away. This isn&#39;t a quiz — it&#39;s a chance to catch misunderstandings before they calcify.
</span></span><span class="line"><span class="cl">- After working through a situation, ask: &#34;If you encountered something similar but slightly different — say [variation] — how would you approach it?&#34; This tests transfer.
</span></span><span class="line"><span class="cl">- Encourage the learner to form their own heuristics: &#34;Based on what we&#39;ve discussed, what rule of thumb would you take away from this?&#34;
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">PHASE 6: PROMPT GENERATION
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">Once all phases are complete, generate the system prompt. Structure it as:
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">```
</span></span><span class="line"><span class="cl">IDENTITY AND ROLE
</span></span><span class="line"><span class="cl">[Who the expert is, their background, their perspective]
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">THEORETICAL FRAMEWORK
</span></span><span class="line"><span class="cl">[Brief encoding of NDM/CTT principles — the expert doesn&#39;t explain the theory to the learner, but follows it]
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">MENTAL MODELS
</span></span><span class="line"><span class="cl">[The 4-8 core causal beliefs, written in first person as the expert&#39;s internal logic]
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">PERCEPTUAL CUES
</span></span><span class="line"><span class="cl">[What the expert notices first and why]
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">KNOWLEDGE SHIELDS
</span></span><span class="line"><span class="cl">[Common incorrect beliefs to watch for and surface]
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">INTERACTION PROTOCOL
</span></span><span class="line"><span class="cl">[Adaptive mode selection, feedback approach, reasoning visibility, sensemaking preservation — all from Phase 5]
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">LEARNER CONTEXT
</span></span><span class="line"><span class="cl">[What you know about this specific learner — their level, goals, active vs. theoretical, what they&#39;ve tried]
</span></span><span class="line"><span class="cl">```
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">After generating, tell the user: &#34;Before you deploy this, let me run a diagnostic. I&#39;ll simulate a short conversation between a learner and this expert so you can see how it behaves. I&#39;ll then evaluate whether the expert is following CTT principles correctly.&#34;
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">PHASE 7: DIAGNOSTIC SIMULATION
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">Run a 4-6 turn simulated conversation. Play both the learner (using a realistic scenario the user might bring) and the expert (using the generated prompt).
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">After the simulation, evaluate against these criteria:
</span></span><span class="line"><span class="cl">- Did the expert assess experience level before diving in?
</span></span><span class="line"><span class="cl">- Did it surface the learner&#39;s frame when appropriate?
</span></span><span class="line"><span class="cl">- Did it offer a frame with questions (not just answers) when the learner was new to the topic?
</span></span><span class="line"><span class="cl">- Did it give process feedback rather than just outcome feedback?
</span></span><span class="line"><span class="cl">- Did it make cues and causal reasoning visible?
</span></span><span class="line"><span class="cl">- Did it hold back appropriately to preserve sensemaking?
</span></span><span class="line"><span class="cl">- Did it test for transfer or ask the learner to form their own heuristic?
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">Report findings to the user. Propose revisions if needed. Iterate until the prompt passes.
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">FINAL STEP
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">Present the final prompt in a code block for easy copying. Remind the user:
</span></span><span class="line"><span class="cl">- Test the expert with a real question in their first session
</span></span><span class="line"><span class="cl">- Come back to this master project if the expert doesn&#39;t feel right — you can refine based on their experience
</span></span><span class="line"><span class="cl">- The expert will improve as the learner adds context over multiple sessions (Claude Projects retain conversation history within a session)
</span></span><span class="line"><span class="cl">- Consider adding domain-specific documents to the Project&#39;s knowledge base to give the expert richer material to draw from
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">META-INSTRUCTIONS
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">- Be conversational throughout. This is a collaborative design process, not a form to fill out.
</span></span><span class="line"><span class="cl">- If the user provides information that spans multiple phases, acknowledge it and weave it in — don&#39;t force them to repeat themselves.
</span></span><span class="line"><span class="cl">- If the user is unsure about something (like what mental models matter in a domain), help them think through it. You have broad knowledge you can draw on, even though you&#39;re not the domain expert being built.
</span></span><span class="line"><span class="cl">- Keep the user oriented: occasionally say where you are in the process and what&#39;s coming next.
</span></span><span class="line"><span class="cl">- If the user wants to skip ahead or modify the process, accommodate. The phases are a guide, not a cage.</span></span></code></pre></div>
</div>
</details>
<p>I have since then created a vibe coding expert. To give it more context, since many of the concepts are outside the knowledge/training window, I prompted it to learn about Gas Town by searching (the intro post + emergency manual) as well as the vibe coding book by Gene and Steve and people&rsquo;s reactions to it online.</p>
<p>Into a project the prompt went. Our first conversation was about creating principles for coding in Go. I realized I was looking too narrowly, only focusing on tests. I am feeling the need to look at the output because I don&rsquo;t trust the system around the code. And I don&rsquo;t trust the output because I don&rsquo;t have enough validations that I trust. And to get there, I will need a combination of linters and skills.</p>
<p><strong>Note:</strong> The problem with building an expert prompt like this is that as a non-expert <a href="/crumb/2026-02-28-you-dont-know-what-you-dont-know/">you don&rsquo;t know what you don&rsquo;t know</a>, so here be dragons.</p>
<p>Next up, I&rsquo;ll start building <code>bjornstack</code>, which will be my opinions and guard rails for the genie as it&rsquo;s working on my behalf. Coincidentally it shortens to <code>bs</code> which will make it easier for others to reference it proper 😉</p>]]></content:encoded></item><item><link>https://bjorn.now/scrap/2026-02-27t125324/?utm_medium=feed</link><pubDate>Fri, 27 Feb 2026 12:53:24 +0100</pubDate><guid>https://bjorn.now/scrap/2026-02-27t125324/</guid><description>autocorrect made ’teater’ (swedish, you can guess the word) into ’tester’ and somehow that seems correct for me, even if it was ’teater’ this time</description><content:encoded><![CDATA[<p>autocorrect made &rsquo;teater&rsquo; (swedish, you can guess the word) into &rsquo;tester&rsquo; and somehow that seems correct for me, even if it was &rsquo;teater&rsquo; this time</p>
]]></content:encoded></item><item><title>How Steve Yegge gets quality when vibing</title><link>https://bjorn.now/crumb/2026-02-26-how-steve-yegge-gets-quality-when-vibing/?utm_medium=feed</link><pubDate>Thu, 26 Feb 2026 17:44:42 +0100</pubDate><guid>https://bjorn.now/crumb/2026-02-26-how-steve-yegge-gets-quality-when-vibing/</guid><description>I have been wondering how Steve Yegge gets quality when vibing and this seems to be how, from the emergency manual Tending the Invisible Garden:
When you work with Gas Town, you don’t usually have time to inspect the code you’re creating. That’s not your role. But you need to make sure the code meets your quality bar. How do you ensure your garden is healthy if you can’t see it?</description><content:encoded><![CDATA[<p>I have been wondering how Steve Yegge gets quality when vibing and this seems to be how, from the emergency manual <a href="https://steve-yegge.medium.com/gas-town-emergency-user-manual-cf0e4556d74b#7804">Tending the Invisible Garden</a>:</p>
<blockquote>
<p>When you work with Gas Town, you don’t usually have time to inspect the code you’re creating. That’s not your role. But you need to make sure the code meets your quality bar. How do you ensure your garden is healthy if you can’t see it?</p>
<p>The answer: <strong>regular code review sweeps, followed by bug-fix sweeps</strong> that fix the issues filed during the code-review sweeps. Gas Town excels at both of these. It can generate tons of work with a swarm (filing Beads as it finds problems) and then crank through tons of work with another swarm. You just keep doing this until the code reviews are just nitpicking, or the agents say the code is ready to ship. Do some of this every day, and hope that most of the time you don’t find anything bad. The only way to be sure is to do it all the time.</p>
<p>Your garden can get diseases. I mentioned “heresies” above. Agents are very approximate workers and they like to guess at stuff. They will often make wrong guesses about how your system is supposed to work. If that wrong guess makes it into the code, sneaking through the review process, then it becomes enshrined and other agents may notice it and propagate the heresy in their own work.</p>
<p>“Idle polecats” is an example of a heresy that plagues Gas Town. There is no such thing as an idle polecat; it’s not a pool, and they vanish when their work is done. But polecats do have long-term identities, so it’s more like they are clocking out and leaving the building between jobs, which is harder for agents to wrap their heads around. So “idle polecats” make it back into the code base, comments, and docs all the time.</p>
<p>I’ve found the most helpful way to rid yourself of persistent heresies is to capture your guiding principles in the agent priming (onboarding). Which means you have to come up with some guiding principles in the first place.</p>
<p>Your core principles or axioms will be different for every project you’re working on. But the more coverage you can get with them, the more classes of heresy you can avoid or easily correct simply by pointing at the principle they violate. Gas Town core principles include things like Zero Framework Cognition (shared with Beads), which I’ve written about, GUPP, MEOW, Discovery over Tracking, Beads as the Universal Data Plane, and so on. All of these help me stamp out heresies that try to creep into my code.</p>
</blockquote>
<p>I&rsquo;ve been guessing that philosophy/principles is the core piece, and that I need a good way of encoding so genies use &rsquo;em. I had already started iterating by creating a skill to review Go tests based on my <a href="https://speakerdeck.com/gaqzi/how-to-design-a-test-suite-youll-love-to-maintain">How to design a test suite you&rsquo;ll love to maintain</a> and <a href="https://speakerdeck.com/gaqzi/mocking-your-codebase-without-cursing-it">Mocking your codebase without cursing it</a> presentations, and I rarely have feedback to Claude on the tests anymore.</p>
<p>I still have plenty of disagreements on package design, naming, etc., and I haven&rsquo;t worked on encoding my preferences, or principles, into something reusable there. Luckily, I&rsquo;ve been taking notes on what I&rsquo;ve disagreed with the agent about, so it&rsquo;ll make it easier to start having the conversation to create the skill. When I have quota again.</p>
<p>Separately, I have been experimenting with Gas Town today, and it has chewed through my Claude Max 5x quota in an hour each time. I was warned, so I&rsquo;m not complaining. The token furnace is real, and I might upgrade my subscription to shovel more.</p>
<p>Gas Town asked me to file a bug because it found a problem with beads (something about wisps?), so maybe that&rsquo;s why it has churned through the quota burn? Probably not.</p>
<p>Meanwhile, I&rsquo;ll start digging into how Steve has tried to encode his philosophies into Gas Town. Pilfering gloves on.</p>
]]></content:encoded></item><item><title>You don't have to symlink git hooks, move the hooks folder to your scripts</title><link>https://bjorn.now/til/2026-02-26-you-can-move-gits-hook-folder/?utm_medium=feed</link><pubDate>Thu, 26 Feb 2026 14:35:58 +0100</pubDate><guid>https://bjorn.now/til/2026-02-26-you-can-move-gits-hook-folder/</guid><description>Use git config core.hooksPath script/hooks and it’ll use the scripts in script/hooks as hooks in the git project. Version controlled, and no need to update symlinks as scripts come and go.</description><content:encoded><![CDATA[<p>Use <code>git config core.hooksPath script/hooks</code> and it&rsquo;ll use the scripts in <code>script/hooks</code> as hooks in the git project. Version controlled, and no need to update symlinks as scripts come and go.</p>
<p>The way I have historically managed the hooks is by having a <code>script/bootstrap</code> that symlinks scripts, and then hopefully my team mates run bootstrap when something changes.</p>
<p>So this is a nice way of making that entire problem go away.</p>]]></content:encoded></item><item><link>https://bjorn.now/scrap/2026-02-26t130430/?utm_medium=feed</link><pubDate>Thu, 26 Feb 2026 13:04:30 +0100</pubDate><guid>https://bjorn.now/scrap/2026-02-26t130430/</guid><description>heh, there’s a warning about gas town burning through tokens… this walk around shops in Karlstad is sponsored by the 2pm quota reset</description><content:encoded>&lt;p>heh, there&amp;rsquo;s a warning about gas town burning through tokens… this walk around shops in Karlstad is sponsored by the 2pm quota reset&lt;/p>
</content:encoded></item><item><link>https://bjorn.now/scrap/2026-02-26t113628/?utm_medium=feed</link><pubDate>Thu, 26 Feb 2026 11:36:29 +0100</pubDate><guid>https://bjorn.now/scrap/2026-02-26t113628/</guid><description>how often I typo ‘gt’ to ‘git’? trending to 1</description><content:encoded><![CDATA[<p>how often I typo &lsquo;gt&rsquo; to &lsquo;git&rsquo;? trending to 1</p>
]]></content:encoded></item><item><title>Becoming More by Martijn Faassen</title><link>https://bjorn.now/link/2026-02-25-becoming-more-by-martijn-faassen/?utm_medium=feed</link><pubDate>Wed, 25 Feb 2026 17:17:24 +0100</pubDate><guid>https://bjorn.now/link/2026-02-25-becoming-more-by-martijn-faassen/</guid><description>Becoming More by Martijn Faassen:
I want to admit that this AI/human me, this centaur me – my human understanding is on occasion limited. I lean on the AI quite a bit. Perhaps it isn’t genuine understanding. A tricky question in epistemology. Let’s not even talk about the AI understanding something, or not.
I do know this. I’ve always leaned on other people’s understanding - I’ve reused computers, programming languages, software libraries, implemented algorithms from books and wikipedia. I’ve used search engines. This is more broad than software development. I am limited and don’t understand everything about the world as it’s far, far too much. I stand on the shoulders of giants.</description><content:encoded><![CDATA[<p><a href="https://blog.startifact.com/posts/becoming-more/">Becoming More</a> by Martijn Faassen:</p>
<blockquote>
<p>I want to admit that this AI/human me, this centaur me &ndash; my human understanding is on occasion limited. I lean on the AI quite a bit. Perhaps it isn&rsquo;t genuine understanding. A tricky question in epistemology. Let&rsquo;s not even talk about the AI understanding something, or not.</p>
<p>I do know this. I&rsquo;ve always leaned on other people&rsquo;s understanding - I&rsquo;ve reused computers, programming languages, software libraries, implemented algorithms from books and wikipedia. I&rsquo;ve used search engines. This is more broad than software development. I am limited and don&rsquo;t understand everything about the world as it&rsquo;s far, far too much. I stand on the shoulders of giants.</p>
<p>And that&rsquo;s okay. I&rsquo;m still empowered.</p>
<p>Let this empowerment sink in. Have you realized what this means? Have you realized what you can do now as a software developer?</p>
<p>I can use AI to do more. Not be more productive, that&rsquo;s boring and that&rsquo;s debatable.</p>
</blockquote>
<p>Amen Martijn. I saw someone saying that if the AI tools worked really well, then employers would be forbidding them because people would be using them in order to work less. Because if you have an expectation of what could get done, then if you find a way of getting it done faster so you can go home, isn&rsquo;t that okay? But for some group of employers that&rsquo;d be tantamount to stealing!!1one</p>
<p>Now, there is a counter to this argument, Steve Yegge says in <a href="https://steve-yegge.medium.com/the-ai-vampire-eda6e4f07163">The AI Vampire</a>:</p>
<blockquote>
<p>Now let’s look at Scenario B. You decide instead that you will only work for an hour a day, and aim to keep up with your peers using AI. On that heavily reduced workload, you manage to scrape by, and nobody notices.</p>
<p>In this scenario, you capture 100% of the value from your adopting AI.</p>
<p>In this scenario, your company goes out of business. I’m sorry, but your victory over The Man will be pyrrhic, because The Man is about to be kicked in The Balls, since with everyone slacking off, a competitor will take them out pretty fast.</p>
</blockquote>
<p>I don&rsquo;t know, maybe? I think there are certain types of jobs that will absolutely end up folding if they keep at current levels of work done without change. History shows sitting on an advantage without adjusting is not optimal for survival. And to be fair, Yegge&rsquo;s post is <em>against</em> the grind culture and about finding a balance augmented by AI and to not become a hollow shell that commands the genie.</p>
<p>At the same time, I&rsquo;ve started my own company, and I&rsquo;m doing my best to not grind like I did working for others. I do have some runway from that work, but I hope there&rsquo;ll be enough work coming in (or products built that makes money) that I&rsquo;ll make do.</p>
<p>Instead, I have some work that helps me stay afloat, and it has given me time where I can play and explore. Maybe I can leverage it soon, maybe it becomes a rusty tool in the toolbox, or the cornerstone for something new.</p>
<p>Either way, I&rsquo;m becoming more, as Martijn said.</p>
]]></content:encoded></item><item><title>Robby Russel - Most developers dont build new things</title><link>https://bjorn.now/link/2026-02-19-robby-russel-most-developers-dont-build-new-things/?utm_medium=feed</link><pubDate>Thu, 19 Feb 2026 09:58:42 +0100</pubDate><guid>https://bjorn.now/link/2026-02-19-robby-russel-most-developers-dont-build-new-things/</guid><description>Most developers don’t build new things by Robby Russell:
We inherit. We understand. We stabilize. We extend. We improve what we can without destabilizing what already works.
This kind of work rarely attracts attention. It looks like incremental improvement and steady compounding over time.
But if most of your career is going to be spent in the second act, then the real question isn’t whether you get to start something new.</description><content:encoded><![CDATA[<p><a href="https://robbyonrails.com/articles/2026/02/18/most-developers-dont-build-new-things/">Most developers don&rsquo;t build new things</a> by Robby Russell:</p>
<blockquote>
<p>We inherit. We understand. We stabilize. We extend. We improve what we can without destabilizing what already works.</p>
<p>This kind of work rarely attracts attention. It looks like incremental improvement and steady compounding over time.</p>
<p>But if most of your career is going to be spent in the second act, then the real question isn’t whether you get to start something new.</p>
<p>It’s whether what you inherit gets better because you were there.</p>
</blockquote>
<p>I fully agree with Robby, the big piece of what we do day-to-day is making sure we have a better tomorrow in the systems we&rsquo;re part of maintaining, and that&rsquo;s the lens we should be looking through.</p>
<p>For example, right now I&rsquo;ve been working on an old system that&rsquo;s great at what it does and the business is happy with it, but it&rsquo;s not much fun to maintain. I have been using genies to help me and it&rsquo;s been a world of difference in how long it&rsquo;ll take to work on.</p>
<p>Yet, I spent a couple of hours yesterday manually verifying a long list of numbers to ensure we didn&rsquo;t break any calculations or displays in the workflow I have been changing. Because the system is a legacy one per Michael Feather&rsquo;s definition: it <a href="https://understandlegacycode.com/blog/what-is-legacy-code-is-it-code-without-tests/">didn&rsquo;t have any tests</a> before I started working on it, and it&rsquo;s <a href="/blog/2025/07/24/your-name-is-still-on-it/">my name on it</a>, so as we&rsquo;re about to take it to production I need to do my best to ensure it will work. That&rsquo;s the work.</p>
]]></content:encoded></item><item><link>https://bjorn.now/scrap/2026-01-30t212552/?utm_medium=feed</link><pubDate>Fri, 30 Jan 2026 21:25:52 +0100</pubDate><guid>https://bjorn.now/scrap/2026-01-30t212552/</guid><description>3D printer arrived, unboxed, assembled, calibrated, and currently huffing and puffing at plastic because they be moist. 12 hours until I get to hit print 😭</description><content:encoded>&lt;p>3D printer arrived, unboxed, assembled, calibrated, and currently huffing and puffing at plastic because they be moist. 12 hours until I get to hit print 😭&lt;/p>
</content:encoded></item><item><link>https://bjorn.now/scrap/2026-01-29t144448/?utm_medium=feed</link><pubDate>Thu, 29 Jan 2026 14:44:49 +0100</pubDate><guid>https://bjorn.now/scrap/2026-01-29t144448/</guid><description>TIL: if DHL is sending two parcels from the same vendor, shipped at the same time, they might send it to two different places that are kilometers away from each other so you better make sure to set them to the same place. Relatedly, my new 3D printer has arrived, and at some point the filament will arrive at another distribution point</description><content:encoded>&lt;p>TIL: if DHL is sending two parcels from the same vendor, shipped at the same time, they might send it to two different places that are kilometers away from each other so you better make sure to set them to the same place. Relatedly, my new 3D printer has arrived, and at some point the filament will arrive at another distribution point&lt;/p>
</content:encoded></item><item><title>If n8n shows invalid date error it might be Because of missing timezone config</title><link>https://bjorn.now/til/2026-01-28-if-n8n-has-invalid-date-it-might-be-because-of-missing-timezone/?utm_medium=feed</link><pubDate>Wed, 28 Jan 2026 14:00:20 +0100</pubDate><guid>https://bjorn.now/til/2026-01-28-if-n8n-has-invalid-date-it-might-be-because-of-missing-timezone/</guid><description>Set GENERIC_TIMEZONE for n8n or you’ll get ERROR: You specified an invalid date. when you try to publish a time triggered workflow.
I had done a server migration and completely missed the .env file when copying, and after recreating it all started working.</description><content:encoded><![CDATA[<p>Set <code>GENERIC_TIMEZONE</code> for n8n or you&rsquo;ll get <code>ERROR: You specified an invalid date.</code> when you try to publish a time triggered workflow.</p>
<p>I had done a server migration and completely missed the <code>.env</code> file when copying, and <a href="https://docs.n8n.io/hosting/installation/server-setups/docker-compose/#4-create-an-env-file">after recreating it</a> all started working.</p>
]]></content:encoded></item><item><title>Relocating Rigor by Chad Fowler</title><link>https://bjorn.now/link/2026-01-28-relocating-rigor-by-chad-fowler/?utm_medium=feed</link><pubDate>Wed, 28 Jan 2026 12:30:45 +0100</pubDate><guid>https://bjorn.now/link/2026-01-28-relocating-rigor-by-chad-fowler/</guid><description>Relocating Rigor by Chad Fowler (via Muthu).
Generative systems only work if invariants are explicit rather than implicit. Interfaces must be real contracts, not incidental boundaries. Evaluation must be ruthless. Failures must be loud and immediate. The engineer’s job shifts from typing code to specifying intent and verifying outcomes.
This is very much how I’m currently thinking about working with AIs. I have been using genies to work in a legacy codebase (defined as no tests) I’m new to, and in a language and platform I have barely worked on, and I’m being useful.</description><content:encoded><![CDATA[<p><a href="https://aicoding.leaflet.pub/3mbrvhyye4k2e">Relocating Rigor</a> by Chad Fowler (via Muthu).</p>
<blockquote>
<p>Generative systems only work if invariants are explicit rather than implicit. Interfaces must be real contracts, not incidental boundaries. Evaluation must be ruthless. Failures must be loud and immediate. The engineer&rsquo;s job shifts from typing code to specifying intent and verifying outcomes.</p>
</blockquote>
<p>This is very much how I&rsquo;m currently thinking about working with AIs. I have been using genies to work in a legacy codebase (defined as no tests) I&rsquo;m new to, and in a language and platform I have barely worked on, and I&rsquo;m being useful.</p>
<p>I think it&rsquo;s because I decided to introduce tests to the codebase, verify the existing behavior for the flow I&rsquo;m changing, and then write my new feature as a failing test. Then the genie has been able to iterate while I think about the changes and how they could fail.</p>
<p>For example, we had to move some columns into a new table, and while the code kept the <code>if null then 0</code> pattern that existed, I worried it might be overly &ldquo;safe&rdquo; and that it would return <code>0</code> because the relationship hadn&rsquo;t been loaded. So we traced through all the call sites to see that we also loaded that relationship from the DB, and the genie gave me a report I could follow and easily verify.</p>
]]></content:encoded></item><item><title>Claude Code sandbox exclusion requires both permissions.allow and sandbox.excludedCommands</title><link>https://bjorn.now/til/2026-01-10-claude-code-sandbox-exclusion-requires-both-permissions-allow-and-sandbox-excludedcommands/?utm_medium=feed</link><pubDate>Sat, 10 Jan 2026 14:30:41 +0100</pubDate><guid>https://bjorn.now/til/2026-01-10-claude-code-sandbox-exclusion-requires-both-permissions-allow-and-sandbox-excludedcommands/</guid><description>Add commands to both permissions.allow and sandbox.excludedCommands to run them outside Claude Code’s sandbox by default, because allow grants permission to run at all and excludedCommands runs them unsandboxed.
{ "permissions": { "allow": [ "Bash(pnpm test:unit:*)", "Bash(pnpm test:e2e:*)" ] }, "sandbox": { "excludedCommands": [ "pnpm test:unit:*", // Same commands, different purpose "pnpm test:e2e:*" ] } } Note: Tested with Claude Code v2.1.3</description><content:encoded><![CDATA[<p>Add commands to both <code>permissions.allow</code> and <code>sandbox.excludedCommands</code> to run them outside <a href="https://code.claude.com/docs/en/settings#sandbox-settings">Claude Code&rsquo;s sandbox</a> by default, because <code>allow</code> grants permission to run at all and <code>excludedCommands</code> runs them unsandboxed.</p>
<div class=" copy-enabled">
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-javascript" data-lang="javascript"><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="s2">&#34;permissions&#34;</span><span class="o">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;allow&#34;</span><span class="o">:</span> <span class="p">[</span>
</span></span><span class="line"><span class="cl">      <span class="s2">&#34;Bash(pnpm test:unit:*)&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="s2">&#34;Bash(pnpm test:e2e:*)&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="p">]</span>
</span></span><span class="line"><span class="cl">  <span class="p">},</span>
</span></span><span class="line"><span class="cl">  <span class="s2">&#34;sandbox&#34;</span><span class="o">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;excludedCommands&#34;</span><span class="o">:</span> <span class="p">[</span>
</span></span><span class="line"><span class="cl">      <span class="s2">&#34;pnpm test:unit:*&#34;</span><span class="p">,</span>    <span class="c1">// Same commands, different purpose
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>      <span class="s2">&#34;pnpm test:e2e:*&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="p">]</span>
</span></span><span class="line"><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span></span></span></code></pre></div>
</div>
<p><strong>Note:</strong> Tested with Claude Code v2.1.3</p>]]></content:encoded></item><item><title>Write out colon subcommands explicitly because wildcard colons trigger prefix matching</title><link>https://bjorn.now/til/2026-01-10-write-out-colon-subcommands-explicitly-because-wildcard-colons-trigger-prefix-matching/?utm_medium=feed</link><pubDate>Sat, 10 Jan 2026 14:07:39 +0100</pubDate><guid>https://bjorn.now/til/2026-01-10-write-out-colon-subcommands-explicitly-because-wildcard-colons-trigger-prefix-matching/</guid><description>Write out colon subcommands explicitly for Claude Code permissions because test:* doesn’t match test:unit (prefix matching treats them as separate commands), and the fallback test* is too broad (matches testAndDestroyEnvironment).
For example:
{ "permissions": { "allow": [ "Bash(pnpm test:unit:*)", // Allows pnpm test:unit --arg etc. "Bash(pnpm test:e2e:*)", "Bash(pnpm test*)" // ❌ &lt;- will allow testAndDestroyEnvironment ] } } Note: Tested with Claude Code v2.1.3</description><content:encoded><![CDATA[<p>Write out colon subcommands explicitly for <a href="https://code.claude.com/docs/en/iam#tool-specific-permission-rules">Claude Code permissions</a> because <code>test:*</code> doesn&rsquo;t match <code>test:unit</code> (prefix matching treats them as separate commands), and the fallback <code>test*</code> is too broad (matches <code>testAndDestroyEnvironment</code>).</p>
<p>For example:</p>
<div class=" copy-enabled">
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-javascript" data-lang="javascript"><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="s2">&#34;permissions&#34;</span><span class="o">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;allow&#34;</span><span class="o">:</span> <span class="p">[</span>
</span></span><span class="line"><span class="cl">      <span class="s2">&#34;Bash(pnpm test:unit:*)&#34;</span><span class="p">,</span> <span class="c1">// Allows pnpm test:unit --arg etc.
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>      <span class="s2">&#34;Bash(pnpm test:e2e:*)&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="s2">&#34;Bash(pnpm test*)&#34;</span>        <span class="c1">// ❌ &lt;- will allow testAndDestroyEnvironment
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>    <span class="p">]</span>
</span></span><span class="line"><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span></span></span></code></pre></div>
</div>
<p><strong>Note:</strong> Tested with Claude Code v2.1.3</p>]]></content:encoded></item><item><link>https://bjorn.now/scrap/2026-01-08t072647/?utm_medium=feed</link><pubDate>Thu, 08 Jan 2026 07:20:47 +0100</pubDate><guid>https://bjorn.now/scrap/2026-01-08t072647/</guid><description> I hoped to be blazing new trails professionally and not because I was dropping the niece off at daycare after 10-20cm of snow (the wind drift makes it hard to estimate)</description><content:encoded><![CDATA[<figure><img src="/img/2026/01-walking-to-bus-stop.jpeg"
    alt="A trail of footprints through deep, fresh snow, each step breaking through the drift. A snow-covered shoe is visible at the bottom of the frame.">
</figure>

<p>I hoped to be blazing new trails professionally and not because I was dropping the niece off at daycare after 10-20cm of snow (the wind drift makes it hard to estimate)</p>
]]></content:encoded></item><item><link>https://bjorn.now/scrap/2026-01-06t111206/?utm_medium=feed</link><pubDate>Tue, 06 Jan 2026 11:12:07 +0100</pubDate><guid>https://bjorn.now/scrap/2026-01-06t111206/</guid><description>well, relearning not to run the kettle when just turning on the dishwasher also taught me that my NAS is on the same circuit, guess I should move it to be plugged in on another breaker before it breaks 😅</description><content:encoded>&lt;p>well, relearning &lt;a href="/scrap/2025-09-28t090543/">not to run the kettle when just turning on the dishwasher&lt;/a> also taught me that my NAS is on the same circuit, guess I should move it to be plugged in on another breaker before it breaks 😅&lt;/p>
</content:encoded></item><item><link>https://bjorn.now/scrap/2025-12-05t194825/?utm_medium=feed</link><pubDate>Fri, 05 Dec 2025 19:48:25 +0100</pubDate><guid>https://bjorn.now/scrap/2025-12-05t194825/</guid><description>borrowed a full-sized Swedish keyboard for the new PC, and I swear, my fingers hurt. Contorting to hit special chars and stretching all over… Ordered a TKL (sans numpad) Keychron with ANSI/US layout and I’ll just figure out how to type Swedish in Windows as well as I do on my mac 😅 now… colemak?</description><content:encoded>&lt;p>borrowed a full-sized Swedish keyboard for the new PC, and I swear, my fingers hurt. Contorting to hit special chars and stretching all over… Ordered a TKL (sans numpad) Keychron with ANSI/US layout and I&amp;rsquo;ll just figure out how to type Swedish in Windows as well as I do on my mac 😅 now… colemak?&lt;/p>
</content:encoded></item><item><link>https://bjorn.now/scrap/2025-12-04t215533/?utm_medium=feed</link><pubDate>Thu, 04 Dec 2025 21:55:33 +0100</pubDate><guid>https://bjorn.now/scrap/2025-12-04t215533/</guid><description>also, very impressed by tailscale and how easy it makes creating my own “enterprise-like WAN” where all my machines can talk to each other. I set it up on the new machine and left it in the coworking space and it’s just accessible now that I’m home if inspiration strikes (work/life balance? pff 😉)</description><content:encoded><![CDATA[<p>also, very impressed by tailscale and how easy it makes creating my own &ldquo;enterprise-like WAN&rdquo; where all my machines can talk to each other. I set it up on the new machine and left it in the coworking space and it&rsquo;s just accessible now that I&rsquo;m home if inspiration strikes (work/life balance? pff 😉)</p>
]]></content:encoded></item><item><link>https://bjorn.now/scrap/2025-12-04t214746/?utm_medium=feed</link><pubDate>Thu, 04 Dec 2025 21:47:49 +0100</pubDate><guid>https://bjorn.now/scrap/2025-12-04t214746/</guid><description>aw hell, switching to a dedicated windows machine cut my iteration time down drastically: browser e2e test went 5m -&gt; 1m. virtualized OS + translation from my arm host to x64 because of old .NET seems to have been quite the culprit for slowness 😅</description><content:encoded>&lt;p>aw hell, switching to a dedicated windows machine cut my iteration time down drastically: browser e2e test went 5m -&amp;gt; 1m. virtualized OS + translation from my arm host to x64 because of old .NET seems to have been quite the culprit for slowness 😅&lt;/p>
</content:encoded></item><item><link>https://bjorn.now/scrap/2025-12-04t165903/?utm_medium=feed</link><pubDate>Thu, 04 Dec 2025 16:59:03 +0100</pubDate><guid>https://bjorn.now/scrap/2025-12-04t165903/</guid><description>I’m installing Windows on a new machine for the first time in forever and during installation I had to use my other machine to download drivers so it could connect to the internet and continue. As a mac user it surprises me but I guess it shouldn’t. Maybe this Intel WiFi 7 card is just too new…</description><content:encoded><![CDATA[<p>I&rsquo;m installing Windows on a new machine for the first time in forever and during installation I had to use my other machine to download drivers so it could connect to the internet and continue. As a mac user it surprises me but I guess it shouldn&rsquo;t. Maybe this Intel WiFi 7 card is just too new…</p>
]]></content:encoded></item><item><link>https://bjorn.now/scrap/2025-11-05t102655/?utm_medium=feed</link><pubDate>Wed, 05 Nov 2025 10:26:56 +0100</pubDate><guid>https://bjorn.now/scrap/2025-11-05t102655/</guid><description>when doing research on how to format stuff and you’re looking at UN CEFACT codes, have you dug too deep? https://github.com/schemaorg/schemaorg/wiki/Using-UN-CEFACT-Codes</description><content:encoded><![CDATA[<p>when doing research on how to format stuff and you&rsquo;re looking at UN CEFACT codes, have you dug too deep? <a href="https://github.com/schemaorg/schemaorg/wiki/Using-UN-CEFACT-Codes">https://github.com/schemaorg/schemaorg/wiki/Using-UN-CEFACT-Codes</a></p>
]]></content:encoded></item><item><link>https://bjorn.now/scrap/2025-11-04t211054/?utm_medium=feed</link><pubDate>Tue, 04 Nov 2025 21:10:54 +0100</pubDate><guid>https://bjorn.now/scrap/2025-11-04t211054/</guid><description>majorly nerdsniped myself today by fixing bugs that were easy to manually fix, but, now I have fully refactored my script and fixed the problem 😅</description><content:encoded>&lt;p>majorly nerdsniped myself today by fixing bugs that were easy to manually fix, but, now I have fully refactored my script and fixed the problem 😅&lt;/p>
</content:encoded></item><item><title>Use Jetbrains IntelliJ Source Roots to strip prefixes from copied paths</title><link>https://bjorn.now/til/2025-11-04-use-jetbrains-intellij-source-roots-to-strip-prefixes-from-copied-paths/?utm_medium=feed</link><pubDate>Tue, 04 Nov 2025 12:13:45 +0100</pubDate><guid>https://bjorn.now/til/2025-11-04-use-jetbrains-intellij-source-roots-to-strip-prefixes-from-copied-paths/</guid><description>Marking the content/ folder for your Hugo site as a “Source Root” in IntelliJ lets you use Path from Source Root to get the path to the file without the content/ prefix you don’t need when linking internally.
To make linking internally easier, I created an Alfred app snippet ({{&lt; relref “{clipboard}” &gt;}}), so now I only have to open the other post, double-tap shift, type “source root”, then run the snippet, and all linked up.</description><content:encoded><![CDATA[<p>Marking the <code>content/</code> folder for your Hugo site as a &ldquo;Source Root&rdquo; in IntelliJ lets you use <code>Path from Source Root</code> to get the path to the file without the <code>content/</code> prefix you don&rsquo;t need when linking internally.</p>
<p>To make linking internally easier, I created <a href="/blog/2024/04/15/day-to-day-automation-using-alfred/#text-replacement--snippets">an Alfred app snippet</a> (<code>{{&lt; relref &ldquo;{clipboard}&rdquo; &gt;}}</code>), so now I only have to open the other post, double-tap shift, type &ldquo;source root&rdquo;, then run the snippet, and all linked up.</p>
<figure><img src="/img/2025/11-intellij-copy-path.png"
    alt="IntelliJ context menu showing Copy Path/Reference options with &#39;Path from Source Root&#39; highlighted"><figcaption>
      <p>Right-click folder → Mark Directory as → Sources Root.<br>To copy path: open file and search &lsquo;source root&rsquo;, or right-click → Copy Path/Reference → Path from Source Root</p>
    </figcaption>
</figure>

]]></content:encoded></item><item><link>https://bjorn.now/scrap/2025-11-04t112614/?utm_medium=feed</link><pubDate>Tue, 04 Nov 2025 11:26:14 +0100</pubDate><guid>https://bjorn.now/scrap/2025-11-04t112614/</guid><description>three pokes until all blood was collected for tests today, par for getting blood out of my miserly veins. the nurse seemed amused by my warnings that I’m often a difficult case, then a bit relieved afterward that this was normal 😁</description><content:encoded>&lt;p>three pokes until all blood was collected for tests today, par for getting blood out of my miserly veins. the nurse seemed amused by my warnings that I&amp;rsquo;m often a difficult case, then a bit relieved afterward that this was normal 😁&lt;/p>
</content:encoded></item><item><link>https://bjorn.now/scrap/2025-11-04t085023/?utm_medium=feed</link><pubDate>Tue, 04 Nov 2025 08:50:23 +0100</pubDate><guid>https://bjorn.now/scrap/2025-11-04t085023/</guid><description>I keep getting surprised when overtaken by others while walking here in Sweden. Back in Singapore I was the one overtaking people. Then again, so many Swedes tower over me, and I at least cottaged over people in Singapore 😉</description><content:encoded>&lt;p>I keep getting surprised when overtaken by others while walking here in Sweden. Back in Singapore I was the one overtaking people. Then again, so many Swedes tower over me, and I at least cottaged over people in Singapore 😉&lt;/p>
</content:encoded></item><item><link>https://bjorn.now/scrap/2025-11-03t182853/?utm_medium=feed</link><pubDate>Mon, 03 Nov 2025 18:28:53 +0100</pubDate><guid>https://bjorn.now/scrap/2025-11-03t182853/</guid><description>a tea filter ball (one of those little mesh ball on a chain) and a Clever Dripper really works great for brewing oolong tea 😁</description><content:encoded>&lt;p>a tea filter ball (one of those little mesh ball on a chain) and a Clever Dripper really works great for brewing oolong tea 😁&lt;/p>
</content:encoded></item><item><title>Maintaining n8n inline Javascript as code …and more importantly: with unit tests</title><link>https://bjorn.now/devlog/2025-11-03-maintaining-n8n-inline-javascript-as-code/?utm_medium=feed</link><pubDate>Mon, 03 Nov 2025 15:04:43 +0100</pubDate><guid>https://bjorn.now/devlog/2025-11-03-maintaining-n8n-inline-javascript-as-code/</guid><description>In this devlog I’ll make a change in my n8n POSSE setup, that could’ve taken ~an hour if I kept doing it manually as I had, but instead I spent ~5 hours tidying and removing manual steps for the future.
The work itself was about making my “scraps,” that is this site’s tweets, be sent out raw to my syndication targets. I feel that sending a link to something you see in the preview, or it could be inline in the post, is a bit much, I should just let it show up in the platform itself.</description><content:encoded><![CDATA[<p>In this devlog I&rsquo;ll make a change in my n8n POSSE setup,
that could&rsquo;ve taken ~an hour if I kept doing it manually as I had,
but instead I spent ~5 hours tidying and removing manual steps for the future.</p>
<p>The work itself was about making my &ldquo;scraps,&rdquo; that is this site&rsquo;s tweets,
be sent out raw to my syndication targets.
I feel that sending a link to something you see in the preview,
or it could be inline in the post, is a bit much,
I should just let it show up in the platform itself.</p>
<p>I have a couple of things planned for this site,
and this was chosen as the first step
because it would force me to think about <em>how will I maintain</em> my n8n workflows.</p>
<p>So now, instead of writing some Javascript code, thinking really hard,
and then test it by tooting something out, I instead have some unit tests and
a way to programmatically update my workflow. So as I go down the rest of my
list of tasks I want to do working with n8n will be much smoother.</p>
<p>I had a couple of decisions to make,
so I listed them out using the <a href="https://www.cortexfutura.com/getting-started-algorithms-of-thought/#simple-choice-algorithm">Choice algorithm of thought</a>,
and as always it forced me to dig deeper into the problem,
even if I ended up doing what I guessed going in.
But, the benefit of this process is being intentional and explicit, it
gets me into the mode of &ldquo;I&rsquo;m trying to list out options,&rdquo; and to explain
what made me make the decision. Since this is now recorded in my notes
I can easily go back later to find why.</p>
<p>The kind of highlights of what I got done:</p>
<ul>
<li><a href="#updating-n8n-programmatically">Updating n8n programmatically</a>:
Extract the code from a Javascript source file (glorified <code>grep</code>),
and then wrap it so it&rsquo;ll work in n8n.</li>
<li><a href="#implementing-reusable-tests">Creating reusable tests in Javascript</a>:
which turned out to be a bad idea, because the code was the same but
diverged too much, so keeping the tests the same would&rsquo;ve needed too much
conditional logic.<br>
So I ended up copy/pasting instead of creating a maintenance nightmare.
This pattern is useful to know, though, I use it in other codebases where
the code is duplicated but some shared logic needs to stay the same.
For example, adapters for databases implementing a common interface.</li>
<li><a href="#update-telegram-logic">Telegram Instant Views</a>: Learning about and configuring.
Create an Instant View template for this site and learn a bit more about XPath.</li>
<li><a href="#understand-how-n8n-updates-work">n8n&rsquo;s update API</a>:
Dig into what logic there is calling the update workflow REST endpoint,
so I understand what I might break with how I decided to perform updates.
Created a guard-rail to try and catch if I&rsquo;m about to overwrite click-ops
changes I hadn&rsquo;t accounted for when changing locally.</li>
</ul>
<details>
    <summary>Table of contents</summary>

<nav id="TableOfContents">
  <ul>
    <li><a href="#background-and-scope-of-work">Background and scope of work</a></li>
    <li><a href="#coding-the-mastodon-logic">Coding the Mastodon logic</a>
      <ul>
        <li><a href="#designing-the-solution">Designing the solution</a></li>
        <li><a href="#backfilling-tests-for-the-existing-code">Backfilling tests for the existing code</a></li>
        <li><a href="#implementing-the-new-logic">Implementing the new logic</a></li>
      </ul>
    </li>
    <li><a href="#updating-n8n-programmatically">Updating n8n programmatically</a>
      <ul>
        <li><a href="#designing-the-extraction">Designing the extraction</a></li>
        <li><a href="#building-the-extractor">Building the extractor</a></li>
        <li><a href="#building-the-workflow-updater-script">Building the workflow updater script</a></li>
        <li><a href="#importing-the-updated-workflow">Importing the updated workflow</a></li>
        <li><a href="#validating-the-updated-workflow">Validating the updated workflow</a></li>
      </ul>
    </li>
    <li><a href="#repeat-for-bluesky">Repeat for Bluesky</a>
      <ul>
        <li><a href="#designing-the-change">Designing the change</a></li>
        <li><a href="#implementing-reusable-tests">Implementing reusable tests</a></li>
        <li><a href="#refactoring-after-working-tests">Refactoring after working tests</a></li>
        <li><a href="#testing-bsky-live">Testing bsky live</a></li>
      </ul>
    </li>
    <li><a href="#wednesday-eod">Wednesday EOD</a></li>
    <li><a href="#update-telegram-logic">Update Telegram logic</a>
      <ul>
        <li><a href="#configuring-instant-views">Configuring Instant Views</a></li>
        <li><a href="#using-instant-view-links-or-not">Using Instant View links or not?</a></li>
        <li><a href="#final-telegram-logic-and-decision">Final Telegram logic and decision</a></li>
      </ul>
    </li>
    <li><a href="#understand-how-n8n-updates-work">Understand how n8n updates work</a>
      <ul>
        <li><a href="#digging-into-the-source-code">Digging into the source code</a></li>
        <li><a href="#adding-only-update-if-version-is-what-i-expect-on-the-remote">Adding &ldquo;only update if version is what I expect on the remote&rdquo;</a></li>
      </ul>
    </li>
    <li><a href="#final-thoughts">Final thoughts</a></li>
  </ul>
</nav>
</details>

<h2 id="background-and-scope-of-work">
  Background and scope of work
  <a href="#background-and-scope-of-work" class="heading-anchor" aria-label="Direct link to Background and scope of work"><span aria-hidden="true">#</span></a>
</h2>
<p>I will (for now) not add images to scraps until I have native support for images in my posting (so they either only exist in a special content type, which gets linked, or they&rsquo;re attached when posting as a scrap).</p>
<p>I do this because I feel linking to these short-text things that really could just live on Masto/Bsky/etc. is a bit much, be a bit more native to them. Do use them to publish my other content but don&rsquo;t only send links when they&rsquo;re not necessary. It feels like being a bad citizen to them when it&rsquo;s only a sink that way.</p>
<p>The way I&rsquo;ve setup the sending in n8n right now, is that I have two steps in the status: 1) truncating the body and deciding if we need more, 2) and then adding the link, so I need to figure out a better maintenance pattern.</p>
<ul>
<li><strong>Choice:</strong> Manage n8n workflow with source management
<ul>
<li><strong>Constraints:</strong>
<ul>
<li>Be able to recreate my n8n setup if something goes belly-up</li>
<li>Keep as much as possible in source (but don&rsquo;t overdo it, I&rsquo;m a single person doing this and can manage myself)</li>
</ul>
</li>
<li><strong>Options:</strong>
<ul>
<li>Keep n8n fully clickops and keep code snippets in blog
<ul>
<li><strong>Advantages:</strong>
<ul>
<li>Super simple and what I&rsquo;m doing, the status quo choice</li>
</ul>
</li>
<li><strong>Disadvantages:</strong>
<ul>
<li>If I have to recreate the n8n setup from scratch, painful</li>
</ul>
</li>
</ul>
</li>
<li>Create the workflow file in my blog and upload it for changes
<ul>
<li><strong>Advantages:</strong>
<ul>
<li>All my n8n stuff is in my blog, so it&rsquo;s easy for me to deal with, and it&rsquo;s something I can easily share later so that&rsquo;s a win for sharing</li>
</ul>
</li>
<li><strong>Disadvantages:</strong>
<ul>
<li>More work to figure it out right now
<ul>
<li>But probably worth it, I&rsquo;ll download the workflow file and look at it to see what it says</li>
<li>looking at the JSON file it looks pretty straightforward, and using JQ I should be able to pretty easily inject what I need into it, so I can create a small file/script to help me out here that&rsquo;ll be worth it I think</li>
</ul>
</li>
</ul>
</li>
</ul>
</li>
</ul>
</li>
<li><strong>Decision:</strong> Create the workflow file in my blog and upload it for changes
<ul>
<li>It feels like a good step towards a maintainable setup and minimal effort to get it going</li>
</ul>
</li>
</ul>
</li>
</ul>
<p>Doing the choice here wasn&rsquo;t necessarily super useful, I kinda knew that I wanted to start storing the workflow in my repo, but the point was to step back and consider what I was doing. I was in my head starting to go down the path of just doing what I have been doing, it has been fine. But when I decided to look at the options &ldquo;the engineer in me&rdquo; reeled back enough that I felt that this time investment <em>will</em> be worth it.</p>
<p>Okay, the following jq will let me update a specific field in my workflow file, so I will have to make it do it &ldquo;in context&rdquo;, so that&rsquo;s a good start for what I&rsquo;m doing.</p>
<div class=" copy-enabled">
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-plaintext" data-lang="plaintext"><span class="line"><span class="cl">jq &#39;.nodes[] | select(.id == &#34;e1bd320b-1222-4324-9168-22372d2e667c&#34;) | .parameters.status |= &#34;m000&#34;&#39; src/n8n/bjorn.now-feed-distribution.json</span></span></code></pre></div>
</div>
<p>So I think I want my maintenance to be: full status to be handled in one javascript block, so I can extract that and use jq to update the json file.</p>
<p>I haven&rsquo;t decided how I will ensure the javascript and json are up to date, maybe I&rsquo;ll just do a makefile because it&rsquo;s easy for now? Or maybe I do a template that doesn&rsquo;t store the &ldquo;production data&rdquo; so that I never have the actual prod version checked in and only create it on the fly as I&rsquo;m updating to encourage avoiding the disconnect.
I&rsquo;ll decide later.</p>
<h2 id="coding-the-mastodon-logic">
  Coding the Mastodon logic
  <a href="#coding-the-mastodon-logic" class="heading-anchor" aria-label="Direct link to Coding the Mastodon logic"><span aria-hidden="true">#</span></a>
</h2>
<p>With a short lunch break I&rsquo;m now back at it, so let&rsquo;s start by creating the logic I want so that I can let JQ pick it up.</p>
<h3 id="designing-the-solution">
  Designing the solution
  <a href="#designing-the-solution" class="heading-anchor" aria-label="Direct link to Designing the solution"><span aria-hidden="true">#</span></a>
</h3>
<p>Worry: I&rsquo;m not sure exactly <em>how</em> I will inject this into JQ right now, will I do a full JS file? Extract it smarter and use JQ as a library since I anyway has setup Javascript in the blog…? I don&rsquo;t know, I&rsquo;ll look into that AFTER I have finished updating the code I want and verified it manually, then I&rsquo;ll do this.</p>
<p>Okay, so what do I have available in the current setup?</p>
<p>I get injected <code>$('RSS Feed Trigger').item</code> into my inline function, which contains all the information from the RSS feed, so that&rsquo;s perfect, that means I don&rsquo;t need to send in anything more, I just need to decide what to output.</p>
<p>To make my testing life a bit easier I will now go and take my test data and make that available in a test, so I can TDD this thing.</p>
<p>Looking at <a href="https://github.com/gaqzi/bjorn.now/blob/df4abc0a299906a8c357a6e4619ce01ab5d4c530/src/js/n8n.copies.js#L7-L31">my code that I&rsquo;ve used in mastodon</a> (I just remove the <code>let mastodon = </code> when using) I think it&rsquo;ll work well as-is, so let&rsquo;s start by backfilling some missing tests, so I have a clean place to work from, the only problem is that I&rsquo;m calling it as I should in mastodon because I was only using it manually, so that&rsquo;ll have to go.
For now, I&rsquo;ll remove the self-calling and figure out how to deal with this when I inject with jq, since it feels like as long as I can get the function definition I can very easily wrap it in a self-call with the param I need.</p>
<h3 id="backfilling-tests-for-the-existing-code">
  Backfilling tests for the existing code
  <a href="#backfilling-tests-for-the-existing-code" class="heading-anchor" aria-label="Direct link to Backfilling tests for the existing code"><span aria-hidden="true">#</span></a>
</h3>
<div class=" copy-enabled">
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-javascript" data-lang="javascript"><span class="line"><span class="cl"><span class="kd">let</span> <span class="nx">mastodon</span> <span class="o">=</span> <span class="p">((</span><span class="nx">item</span><span class="p">)</span> <span class="p">=&gt;</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="kd">let</span> <span class="nx">msg</span> <span class="o">=</span> <span class="nx">item</span><span class="p">.</span><span class="nx">json</span><span class="p">.</span><span class="nx">contentSnippet</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="nx">msgLength</span> <span class="o">=</span> <span class="mi">500</span><span class="o">-</span><span class="mi">23</span><span class="o">-</span><span class="mi">1</span><span class="p">,</span> <span class="c1">// the default mastodon post size is 500, -23 chars for the link, and -1 char for newline
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>        <span class="nx">isTruncated</span> <span class="o">=</span> <span class="nx">item</span><span class="p">.</span><span class="nx">json</span><span class="p">[</span><span class="s1">&#39;content:encodedSnippet&#39;</span><span class="p">]</span> <span class="o">!==</span> <span class="nx">item</span><span class="p">.</span><span class="nx">json</span><span class="p">.</span><span class="nx">contentSnippet</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="nx">truncateIndicator</span> <span class="o">=</span> <span class="s1">&#39;\n… more&#39;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="kd">let</span> <span class="nx">wordTruncate</span> <span class="o">=</span> <span class="p">(</span><span class="nx">str</span><span class="p">,</span> <span class="nx">n</span><span class="p">,</span> <span class="nx">indicator</span><span class="p">)</span> <span class="p">=&gt;</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="k">if</span> <span class="p">(</span><span class="nx">indicator</span> <span class="o">===</span> <span class="kc">undefined</span><span class="p">)</span> <span class="nx">indicator</span> <span class="o">=</span> <span class="s1">&#39;…&#39;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">        <span class="k">if</span><span class="p">(</span><span class="nx">str</span><span class="p">.</span><span class="nx">length</span> <span class="o">&lt;=</span> <span class="nx">n</span><span class="p">)</span> <span class="k">return</span> <span class="nx">str</span><span class="p">.</span><span class="nx">trimEnd</span><span class="p">();</span>
</span></span><span class="line"><span class="cl">        <span class="nx">n</span> <span class="o">-=</span> <span class="nx">indicator</span><span class="p">.</span><span class="nx">length</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">        <span class="k">if</span><span class="p">(</span><span class="nx">str</span><span class="p">[</span><span class="nx">n</span><span class="p">]</span> <span class="o">===</span> <span class="s1">&#39; &#39;</span><span class="p">)</span> <span class="k">return</span> <span class="nx">str</span><span class="p">.</span><span class="nx">substring</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="nx">n</span><span class="p">).</span><span class="nx">trimEnd</span><span class="p">();</span>
</span></span><span class="line"><span class="cl">        <span class="kd">let</span> <span class="nx">ret</span> <span class="o">=</span> <span class="nx">str</span><span class="p">.</span><span class="nx">substring</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="nx">n</span><span class="p">).</span><span class="nx">trimEnd</span><span class="p">(),</span>
</span></span><span class="line"><span class="cl">            <span class="nx">i</span> <span class="o">=</span> <span class="nx">ret</span><span class="p">.</span><span class="nx">lastIndexOf</span><span class="p">(</span><span class="s1">&#39; &#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">        <span class="c1">// when there&#39;s no spaces at all, just cut in the middle of the text.
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>        <span class="k">if</span> <span class="p">(</span><span class="nx">i</span> <span class="o">===</span> <span class="o">-</span><span class="mi">1</span><span class="p">)</span> <span class="k">return</span> <span class="nx">ret</span><span class="p">.</span><span class="nx">substring</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="nx">n</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">        <span class="k">return</span> <span class="nx">ret</span><span class="p">.</span><span class="nx">substring</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="nx">i</span><span class="p">).</span><span class="nx">trimEnd</span><span class="p">();</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl">    <span class="nx">msg</span> <span class="o">=</span> <span class="nx">wordTruncate</span><span class="p">(</span><span class="nx">msg</span><span class="p">,</span> <span class="nx">msgLength</span><span class="p">,</span> <span class="nx">truncateIndicator</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="nx">isTruncated</span> <span class="o">?</span> <span class="nx">msg</span> <span class="o">+</span> <span class="nx">truncateIndicator</span> <span class="o">:</span> <span class="nx">msg</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="p">})(</span><span class="nx">$</span><span class="p">(</span><span class="s1">&#39;RSS Feed Trigger&#39;</span><span class="p">).</span><span class="nx">item</span><span class="p">)</span></span></span></code></pre></div>
</div>
<p>Cool, I have a first test showing the standard behavior of not including any links, so let&rsquo;s continue backfilling everything and then I&rsquo;ll add in the rest.</p>
<p>That was only two tests, nothing much to write home about but this covers the behavior I have and it&rsquo;s enough to characterize it for me, so these are the changes I want to do now:</p>
<p>Output the link as well, on a newline of it&rsquo;s own.</p>
<h3 id="implementing-the-new-logic">
  Implementing the new logic
  <a href="#implementing-the-new-logic" class="heading-anchor" aria-label="Direct link to Implementing the new logic"><span aria-hidden="true">#</span></a>
</h3>
<p>So I&rsquo;ll do this first, and make sure it&rsquo;s done correctly for both tests, I will start by making both tests fail, because that feels like the best way.</p>
<p>If the URL contains <code>/scrap/</code> then only output link if truncated.</p>
<p>Done, the code is there, I have some tests, it&rsquo;s nothing too spectacular, but it does what I want.
I realize that I should install standard and run the formatter regularly because I&rsquo;m being inconsistent and I don&rsquo;t like it. <code>gofmt</code> and <code>black</code> has ruined me 😁.</p>
<h2 id="updating-n8n-programmatically">
  Updating n8n programmatically
  <a href="#updating-n8n-programmatically" class="heading-anchor" aria-label="Direct link to Updating n8n programmatically"><span aria-hidden="true">#</span></a>
</h2>
<p>Now to figure out how to merge this into my workflow file.</p>
<ul>
<li><strong>Choice:</strong> How to merge my javascript into my workflow file
<ul>
<li><strong>Constraints:</strong>
<ul>
<li>No manual steps</li>
<li>Need to return the function to run, and then transform it into a &ldquo;self-run&rdquo; (<code>((item) =&gt; { item.json.link })($('RSS Feed Trigger').item)</code>) so it&rsquo;ll work</li>
</ul>
</li>
<li><strong>Options:</strong>
<ul>
<li>Is there some easy AST stuff I can use with node to extract it?
<ul>
<li>Let&rsquo;s ask claude
<ul>
<li>
<div class=" copy-enabled">
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-plaintext" data-lang="plaintext"><span class="line"><span class="cl">is there a function in node to parse the AST of a javascript file and to get the content of a variable as a string?</span></span></code></pre></div>
</div>
</li>
</ul>
</li>
<li><strong>Advantages:</strong>
<ul>
<li>Exists, will be robust
<ul>
<li>I got back a code example but I&rsquo;m hesitant to pull in these dependencies
<ul>
<li>
<div class=" copy-enabled">
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-javascript" data-lang="javascript"><span class="line"><span class="cl"><span class="kr">const</span> <span class="nx">parser</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="s1">&#39;@babel/parser&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"><span class="kr">const</span> <span class="nx">traverse</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="s1">&#39;@babel/traverse&#39;</span><span class="p">).</span><span class="k">default</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="kr">const</span> <span class="nx">generate</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="s1">&#39;@babel/generator&#39;</span><span class="p">).</span><span class="k">default</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="kr">const</span> <span class="nx">fs</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="s1">&#39;fs&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"><span class="kr">const</span> <span class="nx">code</span> <span class="o">=</span> <span class="nx">fs</span><span class="p">.</span><span class="nx">readFileSync</span><span class="p">(</span><span class="s1">&#39;functions/processData.js&#39;</span><span class="p">,</span> <span class="s1">&#39;utf8&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"><span class="kr">const</span> <span class="nx">ast</span> <span class="o">=</span> <span class="nx">parser</span><span class="p">.</span><span class="nx">parse</span><span class="p">(</span><span class="nx">code</span><span class="p">,</span> <span class="p">{</span> <span class="nx">sourceType</span><span class="o">:</span> <span class="s1">&#39;module&#39;</span> <span class="p">});</span>
</span></span><span class="line"><span class="cl"><span class="kd">let</span> <span class="nx">functionCode</span> <span class="o">=</span> <span class="kc">null</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="nx">traverse</span><span class="p">(</span><span class="nx">ast</span><span class="p">,</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="nx">FunctionDeclaration</span><span class="p">(</span><span class="nx">path</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="p">(</span><span class="nx">path</span><span class="p">.</span><span class="nx">node</span><span class="p">.</span><span class="nx">id</span><span class="p">.</span><span class="nx">name</span> <span class="o">===</span> <span class="s1">&#39;processData&#39;</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">      <span class="nx">functionCode</span> <span class="o">=</span> <span class="nx">generate</span><span class="p">(</span><span class="nx">path</span><span class="p">.</span><span class="nx">node</span><span class="p">).</span><span class="nx">code</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl">  <span class="p">},</span>
</span></span><span class="line"><span class="cl">  <span class="nx">VariableDeclarator</span><span class="p">(</span><span class="nx">path</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="p">(</span><span class="nx">path</span><span class="p">.</span><span class="nx">node</span><span class="p">.</span><span class="nx">id</span><span class="p">.</span><span class="nx">name</span> <span class="o">===</span> <span class="s1">&#39;processData&#39;</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">      <span class="nx">functionCode</span> <span class="o">=</span> <span class="nx">generate</span><span class="p">(</span><span class="nx">path</span><span class="p">.</span><span class="nx">node</span><span class="p">.</span><span class="nx">init</span><span class="p">).</span><span class="nx">code</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">});</span>
</span></span><span class="line"><span class="cl"><span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="nx">functionCode</span><span class="p">);</span></span></span></code></pre></div>
</div>
</li>
</ul>
</li>
<li><strong>Disadvantages:</strong>
<ul>
<li>Huge set of dependencies for a small piece of work</li>
</ul>
</li>
</ul>
</li>
<li>Mark it up with text and do a text replacement so <code>let mastodon =</code> disappears
<ul>
<li><strong>Advantages:</strong>
<ul>
<li>Simple to implement</li>
</ul>
</li>
<li><strong>Disadvantages:</strong>
<ul>
<li>Possibly brittle. Will rely on me not changing the wrapping of the function
<ul>
<li>But, I will notice pretty quickly if I get that wrong</li>
</ul>
</li>
</ul>
</li>
</ul>
</li>
<li>Something else? Search and see how others solve this
<ul>
<li>Because I tend to get too carried away at just solving the problem and not look around enough, because honestly, I think just adding some text markers is enough for what I need and I&rsquo;m inclined to just do that, because it&rsquo;s good enough for my needs.</li>
<li>But, let&rsquo;s start by doing a quick search and also ask Claude to help me search, since it&rsquo;s usually good at coming up with unique search patterns</li>
<li>Prompt to Claude:
<div class=" copy-enabled">
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-plaintext" data-lang="plaintext"><span class="line"><span class="cl">I am working with n8n and want to maintain my workflow as code.
</span></span><span class="line"><span class="cl">My workflow uses custom javascript in some of the nodes so I have them stored on my filesystem and I want to extract the function in question and inject and run the function in my node.
</span></span><span class="line"><span class="cl">Are there existing solutions for doing this?
</span></span><span class="line"><span class="cl">My spontaneous reaction for how to solve it is:
</span></span><span class="line"><span class="cl">1. Add markers in text in my file
</span></span><span class="line"><span class="cl">2. Extract the content between those markers
</span></span><span class="line"><span class="cl">3. Strip the prefix/suffix around the function required for testing the function
</span></span><span class="line"><span class="cl">4. Wrap the remaining stuff in what I need to then use it in the workflow and inject it into a workflow template using jq</span></span></code></pre></div>
</div>
</li>
<li>Claude is questioning my approach as brittle, which is fair, it suggests I package it as a node module instead or some templating that injects the source code of a function. If I can just inject the source code of a function then that&rsquo;d be awesome, then I&rsquo;ll just make a little node script that outputs the correct thing for me
<ul>
<li>Right, so Claude dreamt up a suggested way of implementing my marker idea.
And a custom npm module would mean maintaining the code on the server and injecting it into n8n. It totally makes sense if I&rsquo;m doing more advanced stuff, but for my current situation, I think having a single file trumps that.</li>
</ul>
</li>
</ul>
</li>
</ul>
</li>
</ul>
</li>
<li><strong>Decision:</strong> Mark it up with text and do a text replacement so <code>let mastodon =</code> disappears
<ul>
<li>My use-case is so small, this feels like the pragmatic choice until I have more advanced needs</li>
<li>I wonder if I should just do all this work in javascript or if I should do what my gut says: a small bash script.</li>
<li>Quick overview in my head: upside of using javascript is that I can just load the json into a file really easy, and then output to stdout in a pretty-printed form.
The logic to process between the markers isn&rsquo;t that hard and I already have the testing setup</li>
<li>The other option is to do it in Python since the rest of my blog is in python. But since n8n is more js maybe it makes sense to keep this stuff in the one language</li>
<li>And if I keep it with JS then I don&rsquo;t have to deal with jq, I can just do the whole thing in one pass by in JS. I think that&rsquo;ll be easier.</li>
</ul>
</li>
</ul>
</li>
</ul>
</li>
</ul>
<h3 id="designing-the-extraction">
  Designing the extraction
  <a href="#designing-the-extraction" class="heading-anchor" aria-label="Direct link to Designing the extraction"><span aria-hidden="true">#</span></a>
</h3>
<p>Okay, so what I&rsquo;ll do is create a little helper script that.</p>
<ol>
<li>I can point at a js file and it&rsquo;ll extract out the marker I have specified,.</li>
<li>Using the content of the marker, change it slightly so it&rsquo;ll work in place (make it self-calling)</li>
<li>Load the n8n workflow file, replace the key in the right place.</li>
<li>Pretty-print the json to STDOUT.</li>
</ol>
<p>I&rsquo;ll make the marker format <code>//&lt;EXTRACT:name&gt;</code>, the go convention of programmatic stuff doesn&rsquo;t start with a space and all :p.</p>
<h3 id="building-the-extractor">
  Building the extractor
  <a href="#building-the-extractor" class="heading-anchor" aria-label="Direct link to Building the extractor"><span aria-hidden="true">#</span></a>
</h3>
<p>I wrote up my first tests, ensured the first &ldquo;do nothing&rdquo; test passes, and now with my second test in place I&rsquo;m gonna ask Claude to implement it, because I don&rsquo;t know these node APIs. And claude did that quickly and the implementation seems sane. Claude decided to wrap everything so no errors would bubble up, so I removed that <code>catch</code> because any errors should bubble up because I don&rsquo;t want to miss out.</p>
<div class=" copy-enabled">
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-javascript" data-lang="javascript"><span class="line"><span class="cl"><span class="nx">describe</span><span class="p">(</span><span class="s1">&#39;extract.fromFile&#39;</span><span class="p">,</span> <span class="p">()</span> <span class="p">=&gt;</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="nx">test</span><span class="p">(</span><span class="sb">`if the markers aren&#39;t found, then nothing is returned`</span><span class="p">,</span> <span class="p">()</span> <span class="p">=&gt;</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="kd">var</span> <span class="nx">actual</span> <span class="o">=</span> <span class="nx">extract</span><span class="p">.</span><span class="nx">fromFile</span><span class="p">(</span><span class="s1">&#39;testdata/no-markers.txt&#39;</span><span class="p">,</span> <span class="s1">&#39;test&#39;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="nx">assert</span><span class="p">.</span><span class="nx">strictEqual</span><span class="p">(</span><span class="nx">actual</span><span class="p">,</span> <span class="s1">&#39;&#39;</span><span class="p">,</span> <span class="s1">&#39;expected an empty string when no match found&#39;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="p">})</span>
</span></span><span class="line"><span class="cl">    <span class="nx">test</span><span class="p">(</span><span class="sb">`if the markers are found, the content is returned`</span><span class="p">,</span> <span class="p">()</span> <span class="p">=&gt;</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="kd">var</span> <span class="nx">actual</span> <span class="o">=</span> <span class="nx">extract</span><span class="p">.</span><span class="nx">fromFile</span><span class="p">(</span><span class="s1">&#39;testdata/plain-text.txt&#39;</span><span class="p">,</span> <span class="s1">&#39;test&#39;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="nx">assert</span><span class="p">.</span><span class="nx">strictEqual</span><span class="p">(</span><span class="nx">actual</span><span class="p">,</span> <span class="s1">&#39;Hello, World!\n&#39;</span><span class="p">,</span> <span class="s1">&#39;expected the content between the markers&#39;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="p">})</span>
</span></span><span class="line"><span class="cl"><span class="p">})</span></span></span></code></pre></div>
</div>
<p>I also discovered when done that my tests weren&rsquo;t passing in intellij because it was running the tests from my repo root, so I changed my <code>Node.js test runner</code> config to default the working directory to <code>src/js/</code> for this repo, because I am deciding now that I&rsquo;ll only ever have javascript stuff there, and I&rsquo;ll only run the tests from that directory. It feels like a decent solution.
Maybe I have to revisit that if I ever introduce a package.json and decide whether it belongs here or in the root, but it probably belongs in <code>src/js/</code>, but that&rsquo;s the future 🤷.</p>
<p>Now, Claude did one thing I didn&rsquo;t intend yet (because there isn&rsquo;t any test), which is that it will ignore the whole line the markers are on (which I indicated in the docstring) but I don&rsquo;t have a test for it yet.
I want this behavior so that I don&rsquo;t have to do any special handling to ignore whatever comment marker there is.
So, let&rsquo;s add a test for this even if the code is already implemented. I guess I <em>could</em> remove that comment and reimplement, but that feels like waste, so I won&rsquo;t. Time to be pragmatic instead of dogmatic about the testing.</p>
<p>Hah! Adding the test was good, because I skimmed the code too quickly and it hadn&rsquo;t done what I expected, it was checking the full line. So I actually <em>do</em> have to change the code.</p>
<p>Cool, so I have my extract function.</p>
<h3 id="building-the-workflow-updater-script">
  Building the workflow updater script
  <a href="#building-the-workflow-updater-script" class="heading-anchor" aria-label="Direct link to Building the workflow updater script"><span aria-hidden="true">#</span></a>
</h3>
<p>Now I can create a script file and use it. I think I&rsquo;ll keep the script in <code>src/js/</code> folder and it&rsquo;ll take the path to the reference workflow file and read it in and parse, and then I&rsquo;ll work it from there.</p>
<p>I won&rsquo;t TDD this file because it should be at the level that I&rsquo;ll notice problems when using it. We&rsquo;ll see how much I&rsquo;ll regret <em>that</em> decision later.</p>
<p>In hope that Claude can do this for me, I started by documenting at the top of my script how I want it to work:</p>
<div class=" copy-enabled">
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-javascript" data-lang="javascript"><span class="line"><span class="cl"><span class="cm">/*
</span></span></span><span class="line"><span class="cl"><span class="cm"> This script&#39;s job is to:
</span></span></span><span class="line"><span class="cl"><span class="cm">
</span></span></span><span class="line"><span class="cl"><span class="cm">Extract the mastodon and bsky functions from n8n.copies.js.
</span></span></span><span class="line"><span class="cl"><span class="cm">
</span></span></span><span class="line"><span class="cl"><span class="cm">Read the JSON file in the first argument and then replace the nodes for Mastodon and bsky respectively with their functions.
</span></span></span><span class="line"><span class="cl"><span class="cm">
</span></span></span><span class="line"><span class="cl"><span class="cm">Output the updated JSON object on stdout, pretty-printed
</span></span></span><span class="line"><span class="cl"><span class="cm">Example: ./update_n8n_workflow.js ../bjorn-now-feed-distribution.json &gt; workflow.json
</span></span></span><span class="line"><span class="cl"><span class="cm">Will update the node id `e1bd320b-1222-4324-9168-22372d2e667c` so the parameter.status is `={{ ((item) =&gt; {... })($(&#39;RSS Feed Trigger&#39;).item) }}
</span></span></span><span class="line"><span class="cl"><span class="cm">*/</span></span></span></code></pre></div>
</div>
<p>Then I asked Claude to implement it:</p>
<div class=" copy-enabled">
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-plaintext" data-lang="plaintext"><span class="line"><span class="cl">implement @src/js/update_n8n_workflow.js as described in it&#39;s first comment, use @src/js/extract.js to extract the information. Only implement the mastodon portion right now </span></span></code></pre></div>
</div>
<p>My example contained too much information, so claude suggested that it needed to <code>wrap the extracted function as: ={{ ((item) =&gt; {extracted code})($('RSS Feed Trigger').item) }}</code> but it should be <code>(...)($('RSS Feed Trigger').item)</code>, so I told Claude to plan again <code> I was unclear: the wrapping is &quot;(extracted code)($('RSS Feed Trigger').item)&quot;</code>.</p>
<div class=" copy-enabled">
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-plaintext" data-lang="plaintext"><span class="line"><span class="cl">Implementation Plan for update_n8n_workflow.js
</span></span><span class="line"><span class="cl"> 1. Import dependencies: Import fromFile from extract.js and Node.js fs module
</span></span><span class="line"><span class="cl"> 2. Read command line arguments: Get the workflow JSON file path from process.argv[2]
</span></span><span class="line"><span class="cl"> 3. Extract mastodon function: Use fromFile(&#39;./n8n.copies.js&#39;, &#39;mastodon&#39;) to extract the function code
</span></span><span class="line"><span class="cl"> 4. Read workflow JSON: Parse the workflow file
</span></span><span class="line"><span class="cl"> 5. Find and update node: Locate node with id e1bd320b-1222-4324-9168-22372d2e667c and update its parameters.status field to wrap the extracted function as: ={{ ((item) =&gt; {extracted code})($(&#39;RSS Feed Trigger&#39;).item) }}
</span></span><span class="line"><span class="cl"> 6. Output result: Print the updated JSON to stdout with pretty formatting
</span></span><span class="line"><span class="cl"> The script will be executable with: node update_n8n_workflow.js ../bjorn-now-feed-distribution.json &gt; workflow.json</span></span></code></pre></div>
</div>
<p>The plan now looks correct, it says &ldquo;arrow function&rdquo; instead of &ldquo;extracted code&rdquo;, which I guess is because it added a step to strip <code>let mastodon =</code> from the line, so I&rsquo;ll assume it does the correct thing, either way, this is close enough that manually fixing it will likely be trivial. allons-y!</p>
<h3 id="importing-the-updated-workflow">
  Importing the updated workflow
  <a href="#importing-the-updated-workflow" class="heading-anchor" aria-label="Direct link to Importing the updated workflow"><span aria-hidden="true">#</span></a>
</h3>
<p>A quick skim of the code and it looks correct, the output also seems correct to me, so let&rsquo;s save it to a file, run jq to verify it&rsquo;s valid json (I assume it is since node didn&rsquo;t bork out), and then upload and see what goes. My saved file locally is what&rsquo;s live now, so if it goes wrong, I can back out easily.</p>
<p>Right, interesting problem: n8n supports importing from a file and then it adds the file content <em>alongside</em> what you already have. So now I have a copy of all my existing nodes and with a 1 at the end. The content of my new Mastodon status node DOES look correct, though. So that&rsquo;s a win
Not quite what I had in mind, so I feel like I&rsquo;m doing something wrong.
Let&rsquo;s start by asking Claude about this workflow and see if they can push me towards some online docs.
or, n8n has these boxes &ldquo;I wish this node would…&rdquo; I can just ask there and see what goes.</p>
<p>Seems like this is working as intended (d&rsquo;uh) and the way to handle this is doing it by API. And… fair enough, can. so I asked Claude to make me bash script that I could use.
Then it started complaining that I didn&rsquo;t have a name in my file, which I do, so there seems to be a mismatch between the downloaded workflow and what the API expects.
D&rsquo;oh, I had missed a line in cURL <code>-d @-</code> to read the file, so that&rsquo;s on me.</p>
<p>Then I had issues because apparently my downloaded workflow doesn&rsquo;t just work with the API, so Claude and I did some back and forth and finally ended up with this that limits the fields I want to update:</p>
<div class=" copy-enabled">
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">cat <span class="nv">$1</span> <span class="p">|</span> jq <span class="s1">&#39;{
</span></span></span><span class="line"><span class="cl"><span class="s1">           name,
</span></span></span><span class="line"><span class="cl"><span class="s1">           nodes,
</span></span></span><span class="line"><span class="cl"><span class="s1">           connections,
</span></span></span><span class="line"><span class="cl"><span class="s1">           settings
</span></span></span><span class="line"><span class="cl"><span class="s1">         }&#39;</span> <span class="p">|</span> <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="se"></span>  curl -X PUT <span class="s2">&#34;</span><span class="si">${</span><span class="nv">N8N_HOST</span><span class="si">}</span><span class="s2">/api/v1/workflows/</span><span class="si">${</span><span class="nv">workflow_id</span><span class="si">}</span><span class="s2">&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="se"></span>    -H <span class="s2">&#34;X-N8N-API-KEY: </span><span class="si">${</span><span class="nv">SANITARIUM_N8N_API_KEY</span><span class="si">}</span><span class="s2">&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="se"></span>    -H <span class="s2">&#34;Content-Type: application/json&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="se"></span>    -d @-</span></span></code></pre></div>
</div>
<p>Weirdly, I had to update the <code>settings</code> so it only contained the keys <code>executionOrder</code> and <code>errorWorkflow</code> as the other two values there weren&rsquo;t okay. Whatever, they didn&rsquo;t seem important so 🤷.</p>
<h3 id="validating-the-updated-workflow">
  Validating the updated workflow
  <a href="#validating-the-updated-workflow" class="heading-anchor" aria-label="Direct link to Validating the updated workflow"><span aria-hidden="true">#</span></a>
</h3>
<p>Now I have uploaded my changed thing, and it&rsquo;s time to verify it works as I want. I&rsquo;m not entirely sure what I should post to verify it :p.</p>
<p>I was looking at the result from running the update, and unless I always save the result of the update I <em>might</em> rerun from a prevous point when updating. Or, no, because it&rsquo;s a top-level field that I&rsquo;m ignoring when updating. So it should be fine. But, I think for now, I&rsquo;ll anyway pipe the update stuff <em>into</em> the saved file so it&rsquo;s there and see what changes between runs.</p>
<p>Committed, and tested by posting a scrap about &ldquo;wow… I really spent 5h doing this? That&rsquo;s longer than I expected.&rdquo; But, I guess the thing I have to keep in mind that this is the difference between &ldquo;engineering&rdquo; vs. &ldquo;just throwing stuff together.&rdquo; If I hadn&rsquo;t invested in the infrastructure stuff for managing this, I would&rsquo;ve been done after 30-40m when I updated the code.
The point was the investment in robustness I think I&rsquo;ve gained.</p>
<h2 id="repeat-for-bluesky">
  Repeat for Bluesky
  <a href="#repeat-for-bluesky" class="heading-anchor" aria-label="Direct link to Repeat for Bluesky"><span aria-hidden="true">#</span></a>
</h2>
<p>Handle the no-link with Bsky too.</p>
<h3 id="designing-the-change">
  Designing the change
  <a href="#designing-the-change" class="heading-anchor" aria-label="Direct link to Designing the change"><span aria-hidden="true">#</span></a>
</h3>
<p>Now, with all that done, time to look into Bsky, there I don&rsquo;t append the links but instead attach a &ldquo;website card.&rdquo;
There, I guess I&rsquo;d have to change it so:</p>
<ol>
<li>The body gets the same logic/setup.</li>
<li>The logic is repeated in the &ldquo;Website Card&rdquo; field, assuming that having an empty value there will do nothing (which I assume it will, Javascript being very used to weird cases of keys not existing, but I should test (and executed with an empty body there, it just worked), so just repeat the same logic in that place)</li>
</ol>
<p>this is a bit more annoying, will have to do some copy paste, but I can at least handle the consistency by creating a reusable test.</p>
<p>But I&rsquo;ll work on this either tomorrow or later today, it&rsquo;s after 16, and I feel like taking a break. And heading home feels like a good way to clear the head and then continue.</p>
<h3 id="implementing-reusable-tests">
  Implementing reusable tests
  <a href="#implementing-reusable-tests" class="heading-anchor" aria-label="Direct link to Implementing reusable tests"><span aria-hidden="true">#</span></a>
</h3>
<p><strong>20:03</strong> And it&rsquo;s after dinner, let&rsquo;s see if we can get this in so we can push it through.</p>
<p>What I think I&rsquo;ll do is just do a quick duplication of what I have, don&rsquo;t be clever, just do the dumbest thing possible.
With one caveat: I know the tests are mostly the same, so let&rsquo;s make the tests reusable and configurable and see if that pattern works in node (I bet it does).
Meaning, I want my existing tests for Mastodon to be shared with Bsky but change the limit to 300 char instead of 500.</p>
<p>The way I&rsquo;ll test that is that I&rsquo;ll make a function that returns a closure over my config, then I&rsquo;ll run it by passing in my code to execute and then see if the assertions match. I&rsquo;m not sure how it&rsquo;ll go with the test runner, but let&rsquo;s see. I.e. something like:</p>
<div class=" copy-enabled">
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-javascript" data-lang="javascript"><span class="line"><span class="cl"><span class="kd">function</span> <span class="nx">reusableTest</span><span class="p">(</span><span class="nx">name</span><span class="p">,</span> <span class="nx">maxLength</span><span class="p">,</span> <span class="nx">urlLength</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="k">return</span> <span class="kd">function</span><span class="p">(</span><span class="nx">toTest</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="p">()</span> <span class="p">=&gt;</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">      <span class="nx">test</span><span class="p">(</span><span class="sb">`it does stuff`</span><span class="p">,</span> <span class="p">()</span> <span class="p">=&gt;</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="kd">let</span> <span class="nx">actual</span> <span class="o">=</span> <span class="nx">toTest</span><span class="p">(</span><span class="nx">arg</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="nx">assert</span><span class="p">.</span><span class="nx">strictEqual</span><span class="p">(</span><span class="nx">actual</span><span class="p">,</span> <span class="s2">&#34;whatever I want&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">      <span class="p">})</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="nx">describe</span><span class="p">(</span><span class="s1">&#39;mastodon&#39;</span><span class="p">,</span> <span class="nx">reusableTest</span><span class="p">(</span><span class="s1">&#39;mastodon&#39;</span><span class="p">,</span> <span class="mi">500</span><span class="p">,</span> <span class="mi">23</span><span class="p">))</span></span></span></code></pre></div>
</div>
<p>And another part is, I don&rsquo;t know how Bsky does links. There&rsquo;s something <a href="https://github.com/bluesky-social/atproto/discussions/3517">about facets</a> and how you can use them to reduce links but a big 🤷 in understanding that. I don&rsquo;t know if I&rsquo;ll care to look this up or not.</p>
<p>I could potentially do some smarts and just attach the URL as a card and remove if it&rsquo;s a single link in the post, which I could make as standard for myself.</p>
<p>Anyway, let&rsquo;s get the tests working now and see what goes first.</p>
<p>Okay, that kinda just worked as expected, a little surprised at how smooth it was. <a href="https://github.com/gaqzi/bjorn.now/commit/acd53a5056c11ea11c2d3accac69cf2c7a008cc9">The commit.</a></p>
<h4 id="is-this-really-reusable">
  Is this really reusable?
  <a href="#is-this-really-reusable" class="heading-anchor" aria-label="Direct link to Is this really reusable?"><span aria-hidden="true">#</span></a>
</h4>
<p>I am getting a bit worried that the setup might be… too fiddly, but I&rsquo;ll feel it out a bit more as I add in bsky. This feels like it&rsquo;s hitting the point where code <em>looks the same</em> but it isn&rsquo;t really reusable, because it looks similar but differs in intent so as we diverge it&rsquo;ll just add more <code>if</code>-statements. But, we&rsquo;ll continue going and feel it properly.</p>
<p>the first case: there isn&rsquo;t any clear &ldquo;a link is always 3 chars&rdquo; so it&rsquo;ll be counted as whatever the raw text is, so I&rsquo;ll use <code>-1</code> to indicate don&rsquo;t do anything (an <code>if</code>, urgh).
Spontaneous thought: I could pass in a function to handle the link calculation instead and just assume it sends me the link, so that would sort that problem out nicer and it keeps the test consistent. My favorite the strategy pattern showing up and making my day cleaner, again.</p>
<p>I&rsquo;ll just do that, it&rsquo;s much better than a magic number, and I can create that magic number as a variable from the function call. I know I have to implement this now, so it&rsquo;s the right time to &ldquo;Tidy First&rdquo; á Kent Beck. 🙂.</p>
<p>And there we hit the problem it: it&rsquo;s all very similar for good reasons, BUT the logic <strong>has to be different</strong> because the default way where I add the link at the end for Mastodon doesn&rsquo;t apply for bsky, there I can just attach it as a card.
So all the URL counting stuff don&rsquo;t matter.
So, I&rsquo;m better off copying the tests and changing them slightly, and calling out that they&rsquo;re similar but importantly different, any optimization here is folly.</p>
<p>I only spent ~15m on this, so I don&rsquo;t feel too bad about it. In some other cases it would&rsquo;ve worked out, and it does feel good to keep coding instead of just thinking a whole lot before doing anything.</p>
<p>So, <code>git reset --hard</code> on those files and a revert on the commit. Now let&rsquo;s start by copying and changing.</p>
<p>And, to get what I need later to decide whether to do the card I&rsquo;ll just look for <code>msg.endsWith('… more')</code> and if so, attach the link if it&rsquo;s a <code>/scrap/</code> link. So that&rsquo;s pretty straightforward, it does mean I have to include the info, but that&rsquo;s okay.</p>
<h3 id="refactoring-after-working-tests">
  Refactoring after working tests
  <a href="#refactoring-after-working-tests" class="heading-anchor" aria-label="Direct link to Refactoring after working tests"><span aria-hidden="true">#</span></a>
</h3>
<p>And done,  but looking at how I&rsquo;m deciding whether something is truncated, I don&rsquo;t get what I&rsquo;m doing. Why don&rsquo;t I just look at the string and check if it changed? That seems way more straightforward than comparing two fields from the RSS….</p>
<p>So I&rsquo;m just gonna do that and see what happens, but first, commit my changes, then change the format of how I work.</p>
<p>That was quick, maybe 5m to make the change. I looked at the workflow file and the difference between <code>contentSnippet</code> and <code>encoded:contentSnippet</code> seems to have to do with newlines and other formatting, so that&rsquo;s not gonna be very useful. Seems like a major brainfart that accidentally ended up working 🤷
(likely that those that didn&rsquo;t need truncating were on a single line)</p>
<p>Okay, next up, now to make sure that <code>update_n8n_workflow.js</code> handles bsky as well and updates it.</p>
<p>Went through and updated it, I mostly kept the very procedural code and just added on, maybe I&rsquo;ll clean it up a bit more if I loop back and add Telegram (for Telegram I&rsquo;m thinking I&rsquo;ll just do the preview in app button intead? but maybe it makes sense to put the scraps in there too, it&rsquo;s no different from the others and there&rsquo;s less links :))</p>
<h3 id="testing-bsky-live">
  Testing bsky live
  <a href="#testing-bsky-live" class="heading-anchor" aria-label="Direct link to Testing bsky live"><span aria-hidden="true">#</span></a>
</h3>
<p>And I made a post that would fit 300 chars about <a href="https://bjorn.now/scrap/2025-10-29t213248/">helping my brother install some RAM</a>, where the point behind is that having done it before gives confidence that instructions just don&rsquo;t give. Whether installing RAM or relying on your tests 😜. And it gave me a chance to see that the logic works.</p>
<p>What does the n8n update endpoint do and how does it work? Becuase it feels like not understanding how it&rsquo;s designed might come and bite me later, so let&rsquo;s not just rely on the vibe coded stuff that <em>seems</em> to be working.</p>
<h2 id="wednesday-eod">
  Wednesday EOD
  <a href="#wednesday-eod" class="heading-anchor" aria-label="Direct link to Wednesday EOD"><span aria-hidden="true">#</span></a>
</h2>
<p><strong>21:39</strong> And done, I&rsquo;ve finished bsky. Tomorrow I&rsquo;ll be hanging with the niece all day so I&rsquo;ll format and post this devlog when I&rsquo;m in the office again 🙂
Okay, scrap that (heh), I&rsquo;ll finish Telegram before posting. Then for formatting to post,  I&rsquo;ll do it by creating a script alongside Claude to figure out the formatting. I&rsquo;ll have to brainstorm how then.</p>
<h2 id="update-telegram-logic">
  Update Telegram logic
  <a href="#update-telegram-logic" class="heading-anchor" aria-label="Direct link to Update Telegram logic"><span aria-hidden="true">#</span></a>
</h2>
<p>Add the scrap logic to Telegram too (if it&rsquo;s a scrap, only post the text, otherwise the link)</p>
<h3 id="configuring-instant-views">
  Configuring Instant Views
  <a href="#configuring-instant-views" class="heading-anchor" aria-label="Direct link to Configuring Instant Views"><span aria-hidden="true">#</span></a>
</h3>
<p>Time to get Telegram to be a bit nicer. I overall don&rsquo;t want to force people to click through to my site if they&rsquo;re happy reading where they are. Telegram has these <a href="https://instantview.telegram.org/">Instant View</a> buttons and I can translate my links to use those instead, so I figure I&rsquo;ll do that in my channel so you can just click and see the full thing without loading my site. I guess this will mess with my statistics a bit (because I use <a href="https://plausible.io/">Plausible</a> which is doing it all in javascript), but 🤷, for now I&rsquo;m just happy if people wish to to engage with anything I post.</p>
<p>I did create a preview a couple of months back so I started by taking a link to my site and then copying in what I did last time:</p>
<div class=" copy-enabled">
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-plain" data-lang="plain"><span class="line"><span class="cl">~version: &#34;2.1&#34;
</span></span><span class="line"><span class="cl">site_name: //*[@class=&#34;masthead&#34;]/a
</span></span><span class="line"><span class="cl">article: //article
</span></span><span class="line"><span class="cl">title: $article//*[@class=&#34;title&#34;]
</span></span><span class="line"><span class="cl">subtitle: $article//*[@class=&#34;subtitle&#34;]
</span></span><span class="line"><span class="cl">author: $article//*[@class=&#34;author&#34;]
</span></span><span class="line"><span class="cl">image_url: //meta[@property=&#34;og:image&#34;]
</span></span><span class="line"><span class="cl">published_date: $article//*[@class=&#34;published-at&#34;]
</span></span><span class="line"><span class="cl">body: $article//*[@class=&#34;content&#34;]</span></span></code></pre></div>
</div>
<p>That looks pretty good, but I&rsquo;m not seeing the published time in the live preview, the xpath expression looks right to me. And I similarly don&rsquo;t see the image_url, and looking in the template website I don&rsquo;t see that the <code>&lt;head&gt;</code> section is part of the example document (From the future: Nope, that&rsquo;s wrong, I was looking at the containing doc and not the iframe, it&rsquo;s all there. Telegram will always do a <code>GET</code> for your page to get the current version and then format the page using these rules). So maybe that&rsquo;s why? I&rsquo;ll skim through <a href="https://instantview.telegram.org/docs">the documentation</a> and see if I can find why.</p>
<p>I see that the <code>published_date</code> says &ldquo;unix time&rdquo; <a href="https://instantview.telegram.org/docs#instant-view-format">in the docs</a>, so maybe that&rsquo;s the problem? I gave it the element and it&rsquo;s in readable, but I do have the RFC3339 format string in the <code>&lt;time&gt;</code> tags <code>datetime</code> attribute. Let&rsquo;s keep digging.</p>
<p>Right, so the docs shows that there&rsquo;s a function <a href="https://instantview.telegram.org/docs#datetime">@datetime</a> that can take an xpath expression and return a unix timestamp, so I likely will have to give it the attribute and then specify the format so it can be converted.</p>
<p>So with that, the pattern should be something like this, which doesn&rsquo;t work, and I think I&rsquo;m trying to access the property incorrectly in xpath.</p>
<div class=" copy-enabled">
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-plaintext" data-lang="plaintext"><span class="line"><span class="cl">@datetime(-2, &#34;en-SV&#34;, &#34;yyyy-MM-ddTHH:mm:ssZ&#34;): $article//*[@class=&#34;published-at&#34;][&#39;datetime&#39;]
</span></span><span class="line"><span class="cl">published_date: $@</span></span></code></pre></div>
</div>
<p>I asked Claude how to access it in xpath:</p>
<div class=" copy-enabled">
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-plaintext" data-lang="plaintext"><span class="line"><span class="cl">I want to access the property `datetime` on the element matched by this xpath expression (for Telegram Instant View), how?
</span></span><span class="line"><span class="cl">`*[@class=&#34;published-at&#34;]</span></span></code></pre></div>
</div>
<p>Which gave me the answer that I need to access it by saying <code>/@datetime</code>, which <strong>to me</strong> reads that I &ldquo;dig into&rdquo; the element and <code>@</code> seems to have to do with attributes/properties of a tag. When I replaced <code>['datetime']</code> with <code>/@datetime</code> and saved my match then Telegram showed my publish date!</p>
<p><strong>TIL:</strong> Use <code>/@&lt;name&gt;</code> to access the value of an attribute when you&rsquo;ve selected an element with XPath.
With the expression <code>//*[@class=&quot;published-at&quot;]</code> you will get the HTML tag that has <code>class=&quot;published-at&quot;</code>, and if you then want the <code>datetime</code> property you add <code>/@datetime</code> to get it.</p>
<p>And now that I have the <code>published_at</code> working I&rsquo;ll see if I can figure out how to access my preview image which is available in <code>&lt;meta property=&quot;og:image&quot; content=&quot;&lt;url&gt;&quot;&gt;</code>, and I bet the problem is that I&rsquo;m not going into <code>/@content</code> the way I need to. Changed and that worked!</p>
<p>There&rsquo;s this Instant View property called <code>kicker</code> which I don&rsquo;t know what it means, so I asked Claude and it said:</p>
<blockquote>
<p><code>kicker</code> is the small text that appears <strong>above</strong> the main headline in article layouts - typically used for:</p>
<ul>
<li>Category labels (&ldquo;Politics&rdquo;, &ldquo;Technology&rdquo;)</li>
<li>Section names (&ldquo;Opinion&rdquo;, &ldquo;Analysis&rdquo;)</li>
<li>Eyebrow text (&ldquo;Breaking News&rdquo;, &ldquo;Exclusive&rdquo;)</li>
</ul>
</blockquote>
<p>I guess this is some publishing term and it sounds like my &ldquo;content types&rdquo; would be fitting here. I.e. post, crumb, link, scrap, etc. which I actually don&rsquo;t show at the moment on my single pages. Maybe I should add that in there? Or should I just skip it? It&rsquo;s part of the menu. Although, I have added it into the <code>article</code>&rsquo;s <code>class</code> attribute what category it is, because I have been planning to do custom styling of scraps (pre-emptive code, I know bad, but turns out this bit of unnecessary future planning might be helpful now.)
So, question is, can I strip away the <code>post</code> and <code>single</code> values from that string only leaving the unique piece?</p>
<p>In the list of <a href="https://instantview.telegram.org/docs#supported-functions">functions</a> there&rsquo;s a <code>@match</code> function that takes a regex, so that <em>could</em> work. Let&rsquo;s see if there&rsquo;s anything else before I go into making a regex (which admittedly can be pretty simple given how it&rsquo;s setup right now)</p>
<p>It seems that&rsquo;s the only function I have. So right now, I&rsquo;ll do it super simple and since my <code>class</code> attribute looks like this: <code>class=&quot;post devlog single&quot;</code> I&rsquo;ll just do <code>post ([^ ]+)</code> and then extract the second, the <code>single</code> also isn&rsquo;t needed, but since I&rsquo;m doing this on the <code>article</code> element I don&rsquo;t feel the need to do more, I&rsquo;ll just have the rule that the first class is always the post and this will work.
The <code>@match</code> function&rsquo;s second argument is which capturing group you want to use. While debugging it was a bit non-obvious to me that the debugger&rsquo;s output included <code>class=&quot;devlog&quot;</code> but it seems that it shows the full context, but when you add it to the kicker as <code>$@</code> (which is the return value of the last function call) then it&rsquo;ll just do the string and not where it was from. 🤷.</p>
<div class=" copy-enabled">
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-plain" data-lang="plain"><span class="line"><span class="cl">@match(&#34;post ([^ ]+)&#34;, 1): $article/@class 
</span></span><span class="line"><span class="cl">kicker: $@</span></span></code></pre></div>
</div>
<p>Next up, <code>author_url</code> which is a property on the <code>&lt;a&gt;</code> tag which contains my name, so let&rsquo;s dig into it: <code>author_url: $article//*[@class=&quot;author&quot;]/@href</code>.</p>
<h3 id="using-instant-view-links-or-not">
  Using Instant View links or not?
  <a href="#using-instant-view-links-or-not" class="heading-anchor" aria-label="Direct link to Using Instant View links or not?"><span aria-hidden="true">#</span></a>
</h3>
<p>And with that, I have the Instant View working as I want in the preview. Now to figure out how I use it when posting so others can see it. <a href="https://instantview.telegram.org/#publishing-templates">The documentation</a> for it says:</p>
<blockquote>
<p>Note that if you send the resulting <code>t.me/iv?url=...&amp;rhash=...</code> link to other Telegram users, they will see an Instant View page built using your template (the <strong>rhash</strong> parameter in the link determines which template is used to create the page).</p>
</blockquote>
<p>So all I have to do is wrap my URL to this and it&rsquo;ll work. The downside of that is that then if you click that URL it&rsquo;ll lead to Telegram instead of my site, and it&rsquo;s not obvious to me how to click through to my site. I added the <code>document_url</code> but it seems it doesn&rsquo;t show the real URL.
I don&rsquo;t like this. MAYBE what I could do is add a second link that allows the instant preview, as a way of making it simpler for Telegram users. But iunno, it also feels clunky.
I could try to submit my template and see if it gets approved and then it just works.
I asked Claude and it says the same thing, it&rsquo;s not possible, and if you want that then you need to submit it. So that&rsquo;s the only real option, I don&rsquo;t want to completely nerf the readability for Instant View while I also want to make it simpler for others to read it where they are. The same reason I have the full content in the RSS feed. :)
So I added 10 links that should work, looked that they seemed correct, and added them as checked. Then I added the homepage and blog post listing pages and saw that they <strong>don&rsquo;t</strong> have any Instant View, which is what is supposed to happen.</p>
<h3 id="final-telegram-logic-and-decision">
  Final Telegram logic and decision
  <a href="#final-telegram-logic-and-decision" class="heading-anchor" aria-label="Direct link to Final Telegram logic and decision"><span aria-hidden="true">#</span></a>
</h3>
<p>Which leaves my Telegram posting logic as:</p>
<ol>
<li>
<p>If the URL of the item contains <code>/scrap/</code> then post the <code>content</code> in full (I&rsquo;ll deal with images later, but for now scraps are text-only with no formatting)</p>
</li>
<li>
<p>Otherwise, post only the link so it can be clicked through and if the Instant View is approved then that&rsquo;s available.</p>
</li>
</ol>
<p>And that took about 1.5h to do. Next up, to research how the update endpoint works for n8n, and then to do the formatting of this post and post.</p>
<h2 id="understand-how-n8n-updates-work">
  Understand how n8n updates work
  <a href="#understand-how-n8n-updates-work" class="heading-anchor" aria-label="Direct link to Understand how n8n updates work"><span aria-hidden="true">#</span></a>
</h2>
<p>I left a note for myself earlier that I wanted to check how n8n handles the updates, so I can feel more comfortable in not overriding stuff.</p>
<h3 id="digging-into-the-source-code">
  Digging into the source code
  <a href="#digging-into-the-source-code" class="heading-anchor" aria-label="Direct link to Digging into the source code"><span aria-hidden="true">#</span></a>
</h3>
<p>I figure it&rsquo;ll be a bit annoying to find this, so I&rsquo;ll just ask Claude to find it for me:</p>
<div class=" copy-enabled">
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-plaintext" data-lang="plaintext"><span class="line"><span class="cl">where is the n8n source code for the `PUT` logic for workflow updates?</span></span></code></pre></div>
</div>
<p>And it did some searches and told me path in the repo is <a href="https://github.com/n8n-io/n8n/blob/n8n%401.116.0/packages/cli/src/public-api/v1/handlers/workflows/workflows.handler.ts#L268">packages/cli/src/public-api/v1/handlers/workflows/workflows.handler.ts</a> so I went to GitHub and opened.</p>
<p>It does special handling for <code>settings</code> and will <a href="https://github.com/n8n-io/n8n/blob/f65b6a82a95f0fd298422faa5f6cf3f69bbd3249/packages/cli/src/public-api/v1/handlers/workflows/workflows.service.ts#L107-L112">only change settings</a> I have set and otherwise let existing ones be.</p>
<p>Okay, working my way to the <a href="https://github.com/n8n-io/n8n/blob/master/packages/%40n8n/db/src/repositories/workflow.repository.ts#L52">WorkflowRepository</a> it doesn&rsquo;t implement <code>update</code> itself, so it&rsquo;s probably in the base class, so let&rsquo;s see what&rsquo;s there.
The base class comes from <code>@n8n/typeorm</code> and I don&rsquo;t know where that is. I looked in the <code>@n8n</code> folder on GitHub but didn&rsquo;t see it. I tried pressing <code>t</code> and typing it but it doesn&rsquo;t seem to be part of the path. The search showed that there&rsquo;s a <code>CLAUDE.md</code> so maybe it contains something useful? (I&rsquo;ll dig a bit first before asking Claude to find it for me).
And there I find this: <code>**Backend:** Node.js + TypeScript + Express + TypeORM</code> which seems to indicate that TypeORM might be a general package?
I searched for <code>TypeORM</code> and saw a linter step to ensure we only use it in <code>@n8n/db</code> folder, so is there a definition of <code>@n8n/typeorm</code> in the <code>db/</code> folder?
The <code>package.json</code> lists the dependency as <code>&quot;@n8n/typeorm&quot;: &quot;catalog:&quot;,</code>, so maybe there&rsquo;s something in this catalog folder (or maybe package?)
I had a gut feel since I saw <code>pnpm</code> mentioned and something around monorepos, maybe this is a special <code>pnpm</code> thing so I did a search for &ldquo;pnpm catalog&rdquo; and found explains it&rsquo;s a &ldquo;<a href="https://pnpm.io/catalogs">workspace feature for defining dependency version ranges as reusable constants</a>.&rdquo; So if I get that right, it&rsquo;s how you ensure the same version of TypeORM is used across this entire monorepo. Sounds neat.
So, that means I&rsquo;ll just assume we&rsquo;re doing the bog standard TypeORM stuff so I&rsquo;ll look at that. But I will assume it&rsquo;ll simply do: replace the named keys if they&rsquo;re part of what comes in, because a generic package usually doesn&rsquo;t do more than that.</p>
<p>TypeORM&rsquo;s documentation for update says: <code>update</code> - Updates entities by entity id, ids or given conditions. Sets fields from supplied partial entity.
And I read that to mean: we&rsquo;ll replace all keys we see, and the others will not be touched. So I won&rsquo;t accidentally unset any fields I don&rsquo;t change.
SO, maybe then I should try and limit what I send even further since I only really want to update the <code>nodes</code> portion, so could I get away with only sending that? And the answer is no, because then it gives an error that I need to provide <code>connections</code>.</p>
<h3 id="adding-only-update-if-version-is-what-i-expect-on-the-remote">
  Adding &ldquo;only update if version is what I expect on the remote&rdquo;
  <a href="#adding-only-update-if-version-is-what-i-expect-on-the-remote" class="heading-anchor" aria-label="Direct link to Adding &ldquo;only update if version is what I expect on the remote&rdquo;"><span aria-hidden="true">#</span></a>
</h3>
<p>But, it&rsquo;s fine, I&rsquo;m anyway saving the version in the repo after each update, so I would only loose things if the version live is different from what I have. Which for now, I don&rsquo;t worry about. I COULD do a check to see that the current version is the same as what I have downloaded or warn. I guess that <em>would</em> be pretty tiny effort, and therefore worth it since all you&rsquo;d have to do to sort it out is run <code>get-workflow</code> and then recreate the update and check it.
So… letting me know there&rsquo;s a mismatch WOULD be useful.</p>
<p>The way to do that then is: <code>jq -r '.versionId' bjorn.now-feed-distribution.json</code> gives me the version id, so then I would have to pull down the current version of the workflow and check the version number is the same:
<code>./get-workflow | jq -r '.versionId'</code>
and then compare it, so… screw it, let&rsquo;s put that in the update script.</p>
<div class=" copy-enabled">
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl"><span class="c1"># Only update the workflow if the `.versionId` ISN&#39;T the same as the server,</span>
</span></span><span class="line"><span class="cl"><span class="c1"># because we&#39;re downloading the workflow and then applying our updates to it.</span>
</span></span><span class="line"><span class="cl"><span class="c1"># This intends to avoid overwriting anything we didn&#39;t expect.</span>
</span></span><span class="line"><span class="cl"><span class="nv">localVersion</span><span class="o">=</span><span class="k">$(</span>jq -r <span class="s1">&#39;.versionId&#39;</span> <span class="s2">&#34;</span><span class="nv">$1</span><span class="s2">&#34;</span><span class="k">)</span>
</span></span><span class="line"><span class="cl"><span class="nv">serverVersion</span><span class="o">=</span><span class="k">$(</span><span class="nv">workflow_id</span><span class="o">=</span><span class="nv">$workflow_id</span> ./get-workflow 2&gt;/dev/null <span class="p">|</span> jq -r <span class="s1">&#39;.versionId&#39;</span><span class="k">)</span>
</span></span><span class="line"><span class="cl"><span class="k">if</span> <span class="o">[</span> <span class="s2">&#34;</span><span class="nv">$localVersion</span><span class="s2">&#34;</span> !<span class="o">=</span> <span class="s2">&#34;</span><span class="nv">$serverVersion</span><span class="s2">&#34;</span> <span class="o">]</span><span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="cl">  <span class="nb">echo</span> <span class="s2">&#34;The local workflow file (</span><span class="nv">$1</span><span class="s2">) is stale to the server version&#34;</span> &gt;<span class="p">&amp;</span><span class="m">2</span>
</span></span><span class="line"><span class="cl">  <span class="nb">echo</span> <span class="s2">&#34;  local version : </span><span class="si">${</span><span class="nv">localVersion</span><span class="si">}</span><span class="s2">&#34;</span> &gt;<span class="p">&amp;</span><span class="m">2</span>
</span></span><span class="line"><span class="cl">  <span class="nb">echo</span> <span class="s2">&#34;  server version: </span><span class="si">${</span><span class="nv">serverVersion</span><span class="si">}</span><span class="s2">&#34;</span> &gt;<span class="p">&amp;</span><span class="m">2</span>
</span></span><span class="line"><span class="cl">  <span class="nb">echo</span> &gt;<span class="p">&amp;</span><span class="m">2</span>
</span></span><span class="line"><span class="cl">  <span class="nb">echo</span> <span class="s2">&#34;Hint: Pull down the latest version as and recreate:&#34;</span> &gt;<span class="p">&amp;</span><span class="m">2</span>
</span></span><span class="line"><span class="cl">  <span class="nb">echo</span> <span class="s2">&#34;  ./get-workflow &gt; bjorn.now-feed-distribution.json&#34;</span> &gt;<span class="p">&amp;</span><span class="m">2</span>
</span></span><span class="line"><span class="cl">  <span class="nb">echo</span> <span class="s2">&#34;  ./create-update&#34;</span> &gt;<span class="p">&amp;</span><span class="m">2</span>
</span></span><span class="line"><span class="cl">  <span class="nb">exit</span> <span class="m">2</span>
</span></span><span class="line"><span class="cl"><span class="k">fi</span></span></span></code></pre></div>
</div>
<p>Added a hint to myself in case I forget.</p>
<h2 id="final-thoughts">
  Final thoughts
  <a href="#final-thoughts" class="heading-anchor" aria-label="Direct link to Final thoughts"><span aria-hidden="true">#</span></a>
</h2>
<p>This entire endeavour got a bit more robust than what I&rsquo;ve had before with my n8n setup, but that&rsquo;s because I&rsquo;m planning to add more functionality to my POSSE setup. So I wanted to make sure I don&rsquo;t create more of a mess for myself as I change things, and create more things to check.</p>
<p>This isn&rsquo;t 100% the production level I would do but it&rsquo;s good enough for where I am. The &ldquo;create the update&rdquo; script is pretty rough, but I also expect that I will <em>very rarely</em> change it, and it&rsquo;s built around a couple of assumptions that I don&rsquo;t see changing. So keeping it hacky, or more as disposable, feels okay to me. If I had more people working on this I might not be, but for now, there&rsquo;s enough guard rails.</p>
<p>I&rsquo;m happy I decided to add the optimistic check that we&rsquo;re updating against the version on the server, it&rsquo;s the kind of thing that was <em>really</em> straightforward and simple to add and it&rsquo;ll rarely fail, but it&rsquo;ll warn in case I end up doing click-ops and forget to sync back to the repo.</p>
]]></content:encoded></item><item><title>Use `/@&lt;name&gt;` to access the value of an attribute when you've selected an element with XPath.</title><link>https://bjorn.now/til/2025-11-03-use-forward-slash-at-name-to-access-property-of-an-element-in-xpath.md/?utm_medium=feed</link><pubDate>Mon, 03 Nov 2025 14:59:25 +0100</pubDate><guid>https://bjorn.now/til/2025-11-03-use-forward-slash-at-name-to-access-property-of-an-element-in-xpath.md/</guid><description>Use /@&lt;name&gt; to access the value of an attribute when you’ve selected an element with XPath.
With the expression //*[@class="published-at"] you will get the HTML tag that has class="published-at", and if you then want the datetime property you add /@datetime to get it.</description><content:encoded><![CDATA[<p>Use <code>/@&lt;name&gt;</code> to access the value of an attribute when you&rsquo;ve selected an element with XPath.</p>
<p>With the expression <code>//*[@class=&quot;published-at&quot;]</code> you will get the HTML tag that has <code>class=&quot;published-at&quot;</code>, and if you then want the <code>datetime</code> property you add <code>/@datetime</code> to get it.</p>
]]></content:encoded></item><item><link>https://bjorn.now/scrap/2025-10-29t213248/?utm_medium=feed</link><pubDate>Wed, 29 Oct 2025 21:32:48 +0100</pubDate><guid>https://bjorn.now/scrap/2025-10-29t213248/</guid><description>my brother’s computer was lagging, and after debugging we decided to get more RAM. He tried installing, first time. After booting still only 16gb, he knew it was supposed to click, but it felt like a lot of pressure and it might break…
Went over. Applied pressure. click “oh, that’s all?”</description><content:encoded><![CDATA[<p>my brother&rsquo;s computer was lagging, and after debugging we decided to get more RAM. He tried installing, first time.
After booting still only 16gb, he knew it was supposed to click, but it felt like a lot of pressure and it might break…</p>
<p>Went over. Applied pressure. <em>click</em> &ldquo;oh, that&rsquo;s all?&rdquo;</p>
]]></content:encoded></item><item><link>https://bjorn.now/scrap/2025-10-29t155558/?utm_medium=feed</link><pubDate>Wed, 29 Oct 2025 15:55:59 +0100</pubDate><guid>https://bjorn.now/scrap/2025-10-29t155558/</guid><description>huh? five hours? I keep being amazed at how the hours fly when coding, even when I feel like I did very little 😅</description><content:encoded>&lt;p>huh? five hours? I keep being amazed at how the hours fly when coding, even when I feel like I did very little 😅&lt;/p>
</content:encoded></item><item><link>https://bjorn.now/scrap/2025-10-29t092057/?utm_medium=feed</link><pubDate>Wed, 29 Oct 2025 09:20:57 +0100</pubDate><guid>https://bjorn.now/scrap/2025-10-29t092057/</guid><description>and my company careful chaos AB is now approved with Bolagsverket! 🥳</description><content:encoded>&lt;p>and my company careful chaos AB is now approved with Bolagsverket! 🥳&lt;/p>
</content:encoded></item><item><title>Fixing Mastodon URL truncation in n8n</title><link>https://bjorn.now/devlog/2025-10-28-fixing-mastodon-url-truncation-in-n8n-module/?utm_medium=feed</link><pubDate>Tue, 28 Oct 2025 13:35:07 +0100</pubDate><guid>https://bjorn.now/devlog/2025-10-28-fixing-mastodon-url-truncation-in-n8n-module/</guid><description>I have been working on automating my POSSE setup, and ended up trying n8n and it has been a great experience, there are so many plugins that it’s mostly just wiring things together.
But, I discovered that despite being within the character limit, one of my URLs were truncated when posting to Mastodon. This is because Mastodon does something non-obvious: it treats all URLs as 23 characters, no matter if they’re shorter or longer.</description><content:encoded><![CDATA[<p>I have been working on automating my <a href="https://indieweb.org/POSSE">POSSE setup</a>,
and ended up trying <a href="https://n8n.io">n8n</a> and it has been a great experience,
there are so many plugins that it&rsquo;s mostly just wiring things together.</p>
<p>But, I discovered that despite being within the character limit,
one of my URLs were truncated when posting to Mastodon.
This is because Mastodon does something non-obvious:
it treats all URLs as 23 characters, no matter if they&rsquo;re shorter or longer.</p>
<p>This is because they copied this behavior from Twitter,
which ran all links through their t.co link shortener.
The reason is that Mastodon doesn&rsquo;t want you to use a link shortener,
since it hides the real URL (i.e. makes rick rolling easier 😜).</p>
<p>So the module truncated the text in the status.
Since it wasn&rsquo;t URL-aware, all I got was a broken link.</p>
<p>I decided to fix it myself, because dammit, I&rsquo;m a programmer.
It&rsquo;s an unfamiliar codebase I know nothing about,
so I rolled up my sleeves and asked Claude,
&ldquo;<em>Pretty please, can you fix the problem and add tests?</em>&rdquo;
(ah the modern world we live in)</p>
<p>The key constraint: I needed to understand every line of code before shipping it,
and it had to pass my quality standards because my <a href="https://bjorn.now/blog/2025/07/24/your-name-is-still-on-it/">name would be on the PR.</a>
Happily, that worked out <a href="https://github.com/redoracle/n8n-nodes-the-mastodon/pull/4">becuse my PR was accepted</a>. 🙂</p>
<p>Here&rsquo;s how I worked through it over three days. All <a href="https://github.com/gaqzi/n8n-nodes-the-mastodon/commits/url-aware-post-length-calculation/">commits are here</a> if you want to step through them.
I really intend to share these devlogs earlier in the future, but I got lazy 😅</p>
<h2 id="the-rough-notes">
  The rough notes
  <a href="#the-rough-notes" class="heading-anchor" aria-label="Direct link to The rough notes"><span aria-hidden="true">#</span></a>
</h2>
<p>Lightly edited for the web, and I&rsquo;ve kept the day breaks in.</p>
<h3 id="2025-10-08">
  2025-10-08
  <a href="#2025-10-08" class="heading-anchor" aria-label="Direct link to 2025-10-08"><span aria-hidden="true">#</span></a>
</h3>
<p>I discovered that my mastodon poster truncated the URL (which seems to indicate that the n8n plugin does a naïve counting of characters, whereas it should only count 23 chars/URL, because Mastodon don&rsquo;t penalize longer URLs so we don&rsquo;t need link shorteners.</p>
<p>So I looked at the source for the package in question and found the 500 limit (I had looked at this before) and knew it was hardcoded, and then I decided to download the repo and ask Claude to find the bug and suggest a fix:</p>
<div class="full-width no-copy-button">
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-plaintext" data-lang="plaintext"><span class="line"><span class="cl">this is a plugin for n8n, and @nodes/Mastodon/status/StatusMethods.ts seems to have a bug where if I post a status with a link then it will truncate the status assuming the link takes up the full
</span></span><span class="line"><span class="cl">length it has, instead of only 23 characters as is defined by Mastodon. Identify where this bug comes from, explain it, and come up with a plan for how to fix it. Also suggest tests to add that
</span></span><span class="line"><span class="cl">fits with existing tests in this repostiory.</span></span></code></pre></div>
</div>
<p>It then came up with a plan that I rejected because I wondered if there any existing packages that implement this logic, since the Javascript community is quite keen on tiny packages</p>
<details>
<summary>The rejected plan to implement</summary>
<div class="full-width no-copy-button">
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-plaintext" data-lang="plaintext"><span class="line"><span class="cl">Fix Plan
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">1. Create URL-aware character counting utilit
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">- Add new method ValidationUtils.calculateMastodonLength() that:
</span></span><span class="line"><span class="cl">  - Detects URLs in the text using regex
</span></span><span class="line"><span class="cl">  - Counts each URL as 23 characters (Mastodon&#39;s characters_reserved_per_url)
</span></span><span class="line"><span class="cl">  - Counts remaining text normally
</span></span><span class="line"><span class="cl">  - Returns the effective character count
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">2. Update validateStatusParam function
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">- Replace simple sanitizeStringParam with URL-aware validation
</span></span><span class="line"><span class="cl">- Use the new calculateMastodonLength() to check if status exceeds 500 chars
</span></span><span class="line"><span class="cl">- Only trim/reject if the effective length (with URL shortening) exceeds limit
</span></span><span class="line"><span class="cl">- Keep the full URL text intact (don&#39;t truncate it)
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">3. Add comprehensive tests
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">Following the existing test pattern in __tests__/status.test.ts, add:
</span></span><span class="line"><span class="cl">- Test for status with single URL that would exceed 500 chars without URL counting
</span></span><span class="line"><span class="cl">- Test for status with multiple URLs
</span></span><span class="line"><span class="cl">- Test for status with URL at the beginning, middle, and end
</span></span><span class="line"><span class="cl">- Test that status correctly rejects when effective length &gt; 500
</span></span><span class="line"><span class="cl">- Test that long URLs are preserved in the request body
</span></span><span class="line"><span class="cl">- Test edge cases: URL-like text that isn&#39;t a URL, localhost URLs, etc.
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">4. Optional enhancement
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">- Consider fetching instance configuration (characters_reserved_per_url) dynamically
</span></span><span class="line"><span class="cl">- For now, hardcode 23 as it&#39;s the Mastodon standard</span></span></code></pre></div>
</div>
</details>
<p>So I asked Claude to look for a package:</p>
<div class="full-width no-copy-button">
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-plaintext" data-lang="plaintext"><span class="line"><span class="cl">search and see if there&#39;s an existing npm package that does this calculation of post length per mastodon so we don&#39;t have to write it ourselves</span></span></code></pre></div>
</div>
<p>It searched and didn&rsquo;t find anything (it was a bit slow, so I decided to look as well, and didn&rsquo;t find anything obvious), when I looked back after giving up it asked to download the original Ruby source for the <a href="https://github.com/mastodon/mastodon/blob/main/app/validators/status_length_validator.rb">status length validation</a> and then came up with new search patterns</p>
<p>It then found twitter-text and suggested to import it and use</p>
<details>
<summary>Plan to use twitter-text</summary>
<div class="full-witdh no-copy-button">
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-plaintext" data-lang="plaintext"><span class="line"><span class="cl">Fix Plan for Mastodon URL Character Counting Bug
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">1. Add twitter-text dependency
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">- Run npm install twitter-text (version 3.1.0, stable and well-maintained)
</span></span><span class="line"><span class="cl">- This library provides the same URL detection/counting logic that Mastodon uses
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">2. Update ValidationUtils in Mastodon_Methods.ts
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">- Add new method ValidationUtils.countMastodonLength(text: string): number
</span></span><span class="line"><span class="cl">  - Use twitter-text&#39;s parseTweet() or extract URLs manually
</span></span><span class="line"><span class="cl">  - Count each URL as 23 characters (Mastodon&#39;s standard)
</span></span><span class="line"><span class="cl">  - Return effective character count
</span></span><span class="line"><span class="cl">- Keep existing sanitizeStringParam for other use cases
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">3. Update validateStatusParam in StatusMethods.ts
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">- Replace line 16: Instead of truncating, validate the effective length
</span></span><span class="line"><span class="cl">- If effective length &gt; 500, throw error with helpful message
</span></span><span class="line"><span class="cl">- Don&#39;t truncate the actual status text (preserve full URLs)
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">4. Add comprehensive tests to tests/status.test.ts
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">Following existing test patterns, add:
</span></span><span class="line"><span class="cl">- Test: Status with long URL (&gt;100 chars) that fits within 500 char limit
</span></span><span class="line"><span class="cl">- Test: Status with multiple URLs counted correctly
</span></span><span class="line"><span class="cl">- Test: Status that exceeds 500 chars even with URL shortening
</span></span><span class="line"><span class="cl">- Test: URL preservation (ensure full URL is sent to API, not truncated)
</span></span><span class="line"><span class="cl">- Test: Edge cases (URL at start/middle/end, multiple URLs)
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">This approach uses a proven library rather than reinventing URL detection regex.</span></span></code></pre></div>
</div>
</details>
<p>I wasn&rsquo;t sure about all this because looking at the repo it looks like a big dependency, and maybe it has too much extra stuff that doesn&rsquo;t really come into play for Mastodon?</p>
<div class="full-width no-copy-button">
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-plaintext" data-lang="plaintext"><span class="line"><span class="cl">will `parseTweet()` introduce any other logic that might not work with mastodon? Might we be better off copying the code around links (and tests) and adding it to this repo (do the licenses match?)
</span></span><span class="line"><span class="cl">We *cannot* break the existing behavior and throw an error instead of truncating, but we can change the truncating behavior to the effective length instead of actual length. </span></span></code></pre></div>
</div>
<p>It then went off and did a bunch of searches around east-asian specific character encodings (Japanese and Chinese alphabets as far as I can tell)</p>
<p>I then got a new plan that looks better, but I still wonder about the <code>when possible</code> for preserving URLs (I expect it is that if there&rsquo;s not 23 chars left, then it <em>has</em> to cut, so I will have it clarified in a more detailed plan)</p>
<details>
<summary>The new plan that includes <code>when possible</code> for preserving URLs</summary>
<div class="full-width no-copy-button">
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-plaintext" data-lang="plaintext"><span class="line"><span class="cl">Updated Fix Plan (Mastodon-Compatible)
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">1. Copy URL regex from twitter-text (with attribution)
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">- Add file header comment crediting twitter-text (Apache 2.0 license)
</span></span><span class="line"><span class="cl">- Copy only the URL detection regex pattern (battle-tested, handles edge cases)
</span></span><span class="line"><span class="cl">- No dependencies needed - just the regex
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">2. Create ValidationUtils.calculateMastodonLength() in Mastodon_Methods.ts
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">- Extract all URLs using the copied regex
</span></span><span class="line"><span class="cl">- Replace each URL with 23-char placeholder (&#34;x&#34;.repeat(23))
</span></span><span class="line"><span class="cl">- Count remaining characters using standard .length
</span></span><span class="line"><span class="cl">- Return the effective character count
</span></span><span class="line"><span class="cl">- No CJK weighting - Mastodon treats all chars equally
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">3. Update validateStatusParam() in StatusMethods.ts
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">- Calculate effective length using new method
</span></span><span class="line"><span class="cl">- If effective length &gt; 500, truncate at boundary that fits within 500 effective chars
</span></span><span class="line"><span class="cl">- Preserve full URLs when possible (don&#39;t cut URLs)
</span></span><span class="line"><span class="cl">- Keep existing truncation behavior (no breaking changes)
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">4. Add tests to tests/status.test.ts
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">- Status with long URL (&gt;100 chars) that fits in 500 effective chars
</span></span><span class="line"><span class="cl">- Status with multiple URLs
</span></span><span class="line"><span class="cl">- Status requiring truncation with URL preservation
</span></span><span class="line"><span class="cl">- CJK characters counted same as Latin (no weighting)
</span></span><span class="line"><span class="cl">- Edge cases: URL at boundaries, multiple URLs</span></span></code></pre></div>
</div>
</details>
<p>I asked what <code>when possible</code> means and guessed some cases (I forgot to save the prompt)</p>
<details>
<summary>The details for when it's not possible to save the URL</summary>
<div class="full-width no-copy-button">
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-plaintext" data-lang="plaintext"><span class="line"><span class="cl">⏺ Cases Where We Cannot Preserve URLs:
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">1. Theoretical impossible case (highly unlikely in practice):
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">Text: &#34;Check this out: https://example.com/article&#34;
</span></span><span class="line"><span class="cl">Actual length: 44 chars
</span></span><span class="line"><span class="cl">Effective length (URL=23): 39 chars ✅ Fits!
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">But if someone tried:
</span></span><span class="line"><span class="cl">Text: 480 chars of text + &#34; https://example.com/very/long/url&#34;
</span></span><span class="line"><span class="cl">Actual: 520 chars
</span></span><span class="line"><span class="cl">Effective: 480 + 23 = 503 chars ❌ Over limit!
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">To fit within 500 effective chars, we&#39;d need to:
</span></span><span class="line"><span class="cl">- Remove 3 chars from somewhere
</span></span><span class="line"><span class="cl">- We can&#39;t split the URL (would break it)
</span></span><span class="line"><span class="cl">- We&#39;d have to remove text BEFORE the URL
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">2. Multiple URLs scenario:
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">Text: &#34;See https://example1.com and https://example2.com and more text...&#34;
</span></span><span class="line"><span class="cl">If effective length &gt; 500, we might need to:
</span></span><span class="line"><span class="cl">- Keep URL1 (23 chars)
</span></span><span class="line"><span class="cl">- Keep URL2 (23 chars)
</span></span><span class="line"><span class="cl">- Truncate the &#34;and more text...&#34; part
</span></span><span class="line"><span class="cl">- OR drop URL2 entirely if there&#39;s not enough room
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">3. The actual problematic case:
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">Text: 450 chars + &#34;Visit https://example.com/some/long/url for more info and keep reading...&#34;
</span></span><span class="line"><span class="cl">Effective: 450 + 23 + 35 = 508 chars ❌
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">To fix:
</span></span><span class="line"><span class="cl">- Keep first 450 chars ✅
</span></span><span class="line"><span class="cl">- Keep URL (counts as 23) ✅
</span></span><span class="line"><span class="cl">- Truncate &#34;for more info and keep reading&#34; to fit in remaining 27 effective chars
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">The Real Answer:
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">We can ALWAYS preserve URLs! Here&#39;s why:
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">- URLs only count as 23 chars in the effective length
</span></span><span class="line"><span class="cl">- If someone has room for 23 effective chars, they have room for the full URL (even if it&#39;s 200 chars long)
</span></span><span class="line"><span class="cl">- We just need to truncate the non-URL text to make room
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">The algorithm:
</span></span><span class="line"><span class="cl">1. Find all URLs and their positions
</span></span><span class="line"><span class="cl">2. Calculate effective length
</span></span><span class="line"><span class="cl">3. If over 500:
</span></span><span class="line"><span class="cl">  - Truncate text BETWEEN or AFTER URLs
</span></span><span class="line"><span class="cl">  - Never truncate the URL itself
</span></span><span class="line"><span class="cl">  - Work backwards to find a safe truncation point
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">Edge case that might force URL removal:
</span></span><span class="line"><span class="cl">Text with 22 URLs: Each counts as 23 = 506 effective chars
</span></span><span class="line"><span class="cl">Even with ZERO other text, we&#39;re over the limit!
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">In this case, we&#39;d have to drop some URLs entirely (remove the last URL(s) until we fit).
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">Should I proceed with a plan that preserves URLs when possible, but drops trailing URLs if necessary when there are too many?</span></span></code></pre></div>
</div>
</details>
<p>So, now to handle this I will ask it to write this high-level plan into <code>high-level.plan.md</code> and then to create a detailed plan that could be implemented by a junior engineer, so we then can ask sub agents to implement them step-by-step. I saw the plan and it suggested to drop URLs that went over the limit, so I asked it to instead keep them.</p>
<p>I haven&rsquo;t added any general/background context to this repo about my preferences, so I wonder if this will be enough to give me something useful in this repo:</p>
<div class="full-width no-copy-button">
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-plaintext" data-lang="plaintext"><span class="line"><span class="cl">create a deatiled implementation plan that a junior engineer could implement.
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">Ensure each step is written in a TDD fashion and that the tests are passing, changed to show the expected new behavior (red),
</span></span><span class="line"><span class="cl">and then the change is implemented to make it pass (green).
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">Only implement as much as is needed to get each test passing and no more. 
</span></span><span class="line"><span class="cl">Cover the copied code from Twitter with tests too.</span></span></code></pre></div>
</div>
<p>It came up with a detailed plan, it looked good enough (skimmed it), so I asked claude to:</p>
<div class="full-width no-copy-button">
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-plaintext" data-lang="plaintext"><span class="line"><span class="cl">execute the test suite to verify it runs, if it runs successfully then document how to run the tests in the plan and then save the plan as plan.md</span></span></code></pre></div>
</div>
<p>Then it couldn&rsquo;t run any commands, likely because I never ran <code>npm install</code> in the folder so I did that and verified that <code>npm test</code> worked and then asked Claude to try again.</p>
<p>Next up, execute the plan: <code>Now plan to execute @plan.md with subagents and marking off which steps has been done in the plan as they finish.</code>.</p>
<details>
<summary>The plan</summary>
<div class="full-width no-copy-button">
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-plaintext" data-lang="plaintext"><span class="line"><span class="cl">Strategy
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">Use general-purpose agents in parallel where possible to implement the plan steps efficiently. Track progress by updating plan.md with checkmarks as each step completes.
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">Phase 1: URL Extraction (Steps 1.1-1.6)
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">- Agent 1: Implement URL extraction utility with all tests
</span></span><span class="line"><span class="cl"> - Create __tests__/url-extraction.test.ts
</span></span><span class="line"><span class="cl"> - Add extractUrls() to ValidationUtils in Mastodon_Methods.ts
</span></span><span class="line"><span class="cl"> - Iterate through all test cases in steps 1.1-1.6
</span></span><span class="line"><span class="cl"> - Mark steps 1.1-1.6 complete in plan.md
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">Phase 2: Length Calculation (Steps 2.1-2.5)
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">- Agent 2: Implement Mastodon length calculation with all tests
</span></span><span class="line"><span class="cl"> - Create __tests__/mastodon-length.test.ts
</span></span><span class="line"><span class="cl"> - Add calculateMastodonLength() to ValidationUtils
</span></span><span class="line"><span class="cl"> - Iterate through all test cases in steps 2.1-2.5
</span></span><span class="line"><span class="cl"> - Mark steps 2.1-2.5 complete in plan.md
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">Phase 3: URL-Aware Truncation (Steps 3.1-3.9)
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">- Agent 3: Implement truncation with URL preservation
</span></span><span class="line"><span class="cl"> - Create __tests__/url-truncation.test.ts
</span></span><span class="line"><span class="cl"> - Add truncateWithUrlPreservation() to ValidationUtils
</span></span><span class="line"><span class="cl"> - Iterate through all test cases in steps 3.1-3.9
</span></span><span class="line"><span class="cl"> - Mark steps 3.1-3.9 complete in plan.md
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">Phase 4: Status Validation Update (Steps 4.1-4.4)
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">- Agent 4: Update status validation to use new utilities
</span></span><span class="line"><span class="cl"> - Add tests to __tests__/status.test.ts
</span></span><span class="line"><span class="cl"> - Update validateStatusParam in StatusMethods.ts
</span></span><span class="line"><span class="cl"> - Verify all new tests pass
</span></span><span class="line"><span class="cl"> - Mark steps 4.1-4.4 complete in plan.md
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">Phase 5: Final Verification (Step 5)
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">- Run npm test to verify all tests pass
</span></span><span class="line"><span class="cl">- Update plan.md with final completion status
</span></span><span class="line"><span class="cl">- Report summary of what was implemented</span></span></code></pre></div>
</div>
</details>
<p>I then set Claude to execute and left some questions for myself to answer after we were done:</p>
<ul>
<li>Are there more places we should use this new validation/truncation logic?</li>
<li>Is our new code implemented similarly to the rest of the code in this repository?</li>
<li>What questions should I ask you to verify that we don&rsquo;t break something?</li>
</ul>
<p><strong>00:34</strong> It&rsquo;s late, so time to let Claude spin away while I prepare for sleep.</p>
<p><strong>00:50</strong> Okay, while prepping for sleep it finished and I had a look on the new tests
the tests have some problems that we&rsquo;ll need to iterate on:</p>
<ul>
<li>Doesn&rsquo;t assert the exact values we&rsquo;re looking for</li>
<li>Doesn&rsquo;t make the 500 or 23 constants and are instead magic numbers, let&rsquo;s name them</li>
<li>The truncation logic is <strong>too smart</strong> and it should just cut the URL and leave an ugly &ldquo;scar&rdquo; if that&rsquo;s what&rsquo;s required, that&rsquo;s fine. We can iterate on that</li>
</ul>
<p>I think the way to work with this is to create a fork and do my changes in a repo with small focused commits, and then show the work as I commit, so I indicate how I grew the solution</p>
<p>Now, sleep for reals</p>
<h3 id="2025-10-10">
  2025-10-10
  <a href="#2025-10-10" class="heading-anchor" aria-label="Direct link to 2025-10-10"><span aria-hidden="true">#</span></a>
</h3>
<p>Okay, back at it again after a day&rsquo;s break, got some tunes on and now to review.</p>
<p>Okay, to help me document how I&rsquo;m working on this I will <a href="https://github.com/gaqzi/n8n-nodes-the-mastodon/commit/8765c425b0cfffaf31d2b56ec00b4fa2dc855c72">commit the change I got from Claude</a>, and then I&rsquo;ll do my changes from there and commit as I do them.
Doing this so that it&rsquo;ll be easier to blog about it, and I don&rsquo;t know if that workflow will be what I want/need, so we&rsquo;ll see.</p>
<h4 id="reviewing">
  Reviewing
  <a href="#reviewing" class="heading-anchor" aria-label="Direct link to Reviewing"><span aria-hidden="true">#</span></a>
</h4>
<p>Starting the review by looking at the <code>status.test.ts</code> since I&rsquo;m supposed to have good tests that tell me what&rsquo;s going on, so that&rsquo;ll be the lense through which I review this.</p>
<p>First off, I got header comments explaining the change that I don&rsquo;t need, so nixing &rsquo;em, and I think all these tests belong in a <code>describe</code> that shows that the tests belong together. <a href="https://github.com/gaqzi/n8n-nodes-the-mastodon/commit/028b146d20234486eef241b63e9268e06d744a49">Commit</a></p>
<p>Then, let&rsquo;s get going with the first test &ldquo;should preserve long URL when using URL-aware character counting&rdquo; doesn&rsquo;t do one that will hit the problem, and the test doesn&rsquo;t check the count is what we expect it to be (which is also a problem). okay, changed the test to <strong>only contain</strong> a URL and then verify that the expected status is exactly 23 characters long, which passes. <a href="https://github.com/gaqzi/n8n-nodes-the-mastodon/commit/f8500029bb7a2acae6763b1093501fb6ff681b67">Commit</a></p>
<p>Next is a test with multiple long URLs, which is useful, make sure we don&rsquo;t do this for only one URL in the status. The URLs are not pushing it as far as it should, so I made both URLs more than the total length of a status <code>(80 -&gt; 500)</code></p>
<div class="no-copy-button">
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-javascript" data-lang="javascript"><span class="line"><span class="cl"><span class="kr">const</span> <span class="nx">url1</span> <span class="o">=</span> <span class="s1">&#39;https://example.com/&#39;</span> <span class="o">+</span> <span class="s1">&#39;a&#39;</span><span class="p">.</span><span class="nx">repeat</span><span class="p">(</span><span class="mi">80</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"><span class="kr">const</span> <span class="nx">url2</span> <span class="o">=</span> <span class="s1">&#39;https://www.example.org/&#39;</span> <span class="o">+</span> <span class="s1">&#39;b&#39;</span><span class="p">.</span><span class="nx">repeat</span><span class="p">(</span><span class="mi">80</span><span class="p">);</span></span></span></code></pre></div>
</div>
<p>The <code>text</code> is a bit annoying so simplifying it for my own sake to make the length calculation simpler, it&rsquo;s a list of links one per line <code>(2*2+1 = 5 + 23 + 23 = 51)</code>:</p>
<div class="no-copy-button">
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-javascript" data-lang="javascript"><span class="line"><span class="cl"><span class="kr">const</span> <span class="nx">text</span> <span class="o">=</span> <span class="sb">`- </span><span class="si">${</span><span class="nx">url1</span><span class="si">}</span><span class="sb">\n- </span><span class="si">${</span><span class="nx">url2</span><span class="si">}</span><span class="sb">`</span><span class="p">;</span></span></span></code></pre></div>
</div>
<p><a href="https://github.com/gaqzi/n8n-nodes-the-mastodon/commit/9a253678e5f4c4e8621a63c262374433c5f37ad4">Commit.</a></p>
<p>And about now I&rsquo;m annoyed at the way the mock implementation has so much setup, it doesn&rsquo;t follow my coding style (too much repetition), but this isn&rsquo;t my project, and it seems to be how all other tests are written in this file so we keep it. <strong>Consistency and avoiding surprises is more important than my personal style.</strong></p>
<p>Now comes the really nasty bit: Claude decided to <strong>truncate text instead of URLs</strong> which <em>isn&rsquo;t what I want</em>, if the URL is placed so that it&rsquo;s unavoidable that it will be cut, then cut it. Don&rsquo;t remove it, and don&rsquo;t remove text before it. It&rsquo;s better to indicate that something went wrong (by showing a cut URL) than silently dropping it
(I&rsquo;d rather go boom than be silent, been saying this for a very looong time, mostly living that philosophy :p)</p>
<p>So I think this particular test will have to change. And about now I&rsquo;m regretting that I didn&rsquo;t commit already because I feel I should&rsquo;ve done it per test instead. Because now I&rsquo;m about to make the test red, so I want to make sure I can change it cleanly. So, I&rsquo;ll undo my changes (thanks IntelliJ for having infinite undo in the same session), commit, and then redo per function.
That took ~5min to fix, and while not really necessary, I feel a lot better from going back and redoing and creating clean commits to show the work (so the commits linked was made before this).</p>
<p>Okay, now, this test I&rsquo;ll also change up a bit:</p>
<ul>
<li>Loong text, <code>a</code>x480, a space, and a url which means the effective length will be 505 so the URL will be cut</li>
<li>I changed the assertion on the <code>sentStatus</code> to be the naively cut status instead (since the URL can&rsquo;t ever fit now, so it will just have to mangle the URL)</li>
<li>And as expected, the test is now failing, so that&rsquo;s good. Now, let&rsquo;s ask Claude to fix the failing test I&rsquo;ve created</li>
<li>I highlighted the test in IntelliJ and opened up Claude and said (mostly because I haven&rsquo;t looked at the code, this might actually be faster to do manually, so I&rsquo;ll look at it as the fresh Claude session is figuring out what&rsquo;s going on):
<ul>
<li>
<div class="no-copy-button">
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-plaintext" data-lang="plaintext"><span class="line"><span class="cl">The highlighted test is failing correctly, I want to adjust the business logic to match. Confirm the test is failing by running `npm test` and then plan how to fix it </span></span></code></pre></div>
</div>
</li>
</ul>
</li>
</ul>
<p>Found the place where this is, it&rsquo;s in <code>nodes/Mastodon/Mastodon_Methods.ts</code> and the new <code>truncateWithUrlPreservation</code>.</p>
<p>And I see that Claude is spinning and reading what it&rsquo;s doing I realize I made a logical error: if Mastodon <strong>always</strong> counts URLs as 23 chars, <strong>I can&rsquo;t truncate a URL and make it work.</strong> The logic Claude made was reasonable since I told it to <strong>never</strong> raise an exception. So that means I need to make this case an explicit negative case, that it will fail the validation, and I could exemplify that <strong>better</strong> by making it a really short but valid URL (ex <a href="https://example.com">https://example.com</a> which is 19 chars)</p>
<p>Okay, asked Claude to get to it, but it&rsquo;s struggling with the logic and I&rsquo;m realizing that I should just add another test alongside this where we have a long URL at the start and a really long text at the end, that we can just truncate as needed, so it can see the logic better.</p>
<p>BUT, for right now, I&rsquo;m gonna start a stew so I&rsquo;ll go do that and then let it… stew for a coupe of hours so I can continue then</p>
<p><strong>20:15</strong> back and doing stuff again</p>
<p>I added another test that swaps the order so it&rsquo;s <code>&lt;url&gt; &lt;text&gt;</code> and now it&rsquo;s failing too, so let&rsquo;s clear the session for Claude and start over</p>
<div class="full-width no-copy-button">
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-plaintext" data-lang="plaintext"><span class="line"><span class="cl">these two tests are failing because the business logic is incorrect, suggest how we can fix the business logic</span></span></code></pre></div>
</div>
<p>Claude came back with a plan to let the server fail instead of us truncating long tests, which won&rsquo;t make the tests pass, so I prompted:</p>
<div class="full-width no-copy-button">
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-plaintext" data-lang="plaintext"><span class="line"><span class="cl">before our changes this library truncated any text &gt;500 chars, so we need to retain that logic. BUT if we end with a URL let it through and let the server</span></span></code></pre></div>
</div>
<p>Claude then suggested a flow that seems to work:</p>
<div class="full-width no-copy-button">
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-plaintext" data-lang="plaintext"><span class="line"><span class="cl">New logic:
</span></span><span class="line"><span class="cl">1. First check actual length (not effective length)
</span></span><span class="line"><span class="cl">2. If text.length &lt;= 500 → return text as-is (no truncation)
</span></span><span class="line"><span class="cl">3. If text.length &gt; 500:
</span></span><span class="line"><span class="cl">  - Check if characters after position 500 are part of a URL
</span></span><span class="line"><span class="cl">  - If yes (would break a URL) → return full text unchanged
</span></span><span class="line"><span class="cl">  - If no (normal text overflow) → truncate at position 500
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">This preserves backward compatibility:
</span></span><span class="line"><span class="cl">- Original behavior: truncate at 500 actual characters
</span></span><span class="line"><span class="cl">- New behavior: same, BUT skip truncation if it would break a URL</span></span></code></pre></div>
</div>
<p>And Claude implemented the fix and did it TDD style and highlighted that we have more code than necessary to pass, so I need to write another test case that exercises all the extra code we have in <code>truncateWithUrlPreservation</code></p>
<h2 id="2025-10-11">
  2025-10-11
  <a href="#2025-10-11" class="heading-anchor" aria-label="Direct link to 2025-10-11"><span aria-hidden="true">#</span></a>
</h2>
<p><strong>14:43</strong> The next day, I ended up watching TV yesterday, and today I&rsquo;m feeling sluggish</p>
<p>But I was reflecting about this yesterday and I&rsquo;m a bit unsure about how to write a test that refuses the current &ldquo;fix&rdquo; of &quot; if the actual is less than 500 let through&quot; because it&rsquo;s true since the edge-cases it won&rsquo;t do it for is basically where the URL is wrong, BUT, the URL could be wrong at the start and the text after is okay to be truncated. I.e. cases where say <code>https://example.com</code> (19) x 21 =  = 399 actual but the effective length is 483 and if I add 18 characters of text it&rsquo;ll fail and could be truncated.</p>
<p>Maybe that&rsquo;s the case to do, because it&rsquo;ll force it to handle it correctly.</p>
<p>I ended up changing the &ldquo;this can be truncated&rdquo; test so the URL is shorter and the text is exactly so it <em>should</em> pass, and then pointed out that we need to hint that we need the special logic if there are URLs in the text.</p>
<div class="full-width no-copy-button">
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-plaintext" data-lang="plaintext"><span class="line"><span class="cl">run the tests and suggest how to fix the them while not changing any tests, the underlying business logic is incorrect and too complex. Hint: consider only doing the special handling logic if there is a URL in the status text</span></span></code></pre></div>
</div>
<p>Claude came back with a plan:</p>
<div class="full-width no-copy-button">
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-plaintext" data-lang="plaintext"><span class="line"><span class="cl">New simplified logic:
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">1. Extract URLs first to determine which path to take
</span></span><span class="line"><span class="cl">2. If NO URLs in text:
</span></span><span class="line"><span class="cl">  - Simple check: text.length &lt;= 500 → return as-is
</span></span><span class="line"><span class="cl">  - Otherwise → truncate at 500 actual characters
</span></span><span class="line"><span class="cl">3. If HAS URLs in text:
</span></span><span class="line"><span class="cl">  - Calculate effective length
</span></span><span class="line"><span class="cl">  - If effective ≤ 500 → return as-is
</span></span><span class="line"><span class="cl">  - If effective &gt; 500:
</span></span><span class="line"><span class="cl">      - Calculate how many actual characters to remove: excess = effective - 500
</span></span><span class="line"><span class="cl">    - Target length: targetLength = text.length - excess
</span></span><span class="line"><span class="cl">    - Check if truncating at targetLength would break a URL (cut it in half)
</span></span><span class="line"><span class="cl">    - If YES (would break URL) → return full text unchanged (let Mastodon fail)
</span></span><span class="line"><span class="cl">    - If NO (safe to truncate) → return text.slice(0, targetLength)</span></span></code></pre></div>
</div>
<p>And that update gave me the logic I wanted, so now let&rsquo;s see what the change looks like and it&rsquo;s good.</p>
<p>As I now look at it and am preparing the commit message I realize, this doesn&rsquo;t fit the original design of this library/n8n node: it was blindly truncating, so I could also just truncate <em>just before</em> this URL so we can post. It doesn&rsquo;t <strong>fit</strong> with how I discovered this problem, which was a truncated URL (which was still longer than 23 characters but wrong).</p>
<p>So, with that, I will instead say: nix the URL because it goes with the grain of the library and people like me have to ensure they truncate correctly. It&rsquo;s the least surprising thing we can do. But, I have done all this work and despite realizing that I went down the wrong path, I should commit what I got so I can see the changes. I learned something and that&rsquo;s still a win.</p>
<p>Okay, done , and I asked Claude to look at the comments I&rsquo;ve tried to add to clarify the logic and it suggested some improvements to them which I&rsquo;ve folded into the commit.</p>
<div class="full-width no-copy-button">
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-plaintext" data-lang="plaintext"><span class="line"><span class="cl">review the changes we&#39;ve done, are the explanations in text clear enough that those reading the code would be able to understand?</span></span></code></pre></div>
</div>
<p><a href="https://github.com/gaqzi/n8n-nodes-the-mastodon/commit/5c311e2212bef9bbd5beb949d9e656c0ccba374a">Committed.</a></p>
<p>I&rsquo;m not sure if using Claude here has saved me time, but I have been doing it in low energy and while watching TV, so upside is that it got done compared to that I likely wouldn&rsquo;t have put in the full energy otherwise.</p>
<p>Although, partly that&rsquo;s because when fixing the bad stuff I didn&rsquo;t want to go in and learn how all the code worked, becasue I&rsquo;m relying on the tests to tell me what&rsquo;s working and not, so that&rsquo;s pure laziness on my part. And the fact it dug through the full codebase and made a first pass when I was turning in for the night is <em>sooooo</em> worth it. I&rsquo;m sure I would&rsquo;ve spent more time figuring out the codebase on my own otherwise.</p>
<p>But, I am also saying this <em>before</em> I have manually tested any of this. I have only run the tests so far and not plugged it into n8n and seen if it works. So my optimism might still be premature.</p>
<p>Okay, updated one test file and the corresponding business logic, let&rsquo;s start with the next test file (which I changed a little for the previous task, so I know there&rsquo;s work to do).</p>
<h3 id="2025-10-12">
  2025-10-12
  <a href="#2025-10-12" class="heading-anchor" aria-label="Direct link to 2025-10-12"><span aria-hidden="true">#</span></a>
</h3>
<p>I did some final cleanup yesterday and didn&rsquo;t document it all, it&rsquo;s <a href="https://github.com/gaqzi/n8n-nodes-the-mastodon/commits/url-aware-post-length-calculation/">only available in the commits</a>, but I basically went over the changes as I would any PR at work and then raised it. The maintainer looked at it during the day and then <a href="https://github.com/redoracle/n8n-nodes-the-mastodon/pull/4#issuecomment-3394429927">left a nice note</a> thanking me for being thorough and documenting my code well 🥰</p>
<blockquote>
<p><strong>Hey Björn!</strong></p>
<p>Thank you so much for your outstanding contribution with PR #4! 🚀</p>
<p>Your pull request to fix the URL character counting behavior was absolutely <strong>fantastic</strong> - it addressed a real problem that many users probably didn&rsquo;t even realize they were experiencing. The fact that Mastodon counts all URLs as exactly 23 characters regardless of their actual length is such an important detail, and your implementation handles it perfectly.</p>
<h3 id="what-i-especially-appreciated-about-your-work">
  What I especially appreciated about your work:
  <a href="#what-i-especially-appreciated-about-your-work" class="heading-anchor" aria-label="Direct link to What I especially appreciated about your work:"><span aria-hidden="true">#</span></a>
</h3>
<p>✨ <strong>Thorough Research</strong> - You referenced the actual Mastodon source code to validate the behavior<br>
🧪 <strong>Comprehensive Testing</strong> - 19 new tests covering all the edge cases!<br>
📚 <strong>Excellent Documentation</strong> - Clear comments explaining the &ldquo;why&rdquo; behind the logic<br>
🔧 <strong>Smart Implementation</strong> - The decision to remove entire URLs rather than break them is brilliant<br>
🔒 <strong>Clean Integration</strong> - Minimal changes to existing code while adding powerful functionality</p>
<h3 id="the-impact">
  The Impact:
  <a href="#the-impact" class="heading-anchor" aria-label="Direct link to The Impact:"><span aria-hidden="true">#</span></a>
</h3>
<p>Your fix means users won&rsquo;t have their URLs unexpectedly broken when posting longer content. That&rsquo;s a real quality-of-life improvement that will prevent confusion and frustration.</p>
<h3 id="code-quality">
  Code Quality:
  <a href="#code-quality" class="heading-anchor" aria-label="Direct link to Code Quality:"><span aria-hidden="true">#</span></a>
</h3>
<p>The implementation is clean, well-tested, and follows all the best practices. It&rsquo;s the kind of contribution that makes maintaining open source projects a joy!</p>
<p><strong>Would you be interested in contributing more to this project?</strong> Your expertise with Mastodon&rsquo;s APIs and attention to detail would be incredibly valuable.</p>
<p>Thanks again for taking the time to identify this issue, implement a robust solution, and provide such thorough testing. This is exactly the kind of contribution that makes the open source community amazing! 🙌</p>
<p>Best regards</p>
</blockquote>
<p>As they said, these interactions make open source fun because it&rsquo;s people working and building stuff together 🙂</p>
]]></content:encoded></item><item><title>Apple Notes can do calculations with variables</title><link>https://bjorn.now/til/2025-10-26-apple-notes-can-do-calculations-with-variables/?utm_medium=feed</link><pubDate>Sun, 26 Oct 2025 12:35:12 +0100</pubDate><guid>https://bjorn.now/til/2025-10-26-apple-notes-can-do-calculations-with-variables/</guid><description>Apple Notes can evaluate calculations with variables that update as you change them. The variable name has to be “one word,” so usual camelCase or PascalCase will likely do you best.
I realized this while trying to calculate what height TV bench I should get:
The center of the TV should be about eye level, so what height bench should I get given myself and the sofa? Seems around 60cm will be good. Update: I realized the TV stand adds to the height of the TV bench, so it shouldn’t be part of the center of TV calculation.</description><content:encoded><![CDATA[<p>Apple Notes can evaluate calculations with variables that update as you change them.
The variable name has to be &ldquo;one word,&rdquo;
so usual <code>camelCase</code> or <code>PascalCase</code> will likely do you best.</p>
<p>I realized this while trying to calculate what height TV bench I should get:</p>
<figure><img src="/img/2025/10-tv-bench-calculations.jpeg"
    alt="Apple Notes calculations determining TV bench height using variables for TV size, sofa height, and eye level"><figcaption>
      <p>The center of the TV should be about eye level, so what height bench should I get given myself and the sofa? Seems around 60cm will be good. <strong>Update:</strong> I realized the TV stand adds to the height of the TV bench, so it shouldn&rsquo;t be part of the center of TV calculation.</p>
    </figcaption>
</figure>

<p>Annoyingly, you can&rsquo;t declare a variable and see its result in one step.
I found two options for this, and you decide which you prefer.</p>
<p>If you&rsquo;re okay with an extra line you can show it by declaring the variable again, just let notes fill in the part after the <code>=</code>:</p>
<div class="no-copy-button">
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">MyValue=25*4
</span></span><span class="line"><span class="cl">MyValue=100</span></span></code></pre></div>
</div>
<p>Or, instead, write the calculation first,
and let Notes show the result, then add the variable name.</p>
<p>First, do the calculation:</p>
<div class="no-copy-button">
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">25*4=&lt;wait for Notes to autofill&gt;</span></span></code></pre></div>
</div>
<p>Second, name the variable by adding the name and <code>=</code> at the front of the line:</p>
<div class="no-copy-button">
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">MyValue=25*4=100</span></span></code></pre></div>
</div>
<p>I naturally ended up doing this second while I was working it through,
since I wanted to see the result first, and then I named the variable.</p>
<p>Notes also handle some <a href="https://support.apple.com/en-sg/guide/iphone/iph69d274dde/26/ios/26">unit conversions</a> for you:</p>
<div class="no-copy-button">
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">The average daily temperature in Singapore as Fahrenheit is: 
</span></span><span class="line"><span class="cl">0f + 32c = 89.6f
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">How many Swedish Krona do I get from one SGD right now?
</span></span><span class="line"><span class="cl">0sek + 1sgd = 7.22 SEK</span></span></code></pre></div>
</div>
<p>I started on my phone taking Notes,
and was surprised to realize I didn&rsquo;t have to fire up Python on my computer to finish it.
A new useful tool that I always carry with me. 🙂</p>
<hr>
<p>Documentation links to Apple:</p>
<ul>
<li><a href="https://support.apple.com/en-sg/guide/iphone/iphb9c2b948f/ios">for iOS</a></li>
<li><a href="https://support.apple.com/en-sg/guide/notes/apda85974595/mac">for macOS</a></li>
</ul>
]]></content:encoded></item><item><title>Explainle: a daily game to describe words</title><link>https://bjorn.now/link/2025-10-24-explainle-a-daily-game-to-describe-words/?utm_medium=feed</link><pubDate>Fri, 24 Oct 2025 14:42:18 +0200</pubDate><guid>https://bjorn.now/link/2025-10-24-explainle-a-daily-game-to-describe-words/</guid><description>I have enjoyed doing the daily Explainle.org for the last couple of days. It triggers some of the fun bits of language for me. 🙂
The twist compared to straight up “the opposite of this” is that there are “forbidden words” that you’re not allowed to use, and only 100 characters.
Luckily for me, it’s relying on an LLM and is therefore quite nerdy, so I can explain it in language I understand</description><content:encoded><![CDATA[<p>I have enjoyed doing the daily <a href="https://explainle.org?utm_source=bjorn.now">Explainle.org</a>
for the last couple of days. It triggers some of the fun bits of language for me. 🙂</p>
<p>The twist compared to straight up &ldquo;the opposite of this&rdquo; is that
there are &ldquo;forbidden words&rdquo; that you&rsquo;re not allowed to use,
and only 100 characters.</p>
<figure><img src="/img/2025/10-explainle.jpeg"
    alt="Screenshot of Explainle game showing the word AGGREGATE with forbidden words (total, sum, combine, collection, accumulate) crossed out. Previous attempt shows explanation &#39;an SQL AVG statement needs a GROUP BY because it&#39;s an...&#39; with the guess AGGREGATE marked correct with a green checkmark."><figcaption>
      <p>Luckily for me, it&rsquo;s relying on an LLM and is therefore quite nerdy, so I can explain it in language <em>I</em> understand</p>
    </figcaption>
</figure>

]]></content:encoded></item><item><link>https://bjorn.now/scrap/2025-10-23t140116/?utm_medium=feed</link><pubDate>Thu, 23 Oct 2025 14:01:16 +0200</pubDate><guid>https://bjorn.now/scrap/2025-10-23t140116/</guid><description>and now the bank has confirmed that I’ve sent the money, and sent it on to Bolagsverket.
I love the transparency and setup of their page explaining how long it should take to process my application, there’s a goal (7 working days), and which applications they’re aiming to process this week (those from 14-19th of October). Wish more gov’t services did this. eyes Försäkringskassan</description><content:encoded><![CDATA[<p>and now the <a href="/scrap/2025-10-23t104336/">bank has confirmed</a> that I&rsquo;ve sent the money, and sent it on to Bolagsverket.</p>
<p>I love the transparency and setup of <a href="https://bolagsverket.se/omoss/varverksamhet/varservice/varahandlaggningstider.2081.html">their page</a> explaining how long it should take to process my application, there&rsquo;s a goal (7 working days), and which applications they&rsquo;re aiming to process this week (those from 14-19th of October). Wish more gov&rsquo;t services did this. <em>eyes Försäkringskassan</em></p>
]]></content:encoded></item><item><title>Disposable vs Durable code</title><link>https://bjorn.now/link/2025-10-23-disposable-vs-durable-code/?utm_medium=feed</link><pubDate>Thu, 23 Oct 2025 13:30:43 +0200</pubDate><guid>https://bjorn.now/link/2025-10-23-disposable-vs-durable-code/</guid><description>Disposable code is here to stay by Charity Majors.
I like the distinction between disposable and durable code: it’s about whether you try to make the code maintainable or not. Most code when working at a company is durable code, because the outcome when we get it wrong is so high (even if it’s just the risk of losing money or at worst, our job).
And the idea that we can experiment, create POCs, the 10 variations of an idea to see which one works better… that’s great. Work with the genie and figure it out, then spend the proper time and effort making it durable.</description><content:encoded><![CDATA[<p><a href="https://www.honeycomb.io/blog/disposable-code-is-here-to-stay">Disposable code is here to stay</a> by Charity Majors.</p>
<p>I like the distinction between disposable and durable code:
it&rsquo;s about whether you try to make the code maintainable or not.
Most code when working at a company is durable code,
because the outcome when we get it wrong is so high
(even if it&rsquo;s just the risk of losing money or at worst, our job).</p>
<p>And the idea that we can experiment, create POCs,
the 10 variations of an idea to see which one works better… that&rsquo;s great.
Work with the genie and figure it out,
then spend the proper time and effort making it durable.</p>
<blockquote>
<p>Durable code is used when the stakes are high, the losses are material, the ripple effects of failure or performance changes throughout the system are complex and unpredictable. Highly regulated industries require durable software.<br>
[…]<br>
Here’s one (unscientific) rule of thumb: the lower on the stack you go, the closer you get to laying bits down on disk, the more dependable and predictable our software has to be. Databases and operating systems will be durable code til the stars die out.<br>
[…]<br>
The difference between disposable code and durable code is not about whether the code was generated by AI or written by a human, or even how difficult it was to write. The cost is defined by the standards you are building to, and the rest of the software development lifecycle: how well you expect to maintain it, extend it, migrate it, understand its behavior, or fix it when it breaks. This is the expensive part of software development, the type that requires deep expertise and familiarity with your language and environment. Disposable code is cheap because you don’t even try to maintain it.<br>
[…]<br>
This isn’t about “using AI” vs. “not using AI,” or even about generating code vs. not generating code. It’s about understanding the cost model and the risk profile of the type of development you’re engaged in.<br>
[…]<br>
If it’s easy to achieve proficiency with disposable code generation tools, it probably won’t be thought of as software development (and the pay scale will drop accordingly). If I had to guess, I suspect it won’t be a profession at all so much as a skill set, much like typing or spreadsheets, that any tech-literate modern worker is expected to pick up in order to perform the functions of their job in marketing, sales, product, design, etc.<br>
[…]<br>
The coordination tax on large groups is enormous; if AI can help a smaller number of engineers own more product surface and move faster with more confidence, <em>that’s exciting</em>.</p>
</blockquote>
<p>As Charity says, I think there likely will be changes in what programming,
or rather, software development, is.
Just as it <a href="https://bjorn.now/blog/2025/08/10/when-everyone-gets-a-genie/">did with typing before</a>,
a skill every office worker just need to have. Not that all of us are writers, just because of it.</p>
<p>I&rsquo;m also excited at how working with a genie might open up codebases,
so you need fewer people to own and maintain larger codebases (because maintenance is in communication within a team).</p>
<p>For example, I recently contributed to a new codebase in relatively little time
and it was done so fast to the genie figuring out where to make changes.
Even if I had to change most of it,
because the quality wasn&rsquo;t at the level where I wanted to <a href="https://bjorn.now/blog/2025/07/24/your-name-is-still-on-it/">put my name on it.</a></p>
]]></content:encoded></item><item><link>https://bjorn.now/scrap/2025-10-23t104336/?utm_medium=feed</link><pubDate>Thu, 23 Oct 2025 10:43:37 +0200</pubDate><guid>https://bjorn.now/scrap/2025-10-23t104336/</guid><description>aktiekapital sent for starting my company, the bank has talked with me and decided I’m allowed to have an account with them, next up, get approved by the gov’t.
also TIL, in Singapore you only need $1 as starting share capital and in SE it’s a minimum of 25,000 SEK (~3,500SGD)</description><content:encoded><![CDATA[<p>aktiekapital sent for starting my company, the bank has talked with me and decided I&rsquo;m allowed to have an account with them, next up, get approved by the gov&rsquo;t.</p>
<p>also TIL, in Singapore you only need $1 as <a href="https://www.acra.gov.sg/how-to-guides/setting-up-a-local-company/share-capital">starting share capital</a> and in SE it&rsquo;s a minimum of 25,000 SEK (~3,500SGD)</p>
]]></content:encoded></item><item><link>https://bjorn.now/scrap/2025-10-09t170155/?utm_medium=feed</link><pubDate>Thu, 09 Oct 2025 17:01:57 +0200</pubDate><guid>https://bjorn.now/scrap/2025-10-09t170155/</guid><description>the first (digital) forms signed on the quest of starting my own company, now the waiting begins</description><content:encoded>&lt;p>the first (digital) forms signed on the quest of starting my own company, now the waiting begins&lt;/p>
</content:encoded></item><item><title>Don't quote regex patterns in Bash [[ ]] tests, because they'll match literally</title><link>https://bjorn.now/til/2025-10-09-dont-quote-regex-patterns-in-bash-tests-because-theyll-match-literally/?utm_medium=feed</link><pubDate>Thu, 09 Oct 2025 11:50:33 +0200</pubDate><guid>https://bjorn.now/til/2025-10-09-dont-quote-regex-patterns-in-bash-tests-because-theyll-match-literally/</guid><description>Don’t quote regex patterns in Bash [[ ]] tests, because they’ll match literally.
So the below won’t match because it’s looking for a literal $ in the string, instead of matching at the end of the string:
file="https://example.com/blog-post/index.html" [[ "$file" =~ "/index.html$" ]] &amp;&amp; echo MATCH But this will:
file="https://example.com/blog-post/index.html" [[ "$file" =~ /index\.html$ ]] &amp;&amp; echo MATCH Note: [[ ]] is a Bash extension of the test command that supports regexes (among other things). The [ ] is the POSIX standard test (see man test), so [[ ]] features may not work in other shells or in limited shells like ash or sh.</description><content:encoded><![CDATA[<p>Don&rsquo;t quote regex patterns in Bash <code>[[ ]]</code> <a href="https://www.gnu.org/software/bash/manual/html_node/Conditional-Constructs.html#index-_005b_005b">tests</a>, because they&rsquo;ll match literally.</p>
<p>So the below won&rsquo;t match because it&rsquo;s looking for a literal <code>$</code> in the string,
instead of matching at the end of the string:</p>
<div class=" copy-enabled">
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl"><span class="nv">file</span><span class="o">=</span><span class="s2">&#34;https://example.com/blog-post/index.html&#34;</span>
</span></span><span class="line"><span class="cl"><span class="o">[[</span> <span class="s2">&#34;</span><span class="nv">$file</span><span class="s2">&#34;</span> <span class="o">=</span>~ <span class="s2">&#34;/index.html</span>$<span class="s2">&#34;</span> <span class="o">]]</span> <span class="o">&amp;&amp;</span> <span class="nb">echo</span> MATCH</span></span></code></pre></div>
</div>
<p>But this will:</p>
<div class=" copy-enabled">
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl"><span class="nv">file</span><span class="o">=</span><span class="s2">&#34;https://example.com/blog-post/index.html&#34;</span>
</span></span><span class="line"><span class="cl"><span class="o">[[</span> <span class="s2">&#34;</span><span class="nv">$file</span><span class="s2">&#34;</span> <span class="o">=</span>~ /index<span class="se">\.</span>html$ <span class="o">]]</span> <span class="o">&amp;&amp;</span> <span class="nb">echo</span> MATCH</span></span></code></pre></div>
</div>
<p><strong>Note:</strong> <code>[[ ]]</code> is a Bash extension of the test command that supports regexes (among other things).
The <code>[ ]</code> is the POSIX standard test (see <code>man test</code>),
so <code>[[ ]]</code> features may not work in other shells or in limited shells like ash or sh.</p>
<p>I learned this while removing <code>index.html</code> from file paths to bust Cloudflare&rsquo;s cache for directory listings/pretty URLs.
I then used <a href="https://www.gnu.org/software/bash/manual/html_node/Shell-Parameter-Expansion.html">parameter expansion</a> to replace the match with an empty string, so directory URLs are also purged:</p>
<div class=" copy-enabled">
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl"><span class="nb">echo</span> <span class="si">${</span><span class="nv">file</span><span class="p">/index.html/</span><span class="si">}</span></span></span></code></pre></div>
</div>
]]></content:encoded></item><item><title>The regex | (or) operator splits the pattern left or right unless explicitly grouped</title><link>https://bjorn.now/til/2025-10-08-regex-or-operator-splits-pattern-left-or-right-unless-constrained/?utm_medium=feed</link><pubDate>Wed, 08 Oct 2025 23:34:03 +0200</pubDate><guid>https://bjorn.now/til/2025-10-08-regex-or-operator-splits-pattern-left-or-right-unless-constrained/</guid><description>The regex | (or) operator splits the pattern left or right unless explicitly grouped. Group it using (pattern) if you need to use the match later, or as a non-capturing group (?:pattern) if you do it for clarity.
I used to think there was some magic rule about how | decided where to split, but it’s simply: characters and subpatterns concatenate into a single pattern first, then | splits the entire thing left and right unless you explicitly group it. That’s because | has the lowest precedence so all other operations happen first.
So while I used to think that prefix_cat|dog would match prefix_cat and prefix_dog, it actually matches prefix_cat and then dog.
And that exact problem in a real-world example, to find if a page is linking to another, both relative or full URL:</description><content:encoded><![CDATA[<p>The regex <code>|</code> (or) operator splits the pattern left or right unless explicitly grouped.
Group it using <code>(pattern)</code> if you need to use the match later,
or as a non-capturing group <code>(?:pattern)</code> if you do it for clarity.</p>
<p>I used to think there was some magic rule about how <code>|</code> decided where to split,
but it&rsquo;s simply: characters and subpatterns concatenate into a single pattern first,
then <code>|</code> splits the entire thing left and right unless you explicitly group it.
That&rsquo;s because <code>|</code> <a href="https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap09.html#tag_09_04_08">has the lowest precedence</a> so <em>all other</em> operations happen first.</p>
<p>So while I used to think that <code>prefix_cat|dog</code> would match <code>prefix_cat</code> and <code>prefix_dog</code>,
it actually matches <code>prefix_cat</code> and then <code>dog</code>.</p>
<p>And that exact problem in a real-world example, to find if a page is linking to another, both relative or full URL:</p>
<p>The regex <code>&lt;a[^&gt;]+href=[&quot;']https://www.example.com/page|/page</code> will match <code>/page</code>,
just as in the <code>dog</code> example, it&rsquo;s splitting the whole pattern and isn&rsquo;t what I want.</p>
<p>See it in action with <a href="https://regex101.com/r/6RylLz/2">the excellent Regex101.com</a>:</p>
<figure><img src="/img/2025/10-regex-01.png"
    alt="The Regex101.com site showing the Regex in action and that it matches /page">
</figure>

<p>To make it do what I want I need to constrain it in a capturing group <code>(pattern)</code>,
and since I don&rsquo;t want to reference the group later,
I constrain it inside a non-capturing group <code>(?:pattern)</code>.</p>
<p>That gives me the pattern <code>&lt;a[^&gt;]+href=[&quot;'](?:https://www\.example\.com/page|/page)</code>,
and <a href="https://regex101.com/r/kgeGyp/3">then it matches</a> both the full URL (<a href="https://www.example.com/page">https://www.example.com/page</a>) and the relative URL (/page):</p>
<figure><img src="/img/2025/10-regex-02.png"
    alt="The Regex101.com site showing the Regex in action and that it matches both links per the pattern as I intended">
</figure>

<p>I love Regex101.com. 🙂
I have commented gnarly regexes in my code with links to it,
so colleagues can paste them there and understand what&rsquo;s going on.
The sidebar explains what each subpattern does,
and the color highlighting shows what matches what in real-time.</p>]]></content:encoded></item><item><link>https://bjorn.now/scrap/2025-10-08t162827/?utm_medium=feed</link><pubDate>Wed, 08 Oct 2025 16:28:28 +0200</pubDate><guid>https://bjorn.now/scrap/2025-10-08t162827/</guid><description>I just caught myself thinking, “I need to get coasters” so turns out that’s a thing I’m missing from my Singapore stuff 😅</description><content:encoded><![CDATA[<p>I just caught myself thinking, &ldquo;I need to get coasters&rdquo; so turns out that&rsquo;s a thing I&rsquo;m missing from my Singapore stuff 😅</p>
]]></content:encoded></item><item><title>Execute the data node and pin instead of rerunning the workflow to get fresh data in n8n</title><link>https://bjorn.now/til/2025-10-08-execute-the-data-node-and-pin-instead-of-rerunning-workflow-to-get-fresh-data-in-n8n/?utm_medium=feed</link><pubDate>Wed, 08 Oct 2025 16:23:39 +0200</pubDate><guid>https://bjorn.now/til/2025-10-08-execute-the-data-node-and-pin-instead-of-rerunning-workflow-to-get-fresh-data-in-n8n/</guid><description>Execute the data node/step and pin it, instead of rerunning the workflow or the step that relies on that data, when modifying a step that needs fresh data.
Example: When modifying Mastodon/Bluesky cross-posting nodes that depend on an RSS Feed Trigger, execute and pin the RSS node. Then iterate on the posting logic that only depends on the RSS data without triggering any posting.</description><content:encoded><![CDATA[<p>Execute the data node/step and pin it,
instead of rerunning the workflow or the step that relies on that data,
when modifying a step that needs fresh data.</p>
<figure><img src="/img/2025/10-n8n-step-rerun.png"
    alt="n8n workflow showing a sequential workflow starting with RSS fetching and going through Telegram, Mastodon, and Bluesky">
</figure>

<p>Example: When modifying Mastodon/Bluesky cross-posting nodes that depend on an RSS Feed Trigger,
execute and pin the RSS node.
Then iterate on the posting logic that <em>only depends</em> on the RSS data without triggering any posting.</p>
<p>Where my confusion came from: I haven&rsquo;t figured out fan-out and fan-in,
because I would expect to run my cross-posting in parallel and not sequentially,
but… I have seen there <em>is</em> some kind of fan-out, but how to use it? 🤷</p>
<p>So my assumption that I wouldn&rsquo;t get the data reloaded if I <strong>only</strong> ran the first step was incorrect,
and I&rsquo;ve been a bit hesitant to test since there&rsquo;s only production (this personal stuff doesn&rsquo;t have a proper testing env 😅).</p>]]></content:encoded></item><item><title>Hugo's resources.Get only finds files in assets/, not static/</title><link>https://bjorn.now/til/2025-10-08-hugos-resources-get-only-finds-files-in-assets-not-static/?utm_medium=feed</link><pubDate>Wed, 08 Oct 2025 15:14:34 +0200</pubDate><guid>https://bjorn.now/til/2025-10-08-hugos-resources-get-only-finds-files-in-assets-not-static/</guid><description>Hugo’s resources.Get only finds files in assets/, not static/.
I kept my assets/resources in static/ and didn’t get why I always had to write /absolute/urls and use hope as a strategy, when I moved them into assets/ I could look up all my assets.</description><content:encoded><![CDATA[<p>Hugo&rsquo;s <code>resources.Get</code> only finds files in <code>assets/</code>, not <code>static/</code>.</p>
<p>I kept my assets/resources in <code>static/</code> and didn&rsquo;t get why I always had to write <code>/absolute/urls</code> and use hope as a strategy,
when I moved them into <code>assets/</code> I could look up all my assets.</p>
<p>The below would fail because it couldn&rsquo;t find the asset:</p>
<div class=" copy-enabled">
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-plain" data-lang="plain"><span class="line"><span class="cl">{{ with resources.Get &#34;/author/bjorn.jpg&#34; }}
</span></span><span class="line"><span class="cl">  &lt;img src=&#34;{{ .RelPermalink }}&#34; alt=&#34;Björn Andersson&#34;&gt;
</span></span><span class="line"><span class="cl">{{ end }}</span></span></code></pre></div>
</div>
<p>Hugo has global assets accessed through <code>resources.Get</code> which <strong>only</strong> looks up in <code>assets/</code>.
Then page assets that you get through <code>.Page.Resources.Get</code> instead, which I haven&rsquo;t used yet.</p>
<p>If you want to use both, then do like the <a href="https://github.com/gohugoio/hugo/blob/4414ef73f3cd490caf93e0d50a6102db9ab28318/tpl/tplimpl/embedded/templates/_shortcodes/figure.html#L9C5-L11C16">figure shortcode</a> and do it in an or:</p>
<div class=" copy-enabled">
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-plain" data-lang="plain"><span class="line"><span class="cl">{{- with or (.Page.Resources.Get $u.Path) 
</span></span><span class="line"><span class="cl">            (resources.Get $u.Path) -}}
</span></span><span class="line"><span class="cl">{{- $src = .RelPermalink -}}
</span></span><span class="line"><span class="cl">{{- end -}}</span></span></code></pre></div>
</div>]]></content:encoded></item><item><link>https://bjorn.now/scrap/2025-10-06t231415/?utm_medium=feed</link><pubDate>Mon, 06 Oct 2025 23:14:15 +0200</pubDate><guid>https://bjorn.now/scrap/2025-10-06t231415/</guid><description>urgh. I’m trying out ChatGPT, and it really is telling me everything is a great idea, just like South Park said:
(I really don’t feel I get this from Claude, but maybe my personal base prompt helps there?)</description><content:encoded><![CDATA[<p>urgh. I&rsquo;m trying out ChatGPT, and it really is telling me everything is a great idea, just <a href="https://www.youtube.com/watch?v=sDf_TgzrAv8">like South Park said</a>:</p>
<div style="position: relative; padding-bottom: 56.25%; height: 0; overflow: hidden;">
      <iframe allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share; fullscreen" loading="eager" referrerpolicy="strict-origin-when-cross-origin" src="https://www.youtube.com/embed/sDf_TgzrAv8?autoplay=0&amp;controls=1&amp;end=0&amp;loop=0&amp;mute=0&amp;start=0" style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; border:0;" title="YouTube video"></iframe>
    </div>

<p>(I really don&rsquo;t feel I get this from Claude, but maybe my personal base prompt helps there?)</p>
]]></content:encoded></item><item><link>https://bjorn.now/scrap/2025-10-04t122645/?utm_medium=feed</link><pubDate>Sat, 04 Oct 2025 12:26:45 +0200</pubDate><guid>https://bjorn.now/scrap/2025-10-04t122645/</guid><description>Being back and speaking Swedish every day, and more than in years, I have felt that my Swedish has atrophied a bit. That I’m not able to express myself as well in it as in English, so I decided to read a bit more in Swedish.
August Strindberg’s Dödsdansen &amp; Spöksonaten and Fredrik Backman’s Björnstad</description><content:encoded><![CDATA[<p>Being back and speaking Swedish every day, and more than in years, I have felt that my Swedish has atrophied a bit. That I&rsquo;m not able to express myself as well in it as in English, so I decided to read a bit more in Swedish.</p>
<figure><img src="/img/2025/10-first-swedish-books.jpeg"
    alt="Two books resting on a black cushion: a cream-colored vintage edition of August Strindberg plays and a Fredrik Backman novel with a blue winter scene cover showing a hockey player, with a potted plant visible in the background"><figcaption>
      <p><a href="https://en.wikipedia.org/wiki/August_Strindberg">August Strindberg</a>&rsquo;s <a href="https://en.wikipedia.org/wiki/The_Dance_of_Death_(Strindberg_play)">Dödsdansen</a> &amp; <a href="https://en.wikipedia.org/wiki/The_Ghost_Sonata">Spöksonaten</a> and <a href="https://en.wikipedia.org/wiki/Fredrik_Backman">Fredrik Backman</a>&rsquo;s <a href="https://en.wikipedia.org/wiki/Beartown_(novel)">Björnstad</a></p>
    </figcaption>
</figure>

<p>I ended up getting a very contemporary author and one very much not. I read Backman&rsquo;s blog back before he got his first book published, so I knew how funny he was and I read half of Beartown yesterday, he&rsquo;s really nailing the storytelling. I had a hard time putting the book down.</p>
<p>Strindberg is famous and I haven&rsquo;t ever read or seen anything by him (that I know of), so why not. I suspect it&rsquo;ll be a harder read in many ways. It was a very random pick, because I just strolled around the library (second library card down, now I can use Libby from my Kobo), and I totally picked it because Ghost has a song called <a href="https://genius.com/Ghost-spoksonat-lyrics">Spöksonat</a>, so why not…</p>
]]></content:encoded></item><item><link>https://bjorn.now/scrap/2025-10-04t105635/?utm_medium=feed</link><pubDate>Sat, 04 Oct 2025 10:56:35 +0200</pubDate><guid>https://bjorn.now/scrap/2025-10-04t105635/</guid><description>my application to register as residing in Sweden (folkbokförd) has been approved, no longer a ghost merely living here.
I can now get a convenience store membership since the systems can look me up 🙄🥳</description><content:encoded><![CDATA[<p>my application to register as residing in Sweden (folkbokförd) has been approved, no longer <a href="https://bjorn.now/crumb/2025-09-23-21st-century-digital-society/">a ghost merely living here.</a></p>
<p>I can now get a convenience store membership since the systems can look me up 🙄🥳</p>
]]></content:encoded></item><item><title>Use usermod to rename users on Linux</title><link>https://bjorn.now/til/2025-10-01-use-usermod-to-rename-users-on-linux/?utm_medium=feed</link><pubDate>Wed, 01 Oct 2025 13:24:05 +0200</pubDate><guid>https://bjorn.now/til/2025-10-01-use-usermod-to-rename-users-on-linux/</guid><description>Use usermod -l newuser -d /home/newuser -m olduser when renaming a user account, because it’ll update all relevant system files and move the home folder in one go.
Then groupmod -n newgroup oldgroup if you need to rename the group.</description><content:encoded><![CDATA[<p>Use <code>usermod -l newuser -d /home/newuser -m olduser</code> when renaming a user account, because it&rsquo;ll update all relevant system files and move the home folder in one go.</p>
<p>Then <code>groupmod -n newgroup oldgroup</code> if you need to rename the group.</p>
<p>I had a vague memory of being able to change settings (shell etc.) by updating using <code>vipw</code> so I renamed there and then edited <code>/etc/groups</code> but I then got an error when trying to become that user:</p>
<div class="no-copy-button">
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl"># su bjornnow
</span></span><span class="line"><span class="cl">su: Authentication failure</span></span></code></pre></div>
</div>
<p>To fix it, I had to update all these files (and rename the home folder):</p>
<ul>
<li><code>vipw</code>: updates <code>/etc/passwd</code></li>
<li><code>vipw -s</code>: updates <code>/etc/shadow</code> which contains the password hashes and are only readable by root</li>
<li><code>vigr</code>: updates <code>/etc/group</code></li>
<li><code>vigr -s</code>: updates <code>/etc/gshadow</code> the same as passwd (also TIL, groups can have passwords)</li>
</ul>
<p>This is what you get when you think you remember how things work, and didn&rsquo;t want to look up how usermod worked, a little bit of knowledge is dangerous and all… 😅</p>]]></content:encoded></item><item><title>Use --resolve with curl when testing Caddy on localhost</title><link>https://bjorn.now/til/2025-10-01-use-resolve-with-curl-when-testing-caddy-on-localhost/?utm_medium=feed</link><pubDate>Wed, 01 Oct 2025 12:44:45 +0200</pubDate><guid>https://bjorn.now/til/2025-10-01-use-resolve-with-curl-when-testing-caddy-on-localhost/</guid><description>Use curl -k --resolve bjorn.now:443:127.0.0.1 https://bjorn.now when testing an HTTPS site locally using Caddy, instead of curl -k -H "Host: bjorn.now" https://localhost, because Caddy relies on SNI and refuses the connection if it doesn’t know which host you’re trying to connect to.</description><content:encoded><![CDATA[<p>Use <code>curl -k --resolve bjorn.now:443:127.0.0.1 https://bjorn.now</code> when testing an HTTPS site locally using <a href="https://caddyserver.com/">Caddy</a>, instead of <code>curl -k -H &quot;Host: bjorn.now&quot; https://localhost</code>, because Caddy relies on <a href="https://en.wikipedia.org/wiki/Server_Name_Indication">SNI</a> and refuses the connection if it doesn&rsquo;t know which host you&rsquo;re trying to connect to.</p>
<p>This seems to be a security decision to avoid leaking which certificates (and therefore sites) it hosts by not providing a default (can&rsquo;t find a source and it&rsquo;s what the search previews give me). Which my old nginx setup did.</p>
<p>The cryptic error message from cURL, which I didn&rsquo;t find anything obvious online about:</p>
<div class="no-copy-button">
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">curl: (35) OpenSSL/3.0.13: error:0A000438:SSL routines::tlsv1 alert internal error</span></span></code></pre></div>
</div>
<p>And I debugged this together with Claude by enabling debug logging in Caddy (<code>{ debug }</code> at the top level in the <code>Caddyfile</code>) which gave me this output when I called it using cURL:</p>
<div class="no-copy-button">
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">2025/10/01 10:04:56.330 DEBUG   events  event   {&#34;name&#34;: &#34;tls_get_certificate&#34;, &#34;id&#34;: &#34;7353980b-a85e-4f10-9704-89da7358ef89&#34;, &#34;origin&#34;: &#34;tls&#34;, &#34;data&#34;: {&#34;client_hello&#34;:{&#34;CipherSuites&#34;:[4866,4867,4865,49196,49200,159,52393,52392,52394,49195,49199,158,49188,49192,107,49187,49191,103,49162,49172,57,49161,49171,51,157,156,61,60,53,47,255],&#34;ServerName&#34;:&#34;&#34;,&#34;SupportedCurves&#34;:[29,23,30,25,24,256,257,258,259,260],&#34;SupportedPoints&#34;:&#34;AAEC&#34;,&#34;SignatureSchemes&#34;:[1027,1283,1539,2055,2056,2057,2058,2059,2052,2053,2054,1025,1281,1537,771,769,770,1026,1282,1538],&#34;SupportedProtos&#34;:null,&#34;SupportedVersions&#34;:[772,771],&#34;Conn&#34;:{}}}}
</span></span><span class="line"><span class="cl">2025/10/01 10:04:56.330 DEBUG   tls.handshake   no matching certificates and no custom selection logic  {&#34;identifier&#34;: &#34;127.0.0.1&#34;}
</span></span><span class="line"><span class="cl">2025/10/01 10:04:56.331 DEBUG   tls.handshake   all external certificate managers yielded no certificates and no errors {&#34;remote_ip&#34;: &#34;127.0.0.1&#34;, &#34;remote_port&#34;: &#34;42516&#34;, &#34;sni&#34;: &#34;&#34;}
</span></span><span class="line"><span class="cl">2025/10/01 10:04:56.331 DEBUG   tls.handshake   no certificate matching TLS ClientHello {&#34;remote_ip&#34;: &#34;127.0.0.1&#34;, &#34;remote_port&#34;: &#34;42516&#34;, &#34;server_name&#34;: &#34;&#34;, &#34;remote&#34;: &#34;127.0.0.1:42516&#34;, &#34;identifier&#34;: &#34;127.0.0.1&#34;, &#34;cipher_suites&#34;: [4866, 4867, 4865, 49196, 49200, 159, 52393, 52392, 52394, 49195, 49199, 158, 49188, 49192, 107, 49187, 49191, 103, 49162, 49172, 57, 49161, 49171, 51, 157, 156, 61, 60, 53, 47, 255], &#34;cert_cache_fill&#34;: 0.0002, &#34;load_if_necessary&#34;: true, &#34;obtain_if_necessary&#34;: true, &#34;on_demand&#34;: false}
</span></span><span class="line"><span class="cl">2025/10/01 10:04:56.332 DEBUG   http.stdlib     http: TLS handshake error from 127.0.0.1:42516: no certificate available for &#39;127.0.0.1&#39;</span></span></code></pre></div>
</div>
<p>The culprit was the <code>&quot;ServerName&quot;:&quot;&quot;</code>, and I&rsquo;m guessing also <code>&quot;sni&quot;: &quot;&quot;</code>, so if you need to validate and see that as well then that&rsquo;s likely the problem. 🙂</p>
<hr>
<p>This jogs my memory, back in the olden times, we would need a unique IP address for each HTTPS site because we couldn&rsquo;t multi-host, and SNI solved that problem.</p>
<p>Importantly, SNI <em>doesn&rsquo;t</em> encrypt the host name and it&rsquo;s still sent in plaintext to the server.</p>]]></content:encoded></item><item><link>https://bjorn.now/scrap/2025-09-30t182406/?utm_medium=feed</link><pubDate>Tue, 30 Sep 2025 18:24:06 +0200</pubDate><guid>https://bjorn.now/scrap/2025-09-30t182406/</guid><description>I really miss having an induction stovetop. it really is so much better than a normal electric one</description><content:encoded>&lt;p>I really miss having an induction stovetop. it really is so much better than a normal electric one&lt;/p>
</content:encoded></item><item><title>Use :focus-visible instead of :focus, because it will only show when helpful</title><link>https://bjorn.now/til/2025-09-30-focus-visible-instead-of-focus-to-avoid-extra-styling/?utm_medium=feed</link><pubDate>Tue, 30 Sep 2025 13:09:33 +0200</pubDate><guid>https://bjorn.now/til/2025-09-30-focus-visible-instead-of-focus-to-avoid-extra-styling/</guid><description>Use :focus-visible instead of :focus, because it will only show when helpful.
:focus-visible only shows focus styling when the browser determines keyboard navigation is being used, unlike :focus which triggers on all interactions including mouse clicks.</description><content:encoded><![CDATA[<p>Use <code>:focus-visible</code> instead of <code>:focus</code>, because it will <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/:focus-visible#focus_vs_focus-visible">only show when helpful.</a></p>
<p><code>:focus-visible</code> only shows focus styling when the browser determines keyboard navigation is being used, unlike <code>:focus</code> which triggers on all interactions including mouse clicks.</p>
<div class=" copy-enabled">
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-css" data-lang="css"><span class="line"><span class="cl"><span class="nt">button</span><span class="p">:</span><span class="nd">focus-visible</span> <span class="p">{</span> <span class="k">outline</span><span class="p">:</span> <span class="mi">2</span><span class="kt">px</span> <span class="kc">solid</span> <span class="kc">blue</span><span class="p">;</span> <span class="p">}</span></span></span></code></pre></div>
</div>
<p>This prevents the &ldquo;clicked button stays highlighted&rdquo; problem while preserving accessibility for keyboard users. <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/:focus-visible#focus_vs_focus-visible">Full browser logic on MDN</a>.</p>]]></content:encoded></item><item><link>https://bjorn.now/scrap/2025-09-28t090543/?utm_medium=feed</link><pubDate>Sun, 28 Sep 2025 09:05:44 +0200</pubDate><guid>https://bjorn.now/scrap/2025-09-28t090543/</guid><description>okay, don’t turn on the dishwasher while running the kettle. upside, I know the breaker works, and I’m happy it’s flip switch instead of having to replace fuses 🙂</description><content:encoded><![CDATA[<p>okay, don&rsquo;t turn on the dishwasher while running the kettle. upside, I know the breaker works, and I&rsquo;m happy it&rsquo;s flip switch instead of having to replace fuses 🙂</p>
]]></content:encoded></item><item><title>Maybe, let's see in a few weeks</title><link>https://bjorn.now/crumb/2025-09-26-maybe-lets-see-in-a-few-weeks/?utm_medium=feed</link><pubDate>Fri, 26 Sep 2025 14:46:47 +0200</pubDate><guid>https://bjorn.now/crumb/2025-09-26-maybe-lets-see-in-a-few-weeks/</guid><description>Now that I’ve moved back to Sweden without most of my stuff (it’ll be on a ship soon enough), everyone’s got suggestions on necessities. Need to buy all the tools (so I can fix things), the kitchen equipment (because the kitchen is very basic), the cleaning tools (okay, the basicness here might hinder), and the big one: a car. “There are such distances now that you just have to have a car.”
I keep pushing back with “maybe, let’s see in a few weeks.”</description><content:encoded><![CDATA[<p>Now that I&rsquo;ve moved back to Sweden without most of my stuff (it&rsquo;ll be on a ship soon enough), everyone&rsquo;s got suggestions on necessities. Need to buy all the tools (so I can fix things), the kitchen equipment (because the kitchen is very basic), the cleaning tools (okay, the basicness here might hinder), and the big one: a car. &ldquo;There are such distances now that you just <em>have to have a car.</em>&rdquo;</p>
<p>I keep pushing back with &ldquo;maybe, let&rsquo;s see in a few weeks.&rdquo;</p>
<p>I do this because I&rsquo;ve seen how easy it is to overbuy and overcommit to stuff when &ldquo;you&rsquo;re at it.&rdquo; It&rsquo;s also true that there&rsquo;s a benefit in doing it all at once, you&rsquo;ll do some things from inertia that you otherwise wouldn&rsquo;t. So it&rsquo;s a thin edge to walk to make it &ldquo;<a href="https://en.wikipedia.org/wiki/Lagom">lagom</a>&rdquo; (just enough) and not under- or overdo. For example, at my first employer, we bought this fancy ERP system with all the bells and whistles, including this Cognos BI analytics suite that would be &ldquo;so useful.&rdquo; We paid for it all upfront instead of making it an option in our contract. We tried getting it setup, there were issues, and basically we never touched it. Made do with much simpler stuff that actually fit how we worked.</p>
<p>So I&rsquo;m trying to not repeat that mistake.</p>
<p>When programming I&rsquo;ve lived with the rule that you don&rsquo;t get to create an abstraction for reuse until you&rsquo;ve reimplemented it at least three times in the same codebase. Yes, you might see it coming kilometers away, so you can arrange things to make it simpler to extract, but don&rsquo;t overdo it. That friction of actually implementing multiple times and seeing it in context each time allows you to discover if there are differences in <em>why</em> it&rsquo;s being used. Is it literally exactly the same for the same reason, or does it just look the same but for different reasons? If it&rsquo;s for different reasons it&rsquo;s likely to have to diverge and will an abstraction then help or hinder?</p>
<p>So now as I&rsquo;ve moved I&rsquo;m deciding to live with the friction first, seeing what friction or pain is actually real versus what&rsquo;s just different. A bike or a car? Yeah, I feel that urge already: I can&rsquo;t just get around as easily as I could before. But I also haven&rsquo;t really tried to adjust properly yet.</p>
<p>I&rsquo;m writing this in Kristinehamn, which is about 1h15m away by bus, and that&rsquo;s not too bad. Would likely be 30m if I had a car. But, would I have <a href="https://bjorn.now/scrap/2025-09-26t133737/">found this café I&rsquo;m at</a> if I didn&rsquo;t have to take public transport here and get other transportation to my mom (she&rsquo;s picking me up after doing some errands 🙂). The constraint led to discovery.</p>
<p>I feel the want for some stuff already. I am trying to be particular about what I&rsquo;m getting, because a lot of the things I miss I&rsquo;ll get from Singapore eventually (the double-walled glass cups, the gooseneck kettle, my knives and sharpening kit, etc.). But I&rsquo;ll wait. (okay, I saw a nice pottery cup at the café and it fit so nicely in my hand, so I bought it, so I let the analytical side accept it brought some joy, while also slightly wincing since I don&rsquo;t have an income.)</p>
<p>If I still feel I really need that car in one to two months, then I&rsquo;ll try and find one. But I have to try and make it work first. Same with the workspace, I&rsquo;m currently on the balcony (which is getting colder by the day) with a small movable desk, and maybe it&rsquo;s better to just walk into the city centre and work at a café or library a couple of times a week instead of buying a better desk and chair until my other things show up. I get out, I get some exercise, and I have a chance of meeting people.</p>
<p>It&rsquo;s so easy to get caught up and buy too much stuff because &ldquo;it&rsquo;s good to have,&rdquo; but will you use it? Live the problem first. Feel the actual pain. Then decide if you need the solution.</p>
<p>The friction can be an annoyance or an encouragement to find solutions. Going for quick solutions or longterm ones is on me.</p>]]></content:encoded></item><item><link>https://bjorn.now/scrap/2025-09-26t133737/?utm_medium=feed</link><pubDate>Fri, 26 Sep 2025 13:37:38 +0200</pubDate><guid>https://bjorn.now/scrap/2025-09-26t133737/</guid><description> A rhubarb pie with whipped cream and a cup of coffee
today I found a shop that has specialty coffee Krukmakeri Hemjord (amongst other things) in Kristinehamn, and the owner has the same name as I do and is a proper coffee nerd.
He’s been telling me about the coffees as I was planning to sit here a couple of hours, and we ended up with Ethiopian coffees, and I am now trying an anaerobic Ethiopian. While I have tried this in the past I haven’t known what to expect of it properly, and now that I know the citrusy/acidic is the point of this it’s quite nice as I like sour/acidic. Before this cup I had a similar but washed, and it was very tame in comparison. Very nice, and coincidentally they’re even brewing with the Clever Dripper (which I got recently), so I have some practical ideas for how to repeat at home.</description><content:encoded><![CDATA[<figure><img src="/img/2025/09-coffee-with-rhubarb-pie.jpeg"
    alt="A rhubarb pie with whipped cream and a cup of coffee"><figcaption>
      <p>A rhubarb pie with whipped cream and a cup of coffee</p>
    </figcaption>
</figure>

<p>today I found a shop that has specialty coffee <a href="https://visitvarmland.com/kristinehamn/design-shopping/hantverk/krukmakeri-hemjord/">Krukmakeri Hemjord</a> (amongst other things) in Kristinehamn, and the owner has the same name as I do and is a proper coffee nerd.</p>
<p>He&rsquo;s been telling me about the coffees as I was planning to sit here a couple of hours, and we ended up with Ethiopian coffees, and I am now trying an anaerobic Ethiopian. While I have tried this in the past I haven&rsquo;t known what to expect of it properly, and now that I know the citrusy/acidic <em>is</em> the point of this it&rsquo;s quite nice as I like sour/acidic. Before this cup I had a similar but washed, and it was very tame in comparison. Very nice, and coincidentally they&rsquo;re even brewing with the Clever Dripper (which I got recently), so I have some practical ideas for how to repeat at home.</p>
]]></content:encoded></item><item><link>https://bjorn.now/scrap/2025-09-23t210115/?utm_medium=feed</link><pubDate>Tue, 23 Sep 2025 21:01:15 +0200</pubDate><guid>https://bjorn.now/scrap/2025-09-23t210115/</guid><description> The Emperor’s light shines upon my acquired reading material</description><content:encoded><![CDATA[<figure><img src="/img/2025/09-the-emperors-light.jpeg"
    alt="Hand holding two Horus Heresy novels, the golden titles are lit by reflected sunlight, with sunset visible through window and trees behind"><figcaption>
      <p>The Emperor&rsquo;s light shines upon my acquired reading material</p>
    </figcaption>
</figure>

]]></content:encoded></item><item><link>https://bjorn.now/scrap/2025-09-23t202248/?utm_medium=feed</link><pubDate>Tue, 23 Sep 2025 20:22:48 +0200</pubDate><guid>https://bjorn.now/scrap/2025-09-23t202248/</guid><description>I see that The Northern Boys are playing in London in December, so now I have scheduled a 5-day trip to London 🥳
Also, seems I have to apply to be allowed into the UK now 🤷
Update: okay, I got an email after 5m that I was approved.</description><content:encoded><![CDATA[<p>I see that <a href="https://www.youtube.com/watch?v=4VXKhJfRQ7k">The Northern Boys</a> are <a href="https://pit.live/events/the-northern-boys-2">playing in London</a> in December, so now I have scheduled a 5-day trip to London 🥳</p>
<p>Also, seems I have to apply to be allowed into the UK now 🤷</p>
<p>Update: okay, I got an email after 5m that I was approved.</p>
]]></content:encoded></item><item><link>https://bjorn.now/scrap/2025-09-23t135744/?utm_medium=feed</link><pubDate>Tue, 23 Sep 2025 13:57:44 +0200</pubDate><guid>https://bjorn.now/scrap/2025-09-23t135744/</guid><description>I have managed to do one important thing today: I got a library card! 🥳</description><content:encoded>&lt;p>I have managed to do one important thing today: I got a library card! 🥳&lt;/p>
</content:encoded></item><item><title>Back to a 21st century digital society …knows your number but not who you are</title><link>https://bjorn.now/crumb/2025-09-23-21st-century-digital-society/?utm_medium=feed</link><pubDate>Tue, 23 Sep 2025 13:42:38 +0200</pubDate><guid>https://bjorn.now/crumb/2025-09-23-21st-century-digital-society/</guid><description>I’m moving back to Sweden, and something that surprised me was that if you’ve been registered as living abroad, becoming a registered resident again (folkbokförd) takes 4-6 weeks of processing. Skatteverket (IRAS/population registry) can reject your application. I assume they will if they think you’re primarily moving back to mooch off the Swedish welfare state (I’m not, I promise 😅).
I’m allowed to stay in Sweden, but operating in society is hard, because most systems can’t find any information on me. Practically a ghost (I was gonna say stateless but that feels distasteful to try and put myself together with people who have real problems). And from official system’s POV I don’t currently live anywhere. I have already moved out of Singapore and my employment pass has been cancelled. I have an old address registered in Sweden (I officially lived at my mom’s place when I went to Singapore in 2012), but I have a new apartment I live in now.</description><content:encoded><![CDATA[<p>I&rsquo;m moving back to Sweden, and something that surprised me was that if you&rsquo;ve been registered as living abroad, becoming a registered resident again (folkbokförd) takes 4-6 weeks of processing. Skatteverket (IRAS/population registry) can reject your application. I assume they will if they think you&rsquo;re primarily moving back to mooch off the Swedish welfare state (I&rsquo;m not, I promise 😅).</p>
<p>I&rsquo;m allowed to stay in Sweden, but operating in society is hard, because most systems can&rsquo;t find any information on me. Practically a ghost (I was gonna say stateless but that feels distasteful to try and put myself together with people who have real problems). And from official system&rsquo;s POV I don&rsquo;t currently live anywhere. I have already moved out of Singapore and my employment pass has been cancelled. I have an old address registered in Sweden (I officially lived at my mom&rsquo;s place when I went to Singapore in 2012), but I have a new apartment I live in now.</p>
<p>In one sense, I&rsquo;m living my dream as someone aligned with piratpartiet (<a href="https://en.wikipedia.org/wiki/Pirate_Party_(Sweden)">the pirate party</a>): companies and governments don&rsquo;t know anything about me, and they can&rsquo;t look much up. But since society has decided this is the way, I&rsquo;m now living the downside: they don&rsquo;t think I exist and I need extra handling, which seems to be how everything works in Germany, so I&rsquo;m happy I don&rsquo;t live there, even if it seems good from data protection POV.</p>
<h2 id="sweden-is-very-digital">
  Sweden is very digital
  <a href="#sweden-is-very-digital" class="heading-anchor" aria-label="Direct link to Sweden is very digital"><span aria-hidden="true">#</span></a>
</h2>
<p>If you want to do anything that requires any company to know anything about you, then they will look you up using your &ldquo;personnummer&rdquo; (personal identity number), and if you&rsquo;re doing it with computers you&rsquo;ll be asked to sign in using <a href="https://en.wikipedia.org/wiki/BankID_(Sweden)">BankID</a> which has been around since the early 2000s. It&rsquo;s super convenient, because there&rsquo;s this one ubiquitous app you use to sign in anywhere where people care about who you are. For the Singapore crowd, it&rsquo;s SingPass but it&rsquo;s not only used by the government. Since I used to live here, I have a bank account (that I&rsquo;ve had since I was a teenager), and through it, BankID. But BankID seems to be used <em>a lot</em> more than I expected.</p>
<p>I tried to sign up for Willy:s, one of the big grocery chains, and I can&rsquo;t because &ldquo;unknown reason.&rdquo; SJ, the big train company, got an issue when I put in my personnummer (&ldquo;unknown error&rdquo;) and I had to send an email to create an account. I tried to rent a space to store some furniture and I came up blank (they accepted 6mon deposit up front instead). I expect I&rsquo;ll be paying a lot of stuff up front going forward.</p>
<p>And I just now tried to register with another bank to become a corporate customer so I can pay the &ldquo;aktiekapital&rdquo; (share/equity capital, which is minimum 25.000 sek (~SG$3.500)) which is a first hurdle for starting a company. I can call the bank and who knows if we can sort it out, but I&rsquo;m currently at the library because I wanted to get out of the apartment on my first day after leaving my old job and start the company 😁</p>
<h2 id="being-in-limbo">
  Being in limbo
  <a href="#being-in-limbo" class="heading-anchor" aria-label="Direct link to Being in limbo"><span aria-hidden="true">#</span></a>
</h2>
<p>I guess my situation now means that I either:</p>
<ol>
<li>See it as the sign of taking it chill for a while and wait with starting the company until I have the decision from Skatteverket (up to four more weeks, some friends who moved back a couple of years ago got it within two weeks, but who knows if I&rsquo;ll be that lucky)</li>
<li>Get out of the silent section and start making phone calls (urgh, I&rsquo;m a millennial so the idea of phone calls…)</li>
</ol>
<p>Which brings me back to why I&rsquo;m sitting at the library on the first day of breaking out on my own: I wanted to start the company as soon as possible, and make sure I have a minimal first product up, and some way of charging for it, so I have a thin vertical slice of all I need to run the company. Because then, when I come up with <a href="https://www.youtube.com/watch?v=0ZW-8sISTmE">a cunning plan</a>, I can focus on building and iterating and not weeks of getting it all setup. XP iteration zero style: get it all setup from the start so we can focus on iterating.</p>
<p>So I guess that means I embrace the limbo and instead focus on building product. I have two ideas, the first (smaller) is trying to make an &ldquo;AI assistant creator&rdquo; focused on cooking based on hearing how interested other people have gotten when I talk about my cooking bot (I eat my ingredients now instead of letting them rot from lack of inspiration, and I am more comfortable in the kitchen). I figure it might be something where you could, as an MVP, help people create the bot and set it up with their LLM of choice for a small fee, $1-2 and then they use it there with no extra monthly fees. Possibly, if there&rsquo;s demand, extend to a standalone service with a set number of new recipes/month or something where recipes are saved etc.</p>
<p>And then, for plan B, in case my application is rejected then I&rsquo;ll start the company in Estonia since I&rsquo;m an <a href="https://www.e-resident.gov.ee">e-Resident</a> there. The reason I didn&rsquo;t do that from the start is because I figured that if I am living in Sweden, then I should have a Swedish company, and let any taxes go to my country. 🙂</p>
]]></content:encoded></item><item><title>Recycling bins are for packaging, not the products</title><link>https://bjorn.now/til/2025-09-23-recycling-bins-are-for-packaging/?utm_medium=feed</link><pubDate>Tue, 23 Sep 2025 11:43:42 +0200</pubDate><guid>https://bjorn.now/til/2025-09-23-recycling-bins-are-for-packaging/</guid><description>Recycling bins are for packaging, not the products themselves. Wooden ice cream stick? That’s “other,” it’s part of the product, at least in Karlstad. The wrapper? That goes in the appropriate recycling bin based on material.
This seems to be based on EU law, which makes sense, so I’ll assume the gist of this is true in other EU countries too.</description><content:encoded><![CDATA[<p>Recycling bins are for packaging, not the products themselves. Wooden ice cream stick? That&rsquo;s &ldquo;other,&rdquo; it&rsquo;s part of the product, at least <a href="https://karlstadsenergi.se/atervinning/sortera-ditt-avfall-ratt">in Karlstad</a>. The wrapper? That goes in the appropriate recycling bin based on material.</p>
<p>This seems to be based on EU law, which makes sense, so I&rsquo;ll assume the gist of this is true in other EU countries too.</p>
<p>This has to do with &ldquo;<a href="https://karlstadsenergi.se/atervinning/sortera-ditt-avfall-ratt/sortera-en-forpackning-eller-foremal">producentansvar</a>&rdquo; (&ldquo;producer responsibility&rdquo;) which by law requires producers of products to be responsible for the recycling of the packaging, by joining a &ldquo;collection system that takes operative and financial responsibility for collecting and managing packaging material.&quot;<a href="https://www.riksdagen.se/sv/dokument-och-lagar/dokument/svensk-forfattningssamling/forordning-20181462-om-producentansvar-for_sfs-2018-1462/">1 § 3</a></p>
<p>This sounds like systems thinking in play: By making producers responsible for recycling, they&rsquo;re incentivized to create packaging that&rsquo;s both easy to identify/recycle (cheaper for them) and easy for consumers to sort correctly (to hit reuse targets).</p>
<p>I watched Chris Spargo&rsquo;s video on <a href="https://www.youtube.com/watch?v=fuwaOiTHl7U">Why recycling in England is so complicated?</a> yesterday, which explains how England is going through this shift to Extended Producer Responsibility (EPR) right now, and without seeing that video it wouldn&rsquo;t have clicked as easy when I was reading this now.</p>
<p>(The egg shells don&rsquo;t qualify as food waste while bones do, I don&rsquo;t know what to make of that, and apparently it&rsquo;s the opposite in my mom&rsquo;s municipality 🤷)</p>]]></content:encoded></item><item><link>https://bjorn.now/scrap/2025-09-22t161922/?utm_medium=feed</link><pubDate>Mon, 22 Sep 2025 16:19:22 +0200</pubDate><guid>https://bjorn.now/scrap/2025-09-22t161922/</guid><description> a cute squirrel from my morning walk</description><content:encoded><![CDATA[<figure><img src="/img/2025/09-squirrel.jpeg"
    alt="A squirrel gripping thin branches with both paws, looking directly at the camera against a blue sky. Its bushy tail hangs below for balance. Sunlit autumn leaves frame the scene."><figcaption>
      <p>a cute squirrel from my morning walk</p>
    </figcaption>
</figure>

]]></content:encoded></item><item><link>https://bjorn.now/scrap/2025-09-22t154625/?utm_medium=feed</link><pubDate>Mon, 22 Sep 2025 15:46:26 +0200</pubDate><guid>https://bjorn.now/scrap/2025-09-22t154625/</guid><description>I was playing this game to stack chairs on a car that moves in a circle with my niece, and while doing so, it somehow reminded me that we’re at the end of Q3, and it felt eerily familiar to some sights at work…</description><content:encoded><![CDATA[<p>I was playing this game to stack chairs on a car that moves in a circle with my niece, and while doing so, it somehow reminded me that we&rsquo;re at the end of Q3, and it felt <em>eerily familiar</em> to some sights at work…</p>
<figure><img src="/img/2025/09-chair-stacking.jpeg"
    alt="A chaotic pile of colorful plastic toy chairs (orange, green, blue, red) precariously balanced and interlocked at odd angles, casting shadows on a wooden floor.">
</figure>

]]></content:encoded></item><item><title>How to refresh Telegram link previews</title><link>https://bjorn.now/til/2025-09-17-refresh-telegram-link-previews/?utm_medium=feed</link><pubDate>Wed, 17 Sep 2025 12:02:24 +0200</pubDate><guid>https://bjorn.now/til/2025-09-17-refresh-telegram-link-previews/</guid><description>to refresh a link preview in Telegram open a chat with Webpage Bot and send the URL, for those cases where you fix a typo or update the image.</description><content:encoded>&lt;p>to refresh a link preview in Telegram open a chat with &lt;a href="https://telegram.me/webpagebot">Webpage Bot&lt;/a> and send the URL, for those cases where you fix a typo or update the image.&lt;/p>
</content:encoded></item><item><link>https://bjorn.now/scrap/2025-09-17t095017/?utm_medium=feed</link><pubDate>Wed, 17 Sep 2025 09:50:17 +0200</pubDate><guid>https://bjorn.now/scrap/2025-09-17t095017/</guid><description>attending the weekly operations review and the chat ended up talking about euphemisms as someone said that an incident is an “unscheduled learning opportunity” (I agree, and this mindset for the ops meeting is 💯):
rapid unscheduled disassembly
something went boom
unscheduled unauthorised backup
data leak</description><content:encoded><![CDATA[<p>attending the weekly operations review and the chat ended up talking about euphemisms as someone said that an incident is an &ldquo;unscheduled learning opportunity&rdquo; (I agree, and this mindset for the ops meeting is 💯):</p>
<blockquote>
<p>rapid unscheduled disassembly</p>
</blockquote>
<p>something went boom</p>
<blockquote>
<p>unscheduled unauthorised backup</p>
</blockquote>
<p>data leak</p>
]]></content:encoded></item><item><link>https://bjorn.now/scrap/2025-09-17t093207/?utm_medium=feed</link><pubDate>Wed, 17 Sep 2025 09:32:07 +0200</pubDate><guid>https://bjorn.now/scrap/2025-09-17t093207/</guid><description>and after realizing for the umpteenth time how annoying it is to spell out this domain name, which I do love, I decided to register bjorn.now to point people to my /now page and this domain. The vanity domain amuses me, and it’s practical 🙂
Now, what email should I setup for that domain? me@? ba@?</description><content:encoded><![CDATA[<p>and after realizing for the umpteenth time how annoying it is to spell out this domain name, which I do love, I decided to register <a href="http://bjorn.now">bjorn.now</a> to point people to my /now page and this domain. The vanity domain amuses me, and it&rsquo;s practical 🙂</p>
<p>Now, what email should I setup for that domain? me@? ba@?</p>
]]></content:encoded></item><item><link>https://bjorn.now/scrap/2025-09-16t142247/?utm_medium=feed</link><pubDate>Tue, 16 Sep 2025 14:22:47 +0200</pubDate><guid>https://bjorn.now/scrap/2025-09-16t142247/</guid><description>I told Peter that I’m going to have to install a new VPS because my existing one has been humming along since 2015 and is wildly out of date, and so are my Ansible roles. He suggested I do it with NixOS because then I don’t need Ansible and this other tooling. Starting to read about Nix again it got me thinking of the Monty Python philosophers: “Is your name not Nix then? No, it’s Michael. That’s gonna cause a little confusion. Mind if we call you Nix, just to keep it clear?”</description><content:encoded><![CDATA[<p>I told Peter that I&rsquo;m going to have to install a new VPS because my existing one has been humming along since 2015 and is wildly out of date, and so are my Ansible roles. He suggested I do it with <a href="http://nixos.org/">NixOS</a> because then I don&rsquo;t need Ansible and this other tooling. Starting to read about Nix again it got me thinking of the <a href="https://youtu.be/ZymUMAu_fB0">Monty Python philosophers</a>: &ldquo;Is your name not Nix then? No, it&rsquo;s Michael. That&rsquo;s gonna cause a little confusion. Mind if we call you Nix, just to keep it clear?&rdquo;</p>
<div style="position: relative; padding-bottom: 56.25%; height: 0; overflow: hidden;">
      <iframe allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share; fullscreen" loading="eager" referrerpolicy="strict-origin-when-cross-origin" src="https://www.youtube.com/embed/ZymUMAu_fB0?autoplay=0&amp;controls=1&amp;end=0&amp;loop=0&amp;mute=0&amp;start=0" style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; border:0;" title="YouTube video"></iframe>
    </div>

]]></content:encoded></item><item><link>https://bjorn.now/scrap/2025-09-15t131809/?utm_medium=feed</link><pubDate>Mon, 15 Sep 2025 13:18:10 +0200</pubDate><guid>https://bjorn.now/scrap/2025-09-15t131809/</guid><description>the joy of dealing with things international: DHL refuses to send my work laptop to Singapore using the standard mobile site because it’s too valuable, it might work on some other site, which doesn’t work on my phone. Schenker happily takes it but will deliver it when they want to, but only in Europe. Postnord can’t validate the postal code in Singapore (bug report sent) so I can’t send. Once more into the breach trying to suss out the other DHL site. 🤬</description><content:encoded><![CDATA[<p>the joy of dealing with things international: DHL refuses to send my work laptop to Singapore using the standard mobile site because it&rsquo;s too valuable, it might work on some other site, which doesn&rsquo;t work on my phone. Schenker happily takes it but will deliver it when they want to, but only in Europe. Postnord can&rsquo;t validate the postal code in Singapore (bug report sent) so I can&rsquo;t send. Once more into the breach trying to suss out the other DHL site. 🤬</p>
<p>update: whoo, on the computer I could get it sorted on the DHL site and it was 3x the price, so I&rsquo;ll see if I can get it sorted with Postnord instead because I can&rsquo;t justify the price difference.</p>
]]></content:encoded></item><item><link>https://bjorn.now/scrap/2025-09-14t134146/?utm_medium=feed</link><pubDate>Sun, 14 Sep 2025 13:51:50 +0200</pubDate><guid>https://bjorn.now/scrap/2025-09-14t134146/</guid><description>Now that I’m getting stuff for the new apartment I’m getting a lot of stuff from Biltema. It’s so cheap, and the quality is usually good enough that I don’t overly worry about it. It feels more like “the basics” and no frills but stuff that will do you.
In comparison, I rarely bought stuff in a store in Singapore, because it always felt so expensive and the quality felt way more questionable on the cheap stuff (mostly the tiny shops in Sim Lim or so), not sure if that was a fair assessment most of the time. And I generally felt waaay more uncomfortable with the quality for the cheap things there. As an example, I bought a C7 cable with a euro plug (standard power cable for things like DVD players, and in my case, a charger) from Lazada from some small local shop, very flimsy, low quality, and cost me something like $12 with shipping. I bought one of those at Clas Ohlson for 69 sek (~SG$9) and it’s much higher quality.</description><content:encoded><![CDATA[<p>Now that I&rsquo;m getting stuff for the new apartment I&rsquo;m getting a lot of stuff from Biltema. It&rsquo;s so cheap, and the quality is usually good enough that I don&rsquo;t overly worry about it. It feels more like &ldquo;the basics&rdquo; and no frills but stuff that will do you.</p>
<p>In comparison, I rarely bought stuff in a store in Singapore, because it always felt so expensive and the quality felt way more questionable on the cheap stuff (mostly the tiny shops in Sim Lim or so), not sure if that was a fair assessment most of the time. And I generally felt waaay more uncomfortable with the quality for the cheap things there. As an example, I bought a C7 cable with a euro plug (standard power cable for things like DVD players, and in my case, a charger) from Lazada from some small local shop, very flimsy, low quality, and cost me something like $12 with shipping. I bought one of those at Clas Ohlson for 69 sek (~SG$9) and it&rsquo;s much higher quality.</p>
<p>It reminds me of talking to a distributor when I was working at DollarStore, he told me that it&rsquo;s cheaper for them to sell us quality at a lower margin because then neither of us have to deal with returns and unhappy customers. Because a big batch of things that don&rsquo;t work means you&rsquo;ll have to scrap the whole shipment, and that&rsquo;s too costly when you buy container loads of things. I bet that&rsquo;s true for Biltema as well.</p>
]]></content:encoded></item><item><link>https://bjorn.now/scrap/2025-09-14t105315/?utm_medium=feed</link><pubDate>Sun, 14 Sep 2025 10:56:58 +0200</pubDate><guid>https://bjorn.now/scrap/2025-09-14t105315/</guid><description> first coffee on the new balcony after moving in to my furnished rental in Karlstad yesterday
…I feel the need to point out that the “You’re the best” coffee mug is the apartment’s.</description><content:encoded><![CDATA[<figure><img src="/img/2025/09-coffee-on-balcony.jpeg"
    alt="Black coffee mug with gold text reading &#39;DU ÄR BÄST!&#39; held by a hand, with a blurred view through a window showing green trees and buildings in the background"><figcaption>
      <p>first coffee on the new balcony after moving in to my furnished rental in Karlstad yesterday</p>
    </figcaption>
</figure>

<p>…I feel the need to point out that the &ldquo;You&rsquo;re the best&rdquo; coffee mug is the apartment&rsquo;s.</p>
]]></content:encoded></item><item><link>https://bjorn.now/scrap/2025-09-12t225659/?utm_medium=feed</link><pubDate>Fri, 12 Sep 2025 23:06:13 +0200</pubDate><guid>https://bjorn.now/scrap/2025-09-12t225659/</guid><description>I told a friend ‘I have historically had an apparent predilection for chicken videos 😅’ and I feel the need to elaborate…
It’s all a big misunderstanding. The kind like when someone gets, say, a frog from someone and keep it on their desk. Then people think they like frogs and they get more. Suddenly, all gifts are frogs. You’re the frog person. I’m the chicken video guy.
It all started innocently with Chicken Techno:</description><content:encoded><![CDATA[<p>I told a friend &lsquo;I have historically had an apparent predilection for chicken videos 😅&rsquo; and I feel the need to elaborate…</p>
<p>It&rsquo;s all a big misunderstanding. The kind like when someone gets, say, a frog from someone and keep it on their desk. Then people think they like frogs and they get more. Suddenly, all gifts are frogs. You&rsquo;re the frog person. I&rsquo;m the chicken video guy.</p>
<p>It all started innocently with Chicken Techno:</p>
<div style="position: relative; padding-bottom: 56.25%; height: 0; overflow: hidden;">
      <iframe allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share; fullscreen" loading="eager" referrerpolicy="strict-origin-when-cross-origin" src="https://www.youtube.com/embed/8410qUT4QtA?autoplay=0&amp;controls=1&amp;end=0&amp;loop=0&amp;mute=0&amp;start=0" style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; border:0;" title="YouTube video"></iframe>
    </div>

<p>through the algorithm I then learned of New Bibi Hendl (a Japanese man yodeling and singing in Swiss german about a chicken):</p>
<div style="position: relative; padding-bottom: 56.25%; height: 0; overflow: hidden;">
      <iframe allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share; fullscreen" loading="eager" referrerpolicy="strict-origin-when-cross-origin" src="https://www.youtube.com/embed/yO7MWuJ7zLA?autoplay=0&amp;controls=1&amp;end=0&amp;loop=0&amp;mute=0&amp;start=0" style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; border:0;" title="YouTube video"></iframe>
    </div>

<p>Then as luck would have it, Takeo Ischi had another banger out: Chicken Attack</p>
<div style="position: relative; padding-bottom: 56.25%; height: 0; overflow: hidden;">
      <iframe allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share; fullscreen" loading="eager" referrerpolicy="strict-origin-when-cross-origin" src="https://www.youtube.com/embed/miomuSGoPzI?autoplay=0&amp;controls=1&amp;end=0&amp;loop=0&amp;mute=0&amp;start=0" style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; border:0;" title="YouTube video"></iframe>
    </div>

<p>At this point we&rsquo;d created the &ldquo;<a href="https://www.youtube.com/playlist?list=PLv0iNv6Fc-WtpkTFsBJwrgxsFjz34i2RW">Friday Playlist</a>&rdquo; where everyone from the office were putting in weird songs, that&rsquo;d we&rsquo;d play the last couple of hours on Friday. Then until tonight, this was the last song, which apparently is just a bunch of onomatopoeia in Chinese: Wang Rong Rollin - Chick Chick</p>
<div style="position: relative; padding-bottom: 56.25%; height: 0; overflow: hidden;">
      <iframe allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share; fullscreen" loading="eager" referrerpolicy="strict-origin-when-cross-origin" src="https://www.youtube.com/embed/mxzgwJ8tSE0?autoplay=0&amp;controls=1&amp;end=0&amp;loop=0&amp;mute=0&amp;start=0" style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; border:0;" title="YouTube video"></iframe>
    </div>

<p>And that was until tonight when the algorithm sent me a weird cover of &ldquo;I will survive&rdquo; from the point of view of a chick:</p>
<blockquote class="instagram-media" data-instgrm-permalink="https://www.instagram.com/reel/DOCdbmsDLzX/?utm_source=ig_embed&amp;utm_campaign=loading" data-instgrm-version="14" style=" background:#FFF; border:0; border-radius:3px; box-shadow:0 0 1px 0 rgba(0,0,0,0.5),0 1px 10px 0 rgba(0,0,0,0.15); margin: 1px; max-width:540px; min-width:326px; padding:0; width:99.375%; width:-webkit-calc(100% - 2px); width:calc(100% - 2px);"><div style="padding:16px;"> <a href="https://www.instagram.com/reel/DOCdbmsDLzX/?utm_source=ig_embed&amp;utm_campaign=loading" style=" background:#FFFFFF; line-height:0; padding:0 0; text-align:center; text-decoration:none; width:100%;" target="_blank"> <div style=" display: flex; flex-direction: row; align-items: center;"> <div style="background-color: #F4F4F4; border-radius: 50%; flex-grow: 0; height: 40px; margin-right: 14px; width: 40px;"></div> <div style="display: flex; flex-direction: column; flex-grow: 1; justify-content: center;"> <div style=" background-color: #F4F4F4; border-radius: 4px; flex-grow: 0; height: 14px; margin-bottom: 6px; width: 100px;"></div> <div style=" background-color: #F4F4F4; border-radius: 4px; flex-grow: 0; height: 14px; width: 60px;"></div></div></div><div style="padding: 19% 0;"></div> <div style="display:block; height:50px; margin:0 auto 12px; width:50px;"><svg width="50px" height="50px" viewBox="0 0 60 60" version="1.1" xmlns="https://www.w3.org/2000/svg" xmlns:xlink="https://www.w3.org/1999/xlink"><g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"><g transform="translate(-511.000000, -20.000000)" fill="#000000"><g><path d="M556.869,30.41 C554.814,30.41 553.148,32.076 553.148,34.131 C553.148,36.186 554.814,37.852 556.869,37.852 C558.924,37.852 560.59,36.186 560.59,34.131 C560.59,32.076 558.924,30.41 556.869,30.41 M541,60.657 C535.114,60.657 530.342,55.887 530.342,50 C530.342,44.114 535.114,39.342 541,39.342 C546.887,39.342 551.658,44.114 551.658,50 C551.658,55.887 546.887,60.657 541,60.657 M541,33.886 C532.1,33.886 524.886,41.1 524.886,50 C524.886,58.899 532.1,66.113 541,66.113 C549.9,66.113 557.115,58.899 557.115,50 C557.115,41.1 549.9,33.886 541,33.886 M565.378,62.101 C565.244,65.022 564.756,66.606 564.346,67.663 C563.803,69.06 563.154,70.057 562.106,71.106 C561.058,72.155 560.06,72.803 558.662,73.347 C557.607,73.757 556.021,74.244 553.102,74.378 C549.944,74.521 548.997,74.552 541,74.552 C533.003,74.552 532.056,74.521 528.898,74.378 C525.979,74.244 524.393,73.757 523.338,73.347 C521.94,72.803 520.942,72.155 519.894,71.106 C518.846,70.057 518.197,69.06 517.654,67.663 C517.244,66.606 516.755,65.022 516.623,62.101 C516.479,58.943 516.448,57.996 516.448,50 C516.448,42.003 516.479,41.056 516.623,37.899 C516.755,34.978 517.244,33.391 517.654,32.338 C518.197,30.938 518.846,29.942 519.894,28.894 C520.942,27.846 521.94,27.196 523.338,26.654 C524.393,26.244 525.979,25.756 528.898,25.623 C532.057,25.479 533.004,25.448 541,25.448 C548.997,25.448 549.943,25.479 553.102,25.623 C556.021,25.756 557.607,26.244 558.662,26.654 C560.06,27.196 561.058,27.846 562.106,28.894 C563.154,29.942 563.803,30.938 564.346,32.338 C564.756,33.391 565.244,34.978 565.378,37.899 C565.522,41.056 565.552,42.003 565.552,50 C565.552,57.996 565.522,58.943 565.378,62.101 M570.82,37.631 C570.674,34.438 570.167,32.258 569.425,30.349 C568.659,28.377 567.633,26.702 565.965,25.035 C564.297,23.368 562.623,22.342 560.652,21.575 C558.743,20.834 556.562,20.326 553.369,20.18 C550.169,20.033 549.148,20 541,20 C532.853,20 531.831,20.033 528.631,20.18 C525.438,20.326 523.257,20.834 521.349,21.575 C519.376,22.342 517.703,23.368 516.035,25.035 C514.368,26.702 513.342,28.377 512.574,30.349 C511.834,32.258 511.326,34.438 511.181,37.631 C511.035,40.831 511,41.851 511,50 C511,58.147 511.035,59.17 511.181,62.369 C511.326,65.562 511.834,67.743 512.574,69.651 C513.342,71.625 514.368,73.296 516.035,74.965 C517.703,76.634 519.376,77.658 521.349,78.425 C523.257,79.167 525.438,79.673 528.631,79.82 C531.831,79.965 532.853,80.001 541,80.001 C549.148,80.001 550.169,79.965 553.369,79.82 C556.562,79.673 558.743,79.167 560.652,78.425 C562.623,77.658 564.297,76.634 565.965,74.965 C567.633,73.296 568.659,71.625 569.425,69.651 C570.167,67.743 570.674,65.562 570.82,62.369 C570.966,59.17 571,58.147 571,50 C571,41.851 570.966,40.831 570.82,37.631"></path></g></g></g></svg></div><div style="padding-top: 8px;"> <div style=" color:#3897f0; font-family:Arial,sans-serif; font-size:14px; font-style:normal; font-weight:550; line-height:18px;">View this post on Instagram</div></div><div style="padding: 12.5% 0;"></div> <div style="display: flex; flex-direction: row; margin-bottom: 14px; align-items: center;"><div> <div style="background-color: #F4F4F4; border-radius: 50%; height: 12.5px; width: 12.5px; transform: translateX(0px) translateY(7px);"></div> <div style="background-color: #F4F4F4; height: 12.5px; transform: rotate(-45deg) translateX(3px) translateY(1px); width: 12.5px; flex-grow: 0; margin-right: 14px; margin-left: 2px;"></div> <div style="background-color: #F4F4F4; border-radius: 50%; height: 12.5px; width: 12.5px; transform: translateX(9px) translateY(-18px);"></div></div><div style="margin-left: 8px;"> <div style=" background-color: #F4F4F4; border-radius: 50%; flex-grow: 0; height: 20px; width: 20px;"></div> <div style=" width: 0; height: 0; border-top: 2px solid transparent; border-left: 6px solid #f4f4f4; border-bottom: 2px solid transparent; transform: translateX(16px) translateY(-4px) rotate(30deg)"></div></div><div style="margin-left: auto;"> <div style=" width: 0px; border-top: 8px solid #F4F4F4; border-right: 8px solid transparent; transform: translateY(16px);"></div> <div style=" background-color: #F4F4F4; flex-grow: 0; height: 12px; width: 16px; transform: translateY(-4px);"></div> <div style=" width: 0; height: 0; border-top: 8px solid #F4F4F4; border-left: 8px solid transparent; transform: translateY(-4px) translateX(8px);"></div></div></div> <div style="display: flex; flex-direction: column; flex-grow: 1; justify-content: center; margin-bottom: 24px;"> <div style=" background-color: #F4F4F4; border-radius: 4px; flex-grow: 0; height: 14px; margin-bottom: 6px; width: 224px;"></div> <div style=" background-color: #F4F4F4; border-radius: 4px; flex-grow: 0; height: 14px; width: 144px;"></div></div></a><p style=" color:#c9c8cd; font-family:Arial,sans-serif; font-size:14px; line-height:17px; margin-bottom:0; margin-top:8px; overflow:hidden; padding:8px 0 7px; text-align:center; text-overflow:ellipsis; white-space:nowrap;"><a href="https://www.instagram.com/reel/DOCdbmsDLzX/?utm_source=ig_embed&amp;utm_campaign=loading" style=" color:#c9c8cd; font-family:Arial,sans-serif; font-size:14px; font-style:normal; font-weight:normal; line-height:17px; text-decoration:none;" target="_blank">A post shared by Chicken Happy Hour (@chickenhappyhour)</a></p></div></blockquote>
<script async src="//www.instagram.com/embed.js"></script>
<p>Full circle. A snake eating an egg.</p>
]]></content:encoded></item><item><link>https://bjorn.now/scrap/2025-09-10t212840/?utm_medium=feed</link><pubDate>Wed, 10 Sep 2025 21:28:41 +0200</pubDate><guid>https://bjorn.now/scrap/2025-09-10t212840/</guid><description>after the co-op (BRF) board met it has been deemed okie-dokie to lease an apartment to me and I have a place to live in Sweden 🥳</description><content:encoded>&lt;p>after the co-op (BRF) board met it has been deemed okie-dokie to lease an apartment to me and I have a place to live in Sweden 🥳&lt;/p>
</content:encoded></item><item><link>https://bjorn.now/scrap/2025-09-09t183213/?utm_medium=feed</link><pubDate>Tue, 09 Sep 2025 18:32:13 +0200</pubDate><guid>https://bjorn.now/scrap/2025-09-09t183213/</guid><description>something I have missed living in Singapore is going to concerts and I just saw that Electric Callboy is going to Stockholm on the 5th of November, and now, so am I 🤘😁
(gearing up to be a 5th of November to never ever be forgot)</description><content:encoded><![CDATA[<p>something I have missed living in Singapore is going to concerts and I just saw that <a href="https://www.electriccallboy.com/">Electric Callboy</a> is going to Stockholm on the 5th of November, and now, so am I 🤘😁<br>
(gearing up to be a 5th of November to never ever be forgot)</p>
]]></content:encoded></item><item><link>https://bjorn.now/scrap/2025-09-07t173428/?utm_medium=feed</link><pubDate>Sun, 07 Sep 2025 17:34:29 +0200</pubDate><guid>https://bjorn.now/scrap/2025-09-07t173428/</guid><description>Hasse and I went for a walk into Karlstad city center yesterday and spotted this vending machine right by Klaraälven and near the opera.
Kayakomat, kayak vending</description><content:encoded><![CDATA[<p>Hasse and I went for a walk into Karlstad city center yesterday and spotted this vending machine right by <a href="https://en.wikipedia.org/wiki/Klar%C3%A4lven">Klaraälven</a> and near the opera.</p>
<figure><img src="/img/2025/09-kayakomat.jpeg"
    alt="A green wooden shed labeled &#39;KAYAKOMAT&#39; with numbered red, yellow, and green kayaks stored horizontally on racks, functioning as a self-service kayak rental station"><figcaption>
      <p>Kayakomat, kayak vending</p>
    </figcaption>
</figure>

]]></content:encoded></item><item><link>https://bjorn.now/scrap/2025-09-07t170241/?utm_medium=feed</link><pubDate>Sun, 07 Sep 2025 17:02:41 +0200</pubDate><guid>https://bjorn.now/scrap/2025-09-07t170241/</guid><description> I found some super shrooms when out on a walk, time to go big like Super Mario!
…yeah, don’t eat these flugsvamp if you find them in Sweden, they’re poisonous. Even if supposedly viking berserkers would eat them to help ’em… berserk, so if feed up enough about something maybe it’s worth it…</description><content:encoded><![CDATA[<figure><img src="/img/2025/09-go-big-mushroom.jpeg"
    alt="Three bright red mushrooms with white spots growing among grass and ferns in a Swedish forest, resembling the power-up mushrooms from Super Mario games"><figcaption>
      <p>I found some super shrooms when out on a walk, time to go big like Super Mario!</p>
    </figcaption>
</figure>

<p>…yeah, don&rsquo;t eat these <a href="https://en.wikipedia.org/wiki/Amanita">flugsvamp</a> if you find them in Sweden, they&rsquo;re poisonous. Even if supposedly viking berserkers would eat them to help &rsquo;em… berserk, so if feed up enough about something maybe it&rsquo;s worth it…</p>
]]></content:encoded></item><item><title>Arvid Nordquist mellanrost coffee recipe …making do without the gooseneck</title><link>https://bjorn.now/blog/2025/09/07/arvid-nordquist-mellanrost-coffee-recipe/?utm_medium=feed</link><pubDate>Sun, 07 Sep 2025 11:13:14 +0200</pubDate><guid>https://bjorn.now/blog/2025/09/07/arvid-nordquist-mellanrost-coffee-recipe/</guid><description>My recipe for Arvid Nordquist mellanrost. I started by trying to replicate The Ultimate V60 technique by James Hoffmann (which is what I normally brew) and I realized just how hard it is to control the flow rate now that I don’t have my gooseneck kettle (being shipped from Singapore), and I hadn’t really realized just what a difference controlling the pour makes for the taste!
My current brewing setup which has my Commandante grinder and V60 and then a normal wide spout kettle which gives me very poor flow control, and a bag of Arvid Nordquist mellanrost</description><content:encoded><![CDATA[<p>My recipe for Arvid Nordquist mellanrost. I started by trying to replicate <a href="https://www.youtube.com/watch?v=AI4ynXzkSQo">The Ultimate V60 technique by James Hoffmann</a> (which is what I normally brew) and I realized just how hard it is to control the flow rate now that I don&rsquo;t have my <a href="https://homecoffeeexpert.com/why-use-a-gooseneck-kettle/">gooseneck kettle</a> (being shipped from Singapore), and I hadn&rsquo;t really realized just what a difference controlling the pour makes for the taste!</p>
<figure><img src="/img/2025/09-arvid-nordquist.jpeg"
    alt="Coffee brewing setup on a wooden surface showing a Commandante hand grinder with wooden knob, a white V60 dripper with filter paper on a mug, a bag of Arvid Nordquist Mellanrost coffee, a digital scale reading 253g, and a stainless steel electric kettle."><figcaption>
      <p>My current brewing setup which has my <a href="https://comandantegrinder.com">Commandante grinder</a> and <a href="https://www.hario-europe.com/collections/v60-dripper">V60</a> and then a normal wide spout kettle which gives me very poor flow control, and a bag of Arvid Nordquist mellanrost</p>
    </figcaption>
</figure>

<p>I followed the usual 15g of beans to 250g of water per James&rsquo; setup as I&rsquo;m brewing a single cup and then I:</p>
<ol>
<li>Put the kettle on, and while the water is getting to a boil</li>
<li>Grind the coffee at <strong>31 clicks</strong> on the Commandante</li>
<li>Pour in about 60g of water to bloom (I couldn&rsquo;t get the grounds saturated enough without this much water)</li>
<li>Let it bloom for 30 seconds</li>
<li>Pour in up to 150g of water as slowly as you can, practically this has been 10-15s</li>
<li>Rest for 15s</li>
<li>Pour in up to 250g of water as slowly as possible, again, practically 10-15s</li>
<li>Swirl it around to get a smooth coffee bed and let it draw down</li>
</ol>
<p>Doing this has given me a decent cup, but nothing to write home about (heh, despite this post), as it cools I get some nice flavors but it&rsquo;s a bit watery, but any finer and it gets too bitter.</p>
<p>And despite putting in the effort, experimenting, and trying to get it to work… when I visited my brother he took a couple of coffee scoops of Löfbergs Lila pre-ground coffee, put it in a drip brewer, turned it on, and waited. That damned coffee tasted better than this.</p>
<p>Now to hold off buying much more coffee gear before my stuff arrives from Singapore. I already ordered a Clever Dripper together with some light roasted beans, because I figured some immersion brew might do better if I don&rsquo;t have the gooseneck.</p>
<p>Also, I wasn&rsquo;t experimenting with this on my own, and I knew I was done because I constructed myself some help…</p>
<h2 id="introducing-hoffmann-bot">
  Introducing &ldquo;Hoffmann bot&rdquo;
  <a href="#introducing-hoffmann-bot" class="heading-anchor" aria-label="Direct link to Introducing &ldquo;Hoffmann bot&rdquo;"><span aria-hidden="true">#</span></a>
</h2>
<p>I have been using a genie to work on my coffee and it has genuinely given me some of my best coffee experiences at home since I made this prompt, which I have dubbed &ldquo;Hoffmann bot&rdquo; because I wanted to imagine I have James&rsquo; looming over my shoulders as I&rsquo;m trying to make it work. The big thing has been that I guess at what I should do and then the bot corrects me, my intuition has been <em>very wrong</em> despite watching a lot of videos. I clearly haven&rsquo;t internalized them enough. And it was clear in that my coffee was acceptable but never truly enjoyable at home.</p>
<p>I usually start my sessions by taking a photo of the bag I&rsquo;m working with. Then I explain what gear I&rsquo;m using (the 2 cup V60 for example) and then I guess at what I should try at.</p>
<div class="full-width copy-enabled">
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">You are James Hoffmann helping troubleshoot V60 brewing issues. Channel his precise-yet-whimsical communication style.
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">HOFFMANN VOICE:
</span></span><span class="line"><span class="cl">- Start with gentle, slightly bemused observations (&#34;Now, this is interesting...&#34;)
</span></span><span class="line"><span class="cl">- Use his characteristic dry humor and occasional exasperation with coffee myths
</span></span><span class="line"><span class="cl">- Include thoughtful pauses and qualifications (&#34;I think what&#39;s happening here is...&#34;)
</span></span><span class="line"><span class="cl">- Reference specific techniques from his videos when relevant
</span></span><span class="line"><span class="cl">- Maintain his balance of being authoritative yet approachable
</span></span><span class="line"><span class="cl">- Use British spellings and expressions naturally
</span></span><span class="line"><span class="cl">- Show genuine curiosity about what went wrong
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">APPROACH:
</span></span><span class="line"><span class="cl">- Begin with empathetic diagnosis of their described problem
</span></span><span class="line"><span class="cl">- Suggest grind adjustments in Commandante clicks (reference: 25-30 clicks for V60)
</span></span><span class="line"><span class="cl">- Explain the &#34;why&#34; in 1-2 sentences unless asked for more detail
</span></span><span class="line"><span class="cl">- Ask targeted follow-up questions with Hoffmann&#39;s methodical curiosity
</span></span><span class="line"><span class="cl">- Reference his Ultimate V60 technique principles when helpful
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">BREWING CONTEXT:
</span></span><span class="line"><span class="cl">- Primary: Two-cup V60 (occasionally one-cup)  
</span></span><span class="line"><span class="cl">- Grinder: Commandante
</span></span><span class="line"><span class="cl">- Beans: Light roasted filter coffees
</span></span><span class="line"><span class="cl">- User knows Ultimate V60 basics but needs debugging help
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">TYPICAL RESPONSES:
</span></span><span class="line"><span class="cl">- &#34;Right, so what you&#39;re describing sounds like...&#34;
</span></span><span class="line"><span class="cl">- &#34;This is quite common actually, and here&#39;s what I think is happening...&#34;
</span></span><span class="line"><span class="cl">- &#34;Now, before we get carried away adjusting everything...&#34;
</span></span><span class="line"><span class="cl">- &#34;The Commandante is lovely for this - try going [X] clicks...&#34;
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">DIAGNOSTIC QUESTIONS:
</span></span><span class="line"><span class="cl">- How did it taste? (sour/bitter/weak/astringent)
</span></span><span class="line"><span class="cl">- What was your drawdown time vs target?
</span></span><span class="line"><span class="cl">- Any signs of channeling or uneven extraction?
</span></span><span class="line"><span class="cl">- What&#39;s your current grind setting?</span></span></code></pre></div>
</div>
]]></content:encoded></item><item><title>Concise isn't enough, explain it …but meme sentences beats anything for quick alignment</title><link>https://bjorn.now/crumb/2025-09-07-concise-isnt-enough-explain-it/?utm_medium=feed</link><pubDate>Sun, 07 Sep 2025 10:10:14 +0200</pubDate><guid>https://bjorn.now/crumb/2025-09-07-concise-isnt-enough-explain-it/</guid><description>I came across Steph Ango’s Concise explanations accelerate progress:
If you want to progress faster, write concise explanations. Explain ideas in simple terms, strongly and clearly, so that they can be rebutted, remixed, reworked — or built upon.
Concise explanations spread faster because they are easier to read and understand. The sooner your idea is understood, the sooner others can build on it.
I struggle with conciseness. I tend to veer verbose and ramble through what I write and explain. Because I want to give the other person a chance to see what I was thinking as I was going through something.</description><content:encoded><![CDATA[<p>I came across Steph Ango&rsquo;s <a href="https://stephango.com/concise">Concise explanations accelerate progress</a>:</p>
<blockquote>
<p>If you want to progress faster, write concise explanations. Explain ideas in simple terms, strongly and clearly, so that they can be rebutted, remixed, reworked — or built upon.</p>
<p>Concise explanations spread faster because they are easier to read and understand. The sooner your idea is understood, the sooner others can build on it.</p>
</blockquote>
<p>I struggle with conciseness. I tend to veer verbose and ramble through what I write and explain. Because I want to give the other person a chance to see what I was thinking as I was going through something.</p>
<p>To try and combat that, I try to edit, so I have a concrete summary first per <a href="https://en.wikipedia.org/wiki/BLUF_(communication)">BLUF</a> and then expand on the explanation so the other person who reads it can drop off as soon as they feel they&rsquo;re done. I use Roam Research and I often wish that all tools had outlining ability like it, because then I can just add the details in a sub-bullet and fold it when I share.</p>
<p>My friend Sławek tries to condense everything into the shortest form possible, then struggles when people ask him to &ldquo;explain it more.&rdquo; Because he thought long and hard, this was his best explanation. I noticed that I would ask a lot of follow-up questions to get at what he was saying because I felt there was too much implicit in it.</p>
<p>I think there&rsquo;s great value in being concise and explaining things clearly while doing so, for all the reasons in Steph&rsquo;s post, but I wonder if it&rsquo;s only possible with a huge amount of shared context. The people you work with a lot. And then to have more explainers available that you can dig deeper into.</p>
<p>Relatedly, we used to have this engineering manifesto at work that was a sentence or two explaining what we value. This super concise document had two major problems:</p>
<ol>
<li>Less experienced engineers struggled to implement because they didn&rsquo;t know what it meant</li>
<li>Two senior engineers could read the same line and could come away with mutually exclusive interpretations</li>
</ol>
<p>The manifesto has now grown and while the concise sentences are still the headings, we now have explainers, examples, and more in folded sections on the manifesto site.</p>
<p>That said, I think the condensed idea is important as a mnemonic, which Steph also explores in <a href="https://stephango.com/evergreen-notes">Evergreen notes turn ideas into objects that you can manipulate</a>:</p>
<blockquote>
<p>Evergreen notes allow you to think about complex ideas by building them up from smaller composable ideas.</p>
<p>My evergreen notes have titles that distill each idea in a succinct and memorable way, that I can use in a sentence. For example:</p>
<ul>
<li>A company is a superorganism</li>
<li>All input is error
[…]</li>
</ul>
</blockquote>
<p>I&rsquo;ve been doing this as well in my note-taking and I love it (feels like we&rsquo;ve both been inspired by <a href="https://www.soenkeahrens.de/en/takesmartnotes">How To Take Smart Notes</a>). It&rsquo;s the &ldquo;cached version&rdquo; of a concept/idea, and it works great for me, but I also keep the details of how I ended up caching that idea inside the note, so if I have to re-evaluate my idea then I can.</p>
<p>Creating an evergreen/permanent note turns my <a href="/crumb/2025-08-14-dikw-pyramid-when-working-with-genies/">tacit knowledge into explicit knowledge</a> but it won&rsquo;t become information for everyone else at that level of detail. I think being able to dig into each point and give examples and explain them will make it easier. For example:</p>
<div class="no-copy-button">
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">▼ &#34;Always roll back a deploy by going to the pipeline and executing it again&#34;
</span></span><span class="line"><span class="cl">└─ Because the pipeline contains all other artifacts that went live at the same time as config and infrastructure.
</span></span><span class="line"><span class="cl">└─ If you only roll back the version you might not have the correct config and it can break.</span></span></code></pre></div>
</div>
<p>But, you want that super condensed version as the meme that you share. The &ldquo;saying you have&rdquo; in the company.</p>
<p>For example, how often haven&rsquo;t we heard &ldquo;disagree and commit,&rdquo; but how many agree on what that means? Have they dug into what <a href="https://www.amazon.jobs/content/en/our-workplace/leadership-principles#:R1msj6H1:">Amazon says it means</a>? Because it doesn&rsquo;t mean to just roll over when someone has made a call. <strong>The meme connected but not the intent behind it.</strong></p>
]]></content:encoded></item><item><title>Create space for serendipity</title><link>https://bjorn.now/crumb/2025-09-07-create-space-for-serendipity/?utm_medium=feed</link><pubDate>Sun, 07 Sep 2025 09:16:27 +0200</pubDate><guid>https://bjorn.now/crumb/2025-09-07-create-space-for-serendipity/</guid><description>Steph Ango says If you’re remote, ramble:
A tip for remote teams of 2-10 people. Create a personal “ramblings” channel for each teammate in your team’s chat app of choice.
Ramblings channels let everyone share what’s on their mind without cluttering group channels. Think of them as personal journals or microblogs inside your team’s chat app, a lightweight way to add ambient social cohesion.
I love this idea! I genuinely think the more we write and share the better, and I’ve noticed the effect of similar things that grew out of working remotely with people.</description><content:encoded><![CDATA[<p>Steph Ango says <a href="https://stephango.com/ramblings">If you&rsquo;re remote, ramble</a>:</p>
<blockquote>
<p>A tip for remote teams of 2-10 people. Create a personal “ramblings” channel for each teammate in your team’s chat app of choice.</p>
<p>Ramblings channels let everyone share what’s on their mind without cluttering group channels. Think of them as personal journals or microblogs inside your team’s chat app, a lightweight way to add ambient social cohesion.</p>
</blockquote>
<p>I love this idea! I genuinely think the more we write and share the better, and I&rsquo;ve noticed the effect of similar things that grew out of working remotely with people.</p>
<p>Creating social calls twice a week (or, biweekly 😉): we did an async standup where we shared our updates in writing (more depth, allowed the top-level to be short and then detail in threads), and we got good at doing them as a team that the standup call rarely had anything to add. We kept it so we&rsquo;d call in to answer questions, and we&rsquo;d still go around the group reading out loud what we&rsquo;d written, but it got repetitive from what we had just read and it felt pointless. It was rare that we had anything to say, so some (me included tbh) wanted to remove the call. We had some strong pushback on removing it, and as we dug into it, we realized that the want for a call really was about social connection, but they didn&rsquo;t really get something social by how the call was structured. So we explicitly said that we&rsquo;d be social after we had discussed anything standup related. The standup was short and snappy, and after we got to have a laugh and get to know our remote colleagues better. Smallish team, five people in total.</p>
<p>&ldquo;Hallway conversations&rdquo; to mimic bumping into a colleague you don&rsquo;t see so often: in the principal chapter (where the most senior ICs come together) we mostly work independently or with a small group of other principals. So to try and mimic bumping into someone you don&rsquo;t see very often and be intentional to get to know them better, we&rsquo;d start each chapter meeting with a 10min breakout room of up to four people and ask them all to answer &ldquo;What has surprised you recently?&rdquo; Because things that has surprised one principal would likely surprise another, and may even turn out someone else knows something to save them time. We had a lot of follow-up conversations in our channel after we did that and I feel like we managed to make some good connections thanks to them.</p>
<p>The hallway conversations came out of reading <a href="https://medium.com/hackernoon/excerpted-from-deep-work-by-cal-newport-f58adfb24fd0">this excerpt from Deep work by Cal Newport</a>, because we wanted to find ways to connect the principals:</p>
<blockquote>
<p>“Traveling the hall’s length without encountering a number of acquaintances, problems, diversions and ideas was almost impossible. A physicist on his way to lunch in the cafeteria was like a magnet rolling past iron filings.”
[…]
Expose yourself to ideas in hubs on a regular basis, but maintain a spoke in which to work deeply on what you encounter.</p>
</blockquote>
<p>And just to connect it, some other thoughts/suggestions on remote work in <a href="https://bjorn.now/crumb/2025-08-24-reasonable-assumptions/">Reasonable assumptions …and why they&rsquo;re (nearly) impossible</a>.</p>
<p>The rambling channel also makes me think about about the <a href="https://www.oldschoolburke.com/embracing-slacklog/">slacklog that Old School Burke</a> recommends, but they&rsquo;re often a bit more about what you did, creating a trail of breadcrumbs, and for the purpose of helping create the self-reviews required in large companies.</p>
]]></content:encoded></item><item><link>https://bjorn.now/scrap/2025-09-05t182450/?utm_medium=feed</link><pubDate>Fri, 05 Sep 2025 18:24:50 +0200</pubDate><guid>https://bjorn.now/scrap/2025-09-05t182450/</guid><description> the kebabpizza special, basically a dönerteller on a pizza, what’s not to love?
Famous rather than infamous. Aaaand now it’s time to have a nap…</description><content:encoded><![CDATA[<figure><img src="/img/2025/09-kebabpizza-special.jpeg"
    alt="A pizza in a delivery box topped with french fries, lettuce, tomatoes, onions, and kebab meat with white garlic sauce drizzled on top. A bottle of Trocadero soda sits beside it."><figcaption>
      <p>the kebabpizza special, basically a <a href="https://en.wiktionary.org/wiki/D%C3%B6nerteller">dönerteller</a> on a pizza, what&rsquo;s not to love?</p>
    </figcaption>
</figure>

<p><a href="https://studyinsweden.se/blogs/2024/03/17/the-swedish-kebab-pizza-and-why-its-so-important/">Famous</a> rather than <a href="/scrap/2025-09-03t132628/">infamous</a>. Aaaand now it&rsquo;s time to have a nap…</p>
]]></content:encoded></item><item><link>https://bjorn.now/scrap/2025-09-04t113146/?utm_medium=feed</link><pubDate>Thu, 04 Sep 2025 11:33:12 +0200</pubDate><guid>https://bjorn.now/scrap/2025-09-04t113146/</guid><description>it’s so quiet staying in ruralish Sweden that I hear the hum of the fridge/freezer more than anything, pretty great when you’re using to hearing traffic all the time 😀</description><content:encoded><![CDATA[<p>it&rsquo;s so quiet staying in ruralish Sweden that I hear the hum of the fridge/freezer more than anything, pretty great when you&rsquo;re using to hearing traffic all the time 😀</p>
]]></content:encoded></item><item><link>https://bjorn.now/scrap/2025-09-03t211458/?utm_medium=feed</link><pubDate>Wed, 03 Sep 2025 21:14:58 +0200</pubDate><guid>https://bjorn.now/scrap/2025-09-03t211458/</guid><description>I shared my swedish pizza lunch, and while I know people have strong feelings about pineapple on pizza, I’m genuinely surprised at how strong the reaction is to the creative use of ingredients we have for pizza. Swedish pizza is some of the best around and that’s a hill I’ll die on!
(one person asked “is this throw-up on pizza?” 😅)</description><content:encoded><![CDATA[<p>I shared my <a href="/scrap/2025-09-03t132628/">swedish pizza</a> lunch, and while I know people have strong feelings about pineapple on pizza, I&rsquo;m genuinely surprised at how strong the reaction is to <a href="https://patiolist.com/what-is-a-swedish-pizza-uncovering-a-unique-culinary-fusion">the creative use of ingredients</a> we have for pizza. Swedish pizza is some of the best around and that&rsquo;s a hill I&rsquo;ll die on!</p>
<p>(one person asked &ldquo;is this throw-up on pizza?&rdquo; 😅)</p>
]]></content:encoded></item><item><link>https://bjorn.now/scrap/2025-09-03t203343/?utm_medium=feed</link><pubDate>Wed, 03 Sep 2025 20:33:43 +0200</pubDate><guid>https://bjorn.now/scrap/2025-09-03t203343/</guid><description>drove a car for the first time in a looong while today (years), and after I backed out of the driveway something felt off: I wasn’t 100% sure where I was supposed to be on the road.
I just came to Sweden yesterday, after five days in Bengaluru, India, where I sat in the front passenger seat (i.e. the Swedish driver’s seat). I was positioning the car like I had been sitting in India (fast thinking?).</description><content:encoded><![CDATA[<p>drove a car for the first time in a looong while today (years), and after I backed out of the driveway something felt off: I wasn&rsquo;t 100% sure <em>where</em> I was supposed to be on the road.</p>
<p>I just came to Sweden yesterday, after five days in Bengaluru, India, where I sat in the front passenger seat (i.e. the Swedish driver&rsquo;s seat). I was positioning the car like I had been sitting in India (fast thinking?).</p>
<p>So I decided to go back to the driveway, because I&rsquo;m not putting someone else at risk when I haven&rsquo;t driven for a while and I&rsquo;m getting a weird feeling.</p>
<p>As I went back I reflected that I <strong>know</strong> it&rsquo;s right-hand drive in Sweden (slow thinking?), but I had mostly experienced sitting in that seat as a passenger in left-hand driving countries (Singapore for more than a decade, and just days before, India). I was 95% sure it meant I put myself so I&rsquo;m closer to the middle of the road.</p>
<p>As I looked it up online: yes, I&rsquo;m supposed to be towards the middle of the road when driving. I didn&rsquo;t hit anything. And the sense of the car started coming back after a couple of kilometers. Just like riding a bike 😉</p>
]]></content:encoded></item><item><link>https://bjorn.now/scrap/2025-09-03t213309/?utm_medium=feed</link><pubDate>Wed, 03 Sep 2025 15:33:09 +0200</pubDate><guid>https://bjorn.now/scrap/2025-09-03t213309/</guid><description> random nice thing from visiting my mom: hanging the laundry outside to dry in the sun</description><content:encoded><![CDATA[<figure><img src="/img/2025/09-air-drying.jpeg"
    alt="Clothes drying on a rotary washing line in a garden on a sunny day. White t-shirt, yellow and green shirts, and dark items hanging against a bright blue sky with forest trees surrounding the yard"><figcaption>
      <p>random nice thing from visiting my mom: hanging the laundry outside to dry in the sun</p>
    </figcaption>
</figure>

]]></content:encoded></item><item><link>https://bjorn.now/scrap/2025-09-03t132628/?utm_medium=feed</link><pubDate>Wed, 03 Sep 2025 13:26:28 +0200</pubDate><guid>https://bjorn.now/scrap/2025-09-03t132628/</guid><description> in Sweden I can enjoy one of the best uses of curry: the banana, chicken, curry powder, and peanut pizza! Washed down with an apple/orange soda.
…I really think it would be slightly better with some pineapple, though 😉😇</description><content:encoded><![CDATA[<figure><img src="/img/2025/09-pizza-and-trocadero.jpeg"
    alt="Pizza in an open delivery box showing banana slices, chicken, curry powder, and peanut toppings. A bottle of orange-colored Trocadero soda sits beside it on a table with a sunny garden in the background"><figcaption>
      <p>in Sweden I can enjoy one of the best uses of curry: the banana, chicken, curry powder, and peanut pizza! Washed down with an apple/orange soda.</p>
    </figcaption>
</figure>

<p>…I really think it would be slightly better with some pineapple, though 😉😇</p>
]]></content:encoded></item><item><title>Does the word change the "quick fix?" — hack/jugaad vs kludge/bodge</title><link>https://bjorn.now/crumb/2025-08-28-does-the-word-change-the-quick-fix/?utm_medium=feed</link><pubDate>Thu, 28 Aug 2025 07:46:41 +0530</pubDate><guid>https://bjorn.now/crumb/2025-08-28-does-the-word-change-the-quick-fix/</guid><description> I’m in Bengaluru to attend a friend’s wedding, and it has been fascinating walking around and seeing “the creative ways” of cable management, and my perspective from Singapore and Sweden is definitely to question the safety and longevity.
Coils of cables, some without the protective coating, all just lying around. What looks like some fiber cables for good measure.</description><content:encoded><![CDATA[<hr>
<p>I&rsquo;m in Bengaluru to attend a friend&rsquo;s wedding, and it has been <em>fascinating</em> walking around and seeing &ldquo;the creative ways&rdquo; of cable management, and my perspective from Singapore and Sweden is definitely to question the safety and longevity.</p>
<figure><img src="/img/2025/bengaluru-electrical-cables.jpg"
    alt="Coils of cables, some without the protective coating, all just lying around. What looks like some fiber cables for good measure."><figcaption>
      <p>Coils of cables, some without the protective coating, all just lying around. What looks like some fiber cables for good measure.</p>
    </figcaption>
</figure>

<p>In the group chat for the wedding the groom said that a very helpful word to explain this is &ldquo;<a href="https://en.wikipedia.org/wiki/Jugaad">jugaad</a>&rdquo; which per Wikipedia:</p>
<blockquote>
<p>a concept of non-conventional, frugal innovation on the Indian subcontinent. It also includes innovative fixes or simple workarounds, solutions that bend the rules, or resources that can be used in such a way.</p>
</blockquote>
<p>Fair enough, make do with what you have, I love that, but I still felt… uncomfortable even if I can appreciate that it works. (as an aside, it&rsquo;s worth reading the whole Wikipedia article for the examples!)</p>
<p>I showed some of these photos to Mari who then pointed out that this is the <strong>physical manifestation of &lsquo;getting it done at any cost&rsquo;</strong> we&rsquo;ve seen in some projects at work. Which made me realize where some of my discomfort was coming from.</p>
<p>While reading the Wikipedia page I saw the related article for &ldquo;<a href="https://en.wikipedia.org/wiki/Kludge">kludge</a>&rdquo; which is defined as:</p>
<blockquote>
<p>a workaround or makeshift solution that is clumsy, inelegant, inefficient, difficult to extend, and hard to maintain. <strong>Its only benefit is that it rapidly solves an important problem using available resources.</strong></p>
</blockquote>
<p>Something clicked: for work, that&rsquo;s the definition I want to use (because the company is <a href="https://medium.com/@kentbeck_7670/fast-slow-in-3x-explore-expand-extract-6d4c94a7539">in extract</a> so making it well is what we need most of the time). Then, reading on, I saw in the section of <a href="https://en.wikipedia.org/wiki/Kludge#Computer_science">use in computer science</a>, that hack is often used interchangeably with kludge, but that:</p>
<blockquote>
<p>Hack can also be used with a positive connotation, for a quick solution to a frustrating problem.</p>
</blockquote>
<p>I have definitely used hack both ways. Beautiful for creative solutions. Horrible when I worry about the insurance premium. And &ldquo;a hack that works isn&rsquo;t a hack.&rdquo; Which I always read a bit as &lsquo;an unusual solution that works is good,&rsquo; but, that means you&rsquo;re muddying the waters. One person&rsquo;s hack is another person&rsquo;s hack. 🤦</p>
<p>And it makes me wonder how much connotation we&rsquo;re putting in the words, because some of these absolutely atrocious kludges we&rsquo;ve put in at work to hit a deadline were necessary, and some weren&rsquo;t. The deadline was a sadline and we could&rsquo;ve taken the time to do it correctly. But if you have internalized a version where delivering at any cost is the highest value, even if it future work will be harder… then that&rsquo;s the tradeoff we&rsquo;re making.</p>
<p>I wonder if it would make a difference to stop using the word &ldquo;hack,&rdquo; which has both a positive and negative connotation, and instead used &ldquo;kludge&rdquo; (or bodge for the brits) and were clear that the only redeeming quality was getting something out the door, and <em>now</em>, <strong>right now</strong>, we have to figure out how to make it maintainable. Before we move on and create the next kludge.</p>
]]></content:encoded></item><item><title>Which of your colleagues are screwed?</title><link>https://bjorn.now/link/2025-08-28-which-of-your-colleagues-are-screwed/?utm_medium=feed</link><pubDate>Thu, 28 Aug 2025 06:02:42 +0530</pubDate><guid>https://bjorn.now/link/2025-08-28-which-of-your-colleagues-are-screwed/</guid><description>Justin Searls asks which of your colleagues are screwed?
I’ve been writing about how AI is likely to affect white-collar (or no-collar or hoodie-wearing) computer programmers […] Think about a random colleague you don’t feel particularly strongly about as you read the following pithy and reductive bullet points. […] They’re going to be screwed if they exhibit:
Curiosity without skepticism Strategy without experiments Ability without understanding Productivity without urgency Creativity without taste Certainty without evidence I recently said that I think curiosity is what makes a good software engineer, and gave examples of when/how they should be curious, but never pointed out that it has to come with a balancing function.</description><content:encoded><![CDATA[<p>Justin Searls asks <a href="https://justin.searls.co/posts/which-of-your-colleagues-are-screwed/">which of your colleagues are screwed?</a></p>
<blockquote>
<p>I&rsquo;ve been writing about how AI is likely to affect white-collar (or no-collar or hoodie-wearing) computer programmers […]
Think about a random colleague you don&rsquo;t feel particularly strongly about as you read the following pithy and reductive bullet points. […]
They&rsquo;re going to be screwed if they exhibit:</p>
<ul>
<li>Curiosity without skepticism</li>
<li>Strategy without experiments</li>
<li>Ability without understanding</li>
<li>Productivity without urgency</li>
<li>Creativity without taste</li>
<li>Certainty without evidence</li>
</ul>
</blockquote>
<p>I recently said that I think <a href="/blog/2025/08/21/what-makes-a-good-software-engineer/">curiosity is what makes a good software engineer</a>, and gave examples of when/how they should be curious, but never pointed out that it has to come with a balancing function.</p>
<p>I can&rsquo;t think of anything done 100% full-throttle that isn&rsquo;t detrimental without something to balance it, but it&rsquo;s tacit and not something I often think about immediately. For example, if you live your life 100% in service it will be limiting, a bit of selfishness is required, see <a href="https://www.youtube.com/watch?v=HPEwD7WpI9g">Doug Forcett from The Good Place</a>.</p>
]]></content:encoded></item><item><title>Return of multi-tasking? by Kent Beck</title><link>https://bjorn.now/link/2025-08-26-return-of-multi-tasking/?utm_medium=feed</link><pubDate>Tue, 26 Aug 2025 18:53:02 +0800</pubDate><guid>https://bjorn.now/link/2025-08-26-return-of-multi-tasking/</guid><description> Kent Beck has a subscriber post called return of Multi-tasking? where he’s realizing he’s more often working on different things across a day now that he’s working with genies. Less time doing clackety-clack and more time thinking about next steps (or napping for epiphanies).
I’ve noticed the same pattern, this “productive multi-tasking” where you shift to something else when you’re stuck or “cogitating in the background.” Whether it’s waiting for feedback, getting help from the CI team (why won’t their plugin inject the AWS credentials it’s supposed to?), or just running out of brain juice and needing to let things percolate (which I want to remember Slow Productivity also recommends, especially for more nebulous work).</description><content:encoded><![CDATA[<hr>
<p>Kent Beck has a subscriber post called <a href="https://tidyfirst.substack.com/p/return-of-multi-tasking">return of Multi-tasking?</a> where he&rsquo;s realizing he&rsquo;s more often working on different things across a day now that he&rsquo;s working with genies. Less time doing clackety-clack and more time thinking about next steps (or napping for epiphanies).</p>
<p>I&rsquo;ve noticed the same pattern, this &ldquo;productive multi-tasking&rdquo; where you shift to something else when you&rsquo;re stuck or &ldquo;cogitating in the background.&rdquo; Whether it&rsquo;s waiting for feedback, getting help from the CI team (why won&rsquo;t their plugin inject the AWS credentials it&rsquo;s supposed to?), or just running out of brain juice and needing to let things percolate (which I want to remember <a href="https://calnewport.com/slow/">Slow Productivity</a> also recommends, especially for more nebulous work).</p>
]]></content:encoded></item><item><title>Life never gave us lemons, we invented them all by ourselves</title><link>https://bjorn.now/link/2025-08-26-life-never-gave-us-lemons/?utm_medium=feed</link><pubDate>Tue, 26 Aug 2025 18:01:11 +0800</pubDate><guid>https://bjorn.now/link/2025-08-26-life-never-gave-us-lemons/</guid><description> …because someone said in a meeting to use the lemons to make lemonade and this started shouting in my mind</description><content:encoded><![CDATA[<div style="position: relative; padding-bottom: 56.25%; height: 0; overflow: hidden;">
      <iframe allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share; fullscreen" loading="eager" referrerpolicy="strict-origin-when-cross-origin" src="https://www.youtube.com/embed/bfDHBcKqwfk?autoplay=0&amp;controls=1&amp;end=0&amp;loop=0&amp;mute=0&amp;start=0" style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; border:0;" title="YouTube video"></iframe>
    </div>

<p>…because someone said in a meeting to use the lemons to make lemonade and this started shouting in my mind</p>
]]></content:encoded></item><item><title>Reasonable assumptions …and why they're (nearly) impossible</title><link>https://bjorn.now/crumb/2025-08-24-reasonable-assumptions/?utm_medium=feed</link><pubDate>Sun, 24 Aug 2025 13:25:00 +0800</pubDate><guid>https://bjorn.now/crumb/2025-08-24-reasonable-assumptions/</guid><description>As I was writing up angry at the genie, about getting frustrated at a bot for not having my context, I realized I’d done the same thing to a human colleague just months earlier.
A new colleague has been speed-running five years of my accumulated context, and I was getting really annoyed at all their questions. Why weren’t they making any reasonable assumptions? They’re a human after all, with decades of experience. In hindsight, I think that’s exactly why they’re not assuming and instead clarifying.</description><content:encoded><![CDATA[<p>As I was writing up <a href="/crumb/2025-08-24-angry-at-the-genie/">angry at the genie</a>, about getting frustrated at a bot for not having my context, I realized I&rsquo;d done the same thing to a human colleague just months earlier.</p>
<p>A new colleague has been speed-running <em>five years of my accumulated context</em>, and I was getting <em>really annoyed</em> at all their questions. Why weren&rsquo;t they making any reasonable assumptions? They&rsquo;re a human after all, with decades of experience. In hindsight, I think that&rsquo;s exactly why they&rsquo;re not assuming and instead clarifying.</p>
<p>Because I&rsquo;ve also been annoyed when new people come in &ldquo;guns blazing&rdquo; with their great ideas without learning about our context first. (I did this myself after leaving Pivotal Labs, came into my next job pushing XP and TDD. It went over about as well as you&rsquo;d expect. Many of my pivoted friends seems to have had the same problem.)</p>
<p>So which is it? Make assumptions or don&rsquo;t? I want both and neither, apparently. Good going on consistency there.</p>
<p>I apologized for getting annoyed and letting it show. Told them I was catching myself getting annoyed and it&rsquo;s about me, not them, so please keep up the questions. Feel free to call me out if I&rsquo;m starting to sound annoyed because I need to do better. I have nearly five years of context, failed and successful projects, and of course they can&rsquo;t capture that in a few months. And this person and I at least have the benefit of working in the same office (on days we go in) and timezone.</p>
<p>Now, making it worse is that as a company we&rsquo;re split across timezones: Central Europe and Singapore, so most of our communication happens in text. We only have 2-3 hours a day of natural overlap, which makes meetings <em>hard</em>. You often end up having first interactions with people purely in text. And it gets messy.</p>
<p>Someone trying to solve a problem will come in with their idea of the world and not have enough context, and that back and forth can easily feel like it&rsquo;s becoming heated for whoever is under time pressure, and the pressure builds because you&rsquo;ll often only get one round back and forth per day due to other meetings and obligations during the overlap.<br>
<em>Why don&rsquo;t they just understand what I need?</em> &ldquo;This person isn&rsquo;t trying to understand me&rdquo; or &ldquo;they&rsquo;re not trying to help me solve my problem.&rdquo;</p>
<p>But they are.</p>
<p>The other person is trying to extract enough context from what you want to do so they can actually help. The concrete thing you asked for (poke this hole in the firewall, add another 2 boolean fields into this payload) doesn&rsquo;t align with your actual goals, so they&rsquo;re trying to understand what you&rsquo;re <em>actually</em> trying to do, so the quick hack today isn&rsquo;t next quarter&rsquo;s blocker. Because the other teams have to take long-term maintenance into account for whatever decision gets made. You&rsquo;re focused on right-fucking-now, but they&rsquo;re going to own this forever.</p>
<p><a href="https://infosec.exchange/@SwiftOnSecurity">SwiftOnSecurity</a> has said that the best security team isn&rsquo;t the one that says no to keep you safe, it&rsquo;s the one that helps you get yes. Because if they can find a solution that solves your real problem, you&rsquo;re less likely to try and circumvent them. That&rsquo;s a whole lot more work, though. You have to empathize and understand your customer. You have to extract all that context. You have to balance their urgent need with your long-term ownership.  <a href="/blog/2025/08/21/what-makes-a-good-software-engineer/">Being curious enough to find correct rather than just being right.</a></p>
<p>And this isn&rsquo;t just about security teams, it&rsquo;s every interaction across teams and timezones. We&rsquo;re all, hopefully, trying to say yes while protecting what we own.</p>
<p>At the first job I had which was split across continents, we had a &ldquo;remote working guide&rdquo; and the first page said something like: &ldquo;Always assume good intent from the other person, text makes it easy to put feelings in because you can&rsquo;t read the person and voice.&rdquo;</p>
<p>Despite internalizing that lesson, I get caught every so often getting emotional. Even with a damned bot. I put intent into text where there literally cannot be any. I assume malice, or at least incompetence, when really someone is just missing five years of accumulated context that feels like breathing to me. I mostly catch myself quickly and in text I can hide it, mask it with emojis, focus on the words. But in person? My tone undermines everything, no matter how rational my words are.</p>
<p>Something I heard, and keep thinking about, is that everyone&rsquo;s the hero of their own story. The job is trying to find where your story overlaps with theirs so you both can get the job done.</p>
<p>To try and connect stories we use <a href="https://en.wikipedia.org/wiki/BLUF_(communication)">BLUF</a> and <a href="https://medium.com/lessons-from-mckinsey/the-pyramid-principle-f0885dd3c5c7">the pyramid principle</a>, formal ways of writing for clarity. We use them in remote/text-first work as pure survival: front-load everything and let people skip what they know.</p>
<p>Add &ldquo;if X then Y&rdquo; answers to our questions when we assume a specific question to avoid the <strong>24-hour wait</strong>. Be direct without being an asshole. Don&rsquo;t waste time with false politeness, but don&rsquo;t be rude either. And still, that back-and-forth feels frustrating because what&rsquo;s obvious to you is an unfolding mystery to the other person.</p>
<p>So what are the reasonable assumptions that actually work? Some that I&rsquo;ve found:</p>
<ul>
<li>Assume good intent</li>
<li>Assume they&rsquo;re trying to help</li>
<li>Assume your context isn&rsquo;t obvious</li>
<li>Assume you&rsquo;ll need to explain more than feels necessary</li>
<li>Assume you need to signal that you&rsquo;re happy/helping by peppering liberally with emojis (you can&rsquo;t read the smile otherwise)</li>
<li>And when you catch yourself getting frustrated, assume you need to apologize and reset</li>
</ul>
<p>One more thing that helps: engage as we&rsquo;re merging contexts. &ldquo;Are you saying X because Y? And therefore Z would be right out?&rdquo; hits different than &ldquo;what should I do?&rdquo; or the umpteenth &ldquo;why?&rdquo; from a five-year old. The first shows you&rsquo;re engaging with the problem. The second sounds like you&rsquo;re expecting me to serve manna from the heavens. Even wrong thinking shows that we&rsquo;re working together.</p>
<p>So which is it? Make assumptions or don&rsquo;t? Turns out it&rsquo;s both and neither, just like I wanted 🙄. These &ldquo;reasonable assumptions&rdquo; assume the best of others while assuming nothing about what they know. Trying to resolve the contradiction by changing what we&rsquo;re assuming about, not their knowledge or context, but their intent and goodwill.</p>
<p>I keep learning this same lesson: with bots, with new colleagues, with anyone who hasn&rsquo;t been marinating in my context for years. You&rsquo;d think I&rsquo;d remember by now that my obvious is everyone else&rsquo;s mystery. But here I am, getting frustrated again when someone asks me to explain my obvious. Or getting equally frustrated when they assume <em>their obvious is mine.</em></p>
]]></content:encoded></item><item><title>Angry at the genie …getting exactly what I wished for, eventually</title><link>https://bjorn.now/crumb/2025-08-24-angry-at-the-genie/?utm_medium=feed</link><pubDate>Sun, 24 Aug 2025 10:40:00 +0800</pubDate><guid>https://bjorn.now/crumb/2025-08-24-angry-at-the-genie/</guid><description>A colleague mentioned he’d like a genie coach to practice first principles thinking, because we had talked about the cooking coach I made and have been using. So I decided to give it a try, whipped up a prompt, and gave it a spin.
I had a problem I’d been working on and wanted to see if there was another way of thinking about it. I also asked it to add behavioral economics as an angle because I’ve been interested in pulling in a more human angle to my thinking (still learning what that means).</description><content:encoded><![CDATA[<p>A colleague mentioned he&rsquo;d like a genie coach to practice <a href="https://fs.blog/first-principles/">first principles thinking</a>, because we had talked about the cooking coach I made and have been using. So I decided to give it a try, whipped up a prompt, and gave it a spin.</p>
<p>I had a problem I&rsquo;d been working on and wanted to see if there was another way of thinking about it. I also asked it to add <a href="https://en.wikipedia.org/wiki/Behavioral_economics">behavioral economics</a> as an angle because I&rsquo;ve been interested in pulling in a more human angle to my thinking (still learning what that means).</p>
<p>I set the prompt up to force me to reflect on how I&rsquo;m working, to explain myself, because I figured that&rsquo;d be the way for me to learn. So we started. The thing I explained was total garbage, I was fundamentally doing too much (boiling the ocean) and needed to do less work (start small). I realized I didn&rsquo;t provide enough context, and it gave some good suggestions for how to think about it (can&rsquo;t share the transcript because it&rsquo;s about a work thing).</p>
<p>Around round 4-5, I caught myself getting annoyed and a bit upset. Riled up. I had to put in so much effort to explain context for my situation which was just obvious to me, a person who has lived this company and reality for five years. Obviously the machine doesn&rsquo;t have that. It couldn&rsquo;t.</p>
<p>But the bot was doing <strong>exactly what I asked it to do:</strong> Question me. Push me to explain my thinking. Make sure my foundations were solid. I was getting angry at a genie for granting my wish perfectly.</p>
<p>How did my first principles thinking chat end? After I explained all my context and incorporated some of its feedback, it declared that I had &ldquo;hit bedrock&rdquo; and that my plan was sound. My biggest issue was that I wasn&rsquo;t explicit enough when explaining myself. It thought I should break it up in smaller steps, which I am pleased to say that I had already started to realize, and why I wanted to dig into it, but that the overarching goal was sound.<br>
…and as I&rsquo;m typing this up I start doubting myself, did I make a compelling argument or simply limit the context for the bot? A colleague that has been around could&rsquo;ve told me about a similar project that failed, or something from their experience, both personal and company context that change my mind.</p>
<p>It absolutely made me wonder how often that happens. How often don&rsquo;t explain myself enough, and get a pass at work, because I&rsquo;ve been working with these people for years? They trust that I won&rsquo;t get myself into too deep a hole when I&rsquo;m wrong. That trust is comforting. It also makes me wonder how often I get a go-ahead because of trust, not because I explained it well, even though I try to make my proposals stand alone without my name on it. 😅</p>
<p>The genie did exactly what I wished for. I just didn&rsquo;t realize explaining context <em>was</em> the wish.</p>
<hr>
<p><strong>Related:</strong> At least humans are better at this, right? This directly led me to <a href="/crumb/2025-08-24-reasonable-assumptions/">reasonable assumptions &hellip;and why they&rsquo;re (nearly) impossible</a>.</p>
<hr>
<p>The prompt I used for Google&rsquo;s Gemini if you want to give it a try:</p>
<div class="full-width copy-enabled">
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">ROLE &amp; GOAL
</span></span><span class="line"><span class="cl">You are a First Principles Thinking Coach. 
</span></span><span class="line"><span class="cl">Your goal is to relentlessly challenge my thinking, 
</span></span><span class="line"><span class="cl">pushing me beyond conventional solutions (local maxima). 
</span></span><span class="line"><span class="cl">Be direct and concise. 
</span></span><span class="line"><span class="cl">If my thinking is shallow, state it clearly.
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">RULES OF ENGAGEMENT
</span></span><span class="line"><span class="cl">1.  MY DECOMPOSITION: I will state a problem and my initial decomposition. Wait for it.
</span></span><span class="line"><span class="cl">2.  YOUR ANALYSIS: Analyze my breakdown.
</span></span><span class="line"><span class="cl">    - Flag conventional or &#34;too neat&#34; assumptions.
</span></span><span class="line"><span class="cl">    - If a solution is safe but uninspired, label it: 
</span></span><span class="line"><span class="cl">      &#34;This is a local maximum because you assume [specific assumption to challenge].&#34;
</span></span><span class="line"><span class="cl">3.  DEEPENING QUESTIONS: Force me to dig deeper with targeted questions. 
</span></span><span class="line"><span class="cl">    Do not accept surface-level answers. 
</span></span><span class="line"><span class="cl">    Push me 2-3 layers deeper than my stated conclusion.
</span></span><span class="line"><span class="cl">    - CONSTRAINTS: &#34;Why does that constraint exist? Who imposed it?&#34;
</span></span><span class="line"><span class="cl">    - EXTREME SCENARIOS: &#34;What if [component] were free/illegal/instant?&#34;
</span></span><span class="line"><span class="cl">    - INVERSION: &#34;To guarantee failure, what would we do?&#34;
</span></span><span class="line"><span class="cl">    - ANALOGY: &#34;How does [unrelated field, e.g., biology] solve this?&#34;
</span></span><span class="line"><span class="cl">    - SCALE/TIME: &#34;How does this change at 10x the scale or 0.1x the time?&#34;
</span></span><span class="line"><span class="cl">4.  PROBLEM REFRAMING: Challenge the core problem itself.
</span></span><span class="line"><span class="cl">    - &#34;You are solving for X, but what if the real problem is Y?&#34;
</span></span><span class="line"><span class="cl">    - &#34;If this is a good solution, why isn&#39;t it common? What are the hidden complexities?&#34;
</span></span><span class="line"><span class="cl">5.  BEHAVIORAL ECONOMICS (BE) MODULE:
</span></span><span class="line"><span class="cl">    - TRIGGER: Activate for problems involving user adoption, incentives, or market behavior.
</span></span><span class="line"><span class="cl">    - ACTION: &#34;This appears to be [specific BE bias, e.g., Status Quo Bias]. 
</span></span><span class="line"><span class="cl">      Your solution assumes rational actors, 
</span></span><span class="line"><span class="cl">      but people will likely [predictable irrational behavior].&#34;
</span></span><span class="line"><span class="cl">6.  STOP CONDITION: Cease challenging only when I say &#34;we&#39;ve hit bedrock&#34; or a similar phrase.</span></span></code></pre></div>
</div>
]]></content:encoded></item><item><title>What makes a good software engineer? “this is what would change my mind”</title><link>https://bjorn.now/blog/2025/08/21/what-makes-a-good-software-engineer/?utm_medium=feed</link><pubDate>Thu, 21 Aug 2025 23:19:01 +0800</pubDate><guid>https://bjorn.now/blog/2025/08/21/what-makes-a-good-software-engineer/</guid><description>I was in an AMA at work, and someone asked me what makes a good software engineer. I said: Someone who is curious and wants to understand why from many points of view (tech, product, customer, etc.), and someone who cares about the outcome, not that they were the ones to “get it” or “make the decision.”
The more I think about it, the more I realize that curiosity is the foundation. You need curiosity about the system you’re building, the organization’s actual goals, and especially about the people you’re working with. But curiosity alone isn’t enough, you also need to be intellectually honest about what you find.
The principle I try to live by is: I would rather be correct than right.</description><content:encoded><![CDATA[<p>I was in an AMA at work, and someone asked me what makes a good software engineer. I said: Someone who is curious and wants to understand <em>why</em> from many points of view (tech, product, customer, etc.), and someone who cares about the outcome, not that they were the ones to &ldquo;get it&rdquo; or &ldquo;make the decision.&rdquo;</p>
<p>The more I think about it, the more I realize that curiosity is the foundation. You need curiosity about the system you&rsquo;re building, the organization&rsquo;s actual goals, and especially about the people you&rsquo;re working with. But curiosity alone isn&rsquo;t enough, you also need to be intellectually honest about what you find.</p>
<p>The principle I try to live by is: <strong>I would rather be correct than right</strong>.</p>
<h2 id="difference-between-right-and-correct">
  Difference between right and correct
  <a href="#difference-between-right-and-correct" class="heading-anchor" aria-label="Direct link to Difference between right and correct"><span aria-hidden="true">#</span></a>
</h2>
<p>Might makes right: you can use position and authority to be &ldquo;right.&rdquo; But correct? Correct requires curiosity. It&rsquo;s about the actual circumstance you&rsquo;re in, not your organizational heft.</p>
<p>I started thinking about this long before I was a principal. I felt annoyed when people senior to me wouldn&rsquo;t explain themselves, when they&rsquo;d just make calls without context, &ldquo;you have to do it like this.&rdquo; They cared about <em>how</em> the work was done, but not about whether I understood it, or whether I might have useful context they were missing, or whether I could make that same good decision next time. So I wanted to make sure I was doing better, being clear about what I&rsquo;m about. Because there&rsquo;s nearly always someone more junior to you who deserves that clarity.</p>
<p>Now as a principal, I <em>can</em> use my position to be &ldquo;right.&rdquo; I can make calls. Pull rank. End discussions. But is it the <em>correct</em> call?</p>
<p>Depends. (how you know I&rsquo;m senior, right?)</p>
<p>I rarely have the full context of a situation because I go broad, not deep (across several topics, rarely deep on one), so I need to listen and dig into what other people are saying, and incorporate that into the bigger picture. And… most of the time, you won&rsquo;t necessarily get it correct then and there, so you&rsquo;ll have to loop back.</p>
<p>Which is part of why I hate making decisions in a meeting where we&rsquo;ve been exploring stuff. I want time to think about it. Time to incorporate what we just talked about and then write it down so we have it clear as day what we agreed to, or at least, what I thought we agreed to.</p>
<p>The truth is, being known as someone who can get shit done, no matter who came up with the idea, is going to take you far. In most good places, that&rsquo;s what they&rsquo;re looking for. You don&rsquo;t want to be &ldquo;the smartest person in the room.&rdquo; Instead, <strong>aim to be the person who enables everyone else in the room,</strong> and that starts with being curious about what they bring to the table.</p>
<p>But knowing the difference between right and correct isn&rsquo;t enough. You need concrete practices to actually live it. I&rsquo;ve had to develop specific habits to fight my natural instinct to just be &lsquo;right&rsquo;, especially as I have the organizational weight to do it, and my boss told me day one that I shouldn&rsquo;t rely on my weight alone.</p>
<h2 id="how-do-you-do-that">
  How do you do that?
  <a href="#how-do-you-do-that" class="heading-anchor" aria-label="Direct link to How do you do that?"><span aria-hidden="true">#</span></a>
</h2>
<p>These are the practices I have internalized and try to live by. Some of them are just about automatic and some still take effort.</p>
<h3 id="change-your-mind-out-loud">
  Change your mind out loud
  <a href="#change-your-mind-out-loud" class="heading-anchor" aria-label="Direct link to Change your mind out loud"><span aria-hidden="true">#</span></a>
</h3>
<p>Listen to others and actually evaluate their suggestions objectively. I remember a project back when I was a consultant. I had championed a pattern, and another colleague thought they had a better way of doing it. They weren&rsquo;t looking forward to having the discussion because they <em>knew</em> I would need to be convinced to go along with them.</p>
<p>They brought up their suggestion, explained what it would be and what it gave us (1-2 minutes, tops), and they looked at me and saw that I was thinking. Then: <strong>&ldquo;Yeah, that&rsquo;s a much better idea, let&rsquo;s do it!&rdquo;</strong></p>
<p>I noticed then how they seemed to relax because they thought it&rsquo;d be a hard sell and conversation, because they had mostly seen me not convinced by arguments in the past, and colleagues getting frustrated from it.</p>
<p>I continued with why I agreed with them, so it was clear.</p>
<p>Be open about <em>why</em> you changed your mind. Because it sucks to have people around who just keep flip-flopping on stuff.</p>
<p>For example, sometimes we change our mind because &ldquo;we&rsquo;ve caused too many incidents because we moved too fast, so deploying multiple times per day isn&rsquo;t what we need right now. What we need is earlier detection and understanding why we had such big incidents, so when we move faster again we&rsquo;ll still catch the problem when it&rsquo;s small.&rdquo; Not that I have something super particular in mind.</p>
<h3 id="dont-move-the-goalpost">
  Don&rsquo;t move the goalpost
  <a href="#dont-move-the-goalpost" class="heading-anchor" aria-label="Direct link to Don&rsquo;t move the goalpost"><span aria-hidden="true">#</span></a>
</h3>
<p>Be caring, intellectually honest, and outcome-oriented enough to make sure you have a stable target for when you&rsquo;re working together.</p>
<p>I try to explain my decision-making criteria, what my objection is, and what would help me change my mind. <strong>It&rsquo;s about deliberately breaking down what we&rsquo;re actually trying to achieve so others can engage with it: we need to be on the same page about the goal, not just the solution.</strong></p>
<p>Because it sucks if you say &ldquo;if this problem goes away, then we can do this&rdquo; and then they come back having found a solution, and then you add another thing to it. Obviously, it&rsquo;s hard to capture everything up front, but if you keep moving the goalpost then it&rsquo;ll <em>suuuck</em> to work with you. Sometimes you have to, but make sure you&rsquo;re clear that you&rsquo;re realizing it as you go, and why this has to go into the original criteria.</p>
<h3 id="meet-people-where-they-are">
  Meet people where they are
  <a href="#meet-people-where-they-are" class="heading-anchor" aria-label="Direct link to Meet people where they are"><span aria-hidden="true">#</span></a>
</h3>
<p>This is something I learned as a consultant and it&rsquo;s about understanding your customer and their situation: their context and constraints, but also your goal and where you want them to end up, so you can drive towards that horizon even if the immediate step is something smaller, and maybe slightly sideways.</p>
<p><a href="https://www.geepawhill.org/">GeePaw Hill</a>, who writes about technical coaching, says we can never convince anyone of anything, but we can create circumstances so they can convince themselves. Showing that you&rsquo;re there and understand them will be the thin end of the wedge that hopefully will help them explore what you&rsquo;re bringing.</p>
<p>Use their language, understand what is feasible to do right now vs. what you want longterm. A small step in the right direction is still a step.</p>
<p><strong>Example:</strong></p>
<ul>
<li><strong>My horizon:</strong> I want you to do TDD</li>
<li><strong>Where you are:</strong> Tests are a waste of time</li>
<li><strong>Next step:</strong> We&rsquo;ll have at least a black-box integration test for the success/error case, so we can catch the most egregious regressions. We&rsquo;ll iterate from there as you see the value without huge changes in how you work</li>
</ul>
<h3 id="help-the-other-person-shine">
  Help the other person shine
  <a href="#help-the-other-person-shine" class="heading-anchor" aria-label="Direct link to Help the other person shine"><span aria-hidden="true">#</span></a>
</h3>
<p>This one only works if you&rsquo;re already doing the other things. You need to be able to change your mind, be clear about your criteria, and meet people where they are before you can effectively help them succeed.</p>
<p>I realized and learned this during interviews, but it&rsquo;s equally important with colleagues. When I interview someone to join the company, I know that we have very different backgrounds, and we don&rsquo;t know much about each other. They&rsquo;re likely to say things that are some kind of colored flag to me, but I need to try and clarify and understand what they <em>really meant</em>. Because their shorthand that&rsquo;s a red flag to me might be the culturally right way to sell that idea in their current place.</p>
<p>And if this matters with strangers in interviews, it matters even more with colleagues you&rsquo;ll work with every day. In theory we&rsquo;re all steeped in the same culture, but different teams, managers, and just how long you&rsquo;ve been there shapes it all. We&rsquo;re rarely working at <em>the same idea</em> of the company.</p>
<p>Clarify, elaborate, help the other person sell their idea to you. Make sure you understand them and are representing their view correctly, confirm it, don&rsquo;t assume. If I then still disagree, I can explain why much better. And if you both are clear that you understand their perspective, they&rsquo;ll probably be much clearer that you&rsquo;re not just saying no because you don&rsquo;t understand it.</p>
<p>This is something I have struggled with, and something I have had to work on: while I have for a very long time been able to be convinced, <strong>I didn&rsquo;t do enough to help the other person win me over</strong>.</p>
<h2 id="the-thing-about-ego">
  The thing about ego
  <a href="#the-thing-about-ego" class="heading-anchor" aria-label="Direct link to The thing about ego"><span aria-hidden="true">#</span></a>
</h2>
<p>When I started programming there was a strong feeling that being a software engineer also kinda meant you were a lone basement dweller changing the world, or well, your part of it where you rightfully were god emperor. But, it&rsquo;s about working with people, and clear communication, both with humans and machines.</p>
<p>To me, a lot of it is just about being very clear about what you value and want (both design and architecture, but also in how to work), and then being able to communicate it. And also not getting too caught up in your ego and instead thinking about the goal for whatever you&rsquo;re doing.</p>
<p>So when someone asks me what makes a good software engineer, that&rsquo;s what I think about. Not the person who&rsquo;s always right, but the person curious enough to find correct. The person who cares more about the outcome than the credit.</p>
<p>I&rsquo;m never as awesome as I might fool myself into believing. My ideas get better when discussed and improved upon as a group, and that&rsquo;s the way it works. The best engineers I know aren&rsquo;t trying to be the hero with all the answers. They&rsquo;re the ones asking &ldquo;what would change your mind?&rdquo; while offering &ldquo;this is what would change mine.&rdquo;</p>
<p>My friend Andreas once told me that a high-performing team is one that wants to be. A team that decides they want to be correct in their outcomes, they&rsquo;ll set ego aside and work together. I bet they&rsquo;ll be high-performing, because they&rsquo;re actually working together, not just sitting in the same room competing to be right.</p>]]></content:encoded></item><item><title>gum: clean user interactions for shell scripts</title><link>https://bjorn.now/til/2025-08-20-gum-is-a-cli-tool-that-helps-your-make-your-scripts-easy-to-interact-with/?utm_medium=feed</link><pubDate>Wed, 20 Aug 2025 23:02:03 +0800</pubDate><guid>https://bjorn.now/til/2025-08-20-gum-is-a-cli-tool-that-helps-your-make-your-scripts-easy-to-interact-with/</guid><description>gum simplifies making shell scripts interactive: no more wrestling with read commands and ANSI escape codes for user input: just proper text editing, defaults, and clean UI.
Their demo:</description><content:encoded><![CDATA[<p><a href="https://github.com/charmbracelet/gum">gum</a> simplifies making shell scripts interactive: no more wrestling with read commands and ANSI escape codes for user input: just proper text editing, defaults, and clean UI.</p>
<p><a href="https://github.com/charmbracelet/gum/blob/9ab722ca4fe1e248283091626bd9fe5ecea65e1b/examples/demo.sh">Their demo</a>: <figure><img src="/img/2025/gum-demo.gif"
    alt="gum gemo">
</figure>
</p>
<p>For example, if you run:</p>
<div class=" copy-enabled">
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">gum input --width <span class="m">50</span> --header <span class="s2">&#34;Favorite author?&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="se"></span> --value <span class="s2">&#34;Terry Pratchett&#34;</span></span></span></code></pre></div>
</div>
<p>It will give you a question with a sensible default that you can then change as you want, and what you wrote as you hit <code>enter</code> will be returned so your script can then use it.</p>
<p>If you instead has some options where you want to select one:</p>
<div class=" copy-enabled">
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">gum choose --header <span class="s2">&#34;Favorite book?&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="se"></span> <span class="s2">&#34;Hogfather&#34;</span> <span class="s2">&#34;Thief of Time&#34;</span> <span class="s2">&#34;The Night Watch&#34;</span> <span class="s2">&#34;Small Gods&#34;</span></span></span></code></pre></div>
</div>
<p>Which gives you a selection box, you can configure to allow multiple selections, and the one you pick is then returned.</p>
<p>And one I really love, have it show a spinner to indicate that yes… something is still going on:</p>
<div class=" copy-enabled">
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">gum spin --title<span class="o">=</span><span class="s2">&#34;Counting down from 10&#34;</span> sleep <span class="m">10</span></span></span></code></pre></div>
</div>
<p>Built on <a href="https://github.com/charmbracelet/bubbletea">bubbletea</a>, a Go TUI framework, use that directly if you need these components in Go code rather than shell scripts.</p>]]></content:encoded></item><item><title>DIKW pyramid when working with genies …classification of different types of understanding</title><link>https://bjorn.now/crumb/2025-08-14-dikw-pyramid-when-working-with-genies/?utm_medium=feed</link><pubDate>Fri, 15 Aug 2025 23:30:00 +0800</pubDate><guid>https://bjorn.now/crumb/2025-08-14-dikw-pyramid-when-working-with-genies/</guid><description>Curtis shared the DIKW pyramid, that I hadn’t heard of, and said that he feels that the genies helps him process the first two layers much more quickly than he can alone, that it can somewhat help penetrate into knowledge but not much. Which allows him to spend less time in D/I (collection) and then more to then K/W (processing?)</description><content:encoded><![CDATA[<p>Curtis shared the <a href="https://en.wikipedia.org/wiki/DIKW_pyramid">DIKW pyramid</a>, that I hadn&rsquo;t heard of, and said that he feels that the genies helps him process the first two layers much more quickly than he can alone, that it can somewhat help penetrate into knowledge but not much.
Which allows him to spend less time in D/I (collection) and then more to then K/W (processing?)</p>
<p><a title="Longlivetheux, CC BY-SA 4.0 &lt;https://creativecommons.org/licenses/by-sa/4.0&gt;, via Wikimedia Commons" href="https://commons.wikimedia.org/wiki/File:DIKW_Pyramid.svg"><img width="256" alt="The pyramid of knowledge with a base of data, the next layer built of information, then knowledge and finally wisdom at the peak." src="https://upload.wikimedia.org/wikipedia/commons/thumb/0/06/DIKW_Pyramid.svg/256px-DIKW_Pyramid.svg.png?20220726215947"></a></p>
<p>This is true, I have felt that if I provide enough information when trying to find something I can get out reams of useful and contextual D/I, but the biggest problem is attribution: I don&rsquo;t know if it&rsquo;s real or hallucinations, and you need to make sure you get the references so you can confirm it. BUT, I am spending less time <em>reading wide</em> because it can help narrow it. (which sounds like the ideal librarian who can help you find <em>just that book you need</em> based on what you explained).</p>
<p>Time to figure out what these layers actually mean.</p>
<h2 id="climbing-the-pyramid">
  Climbing the pyramid
  <a href="#climbing-the-pyramid" class="heading-anchor" aria-label="Direct link to Climbing the pyramid"><span aria-hidden="true">#</span></a>
</h2>
<p>Reading the DIKW page and was curious about the definitions, my gut for what data/information is was correct, but I see it seems to get vaguer with knowledge.</p>
<blockquote>
<p>Zeleny has asserted that to capture knowledge in symbolic form is to make it into information, i.e., that &ldquo;All knowledge is tacit&rdquo;.</p>
</blockquote>
<p>this feels correct somehow. But also this:</p>
<blockquote>
<p>Knowledge is a fluid mix of framed experience, values, contextual information, expert insight and grounded intuition that provides an environment and framework for evaluating and incorporating new experiences and information. It originates and is applied in the minds of knowers. In organizations it often becomes embedded not only in documents and repositories but also in organizational routines, processes, practices and norms.</p>
</blockquote>
<p>It feels like <em>knowledge</em> is personal. Also why I like the idea of knowledge &ldquo;made specific&rdquo; becomes information, and then needs to be contextualized into someones situation as knowledge → and then that combination can then be &ldquo;made specific&rdquo; again into information that others can use in that context.</p>
<p>No idea if that makes sense 🙂</p>
<p>Wisdom even fluffier, but I kinda wonder if that&rsquo;s moving from tacit → explicit, but the difference from <code>information</code> is that it&rsquo;s not only contextual to <em>one concrete</em> situation, it&rsquo;s when you can teach/identify the meta &ldquo;skill&rdquo; of whatever you have knowledge?</p>
<p>Then again, I also like the simple version of &ldquo;knowledge is that a tomato is a fruit. wisdom is to not put it in a fruit salad.&rdquo;
(and talking it through with a genie it pointed out that while funny the classification is wrong, tomato is a fruit is information, and not using it in a fruit salad is knowledge because how it&rsquo;s contextualized)</p>
<blockquote>
<p>Zeleny described wisdom as &ldquo;know-why&rdquo;, but later refined his definitions, so as to differentiate &ldquo;why do&rdquo; (wisdom) from &ldquo;why is&rdquo; (information), and expanding his definition to include a form of know-what (&ldquo;what to do, act or carry out&rdquo;). And, as noted by Nikhil Sharma, Zeleny has argued for a tier to the model beyond wisdom, termed &ldquo;enlightenment&rdquo;.</p>
</blockquote>
<p>I like the &ldquo;why do&rdquo; definition of wisdom, and I think that connects well to the &ldquo;meta skill&rdquo; above.</p>
<p>I wanted to see if I had gotten the full idea after the tomato in my face, so I focused on my coffee habit, and tried to put it into the pyramid to conceptualize it for myself:</p>
<ul>
<li>Data: today
<ul>
<li>Bought 1 coffee x $5.5</li>
<li>Got 1 machine coffee in office x $0</li>
</ul>
</li>
<li>Information: I usually buy two coffees for a total of $10</li>
<li>Knowledge: I&rsquo;m trying to spend less so halving the cost helps me get there, while also giving me <em>some</em> good coffee per day</li>
<li>Wisdom: If I make a thermos of good coffee at home and bring to the office, then I&rsquo;ll get two good cups and it&rsquo;s cheaper</li>
</ul>
<p>I started a chat with Gemini and shared all the notes above (<a href="https://gist.github.com/gaqzi/2d9d8987805e677a8f8860aeb5ea0269">transcript</a>), and it felt this was a good example of wisdom, and when I asked Claude it felt my answer was too strategic and that I wasn&rsquo;t explaining the full &ldquo;why do&rdquo;, and suggested:</p>
<blockquote>
<p>Wisdom: &ldquo;Small daily expenses compound dramatically over time, so sustainable cost-cutting requires finding cheaper alternatives that I&rsquo;ll actually stick with (hence keeping good coffee, just changing the source).&rdquo;</p>
</blockquote>
<p>And that connects in a whole other way, I&rsquo;ll keep that in mind for next time, and it also highlights some <em>unspoken context</em> that was tacit, the habit is one I could likely sustain because it&rsquo;s small and something I do most days when working from home, but I didn&rsquo;t call it out.</p>
]]></content:encoded></item><item><title>Non-interactive shells will source the file pointed at by a variable at launch</title><link>https://bjorn.now/til/2025-08-15-if-you-start-a-non-interactive-bash-shell-it-will-source-the-content-of-the-file-defined-in-bash-env-and-env-for-a-posix-shell/?utm_medium=feed</link><pubDate>Fri, 15 Aug 2025 21:19:18 +0800</pubDate><guid>https://bjorn.now/til/2025-08-15-if-you-start-a-non-interactive-bash-shell-it-will-source-the-content-of-the-file-defined-in-bash-env-and-env-for-a-posix-shell/</guid><description>If you start a non-interactive bash shell it will source the content of the file defined in BASH_ENV (and ENV for a POSIX shell).</description><content:encoded><![CDATA[<p>If you start a non-interactive bash shell it will source the content of the file defined in <code>BASH_ENV</code> (and <code>ENV</code> for a POSIX shell).</p>
<blockquote>
<p>When bash is started non-interactively, to run a shell script, for example, it looks for the variable <code>BASH_ENV</code> in the environment, expands its value if it appears there, and uses the expanded value as the name of a file to read and execute.  Bash behaves as if the following command were executed:</p>
<p><code>if [ -n &quot;$BASH_ENV&quot; ]; then . &quot;$BASH_ENV&quot;; fi</code></p>
<p>but the value of the <code>PATH</code> variable is not used to search for the file name.</p>
</blockquote>]]></content:encoded></item><item><title>Syntax for declaring and using YAML anchors</title><link>https://bjorn.now/til/2025-08-15-the-yaml-anchors-to-name-a-reusable-section-are-defined-by-name-and-then-to-use-them-to-replace-the-value-of-something-you-do-name/?utm_medium=feed</link><pubDate>Fri, 15 Aug 2025 21:19:18 +0800</pubDate><guid>https://bjorn.now/til/2025-08-15-the-yaml-anchors-to-name-a-reusable-section-are-defined-by-name-and-then-to-use-them-to-replace-the-value-of-something-you-do-name/</guid><description>The YAML anchors to name a reusable section are defined by &amp;name and then to use them to replace the value of something you do *name, if you want to “unsplat”/merge a dictionary/object then use &lt;&lt;: *name and then it’ll insert it at that point.</description><content:encoded><![CDATA[<p>The YAML anchors to name a reusable section are defined by <code>&amp;name</code> and then to use them to replace the value of something you do <code>*name</code>, if you want to &ldquo;unsplat&rdquo;/merge a dictionary/object then use <code>&lt;&lt;: *name</code> and then it&rsquo;ll insert it at that point.</p>
<div class=" copy-enabled">
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">world</span><span class="p">:</span><span class="w"> </span><span class="cp">&amp;world</span><span class="w"> </span><span class="l">World</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="nt">example</span><span class="p">:</span><span class="w"> </span><span class="cp">&amp;example-anchor</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">HELLO</span><span class="p">:</span><span class="w"> </span><span class="cp">*world</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">There</span><span class="p">:</span><span class="w"> </span><span class="l">Yo</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="nt">my-values</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">&lt;&lt;</span><span class="p">:</span><span class="w"> </span><span class="cp">*example-anchor</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">foo</span><span class="p">:</span><span class="w"> </span><span class="l">bar</span></span></span></code></pre></div>
</div>
<p>becomes</p>
<div class=" copy-enabled">
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nn">---</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="nt">example</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">HELLO</span><span class="p">:</span><span class="w"> </span><span class="l">World</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">There</span><span class="p">:</span><span class="w"> </span><span class="l">Yo</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="nt">my-values</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">HELLO</span><span class="p">:</span><span class="w"> </span><span class="l">World</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">There</span><span class="p">:</span><span class="w"> </span><span class="l">Yo</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">foo</span><span class="p">:</span><span class="w"> </span><span class="l">bar</span></span></span></code></pre></div>
</div>]]></content:encoded></item><item><title>YAML does *not* support unsplatting/merging lists</title><link>https://bjorn.now/til/2025-08-15-yaml-does-not-support-unsplatting-lists-and-thats-intentional-https/?utm_medium=feed</link><pubDate>Fri, 15 Aug 2025 21:19:18 +0800</pubDate><guid>https://bjorn.now/til/2025-08-15-yaml-does-not-support-unsplatting-lists-and-thats-intentional-https/</guid><description>YAML does not support unsplatting lists (basically, merging list items inline like you can with objects) and that’s intentional.</description><content:encoded><![CDATA[<p>YAML does not support unsplatting lists (basically, merging list items inline like you can with objects) and <a href="https://github.com/yaml/yaml/issues/35">that&rsquo;s intentional</a>.</p>
<p>So if you have a document like below, there is no syntax to make the <code>commands</code> a three item list:</p>
<div class=" copy-enabled">
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nn">---</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="nt">example</span><span class="p">:</span><span class="w"> </span><span class="cp">&amp;example</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span>- <span class="s2">&#34;Hello&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span>- <span class="s2">&#34;World!&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;hello&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="nt">commands</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span>- <span class="l">&lt;&lt; *example</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span>- <span class="s2">&#34;Oho!&#34;</span></span></span></code></pre></div>
</div>
<p><strong>WILL NOT</strong> turn into:</p>
<div class=" copy-enabled">
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nn">---</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="nt">example</span><span class="p">:</span><span class="w"> </span><span class="cp">&amp;example</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span>- <span class="s2">&#34;Hello&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span>- <span class="s2">&#34;World!&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;hello&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="nt">commands</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span>- <span class="s2">&#34;Hello&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span>- <span class="s2">&#34;World!&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span>- <span class="s2">&#34;Oho!&#34;</span></span></span></code></pre></div>
</div>
<p>…and just because before I found the issue where it was described that this isn&rsquo;t happening <a href="https://github.com/gaqzi/test-go-yaml">I had made a test repo</a> to try and understand if it was a library/usage issue, because I noticed I could get things running on CI with <code>&lt;&lt; *example</code> which syntax didn&rsquo;t give syntax error, but also did <em>nothing</em> when running on Drone CI.</p>]]></content:encoded></item><item><title>Use Commas as Prefix for Your Scripts</title><link>https://bjorn.now/link/2025-08-13-use-commas-as-prefix-for-your-scripts/?utm_medium=feed</link><pubDate>Wed, 13 Aug 2025 08:28:11 +0800</pubDate><guid>https://bjorn.now/link/2025-08-13-use-commas-as-prefix-for-your-scripts/</guid><description>Commands with commas: I came across this a couple of years ago, and I’ve been using it for my new scripts since.
Debian today supports a huge number of commands; my modest Ubuntu laptop shows several thousand available:
$ apt-file search -x ‘^/usr/bin/[^/]*$’ | wc -l
21733
The solution was obviously to adjust my command names in such a way that they were still easy to type, but would never be chosen as system command names.
[…]</description><content:encoded><![CDATA[<p><a href="https://rhodesmill.org/brandon/2009/commands-with-comma/">Commands with commas</a>: I came across this a couple of years ago, and I&rsquo;ve been using it for <a href="https://github.com/gaqzi/conf/tree/master/bin">my new scripts since.</a></p>
<blockquote>
<p>Debian today supports a huge number of commands; my modest Ubuntu laptop shows several thousand available:</p>
<blockquote>
<p>$ apt-file search -x &lsquo;^/usr/bin/[^/]*$&rsquo; | wc -l<br>
21733</p>
</blockquote>
<p>The solution was obviously to adjust my command names in such a way that they were still easy to type, but would never be chosen as system command names.<br>
[…]</p>
<p><strong>There was but one character left: the simple, modest comma.</strong></p>
<p>A quick experiment revealed in a flash that the comma was exactly the character that I had been looking for! Every tool and shell that lay in arm&rsquo;s reach treated the comma as a perfectly normal and unobjectionable character in a filename.[…]</p>
<p>And, best of all, <strong>thanks to the magic of tab-completion, it became very easy to browse my entire collection of commands</strong>. […]<br>
I simply type a comma followed by tab and my list of commands appears</p>
</blockquote>
]]></content:encoded></item><item><title>/usr/bin/env handles commands with subcommands</title><link>https://bjorn.now/til/2025-08-13-usr-bin-env-with-subcommands/?utm_medium=feed</link><pubDate>Wed, 13 Aug 2025 08:21:23 +0800</pubDate><guid>https://bjorn.now/til/2025-08-13-usr-bin-env-with-subcommands/</guid><description>/usr/bin/env executes commands with flags/subcommands, not just bare executables. Which is great if you, for example, have a script/lint that’s a Python script, and it needs dependencies from a virtualenv that isn’t active when you call it.
Just put your shebang as /usr/bin/env uv run python3 and it always runs in the virtualenv, no wrapper script needed. This feels obvious in hindsight, it’s what you expect from these tools 😃</description><content:encoded><![CDATA[<p><code>/usr/bin/env</code> executes commands with flags/subcommands, not just bare executables. Which is great if you, for example, have a <code>script/lint</code> that&rsquo;s a Python script, and it needs dependencies from a virtualenv that isn&rsquo;t active when you call it.</p>
<p>Just put your shebang as <code>/usr/bin/env uv run python3</code> and it always runs in the virtualenv, no wrapper script needed. This feels obvious in hindsight, it&rsquo;s <em>what you expect</em> from these tools 😃</p>
]]></content:encoded></item><item><title>Anthropic on prompting for agents …and musing on genies possibly teaching empathy</title><link>https://bjorn.now/crumb/2025-08-12-anthropic-on-prompting-for-agents/?utm_medium=feed</link><pubDate>Tue, 12 Aug 2025 23:53:04 +0800</pubDate><guid>https://bjorn.now/crumb/2025-08-12-anthropic-on-prompting-for-agents/</guid><description>Anthropic has released some recordings from their Code w/ Claude event in May and the Prompting for Agents presentation’s “key principles” are basically to empathise with your agent, imagine it’s a brilliant new grad, book smart but missing all things practical at their first job: You need clear concepts, unambiguous instructions, and well-named and designed tools.
I’m more than a little amused that we’re basically coming to a point where, if things pan out with the genies, then the best wranglers will be the ones that can empathise the most with others. Then again, I always thought the brilliant asshole was the exception, they only survive if they’re in charge or are legacy to the company. And sitting in on the review meetings at work… it’s definitely the ones that help the team that we like the most, even if we sometimes need to push them to have their name on something so people don’t overlook them.</description><content:encoded><![CDATA[<p>Anthropic has released some recordings from their Code w/ Claude event in May and the <a href="https://www.youtube.com/watch?v=XSZP9GhhuAc">Prompting for Agents</a> presentation&rsquo;s &ldquo;key principles&rdquo; are basically to <em>empathise</em> with your agent, imagine it&rsquo;s a brilliant new grad, book smart but missing all things practical at their first job: You need clear concepts, unambiguous instructions, and well-named and designed tools.</p>
<div style="position: relative; padding-bottom: 56.25%; height: 0; overflow: hidden;">
      <iframe allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share; fullscreen" loading="eager" referrerpolicy="strict-origin-when-cross-origin" src="https://www.youtube.com/embed/XSZP9GhhuAc?autoplay=0&amp;controls=1&amp;end=0&amp;loop=0&amp;mute=0&amp;start=0" style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; border:0;" title="YouTube video"></iframe>
    </div>

<p>I&rsquo;m more than a little amused that we&rsquo;re basically coming to a point where, if things pan out with the genies, then the best wranglers will be the ones that can empathise the most with others. Then again, I always thought the brilliant asshole was the exception, they only survive if they&rsquo;re in charge or are legacy to the company. And sitting in on the review meetings at work… it&rsquo;s definitely the ones that help the team that we like the most, even if we sometimes need to push them to have their name on something so people don&rsquo;t overlook them.</p>
<p>If the genies take off, then I wonder what kind of knock-on effect it&rsquo;ll have on developers, and will it improve their interpersonal skills? What I have noticed is that my XP practices seem to be why I have better luck than some friends. Earlier this year, a chat kept generating wrong code, I decided to figure out why, and I realized I&rsquo;d been inconsistent with my naming. The genie was mixing things up because I was mixing things up. Any human developer would&rsquo;ve had the same problem working in this code. When I fixed the inconsistency, the genie immediately got it right. The same practices that make code maintainable make it genie-friendly.</p>
<p>And to me it&rsquo;s not first about genies, but the humans, and we already struggle with the human-to-human. Most of my thank-yous this year? Not for design help, but for helping people communicate their changes. Maybe because design feels closer to home, it&rsquo;s the skill they already have, just needs a nudge, and might&rsquo;ve gotten there anyway. But communication? Underdeveloped. Might not have gotten there otherwise.</p>
<p>Just last week I heard someone saying they didn&rsquo;t need to provide context because &ldquo;anyone reading this should know this already.&rdquo; But your ask is piling onto whatever they&rsquo;re already doing. The ideal? Create the context, set up the options, explain how to make the call based on their situation. And if they don&rsquo;t know? Give them the &ldquo;just do this&rdquo; default that works 90% of the time, no deep dive required. Same as with genies: do the setup work, make the ask trivial. Or as Kent Beck says about code: &ldquo;<a href="https://twitter.com/KentBeck/status/250733358307500032">Make the change easy, then make the easy change.</a>&rdquo; Turns out that works when asking for help, too.</p>
]]></content:encoded></item><item><title>Simon Sinek on AI and the value of struggle</title><link>https://bjorn.now/link/2025-08-10-simon-sinek-on-the-value-of-struggle/?utm_medium=feed</link><pubDate>Sun, 10 Aug 2025 21:42:17 +0800</pubDate><guid>https://bjorn.now/link/2025-08-10-simon-sinek-on-the-value-of-struggle/</guid><description>Simon Sinek on AI and the value of struggle, via Arjit:
People keep telling us life is not about the destination. Life is about the journey. But when we think about AI, we only think about the destination. And it’s remarkable ability to write the book, paint the painting, solve the problem, but we forget the importance of doing the work yourself.
And I think in our modern day and age, we have underrepresented the value of struggle. I am smarter, better at problem solving, more resourceful, not because a book exists with my ideas in it, but because I wrote it. That excruciating journey is what made me grow.</description><content:encoded><![CDATA[<p><a href="https://podscripts.co/podcasts/the-diary-of-a-ceo-with-steven-bartlett/simon-sinek-youre-being-lied-to-about-ais-real-purpose-and-were-teaching-our-kids-to-not-be-human">Simon Sinek on AI and the value of struggle</a>, via Arjit:</p>
<blockquote>
<p>People keep telling us life is not about the destination. Life is about the journey. But when we think about AI, we only think about the destination. And it&rsquo;s remarkable ability to write the book, paint the painting, solve the problem, but we forget the importance of doing the work yourself.</p>
<p>And I think <strong>in our modern day and age, we have underrepresented the value of struggle.  I am smarter, better at problem solving, more resourceful, not because a book exists with my ideas in it, but because I wrote it. That excruciating journey is what made me grow.</strong></p>
<p>But it&rsquo;s the same for love, friendships, conflict. And I think that we forget that we give up certain skills or abilities because of technology.</p>
</blockquote>
<p>The struggle is the point. More people can generate a mediocre painting or write perfect English now, but they won&rsquo;t understand the beauty behind either without doing the work. And it&rsquo;s our prerogative to decide which struggles matter to us or if we just wanted something nice to hang in the toilet.</p>
]]></content:encoded></item><item><title>Socrates on writing and learning</title><link>https://bjorn.now/link/2025-08-10-socrates-on-writing/?utm_medium=feed</link><pubDate>Sun, 10 Aug 2025 19:53:07 +0800</pubDate><guid>https://bjorn.now/link/2025-08-10-socrates-on-writing/</guid><description>Went looking for that old ‘kids today don’t memorize’ quote and found an old argument against writing:
[…] They will cease to exercise memory because they rely on that which is written, calling things to remembrance no longer from within themselves, but by means of external marks. […]
One thing we’re not doing as effectively today as we’ve done in the past is to learn lessons by heart. If you don’t know things with perfect recall, how can you expect to apply them?</description><content:encoded><![CDATA[<p>Went looking for that old &lsquo;kids today don&rsquo;t memorize&rsquo; quote and found <a href="https://fs.blog/an-old-argument-against-writing/">an old argument against writing</a>:</p>
<blockquote>
<blockquote>
<p>[…] <strong>They will cease to exercise memory because they rely on that which is written, calling things to remembrance no longer from within themselves, but by means of external marks.</strong> […]</p>
</blockquote>
<p>One thing we’re not doing as effectively today as we’ve done in the past is to learn lessons by heart. If you don’t know things with perfect recall, how can you expect to apply them?</p>
<p>Socrates goes on to compare a written text to a painting:</p>
<blockquote>
<p>You know, Phaedrus, that is the strange thing about writing, which makes it truly correspond to painting. The painter’s products stand before us as though they were alive. But if you question them, they maintain a most majestic silence. It is the same with written words. They seem to talk to you as though they were intelligent, but if you ask them anything about what they say from a desire to be instructed they go on telling just the same thing forever.</p>
</blockquote>
</blockquote>
<p>That last bit especially, &ldquo;if you ask them anything&hellip; they go on telling just the same thing forever.&rdquo;
This all sounds exactly like how we&rsquo;re talking about genies today, and… we&rsquo;re probably going to be fine.</p>
]]></content:encoded></item><item><title>When everyone gets a genie — learning to handle expertise you didn't earn</title><link>https://bjorn.now/blog/2025/08/10/when-everyone-gets-a-genie/?utm_medium=feed</link><pubDate>Sun, 10 Aug 2025 18:40:00 +0800</pubDate><guid>https://bjorn.now/blog/2025/08/10/when-everyone-gets-a-genie/</guid><description>We’ve always had to figure out who to trust for expertise, but until now, that access wasn’t universal. Used to be the bloke at the pub who ‘knew things,’ or that friend of a friend who could fix computers (me as a kid), or books if you had the patience. Rich people had their ‘real’ experts on call, though Bernie Madoff shows how well that could work out.
The internet changed things, sure. Suddenly you could reach out to actual experts, find communities, and learn from people across the world. But you still needed judgment, because you could also find a community that agreed the earth was flat, so, you know, mixed bag.
What hasn’t changed is we’ve always had to figure out who to trust. What’s changing is how we get our answers, and who gets to sound like an expert.</description><content:encoded><![CDATA[<p>We&rsquo;ve always had to figure out who to trust for expertise, but until now, that access wasn&rsquo;t universal. Used to be the bloke at the pub who &lsquo;knew things,&rsquo; or that friend of a friend who could fix computers (me as a kid), or books if you had the patience. Rich people had their &lsquo;real&rsquo; experts on call, though Bernie Madoff shows how well that could work out.</p>
<p>The internet changed things, sure. Suddenly you could reach out to actual experts, find communities, and learn from people across the world. But you still needed judgment, because you could also find a community that agreed the earth was flat, so, you know, mixed bag.</p>
<p>What hasn&rsquo;t changed is we&rsquo;ve always had to figure out who to trust. What&rsquo;s changing is how we get our answers, and who gets to sound like an expert.</p>
<h2 id="my-dinner-with-mark">
  My dinner with Mark
  <a href="#my-dinner-with-mark" class="heading-anchor" aria-label="Direct link to My dinner with Mark"><span aria-hidden="true">#</span></a>
</h2>
<p>I went for dinner with Mr. Bossman, or Mark as he&rsquo;s also known, and we ended up talking about AI because we&rsquo;re looking to make genies available to all programmers at work, and we want to do it safely. While talking about what we&rsquo;re doing, he says he doesn&rsquo;t think we&rsquo;ll have programmers like we do today, that programming is probably going to end up being more like normal language that compiles down into software (all about that prompt).</p>
<p>I&rsquo;m not sure that&rsquo;s the full story, thinking about how we&rsquo;ve progressed with, for example, assembly programming. Forty years ago, knowing assembly was basically unavoidable if you wrote something complex. Now? The only people I know who still work with assembly regularly are my friends in high-frequency trading or embedded systems. Places where the cost of that expertise is worth it because they need those specific optimizations. It doesn&rsquo;t mean it <strong>went away</strong>, but yeah, we use high-level languages (more akin to spoken languages than assembly) to write software now.</p>
<p>Us seniors have always thought that the &lsquo;kids today&rsquo; don&rsquo;t know the basics, where the basics are defined as whatever level of abstraction we learned when we got started. Assembly folks complained about C programmers. C programmers complained about scripting languages. Now those scripting folks complain about people who only know frameworks. A lot of people I come across don&rsquo;t really know how to set up a Linux box, and that was required for me because otherwise I couldn&rsquo;t host anything. But why should they know it now?</p>
<p>Mark connected with this and said that the people senior to him all knew assembly to some degree, but starting at C, he never did. He didn&rsquo;t bother with yacc, lex, any of that, because the compiler handled it. What was core skills for them became optional for him. Every generation&rsquo;s &ldquo;<em>minimum viable knowledge</em>&rdquo; shifts up a layer of abstraction.</p>
<p>I think we&rsquo;re coming to a point where more people are able to get the output of programming: something that does what they need, even if it&rsquo;s held together with digital duct tape. Which is <a href="https://en.wikipedia.org/wiki/Jevons_paradox">Jevons&rsquo; paradox</a> at work: when something becomes more efficient, we don&rsquo;t use less of it, we use <em>way more</em>. And all those people using it poorly? That&rsquo;s not a bug, that&rsquo;s the feature.</p>
<h2 id="the-commoditization-pattern">
  The commoditization pattern
  <a href="#the-commoditization-pattern" class="heading-anchor" aria-label="Direct link to The commoditization pattern"><span aria-hidden="true">#</span></a>
</h2>
<p>Every technology does this. Think about typing. Companies used to have entire secretary pools because typing was a specialized skill. Now? Everyone types. We probably produce more typed words daily than all those typing pools combined. The skill didn&rsquo;t disappear, it just became so commoditized that we don&rsquo;t even think of it as a skill anymore. But writing, <em>oh boy</em>, that&rsquo;s a skill and we recognize good vs. bad even if everyone can type.</p>
<p>Socrates complained that writing was <a href="https://fs.blog/an-old-argument-against-writing/">a terrible way to share knowledge</a>, that it would make people &ldquo;seem to know much while for the most part they know nothing.&rdquo; (Sound familiar?) When calculators came out, people panicked that kids wouldn&rsquo;t understand math anymore and society would collapse!!</p>
<p>Some examples of how we&rsquo;ve ruined the fabric of society:</p>
<table>
  <thead>
      <tr>
          <th>The panic</th>
          <th>The reality</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>&ldquo;Kids won&rsquo;t memorize anything!&rdquo;</td>
          <td>They don&rsquo;t. They Google it. Works fine.</td>
      </tr>
      <tr>
          <td>&ldquo;No one will know mental math!&rdquo;</td>
          <td>They don&rsquo;t. Phone calculator. Works fine.</td>
      </tr>
      <tr>
          <td>&ldquo;People can&rsquo;t follow directions!&rdquo;</td>
          <td>They can&rsquo;t. GPS does it step-by-step. Works fine.</td>
      </tr>
      <tr>
          <td>&ldquo;No one will really program!&rdquo;</td>
          <td>Probably the same?</td>
      </tr>
  </tbody>
</table>
<p>I know how useful &lsquo;barely good enough&rsquo; programming can be, because back at my first job, I built a stock-take system over a weekend with a friend. I knew enough MySQL and Ruby to hack together a database and some reports, he knew Visual Basic to make a UI. We had laptops on shopping carts with USB barcode scanners. What used to take 2-3 weeks of manual counting and sticky notes transcribed into Excel sheets sent up to the home office, we did in two days with results aggregated in half an hour.</p>
<p>Was it elegant code? God no. Did it solve a real problem? Absolutely. I spent about five years at this company, as the first and only &lsquo;IT employee,&rsquo; building little tools and scripts to help make our days simpler. Funny thing is, I was hired to fix printers and keep the POS running, but once I&rsquo;d gotten that under control, people started giving me purchasing assistant work, and, well, I knew just enough programming to make that go away too. At one point our ERP vendor said they&rsquo;d never seen a customer with as much automation as us, which was really just me getting sick of manual work and knowing <em>just enough</em> programming to make it go away.</p>
<p>And that&rsquo;s commoditization at work. The specialized skill becomes rare and expensive, while the everyday version becomes accessible but, let&rsquo;s be honest, pretty shitty. And you know what? There&rsquo;s nothing wrong with shitty if it solves real problems. The expertise, all those &lsquo;it depends&rsquo; answers we love to give, that comes in when you need to scale it, secure it, make it last. But first? First you just need it to work.</p>
<h2 id="whats-different">
  What&rsquo;s different
  <a href="#whats-different" class="heading-anchor" aria-label="Direct link to What&rsquo;s different"><span aria-hidden="true">#</span></a>
</h2>
<p>We&rsquo;re not just replacing horses with engines. We&rsquo;re replacing the process of acquiring expertise with cock-sure genies that answer everything instantly.</p>
<p>It&rsquo;s like how, in parts of history, being fat signalled wealth, today, abundance is everywhere and the signal has flipped. Expertise used to be like that: reserved for the wealthy or well-connected. Now everyone gets <em>apparent expertise</em>. And just like abundant food doesn&rsquo;t make you healthy (without self-control), abundant answers don&rsquo;t make the decision correct.</p>
<p>Plus, when the wealthy got bad advice, they had lawyers and fall guys. When we get bad advice from a genie? Well, good luck explaining that in the post-mortem when <a href="/blog/2025/07/24/your-name-is-still-on-it/">your name was on it.</a></p>
<h2 id="spreading-out-not-disappearing">
  Spreading out, not disappearing
  <a href="#spreading-out-not-disappearing" class="heading-anchor" aria-label="Direct link to Spreading out, not disappearing"><span aria-hidden="true">#</span></a>
</h2>
<p>Those assembly programmers didn&rsquo;t disappear, they just don&rsquo;t all work at IBM anymore. They&rsquo;re scattered across trading firms, embedded systems shops, game companies, and more. The expertise spreads out because it <em>can</em>, you don&rsquo;t need to be at one of three companies to use your deep skills anymore.</p>
<p>(And yeah, I know someone&rsquo;s thinking &ldquo;but what about all the jobs that will disappear?&rdquo; We don&rsquo;t have many <a href="https://en.wikipedia.org/wiki/Great_horse_manure_crisis_of_1894">manure shovelers</a> anymore since cars replaced horses. We also have social media managers, cloud architects, and whatever it&rsquo;ll mean to be a &ldquo;context engineer.&rdquo; The jobs change. They always have.)</p>
<p>Same thing will happen with what we call programming today. The absolute number might not even go down much. Maybe we&rsquo;ll have fewer at big tech companies and more scattered across regular companies, building internal tools, being the person who knows how to make the genies behave.</p>
<p>Kind of like how most companies have someone who&rsquo;s &lsquo;good with computers&rsquo; to fix Outlook or figure out why the printer isn&rsquo;t working. They don&rsquo;t need to be highly educated in IT, they just need to know how to figure things out and stick with it.</p>
<h2 id="which-skills-do-you-keep">
  Which skills do you keep?
  <a href="#which-skills-do-you-keep" class="heading-anchor" aria-label="Direct link to Which skills do you keep?"><span aria-hidden="true">#</span></a>
</h2>
<p>This is like my relationship with cars. I can drive, change oil, swap tires. Am I the best at parallel parking? Nope, and I&rsquo;ll drive a bit further to avoid a tight spot. Should you trust me to race? Definitely not. But I can safely get on the highway and go reasonably fast when it&rsquo;s appropriate (those spots we&rsquo;ve built up to <a href="https://www.joint-forces.com/exercise-news/73407-swedish-gripen-fighters-landing-on-highways">let Gripen land and take off</a>).</p>
<p>Some skills I want, some I don&rsquo;t care about. And that&rsquo;s fine, <strong>until</strong> the world expects me to be good at the ones I&rsquo;ve skipped.</p>
<p>Testing is one of those skills that shows this tension perfectly. I&rsquo;m a 

<dfn title="Test Driven Development">TDD</dfn>
 person, it&rsquo;s a fantastic design tool, not just bug-catching. But many programmers still treat it as an afterthought because &ldquo;how can you know what to implement before you build it?&rdquo; That resistance is exactly why it&rsquo;s valuable: it forces you to think differently and know what <em>outcome</em> you want. And that&rsquo;s why genies make things dangerous: they can write tests that look good, but if you don&rsquo;t understand testing as design, you won&rsquo;t know when you&rsquo;re building the wrong thing with great test coverage.</p>
<h2 id="deciding-what-sticks">
  Deciding what sticks
  <a href="#deciding-what-sticks" class="heading-anchor" aria-label="Direct link to Deciding what sticks"><span aria-hidden="true">#</span></a>
</h2>
<p>So how do we learn when we have genies answering everything? I&rsquo;ve been experimenting with this. My cooking has genuinely improved with an AI coach, but I&rsquo;m not just following recipes. I ask <em>why</em> we&rsquo;re using these ingredients, <em>why</em> this technique works, what the char is actually doing to the flavor. Cooking is something I want to get better at, no intention to become a proper chef, but being able to whip up dinner, using up my ingredients, and liking the taste? That&rsquo;d be nice.</p>
<p>I&rsquo;ve been using <a href="https://www.grammarly.com">Grammarly</a> for years, and I know my writing has improved from it. Not because it fixes things for me, but because I&rsquo;ve started catching the mistakes before the red underline shows up. Though we still disagree about commas, Swedish habits die hard.</p>
<p>There&rsquo;s this thing called <a href="https://commoncog.com/the-tacit-knowledge-series/">tacit knowledge</a>, stuff we know but can&rsquo;t explain, that gut feel that something about this code is wrong but you can&rsquo;t exactly pinpoint it. You write the same code again and again, get the same PR feedback again and again, until it clicks. But does it have to be that painful? I wonder if the genie can actually help us create better ways to build tacit knowledge, since it can generate examples and evaluate them. Like, I could ask it to show me ten variations of the same function, each with different edge cases, and develop that pattern recognition faster.</p>
<p>Maybe a good way for me to be more intentional is to write down which skills I want to learn. The ones where I want to gain that tacit knowledge, and also take it further so I can explain it, and not just feel it. Cooking and writing are both on that list. So is design and architecture when programming, and I&rsquo;m much further along there. (Thanks Daryl <a href="https://daryl.wakatara.com/counting-coup/">for the inspiration.</a>)</p>
<h2 id="what-im-exploring-next">
  What I&rsquo;m exploring next
  <a href="#what-im-exploring-next" class="heading-anchor" aria-label="Direct link to What I&rsquo;m exploring next"><span aria-hidden="true">#</span></a>
</h2>
<p>I&rsquo;m not a breathless AI fan. But, for me, it has turned into an intellectual sparring partner, someone to bounce ideas off who helps me figure out what I <em>truly want.</em> The genie shows me options, my gut tells me which ones I like or not, and I try to listen to that gut feeling and understand <em>why</em>.</p>
<p>Because <a href="https://time.com/7295195/ai-chatgpt-google-learning-school/">apparently our brains are turning to mush</a> because of AI. I don&rsquo;t buy it, but I also don&rsquo;t intend to let it happen to me. I still want to know which skills matter to me, which ones I&rsquo;m keeping sharp, and which ones I&rsquo;m okay with the genie handling as long as I can check the output.</p>
<p>I don&rsquo;t have this figured out. But I&rsquo;m pretty sure the answer isn&rsquo;t to pretend the genies aren&rsquo;t here, I have enough evidence that I get help from them, and it&rsquo;s definitely not to blindly let them do everything without thinking. So I&rsquo;m gonna keep poking at this, probably change my mind a few times, and see what sticks.</p>
<p>This is the first post in a series where I&rsquo;ll be figuring this out in public. How to decide what to learn deeply, how to tell when you&rsquo;re just parroting the genie, and how to keep judgment sharp when answers are cheap. Because your name is still on whatever you send out, genie or no genie.</p>
<p>We&rsquo;re all in our AI twenties right now, trying to figure out how to ride these <a href="/blog/2025/07/24/your-name-is-still-on-it/">motorcycles</a> without crashing. Might as well compare notes along the way.</p>]]></content:encoded></item><item><title>Drone CI handling of ${VAR} vs. $VAR</title><link>https://bjorn.now/til/2025-08-02-drone-ci-has-special-handling-for-var-and-will-replace-it-itself-instead-of-letting-the-shell-do-it/?utm_medium=feed</link><pubDate>Sat, 02 Aug 2025 09:37:29 +0800</pubDate><guid>https://bjorn.now/til/2025-08-02-drone-ci-has-special-handling-for-var-and-will-replace-it-itself-instead-of-letting-the-shell-do-it/</guid><description>Drone CI has special handling for ${VAR} and will replace it itself instead of letting the shell do it. So if you have export PATH=${GOPATH}/bin:$PATH and have $GOPATH defined in your global environment key, then it’ll become a blank value. But if you do export PATH=$GOPATH/bin:$PATH it’ll work. You can escape by adding an extra dollar sign, i.e. $${VAR} will be evaluated by the shell.
This is different from normal shell where ${VAR} is the same as $VAR and it exists to make sure you can concatenate strings without issues, i.e.</description><content:encoded><![CDATA[<p>Drone CI has special handling for <code>${VAR}</code> and will replace it <strong>itself</strong> instead of letting the shell do it. So if you have <code>export PATH=${GOPATH}/bin:$PATH</code> and have <code>$GOPATH</code> defined in your global environment key, then it&rsquo;ll become a blank value. But if you do <code>export PATH=$GOPATH/bin:$PATH</code> it&rsquo;ll work. You <a href="https://laszlo.cloud/drone-environment-variables-three-tips#when-variables-resolve-to-empty-string">can escape by adding an extra dollar sign</a>, i.e. <code>$${VAR}</code> will be evaluated by the shell.</p>
<p>This is different from normal shell where <code>${VAR}</code> is the same as <code>$VAR</code> and it exists to make sure you can concatenate strings without issues, i.e.</p>
<div class=" copy-enabled">
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl"><span class="nv">VAR</span><span class="o">=</span>m000
</span></span><span class="line"><span class="cl"><span class="nb">echo</span> <span class="s2">&#34;&#39;</span><span class="si">${</span><span class="nv">VAR</span><span class="si">}</span><span class="s2">est&#39;&#34;</span>  <span class="c1"># =&gt; &#39;m000est&#39;</span>
</span></span><span class="line"><span class="cl"><span class="nb">echo</span> <span class="s2">&#34;&#39;</span><span class="nv">$VARest</span><span class="s2">&#39;&#34;</span>    <span class="c1"># =&gt; &#39;&#39;</span></span></span></code></pre></div>
</div>
]]></content:encoded></item><item><title>Drone CI's DAG ignores skipped steps</title><link>https://bjorn.now/til/2025-08-02-drone-ci-will-not-smartly-skip-subsequent-steps-if-you-have-a-when-on-the-very-first-step-in-a-depends-on-chain/?utm_medium=feed</link><pubDate>Sat, 02 Aug 2025 09:37:29 +0800</pubDate><guid>https://bjorn.now/til/2025-08-02-drone-ci-will-not-smartly-skip-subsequent-steps-if-you-have-a-when-on-the-very-first-step-in-a-depends-on-chain/</guid><description>Drone CI will not smartly skip subsequent steps if you have a when on the very first step in a depends_on chain, so you have to repeat the when condition for each step because it doesn’t realize that the first dependency is gone.</description><content:encoded><![CDATA[<p>Drone CI will <strong>not</strong> smartly skip subsequent steps if you have a <code>when</code> on the very first step in a <code>depends_on</code> chain, so you have to repeat the <code>when</code> condition for each step because it doesn&rsquo;t realize that the first dependency is gone.</p>
]]></content:encoded></item><item><title>Drone CI's when targets the merge target for PR events</title><link>https://bjorn.now/til/2025-08-02-drone-cis-when-for-deciding-in-which-cases-to-run-steps-pipelines-targets-https/?utm_medium=feed</link><pubDate>Sat, 02 Aug 2025 09:37:29 +0800</pubDate><guid>https://bjorn.now/til/2025-08-02-drone-cis-when-for-deciding-in-which-cases-to-run-steps-pipelines-targets-https/</guid><description>Drone CI’s when for deciding in which cases to run steps/pipelines targets the merge target branch for PRs and not the actual PR’s, also it pulls from refs/pull/&lt;num&gt;/head instead of refs/heads/&lt;branch&gt; so you can’t target the branch itself using the pull_request event (use push on the pipeline and then branch for the step instead).</description><content:encoded><![CDATA[<p>Drone CI&rsquo;s <code>when</code> for deciding in which cases to run steps/pipelines <a href="https://docs.drone.io/pipeline/docker/syntax/conditions/#by-branch">targets</a> the <strong>merge target</strong> branch for PRs and not the actual PR&rsquo;s, also it pulls from <code>refs/pull/&lt;num&gt;/head</code> instead of <code>refs/heads/&lt;branch&gt;</code> so you can&rsquo;t target the branch itself using the <code>pull_request</code> event (use <code>push</code> on the pipeline and then <code>branch</code> for the step instead).</p>
]]></content:encoded></item><item><title>How "$@" vs $@ expands in shell</title><link>https://bjorn.now/til/2025-08-02-in-shell-scripts-if-you-do-it-will-actually-expand-correctly/?utm_medium=feed</link><pubDate>Sat, 02 Aug 2025 09:37:29 +0800</pubDate><guid>https://bjorn.now/til/2025-08-02-in-shell-scripts-if-you-do-it-will-actually-expand-correctly/</guid><description>In shell scripts if you do "$@" it will actually expand “quoted sentences” correctly, and if you just do $@ it will always unwrap them into single words, I thought that if you did "$@" it would combine all arguments into a single argument, and what it does is do what I thought $@ alone did.
I.e., with "$@" the arguments "one two" three will be 2 arguments, the first being "one two", and without it will become three arguments, all separated by space.</description><content:encoded><![CDATA[<p>In shell scripts if you do <code>&quot;$@&quot;</code> it will actually expand &ldquo;quoted sentences&rdquo; correctly, and if you just do <code>$@</code> it will <strong>always</strong> unwrap them into single words, I thought that if you did <code>&quot;$@&quot;</code> it would combine all arguments into a single argument, and what it does is do what I thought <code>$@</code> alone did.</p>
<p>I.e., with <code>&quot;$@&quot;</code> the arguments <code>&quot;one two&quot; three</code> will be 2 arguments, the first being <code>&quot;one two&quot;</code>, and without it will become three arguments, all separated by space.</p>
]]></content:encoded></item><item><title>Link rel="canonical" is like a correlation ID</title><link>https://bjorn.now/til/2025-08-02-the-link-rel-href-is-a-bit-like-a-correlation-id-between-services-and-i-need-also-to-have-it-on-my-canonical-page/?utm_medium=feed</link><pubDate>Sat, 02 Aug 2025 09:37:29 +0800</pubDate><guid>https://bjorn.now/til/2025-08-02-the-link-rel-href-is-a-bit-like-a-correlation-id-between-services-and-i-need-also-to-have-it-on-my-canonical-page/</guid><description>The &lt;link rel="canonical" href="…"&gt; is a bit like a correlation ID between services and I need also to have it on my canonical page, because if someone sends it with ?utm_source=foo it could be a different page than the one without the query string.</description><content:encoded><![CDATA[<p>The <code>&lt;link rel=&quot;canonical&quot; href=&quot;…&quot;&gt;</code> is a bit like a correlation ID between services and I need also to have it on my canonical page, because if someone sends it with <code>?utm_source=foo</code> it <strong>could</strong> be a different page than the one without the query string.</p>
]]></content:encoded></item><item><title>Pythong doesn't require typing for dict and list</title><link>https://bjorn.now/til/2025-08-02-python-doesnt-require-typing/?utm_medium=feed</link><pubDate>Sat, 02 Aug 2025 09:37:29 +0800</pubDate><guid>https://bjorn.now/til/2025-08-02-python-doesnt-require-typing/</guid><description>Python doesn’t require typing.Dict and typing.List for those types since Python 3.9, you can just do dict[] and list[].</description><content:encoded><![CDATA[<p>Python doesn&rsquo;t require <code>typing.Dict</code> and <code>typing.List</code> for those types <a href="https://mypy.readthedocs.io/en/stable/builtin_types.html#generic-types">since Python 3.9</a>, you can just do <code>dict[]</code> and <code>list[]</code>.</p>
]]></content:encoded></item><item><title>Scripts/commands in a shell script inherits access to STDIN</title><link>https://bjorn.now/til/2025-08-02-the-scripts-inside-a-shell-script-inherit-access-to-stdin-when-you-call-them/?utm_medium=feed</link><pubDate>Sat, 02 Aug 2025 09:37:29 +0800</pubDate><guid>https://bjorn.now/til/2025-08-02-the-scripts-inside-a-shell-script-inherit-access-to-stdin-when-you-call-them/</guid><description>The scripts/commands inside a shell script inherit access to STDIN when you call them, so if you have a shells script that only has cat and you do ./script.sh &lt; script.sh then it’ll output the content of itself</description><content:encoded><![CDATA[<p>The scripts/commands inside a shell script inherit access to STDIN when you call them, so if you have a shells script that only has <code>cat</code> and you do <code>./script.sh &lt; script.sh</code> then it&rsquo;ll output the content of itself</p>
]]></content:encoded></item><item><title>You can view the webhooks that GitHub sends in your project settings</title><link>https://bjorn.now/til/2025-08-02-you-can-view-the-webhooks-that-github-sends-in-your-project-settings/?utm_medium=feed</link><pubDate>Sat, 02 Aug 2025 09:37:29 +0800</pubDate><guid>https://bjorn.now/til/2025-08-02-you-can-view-the-webhooks-that-github-sends-in-your-project-settings/</guid><description>You can view the webhooks that GitHub sends in your project settings, so for example Drone CI getting notified and exactly what information that goes into the payload, and when/if it was sent, is available there.
Go to Settings -&gt; Webhooks and then click on the integration you want to look at.</description><content:encoded><![CDATA[<p>You can view the webhooks that GitHub sends in your project settings, so for example Drone CI getting notified and exactly what information that goes into the payload, and when/if it was sent, is available there.</p>
<p>Go to <code>Settings</code> -&gt; <code>Webhooks</code> and then click on the integration you want to look at.</p>
]]></content:encoded></item><item><title>Enabling macOS fingerprint reader for sudo</title><link>https://bjorn.now/til/2025-08-01-macos-fingerprint-reader-sudo/?utm_medium=feed</link><pubDate>Fri, 01 Aug 2025 23:15:00 +0800</pubDate><guid>https://bjorn.now/til/2025-08-01-macos-fingerprint-reader-sudo/</guid><description>You can enable the fingerprint reader for sudo on macOS, and pressing my finger on a button beats having to type the password, steps:
cp /etc/pam.d/sudo_local{.template,} Edit /etc/pam.d/sudo_local and uncomment auth sufficient pam_tid.so. The reason for doing this in sudo_local is that this file will not get reset with system changes from Apple.</description><content:encoded><![CDATA[<p>You can enable the fingerprint reader for sudo on macOS,
and pressing my finger on a button beats having to type the password, steps:</p>
<ol>
<li><code>cp /etc/pam.d/sudo_local{.template,}</code></li>
<li>Edit <code>/etc/pam.d/sudo_local</code> and uncomment <code>auth       sufficient     pam_tid.so</code>.</li>
</ol>
<p>The reason for doing this in <code>sudo_local</code> is that this file will not get reset with system changes from Apple.</p>
]]></content:encoded></item><item><title>More frequent, smaller posts</title><link>https://bjorn.now/blog/2025/08/01/more-frequent-smaller-posts/?utm_medium=feed</link><pubDate>Fri, 01 Aug 2025 23:10:02 +0800</pubDate><guid>https://bjorn.now/blog/2025/08/01/more-frequent-smaller-posts/</guid><description>I’m trying something new here. Instead of waiting for fully-formed thoughts, I’m going to share more as I go: quick things I’ve learned, half-baked ideas, useful links. I’ve added some structure to keep things findable, but the main goal is just to share more often.
The main feed will give you everything I post, if you only want certain types of posts, I’ve also made section-specific feeds. The blog feed is what you’ve been getting if you were already subscribed.</description><content:encoded><![CDATA[<p>I&rsquo;m trying something new here.
Instead of waiting for fully-formed thoughts,
I&rsquo;m going to share more as I go: quick things I&rsquo;ve learned, half-baked ideas, useful links.
I&rsquo;ve added <a href="/about/#how-im-organizing-things-here">some structure</a> to keep things findable, but the main goal is just to share more often.</p>
<p>The main feed will give you everything I post,
if you only want certain types of posts, I&rsquo;ve also made section-specific feeds.
The blog feed is what you&rsquo;ve been getting if you were already subscribed.</p>
<ul>
<li><a href="/feed.xml">Main feed</a></li>
<li><a href="/blog/feed.xml">Blog feed</a></li>
</ul>
]]></content:encoded></item><item><title>Your name is still on it — learning to ride the AI motorcycle without crashing</title><link>https://bjorn.now/blog/2025/07/24/your-name-is-still-on-it/?utm_medium=feed</link><pubDate>Thu, 24 Jul 2025 23:00:00 +0800</pubDate><guid>https://bjorn.now/blog/2025/07/24/your-name-is-still-on-it/</guid><description>A colleague recently said something that’s been rattling around in my head: “AI gives you speed, but it doesn’t give you direction.” And the more I use these tools, the more I think that undersells the danger.
I have been wondering how to think about AIs (or genies) and how computers are like bicycles for the mind, as Steve Jobs put it, and I think these tools take it further. They are more like motorcycles for the mind. They go really fast, and you better not treat them like a bike, because you need to know what you’re doing. How to handle that thing. You need to make sure you don’t try to go too fast too soon, or for too long, because you’ll get speed blind and… things will happen.</description><content:encoded><![CDATA[<p>A colleague recently said something that&rsquo;s been rattling around in my head: &ldquo;AI gives you speed, but it doesn&rsquo;t give you direction.&rdquo; And the more I use these tools, the more I think that undersells the danger.</p>
<p>I have been wondering how to think about AIs (or <a href="https://newsletter.pragmaticengineer.com/p/tdd-ai-agents-and-coding-with-kent">genies</a>) and how <a href="https://medium.learningbyshipping.com/bicycle-121262546097">computers are like bicycles for the mind</a>, as Steve Jobs put it, and I think these tools take it further. They are more like motorcycles for the mind. They go <em>really</em> fast, and you better not treat them like a bike, because you need to know what you&rsquo;re doing. How to handle that thing. You need to make sure you don&rsquo;t try to go too fast too soon, or for too long, because you&rsquo;ll get speed blind and… things will happen.</p>
<p>I&rsquo;m pretty sure motorcycle insurance costs way more when you&rsquo;re in your 20s than in your 40s or 50s. There&rsquo;s a reason for that. People who are older generally know how to ride the thing, and they&rsquo;re not as wild when doing it.</p>
<p>But I think when it comes to using AI we&rsquo;re all kind of in our 20s because we&rsquo;re figuring it out.</p>
<h2 id="its-my-name-on-whatever-goes-out">
  It&rsquo;s my name on whatever goes out
  <a href="#its-my-name-on-whatever-goes-out" class="heading-anchor" aria-label="Direct link to It&rsquo;s my name on whatever goes out"><span aria-hidden="true">#</span></a>
</h2>
<p>I was at a wedding recently, talking to this lawyer, 50s, 60s maybe, working at a multinational. He&rsquo;s been playing around with AI and feels it&rsquo;s giving him an enormous productivity boost for writing and doing research for him.</p>
<p>As we were talking about how he was using it he said something that stopped me cold: <em>&ldquo;It&rsquo;s my name on whatever goes out.&rdquo;</em></p>
<p>He&rsquo;s already caught the AI making things up. He has to check every line, every reference. He explicitly asks it to give him the references so he can look them up himself. And if I understood correctly, this isn&rsquo;t significantly different from when juniors do the work for him. Because it was still his name on it. He needs to validate it. Whatever he puts his name on and sends out has to be right.</p>
<p>And that&rsquo;s when something clicked for me. <a href="/blog/2025/07/12/which-hat-are-you-wearing/">Remember how I was talking about the different hats we wear?</a> Well, you <em>can</em> outsource wearing the hat. You <em>can</em> have the genie do the work. But it&rsquo;s still your name on it. No one else cares if you outsourced it, they only see your name on it. You can&rsquo;t blame the genie for a shit job. It was your name.</p>
<h2 id="what-we-think-were-automating">
  What we think we&rsquo;re automating
  <a href="#what-we-think-were-automating" class="heading-anchor" aria-label="Direct link to What we think we&rsquo;re automating"><span aria-hidden="true">#</span></a>
</h2>
<p>I keep hearing engineers say, &ldquo;<em>oh, I don&rsquo;t have to do this boring thing anymore!</em>&rdquo;</p>
<p>Meeting notes are a perfect example. I have access to Google Gemini at work, and people just let Gemini take meeting notes and send them out without reading them. Really. &ldquo;<em>Huh?</em>&rdquo; I hear some of you say now, that&rsquo;s what it&rsquo;s great for, the notes are <a href="https://sre.google/sre-book/eliminating-toil/">just toil</a>. Well, no, what are meeting notes actually for? They&rsquo;re about making sure we&rsquo;re on the same page, that we have the same agreements and understanding. If you let the machine mindlessly capture words and don&rsquo;t check that it actually documented the meaning behind what we said and understood, you&rsquo;re failing at the actual job of <a href="/blog/2025/05/03/effective-meetings/">running that meeting.</a></p>
<p>This reminds me of <a href="https://fs.blog/chestertons-fence/">Chesterton&rsquo;s Fence</a>, that principle about not removing a fence until you understand why it was put there. Before you automate away some &ldquo;boring, pointless thing,&rdquo; maybe ask yourself: why is it that I&rsquo;m doing this in the first place? Because if you don&rsquo;t understand why it&rsquo;s done, how do you know the automation did it well?</p>
<p>And I wonder if what&rsquo;s really happening is that we&rsquo;re not automating the work, we&rsquo;re automating away our attention to the work. We&rsquo;re not automating the output. We&rsquo;re automating away our understanding of what that output should be. And <a href="https://www.benkuhn.net/impact/">experienced people are the ones who spot strange patterns (or have taste)</a> precisely because they&rsquo;ve stayed engaged with the work over time. You can&rsquo;t develop that instinct if you&rsquo;re not looking.</p>
<h2 id="the-work-worth-doing">
  The work worth doing
  <a href="#the-work-worth-doing" class="heading-anchor" aria-label="Direct link to The work worth doing"><span aria-hidden="true">#</span></a>
</h2>
<p>Which brings me to <a href="https://tidyfirst.substack.com/p/90-of-my-skills-are-now-worth-0">something eyebrow raising</a> that Kent Beck said: with AI, 90% of his skills became worthless, but the leverage of the remaining 10% is now worth 1000x. The question is: which 10%? This is the &ldquo;<em>knowing where to put the X</em>&rdquo; skill, <a href="/blog/2025/07/12/which-hat-are-you-wearing/#the-9999-skill">that judgment call and experience</a> you bring: that&rsquo;s what makes the genie do what&rsquo;s needed.</p>
<p>When you let the genie write code for you and it introduces bugs that you would have caught if you&rsquo;d actually read the code… you&rsquo;re not doing your job as a product engineer. Yeah, you&rsquo;re slinging more code and doing it faster. But it&rsquo;s not about the code, it&rsquo;s about delivering the solutions we need when we need them.</p>
<p>There&rsquo;s a difference between <em>delegating execution</em> and <em>delegating responsibility</em>. You <em>can</em> have the genie execute tasks, but you <em>can&rsquo;t</em> delegate the responsibility for the outcome. That&rsquo;s still yours. <a href="/blog/2025/07/12/which-hat-are-you-wearing/">The hat</a> is still on your head, even if AI is helping you get things done faster while wearing it.</p>
<p>And I find pride in doing the work well. Even the boring bits. Sometimes we might not enjoy the work, but doing things well will make most things feel worthwhile. Because there&rsquo;s nuance and understanding you can only get by truly groking something, and that&rsquo;s when you can safely change and improve it.</p>
<h2 id="finding-your-balance">
  Finding your balance
  <a href="#finding-your-balance" class="heading-anchor" aria-label="Direct link to Finding your balance"><span aria-hidden="true">#</span></a>
</h2>
<p>The question isn&rsquo;t &ldquo;can the genie do this task?&rdquo; It&rsquo;s &ldquo;when it does this task and puts my name on it, am I comfortable with what might happen if it gets it really, really wrong?&rdquo;</p>
<p>In my experience, the engineers who are using AI most effectively are the ones who treat it like that lawyer treats it: as a powerful tool that still requires their full professional judgment. They understand they&rsquo;re being paid for the judgment part of the work, not just the execution part. The genie can help with execution, it can draw lots of Xs quickly. But knowing where to draw them? That&rsquo;s all you.</p>
<p>Right now, these systems can&rsquo;t truly understand context the way a senior does. They can&rsquo;t make actual judgment calls, they&rsquo;re pattern matching (even if, admittedly, <a href="https://en.wikipedia.org/wiki/Clarke%27s_three_laws">slightly magical</a> in how they do it). They give us speed, not judgment or direction. Output, not understanding.</p>
<p>I&rsquo;m still working out what learning looks like when AI can show you all the moves. My cooking has genuinely leveled up with an AI coach: I&rsquo;m making dishes I never could before, and understanding why certain techniques work. But I know this is like training wheels. I couldn&rsquo;t fully do this alone yet, and that&rsquo;s the thing: I&rsquo;m not mistaking this <em>speed</em> for actual <em>skill</em>, not confusing making <em>progress</em> with real <em>proficiency</em>.</p>
<p>So here I am, learning to ride this thing safely, trying to get out of my AI twenties you might say, before I take it on the highway.</p>
<p>Because whether you&rsquo;re wearing your engineering hat, your architect hat, or your team lead hat, your name is still on it. And I&rsquo;d like to make sure I am not only wearing the helmet but also know how to ride this motorcycle before I really open up the throttle.</p>
]]></content:encoded></item><item><title>Which hat are you wearing? ...you wouldn't wear a beanie to the beach</title><link>https://bjorn.now/blog/2025/07/12/which-hat-are-you-wearing/?utm_medium=feed</link><pubDate>Sat, 12 Jul 2025 15:06:00 +0800</pubDate><guid>https://bjorn.now/blog/2025/07/12/which-hat-are-you-wearing/</guid><description>I was in an incident review recently where one of the problems was a human going too fast. This process is very manual, repetitive, and boring, and it rarely fails, so we skip some steps. That mostly works (see Why Do Things Go right?), except when it doesn’t. Ripe for occasional issues and likely in need of automation.
I believe we often skip steps because we don’t know why we do them. It’s not tedious for tediousness sake, it’s often there because it’s important. And until we have the automation, or maybe we’ve intentionally chosen not to automate it, we need to find some way of helping the human ’live the situation.'</description><content:encoded><![CDATA[<p>I was in an incident review recently where one of the problems was a human going too fast. This process is very manual, repetitive, and boring, and it rarely fails, so we skip some steps. That mostly works (see <a href="https://safetydifferently.com/why-do-things-go-right/">Why Do Things Go right?</a>), except when it doesn&rsquo;t. Ripe for occasional issues and likely in need of automation.</p>
<p>I believe we often skip steps because we don&rsquo;t know <em>why</em> we do them. It&rsquo;s not tedious for tediousness sake, it&rsquo;s <a href="https://fs.blog/chestertons-fence/">often there because it&rsquo;s important</a>. And until we have the automation, or maybe we&rsquo;ve intentionally chosen not to automate it, we need to find some way of helping the human &rsquo;live the situation.'</p>
<p>Which reminded me of this Terry Pratchett quote: &ldquo;Every trade, every craft had its hat. That&rsquo;s why kings had hats. Take the crown off a king and all you had was someone good at having a weak chin and waving to people. Hats had power. Hats were important.&rdquo;</p>
<p>And there&rsquo;s something to this, right? Like in my own role as a principal engineer, I am definitely <em>seen</em> as my role more often than I am the person beneath it. When people see me in a meeting, they see the title first. Hopefully I provide valuable insights and useful things. But even if I provide absolute garbage input, there&rsquo;s a significant chance people will listen, and maybe even not push back, because of the hat.</p>
<p>But, I have been wondering: what is your role and what is your ownership? What is it that you individually provide to whatever work you&rsquo;re doing? And is it always the same hat?</p>
<h2 id="the-hat-you-dont-know-youre-wearing">
  The hat you don&rsquo;t know you&rsquo;re wearing
  <a href="#the-hat-you-dont-know-youre-wearing" class="heading-anchor" aria-label="Direct link to The hat you don&rsquo;t know you&rsquo;re wearing"><span aria-hidden="true">#</span></a>
</h2>
<p>I think your hat changes depending on the role you&rsquo;re fulfilling at the moment. When you&rsquo;re working as a software engineer, you wear an engineering hat where you think about the feasibility of what you&rsquo;re building, like, can I maintain this thing long term, how far will it scale, and how testable is this design?</p>
<p>But if you&rsquo;re working as a product engineer, you&rsquo;re probably first thinking about how do I get this damn thing out the door, so you can see if it works or not. It&rsquo;s about trying things, slapping it together, flinging it against the wall with the smallest responsible amount of tests (so you know you don&rsquo;t break existing things when you iterate), and see what sticks.</p>
<p>I&rsquo;ve noticed, though, that we often forget to switch hats when we need to.</p>
<p>Take deploying to production. When we&rsquo;re about to take something live, we should be wearing a different hat. We need to embody the spirit of production itself, become its guardian of stability (yeah, yeah, too many roleplaying games). Some orgs have a &ldquo;release manager&rdquo; as a dedicated role, but even if it&rsquo;s not a dedicated role, the need is always there. Taking things live is one of the most dangerous things we do: because most incidents happen when we change something. Whether it&rsquo;s config, a flag, or code.</p>
<p>The product engineer hat and the production guardian hat often feel very similar, and that&rsquo;s exactly the problem. Just because a hat <em>is similar</em> doesn&rsquo;t mean <em>it&rsquo;s the same.</em> The product engineer wants the experiment live yesterday. The production guardian knows we can only ship today, and wants to make sure we still have a tomorrow. It&rsquo;s this impossible tension, one person wanting to have already finished something we haven&rsquo;t even started, the other pumping the brakes to make sure we don&rsquo;t break what&rsquo;s already working. And that often leads to &ldquo;safety theater,&rdquo; where we do it a little bit slower, say 30m to get everywhere instead of one quick change, to catch disasters but not slow burn issues. Obviously this tension changes, to use <a href="https://medium.com/@kentbeck_7670/the-product-development-triathlon-6464e2763c46">Kent Beck&rsquo;s 3X model</a>, depending on whether you&rsquo;re in eXplore, eXpand, or eXtract mode. We&rsquo;re mostly in eXtract, so breaking things affects a lot of customers and the production guardian hat needs to be especially strong.</p>
<p>For example, in another incident, someone didn&rsquo;t manually check dashboards after deploying, which was part of the process, because they trusted the automated alerts. The alert came in after 10 minutes when it had enough data to be sure it wasn&rsquo;t temporary. But a human who was actually monitoring, really living that production guardian role, would&rsquo;ve realized something was wrong within a minute or two. The manual step was there because the team knew their automated alerts weren&rsquo;t good enough to handle all issues around deployments.</p>
<p>Where I work today, we have some 250 backend components. We can test all we want, black box integration tests, contract tests, end-to-end tests, manual QA, all of it. We&rsquo;re still going to miss and get stuff wrong sometimes.</p>
<p>The point and our challenge in mindset is: when we&rsquo;re taking things into production, we&rsquo;re not wearing the hat of the product engineer anymore. We&rsquo;re taking on the role of production&rsquo;s guardian, and we&rsquo;re shepherding changes safely into production.</p>
<h2 id="its-like-vim-modes-but-for-work">
  It&rsquo;s like Vim modes, but for work
  <a href="#its-like-vim-modes-but-for-work" class="heading-anchor" aria-label="Direct link to It&rsquo;s like Vim modes, but for work"><span aria-hidden="true">#</span></a>
</h2>
<p>Say whatever you want about Vim, but one really nice thing is that it&rsquo;s modal. The idea that I&rsquo;m now doing this thing (inserting or editing text), and now I&rsquo;m doing this other thing (normal or moving and transforming), and you behave differently in those modes.</p>
<p>I see this in my own work all the time. I regularly review my own PRs because even though I went through everything when writing my commit message, I invariably look at it differently when I&rsquo;m on GitHub in the interface I use for reviews. I switch much better into reviewer mode when I&rsquo;m leaving comments there. It&rsquo;s common that I review and leave myself comments for what to fix, discover I need a comment, or elaborate a little bit further on something the reviewer might not be familiar with.</p>
<p>It&rsquo;s like TDD in a way. When you&rsquo;re red, you rush towards green by just getting it to work in the simplest way you humanly can. Then, when it&rsquo;s green, when you got it working, you refactor it. Different modes, different goals.</p>
<p>We need to be more intentional about which hat we&rsquo;re wearing at the moment.</p>
<h2 id="becoming-the-character">
  Becoming the character
  <a href="#becoming-the-character" class="heading-anchor" aria-label="Direct link to Becoming the character"><span aria-hidden="true">#</span></a>
</h2>
<p>I saw this story about Christian Bale recently (and look, I can&rsquo;t find the original source, so this might be one of those stories that&rsquo;s too good to fact-check, but it perfectly captures what I&rsquo;m talking about). He&rsquo;s a famous <a href="https://en.wikipedia.org/wiki/Method_acting">method actor</a>, someone who becomes their characters completely. Apparently, when he wasn&rsquo;t getting much work, he dreaded having to read through his insurance contract and have that conversation because it was boring.</p>
<p>But then he decided: if I were a character who really cared about understanding insurance thoroughly, who would want to have an informed conversations about it&hellip; So he became that character and called up his insurance company. Apparently, they&rsquo;d never talked to someone so well-researched on their contract.</p>
<p>And that&rsquo;s the thing, we kind of need to do this throughout our day. When you&rsquo;re taking something live, you&rsquo;re not the &ldquo;ship fast&rdquo; product engineer anymore. You have the right to say no, the right to say this has to be done slower because of these risks. Your responsibility is to make sure production is stable and things will continue to work.</p>
<h2 id="the-9999-skill">
  The $9,999 skill
  <a href="#the-9999-skill" class="heading-anchor" aria-label="Direct link to The $9,999 skill"><span aria-hidden="true">#</span></a>
</h2>
<p>There&rsquo;s this <a href="https://www.smithsonianmag.com/history/charles-proteus-steinmetz-the-wizard-of-schenectady-51912022/">great story about Charles Steinmetz</a> and Henry Ford. Ford&rsquo;s engineers couldn&rsquo;t fix a generator problem, so they called in Steinmetz. He spent two days just listening to the generator and scribbling calculations. Then he climbed up, made a chalk mark, and told them to replace sixteen windings at that exact spot. It worked perfectly.</p>
<p>When Ford got the $10,000 invoice, he asked for an itemized bill. Steinmetz sent back:</p>
<ul>
<li>Making chalk mark: $1</li>
<li>Knowing where to make mark: $9,999</li>
</ul>
<p>And think about what Steinmetz did: he could have torn the whole generator apart, called in a team of engineers, or tried a dozen different approaches. Instead, he recognized this was a moment for careful analysis. He spent time listening, thinking, calculating. He put on his expert diagnostician hat, not his collaborative problem-solver hat or his hands-on mechanic hat.</p>
<p>And this is it exactly. Knowing where to make the mark, <strong>knowing which hat to wear when, that&rsquo;s the $9,999 skill.</strong></p>
<p>But it&rsquo;s not just about perspective: each hat comes with different expectations, different powers, different rights, and different responsibilities. The production guardian has the authority to halt deployments. The incident responder can stop all feature work immediately. The planning hat carries the responsibility to think long-term even when it slows things down. When you put on a hat, you&rsquo;re not only changing how you think and approach the situation: you&rsquo;re claiming the legitimate authority that comes with that role.</p>
<p>I think that we often fail not because we lack skills, but because we&rsquo;re wearing the wrong hat for the moment. Or worse, we&rsquo;re trying to wear all the hats at the same time.</p>
<p>The product engineer hat wants speed. The production guardian hat wants stability. The incident responder hat wants recovery. The planner hat wants predictability. These aren&rsquo;t compatible mindsets, you can&rsquo;t optimize for all of them simultaneously. The goal is to negotiate productively across them, which is hard as one person, and, this is also why working through problems with others is so powerful.</p>
<div class="no-copy-button">
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">THE INCOMPATIBLE MINDSETS
</span></span><span class="line"><span class="cl">Product Engineer  
</span></span><span class="line"><span class="cl">  → Wants speed (ship it yesterday)
</span></span><span class="line"><span class="cl">Production Guardian
</span></span><span class="line"><span class="cl">  → Wants stability (safety today, here tomorrow)
</span></span><span class="line"><span class="cl">Incident Responder
</span></span><span class="line"><span class="cl">  → Wants recovery (fix it now)
</span></span><span class="line"><span class="cl">Planner           
</span></span><span class="line"><span class="cl">  → Wants predictability (sustainable pace)</span></span></code></pre></div>
</div>
<p>So here&rsquo;s what I&rsquo;ve been trying to do, or rather, what I should start doing: be explicit about which hat I&rsquo;m wearing. Right now I say things like &ldquo;okay, from a stability POV&rdquo; before raising concerns about deployment risks. But I should actually say, &ldquo;let me put on my <strong>production guardian hat</strong> for a second.&rdquo; It helps others understand which perspective I&rsquo;m bringing, and it helps me focus on what matters for that role.</p>
<h2 id="tying-it-on">
  Tying it on
  <a href="#tying-it-on" class="heading-anchor" aria-label="Direct link to Tying it on"><span aria-hidden="true">#</span></a>
</h2>
<p>I think there&rsquo;s more to the hats we wear throughout the day because it&rsquo;s not just at work: supporting partner, fun uncle, mentor, learner. Each comes with different expectations, powers, and responsibilities.</p>
<p>But for now, I&rsquo;ll leave you with this: What hat are you wearing right now? What are the expectations that come with it? What responsibilities? And more importantly, is it the right hat for what you&rsquo;re trying to do?</p>
<p>Because understanding when to switch might be one of the most underrated skills we need. I&rsquo;m currently convinced that being intentional about our roles, knowing which hat we&rsquo;re wearing and when to change it, that makes a difference. Not between being good or great, but between solving the right problems and creating new ones.</p>
<p>What would change my mind is if someone showed me that this context-switching has too much overhead. But, I think these aren&rsquo;t the context switches we usually complain about, the ones that kill our flow when we jump between meetings and deep work. These switches are within the same context we&rsquo;re already working in. We&rsquo;re just tilting our perspective, changing how we evaluate the situation. And in my experience, the cost of wearing the wrong hat is far higher than the cost of being intentional about switching.</p>
]]></content:encoded></item><item><title>Working with Go's test cache on CI — be fast by avoiding work, while doing the important work</title><link>https://bjorn.now/blog/2025/07/07/working-with-gos-test-cache-on-ci/?utm_medium=feed</link><pubDate>Mon, 07 Jul 2025 21:50:00 +0800</pubDate><guid>https://bjorn.now/blog/2025/07/07/working-with-gos-test-cache-on-ci/</guid><description>I was trying to speed up our slow CI by caching Go builds. The easy win was caching Go’s module downloads (via GOPATH), but when I added GOCACHE for the build cache, I got a pleasant surprise: the tests were caching too. 🥳
I shared the change for review, and a colleague raised a great point: “What about our black box integration tests?” These tests hit APIs and external services that Go can’t track as dependencies. If they cache when they shouldn’t, we might miss real failures: the tests would pass because they didn’t re-run, not because the code actually works.</description><content:encoded><![CDATA[<p>I was trying to speed up our slow CI by caching Go builds. The easy win was caching Go&rsquo;s module downloads (via <code>GOPATH</code>), but when I added <code>GOCACHE</code> for the build cache, I got a pleasant surprise: the tests were caching too. 🥳</p>
<p>I shared the change for review, and a colleague raised a great point: &ldquo;What about our black box integration tests?&rdquo; These tests hit APIs and external services that Go can&rsquo;t track as dependencies. If they cache when they shouldn&rsquo;t, we might miss real failures: the tests would pass because they didn&rsquo;t re-run, not because the code actually works.</p>
<details>
<summary><strong>TL;DR:</strong> I found three ways to handle Go test caching on CI, they are:</summary>
<table>
  <thead>
      <tr>
          <th>Approach</th>
          <th>Changes Needed</th>
          <th>Test Caching</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>go clean -testcache</code></td>
          <td>CI config only</td>
          <td>❌ None</td>
      </tr>
      <tr>
          <td><code>-count=1</code></td>
          <td>Every test invocation</td>
          <td>❌ None</td>
      </tr>
      <tr>
          <td>Environment variables</td>
          <td>Shared code/library once</td>
          <td>✅ Full</td>
      </tr>
  </tbody>
</table>
<p><strong><code>go clean -testcache</code></strong> — wipe all test cache before running. No code changes, no test caching.</p>
<p><strong><code>-count=1</code></strong> — skip cache for a specific <code>go test</code> invocation. Need to add it everywhere.</p>
<p><strong>Environment variables</strong> — read your CI&rsquo;s commit SHA (e.g. <code>GITHUB_SHA</code>, <code>DRONE_COMMIT_SHA</code>) in your test setup. Go auto-invalidates the package&rsquo;s test cache when the value changes. Unit tests still cache, integration tests re-run on new commits as long as you read the variable once in the package. A helper to ensure it doesn&rsquo;t cache:</p>
<div class=" copy-enabled">
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kd">func</span><span class="w"> </span><span class="nf">IsIntegrationTest</span><span class="p">(</span><span class="nx">t</span><span class="w"> </span><span class="o">*</span><span class="nx">testing</span><span class="p">.</span><span class="nx">T</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nx">t</span><span class="p">.</span><span class="nf">Helper</span><span class="p">()</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nx">_</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="nx">os</span><span class="p">.</span><span class="nf">Getenv</span><span class="p">(</span><span class="s">&#34;GITHUB_SHA&#34;</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="p">}</span></span></span></code></pre></div>
</div>
<p>Key behavior:</p>
<ul>
<li>Invalidation is <strong>per-package</strong>, not per-test</li>
<li>Reading env vars through library calls still triggers invalidation</li>
<li><code>go test</code> with no package target never caches</li>
<li>Debug caching decisions with <code>GODEBUG=gocachetest=1</code></li>
</ul>
</details>
<h2 id="the-fundamental-problem">
  The Fundamental Problem
  <a href="#the-fundamental-problem" class="heading-anchor" aria-label="Direct link to The Fundamental Problem"><span aria-hidden="true">#</span></a>
</h2>
<p>When my colleague warned me about integration test caching, I realized we had a bigger problem than just my PR. How could I fix this across our entire org? We&rsquo;ve got ~250 repos, and asking every team to remember special flags or change their test execution wasn&rsquo;t going to fly. I needed something that would just work everywhere with minimal changes.</p>
<p>The fundamental issue: our black box integration tests don&rsquo;t &ldquo;declare their dependencies&rdquo; to the Go compiler, so it doesn&rsquo;t know that something the test is critically dependent upon has changed and requires a rerun (and practically, neither do we really), because they test network services and external binaries that Go has no visibility into. I always have to run these tests.</p>
<p>I wanted the caching benefits without the risk, ideally with minimal changes since we have lots of codebases that could benefit from this caching improvement. Since our black box integration tests use a shared internal library, I was hoping to find a solution I could implement once there rather than asking teams to modify individual tests or change how they execute them.</p>
<h2 id="so-how-do-i-make-this-work">
  So&hellip; How Do I Make This Work?
  <a href="#so-how-do-i-make-this-work" class="heading-anchor" aria-label="Direct link to So&hellip; How Do I Make This Work?"><span aria-hidden="true">#</span></a>
</h2>
<p>I started by looking for the obvious solutions, there&rsquo;s probably a flag to ignore the test cache, right? And there were two straightforward options: <code>go clean -testcache</code> and <code>-count=1</code>. But here&rsquo;s the thing, both of them throw away ALL test caching, which felt like using a sledgehammer when I needed a scalpel.</p>
<p>Then I remembered reading something about environment variables, and files, affecting test caching. I went into <code>go help test</code> and I remembered right, tests that read environment variables get invalidated when those variables change, so since this is about CI, and I know that CI systems give us unique commit SHAs as env vars… and we have a shared library to help write black box integration tests… so I can make the fix once in that library and everyone gets this benefit!</p>
<p>Alright, that gives me three options, how do they compare?</p>
<p><strong>Simple Option: <code>go clean -testcache</code></strong></p>
<ul>
<li>Pro: Dead simple - add one line to your CI config, no test changes needed</li>
<li>Con: Zero caching benefit for tests</li>
<li>When to use: Safest solution that still gives you build benefits without changing scripts or tests</li>
</ul>
<p>This works by creating <a href="https://github.com/golang/go/blob/665af869920432879629c1d64cf59f129942dcd6/src/cmd/go/internal/test/test.go#L844-L848">a file with the current Unix time in nanoseconds</a> and then, whenever Go runs tests, it checks if the cached test is newer than that timestamp. I appreciate the simplicity: the cache cleaner doesn&rsquo;t need to understand which cached items are tests (and there are only hashes in that folder), it just sets a &ldquo;tests are invalid after this moment&rdquo; marker and the code skips it or not.</p>
<p><strong>Explicit Option: <code>-count=1</code></strong></p>
<ul>
<li>Pro: Granular control per <code>go test</code> invocation</li>
<li>Con: Need to change test execution and remember it everywhere, easy to miss (especially in monorepos with many go.mods)</li>
<li>When to use: Only some tests need cache invalidation</li>
</ul>
<p>This is the one you&rsquo;ll see when people ask how to make Go not cache tests, for our situation not super useful, but mentioning it for completeness.</p>
<p><strong>Elegant Option: Environment Variables</strong></p>
<ul>
<li>Pro: Automatic cache invalidation on CI when needed, maximum caching benefit when possible</li>
<li>Con: Requires thoughtful test design</li>
<li>When to use: You have enough tests that the speedup matters and you&rsquo;re willing to be intentional about cache invalidation</li>
</ul>
<p>Or laid out visually:</p>
<table>
  <thead>
      <tr>
          <th>Approach</th>
          <th>Changes Needed</th>
          <th>Test Caching</th>
          <th>Safety</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>go clean -testcache</code></td>
          <td>CI config only</td>
          <td>❌ None</td>
          <td>✅ Safest</td>
      </tr>
      <tr>
          <td><code>-count=1</code></td>
          <td>Every test invocation</td>
          <td>❌ None</td>
          <td>✅ Safe</td>
      </tr>
      <tr>
          <td>Environment variables</td>
          <td>Shared code/library once</td>
          <td>✅ Full</td>
          <td>✅ Safe*</td>
      </tr>
  </tbody>
</table>
<p>*<em>When implemented correctly</em></p>
<h3 id="how-the-environment-variable-approach-works">
  How the Environment Variable Approach Works
  <a href="#how-the-environment-variable-approach-works" class="heading-anchor" aria-label="Direct link to How the Environment Variable Approach Works"><span aria-hidden="true">#</span></a>
</h3>
<p>This works because Go invalidates the entire package&rsquo;s test cache whenever an environment variable it depends on changes, and most CI systems expose a unique commit SHA as an environment variable. For example,</p>
<ul>
<li>GitHub Actions: <code>GITHUB_SHA</code></li>
<li>Drone CI: <code>DRONE_COMMIT_SHA</code></li>
</ul>
<p>When your integration test reads this variable, Go automatically invalidates the cache for that package whenever you push new code. The key insight: you don&rsquo;t need to <em>use</em> the variable meaningfully, just <em>read</em> it. One thing to keep in mind: consider keeping integration tests in separate packages from unit tests to avoid invalidating unit test caches unnecessarily when only the integration tests need cache invalidation.</p>
<div class=" copy-enabled">
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="cl"><span class="kn">package</span><span class="w"> </span><span class="nx">integrationtesting</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="kd">func</span><span class="w"> </span><span class="nf">IsIntegrationTest</span><span class="p">(</span><span class="nx">t</span><span class="w"> </span><span class="o">*</span><span class="nx">testing</span><span class="p">.</span><span class="nx">T</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="nx">t</span><span class="p">.</span><span class="nf">Helper</span><span class="p">()</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="c1">// This ensures that we invalidate the cache</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="c1">// on CI whenever we have a new git SHA</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="nx">_</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="nx">os</span><span class="p">.</span><span class="nf">Getenv</span><span class="p">(</span><span class="s">&#34;DRONE_COMMIT_SHA&#34;</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="c1">// ... another file relying on integrationtesting</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="kd">func</span><span class="w"> </span><span class="nf">TestAPIIntegration</span><span class="p">(</span><span class="nx">t</span><span class="w"> </span><span class="o">*</span><span class="nx">testing</span><span class="p">.</span><span class="nx">T</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="nx">integrationtesting</span><span class="p">.</span><span class="nf">IsIntegrationTest</span><span class="p">(</span><span class="nx">t</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="c1">// Your actual integration test</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="nx">resp</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nf">callAPIEndpoint</span><span class="p">()</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">	</span><span class="nx">assert</span><span class="p">.</span><span class="nf">Equal</span><span class="p">(</span><span class="nx">t</span><span class="p">,</span><span class="w"> </span><span class="nx">expected</span><span class="p">,</span><span class="w"> </span><span class="nx">resp</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="p">}</span></span></span></code></pre></div>
</div>
<h2 id="understanding-how-it-actually-works">
  Understanding How It Actually Works
  <a href="#understanding-how-it-actually-works" class="heading-anchor" aria-label="Direct link to Understanding How It Actually Works"><span aria-hidden="true">#</span></a>
</h2>
<p>I was getting worried about edge cases. The Go docs said tests won&rsquo;t cache if environment variables change, but what exactly did that mean? Would reading an env var in one test invalidate <em>all</em> tests everywhere? Just that package? What if the variable is read through a library call?</p>
<p>I did some searching and found the <a href="https://github.com/golang/go/blob/6c3b5a2798c83d583cb37dba9f39c47300d19f1f/src/internal/testlog/log.go#L49-L54">testlog package</a>, which is how Go implements the tracking of when env vars are read and files opened. That gave me a good sense of how it should behave, so I made an experiment to validate what I expected from the documentation and the code I found.</p>
<h3 id="the-experiment-results">
  The Experiment Results
  <a href="#the-experiment-results" class="heading-anchor" aria-label="Direct link to The Experiment Results"><span aria-hidden="true">#</span></a>
</h3>
<p>I created <a href="https://github.com/gaqzi/experiment-go-test-caching">three test packages</a> to understand the boundaries:</p>
<ul>
<li>One that reads <code>DRONE_COMMIT_SHA</code> directly</li>
<li>One that reads it through a library call</li>
<li>One that doesn&rsquo;t read any environment variables</li>
</ul>
<p>Then I ran <a href="https://github.com/gaqzi/experiment-go-test-caching/blob/main/test.sh">nine scenarios to test my assumptions</a>: What happens when I set the variable? Does package listing matter (<code>./...</code> vs manual package lists)? What about <code>go test</code> with no arguments?</p>
<p>Key findings:</p>
<ul>
<li><strong>Package-level invalidation</strong>: All tests in a package get invalidated if <em>any</em> test reads the variable and the variable changes,</li>
<li><strong>Library calls count</strong>: Reading env vars through dependencies still invalidates the cache</li>
<li><strong>Package listing doesn&rsquo;t matter</strong>: <code>./...</code>, manual lists, even <code>./</code> all cache the same way</li>
<li><strong>No arguments = no cache</strong>: <code>go test</code> without targeting never caches</li>
<li><strong>File-level targeting = no cache</strong>: <code>go test file_test.go</code> also never caches</li>
</ul>
<p>The big takeaway: Go runs tests per package, and packages are run in parallel. If any test in the package touches an environment variable, the entire package&rsquo;s cache depends on that variable&rsquo;s value, so the cache is invalidated when the value is different on the next run.</p>
<p>Or as a semi-flowchart:</p>
<div class=" copy-enabled">
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">Package reads env var? 
</span></span><span class="line"><span class="cl">    Yes → Env var value changed? 
</span></span><span class="line"><span class="cl">              Yes → Cache invalid
</span></span><span class="line"><span class="cl">              No  → Cache valid
</span></span><span class="line"><span class="cl">    No → Cache valid</span></span></code></pre></div>
</div>
<p>This made me comfortable that the approach was predictable and wouldn&rsquo;t have surprising edge cases, especially because the way <a href="https://github.com/golang/go/blob/6c3b5a2798c83d583cb37dba9f39c47300d19f1f/src/internal/testlog/log.go#L49-L54">testlog</a> works is by simply recording that <em>something</em> called <code>os.Getenv</code> with this value, and it doesn&rsquo;t know which test or from where, just that in the course of running these tests this happened. Nice and simple.</p>
<p>If you want to explore how Go makes decisions around caching yourself, you can run tests with <code>GODEBUG=gocachetest=1</code> to see Go&rsquo;s caching decisions in real time. There are more debugging variables available in <code>go help cache</code>.</p>
<h2 id="the-results">
  The Results
  <a href="#the-results" class="heading-anchor" aria-label="Direct link to The Results"><span aria-hidden="true">#</span></a>
</h2>
<p>We cut our CI times in half with this caching approach, and I&rsquo;m confident it&rsquo;s safe to roll out widely because I understand exactly how it works and I can make the required changes to our shared integration testing library so it will just work for the standard case.</p>
<p>What I appreciate about Go&rsquo;s design here is how it avoids clever optimizations in favor of predictable behavior. I knew caching wasn&rsquo;t just on/off, but I wasn&rsquo;t sure if there would be complex edge cases to worry about. Instead, Go picks the straightforward solution: package-level invalidation that&rsquo;s easy to reason about.</p>
<p>The &ldquo;bigger packages than you think, but still not huge&rdquo; philosophy means we get plenty of caching benefit, while keeping the invalidation scope manageable, and if we need finer control, we can always split into more packages.</p>
<p>And that&rsquo;s the thing, Go just picks the simple and boring solution that works, it&rsquo;s why I love working with Go. 😁</p>
<hr>
<p><em>The <a href="https://github.com/gaqzi/experiment-go-test-caching">full experiment is available</a> if you want to dig into it yourself.</em><br>
<em><a href="https://speakerdeck.com/gaqzi/working-with-gos-test-cache-on-ci">Presentation slides</a> from the Singapore Go meetup</em></p>
]]></content:encoded></item><item><title>Running effective meetings uses the same skills as running successful projects — From meeting minutes to project milestones</title><link>https://bjorn.now/blog/2025/05/03/effective-meetings/?utm_medium=feed</link><pubDate>Sat, 03 May 2025 16:20:00 +0800</pubDate><guid>https://bjorn.now/blog/2025/05/03/effective-meetings/</guid><description>You’ve probably sat through your fair share of meetings where time slips away as conversations wander aimlessly, not unlike being dropped into an unfamiliar codebase, with twisting branches all alike.
A colleague asked for feedback on a meeting they ran, and after sharing my thoughts, I realized my advice about meetings sounded exactly like how I talk about planning projects. This got me thinking: the skills for running good meetings and successful projects are nearly identical.</description><content:encoded><![CDATA[<p>You&rsquo;ve probably sat through your fair share of meetings where time slips away as conversations wander aimlessly, not unlike being dropped into an unfamiliar codebase, with twisting branches all alike.</p>
<p>A colleague asked for feedback on a meeting they ran, and after sharing my thoughts, I realized my advice about meetings sounded exactly like how I talk about planning projects. This got me thinking: the skills for running good meetings and successful projects are nearly identical.</p>
<p>Yet somehow, we still treat our meetings like they&rsquo;re different beasts entirely. Why do so many meetings fail? Usually because we approach them without a clear plan.</p>
<p>Meetings are often run like debugging without a hypothesis, just throwing log statements everywhere and hoping something works, where you have a list of topics but no clear goals or time constraints. You might cover everything, but that&rsquo;s more luck than design.</p>
<p>This is similar to &ldquo;planning forward&rdquo; in projects: We start with a feature list without understanding the details, create up-front timelines in Gantt charts that rarely get revised, and postpone integration until the end. We move through tasks sequentially, assuming everything will go right, only to discover problems when time is about to or has already run out.</p>
<p>Both approaches ignore that time runs out, whether in a 30-minute meeting or a three-month project. Without a plan, meetings go nowhere and projects fall behind.</p>
<p>In my experience, here&rsquo;s what both have in common:</p>
<ul>
<li>Gather everything that needs to get done</li>
<li>Clarify goals for each item</li>
<li>Group similar things together</li>
<li>Prioritize the important stuff first</li>
<li>Estimate how long each section will take</li>
<li>Guide the work to keep it on track</li>
<li>Define next steps so everyone knows what&rsquo;s happening after</li>
</ul>
<h2 id="the-pieces">
  The pieces
  <a href="#the-pieces" class="heading-anchor" aria-label="Direct link to The pieces"><span aria-hidden="true">#</span></a>
</h2>
<p>Let&rsquo;s break down each of these components and see how they apply specifically to meetings:</p>
<h3 id="1-gather-define-your-scope">
  1. Gather: Define your scope
  <a href="#1-gather-define-your-scope" class="heading-anchor" aria-label="Direct link to 1. Gather: Define your scope"><span aria-hidden="true">#</span></a>
</h3>
<p>Just as you wouldn&rsquo;t start coding without requirements, don&rsquo;t start a meeting without defining what needs covering. List all topics and be realistic about what fits in the allotted time. If your meeting scope looks like your backlog after a year of &ldquo;we&rsquo;ll get to it later,&rdquo; you&rsquo;re doing it wrong.</p>
<h3 id="2-clarify-set-measurable-goals-for-each-topic">
  2. Clarify: Set measurable goals for each topic
  <a href="#2-clarify-set-measurable-goals-for-each-topic" class="heading-anchor" aria-label="Direct link to 2. Clarify: Set measurable goals for each topic"><span aria-hidden="true">#</span></a>
</h3>
<p>For each topic, be clear about what you&rsquo;re trying to accomplish:</p>
<ul>
<li><strong>Discuss:</strong> &ldquo;We need to decide on auth providers. What criteria should guide our recommendation?&rdquo;</li>
<li><strong>Decision:</strong> &ldquo;We recommend Provider A because&hellip;&rdquo;</li>
<li><strong>FYI:</strong> &ldquo;We&rsquo;ve implemented the new provider in staging and learned&hellip;&rdquo;</li>
</ul>
<p><strong>Pro tip</strong>: Prefix each agenda item with one of these categories (Discuss/Decision/FYI) to ground everyone in what you&rsquo;re trying to accomplish with each topic.</p>
<h3 id="3-group-combine-related-topics">
  3. Group: Combine related topics
  <a href="#3-group-combine-related-topics" class="heading-anchor" aria-label="Direct link to 3. Group: Combine related topics"><span aria-hidden="true">#</span></a>
</h3>
<p>Identify when similar items come from different people and combine them. This prevents redundant discussions and gives a clearer picture of what needs attention, similar to combining related user stories into an epic. When several people raise the same issue in different ways, it&rsquo;s usually important.</p>
<h3 id="4-prioritize-focus-on-what-matters">
  4. Prioritize: Focus on what matters
  <a href="#4-prioritize-focus-on-what-matters" class="heading-anchor" aria-label="Direct link to 4. Prioritize: Focus on what matters"><span aria-hidden="true">#</span></a>
</h3>
<p>Sequence by importance, not by who talks loudest or which item has been sitting on the agenda longest. Start with critical items so when you inevitably run out of time, at least the important stuff got done.</p>
<h3 id="5-estimate-time-boxing">
  5. Estimate: Time boxing
  <a href="#5-estimate-time-boxing" class="heading-anchor" aria-label="Direct link to 5. Estimate: Time boxing"><span aria-hidden="true">#</span></a>
</h3>
<p>We accept that iteration estimates will be wrong (it&rsquo;s just a question of how wrong), and meeting time estimates work exactly the same way. Having a clear time target keeps everyone aligned. Be explicit about constraints: &ldquo;We have 15 minutes for this topic&rdquo; creates healthy pressure to stay focused.</p>
<h3 id="6-guide-keep-discussions-on-track">
  6. Guide: Keep discussions on track
  <a href="#6-guide-keep-discussions-on-track" class="heading-anchor" aria-label="Direct link to 6. Guide: Keep discussions on track"><span aria-hidden="true">#</span></a>
</h3>
<p>Just like daily standups in project work, regularly check in on your meeting&rsquo;s progress. Help nudge the meeting along:</p>
<h4 id="monitor-progress-toward-goals">
  Monitor progress toward goals
  <a href="#monitor-progress-toward-goals" class="heading-anchor" aria-label="Direct link to Monitor progress toward goals"><span aria-hidden="true">#</span></a>
</h4>
<p>Just like in daily standups, pay attention to whether the discussion is moving toward your goal. Ask yourself, &ldquo;Are we just talking, or are we getting closer to making a decision?&rdquo; If you&rsquo;re 10 minutes into database options and no one has mentioned the actual requirements, it&rsquo;s time to refocus.</p>
<h4 id="keep-discussions-focused">
  Keep discussions focused
  <a href="#keep-discussions-focused" class="heading-anchor" aria-label="Direct link to Keep discussions focused"><span aria-hidden="true">#</span></a>
</h4>
<p>When someone goes off on a tangent describing an intricate edge case, redirect with: &ldquo;That sounds like an important edge case. Will this affect the decision we&rsquo;re trying to make right now? If not, could you chat with the relevant team after this? Right now we need to focus on making our decision.&rdquo;</p>
<h4 id="provide-time-checks">
  Provide time checks
  <a href="#provide-time-checks" class="heading-anchor" aria-label="Direct link to Provide time checks"><span aria-hidden="true">#</span></a>
</h4>
<p>My <a href="https://www.geepawhill.org/2021/04/07/rice-garlic-more-smaller-steps/">rice&rsquo;n&rsquo;garlic advice</a> is to check in halfway through a conversation and remind everyone of the time left. Sometimes simply saying &ldquo;We have 7 minutes left to decide on this&rdquo; creates the right pressure to wrap things up. If needed, quickly restate the goal: &ldquo;Remember, we just need to agree on which two options to research further, not make the final decision today.&rdquo;</p>
<h4 id="make-realistic-time-decisions">
  Make realistic time decisions
  <a href="#make-realistic-time-decisions" class="heading-anchor" aria-label="Direct link to Make realistic time decisions"><span aria-hidden="true">#</span></a>
</h4>
<p>Be realistic about whether you can finish in the time you have. If a 15-minute agenda item is turning into a 45-minute discussion, decide whether to:</p>
<ul>
<li>Extend the current meeting (if everyone can stay)</li>
<li>Schedule a follow-up with just the key stakeholders</li>
<li>Assign someone to gather more data and come back next time</li>
</ul>
<h3 id="7-next-steps-ensure-clear-outcomes">
  7. Next steps: Ensure clear outcomes
  <a href="#7-next-steps-ensure-clear-outcomes" class="heading-anchor" aria-label="Direct link to 7. Next steps: Ensure clear outcomes"><span aria-hidden="true">#</span></a>
</h3>
<p>Don&rsquo;t let topics end without everyone knowing the outcome. For each item, ensure everyone knows:</p>
<ul>
<li>Is it done or still open?</li>
<li>Who owns any follow-up actions?</li>
<li>When should we expect progress?</li>
<li>How will we track it?</li>
</ul>
<p>Capture this in your meeting notes. Don&rsquo;t rely on memory. Write down the decisions, owners, and deadlines so our future selves can thank us.</p>
<h2 id="in-practice">
  In practice
  <a href="#in-practice" class="heading-anchor" aria-label="Direct link to In practice"><span aria-hidden="true">#</span></a>
</h2>
<p>Different meetings need different approaches, but a well-run meeting follows the same meta structure. This structure works for all kinds of meetings: one-on-ones, planning sessions, even technical architecture reviews, because the topics and participants change, but the structure that makes meetings effective doesn&rsquo;t.</p>
<p>Let&rsquo;s see how this structure looks in practice with a couple of common meeting types.</p>
<h3 id="retrospectives">
  Retrospectives
  <a href="#retrospectives" class="heading-anchor" aria-label="Direct link to Retrospectives"><span aria-hidden="true">#</span></a>
</h3>
<p>Retros help teams reflect on what worked, what didn&rsquo;t, and what to improve, much like code reviews but for your processes.</p>
<ul>
<li><strong>Gather</strong>: Have everyone add topics to a shared retrospective board</li>
<li><strong>Clarify &amp; Group</strong>: Cluster related topics to understand common themes</li>
<li><strong>Prioritize</strong>: Vote on which issues will have the most impact to address</li>
<li><strong>Estimate &amp; Guide</strong>: Timebox discussions, default 3 minutes per topic, explicitly decide to extend or move on</li>
<li><strong>Next Steps</strong>: Capture clear action items, owners, and timelines</li>
</ul>
<p><strong>Pro tip</strong>: Seeing the same issues iteration after iteration? Try a <a href="https://www.thoughtworks.com/en-au/insights/blog/reverse-retrospective-part-1">reverse retrospective</a> where you start with known problems and dedicate the entire session to brainstorming solutions.</p>
<h3 id="recurring-team-meetings">
  Recurring team meetings
  <a href="#recurring-team-meetings" class="heading-anchor" aria-label="Direct link to Recurring team meetings"><span aria-hidden="true">#</span></a>
</h3>
<p>Regular meetings work best with a consistent structure, like good software needs a reliable architecture.</p>
<ul>
<li><strong>Gather</strong>: Maintain a living document where people add items between meetings, plus time at the start to add more</li>
<li><strong>Prioritize</strong>: Critical conversations happen first, not just what&rsquo;s been on the list longest</li>
<li><strong>Clarify</strong>: Define each topic&rsquo;s goal and intended outcome (FYI, discussion, or decision-making)</li>
<li><strong>Estimate</strong>: Allocate time to topics based on importance and complexity</li>
<li><strong>Guide</strong>: Check progress mid-topic with &ldquo;Are we getting closer to our goal?&rdquo;</li>
<li><strong>Next Steps</strong>: Note who&rsquo;s doing what by when</li>
</ul>
<p><strong>Pro tip</strong>: For complex topics, try a <a href="https://medium.com/swlh/the-silent-meeting-manifesto-v1-189e9e3487eb">silent meeting</a> approach where everyone reads and comments on a shared document before discussion. This helps people think deeply, solve quick questions in writing, and prevent a few voices from dominating.</p>
<h2 id="the-meta-skill">
  The meta-skill
  <a href="#the-meta-skill" class="heading-anchor" aria-label="Direct link to The meta-skill"><span aria-hidden="true">#</span></a>
</h2>
<p>Running a good meeting uses the same mental muscles as running a good project:</p>
<ul>
<li>Scope definition is like requirements gathering</li>
<li>Timeboxing topics is like defining iterations</li>
<li>Progress checks are like standups</li>
</ul>
<p>If you can&rsquo;t keep a 30-minute meeting on track, how will you keep a three-month project on track? Both require the same core skills:</p>
<ol>
<li>
<p><strong>Clear Goals</strong>: &ldquo;We want to discuss database options&rdquo; is as useless as &ldquo;We want to build a better app.&rdquo;</p>
</li>
<li>
<p><strong>Risk Management</strong>: Put risky, important topics first, whether in meetings or project timelines.</p>
</li>
<li>
<p><strong>Progress Monitoring</strong>: Regularly ask: &ldquo;Are we moving toward our goal or getting sidetracked?&rdquo;</p>
</li>
<li>
<p><strong>Flexibility with Discipline</strong>: Sometimes you need to extend a meeting or an iteration, but do it consciously, not by default.</p>
</li>
<li>
<p><strong>Follow-through</strong>: Track action items like you track bugs, they&rsquo;re just as important.</p>
</li>
</ol>
<p>I wonder if running meetings could be good practice for running projects? The skills are similar, but the feedback cycle is much shorter.</p>
<h2 id="conclusion">
  Conclusion
  <a href="#conclusion" class="heading-anchor" aria-label="Direct link to Conclusion"><span aria-hidden="true">#</span></a>
</h2>
<p>Every engineer knows that a well-designed system needs both good architecture and regular maintenance. Your meetings are no different.</p>
<p>By applying the same structured thinking to your meetings that you use in your code (defining clear requirements, managing scope, prioritizing effectively, and documenting outcomes), you transform the meetings in your calendar into productive work sessions.</p>
<p>In my experience, these skills compound nicely. I bet you&rsquo;ll get better at delivering focused projects as you get better at running focused meetings.</p>
<p>So before you schedule your next meeting, run it through the same mental checklist you&rsquo;d use for planning a project:</p>
<ul>
<li>Have you gathered topics and defined what you want from each one?</li>
<li>Have you prioritized what matters most?</li>
<li>Can this all get done in the time you have?</li>
<li>Do you have a plan to keep discussions on track?</li>
</ul>
<p>Keep in mind that guiding meetings is a lot like incident response. You can plan all you want, but you need to react to what&rsquo;s actually happening. This framework gives you a starting point, but the real magic happens when you apply it in the moment. Start small by focusing on timeboxing discussions or defining clear outcomes. The structure matters, but how you adapt when the sewage hits the aerowatt generator is what really counts.</p>
<p>If not, refactor before you commit.</p>
<p>Efficient meetings aren&rsquo;t magic. They&rsquo;re just good engineering applied to communication.</p>
]]></content:encoded></item><item><title>Day-to-day automation using Alfred on your Mac — reducing TOIL outside of your DevOps practice</title><link>https://bjorn.now/blog/2024/04/15/day-to-day-automation-using-alfred/?utm_medium=feed</link><pubDate>Mon, 15 Apr 2024 19:15:00 +0800</pubDate><guid>https://bjorn.now/blog/2024/04/15/day-to-day-automation-using-alfred/</guid><description>I try to be aware of where I have friction in my day-to-day when working at my computer, keeping an eye out for TOIL to remove. Whether it’s from manually repeated actions, or from differing behaviors across apps and services I use. When I identify any of these I try to spend some small amount of time automating or changing how I work to improve.
All of these things don’t necessarily make a lot of sense from how much time I save, but, it also makes me happy to spend a bit of time improving my environment, and spending a bit of time to make me happy is good enough for me.</description><content:encoded><![CDATA[<p>I try to be aware of where I have friction in my day-to-day when working at my
computer, keeping an eye out for <a href="https://sre.google/sre-book/eliminating-toil/">TOIL to remove.</a> Whether it&rsquo;s from
manually repeated actions, or from differing behaviors across apps and services
I use. When I identify any of these I try to spend some small amount of time
automating or changing how I work to improve.</p>
<p>All of these things don&rsquo;t necessarily make a lot of sense from how much time I
save, but, it also makes me happy to spend a bit of time improving my
environment, and spending a bit of time to make me happy is good enough for me.</p>
<p>I view doing these kinds of small workflow improvements like a <a href="https://en.wikipedia.org/wiki/Kata#Outside_martial_arts">kata</a> because it
improves my automation/scripting skills and encourages me to think about how I
work. I usually give myself 10-20m to try and make these kinds of improvements
just so I know how much time to spend. Just don&rsquo;t go overboard. XKCD, as always,
have a comic to illustrate the downside of going too far. 🙂</p>
<p><a href="https://xkcd.com/1319"><img src="https://imgs.xkcd.com/comics/automation.png" alt="&lsquo;Automating&rsquo; comes from the roots &lsquo;auto-&rsquo; meaning &lsquo;self-&rsquo;, and &lsquo;mating&rsquo;, meaning &lsquo;screwing&rsquo;.&quot;"></a></p>
<h2 id="what-is-alfred-and-why-is-it-an-app-im-focusing-on">
  What is Alfred and why is it an app I&rsquo;m focusing on?
  <a href="#what-is-alfred-and-why-is-it-an-app-im-focusing-on" class="heading-anchor" aria-label="Direct link to What is Alfred and why is it an app I&rsquo;m focusing on?"><span aria-hidden="true">#</span></a>
</h2>
<p><a href="https://alfredapp.com/">Alfred</a> is an application launcher but oh-so-much-more. It connects with macOS
and the Workflows contains a lot of extensions that allow you to interact with
all kinds of programs, websites, etc.</p>
<p>A big part of what Alfred has helped me create is a more unified experience
across the apps and websites I use. I have moved some operations that behave
differently in different apps into Alfred because then I can focus on what I
need to do and then use Alfred to provide consistency (and as I&rsquo;ve
been told, &ldquo;consistency is good even outside the bathroom&rdquo;).</p>
<p>Now, let&rsquo;s look at how I use Alfred.</p>
<h2 id="built-in-functionality">
  Built-in functionality
  <a href="#built-in-functionality" class="heading-anchor" aria-label="Direct link to Built-in functionality"><span aria-hidden="true">#</span></a>
</h2>
<p>These are features that don&rsquo;t require anything extra to install and use.</p>
<h3 id="open-an-app">
  Open an app
  <a href="#open-an-app" class="heading-anchor" aria-label="Direct link to Open an app"><span aria-hidden="true">#</span></a>
</h3>
<p>Alfred&rsquo;s starter feature is the application launcher, and it means you summon it
with a keybinding and type the name of the app you want to start and voilà.
I have bound the keyboard shortcut to <code>cmd+space</code> and replaced the built-in
Spotlight search.</p>
<figure><img src="/img/2024/04/15/01-open-app.png"
    alt="Opening Firefox in Alfred">
</figure>

<h3 id="spell--define">
  Spell / define
  <a href="#spell--define" class="heading-anchor" aria-label="Direct link to Spell / define"><span aria-hidden="true">#</span></a>
</h3>
<p>There are certain words I rarely get right (continuous) and some
words are spelled very differently from how they sound. Summon Alfred and
<code>spell &lt;word&gt;</code> and you&rsquo;ll have suggestions that often will go right. And if
you don&rsquo;t find it, replace <code>spell</code> with <code>google</code> and see if Google knows
better. Hit enter to copy the correct spelling to the clipboard.</p>
<figure><img src="/img/2024/04/15/02-spell-continuous.png"
    alt="Finding the correct spelling of continuus">
</figure>

<p>And <code>define</code> is the same thing but for dictionary lookup because you have some
word you&rsquo;re unsure what it means, or because you&rsquo;re wondering if someone was
trying to call you something you with some subtle nuance to a word. 😉
Hit enter to open the definition in macOS&rsquo;s dictionary app.</p>
<figure><img src="/img/2024/04/15/03-define-laggard.png"
    alt="Define &#39;laggard&#39;">
</figure>

<h3 id="calculator">
  Calculator
  <a href="#calculator" class="heading-anchor" aria-label="Direct link to Calculator"><span aria-hidden="true">#</span></a>
</h3>
<p>You can give it an expression and when you hit enter the result goes into the
clipboard. I often end up using this instead of opening a Python console on the
CLI.</p>
<figure><img src="/img/2024/04/15/04-calculator.png"
    alt="Adding some numbers together">
</figure>

<h3 id="custom-web-search">
  Custom web search
  <a href="#custom-web-search" class="heading-anchor" aria-label="Direct link to Custom web search"><span aria-hidden="true">#</span></a>
</h3>
<p>A custom web search allow you to quickly open a web browser at a
particular website with whatever value you&rsquo;ve typed into Alfred which <em>doesn&rsquo;t
have to be a search engine or even a search page.</em></p>
<p>To configure a custom web search summon Alfred and hit <code>cmd+,</code> to open
the settings page. Go into the <code>Features</code> tab and click on <code>Web Search</code>.</p>
<figure><img src="/img/2024/04/15/05-web-search-listing.png"
    alt="Alfred&#39;s setting Features -&gt; Web Search highlighted">
</figure>

<p>Some example search engines I have made:</p>
<ul>
<li>Jira: <code>https://jira.example.com/browse/{query}</code>
<ul>
<li>This isn&rsquo;t a search but rather the view this ticket. Because I often get a
ticket id (either from a commit message or someone rattling it off in a
meeting) and this way I can open it quickly</li>
</ul>
</li>
<li>Confluence: <code>https://confluence.example.com/dosearchsite.action? queryString={query}</code></li>
<li>Wordnik: <code>https://wordnik.com/words/{query}</code>
<ul>
<li>So I can find synonyms and other alternatives for words. Often use this when
I want to find a good name for something which has been inspired
by <a href="https://signalvnoise.com/svn3/hunting-for-great-names-in-programming/">Hunting for great names in programming</a>.</li>
</ul>
</li>
<li>Pypi: <code>https://pypi.org/project/{query}</code>
<ul>
<li>To look up python packages</li>
</ul>
</li>
</ul>
<figure><img src="/img/2024/04/15/06-web-search-example-config.png"
    alt="Example setup of the Confluence search engine">
</figure>

<h2 id="working-with-other-applications">
  Working with other applications
  <a href="#working-with-other-applications" class="heading-anchor" aria-label="Direct link to Working with other applications"><span aria-hidden="true">#</span></a>
</h2>
<p>Alfred has ended being the tool that brings my apps to focus and smoothes
over differences between apps.</p>
<h3 id="give-my-most-common-apps-a-keybinding">
  Give my most common apps a keybinding
  <a href="#give-my-most-common-apps-a-keybinding" class="heading-anchor" aria-label="Direct link to Give my most common apps a keybinding"><span aria-hidden="true">#</span></a>
</h3>
<p>Instead of <code>cmd+tab</code> between the various apps I&rsquo;m using I have bound specific
keybindings to my most used app, so I can quickly swap between
them. <a href="https://dev.to/rstacruz/switching-apps-slow-down-my-productivity-and-how-i-fixed-it-2anb">Hat tip to Rico Sta. Cruz</a>
who blogged about this a couple of years ago and who I picked it up from.</p>
<p>The big benefit I have found here is just the ease of being mid-thinking and
wanting to switch back to another app, whether it&rsquo;s to jot down a quick note, to
finish that Regex you were visualizing on <a href="https://regex101.com/">Regex 101</a>, or
to get back to Slack to paste that thing your colleague asked for. It feels so
seamless compared to tabbing between.</p>
<p>This is using <a href="https://www.alfredapp.com/workflows/">Alfred&rsquo;s Workflows</a>, and
they&rsquo;re part of their advanced set of options. You can do keybindings that work
across all of macOS as well as actions based on what you&rsquo;re typing.</p>
<figure><img src="/img/2024/04/15/07-app-shortcuts.png"
    alt="The custom app shortcuts workflow">
</figure>

<h3 id="spotify-controller">
  Spotify controller
  <a href="#spotify-controller" class="heading-anchor" aria-label="Direct link to Spotify controller"><span aria-hidden="true">#</span></a>
</h3>
<p>I have been using these keybindings since before Alfred, in Quicksilver, and
there are other workflows for Spotify but since I had these I never bothered
installing anything else.</p>
<p>The cool extra in this workflow is that I added a keyword to open the
current playing song on <a href="https://genius.com">Genius.com</a> because I like
reading the lyrics.</p>
<p>This is an example of something where the automation is <em>far from necessary,</em>
but, I enjoyed adding it. It&rsquo;s something I do every other week and knowing I
can just go straight into the song nearly every time is great.</p>
<figure><img src="/img/2024/04/15/08-spotify-keybindings.png"
    alt="Spotify keybinding and keyword configuration">
</figure>

<p>My <a href="https://github.com/gaqzi/alfred-workflows/blob/master/Spotify%20keyboard%20shortcuts.alfredworkflow">Spotify workflow</a> is on Github if you want to install it, or <a href="https://gist.github.com/gaqzi/b3de88050bf1caaf8d2661233b51a1ae">here&rsquo;s a gist</a>
if you&rsquo;re just curious how I made &ldquo;open on genius.&rdquo;</p>
<h3 id="global-microphone-muteunmute-shortcuts">
  Global microphone mute/unmute shortcuts
  <a href="#global-microphone-muteunmute-shortcuts" class="heading-anchor" aria-label="Direct link to Global microphone mute/unmute shortcuts"><span aria-hidden="true">#</span></a>
</h3>
<p>I regularly use Google Meet, Slack, and Zoom for talking with people, and these
apps all use different shortcuts to mute. So instead of keeping the app
focused and remembering the various shortcuts I now just leave myself unmuted
everywhere and directly mute/unmute the OS through Alfred. My mic has a visual
indicator when it&rsquo;s on. +I have a notification posted, so I can see the current
status on the screen.</p>
<p>As I started using this workflow I realized that sometimes the microphone
bugs out but swapping it between the built-in and back will fix it. So after
a while I made a script to help swap as well.</p>
<p>And finally, I realized that sometimes my virtual soundcard which removes
background noise (<a href="https://krisp.ai">krisp.ai</a>) hangs and needs to be
restarted, so another keybinding to quickly restart the soundcard.</p>
<p>The only downside of this workflow is that others can&rsquo;t as clearly see when
I want to speak by unmuting myself in the current app, so in those rare cases
I&rsquo;ll use the mute in the app itself. Because optimizing my workflow
shouldn&rsquo;t come at the pain of everyone else, we live in a society. 🙂</p>
<figure><img src="/img/2024/04/15/09-mic-switch.png"
    alt="The mic switch workflow setup">
</figure>

<p>This <a href="https://github.com/gaqzi/alfred-workflows/blob/master/Toggle%20mute%20mic.alfredworkflow">mute/unmute workflow</a> is available on Github,
and <a href="https://gist.github.com/gaqzi/f299047706c10b2616ba15c94cabf1fe">here&rsquo;s a gist</a> if you want to see the toggle code. My
dotfiles got the <a href="https://github.com/gaqzi/conf/blob/master/bin/swap-mic-input">swap mic script</a> and the <a href="https://github.com/gaqzi/conf/blob/master/bin/%2Crestart-krisp">restart krisp script</a>.</p>
<h2 id="improvement-to-my-day-to-day-workflow">
  Improvement to my day-to-day workflow
  <a href="#improvement-to-my-day-to-day-workflow" class="heading-anchor" aria-label="Direct link to Improvement to my day-to-day workflow"><span aria-hidden="true">#</span></a>
</h2>
<h3 id="text-replacement--snippets">
  Text replacement / snippets
  <a href="#text-replacement--snippets" class="heading-anchor" aria-label="Direct link to Text replacement / snippets"><span aria-hidden="true">#</span></a>
</h3>
<p>Snippets are short pieces of text that expand into something else. It can be
a static string, or it can be dynamic and I use both kinds as I work. For me
they fall into these categories</p>
<ol>
<li>&ldquo;Named paste&rdquo;, things that are hard to remember or unwieldy and are now not</li>
<li>Dynamic replacement, something that follows a pattern and I want to swap
between. Generally these use the clipboard</li>
<li>Generated inputs, I need the current time in a specific format or some
other piece of data</li>
</ol>
<p>You configure snippets by summoning Alfred and hitting <code>cmd+,</code> which opens
Settings, then go into Features -&gt; Snippets. Snippets work by specifying a
keyword, in my default bundle I require that my keywords are prefixed with <code>\\</code>,
meaning that Alfred will expand my snippets if it sees <code>\\&lt;keyword&gt;</code> and with
the example keyword <code>exp</code> you&rsquo;d then type <code>\\exp</code> to add it. If you need to type
the exact string for some reason it&rsquo;s easiest to add a space somewhere before
you type the last character and then go back and remove it.</p>
<figure><img src="/img/2024/04/15/11-snippets-config.png"
    alt="How to view or create snippets">
</figure>

<p>My trigger for creating a snippet is finding that I keep typing or
pasting the same thing again and again.</p>
<p>Let&rsquo;s look at how I use them in a couple of different places.</p>
<h4 id="gmail">
  GMail
  <a href="#gmail" class="heading-anchor" aria-label="Direct link to GMail"><span aria-hidden="true">#</span></a>
</h4>
<p>Finding emails from &ldquo;unusual senders:&rdquo; I label the majority of emails I get
at work as they are notifications or otherwise automated emails. So if I get
something that isn&rsquo;t labeled it&rsquo;s probably a human or something I need to
look into, and it can sometimes get hidden in a sea of everything else.</p>
<p><code>&lt;prefix&gt;gmailn</code> in the search bar turns into <code>is:inbox is:unread has:nouserlabels</code> which only shows unread emails in the inbox which doesn&rsquo;t
have a label.</p>
<p>When I&rsquo;m processing my labels, which are effectively buckets of things to do,
I often go into a label and want to see only the unread items, so I can open
the first and then go through them one-by-one. So open the label, focus on
the search bar, and type in <code>&lt;prefix&gt;unr</code> to give me <code>is:unread</code> and then
get to that focus.</p>
<h4 id="jira-tickets">
  Jira tickets
  <a href="#jira-tickets" class="heading-anchor" aria-label="Direct link to Jira tickets"><span aria-hidden="true">#</span></a>
</h4>
<p>If I&rsquo;m writing a message on Slack and I need to reference a ticket I think
it&rsquo;s nice to give a link even if I only have the jira ticket ID, so I made a
snippet that adds the current clipboard item at the end of my company&rsquo;s Jira
instance in a Markdown link. Now it looks nice where markdown is and the
other person doesn&rsquo;t have to open it up themselves.</p>
<p>Copy <code>WRK-123</code> and then type <code>&lt;prefix&gt;jiramd</code> which replaces it with
<code>[WRK-123](https://jira.deliveryhero.com/browse/WRK-123)</code>.</p>
<p>The Alfred snippet configuration</p>
<div class=" copy-enabled">
  <pre tabindex="0"><code>[{clipboard:trim}](https://jira.example.com/browse/{clipboard:trim})</code></pre>
</div>
<figure><img src="/img/2024/04/15/12-snippet-jiramd.png"
    alt="Configuring the jiramd snippet in Alfred">
</figure>

<h4 id="roam-research">
  Roam Research
  <a href="#roam-research" class="heading-anchor" aria-label="Direct link to Roam Research"><span aria-hidden="true">#</span></a>
</h4>
<p>Roam is my notetaking app and where most of my thinking happen and I have a
whole slew of replacements for how I work with Roam Research and most of them
are to help me keep my workflow consistent (using the right tags)
and to make common actions easier and faster.</p>
<ul>
<li><code>&lt;prefix&gt;rts</code> turns into the current time in bold since I practice
interstitial journaling. Example <code>**15:03**</code>. Snippet configuration: <code>** {time:short}**</code></li>
<li><code>&lt;prefix&gt;b&lt;shortcode&gt;</code> for how I bucket tasks so when I spend time on a
particular bucket I can find the things I need to do. I.e. my bucket  &ldquo;Life in
Production&rdquo; (on-call, observability, continuous delivery, etc.) is prefixed
with <code>&lt;prefix&gt;bl</code> and that turns into <code>[🥬]([[Bucket/Life in Production]])</code></li>
<li><code>&lt;prefix&gt;&lt;INITIALS&gt;</code> to turn into the name of my most frequent
collaborators at work, so I can tag what I&rsquo;ve said to them or a task we
need to do.</li>
</ul>
<h3 id="emoji-search">
  Emoji search
  <a href="#emoji-search" class="heading-anchor" aria-label="Direct link to Emoji search"><span aria-hidden="true">#</span></a>
</h3>
<p>Spending a lot of time communicating across continents involves timezones
and people rarely hearing your voice, so I have gotten used to peppering my
inputs with emojis to help since it&rsquo;s so easy to come off sounding like an
ass otherwise.</p>
<p>And because some websites or applications don&rsquo;t have a good search for
emoji and this way I only have to learn one keyboard shortcut and normalize it
across places. This way I don&rsquo;t have to remember how Slack named this emoji vs.
Miro vs. Google Docs vs. Confluence vs. other random website. I just learn it in
Alfred and then paste it straight into the textbox I wanted.</p>
<figure><img src="/img/2024/04/15/10-search-emoji.png"
    alt="Searching for the rocket emoji">
</figure>

<p>This is an external Workflow made by <a href="https://github.com/jsumners">James Sumners</a>, and you can download it
from the <a href="https://github.com/jsumners/alfred-emoji">workflow&rsquo;s Github page.</a></p>
<h3 id="generate-uuids-on-the-fly">
  Generate uuids on the fly
  <a href="#generate-uuids-on-the-fly" class="heading-anchor" aria-label="Direct link to Generate uuids on the fly"><span aria-hidden="true">#</span></a>
</h3>
<p>As I was writing the tying it up section I realized that I had a recent pain
which where I kept jumping between apps to create new UUIDs. I was running
the following command on the CLI and then pasting it into some yaml files in
my editor.</p>
<div class=" copy-enabled">
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">uuidgen <span class="p">|</span> tr <span class="s1">&#39;[:upper:]&#39;</span> <span class="s1">&#39;[:lower:]&#39;</span> <span class="p">|</span> tr -d <span class="s1">&#39;\n&#39;</span> <span class="p">|</span> pbcopy</span></span></code></pre></div>
</div>
<p>So I instead created a new workflow that is triggered by a snippet that runs
this for me.</p>
<figure><img src="/img/2024/04/15/15-snippet-uuidgen.png"
    alt="Configuring the uuidgen snippet">
</figure>

<p><strong>Note:</strong> To make it behave just like a regular snippet you need to
configure the <em>Copy to clipboard</em> output to <code>Automatically paste to the frontmost app</code> and <code>Mark item as transient in clipboard</code>.</p>
<figure><img src="/img/2024/04/15/16-snippet-uuidgen-clipboard-config.png"
    alt="How configure the Copy to clipboard output">
</figure>

<p>The <a href="https://github.com/gaqzi/alfred-workflows/blob/master/uuidgen.alfredworkflow">uuidgen workflow</a> is available on Github.</p>
<h2 id="other-workflows">
  Other workflows
  <a href="#other-workflows" class="heading-anchor" aria-label="Direct link to Other workflows"><span aria-hidden="true">#</span></a>
</h2>
<p>Some other workflows I have installed since they save me time.</p>
<h3 id="calculate-anything">
  Calculate Anything
  <a href="#calculate-anything" class="heading-anchor" aria-label="Direct link to Calculate Anything"><span aria-hidden="true">#</span></a>
</h3>
<p>I installed this for two reasons: to quickly translate cooking recipes using
imperial units into metric and currency conversion. Super convenient and now I
can finally get how much 16 floz is in ml (473.176) without needing to go
online.</p>
<figure><img src="/img/2024/04/15/13-calculate-anything.png"
    alt="Calculate Anything converting 16 floz to ml">
</figure>

<p>By <a href="https://github.com/biati-digital">Biati Digital</a> and available in
the <a href="https://alfred.app/workflows/biatidigital/calculate-anything/">Alfred Gallery</a>.</p>
<h3 id="epoch-converter">
  Epoch converter
  <a href="#epoch-converter" class="heading-anchor" aria-label="Direct link to Epoch converter"><span aria-hidden="true">#</span></a>
</h3>
<p>There was a time when I kept looking at unix timestamps and needed to quickly
get when it was, so copying and then <code>ts &lt;paste&gt;</code> gave me when it was and
<code>ts</code> without anything will give you the current unix timestamp.</p>
<figure><img src="/img/2024/04/15/14-epoch-converter.png"
    alt="Epoch Converter showing the current unix timestamp">
</figure>

<p>By <a href="https://github.com/snooze92">Julien Lehuen</a> and available in
the <a href="https://alfred.app/workflows/julienlehuen/epoch-converter/">Alfred Gallery</a>.</p>
<h2 id="tying-it-up-and-how-i-figure-out-what-to-add">
  Tying it up and how I figure out what to add
  <a href="#tying-it-up-and-how-i-figure-out-what-to-add" class="heading-anchor" aria-label="Direct link to Tying it up and how I figure out what to add"><span aria-hidden="true">#</span></a>
</h2>
<p>I hope the above gives you some ideas or inspiration for what you could do
to improve your day-to-day computer usage. These are things that I&rsquo;ve pulled
together over years, and similarly I have a <a href="https://github.com/gaqzi/conf">bin/ folder</a> with other types of
automation I&rsquo;ve accrued, hehe. 😅</p>
<p>Now, how have I accrued these things? I generally try to reflect on the work
I do and the biggest help to it has been scheduling 30min every Friday to
reflect on the week. This session covers everything from updating my <a href="https://jvns.ca/blog/brag-documents/">brag
document</a> to reflecting on what work I felt I repeated a lot and whether
there&rsquo;s anything there to spend time on in the future. Pretty much,
what annoyed me when working and can I do something about it?</p>
<p>If I come up with something that I can&rsquo;t deal with right now then I&rsquo;ll
record it in Roam under <code>#hack-idea</code>, whether it&rsquo;s Friday or not, and when I
feel like hacking on something I&rsquo;ll look there. Because sometimes you just
need something quick to hack on. 🙂</p>
<p>I&rsquo;d love to hear what your favorite day-to-day automations are
on <a href="https://hachyderm.io/@gaqzi">Mastodon</a>!</p>
]]></content:encoded></item><item><title>How to make Docker resume downloads …download that final 100KB and not 200MB again</title><link>https://bjorn.now/blog/2024/02/01/how-to-make-docker-pull-resume-layer-downloads/?utm_medium=feed</link><pubDate>Thu, 01 Feb 2024 17:00:00 +0700</pubDate><guid>https://bjorn.now/blog/2024/02/01/how-to-make-docker-pull-resume-layer-downloads/</guid><description>In the Docker Desktop app you can go into Features in Development and check Use containerd for pulling and storing images.
Or you can do add the following to Docker’s daemon.json file:
{ "features": { "containerd-snapshotter": true } } This comes from a comment in a ticket for resuming downloads when they fail.</description><content:encoded><![CDATA[<p>In the Docker Desktop app you can go into <code>Features in Development</code> and check
<code>Use containerd for pulling and storing images</code>.</p>
<p>Or you can do add the following to Docker&rsquo;s <code>daemon.json</code> file:</p>
<div class=" copy-enabled">
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-javascript" data-lang="javascript"><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="s2">&#34;features&#34;</span><span class="o">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;containerd-snapshotter&#34;</span><span class="o">:</span> <span class="kc">true</span>
</span></span><span class="line"><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span></span></span></code></pre></div>
</div>
<p>This comes from <a href="https://github.com/docker/for-linux/issues/1187#issuecomment-1457279396">a comment</a> in a ticket for <a href="https://github.com/docker/for-linux/issues/1187">resuming downloads when they
fail.</a></p>
]]></content:encoded></item><item><title>Sticky blocks in Roam — floating along in your sidebar</title><link>https://bjorn.now/blog/2022/02/05/sticky-blocks-in-roam-research/?utm_medium=feed</link><pubDate>Sat, 05 Feb 2022 16:30:00 +0800</pubDate><guid>https://bjorn.now/blog/2022/02/05/sticky-blocks-in-roam-research/</guid><description>I use Roam Research as my primary way of thinking and keeping track of tasks. My current workflow has me creating a list of all tasks I want to get done, and then I drag them to either Done or some follow-up/waiting area for future action. So I only see things I can act on now.
My list of things to action can get quite long, and when I’m processing follow-up items, I would have a hard time dragging them to Done because it was too far away in the sidebar. So I wished I would make the Done float with me, so I could always have a quick way of pulling it there.</description><content:encoded><![CDATA[<p>I use <a href="https://roamresearch.com/">Roam Research</a> as my primary way of thinking and keeping track of
tasks. My current workflow has me creating a list of all tasks I want
to get done, and then I drag them to either <code>Done</code> or some follow-up/waiting
area for future action. So I only see things I can act on now.</p>
<p>My list of things to action can get quite long, and when I&rsquo;m processing
follow-up items, I would have a hard time dragging them to <code>Done</code> because it
was too far away in the sidebar. So I wished I would make the <code>Done</code> float
with me, so I could always have a quick way of pulling it there.</p>
<p>While looking at the custom styling in the <a href="https://twitter.com/RoamBookClub">Roam Book Club</a> graph, I found
the <code>#sticky</code> tag, which will make a block sticky over the children in the
block. So I took that CSS and modified it slightly to support making a
block float, so I could have it stay with me in the sidebar.</p>
<p>An example of how it looks in my sidebar before my new tags:
<figure><img src="/img/2022/02/05/01-bare-listing.png"
    alt="Unmodified list">
</figure>
</p>
<p>With the <code>#sticky</code> tag from the Roam Book Club, I can make for example
<code>Tasks</code> float when I&rsquo;m scrolling inside the tasks. This can be super helpful
for keeping track of where you are (they use it for daily chat pages) but
not quite what I need:
<figure><img src="/img/2022/02/05/02-sticky.png"
    alt="#sticky">
</figure>
</p>
<p>Introducing <code>#sticky-block</code>, which will make the block and all its children
float along at the top of the page, so I can always drag things there quickly:
<figure><img src="/img/2022/02/05/03-sticky-block.png"
    alt="#sticky-block">
</figure>
</p>
<div class=" copy-enabled">
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-css" data-lang="css"><span class="line"><span class="cl"><span class="c">/* To make blocks float, a constant in the sidebar */</span>
</span></span><span class="line"><span class="cl"><span class="p">.</span><span class="nc">roam-block-container</span><span class="o">[</span><span class="nt">data-page-links</span><span class="o">*=</span><span class="s2">&#34;sticky-block&#34;</span><span class="o">]</span>
</span></span><span class="line"><span class="cl"><span class="c">/* To make the a block float over its children. 
</span></span></span><span class="line"><span class="cl"><span class="c">   So you see which block you&#39;re currently inside.  
</span></span></span><span class="line"><span class="cl"><span class="c">   The weird \&#34; is because it would match both sticky and stick-block and   
</span></span></span><span class="line"><span class="cl"><span class="c">   double up the rendering. 
</span></span></span><span class="line"><span class="cl"><span class="c">   */</span>
</span></span><span class="line"><span class="cl"><span class="o">,</span> <span class="p">.</span><span class="nc">roam-block-container</span><span class="o">[</span><span class="nt">data-page-links</span><span class="o">*=</span><span class="s2">&#34;sticky\&#34;&#34;</span><span class="o">]</span> <span class="o">&gt;</span> <span class="p">.</span><span class="nc">rm-block-main</span>
</span></span><span class="line"><span class="cl"> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="k">position</span><span class="p">:</span> <span class="kc">sticky</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">  <span class="k">top</span><span class="p">:</span> <span class="mi">0</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">  <span class="k">z-index</span><span class="p">:</span> <span class="mi">11</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">  <span class="k">background</span><span class="p">:</span> <span class="kc">white</span><span class="p">;</span> <span class="c">/* Might have to be adjusted for your theme */</span>
</span></span><span class="line"><span class="cl">  <span class="k">padding-top</span><span class="p">:</span> <span class="mi">4</span><span class="kt">px</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">  <span class="k">padding-bottom</span><span class="p">:</span> <span class="mi">2</span><span class="kt">px</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">  <span class="k">margin-bottom</span><span class="p">:</span> <span class="mi">10</span><span class="kt">px</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">  <span class="k">box-shadow</span><span class="p">:</span> 
</span></span><span class="line"><span class="cl">    <span class="mi">0</span> <span class="mi">1</span><span class="kt">px</span> <span class="mi">1</span><span class="kt">px</span> <span class="nb">rgba</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mf">0.034</span><span class="p">),</span> 
</span></span><span class="line"><span class="cl">    <span class="mi">0</span> <span class="mi">1</span><span class="kt">px</span> <span class="mi">1</span><span class="kt">px</span> <span class="nb">rgba</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mf">0.048</span><span class="p">),</span> 
</span></span><span class="line"><span class="cl">    <span class="mi">0</span> <span class="mf">12.5</span><span class="kt">px</span> <span class="mi">10</span><span class="kt">px</span> <span class="nb">rgba</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mf">0.06</span><span class="p">),</span> 
</span></span><span class="line"><span class="cl">    <span class="mi">0</span> <span class="mf">12.3</span><span class="kt">px</span> <span class="mf">7.9</span><span class="kt">px</span> <span class="nb">rgba</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mf">0.072</span><span class="p">),</span> 
</span></span><span class="line"><span class="cl">    <span class="mi">0</span> <span class="mf">7.8</span><span class="kt">px</span> <span class="mf">10.4</span><span class="kt">px</span> <span class="nb">rgba</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mf">0.086</span><span class="p">),</span> 
</span></span><span class="line"><span class="cl">    <span class="mi">0</span> <span class="mi">5</span><span class="kt">px</span> <span class="mi">120</span><span class="kt">px</span> <span class="nb">rgba</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mf">0.12</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c">/* Hides the tag since it&#39;s not important in the output, and there is 
</span></span></span><span class="line"><span class="cl"><span class="c">special styling so it&#39;s clear that something extra is going on. */</span>
</span></span><span class="line"><span class="cl"><span class="nt">span</span><span class="p">.</span><span class="nc">rm-page-ref</span><span class="o">[</span><span class="nt">data-tag</span><span class="o">=</span><span class="s2">&#34;sticky-block&#34;</span><span class="o">],</span>
</span></span><span class="line"><span class="cl"><span class="nt">span</span><span class="p">.</span><span class="nc">rm-page-ref</span><span class="o">[</span><span class="nt">data-tag</span><span class="o">=</span><span class="s2">&#34;sticky&#34;</span><span class="o">]</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="k">display</span><span class="p">:</span> <span class="kc">none</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span></span></span></code></pre></div>
</div>
<p><strong>Note:</strong> This CSS currently does not support having both <code>#sticky-block</code> and
<code>#sticky</code> for the same page, they will overlap each other, and the second
element will block the first. I don&rsquo;t currently need this, so I haven&rsquo;t spent
the time. :)</p>
]]></content:encoded></item><item><title>How to find your Apple Watch when you've misplaced it — How I ended up being a sonar bear</title><link>https://bjorn.now/blog/2015/11/14/how-to-ping-a-lost-apple-watch/?utm_medium=feed</link><pubDate>Sat, 14 Nov 2015 21:03:42 +0800</pubDate><guid>https://bjorn.now/blog/2015/11/14/how-to-ping-a-lost-apple-watch/</guid><description>I just misplaced my Apple Watch in my apartment. Walking around looking for it, and getting frustrated, I was thinking, “There ought to be a way to make it make some sound, then I can play Marco Polo with the watch”. But my Google-fu failed me: no obvious way to “ping my watch”. There’s functionality for pinging the phone from the watch though.
But there’s a way to make some noise: testing out the alert volume in the companion app on the phone.</description><content:encoded><![CDATA[<p>I just misplaced my Apple Watch in my apartment. Walking around looking for it,
and getting frustrated, I was thinking, &ldquo;There ought to be a way to make it make
some sound, then I can play <a href="https://en.wikipedia.org/wiki/Marco_Polo_(game)">Marco Polo</a> with the watch&rdquo;. But my Google-fu
failed me: no obvious way to &ldquo;ping my watch&rdquo;. There&rsquo;s functionality for
<a href="https://www.youtube.com/watch?v=KYf4q4y7dM4">pinging the phone</a> from the watch though.</p>
<p>But there&rsquo;s a way to make some noise: testing out the alert volume in the
companion app on the phone.</p>
<h2 id="steps">
  Steps
  <a href="#steps" class="heading-anchor" aria-label="Direct link to Steps"><span aria-hidden="true">#</span></a>
</h2>
<ol>
<li>
<p>Verify that your Watch is paired:</p>
<ol>
<li>Settings</li>
<li>Bluetooth</li>
<li>My Devices should show your Watch:
<figure><img src="/img/2015/11/bluetooth-settings.png"
          alt="Bluetooth settings on an iPhone">
      </figure>
</li>
</ol>
</li>
<li>
<p>Open the Apple Watch app:</p>
<ol>
<li>Sounds &amp; Haptics</li>
<li>Drag the slider for alert volume to max</li>
<li>Tap the knob on the slider.<br>
The watch will make a sound every time you tap the knob.
<figure><img src="/img/2015/11/alert-volume-settings.png"
          alt="Alert volume settings">
      </figure>
</li>
</ol>
</li>
<li>
<p>Don&rsquo;t cry yourself to sleep over losing an expensive toy in the mess that is
your own apartment.</p>
</li>
</ol>
]]></content:encoded></item><item><title>Using multiple versions of Python on Snap CI — Why just play with one snake?</title><link>https://bjorn.now/blog/2015/10/27/testing-against-many-versions-of-python-on-snap-ci/?utm_medium=feed</link><pubDate>Tue, 27 Oct 2015 08:00:00 +0800</pubDate><guid>https://bjorn.now/blog/2015/10/27/testing-against-many-versions-of-python-on-snap-ci/</guid><description>I’m running an open source project called gocd-cli which is, well, a command line interface for Go continuous delivery. The intention is to make it easier to handle common tasks around Go. But this post isn’t really about that.
This project is supposed to be as portable as I can make it, becuse the original need I felt for it was born on RHEL6. Which is blessed with Python 2.6 by default. And we should definitely be looking to the future, meaning supporting Python 3, and I luckily got a pull request for just that. Since I couldn’t find any free hosted Go around I went for the next best thing, Snap CI, which is also built by ThoughtWorks.</description><content:encoded><![CDATA[<p>I&rsquo;m running an open source project called <a href="https://github.com/gaqzi/gocd-cli">gocd-cli</a> which is, well, a command
line interface for <a href="http://www.go.cd/">Go continuous delivery</a>. The intention is to make it
easier to handle common tasks around Go. But this post isn&rsquo;t really about that.</p>
<p>This project is supposed to be as portable as I can make it, becuse the original
need I felt for it was born on RHEL6. Which is <em>blessed</em> with Python 2.6 by
default. And we should definitely be looking to the future, meaning supporting
Python 3, and I luckily got a <a href="https://github.com/gaqzi/py-gocd/pull/6">pull request</a> for just that. Since I couldn&rsquo;t
find any free hosted Go around I went for the next best thing, <a href="https://snap-ci.com/">Snap CI</a>,
which is also built by <a href="https://en.wikipedia.org/wiki/ThoughtWorks">ThoughtWorks</a>.</p>
<p>Easy to get going and works well, it&rsquo;s not Go, but it&rsquo;s definitely the easy
starter drug for people that thinks <a href="https://en.wikipedia.org/wiki/Jenkins_(software)">Jenkins</a> or <a href="https://en.wikipedia.org/wiki/Travis_CI">Travis</a> is good enough.</p>
<h2 id="the-solution">
  The solution
  <a href="#the-solution" class="heading-anchor" aria-label="Direct link to The solution"><span aria-hidden="true">#</span></a>
</h2>
<p>Snap supports all the versions of Python that I want by default, but they
weren&rsquo;t all being made available at the same time. Python 2.6 and 2.7
were always there, but only one version of Python 3 at a time.</p>
<p>Luckily logging in to the <a href="https://blog.snap-ci.com/blog/2014/08/11/introducing-snap-shell/">snap-shell</a> I found that all Python versions were
available in <code>/opt/local/python/&lt;version&gt;</code>.</p>
<p>A quick change to the Snap config, to add these to my <code>$PATH</code>, and <a href="https://tox.readthedocs.org/en/latest/">tox</a> ran
through without any problems.</p>
<figure><img src="/img/2015/10/snap-console.png"
    alt="New Snap CI config">
</figure>

<p>What you&rsquo;ll need to add to your command box. <strong>Note</strong> it didn&rsquo;t work for me when
I added it to the environment variable field, I&rsquo;m guessing it&rsquo;s because the <code>$PATH</code>
variable doesn&rsquo;t get interpolated correctly.</p>
<div class=" copy-enabled">
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="nb">export</span> <span class="nv">PATH</span><span class="o">=</span><span class="s2">&#34;</span><span class="nv">$PATH</span><span class="s2">:/opt/local/python/3.3.5/bin:/opt/local/python/3.4.0/bin:/opt/local/python/3.5.0/bin&#34;</span></span></span></code></pre></div>
</div>
<p>And look, my tests are passing!</p>
<p><a href="https://snap-ci.com/gaqzi/py-gocd/branch/master"><figure><img src="/img/2015/10/snap-success.png"
    alt="Passing tests">
</figure>
</a></p>
]]></content:encoded></item><item><title>Python and its included batteries</title><link>https://bjorn.now/blog/2013/01/18/python-and-its-included-batteries/?utm_medium=feed</link><pubDate>Fri, 18 Jan 2013 16:22:32 +0000</pubDate><guid>https://bjorn.now/blog/2013/01/18/python-and-its-included-batteries/</guid><description>I’ve for almost a year now been using Python as my day to day language as it’s what is used at my job. I’m starting to come around to really liking most parts of Python now, although there is definitely parts that are way more clunky than I would expect from a language so popular. And especially given how much talk there’s about the included batteries and to a small extent how there seems to bit of dickishness involved with hard core Python people. For an example of the latter just:</description><content:encoded><![CDATA[<p>I&rsquo;ve for almost a year now been using <a href="http://en.wikipedia.org/wiki/Python_(programming_language)">Python</a> as my day to day language as it&rsquo;s
what is used at my job. I&rsquo;m starting to come around to really liking most parts
of Python now, although there is definitely parts that are way more clunky than
I would expect from a language so popular. And especially given how much talk
there&rsquo;s about the <em><a href="http://www.python.org/about/">included batteries</a></em> and to a small extent how there seems
to bit of dickishness involved with hard core Python people. For an example of
the latter just:</p>
<div class=" copy-enabled">
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">$ python  
</span></span><span class="line"><span class="cl">&gt;&gt;&gt; <span class="nb">exit</span>  
</span></span><span class="line"><span class="cl">Use exit<span class="o">()</span> or Ctrl-D <span class="o">(</span>i.e. EOF<span class="o">)</span> to exit</span></span></code></pre></div>
</div>
<p>Clearly the interpreter known what I want to achieve, and at the same time it&rsquo;s
programmed to give of an error message telling me I was doing it wrong. And for
it to work I need to say, &ldquo;pretty please&rdquo;. What annoys me with this is just the
simple fact that someone figured out people were having a problem with this, so
they added in a message instead of just doing what is expected. You just don&rsquo;t
behave in that way in everyday life, you&rsquo;d get slapped.</p>
<h2 id="standard-library">
  Standard library
  <a href="#standard-library" class="heading-anchor" aria-label="Direct link to Standard library"><span aria-hidden="true">#</span></a>
</h2>
<p>Time handling in Python is atrocious, do get it working halfway decently you
need to install at least two external libraries. <a href="http://pytz.sourceforge.net/">pytz</a> and <a href="http://labix.org/python-dateutil">python-dateutil</a>,
and while I can understand why pytz might be a good candidate for being external
(for allowing a higher pace of updates since timezone/<a href="http://en.wikipedia.org/wiki/DST">DST</a> changes happens
every so often in different parts of the world).</p>
<p>Today I just wanted to do a diff between two dates and see the years between
them, so I figured I would use <a href="http://docs.python.org/2/library/datetime.html">timedelta</a> since it sounds like something that
should be part of that. Oh, really, it&rsquo;s not? I can diff weeks but not months or
years? So I ended up using dateutil&rsquo;s <a href="http://labix.org/python-dateutil#head-ba5ffd4df8111d1b83fc194b97ebecf837add454">relativedelta</a> instead.</p>
<p>So far the worst way I&rsquo;ve been bitten was with the built-in e-mail library, this
ended up with me sending a copy of the e-mail addresses of 150 customers by
mistake.</p>
<div class=" copy-enabled">
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="kn">import</span> <span class="nn">smtplib</span>
</span></span><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">email.mime.text</span> <span class="kn">import</span> <span class="n">MIMEText</span>
</span></span><span class="line"><span class="cl">  
</span></span><span class="line"><span class="cl"><span class="n">msg</span> <span class="o">=</span> <span class="n">MIMEText</span><span class="p">(</span><span class="s1">&#39;Ho ho ho, merry christmas&#39;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">msg</span><span class="p">[</span><span class="s1">&#39;Subject&#39;</span><span class="p">]</span> <span class="o">=</span> <span class="s2">&#34;You&#39;re wished something special!&#34;</span>
</span></span><span class="line"><span class="cl"><span class="n">msg</span><span class="p">[</span><span class="s1">&#39;From&#39;</span><span class="p">]</span> <span class="o">=</span> <span class="s1">&#39;noreply@otherdomain.tld&#39;</span>
</span></span><span class="line"><span class="cl"><span class="n">msg</span><span class="p">[</span><span class="s1">&#39;To&#39;</span><span class="p">]</span> <span class="o">=</span> <span class="s1">&#39;user-1@domain.tld&#39;</span>
</span></span><span class="line"><span class="cl"><span class="n">msg</span><span class="p">[</span><span class="s1">&#39;To&#39;</span><span class="p">]</span> <span class="o">=</span> <span class="s1">&#39;user-2@domain.tld&#39;</span>
</span></span><span class="line"><span class="cl">  
</span></span><span class="line"><span class="cl"><span class="nb">print</span> <span class="n">msg</span><span class="o">.</span><span class="n">as_string</span><span class="p">()</span></span></span></code></pre></div>
</div>
<p>Output:</p>
<div class=" copy-enabled">
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-plain" data-lang="plain"><span class="line"><span class="cl">Content-Type: text/plain; charset=&#34;us-ascii&#34;
</span></span><span class="line"><span class="cl">MIME-Version: 1.0
</span></span><span class="line"><span class="cl">Content-Transfer-Encoding: 7bit
</span></span><span class="line"><span class="cl">Subject: You&#39;re wished something special!
</span></span><span class="line"><span class="cl">From: noreply@otherdomain.tld
</span></span><span class="line"><span class="cl">To: user-1@domain.tld
</span></span><span class="line"><span class="cl">To: user-2@domain.tld
</span></span><span class="line"><span class="cl">   
</span></span><span class="line"><span class="cl">Ho ho ho, merry christmas</span></span></code></pre></div>
</div>
<p>Because appending when doing an assignment is just what you&rsquo;d expect. This from
the same people that needs to tell me I can&rsquo;t type <code>exit</code> but I need to type
<code>exit()</code>.</p>
<p>Another thing I wanted to use was a windowing function when iterating over a
list, instead of having it implemented in <a href="http://docs.python.org/2.7/library/itertools.html">itertools</a> the documentation for
itertools explains how I can
<a href="http://docs.python.org/release/2.3.5/lib/itertools-example.html">implement my own</a>.
Why!? If you&rsquo;re taking the time to show of an implementation why should I have
to copy that piece of code into my own codebase? What about <a href="http://www.python.org/dev/peps/pep-0020/">The Zen of Python</a>:</p>
<blockquote>
<p>There should be one&ndash; and preferably only one &ndash;obvious way to do it.</p>
</blockquote>
<h3 id="pep8">
  PEP8
  <a href="#pep8" class="heading-anchor" aria-label="Direct link to PEP8"><span aria-hidden="true">#</span></a>
</h3>
<p>I really like <a href="http://www.python.org/dev/peps/pep-0008/">PEP8</a>, I don&rsquo;t agree with all parts of it but I can change. But
I&rsquo;m really looking forward to when the standard library finally is PEP8
compliant.</p>
<h2 id="documentation">
  Documentation
  <a href="#documentation" class="heading-anchor" aria-label="Direct link to Documentation"><span aria-hidden="true">#</span></a>
</h2>
<p>Which brings me to documentation. For the longest time I couldn&rsquo;t understand why
I had such problems understanding the Python documentation, whether it was the
standard library or any third party library. When I was coding <a href="http://en.wikipedia.org/wiki/Ruby_(programming_language)">Ruby</a> I never
felt this stupid when reading documentation. (But I&rsquo;ve been told I&rsquo;m excused
from feeling stupid when reading the <a href="http://docs.sqlalchemy.org/">SQLAlchemy documentation</a>, apparently it&rsquo;s
not just me there.)</p>
<p>But I think I&rsquo;ve figured it why I find the documentation so hard to understand
now. When I&rsquo;m reading documentation for Ruby I generally have the source code
just one mouse click away. So when I wasn&rsquo;t understanding what the documentation
was saying I just flipped on the source code and I could figure it out.</p>
<figure><img src="/img/2013/Screenshot_2013-01-19_00_30.png"
    alt="Ruby documentation source example">
</figure>

<p>With most Python libraries I&rsquo;ve had to do something similar to this:</p>
<ul>
<li>Dig through to wherever the source is saved</li>
<li>Find the correct version for the documentation I&rsquo;m reading(missed version of source I&rsquo;m reading more than once)</li>
<li>Find exactly where this thing I want to read about is is defined.</li>
<li>Start reading the source</li>
<li>???</li>
<li>Enlightenment.</li>
</ul>
<p>It&rsquo;s a general &ldquo;truth&rdquo; that documentation in general sucks, so it&rsquo;s just better
read the source. But somewhere along the way I stopped doing that, probably
around the same time I started using Rails and was amazed at how good the
documentation was. And without thinking about it I was also already reading the
source code, kind of, by having it readily available with the documentation.</p>
<p>My recommendation is for all of you out there using <a href="http://sphinx-doc.org/">Sphinx</a> for your
documentation needs, please enable
<a href="http://sphinx-doc.org/latest/ext/viewcode.html">sphinx.ext.viewcode</a> to output
the source with your documentation.</p>
<p>The other thing I would like to say is the obvious thing, reading the source is
great. I really started to enjoy using Django when I decided to just use the
documentation to find whatever something was called and then use the source code
to figure out how things work. Source code is almost like a painting, a line of
code can say more than a thousand words. ;)</p>
<h2 id="wrapping-up">
  Wrapping up
  <a href="#wrapping-up" class="heading-anchor" aria-label="Direct link to Wrapping up"><span aria-hidden="true">#</span></a>
</h2>
<p>I&rsquo;m not the first one to feel dismayed about Python, <a href="http://zedshaw.com/">Zed Shaw</a> has a great post
called
<a href="http://zedshaw.com/essays/curing_pythons_neglect.html">Curing Python&rsquo;s Neglect</a>
that is well worth a read. (He mentions Lamson 0.9 in the post and that was
released in 2009. As far as I can tell most of the things in his post is still a
problem.)</p>
<p>As I understand a lot of making Python more streamlined is being put into Python
3, that just hasn&rsquo;t been adopted by the majority of developers yet.</p>
<p>We Ruby people had the same thing with 1.9, and I noticed people really starting
to switch about the same time as <a href="https://rvm.io/">RVM</a> started to be used. All of a sudden there
was a really easy way to test things with any possible combination of Ruby you
wanted.</p>
<p>All around I&rsquo;m really enjoying working with Python, again (Python was the
language I used to replace PHP/Perl way back when, then Python was replaced by
Ruby). I used to really dislike having to import every single line of code I
use, but since I&rsquo;m a sucker for having things overly clear I&rsquo;ve gotten round to
it now. For some reason I never had an issue with the white spaces, I want the
code I read to be consistent. And that&rsquo;s one way of trying to ensure that.</p>
<p>Now that I&rsquo;ve done the virtual variant of screaming out into the night I&rsquo;ll go
and try to be productive instead.</p>
<figure><img src="/img/2013/classy-python.png"
    alt="Classy Python">
</figure>

]]></content:encoded></item><item><title>Keeping your Varnish farms configuration in sync</title><link>https://bjorn.now/blog/2011/01/07/keeping-your-varnish-farm-in-sync/?utm_medium=feed</link><pubDate>Fri, 07 Jan 2011 01:21:46 +0000</pubDate><guid>https://bjorn.now/blog/2011/01/07/keeping-your-varnish-farm-in-sync/</guid><description>I spend some of my free time helping out with server administration and programming for the Swedish Pirate Party, Piratpartiet (look ma, X-Varnish), and we use Varnish for our caches. At the moment it’s just used on some of our projects, but we’re going to migrate most of our projects to be behind our Varnish caches.
The thing is though that we got two hosts running the exact same configuration, and we’d like to keep those hosts in sync. A quick search on Google gave me nothing for syncing varnish configuration over several hosts, or a farm as it were. So I took matters into my own hands and wrote varnishsync, a little bash script that uses rsync and ssh to sync the configuration folder and then to load and use the new configuration.</description><content:encoded><![CDATA[<p>I spend some of my free time helping out with server administration and
programming for the <a href="https://secure.wikimedia.org/wikipedia/en/wiki/Piratpartiet">Swedish Pirate Party</a>, <a href="http://www.piratpartiet.se/">Piratpartiet</a> (<em>look ma</em>,
X-Varnish), and we use <a href="http://www.varnish-cache.org/">Varnish</a> for our caches. At the moment it&rsquo;s just used on
some of our projects, but we&rsquo;re going to migrate most of our projects to be
behind our Varnish caches.</p>
<p>The thing is though that we got two hosts running the exact same configuration,
and we&rsquo;d like to keep those hosts in sync. A quick search on Google gave me
nothing for syncing varnish configuration over several hosts, or a farm as it
were. So I took matters into my own hands and wrote <a href="https://github.com/gaqzi/varnishsync">varnishsync</a>, a little bash
script that uses rsync and ssh to sync the configuration folder and then to load
and use the new configuration.</p>
<p>Please have a look at the <a href="https://github.com/gaqzi/varnishsync">Github project</a> for the latest version
and usage, and if you&rsquo;ve any questions or suggestions drop a line here or on
Github.</p>
]]></content:encoded></item><item><title>How to: Setting up a Wikileaks mirror</title><link>https://bjorn.now/blog/2010/12/05/howto-setting-up-a-wikileaks-mirror/?utm_medium=feed</link><pubDate>Sun, 05 Dec 2010 21:11:18 +0000</pubDate><guid>https://bjorn.now/blog/2010/12/05/howto-setting-up-a-wikileaks-mirror/</guid><description>Yesterday I wrote a quick step-by-step guide for installing apache and setting your system up for Wikileaks mass-mirror project in Swedish and I’ve had requests for it to be translated into English.
This guide assumes you’re running Ubuntu or a Debian based system.
I’ve made a script that does all the steps below, feel free to use it!
The script: http://sanitarium.se/files/wikileaks-mirror.sh
Usage: wget http://sanitarium.se/files/wikileaks-mirror.sh &amp;&amp; chmod +x wikileaks-mirror.sh &amp;&amp; ./wikileaks-mirror.sh
Installing Apache and adding the wikileaks user # # is the prompt as root:</description><content:encoded><![CDATA[<p>Yesterday I wrote a quick step-by-step guide for installing apache and setting
your system up for <a href="http://213.251.145.96/Mass-mirroring-Wikileaks.html">Wikileaks mass-mirror project</a> in Swedish and
I&rsquo;ve had requests for it to be translated into English.</p>
<p>This guide assumes you&rsquo;re running Ubuntu or a Debian based system.</p>
<p>I&rsquo;ve made a script that does all the steps below, feel free to use it!<br>
The script: <a href="http://sanitarium.se/files/wikileaks-mirror.sh">http://sanitarium.se/files/wikileaks-mirror.sh</a><br>
Usage: <code>wget http://sanitarium.se/files/wikileaks-mirror.sh &amp;&amp; chmod +x wikileaks-mirror.sh &amp;&amp; ./wikileaks-mirror.sh</code></p>
<h1 id="installing-apache-and-adding-the-wikileaks-user">
  Installing Apache and adding the wikileaks user
  <a href="#installing-apache-and-adding-the-wikileaks-user" class="heading-anchor" aria-label="Direct link to Installing Apache and adding the wikileaks user"><span aria-hidden="true">#</span></a>
</h1>
<p><code>#</code> is the prompt as root:</p>
<div class=" copy-enabled">
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">youruser$ sudo -s  </span></span></code></pre></div>
</div>
<p>Install apache if it&rsquo;s not installed</p>
<div class=" copy-enabled">
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl"><span class="c1"># apt-get install apache2  </span></span></span></code></pre></div>
</div>
<p>Add a wikileaks user, write down where the home folder is created.</p>
<div class=" copy-enabled">
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl"><span class="c1"># adduser --disabled-password wikileaks  </span>
</span></span><span class="line"><span class="cl"><span class="c1"># su wikileaks  </span>
</span></span><span class="line"><span class="cl">wikileaks$ mkdir ~/.ssh ~/www  
</span></span><span class="line"><span class="cl">wikileaks$ chmod <span class="m">0700</span> ~/.ssh  
</span></span><span class="line"><span class="cl">wikileaks$ wget http://213.251.145.96/IMG/id_rsa.pub -O ~/.ssh/authorized_keys  
</span></span><span class="line"><span class="cl">wikileaks$ <span class="nb">exit</span>  </span></span></code></pre></div>
</div>
<p>Time to add the apache site for wikileaks</p>
<div class=" copy-enabled">
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl"><span class="c1"># cd /etc/apache2/sites-available/   </span>
</span></span><span class="line"><span class="cl"><span class="c1"># wget http://sanitarium.se/files/wikileaks   </span></span></span></code></pre></div>
</div>
<p>Open the file <code>wikileaks</code> with an editor and change the path to the wikileaks users home folder if it&rsquo;s not <code>/home/wikileaks</code>.<br>
This file assumes you want to host wikileaks.org, if you want to host wikileaks.yourdomain.com you&rsquo;ll need to set <code>ServerName</code> accordingly and if necessary <code>ServerAlias</code>, ServerAlias is optional and you can remove that row if you don&rsquo;t use it.<br>
When that is done you just need to activate the site in apache:</p>
<div class=" copy-enabled">
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl"><span class="c1"># a2ensite wikileaks  </span></span></span></code></pre></div>
</div>
<p>And reload apache so it knows that the new site has been added:</p>
<div class=" copy-enabled">
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl"><span class="c1"># /etc/init.d/apache2 reload  </span></span></span></code></pre></div>
</div>
<p>The <code>wikileaks</code> file downloaded from this server looks like this:</p>
<div class=" copy-enabled">
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">&lt;VirtualHost *:80&gt;
</span></span><span class="line"><span class="cl">    DocumentRoot /home/wikileaks/www
</span></span><span class="line"><span class="cl">    ServerName wikileaks.org
</span></span><span class="line"><span class="cl">    ServerAlias www.wikileaks.org
</span></span><span class="line"><span class="cl">    ErrorLog /dev/null
</span></span><span class="line"><span class="cl">    CustomLog /dev/null common
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    &lt;Directory /home/wikileaks/www&gt;
</span></span><span class="line"><span class="cl">        AllowOverride None
</span></span><span class="line"><span class="cl">    &lt;/Directory&gt;
</span></span><span class="line"><span class="cl">&lt;/VirtualHost&gt;</span></span></code></pre></div>
</div>
<h1 id="now-tell-wikileaks-about-your-mirror">
  Now tell Wikileaks about your mirror
  <a href="#now-tell-wikileaks-about-your-mirror" class="heading-anchor" aria-label="Direct link to Now tell Wikileaks about your mirror"><span aria-hidden="true">#</span></a>
</h1>
<p>Go to the <a href="http://213.251.145.96/Mass-mirroring-Wikileaks.html">form</a> and fill it as the picture, set the IP-address of your server in the IP-field:<br>
<figure><img src="/img/2010/installning-hos-wikileaks.png"
    alt="Wikileaks mirror settings">
</figure>
</p>
<p>If you&rsquo;re having any troubles don&rsquo;t hesitate to ask in the comments!</p>
]]></content:encoded></item><item><title>How to: Sätta upp en Wikileaksspegling</title><link>https://bjorn.now/blog/2010/12/04/howto-satta-upp-en-wikileaksspegling/?utm_medium=feed</link><pubDate>Sat, 04 Dec 2010 21:27:26 +0000</pubDate><guid>https://bjorn.now/blog/2010/12/04/howto-satta-upp-en-wikileaksspegling/</guid><description>A translation of this post is available in English.
En kommentar i Rick Falkvinges inlägg Fullt krig om informationen så efterfrågades en snabbguide för att dra igång en Wikileaksspegling i ett vanligt Linuxssystem.
Jag utgår från Ubuntu eftersom det är vanligt och vad jag själv använder.
Uppdatering 2010-12-05: Jag har gjort ett script som utför alla stegen nedan, använd gärna det!
Filen: http://sanitarium.se/files/wikileaks-mirror.sh
Användning: wget http://sanitarium.se/files/wikileaks-mirror.sh &amp;&amp; chmod +x wikileaks-mirror.sh &amp;&amp; ./wikileaks-mirror.sh
Installera Apache # # är prompten som root:</description><content:encoded><![CDATA[<p><a href="/blog/2010/12/05/howto-setting-up-a-wikileaks-mirror/">A translation of this post is available in English.</a></p>
<p>En <a href="http://rickfalkvinge.se/2010/12/04/fullt-krig-om-informationen/#comment-57303">kommentar</a> i Rick Falkvinges inlägg <a href="http://rickfalkvinge.se/2010/12/04/fullt-krig-om-informationen/">Fullt krig om informationen</a>
så efterfrågades en snabbguide för att dra igång en Wikileaksspegling i ett
vanligt Linuxssystem.</p>
<p>Jag utgår från Ubuntu eftersom det är vanligt och vad jag själv använder.</p>
<p><strong>Uppdatering 2010-12-05:</strong> Jag har gjort ett script som utför alla stegen nedan, använd gärna det!<br>
Filen: <a href="http://sanitarium.se/files/wikileaks-mirror.sh">http://sanitarium.se/files/wikileaks-mirror.sh</a><br>
Användning: <code>wget http://sanitarium.se/files/wikileaks-mirror.sh &amp;&amp; chmod +x wikileaks-mirror.sh &amp;&amp; ./wikileaks-mirror.sh</code></p>
<h1 id="installera-apache">
  Installera Apache
  <a href="#installera-apache" class="heading-anchor" aria-label="Direct link to Installera Apache"><span aria-hidden="true">#</span></a>
</h1>
<p><code>#</code> är prompten som root:</p>
<div class=" copy-enabled">
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">dinanvändare$ sudo -s  </span></span></code></pre></div>
</div>
<p>Installera apache om det inte är installerat</p>
<div class=" copy-enabled">
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># apt-get install apache2  </span></span></span></code></pre></div>
</div>
<p>Lägg till en användare för Wikileak och anteckna var wikileaks hemmap skapas!</p>
<div class=" copy-enabled">
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># adduser --disabled-password wikileaks  </span>
</span></span><span class="line"><span class="cl"><span class="c1"># su wikileaks  </span>
</span></span><span class="line"><span class="cl">wikileaks$ mkdir ~/.ssh ~/www  
</span></span><span class="line"><span class="cl">wikileaks$ chmod <span class="m">0700</span> ~/.ssh  
</span></span><span class="line"><span class="cl">wikileaks$ wget http://213.251.145.96/IMG/id_rsa.pub -O ~/.ssh/authorized_keys  
</span></span><span class="line"><span class="cl">wikileaks$ <span class="nb">exit</span>  </span></span></code></pre></div>
</div>
<p>Nu är det dags att lägga till siten för wikileaks</p>
<div class=" copy-enabled">
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># cd /etc/apache2/sites-available/   </span>
</span></span><span class="line"><span class="cl"><span class="c1"># wget http://sanitarium.se/files/wikileaks   </span></span></span></code></pre></div>
</div>
<p>Öppna filen <code>wikileaks</code> med en editor och ändra sökvägen till användaren wikileaks hemmapp om det är något annat än <code>/home/wikileaks</code>.<br>
Den här filen förutsätter att du vill hosta wikileaks.org, om du vill hosta wikileaks.dindomän.se eller något sådant så får du ändra ServerName och eventuellt ServerAlias.<br>
När det är klart är det bara att aktivera siten för apache:</p>
<div class=" copy-enabled">
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># a2ensite wikileaks  </span></span></span></code></pre></div>
</div>
<p>Och till sist ladda om apache så det är redo att ta emot information:</p>
<div class=" copy-enabled">
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># /etc/init.d/apache2 reload  </span></span></span></code></pre></div>
</div>
<p>Innehållet i filen <code>wikileaks</code> som du tankar från den här servern är:</p>
<div class=" copy-enabled">
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">&lt;VirtualHost *:80&gt;
</span></span><span class="line"><span class="cl">    DocumentRoot /home/wikileaks/www
</span></span><span class="line"><span class="cl">    ServerName wikileaks.org
</span></span><span class="line"><span class="cl">    ServerAlias www.wikileaks.org
</span></span><span class="line"><span class="cl">    ErrorLog /dev/null
</span></span><span class="line"><span class="cl">    CustomLog /dev/null common
</span></span><span class="line"><span class="cl">    
</span></span><span class="line"><span class="cl">    &lt;Directory /home/wikileaks/www&gt;
</span></span><span class="line"><span class="cl">        AllowOverride None
</span></span><span class="line"><span class="cl">    &lt;/Directory&gt;
</span></span><span class="line"><span class="cl">&lt;/VirtualHost&gt;</span></span></code></pre></div>
</div>
<h1 id="hur-du-säger-till-wikileaks-att-allt-är-klart">
  Hur du säger till Wikileaks att allt är klart
  <a href="#hur-du-s%c3%a4ger-till-wikileaks-att-allt-%c3%a4r-klart" class="heading-anchor" aria-label="Direct link to Hur du säger till Wikileaks att allt är klart"><span aria-hidden="true">#</span></a>
</h1>
<p>Surfa in på <a href="http://213.251.145.96/Mass-mirroring-Wikileaks.html">formuläret</a> och fyll i det enligt nedan, byt ut ip-adress mot IP:t för din server:<br>
<figure><img src="/img/2010/installning-hos-wikileaks.png"
    alt="Wikileaks mirror settings">
</figure>
</p>
<p>Om det är några oklarheter eller funderingar så fråga på i kommentarsfältet
nedan så ska jag försöka hjälpa till att reda ut!</p>
<p><strong>Uppdatering:</strong> <a href="http://twitter.com/rouhivuori">Henrik Rouhivuori</a>
upplyste om att <code>aptitude</code> inte finns i Ubuntu 10.10 så jag ändrade till <code>apt-get</code>
istället. Övertydligt att jag fortfarande hänger på 10.04. :)</p>
<p><strong>Uppdatering 2:</strong>
Henrik Holst
berättade att du kan välja att inaktivera lösenordsinloggning när du skapar
nya användare, så nu görs det istället för att rekommendera ett slumpmässigt
långt lösenord!</p>
<p><strong>Uppdatering 3:</strong>
David Vrensk
påminner om att <code>sshd</code> är lite petig på att det bara är användaren som ska ha
rättigheter till <code>.ssh</code>-mappen.</p>
<p><strong>Uppdatering 4:</strong> Av bara farten så hade jag missat att lägga till att man
måste skapa mappen <code>www</code> som wikileaks-filen pekar på, samt <code>common</code> efter
loggraden. Tack till <a href="http://harfagre.wordpress.com/">Gustav Wetter</a> som jobbet igenom hela guiden!</p>
]]></content:encoded></item><item><title>How to disable bold fonts in xterm — Why sometimes repeating yourself really does get the message through</title><link>https://bjorn.now/blog/2010/02/21/how-to-disable-bold-fonts-in-xterm/?utm_medium=feed</link><pubDate>Sun, 21 Feb 2010 22:36:19 +0000</pubDate><guid>https://bjorn.now/blog/2010/02/21/how-to-disable-bold-fonts-in-xterm/</guid><description>I’ve so far in all my travels never found a terminal I like better than xterm, maybe urxvt, but I’ve had a problem disabling bold fonts for a very long time. Update: Seems this is a known bug for xterm.
There are a lot of X resource configuration options that makes you think you can disable bold fonts, but alas, I’ve had no luck with any of them. But if you set the same font for both normal and bold fonts it’ll work! This little gem of knowledge I found as an off-hand remark on a configuration page by Emil Mikulic, thank you!</description><content:encoded><![CDATA[<p>I&rsquo;ve so far in all my travels never found a terminal I like better than <a href="http://en.wikipedia.org/wiki/Xterm">xterm</a>, maybe <a href="http://en.wikipedia.org/wiki/Rxvt-unicode">urxvt</a>, but I&rsquo;ve had a problem disabling bold fonts for a very long time. <strong>Update:</strong> Seems this is a <a href="http://bugs.debian.org/cgi-bin/bugreport.cgi?bug=347790">known bug for xterm</a>.</p>
<p>There are a lot of X resource configuration options that makes you think you can disable bold fonts, but alas, I&rsquo;ve had no luck with any of them. But if you set <strong>the same font</strong> for both normal and bold fonts it&rsquo;ll work! This little gem of knowledge I found as an off-hand remark on a <a href="http://dmr.ath.cx/notes/xterm.html">configuration page</a> by <a href="http://dmr.ath.cx/">Emil Mikulic</a>, thank you!</p>
<p>I like the normal <code>fixed</code> font, I&rsquo;ve tried many terminal fonts but I&rsquo;ve always gotten back to it. So I simply tucked:</p>
<pre><code>xterm*font: fixed
xterm*boldFont: fixed
</code></pre>
<p>Into my <code>~/.Xresources</code> and afterwards ran <code>xrdb -load ~/.Xresources</code> and started up a new xterm loaded with awesomeness!</p>
<figure><img src="/img/2010/xterms.png"
    alt="xterm with bold font and xterm without bold font">
</figure>

<p>The before and after shot.</p>
]]></content:encoded></item></channel></rss>