What problem does it solve?
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.
The Tracing Process
1. Observe the Symptom
Error: git init failed in /Users/jesse/project/packages/core
2. Find Immediate Cause
What code directly causes this?
await execFileAsync('git', ['init'], { cwd: projectDir });
3. Ask: What Called This?
WorktreeManager.createSessionWorktree(projectDir, sessionId)
→ called by Session.initializeWorkspace()
→ called by Session.create()
→ called by test at Project.create()
4. Keep Tracing Up
What value was passed?
projectDir = '' (empty string!)
- Empty string as
cwd resolves to process.cwd()
- That's the source code directory!
5. Find Original Trigger
Where did empty string come from?
const context = setupCoreTest(); // Returns { tempDir: '' }
Project.create('name', context.tempDir); // Accessed before beforeEach!
Adding Stack Traces
When you can't trace manually, add instrumentation:
// 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 });
}
Critical: Use console.error() in tests (not logger - may not show)
Run and capture:
npm test 2>&1 | grep 'DEBUG git init'
Analyze stack traces:
- Look for test file names
- Find the line number triggering the call
- Identify the pattern (same test? same parameter?)
Real Example: Empty projectDir
Symptom: .git created in packages/core/ (source code)
Trace chain:
git init runs in process.cwd() ← empty cwd parameter
- WorktreeManager called with empty projectDir
- Session.create() passed empty string
- Test accessed
context.tempDir before beforeEach
- setupCoreTest() returns
{ tempDir: '' } initially
Root cause: Top-level variable initialization accessing empty value
Fix: Made tempDir a getter that throws if accessed before beforeEach
Also added defense-in-depth:
- Layer 1: Project.create() validates directory
- Layer 2: WorkspaceManager validates not empty
- Layer 3: NODE_ENV guard refuses git init outside tmpdir
- Layer 4: Stack trace logging before git init
Key Principle
digraph 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];
"Add validation at each layer" [shape=box];
"Bug impossible" [shape=doublecircle];
"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"];
"Fix at source" -> "Add validation at each layer";
"Add validation at each layer" -> "Bug impossible";
}
File: .claude/skills/root-cause-tracing/find-polluter.sh
#!/bin/bash
# Bisection script to find which test creates unwanted files/state
# Usage: ./find-polluter.sh <file_or_dir_to_check> <test_pattern>
# Example: ./find-polluter.sh '.git' 'src/**/*.test.ts'
...