root-cause — read-only bug analysis
You are performing read-only analysis: no file edits, no branches, no
commands that mutate state. Output is a brief that a human or /fix acts on.
The iron rule: no fix without a root cause. A fix proposed before the cause
is understood is a guess, and guesses create new bugs. Symptom patches are
failure even when they make the error disappear.
Announce at start: "Using the root-cause skill — read-only analysis of this
bug."
Step 0: Load context
- The bug report — full description, repro steps, linked threads/issues.
.ai/lessons.md — known pitfalls in the affected area.
- Root
AGENTS.md Task Router → the matching .claude/rules/ guides.
- The affected module/package
AGENTS.md.
Step 1: Establish the facts
- Read the error completely — full stack trace, message, HTTP status, file
and line numbers. The error text often contains the answer.
- Reproduce or trace. State the exact repro steps. If you cannot reproduce
even mentally, gather more data — do not guess.
- Check recent changes:
git log --oneline -20 -- <affected paths> and the
branch diff. Most bugs live in what changed last.
- Trace the data flow from entry point to origin: route → loader/action →
service function → query → response. Note every transformation. Follow the
bad value backward to where it is first wrong — fix at the source, not
where the symptom surfaces (see
references/root-cause-tracing.md).
- Check the schema: read the newest relevant migrations (order by
timestamp) and generated types. Column names and constraints must match what
the code assumes.
Step 2: Carbon-specific failure modes
Check each of these before inventing exotic theories:
| Check |
What to look for |
| companyId scoping |
A query missing companyId filtering → cross-tenant leak or empty results |
| Stale generated types |
Code references columns from a new migration but pnpm run generate:types wasn't run — typecheck greens lie |
| RLS policy gaps |
New table/column without policies; check the table's migrations |
| Permission strings |
requirePermissions() / permissions.can() scopes are string literals — invisible to typecheck; grep the whole repo after any scope rename |
| Service signature drift |
Caller args don't match the service function's current signature |
| Migration ordering |
A migration backdated older than deployed ones applies out of order on remotes (see .ai/lessons.md) |
| Form submission |
ValidatedForm only submits on a native submit with a submitter; react-aria number/date fields commit hidden inputs on blur (see /test for details) |
| Import staleness |
Import from a moved path with no re-export bridge |
Step 3: One hypothesis at a time
- Form a single specific hypothesis: "X in file Y causes the bug because Z."
- Verify it against the code you can read. If the code contradicts it, discard
it — don't force-fit.
- Distinguish cause from symptom: a UI
TypeError may originate three layers
down. Keep tracing until you reach the origin.
- Three-strikes rule: if 3 hypotheses have failed, the problem is likely
architectural (shared state, coupling, a wrong pattern) — STOP, write up what
you ruled out, and surface the architectural question to the human instead of
producing hypothesis #4.
Step 4: Confidence
| Level |
Meaning |
| HIGH |
Cause identified with code evidence; fix path obvious |
| MEDIUM |
Strong code-supported hypothesis; runtime confirmation would help |
| LOW |
Multiple plausible causes or the bug appears runtime/environment-dependent |
If MEDIUM or LOW and the bug involves runtime state, ordering, caching,
concurrency, or manual reproduction → recommend /debugging-difficult-bugs
(temporary JSONL instrumentation) as the next step instead of guessing. Never
present a guess as a finding.
Step 5: Output the brief
Produce exactly this structure (~400 words max):
## Root-Cause Brief
**Bug:** <one line>
**Summary:** <2–3 sentences: symptom and observable impact>
**Root cause:** <why it happens, citing file:line>
**Confidence:** HIGH | MEDIUM | LOW
<if not HIGH: what is uncertain and what would resolve it — e.g. "instrument via /debugging-difficult-bugs">
**Files to change:**
- `path/to/file.ts` — <what and why>
**Approach:**
1. <step>
**Risks:**
- <e.g. "signature change affects 3 callers">
**BC impact:** <NONE | FROZEN/STABLE surfaces touched, per BACKWARD_COMPATIBILITY.md>
Guardrails
- Read-only. No edits, no
git writes, no migrations, no DB commands.
- No speculative fixes. "Try this and see" is not a finding.
- Stay scoped. Analyze the reported bug only; note unrelated discoveries in
one line at the end, don't chase them.
- Cite evidence. Every claim references a file, line, migration, or policy.
Red flags — thinking any of these means you're guessing, not analyzing; STOP:
- "it's probably X, let me suggest the fix" (no verified cause → no fix)
- "I'll propose two possible fixes and let them pick" (that's two guesses)
- "one more hypothesis" after three have failed (that's an architecture
question now — surface it)
- "the error message is misleading, ignore it" (read it again; it usually isn't)
References (read when the situation matches)
references/root-cause-tracing.md — tracing a bad value backward through the
call stack to its origin
references/defense-in-depth.md — layering validation after the cause is found
references/condition-based-waiting.md (+ condition-based-waiting-example.ts)
— replacing arbitrary timeouts with condition polling in flaky async tests
references/find-polluter.sh — bisecting which earlier test pollutes a
failing test
1---2name: root-cause3description: Read-only root-cause analysis for any bug, test failure, or unexpected behavior — before proposing or writing any fix. Produces a brief with the root cause, files to change, approach, and risks. No edits, no commits, no state-changing commands. Use before /fix or /conductor whenever the cause isn't already proven. If static reading can't reach a confident cause, it hands off to /debugging-difficult-bugs for runtime instrumentation.4---5<!-- Workflow pattern inspired by Open Mercato (MIT License)6 https://github.com/open-mercato/open-mercato7 Copyright (c) 2025-2026 Open Mercato contributors -->89# root-cause — read-only bug analysis1011You are performing **read-only** analysis: no file edits, no branches, no12commands that mutate state. Output is a brief that a human or `/fix` acts on.1314The iron rule: **no fix without a root cause.** A fix proposed before the cause15is understood is a guess, and guesses create new bugs. Symptom patches are16failure even when they make the error disappear.1718**Announce at start:** "Using the root-cause skill — read-only analysis of this19bug."2021## Step 0: Load context22231. The bug report — full description, repro steps, linked threads/issues.242. `.ai/lessons.md` — known pitfalls in the affected area.253. Root `AGENTS.md` Task Router → the matching `.claude/rules/` guides.264. The affected module/package `AGENTS.md`.2728## Step 1: Establish the facts29301. **Read the error completely** — full stack trace, message, HTTP status, file31 and line numbers. The error text often contains the answer.322. **Reproduce or trace.** State the exact repro steps. If you cannot reproduce33 even mentally, gather more data — do not guess.343. **Check recent changes**: `git log --oneline -20 -- <affected paths>` and the35 branch diff. Most bugs live in what changed last.364. **Trace the data flow** from entry point to origin: route → loader/action →37 service function → query → response. Note every transformation. Follow the38 bad value **backward** to where it is first wrong — fix at the source, not39 where the symptom surfaces (see `references/root-cause-tracing.md`).405. **Check the schema**: read the *newest* relevant migrations (order by41 timestamp) and generated types. Column names and constraints must match what42 the code assumes.4344## Step 2: Carbon-specific failure modes4546Check each of these before inventing exotic theories:4748| Check | What to look for |49|-------|------------------|50| companyId scoping | A query missing `companyId` filtering → cross-tenant leak or empty results |51| Stale generated types | Code references columns from a new migration but `pnpm run generate:types` wasn't run — typecheck greens lie |52| RLS policy gaps | New table/column without policies; check the table's migrations |53| Permission strings | `requirePermissions()` / `permissions.can()` scopes are string literals — invisible to typecheck; grep the whole repo after any scope rename |54| Service signature drift | Caller args don't match the service function's current signature |55| Migration ordering | A migration backdated older than deployed ones applies out of order on remotes (see `.ai/lessons.md`) |56| Form submission | `ValidatedForm` only submits on a native submit with a `submitter`; react-aria number/date fields commit hidden inputs on **blur** (see `/test` for details) |57| Import staleness | Import from a moved path with no re-export bridge |5859## Step 3: One hypothesis at a time60611. Form a **single** specific hypothesis: "X in file Y causes the bug because Z."622. Verify it against the code you can read. If the code contradicts it, discard63 it — don't force-fit.643. Distinguish cause from symptom: a UI `TypeError` may originate three layers65 down. Keep tracing until you reach the origin.664. **Three-strikes rule:** if 3 hypotheses have failed, the problem is likely67 architectural (shared state, coupling, a wrong pattern) — STOP, write up what68 you ruled out, and surface the architectural question to the human instead of69 producing hypothesis #4.7071## Step 4: Confidence7273| Level | Meaning |74|-------|---------|75| HIGH | Cause identified with code evidence; fix path obvious |76| MEDIUM | Strong code-supported hypothesis; runtime confirmation would help |77| LOW | Multiple plausible causes or the bug appears runtime/environment-dependent |7879If MEDIUM or LOW **and** the bug involves runtime state, ordering, caching,80concurrency, or manual reproduction → recommend `/debugging-difficult-bugs`81(temporary JSONL instrumentation) as the next step instead of guessing. Never82present a guess as a finding.8384## Step 5: Output the brief8586Produce exactly this structure (~400 words max):8788```markdown89## Root-Cause Brief9091**Bug:** <one line>92**Summary:** <2–3 sentences: symptom and observable impact>93**Root cause:** <why it happens, citing file:line>94**Confidence:** HIGH | MEDIUM | LOW95<if not HIGH: what is uncertain and what would resolve it — e.g. "instrument via /debugging-difficult-bugs">9697**Files to change:**98- `path/to/file.ts` — <what and why>99100**Approach:**1011. <step>102103**Risks:**104- <e.g. "signature change affects 3 callers">105106**BC impact:** <NONE | FROZEN/STABLE surfaces touched, per BACKWARD_COMPATIBILITY.md>107```108109## Guardrails110111- **Read-only.** No edits, no `git` writes, no migrations, no DB commands.112- **No speculative fixes.** "Try this and see" is not a finding.113- **Stay scoped.** Analyze the reported bug only; note unrelated discoveries in114 one line at the end, don't chase them.115- **Cite evidence.** Every claim references a file, line, migration, or policy.116117Red flags — thinking any of these means you're guessing, not analyzing; STOP:118119- "it's probably X, let me suggest the fix" (no verified cause → no fix)120- "I'll propose two possible fixes and let them pick" (that's two guesses)121- "one more hypothesis" after three have failed (that's an architecture122 question now — surface it)123- "the error message is misleading, ignore it" (read it again; it usually isn't)124125## References (read when the situation matches)126127- `references/root-cause-tracing.md` — tracing a bad value backward through the128 call stack to its origin129- `references/defense-in-depth.md` — layering validation after the cause is found130- `references/condition-based-waiting.md` (+ `condition-based-waiting-example.ts`)131 — replacing arbitrary timeouts with condition polling in flaky async tests132- `references/find-polluter.sh` — bisecting which earlier test pollutes a133 failing test