prompts/skills/root-cause-tracing/SKILL.md
Use when errors occur deep in execution and you need to trace back to find the original trigger - systematically traces bugs backward through call stack, adding instrumentation when needed, to identify source of invalid data or incorrect behavior
npx skillsauth add ramblurr/nix-devenv root-cause-tracingInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Bugs often manifest deep in the call stack (git init in wrong directory, file created in wrong location, database opened with wrong path). Your instinct is to fix where the error appears, but that's treating a symptom.
Core principle: Trace backward through the call chain until you find the original trigger, then fix at the source.
digraph when_to_use {
"Bug appears deep in stack?" [shape=diamond];
"Can trace backwards?" [shape=diamond];
"Fix at symptom point" [shape=box];
"Trace to original trigger" [shape=box];
"Bug appears deep in stack?" -> "Can trace backwards?" [label="yes"];
"Can trace backwards?" -> "Trace to original trigger" [label="yes"];
"Can trace backwards?" -> "Fix at symptom point" [label="no - dead end"];
}
Use when:
Error: git init failed in /Users/jesse/project/packages/core
What code directly causes this?
TypeScript:
await execFileAsync('git', ['init'], { cwd: projectDir });
Clojure:
(shell/sh "git" "init" :dir project-dir)
TypeScript:
WorktreeManager.createSessionWorktree(projectDir, sessionId)
→ called by Session.initializeWorkspace()
→ called by Session.create()
→ called by test at Project.create()
Clojure:
(worktree/create-session-worktree project-dir session-id)
→ called by (session/initialize-workspace)
→ called by (session/create)
→ called by test at (project/create)
What value was passed?
projectDir = '' / project-dir = nil (empty/nil!)process.cwd() / (System/getProperty "user.dir")Where did the bad value come from?
TypeScript:
const context = setupCoreTest(); // Returns { tempDir: '' }
Project.create('name', context.tempDir); // Accessed before beforeEach!
Clojure:
(def test-system (setup-test-system)) ; Returns {:project-dir nil}
(project/create "name" (:project-dir test-system)) ; Accessed before fixture!
When you can't trace manually, add instrumentation:
TypeScript:
// Before the problematic operation
async function gitInit(directory: string) {
const stack = new Error().stack;
console.error('DEBUG git init:', {
directory,
cwd: process.cwd(),
nodeEnv: process.env.NODE_ENV,
stack,
});
await execFileAsync('git', ['init'], { cwd: directory });
}
Clojure:
(require '[clojure.stacktrace :as st])
(defn git-init [directory]
;; Capture and print stack trace to stderr
(let [stack (with-out-str (st/print-stack-trace (Exception.)))]
(binding [*out* *err*]
(prn "DEBUG git init:"
{:directory directory
:cwd (System/getProperty "user.dir")
:stack stack})))
;; Simpler alternative: (Thread/dumpStack)
(shell/sh "git" "init" :dir directory))
Critical: Use console.error() (JS) or (binding [*out* *err*] ...) (Clojure) - logger may be suppressed
Run and capture:
# npm projects
npm test 2>&1 | grep 'DEBUG git init'
# Clojure with Kaocha
clj -M:test --focus my.ns/test-name 2>&1 | grep 'DEBUG'
# Babashka
bb test --focus com.example.the-ns/the-test-name
Analyze stack traces:
If something appears during tests but you don't know which test:
Use the bisection script: @find-polluter.sh
./find-polluter.sh '.git' 'src/**/*.test.ts'
Runs tests one-by-one, stops at first polluter. See script for usage.
Symptom: .git created in packages/core/ (source code)
Trace chain:
git init runs in process.cwd() ← empty cwd parametercontext.tempDir before beforeEach{ tempDir: '' } initiallyRoot cause: Top-level variable initialization accessing empty value
Fix: Made tempDir a getter that throws if accessed before beforeEach
Also added validation at multiple layers:
Symptom: Database file created in project root instead of temp directory
Trace chain:
(jdbc/execute! ds ...) fails with permission/path errordb/init-db called with nil path → defaults to current dircore/start-system passed nil from config map(:db-path test-system) before use-fixtures :each ransetup-test-system returns {:db-path nil} before fixture initializes itRoot cause: Accessing system map at top-level before fixture setup
Fix: Made db-path a delay that throws if accessed before fixture:
(defn setup-test-system []
(let [initialized? (atom false)
db-path (atom nil)]
{:db-path (delay
(when-not @initialized?
(throw (ex-info "Accessed db-path before fixture setup!" {})))
@db-path)
:initialize! (fn [path]
(reset! db-path path)
(reset! initialized? true))}))
Also added validation:
db/init-db validates path is non-nil and absolutedigraph principle {
"Found immediate cause" [shape=ellipse];
"Can trace one level up?" [shape=diamond];
"Trace backwards" [shape=box];
"Is this the source?" [shape=diamond];
"Fix at source" [shape=box];
"NEVER fix just the symptom" [shape=octagon, style=filled, fillcolor=red, fontcolor=white];
"Found immediate cause" -> "Can trace one level up?";
"Can trace one level up?" -> "Trace backwards" [label="yes"];
"Can trace one level up?" -> "NEVER fix just the symptom" [label="no"];
"Trace backwards" -> "Is this the source?";
"Is this the source?" -> "Trace backwards" [label="no - keeps going"];
"Is this the source?" -> "Fix at source" [label="yes"];
}
NEVER fix just where the error appears. Trace back to find the original trigger.
| Concern | TypeScript | Clojure |
|---------|------------|---------|
| Print to stderr | console.error() | (binding [*out* *err*] (println ...)) |
| Get stack trace | new Error().stack | (st/print-stack-trace (Exception.)) |
| Show cause chain | Manual traversal | (st/print-cause-trace ex) |
| JVM flag | N/A | -XX:-OmitStackTraceInFastThrow |
General tips:
Clojure compiles to JVM bytecode, so stack traces show Java class names. Learn to decode them:
Naming conventions:
my.namespace/my-fn → my.namespace$my_fn (hyphens become underscores)my.namespace/-main → my.namespace$_main (leading hyphen becomes underscore)Anonymous functions:
my.namespace$parent_fn$fn__123.invoke - anonymous fn inside parent-fnFilter the noise:
clojure.lang.*, clojure.core.*, clojure.main initially$ and read the function name)Find root cause:
JVM gotcha:
-XX:-OmitStackTraceInFastThrowFrom debugging session (2025-10-03):
testing
Use this OCP when executing or preparing to execute commands that change a live or important system, service reloads/restarts, package changes, deployments, migrations, firewall/network/access changes, credential rotation, NixOS switch/test/boot/deploy, or incident mitigation. It guides safe operations with a persisted ledger for scope, preflight, baseline, rollback, validation, and evidence.
development
Create new agent skills with proper structure, progressive disclosure, and bundled resources. Use when user wants to create, write, or build a new skill.
documentation
Naming conventions for workflow documents in prompts/. Use when creating plans, PRDs, research reports, idea capture or other workflow documents. Triggers on (1) creating new planning documents, (2) naming PRDs or research reports, (3) questions about document organization in prompts/.
testing
Grilling session that challenges your plan against the existing domain model, sharpens terminology, and updates documentation (CONTEXT.md, ADRs) inline as decisions crystallise. Use when user wants to stress-test a plan against their project's language and documented decisions.