$npx -y skills add humorless/clj-native-agent --skill clj-debugUse when debugging Clojure or Babashka code, especially before adding log statements or println - redirects to REPL-based inline inspection instead
| 1 | ## Why REPL-based Debugging for Clojure |
| 2 | |
| 3 | Adding logs and re-running tests creates a slow feedback loop. Clojure's REPL lets you inspect values directly at the point of failure, test fixes instantly, and validate corrections before touching the source file. This is faster and safer than println or formal debuggers. |
| 4 | |
| 5 | ## Core Pattern: Stop Before Logging |
| 6 | |
| 7 | **RED FLAG:** You're about to add `println`, `tap>`, `(log ...)`, or modify source code to debug. |
| 8 | |
| 9 | **Instead:** Use inline def in nrepl. Evaluate modified code directly without changing files, capture values, inspect them, test your fix, then commit with confidence. |
| 10 | |
| 11 | ## The Inline Def Pattern: Capture and Inspect |
| 12 | |
| 13 | When a function fails, add a temporary `(def variable-name variable-name)` inside the function and evaluate the modified function in nrepl. This captures the value without changing the source file. |
| 14 | |
| 15 | ### Example: Discovering a Calling Convention Bug |
| 16 | |
| 17 | Your function fails: |
| 18 | |
| 19 | ```clojure |
| 20 | (defn foo [& [{:keys [a b c] :as m}]] |
| 21 | (+ a b c)) |
| 22 | |
| 23 | (foo :a 1 :b 2 :c 3) |
| 24 | ;; => java.lang.NullPointerException |
| 25 | ``` |
| 26 | |
| 27 | In nrepl, write the function with an inline def to capture the argument: |
| 28 | |
| 29 | ```clojure |
| 30 | ;; Evaluate this in nrepl (don't modify the source file): |
| 31 | (defn foo [& [{:keys [a b c] :as m}]] |
| 32 | (def m m) ;; TODO: remove before commit |
| 33 | (+ a b c)) |
| 34 | |
| 35 | ;; Now call it: |
| 36 | (foo :a 1 :b 2 :c 3) |
| 37 | ;; => java.lang.NullPointerException |
| 38 | |
| 39 | ;; Inspect what was captured: |
| 40 | m |
| 41 | ;; => :a |
| 42 | ;; Aha! m is :a, not a map. The caller is passing wrong format. |
| 43 | ``` |
| 44 | |
| 45 | Now you understand the bug: the function expects a map `{:a 1 :b 2 :c 3}`, not keyword arguments. |
| 46 | |
| 47 | ### Example: Testing a Fix in nrepl Before Committing |
| 48 | |
| 49 | Once you understand the problem, test the corrected version in nrepl: |
| 50 | |
| 51 | ```clojure |
| 52 | ;; Test 1: Fix the calling convention |
| 53 | (foo {:a 1 :b 2 :c 3}) |
| 54 | ;; => 6 ✓ Works! |
| 55 | |
| 56 | ;; Test 2: Or fix the function to accept varargs correctly |
| 57 | (defn foo [& [{:keys [a b c] :as m}]] |
| 58 | ;; No def here — we understand the bug now |
| 59 | (+ a b c)) |
| 60 | |
| 61 | (foo {:a 1 :b 2 :c 3}) |
| 62 | ;; => 6 ✓ Verified! |
| 63 | |
| 64 | ;; Or change the function signature: |
| 65 | (defn foo [& args] |
| 66 | (def args args) ;; Capture to verify varargs format |
| 67 | (let [{:keys [a b c]} (first args)] |
| 68 | (+ a b c))) |
| 69 | |
| 70 | (foo {:a 1 :b 2 :c 3}) |
| 71 | ;; args => ({:a 1, :b 2, :c 3}) ✓ Correct format now |
| 72 | ``` |
| 73 | |
| 74 | Once the fix works in nrepl, update the source file confidently. Remove any `(def ...)` lines before committing. |
| 75 | |
| 76 | ## Discovery and Fix Workflow |
| 77 | |
| 78 | This is the complete cycle — use `/brepl` to evaluate each step: |
| 79 | |
| 80 | ```dot |
| 81 | digraph debugging_workflow { |
| 82 | rankdir=TB; |
| 83 | node [shape=box, style=rounded]; |
| 84 | edge [fontsize=10]; |
| 85 | |
| 86 | "Test Fails or\nError Occurs" -> "Read Error\nMessage &\nStack Trace"; |
| 87 | |
| 88 | "Read Error\nMessage &\nStack Trace" -> "Identify Where\nto Inspect\n(Which function/\nwhich args?)"; |
| 89 | |
| 90 | "Identify Where\nto Inspect\n(Which function/\nwhich args?)" -> "Write Function\nwith inline def\nin nrepl\n(def var var)"; |
| 91 | |
| 92 | "Write Function\nwith inline def\nin nrepl\n(def var var)" -> "Evaluate Modified\nFunction in nrepl\n(defn foo ...)"; |
| 93 | |
| 94 | "Evaluate Modified\nFunction in nrepl\n(defn foo ...)" -> "Call Function\nwith Test Args\n(foo ...)"; |
| 95 | |
| 96 | "Call Function\nwith Test Args\n(foo ...)" -> "Inspect Captured\nValue at REPL\n(var / keys / type)"; |
| 97 | |
| 98 | "Inspect Captured\nValue at REPL\n(var / keys / type)" -> "Form Hypothesis\n(Why is this\nwrong?)"; |
| 99 | |
| 100 | "Form Hypothesis\n(Why is this\nwrong?)" -> "Test Corrected Code\nin nrepl\n(defn foo ...)"; |
| 101 | |
| 102 | "Test Corrected Code\nin nrepl\n(defn foo ...)" -> "Verify Fix Works\n(Call & verify\nresults)"; |
| 103 | |
| 104 | "Verify Fix Works\n(Call & verify\nresults)" -> "Update Source File\n(Remove inline def,\napply fix)"; |
| 105 | |
| 106 | "Update Source File\n(Remove inline def,\napply fix)" -> "Run Test Suite\n(Confirm fix\nsolves problem)"; |
| 107 | } |
| 108 | ``` |
| 109 | |
| 110 | ## Working with Integrant Components |
| 111 | |
| 112 | If the code uses Integrant components, understand this: **When you modify component code and evaluate it in nrepl, the running component may not pick up the change.** You'll see the old behavior even though the new code is loaded. |
| 113 | |
| 114 | ### When to Reset the Component |
| 115 | |
| 116 | Use this decision tree: |
| 117 | |
| 118 | ```dot |
| 119 | digraph integrant_decision { |
| 120 | rankdir=TB; |
| 121 | node [shape=diamond, style=rounded]; |
| 122 | |
| 123 | start [label="Modified code that\nthe component\nuses?", shape=diamond]; |
| 124 | no_reset [label="No reset needed\n(Only inspecting\nwith def or\ntesting pure\nfunctions)", shape=box, style=rounded]; |
| 125 | do_reset [label="Reset component\nto pick up\nchanges", shape=box, style=rounded]; |
| 126 | |
| 127 | start -> no_reset [label="No"]; |
| 128 | start -> do_reset [label="Yes"]; |
| 129 | } |
| 130 | ``` |
| 131 | |
| 132 | **How to reset:** |
| 133 | |
| 134 | ```clojure |
| 135 | ;; After evaluating your code change in nrepl: |
| 136 | (require '[your.component :as com |