A Skill which makes Claude Code a better pair programmer by allowing it to interact with your running re-frame application.
A coding agent working with just the static code is working with a limited perspective. This Skill makes Claude Code more capable by giving it read/write access to:
- the internal state of the application
- the dynamics of your running application
It can:
- use the REPL
- obtain functionality from
re-frame-10x, including trace execution for every epoch (the entire 6-domino cascade that occurs in response to an event)
With these capabilities, Claude Code can even iteratively perform experiments by patching parts of the system, undoing state, retrying events and seeing the results.
Pre-alpha — code is written, but not yet exercised against a running re-frame app. The repository now contains the SKILL.md, the scripts/runtime.cljs injection payload, a babashka-based ops dispatcher (scripts/ops.clj) behind thin shell shims, the plugin manifest, the npm package manifest, and a GitHub Actions workflow that publishes to npm on tag.
What's not done: the spike (see STATUS.md and §8a of the spec). Six concrete unknowns need to be verified against a minimal fixture app before calling this beyond pre-alpha — most importantly the 10x epoch-buffer accessor, the live-watch transport, and re-com's data-rc-src format.
Read STATUS.md for the per-phase implementation state; docs/initial-spec.md for the full design; docs/TESTING.md for the four-surface test plan; docs/LOCAL_DEV.md for running from a clone without waiting for an npm release; RELEASING.md for the release flow.
Designed for web apps built from the following stack — in pre-alpha, it has not yet been exercised end-to-end against a running app of any shape (see STATUS.md for what's verified vs. pending the spike):
- A re-frame application
re-frame-10xloaded as a dev-time preload, withre-frame.trace.trace-enabled?set totruevia:closure-defines(this is 10x's own requirement)re-com— and for the DOM ↔ source bridge specifically, re-com's debug instrumentation must be enabled in dev and call sites must pass:src (at). Without both, thedom/*ops degrade gracefully (returnnil) and Claude will say so.- shadow-cljs as the build tool, with nREPL enabled on the dev build
You don't need to make any changes to your code/project to use it, but you will need babashka installed because the skill's shell shims use it. See the babashka install guide.
Without this Skill, Claude Code writes edits to source files and shadow-cljs hot-reloads them into the running program.
re-frame-pair adds a second mode: Claude can also make ephemeral changes to application code via the REPL — hot-swap an event handler or a subscription, try it, discard if it didn't work.
The difference between the two modes is that REPL changes last until the next full page reload whereas source edits stick.
Here's the kinds of conversations you can have with Claude.
You: What changes did the last event make to
app-dbunder:shopping-cart?Claude: 2 items, subtotal 47.50, no coupon.
{:items [{:sku "BK-42" :qty 2} {:sku "TS-9" :qty 1}] :total 47.50 :coupon nil}
You: I dispatched
[:profile/save ...]but the name in the header isn't changing.Claude: The header reads
[:user/display-name], derived from[:user/profile]. Your handler wrote to[:user :pending-profile], not[:user :profile]. Layer 2 returned=, the Layer 3 short-circuited, the header didn't re-render. Want me to hot-swap the handler to write the intended path?
You: I just clicked the "Save" button and nothing happened. Where is that button and why doesn't it work?
Claude:
src/app/profile/view.cljs:84. It's are-com/buttonwith:on-click #(rf/dispatch [:profile/save]). The click did dispatch — trace shows{:db ...}returned — but no:profile/form-dirty?flip happened, so nothing visible changed. What feedback were you expecting: toast, spinner, redirect?
You: What happens when I dispatch
[:cart/apply-coupon "SPRING25"]?Claude: Interceptors:
inject-cofx :now,path :shopping-cart, handler. Effects:{:db ...}only — no HTTP, no further dispatches. app-db diff::couponnil→"SPRING25",:total47.50→35.62. Subs re-ran:[:cart/total],[:cart/coupon-status];[:cart/items]cache-hit. One render: the total badge atcart_view.cljs:23. Pure handler, nothing outside the cart subtree touched.
You: Tell me about every
:checkout/*event as I click through.Claude: (watches in the background; narrates each matching event as it fires, one short paragraph, with effect cascade) — idle timeout after 30s of silence, so you don't need to remember to stop it.
You: Make
:cart/apply-couponreject expired coupons instead of silently accepting them.Claude: Proposed change:
(rf/reg-event-db :cart/apply-coupon (fn [db [_ code]] (if (contains? (:expired-coupons db) code) (assoc db :cart/coupon-error (str code " is expired")) (assoc-in db [:shopping-cart :coupon] code))))Registered. Try clicking apply — I'll watch the next epoch.
You: Are there any items in the cart with
:qty <= 0?Claude: [at the REPL]
(->> @re-frame.db/app-db :shopping-cart :items (filter #(<= (:qty %) 0)))— returns(). None right now, but the invariant isn't enforced by the:cart/add-itemhandler. Want me to propose a guard?
You: I notice that sometime in the last few events the "Done" button got disabled. I don't remember which action did it. Can you figure out when that happened?
Claude: [walks back through the last ~20 epochs in re-frame-10x's buffer, looking for the transition] The event
[:cart/apply-coupon "EXPIRED"]four epochs ago turned:session-statefrom:authenticatedto:unauthenticated— that's what greyed the button. Looks like a bug in the coupon handler, not an intended side-effect. Want me to fix it?
You: When I first enter the Dashboard panel, the global reset button doesn't work. Fix it, then return to the previous state and fire the same event again, iterating until it works.
Claude: I stepped re-frame-10x's state back to the pre-click epoch (noting: any HTTP effects or navigation that fired earlier are not reversed — undo only rewinds
app-db). The panel-level subscription was wrong; I patched it. Re-ran the event,app-dbnow updates as expected. Want me to put the patch into the source code?
You: Transfer the patch to the source code.
Claude: That change has been made to the source code.
Not yet published. The first tagged release (
v0.1.0-alpha.1+) will appear on npm as@day8/re-frame-pair.
To run it before then — straight from a clone, with edits live — see docs/LOCAL_DEV.md. Symlink the repo into ~/.claude/skills/re-frame-pair/, install babashka, done.
re-frame-pair adds nothing to the host project beyond what 10x and re-com already require. On first connect, the skill injects its runtime helpers into your app over the REPL — no extra deps, no extra preloads, no extra closure-defines attributable to re-frame-pair.
Both distributions (Agent Skill and Claude Code Plugin) ship the same SKILL.md and scripts/* shims. Choose a scope.
As an Agent Skill (recommended; portable across Claude clients):
npx skills add day8/re-frame-pairAs a Claude Code Plugin:
/plugin install re-frame-pair@day8Lands in your user Claude config (~/.claude/). Best when you work on several re-frame apps, or you're the only Claude Code user on this project.
Install into the project's own .claude/skills/re-frame-pair/ directory and commit it. Teammates who clone the repo and open Claude Code there get the skill on first use, pinned to the committed version.
cd your-re-frame-project
npx skills add --scope project day8/re-frame-pair # planned syntax
git add .claude/skills/re-frame-pairFor the plugin variant, reference re-frame-pair@day8 in a checked-in .claude/plugin.json.
- Global if you're the only person using Claude Code here, or you hop between re-frame apps.
- Project-local if your team wants one pinned, shared version.
- Both is fine — the project-local install takes precedence when both are present.
On first use in a session:
- The skill locates your shadow-cljs nREPL port.
- It sends a handful of ClojureScript forms over nREPL to create a
re-frame-pair.runtimenamespace in your app, populated with helpers and readers for re-frame-10x's epoch buffer. - Live-watch ops (
watch/*) stream matching epochs back to the shell by holding a long-running eval open and polling 10x's buffer at animation-frame cadence. Hot-reload confirmation is probe-based: after an edit, the skill polls a short CLJS form that changes when the new code has landed in the browser (seedocs/initial-spec.md§4.5). The script is namedtail-build.shfor historical reasons — it does not actually tail the shadow-cljs server log.
On full page refresh, the skill detects that its session sentinel is gone and re-injects automatically.
Once the skill is installed, there are two ways to reach it from a Claude Code conversation.
The skill's description auto-matches when you talk about the running re-frame app. Ask in natural language:
What's in
app-dbunder:shopping-cart?Why didn't the header update after
[:profile/save ...]?Fire the delete button on the first row of the table.
Claude connects on first use of the session and stays connected until you exit.
/re-frame-pair
…or name it in a prompt:
Using re-frame-pair, trace
[:cart/apply-coupon "SPRING25"]and show me the cascade.
Useful when you want to force the tool, or when the phrasing of your question doesn't obviously lean on the running app.
The skill's first op in a session is discover-app.sh, which:
- Finds the running shadow-cljs nREPL (from
target/shadow-cljs/nrepl.port, falling back to.shadow-cljs/nrepl.portor theSHADOW_CLJS_NREPL_PORTenv var — the exact location depends on shadow-cljs version and config). - Verifies a browser runtime is attached to that build.
- Checks that
re-frame-10xandre-comare loaded, and thatre-frame.trace/trace-enabled?is true. - Reports
connectedor names the single failing check with a one-line fix suggestion.
Once verified, the skill injects its runtime namespace (re-frame-pair.runtime) into the app over nREPL. All subsequent ops reuse that connection.
The pieces (design; see Status above):
discover-app.shfinds the running shadow-cljs build and its nREPL port, switches the session into:cljsmode for that build, and verifies re-frame-10x, re-com, andtrace-enabled?are in place.eval-cljs.shsends short ClojureScript forms over nREPL into the browser runtime and returns edn.inject-runtime.shcreates there-frame-pair.runtimenamespace in the app on connect, populating it with helpers and epoch-buffer readers. The session sentinel (a UUID) is interned here so full-page-refresh detection is a simple lookup.SKILL.mdteaches Claude a verb vocabulary (read / write / trace / watch / hot-reload / undo) mapped onto those forms, plus diagnostic recipes composed from them.- All trace reads come from re-frame-10x's epoch buffer — no second trace callback, one source of truth. Render entries tagged with
:re-com?(and where possible a layout/input/content category) let Claude apply component-aware diagnostics.
See docs/initial-spec.md for the full operation catalogue, architecture, error surfaces, versioning, and phased delivery plan.
MIT