Shared REPL as Gossip Protocol: Multi-Machine Agentic Development

Table of Contents

1. Overview

A persistent Clojure nREPL server, bound to 0.0.0.0 on a LAN host, becomes a shared evaluation context for multiple participants: human operators (via Emacs/CIDER), AI agents (via tmux send-keys into nREPL; the earlier emacsclient -e path is superseded, see 11), and monitoring systems (via health checks). The REPL holds loaded namespaces, bound vars, and cached data structures in memory. Participants read the same state without message passing.

This is not a distributed system in the Kleppmann sense. There is one JVM, one heap, one evaluation context. The "distribution" is in the access pattern: multiple machines reach the same REPL over the network. The durable state lives in git. The REPL is the ephemeral materialized view.

2. The gossip analogy

In gossip protocols, nodes converge on shared state by exchanging observations. Each node maintains a local view and periodically reconciles with peers. There is no central coordinator.

The nREPL model inverts this: one shared heap, multiple observers. But the access pattern resembles gossip in three ways:

  1. Eventual consistency. When one participant loads a namespace, all subsequent participants see it. The convergence is immediate (shared heap) rather than eventual (network propagation), but the principle is the same: no participant needs to explicitly notify others.
  2. No coordinator. There is no queue, no broker, no orchestrator deciding who evaluates what. Any participant can require a namespace, bind a var, or query data at any time. Conflicts are resolved by Clojure's concurrency primitives (atoms, refs) or by convention (last writer wins on def).
  3. Crashing is safe. If a participant disconnects (laptop closes, agent session ends), the REPL state persists. The JVM holds the heap. Reconnecting picks up where the previous session left off. This is the "crashing is safe" property of gossip: partial failures don't corrupt shared state.

The analogy breaks down at durability: gossip protocols replicate state across nodes for fault tolerance. The nREPL model has a single point of failure (the JVM). Durability comes from git, not from replication.

3. Architecture

                    ┌─────────────────────────────────┐
                    │     JVM (nexus:42527)            │
                    │     Clojure 1.12 + nREPL         │
                    │                                  │
                    │  loaded: wal-sh.site.logs         │
                    │          wal-sh.tools.transform   │
                    │          wal-sh.verify.chain      │
                    │          wal-sh.site.beads        │
                    │                                  │
                    │  bound:  entries, issues, drawers │
                    │                                  │
                    │  data:   .beads/issues.jsonl      │
                    │          .verify/chain.jsonl      │
                    │          logs/wal.sh/https/       │
                    └───┬──────────┬──────────┬────────┘
                        │          │          │
              ┌─────────┘    ┌─────┘    ┌─────┘
              │              │          │
   ┌──────────▼──┐  ┌───────▼────┐  ┌──▼──────────────┐
   │ nexus tmux  │  │ laptop     │  │ nexus            │
   │ shared      │  │ CIDER      │  │ Claude Code      │
   │ (Emacs)     │  │ (Emacs)    │  │ (tmux send-keys) │
   │             │  │            │  │ (emacsclient -e) │
   │ human       │  │ human      │  │ agent            │
   │ reviews     │  │ explores   │  │ audits           │
   └─────────────┘  └────────────┘  └──────────────────┘
              │              │          │
              └──────────────┴──────────┘
                        │
                    git push/pull
                        │
              ┌─────────▼─────────┐
              │   github.com      │
              │   (durable log)   │
              └───────────────────┘

4. Participant roles

Role Machine Interface Reads Writes
Operator nexus (tmux) Emacs CIDER all code, data
Explorer laptop (LAN) Emacs CIDER all vars, requires
Agent nexus (Claude Code) tmux send-keys REPL output evals, files
Monitor hydra (monit) TCP health check port liveness none

All four roles share the same REPL heap. The operator and explorer are indistinguishable at the nREPL level: both are CIDER clients sending eval requests. The agent uses a lower-level interface (tmux into nREPL; emacsclient -e is superseded because queued calls wedge the Emacs daemon) but reaches the same nREPL. The monitor only checks liveness, not state.

5. Shared memory model

The REPL heap is the shared memory. It holds:

5.1. Loaded namespaces

(all-ns) returns every namespace that any participant has required. Once loaded, a namespace stays in the heap until the JVM restarts. This is the gossip convergence: participant A loads wal-sh.site.logs, participant B sees it immediately via (find-ns 'wal-sh.site.logs).

5.2. Bound vars

(def entries (logs/read-logs)) creates a var visible to all participants. The next participant can (count entries) without re-reading the log files. The data is already in the heap.

This is the cache coherence property: participants share a read cache without invalidation protocol. The cache is correct as long as the underlying files haven't changed. When files change (e.g. gmake logs syncs new access logs), a participant must re-evaluate (def entries (logs/read-logs)) to refresh the cache. Other participants see the stale value until they do the same or until the first participant re-defs the var.

5.3. Atoms and refs

For coordinated state, Clojure's concurrency primitives work across participants because they share the same JVM. An atom updated by the agent is immediately visible to the operator:

;; Agent creates shared state
(def !audit-results (atom []))

;; Agent appends a result
(swap! !audit-results conj {:file "2026-06-16.org" :verdict "correct"})

;; Operator reads it from their CIDER session
@!audit-results
;; => [{:file "2026-06-16.org" :verdict "correct"}]

6. Durable state: git as the replication log

The REPL heap is ephemeral. A JVM restart loses all loaded namespaces, bound vars, and cached data. Durability comes from git:

Ephemeral (REPL) Durable (git)
loaded namespaces src/**/*.clj
bound vars (entries, issues) logs/, .beads/, .verify/
atom state not persisted (by design)
eval history .beads/interactions.jsonl

The Kleppmann pattern: git is the replicated log, the REPL is the local materialized view. Each machine checks out the log (git pull), materializes a view (require, def), and operates on the view. Writes go back through the log (git commit, git push).

7. Failure modes

Failure Effect Recovery
JVM crash all REPL state lost scripts/dev-session-start.sh, re-require
Network partition laptop can't reach nREPL SSH tunnel fallback, or wait
Git conflict two participants edit same file git pull --rebase, manual merge
Stale var participant reads old data re-def from source (logs/read-logs)
nREPL port change .nrepl-port mismatch _external-connect.el reads port file

The single-JVM model means failures are total (crash loses everything) or absent (the heap is consistent by construction). There are no partial failure modes, no split-brain, no quorum. This is a feature: the distributed systems problems are pushed to git, where they are well-understood.

8. Monitoring (hydra)

Hydra monitors nexus nREPL via monit:

check host nexus-nrepl with address 192.168.86.100
  if failed port 42527 type tcp with timeout 5 seconds
    then alert

Future: eval-based health check that verifies namespace count and data freshness:

;; Health probe (speculative)
(let [nss (count (filter #(or (.startsWith (str %) "wal-sh")
                              (.startsWith (str %) "pocket-es"))
                         (map ns-name (all-ns))))]
  {:status (if (pos? nss) :ok :degraded)
   :namespaces nss
   :uptime-ms (- (System/currentTimeMillis) (.getStartTime (java.lang.management.ManagementFactory/getRuntimeMXBean)))})

9. Speculative extensions

9.1. Multi-REPL gossip

Multiple JVMs on different machines, each with their own nREPL. A gossip layer periodically syncs loaded namespaces and selected var values between REPLs. Not implemented; would require a coordination protocol that the single-JVM model avoids.

9.2. REPL-as-queue

Agents post work items to an atom. Other agents (or humans) dequeue and process them. The atom is the queue, the REPL is the broker. Fragile (no persistence, no ordering guarantees beyond atom semantics) but zero-infrastructure.

9.3. mDNS service discovery (aq integration)

aq (Ambient Agent Queue) already provides the gossip layer for multi-agent development at L1.5 in the seven-concern stack. The nREPL server is a natural aq broadcast target: an agent starting a REPL announces (aq announce :nrepl {:host "nexus" :port 42527 :project "www.wal.sh"}) and peers discover it without configuration.

The TRAMP principle from aq applies directly: (cider-connect) should work the same whether the nREPL is local, on the LAN, or tunneled through SSH. The _external-connect.el prototype already does this – nexus--nrepl-port reads the port file via TRAMP, nexus-connect dials the host. Adding mDNS discovery (_nrepl._tcp) removes the hardcoded host/port.

9.4. OpenTelemetry export

Push REPL eval metrics (namespace load count, eval latency, error rate) to the OTEL collector at 192.168.86.100:4317. The collector is already configured (see .envrc). The REPL becomes a telemetry source, not just a target.

10. The REPL is first class

The REPL is for asking questions you don't yet know to ask. Tests are where you put answers you've already found. The REPL comes first.

This means every namespace – including browser-side .core modules – should be explorable from the REPL. Even if global-pollution.core has no DOM to render into, calling (classify-pollution-type "ad-tag") interactively reveals edge cases the test suite hasn't imagined yet. The REPL is not a substitute for tests. Tests are a reification of what the REPL taught you.

This principle has consequences for the gossip model:

  1. All 56 namespaces stay in the completion list. Don't filter "browser-only" modules out of the REPL. If someone wants to poke at the pure logic, that's valid exploration.
  2. Zero-arg defaults are ergonomics, not convenience. If a function requires three args to call, it's hostile to exploration. The person at the REPL doesn't know the argument shape yet – that's why they're at the REPL.
  3. The gossip value of the shared REPL is discovery, not coordination. When the agent loads wal-sh.site.beads and binds (def issues (beads/issues)), the human reconnecting later sees those issues already in the heap. They didn't ask for them. The REPL presented a question they didn't know to ask: "what's the current issue state?" That's gossip.
  4. Cached atoms are shared observations. The defonce pattern (logs !entries, beads !issues, annotations !drawers) means each participant's exploration leaves a residue that the next participant inherits. The REPL accumulates knowledge across sessions. (reload!) is the explicit "I know the world changed, refresh my view."

11. Six access paths, one substrate

Any datum reachable from the REPL is reachable from six interfaces. The access path varies; the answer does not.

# Path Example Who uses it
1 CLI/grep grep 'test-sentinel' logs/wal.sh/https/access.log Scripts, cron, quick checks
2 CLI clj/bb clj -e '(require ...) (logs/pixel-events "js-error")' One-shot queries, CI
3 emacsclient -e (superseded) emacsclient -e '(cider-interactive-eval ...)' Formerly the Claude Code agent on nexus; banned from agent code, use path 4
4 tmux + nREPL stdin tmux send-keys -t clj-repl '(expr)' Enter Agent when CIDER unavailable
5 tmux + Emacs + CIDER C-x C-e in the shared pane, or laptop CIDER over LAN Human operator, explorer
6 Human + agent pairing Human evals in CIDER, agent reads tmux pane; or agent evals, human sees result Collaborative verification

Verified end-to-end with a sentinel test: one curl to production (/static/t.gif?e=js-error&msg=test-sentinel&token=Walsh-Research), then extracted via all six paths. Each returned the same record.

This structure is nearly universal. Any system with:

  • a persistent evaluation context (REPL, database connection, shell session)
  • a durable log (access log, git, append-only file)
  • multiple interfaces to the evaluation context (CLI, editor, agent, API)

has the same six-path property. The REPL is the substrate that unifies them. grep and clj and CIDER and tmux are different lenses on the same heap. The question "did the sentinel land?" has one answer regardless of which lens you use to ask it.

The practical consequence: never build a dashboard when a REPL query will do. The dashboard freezes one question. The REPL lets you ask the next question you haven't thought of yet.

12. Relationship to aq and efrit

12.1. aq (Ambient Agent Queue)

aq is the gossip layer at L1.5 between bd (beads, L1) and knowledge retrieval (L2). Three primitives: sb (where am I?), cprr (why am I here?), aq (who else knows?). The shared REPL model described here is what aq broadcasts look like at the evaluation layer: not "who else is editing this file" but "who else has this namespace loaded." The shipped counterpart is aq (Go, 2026-04), the filesystem-channel design this note positions itself against: agents there broadcast intent through filesystem-backed channels, where this note broadcasts through a shared heap.

The nREPL is a degenerate case of aq gossip: one node, all participants. Scaling to multiple JVMs (nexus + hydra each running their own nREPL) would require aq broadcasts to coordinate which REPL holds which data. Until then, the single-JVM model is simpler and sufficient.

12.2. efrit (Emacs AI assistant)

efrit is the Emacs-side harness for agent interaction: queue-based command dispatch, Claude API integration, structured eval. The _external-connect.el file is a minimal version of what efrit provides – auto-connect, namespace discovery, seeded scratch buffers. A full efrit integration would replace the manual nexus-connect with an efrit-repl-attach that discovers nREPLs via aq and connects CIDER automatically.

The shared REPL model gives efrit a new capability: an agent running on nexus (via Claude Code) and an agent running in Emacs (via efrit) share heap state without IPC. The REPL is the IPC.

13. Relationship to existing research

Note Connection
REPL-Driven Flight Tracking First use of the tmux+clj pattern for live data
REPL-Driven Feed Crawling tech-crawler warehouse queries via REPL
REPL-Driven Chat Mutation Session transcript analysis
Annotation Systems Property drawer verification via wal-sh.site.annotations
Editorial Workflow (TLA+), docs/editorial-workflow.tla Termination model for the daily-publish pipeline
pocket-es Query Surface Search index built and queried via REPL

14. References

Hickey, Rich. 2014. “Transducers.” Talk, Strange Loop 2014. https://www.youtube.com/watch?v=6mTbuzafcII.
Higginbotham, Daniel. 2015. Clojure for the Brave and True. No Starch Press. https://www.braveclojure.com/.
Kleppmann, Martin. 2017. Designing Data-Intensive Applications. O’Reilly Media.
Meadows, Donella H. 2008. Thinking in Systems: A Primer. Chelsea Green.
Miller, Alex, Stuart Halloway, and Aaron Bedra. 2018. Programming Clojure. 3rd ed. Pragmatic Bookshelf.

15. Provenance mapping: Claude Code tool calls to org-mode annotations

Point-in-time snapshot (2026-06-17). This mapping will evolve as the tooling matures.

Claude Code (chat JSONL) ProvenanceGuard (MCP trace) org-mode (property drawer) Verification ledger
:tool "Bash" source_id: tool_output::Bash :SOURCE: tool_output::Bash verifier: daily-publish-audit
:tool "Read" source_id: tool_output::Read :SOURCE: tool_output::Read subject: 2026-06-17-top
:tool "WebFetch" source_id: tool_output::WebFetch :SOURCE: tool_output::WebFetch verdict: correct
:result-content text: "..." heading body prose change-hash: sha256(...)
:ts (implicit) :VERIFIED_AT: 2026-06-17T10:15Z timestamp: ...
session JSONL file trace record .org file .verify/chain.jsonl block

The four representations of one fact:

  1. Chat transcript (.beads/interactions.jsonl, Claude Code JSONL): the tool call happened, with input and output.
  2. MCP trace (ProvenanceGuard schema, provenance.cljc): the tool output carries a source_id that links claim to source.
  3. Org heading (property drawer on the heading): the claim is annotated with :VERIFIED_AT:, :VERIFIED_BY:, :VERDICT:, :SOURCE:. Human-readable, grep-able, git-tracked.
  4. Verification ledger (.verify/chain.jsonl): append-only hash chain binding the change hash + verifier + verdict to the previous block. Tamper-evident.

These are not competing systems. They are four views of the same event at different abstraction levels. The REPL (wal-sh.site.provenance, wal-sh.site.chat, wal-sh.site.annotations, wal-sh.verify.chain) can query across all four and cross-reference them.

15.1. Connection to existing research

Research note What it contributes
Annotation Systems The property drawer convention, :CUSTOM_ID: anchoring, annotation survival across document reorganization
REPL-Driven Chat Mutation Claude Code JSONL as content-addressed conversation tree; the wal-sh.site.chat namespace for tool-call extraction
REPL-Driven Compliance The verification loop: crawl -> check -> annotate -> audit; same pipeline the daily-publish script implements
REPL-Driven Feed Crawling tech-crawler as provenance source for the morning brief; each feed item carries :source back to the RSS/Atom origin
Editorial Workflow (TLA+), docs/editorial-workflow.tla Termination proof for the audit pipeline; the audit-error verdict is the failure mode ProvenanceGuard calls "blocked"
pocket-es Query Surface The search index as a materialized view of all org files; provenance of search results traces back to the source .org

15.2. Open questions

  • Can we auto-generate the :SOURCE: drawer from Claude Code's tool call log? The chat JSONL has the :tool and :id for each call. Mapping :id to the org heading that resulted from it is the missing link.
  • ProvenanceGuard uses NLI (natural language inference) to verify claims against evidence. Our equivalent is the LLM-as-judge in daily-publish.sh. Should the audit produce per-claim verdicts (one drawer per claim) or per-heading verdicts (current approach)?
  • The verification ledger is append-only. ProvenanceGuard's traces are also append-only. Should they be the same file? The ledger is hash-chained; traces are not. Merging them would add provenance to the chain but lose the hash integrity unless each trace block also chains.