Machine learning using Clojure, libpython-clj2, and Pytorch
Machine learning explained using the parabola example
Machine learning explained using the parabola example
Most image codecs you know about such as JPEG, JPEG 2000, JPEG XS, WebP are like layer cakes. You have transform sitting on top, entropy coding at the bottom, and rate control floats somewhere in the middle. And then there's a metadata layer wrapping it all up. The interesting bits are hidden under tons of framing code, profile parsers, and standards plumbing. If you just want to see how wavelet coefficients become bits, you have to dig deep into the guts of the codec.
I wrote libwce as a bare-bones implementation, consisting of only a single lib.rs file, weighing in at 500 lines. It just implements a patent-clean Bit-Plane Count (BPC)-style entropy layer in the spirit of JPEG XS, and nothing else. There is no boilerplate or dependencies with the library relying solely on stdlib.
A raw video stream is basically a grid of pixels, most of which share very similar color and brightness values with their immediate neighbors. Storing every pixel individually wastes a ton of bandwidth since there is a lot of repeated data in the stream. Codecs are used to compress this information by transforming the image from the spatial domain into the frequency domain. Rather than tracking individual pixels, a codec uses mathematical frequencies to describe color changes across the image. Older formats like standard JPEG end up chopping the image into squares and applying a discrete cosine transform, leading to the blocky artifacts we all know and love.
A wavelet is a newer approach that solves the problem by applying the transform process to the whole image at once, splitting the signal into low-frequency structural data and high-frequency detail data across multiple scales. After the wavelet transform, you end up with a 2D array of signed integer coefficients, most of which are near zero, with a long Laplacian tail. The purpose of the entropy layer is to compress this array down to a small number of significant bits.
BPC coding is done using groups of four coefficients at a time. For each group, you have to determine the smallest bpc such that every coefficient can be held. This is the bit-plane count representing the index above which all coefficient bits in the group are zero. In libwce, all the bpc values are written first into a single bitstream, then for each group the four coefficients are emitted coeff-major. These are the magnitude bits of each coefficient followed immediately by a single sign bit when that coefficient is nonzero. That takes care of all the data processing you need to do. Then, you get to the actual compression when you go to encode these bpc values. Neighboring groups tend to have similar sizes, so instead of writing each bpc as a raw 6-bit number, you can estimate it from its neighbors and, instead, write a small residual which tends to be tiny.
Here, libwce uses RUNNING (DPCM delta vs the previous group's bpc, zigzag-mapped and Rice-coded) and ZERO (unsigned residual against lossy_bits) predictors which can be optionally combined with a 1-bit-per-8-group sparse-block flag that short-circuits all-deadzone blocks. That leaves you with four predictor × flag combinations, and the encoder sweeps Rice-k across seven values inside each, picking the best per band via a single-pass cost search. All combinations give the same decoded result, but they produce different types of bitstreams. Each one works best for different pixel distribution such as textured regions, flat parts, or sub-bands which are mostly zeros.
Here's a complete decoder for one sub-band:
let mut coeffs = vec![0i32; N];
let lossy_bits = decode(buf, &mut coeffs).unwrap();
dequantize_optimal(&mut coeffs, lossy_bits, scale_b);
The library itself is stateless, and only works with whatever buffers you provide. It doesn't use I/O or hidden globals, and works purely through caller-owned buffers (a small BPC scratch buffer is allocated internally).
The repo has 3 demos. The most fun one is image_compress, which is a full codec built on top of libwce. It uses Haar wavelet in, libwce in the middle, and inverse Haar on the way out which run across four quality presets.
preset lossy_bits payload .wce file ratio PSNR
LL HL LH HH bytes bytes
near-lossless 2 4 4 5 146537 146597 1.52x 49.06 dB
balanced 4 6 6 7 92631 92691 2.40x 37.54 dB
aggressive 6 8 8 9 49516 49576 4.48x 28.79 dB
very lossy 8 10 10 11 21923 21983 10.11x 21.62 dB
The whole process consisting of DWT, sub-band coding, quantization, and writing to a container takes under 500 lines of code. If you open the four reconstituted PGMs side by side and you'll see quality degrade as compression increases. At q1, the image will be indistinguishable from the original; q2 has minor smoothing in flat areas; q3 starts to show noticeable wavelet ringing around edges; and q4 is blocky in a recognizable wavelet way, looking eldritch but still legible.
The second demo, mode_shootout, runs a synthetic Laplacian sub-band through every predictor × flag combination and displays the winner.
mode total ratio ok
-------------- ----- ------ --
RUN, flag=off 658 12.45x Y
RUN, flag=on 666 12.30x Y
ZERO, flag=off 652 12.56x Y
ZERO, flag=on 660 12.41x Y
auto-pick 612 13.39x Y
best forced: ZERO, flag=off (652 bytes)
auto-pick beat best forced by 40 bytes (better rice_k).
This is precisely the kind of thing that's a pain to do within the confines of a full codec, where you’d have to fiddle with instrumenting internals, disable rate control, and then mock the framing layer. With libwce, mode comparison is just how the API works. You use the same sub-band through encode_with_options with each predictor × flag combination, then count the bytes and pick the winner, which is exactly what encode itself does internally.
The third demo, stream_surgery, does 256 random bitflips and 256 random byte scrambles across the encoded bitstream, 300 truncation points covering every 4-byte prefix, and a set of adversarial cases including all-ones “unary bombs” along with crafted bad headers.
bit-flip (anywhere) : 256/256 returned, avg 36 / max 1024 coeffs differ (of 1024)
random byte (anywhere) : 256/256 returned without crash
truncation (every prefix) : 300/300 prefix lengths returned
adversarial (bombs + bad hdrs): 7 cases returned cleanly
The demo shows how every case gets successfully decoded without any hangups or a crash.
Finally, it's worth reiterating that I intentionally didn't write libwce to be a full codec implementation, which would necessitate adding a container format, rate control, and other plumbing. It's designed to illustrate how the most conceptually interesting layer of a mezzanine codec works and to make it easier to study and modify without the weight of the full codec around it. What you get is just the entropy layer that you can wire into your own pipeline.
The repo is at https://github.com/yogthos/libwce. Clone it and play with it. It's written with readability in mind.
A look at the new clojure.java.process namespace and debugging an AWS Batch jobs slowdown.
Notes
curl -I http://localhost:4125/files/somethingcurl -X GET http://localhost:4125/ops/files | jq

python\nclass SmartHomeAgent:\n def __init__(self, name):\n self.name = name\n self.log = []\n\n def act_on_temperature(self, current_temp, desired_temp):\n if current_temp > desired_temp + 2:\n action = "Turning on AC"\n self.log_action(action, f"Current: {current_temp}°C, Desired: {desired_temp}°C")\n # ... actual AC control code\n elif current_temp < desired_temp - 2:\n action = "Turning on Heater"\n self.log_action(action, f"Current: {current_temp}°C, Desired: {desired_temp}°C")\n # ... actual Heater control code\n else:\n action = "No action needed"\n self.log_action(action, f"Current: {current_temp}°C, Desired: {desired_temp}°C")\n return action\n\n def log_action(self, action, details):\n self.log.append(f"[{self.name}] {datetime.now()}: {action} - {details}")\n\n# Usage\nagent = SmartHomeAgent("ClimateControl")\nagent.act_on_temperature(25, 22)\nprint(agent.log)\n
\n\nThis basic logging provides a human-readable trail, fostering trust by showing, not just doing.\n\n## User Autonomy, Not "Hard-to-Cancel": Learning from Shutterstock\n\nThe $35 million settlement Shutterstock faced over difficult subscription cancellations is a potent lesson: users demand control over automated systems. For AI agents, this translates directly to how we design interaction and management. Your agent shouldn't feel like a digital trap.\n\nKey design principles for user autonomy:\n\n* Explicit Opt-in/Opt-out: Clear consent for agent actions and data usage.\n* Easy Pause and Stop: Users must be able to halt or reconfigure an agent's operation immediately.\n* Understandable Configuration: Agent settings should be accessible and intuitive, not buried in obscure files.\n\nThink about how your agent's lifecycle is managed. Here's a conceptual AgentController:\n\n
python\n# pseudo-code for an AgentController\nclass AgentController:\n def __init__(self, agent):\n self.agent = agent\n self._running = False\n\n def start(self):\n if not self._running:\n print(f"Starting {self.agent.name}...")\n self._running = True\n # thread or process start logic for agent.run()\n self.agent.start_service()\n\n def pause(self):\n if self._running:\n print(f"Pausing {self.agent.name}...")\n self._running = False\n self.agent.pause_service()\n\n def stop(self):\n if self._running:\n print(f"Stopping {self.agent.name} permanently...")\n self._running = False\n self.agent.stop_service()\n # Clean up resources\n\n def configure(self, new_settings):\n print(f"Configuring {self.agent.name} with new settings.")\n self.agent.update_settings(new_settings)\n\n# When you're building your agents, consider how these controls are exposed to the user.\n
\n\nFor more effective agent management, especially concerning permissions and operational boundaries on local hardware, check out AgentGuard. It helps you build in these essential controls from the ground up.\n\n## Privacy by Design, Not by Accident: The FBI's Data Ambition\n\nThe FBI's desire for nationwide license plate reader access is a stark reminder of the sheer scale of data collection possible today. For local AI agents, privacy should be a default setting, not an afterthought.\n\nWhen designing your agents, prioritize:\n\n* Local-First Processing: Perform computations and store data on the user's device whenever possible.\n* Data Minimization: Only collect and process the data absolutely necessary for the agent's function.\n* Transparent Data Policies: Clearly communicate what data an agent uses, why, and whether it ever leaves the device.\n\nBuilding agents for consumer hardware gives us a distinct advantage here. We can champion local intelligence and ensure that user data stays private by default, not by policy fine print.\n\n## Architecting for Clarity: The Lisp Connection\n\nThe Lisp family of languages (Common Lisp, Racket, Clojure) are hyperpolyglots for a reason: their power in symbolic computation and metaprogramming encourages clarity in expressing complex logic. While you might not be writing your agent in Emacs Lisp, the principles of clear, inspectable, and modular design are paramount.\n\nAn agent with well-defined modules for perception, decision-making, and action is easier to debug, understand, and, crucially, to trust. Avoid monolithic codebases where an agent's reasoning is opaque.\n\n
python\n# Conceptual Agent Architecture\nclass AgentBrain:\n def __init__(self, perception_module, decision_module, action_module):\n self.perception = perception_module\n self.decision = decision_module\n self.action = action_module\n\n def run_cycle(self, environment_data):\n perceived_state = self.perception.process(environment_data)\n desired_action = self.decision.evaluate(perceived_state)\n self.action.execute(desired_action)\n return desired_action # For logging/traceability\n\n# Each module can have its own transparent logic, making the overall agent's behavior understandable.\n
\n\n## The Intentional Click: Feedback Loops and Refinement\n\nEven a seemingly simple site like clickclickclick.click can serve as a quirky reminder of direct user interaction. How do your agents confirm intent? How do they solicit feedback effectively? It's not about mindlessly automating every single interaction, but about designing clear, intentional communication channels between the user and the agent.\n\nConsider points where your agent might ask, "Did I do that correctly?" or "Is this what you intended?" rather than just assuming. This explicit feedback loop refines the agent's understanding and reinforces the user's sense of control.\n\n## Building for a Trustworthy AI Future\n\nThe AI revolution is here, and it's happening everywhere, from the largest data centers to the devices in our pockets. As developers crafting AI agents for consumer hardware, we stand at a critical juncture. We have the unique opportunity-and responsibility-to build agents that are not just intelligent and efficient, but also trustworthy, accountable, and respectful of user autonomy and privacy.\n\nThis summer's "AI gold rush" shouldn't just be about speed; it should be about quality, ethics, and user-centric design. By focusing on transparency, control, and privacy by default, we can ensure our AI agents truly empower, rather than overwhelm, the people who use them.\n\nTo help manage these critical aspects of your agent's lifecycle, from permissions to operational safety, explore AgentGuard. It's designed to support you in building the next generation of conscientious AI automation. Start building agents that earn trust, today.\n
Note: I wrote this in 2018, while I was helping a young startup and convincing them to open source their core application. If I were to write it nowadays, in 2024, I’d elaborate a bit more, including James Governor’s evergreen remark about how “You can make money with open source, but it’s extremely hard to make money from open source.” (source). Leaving it as-is, however.*
I’ve been advising a startup on the data transformation space. As part of this, we re-wrote the core engine in Clojure. The new version is, at the worst case, 16 times as fast in the same hardware, and in some cases over 200 times faster. And it does it in a fraction of the lines of code.
We did this in under 3 months of part-time work. We couldn’t focus our entire attention on it, as we had other concerns as well - I was involved with general team and management tasks, and the second developer was helping on other internal projects as well. To further raise the bar: we had to keep it functionality-compatible with the current version, so I had to get acquainted with the existing feature set, and it was the other developer’s first Clojure project.
Clojure made our lives so much easier. But this is not a post about why Clojure is cool.
I’ve been arguing about why they should open source anything that is not enterprise-specific, including this layer. There’s many advantages, which I won’t go over right now, but there’s also a looming threat.
Layers are getting commoditized faster and faster. More and more, there is demand for people who are good at wiring things together (beyond the gem install hairball approach), or tools that help with that wiring.
I suspect that’s a big part of what’s driving how many companies like Seldon are going open source-first, or how Unreal opened their code as a way to compete with Unity.
“what’s driving how many… are going” is a doozy
Remember: This rewrite took about 3 man-months, with our attention pulled in multiple directions, while we strove to remain feature-compatible with the old engine. You have to assume anyone else who has the technical chops but doesn’t have that baggage can do it as well. Do you want to be disrupted by some motivated, random person who thought what you were doing was cool, but neither needed your entire feature set nor wanted to be shackled to your cloud version?
Better to commoditize yourself before someone else does it to you. You get to have a say in how it happens, use it as fuel to propel you somewhere new. And you get to tell your customers “if you think this thing we give away is cool, you should see the part we charge for”.
OK, this is going to be a tough one. Strap down, even if it pisses you off at first - I wouldn’t be writing it if I didn’t think it was helpful.
A programmer has something he wants to build.
He finds a framework.
It looks easy enough to get into. There’s a few tutorials available, which show step by step how to build a basic application of just the right type.
He’s happy. He feels really productive. CRUD just “drops out” of the framework’s design. It’s like it was created with his problem in mind.
Then he’s done with the tutorials. He starts plugging in his own, specific requirements. Every so often he hits a snag, but that’s OK, he can just twist the framework’s arm a bit, right?
It all goes well for a while. Then he hits a big one. The framework doesn’t do quite work the way he wants it to.
He posts a question. Maybe he doesn’t get an answer. Maybe he does, and he’s told it’s not within the benevolent dictators’ design parameters.
Crap. Oh well. He can start working around the decisions that don’t match his application. This will add some development overhead. At some point it’ll be enough that he has to consider his options.
That’s OK. He has the source. He can fork it, and do his custom stuff - he knows it well enough by now. He’ll lose the upstream fixes, unless he puts in some real effort into integrating them, but at least he doesn’t have to ditch the codebase.
This will tide his team over until the re-write, which they’ll build upon a different framework. One that looks like it better suits their needs.
That’s just the way things are, right?
If you take someone’s code, they have made some decisions for you. Sometimes the decisions work in your favor, sometimes they don’t.
There’s an alluring side to this. The main reason why a framework might feel solid at first is because someone made a lot of decisions for you - one might say such a framework is opinionated. Those decisions seem to make sense, and they get you from zero to CRUD in just a few steps.
If you happen to be building the exact type of application that the framework creators have in mind, if their way of thinking immediately clicks with you, that’ll take you a long way.
But when you try to move away just bit from the set of problems they decided to address, you realize that those choices accrete. On a solidly integrated framework, you don’t get to pick and choose. Pulling away even a single one requires considerable effort.
Most of the time, either you take it or you leave it.
Frameworks expect to have an answer to most of your fundamental questions. Can you really rely on them to do that?
Consider an alternate path.
At first glance, Clojure’s Luminus is not as solidly integrated as other alternatives. For every potential component, you get to pick and choose - and never mind PostgreSQL vs. MongoDB vs. Datomic.
Even once you’ve made a decision, chances are you won’t find any step-by-step zero-to-CRUD tutorials for your specific combination.
So… how does this help again?
No, Luminus is not making any decisions for you. The very few areas where it provides a single choice, like Selmer for templating, are orthogonal to your application’s design.
Unlike the frameworks in fashion on other languages - or even some that are starting to trend for Clojure - it’s not opinionated. One could say it’s open minded.
Here’s what happens when you create a new Luminus application.
While it’s happy to show you its conventions with the templates that it generates, you won’t find it making any choices for you in advance. But more importantly, at no point does it try to wrestle control of those decisions from you.
It lets you make up your own mind.
Each library Luminus includes does make some decisions internally, for obvious reasons. But these are mechanical in nature, and given that Luminus doesn’t want its basic components to be strongly fused together, you are free to swap out any particular part.
Think of it as the bevy of functions you have on Clojure. Every one of them provides a piece of independent, re-usable functionality. Yes, the internal decisions of how the functions were implemented are made for you in advance, but it’s up to you to decide how to put them together to build your application, or if even you’d rather use an external library.
Yes, this means there will be a slightly steeper learning curve. You’ll need to know what these choices mean, before you can make an informed decision.
That means that on day-1, you’ll feel less productive than how you would on an opinionated framework, because you don’t get to coast on decisions someone else has made for you.
If you are looking for day-1 productivity, it may not be for you.
I’m not. I’m looking at life time value.
After using this loosely-coupled approach on projects for a couple of years, I’ve realized two things.
I get to replace any single part of the system, if I find it doesn’t suit my needs.
I may not need to. Hopefully I made the right decision up-front. But if for whatever reason I didn’t, I know that no other choice is tightly integrated with it, so I can gradually pull it out and replace it - I don’t need to switch frameworks, or fork it, or live with it.
I get to change my mind.
Up until a few months ago, YeSQL was the standard way to access SQL databases in Luminus. When YeSQL support slowed down, Dmitri Sotnikov decided to replace it with HugSQL, which follows the same approach but takes it further and is actively developed.
Want to guess how many existing Luminus projects it affected?
Zero.
How can I state that with certainty?
Because it couldn’t possibly have affected a single one. Your choices are only for which libraries is your initial template generated from. After that, you are free to decide which of these do you want to update at any point, depending on if it suits you or not.
Any changes to future Luminus choices will have zero impact on you.
This means initially you’ll be taking baby steps. You may feel like an idiot as you go about figuring things out.
We all hate that. I get it.
It’s part of what makes frameworks so appealing when facing a new platform: they promise you’ll hit the ground running. Nobody likes to crawl when they’re used to just zoom by basic tasks.
But when changing contexts, we have to take baby steps first and then go on from there, because every decision we make is a trade-off.
When choosing a loosely-coupled approach, you’re saying that day-1 productivity is less important than long-term maintenance and total application lifetime productivity.
Personally, I’d rather spend time up-front understanding the choices I’m making. If I don’t have fundamental decisions made for me in advance, I have to look at what the options are, and can decide to change some pieces.
This understanding increases transparency, which in turn enhances my comprehension of the very foundations that I’ll be building upon for months or years.
Given I spend more time on application support and extension than on the first few days, it’s a trade-off I’m happy to make.

Hello Clojurians,
I’m happy to announce that the Clojure book has gotten a bit better! A few days ago, I received an email from someone named Alex Bedner. He read my Clojure book from start to finish and was kind enough to share some corrections. I’ve implemented them, and you can find the updated book here: https://clojure-book.gitlab.io/
I did try to upload the print copy to Amazon, but for some reason they require additional verification from my side — and I’m not entirely sure what that entails. I’m not great with bureaucracy, so getting the Amazon listing sorted will take some time. In the meantime, Alex Bedner’s proofread version of the Clojure book is available at the link above. I’m deeply grateful to him.
There’s still a lot to do, and I remain committed to Clojure. Here are the things I’d like to finish in the book:
Creating Your Own Libraries (https://clojure-book.gitlab.io/book.html#_creating_your_own_libraries) — this section currently has no accompanying video. I’ve been meaning to make one, but haven’t gotten around to it yet — I can’t quite explain why.
Debugging with Calva (https://clojure-book.gitlab.io/book.html#_debugging_with_calva) — I need to write this section, with plenty of screenshots so readers know how to debug with Calva. At the very least, I’d like to link to the Calva website, where the creator has put together some excellent videos.
I’d also like to add a section on Paredit at some point — we’ll see how that goes.
Finally, I’d love to use LLMs to help proofread and polish the book further. English isn’t my first language, and I’ve found that AI tools have been genuinely helpful in improving the quality of my writing lately.
Once again, thank you to everyone who has said kind words about the book, pointed out corrections, offered criticism, or praised it. All of it is valuable input, and I’ll make the most of every bit of it.
Welcome to the Clojure Deref! This is a weekly link/news roundup for the Clojure ecosystem (feed: RSS).
The Clojure development team is pleased to announce a Clojure Dev Call on May 26 @ 17:00 UTC!
Join the Clojure dev team for an update on what we’ve been working on and what’s on our horizon. We’ll save time for a Q&A, so bring your questions.
Join us for the largest gathering of Clojure developers in the world! Meet new people and reconnect with old friends. Enjoy two full days of talks, a day of workshops, social events, and more.
September 30 – October 2, 2026
Charlotte Convention Center, Charlotte, NC
The CFP is open until June 14
Early Bird tickets are on sale now.
After a decade away, EuroClojure returns to mark twenty years of Clojure. Join us in Prague for three days of workshops, talks, and thoughtful conversation.
May 19-21, 2027
2027.euroclojure.org
Join the mailing list for early bird tickets and announcements. Share your ideas and content suggestions when you sign up.
Clojure real-world-data 59: May 22
Clojure/Conj 2026: Sep 30-Oct 2. CFP is open until June 14. Early Bird tickets.
Adam Tornhill - Transforming Software Evolution: Lessons from 10 Years of Clojure in Production - Øredev Conference
ClojureScript 1.12.145 has await! - mccormix
Bowling game coding challenge in Clojure - Andrey Fadeev
Learn Ring - 13. CSS - Clojure Diary
Jigsaw: Music Theory Semantic Graph Demo (Clojure) - Connor Finley
DeepSeek V4 Flash is superhuman with BigConfig! - Alberto Miorin
Babashka: Reverting VM Snapshots | Clojure - Ken Huang
Glint game engine #9 - Let there be light - (Off By One Error
Apropos with Bozhidar Batsov - apropos clojure
Babashka Conf
Scripting with SCI on your iPhone - Adrian Smith - Babashka-tv
Friendly Command Line Tools and Dev Automation - Arne Brasseur - Babashka-tv
Write charming TUIs in Babashka - Timo Kramer - Babashka-tv
Swish by Rod Schmidt
Mike Møller Nielsen
Leiningen for windows - build and run Clojure programs - Mike Møller Nielsen
Clojure up and running in IntelliJ - Mike Møller Nielsen
Clojure when and how to use a Vector - Mike Møller Nielsen
Clojure when and how to use a list - Mike Møller Nielsen
Clojure when and how to use Maps - Mike Møller Nielsen
Annually-Funded Developers' Update: March & April 2026 - Kathy Davis
jank now has its own custom IR - Jeaye Wilkerson
ifgame - An Interactive Fiction game library for Clojure - Rod Schmidt
Playing with SVGs with hiccup and clay – Clojure Civitas - Lorelai Lyons
The Mayor Method — how I work with AI - Mike Thompson
Agent-Ready Stack - Laurence Chen
Port: a minimalist prepl client for Emacs - Bozhidar Batsov
Dotolist — creating a one-click team - Tomáš Baránek
How we 40x’d the performance of a class of temporal queries · XTDB - James Henderson
Linux JVM MIDI Primer - Arne Brasseur
Making tests silent on success - Mike Thompson
Expert Clojure Workflows for AI Agents: Four Skills from Production Experience - Laurence Chen
Introduction to Supervised Machine Learning with metamorph.ml - Carsten Behring
Introduction to Unsupervised Machine Learning with metamorph.ml - Carsten Behring
8 years of Clojure - Josh Kingsley
Branches as Values, Merges as Queries - Christian Weilbach
Clofer: Clojure, Forged in Iron - Karthikeyan A K
Alex Bedner proof read my Clojure Book - Karthikeyan A K
List of Clojure-like projects - now with trending, most active and newest blocks - Igor Levdansky
Debut release
let-go - Almost Clojure written in Go.
mino - A tiny, embeddable, REPL-friendly Lisp implemented in pure ANSI C.
clj-p4 - Read-only Perforce-to-Git bridge in Clojure, with stream and classic-depot support.
gloat-demo-fiber - A JSON REST API built with Fiber and Gloat, demonstrating Go/Clojure interoperability for web applications.
zulipdata - A Clojure library to fetch and analyse data from the Clojurians Zulip chat
rechentafel - A spreadsheet evaluator for Clojure/Script.
re-frame2 - The AI first-born of re-frame
ifgame - An interactive fiction game written in Clojure
drip - Drip is a transactional job queue for MariaDB, PostgreSQL, and SQLite
port - A minimalist Clojure interactive programming environment for Emacs, built on prepl
generate - code generation for Clojure projects
bara-lang - Clojure for nim language
gsheetplus - Low-level and high-level wrapper to work with Google Sheets. Reading, writing and sheet management.
winze - An agentic AI memory system (mcp server) with semantic search and GUI search/edit tool for Markdown knowledge bases
ghosttyfx - JavaFX terminal that uses libghostty
cljfx/ghosttyfx - Cljfx wrapper of GhosttyFX
beagle - a typed Lisp authoring surface for agent-written dynamic code.
ducktape - Connect tech.v3.dataset to DuckDB
phel-log - Data-driven logging library for Phel. Levels, namespace filtering, pluggable appenders, PSR-3 adapter, Monolog handler bridge. Inspired by Timbre + Monolog.
phel-symfony-demo - Symfony + Phel demo: Ring-style handlers over plain maps, no ORM
stube - Web framework inspired by Seaside and UnCommonWeb
clj-native-agent - Structural, semantic, and surgical skills for Clojure AI agents. Mimics expert practitioner workflows for code discovery, debugging, and editing using Babashka and rewrite-clj.
llmisp - JSON AST > Clojure
spindel - Cross-platform FRP runtime with a git-like memory model.
rephrase - Rephrase exceptions to be more beginner-friendly
Updates
clojure_cli 1.12.5.1645 - Clojure CLI
clojurescript 1.12.145 - Clojure to JS compiler
core.unify 0.7.2 - Unification library
clj-watson 6.1.0 - A Clojure tool that checks for vulnerable dependencies
aleph 0.9.8 - Asynchronous streaming communication for Clojure - web server, web client, and raw TCP/UDP
awesome-backseat-driver 1.0.13 - Plugin marketplace for Clojure AI context in GitHub Copilot: agents, skills, and workflows for REPL-first interactive programming with Calva Backseat Driver
logging4j2 1.0.8 - A Clojure wrapper for log4j2
core.async.flow-monitor 0.1.5 - A real-time monitoring and interaction tool for clojure.core.async.flow
martian-aleph 0.1.4 - Martian plugin to use the Aleph http client
thneed 1.1.9 - An eclectic set of Clojure utilities that I’ve found useful enough to keep around.
statecharts 1.4.0-RC13 - A Statechart library for CLJ(S)
joyride 0.0.75 - Making VS Code Hackable like Emacs since 2022
stratum 0.2.67 - Versioned, fast and scalable columnar database.
deps-new 0.12.1 - Create new projects for the Clojure CLI / deps.edn
bareforge 0.6.0 - Companion visual builder for BareDOM web components. Drag components, declare reactive state, export fully interactive CLJS or JS project
pg-datahike 0.1.43 - Postgres compatibility layer for Datahike.
de-dupe 0.3.0 - A ClojureScript library which "de-duplicates" Persistent Data Structures so they can be more efficiently serialised.
ClojureStorm 1.12.5 - A fork of the official Clojure compiler with extra code to make it a dev compiler
graal-build-time 1.0.6 - Initialize Clojure classes at build time with GraalVM native-image
metamorph.ml 1.6.2 - Machine learning functions based on metamorph and machine learning pipelines
oksa 1.2.1 - Generate GraphQL queries using Clojure data structures.
pomegranate 1.3.27 - A sane Clojure API for Maven Artifact Resolver + dynamic runtime modification of the classpath
calva 2.0.588 - Clojure & ClojureScript Interactive Programming for VS Code
cli-tools 0.16.0-beta-7 - CLIs and subcommands for Clojure or Babashka
fulcro-spec 3.2.9 - A library that wraps clojure.test for a better BDD testing experience.
baredom 3.1.0 - BareDOM: Lightweight CLJS UI components built on web standards (Custom Elements, Shadow DOM, ES modules). No framework, just the DOM
o11ylite 2026.5.16-1112955 - Free, open-source OpenTelemetry backend powered by DuckDB 🦆 and SQLite
calva-backseat-driver 0.0.34 - VS Code AI Agent Interactive Programming. Tools for CoPIlot and other assistants. Can also be used as an MCP server.
plumcp 0.2.1 - Clojure/ClojureScript library for making MCP server and client
teensyp 0.5.0 - A small, zero-dependency Clojure TCP server that uses Java NIO
build-uber-log4j2-handler 2.26.0 - A conflict handler for log4j2 plugins cache files for the tools.build uber task.
phel-lang 0.39.0 - A functional, Lisp-inspired language that compiles to PHP. Inspired by Clojure, Phel brings macros, persistent data structures, and expressive functional idioms to the PHP ecosystem.
dexter 0.1-beta-2 - Dexter - Graphical Dependency Explorer
Hello Fellow Clojurists!
This is the second of six reports from the developers who are receiving annual funding for 2026. Thanks to everyone for supporting their work and important contributions to the Clojure community.
Bozhidar Batsov: nREPL, Clojure Mode, ts-mode, Orchard, CIDER, and more
Clojure Camp: Badges, nano-conj, exercises
Eric Dallo: eca, eca-desktop, clojure-lsp, clojure-lsp-intellij
Jeaye Wilkerson: Jank compiler architecture and optimization
Michiel Borkent: babashka, ClojureScript async/await, Rebel, squint, and much more
2026 Annual Funding Report 2. Published May 1, 2026.
The last two months have been some of the busiest I’ve had in clojure-emacs
land in a long while. No big “X.Y” CIDER release to point at yet, but there’s
been a steady stream of important work across most of the sibling projects.
Below are some of the highlights.
Two clojure-mode releases back-to-back, after a long stretch of relative
quiet:
clojure-sort-ns no longer mangles :gen-class, clojure-thread-last-all doesn’t eat closing parens into line comments, clojure-add-arity preserves arglist metadata, letfn bindings get a function face, and edn-mode indents data like data. On the feature side: a new clojure-preferred-build-tool for projects that have several build tool files lying around, a dedicated clojure-discard-face for #_ forms, and project root detection for ClojureCLR’s deps-clr.edn.((:block N)), ((:inner D)), …) that clojure-ts-mode and cljfmt already use. The legacy format (integers, :defn, positional lists) still works, but it’s slated for removal in clojure-mode 6. There’s a new public clojure-get-indent-spec API, and CI moved off CircleCI onto GitHub Actions.Unifying the indent format across our Clojure tooling has been on my todo list for years - happy to see it finally happen!
No release in this window, but I poured a lot of effort into the test suite - proper indentation and font-lock test coverage, a new configuration option test suite, and integration tests against sample files. Boring infrastructure work, but it’s the kind of thing that pays compound interest as the mode keeps maturing.
Side note: In the past several months I’ve spent a bit of time hacking on neocaml and my work there provided quite a bit of inspiration for improvements to clojure-ts-mode. (and, of course - clojure-ts-mode inspired me to create neocaml in the first place)
Orchard v0.41.0
shipped on April 13 with a rewrite of orchard.xref, a refactor of
orchard.indent, and a test modernization pass. I chipped in some smaller
fixes - a cycle guard in fn-transitive-deps, validating input to
stacktrace/analyze, compiling a hot regex once instead of on every call, and
standardizing the -test suffix across the test suite.
We’ve also migrated from Lein to tools.deps for Orchard and probably we’ll continue in this direction for the rest of our Clojure projects in the nREPL and Clojure Emacs GitHub orgs. No rush on that front, though.
cider-nrepl v0.59.0
landed on April 14. The headline change from my side is that the ops are now
properly namespaced (#710) -
something that’s been sitting in the issue tracker for a very long time. CIDER
on master already speaks the new namespaced ops with a fallback to the legacy
names for older cider-nrepl versions, so upgrades should be painless.
Other notable bits: Compliment and clj-reload are no longer shaded, the test
middleware properly binds *report-counters*, and we extract inline maps from
composite Lein profiles correctly.
refactor-nrepl got a serious modernization push in April - no release yet,
but a lot of cleanup landed:
http-kit in favor of the JDK’s built-in HTTP client (one fewer dependency, one less thing to break).hotload-dependency on top of tools.deps.def-op macro to simplify how ops are defined and how errors are handled.make lint target, and modernized CircleCI executors.Expect a refactor-nrepl release once the dust settles.
Just a small cleanup pass
to align with the new Emacs 28+ baseline. clj-refactor is in maintenance mode
these days, but I’m trying to keep it healthy.
Eventually I’ll be taking a closer look at moving some of its functionality to clojure-mode and CIDER, but there’s more ground-work that needs to be done first.
No CIDER release this cycle, but master accumulated a lot of structural
work that will land in CIDER 1.22:
nrepl-client.el decoupled from CIDER. It no longer depends on sesman or any of CIDER’s UI layer, which makes the nREPL client genuinely reusable outside CIDER. I also split cider-connection.el into cider-session.el + cider-connection.el and decoupled cider-eval.el from cider-repl.el.cider-jack-in got a serious overhaul: a proper jack-in tool registry, unified entry points around shared helpers, TRAMP fixes, sane runtime defaults, and cider--resolve-command now actually works over TRAMP.nrepl-completed-requests so it can’t grow without bound.cider-nrepl 0.59 - with a fallback to the legacy op names so older cider-nrepl keeps working.nrepl-make-eval-handler with a keyword-arg API (and removal of the trivial wrappers that used to live around it).This is shaping up to be one of those releases that doesn’t look flashy on the outside but cleans up a ton of long-standing internal mess. The decoupling work in particular has been on my mind for years.
I’ve also started working on experimental support for prepl in CIDER. I’m not sure if this will ever land or be properly supported, but it’s been a fun side quest to me to ensure that CIDER logic is decoupled properly from its bundled nREPL client that I plan to eventually spin out of CIDER.
Slightly outside the clojure-emacs org, but related: I’ve been helping
MrAnderson get back on its
feet. Three PRs landed in March/April:
clj-kondo.exports from jar extractionMrAnderson is what cider-nrepl (and others) use to shade dependencies, so a
healthy MrAnderson is in everyone’s interest. There’s more to come here!
CIDER 1.22 once the jack-in and decoupling work settles, a refactor-nrepl
release to ship all the April cleanup, and hopefully a fresh MrAnderson cut
soon after.
As always - thanks to everyone who pitched in, especially Sashko Yakushev who
once again did much of the the heavy lifting on cider-nrepl and Orchard.
And a HUGE THANKS to the members of Clojurists Together for supporting my Clojure OSS work! You rock!
2026 Annual Funding Report 2. Published May 12, 2026.
2026 Annual Funding Report 2. Published May 8, 2026.
Excited 2 months of lots of work and help from Clojurians! We had improvements in eca and clojure-lsp mainly, and new projects as well.
ECA keeps growing a lot, receiving lots of contributions, with more than 800 stars already I’m planning a stable release soon, in these 2 months we had lots of releases with ton of stuff, so I will focus on the main highlights:
/plugins, /plugin-install and /plugin-uninstall.shell_command approval.background parameter on shell_command and a dedicated bg_job tool to manage long-running processes like dev servers and watchers./model: New chat/list, chat/open and /fork commands let clients render a chat sidebar and clone existing conversations, plus a new /model command to switch model mid-chat.ask_user tool: LLMs can now ask the user questions with optional selectable options, fully integrated with hooks and trust modes.image_generation tool via the Responses API, including image edits across turns and MCP tools that return image content.chat/promptSteer notification to inject user messages into a running prompt at the next LLM turn boundary, without stopping it.mcp/addServer, mcp/removeServer, mcp/updateServer, mcp/enableServer and mcp/disableServer requests to manage MCP servers at runtime, plus much better OAuth spec compliance and a switch from the Java SDK to plumcp.Since ECA has been pretty stable and built in top of a nice extensible protocol, it worked so well that we decide to offer the same server capabilities to a Desktop client, similar to Claude Desktop but reusing the same server, this makes possible to have the same experience without an Editor, especially useful for non techinical people in price for a thin layer connecting to the server.
That’s a lot for ECA and all part of the amazing community that’s been activelly helping with issues, feedback and contributions.
We had some important bumps with lots of fixes, new code actions and contributions! The main highlight here is the arrival of performance tests in the project, which is a long waited thing which would help unblock performance optmizations in clojure-lsp since now we have a way to reliable know a mean, p90, p80 of how much time clojure-lsp spend on its features like initialization.
cyclic-dependencies linter falsely reporting cycles for (require ...) calls inside (comment ...) forms. #2107create-test code action appending a duplicate deftest when one with the matching name already exists, now navigating to the existing deftest instead. #2274:clean :ns-inner-blocks-indentation from :next-line to :keep, so clean-ns (including the automatic run after add-missing-libspec, add-require-suggestion, add-missing-import, and move-form) no longer reflows the :require/:import block when the user has not configured an indentation style. Users who want the previous behavior can set :clean :ns-inner-blocks-indentation :next-line explicitly. #2261add-missing-require refer suggestions leaking across languages, so a .clj file no longer offers refers defined only in .cljs files (and vice versa). #2271:private-by-default-on-extract? setting to control whether extracted functions and defs are private by default. #2258:as-alias by working around clj-depend bug.2026.04.16-20260503.191510-9.:gen-class check. #22421.2.54.:refer with :as and replace :as with :refer, with support for merging into existing :refer vectors.createServerInstaller and createLanguageServerSettingsContributor on ClojureLanguageServerFactory. Newer LSP4IJ versions added these as default interface methods, but our def-extension/gen-class-backed factory always overrides interface methods, so explicit stubs are required.slurp-action-test against newer LSP4IJ versions. Bump clj4intellij to 0.9.0 and switch the test fixture to setup-heavy so the project base has a real filesystem path (LSP4IJ’s FileSystemWatcherManager calls VirtualFile.toNioPath(), which throws on the in-memory TempFileSystem used by light fixtures).QuoteHandler compile error by merging BAD_CHARACTER into the quote-handler TokenSet.2026 Annual Funding Report 2. Published May 8, 2026.
Howdy folks! Thank you so much for the sponsorship this year. For the last two months, I have been focused on compiler architecture and optimization for jank.
On the compiler architecture side, I have designed and implemented a custom intermediate representation (IR) for jank programs which sets the stage for writing Clojure-specific optimization passes. This IR operates at the level of Clojure’s semantics, which is much higher level than LLVM IR, and so we are able to perform optimizations which LLVM could never do for us.
On the optimization side, I have taken the first benchmark of many, recursive fibonacci, and I have optimized it to be nearly twice as fast as Clojure JVM, for the same exact code. This benchmark is the first of many and I will be following up with more benchmark optimization results in the coming two months, using our new IR as a platform for optimizing jank’s performance.
To read all about the details of jank’s IR and the benchmark optimization, take
a look at this blog post:
2026 Annual Funding Report 2. Published May 11, 2026.
In this post I’ll give updates about open source I worked on during March and April 2026.
To see previous OSS updates, go here.
I’d like to thank all the sponsors and contributors that make this work possible. Without you, the below projects would not be as mature or wouldn’t exist or be maintained at all! So a sincere thank you to everyone who contributes to the sustainability of these projects.
Current top tier sponsors:
Open the details section for more info about sponsoring.
If you want to ensure that the projects I work on are sustainably maintained, you can sponsor this work in the following ways. Thank you!
Babashka Conf 2026 was held on May 8th in the OBA Oosterdok library in Amsterdam! David Nolen, primary maintainer of ClojureScript, was our keynote speaker. We’re excited to have Nubank, Exoscale, Bob, Flexiana and Itonomi as sponsors. Nubank and Exoscale are hiring. Wendy Randolph was our event host. For the schedule and other info, see babashka.org/conf.
The day after babashka conf, Dutch Clojure Days 2026 was also held - so it was a great weekend in Amsterdam!
Hope to have seen many of you there!
In the last two months I spent significant time organizing babashka conf, but made progress in several projects as well.
My upstream work to enable async/await in ClojureScript was merged in the beginning of March. The implementation mirrors squint. Thanks David for reviewing and merging. Also deftest now supports an ^:async annotation so you can use async/await and don’t need to mess around with the cljs.test/async macro anymore:
I’ll be presenting this work at the Dutch Clojure Days.
Rebel-readline is now bb compatible. The work involved mainly exposing more JLine stuff and making sure rebel-readline didn’t hit any internal JLine APIs. One step to drive this to completion was to make a dependency, compliment, bb compatible. Thanks both to Bruce and Alexander for the cooperation.
Squint now supports cljs.test and multimethods! clojure-mode was ported to use the new cljs.test.
On the cream front, I put in effort to make the binary smaller and have been keeping up with the new GraalVM EA releases. I’ve been posting bug reports to the crema maintainer. Currently there’s still an unfixed bug around core.async that I have trouble reproducing in pure Java. I also added lots of library tests to CI so I can ensure stability in the long run. For now it remains experimental, but the direction is promising.
A performance PR to weavejester/dependency speeds up depend, depends? and topo-sort significantly, so clerk notebooks render faster.
The cljfmt library, also by @weavejester, now fully runs from source in babashka. The Java diff library that wasn’t bb-compatible was replaced with text-diff, but only for the babashka path. The JVM build of cljfmt still uses the original Java diff library, with a possible switch later once text-diff has matured.
Several SCI fixes were made to improve Clojure compatibility between babashka and Clojure. E.g. records can now support extending to IFn which was a blocker for some Clojure libs that tried to run in bb so far.
Clj-kondo 2026.04.15 got a few new linters thanks to @jramosg for stewarding most of these. It also has better out of the box potemkin support, and @alexander-yakushev contributed a wave of performance improvements.
Updates per project below. Bullets are highlights; see each project’s CHANGELOG.md for the full list.
babashka: native, fast starting Clojure interpreter for scripting.
Completer, Highlighter, ParsedLine, Writer, Readerclojure.repl/special-doc and clojure.repl/set-break-handler!clojure.main/repl-readorg.jline.reader.Buffer to class allowlistclojure.java.javadoc namespace with javadoc available in REPL #1933(doc var), (doc set!) and other special forms #1932(source inc) and (source babashka.fs/exists?) for built-in vars #1935BABASHKA_REPL_HISTORY env var for configurable REPL history location #1930deftype and defrecord inside non-top-level forms (e.g. let, testing) #1936java.util.HexFormat interop support:as-alias-version as an alias for --versionclojure.lang.EdnReader$ReaderException--prepare flag skipping next tokenclojure.data.xml.tree/{flatten-elements,event-tree}, clojure.data.xml.event record constructors, and clojure.data.xml.jvm.parse/string-sourcejava.net.Proxy and java.net.Proxy$Type Java classes (@jeeger)java.lang.reflect.Constructor, java.lang.reflect.Executable, java.util.stream.Collectors, java.util.Comparator (for reify), and morenextjournal.markdown to 0.7.255, edamame to 1.5.39, data.xml to 0.2.0-alpha11, jsoup to 1.22.2, rewrite-clj to 1.2.54, tools.cli to 1.4.256, transit-clj to 1.1.357, fs to 0.5.32SCI: Configurable Clojure/Script interpreter suitable for scripting
recur with 20+ args in loop (#1035)recur arity, throw when it doesn’t match (#1034)IFn on defrecord, deftype and reify (#808, #1036)IPrintWithWriter as protocol (#1032)doc macrons-mapclj-kondo: static analyzer and linter for Clojure code that sparks joy.
:not-nil? which suggests (some? x) instead of (not (nil? x)), and similar patterns with when-not and if-not (default level: :off):protocol-method-arity-mismatch which warns when a protocol method is implemented with an arity that doesn’t match any arity declared in the protocol (@jramosg):missing-protocol-method-arity (off by default) which warns when a protocol method is implemented but not all declared arities are covered:redundant-declare which warns when declare is used after a var is already defined (@jramosg)import-fn, import-macro, and import-defimport-vars :refer and :rename syntaxpmap and future-related functions (future, future-call, future-done?, future-cancel, future-cancelled?) (@jramosg)throw with string in CLJS no longer warns about type mismatch (@jramosg)/bin/clj-kondo (@harryzcy)StackOverflowError occurs during analysiscream: Clojure + GraalVM Crema native binary
clojure.core.async-testhttpkit, nextjournal/markdown, clj-yaml, core.async ioc-macrossquint: CLJS syntax to JS compiler
defmulti, defmethod, get-method, methods, remove-method, remove-all-methods, prefer-method, prefers, plus hierarchy ops isa?, derive, underive, make-hierarchy, parents, ancestors, descendants (#806)cljs.test/report is now a multimethod, extensible via defmethod. test-var now fires :begin-test-var / :end-test-var events.await in async functions, in anticipation of CLJS next. The legacy js-await and js/await forms continue to work as aliases for now.cljs.test / clojure.test support: deftest, is, testing, are, use-fixtures, async, run-testswith-meta now preserves callability when applied to a function.cljc files via :require (no need for :require-macros); resolve qualified symbols from macro expansionssquint.compiler/compile* and squint.compiler/transpile* which accept either a string or a sequence of pre-parsed forms, skipping the forms -> string -> forms roundtrip for SSR use cases#html / #jsx were erased when an attrs map was present without a :class keycherry: Experimental ClojureScript to ES6 module compiler
await as a special form, in anticipation of CLJS next:require-macros clauses with :refer now properly accumulate instead of overwriting each othercherry.test with clojure.test-compatible testing API: deftest, is, testing, are, use-fixtures, async, run-tests. Macros are compiler built-ins (shared with squint), so no :require-macros plumbing is needed in user code.nbb: Scripting in Clojure on Node.js using SCI
IFn on defrecord and reifyp/then results)fs: file system utility library for Clojure
touch fn (@lread & @borkdude)Coercions and Returns / Argument Naming Conventions sections to README (@lread):nofollow-links option (@lread)split-ext and extension on dotfiles with parent dirs (e.g. foo/.gitignore)gzip & gunzip now honor dest dir when specified (@lread)umask on created files and directories (@lread)clerk: Moldable Live Programming for Clojure
weavejester/dependency (#808)v0.12.51 (#793), enables async/await in viewer functionspresent+reset! (#809)build-graph crash on non-Clojure source files (#810)edamame: configurable EDN and Clojure parser with location metadata and more
Nextjournal Markdown: A cross-platform Clojure/Script parser for Markdown
:disable-footnotes true to disable parsing footnotes #67quickdoc: Quick and minimal API doc generation for Clojure
grasp: Grep Clojure code using clojure.spec regexes
grasp.implbabashka.nrepl: The nREPL server from babashka as a library
send to prevent interleaved bencode frames from concurrent writesinfo and lookup op refinements: lookup carries nested info map whereas info is a flatmappod-babashka-instaparse: instaparse from babashka
add-line-and-column-info-to-metadata--features=clj_easy.graal_build_time.InitClojureClasses to native-imageinstaparse-bb: Use instaparse from babashka
add-line-and-column-info-to-metadata and get-failureparser (e.g. :output-format :enlive)java.net.URL for grammarsbabashka-sql-pods: babashka pods for SQL databases
next.jdbc, cheshire (Jackson 2.12 -> 2.20), PostgreSQL, MSSQL, HSQLDB, MySQL Connector/J drivershttp-client: HTTP client built on java.net.http
httpstat.us examples with httpbin.org in testsneil: A CLI to add common aliases and features to deps.edn-based projects
deps.clj: a faithful port of the clojure CLI bash script to Clojure
Contributions to third party projects:
depend, depends?, and topo-sort:bb reader conditionals to replace the AutoFlattenSeq deftype with plain vectors plus metadata markers, swap the Segment deftype for a reify-based CharSequence, and add a CI test runner. Open, awaiting review.These are (some of the) other projects I’m involved with but little to no activity happened in the past two months.
A Clojure Lambda function for monitoring AWS CloudWatch Logs subscriptions, using Gatus to monitor scheduled jobs, and configuring Amazon EventBridge Scheduler to run an ECS task twice a day.
When you let an AI agent write Clojure code, you expect it to leverage the language's superpowers—the REPL's interactivity, structural editing, format-preserving code manipulation, and the rich ecosystem of wrapper libraries. Instead, what you typically see is mediocre code written slowly, as the agent makes the same mistakes every developer learns to avoid.
I discovered this the hard way.
While building lite-crm with Claude Code, I deliberately avoided the –dangerously-skip-permissions flag. Instead, I sat beside the agent and watched it work—observing its patterns, frustrations, and failures. What I saw was an agent trained on millions of codebases but ignorant of how Clojure practitioners actually think.
Three concrete problems emerged:
When encountering Java interop, the agent jumps straight into direct interoperability without ever asking: "Is there a Clojure wrapper library for this?"
The result: Uglier code, harder to maintain, and a missed opportunity for idiomatic Clojure.
Code formatters like cljfmt are essential—but they create a sneaky problem. When the agent modifies source and the formatter shifts indentation by a single space, the agent's subsequent str_replace operations fail due to whitespace mismatch.
The result: I watched it fail, retry, fail again, then give up and rewrite entire files. Enormous token waste.
When a test failed, the agent fell back on the crudest debugging technique: add println statements, run the test, inspect output, delete the logs, restore the code. Repeat.
This is especially wasteful in a Clojure project where I've provided direct access to the REPL via the brepl CLI. The agent could inspect values interactively, test hypotheses instantly, and trace execution without touching source code. But it never did.
These weren't knowledge gaps. They were behavioral gaps—places where the agent's default approach conflicted with Clojure expertise.
In the context of Clojure Stack Lite (which includes proper testing harness and real database, not mocks), the agent wasn't just writing suboptimal code—it was making design decisions based on unfamiliar tools.
I decided to address this not by teaching the agent more facts, but by redirecting its behavior.
The result is four skills, each targeting a specific behavioral pattern that distinguishes novice agents from expert Clojure practitioners:
The Problem: Agents default to adding println, tap>, or logging statements, then running tests to inspect output.
The Pattern: In Clojure, this is backwards. The REPL lets you pin a value with def, explore its structure instantly, test hypotheses interactively—all without modifying code.
What the skill does: When you're about to debug, clj-debug redirects from logging patterns to REPL-based inline inspection. It teaches the agent to use def, keys, keyword access, and structural exploration—the actual workflow expert Clojure developers follow.
Behavioral change: From edit-test-inspect cycle to interactive REPL inspection. This is faster, non-invasive, and gives immediate feedback.
The Problem: When encountering unfamiliar Java classes or macros, agents jump to direct integration without exploring whether an idiomatic Clojure wrapper already exists.
The Pattern: Expert Clojure developers follow a deliberate workflow:
What the skill does: clj-discover codifies this workflow, ensuring the agent prioritizes idiomatic libraries and systematic exploration before writing integration code.
Behavioral change: From direct interop to research-first integration. The result is cleaner, more maintainable code.
The Problem: Code formatters shift indentation by spaces, breaking text-based str_replace. The agent then wastes tokens failing repeatedly or rewriting entire files.
The Pattern: Clojure is homoiconic—code is data. Two S-expressions are semantically equivalent even if formatted differently. Expert editors handle this automatically via structural editing.
What the skill does: clj-replace compares code by structure (S-expression equivalence) rather than text, ignoring whitespace while preserving the original file's formatting style. It uses the rewrite-clj library to parse, match, and replace nodes safely.
Behavioral change: From brittle text matching to robust structural matching. Formatting variations become irrelevant.
The Problem: Without guidance, agents write tangled code where reusable mechanisms are mixed with business policy, creating inflexible designs that accumulate technical debt.
The Pattern: Arne Brasseur's mechanism/policy separation principle is core to building maintainable Clojure systems. Mechanism is context-free, stable, and reusable. Policy is opinionated, domain-specific, and volatile. Expert developers keep these separate.
What the skill does: clj-refactor scans code for opportunities to extract mechanisms from policy—functions where hard-coded values or implicit context can be made explicit, dependencies can be pushed to parameters, and reusable logic can be isolated.
Behavioral change: From monolithic functions to extracted, composable mechanisms. Code becomes easier to test, reuse, and reason about.
Note: Unlike clj-debug, clj-discover, and clj-replace—which activate automatically when the agent encounters problems—clj-refactor is user-initiated. You invoke it when you want the agent to analyze code for refactoring opportunities, not in response to a failure.
These aren't reference manuals or API documentation. They're workflow redirects—rules that teach AI agents to think like expert Clojure developers instead of generic code writers.
The underlying philosophy is simple: A skill's value is measured by behavioral change, not knowledge transfer.
When an agent uses clj-debug, it stops adding logging. When it uses clj-discover, it checks for idiomatic wrappers before raw interop. When it uses clj-replace, formatting becomes irrelevant. When you invoke clj-refactor, the agent identifies tangled mechanisms and suggests extraction. Each skill shifts the agent's default patterns closer to expert practice.
This matters because Clojure is a language of leverage. The REPL, immutability, homoiconicity, and the functional approach all reward practitioners who use them correctly. An agent that doesn't leverage these features isn't just writing slow code—it's missing the point of the language.
The goal is simple: your AI agent shouldn't just write Clojure code—it should think like a Clojure developer. These four skills make that possible.
Find them at: github.com/humorless/clj-native-agent
May 2026
Snapshotting via copy-on-write is a well-trodden idea. ZFS and btrfs do it at the filesystem block layer; Neon and Aurora do it at the database page layer; Datomic and Datahike do it at the data-model layer. What differs is where the immutability lives, and that determines what you can do with the snapshots once you have them.
In Datahike, the database value itself is immutable. A datom never mutates; a query is always against a specific commit; a branch is a database value you can hand to a function. That last property changes the calculus in three ways.
First, branching is the same primitive as every other transaction. There’s no special bulk-load path, no restore mode, no control-plane operation — just a couple of small writes to storage.
Second, branches are database values you can pass to a query. The same query interface that reads the head of :db reads any historical commit on any branch. No special “as-of” mode, no separate replica.
Third, merging becomes a query. ZFS can clone a snapshot but can’t merge two of them — a filesystem doesn’t understand its own contents well enough to resolve a conflict. Datahike does: branches are database values, Datalog queries take multiple databases as inputs, so “what’s in :feature and not in :db” is a query you write. Filtering, transformation, and conflict resolution are all the same language you query the database with.
The rest walks through datahike.versioning in order, with a brief note at the end on how the same surface shows up in the other bindings.
A Datahike database is a persistent sorted set of datoms — five-tuples of [entity attribute value transaction op]. The storage layer is persistent-sorted-set, a B-tree-based immutable data structure designed for on-disk storage of sorted runs of datoms.
What matters for branching is the persistence property: every node is immutable. A transaction that adds, retracts, or modifies datoms walks from root to leaf, creates new nodes along the changed path, and leaves the unchanged subtrees pointing at exactly the same nodes as the prior snapshot. Both the old and new trees are valid; both are queryable; the new tree’s root is the only thing the system needs to know about to read it.
This is the same idea behind Clojure’s persistent vectors and Git’s object store. Datomic introduced it to databases in 2012; Datahike is the open-source descendant. Sharing is at the level of tree nodes: with a branching factor of 512, the tree stays shallow even for very large databases, and a transaction rewrites only the leaf and the few internal nodes on its path. Every other subtree is shared by pointer with the previous snapshot.
Each node is content-addressable — its key in konserve (the storage abstraction) is derived from its contents. konserve maps the same protocol over filesystems, S3, JDBC databases, IndexedDB in browsers, and others. A node written once is never rewritten. The only thing that ever changes is a small map at a well-known key listing the root pointers for the indices in the current snapshot. That map is a commit. A branch is a named pointer at a commit, registered in a :branches set under a known key.
require('[datahike.api :as d])
d/branch!(conn :db :feature)
(require '[datahike.api :as d])
(d/branch! conn :db :feature)
The system reads the commit-id currently at :db, verifies it points at a real commit, writes a new key mapping :feature → <commit-id>, and updates the :branches set to include :feature. Two key writes in the simple case — plus a CoW-branch operation for any attached secondary index (Lucene full-text, vector indices) that implements the branching protocol.
Wall-clock time depends almost entirely on the storage backend:
No tree nodes are copied. :feature and :db reach through the same physical objects in storage. A million-datom branch costs nothing extra at fork time, and a hundred branches are still a hundred small writes — not a hundred database copies.
If the source doesn’t exist, branch! raises :from-branch-does-not-point-to-existing-branch-or-commit. If the target name is already taken, it raises :branch-already-exists. Both are explicit; you don’t get silent overwrites.
Branches are first-class. You read them by name (branch-as-db), by commit-id (commit-as-db), or by holding a connection that was opened with a :branch in its config.
def feature-db: d/branch-as-db(conn :feature)
def main-db: d/branch-as-db(conn :db)
d/q('[:find ?e :where [?e :widget/sku]] feature-db)
;; Or pin to a specific historical commit by UUID
def older-db: d/commit-as-db(conn #uuid "b4f2e1c0-2feb-5b61-be14-5590b9e01e48")
(def feature-db (d/branch-as-db conn :feature))
(def main-db (d/branch-as-db conn :db))
(d/q '[:find ?e :where [?e :widget/sku]] feature-db)
;; Or pin to a specific historical commit by UUID
(def older-db (d/commit-as-db conn #uuid "b4f2e1c0-2feb-5b61-be14-5590b9e01e48"))
branch-as-db returns a database value — immutable, ready to query, safe to hold across calls. commit-as-db does the same for any historical commit, whether or not a branch still names it. Both work without an open connection on the target branch.
To write to a branch, connect with :branch in the config and transact normally:
def feature-conn: d/connect(assoc(cfg :branch :feature))
d/transact(feature-conn [{:widget/sku "Z", :widget/weight 99}])
(def feature-conn (d/connect (assoc cfg :branch :feature)))
(d/transact feature-conn [{:widget/sku "Z" :widget/weight 99}])
The write goes to :feature’s head; :db is undisturbed. Each branch has its own writer; transactions on different branches don’t serialize against each other.
Every transaction produces a commit whose :meta :datahike/parents set records its parents. branch! produces single-parent commits (the previous head of the branch). merge! produces commits with multiple parents. Walking back from any commit gives you the lineage.
require('[superv.async :refer [<?? S]]
'[datahike.versioning :refer [branch-history]])
d/commit-id(@conn)
;; => #uuid "b4f2e1c0-…"
d/parent-commit-ids(@conn)
;; => #{#uuid "…"} ; single parent on a normal commit
;; => #{#uuid "…" "…"} ; two (or more) parents on a merge commit
<??(S branch-history(conn))
;; => sequence of stored DB values, in order from the current head back
;; through every ancestor reachable via :datahike/parents
(require '[superv.async :refer [<?? S]]
'[datahike.versioning :refer [branch-history]])
(d/commit-id @conn)
;; => #uuid "b4f2e1c0-…"
(d/parent-commit-ids @conn)
;; => #{#uuid "…"} ; single parent on a normal commit
;; => #{#uuid "…" "…"} ; two (or more) parents on a merge commit
(<?? S (branch-history conn))
;; => sequence of stored DB values, in order from the current head back
;; through every ancestor reachable via :datahike/parents
branch-history is the workhorse for inspection: it walks the parent graph from the connection’s current branch backward and returns each commit as a DB value, with duplicates pruned. Useful for time-travel reports, audit trails, and assembling queries against arbitrary historical states.
merge-db plus DatalogThis is where the “branches as values” property earns its keep.
merge-db records a new commit on the current branch whose :datahike/parents includes both the previous head and :feature’s head. The tx-data is regular transaction data; Datahike applies it the same way it applies any transaction. The operation is routed through the writer so it serializes cleanly against concurrent transactions on the same branch. (Sync; there’s also d/merge-db! for the async path, intended for go blocks and listener callbacks.)
What merge-db does not do: figure out the tx-data for you.
That’s a feature, not a gap. Because branches are database values and Datalog queries take multiple databases as inputs, the diff between branches is a query:
d/q('[:find ?e ?a ?v
:in $feature $main
:where [$feature ?e ?a ?v _]
[:db/txInstant not= ?a]
not([$main ?e ?a ?v _])]
feature-db main-db)
(d/q '[:find ?e ?a ?v
:in $feature $main
:where
[$feature ?e ?a ?v _]
[(not= :db/txInstant ?a)]
(not [$main ?e ?a ?v _])]
feature-db main-db)
:in $feature $main binds two databases; :where clauses pick which one each pattern matches against. The result is the set of datoms present in :feature but absent in :db — directly transformable to tx-data.
Real merges are more selective. A few patterns that fall out naturally:
Filter by attribute — merge only the schema changes, leave the data behind:
d/q('[:find ?e ?a ?v
:in $feature $main
:where [$feature ?e ?a ?v _]
[contains?(#{:db/ident :db/valueType :db/cardinality} ?a)]
not([$main ?e ?a ?v _])]
feature-db main-db)
(d/q '[:find ?e ?a ?v
:in $feature $main
:where
[$feature ?e ?a ?v _]
[(contains? #{:db/ident :db/valueType :db/cardinality} ?a)]
(not [$main ?e ?a ?v _])]
feature-db main-db)
Last-write-wins on conflicting attributes — for each (e, a), pick the value with the latest transaction time across both branches:
d/q('[:find ?e ?a max(?t) ?v
:in $feature $main
:where or-join([?e ?a ?v ?t] [$feature ?e ?a ?v ?t] [$main ?e ?a ?v ?t])]
feature-db main-db)
(d/q '[:find ?e ?a (max ?t) ?v
:in $feature $main
:where
(or-join [?e ?a ?v ?t]
[$feature ?e ?a ?v ?t]
[$main ?e ?a ?v ?t])]
feature-db main-db)
Application-defined resolution — Datalog predicate clauses can call arbitrary functions, so routing each conflict through a domain resolver fits the same shape:
d/q('[:find ?e ?a ?v-resolved
:in $feature $main ?resolve
:where [$feature ?e ?a ?v-f _]
[$main ?e ?a ?v-m _]
[?v-f not= ?v-m]
[?resolve(?e ?a ?v-f ?v-m) ?v-resolved]]
feature-db main-db your-resolver-fn)
(d/q '[:find ?e ?a ?v-resolved
:in $feature $main ?resolve
:where
[$feature ?e ?a ?v-f _]
[$main ?e ?a ?v-m _]
[(not= ?v-f ?v-m)]
[(?resolve ?e ?a ?v-f ?v-m) ?v-resolved]]
feature-db main-db your-resolver-fn)
Once you have the tx-data — however you computed it — d/merge-db applies it and records the commit with both parents:
d/merge-db(conn
#{:feature}
mapv(fn [[e a v]]:
[:db/add e a v]
end diff-tuples))
(d/merge-db conn #{:feature}
(mapv (fn [[e a v]] [:db/add e a v]) diff-tuples))
branch-history then shows the merge commit; d/parent-commit-ids returns the full parent set.
The takeaway: Datahike doesn’t ship a built-in 3-way merge algorithm because it doesn’t need to. The merge algorithm is whatever Datalog query expresses your domain’s resolution rule. Three-way merge of textual files is hard because text has no semantics; merging datoms is a query because the data already carries its own structure.
This generalizes further than it looks. Martin Kleppmann has shown that CRDTs themselves can be expressed as pure Datalog queries over the operation log. Datahike’s merge model lets you adopt that approach incrementally: start with last-write-wins, add domain-specific resolvers where it matters, formalize as CRDT-shaped queries if you want full convergence guarantees.
force-branch!force-branch! is the equivalent of git reset --hard. Pass a database value, a target branch, and the set of parent branches or commit-ids to attribute the new head to:
;; Rewind :feature to a known-good historical commit, treating it
;; as a fresh start from :db.
d/force-branch!(d/commit-as-db(conn #uuid "b4f2e1c0-…") :feature #{:db})
;; Rewind :feature to a known-good historical commit, treating it
;; as a fresh start from :db.
(d/force-branch! (d/commit-as-db conn #uuid "b4f2e1c0-…")
:feature
#{:db})
The branch head is overwritten unconditionally; the previous head becomes unreachable from the branch name. Existing connections to :feature are now stale and must be released and reconnected.
Useful for rolling back a bad branch after experimentation, pinning a branch to a known commit for audit, or rewriting a branch’s lineage when you need to. Use with care — the prior data isn’t deleted (GC controls that) but you’ve removed the named entry point, so if no other branch or commit-id references it, it goes away on the next sweep.
delete-branch! and gc-storageRemoves :feature from the :branches set. The branch’s data stays in konserve, reachable by commit-id, until garbage collection sweeps it — that’s intentional, so you can recover a deleted branch if you change your mind. Live connections to :feature will fail after this; remote readers should release.
You can’t delete :db. It’s the default main branch and removing it would orphan the database; if you want the database gone, delete the database. Other branches are fair game.
Storage reclamation is a separate, explicit step:
require('[superv.async :refer [<?? S]])
;; Default: only reclaim space from deleted branches.
<??(S d/gc-storage(conn))
;; With a cutoff date: keep snapshots newer than the date plus all
;; branch heads; delete intermediate snapshots older than the date.
let [thirty-days-ago new java.util.Date(System/currentTimeMillis() - 30 * 24 * 60 * 60 * 1000)]:
<??(S d/gc-storage(conn thirty-days-ago))
end
(require '[superv.async :refer [<?? S]])
;; Default: only reclaim space from deleted branches.
(<?? S (d/gc-storage conn))
;; With a cutoff date: keep snapshots newer than the date plus all
;; branch heads; delete intermediate snapshots older than the date.
(let [thirty-days-ago (java.util.Date. (- (System/currentTimeMillis)
(* 30 24 60 60 1000)))]
(<?? S (d/gc-storage conn thirty-days-ago)))
Two things worth knowing about how gc-storage interacts with branch history:
Branch heads are always kept, regardless of cutoff. Every live branch’s current head survives every GC run; GC only removes the intermediate snapshots between commits — the dots between branch heads on the graph, not the latest dot on any branch.
Intermediate commits become unreachable below the cutoff. A 7-day cutoff means branch-history walks only return commits within that window plus the current heads, and d/commit-as-db lookups for older UUIDs fail because the snapshot is gone. The cutoff should also comfortably exceed your longest-running reader’s lifetime — Datahike’s distributed readers walk storage directly without coordinating with a writer, so a snapshot vanishing mid-query is a real failure mode. You’re trading old audit history (and reader safety) for disk space; pick the window to match your readers, compliance posture, and storage budget.
Without a date, d/gc-storage is always safe — it only reclaims storage from deleted branches. Datahike also ships an experimental online-GC mode that runs incrementally during transactions on single-branch databases; offline d/gc-storage is what you reach for in multi-branch setups.
For how gc-storage composes with purge (GDPR-driven datom deletion) and the broader governance story, see Data Governance in Versioned Systems.
A handful of workflows that branching makes affordable:
commit-as-db to find when the offending state appeared.None of these require special infrastructure. The same primitives that read the database also read every branch.
The versioning API is part of the Clojure API spec, and the Java, JavaScript / TypeScript, Python (pydatahike), C (libdatahike), and CLI (dthk) bindings are all auto-generated from it. Java surfaces it as Datahike.branchAsync / branchAsDb / mergeDb; JavaScript as d.branchBang / branchAsDb / mergeDb; equivalent forms in the others. The dthk CLI also supports the more general Datalog-driven merge workflow via dthk query with multi-source input and dthk transact — see the CLI doc for an example.
In SQL via pg-datahike, the read side is wired through session variables and a datahike.* function namespace: SET datahike.branch = 'feature', SET datahike.commit_id = '<uuid>', plus datahike.branches(), datahike.create_branch(), datahike.delete_branch(). Write-side merge-db over SQL is on the roadmap. See Datahike Speaks Postgres for the full pgwire surface.
For how the same branching model extends beyond Datahike — to Stratum (SQL / columnar), vector and full-text indices, and other systems via a shared protocol — see Yggdrasil: Branching Protocols.
purge removes datoms from the current branch; if you need them gone from every branch that referenced them (for GDPR or similar), the operation walks each branch independently. See Data Governance in Versioned Systems.merge-db is not yet exposed over SQL. Reads against any branch work; writes always land on the connection’s default branch in 0.1.The branching API is in datahike.api (with branch-history still in datahike.versioning). For SQL access, see pg-datahike and the wire-protocol writeup. Repo: github.com/replikativ/datahike.
Feedback to contact@datahike.io or open an issue.
How we at OTTO are re-engineering AWS microservices using AI agents and Git-based architecture 👉 Read now!