Debug
Quick Ref: Systematic bug investigation: reproduce → isolate → hypothesize → verify → fix. Output:
.agents/research/YYYY-MM-DD-debug-*.md
YOU MUST EXECUTE THIS WORKFLOW. Do not just describe it.
Required output: Every finding MUST include Urgency, Risk, ROI, and Blast Radius ratings using the Issue Rating Table format. Do not omit these ratings.
Pre-flight: Git Safety Check
git status --short
If uncommitted changes exist:
AskUserQuestion with questions:
[
{
"question": "You have uncommitted changes. Commit before proceeding?",
"header": "Git",
"options": [
{"label": "Commit first (Recommended)", "description": "Save current work so you can revert if this skill modifies files"},
{"label": "Continue without committing", "description": "Proceed — I accept the risk"}
],
"multiSelect": false
}
]
If "Commit first": Ask for a commit message, stage changed files, and commit. Then proceed.
Step 1: Gather Bug Report
Use AskUserQuestion if the user hasn't provided enough detail:
AskUserQuestion with questions:
[
{
"question": "What type of issue are you seeing?",
"header": "Bug type",
"options": [
{"label": "Crash", "description": "App crashes, EXC_BAD_ACCESS, fatal error, etc."},
{"label": "Wrong behavior", "description": "App runs but does the wrong thing"},
{"label": "UI issue", "description": "Layout broken, animation wrong, view not updating"},
{"label": "Performance", "description": "Slow, laggy, high memory, battery drain"}
],
"multiSelect": false
}
]
Collect these details (from user message or by asking):
- Expected behavior — What should happen?
- Actual behavior — What happens instead?
- Steps to reproduce — How to trigger it
- Error messages — Console output, crash logs, compiler errors
- When it started — Recent change? Always broken?
Step 2: Reproduce
Goal: Confirm the bug exists and understand the trigger conditions.
2.1: Check Recent Changes
If the bug started recently, look at what changed:
# Recent commits touching Swift files
git log --oneline -10 -- '*.swift'
# Files changed in last N commits
git diff --name-only HEAD~5
# Full diff of recent changes
git diff HEAD~3 -- '*.swift'
2.2: Search for Error Messages
If the user provided an error message, find it in code:
# Search for the error string in source
Grep pattern="error message text" glob="**/*.swift"
# Search for fatalError / precondition calls
Grep pattern="fatalError|preconditionFailure|assertionFailure" glob="**/*.swift"
2.3: Document Reproduction
Record:
- Reproducible? Always / intermittent / only under specific conditions
- Minimum steps: Fewest actions to trigger the bug
- Environment factors: Simulator vs device, debug vs release, iOS version
Step 3: Isolate
Goal: Narrow down to the smallest code path that causes the bug.
3.1: Trace the Code Path
Start from the user-facing symptom and trace inward:
# Find the entry point (e.g., button tap, view appear)
Grep pattern="<symptom_function_or_action>" glob="**/*.swift"
# Read the view/controller involved
Read file_path="<path_to_affected_file>"
# Trace function calls — use LSP when available for accurate resolution
LSP operation="goToDefinition" filePath="<file>" line=<N> character=<N>
LSP operation="findReferences" filePath="<file>" line=<N> character=<N>
# Fallback: Read each file along the call chain manually
3.2: Check the Blast Radius
Understand what the affected code touches:
# Find all callers of the broken function
Grep pattern="\.<brokenFunctionName>\(" glob="**/*.swift"
# Check protocol conformances if relevant
Grep pattern=":\s*<AffectedProtocol>" glob="**/*.swift"
3.3: Rule Out Environmental Factors
# Check for build configuration differences
Grep pattern="#if DEBUG|#if targetEnvironment" glob="**/*.swift"
Step 4: Gather Evidence
Goal: Collect concrete data about what the code is actually doing.
4.1: Read the Code
Read every file in the suspected code path. Don't guess — read.
# Read the primary file
Read file_path="<path_to_suspected_file>"
# Read related files along the call chain
Read file_path="<path_to_related_file>"
4.2: Check Git History
# When was the broken code last modified?
git log --oneline -5 -- "path/to/file.swift"
# What exactly changed?
git log -p -1 -- "path/to/file.swift"
# Find when a specific pattern was introduced or removed
git log -p -S "suspiciousCode" -- '*.swift'
4.3: Search for Related Patterns
# Find similar patterns that might also be affected
Grep pattern="<same_pattern_as_bug>" glob="**/*.swift"
# Find TODO/FIXME comments near the affected code
Grep pattern="TODO|FIXME|HACK|WORKAROUND" path="<affected_directory>"
Step 5: Hypothesize
Goal: Form ranked hypotheses based on evidence.
List possible causes ranked by likelihood:
| # | Hypothesis | Likelihood | Evidence | How to Verify |
|---|---|---|---|---|
| 1 | [Most likely cause] | High | [What evidence supports this] | [Specific check to confirm/deny] |
| 2 | [Second possibility] | Medium | [Evidence] | [Check] |
| 3 | [Third possibility] | Low | [Evidence] | [Check] |
Common iOS Bug Patterns
State & Data:
- Optional unwrapped when nil (check for
as!and force unwraps) - Array index out of bounds (check for subscript access without bounds checking)
- State mutation on wrong thread (check for
@MainActormissing on UI updates) - Stale data after model change (check if view re-renders on data change)
Concurrency:
- Data race (multiple tasks writing same property without synchronization)
- Deadlock (two actors waiting on each other)
- Missing
await(forgetting to await an async call, getting old value) - Task cancelled but not checked (long operation ignoring
Task.isCancelled)
Memory:
- Retain cycle in closure (check for missing
[weak self]in escaping closures in classes) - Delegate not declared
weak(strong reference cycle) - Timer not invalidated (keeps firing after view dismissed)
- Observation leak (NotificationCenter observer not removed)
UI:
- View not updating (missing
@Published, wrong property wrapper) - Layout constraint conflict (ambiguous Auto Layout)
- Off-main-thread UI update (background queue modifying UI)
- Animation state stuck (completion handler not called)
Step 6: Verify Hypotheses
Goal: Test each hypothesis starting with the most likely.
For each hypothesis, design a specific check that will definitively confirm or deny it:
# Example: Hypothesis is "nil optional crash"
# Test: Search for force unwraps in the affected file
Grep pattern="as!" path="<affected_file>"
# Example: Hypothesis is "missing weak self"
# Test: Find closures in the affected class (only relevant for classes, not structs)
Grep pattern="\.sink|\.receive|completion.*=" path="<affected_file>"
# Example: Hypothesis is "race condition"
# Test: Find shared mutable state
Grep pattern="var\s+\w+.*=" path="<affected_file>"
Record results for each hypothesis:
Hypothesis 1: [description]
Test: [what I checked]
Result: CONFIRMED / DENIED / INCONCLUSIVE
Evidence: [what I found]
Continue until root cause is identified.
Step 7: Root Cause Analysis
Goal: Document the confirmed root cause clearly.
## Root Cause
**What:** [One-sentence description of the bug]
**Why:** [Underlying reason — why did this code get written this way?]
**Where:** [File:line where the bug lives]
**When introduced:** [Commit hash/date if identifiable]
Step 8: Fix
Goal: Implement the minimal correct fix.
8.1: Plan the Fix
Before editing, document:
| File | Change Required | Risk |
|---|---|---|
File.swift:45 | [what to change] | Low/Med/High |
Blast radius: How many files are affected? Regression risk: Low / Medium / High Test coverage: Do existing tests cover this? New tests needed?
8.2: Implement the Fix
Use Edit tool to make targeted changes. Keep the fix minimal — don't refactor unrelated code.
8.3: Check for Similar Bugs
After fixing, search for the same pattern elsewhere:
# If the bug was a force cast, find other force casts
# INTENTIONAL: as! after guard let/is check is already validated
Grep pattern="as!" glob="**/*.swift"
# If the bug was a missing weak self, find other closures in classes
# INTENTIONAL: SwiftUI struct views don't need [weak self]
Grep pattern="\.sink\s*\{[^}]*self\." glob="**/*ViewModel*.swift"
Grep pattern="\.sink\s*\{[^}]*self\." glob="**/*Manager*.swift"
Or use /scan-similar-bugs for a more thorough scan.
Step 9: Verify Fix
Goal: Confirm the fix works and doesn't break anything.
- Bug no longer reproduces with the original steps
- Build succeeds without warnings
- Existing tests pass
- New test added to prevent regression
- No new issues introduced in related functionality
Step 10: Generate Report
Display the investigation summary, root cause, and fix inline, then write report to .agents/research/YYYY-MM-DD-debug-{summary}.md:
# Bug Investigation Report
**Date:** YYYY-MM-DD
**Bug:** [one-line description]
**Urgency:** 🔴 Critical / 🟡 High / 🟢 Medium / ⚪ Low
**Status:** Fixed
## Symptoms
[What the user observed]
## Root Cause
**What:** [description]
**Where:** FileName.swift:line
**Why:** [underlying reason]
**Introduced:** [commit or date if known]
## Fix Applied
| File | Change | Lines |
|------|--------|-------|
| FileName.swift | [description] | 45-48 |
## Verification
- [x] Bug no longer reproduces
- [x] Build succeeds
- [x] Tests pass
- [ ] Regression test added
## Issue Rating Table
| # | Finding | Urgency | Risk: Fix | Risk: No Fix | ROI | Blast Radius | Fix Effort |
|---|---------|---------|-----------|-------------|-----|-------------|------------|
| 1 | [Primary bug] File.swift:45 — [description] | 🔴 Critical | ⚪ Low | 🔴 Critical | 🟠 Excellent | ⚪ 1 file | Trivial |
Use the Issue Rating scale:
- **Urgency:** 🔴 CRITICAL (crash/data loss) · 🟡 HIGH (incorrect behavior) · 🟢 MEDIUM (degraded UX) · ⚪ LOW (cosmetic/minor)
- **Risk: Fix:** Risk of the fix introducing regressions (⚪ Low for isolated changes, 🟡 High for shared code paths)
- **Risk: No Fix:** User-facing consequence if left unfixed
- **ROI:** 🟠 Excellent · 🟢 Good · 🟡 Marginal · 🔴 Poor
- **Blast Radius:** How many callers/files are exposed to this bug
- **Fix Effort:** Trivial / Small / Medium / Large
## Similar Patterns Found
[List any other instances of the same bug pattern, or "None found"]
Include each similar pattern as a row in the Issue Rating Table above.
## Prevention
[What could prevent this class of bug in the future]
Worked Example
User: "My app crashes when I tap an item in the list"
Step 1 — Gather: Crash on tap, EXC_BAD_ACCESS, started after last commit
Step 2 — Reproduce: git log shows ItemDetailView.swift changed yesterday
Step 3 — Isolate: Trace: List tap → NavigationLink → ItemDetailView.init → viewModel.loadItem()
Step 4 — Evidence: Read ItemDetailViewModel.swift, find `item.category!` on line 34
Step 5 — Hypothesize:
#1: Force unwrap of nil optional (HIGH) — category is Optional, forced
#2: Array index out of bounds (LOW) — no array access nearby
Step 6 — Verify: Check data — category is nil for items imported from CSV
Step 7 — Root cause: Force unwrap of item.category which is nil for CSV imports
Step 8 — Fix: Replace `item.category!` with `item.category ?? "Uncategorized"`
Step 9 — Verify: Build passes, tap works, added test for nil category
Step 10 — Report: Written to .agents/research/2026-02-24-debug-item-detail-crash.md
Troubleshooting
| Problem | Solution |
|---|---|
| Can't reproduce the bug | Ask for exact steps, device/simulator, iOS version, data state |
| Multiple hypotheses seem equally likely | Test the cheapest-to-verify one first |
| Fix breaks something else | Revert, widen the blast radius analysis, find the shared dependency |
| Bug is intermittent | Likely a race condition — focus on concurrency patterns |
| No error message, just wrong behavior | Add strategic print/breakpoint to narrow the code path |
| Bug only happens in release builds | Check #if DEBUG blocks, compiler optimizations, stripped symbols |