
Issue Troubleshooting
- 19 installs
- Updated August 2, 2026
- anian0/pick-skills
issue-troubleshooting is a Claude skill that runs a systematic five-phase debugging process to find a bug's root cause and implement or plan the fix.
About
This skill provides a systematic five-phase process for finding the root cause of a bug and fixing it rather than patching symptoms. It gates fix proposals behind confirmed root-cause investigation, assesses whether a fix is small or large, and for large fixes hands off to implementation-planning to build a structured repair plan. A developer uses it whenever hitting a bug, test failure, unexpected behavior, performance issue or build failure.
- Five-phase debugging workflow: root-cause investigation, pattern analysis, hypothesis verification, fix-scope assessment
- Enforces an iron rule of no fix proposals before root cause is confirmed, and stops to question architecture after 3+ fa
- For large fixes it hands off to implementation-planning to create a structured [BUGFIX] plan with a regression-verificat
Issue Troubleshooting by the numbers
- 19 all-time installs (skills.sh)
- Ranked #399 of 596 Debugging skills by installs in the Skillselion catalog
- Data as of Aug 3, 2026 (Skillselion catalog sync)
issue-troubleshooting capabilities & compatibility
free; runs locally, no API keys
- Capabilities
- debugging · root cause analysis · test failure diagnosis · fix planning
- Use cases
- debugging · testing
- Runs
- Runs locally
- Pricing
- Free
What issue-troubleshooting says it does
**核心原则:先找到根因,再动手修复。治症不治本就是失败。**
没有根因调查,就不允许提修复方案
npx skills add https://github.com/anian0/pick-skills --skill issue-troubleshootingAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 19 |
|---|---|
| Last updated | August 2, 2026 |
| Repository | anian0/pick-skills ↗ |
What it does
Find and fix a bug's root cause via a five-phase process, escalating large fixes to a structured repair plan.
Who is it for?
Disciplined root-cause debugging of bugs, test failures and build/performance issues, escalating big fixes to a structured plan.
Skip if: Greenfield feature planning or quick symptom patches that skip root-cause analysis.
When should I use this skill?
The user hits a bug, test failure, exception, performance problem or build failure, even when they only say 'this errored'.
What you get
A confirmed root cause, a fix targeting it, and a troubleshooting report - with large fixes routed through a structured plan.
- A confirmed root cause and targeted fix
- A troubleshooting report saved under workplace/1.X/troubleshooting/
By the numbers
- 5-phase process
- small-vs-large fix threshold of 3 files / 4 hours
- stop-and-question after 3+ failed fixes
Files
问题排查与修复
概述
随机猜测式修 bug 浪费时间,还会引入新问题。快速补丁掩盖根因,迟早复发。
核心原则:先找到根因,再动手修复。治症不治本就是失败。
本 skill 在经典调试流程基础上,新增修复规模评估环节——当修复工作量较大时, 联动 `implementation-planning` skill 创建结构化修复计划,避免盲目修改导致混乱。
版本号约定
1.X 为占位符,详见 requirements-workshop/SKILL.md。真实路径必须替换为具体数字(如 workplace/1/tech-design/)。
铁律
没有根因调查,就不允许提修复方案未完成 Phase 1 之前,不能提出任何修复建议。
适用场景
适用于任何技术问题:测试失败、生产 bug、异常行为、性能问题、构建失败、集成问题。
以下场景尤其需要严格遵循流程:问题看似简单、有时间压力、已多次修复未果、上次修复未生效、未完全理解问题时。
五阶段流程
每个阶段必须完成后才能进入下一阶段。
Phase 1:根因调查
在提出任何修复之前:
1. 仔细阅读错误信息
- 不要跳过任何错误或警告
- 错误信息往往包含精确线索
- 完整阅读堆栈跟踪
- 记录行号、文件路径、错误码
2. 稳定复现
- 能可靠触发吗?
- 精确的复现步骤是什么?
- 每次都出现吗?
- 无法复现 → 收集更多数据,不要猜
3. 检查近期变更
- 什么变更可能导致了这个问题?
- Git diff、近期提交
- 新依赖、配置变更
- 环境差异
4. 多组件系统收集证据
当系统有多个组件(CI → 构建 → 签名,API → 服务 → 数据库):
在提出修复之前,添加诊断埋点:
对每个组件边界:
- 记录进入组件的数据
- 记录离开组件的数据
- 验证环境/配置传播
- 检查每层状态
运行一次收集证据,定位故障出现在哪一层
再针对该组件深入调查5. 追踪数据流
当错误在调用栈深处:
参见 references/root-cause-tracing.md 的完整回溯追踪技术。
简版:
- 错误值从哪来?
- 谁传入了错误值?
- 持续向上追溯直到源头
- 在源头修复,不在表象处修复
不确定是哪个测试造成污染时: 使用 scripts/find-polluter.sh 二分定位:
./scripts/find-polluter.sh '.git' 'src/**/*.test.ts'⚠️ 特殊情形:测试通过但主流程不对
如果你发现"测试全部通过,但手动跑主流程明显不正常",这是降级逻辑的典型信号,在继续 Phase 2 之前先做降级排查:
搜索代码中的降级信号:
- catch 后返回假成功(try { ... } catch { return { success: true } })
- 功能缩水:声称实现A,实际执行B(如"上传文件"只存了文件名)
- 条件跳过:if (error || !config) return defaultValue
- Mock泄漏:硬编码返回值残留在生产路径
- 静默忽略:日志打印错误但继续往下走找到降级逻辑后,先移除它,再重新从 Phase 1 步骤 1 开始——因为降级逻辑一旦移除,真实错误才会暴露出来,之前的错误信息和复现步骤很可能会变化。
Phase 2:模式分析
修复前先找模式:
1. 找正常工作的参照
- 在同一代码库找到类似的正常工作代码
- 什么能正常工作?跟出问题的有什么相似?
2. 对比参考实现
- 如果是在实现某个模式,完整阅读参考实现
- 不要略读——逐行阅读
- 完全理解模式后再应用
3. 识别差异
- 正常代码和异常代码之间有什么不同?
- 列出每个差异,不管多小
- 不要假定"这不重要"
4. 理解依赖
- 需要哪些其他组件?
- 需要什么设置、配置、环境?
- 有哪些隐含假设?
Phase 3:假设验证
科学方法:
1. 提出单一假设
- 明确陈述:"我认为 X 是根因,因为 Y"
- 写下来
- 要具体,不要含糊
2. 最小化测试
- 做最小改动来验证假设
- 一次只改一个变量
- 不要同时修多个东西
3. 验证后再继续
- 验证通过?→ 进入 Phase 4 规模评估
- 没通过?→ 提出新假设
- 不要在失败的假设上叠加新修改
4. 不懂就说不懂
- 说"我不理解 X"
- 不要装懂
- 寻求帮助
- 继续研究
5. 根因未确认前,禁止提出修复方案
- 如果你还在"怀疑可能是X"阶段,不允许说"我来修一下X试试"
- 必须完成假设验证(步骤3通过)才能进入 Phase 4
- 提前提修复方案 = 在没理解问题时动手 = 必然掩盖根因或引入新问题
Phase 4:修复规模评估
在进入实施之前,评估修复的规模和复杂度。这是本 skill 区别于普通调试流程的关键步骤。
规模评估标准
对已确认的根因,评估以下维度:
| 维度 | 小修 | 大修 |
|---|---|---|
| 涉及文件数 | ≤ 3 个文件 | > 3 个文件 |
| 涉及模块 | 单一模块 | 跨模块/跨层 |
| 预估工时 | < 4 小时 | ≥ 4 小时 |
| 变更性质 | 逻辑修正/参数调整 | 架构调整/接口变更/新增组件 |
| 测试影响 | 修改少量测试 | 需要新增测试套件/重构测试结构 |
| 前后端 | 仅一端 | 前后端都需改动 |
决策规则
小修(满足以下全部条件)→ 直接进入 Phase 5:
- 修改 ≤ 3 个文件
- 不跨模块
- 预估 < 4 小时
- 不涉及接口变更
大修(满足以下任一条件)→ 联动 implementation-planning:
- 修改 > 3 个文件
- 跨模块或跨层
- 预估 ≥ 4 小时
- 涉及接口变更、架构调整
- 前后端都需改动
- Phase 3 中已失败 3+ 次假设(说明问题可能在架构层面)
联动 implementation-planning 的流程
当判断为大修时:
1. 整理修复方案概要
- 根因说明(来自 Phase 1-3 的结论)
- 修复范围(涉及的模块/层/文件)
- 修复策略(来自 Phase 2 的模式分析)
- 修复约束(不能破坏的现有行为、兼容性要求)
2. 检查技术方案是否已存在(只读引用)
- 如果
workplace/1.X/tech-design/下有相关技术方案 → 引用其架构、数据模型、API 设计等章节作为修复依据 - 如果没有 → 不创建技术方案文档(tech-design HARD-GATE 要求必须有需求文档),将修复方案概要直接作为 implementation-planning 的输入
3. 调用 implementation-planning skill
- 将修复方案概要作为输入
- 生成的计划中每个模块标注
[BUGFIX]前缀,区分于正常需求开发 - 计划中增加"回归验证"模块作为最后一个模块
- 计划确认后,调用
plan-executionskill 执行
4. 修复计划模板补充
联动生成的计划中,每个模块详情需额外包含:
**关联根因**:[本模块修复的根因部分]
**风险点**:[本模块修改可能影响的现有行为]
**回退方案**:[如果修复引入新问题如何回退]计划末尾增加回归验证模块:
### M{N}: 回归验证
**目标**:确认修复未引入新问题
**层**:跨层
**前置依赖**:所有修复模块
**子步骤**:
1. 运行全量测试套件
2. 验证原 bug 场景已修复
3. 检查关联功能的回归
**验收标准**:
- 全量测试 PASS
- 原 bug 复现步骤不再触发
- 关联功能无回归5. 向用户说明
- 告知用户为什么判定为大修
- 展示修复方案概要
- 提交计划供用户确认后再执行
Phase 5:修复实施
根据 Phase 4 的决策,走两条路线:
路线 A:小修直接实施
1. 创建失败测试用例
- 最简复现
- 尽量自动化测试
- 无框架时用一次性脚本
- 修复前必须有测试
2. 实施单一修复
- 针对已确认的根因
- 一次改一处
- 不做"顺手优化"
- 不捆绑重构
涉及超时/等待场景时,参见 references/condition-based-waiting.md 用条件轮询替代任意超时。3. 验证修复
- 测试通过了?
- 没有破坏其他测试?
- 问题确实解决了?
修复验证通过后,参见 references/defense-in-depth.md 考虑在多个层添加防御,防止同类问题复现。4. 修复未生效
- 停下来
- 计数:已经试了几次修复?
- < 3 次 → 回到 Phase 1,用新信息重新分析
- ≥ 3 次 → 进入架构质疑(步骤 5)
- 不要在 3 次失败后继续试第 4 次
5. 3+ 次修复失败:质疑架构
架构问题的信号:
- 每次修复都在不同位置暴露新的共享状态/耦合
- 修复需要"大规模重构"才能实施
- 每次修好在 A 处,B 处又出问题
停下来质疑根本问题:
- 这个模式本身是否合理?
- 是不是在靠惯性坚持?
- 应该重构架构还是继续修补?
在尝试更多修复前,与用户讨论。
这不是假设失败——这是架构选错了。
→ 此时应联动 implementation-planning,将架构重构作为正式计划推进。
路线 B:大修按计划执行
修复计划由 implementation-planning 生成后,调用 plan-execution skill 按模块顺序逐个实施,最后一个模块为回归验证。
产出排查报告
修复完成后(无论 Route A 还是 Route B),将排查过程与结论记录到 workplace/1.X/troubleshooting/:
命名:YYYY-MM-DD-{问题关键词}-排查报告.md
# {问题关键词} 排查报告
## 问题描述
- 现象:[错误信息/异常行为]
- 复现步骤:[步骤]
- 影响范围:[受影响功能/用户]
## 根因分析
- 根因:[一句话]
- 证据链:[Phase 1-3 收集的关键证据]
- 排除的假设:[已验证不成立的其他假设]
## 修复内容
- 修复方式:[一句话]
- 修改文件:[列表]
- 新增测试:[测试文件路径 + 覆盖场景]
## 验证结果
- 测试执行:[命令 + 输出]
- 回归检查:[确认无破坏其他功能]
## 经验教训(可选)
- [根因类型、预防措施、对架构的启示]填写说明:
- Route A:完整填写所有章节
- Route B:修复内容写"参见修复计划 {plan文件路径}",验证结果在 plan-execution 完成后补充
自检:
- 问题描述是否包含可复现步骤?
- 根因是否追溯到源头(不是中间表象)?
- 修复是否针对根因(不是"试试看")?
- 测试是否覆盖根因场景?
---
红灯信号——停下来遵循流程
当你发现自己在想以下任何一条,立即回到 Phase 1:
| 红灯思维 | 为什么必须停下 |
|---|---|
| "先快速修一下,回头再查" | 第一次修复定基调,从头就做对 |
| "改一下 X 试试看" | 没根因就动手 = 瞎猜 |
| "把几个改动一起提交" | 无法隔离哪个有效,还会引入新 bug |
| "跳过测试,我手动验证" | 没测试的修复靠不住,测试先证明问题存在 |
| "大概是 X 的问题,我先改了" | 看到症状 ≠ 理解根因 |
| "不完全理解但这样可能行" | 一知半解必定出 bug |
| "参考文档太长,我按自己的方式来" | 完整阅读参考实现 |
| "问题很简单,不需要流程" | 简单问题也有根因,流程对简单 bug 更快 |
| "紧急,没时间走流程" | 系统排查比瞎猜快 |
| "再试一次修复"(已失败 2+ 次) | 3+ 次失败 = 架构问题,质疑模式不要继续试(见 Phase 5 路线 A 步骤 5) |
| 每次修复都在不同位置暴露新问题 | 架构选错了,联动 implementation-planning 推进重构 |
| "大修就直接动手改吧" | 大修没有计划必乱,用 implementation-planning 管住范围 |
| "测试全通过但功能不对,先继续修" | 测试通过≠功能正确,先排查是否有降级逻辑让错误变隐形(见 Phase 1 步骤 4) |
| "加个catch处理一下这个错误" | catch掩盖错误就是降级逻辑,必须先搞清楚为什么会有这个错误 |
| "这里加个fallback值保证不崩" | fallback是在掩盖根因,问题还在,只是看不见了 |
来自用户的纠偏信号
注意这些提示:
- "是这样吗?" → 你假设了但没验证
- "能不能看到……?" → 应该先加证据收集
- "别猜了" → 你在没有理解的情况下提出修复
- "好好想想" → 质疑根本问题,不是表象
- "是不是卡住了?" → 你的方法不奏效
看到这些信号时:停下来,回到 Phase 1。
快速参考
| 阶段 | 关键活动 | 成功标准 |
|---|---|---|
| 1. 根因调查 | 读错误、复现、查变更、收证据 | 理解"是什么"和"为什么" |
| 2. 模式分析 | 找参照、对比差异 | 识别关键差异 |
| 3. 假设验证 | 提出假设、最小测试 | 假设确认或提出新假设 |
| 4. 规模评估 | 评估修复范围和复杂度 | 判定小修直接修 / 大修走计划 |
| 5. 修复实施 | 小修:测试→修复→验证→产出报告 / 大修:按计划执行→产出报告 | bug 解决,排查报告已归档 |
当流程揭示"找不到根因"
如果系统调查后发现问题是环境、时序或外部因素导致的:
1. 你已经完成了流程 2. 记录调查了什么 3. 实现适当的处理(重试、超时、错误提示) 4. 添加监控/日志供未来调查
但注意: 95% 的"找不到根因"其实是调查不够深入。
辅助技术
以下技术是本 skill 的一部分,参考文档位于 references/,配套脚本位于 scripts/:
- `references/root-cause-tracing.md` - 沿调用栈回溯追踪 bug 到原始触发点(配套脚本
scripts/find-polluter.sh) - `references/defense-in-depth.md` - 找到根因后在多个层添加验证
- `references/condition-based-waiting.md` - 用条件轮询替代任意超时(配套实现见
scripts/condition-based-waiting-example.ts)
关联 skill
| Skill | 关系 |
|---|---|
| implementation-planning | 修复规模大时(>3 文件/跨模块/≥4h),创建结构化修复计划 |
| tech-design | 大修时只读引用已有技术方案(架构/数据模型/API 等章节),不得创建新的技术方案文档 |
| plan-execution | 大修修复计划创建后,按模块顺序执行开发与验证 |
Condition-Based Waiting
Overview
Flaky tests often guess at timing with arbitrary delays. This creates race conditions where tests pass on fast machines but fail under load or in CI.
Core principle: Wait for the actual condition you care about, not a guess about how long it takes.
When to Use
digraph when_to_use {
"Test uses setTimeout/sleep?" [shape=diamond];
"Testing timing behavior?" [shape=diamond];
"Document WHY timeout needed" [shape=box];
"Use condition-based waiting" [shape=box];
"Test uses setTimeout/sleep?" -> "Testing timing behavior?" [label="yes"];
"Testing timing behavior?" -> "Document WHY timeout needed" [label="yes"];
"Testing timing behavior?" -> "Use condition-based waiting" [label="no"];
}Use when:
- Tests have arbitrary delays (
setTimeout,sleep,time.sleep()) - Tests are flaky (pass sometimes, fail under load)
- Tests timeout when run in parallel
- Waiting for async operations to complete
Don't use when:
- Testing actual timing behavior (debounce, throttle intervals)
- Always document WHY if using arbitrary timeout
Core Pattern
// ❌ BEFORE: Guessing at timing
await new Promise(r => setTimeout(r, 50));
const result = getResult();
expect(result).toBeDefined();
// ✅ AFTER: Waiting for condition
await waitFor(() => getResult() !== undefined);
const result = getResult();
expect(result).toBeDefined();Quick Patterns
| Scenario | Pattern |
|---|---|
| Wait for event | waitFor(() => events.find(e => e.type === 'DONE')) |
| Wait for state | waitFor(() => machine.state === 'ready') |
| Wait for count | waitFor(() => items.length >= 5) |
| Wait for file | waitFor(() => fs.existsSync(path)) |
| Complex condition | waitFor(() => obj.ready && obj.value > 10) |
Implementation
Generic polling function:
async function waitFor<T>(
condition: () => T | undefined | null | false,
description: string,
timeoutMs = 5000
): Promise<T> {
const startTime = Date.now();
while (true) {
const result = condition();
if (result) return result;
if (Date.now() - startTime > timeoutMs) {
throw new Error(`Timeout waiting for ${description} after ${timeoutMs}ms`);
}
await new Promise(r => setTimeout(r, 10)); // Poll every 10ms
}
}See condition-based-waiting-example.ts in this directory for complete implementation with domain-specific helpers (waitForEvent, waitForEventCount, waitForEventMatch) from actual debugging session.
Common Mistakes
❌ Polling too fast: setTimeout(check, 1) - wastes CPU ✅ Fix: Poll every 10ms
❌ No timeout: Loop forever if condition never met ✅ Fix: Always include timeout with clear error
❌ Stale data: Cache state before loop ✅ Fix: Call getter inside loop for fresh data
When Arbitrary Timeout IS Correct
// Tool ticks every 100ms - need 2 ticks to verify partial output
await waitForEvent(manager, 'TOOL_STARTED'); // First: wait for condition
await new Promise(r => setTimeout(r, 200)); // Then: wait for timed behavior
// 200ms = 2 ticks at 100ms intervals - documented and justifiedRequirements: 1. First wait for triggering condition 2. Based on known timing (not guessing) 3. Comment explaining WHY
Real-World Impact
From debugging session (2025-10-03):
- Fixed 15 flaky tests across 3 files
- Pass rate: 60% → 100%
- Execution time: 40% faster
- No more race conditions
Defense-in-Depth Validation
Overview
When you fix a bug caused by invalid data, adding validation at one place feels sufficient. But that single check can be bypassed by different code paths, refactoring, or mocks.
Core principle: Validate at EVERY layer data passes through. Make the bug structurally impossible.
Why Multiple Layers
Single validation: "We fixed the bug" Multiple layers: "We made the bug impossible"
Different layers catch different cases:
- Entry validation catches most bugs
- Business logic catches edge cases
- Environment guards prevent context-specific dangers
- Debug logging helps when other layers fail
The Four Layers
Layer 1: Entry Point Validation
Purpose: Reject obviously invalid input at API boundary
function createProject(name: string, workingDirectory: string) {
if (!workingDirectory || workingDirectory.trim() === '') {
throw new Error('workingDirectory cannot be empty');
}
if (!existsSync(workingDirectory)) {
throw new Error(`workingDirectory does not exist: ${workingDirectory}`);
}
if (!statSync(workingDirectory).isDirectory()) {
throw new Error(`workingDirectory is not a directory: ${workingDirectory}`);
}
// ... proceed
}Layer 2: Business Logic Validation
Purpose: Ensure data makes sense for this operation
function initializeWorkspace(projectDir: string, sessionId: string) {
if (!projectDir) {
throw new Error('projectDir required for workspace initialization');
}
// ... proceed
}Layer 3: Environment Guards
Purpose: Prevent dangerous operations in specific contexts
async function gitInit(directory: string) {
// In tests, refuse git init outside temp directories
if (process.env.NODE_ENV === 'test') {
const normalized = normalize(resolve(directory));
const tmpDir = normalize(resolve(tmpdir()));
if (!normalized.startsWith(tmpDir)) {
throw new Error(
`Refusing git init outside temp dir during tests: ${directory}`
);
}
}
// ... proceed
}Layer 4: Debug Instrumentation
Purpose: Capture context for forensics
async function gitInit(directory: string) {
const stack = new Error().stack;
logger.debug('About to git init', {
directory,
cwd: process.cwd(),
stack,
});
// ... proceed
}Applying the Pattern
When you find a bug:
1. Trace the data flow - Where does bad value originate? Where used? 2. Map all checkpoints - List every point data passes through 3. Add validation at each layer - Entry, business, environment, debug 4. Test each layer - Try to bypass layer 1, verify layer 2 catches it
Example from Session
Bug: Empty projectDir caused git init in source code
Data flow: 1. Test setup → empty string 2. Project.create(name, '') 3. WorkspaceManager.createWorkspace('') 4. git init runs in process.cwd()
Four layers added:
- Layer 1:
Project.create()validates not empty/exists/writable - Layer 2:
WorkspaceManagervalidates projectDir not empty - Layer 3:
WorktreeManagerrefuses git init outside tmpdir in tests - Layer 4: Stack trace logging before git init
Result: All 1847 tests passed, bug impossible to reproduce
Key Insight
All four layers were necessary. During testing, each layer caught bugs the others missed:
- Different code paths bypassed entry validation
- Mocks bypassed business logic checks
- Edge cases on different platforms needed environment guards
- Debug logging identified structural misuse
Don't stop at one validation point. Add checks at every layer.
Root Cause Tracing
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.
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
The Tracing Process
1. Observe the Symptom
Error: git init failed in /Users/jesse/project/packages/core2. 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
cwdresolves toprocess.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?)
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:
./find-polluter.sh '.git' 'src/**/*.test.ts'Runs tests one-by-one, stops at first polluter. See script for usage.
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
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.
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
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
// Complete implementation of condition-based waiting utilities
// From: Lace test infrastructure improvements (2025-10-03)
// Context: Fixed 15 flaky tests by replacing arbitrary timeouts
import type { ThreadManager } from '~/threads/thread-manager';
import type { LaceEvent, LaceEventType } from '~/threads/types';
/**
* Wait for a specific event type to appear in thread
*
* @param threadManager - The thread manager to query
* @param threadId - Thread to check for events
* @param eventType - Type of event to wait for
* @param timeoutMs - Maximum time to wait (default 5000ms)
* @returns Promise resolving to the first matching event
*
* Example:
* await waitForEvent(threadManager, agentThreadId, 'TOOL_RESULT');
*/
export function waitForEvent(
threadManager: ThreadManager,
threadId: string,
eventType: LaceEventType,
timeoutMs = 5000
): Promise<LaceEvent> {
return new Promise((resolve, reject) => {
const startTime = Date.now();
const check = () => {
const events = threadManager.getEvents(threadId);
const event = events.find((e) => e.type === eventType);
if (event) {
resolve(event);
} else if (Date.now() - startTime > timeoutMs) {
reject(new Error(`Timeout waiting for ${eventType} event after ${timeoutMs}ms`));
} else {
setTimeout(check, 10); // Poll every 10ms for efficiency
}
};
check();
});
}
/**
* Wait for a specific number of events of a given type
*
* @param threadManager - The thread manager to query
* @param threadId - Thread to check for events
* @param eventType - Type of event to wait for
* @param count - Number of events to wait for
* @param timeoutMs - Maximum time to wait (default 5000ms)
* @returns Promise resolving to all matching events once count is reached
*
* Example:
* // Wait for 2 AGENT_MESSAGE events (initial response + continuation)
* await waitForEventCount(threadManager, agentThreadId, 'AGENT_MESSAGE', 2);
*/
export function waitForEventCount(
threadManager: ThreadManager,
threadId: string,
eventType: LaceEventType,
count: number,
timeoutMs = 5000
): Promise<LaceEvent[]> {
return new Promise((resolve, reject) => {
const startTime = Date.now();
const check = () => {
const events = threadManager.getEvents(threadId);
const matchingEvents = events.filter((e) => e.type === eventType);
if (matchingEvents.length >= count) {
resolve(matchingEvents);
} else if (Date.now() - startTime > timeoutMs) {
reject(
new Error(
`Timeout waiting for ${count} ${eventType} events after ${timeoutMs}ms (got ${matchingEvents.length})`
)
);
} else {
setTimeout(check, 10);
}
};
check();
});
}
/**
* Wait for an event matching a custom predicate
* Useful when you need to check event data, not just type
*
* @param threadManager - The thread manager to query
* @param threadId - Thread to check for events
* @param predicate - Function that returns true when event matches
* @param description - Human-readable description for error messages
* @param timeoutMs - Maximum time to wait (default 5000ms)
* @returns Promise resolving to the first matching event
*
* Example:
* // Wait for TOOL_RESULT with specific ID
* await waitForEventMatch(
* threadManager,
* agentThreadId,
* (e) => e.type === 'TOOL_RESULT' && e.data.id === 'call_123',
* 'TOOL_RESULT with id=call_123'
* );
*/
export function waitForEventMatch(
threadManager: ThreadManager,
threadId: string,
predicate: (event: LaceEvent) => boolean,
description: string,
timeoutMs = 5000
): Promise<LaceEvent> {
return new Promise((resolve, reject) => {
const startTime = Date.now();
const check = () => {
const events = threadManager.getEvents(threadId);
const event = events.find(predicate);
if (event) {
resolve(event);
} else if (Date.now() - startTime > timeoutMs) {
reject(new Error(`Timeout waiting for ${description} after ${timeoutMs}ms`));
} else {
setTimeout(check, 10);
}
};
check();
});
}
// Usage example from actual debugging session:
//
// BEFORE (flaky):
// ---------------
// const messagePromise = agent.sendMessage('Execute tools');
// await new Promise(r => setTimeout(r, 300)); // Hope tools start in 300ms
// agent.abort();
// await messagePromise;
// await new Promise(r => setTimeout(r, 50)); // Hope results arrive in 50ms
// expect(toolResults.length).toBe(2); // Fails randomly
//
// AFTER (reliable):
// ----------------
// const messagePromise = agent.sendMessage('Execute tools');
// await waitForEventCount(threadManager, threadId, 'TOOL_CALL', 2); // Wait for tools to start
// agent.abort();
// await messagePromise;
// await waitForEventCount(threadManager, threadId, 'TOOL_RESULT', 2); // Wait for results
// expect(toolResults.length).toBe(2); // Always succeeds
//
// Result: 60% pass rate → 100%, 40% faster execution
#!/usr/bin/env 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'
set -e
if [ $# -ne 2 ]; then
echo "Usage: $0 <file_to_check> <test_pattern>"
echo "Example: $0 '.git' 'src/**/*.test.ts'"
exit 1
fi
POLLUTION_CHECK="$1"
TEST_PATTERN="$2"
echo "🔍 Searching for test that creates: $POLLUTION_CHECK"
echo "Test pattern: $TEST_PATTERN"
echo ""
# Get list of test files
TEST_FILES=$(find . -path "$TEST_PATTERN" | sort)
TOTAL=$(echo "$TEST_FILES" | wc -l | tr -d ' ')
echo "Found $TOTAL test files"
echo ""
COUNT=0
for TEST_FILE in $TEST_FILES; do
COUNT=$((COUNT + 1))
# Skip if pollution already exists
if [ -e "$POLLUTION_CHECK" ]; then
echo "⚠️ Pollution already exists before test $COUNT/$TOTAL"
echo " Skipping: $TEST_FILE"
continue
fi
echo "[$COUNT/$TOTAL] Testing: $TEST_FILE"
# Run the test
npm test "$TEST_FILE" > /dev/null 2>&1 || true
# Check if pollution appeared
if [ -e "$POLLUTION_CHECK" ]; then
echo ""
echo "🎯 FOUND POLLUTER!"
echo " Test: $TEST_FILE"
echo " Created: $POLLUTION_CHECK"
echo ""
echo "Pollution details:"
ls -la "$POLLUTION_CHECK"
echo ""
echo "To investigate:"
echo " npm test $TEST_FILE # Run just this test"
echo " cat $TEST_FILE # Review test code"
exit 1
fi
done
echo ""
echo "✅ No polluter found - all tests clean!"
exit 0
Related skills
FAQ
When does it hand off to implementation-planning?
On large fixes: more than 3 files, cross-module or cross-layer, 4+ hours estimated, interface/architecture changes, both front and back end, or 3+ failed hypotheses.
What are the five phases?
Root-cause investigation, pattern analysis, hypothesis verification, fix-scope assessment, and fix implementation.