<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="4.4.1">Jekyll</generator><link href="/feed.xml" rel="self" type="application/atom+xml" /><link href="/" rel="alternate" type="text/html" /><updated>2026-06-30T08:19:16+00:00</updated><id>/feed.xml</id><title type="html">Claes Adamsson</title><subtitle>A personal site for projects, documentation, and thoughts on software development and delivery.
</subtitle><author><name>Claes Adamsson</name><email>claes.adamsson@gmail.com</email></author><entry><title type="html">tbdflow-ui: a desktop dashboard built with hica</title><link href="/2026/06/30/tbdflow-ui/" rel="alternate" type="text/html" title="tbdflow-ui: a desktop dashboard built with hica" /><published>2026-06-30T00:00:00+00:00</published><updated>2026-06-30T00:00:00+00:00</updated><id>/2026/06/30/tbdflow-ui</id><content type="html" xml:base="/2026/06/30/tbdflow-ui/"><![CDATA[<p><a href="https://www.hica.dev">hica</a> recently got initial package management, and I wanted to build something meaningful to put it to practise. I’ve added examples and working programs to the <a href="https://github.com/cladam/hica">repo</a>, but I wanted a real showcase using the <code class="language-plaintext highlighter-rouge">imgui</code> library. <a href="https://github.com/cladam/tbdflow">tbdflow</a> was the obvious candidate.</p>

<p><code class="language-plaintext highlighter-rouge">tbdflow</code> needed structured JSON output first, otherwise IPC would have meant brittle string parsing. Adding <code class="language-plaintext highlighter-rouge">--json</code> also improved the overall developer experience: if you use tbdflow in scripts or have a Genie committing code through it, you get machine-readable output instead of human-readable text. Supported by <code class="language-plaintext highlighter-rouge">info</code>, <code class="language-plaintext highlighter-rouge">status</code>, <code class="language-plaintext highlighter-rouge">radar</code>, <code class="language-plaintext highlighter-rouge">sync</code>, <code class="language-plaintext highlighter-rouge">recover --list</code>, <code class="language-plaintext highlighter-rouge">task show</code>, and <code class="language-plaintext highlighter-rouge">note --show</code>.</p>

<p>The commands work great in the terminal. But sometimes you just want to glance at a repository’s health without switching context, a quick ambient read. That peripheral awareness is important for comprehension, especially across multiple repos (teams should really be on a monorepo, but that’s a separate post).</p>

<p>The result is <code class="language-plaintext highlighter-rouge">tbdflow-ui</code>: a persistent three-panel desktop window that sits alongside your editor and keeps the workflow visible.</p>

<div align="center">
  <img src="/assets/images/tbdflow-ui-screenshot.png" alt="tbdflow-ui screenshot" width="700" />
</div>

<h3 id="what-it-does">What it does</h3>

<p>The window is organised into three panels.</p>

<p>The <strong>left panel</strong> is your context: which repo you’re in, which branch, whether CI is enabled, whether radar is active. A quick glance tells you where you are and whether the trunk is healthy.</p>

<p>The <strong>centre panel</strong> is where the work happens. Sync with one click. The Intent Log is right in front: type a note, press Add Note, it calls <code class="language-plaintext highlighter-rouge">tbdflow note "&lt;text&gt;"</code> and the log refreshes immediately. The breadcrumb is the point: reasoning is sharpest at the moment you write the code, and a field sitting in front of you captures it before the next context switch. Rules that rely on documenting something later don’t have the same pull. I wrote about the underlying risk in <a href="/2026/02/22/the-comprehension-crisis/">The Comprehension Crisis</a>. Below that is the commit form: a type dropdown populated from your repo’s <code class="language-plaintext highlighter-rouge">tbdflow --json info</code> config and a message. There is an <em>Advanced</em> toggle for the rest of the supported options.</p>

<p>The <strong>right panel</strong> provides Awareness: trunk status with a colour-coded signal (green, pending, red), churn hotspots from the last three days highlighted on a red-to-yellow scale, and an overlap count across active branches.</p>

<p>It also supports <strong>multiple repos</strong>. Click Browse, pick a directory, all panels reload. Handy if you’re switching between a few repos throughout the day.</p>

<h3 id="the-design-rule">The design rule</h3>

<p>There’s one rule the UI follows strictly: <strong>the CLI is the source of truth.</strong></p>

<p><code class="language-plaintext highlighter-rouge">tbdflow-ui</code> never touches Git directly. Every piece of data comes from <code class="language-plaintext highlighter-rouge">tbdflow --json &lt;command&gt;</code>. The commit button constructs a full <code class="language-plaintext highlighter-rouge">tbdflow commit -t &lt;type&gt; -m "&lt;message&gt;"</code> command string and executes it. The intent log reads from <code class="language-plaintext highlighter-rouge">tbdflow --json notes</code>. Radar reads from <code class="language-plaintext highlighter-rouge">tbdflow --json radar</code>.</p>

<p>This matters because it means the UI can never drift out of sync with the CLI’s logic. If <code class="language-plaintext highlighter-rouge">tbdflow</code> adds a new commit flag tomorrow, the UI gets it by passing through the JSON output, not by reimplementing the logic.</p>

<h3 id="why-a-desktop-app">Why a desktop app?</h3>

<p>A terminal is perfect when you’re actively working. A desktop window serves a different purpose. It provides ambient awareness: trunk health, overlaps, hotspots, and your current intent are visible without interrupting your flow. The goal wasn’t to replace the CLI, but to complement it.</p>

<h3 id="built-with-hica">Built with hica</h3>

<p>This is the part that interests me most, because <code class="language-plaintext highlighter-rouge">tbdflow-ui</code> is written in <a href="https://www.hica.dev">hica</a>, my own functional, expression-based language.</p>

<p>hica transpiles to Koka, and Koka compiles to C. So the pipeline for <code class="language-plaintext highlighter-rouge">tbdflow-ui</code> is:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>.hc → hica → .kk → Koka → C → native binary
</code></pre></div></div>

<p>For a CLI tool, that pipeline is fairly unremarkable. For a GUI app with Dear ImGui, it gets more interesting. ImGui is a C++ immediate-mode GUI library. Getting hica to talk to it meant building a thin C FFI layer that exposes ImGui’s widgets as plain C functions, then wrapping those in a hica <code class="language-plaintext highlighter-rouge">imgui</code> library.</p>

<p>The result is that the hica source looks like this:</p>

<div class="language-hica highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">fun</span><span class="w"> </span><span class="nf">render_sidebar_context</span><span class="p">(</span><span class="n">c</span><span class="p">:</span><span class="w"> </span><span class="nc">Config</span><span class="p">,</span><span class="w"> </span><span class="n">s</span><span class="p">:</span><span class="w"> </span><span class="nc">Status</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
  </span><span class="nf">label</span><span class="p">(</span><span class="s2">"Branch"</span><span class="p">)</span><span class="w">
  </span><span class="nf">gui_text</span><span class="p">(</span><span class="n">s</span><span class="p">.</span><span class="n">current_branch</span><span class="p">)</span><span class="w">
  </span><span class="nf">gui_spacing</span><span class="p">()</span><span class="w">

  </span><span class="nf">label</span><span class="p">(</span><span class="s2">"Mode"</span><span class="p">)</span><span class="w">
  </span><span class="nf">gui_text</span><span class="p">(</span><span class="n">c</span><span class="p">.</span><span class="n">mode</span><span class="p">)</span><span class="w">
  </span><span class="nf">gui_spacing</span><span class="p">()</span><span class="w">

  </span><span class="nf">label</span><span class="p">(</span><span class="s2">"CI Status:"</span><span class="p">)</span><span class="w">
  </span><span class="nf">gui_same_line</span><span class="p">()</span><span class="w">
  </span><span class="k">if</span><span class="w"> </span><span class="n">c</span><span class="p">.</span><span class="n">ci_check_enabled</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nf">gui_text_colored</span><span class="p">(</span><span class="s2">"- Enabled"</span><span class="p">,</span><span class="w"> </span><span class="mf">0.06</span><span class="p">,</span><span class="w"> </span><span class="mf">0.71</span><span class="p">,</span><span class="w"> </span><span class="mf">0.65</span><span class="p">,</span><span class="w"> </span><span class="mf">1.0</span><span class="p">)</span><span class="w">
  </span><span class="p">}</span><span class="w"> </span><span class="k">else</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nf">gui_text_colored</span><span class="p">(</span><span class="s2">"o Disabled"</span><span class="p">,</span><span class="w"> </span><span class="mf">0.94</span><span class="p">,</span><span class="w"> </span><span class="mf">0.33</span><span class="p">,</span><span class="w"> </span><span class="mf">0.31</span><span class="p">,</span><span class="w"> </span><span class="mf">1.0</span><span class="p">)</span><span class="w">
  </span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p>No FFI boilerplate in application code. <strong>No pointer management, no widget IDs, no frame-begin/frame-end ceremony</strong>. hica’s effect system means the rendering context flows implicitly; IO effects are tracked at the type level without being specified on every function.</p>

<p>The commit command builder is a good example of how hica code reads. It’s purely functional: a chain of string conditionals that assembles the final shell command:</p>

<div class="language-hica highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">pub</span><span class="w"> </span><span class="kd">fun</span><span class="w"> </span><span class="nf">build_commit_cmd</span><span class="p">(</span><span class="n">ctype</span><span class="p">:</span><span class="w"> </span><span class="kt">string</span><span class="p">,</span><span class="w"> </span><span class="n">msg</span><span class="p">:</span><span class="w"> </span><span class="kt">string</span><span class="p">,</span><span class="w"> </span><span class="n">scope</span><span class="p">:</span><span class="w"> </span><span class="kt">string</span><span class="p">,</span><span class="w"> </span><span class="n">body</span><span class="p">:</span><span class="w"> </span><span class="kt">string</span><span class="p">,</span><span class="w"> 
                         </span><span class="n">tag</span><span class="p">:</span><span class="w"> </span><span class="kt">string</span><span class="p">,</span><span class="w"> </span><span class="n">issue</span><span class="p">:</span><span class="w"> </span><span class="kt">string</span><span class="p">,</span><span class="w"> </span><span class="n">breaking</span><span class="p">:</span><span class="w"> </span><span class="kt">bool</span><span class="p">,</span><span class="w"> </span><span class="n">no_verify</span><span class="p">:</span><span class="w"> </span><span class="kt">bool</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
  </span><span class="kd">let</span><span class="w"> </span><span class="n">base</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"tbdflow commit -t "</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="n">ctype</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="s2">" -m </span><span class="se">\"</span><span class="s2">"</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="n">msg</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="s2">"</span><span class="se">\"</span><span class="s2">"</span><span class="w">
  </span><span class="kd">let</span><span class="w"> </span><span class="n">s1</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">if</span><span class="w"> </span><span class="n">scope</span><span class="w"> </span><span class="o">!=</span><span class="w"> </span><span class="s2">""</span><span class="w">   </span><span class="p">{</span><span class="w"> </span><span class="n">base</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="s2">" -s "</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="n">scope</span><span class="w"> </span><span class="p">}</span><span class="w">             </span><span class="k">else</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="n">base</span><span class="w"> </span><span class="p">}</span><span class="w">
  </span><span class="kd">let</span><span class="w"> </span><span class="n">s2</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">if</span><span class="w"> </span><span class="n">body</span><span class="w">  </span><span class="o">!=</span><span class="w"> </span><span class="s2">""</span><span class="w">   </span><span class="p">{</span><span class="w"> </span><span class="n">s1</span><span class="w">   </span><span class="o">+</span><span class="w"> </span><span class="s2">" --body </span><span class="se">\"</span><span class="s2">"</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="n">body</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="s2">"</span><span class="se">\"</span><span class="s2">"</span><span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="k">else</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="n">s1</span><span class="w"> </span><span class="p">}</span><span class="w">
  </span><span class="kd">let</span><span class="w"> </span><span class="n">s3</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">if</span><span class="w"> </span><span class="n">tag</span><span class="w">   </span><span class="o">!=</span><span class="w"> </span><span class="s2">""</span><span class="w">   </span><span class="p">{</span><span class="w"> </span><span class="n">s2</span><span class="w">   </span><span class="o">+</span><span class="w"> </span><span class="s2">" --tag "</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="n">tag</span><span class="w"> </span><span class="p">}</span><span class="w">            </span><span class="k">else</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="n">s2</span><span class="w"> </span><span class="p">}</span><span class="w">
  </span><span class="kd">let</span><span class="w"> </span><span class="n">s4</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">if</span><span class="w"> </span><span class="n">issue</span><span class="w"> </span><span class="o">!=</span><span class="w"> </span><span class="s2">""</span><span class="w">   </span><span class="p">{</span><span class="w"> </span><span class="n">s3</span><span class="w">   </span><span class="o">+</span><span class="w"> </span><span class="s2">" --issue "</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="n">issue</span><span class="w"> </span><span class="p">}</span><span class="w">        </span><span class="k">else</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="n">s3</span><span class="w"> </span><span class="p">}</span><span class="w">
  </span><span class="kd">let</span><span class="w"> </span><span class="n">s5</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">if</span><span class="w"> </span><span class="n">breaking</span><span class="w">      </span><span class="p">{</span><span class="w"> </span><span class="n">s4</span><span class="w">   </span><span class="o">+</span><span class="w"> </span><span class="s2">" --breaking"</span><span class="w"> </span><span class="p">}</span><span class="w">              </span><span class="k">else</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="n">s4</span><span class="w"> </span><span class="p">}</span><span class="w">
  </span><span class="kd">let</span><span class="w"> </span><span class="n">s6</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">if</span><span class="w"> </span><span class="n">no_verify</span><span class="w">     </span><span class="p">{</span><span class="w"> </span><span class="n">s5</span><span class="w">   </span><span class="o">+</span><span class="w"> </span><span class="s2">" --no-verify"</span><span class="w"> </span><span class="p">}</span><span class="w">             </span><span class="k">else</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="n">s5</span><span class="w"> </span><span class="p">}</span><span class="w">
  </span><span class="n">s6</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p>Pure functions with clear data flow and no side effects until <code class="language-plaintext highlighter-rouge">exec()</code> is called.</p>

<h3 id="the-hica-imgui-library">The hica imgui library</h3>

<p>One thing I really like is the theming. Since the entire rendering surface is a hica module, I can write themes as plain hica files:</p>

<div class="language-hica highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">pub</span><span class="w"> </span><span class="kd">fun</span><span class="w"> </span><span class="nf">apply_theme</span><span class="p">()</span><span class="w"> </span><span class="p">{</span><span class="w">
  </span><span class="nf">gui_set_color_text</span><span class="p">(</span><span class="mf">0.9333</span><span class="p">,</span><span class="w"> </span><span class="mf">0.9411</span><span class="p">,</span><span class="w"> </span><span class="mf">0.9529</span><span class="p">)</span><span class="w">
  </span><span class="nf">gui_set_color_bg</span><span class="p">(</span><span class="mf">0.0941</span><span class="p">,</span><span class="w"> </span><span class="mf">0.1098</span><span class="p">,</span><span class="w"> </span><span class="mf">0.1451</span><span class="p">)</span><span class="w">
  </span><span class="nf">gui_set_color_accent</span><span class="p">(</span><span class="mf">0.2314</span><span class="p">,</span><span class="w"> </span><span class="mf">0.5098</span><span class="p">,</span><span class="w"> </span><span class="mf">0.9647</span><span class="p">)</span><span class="w">
  </span><span class="nf">gui_set_color_plot</span><span class="p">(</span><span class="mf">0.0588</span><span class="p">,</span><span class="w"> </span><span class="mf">0.7098</span><span class="p">,</span><span class="w"> </span><span class="mf">0.6549</span><span class="p">)</span><span class="w">
  </span><span class="nf">gui_set_style_rounding</span><span class="p">(</span><span class="mf">4.0</span><span class="p">,</span><span class="w"> </span><span class="mf">4.0</span><span class="p">,</span><span class="w"> </span><span class="mf">4.0</span><span class="p">)</span><span class="w">
  </span><span class="nf">gui_set_style_spacing</span><span class="p">(</span><span class="mf">8.0</span><span class="p">,</span><span class="w"> </span><span class="mf">6.0</span><span class="p">,</span><span class="w"> </span><span class="mf">16.0</span><span class="p">)</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p>Drop a different theme file in, call a different function. The app ships with a “Stillness Dark / Nordic Blue” theme and a One Dark Pro theme. Switching is one function call. The <code class="language-plaintext highlighter-rouge">imgui</code> library has a <a href="https://github.com/cladam/imgui/tree/main/examples/theme-editor">theme-editor</a> if you want to create your own themes.</p>

<h3 id="what-this-means-for-hica">What this means for hica</h3>

<p><code class="language-plaintext highlighter-rouge">tbdflow-ui</code> is the most complete application I’ve built with hica so far, and hica itself didn’t require any language changes to build it. The language was already capable. What did evolve was the <code class="language-plaintext highlighter-rouge">imgui</code> FFI library: tbdflow-ui had requirements the earlier layer didn’t cover, so those gaps got filled as the application needed them.</p>

<p>That’s a healthy feedback loop. A real program with real requirements is a better driver for a library than designing the API in the abstract.</p>

<p>The takeaway is that hica can build real desktop GUI applications. A tool with multiple data sources, reactive panels, live CLI integration, clipboard access, hyperlinks, and two polished themes. The C FFI story works. The linking story works (Koka link flags for C++ static libs with <code class="language-plaintext highlighter-rouge">--cclinkopts=-lc++</code>). It compiles to a single native binary.</p>

<h3 id="try-it">Try it</h3>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>curl <span class="nt">-fsSL</span> https://github.com/cladam/tbdflow-ui/releases/latest/download/install.sh | sh
</code></pre></div></div>

<p>Pre-built for macOS ARM64 and Linux x86_64. Requires <code class="language-plaintext highlighter-rouge">tbdflow</code> v0.33.0+ and SDL2 on your system.</p>

<p>If you want to build from source, install <a href="https://www.hica.dev">hica</a> and run <code class="language-plaintext highlighter-rouge">hica run</code> in the repo root.</p>

<ul>
  <li><a href="https://github.com/cladam/tbdflow-ui"><strong>tbdflow-ui source</strong></a> - MIT licensed</li>
  <li><a href="https://github.com/cladam/tbdflow"><strong>tbdflow</strong></a> - the CLI this wraps</li>
  <li><a href="https://www.hica.dev"><strong>hica</strong></a> - the language it’s written in</li>
</ul>

<p>This is an early release. tbdflow-ui will be improved continuously.</p>

<blockquote>
  <p><em>Throughput is a safety feature!</em></p>
</blockquote>]]></content><author><name>Claes Adamsson</name><email>claes.adamsson@gmail.com</email></author><category term="tbdflow" /><category term="hica" /><category term="gui" /><category term="tbd" /><summary type="html"><![CDATA[hica recently got initial package management, and I wanted to build something meaningful to put it to practise. I’ve added examples and working programs to the repo, but I wanted a real showcase using the imgui library. tbdflow was the obvious candidate.]]></summary></entry><entry><title type="html">Spec-Driven Development and the Return of Big Batch Thinking</title><link href="/2026/06/22/spec-driven-development/" rel="alternate" type="text/html" title="Spec-Driven Development and the Return of Big Batch Thinking" /><published>2026-06-22T00:00:00+00:00</published><updated>2026-06-22T00:00:00+00:00</updated><id>/2026/06/22/spec-driven-development</id><content type="html" xml:base="/2026/06/22/spec-driven-development/"><![CDATA[<p>Some are calling it <strong>“Spec-Driven Development.”</strong> Write a detailed specification for a large chunk of system functionality, hand it to an AI agent, and let it generate a working codebase. The argument is that shipping small, vertical slices (define a tiny piece, build it, get feedback, repeat) is too slow for the AI era.</p>

<p>I find myself sceptical, and not because of the AI part.</p>

<h3 id="a-pattern-that-feels-familiar">A pattern that feels familiar</h3>

<p>The workflow looks like this:</p>

<ol>
  <li><strong>Design</strong> a large batch of features upfront.</li>
  <li><strong>Generate</strong> a large body of code from that design.</li>
  <li><strong>Review, test, and debug</strong> the output.</li>
  <li><strong>Ship</strong> it in a single release.</li>
</ol>

<p>In DevOps, this is Big Batch Delivery. We have decades of evidence for why it struggles. Large batches amplify risk at every stage. A single flawed assumption early in the design doesn’t just invalidate one feature; it invalidates everything built on top of it.</p>

<blockquote>
  <p>Software requirements aren’t facts. They are a network of unverified hypotheses.</p>
</blockquote>

<p>The entire purpose of iterative delivery is to test those hypotheses as cheaply and as early as possible, long before you have built an ecosystem on top of them.</p>

<p>AI doesn’t change this. <strong>The bottleneck was never how fast code gets written. It is the feedback loop.</strong></p>

<h3 id="the-review-problem">The review problem</h3>

<p>When an AI agent produces a large, multi-thousand-line output from a single specification, that code still has to be integrated, understood, and maintained by a human team. In practice, it lands in a review gate.</p>

<p>I explored this in <a href="https://cladam.github.io/2026/02/19/the-pull-request-trap/">The Pull Request Trap</a>. Human comprehension doesn’t scale with line count. A team reviewing a two-thousand-line AI-generated diff faces the same cognitive limits as a team reviewing a two-thousand-line human-generated one. Under time pressure, reviews go superficial. The gate exists, but the signal is weak.</p>

<p>Slower integration is the obvious problem. The less obvious one is structural debt introduced without anyone understanding it well enough to manage it.</p>

<h3 id="the-comprehension-dimension">The comprehension dimension</h3>

<p>There is also a subtler problem. When you optimise purely for generation speed, delivery and understanding come apart.</p>

<p>I wrote about this in <a href="https://cladam.github.io/2026/02/22/the-comprehension-crisis/">The Comprehension Crisis</a>. If a model generates the spec, the code, and the explanation, it is easy to ship without ever owning the reasoning behind it. The system looks faster but the team’s capability quietly degrades.</p>

<blockquote>
  <p>Human comprehension must scale with delivery, or ownership evaporates.</p>
</blockquote>

<p>With large, spec-driven outputs, the team inherits a codebase they didn’t reason through. When something breaks, and it will, debugging starts from a much weaker position.</p>

<h3 id="what-elephant-carpaccio-teaches-us">What Elephant Carpaccio teaches us</h3>

<p>The practice of slicing work into the smallest provable unit has a name: <a href="https://alistaircockburn.com/Elephant-Carpaccio">Elephant Carpaccio</a>. The goal isn’t smaller work for its own sake. It is to shorten the distance between a decision and its validation.</p>

<p>This doesn’t change when an AI is doing the coding. Fast generation only helps if you can confirm assumptions just as fast. That speed advantage disappears the moment you batch everything back up into a slow, uncertain release.</p>

<p>Some things stay true regardless of who writes the code:</p>

<ul>
  <li><strong>Feed the smallest useful slice to the agent.</strong> A tightly scoped prompt produces output that is easier to review, test, and validate in production.</li>
  <li><strong>Prefer continuous integration over gates.</strong> Trunk-based development with non-blocking reviews keeps changes flowing and keeps comprehension distributed across the team.</li>
  <li><strong>Treat telemetry as the ground truth.</strong> Real user behaviour, not the initial spec, is where value is confirmed or refuted.</li>
  <li><strong>Throughput and comprehension are not in tension.</strong> High throughput with deep understanding lets you isolate variables and adapt quickly. High throughput without understanding is how you accumulate hidden debt.</li>
</ul>

<h3 id="the-underlying-question">The underlying question</h3>

<p>Spec-Driven Development probably has a place: where requirements are stable, well-understood, and easy to verify in bulk. Most product work isn’t like that.</p>

<p>Writing a complete spec and getting a complete codebase back feels productive. <strong>Feeling productive and being productive are different things.</strong> If a core assumption in the spec is wrong, the cost scales with how much was built before anyone found out.</p>

<p>Micro-sized batches and tight feedback loops aren’t a constraint on what AI can do. They’re how you find out if what the agent produced was worth building.</p>

<blockquote>
  <p><em>Throughput is a safety feature!</em></p>
</blockquote>]]></content><author><name>Claes Adamsson</name><email>claes.adamsson@gmail.com</email></author><category term="DevOps" /><category term="AI" /><category term="comprehension" /><category term="TBD" /><summary type="html"><![CDATA[Some are calling it “Spec-Driven Development.” Write a detailed specification for a large chunk of system functionality, hand it to an AI agent, and let it generate a working codebase. The argument is that shipping small, vertical slices (define a tiny piece, build it, get feedback, repeat) is too slow for the AI era.]]></summary></entry><entry><title type="html">Don’t trust, instruct and verify</title><link href="/2026/06/10/hica-assistant/" rel="alternate" type="text/html" title="Don’t trust, instruct and verify" /><published>2026-06-10T00:00:00+00:00</published><updated>2026-06-10T00:00:00+00:00</updated><id>/2026/06/10/hica-assistant</id><content type="html" xml:base="/2026/06/10/hica-assistant/"><![CDATA[<p>Generic LLMs are bad at niche programming languages. Ask one to write hica code and it invents syntax, uses functions that aren’t in the prelude, or produces Koka with the wrong variable names. It’s never seen hica, so it guesses from whatever looks closest.</p>

<p>A better-informed model beats a smarter one.</p>

<h3 id="the-idea">The idea</h3>

<p>Ollama lets you create custom models with a <code class="language-plaintext highlighter-rouge">Modelfile</code>: base LLM, parameters, and a system prompt. That prompt is permanent context the model reads before every message.</p>

<p><code class="language-plaintext highlighter-rouge">hica-assistant</code> is built on <code class="language-plaintext highlighter-rouge">qwen3.6:35b-a3b</code> (a mixture-of-experts model, fast on a Mac) with a system prompt covering the compiler pipeline, syntax rules, the prelude API with types, the stdlib, and a table of pitfalls from real bugs I’ve encountered during development.</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ollama create hica-assistant <span class="nt">-f</span> Modelfile
ollama run hica-assistant
</code></pre></div></div>

<p>No API key needed, no data leaving the machine (works offline) and no rate limits.</p>

<h3 id="the-verify-part">The verify part</h3>

<p>Every snippet the model generates gets checked with the hica compiler:</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>hica check generated.hc
hica run generated.hc
hica <span class="nb">test </span>generated.hc
</code></pre></div></div>

<p>When it fails, I find the gap in the Modelfile, add the rule, and rebuild. The system prompt is the ground truth and very updateable. My chat history is throwaway.</p>

<p>Let’s look at a concrete example: the model kept writing <code class="language-plaintext highlighter-rouge">is_empty(list)</code> to check whether a list is empty, that’s wrong as <code class="language-plaintext highlighter-rouge">is_empty</code> takes a <code class="language-plaintext highlighter-rouge">string</code>. I added a row to the pitfalls table, ran <code class="language-plaintext highlighter-rouge">ollama create hica-assistant -f Modelfile</code>, and the next response used <code class="language-plaintext highlighter-rouge">match xs { [] =&gt; ... }</code> correctly.</p>

<p>The loop: prompt, generate, compile, fix Modelfile, rebuild.</p>
<h3 id="it-works-for-real-code">It works for real code</h3>

<p>I asked it to write a filter-map-fold pipeline. First try, both styles:</p>

<div class="language-hica highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">fun</span><span class="w"> </span><span class="nf">main</span><span class="p">()</span><span class="w"> </span><span class="p">{</span><span class="w">
  </span><span class="c1">// pipe style</span><span class="w">
  </span><span class="kd">let</span><span class="w"> </span><span class="n">a</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">[</span><span class="mi">1</span><span class="p">,</span><span class="w"> </span><span class="mi">2</span><span class="p">,</span><span class="w"> </span><span class="mi">3</span><span class="p">,</span><span class="w"> </span><span class="mi">4</span><span class="p">,</span><span class="w"> </span><span class="mi">5</span><span class="p">]</span><span class="w">
    </span><span class="o">|&gt;</span><span class="w"> </span><span class="nf">filter</span><span class="p">((</span><span class="n">x</span><span class="p">)</span><span class="w"> </span><span class="o">=&gt;</span><span class="w"> </span><span class="n">x</span><span class="w"> </span><span class="o">%</span><span class="w"> </span><span class="mi">2</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="mi">0</span><span class="p">)</span><span class="w">
    </span><span class="o">|&gt;</span><span class="w"> </span><span class="nf">map</span><span class="p">((</span><span class="n">x</span><span class="p">)</span><span class="w"> </span><span class="o">=&gt;</span><span class="w"> </span><span class="n">x</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="mi">10</span><span class="p">)</span><span class="w">
    </span><span class="o">|&gt;</span><span class="w"> </span><span class="nf">fold</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span><span class="w"> </span><span class="p">(</span><span class="n">acc</span><span class="p">,</span><span class="w"> </span><span class="n">x</span><span class="p">)</span><span class="w"> </span><span class="o">=&gt;</span><span class="w"> </span><span class="n">acc</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="n">x</span><span class="p">)</span><span class="w">
  </span><span class="nf">println</span><span class="p">(</span><span class="n">a</span><span class="p">)</span><span class="w">   </span><span class="c1">// 60</span><span class="w">

  </span><span class="c1">// dot-call style, same result</span><span class="w">
  </span><span class="kd">let</span><span class="w"> </span><span class="n">b</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">[</span><span class="mi">1</span><span class="p">,</span><span class="w"> </span><span class="mi">2</span><span class="p">,</span><span class="w"> </span><span class="mi">3</span><span class="p">,</span><span class="w"> </span><span class="mi">4</span><span class="p">,</span><span class="w"> </span><span class="mi">5</span><span class="p">]</span><span class="w">
    </span><span class="p">.</span><span class="nf">filter</span><span class="p">((</span><span class="n">x</span><span class="p">)</span><span class="w"> </span><span class="o">=&gt;</span><span class="w"> </span><span class="n">x</span><span class="w"> </span><span class="o">%</span><span class="w"> </span><span class="mi">2</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="mi">0</span><span class="p">)</span><span class="w">
    </span><span class="p">.</span><span class="nf">map</span><span class="p">((</span><span class="n">x</span><span class="p">)</span><span class="w"> </span><span class="o">=&gt;</span><span class="w"> </span><span class="n">x</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="mi">10</span><span class="p">)</span><span class="w">
    </span><span class="p">.</span><span class="nf">fold</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span><span class="w"> </span><span class="p">(</span><span class="n">acc</span><span class="p">,</span><span class="w"> </span><span class="n">x</span><span class="p">)</span><span class="w"> </span><span class="o">=&gt;</span><span class="w"> </span><span class="n">acc</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="n">x</span><span class="p">)</span><span class="w">
  </span><span class="nf">println</span><span class="p">(</span><span class="n">b</span><span class="p">)</span><span class="w">   </span><span class="c1">// 60</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p>Both compile. Both produce 60. <code class="language-plaintext highlighter-rouge">a |&gt; f</code> and <code class="language-plaintext highlighter-rouge">a.f()</code> are the same thing and the model knows when to reach for each.</p>

<h3 id="bounded-knowledge-is-a-feature">Bounded knowledge is a feature</h3>

<p>The model only knows what’s in the Modelfile. Ask it about <code class="language-plaintext highlighter-rouge">get_cwd()</code>, which doesn’t exist in hica yet, and it says so and gives the real workaround (<code class="language-plaintext highlighter-rouge">get_env("PWD")</code>). A generic model would invent <code class="language-plaintext highlighter-rouge">get_cwd()</code> confidently and leave you debugging a compile error.</p>

<p>When something is missing from the Modelfile, I add it. Right now it covers the prelude, <code class="language-plaintext highlighter-rouge">std/io</code>, <code class="language-plaintext highlighter-rouge">std/list</code>, <code class="language-plaintext highlighter-rouge">std/datetime</code>, and the main pitfall patterns. The file grows with the language.</p>

<h3 id="the-modelfile-is-in-the-repo">The Modelfile is in the repo</h3>

<p>The <a href="https://github.com/cladam/hica/blob/main/Modelfile">Modelfile</a> is in the hica repo, Apache 2.0 licensed.</p>

<p>The approach works for any language, framework, or internal API a generic model hasn’t seen. Write down what it needs to know, verify with real tooling, iterate.</p>]]></content><author><name>Claes Adamsson</name><email>claes.adamsson@gmail.com</email></author><category term="hica" /><category term="ollama" /><category term="LLM" /><category term="AI" /><summary type="html"><![CDATA[Generic LLMs are bad at niche programming languages. Ask one to write hica code and it invents syntax, uses functions that aren’t in the prelude, or produces Koka with the wrong variable names. It’s never seen hica, so it guesses from whatever looks closest.]]></summary></entry><entry><title type="html">Building a Lisp in hica</title><link href="/2026/05/25/hilisp/" rel="alternate" type="text/html" title="Building a Lisp in hica" /><published>2026-05-25T00:00:00+00:00</published><updated>2026-05-25T00:00:00+00:00</updated><id>/2026/05/25/hilisp</id><content type="html" xml:base="/2026/05/25/hilisp/"><![CDATA[<p>Version 0.29.3 of <strong>hica</strong> is out and the core is getting pretty stable. I wanted to explore whether hica could be used to write a small Lisp, as a way to stress-test the hica compiler: closures, recursive data structures, lexical scoping, higher-order functions.</p>

<p><strong>HiLisp</strong> is now live and ready for tinkering.</p>

<h3 id="why-a-lisp">Why a Lisp?</h3>

<p>Many developers write a Lisp at some point. Same thing here really.</p>

<p>The useful part is that maintaining HiLisp <em>is</em> hica development. Every script and example exercises the compiler and provides feedback. I ended up fixing several bugs in hica while building HiLisp.</p>

<p>The main inspiration was <a href="https://carp-lang.github.io/Carp/LanguageGuide.html">Carp</a>, a Lisp with ML/Rust-like semantics. HiLisp borrows its vocabulary of special forms: <code class="language-plaintext highlighter-rouge">defn</code>, <code class="language-plaintext highlighter-rouge">def</code>, <code class="language-plaintext highlighter-rouge">fn</code>, <code class="language-plaintext highlighter-rouge">let</code>, <code class="language-plaintext highlighter-rouge">do</code>, <code class="language-plaintext highlighter-rouge">cond</code>, <code class="language-plaintext highlighter-rouge">quote</code>.</p>

<p>Also, there is objective beauty in parentheses.</p>

<h3 id="what-it-looks-like">What it looks like</h3>

<div class="language-lisp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">(</span><span class="nv">defn</span> <span class="nv">factorial</span> <span class="p">(</span><span class="nv">n</span><span class="p">)</span>
  <span class="p">(</span><span class="k">if</span> <span class="p">(</span><span class="nb">&lt;=</span> <span class="nv">n</span> <span class="mi">1</span><span class="p">)</span> <span class="mi">1</span> <span class="p">(</span><span class="nb">*</span> <span class="nv">n</span> <span class="p">(</span><span class="nv">factorial</span> <span class="p">(</span><span class="nb">-</span> <span class="nv">n</span> <span class="mi">1</span><span class="p">)))))</span>

<span class="p">(</span><span class="nv">println</span> <span class="p">(</span><span class="nv">factorial</span> <span class="mi">10</span><span class="p">))</span>   <span class="c1">; 3628800</span>
</code></pre></div></div>

<p>Closures work:</p>

<div class="language-lisp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">(</span><span class="nv">defn</span> <span class="nv">make_adder</span> <span class="p">(</span><span class="nv">n</span><span class="p">)</span> <span class="p">(</span><span class="nv">fn</span> <span class="p">(</span><span class="nv">x</span><span class="p">)</span> <span class="p">(</span><span class="nb">+</span> <span class="nv">x</span> <span class="nv">n</span><span class="p">)))</span>
<span class="p">(</span><span class="nv">def</span> <span class="nv">add10</span> <span class="p">(</span><span class="nv">make_adder</span> <span class="mi">10</span><span class="p">))</span>
<span class="p">(</span><span class="nv">println</span> <span class="p">(</span><span class="nv">add10</span> <span class="mi">32</span><span class="p">))</span>   <span class="c1">; 42</span>
</code></pre></div></div>

<p>And higher-order functions:</p>

<div class="language-lisp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">(</span><span class="nv">defn</span> <span class="nb">map</span> <span class="p">(</span><span class="nv">f</span> <span class="nv">lst</span><span class="p">)</span>
  <span class="p">(</span><span class="k">if</span> <span class="p">(</span><span class="nb">=</span> <span class="nv">lst</span> <span class="o">'</span><span class="p">())</span> <span class="o">'</span><span class="p">()</span>
    <span class="p">(</span><span class="nb">cons</span> <span class="p">(</span><span class="nv">f</span> <span class="p">(</span><span class="nb">car</span> <span class="nv">lst</span><span class="p">))</span> <span class="p">(</span><span class="nb">map</span> <span class="nv">f</span> <span class="p">(</span><span class="nb">cdr</span> <span class="nv">lst</span><span class="p">)))))</span>

<span class="p">(</span><span class="nv">println</span> <span class="p">(</span><span class="nb">map</span> <span class="p">(</span><span class="nv">fn</span> <span class="p">(</span><span class="nv">x</span><span class="p">)</span> <span class="p">(</span><span class="nb">*</span> <span class="nv">x</span> <span class="nv">x</span><span class="p">))</span> <span class="o">'</span><span class="p">(</span><span class="mi">1</span> <span class="mi">2</span> <span class="mi">3</span> <span class="mi">4</span> <span class="mi">5</span><span class="p">)))</span>
<span class="c1">; (1 4 9 16 25)</span>
</code></pre></div></div>

<h3 id="the-pipeline">The pipeline</h3>

<p>HiLisp is a classic tree-walker interpreter:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>source text → tokenise → parse → eval
</code></pre></div></div>

<p>Each phase lives in its own hica module: <code class="language-plaintext highlighter-rouge">tokeniser.hc</code>, <code class="language-plaintext highlighter-rouge">parser.hc</code>, <code class="language-plaintext highlighter-rouge">eval.hc</code>. The AST is a recursive <code class="language-plaintext highlighter-rouge">LVal</code> enum representing numbers, booleans, strings, symbols, lists, lambdas, and nil. Pattern matching over that enum drives the evaluator.</p>

<p>The interesting bit was environments. Each lambda captures a reference to its defining environment for proper lexical scoping. I expected that to need some gymnastics, but structs and recursive types in hica made it pretty straightforward.</p>

<h3 id="try-it">Try it</h3>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>./hilisp                         <span class="c"># REPL</span>
./hilisp examples/recursion.hl   <span class="c"># run a file</span>

<span class="c"># Build from source (needs hica ≥ 0.29.3 and Koka 3.2.3)</span>
hica build <span class="nt">-o</span> hilisp
</code></pre></div></div>

<p>The <code class="language-plaintext highlighter-rouge">examples/</code> folder contains runnable <code class="language-plaintext highlighter-rouge">.hl</code> files covering arithmetic, closures, recursion, lists, and sorting. There’s also a <code class="language-plaintext highlighter-rouge">lib/prelude.hl</code> with helpers like <code class="language-plaintext highlighter-rouge">map</code>, <code class="language-plaintext highlighter-rouge">filter</code>, and <code class="language-plaintext highlighter-rouge">fold</code>.</p>

<ul>
  <li><a href="https://github.com/cladam/hica-lisp"><strong>Source</strong></a>, MIT license.</li>
  <li><a href="https://github.com/cladam/hica-lisp/blob/main/docs/lisp-primer.md"><strong>Lisp primer</strong></a>,  start here if you’ve never worked with a Lisp.</li>
</ul>

<p>Going forward, I’ll definitely be using HiLisp as part of hica development itself, both as a playground and as a way to continuously exercise the compiler with real code.</p>]]></content><author><name>Claes Adamsson</name><email>claes.adamsson@gmail.com</email></author><category term="hica" /><category term="lisp" /><category term="languages" /><category term="learning" /><summary type="html"><![CDATA[Version 0.29.3 of hica is out and the core is getting pretty stable. I wanted to explore whether hica could be used to write a small Lisp, as a way to stress-test the hica compiler: closures, recursive data structures, lexical scoping, higher-order functions.]]></summary></entry><entry><title type="html">Test-Driven Development with hica</title><link href="/2026/05/20/tdd-hica/" rel="alternate" type="text/html" title="Test-Driven Development with hica" /><published>2026-05-20T00:00:00+00:00</published><updated>2026-05-20T00:00:00+00:00</updated><id>/2026/05/20/tdd-hica</id><content type="html" xml:base="/2026/05/20/tdd-hica/"><![CDATA[<p>hica has inline <code class="language-plaintext highlighter-rouge">test</code> blocks. They sit right next to your functions: no extra test files, no additional imports and no framework.
Run <code class="language-plaintext highlighter-rouge">hica test</code> and you’re done, that’s it!</p>

<p>Early on in hica’s design I wanted the simplest possible testing workflow, and I think I succeeded (IMHO)</p>

<p>This post walks through how TDD works in hica and why the compiler changes what you need to test.</p>

<h3 id="tdd-isnt-new">TDD isn’t new</h3>

<p>TDD has a reputation as a methodology someone invented. It’s most definitely not. Kent Beck has said he “rediscovered” it by studying older literature and watching what good programmers already did. 
Dijkstra argued in 1972 that correctness proof and programs should “grow hand in hand.”</p>

<p>Andrea Laforgia captures this well in his <a href="https://a4al6a.substack.com/p/td">T*D piece</a>. 
TDD, trunk-based development, and collaborative programming are patterns that keep emerging when teams care about quality and fast delivery. 
When you remove friction from testing, people test first. That’s what happened with hica.</p>

<h3 id="the-compiler-does-half-the-work">The compiler does half the work</h3>

<p>hica has Hindley-Milner inference and exhaustive pattern matching. A lot of the defensive tests I’d write in other languages aren’t needed:</p>

<div class="language-hica highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">type</span><span class="w"> </span><span class="nc">Direction</span><span class="w"> </span><span class="p">{</span><span class="w">
  </span><span class="nc">North</span><span class="p">,</span><span class="w"> </span><span class="nc">South</span><span class="p">,</span><span class="w"> </span><span class="nc">East</span><span class="p">,</span><span class="w"> </span><span class="nc">West</span><span class="w">
</span><span class="p">}</span><span class="w">

</span><span class="kd">fun</span><span class="w"> </span><span class="nf">move</span><span class="p">(</span><span class="n">pos</span><span class="p">,</span><span class="w"> </span><span class="n">dir</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
  </span><span class="k">match</span><span class="w"> </span><span class="n">dir</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nc">North</span><span class="w"> </span><span class="o">=&gt;</span><span class="w"> </span><span class="p">(</span><span class="n">pos</span><span class="p">.</span><span class="mi">0</span><span class="p">,</span><span class="w"> </span><span class="n">pos</span><span class="p">.</span><span class="mi">1</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="mi">1</span><span class="p">),</span><span class="w">
    </span><span class="nc">South</span><span class="w"> </span><span class="o">=&gt;</span><span class="w"> </span><span class="p">(</span><span class="n">pos</span><span class="p">.</span><span class="mi">0</span><span class="p">,</span><span class="w"> </span><span class="n">pos</span><span class="p">.</span><span class="mi">1</span><span class="w"> </span><span class="o">-</span><span class="w"> </span><span class="mi">1</span><span class="p">),</span><span class="w">
    </span><span class="nc">East</span><span class="w">  </span><span class="o">=&gt;</span><span class="w"> </span><span class="p">(</span><span class="n">pos</span><span class="p">.</span><span class="mi">0</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="mi">1</span><span class="p">,</span><span class="w"> </span><span class="n">pos</span><span class="p">.</span><span class="mi">1</span><span class="p">),</span><span class="w">
    </span><span class="nc">West</span><span class="w">  </span><span class="o">=&gt;</span><span class="w"> </span><span class="p">(</span><span class="n">pos</span><span class="p">.</span><span class="mi">0</span><span class="w"> </span><span class="o">-</span><span class="w"> </span><span class="mi">1</span><span class="p">,</span><span class="w"> </span><span class="n">pos</span><span class="p">.</span><span class="mi">1</span><span class="p">)</span><span class="w">
  </span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p>Forget the null case (there’s no null). Pass a string where a <code class="language-plaintext highlighter-rouge">Direction</code> is expected? It won’t compile. Leave out <code class="language-plaintext highlighter-rouge">West</code>? Compile error. 
The type checker handles structural correctness. What’s left for tests is whether the function <em>does the right thing</em>.</p>

<p>Rule of thumb: if the compiler can’t distinguish a correct implementation from an incorrect one, write a test.</p>

<h3 id="red-green-refactor">Red-green-refactor</h3>

<p>Let’s build a simple password validator from scratch.</p>

<p><strong>Red.</strong> Stub the interface, assert what you want:</p>

<div class="language-hica highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">fun</span><span class="w"> </span><span class="nf">validate</span><span class="p">(</span><span class="n">password</span><span class="p">)</span><span class="w"> </span><span class="o">=&gt;</span><span class="w"> </span><span class="kc">false</span><span class="w">

</span><span class="k">test</span><span class="w"> </span><span class="s2">"valid password has 6+ characters"</span><span class="w"> </span><span class="p">{</span><span class="w">
  </span><span class="nf">assert</span><span class="p">(</span><span class="nf">validate</span><span class="p">(</span><span class="s2">"secret123"</span><span class="p">))</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">hica test password.hc</code> fails. Good, that’s expected.</p>

<p><strong>Green.</strong> Minimum code to pass:</p>

<div class="language-hica highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">fun</span><span class="w"> </span><span class="nf">validate</span><span class="p">(</span><span class="n">password</span><span class="p">)</span><span class="w"> </span><span class="o">=&gt;</span><span class="w"> </span><span class="nf">str_length</span><span class="p">(</span><span class="n">password</span><span class="p">)</span><span class="w"> </span><span class="o">&gt;=</span><span class="w"> </span><span class="mi">6</span><span class="w">
</span></code></pre></div></div>

<p>Passes.</p>

<p><strong>Red again.</strong> New requirement: passwords need at least one digit. Add a test for it. It should fail against the current <code class="language-plaintext highlighter-rouge">validate</code> implementation:</p>

<div class="language-hica highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">test</span><span class="w"> </span><span class="s2">"rejects password without digit"</span><span class="w"> </span><span class="p">{</span><span class="w">
  </span><span class="nf">assert_false</span><span class="p">(</span><span class="nf">validate</span><span class="p">(</span><span class="s2">"noNumbersHere"</span><span class="p">))</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p><strong>Green.</strong> Update the implementation to pass all tests:</p>

<div class="language-hica highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">fun</span><span class="w"> </span><span class="nf">has_digit</span><span class="p">(</span><span class="n">s</span><span class="p">)</span><span class="w"> </span><span class="o">=&gt;</span><span class="w"> </span><span class="nf">any</span><span class="p">(</span><span class="nf">chars</span><span class="p">(</span><span class="n">s</span><span class="p">),</span><span class="w"> </span><span class="p">(</span><span class="n">c</span><span class="p">)</span><span class="w"> </span><span class="o">=&gt;</span><span class="w"> </span><span class="nf">is_digit</span><span class="p">(</span><span class="n">c</span><span class="p">))</span><span class="w">

</span><span class="kd">fun</span><span class="w"> </span><span class="nf">validate</span><span class="p">(</span><span class="n">password</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
  </span><span class="kd">let</span><span class="w"> </span><span class="n">long_enough</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nf">str_length</span><span class="p">(</span><span class="n">password</span><span class="p">)</span><span class="w"> </span><span class="o">&gt;=</span><span class="w"> </span><span class="mi">6</span><span class="w">
  </span><span class="n">long_enough</span><span class="w"> </span><span class="o">&amp;&amp;</span><span class="w"> </span><span class="nf">has_digit</span><span class="p">(</span><span class="n">password</span><span class="p">)</span><span class="w">
</span><span class="p">}</span><span class="w">

</span><span class="k">test</span><span class="w"> </span><span class="s2">"valid password has 6+ characters"</span><span class="w"> </span><span class="p">{</span><span class="w">
  </span><span class="nf">assert</span><span class="p">(</span><span class="nf">validate</span><span class="p">(</span><span class="s2">"secret123"</span><span class="p">))</span><span class="w">
</span><span class="p">}</span><span class="w">

</span><span class="k">test</span><span class="w"> </span><span class="s2">"rejects password without digit"</span><span class="w"> </span><span class="p">{</span><span class="w">
  </span><span class="nf">assert_false</span><span class="p">(</span><span class="nf">validate</span><span class="p">(</span><span class="s2">"noNumbersHere"</span><span class="p">))</span><span class="w">
</span><span class="p">}</span><span class="w">

</span><span class="k">test</span><span class="w"> </span><span class="s2">"rejects short password"</span><span class="w"> </span><span class="p">{</span><span class="w">
  </span><span class="nf">assert_false</span><span class="p">(</span><span class="nf">validate</span><span class="p">(</span><span class="s2">"ab1"</span><span class="p">))</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p>All three pass. Refactor whenever, the tests catch regressions.</p>

<h3 id="no-context-switch">No context switch</h3>

<p>Tests live next to the code:</p>

<div class="language-hica highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">fun</span><span class="w"> </span><span class="nf">double</span><span class="p">(</span><span class="n">x</span><span class="p">)</span><span class="w"> </span><span class="o">=&gt;</span><span class="w"> </span><span class="n">x</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="mi">2</span><span class="w">

</span><span class="k">test</span><span class="w"> </span><span class="s2">"double works"</span><span class="w"> </span><span class="p">{</span><span class="w">
  </span><span class="nf">assert_eq</span><span class="p">(</span><span class="nf">double</span><span class="p">(</span><span class="mi">3</span><span class="p">),</span><span class="w"> </span><span class="mi">6</span><span class="p">)</span><span class="w">
  </span><span class="nf">assert_eq</span><span class="p">(</span><span class="nf">double</span><span class="p">(</span><span class="mi">0</span><span class="p">),</span><span class="w"> </span><span class="mi">0</span><span class="p">)</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>running 1 test(s)...

  ✓ double works

1 test(s) passed
</code></pre></div></div>

<p>When something breaks:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  ✗ double works
    assertion failed: assert_eq(double(3), 5)
      expected: 5
      actually: 6
</code></pre></div></div>

<p>Use <code class="language-plaintext highlighter-rouge">assert_eq</code> over <code class="language-plaintext highlighter-rouge">assert</code>. “expected 5, got 6” tells you what happened, <code class="language-plaintext highlighter-rouge">assert</code> just says “false”.</p>

<h3 id="a-typical-session">A typical session</h3>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>hica repl                <span class="c"># explore an API before writing tests</span>
nvim password.hc         <span class="c"># write a failing test</span>
hica <span class="nb">test </span>password.hc    <span class="c"># see it fail</span>
nvim password.hc         <span class="c"># make it pass</span>
hica <span class="nb">test </span>password.hc    <span class="c"># see it pass</span>
hica check password.hc   <span class="c"># fast type-check between edits</span>
tbdflow commit <span class="nt">-t</span> feat <span class="nt">-m</span> <span class="s2">"add password validation"</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">hica check</code> only type-checks, no compilation and near instant. I run it between almost every edit, like a spellchecker for logic 🙂
<code class="language-plaintext highlighter-rouge">hica repl</code> is useful when you’re not sure what a function returns. Try it interactively first, then write the test.</p>

<h3 id="one-test-per-behaviour">One test per behaviour</h3>

<p>“Validates length” and “requires digit” are separate tests. If a test name needs “and” in it, split it.</p>

<p>Happy testing!</p>

<blockquote>
  <p><em>Throughput is a safety feature!</em> (now with tests)</p>
</blockquote>]]></content><author><name>Claes Adamsson</name><email>claes.adamsson@gmail.com</email></author><category term="hica" /><category term="TDD" /><category term="testing" /><category term="DevOps" /><summary type="html"><![CDATA[hica has inline test blocks. They sit right next to your functions: no extra test files, no additional imports and no framework. Run hica test and you’re done, that’s it!]]></summary></entry><entry><title type="html">Introducing hica: a language that transpiles to Koka</title><link href="/2026/05/13/introducing-hica/" rel="alternate" type="text/html" title="Introducing hica: a language that transpiles to Koka" /><published>2026-05-13T00:00:00+00:00</published><updated>2026-05-13T00:00:00+00:00</updated><id>/2026/05/13/introducing-hica</id><content type="html" xml:base="/2026/05/13/introducing-hica/"><![CDATA[<p>In my post about cloning <a href="https://cladam.github.io/2026/05/06/koka-ls-clone/">ls in Koka</a> I mentioned wanting to build a small language that transpiles to Koka. That idea has become <strong>hica</strong>!</p>

<h3 id="the-itch">The itch</h3>

<p>hica is a statically typed, expression-oriented language. It compiles <code class="language-plaintext highlighter-rouge">.hc</code> source files through a Pratt parser and Hindley-Milner type checker, emits Koka <code class="language-plaintext highlighter-rouge">.kk</code> source, and lets Koka handle the rest.</p>

<p>I’ve wanted to design my own language for years. Every attempt fizzled out in the same place: the plumbing… Lexers, type systems, backends, memory management – the mountain of infrastructure you need before you can even think about syntax. It felt like building a house by first inventing concrete in a way…</p>

<p>But then it occurred to me, if I target Koka, I skip all those parts. Koka already has Perceus for memory, C/JS/WASM backends, a standard library, optimisation passes, and the effect runtime. I just need to build the front half: syntax, type checker, diagnostics, and an emitter that spits out <code class="language-plaintext highlighter-rouge">.kk</code> files. Koka handles the rest.</p>

<p>The constraint is that hica can only express what Koka can express, but given that hica’s design pillars (algebraic effects, Perceus memory, expression-oriented, strong inference) are exactly what Koka already provides, that costs almost nothing.</p>

<h3 id="what-it-looks-like">What it looks like</h3>

<div class="language-hica highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">fun</span><span class="w"> </span><span class="nf">fizzbuzz</span><span class="p">(</span><span class="n">n</span><span class="p">)</span><span class="w"> </span><span class="o">=&gt;</span><span class="w">
  </span><span class="k">if</span><span class="w"> </span><span class="n">n</span><span class="w"> </span><span class="o">%</span><span class="w"> </span><span class="mi">15</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="mi">0</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="s2">"fizzbuzz"</span><span class="w"> </span><span class="p">}</span><span class="w">
  </span><span class="k">else</span><span class="w"> </span><span class="k">if</span><span class="w"> </span><span class="n">n</span><span class="w"> </span><span class="o">%</span><span class="w"> </span><span class="mi">3</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="mi">0</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="s2">"fizz"</span><span class="w"> </span><span class="p">}</span><span class="w">
  </span><span class="k">else</span><span class="w"> </span><span class="k">if</span><span class="w"> </span><span class="n">n</span><span class="w"> </span><span class="o">%</span><span class="w"> </span><span class="mi">5</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="mi">0</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="s2">"buzz"</span><span class="w"> </span><span class="p">}</span><span class="w">
  </span><span class="k">else</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="s2">"</span><span class="si">{n}</span><span class="s2">"</span><span class="w"> </span><span class="p">}</span><span class="w">

</span><span class="kd">fun</span><span class="w"> </span><span class="nf">main</span><span class="p">()</span><span class="w"> </span><span class="p">{</span><span class="w">
  </span><span class="k">for</span><span class="w"> </span><span class="n">i</span><span class="w"> </span><span class="k">in</span><span class="w"> </span><span class="mi">1</span><span class="o">..</span><span class="mi">100</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nf">println</span><span class="p">(</span><span class="nf">fizzbuzz</span><span class="p">(</span><span class="n">i</span><span class="p">))</span><span class="w">
  </span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p>Everything is an expression: <code class="language-plaintext highlighter-rouge">if</code>, <code class="language-plaintext highlighter-rouge">match</code>, blocks all return values. No <code class="language-plaintext highlighter-rouge">return</code> keyword. <code class="language-plaintext highlighter-rouge">"{n}"</code> is string interpolation. The name stands for <strong>H</strong>indley-milner <strong>I</strong>nference <strong>C</strong>ompiler with <strong>A</strong>lgebraic effects.</p>

<p>The pipeline is straightforward:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>.hc source → Lex → Parse → Desugar → Type Check → Emit Koka (.kk) → Koka Compiler → C/JS/WASM
</code></pre></div></div>

<p>Each compiler phase uses algebraic effects for its state: diagnostics, type variables and symbol scopes are all effect-tracked. Compilers in Koka are surprisingly natural. The type checker runs Hindley-Milner unification and the emitter annotates the Koka output with the inferred types.</p>

<h3 id="the-tooling">The tooling</h3>

<p>I really enjoy building CLI tools, hica comes with <code class="language-plaintext highlighter-rouge">hica run</code>, <code class="language-plaintext highlighter-rouge">hica build</code>, <code class="language-plaintext highlighter-rouge">hica check</code> (which reports effects!), <code class="language-plaintext highlighter-rouge">hica test</code>, <code class="language-plaintext highlighter-rouge">hica fmt</code>, <code class="language-plaintext highlighter-rouge">hica new</code>, <code class="language-plaintext highlighter-rouge">hica clean</code>. 
The CLI is using my Clap inspired <a href="https://github.com/cladam/klap">klap library</a>, which handles flags, subcommands, and help generation.</p>

<p>I needed to test hica properly and have reused <a href="https://github.com/cladam/kunit">kunit</a> to validate and verify its functionality, hundreds of tests across lexer, parser, checker, codegen, and CLI.</p>

<h3 id="built-with-help-of-a-genie">Built with help of a Genie</h3>

<p>I used a Genie (what I call my GenAI model – thanks <a href="https://tidyfirst.substack.com/">Kent Beck</a> for the term) as a pair-programming partner throughout. I maintain <a href="https://github.com/cladam/hica/blob/main/documentation/backlog.md">the backlog</a> and iterate until it’s right. 
It lets me focus on the design decisions; the Genie handles the mechanical parts. It’s been superfun, and a breath of fresh air compared to grinding through boilerplate alone.</p>

<h3 id="try-it">Try it</h3>

<p>The language is fun to use, and really usable with structs, enums, full pattern matching, modules, closures, UFCS, built-in testing.</p>

<p>I have tried to document along the way, and to be honest, I think I did a good job with it, take a look:</p>

<ul>
  <li><a href="https://cladam.github.io/hica/playground/"><strong>Playground</strong></a>: try hica in the browser, no installation needed.</li>
  <li><a href="https://cladam.github.io/hica/docs/"><strong>Docs</strong></a>: language reference, quick start, and style guide.</li>
  <li><a href="https://github.com/cladam/hica"><strong>Source</strong></a>: Apache-2.0 licensed.</li>
</ul>

<h3 id="thank-you-koka">Thank you, Koka</h3>

<p>hica wouldn’t exist without Koka. The effect system, Perceus, the C backend is all inherited. Huge thanks to everyone working on Koka for making it possible for someone like me to build a real language.</p>]]></content><author><name>Claes Adamsson</name><email>claes.adamsson@gmail.com</email></author><category term="koka" /><category term="languages" /><category term="hica" /><category term="learning" /><summary type="html"><![CDATA[In my post about cloning ls in Koka I mentioned wanting to build a small language that transpiles to Koka. That idea has become hica!]]></summary></entry><entry><title type="html">Rebuilding GNU ls in Koka</title><link href="/2026/05/06/koka-ls-clone/" rel="alternate" type="text/html" title="Rebuilding GNU ls in Koka" /><published>2026-05-06T00:00:00+00:00</published><updated>2026-05-06T00:00:00+00:00</updated><id>/2026/05/06/koka-ls-clone</id><content type="html" xml:base="/2026/05/06/koka-ls-clone/"><![CDATA[<p>In a <a href="https://cladam.github.io/2026/04/06/koka-development/">previous post</a>, I introduced <a href="https://koka-lang.github.io/koka/doc/index.html">Koka</a> and why I started porting <code class="language-plaintext highlighter-rouge">ls</code> to it. But I never wrote about the actual backstory.</p>

<p>I stumbled onto Koka in my GitHub feed and it immediately got me hooked. This sentance alone is golden – “Koka is a strongly typed functional-style language with effect types and handlers that transpiles to C11”.</p>

<p>Right around then, I saw a LinkedIn post where someone had built a parallel <code class="language-plaintext highlighter-rouge">ls</code> in modern C++ and tagged <a href="https://codingchallenges.fyi/">John Cricket</a>, which led me to his coding challenges.</p>

<p>If you really want to learn a language you should be hands-on and do exercises, John has listed several fun and challenging exercises but <code class="language-plaintext highlighter-rouge">ls</code> wasn’t on the list, so that was the one I picked 🙂</p>

<h3 id="the-ambition">The Ambition</h3>

<p>The goal: Rebuild GNU <code class="language-plaintext highlighter-rouge">ls</code> in Koka. 100% byte-for-byte compatible output.</p>

<p>That sounded like “a weekend project, maybe two” until I opened <code class="language-plaintext highlighter-rouge">ls.c</code>… The quick command I run hundred times a day is a 5,000-line beast of C, 83 CLI flags, and decades of accumulated edge cases. It’s a masterpiece of over-engineering, and it’s rock solid.</p>

<h3 id="getting-into-the-guts">Getting Into the Guts</h3>

<p>I’m working against the 9.10 codebase, and the architecture is a four-stage gauntlet: <strong>Setup → Parsing → Execution → Output</strong>.</p>

<p>The setup alone is a 1,000-line jungle of globals and structs. <code class="language-plaintext highlighter-rouge">decode_switches()</code>, the monster that parses options, is nearly 600 lines long!</p>

<p>I have created a couple of notes if people want to learn more about the internals and tips on how to use GNU ls (and my version of course):</p>

<ul>
  <li><a href="https://github.com/cladam/koka-labs/blob/main/docs/decoded-gnu-ls.md">Decoded GNU ls</a> – The architecture walkthrough.</li>
  <li><a href="https://github.com/cladam/koka-labs/blob/main/docs/gnu-ls-columns.md">Column Layout</a> – The math behind <code class="language-plaintext highlighter-rouge">-C</code>.</li>
  <li><a href="https://github.com/cladam/koka-labs/blob/main/docs/gnu-ls-exit-codes.md">Exit Codes</a> – The three states of “oops.”</li>
  <li><a href="https://github.com/cladam/koka-labs/blob/main/docs/gnu-ls-guide.md">Usage Guide</a> – Practical tips for GNU <code class="language-plaintext highlighter-rouge">ls</code>.</li>
  <li><a href="https://github.com/cladam/koka-labs/blob/main/docs/gnu-ls-recursive.md">Recursive Listing</a> – How <code class="language-plaintext highlighter-rouge">-R</code> traverses the tree.</li>
</ul>

<p>The official docs and the <a href="https://www.man7.org/linux/man-pages/man1/ls.1.html">man page</a> are fine for users, but studying the source code is the only way to see how it really works.</p>

<h3 id="the-roadmap">The Roadmap</h3>

<p>I drafted a plan of 6 phases + the infrastructure needed like a testing framwork and proper CI using GitHub Actions.</p>

<ol>
  <li><strong>Phase 1 — Foundation</strong></li>
  <li><strong>Phase 2 — Which files are listed</strong></li>
  <li><strong>Phase 3 — What information is listed</strong></li>
  <li><strong>Phase 4 — Sorting the output</strong></li>
  <li><strong>Phase 5 — General output formatting</strong></li>
  <li><strong>Phase 6 — Formatting the file names</strong></li>
</ol>

<p>To keep myself in check, I have built a test framework (kunit) that diffs my output against the original GNU binary. CI fails immediatly if I’m out of line…</p>

<h3 id="why-koka">Why Koka?</h3>

<p><code class="language-plaintext highlighter-rouge">ls</code> is actually a perfect stress test for Koka’s unique features:</p>
<ul>
  <li><strong>The Effect System:</strong> <code class="language-plaintext highlighter-rouge">ls</code> is a mix of pure logic (sorting/formatting) and messy side effects (disk I/O, <code class="language-plaintext highlighter-rouge">stat()</code> calls). Koka’s effects make that boundary visible in the types.</li>
  <li><strong>Data Modelling:</strong> File types, sort modes, format styles, indicator kinds. The enums in <code class="language-plaintext highlighter-rouge">ls.c</code> map naturally to Koka’s algebraic types.</li>
  <li><strong>Perceus:</strong> This is Koka’s secret weapon. It’s a reference-counting system that allows functional code to perform like C.</li>
  <li><strong>The FFI:</strong> Koka’s standard library is still growing, so I’ve had to write C shims for things like symlink detection. The FFI (Foreign Function Interface) has been surprisingly painless and fun.</li>
</ul>

<h3 id="learning-in-public">Learning in Public</h3>

<p>I’ll be the first to admit: I’m a decent but not pro programmer and a total Koka novice. GNU <code class="language-plaintext highlighter-rouge">ls</code> is not “beginner friendly” territory. The C is dense and Koka’s documentation is still thin compared to other mainstream languages.</p>

<p>I’ve spent a lot of time “rubber-ducking” with a Genie to get through the nuts and bolts, figuring out why <code class="language-plaintext highlighter-rouge">stat()</code> and <code class="language-plaintext highlighter-rouge">lstat()</code> treat symlinks differently, or how <code class="language-plaintext highlighter-rouge">-l</code> is supposed to silently override <code class="language-plaintext highlighter-rouge">-C</code>.</p>

<p>This project doesn’t have a delivery-date. It’s just me, a 40-year-old C program, and a language that really got me hooked. It’s been a blast.</p>

<p>I’m not going to rebuild all of Coreutils, no way. <code class="language-plaintext highlighter-rouge">ls</code> is more than enough. But the process has triggered a different curiosity: I want to build a small language that transpiles to Koka. 🤓
Follow along at <a href="https://github.com/cladam/koka-labs">koka-labs</a>.</p>]]></content><author><name>Claes Adamsson</name><email>claes.adamsson@gmail.com</email></author><category term="koka" /><category term="languages" /><category term="learning" /><category term="coreutils" /><summary type="html"><![CDATA[In a previous post, I introduced Koka and why I started porting ls to it. But I never wrote about the actual backstory.]]></summary></entry><entry><title type="html">A Time Machine for Your Working Directory</title><link href="/2026/04/19/the-wip-guard/" rel="alternate" type="text/html" title="A Time Machine for Your Working Directory" /><published>2026-04-19T00:00:00+00:00</published><updated>2026-04-19T00:00:00+00:00</updated><id>/2026/04/19/the-wip-guard</id><content type="html" xml:base="/2026/04/19/the-wip-guard/"><![CDATA[<p>In a <a href="https://cladam.github.io/2026/04/12/capturing-intent/">previous post</a>, I introduced the Intent Log, a way to
capture the <em>why</em> alongside the <em>what</em> during development. The Intent Log targets and solves a piece of the comprehension problem.
But there’s a related problem it didn’t address: <strong>the safety problem.</strong></p>

<p>In Trunk-Based Development, you sync with the remote constantly. That’s the whole point.
tbdflow is running <code class="language-plaintext highlighter-rouge">git fetch</code> and <code class="language-plaintext highlighter-rouge">git rebase --autostash</code> under the hood and originally I had trust that uncommitted files would stay untouched but there are occasions when this assumption breaks.
If a genie (or even you) runs a <code class="language-plaintext highlighter-rouge">git pull</code> then the work would be gone unless you do the git stash-pull-stash pop loop.</p>

<p>I’ve been building a feature I’m calling <strong>WIP Guard</strong>, a (semi-)continuous, invisible safety snapshots that give you a time
machine for your working directory.</p>

<h2 id="the-three-ways-work-gets-lost">The three ways work gets lost</h2>

<p>There are three scenarios that keep developers anxious in a high-frequency integration workflow:</p>

<ol>
  <li>
    <p><strong>The buried stash.</strong> A rebase fails. Git auto-stashed your changes, but the stash is now tangled in a half-applied
rebase. Less experienced developers might panic and even experienced ones will lose minutes untangling it.</p>
  </li>
  <li>
    <p><strong>The accidental overwrite.</strong> A developer runs <code class="language-plaintext highlighter-rouge">git stash pop</code> or <code class="language-plaintext highlighter-rouge">git pull</code> manually after a failed <code class="language-plaintext highlighter-rouge">tbdflow sync</code>.
The previous working state is gone.</p>
  </li>
  <li>
    <p><strong>The context gap.</strong> You wrote a breadcrumb with <code class="language-plaintext highlighter-rouge">tbdflow + "trying trait-based approach"</code> twenty minutes ago. But
the <em>code</em> that inspired that thought? It only exists in your working directory, which has since moved on. The note
is orphaned from the state it describes.</p>
  </li>
</ol>

<p>WIP Guard solves all three.</p>

<h2 id="how-it-works-git-stash-create">How it works: <code class="language-plaintext highlighter-rouge">git stash create</code></h2>

<p>The key insight is that Git already has a mechanism for creating immutable snapshots of your working directory:
<code class="language-plaintext highlighter-rouge">git stash create</code>. Unlike a regular <code class="language-plaintext highlighter-rouge">git stash</code>, this command:</p>

<ul>
  <li>Creates a commit object representing your index and working tree</li>
  <li>Does <strong>not</strong> add it to the stash reflog (so it can’t interfere with your manual stashes)</li>
  <li>Produces an immutable hash that stays in the Git object store until garbage collection (typically 14–30 days)</li>
</ul>

<p>WIP Guard calls <code class="language-plaintext highlighter-rouge">git stash create</code> silently at strategic moments and stores the resulting hash in the Intent Log.
It doesn’t introduce any new concepts, just using standard Git and much more manageable.</p>

<h2 id="the-four-hooks">The four hooks</h2>

<p>WIP Guard is “continuous” because it hooks into commands you’re already running. I didn’t want to add a new command, rather integrate it as helpful magic.</p>

<h3 id="hook-a-the-intent-hook">Hook A: The Intent Hook</h3>

<p>Every time you record a breadcrumb with <code class="language-plaintext highlighter-rouge">tbdflow +</code>, the CLI now captures a snapshot alongside the note:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ tbdflow + "factory pattern is too complex, switching to traits"
Note recorded: "factory pattern is too complex, switching to traits"
WIP snapshot: a7b8c9d0e1
</code></pre></div></div>

<p>That hash is stored inside the note object in <code class="language-plaintext highlighter-rouge">.tbdflow-intent.json</code>. The breadcrumb is
a thought <em>linked to the exact code that inspired it</em>. You can go back to the code as it was at the moment of
realisation.</p>

<h3 id="hook-b-the-sync-hook">Hook B: The Sync Hook</h3>

<p>Before <code class="language-plaintext highlighter-rouge">tbdflow sync</code> initiates a rebase, it now does two things:</p>

<ol>
  <li>
    <p><strong>Anti-collision check.</strong> It verifies that <code class="language-plaintext highlighter-rouge">.git/REBASE_HEAD</code>, <code class="language-plaintext highlighter-rouge">.git/MERGE_HEAD</code>, and <code class="language-plaintext highlighter-rouge">.git/CHERRY_PICK_HEAD</code> don’t
exist. If a git operation is already in progress, tbdflow halts and tells you to resolve it first. This alone
prevents a whole class of “Git ghost” states.</p>
  </li>
  <li>
    <p><strong>Pre-sync snapshot.</strong> Regardless of whether you’ve added notes, tbdflow captures a safety snapshot and stores it as
<code class="language-plaintext highlighter-rouge">last_sync_snapshot</code> in the intent log. If the rebase goes sideways, your pre-rebase state is one command away.</p>
  </li>
</ol>

<h3 id="hook-c-the-radar-hook">Hook C: The Radar Hook</h3>

<p><code class="language-plaintext highlighter-rouge">tbdflow radar</code> already scans for overlapping work on remote branches. Now it also silently captures a snapshot if your
working directory is dirty and the last snapshot is more than 30 minutes old. This catches the case where a developer is
heads-down coding and hours pass between syncs.</p>

<h3 id="hook-d-the-undo-hook">Hook D: The Undo Hook</h3>

<p>This one is easy to overlook. <code class="language-plaintext highlighter-rouge">tbdflow undo</code> is the “panic button”. It checks out main, fast-forwards, and reverts a
commit. That’s a destructive sequence for your working directory. If you had uncommitted changes on a feature branch
when you hit the panic button, they’d be gone.</p>

<p>Now, <code class="language-plaintext highlighter-rouge">tbdflow undo</code> captures a snapshot and runs the same anti-collision check before doing anything. The panic button
has its own safety catch.</p>

<h2 id="the-escape-hatch-tbdflow-recover">The escape hatch: <code class="language-plaintext highlighter-rouge">tbdflow recover</code></h2>

<p>All of this snapshotting would be pointless without a clean recovery path. That’s <code class="language-plaintext highlighter-rouge">tbdflow recover</code>:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ tbdflow recover --list
Available WIP snapshots:
  #     Type       Timestamp              Note                                     Hash
  ------------------------------------------------------------------------------------------
  1     intent     2026-04-18T14:15:00     trying trait-based approach              a7b8c9d0e1
  2     intent     2026-04-18T14:42:00     added error variants                     f2e3d4c5b6
  3     pre-sync   2026-04-18T15:01:00     Pre-sync safety snapshot                 e1f2g3h4i5
</code></pre></div></div>

<p>To restore:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ tbdflow recover 1
Warning: This will apply the snapshot over your current working directory.
Snapshot applied successfully.
The snapshot remains available for future recovery.
</code></pre></div></div>

<p>Note that we use <code class="language-plaintext highlighter-rouge">git stash apply</code>, not <code class="language-plaintext highlighter-rouge">git stash pop</code>. The snapshot commit stays in the Git store. You can apply it
again if needed. The snapshot is immutable which means you can’t accidentally destroy your safety net.</p>

<h2 id="the-lifecycle-what-happens-when-you-commit">The lifecycle: what happens when you commit?</h2>

<p>So what happens when you commit? If snapshots live in the Intent Log, and the Intent Log gets consumed by <code class="language-plaintext highlighter-rouge">tbdflow commit</code>, don’t the
snapshots disappear?</p>

<p>Yes they do, and that’s by design. The commit is the lifecycle boundary. Once your work is committed and pushed to trunk, it’s
in git history. The snapshots have served their purpose. When tbdflow clears the intent log after a successful push, it
tells you:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Releasing 3 WIP snapshot(s) — your work is now in git history.
Intent log consumed and cleared.
</code></pre></div></div>

<p>The Git objects themselves aren’t deleted, they linger in the object store until garbage collection. But the references
are gone, and that’s fine. Your work is safe in a commit now.</p>

<p>There’s an important subtlety here: <strong>this only happens on trunk commits.</strong> If you’re committing to a feature branch,
the intent log (and all its snapshots) are preserved. The safety net stays up until your work reaches main.</p>

<h2 id="the-stale-branch-guard">The stale-branch guard</h2>

<p>One important safety detail: if you switch branches and try to recover a snapshot from a different branch context,
tbdflow warns you:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Stale intent log detected: notes were captured on 'feat/auth', but you are now on 'main'.
</code></pre></div></div>

<p>This prevents the subtle mistake of applying a snapshot from one feature’s context onto another feature’s working
directory.</p>

<h2 id="why-this-matters-for-tbd-adoption">Why this matters for TBD adoption</h2>

<p>The number one objection I hear when advocating for Trunk-Based Development is: <strong>“What if I lose my work?”</strong></p>

<p>It’s a fair concern. In a branch-heavy workflow, your feature branch is your safety net. You can push half-finished work
to a remote branch and it’s backed up. In TBD, your work-in-progress lives locally until it’s ready for trunk. That’s a
trust gap.</p>

<p>WIP Guard closes that gap. Every breadcrumb you drop, every sync you run, every radar scan — they all leave behind an
immutable snapshot of your working directory. Your work is preserved automatically, without you doing anything different
from what you’d normally do.</p>

<p>The recovery path is one command, not a Stack Overflow deep-dive into <code class="language-plaintext highlighter-rouge">git reflog</code> and <code class="language-plaintext highlighter-rouge">git fsck</code>.</p>

<h2 id="try-it">Try it</h2>

<p>WIP Guard is available now. If you’re already using the Intent Log, you get snapshots for free — just keep using
<code class="language-plaintext highlighter-rouge">tbdflow +</code> as you work. If something goes wrong, <code class="language-plaintext highlighter-rouge">tbdflow recover --list</code> shows you what’s available.</p>

<p>Give it a try: <a href="https://github.com/cladam/tbdflow">tbdflow on GitHub</a>.</p>

<blockquote>
  <p><em>Throughput is a safety feature!</em></p>
</blockquote>]]></content><author><name>Claes Adamsson</name><email>claes.adamsson@gmail.com</email></author><category term="DevOps" /><category term="tbdflow" /><category term="tbd" /><category term="safety" /><summary type="html"><![CDATA[In a previous post, I introduced the Intent Log, a way to capture the why alongside the what during development. The Intent Log targets and solves a piece of the comprehension problem. But there’s a related problem it didn’t address: the safety problem.]]></summary></entry><entry><title type="html">Capturing Intent Before the Commit</title><link href="/2026/04/11/capturing-intent/" rel="alternate" type="text/html" title="Capturing Intent Before the Commit" /><published>2026-04-11T00:00:00+00:00</published><updated>2026-04-11T00:00:00+00:00</updated><id>/2026/04/11/capturing-intent</id><content type="html" xml:base="/2026/04/11/capturing-intent/"><![CDATA[<p>In previous posts, I’ve talked about the <a href="https://cladam.github.io/2026/02/22/the-comprehension-crisis/">Comprehension Crisis</a>: the risk that as we move faster, especially with AI agents, we lose the “why” behind our code. A <code class="language-plaintext highlighter-rouge">git diff</code> tells you what changed. It says nothing about what was tried and rejected. It’s a record of the result, but it deletes the tries and struggles.</p>

<p>I’ve been exploring a new feature in <code class="language-plaintext highlighter-rouge">tbdflow</code> to bridge that gap: the <strong>Intent Log</strong>.</p>

<p>The goal is pretty simple (but ambitious). I want to capture the developer’s (or agent’s) reasoning while it’s fresh, without adding the documentation tax that usually kills these efforts.</p>

<h3 id="low-friction-breadcrumbs">Low friction breadcrumbs</h3>

<p>For many of us, jotting things down is a vital mechanism for staying in flow. We need to leave breadcrumbs to maintain context as we navigate complex problems.</p>

<p>The problem I see isn’t the act of writing; it’s the friction of the “diary.” We don’t want to leave our environment to update a formal document or a separate ticket. We want to capture those internal realisations or “failed tries” directly in the stream of work.</p>

<p>To solve this, I’ve added two ways to log a breadcrumb during development:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>tbdflow note &lt;message&gt;             <span class="c"># Log a note</span>
tbdflow + &lt;message&gt;                <span class="c"># Shorthand alias</span>
</code></pre></div></div>

<p>With <code class="language-plaintext highlighter-rouge">+</code>, the friction is close to zero. No shell escaping, no context switching, no separate file to manage. It also works directly without doing <code class="language-plaintext highlighter-rouge">task start &lt;description&gt;</code>.</p>

<h3 id="the-discipline">The discipline</h3>

<p>I’ve added breadcrumb instructions to both tbdflow <code class="language-plaintext highlighter-rouge">SKILL.md</code> and <code class="language-plaintext highlighter-rouge">AGENTS.md</code>, they are good instructions so same rules should apply whether you’re a human or an AI agent working in the repo:</p>

<ul>
  <li>Drop a breadcrumb whenever you change approach or reject an alternative.</li>
  <li>Before a complex commit, there should be at least one or two breadcrumbs explaining major decisions.</li>
  <li>Do not wait until commit time — log as you go.</li>
</ul>

<p>The point is that reasoning captured after the fact is reconstruction. Reasoning captured in the moment is evidence. The intent log only works if the habit is there.</p>

<h3 id="the-lifecycle-of-a-thought">The lifecycle of a thought</h3>

<p>The Intent Log follows a strict lifecycle designed to keep the repository clean while enriching the history.</p>

<p><strong>Capture:</strong> The first time you use a note command, <code class="language-plaintext highlighter-rouge">tbdflow</code> creates a local <code class="language-plaintext highlighter-rouge">.tbdflow-intent.json</code>. This file is automatically added to <code class="language-plaintext highlighter-rouge">.gitignore</code> so it never accidentally hits the trunk.</p>

<p><strong>Tasks:</strong> For more structured work, you can use <code class="language-plaintext highlighter-rouge">tbdflow task start "Refactor auth logic"</code>. This sets a high-level context that all subsequent notes are attached to.</p>

<p><strong>Integrate:</strong> When you run <code class="language-plaintext highlighter-rouge">tbdflow commit</code>, the CLI reads your notes and injects them into the body of the commit message, positioned before any breaking change or TODO footers.</p>

<p><strong>Cleanup:</strong> Once the commit is pushed to the trunk, the local JSON file is deleted. The reasoning is preserved in git history, but the workspace is reset.</p>

<h3 id="handling-branch-drift">Handling branch drift</h3>

<p>One challenge with local logging is branch switching. If you start a task on a feature branch and switch to a hotfix, your notes shouldn’t follow you blindly.</p>

<p><code class="language-plaintext highlighter-rouge">tbdflow</code> now tracks branch ownership in the intent log. If you try to add a note on a different branch than where you started, the CLI will warn you:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Stale intent log detected: notes were captured on <span class="s1">'feat/auth'</span>, but you are now on <span class="s1">'main'</span><span class="nb">.</span>
</code></pre></div></div>

<p>If you explicitly start a new task on the new branch, <code class="language-plaintext highlighter-rouge">tbdflow</code> will rebind the existing notes to the new context. The developer is always aware of which stream their thoughts belong to.</p>

<h3 id="why-this-helps-with-tbd-adoption">Why this helps with TBD adoption</h3>

<p><strong>Context for the auditor.</strong> In a Non-blocking Review, the code is already integrated. The auditor needs to know the intent to verify whether the implementation matches the goal. Seeing that a developer tried and rejected a pattern saves the auditor from suggesting the same thing. Hopefully it turns into a knowledge-sharing moment.</p>

<p><strong>Agent transparency.</strong> When an AI agent is working, it can use the <code class="language-plaintext highlighter-rouge">+</code> shorthand to think out loud. The human developer can see the agent’s reasoning before the final commit is pushed. It turns the agent from a black box into a transparent colleague.</p>

<p><strong>Fighting cognitive atrophy.</strong> By capturing these notes, the history of the struggle isn’t lost. Anyone picking up a task or reverting a change can see the decision-making process.</p>

<h3 id="the-result">The result</h3>

<p>The Intent Log captures metadata at the point of creation. It requires zero context-switching and provides value during the post-integration audit.</p>

<p>It makes for a more honest git history where the “why” is just as accessible as the “what.”</p>

<p>If you want to see how the Intent Log formats the final commit message, <code class="language-plaintext highlighter-rouge">tbdflow</code> includes a <code class="language-plaintext highlighter-rouge">--dry-run</code> flag that prints it before execution. You can read more about why I believe dry-runs are essential for workflow tools <a href="https://cladam.github.io/2025/08/23/dry-run/">here</a>.</p>

<p>Give it a try and let me know if the workflow fits you and your team: <a href="https://github.com/cladam/tbdflow">tbdflow on GitHub</a>.</p>

<blockquote>
  <p><em>Throughput is a safety feature!</em></p>
</blockquote>]]></content><author><name>Claes Adamsson</name><email>claes.adamsson@gmail.com</email></author><category term="DevOps" /><category term="tbdflow" /><category term="tbd" /><category term="culture" /><summary type="html"><![CDATA[In previous posts, I’ve talked about the Comprehension Crisis: the risk that as we move faster, especially with AI agents, we lose the “why” behind our code. A git diff tells you what changed. It says nothing about what was tried and rejected. It’s a record of the result, but it deletes the tries and struggles.]]></summary></entry><entry><title type="html">Porting Coreutils to Koka</title><link href="/2026/04/06/koka-development/" rel="alternate" type="text/html" title="Porting Coreutils to Koka" /><published>2026-04-06T00:00:00+00:00</published><updated>2026-04-06T00:00:00+00:00</updated><id>/2026/04/06/koka-development</id><content type="html" xml:base="/2026/04/06/koka-development/"><![CDATA[<p>I like to explore different programming languages, and I often try them out but most of them ends up as a “Hello World” in a folder I never reopen, digital fossils of a Saturday afternoon curiosity… 
My actual day-to-day has been settled for a while: Kotlin when I’m building for the web, Rust when I need a CLI tool. I do think about them a lot, but their syntax, semantics, and functionality don’t tickle me the same way anymore.</p>

<p>Then Koka came along…</p>

<h3 id="why-koka">Why Koka</h3>

<p>It’s a research language from Microsoft Research. Functional, but not in the way that usually makes me bounce off after an afternoon.</p>

<p>The dot selection syntax was the first thing that felt right. Coming from Kotlin, I’m used to chaining: <code class="language-plaintext highlighter-rouge">names.filter(is-hidden).sort(cmp).foreach(println)</code>. Koka lets me write like that. No inside-out nesting. Data flows left to right, which is how I think about it anyway.</p>

<p>But what really got me was the effect system. In most languages, a function signature tells you what goes in and what comes out. In Koka, it also tells you what the function <em>does</em>. A signature like <code class="language-plaintext highlighter-rouge">fn(path) -&gt; &lt;fsys,exn&gt; list&lt;string&gt;</code> says: this touches the filesystem and might throw. It’s right there on the tin and not buried in documentation.</p>

<p>The compiler enforces it. There is no way to sneak in a side-effect, type systems are great like that. It’s “honest programming” in a way.</p>

<h3 id="porting-ls">Porting <code class="language-plaintext highlighter-rouge">ls</code></h3>

<p>I don’t learn languages from tutorials. I need a real problem. So I started <a href="https://github.com/cladam/koka-labs">koka-labs</a>, where I’m porting <code class="language-plaintext highlighter-rouge">ls</code> and <code class="language-plaintext highlighter-rouge">wc</code> from GNU Coreutils. First up: <code class="language-plaintext highlighter-rouge">ls</code>.</p>

<p><code class="language-plaintext highlighter-rouge">ls</code> looks simple. List files, print strings. But a proper implementation touches a lot:</p>

<ul>
  <li><strong>Sorting</strong> I had to write an insertion sort to be able to reverse sort and get a feel of how <code class="language-plaintext highlighter-rouge">Koka</code> handles recursion.</li>
  <li><strong>File metadata</strong> Hidden files, permissions, timestamps. This is where I’ll hit the boundary between pure logic and actual OS interaction, that will be fun when I get to more “advanced” listing.</li>
  <li><strong>Perceus</strong> Koka’s reference counting system. This is what makes functional code run at speeds you wouldn’t expect from a language without manual memory management.</li>
</ul>

<p>Working through it piece by piece, following the GNU philosophy, has been a good way to see how Koka keeps things clean while dealing with the mess of a real operating system.</p>

<p>I have even done some C to implement missing features in stdlib needed for <code class="language-plaintext highlighter-rouge">ls -F</code>. This was super-easy, I created a C file in the same directory with a couple of small functions and then it was just to import and wire it up.</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// Import my C code</span>
<span class="k">extern</span> <span class="n">import</span>
  <span class="n">c</span> <span class="n">file</span> <span class="s">"fs-inline.c"</span>

<span class="c1">// C FFI</span>
<span class="k">extern</span> <span class="n">is</span><span class="o">-</span><span class="n">symlink</span><span class="p">(</span><span class="n">p</span> <span class="o">:</span> <span class="n">string</span><span class="p">)</span> <span class="o">:</span> <span class="n">fsys</span> <span class="n">bool</span>
  <span class="n">c</span> <span class="s">"kk_os_is_symlink"</span>
</code></pre></div></div>

<h3 id="what-stands-out-so-far">What stands out so far</h3>

<p>The effect system changes how I structure code. When side effects are visible in the types, separatng pure logic from IO becomes natural. The language makes that the easiest path.</p>

<p>Perceus is the other thing, functional languages have a reputation for being slow or memory-hungry. Koka’s approach to reference counting gives you immutability without the usual performance cost.</p>

<h3 id="no-rush">No rush</h3>

<p>There’s no timeline on this, and no requirements. Just a compiler and a rabbit hole. I’ll keep porting tools and writing about what I find.</p>

<p>The repo is public if you want to follow along.</p>]]></content><author><name>Claes Adamsson</name><email>claes.adamsson@gmail.com</email></author><category term="koka" /><category term="languages" /><category term="learning" /><summary type="html"><![CDATA[I like to explore different programming languages, and I often try them out but most of them ends up as a “Hello World” in a folder I never reopen, digital fossils of a Saturday afternoon curiosity… My actual day-to-day has been settled for a while: Kotlin when I’m building for the web, Rust when I need a CLI tool. I do think about them a lot, but their syntax, semantics, and functionality don’t tickle me the same way anymore.]]></summary></entry></feed>