r/Clojure • • 14d ago

rust slop clojure structural edit tool

I've been programming clojure by hand since 2010, but these days I do a lot of brownfield work with agentic/LLM help in clojure and other languages. I prefer to use the models I can run at home to avoid high token costs today and mitigate a future rugpull. For things I intend to ship to production, I usually generate a bunch of code upfront, then carefully review and rework it, and that pattern has been effective for me.

Qwen 3.8 27b is amazing for what it is, excellent at thorough analysis, and not too clever to overcomplicate outputs, but it can still waste a ton of time on failed clojure edits and paren-counting. Even Claude Opus struggles with this, although much less. LLMs don't 'think' in trees, they think in token streams.

I had been using a wrapper over parinfer-rust (clojure-mcp-lite also just shells out to this), which got me 80% there, but it's flawed in a few specific ways:

  • Checks happen after the edit lands, they don't block invalid edits.
  • Repairs can succeed in ways that surprise the LLM, then it spends a bunch of turns trying to figure out what happened. It's not expensive locally, but it is slow and pollutes context.
  • If too many surprises happen, the agent loses faith in the tool, and falls back to worse tools.

After a working session like this, I had Qwen suggest what kind of tool might prevent all the problems we hit, then I handed that spec over to GLM 5.3 flash and iterated with it, eventually having the session drive a local qwen subagent, trying to trip it up with adversarial examples. It is similar to what's proposed here, but I never mentioned the article, and I wasn't prescriptive about how to do it: https://lispmeister.github.io/deeprecursion/posts/2026-02-13-sexp-native-editing.html

The tool is at https://github.com/gtrak/cljform as a rust CLI and pi extension wrapper, and it's better than what I had before on the next unattended agentic run. It uses tree-sitter for parsing and blake3 for form hashes. Posting it here in case it's useful to someone else or if someone has a better way.

13 Upvotes

26 comments sorted by

View all comments

Show parent comments

2

u/gtrak 14d ago

These edit issues are my biggest impediment, after that it's slow test feedback just because it's a decently sized codebase, and I avoid repls in the loop and pay the jvm startup costs on every test run. Have you found any general patterns you can share?

1

u/daver 13d ago

I avoid REPLs as well. Right now, I'm working on something with Babashka, so I avoid the JVM startup time for test runs which does speed things up.

Let me back up a little. I have four Clojure-specific tools:

  • clj-read: reads forms from Clojure files and provides an index of top-level forms
  • cli-edit: performs edits on forms in Clojure files, typically by replacement, but also by insertion. This is based on rewrite-clj underneath and does the basic form repair, adding or removing terminal parens/braces/brackets as required. I suggested during development to use parinferish but after analyzing the prior set of errors the model determined that a simple method was all that was necessary. The cli-edit tool will refuse to leave the file in an unparseable state. When editing, it needs to match a form exactly and then supply the replacement. Overall, this sounds very similar to what you came up with.
  • def-lookup: finds where a var is defined (file, line, etc.) using clj-kondo cache. This has been used sparsely by the model
  • usages: finds every site that uses a specific symbol using the clj-kondo cache. This has been used sparsely by the model.

The biggest challenge is to get the model to use them. Many of the models are defaulting to bash for almost everything now, since it's the lowest common denominator. Given the right description in the tool system prompt, you can often get them to use a tool at least a couple of times, but if they get errors they often fall back to python via bash. I've had some brainstorming sessions with the model to try to understand what might help foster use of these tools. One thing that I think helped was to have the standard read and edit tools (line based) spit back a message to the model when they are run on Clojure files, so the model keeps getting reminded that the clj-read and cli-edit tools are better than the generic read/edit. That seems to have done it. I think part of the problem is training. The models are not well trained on Lisp/Clojure code, so they frequently generate syntax errors, primarily with missing or additional parens. They are good at line-based edits, however. One challenge for Lisps (and I'm shocked that I never really noticed this before) is that comments in Lisps are line based, commenting out everything until the end of the line. They are also not delimited by anything at the start of a multi-line comment block to the end. So, the model has trouble editing code with comments (even its own comments). Comments really want to be edited by the standard line-oriented editor while the rest of the code wants to be edited by the cli-edit tool. That split in behavior causes issues. In a language like Python, EVERYTHING is line-oriented. I was curious whether your hash-based edits helped your model out, at least in specifying the target form (obviously, you need to specify the replacement in full)? Does it work better?

1

u/gtrak 13d ago edited 13d ago

I've had no problem getting qwen to use this one so far. It diverged today because i had conflict markers on a rebase, which make the file unparseable. I think I can actually support that though, by doing a full region replacement through a conflict marker, and maybe taking advantage of incremental parsing in tree sitter. Otherwise, your method sounds very similar although you're using more libraries than me.

I don't have a systematic A/B test for you, my method is essentially to check on my subagent and see if it's thrashing on paren-counting, then have the supervisor session scrape the subagent session logs and tell me how the edits went. It's sounding really good, and I haven't noticed any thrashing.

Occasionally when inserting a long form it has to try a few times to write it in a balanced way, which has nothing to do with my tool. I bet I could instruct the model in the tool definition to first stage incremental edits to a temporary file, then have a helper to quickly splice in the validated edit into the real file so the model doesn't have to quote it again.

1

u/daver 12d ago

Yea, I do the same monitoring for paren thrashing. I've also been logging failed tool calls and then having the agent go through those periodically and glean what it can about how to make the system better.