<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Forem: David Hoze</title>
    <description>The latest articles on Forem by David Hoze (@david_hoze).</description>
    <link>https://forem.com/david_hoze</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F2738930%2F937874a7-c404-4a4b-8796-a57d9a5b7e27.png</url>
      <title>Forem: David Hoze</title>
      <link>https://forem.com/david_hoze</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/david_hoze"/>
    <language>en</language>
    <item>
      <title>Introducing claude-collab</title>
      <dc:creator>David Hoze</dc:creator>
      <pubDate>Sun, 08 Mar 2026 04:09:11 +0000</pubDate>
      <link>https://forem.com/david_hoze/introducing-claude-collab-5mm</link>
      <guid>https://forem.com/david_hoze/introducing-claude-collab-5mm</guid>
      <description>&lt;p&gt;Hi there.&lt;/p&gt;

&lt;p&gt;I've been working on my project — &lt;a href="https://github.com/david-hoze/bit" rel="noopener noreferrer"&gt;&lt;code&gt;bit&lt;/code&gt;&lt;/a&gt; — and wanted to work on several ideas at once. I tried doing it with 3 working directories, git pushing and pulling and merging... it really gave me a headache. So I started using agents in the same directory, and telling them not to interfere with each other. It worked OK, but at some point it got hard always coordinating them, making sure they don't step on each other's toes. That's why I built &lt;a href="https://github.com/david-hoze/claude-collab" rel="noopener noreferrer"&gt;claude-collab&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Implemented in Haskell, it's a single binary CLI tool that gives multiple Claude Code agents a way to coordinate. No server, no database, just the filesystem. The idea is simple: give agents two primitives — &lt;strong&gt;claim&lt;/strong&gt; a file and &lt;strong&gt;commit&lt;/strong&gt; your work — and a &lt;strong&gt;reservation system&lt;/strong&gt; for resources you can't share. That's basically it. The agents are smart enough to figure out the rest.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem
&lt;/h2&gt;

&lt;p&gt;Picture this: you have Agent A working on auth, Agent B working on the config system, and Agent C writing tests. Sounds great until Agent A and Agent B both decide to touch &lt;code&gt;src/utils.hs&lt;/code&gt; at the same time. Or Agent C runs &lt;code&gt;run-test-suite.sh&lt;/code&gt; while Agent A is mid-refactor and everything blows up. Or two agents commit at the same time and you get a mess.&lt;/p&gt;

&lt;p&gt;You &lt;em&gt;could&lt;/em&gt; just tell them "hey don't touch each other's files" but... they're agents. They don't always listen. And even when they do, there's no structured way for them to know what the other agents are doing.&lt;/p&gt;

&lt;h2&gt;
  
  
  How It Works
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Setup: One Command
&lt;/h3&gt;

&lt;p&gt;Run &lt;code&gt;claude-collab install&lt;/code&gt; in your project root and you're done. It installs three Claude Code hooks that automate the entire workflow:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;SessionStart&lt;/strong&gt; — auto-registers each agent when a session opens&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PreToolUse&lt;/strong&gt; (Edit/Write) — auto-claims files before any edit&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SessionEnd&lt;/strong&gt; — auto-cleans up when a session closes&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This means agents don't need to remember to register or claim files. The only manual step is committing — and that's intentional. You want agents to commit deliberately, after they finished a feature, not after every edit.&lt;/p&gt;

&lt;h3&gt;
  
  
  Agent Registration
&lt;/h3&gt;

&lt;p&gt;Under the hood, each agent registers itself when its session starts:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;claude-collab init &lt;span class="nt"&gt;--name&lt;/span&gt; auth-refactor
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This gives it a unique hash (like &lt;code&gt;a3f8b201&lt;/code&gt;) and a human-readable name. The name is stored in the registry and can be used as an alias for the hash in all subsequent commands. So instead of &lt;code&gt;claude-collab commit a1b2c3d4 -m "msg"&lt;/code&gt; the agent can do &lt;code&gt;claude-collab commit auth-refactor -m "msg"&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Claiming Files
&lt;/h3&gt;

&lt;p&gt;Before an agent edits a file, it claims it (automatically, via the hook):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;claude-collab files claim a3f8b201 src/auth.ts
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If nobody else has it — great, you got it. If another agent already claimed it? You get a rejection (exit code 1), which forces the agent to pause and negotiate. This is the key design choice: the friction is intentional. It creates a natural "hey, let's talk about this" moment.&lt;/p&gt;

&lt;p&gt;The rejected agent can then message the other agent using Claude Code's native messaging and either:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Wait for the other agent to finish&lt;/li&gt;
&lt;li&gt;Co-claim the file with &lt;code&gt;--shared&lt;/code&gt; if they're working on different parts
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# After negotiating with the other agent...&lt;/span&gt;
claude-collab files claim a3f8b201 src/auth.ts &lt;span class="nt"&gt;--shared&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Committing
&lt;/h3&gt;

&lt;p&gt;When an agent is done with its work:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;claude-collab commit a3f8b201 &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="s2"&gt;"refactor auth validation"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This stages and commits only the files that agent claimed. No accidentally committing someone else's work. The committed files are automatically unclaimed, freeing them up for others.&lt;/p&gt;

&lt;p&gt;But here's where it gets interesting — what about co-claimed files? If two agents both edited the same file, you can't just have them both commit separately.&lt;/p&gt;

&lt;p&gt;So I used git's staging mechanism. Instead of trying to figure out how to commit only one agent's changes and not the other, &lt;code&gt;claude-collab&lt;/code&gt; simply stages the first agent's files and waits for the other agent to finish. When the second agent commits, it triggers the real &lt;code&gt;git commit&lt;/code&gt; with everyone's changes combined:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[a3f8b201] refactor token validation | [d4e5] add rate limiting
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One clean commit with both agents' work. No merge conflicts, no manual intervention. And the first agent doesn't need to sit around waiting — it can claim new files, make more edits, and even run another &lt;code&gt;commit&lt;/code&gt; for different work while the co-claimed files stay staged in the background.&lt;/p&gt;

&lt;h3&gt;
  
  
  Resource Reservations
&lt;/h3&gt;

&lt;p&gt;Some operations can't run in parallel — two agents running the test suite at the same time would interfere with each other. So there's a reservation system:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Reserve the test suite&lt;/span&gt;
claude-collab reserve a3f8b201 &lt;span class="nb"&gt;test&lt;/span&gt;

&lt;span class="c"&gt;# Run your tests&lt;/span&gt;
run-test-suite.sh

&lt;span class="c"&gt;# Release it&lt;/span&gt;
claude-collab release a3f8b201 &lt;span class="nb"&gt;test&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After running tests, agents message each other with the results using Claude Code's native messaging, so others can skip redundant work.&lt;/p&gt;

&lt;p&gt;If another agent already has the resource, &lt;code&gt;reserve&lt;/code&gt; will wait (polling every 500ms) until it's free or times out. Each reservation has a TTL so if an agent crashes without releasing, the reservation expires. The TTL is configured per-resource in &lt;code&gt;.claude/agents/resources.json&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Two resources come pre-configured: &lt;code&gt;test&lt;/code&gt; and &lt;code&gt;build&lt;/code&gt;. You can add your own by editing that file.&lt;/p&gt;

&lt;h2&gt;
  
  
  Under the Hood
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Locking
&lt;/h3&gt;

&lt;p&gt;All the coordination uses &lt;strong&gt;mkdir-based locks&lt;/strong&gt;. &lt;code&gt;mkdir&lt;/code&gt; is atomic on every major filesystem (NTFS, ext4, APFS), so it's a cheap and reliable mutex. There are two locks:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Git lock&lt;/strong&gt; — serializes git commits so two agents can't commit simultaneously (30s stale timeout)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reserve lock&lt;/strong&gt; — protects reservation updates (5s stale timeout)&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Crash Recovery
&lt;/h3&gt;

&lt;p&gt;If an agent crashes mid-task, nothing deadlocks. Lock directories older than their stale timeout get force-broken by the next agent that needs them. Resource reservations expire after their TTL, so a crashed agent's reservation doesn't block others forever. And the &lt;code&gt;SessionEnd&lt;/code&gt; hook runs cleanup automatically when a session closes — unclaiming files, releasing reservations, and removing the agent from the registry.&lt;/p&gt;

&lt;p&gt;The one thing that doesn't auto-recover is staged co-claimed files. If Agent A stages changes for a two-phase commit and then crashes before Agent B commits, those files stay staged in the registry. But that's a deliberate trade-off — it's better to leave staged work intact for manual recovery than to silently discard it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Atomic Writes
&lt;/h3&gt;

&lt;p&gt;All JSON file writes (the registry, reservations, resources) use write-to-temp-then-rename. This prevents partial writes from corrupting the data if an agent crashes mid-write. On Windows, the destination is removed before renaming since Windows &lt;code&gt;rename&lt;/code&gt; fails if the target already exists.&lt;/p&gt;

&lt;h2&gt;
  
  
  Putting It All Together
&lt;/h2&gt;

&lt;p&gt;Here's what a typical multi-agent session looks like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Agent 1: auth work&lt;/span&gt;
claude-collab init &lt;span class="nt"&gt;--name&lt;/span&gt; auth-agent
claude-collab files claim auth-agent src/auth.ts src/session.ts
&lt;span class="c"&gt;# ... edit files ...&lt;/span&gt;
claude-collab commit auth-agent &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="s2"&gt;"refactor auth flow"&lt;/span&gt;
claude-collab cleanup auth-agent

&lt;span class="c"&gt;# Agent 2: tests (running concurrently)&lt;/span&gt;
claude-collab init &lt;span class="nt"&gt;--name&lt;/span&gt; test-agent
claude-collab reserve test-agent &lt;span class="nb"&gt;test
&lt;/span&gt;npm &lt;span class="nb"&gt;test
&lt;/span&gt;claude-collab release test-agent &lt;span class="nb"&gt;test
&lt;/span&gt;claude-collab cleanup test-agent
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And if they need the same file? They negotiate through Claude Code's native messaging, co-claim, and the two-phase commit handles the rest.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Design Philosophy
&lt;/h2&gt;

&lt;p&gt;The whole thing is built on one idea: &lt;strong&gt;don't try to be smarter than the agents&lt;/strong&gt;. Instead of building a complex locking system with priority queues and deadlock detection, just give agents simple tools and let them talk through Claude Code's native messaging. They're Claude Code instances — they can negotiate, delegate, and figure things out. The claim system creates the pause; the agents fill it with conversation.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Working Example
&lt;/h2&gt;

&lt;p&gt;Here's a real session where I used agents to fix test failures, and two of them ended up touching the same file. Pay attention to the &lt;strong&gt;bold note&lt;/strong&gt; on &lt;code&gt;binary-fixer&lt;/code&gt; — that's the co-claim and two-phase commit in action.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;❯ OK, run another team to fix all failures
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;2 agents launched:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Agent&lt;/th&gt;
&lt;th&gt;Task&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;@export-fixer&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Fix export/import junction routing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;@binary-fixer&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Fix binary test edge cases&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Team &lt;code&gt;fix-failures&lt;/code&gt; is up with 3 agents:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Agent&lt;/th&gt;
&lt;th&gt;Task&lt;/th&gt;
&lt;th&gt;Failures&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;export-fixer&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Export/import broken in junction mode&lt;/td&gt;
&lt;td&gt;14 CLI test failures&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;binary-fixer&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Binary test edge cases&lt;/td&gt;
&lt;td&gt;4 failures (restore, mv subdir, merge -X theirs)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;init-fixer&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;init-nested test 8&lt;/td&gt;
&lt;td&gt;1 CLI test failure&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;blockquote&gt;
&lt;p&gt;Skipping &lt;code&gt;network-remote.test&lt;/code&gt; (9 failures) — those require &lt;code&gt;\\tsclient&lt;/code&gt; UNC path which is infrastructure-only.&lt;/p&gt;

&lt;p&gt;That's 19 fixable failures across the three agents. I'll relay results as they come in.&lt;br&gt;
&lt;/p&gt;
&lt;/blockquote&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;❯ Don't skip network-remote.test.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Added &lt;code&gt;network-fixer&lt;/code&gt; to handle the 9 &lt;code&gt;network-remote.test&lt;/code&gt; failures. Now all &lt;strong&gt;28 failures&lt;/strong&gt; are covered:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Agent&lt;/th&gt;
&lt;th&gt;Failures&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;export-fixer&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;14 (export + import)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;binary-fixer&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;4 (restore, mv, merge)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;init-fixer&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;1 (init-nested)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;network-fixer&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;9 (network-remote)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;p&gt;&lt;strong&gt;&lt;code&gt;@init-fixer&lt;/code&gt;&lt;/strong&gt; — Fixed init-nested test 8, committed &lt;code&gt;cf618cd&lt;/code&gt;. Test expectation was outdated for the gitfile change. ✅&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;@binary-fixer&lt;/code&gt;&lt;/strong&gt; — All 3 binary test fixes done and verified. 1 real code bug (&lt;code&gt;bit mv&lt;/code&gt; to subdirectory) and 2 test bugs. &lt;strong&gt;The &lt;code&gt;Passthrough.hs&lt;/code&gt; commit is blocked on a co-claim with &lt;code&gt;d6d55282&lt;/code&gt; — likely &lt;code&gt;export-fixer&lt;/code&gt; who also needs to edit &lt;code&gt;Passthrough.hs&lt;/code&gt;. When &lt;code&gt;export-fixer&lt;/code&gt; commits, both changes will land together.&lt;/strong&gt; ✅&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;@export-fixer&lt;/code&gt;&lt;/strong&gt; — Fixed export/import/init junction routing, all tests pass. The &lt;code&gt;isBitOnlyCommand&lt;/code&gt; guard is the right approach — keeps the junction early-exit but carves out bit-specific commands. ✅&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;@network-fixer&lt;/code&gt;&lt;/strong&gt; — &lt;code&gt;network-remote.test&lt;/code&gt; fixed, 17/17 pass, committed. Network tests now degrade gracefully — pass with real UNC when available, skip cleanly when not. ✅&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Worked for 15m 59s&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Results: 28 failures → 0
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Agent&lt;/th&gt;
&lt;th&gt;Failures Fixed&lt;/th&gt;
&lt;th&gt;Type&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;init-fixer&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;1 (init-nested)&lt;/td&gt;
&lt;td&gt;Test expectation updated for gitfile&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;binary-fixer&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;4 (restore, mv subdir, merge -X theirs)&lt;/td&gt;
&lt;td&gt;1 code bug + 2 test bugs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;export-fixer&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;14 (export + import)&lt;/td&gt;
&lt;td&gt;Code bug — junction early-exit skipping bit-only commands&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;network-fixer&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;9 (network-remote)&lt;/td&gt;
&lt;td&gt;Added graceful skip when &lt;code&gt;\\tsclient&lt;/code&gt; unavailable&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Total: 28 failures → 0.&lt;/strong&gt; The full CLI suite now passes clean (minus cloud tests needing specific remotes).&lt;/p&gt;

&lt;p&gt;All agents shut down. 🧹&lt;/p&gt;

&lt;h2&gt;
  
  
  What's Next
&lt;/h2&gt;

&lt;p&gt;You can grab a pre-built binary — &lt;a href="https://github.com/david-hoze/claude-collab/releases/download/v0.1.0.0/claude-collab.exe" rel="noopener noreferrer"&gt;Windows&lt;/a&gt; or &lt;a href="https://github.com/david-hoze/claude-collab/releases/download/v0.1.0.0/claude-collab-0.1.0.0-linux-x86_64.tar.gz" rel="noopener noreferrer"&gt;Linux x86_64&lt;/a&gt; — or build from source with &lt;code&gt;cabal build &amp;amp;&amp;amp; cabal install&lt;/code&gt;. Once it's on your PATH, run &lt;code&gt;claude-collab install&lt;/code&gt; in your project root. It sets up &lt;code&gt;CLAUDE_COLLAB.md&lt;/code&gt;, the hooks, and adds a few lines to &lt;code&gt;CLAUDE.md&lt;/code&gt; so your agents know how to use it.&lt;/p&gt;

&lt;p&gt;Happy collaborating!&lt;/p&gt;

</description>
      <category>agents</category>
      <category>ai</category>
      <category>cli</category>
      <category>showdev</category>
    </item>
    <item>
      <title>Cast Your Bread Upon the Waters</title>
      <dc:creator>David Hoze</dc:creator>
      <pubDate>Sun, 15 Feb 2026 20:35:54 +0000</pubDate>
      <link>https://forem.com/david_hoze/cast-your-bread-upon-the-waters-4gkn</link>
      <guid>https://forem.com/david_hoze/cast-your-bread-upon-the-waters-4gkn</guid>
      <description>&lt;p&gt;Hi, my name is David, and I'm a very good developer. For insane reasons I cannot specify right now, I stopped working in the tech industry. I now work approximately 3 hours a day fund raising for the religious schools of my orthodox congregation. I study Torah at night, where Rachel Imenu (yes, from the bible) is buried, and like it a lot. I wasn't laid off because of AI. My story is different.&lt;/p&gt;

&lt;p&gt;During my time here calling people and asking them to renew their donations, I started hacking around with stuff. One thing led to another, and I started thinking of an idea. Why can't I just &lt;code&gt;git add&lt;/code&gt; mp3 files, pdfs, or whatever, and just commit and push them? I know, no diffs, and git doesn't handle that well and so on and so on. But &lt;strong&gt;why&lt;/strong&gt;? The more I thought of it, ideas started popping. Not some extension or a tool with different semantics, the &lt;strong&gt;same&lt;/strong&gt; git semantics and simplicity, just for other files. Ideas started coming to my head, and I thought about it all day long.&lt;/p&gt;

&lt;p&gt;When things started to form into shape, I consulted AI and started writing my program. I found out how easy it was to write code using AI, and from copying and pasting, I slowly started to find out about developing more seriously with AI. Cursor IDE was my first, and it amazed me how easy it was to write code, specs, tests (things I was always too lazy to do as a developer), and it was just FUN. AI did all the heavy-lifting for me, and I just had to interfere when it was doing silly things, and pointing it to the right direction.&lt;/p&gt;

&lt;p&gt;I wasn't a developer anymore. I have become an architect. The years of experience came into play. I knew what good code looked like, and I could spot architectural mistakes. I no longer had to meddle with the details, I just had to think of an architecture, weigh subtle alternatives and make important decisions. I have become a manager of junior programmers, Claude et al. It was amazing. But what was more amazing were the circumstances that led me to it. I have very little money, and for various reasons, I can not be employed right now as a developer. That actually gave me unlimited freedom. I could just program what I wanted, not caring if anyone liked it, would buy it or whatever.&lt;/p&gt;

&lt;p&gt;So, I created &lt;a href="https://github.com/david-hoze/bit" rel="noopener noreferrer"&gt;&lt;code&gt;bit&lt;/code&gt;&lt;/a&gt;, which I'm very proud of. It's still a work in progress, but I have high hopes for it. So, what am I saying? Maybe you're a developer who has not found himself in the right place in the tech industry... Maybe you're over-qualified in what you do. Maybe you lost your jobs because of current trends, or maybe you're just wondering what your role is in the world of AI development. I want to tell you what I think. I think that AI poses an opportunity. It's an opportunity to do what you've always wanted.. Write in any language you like, and develop that amazing tool you always wanted to. With AI you can do in weeks what would take months or years, and all by yourself. As a seasoned dev, you can instruct AI to build GOOD and maintainable software. You can design a product, and use skills a manager might not have, i.e. reading code, understanding trade-offs... You can skim over code, find out architectural mistakes AI makes (and it still makes them), and guide it to give very good results...&lt;/p&gt;

&lt;p&gt;King Solomon says "Cast your bread upon the waters, for after many days you will find it again" (Ecclesiastes 11:1). I have the advantage of having nothing to lose. That gives me a lot of freedom. Maybe my story will inspire people to create something, without expecting return.&lt;/p&gt;

</description>
      <category>career</category>
      <category>devjournal</category>
      <category>git</category>
      <category>sideprojects</category>
    </item>
    <item>
      <title>Writing idiomatic Haskell with AI</title>
      <dc:creator>David Hoze</dc:creator>
      <pubDate>Thu, 12 Feb 2026 15:29:04 +0000</pubDate>
      <link>https://forem.com/david_hoze/writing-idiomatic-haskell-with-ai-1p13</link>
      <guid>https://forem.com/david_hoze/writing-idiomatic-haskell-with-ai-1p13</guid>
      <description>&lt;p&gt;So, I checked out my project, &lt;a href="https://github.com/david-hoze/bit" rel="noopener noreferrer"&gt;&lt;code&gt;bit&lt;/code&gt;&lt;/a&gt; — a version-control tool for binary files, that I talked about in my &lt;a href="https://dev.to/david_hoze/how-i-built-my-project-in-haskell-without-knowing-haskell-6ng"&gt;previous article&lt;/a&gt;, and the code looked pretty decent. However, it really looked like it was imperative code written in Haskell. So true, the types and the ADTs were great (though not fully taken advantage of in a lot of the project), and I believe that the mere fact that a function is pure and has no side-effects, lets AI reason about the function much more easily. But it looked like the AI wasn't fully taking advantage of all of Haskell's features.&lt;/p&gt;

&lt;h2&gt;
  
  
  Letting AI do the research
&lt;/h2&gt;

&lt;p&gt;So.. I asked Claude to research and give me two documents, a &lt;a href="https://github.com/david-hoze/bit/blob/master/docs/idiomatic-haskell.md" rel="noopener noreferrer"&gt;Guide for Writing Idiomatic Haskell&lt;/a&gt; a &lt;a href="https://github.com/david-hoze/bit/blob/master/docs/haskell-type-safety.md" rel="noopener noreferrer"&gt;Guide for Type Safety in Haskell&lt;/a&gt; . I then used Cursor IDE to run Opus 4.6 and use those guides to refactor the code. At first, it just changed a lot of &lt;code&gt;return&lt;/code&gt;s to &lt;code&gt;pure&lt;/code&gt;s (and not all of them). I told the agent it looks a little weird that it only changed that, so it admitted and made a deeper pass over the code base, that had a lot of changes. I then asked it again, and it did another one, again with a lot of changes. I did that for 12 (!) rounds, and each time it found something new.. I'm talking about Opus 4.6 here.. One of the times I tried Sonnet 4.5, but it did some weird refactoring.. These refactors require more subtlety and reasoning I guess.&lt;/p&gt;

&lt;p&gt;This isn't surprising — &lt;a href="https://arxiv.org/html/2403.15185v1" rel="noopener noreferrer"&gt;recent research&lt;/a&gt; found that LLMs struggle with Haskell specifically because functional languages make up a tiny fraction of training data (Haskell is just 0.29% of The Stack]), a major code training dataset). The models know the syntax, but they default to imperative patterns unless pushed.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the AI actually changed
&lt;/h2&gt;

&lt;p&gt;After twelve rounds of refactoring guided by the two reference documents, the diff touched 30 Haskell files across roughly 1,500 lines. Many changes were mechanical — &lt;code&gt;pure&lt;/code&gt; over &lt;code&gt;return&lt;/code&gt;, &lt;code&gt;void&lt;/code&gt; over &lt;code&gt;_ &amp;lt;-&lt;/code&gt; — but three categories stood out as the AI applying genuine Haskell reasoning, not just surface-level substitution.&lt;/p&gt;

&lt;h3&gt;
  
  
  Killing boolean blindness with sum types
&lt;/h3&gt;

&lt;p&gt;The original code tracked push behavior with two booleans on the environment record:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight haskell"&gt;&lt;code&gt;&lt;span class="kr"&gt;data&lt;/span&gt; &lt;span class="kt"&gt;BitEnv&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;BitEnv&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="o"&gt;...&lt;/span&gt;
    &lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;envForce&lt;/span&gt;          &lt;span class="o"&gt;::&lt;/span&gt; &lt;span class="kt"&gt;Bool&lt;/span&gt;
    &lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;envForceWithLease&lt;/span&gt; &lt;span class="o"&gt;::&lt;/span&gt; &lt;span class="kt"&gt;Bool&lt;/span&gt;
    &lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;...&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Since force with lease is already force, the combination &lt;code&gt;(True, True)&lt;/code&gt; was meaningless. And indeed, the command parser had a runtime guard to reject it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight haskell"&gt;&lt;code&gt;&lt;span class="n"&gt;when&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;isForce&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;isForceWithLease&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;$&lt;/span&gt; &lt;span class="kr"&gt;do&lt;/span&gt;
    &lt;span class="n"&gt;hPutStrLn&lt;/span&gt; &lt;span class="n"&gt;stderr&lt;/span&gt; &lt;span class="s"&gt;"fatal: Cannot use both --force and --force-with-lease"&lt;/span&gt;
    &lt;span class="n"&gt;exitWith&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;ExitFailure&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So, on one of the passes, the AI replaced both booleans with a single sum type:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight haskell"&gt;&lt;code&gt;&lt;span class="kr"&gt;data&lt;/span&gt; &lt;span class="kt"&gt;ForceMode&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;NoForce&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kt"&gt;Force&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kt"&gt;ForceWithLease&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It was one field less, no runtime guard, and every consumer switched from a nested &lt;code&gt;if-else-if&lt;/code&gt; to &lt;code&gt;case fMode of&lt;/code&gt; — which looks nicer, and is checked by the compiler exhaustiveness. And on the safety angle, the illegal state &lt;code&gt;(True, True)&lt;/code&gt; is no longer representable.&lt;/p&gt;

&lt;p&gt;The AI applied this same transformation twice more, and in each case, the pattern was the same: an undernamed boolean became a properly named type, that anyone (including AI) can read immediately and understand.&lt;/p&gt;

&lt;p&gt;This is what Robert Harper calls &lt;a href="https://existentialtype.wordpress.com/2011/03/15/boolean-blindness/" rel="noopener noreferrer"&gt;boolean blindness&lt;/a&gt; — a &lt;code&gt;Bool&lt;/code&gt; carries no information beyond its value, so the moment you branch on it, you've lost the &lt;em&gt;meaning&lt;/em&gt; of what was tested. And in software safety terms, no test can prove the absence of a &lt;code&gt;(True, True)&lt;/code&gt; code path as reliably as a type that simply can't express it. (An &lt;a href="https://earlbarr.com/publications/typestudy.pdf" rel="noopener noreferrer"&gt;ICSE 2017 study&lt;/a&gt; found that static type systems catch roughly 15% of public bugs in JavaScript projects — bugs that tests missed...)&lt;/p&gt;

&lt;h3&gt;
  
  
  Replacing verbose case expressions with combinators
&lt;/h3&gt;

&lt;p&gt;A nice replacement is this one:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight haskell"&gt;&lt;code&gt;&lt;span class="n"&gt;bs&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;-&lt;/span&gt; &lt;span class="kt"&gt;BS&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;readFile&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;
&lt;span class="kr"&gt;let&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kr"&gt;case&lt;/span&gt; &lt;span class="n"&gt;decodeUtf8'&lt;/span&gt; &lt;span class="n"&gt;bs&lt;/span&gt; &lt;span class="kr"&gt;of&lt;/span&gt;
      &lt;span class="kt"&gt;Left&lt;/span&gt; &lt;span class="kr"&gt;_&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt;
      &lt;span class="kt"&gt;Right&lt;/span&gt; &lt;span class="n"&gt;txt&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="kt"&gt;T&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;unpack&lt;/span&gt; &lt;span class="n"&gt;txt&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The AI recognized every instance as the &lt;code&gt;either&lt;/code&gt; eliminator and collapsed them:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight haskell"&gt;&lt;code&gt;&lt;span class="n"&gt;bs&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;-&lt;/span&gt; &lt;span class="kt"&gt;BS&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;readFile&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;
&lt;span class="kr"&gt;let&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;either&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;const&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;T&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;unpack&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;decodeUtf8'&lt;/span&gt; &lt;span class="n"&gt;bs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A lot of boilerplate is gone here, less clutter for the mind (and for AI) to deal with.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reducing ambiguity per token with Functor
&lt;/h2&gt;

&lt;p&gt;This &lt;code&gt;classifyRemoteState&lt;/code&gt; example though, has a deeper advantage:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight haskell"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Before&lt;/span&gt;
&lt;span class="n"&gt;classifyRemoteState&lt;/span&gt; &lt;span class="n"&gt;remote&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kr"&gt;do&lt;/span&gt;
    &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;-&lt;/span&gt; &lt;span class="kt"&gt;Transport&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;listRemoteItems&lt;/span&gt; &lt;span class="n"&gt;remote&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
    &lt;span class="kr"&gt;case&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="kr"&gt;of&lt;/span&gt;
        &lt;span class="kt"&gt;Left&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;pure&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;StateNetworkError&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="kt"&gt;Right&lt;/span&gt; &lt;span class="n"&gt;items&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;pure&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;interpretRemoteItems&lt;/span&gt; &lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;-- After&lt;/span&gt;
&lt;span class="n"&gt;classifyRemoteState&lt;/span&gt; &lt;span class="n"&gt;remote&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
    &lt;span class="n"&gt;either&lt;/span&gt; &lt;span class="kt"&gt;StateNetworkError&lt;/span&gt; &lt;span class="n"&gt;interpretRemoteItems&lt;/span&gt;
      &lt;span class="o"&gt;&amp;lt;$&amp;gt;&lt;/span&gt; &lt;span class="kt"&gt;Transport&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;listRemoteItems&lt;/span&gt; &lt;span class="n"&gt;remote&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;&amp;lt;$&amp;gt;&lt;/code&gt; means that we're chaining the result from &lt;code&gt;Transport.listRemoteItems remote 1&lt;/code&gt; to either one of the options, an error or the remote items, but it also means that no side effects happen when chaining.&lt;/p&gt;

&lt;p&gt;The former &lt;code&gt;do&lt;/code&gt; block on the other hand, uses monadic bind (&lt;code&gt;&amp;gt;&amp;gt;=&lt;/code&gt;), which tells the compiler and the reader "the next step &lt;strong&gt;might depend&lt;/strong&gt; on the result of the previous one." But the &lt;code&gt;&amp;lt;$&amp;gt;&lt;/code&gt; version uses &lt;code&gt;Functor&lt;/code&gt;, which says something stronger: "this is a &lt;strong&gt;pure&lt;/strong&gt; transformation over an effectful value — unlike the &lt;code&gt;do&lt;/code&gt; notation, the function inside the &lt;code&gt;&amp;lt;$&amp;gt;&lt;/code&gt; can't print, can't read files, can't launch side effects based on whether it got a Left or Right.&lt;/p&gt;

&lt;p&gt;The model processes both versions either way — but the difference lies in &lt;em&gt;pattern recognition load&lt;/em&gt;. When the model sees &lt;code&gt;&amp;lt;$&amp;gt;&lt;/code&gt;, it can classify the entire expression in one step: "pure function applied over an effect, move on." When it sees the &lt;code&gt;do&lt;/code&gt; version, it has to read each line to reach the same conclusion: "bind, then case, then pure in both branches — ok, so this is just a pure transformation."&lt;/p&gt;

&lt;p&gt;This change actually reduces &lt;strong&gt;ambiguity per token&lt;/strong&gt;. Each expression carries more information about what it &lt;em&gt;can't&lt;/em&gt; do, which means the model's context window is doing more useful work. It's simple information theory: higher signal per token means &lt;strong&gt;less&lt;/strong&gt; work to resolve the meaning of the surrounding context.&lt;/p&gt;

&lt;p&gt;There's growing evidence for this. &lt;a href="https://arxiv.org/abs/2404.07965" rel="noopener noreferrer"&gt;A NeurIPS 2024 paper&lt;/a&gt; showed that not all tokens contribute equally to learning — roughly half are "easy tokens" that carry little information, while training selectively on high-information tokens improved math reasoning by up to 30%. And &lt;a href="https://arxiv.org/abs/2404.08335" rel="noopener noreferrer"&gt;research on tokenization theory&lt;/a&gt; has shown that how information is packed into tokens directly affects whether transformers can learn underlying structure. The implication for code is that expressions which encode more meaning per token — like &lt;code&gt;&amp;lt;$&amp;gt;&lt;/code&gt; signaling purity — give the model richer signal to work with.&lt;/p&gt;

&lt;p&gt;The win is &lt;strong&gt;local reasoning&lt;/strong&gt;. The &lt;code&gt;&amp;lt;$&amp;gt;&lt;/code&gt; version communicates its intent in its type structure rather than requiring you to read the implementation to confirm "yes, &lt;code&gt;result&lt;/code&gt; is only used once, in a pure context, and &lt;code&gt;pure&lt;/code&gt; is the only effect after the bind."&lt;/p&gt;

&lt;h3&gt;
  
  
  Introducing an ADT and finding a bug
&lt;/h3&gt;

&lt;p&gt;Sometimes the refactor itself lets AI unravel logical bugs. &lt;code&gt;compareHistory&lt;/code&gt; in &lt;code&gt;RemoteManagement.hs&lt;/code&gt; compared local and remote histories, to check whether a push would fast-forward or not. It checked both directions, and had a pattern match on the resulting booleans:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight haskell"&gt;&lt;code&gt;&lt;span class="n"&gt;localAhead&lt;/span&gt;  &lt;span class="o"&gt;&amp;lt;-&lt;/span&gt; &lt;span class="kt"&gt;Git&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;checkIsAhead&lt;/span&gt; &lt;span class="n"&gt;rHash&lt;/span&gt; &lt;span class="n"&gt;lHash&lt;/span&gt;
&lt;span class="n"&gt;remoteAhead&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;-&lt;/span&gt; &lt;span class="kt"&gt;Git&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;checkIsAhead&lt;/span&gt; &lt;span class="n"&gt;lHash&lt;/span&gt; &lt;span class="n"&gt;rHash&lt;/span&gt;

&lt;span class="kr"&gt;case&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;localAhead&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;remoteAhead&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kr"&gt;of&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;False&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;putStrLn&lt;/span&gt; &lt;span class="s"&gt;"    main pushes to main (fast-forwardable)"&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;putStrLn&lt;/span&gt; &lt;span class="s"&gt;"    main pushes to main (local out of date)"&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;False&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;putStrLn&lt;/span&gt; &lt;span class="s"&gt;"    main pushes to main (local out of date)"&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;putStrLn&lt;/span&gt; &lt;span class="s"&gt;"    main pushes to main (up to date)"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When the AI first wrote this code, it was just filling in strings. Notice though, that for the &lt;code&gt;(False, False)&lt;/code&gt; case — neither side ahead of the other, which means the repos &lt;strong&gt;diverged&lt;/strong&gt;. The AI conveniently printed "local out of date", which sounds plausible enough if you're not thinking too hard. And clearly it wasn't. What caused this? The string has no structure, no type checker reading it, no compiler verifying it means what it says. It's just characters going to a terminal.&lt;/p&gt;

&lt;p&gt;Then I asked the AI to refactor using an ADT. It introduced &lt;code&gt;PushRefStatus&lt;/code&gt; with three constructors — &lt;code&gt;PushRefUpToDate&lt;/code&gt;, &lt;code&gt;PushRefFastForwardable&lt;/code&gt;, &lt;code&gt;PushRefLocalOutOfDate&lt;/code&gt; — and a bridge function to convert the boolean pair. But when it got to &lt;code&gt;(False, False)&lt;/code&gt; and had to map it to a constructor, something shifted. It couldn't just type a vague phrase and move on. It had to pick a &lt;em&gt;name&lt;/em&gt; — a name that would appear in type signatures, in pattern matches, in code review. And &lt;code&gt;PushRefLocalOutOfDate&lt;/code&gt; was the wrong name.&lt;/p&gt;

&lt;p&gt;If the hashes aren't equal and neither side is ahead of the other, the histories have &lt;em&gt;diverged&lt;/em&gt; — both sides have commits the other lacks. The AI flagged this itself during the refactor: the act of naming the state precisely made the incorrectness visible.&lt;/p&gt;

&lt;p&gt;The fix was to add a fourth constructor:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight haskell"&gt;&lt;code&gt;&lt;span class="kr"&gt;data&lt;/span&gt; &lt;span class="kt"&gt;PushRefStatus&lt;/span&gt;
  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;PushRefUpToDate&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kt"&gt;PushRefFastForwardable&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kt"&gt;PushRefLocalOutOfDate&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kt"&gt;PushRefDiverged&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the principle Yaron Minsky articulated as &lt;a href="https://blog.janestreet.com/effective-ml/" rel="noopener noreferrer"&gt;"make illegal states unrepresentable"&lt;/a&gt; — but here it worked in a subtler way. The illegal state wasn't a type error; it was a semantic error that became visible when the type demanded precision. Alexis King's &lt;a href="https://lexi-lambda.github.io/blog/2019/11/05/parse-don-t-validate/" rel="noopener noreferrer"&gt;"Parse, don't validate"&lt;/a&gt; makes the same argument from a different angle: a parser (or an ADT) forces you to commit to what your data means, where a validator (or a string) lets you be vague.&lt;/p&gt;

&lt;p&gt;This is something worth internalizing about AI-assisted development. When the output is a string, the AI can be vague and get away with it — "local out of date" is close enough, and no tool will object. But when the output is a type, vagueness has a cost. A constructor name is a commitment: it appears everywhere the value is handled, and it has to be accurate at every site. The ADT didn't just replace the booleans — it raised the precision bar high enough that the AI couldn't miss a case it had previously gotten wrong.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;AI is used to writing imperative code, but it in fact &lt;strong&gt;knows&lt;/strong&gt; how to write Haskell code, it just needs to be pushed in that direction. This gives FP beginners an easier start when trying to enter this world. It does the heavy-&lt;code&gt;lifting&lt;/code&gt; (pun intended) for us, and the result is a more expressive, more reliable and robust code, that's easier for AI, or experienced FP programmers, to reason about.&lt;/p&gt;

&lt;p&gt;Overall I'm having a lot of fun writing Haskell with AI. Claude Opus 4.6 doesn't seem to "struggle" with Haskell, it's smart enough. I'm learning a lot of cool concepts as I go along, an can apply them with a lot of the tedious work being done by AI.&lt;/p&gt;

</description>
      <category>haskell</category>
      <category>cursor</category>
      <category>ai</category>
    </item>
    <item>
      <title>How I Built a Version Control Tool in Haskell Using AI – Even Though I'm a Swift Developer</title>
      <dc:creator>David Hoze</dc:creator>
      <pubDate>Sat, 07 Feb 2026 23:54:53 +0000</pubDate>
      <link>https://forem.com/david_hoze/how-i-built-my-project-in-haskell-without-knowing-haskell-6ng</link>
      <guid>https://forem.com/david_hoze/how-i-built-my-project-in-haskell-without-knowing-haskell-6ng</guid>
      <description>&lt;p&gt;Though I'm an experienced Swift developer, I barely know Haskell. I vaguely understand what a monad is, and I once spent an afternoon fighting cabal before giving up. I had no business writing a serious program in Haskell.&lt;/p&gt;

&lt;p&gt;And yet I built &lt;a href="https://github.com/david-hoze/bit" rel="noopener noreferrer"&gt;&lt;code&gt;bit&lt;/code&gt;&lt;/a&gt; — a version-control tool for binary files, like Git but for large media, datasets, and anything too big for Git to handle sanely — almost entirely through AI-assisted programming in Haskell. It's a real CLI tool that I use daily. What I discovered runs directly against the conventional wisdom about which languages work best with AI.&lt;/p&gt;

&lt;h3&gt;
  
  
  Table of Contents
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;The Conventional Wisdom Is Backwards&lt;/li&gt;
&lt;li&gt;What Happens When You Make AI Write Haskell?&lt;/li&gt;
&lt;li&gt;The Workflow&lt;/li&gt;
&lt;li&gt;AI Doesn't Just Write FP — It Discovers FP&lt;/li&gt;
&lt;li&gt;The Training Data Paradox Is Temporary&lt;/li&gt;
&lt;li&gt;It Works&lt;/li&gt;
&lt;li&gt;The Real Question&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The Conventional Wisdom Is Backwards
&lt;/h2&gt;

&lt;p&gt;Ask around and you'll hear: use Python, use JavaScript. Most training data, gentlest syntax.&lt;/p&gt;

&lt;p&gt;That's true. AI generates Python fluently. The problem is what happens next. It always compiles, because almost everything compiles in Python. Three weeks later you discover a silently mutated dictionary broke your data pipeline. The AI wrote confident, fluent, completely buggy code. Yes, Python is forgiving, but in a forgiving language, AI's mistakes go undetected.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Happens When You Make AI Write Haskell?
&lt;/h2&gt;

&lt;p&gt;At first, nothing compiles. &lt;a href="https://www.haskell.org/ghc/" rel="noopener noreferrer"&gt;GHC&lt;/a&gt; (the Haskell compiler) rejects almost everything, &lt;strong&gt;but&lt;/strong&gt; the errors are &lt;em&gt;specific&lt;/em&gt; — not "something went wrong" but "this function returns &lt;code&gt;IO String&lt;/code&gt; and you're using it where &lt;code&gt;Either Error String&lt;/code&gt; is expected."&lt;/p&gt;

&lt;p&gt;So, &lt;a href="https://www.cursor.com/" rel="noopener noreferrer"&gt;Cursor&lt;/a&gt; sees the errors right away, and everything is immediately fixed. One or two rounds and it compiled. And when Haskell compiles, it usually works.&lt;/p&gt;

&lt;p&gt;I'd gone from "fluent code that's silently broken" to "broken code that converges on correct code almost immediately."&lt;/p&gt;

&lt;h2&gt;
  
  
  The Workflow
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Brainstorm&lt;/strong&gt; → I describe what I want using Claude Opus 4.5 with extended thinking, and research mode when I thought it was necessary. I then brainstorm about the idea, and when it's final, I ask Claude to give me a Cursor prompt. Claude has access to my code, so it writes a &lt;strong&gt;really good&lt;/strong&gt; prompt for Cursor.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Write code and tests&lt;/strong&gt; → Cursor then writes the code and tests using Sonnet 4.5 (cheap model) that does it very fast and accurate, cuz it has a killer prompt from Claude.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Compile and fix&lt;/strong&gt; → Cursor handles all of the dependency and GHC errors. Very smooth.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Test and fix&lt;/strong&gt; → Also very cool. I have time now to write this article.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Reflect&lt;/strong&gt; → After a feature is done, I ask Cursor this question:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;You introduced some bugs while implementing this feature, right? Analyze why this happened structurally. What about the code's design made it easy to break [X] when touching [Y]? Suggest a refactor that would make this class of bug impossible or caught at compile time.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I expected generic advice. I got precise analysis of real design flaws that had caused real bugs minutes earlier. I took Cursor's suggestions back to Claude and asked it what we should do about this. Claude took some of Sonnet's suggestions seriously and wrote me a refactor prompt for Cursor. And there were some suggestions it declined. My code is now refactored to prevent &lt;strong&gt;real&lt;/strong&gt; bugs AI introduced to the system, because it wasn't built well enough.&lt;/p&gt;

&lt;p&gt;The code didn't just get better. It got &lt;em&gt;harder to break&lt;/em&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  AI Doesn't Just Write FP — It Discovers FP
&lt;/h2&gt;

&lt;p&gt;Here's what I didn't expect. I asked the AI whether functional programming had abstractions that could improve my code's structure. It searched the web, read FP articles, and came back with concepts I'd never heard of.&lt;/p&gt;

&lt;p&gt;There's a &lt;a href="https://wiki.haskell.org/Arrow_tutorial" rel="noopener noreferrer"&gt;Kleisli arrow&lt;/a&gt; composition in my codebase now — it's called &lt;code&gt;Pipeline&lt;/code&gt; — and it elegantly chains pure transformations: scan → diff → plan, where the pure core has no IO at all and is fully property-testable. I don't understand the category theory. But I don't need to.&lt;/p&gt;

&lt;p&gt;Here's what it actually looks like in my code. The entire sync logic is a pure function — no network calls, no filesystem access, just data in and data out:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight haskell"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- The pure core: no IO, fully property-testable&lt;/span&gt;
&lt;span class="n"&gt;diffAndPlan&lt;/span&gt; &lt;span class="o"&gt;::&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kt"&gt;FileEntry&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kt"&gt;FileEntry&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kt"&gt;RcloneAction&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;diffAndPlan&lt;/span&gt; &lt;span class="n"&gt;sourceFiles&lt;/span&gt; &lt;span class="n"&gt;targetFiles&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
  &lt;span class="kr"&gt;let&lt;/span&gt; &lt;span class="n"&gt;sourceIndex&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;buildIndexFromFileEntries&lt;/span&gt; &lt;span class="n"&gt;sourceFiles&lt;/span&gt;
      &lt;span class="n"&gt;targetIndex&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;buildIndexFromFileEntries&lt;/span&gt; &lt;span class="n"&gt;targetFiles&lt;/span&gt;
      &lt;span class="n"&gt;diffs&lt;/span&gt;       &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;computeDiff&lt;/span&gt; &lt;span class="n"&gt;sourceIndex&lt;/span&gt; &lt;span class="n"&gt;targetIndex&lt;/span&gt;
  &lt;span class="kr"&gt;in&lt;/span&gt;  &lt;span class="n"&gt;map&lt;/span&gt; &lt;span class="n"&gt;planAction&lt;/span&gt; &lt;span class="n"&gt;diffs&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It goes further. Here's what AI built into my codebase — concepts I have some intuition for but do not fully understand:&lt;/p&gt;

&lt;h3&gt;
  
  
  Phantom Types
&lt;/h3&gt;

&lt;p&gt;A &lt;a href="https://wiki.haskell.org/Phantom_type" rel="noopener noreferrer"&gt;phantom type&lt;/a&gt; is a type parameter that appears in a type's definition but isn't used in its data. It exists purely for the compiler to enforce constraints.&lt;/p&gt;

&lt;p&gt;My hash type uses one so the compiler distinguishes MD5 from SHA256. Mixing hash algorithms is a compile error. One line of type machinery that eliminates an entire class of bugs forever:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight haskell"&gt;&lt;code&gt;&lt;span class="kr"&gt;data&lt;/span&gt; &lt;span class="kt"&gt;HashAlgo&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;MD5&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kt"&gt;SHA256&lt;/span&gt;

&lt;span class="kr"&gt;newtype&lt;/span&gt; &lt;span class="kt"&gt;Hash&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="o"&gt;::&lt;/span&gt; &lt;span class="kt"&gt;HashAlgo&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;Hash&lt;/span&gt; &lt;span class="kt"&gt;Text&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now if a function expects &lt;code&gt;Hash 'MD5&lt;/code&gt; and you pass it &lt;code&gt;Hash 'SHA256&lt;/code&gt;, GHC stops you at compile time. No runtime check needed. I think it looks cool. Couldn't write it myself.&lt;/p&gt;

&lt;h3&gt;
  
  
  Opaque Types with Smart Constructors
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;Remote&lt;/code&gt; exports its type but hides the constructor. You can only create one through &lt;code&gt;mkRemote&lt;/code&gt;. Invalid remotes are unrepresentable:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight haskell"&gt;&lt;code&gt;&lt;span class="kr"&gt;module&lt;/span&gt; &lt;span class="nn"&gt;Bit.Remote&lt;/span&gt;
  &lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="kt"&gt;Remote&lt;/span&gt;          &lt;span class="c1"&gt;-- type exported, constructor hidden&lt;/span&gt;
  &lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;mkRemote&lt;/span&gt;        &lt;span class="c1"&gt;-- the only way to create a Remote&lt;/span&gt;
  &lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;remoteName&lt;/span&gt;      &lt;span class="c1"&gt;-- read-only access&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kr"&gt;where&lt;/span&gt;

&lt;span class="kr"&gt;data&lt;/span&gt; &lt;span class="kt"&gt;Remote&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;Remote&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;_remoteName&lt;/span&gt; &lt;span class="o"&gt;::&lt;/span&gt; &lt;span class="kt"&gt;String&lt;/span&gt;
  &lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_remoteUrl&lt;/span&gt;  &lt;span class="o"&gt;::&lt;/span&gt; &lt;span class="kt"&gt;String&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;mkRemote&lt;/span&gt; &lt;span class="o"&gt;::&lt;/span&gt; &lt;span class="kt"&gt;String&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="kt"&gt;String&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="kt"&gt;Remote&lt;/span&gt;
&lt;span class="n"&gt;mkRemote&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;Remote&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is a standard pattern in Haskell for maintaining &lt;a href="https://wiki.haskell.org/Smart_constructors" rel="noopener noreferrer"&gt;invariants through the type system&lt;/a&gt;. Code outside this module literally cannot construct an invalid &lt;code&gt;Remote&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  ADTs for Every Domain Concept
&lt;/h3&gt;

&lt;p&gt;An &lt;a href="https://wiki.haskell.org/Algebraic_data_type" rel="noopener noreferrer"&gt;ADT (Algebraic Data Type)&lt;/a&gt; is a type defined by enumerating its possible variants. The compiler forces you to handle every variant — miss one and GHC gives you a warning (or an error, if you enable &lt;code&gt;-Wall&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;Every domain concept in my project is modeled this way:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight haskell"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- What changed between local and remote?&lt;/span&gt;
&lt;span class="kr"&gt;data&lt;/span&gt; &lt;span class="kt"&gt;GitDiff&lt;/span&gt;
  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;Renamed&lt;/span&gt; &lt;span class="kt"&gt;LightFileEntry&lt;/span&gt; &lt;span class="kt"&gt;LightFileEntry&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kt"&gt;Added&lt;/span&gt;   &lt;span class="kt"&gt;LightFileEntry&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kt"&gt;Deleted&lt;/span&gt; &lt;span class="kt"&gt;LightFileEntry&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kt"&gt;Modified&lt;/span&gt; &lt;span class="kt"&gt;LightFileEntry&lt;/span&gt;

&lt;span class="c1"&gt;-- What should rclone do about it?&lt;/span&gt;
&lt;span class="kr"&gt;data&lt;/span&gt; &lt;span class="kt"&gt;RcloneAction&lt;/span&gt;
  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;Move&lt;/span&gt; &lt;span class="kt"&gt;Path&lt;/span&gt; &lt;span class="kt"&gt;Path&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kt"&gt;Copy&lt;/span&gt; &lt;span class="kt"&gt;Path&lt;/span&gt; &lt;span class="kt"&gt;Path&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kt"&gt;Delete&lt;/span&gt; &lt;span class="kt"&gt;Path&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kt"&gt;Swap&lt;/span&gt; &lt;span class="kt"&gt;Path&lt;/span&gt; &lt;span class="kt"&gt;Path&lt;/span&gt; &lt;span class="kt"&gt;Path&lt;/span&gt;

&lt;span class="c1"&gt;-- The planner: pure function, no IO&lt;/span&gt;
&lt;span class="n"&gt;planAction&lt;/span&gt; &lt;span class="o"&gt;::&lt;/span&gt; &lt;span class="kt"&gt;GitDiff&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="kt"&gt;RcloneAction&lt;/span&gt;
&lt;span class="n"&gt;planAction&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Modified&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;      &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;Copy&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;filePath&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;filePath&lt;/span&gt;
&lt;span class="n"&gt;planAction&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Renamed&lt;/span&gt; &lt;span class="n"&gt;old&lt;/span&gt; &lt;span class="n"&gt;new&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;Move&lt;/span&gt; &lt;span class="n"&gt;old&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;filePath&lt;/span&gt; &lt;span class="n"&gt;new&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;filePath&lt;/span&gt;
&lt;span class="n"&gt;planAction&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Added&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;         &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;Copy&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;filePath&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;filePath&lt;/span&gt;
&lt;span class="n"&gt;planAction&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Deleted&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;       &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;Delete&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;filePath&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If I add a new variant to &lt;code&gt;GitDiff&lt;/code&gt; tomorrow, the compiler immediately tells me every function that needs updating. In Python, that's a bug waiting to happen at runtime.&lt;/p&gt;

&lt;h3&gt;
  
  
  A Free Monad Effect System (That Got Removed)
&lt;/h3&gt;

&lt;p&gt;AI built a &lt;a href="https://serokell.io/blog/introduction-to-free-monads" rel="noopener noreferrer"&gt;free monad&lt;/a&gt; effect system — with a pure interpreter that simulates the entire program without touching IO, using a fake filesystem in memory. I used it, and then the AI &lt;em&gt;itself&lt;/em&gt; analyzed the tradeoff and recommended removing it: the complexity wasn't justified since I had no pure tests yet.&lt;/p&gt;

&lt;p&gt;It's documented in my spec under "What We Chose": &lt;code&gt;ReaderT BitEnv IO (no free monad) — rejected as premature.&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;AI didn't just apply FP — it applied it, evaluated the cost, and rolled it back when simpler was better.&lt;/p&gt;

&lt;h3&gt;
  
  
  A ConcurrentIO Newtype Without MonadIO
&lt;/h3&gt;

&lt;p&gt;AI built a &lt;a href="https://wiki.haskell.org/Newtype" rel="noopener noreferrer"&gt;&lt;code&gt;ConcurrentIO&lt;/code&gt; newtype&lt;/a&gt; that deliberately hides its constructor and omits &lt;code&gt;MonadIO&lt;/code&gt;, so nobody can smuggle unsafe lazy IO into concurrent code. The comment in the source says:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight haskell"&gt;&lt;code&gt;&lt;span class="kr"&gt;newtype&lt;/span&gt; &lt;span class="kt"&gt;ConcurrentIO&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;UnsafeConcurrentIO&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;runConcurrentIO&lt;/span&gt; &lt;span class="o"&gt;::&lt;/span&gt; &lt;span class="kt"&gt;IO&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="kr"&gt;deriving&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Functor&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;Applicative&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;Monad&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="c1"&gt;-- NOTE: No MonadIO instance! This is intentional.&lt;/span&gt;
  &lt;span class="c1"&gt;-- Deriving MonadIO would allow 'liftIO' to smuggle arbitrary lazy IO.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I know enough to appreciate this. I don't know enough to have designed it.&lt;/p&gt;

&lt;h3&gt;
  
  
  What We Deliberately Don't Do
&lt;/h3&gt;

&lt;p&gt;And then there's a "What We Deliberately Do NOT Do" section in my spec, where AI listed FP abstractions it &lt;em&gt;considered and rejected&lt;/em&gt;: typed state machines, representable functors, group structures. It reasoned about the right level of abstraction for each problem.&lt;/p&gt;

&lt;p&gt;I want to be clear: I am not familiar with most of these concepts. I have intuition — they feel right, they look elegant. But the AI found them, read the articles and the docs, and applied them to my code! I don't have to be afraid, because the compiler shouts at the errors, the tests find the bugs, and everything &lt;strong&gt;just magically works&lt;/strong&gt;! No, really.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Training Data Paradox Is Temporary
&lt;/h2&gt;

&lt;p&gt;Supposedly AI is worse at Haskell today because there's less training data. From my experience, it matters less than you'd think. My AI needed two abilities: generate a reasonable first attempt, and respond intelligently to compiler errors. The first requires some training data. The second requires reasoning — and that is improving fast.&lt;/p&gt;

&lt;p&gt;My codebase is proof. The AI didn't retrieve memorized Haskell patterns for phantom types or free monads. It &lt;em&gt;reasoned&lt;/em&gt; about type relationships, searched for solutions, and applied concepts from articles it had &lt;strong&gt;never&lt;/strong&gt; seen during training. As models get better at reasoning, the importance of training data volume shrinks. And the languages with the strictest compilers will have the biggest advantage, because they provide the richest feedback signal.&lt;/p&gt;

&lt;p&gt;The conventional wisdom says: use the language AI knows best. I think the better advice is: use the language whose compiler teaches AI the most.&lt;/p&gt;

&lt;h2&gt;
  
  
  It Works
&lt;/h2&gt;

&lt;p&gt;I should mention: &lt;code&gt;bit&lt;/code&gt; isn't a toy project. I use it daily. I'm its only user so far (just pushed it to &lt;a href="https://github.com/david-hoze/bit" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; a week ago), but I'm also its active developer — building features and using them as I go, and... it works. It's a pleasure to write this way. I just tell Claude what I want, and we think together about how to do it. The test suite is comprehensive because AI wrote tests for every feature, so I know everything's good. The architecture is clean because AI audited its own mistakes and proposed structural fixes. And the code uses FP concepts I barely understand.&lt;/p&gt;

&lt;p&gt;Mind this: I built this in a language I barely know, using concepts I can't fully explain, with an AI that learned those concepts on the fly. And the result is more robust than most codebases I've seen written by teams of experienced developers.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Real Question
&lt;/h2&gt;

&lt;p&gt;For decades we chose languages based on how easy they were for humans to write. Python won that race. But the question is shifting. It's no longer "what's easy to write" but "what's easy to write &lt;em&gt;correctly&lt;/em&gt;, when AI is doing most of the writing?"&lt;/p&gt;

&lt;p&gt;The next time you start a project with AI, consider reaching for the language that makes AI &lt;em&gt;accountable&lt;/em&gt;, not just productive. You might be surprised how far you get — even in a language you barely know.&lt;/p&gt;

&lt;p&gt;I was.&lt;/p&gt;

</description>
      <category>haskell</category>
      <category>cursor</category>
      <category>ai</category>
    </item>
  </channel>
</rss>
