Skill Tracer
Skills › Superpowers › systematic-debugging › root-cause-tracing.md

root-cause-tracing.md

169 lines · skills/systematic-debugging/root-cause-tracing.md at 2ce8aa1

Open on GitHub

Most marks come from matching words in the file, so treat them as pointers.

1# Root Cause Tracing
2

Root Cause Tracing

Raw lines 1 to 2
3## Overview
4
5Bugs 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.
6
7**Core principle:** Trace backward through the call chain until you find the original trigger, then fix at the source.
8

Overview

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.

Raw lines 3 to 8
9## When to Use
11```dot
12digraph when_to_use {
13 "Bug appears deep in stack?" [shape=diamond];
14 "Can trace backwards?" [shape=diamond];
15 "Fix at symptom point" [shape=box];
16 "Trace to original trigger" [shape=box];
17 "BETTER: Also add defense-in-depth" [shape=box];
19 "Bug appears deep in stack?" -> "Can trace backwards?" [label="yes"];
20 "Can trace backwards?" -> "Trace to original trigger" [label="yes"];
21 "Can trace backwards?" -> "Fix at symptom point" [label="no - dead end"];
22 "Trace to original trigger" -> "BETTER: Also add defense-in-depth";
24```
26**Use when:**
27- Error happens deep in execution (not at entry point)
28- Stack trace shows long call chain
29- Unclear where invalid data originated
30- Need to find which test/code triggers the problem

When to Use

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];
    "BETTER: Also add defense-in-depth" [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"];
    "Trace to original trigger" -> "BETTER: Also add defense-in-depth";
}

Use when:

  • Error happens deep in execution (not at entry point)
  • Stack trace shows long call chain
  • Unclear where invalid data originated
  • Need to find which test/code triggers the problem
Raw lines 9 to 31
32## The Tracing Process

The Tracing Process

Raw lines 32 to 33
34### 1. Observe the Symptom
35```
36Error: git init failed in ~/project/packages/core
37```

1. Observe the Symptom

Error: git init failed in ~/project/packages/core
Raw lines 34 to 38
39### 2. Find Immediate Cause
40**What code directly causes this?**
41```typescript
42await execFileAsync('git', ['init'], { cwd: projectDir });
43```

2. Find Immediate Cause

What code directly causes this?

await execFileAsync('git', ['init'], { cwd: projectDir });
Raw lines 39 to 44
45### 3. Ask: What Called This?
46```typescript
47WorktreeManager.createSessionWorktree(projectDir, sessionId)
48 → called by Session.initializeWorkspace()
49 → called by Session.create()
50 → called by test at Project.create()
51```

3. Ask: What Called This?

WorktreeManager.createSessionWorktree(projectDir, sessionId)
  → called by Session.initializeWorkspace()
  → called by Session.create()
  → called by test at Project.create()
Raw lines 45 to 52
53### 4. Keep Tracing Up
54**What value was passed?**
55- `projectDir = ''` (empty string!)
56- Empty string as `cwd` resolves to `process.cwd()`
57- That's the source code directory!

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!
Raw lines 53 to 58
59### 5. Find Original Trigger
60**Where did empty string come from?**
61```typescript
62const context = setupCoreTest(); // Returns { tempDir: '' }
63Project.create('name', context.tempDir); // Accessed before beforeEach!
64```

5. Find Original Trigger

Where did empty string come from?

const context = setupCoreTest(); // Returns { tempDir: '' }
Project.create('name', context.tempDir); // Accessed before beforeEach!
Raw lines 59 to 65
66## Adding Stack Traces
68When you can't trace manually, add instrumentation:
70```typescript
71// Before the problematic operation
72async function gitInit(directory: string) {
73 const stack = new Error().stack;
74 console.error('DEBUG git init:', {
75 directory,
76 cwd: process.cwd(),
77 nodeEnv: process.env.NODE_ENV,
78 stack,
79 });
81 await execFileAsync('git', ['init'], { cwd: directory });
83```
85**Critical:** Use `console.error()` in tests (not logger - may not show)
87**Run and capture:**
88```bash
89npm test 2>&1 | grep 'DEBUG git init'
90```
92**Analyze stack traces:**
93- Look for test file names
94- Find the line number triggering the call
95- Identify the pattern (same test? same parameter?)

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?)
Raw lines 66 to 96
97## Finding Which Test Causes Pollution
99If something appears during tests but you don't know which test:
101Use the bisection script `find-polluter.sh` in this directory:
103```bash
104bash ./find-polluter.sh '.git' 'src/**/*.test.ts'
105```
107Runs tests one-by-one, stops at first polluter. See script for usage.

Finding Which Test Causes Pollution

If something appears during tests but you don't know which test:

Use the bisection script find-polluter.sh in this directory:

bash ./find-polluter.sh '.git' 'src/**/*.test.ts'

Runs tests one-by-one, stops at first polluter. See script for usage.

Raw lines 97 to 108
Runs code bash ./find-polluter.sh :104
109## Real Example: Empty projectDir
111**Symptom:** `.git` created in `packages/core/` (source code)
113**Trace chain:**
1141. `git init` runs in `process.cwd()` ← empty cwd parameter
1152. WorktreeManager called with empty projectDir
1163. Session.create() passed empty string
1174. Test accessed `context.tempDir` before beforeEach
1185. setupCoreTest() returns `{ tempDir: '' }` initially
120**Root cause:** Top-level variable initialization accessing empty value
122**Fix:** Made tempDir a getter that throws if accessed before beforeEach
124**Also added defense-in-depth:**
125- Layer 1: Project.create() validates directory
126- Layer 2: WorkspaceManager validates not empty
127- Layer 3: NODE_ENV guard refuses git init outside tmpdir
128- Layer 4: Stack trace logging before git init

Real Example: Empty projectDir

Symptom: .git created in packages/core/ (source code)

Trace chain:

  1. git init runs in process.cwd() ← empty cwd parameter
  2. WorktreeManager called with empty projectDir
  3. Session.create() passed empty string
  4. Test accessed context.tempDir before beforeEach
  5. 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
Raw lines 109 to 129
130## Key Principle
132```dot
133digraph principle {
134 "Found immediate cause" [shape=ellipse];
135 "Can trace one level up?" [shape=diamond];
136 "Trace backwards" [shape=box];
137 "Is this the source?" [shape=diamond];
138 "Fix at source" [shape=box];
139 "Add validation at each layer" [shape=box];
140 "Bug impossible" [shape=doublecircle];
141 "NEVER fix just the symptom" [shape=octagon, style=filled, fillcolor=red, fontcolor=white];
143 "Found immediate cause" -> "Can trace one level up?";
144 "Can trace one level up?" -> "Trace backwards" [label="yes"];
145 "Can trace one level up?" -> "NEVER fix just the symptom" [label="no"];
146 "Trace backwards" -> "Is this the source?";
147 "Is this the source?" -> "Trace backwards" [label="no - keeps going"];
148 "Is this the source?" -> "Fix at source" [label="yes"];
149 "Fix at source" -> "Add validation at each layer";
150 "Add validation at each layer" -> "Bug impossible";
152```
154**NEVER fix just where the error appears.** Trace back to find the original trigger.

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";
}

NEVER fix just where the error appears. Trace back to find the original trigger.

Raw lines 130 to 155
156## Stack Trace Tips
158**In tests:** Use `console.error()` not logger - logger may be suppressed
159**Before operation:** Log before the dangerous operation, not after it fails
160**Include context:** Directory, cwd, environment variables, timestamps
161**Capture stack:** `new Error().stack` shows complete call chain

Stack Trace Tips

In tests: Use console.error() not logger - logger may be suppressed Before operation: Log before the dangerous operation, not after it fails Include context: Directory, cwd, environment variables, timestamps Capture stack: new Error().stack shows complete call chain

Raw lines 156 to 162
163## Real-World Impact
165From debugging session (2025-10-03):
166- Found root cause through 5-level trace
167- Fixed at source (getter validation)
168- Added 4 layers of defense
169- 1847 tests passed, zero pollution

Real-World Impact

From debugging session (2025-10-03):

  • Found root cause through 5-level trace
  • Fixed at source (getter validation)
  • Added 4 layers of defense
  • 1847 tests passed, zero pollution
Raw lines 163 to 169