A live nREPL wire into your running Spring Boot app. Dev only. You've been warned.
Your AI agent can read your code. What it can't do is ask your app a question.
Livewire fixes that. It embeds a Clojure nREPL server inside a running Spring Boot application — giving an AI agent (or a curious developer) a live, stateful probe into the JVM. Beans, queries, transactions, security context, and all.
;; How many queries does /api/books actually fire?
(trace/detect-n+1
(trace/trace-sql
(lw/run-as "member1"
(.getBooks (lw/bean "bookController")))))
;; => {:total-queries 481, :suspicious-queries [{...} {:count 200} ...]}481 queries. For a list page. Now you know. Now you can fix it — without restarting the app.
Not a Clojure developer? Don't worry about the syntax above — the agent writes and runs it for you. The parentheses look foreign at first, but the language is actually small, consistent, and surprisingly readable once your eyes adjust. If you ever feel curious enough to type a snippet yourself, the basics take an afternoon. You won't regret it. But you don't have to.
Livewire is young. The core ideas are solid and it works well in practice, but the API surface
(Clojure function names and signatures, CLI wrapper scripts, SKILL.md structure) will change
as the tool evolves.
This matters less here than it would in a traditional library — and that's by design.
The real contract between Livewire and an AI agent isn't the Clojure API; it's SKILL.md.
The agent reads the skill file fresh at the start of every session. When the API changes,
SKILL.md changes with it, and the agent adapts automatically — no version pinning, no
migration guide to follow. The surface can evolve without breaking the workflow.
Development itself is heavily AI-assisted, which keeps SKILL.md honest: the same agent
that uses Livewire helps build it, so the documentation reflects how the tool actually
behaves, not how it was originally specced.
What this means in practice:
- Pin a version in your
pom.xml/build.gradleif you need stability - After upgrading, copy the new
skills/livewire/directory from the release (SKILL.mdand thebin/wrapper scripts) — that's all the migration there is - Feedback, bug reports, and ideas are very welcome
Modern Spring Boot development has a fundamental feedback loop problem. AI agents make it worse.
edit → restart (30–120s) → observe → repeat
Agents reason statically. They read the code, form a hypothesis, and apply a fix — but they can't observe the running system. So they guess. And when they're wrong, you restart again.
Livewire breaks the loop:
observe → hypothesise → hot-swap → verify → recompile
(zero restarts ──────────────────────────────)
Recompile and the query-watcher auto-applies your @Query changes live.
No REPL call needed — no restart either.
Curious? Run this against any Spring Boot app already running on your machine. No code changes, no dependency, no restart:
jshellThen at the jshell prompt, paste:
/open https://raw.githubusercontent.com/brdloush/livewire/refs/heads/main/attach.jsh
This downloads a small agent bundle, lists the running JVMs, and lets you pick one.
Once attached you get info(), beans(), eval(), sql(), and demo() in a live jshell prompt.
Java 21+ note: start your target app with
-XX:+EnableDynamicAgentLoadingto allow dynamic agent injection. For Maven:mvn spring-boot:run -Dspring-boot.run.jvmArguments="-XX:+EnableDynamicAgentLoading"
| Requirement | Notes |
|---|---|
| ☕ Java 17+ | Required |
| 🍃 Spring Boot 3.x or 4.x | Hibernate 6 and 7 both supported |
| 🤖 AI agent | Claude Code, ECA, or any agent with an nREPL tool |
| 🍺 bbin | For installing clj-nrepl-eval |
⚡ clj-nrepl-eval |
CLI wrapper the agent uses to talk to the nREPL (see Connecting below) |
Scope it to your local/dev profile — Livewire should never ship to production.
Maven
<dependency>
<groupId>net.brdloush</groupId>
<artifactId>livewire</artifactId>
<version>0.12.0</version>
<!-- scope to dev — never ship this to production -->
</dependency>Gradle
// developmentOnly or a dev-profile configuration
developmentOnly 'net.brdloush:livewire:0.12.0'Livewire auto-configures itself when two conditions are met:
- The JAR is on the classpath
- The property
livewire.enabled=trueis set
Add it to whichever local properties file your project already uses:
# application-local.properties (or -dev, -sandbox, whatever you call it)
livewire.enabled=true
# Optional: override the default nREPL port
livewire.nrepl.port=7888# application-local.yml
livewire:
enabled: true
# nrepl:
# port: 7888You'll see this in the logs on startup:
[livewire] nREPL server started on port 7888 with user aliases (lw, q, intro, trace, qw, hq, jpa, mvc, faker, cg)
That's it. No annotations, no Spring profiles to configure, no code changes.
M-x cider-connect-clj → localhost → 7888
All namespaces are pre-aliased in the user namespace at startup — no require needed:
| Alias | Namespace |
|---|---|
lw |
core — beans, transactions, run-as, properties |
q |
query — raw SQL, diff-entity |
intro |
introspect — endpoints, entities, schema |
trace |
trace — SQL tracing, N+1 detection |
qw |
query-watcher — auto-apply @Query on recompile |
hq |
hot-queries — live @Query swap + restore |
jpa |
jpa-query — JPQL via live EntityManager |
mvc |
mvc — response serialization |
faker |
faker — test data generation via datafaker heuristics |
cg |
callgraph — blast-radius impact analysis, method-dep-map, dead-methods |
Just connect and start typing — (lw/info) is a good smoke-test.
clojure -Sdeps '{:deps {nrepl/nrepl {:mvn/version "1.3.1"}}}' \
-M -m nrepl.cmdline --connect --host 127.0.0.1 --port 7888For AI agent use, you'll need clj-nrepl-eval — a tiny CLI tool the agent uses to
evaluate Clojure expressions against the live nREPL. Install it via
bbin:
bbin install https://github.com/bhauman/clojure-mcp-light.git \
--tag v0.2.1 \
--as clj-nrepl-eval \
--main-opts '["-m" "clojure-mcp-light.nrepl-eval"]'Then point your agent at port 7888 and load the Livewire skill (see the SKILL.md section
below). All namespaces are pre-aliased at startup — no manual require needed.
Start every session with lw-start — it discovers the nREPL, prints app info, and confirms
the connection is live:
lw-start
# [livewire] connected to localhost:7888
# {:application-name "my-app", :spring-boot-version "4.0.1",
# :hibernate-version "7.2.0.Final", :java-version "21"};; Convert any Java object to a Clojure map — works for both plain JavaBeans and
;; Java records (DTOs). Use this instead of clojure.core/bean, which silently
;; returns {} for records (no error, just missing fields).
(lw/bean->map some-dto)
;; => {:totalBooks 200, :totalAuthors 30, ...}
;; What repos are registered?
(lw/find-beans-matching ".*Repository.*")
;; => ("bookRepository" "authorRepository" "reviewRepository" ...)
;; All registered bean names
(lw/bean-names)
;; => ("bookRepository" "authorRepository" ... "dataSource" ...)
;; All beans of a given type
(lw/beans-of-type javax.sql.DataSource)
;; => [{:name "dataSource", :bean #object[HikariDataSource ...]}]
;; What DB URL is the app actually talking to?
(lw/props-matching "spring\\.datasource\\.url")
;; => {"spring.datasource.url" "jdbc:postgresql://localhost:32808/test"}
;; Runtime environment summary + primary DataSource details
(lw/info)
;; => {:spring-boot "4.0.1", :spring "7.0.2", :hibernate "7.2.0.Final", :java "25", ...
;; :datasource {:db-product "PostgreSQL 16.9"
;; :jdbc-url "jdbc:postgresql://localhost:5432/myapp"
;; :db-user "app" :driver "PostgreSQL JDBC Driver 42.7.8"
;; :pool-name "HikariPool-1" :pool-size-max 10}}
;; Wiring of a single bean — what it injects and what injects it
(lw/bean-deps "bookService")
;; => {:bean "bookService"
;; :class "com.example.BookService"
;; :dependencies ["bookRepository"]
;; :dependents ["adminController" "bookController"]}
;; App-level wiring graph — defaults to your own classes only (auto-detected via @SpringBootApplication)
(lw/all-bean-deps)
;; => [{:bean "adminService", :class "com.example.AdminService", :dependencies [...], :dependents [...]} ...]
;; Find high-fan-out coupling candidates
(->> (lw/all-bean-deps)
(sort-by #(count (:dependencies %)) >)
(take 10)
(mapv #(select-keys % [:bean :class :dependencies])))
;; Include Spring infrastructure beans too
(lw/all-bean-deps :app-only false)
;; CLI shorthands
;; lw-bean-deps bookService
;; lw-all-bean-deps
;; @Transactional surface of a single bean
(lw/bean-tx "bookService")
;; => {:bean "bookService" :class "com.example.BookService"
;; :methods [{:method "archiveBook" :propagation :required :read-only false ...}
;; {:method "getAllBooks" :propagation :required :read-only true ...}]}
;; All app-level beans with transactional methods (auto-filtered via @SpringBootApplication)
(lw/all-bean-tx)
;; Smell check: reads not marked read-only
(->> (lw/all-bean-tx)
(mapcat (fn [b] (map #(assoc % :bean (:bean b)) (:methods b))))
(filter #(and (not (:read-only %))
(re-find #"(?i)^(get|find|list|count|search|fetch)" (:method %)))))
;; CLI shorthands
;; lw-bean-tx bookService
;; lw-all-bean-tx;; Raw SQL through the live DataSource — always cap results
(lw/in-readonly-tx
(q/sql "SELECT id, title FROM book LIMIT 5"))
;; => [{:id 1, :title "All the King's Men"} ...]
;; Repository calls — always page, never call .findAll without a Pageable
(lw/in-readonly-tx
(->> (.findAll (lw/bean "bookRepository")
(org.springframework.data.domain.PageRequest/of 0 3))
.getContent
(mapv #(select-keys (clojure.core/bean %) [:id :title :isbn]))))
;; => [{:id 1, :title "All the King's Men", :isbn "979-0-925405-37-0"} ...]
;; Mutations roll back automatically — safe to experiment
(lw/in-tx
(.save (lw/bean "bookRepository") ...)
(.count (lw/bean "bookRepository")))
;; => 201 (and then silently rolled back)Spring Security doesn't know about your REPL. Without a SecurityContext it'll throw
AuthenticationCredentialsNotFoundException the moment you call anything @PreAuthorize-guarded.
run-as sets one for the duration of the call:
;; ✅ Preferred: vector form — [username role1 role2 ...]
(lw/run-as ["repl-user" "ROLE_MEMBER"]
(.getBookById (lw/bean "bookController") 25))
;; Multiple roles
(lw/run-as ["repl-user" "ROLE_ADMIN" "ROLE_MEMBER"]
(.getStats (lw/bean "adminController")))
;; ⚠️ Plain string form — only grants ROLE_USER + ROLE_ADMIN
;; Will throw AuthorizationDeniedException for MEMBER/VIEWER-gated endpoints
(lw/run-as "admin"
(.getBookById (lw/bean "bookController") 25));; See every SQL a call fires — wrap it and look
(trace/trace-sql
(lw/in-readonly-tx
(.count (lw/bean "bookRepository"))))
;; => {:result 200, :count 1, :duration-ms 8,
;; :queries [{:sql "select count(*) from book b1_0", :caller "..."}]}
;; Detect N+1 automatically
(trace/detect-n+1
(trace/trace-sql
(lw/run-as "member1"
(.getBooks (lw/bean "bookController")))))
;; => {:total-queries 481,
;; :suspicious-queries [{:sql "select ... from book_genre ...", :count 200}
;; {:sql "select ... from review ...", :count 200}
;; {:sql "select ... from library_member", :count 50}
;; {:sql "select ... from author ...", :count 30}]}
;; For @Async / CompletableFuture — capture SQL across all threads
(trace/trace-sql-global
(lw/run-as "member1"
(.getBooksByGenreAsync (lw/bean "bookService") 1)))
;; => {:result [...], :count 12, :queries [...]}481 queries for one endpoint. Four N+1 suspects flagged automatically. Now let's fix it.
No restart needed. Swap the JPQL, verify with trace-sql, iterate, commit the fix:
;; See what @Query methods exist on a repo
(hq/list-queries "bookRepository")
;; => ({:method "findAllWithAuthorAndGenres",
;; :jpql "SELECT DISTINCT b FROM Book b JOIN FETCH b.author LEFT JOIN FETCH b.genres"}
;; {:method "findByGenreId", ...} ...)
;; Swap to a candidate fix
(hq/hot-swap-query! "bookRepository" "findAllWithAuthorAndGenres"
"SELECT DISTINCT b FROM Book b JOIN FETCH b.author
LEFT JOIN FETCH b.genres LEFT JOIN FETCH b.reviews")
;; Verify — does the query count drop?
(trace/trace-sql
(lw/run-as "member1"
(.getBooks (lw/bean "bookController"))))
;; Restore when done — don't leave swapped queries hanging
(hq/reset-all!)
;; => [["bookRepository" "findAllWithAuthorAndGenres"]]Alternatively: just edit the @Query in your IDE, recompile, and the query-watcher
picks it up automatically — same result, no REPL call:
;; Check watcher status
(qw/status)
;; => {:running? true, :disk-state-size 8}
;; After recompiling in your IDE — watcher fires automatically:
;; [query-watcher] detected change: bookRepository#findAllWithAuthorAndGenres
;; [hot-queries] watcher re-swapped ✓;; All HTTP endpoints — auth, param sources, and required/optional flags
(->> (intro/list-endpoints)
(filter #(re-find #"books" (str (:paths %))))
(mapv #(select-keys % [:paths :methods :handler-method :pre-authorize :required-roles :parameters])))
;; => [{:paths ["/api/books"], :methods ["GET"],
;; :handler-method "getBooks", :pre-authorize "hasRole('MEMBER')",
;; :required-roles ["MEMBER"], :parameters []}
;; {:paths ["/api/books/{id}"], :methods ["GET"],
;; :handler-method "getBookById", :required-roles ["MEMBER"],
;; :parameters [{:name nil, :type "java.lang.Long", :source :path, :required true}]}]
;; All Hibernate-managed entities
(map :name (intro/list-entities))
;; => ("Author" "Book" "Genre" "LoanRecord" "LibraryMember" "Review")
;; Entity schema for one entity — straight from Hibernate's live metamodel
(intro/inspect-entity "Book")
;; => {:table-name "book",
;; :identifier {:name "id", :columns ["id"], :type "long"},
;; :properties [{:name "title", :columns ["title"], :type "string", :is-association false,
;; :constraints ["@NotBlank" "@Size(min=0,max=255)"]} ...]}
;; Full domain model in one call — great for ER diagrams
(intro/inspect-all-entities)
;; => {"Book" {:table-name "book", :identifier {...}, :properties [...]}
;; "Author" {:table-name "author", ...}
;; ...};; Execute any JPQL via the live EntityManager — returns plain Clojure maps
(jpa/jpa-query "SELECT b FROM Book b ORDER BY b.id" :page 0 :page-size 5)
;; => [{:id 1, :title "All the King's Men", :author {:id 6, ...}, :genres "<lazy>"} ...]
;; Scalar projections with AS aliases become named keys
(jpa/jpa-query
"SELECT b.title AS title, COUNT(lr) AS loans
FROM Book b JOIN b.loanRecords lr
GROUP BY b.id, b.title ORDER BY COUNT(lr) DESC"
:page-size 5)
;; => [{:title "Vanity Fair", :loans 7} ...]Lazy collections render as "<lazy>" rather than firing surprise queries.
Paged by default (:page-size 20).
;; Invoke a controller method under a live SecurityContext
;; and serialize with the exact same Jackson ObjectMapper Spring MVC uses
(mvc/serialize
(lw/run-as ["repl-user" "ROLE_MEMBER"]
(.getBooks (lw/bean "bookController")))
:limit 3)
;; => ^{:total 200, :returned 3, :content-size 51529, :content-size-gzip 8299}
;; [{"id" 1, "title" "All the King's Men", ...} ...]Or use the CLI wrapper (runs traced, returns JSON + metadata):
lw-call-endpoint bookController getBooks ROLE_MEMBER
lw-call-endpoint --limit 5 adminController getMostLoaned ROLE_ADMINNo database changes — the thunk runs inside a transaction that always rolls back. The diff shows exactly which fields changed and their old/new values:
(q/diff-entity "Book" 100
(fn [] (.archiveBook (lw/bean "bookService") (long 100))))
;; => {:before {:archived false, :archivedAt nil, :availableCopies 5,
;; :title "A Handful of Dust", ...}
;; :after {:archived true, :archivedAt "2026-03-28T16:16:20", :availableCopies 5}
;; :changed {:archived [false true]
;; :archivedAt [nil "2026-03-28T16:16:20"]}}Useful for discovering unintended writes, verifying a fix touched exactly the right fields, or systematically calling suspect service methods until the guilty one confesses.
No fixture files, no test annotations, no recompile. faker/build-entity constructs a valid
Hibernate entity using realistic fake data, optionally persisting it in a rolled-back transaction
so you can call services against a real, DB-assigned id without leaving any data behind:
;; Simple entity — no required FKs, just scalar fields
(faker/build-entity "Author")
;; => #object[Author ... {:firstName "Evelyn", :lastName "Hartwell",
;; :birthYear 1923, :nationality "Montenegrins"}]
;; Let Livewire resolve the full dependency chain automatically
(faker/build-entity "Book" {:auto-deps? true :persist? true})
;; Speculative pattern: persist + rollback — get a real DB-assigned id without leaving data behind
(let [review (faker/build-entity "Review" {:auto-deps? true :persist? true :rollback? true})]
;; Call your rating service with a real id — nothing persists after this block
(.computeAverageRating (lw/bean "ratingService") (.getId review)))Property values are selected by a heuristic table matched against name and type — 54+ patterns
covering person (firstName, email, phone), address (city, zipCode, latitude), internet
(url, token, imageUrl), company (department, jobTitle), financial (iban, currency),
content (description, bio), appearance (colorHex), and suffix rules (*Year, *At/*Since
for timestamps). jakarta.validation.constraints annotations are respected at generation time:
@Min/@Max clamp numeric ranges, @Size clamps string length, @Email ensures a valid email
address, and @Positive/@PositiveOrZero force non-negative values. If you pass an override that
violates @NotNull, @NotBlank, @Min, or @Max, build-entity throws immediately with a clear
message — before any transaction is opened. Lookup tables (e.g. Genre) are fetched from the DB
rather than created new to avoid unique-constraint violations.
Use build-test-recipe to capture the full entity graph into a recipe map of typed values — ideal
for seeding @BeforeEach setup and assertions in integration tests:
;; Returns every scalar field with its Java type and generated value, plus the repo bean name
(faker/build-test-recipe "Review")
;; => {:Review {:repo "reviewRepository"
;; :fields {:rating {:type "short" :value 5}
;; :comment {:type "string" :value "A remarkable journey..."}
;; :reviewedAt {:type "LocalDateTime" :value #object[LocalDateTime ...]}}}
;; :Book {:repo "bookRepository"
;; :fields {:title {:type "string" :value "The Midnight Crisis"} ...}}
;; ...}
;; CLI
;; lw-build-test-recipe ReviewUse :type to apply the correct Java cast in setter calls — (short 5) not 5 for a short
field. The :repo key gives the Spring bean name directly — no separate lookup needed.
To find which repository manages a given entity (or vice versa), use repo-entity /
all-repo-entities — no convention guessing, authoritative at runtime:
(lw/repo-entity "bookRepository")
;; => {:bean "bookRepository" :entity "Book" :entity-fqn "com.example.domain.Book" :id-type "Long"}
(lw/all-repo-entities)
;; => [{:bean "authorRepository" :entity "Author" :entity-fqn "..." :id-type "Long"} ...]
;; CLI
;; lw-repo-entity bookRepository
;; lw-all-repo-entitiesRequires net.datafaker:datafaker on the target application's classpath. Call
(faker/available?) first to confirm:
(faker/available?) ;; => true
;; CLI
;; lw-build-entity Review '{:auto-deps? true :persist? true :rollback? true}'Before modifying a repository, service method, or query, see which HTTP endpoints, schedulers, and event listeners would be affected — straight from the live bytecode:
;; Which endpoints call bookRepository/findAll, directly or via services?
(cg/blast-radius "bookRepository" "findAll")
;; => {:target {:bean "bookRepository" :method "findAll"}
;; :affected [{:bean "bookService" :method "getAllBooks" :depth 1 :entry-point nil}
;; {:bean "bookController" :method "getBooks" :depth 2
;; :entry-point {:type :http-endpoint :paths ["/api/books"] :http-methods ["GET"]
;; :pre-authorize "hasRole('MEMBER')"}}
;; {:bean "bookStatsReporter" :method "reportNightlyStats" :depth 2
;; :entry-point {:type :scheduler :cron "0 0 2 * * *"}}]
;; :warnings ["Method name 'findAll' matched multiple signatures — all overloads are included"]}
;; What breaks if I change bookService/archiveBook?
(cg/blast-radius "bookService" "archiveBook")
;; All methods at once — per-method {method → {:callers [...]}} map
;; Methods with empty :callers have no known entry points
(cg/blast-radius "bookService" "*" :per-method? true)
;; CLI
;; lw-blast-radius bookRepository findAll
;; lw-blast-radius bookService archiveBook
;; lw-blast-radius-all bookService ← per-method map, one commandThe call-graph index is built once (~30ms for a typical app) and cached for the session.
Call (cg/reset-blast-radius-cache!) after hot-patching a class.
Identifies public methods with no inbound callers. Splits into two categories so you know whether to delete or refactor:
(cg/dead-methods "bookService")
;; => {:bean "bookService"
;; :dead [] ; no callers anywhere — true dead code
;; :internal-only [{:method "archiveBook" :intra-callers ["adminHelper"]} ...]
;; :reachable-count 4
;; :dead-count 0
;; :internal-only-count 1
;; :warnings ["2 @EventListener beans detected — ..."]}:dead— no callers inside or outside the bean. Delete candidates.:internal-only— called only by sibling methods. Public by accident (testability, Kotlin default visibility). Refactoring candidates.
Only ACC_PUBLIC methods are analysed. Warns automatically when messaging beans
(@EventListener, @KafkaListener, etc.) or db-scheduler tasks are detected, since
those callers are not statically traceable.
# CLI
lw-dead-methods bookServiceFor each method on a bean, see exactly which injected deps it uses in bytecode — the missing link between "this bean has 6 deps" and "where to cut":
(cg/method-dep-map "adminService")
;; => {:bean "adminService"
;; :methods [{:method "getSystemStats"
;; :deps ["authorRepository" "bookRepository" "reviewRepository" ...]
;; :orchestrator? false}
;; {:method "getTop10MostLoanedBooks"
;; :deps ["bookRepository"]
;; :orchestrator? false}]
;; :dep-frequency [{:dep "bookRepository" :used-by-count 2 :methods [...]} ...]
;; :unaccounted-deps []}
;; Add intra-class call direction for split planning
(cg/method-dep-map "adminService" :intra-calls? true) ; which siblings does each method call?
(cg/method-dep-map "adminService" :callers? true) ; which siblings call each method?
;; CLI
;; lw-method-dep-map adminServiceWhen you want the split boundaries suggested automatically rather than reading the dep map
yourself, use method-dep-clusters. It groups methods by shared dep footprint, flags
which deps move cleanly with each group, and highlights any intra-call violations that
would make a proposed split unsafe — all in one call:
(cg/method-dep-clusters "memberService")
;; => {:clusters [{:id 0 :methods ["getActiveLoansForMember"]
;; :exclusive-deps ["loanRecordRepository"] :shared-deps [] :intra-call-violations []}
;; {:id 1 :methods ["getAllMembers" "getMemberById"]
;; :exclusive-deps ["libraryMemberRepository"] :shared-deps [] :intra-call-violations []}]
;; :orchestrators [] :dep-free [] :shared-deps-summary [] :unaccounted-deps []}
;; CLI
;; lw-method-dep-clusters memberService
;; lw-method-dep-clusters adminService --expand-privateLivewire is a dev-only tool and is intentionally not subtle about it.
The nREPL can query any table, call any service, and access anything the JVM can touch. This is the point — and the risk. Never enable Livewire against a production database or any environment with real user data.
Use it with:
- A local development database seeded with anonymized or synthetic data
- A sandbox / staging environment that is completely isolated from production
- Testcontainers-spun databases (like the Bloated Shelf playground below)
- A self-hosted LLM — your ground, your rules, no data leaving the building
There is no sandbox. Connecting to port 7888 means executing arbitrary JVM code. Exposing this port outside localhost is equivalent to handing over the JVM process.
# ✅ default — localhost only
livewire.nrepl.bind=127.0.0.1
# ❌ please don't
livewire.nrepl.bind=0.0.0.0Livewire defaults to 127.0.0.1 and will not bind to a broader interface unless
you explicitly tell it to. That's a guardrail, not a permission slip.
Livewire is provided as-is under the MIT license. The authors accept no liability for misuse, data exposure, or any damage resulting from use outside its intended scope.
Bloated Shelf is a real Spring Boot app — Spring Security, JPA, PostgreSQL, multiple roles, a handful of controllers and services, a domain model with real relationships. It happens to have an N+1 problem baked in, but that's just one reason to visit.
The real reason: it's a safe, self-contained Spring app you can hand to an AI agent along with a live Livewire nREPL and just... see what happens.
You will be surprised. Give an agent live, responsive tools with a fast feedback loop and it stops guessing. It starts exploring. It asks questions. It forms hypotheses, tests them in seconds, and builds on what it learns. The creativity that comes out of a well-equipped agent with shiny new toys is something you have to see to believe.
30 authors · 200 books · 50 members · ~5 reviews/book · all lazily loaded
git clone https://github.com/brdloush/bloated-shelf
cd bloated-shelf
mvn spring-boot:run -Dspring-boot.run.profiles=dev,seedTestcontainers spins up a PostgreSQL 16 container automatically. No external database needed. The Livewire nREPL comes up on port 7888.
- 🔎 Hunt the N+1: call the
bookControllerand watchtrace-sqlreport 481 queries — then fix it without restarting - 🧭 Discover the app cold: ask an agent to map out the domain model, endpoints, and auth rules using only the live REPL — no source reading
- 🔥 Hot-swap queries: iterate on JPQL live, measure each variant with
trace-sql, find the winner, commit - 🔒 Test auth boundaries: call the same endpoint under different roles with
lw/run-as, see what changes - 📊 Profile and compare: trace the naive N+1 endpoints against the clean aggregation queries in
adminController - 🧪 Prototype in Clojure, ship in Java: re-implement a service method as a REPL expression, validate query count, then write the real fix
- 💬 Ask nontrivial questions about your data: "Which genre has the most overdue loans?", "Who are the top reviewers and what do they have in common?" — the agent will introspect the entity model, figure out the schema, iterate on queries, and come back with an actual answer. Powerful BI in an agentic chat, no dashboard required
- 🤖 Let the agent loose: point a capable agent at the nREPL, give it
SKILL.mdas context, and watch what it does with the freedom
The app ships with an AGENTS.md
covering worked REPL examples, bean names, credentials, and a quick smoke-test —
a solid starting point for an agentic session.
skills/livewire/SKILL.md is the most important file
in this repository if you're working with an AI agent.
It covers the full API, worked examples, known pitfalls, and escalation strategies for debugging without restarts. It's written for agents — but it's perfectly readable by humans too.
Without SKILL.md in the agent's context, cooperation will be poor.
The agent will hallucinate method signatures, call things that don't exist,
and make sloppy guesses about behaviour it could just... ask the live app about.
With it, the agent knows exactly what tools it has, how to use them, and what to watch out for.
The skill is a directory — skills/livewire/ — containing SKILL.md, the bin/
CLI wrapper scripts (lw-jpa-query, lw-call-endpoint, lw-sql, etc.), and the
references/ files that the agent loads on demand for specific topics. Copy the whole
directory, not just the markdown file.
First, clone this repository (you don't need to build anything — you just need the files):
git clone https://github.com/brdloush/livewireThen copy the skill directory into your Spring Boot project:
# Per-project — checked in alongside the app that uses Livewire (recommended)
cp -r livewire/skills/livewire /your/spring-app/.claude/skills/livewire
# Or symlink it so upgrades are instant (just git pull in the livewire clone)
ln -s /path/to/livewire/skills/livewire /your/spring-app/.claude/skills/livewireMake sure the bin/ scripts are executable:
chmod +x /your/spring-app/.claude/skills/livewire/bin/*Even if the file is in place, explicitly telling the agent to load the skill at the start of a session gets much better results than hoping it gets picked up passively:
"Load the Livewire skill."
That one sentence changes the entire session. The agent switches from guessing to probing. From static analysis to live questions. From "I think the query might be..." to "I just measured it — 481 queries. Here's why, and here's the fix."
Quick namespace cheatsheet:
| Namespace | Require as | What it does |
|---|---|---|
net.brdloush.livewire.core |
lw |
Beans, transactions, run-as, properties, repo→entity mapping |
net.brdloush.livewire.query |
q |
Raw SQL, diff-entity |
net.brdloush.livewire.trace |
trace |
SQL tracing, N+1 detection |
net.brdloush.livewire.hot-queries |
hq |
Live @Query swap + restore |
net.brdloush.livewire.query-watcher |
qw |
Auto-apply @Query on recompile |
net.brdloush.livewire.introspect |
intro |
Endpoints, entities, schema |
net.brdloush.livewire.jpa-query |
jpa |
JPQL via live EntityManager, smart entity serialization |
net.brdloush.livewire.mvc |
mvc |
Response serialization via Spring MVC's Jackson ObjectMapper |
net.brdloush.livewire.faker |
faker |
Realistic test entity generation, build-test-recipe |
net.brdloush.livewire.callgraph |
cg |
Blast radius, dead methods, method dependency fingerprinting |
- 📖 Read the full SKILL.md — every function, pitfall, and worked example across all namespaces
- 🚀 Try the bloated-shelf demo app — a realistic N+1 scenario ready to investigate
- 🐛 Found a bug or have an idea? Open an issue
Java Troubleshooting on Steroids with Clojure REPL by Jakub Holý (2019) — the idea that you can wire a Clojure REPL into a running JVM and talk to it live was the seed this project grew from. Worth a read.
Don't touch live wires in production. But in dev? Grab on.