Releases & changelog
Every release is an annotated git tag, built by goreleaser into binaries for
six platforms (linux/macOS/Windows × amd64/arm64) plus checksums.txt. This
page is the same content as the
GitHub releases, distilled —
the tag annotation is the single source, so the two cannot drift apart.
Install the newest with the instructions on Install, or grab a specific version’s assets from the release page linked in each heading.
v0.8.9 — 2026-08-01
Section titled “v0.8.9 — 2026-08-01”A dev build claimed to be a release — and quietly built against the published runtime instead of your working tree.
-
A local
go buildreported as a release, andcljgo buildthen compiled against the PUBLISHED runtime. Go stampsBuildInfo.Main.Versionfrom the nearest VCS tag even for a plain local build inside the repo, so once v0.8.5 was tagged,IsRelease()was true for every dev build (a regression from ADR 0116, v0.8.5).The wrong version line was the visible half. The other: a true
IsRelease()makes the generatedgo.modpinrequire …/cljgo v0.8.5with noreplace— so anyone building a project with their own locally-built cljgo silently compiled against the released runtime, with changes topkg/lang,pkg/emitand everything else ignored and nothing to indicate it. It stayed hidden because CI and normal development setCLJGO_SRC, which outranks the pin.The discriminator is exact:
go install module@vX.Y.Zresolves through the module proxy and records a checksum but novcs.*settings, while any build from a checkout recordsvcs=git. VCS data means built-from-source, which is what a release is not. (#189) -
cljgo versionin a git worktree — an honest hook, not a fake fix. This cannot be fixed at runtime:debug.BuildInforecords no build directory, the binary may run anywhere, and a worktree build is indistinguishable from a main-checkout build from the inside. The truth exists only at build time, so there is now a hook for it:Terminal window go build -ldflags "-X github.com/muthuishere/cljgo/pkg/version.DevCommit=$(git rev-parse HEAD)"Set, it wins over buildvcs; unset, nothing changes. Releases are unaffected. The misleading
module vX.Y.Zfragment is also gone for source builds — it means “the user asked for @vX.Y.Z”, and Go’s nearest-tag stamp is a different claim. (#180) -
Non-UTF-8 Maven POMs parse.
parsePOMhad noCharsetReader, soencoding/xmlrefused any declared encoding other than UTF-8 and the Go error leaked verbatim into aG5011saying “the repository served something that is not a Maven POM”. It is one — cljgo simply refused to read it, and nothing about the coordinate was wrong. Real artifacts hit this (commons-parent).
Worth recording how #189 survived: the test fixture paired a module version
with vcs.revision — a combination that never occurs. The two cases are
told apart by exactly the VCS data that fiction gave to both.
v0.8.8 — 2026-08-01
Section titled “v0.8.8 — 2026-08-01”One fix, and it is the kind you cannot see.
-
Maven Central is now searched before Clojars, matching tools.deps. The default list was Clojars-then-Central, with a comment claiming that was the tools.deps default. It is not: tools.deps returns “central, then clojars, then other repos” unconditionally (
clojure/tools/deps/util/maven.clj:161-165).cljgo fetches from the first repository that answers, so a coordinate published to both resolved to a different artifact on cljgo than on the JVM — with no conflict, no diagnostic, and nothing in the project files to hint at it. For a dual-host
.cljclibrary this is the worst possible failure: the promise is identical behaviour on both hosts, and this broke it below the level anyone would think to look. You would be debugging a behavioural difference in your own code while the cause was which jar got fetched.(mvn-repo …)still prepends, so a project can override. The order is now pinned by a test citing the tools.deps source, so it cannot drift back. (#187) -
build.cljgobeatsdeps.ednfrom anywhere in the search. ADR 0119’s precedence was applied per directory, socljgo run /elsewhere/foo.cljccould take/elsewhere’sdeps.ednand return before reaching abuild.cljgoin the working directory. Where a cljgo build file exists,deps.ednis now not consulted at all.
Found by spike s79 while surveying what deps.edn :deps translation
would cost — reading :deps does not cause it, it only exposes it. The
consequence is fetch-time and needs the network to demonstrate, so this
shipped as a source-to-source argument plus a pinned invariant, not an
observed mis-resolution.
v0.8.7 — 2026-08-01
Section titled “v0.8.7 — 2026-08-01”The REPL could not see your project. Both halves of that, plus the design change that stops it recurring.
cljgo replandcljgo nreplresolved nothing from the project.(require 'myproj.core)failed in a project whose owncljgo runandcljgo buildresolved it — including in a freshly generatedcljgo newproject, which could not require its own namespace in its own REPL. A REPL-vs-run divergence, the class ADR 0007 calls unforgivable.requirewas never broken: it was searching an empty list, because neither entry point established the load path. The nREPL case is the worse one — that is the editor path, so it was invisible from a shell and landed on anyone using an IDE. (#185)- Resolution now happens once, before the subcommand dispatch — five
scattered call sites became one, so a new entry point cannot forget it.
The shape is taken from reading the alternatives: JVM Clojure has no
in-process bootstrap at all (the classpath is fixed by an external launcher
before the JVM starts, and
clojure.main/repltouches the classloader only to widen it, never to establish it); let-go installs one resolver before mode dispatch; Glojure drains its classpath into one global load path as the first statement ofmain. (ADR 0118) deps.edn’s:pathsis honoured when a project has no cljgo build file..cljcis the dual-host mechanism, so the projects cljgo most needs to work with declare their roots in Clojure’s own file and have no reason to carry a second one.build.cljgostill wins where both exist. Narrow on purpose::pathsonly, vector form only, parsed as data — not:deps, not aliases, which carry tools.deps semantics cljgo does not implement. (ADR 0119)- A conformance test was racy, not flaky.
agent-lifecyclefailed about one full run in N on the interpreted leg and passed 12/12 when hunted in isolation. The cause was the test using an error handler as a synchronisation point for a state transition that happens after it — racy on both hosts. cljgo’s ordering matches the JVM exactly (Agent.java:130-142); the first hypothesis, that this was a runtime divergence, was wrong. - Two pieces of pure waste deleted, both independent of the above:
filepath.Dir("")andfilepath.Dir("rel.clj")are both".", so the common shapes resolved the same directory twice (74 ms on thecljgo newlayout), andbuild.LoadPlanran twice per pass for a plan that was discarded. Measured:cljgo runin a dep-free project 202.7 ms → 89.4 ms.
Measured and recorded, not acted on: project resolution boots a whole
tree-walking interpreter (38 ms, 54 MB, 875k allocs) to read a build file,
and that constant does not scale with dependency count — a dep-free project
is the worst case. Spikes s72 and s78.
v0.8.6 — 2026-08-01
Section titled “v0.8.6 — 2026-08-01”The AOT test path did not work for dual-host projects, and the docs recommended an unsafe idiom. Both found by consumers.
cljgo test --compiledcould not run a.cljcsuite.nsNameForderived a namespace symbol by dropping the file extension using its own hand-written list, which trimmed only.cljgand.clj. Sotest/demo/core_test.cljcbecame the namespacedemo.core-test.cljc— the extension turned into a name segment — and the compiled leg required a name that cannot exist. It failed in the direction that looks fine:cljgo testgreen,--compiledunable to build, so anyone treating--compiledas their AOT test path silently had no AOT coverage at all. That is the REPL-vs-binary divergence ADR 0007 calls unforgivable, occurring in the harness meant to detect it..cljgowas broken identically — found by the regression test, reported by nobody.pkg/eval.SourceExtsis now the single list both the resolver and the test harness read. (#182):exit-codehad an EOF race, and cljgo’s own comment taught it. The docs saidnilis falsey so(if ((:exit-code p)) …)reads as “has it finished”. It does not:nilmeans not yet reaped, and reaping happens strictly after the child’s stdout reaches EOF. Draining:outthen reading:exit-codereportsnilfor a process that has already exited — measured at 5 runs of 5 onsh -c 'echo hi >&2; exit 3', correct on all 5 after 250 ms. The framing had already propagated into a consumer library’s docstring. The semantics were always right; the recommended read was not. Use:waitafter draining — it returns as soon as the reaper has the status and cannot reportnil.:exit-codeis for polling a child you are not draining.G5026replacesnamespace X not found after loading its source, which named the wrong condition: the file was found and evaluated, it just never defined the namespace (nonsform, or one disagreeing with its path). “Not found” sent readers hunting for a file sitting right where it should be.- Release notes no longer contradict the release.
.goreleaser.yaml’s Install footer is injected into every release body and claimedcljgo buildneeds a checkout of this repo andCLJGO_SRC— the exact opposite of what v0.8.5 fixed. The published v0.8.5 body is corrected in place with a visible note rather than silently overwritten.
v0.8.5 — 2026-08-01
Section titled “v0.8.5 — 2026-08-01”Two defects that made a released cljgo unusable, both found by consumers while the gate stayed green.
- A
go installed cljgo refused to build anything.go install github.com/muthuishere/cljgo/cmd/cljgo@v0.8.4produced a binary that failed on every project with “this is a dev cljgo binary (version 0.1.0-dev), so the generated go.mod needs a local runtime tree”.IsRelease()consulted only the version goreleaser stamps via ldflags, whichgo installnever sets — so the release-pin path was skipped and a binary whose whole promise is needing no source checkout demanded one.templates/web/Dockerfileinstalls cljgo exactly that way, so the shipped web template could not build in Docker at any release. The requested tag was never missing: Go records it in the binary, andpkg/versionalready read that field to print the provenance line — the line was right, the gate was wrong. (ADR 0116) - A JVM
build.cljbroke everycljgo runin the project. cljgo acceptedbuild.cljas one of its own build-file names. That was harmless while onlycljgo buildread the build file; v0.8.3 put it on thecljgo runpath, so cljgo began evaluating another tool’s build script, and its(:require [clojure.tools.build.api])is unresolvable here.build.cljis the tools.build convention and the only way a dual-host library publishes to Clojars — so this blocked exactly the projects cljgo exists to serve, across v0.8.3 and v0.8.4. cljgo now probesbuild.cljgothenbuild.cljgonly. This is about the build file’s name only —.cljremains a fully accepted source extension (ADR 0055), unchanged. Breaking if you named a cljgo build filebuild.clj: rename it. (ADR 0117) :exit-codeoncljg.process/spawn— the non-blocking counterpart of:wait, and the half of the #173 report that shipping only:alive?left open. It separates “exited with status N” from “still running” without blocking, which a closed stdout alone cannot express.nilwhile running, so(if ((:exit-code p)) …)reads as “has it finished”.G5025— a build file with no(defn build [b] …)reportedcompiler error at <build-driver>:1:38, naming a file cljgo synthesizes and giving a column into that nonexistent file, while never naming the build file the user wrote. It now names the file, the missing form, and a fix.- Subprocess output is now asserted complete and prompt. Two truncation
bugs shipped this cycle because nothing checked either property. New tests
drive output far past any pipe buffer through
cljg.io/execandcljg.process/spawnand assert exact byte counts. Measured limitation, recorded in the test file: reverting the fix, they pass on fast hardware — only with a slowed reader (what a slower machine amounts to) do they fail, at167936bytes against208894expected. A green local gate is necessary and never sufficient here.
v0.8.4 — 2026-07-31
Section titled “v0.8.4 — 2026-07-31”Eight consumer-reported defects, and a clojure.core that is now measured
against the JVM instead of assumed.
Every item here came from a real project using cljgo, filed with a repro.
clojure.coreno longer carries names JVM Clojure lacks. The ADR 0014 Result/Option family (ok,err,just,none, …) was interned straight intoclojure.coreand auto-referred everywhere, so a user who legally wrote(defn ok [x] …)got a shadow warning that cannot happen on the JVM. It now lives incljx.meta, required deliberately.try/catchremains the idiom for handling failure, exactly as on the JVM. (ADR 0115)- Core parity is now a measured, enforced property, not a claim. A ratchet
test diffs cljgo’s
clojure.coreagainst the real JVM 1.12.5 public var set in both directions and fails on any new divergence — and on any closed one, so improvements get locked in. Today: 691 cljgo publics against the JVM’s 679, 59 cljgo-only, 47 missing. Most of the 59 are internal helpers that are simply missing:private.- It immediately caught
nan?, a lowercase alias of the JVM’sNaN?with both spellings live. Removed: teaching a spelling that does not port is exactly what the precedence principle forbids.
- It immediately caught
cljg.ioerrors read as Clojure, with the raw Go text preserved inex-dataunder:go/errorso nothing is lost. No more doubledop "path": syscall path: reasonshapes or syscall names the caller never invoked. (ADR 0114)cljg.io/exec’s:timeout-msnow bounds the CALL, not just the process. A command that forked a grandchild held the output pipes open, so a killedsleep 5still returned ~16× past its deadline. A timeout that does not bound the call is a broken promise; measured 5.02 s → 0.35 s against a 300 ms deadline.cljg.processgains:alive?— a non-blocking liveness check.:waitwas the only option, so “is the child alive?” was unanswerable without running your own reaper.cljg.io/real-pathresolves symlinks and refuses cycles withG5024.absoluteis purely lexical and could not guard either.clojure.core/load-filewas interned with anilroot: it resolved, looked supported, and died on “cannot call nil”. Implemented for the interpreter; compiled binaries now say plainly that they have no reader rather than failing obscurely.- A fresh clone tells you what is wrong.
cljgo runin a project whose dependencies were never resolved raised a bare “could not locate namespace”; it now raisesG5023naming the build file and telling you to runcljgo build. cljgo versionidentifies the build. Every unstamped binary reported0.1.0-dev, so a bug report could not be tied to a commit. Dev builds now carry their commit and dirty flag; released binaries still show the clean tag.
v0.8.3 — 2026-07-31
Section titled “v0.8.3 — 2026-07-31”Two build defects that shipped green-looking wrong answers, and a benchmark claim that did not reproduce.
-
A
test/tree was invisible tocljgo runandcljgo build. A namespace resolved only relative to the requiring file, sotest/app/core_test.cljgrequiringapp.corelooked fortest/app/core.cljgand failed — and the error named the namespace, not the paths tried, so the cause was close to unguessable. The only workaround was moving the suite undersrc/, which dual-host.cljcprojects cannot do.Projects now have source roots, defaulting to
srcandtest(whichever exist), declarable with(paths b ["src" "spec"]). They are published even when a project has no dependencies — the old code bailed before setting any roots if there was nobuild.lock.edn. Roots append after the requiring file’s own directory, and providers still outrank every root, soclojure.*can never be shadowed. -
Two
(exe …)artifacts in onebuild.cljgocorrupted the second binary, whichever it was. cljgo’s namespace registry is process-global, so the second build saw the first build’s namespaces already interned, never loaded them, and emitted a program with those namespaces missing — their vars interned as hollow shells and unbound at runtime. The first binary was fine, so a project building an app plus a test runner shipped a working app and a test suite that could not run. Both declaration orders are now covered by tests that drive the real binary against real project trees. -
The published bri web-framework benchmark did not reproduce, and the page now says so. It claimed 78,126 req/s; re-run at v0.8.2 on the same machine class bri measures 31,404. clj-httpkit re-measured within 6% of its own published figure in the same session, so the ~2.4× movement is ours. The claims of “comparable-or-better throughput” and “top tier with Rust/Deno/Bun/http-kit” are withdrawn; the structural wins (~42× smaller image, ~40× faster cold-start, an order of magnitude less memory) stand. See benchmarks.
-
Performance, language-wide. Hash-map reads no longer allocate (
findwas heap-allocating an entry per lookup, and the hotgetpath discarded the key immediately); keyword hashes are cached at intern time rather than recomputed on every map operation (2.3× on keyword map lookup); and bulk hash-map construction now builds through a transient like Clojure’s ownPersistentHashMap.create(3.4× faster with 5.7× fewer allocations at 128 entries). -
Request-path work in bri — a pre-sized body read (256 KiB: 2× faster, 41% less memory, allocations now flat regardless of payload) and a needless vector construction removed from JSON encoding. Worth knowing: this made
requestMap2.3× faster and moved end-to-end throughput 0–2%. The request path was never the bottleneck, and the benchmarks page says that too. -
The web benchmark suite is now in the repo at
benchmark/web/— runner, corpus and Dockerfiles — and the bri image builds cljgo from your checkout rather than a release. Promoting it revealed the old corpus no longer compiled at all.
v0.8.2 — 2026-07-31
Section titled “v0.8.2 — 2026-07-31”A silent test-runner failure, a lockfile that dead-ended, and portable date patterns.
cljgo testsilently skipped every.cljcfile. It reportedRan 0 tests containing 0 assertionsand exited 0 — a green build that had run nothing. If your project keeps its tests in.cljc(the portable choice this project recommends), CI has been passing without testing anything. This is the reason to upgrade.- Editing a dependency version no longer dead-ends the build. Bumping a
version in
build.cljgoused to stop withnote: run resolve with -update to re-pin— naming a command that has never existed, leavingrm build.lock.ednas the only findable remedy, which also discards every unrelated pin. The lock now refreshes itself, and does so minimally: what you bumped moves, and nothing else does. (ADR 0112) cljgo build --locked(orCLJGO_LOCKED=1) makes a stale lock an error instead of a refresh — thenpm ci/cargo --lockedposition, for the case where a merge takesbuild.cljgofrom one branch andbuild.lock.ednfrom another. Refuses withG5021, naming what diverged, and writes nothing.cljg.date/format-patternandparse-patterntake a java.time pattern —"yyyy-MM-dd HH:mm:ss"means the same thing here and on the JVM, so a.cljclibrary’s:cljbranch needs no translation at all. Verified against a 4,000-pattern JVM differential corpus: 0 divergences, 0 patterns accepted that the JVM rejects. Tokens with no exact equivalent are refused by name (G5022), never approximated. The existing Go-layoutformat/parseare unchanged. (ADR 0113)- Locale is ENGLISH, and the docs say so explicitly — not
ROOT, which collapsesMMMMtoJulandEEEEtoFriand would diverge silently on exactly the tokens the advice protects.
- Locale is ENGLISH, and the docs say so explicitly — not
- 15 error codes that existed but were undocumented are now on the diagnostics page, with a test that fails if the registry and the published table ever drift apart again.
- Emit-comment truncation could split a multi-byte rune, producing invalid UTF-8 in generated Go.
- Benchmarks re-measured at this version. The AOT head-to-head was re-run
as one fresh session — cljgo and both competitors re-timed together. Glojure
still wins 6 of 8 rows; cljgo still takes
takandfib; the binary is byte-identical to v0.8.1 (7,049,666 B), because this release’s code is build-time only or lives inpkg/bri, which a plain AOT program does not link. cljgo still starts slower than Glojure (5.1 ms vs 3.9 ms). See the benchmarks page — and note the caution there about not diffing timings across sessions. The other tables on that page still predate v0.7.0 and continue to say so.
v0.8.1 — 2026-07-31
Section titled “v0.8.1 — 2026-07-31”Clojars consumption works, and there is now a test that proves it.
- Consuming pure-Clojure libraries from Clojars works end to end. Declare
(dep b "group/artifact" {:mvn/version "V"})inbuild.cljgo, build in project mode (no file argument), and the POM chain, transitive walk, jar extraction and load path all resolve. Proven by runningcom.stuartsierra/dependency 1.0.0as a compiled binary. See Dependencies & publishing. - Libraries that genuinely need the JVM are refused per namespace, with a
coded and located diagnostic —
I4002for real Java interop,R1012for a starved reader conditional — rather than half-loading. - A network integration test (
CLJGO_CLOJARS_IT=1) now checks both directions against the live repositories, so this class of defect cannot reach a release unnoticed again. It is skipped by default: a green build never depends on Clojars being up. - Five defects found only by taking the resolver to real libraries:
defnattr-map,defrecordnamespace qualification, nested starved conditionals, doubled error notes, and lockfile write ordering. - Benchmarks re-measured at this version. Glojure AOT still wins 6 of 8 rows; startup regressed 4.7 → 6.6 ms and the binary grew to 7.0 MB. Both are on the benchmarks page rather than buried.
v0.8.0 — 2026-07-30
Section titled “v0.8.0 — 2026-07-30”Clojars, the Go standard library, and bytes/dates.
- Consume from Clojars (ADR 0095). Coordinate → transitive
.pomwalk → jar → extraction → load path →require. Pure Go (net/http+archive/zip+encoding/xml) — no JVM, nomvn, no Aether. Honest scope, stated in the docs: pure-Clojure libraries. The reachable set is utility and algorithm libraries, not the Java-wrapping mainstream. Of seven sampled live: 2 fully consumable, 4 partial, 1 unusable.- Gating is per namespace, not per library — one jar routinely mixes both (hiccup is 8 pure + 2 Java), so a whole-library gate is wrong in both directions.
- Reader conditionals classify on reader output, not a text scan, so
they are immune to
#?(inside strings and comments. A.cljcwhose real body is:clj-only fails loud (R1012) instead of silently loading an empty namespace. - Unimplemented Maven features (
${property},dependencyManagement, parent, ranges, SNAPSHOT, profiles, classifiers) name-error rather than half-resolve.
require-goreaches the Go standard library (ADR 0109).- Bytes, dates, base64, and two core parity bugs (ADR 0110).
cljg.io/cljg.streambyte-level I/O,cljg.dateISO format and parse, base64.(str (random-uuid))is the bare 36-character form again — it had been leaking the#uuidtag into every id on the wire, silently;pr-strkeeps the tag, and both the split and the round-trip are frozen.clojure.string/replaceandreplace-firstaccept a function replacement, with the JVM’s contract for what the fn receives. - Fixes: a Windows-only file-handle leak in
cljg.stream(POSIX unlinks open files, so two platforms hid it and onlywindows-latestcaught it); anchored and zero-width regex parity instr/replaceon both the fn and the$-template path; byte routes agree on sign and producers compose with consumers.
Every fix in this cycle was checked by an adversarial verifier that re-derived the JVM oracle itself. That caught three defects the fixes introduced, all closed before the tag.
v0.7.0 — 2026-07-28
Section titled “v0.7.0 — 2026-07-28”The extensions tier, and all eleven known issues closed.
cljx.*— the extensions tier.cljx.coreis opt-in ergonomics (add!/bump!/del!/dbg) as transparent aliases whose docstrings name theswap!form they replace (ADR 0106).cljx.testis Bun-flavoured testing layered onclojure.test: mocks, spies, stubs, output capture by default (ADR 0105).cljg.*wave 1 (ADR 0103):cljg.http/serve,cljg.socket,cljg.net.dns,cljg.compress,cljg.security,cljg.system/sleep. All pure Go,CGO_ENABLED=0, and lazily registered — a binary that never requires them pays zero bytes.- Performance. ADR 0067’s numeric inference gained a second op table
(
quot/rem/bit-*/unchecked-*/predicates). One untyped op used to demote an entire carrier graph, so closing the table re-enables whole regions: collatz 1956.6 → 32.3 ms, bitmix 663.6 → 41.0 ms, digit-sum 146.0 → 13.9 ms, gcd 343.6 → 33.8 ms — for +192 bytes of binary. ADR 0064 added cross-var direct calls (~1.15× on call-heavy code, +1.5% binary). ADR 0108’s--seal-coreis opt-in only: it measured ~1–2% on micros and nothing end-to-end, so it is not the default — it exists for the JVM’s:inlinesemantics, not for speed. - All 11 known issues closed:
clojure.core.matchmap patterns across differing key sets;for:when/:while; fn metadata;extend-protocolon qualified class names; compiled test binaries exiting 0 on a red suite;.getMessageon host errors; build-vs-run diagnostic parity;sleep; a public*out*writer.
v0.6.0 — 2026-07-24
Section titled “v0.6.0 — 2026-07-24”Opt-in OpenTelemetry.
bri.otel (ADR 0074) adds distributed tracing as an
opt-in namespace: a server span per request (named by route pattern), W3C
traceparent adopt and inject, an OTLP exporter driven by the OTEL_* env
vars, service.name from config, request-id and subject as span attributes,
correlated with the existing Prometheus metrics and structured logs.
Zero cost when unused, and proven so: it is linked only when the app
requires bri.otel, and a plain bri.http binary carries zero OpenTelemetry
symbols while a bri.otel binary carries the SDK. Pure Go, CGO_ENABLED=0
static, dual-mode byte-identical.
v0.5.0 — 2026-07-24
Section titled “v0.5.0 — 2026-07-24”First-class bri — the one-person web framework, end to end.
cljgo new --template web blog && cd blogcljgo generate resource Note title:string body:textcljgo test # green: a working, authenticated, DB-backed CRUDdocker build . # a ~15 MB static-binary imagebri.db(ADR 0072): a pure-Go data layer — modernc SQLite (the zero-install default) plus pgx/Postgres, parametrized queries, transactions, migrations. AOT-compiled and interpreted, byte-identical,CGO_ENABLED=0static.- The resource generator (ADR 0073):
cljgo generate resourcescaffolds migration + model + handlers + routes + a green CRUD test. - The conformance suite got ~5.3× faster (237 s → 45 s) via a batched compiled harness.
v0.4.0 — 2026-07-24
Section titled “v0.4.0 — 2026-07-24”bri goes to production.
The flagship web framework AOT-compiles to a single static CGO_ENABLED=0
binary, deployable as a ~15 MB Docker image (ADR 0071), while the interpreter
path stays byte-identical.
Measured against the field (Docker, oha): compiled bri.http at ~78k req/s,
p99 1.4 ms, ~16 MB RSS, ~30 ms cold start — top tier alongside Rust/Deno/Bun,
ahead of Go net/http, Node, .NET, Spring Boot and FastAPI; and against JVM
Clojure web, ~55× smaller image, ~40–55× faster start, ~22–30× less memory.
Reproduce with spikes/s45-bri-aot-docker/bench/run.sh.
v0.3.1 — 2026-07-24
Section titled “v0.3.1 — 2026-07-24”REPL session resume from the command line (ADR 0070).
cljgo repl :resume <#|id> (and a bare cljgo repl <id>) resumes a saved
session on boot — completing the CLI leg ADR 0016 promised but never shipped,
so the farewell line’s command now actually works. With no id,
:resume/:sessions list a numbered, newest-first table; resume by the short
number. Sessions record their working directory and resume cds back into it,
so require/load resolve as they did.
v0.3.0 — 2026-07-23
Section titled “v0.3.0 — 2026-07-23”Performance, fundamentals, and the web API tier.
- Perf campaign (ADRs 0063–0067): emitted code went from ~35× to ~4.8× of handwritten Go; the AOT binary won every recursion and data-structure benchmark row outright and tied startup at 5.0 ms.
clojure.corecomplete: 632/679 oracle vars (93%), with the other 47 documented as JVM machinery; every satellite namespace at 100%.- bri’s API-first security tier (ADR 0069): JWT auth, composable guards, a Compojure-style router, CORS, metrics, audit, auto-ban — one blessed way, one static binary.
examples/web-api: a runnable JWT-secured JSON API to copy.
v0.2.0 — 2026-07-16
Section titled “v0.2.0 — 2026-07-16”The adoption release: a downloaded binary plus the Go toolchain is the whole story.
cljgo buildworks from the binary alone — release binaries pin the published runtime module and interop host facts resolve from the generated module. No checkout, noCLJGO_SRC(ADRs 0028/0033).- nREPL:
cljgo nrepl, so Calva and CIDER connect (ADR 0031). format/printfwith Java’s grammar, corpus-verified (ADR 0030).- BigDecimal rewritten as unscaled
big.Int+ scale, Java’s exact model — killing silent long-literal corruption (ADR 0032). - 9.5× faster boot (211 ms → 22 ms) and faster dynamic-var access everywhere, via goroutine-ID lookup in assembly (ADR 0034).
- clojure-test-suite: 217/242 (89.7%), up from 57/242.
v0.1.0 — 2026-07-16
Section titled “v0.1.0 — 2026-07-16”First public release. Clojure hosted on Go: a compiler written in Go that AOT-emits plain Go source, plus a tree-walk evaluator that is the REPL and the macro engine.
- Zero-ceremony Go interop in both modes — the Go toolchain is the classpath.
- Real goroutines and channels for
(go …), with no CPS rewrite. - clojure-test-suite: 102/242 files passing (42.1%).
- Verified on Linux, macOS and Windows.
Questions or feedback on this page? Comment below with your GitHub account — comments are public and live in the project's GitHub Discussions.