Table of Contents

1. Agentic REPL Harness

For AI agents driving the REPL through tmux, the key pattern is eval-wait-capture with state detection.

1.1. Core Pattern

eval_repl() {
    local code="$1" timeout="${2:-3}"
    tmux send-keys -t repl "$code" Enter
    sleep "$timeout"
    tmux capture-pane -t repl -p | tail -20
}

1.2. Common Mistakes

  1. Not waiting for output — capture happens before eval completes
  2. Assuming REPL state — sent Clojure to a shell prompt
  3. Namespace not loaded(r/help) fails without require
  4. Working directory wrong — file operations fail silently
  5. Quoting issues — shell interprets quotes before tmux sees them

1.3. Robust Eval with Completion Detection

eval_with_wait() {
    local code="$1" max_wait="${2:-30}"
    local marker="__DONE_$$__"

    # Send code wrapped with completion marker
    tmux send-keys -t repl \
      "(do $code (println \"$marker\"))" Enter

    # Poll for marker
    local elapsed=0
    while [[ $elapsed -lt $max_wait ]]; do
        if tmux capture-pane -t repl -p | grep -q "$marker"; then
            tmux capture-pane -t repl -p | grep -B 20 "$marker" | head -19
            return 0
        fi
        sleep 1
        ((elapsed++))
    done
    echo "TIMEOUT" && return 1
}

1.4. State Detection

detect_repl() {
    local output=$(tmux capture-pane -t repl -p | tail -5)
    if echo "$output" | grep -qE '=>|user>|λ'; then
        echo "clojure"
    else
        echo "shell"
    fi
}

1.5. Hypothesis-Driven Workflow

# 1. Observe shape
./repl-eval '(keys (first data))'

# 2. Sample distribution
./repl-eval '(->> data (map :type) frequencies (sort-by val >))'

# 3. Bind result
./repl-eval '(def errors (->> data (filter #(= :error (:type %)))))'

# 4. Refine hypothesis
./repl-eval '(->> errors (group-by :source) (map (fn [[k v]] [k (count v)])))'

The REPL maintains state across evaluations — intermediate results persist for the entire session. This is the key advantage over scripts: each eval builds on previous work without re-computation.

1.6. Daemon Architecture (Recommended)

The optimal setup: JVM nREPL runs as a daemon, Babashka scripts query it.

┌─────────────┐    Bencode/TCP    ┌─────────────────┐
│  Babashka   │ ←───────────────→ │   JVM nREPL     │
│  (10ms)     │    port 1667      │   (daemon)      │
│  Scripts    │                   │   Full Clojure  │
└─────────────┘                   │   AeroAPI libs  │
                                  │   Stateful      │
                                  └─────────────────┘

Why this architecture:

  • JVM startup cost paid once (daemon)
  • Babashka scripts start in 10ms
  • Full Clojure available (specs, test.check, Java interop)
  • State persists across queries (defs, loaded namespaces)
  • LAN-accessible for remote agents
# Start daemon once
clj -M:dev -m nrepl.cmdline --bind 0.0.0.0 --port 1667 &

# Fast queries via Babashka (111ms vs 2.5s)
bb scripts/nrepl-query.bb '(count (all-ns))'
bb scripts/nrepl-query.bb '
  (require (quote [aeroapi.core :as api]))
  (def client (api/from-env))
  (count (:arrivals (api/request client {:method :get :path "/airports/KBOS/flights/arrivals"})))
'

1.7. Babashka nREPL Client

Babashka speaks raw Bencode to the JVM daemon:

#!/usr/bin/env bb
;; nrepl-query.bb - Fast queries to JVM nREPL daemon

(defn bencode [data]
  (cond
    (integer? data) (str "i" data "e")
    (string? data) (str (count (.getBytes data "UTF-8")) ":" data)
    (map? data) (str "d" (apply str (mapcat (fn [[k v]] [(bencode k) (bencode v)])
                                             (sort-by key data))) "e")))

(defn nrepl-eval [code]
  (let [sock (java.net.Socket. "localhost" 1667)
        out (.getOutputStream sock)]
    ;; Clone + eval + collect responses
    (.write out (.getBytes (bencode {"op" "clone"})))
    ;; ... parse bencode responses ...
    ))

(when (seq *command-line-args*)
  (println (:value (nrepl-eval (first *command-line-args*)))))

1.8. Performance Comparison

Benchmark: 100 iterations of (+ 1 2) against running JVM nREPL daemon.

Client Min Max Avg Speedup
bb nrepl-query.bb 103ms 184ms 115ms 21x
clj -M:dev -e 2400ms 2500ms 2462ms baseline
=== Benchmark Results (hydra, FreeBSD 14.3) ===

Simple query '(+ 1 2)' - 100 iterations:
  Min: 103ms  Max: 184ms  Avg: 115ms

API query with AeroAPI call - 10 iterations:
  Avg: 1081ms (includes ~500ms network latency)

JVM client - 3 iterations:
  Avg: 2462ms

Key insight: The Babashka client overhead is ~115ms. The JVM client pays ~2400ms of startup cost every invocation. For agent automation where you may issue hundreds of queries, this 21x speedup adds up.

The daemon architecture gives agents full JVM Clojure power with Babashka-level startup times. Best of both worlds.

1.9. Considerations

  1. Daemon lifecycle — Start nREPL on boot or first use. It holds state (loaded namespaces, defs) across queries. Restart to reset.
  2. Connection pooling — Each nrepl-query.bb call opens a new connection. For burst queries, consider keeping a session open.
  3. Error handling — Bencode parse errors are silent. Add logging in production scripts.
  4. LAN security — Binding to 0.0.0.0 exposes nREPL. Use firewall rules or bind to specific interface (Tailscale IP recommended).
  5. Memory — JVM daemon holds loaded code in memory. For long-running daemons, monitor heap usage.

1.10. Direct nREPL (JVM Client)

For JVM-to-JVM communication (editor integration):

(require '[nrepl.core :as nrepl])
(with-open [conn (nrepl/connect :host "hydra" :port 1667)]
  (let [client (nrepl/client conn 5000)
        session (:new-session (first (nrepl/message client {:op "clone"})))]
    (doseq [msg (nrepl/message client {:op "eval" :code "(+ 1 2)" :session session})]
      (when (:value msg) (println (:value msg))))))

1.11. Method Comparison

Method Sync Structured Startup Best For
tmux send-keys No No 0 Human-in-loop
bb → JVM nREPL Yes Yes 10ms Agent automation
JVM → JVM nREPL Yes Yes 2400ms Editor/CIDER
bb –nrepl-server Yes Yes 10ms Babashka-only projects

1.12. Skill Reference

Full skill with references: gist:nrepl-bencode-experience