{"id":50934884,"url":"https://github.com/day8/re-frame-pair","last_synced_at":"2026-06-17T08:01:58.022Z","repository":{"id":352446721,"uuid":"1215172607","full_name":"day8/re-frame-pair","owner":"day8","description":"A Skill for pair-programing with Claude Code on a re-frame app.","archived":false,"fork":false,"pushed_at":"2026-05-05T10:39:09.000Z","size":939,"stargazers_count":12,"open_issues_count":1,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2026-05-05T11:14:59.548Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"","language":"Clojure","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/day8.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":"AGENTS.md","dco":null,"cla":null}},"created_at":"2026-04-19T15:19:56.000Z","updated_at":"2026-05-05T10:39:11.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/day8/re-frame-pair","commit_stats":null,"previous_names":["day8/re-frame-pair"],"tags_count":6,"template":false,"template_full_name":null,"purl":"pkg:github/day8/re-frame-pair","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/day8%2Fre-frame-pair","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/day8%2Fre-frame-pair/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/day8%2Fre-frame-pair/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/day8%2Fre-frame-pair/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/day8","download_url":"https://codeload.github.com/day8/re-frame-pair/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/day8%2Fre-frame-pair/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34439296,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-17T02:00:05.408Z","response_time":127,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2026-06-17T08:01:57.911Z","updated_at":"2026-06-17T08:01:58.003Z","avatar_url":"https://github.com/day8.png","language":"Clojure","funding_links":[],"categories":[],"sub_categories":[],"readme":"# re-frame-pair\n\nLets an AI (Claude, Codex?) debug and develop against your **running re-frame application**. \n\nClaude can inspect `app-db`, dispatch events, trace the 6 dominoes, hot-swap handlers, time-travel, and map visible UI back to source code — all live against the runtime, with no source edits required for probes.\n\nA coding agent working with just static code is working with a limited perspective. It can read handlers, subscriptions, and views, but it has to guess what happened in the browser at runtime. With re-frame-pair, Claude can ask the running app: which event fired, what changed in `app-db`, which effects fired, which subscriptions re-ran or cache-hit, which views rendered, and where the relevant source lives.\n\nThe intended loop is empirical:\n\n1. Observe the current runtime state.\n2. Inspect the relevant epoch.\n3. Form a hypothesis.\n4. Undo state, probe with a new dispatch, read app-db, hot-swap handlers, side-effect stub, or REPL eval.\n5. Compare the new epoch with the baseline.\n6. Only then edit source.\n\nREPL changes are **ephemeral** (try-and-discard); source edits **stick**.\n\nPost-mortems are a core use case. Instead of asking the developer to describe every click and state transition that led to a bug, Claude can inspect the recent epoch trail and identify the event that introduced the bad value, including its parent dispatch, effects, `app-db` diff, subscriptions, renders, and source call sites.\n\n## Examples\n\n\u003e **You**: What changes did the last event make to `app-db` under `:shopping-cart`?\n\u003e\n\u003e **Claude**: 2 items, subtotal 47.50, no coupon.\n\u003e `{:items [{:sku \"BK-42\" :qty 2} {:sku \"TS-9\" :qty 1}] :total 47.50 :coupon nil}`\n\n---\n\n\u003e **You**: I dispatched `[:profile/save ...]` but the name in the header isn't changing.\n\u003e\n\u003e **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?\n\n---\n\n\u003e **You**: I just clicked the \"Save\" button and nothing happened. Where is that button and why doesn't it work?\n\u003e\n\u003e **Claude**: `src/app/profile/view.cljs:84`. It's a `re-com/button` with `: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?\n\n---\n\n\u003e **You**: My `[:cart/apply-coupon]` handler fired but I have three buttons that could have triggered it. Which one?\n\u003e\n\u003e **Claude**: That dispatch came from `app/cart/header.cljs:42` — the rightmost \"Apply\" button in the header, not either of the panel buttons.\n\n---\n\n\u003e **You**: What happens when I dispatch `[:cart/apply-coupon \"SPRING25\"]`?\n\u003e\n\u003e **Claude**: Interceptors: `inject-cofx :now`, `path :shopping-cart`, handler. Effects: `{:db ...}` only — no HTTP, no further dispatches. app-db diff: `:coupon` `nil` → `\"SPRING25\"`, `:total` `47.50` → `35.62`. Subs re-ran: `[:cart/total]`, `[:cart/coupon-status]`; `[:cart/items]` cache-hit. One render: the total badge at `cart_view.cljs:23`. Pure handler, nothing outside the cart subtree touched.\n\n---\n\n\u003e **You**: Tell me about every `:checkout/*` event as I click through.\n\u003e\n\u003e **Claude**: *(watches in the background; narrates each matching event as it fires, one short paragraph per match, with effect cascade — idle timeout after 30s of silence, so you don't need to remember to stop it)*\n\n---\n\n\u003e **You**: Make `:cart/apply-coupon` reject expired coupons instead of silently accepting them.\n\u003e\n\u003e **Claude**: Proposed change:\n\u003e ```clojure\n\u003e (rf/reg-event-db :cart/apply-coupon\n\u003e   (fn [db [_ code]]\n\u003e     (if (contains? (:expired-coupons db) code)\n\u003e       (assoc db :cart/coupon-error (str code \" is expired\"))\n\u003e       (assoc-in db [:shopping-cart :coupon] code))))\n\u003e ```\n\u003e Registered. Try clicking apply — I'll watch the next epoch.\n\n---\n\n\u003e **You**: Are there any items in the cart with `:qty \u003c= 0`?\n\u003e\n\u003e **Claude**: *[at the REPL]* `(-\u003e\u003e @re-frame.db/app-db :shopping-cart :items (filter #(\u003c= (:qty %) 0)))` — returns `()`. None right now, but the invariant isn't enforced by the `:cart/add-item` handler. Want me to propose a guard?\n\n---\n\n\u003e **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?\n\u003e\n\u003e **Claude**: *[walks back through the last ~20 epochs]* The event `[:cart/apply-coupon \"EXPIRED\"]` four epochs ago turned `:session-state` from `:authenticated` to `: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?\n\n---\n\n\u003e **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.\n\u003e\n\u003e **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-db` now updates as expected. Want me to put the patch into the source code?\n\n---\n\n\u003e **You**: Use my running app to test for edge cases on `[:cart/apply-coupon ...]`. Inject synthetic events with weird inputs, look at the results, and undo between each experiment.\n\u003e\n\u003e **Claude**: Captured the current epoch as the rewind point, then walked four inputs. `\"\"` — handler accepted it, flipped `:status` to `:applied`. *Undo.* `\"  \"` — same; whitespace not stripped. *Undo.* `nil` — handler threw inside the validation branch on `(seq nil)`. *Undo.* 200-char string — accepted, no length cap. *Undo.* Three input-validation bugs surfaced in under a minute; want me to draft a guard interceptor that rejects empty/whitespace/nil/oversized codes before the handler runs?\n\n---\n\n\u003e **You**: I want to iterate on `[:user/login {...}]` but it fires `:http-xhrio` and I don't want to hit the real auth endpoint each time.\n\u003e\n\u003e **Claude**: Substituting `:http-xhrio` and `:navigate` with record-only stubs for this dispatch. The handler still receives its coeffects, the cascade still runs, but the captured effect values land in `stubbed-effects-since` instead of being executed. Each iteration's stubs expire when the cascade settles — no global state to restore.\n\n---\n\n\u003e **You**: Transfer the patch to the source code.\n\u003e\n\u003e **Claude**: That change has been made to the source code.\n\n## Requirements\n\n### Install\n\n| Dep | Version | Notes |\n|---|---|---|\n| [shadow-cljs](https://shadow-cljs.github.io/) | 2.20+ | required; nREPL enabled on the dev build |\n| [babashka](https://babashka.org) | 1.0+ | required; the skill's shell shims use it — see [babashka install](https://github.com/babashka/babashka#installation) |\n\n### Dependencies\n\n| Dep | Version | Notes |\n|---|---|---|\n| [re-frame](https://github.com/day8/re-frame) | 1.4.7  | required  (works on 1.4+) |\n| [re-frame-10x](https://github.com/day8/re-frame-10x) | 1.12.3  | (works on 1.10+) required; dev preload, with `re-frame.trace.trace-enabled?` set true via `:closure-defines` |\n| [re-com](https://github.com/day8/re-com) | 2.29.3 | (works on 2.20+) **optional** — required only for the DOM ↔ source bridge. Debug instrumentation must be on AND call sites must pass `:src (at)`; without both, `dom/*` ops degrade gracefully (return `nil`) |\n| [day8.re-frame/tracing](https://github.com/day8/re-frame-debux) + tracing-stubs | 0.9.2 | **optional** — adds per-form trace; see *Optional: per-form trace via re-frame-debux* below |\n\nYou don't need to make any changes to your code/project to use it.\n\n### Try it out\n\n\nStarting fresh? [`re-frame-template`](https://github.com/day8/re-frame-template) scaffolds a project that satisfies this stack out of the box: `lein new re-frame your-app +10x +re-com` produces an app re-frame-pair can attach to without further changes.\n\nTo pin a known-good combination, drop these into your `shadow-cljs.edn` (or `deps.edn` / `project.clj`):\n\n```clojure\n[re-frame                       \"1.4.7\"]   ; the core\n[day8.re-frame/re-frame-10x     \"1.12.3\"]  ; debug panel — dev preload\n[re-com                         \"2.29.3\"]  ; UI components (optional)\n```\n\n### Try it out with re-frame-debux\n\nAdd these two dependencies:\n\n```clojure\n[day8.re-frame/tracing          \"0.9.2\"]   ; debux integration\n[day8.re-frame/tracing-stubs    \"0.9.2\"]   ; production stubs for above\n```\n\nMake sure your `:release` build aliases `day8.re-frame.tracing` to the stubs so per-form trace machinery doesn't ship into production. Wire it into `shadow-cljs.edn` like this:\n\n```clojure\n{:builds\n {:app\n  {:devtools {:preloads [day8.re-frame-10x.preload]}    ; 10x in dev\n   :dev     {:compiler-options\n              {:closure-defines\n               {re-frame.trace.trace-enabled?     true\n                day8.re-frame.tracing.trace-enabled? true}}}\n   :release {:build-options\n              {:ns-aliases\n               {day8.re-frame.tracing day8.re-frame.tracing-stubs}}}}}}\n```\nThe REPL-driven recipes live in [`docs/skill/debux.md`](docs/skill/debux.md).\n\n\n## Two modes\n\nA normal coding agent can only modify source files. re-frame-pair gives Claude a second mode: **ephemeral changes via the REPL**. Claude can hot-swap an event handler, redefine a subscription, swap an effect handler, or `reset!` `app-db` directly — the change takes effect immediately in the running app, with no source edit, no recompile, no commit.\n\nThat makes probing and iteration cheap: try a fix, dispatch the event, watch the resulting epoch, throw it away and try something else. When a REPL-only patch turns out to be the right shape, transfer it to source.\n\nREPL changes survive hot-reloads of unaffected namespaces, but are lost on full page reload. Source edits stick.\n\n## Install\n\nre-frame-pair is **not yet published to npm**. To install, clone this repo and follow [`docs/LOCAL_DEV.md`](docs/LOCAL_DEV.md) — it covers the three install paths (global symlink, copy, project-local), prerequisites, the dev loop for iterating on the skill itself, and troubleshooting.\n\n\u003e ⚠️ **Pull regularly.** This skill is iterating quickly on feedback and experience — `git pull` your clone every week or two so Claude has the latest vocabulary, recipes, and runtime helpers. The recipes Claude reaches for change as the SKILL learns; you don't want yesterday's playbook against today's runtime.\n\nre-frame-pair adds nothing to the host project beyond what 10x and re-com already require. On first connect it injects helpers into your app over the REPL — no extra deps, no extra preloads, no extra closure-defines attributable to re-frame-pair.\n\nOnce published to npm (planned `@day8/re-frame-pair`), install will be:\n\n```bash\nnpx skills add day8/re-frame-pair        # Agent Skill — portable across Claude clients\n/plugin install re-frame-pair@day8        # or as a Claude Code Plugin\n```\n\n## How to invoke it\n\n**Implicit** — once installed, the skill auto-matches when you talk about the running re-frame app. Ask in natural language:\n\n\u003e What's in `app-db` under `:shopping-cart`?\n\u003e\n\u003e Why didn't the header update after `[:profile/save ...]`?\n\u003e\n\u003e Fire the delete button on the first row of the table.\n\nClaude connects on first use of the session and stays connected until you exit.\n\n**Explicit** — `/re-frame-pair` slash command, or name it in a prompt:\n\n\u003e Using re-frame-pair, trace `[:cart/apply-coupon \"SPRING25\"]` and show me the cascade.\n\nUseful when you want to force the tool, or when the question doesn't obviously lean on the running app.\n\n## How it works\n\nOn first use the skill runs `discover-app.sh`:\n\n1. Locates the running shadow-cljs nREPL (`target/shadow-cljs/nrepl.port`, falling back to `.shadow-cljs/nrepl.port` or the `SHADOW_CLJS_NREPL_PORT` env var).\n2. Verifies a browser runtime is attached, that re-frame-10x and re-com are loaded, and that `re-frame.trace/trace-enabled?` is true.\n3. Injects the `re-frame-pair.runtime` namespace (helpers + epoch-buffer readers) into your app over nREPL.\n4. Returns a startup payload including health, app-db keys/shape, and a compact tail of recent events so the agent can orient itself before asking for more detail.\n\nAll subsequent ops reuse the connection. On a full page refresh the skill detects its session sentinel is gone and re-injects automatically.\n\nLive-watch ops hold a long-running eval open and poll the epoch buffer at animation-frame cadence. Hot-reload confirmation is probe-based: after a source edit, `tail-build.sh` polls a short CLJS form that flips when the new code lands in the browser. The script is named `tail-build.sh` for historical reasons — it does not actually tail the shadow-cljs server log.\n\nEpoch reads come from re-frame's own `register-epoch-cb` callback when available — once a `:event` trace completes, re-frame delivers an assembled epoch record (sub-runs, renders, effects, `app-db` before/after) that the skill drains into a native ring buffer. On older re-frame builds, the skill falls back to reading 10x's epoch buffer via the public `day8.re-frame-10x.public` ns, with the legacy inlined-rf walk as a third fallback for 10x JARs predating the public surface. Render entries tagged with `:re-com?` (and a layout/input/content category where possible) let Claude apply component-aware diagnostics.\n\n\n## How this differs from re-frame-10x alone\n\nre-frame-10x is the developer-facing devtool — a UI panel that shows epochs, app-db, the subscription cache. **A human reads the panel.**\n\nre-frame-pair gives that same epoch stream to **Claude** as programmatic data, plus the agency to dispatch events, hot-swap handlers, time-travel, and probe via the REPL. The skill *consumes* re-frame-10x's buffer (and re-frame's native epoch callback when present); 10x is the source of truth, re-frame-pair is the agent's interface to it.\n\nYou can run both at once. They don't conflict — 10x renders its panel, re-frame-pair reads the same epoch records.\n\n## Status\n\n**Beta.** Validated end-to-end against a live fixture - see test/fixture (re-frame + re-frame-10x + re-com under `shadow-cljs watch`). \n\nSee [`STATUS.md`](STATUS.md).\n\n## License\n\nMIT.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fday8%2Fre-frame-pair","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fday8%2Fre-frame-pair","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fday8%2Fre-frame-pair/lists"}