Root-Cause-by-Layer — inner loop (∂L/∂code), the gradient computer
The Metaphor
The pathologist at the autopsy table. The visible symptom — the bleeding, the bruise, the collapsed lung — is never the disease. The disease lives in a failing organ, a contract between systems, a subtle pressure gradient two layers deep. A surgeon who sutures the bleeding without opening up dies a different patient next week. Five layers, always. Stop at layer 2 and the bug re-emerges in a different guise.
Overview
In Gradient Descent for Agents, this skill is how you compute ∂L/∂code. Without the 5-layer ladder, any proposed edit is a random direction in parameter space — on average it raises generalization loss even when it happens to lower training loss. A bug is a symptom. The disease almost always lives at a layer boundary, a contract violation, or a conceptual mismatch — rarely on the line that threw the exception. Walk the 5-layer ladder before proposing any fix.
Core principle: A change that turns the symptom green without a causal story at layers 3–5 is a suppression, not a cure. It re-emerges in a different guise within a few iterations.
Violating the letter of the layered analysis is violating the spirit. "I know the cause already, I'll skip writing it out" is the most common skip — write it down anyway. The discipline is the externalization.
When to Use
Any of these triggers means invoke this skill before proposing code:
- A test is red or flaky and you want to ship a fix
- An exception, type error, assertion failure, or unexpected output
- A CI gate is rejecting and you're considering widening the rule
- An LLM/tool returned a malformed result and you're considering a retry/regex
- You catch yourself typing
try:, hasattr(, @pytest.mark.skip, # TODO clean up, or a retry loop as the primary fix
- Deadline / sunk-cost pressure is telling you "just get it green"
Do not use for: cosmetic edits, rename refactors, adding new features with no failing signal. This skill is for diagnosing, not building.
The Five Layers
Apply in order. You do not have a root cause until you can answer both 4 and 5 out loud.
- Symptom — The exact error, failed assertion, rejected output, wrong number. Copy the raw text. Do not paraphrase.
- Mechanism — Which code path produced the symptom? Name the function, branch, and state that led to it.
- Contract — Which input/output shape, type, invariant, or documented promise did the mechanism violate? Whose contract — the caller's or the callee's? Is the contract explicit (signature/docstring/schema) or only implicit?
- Structural origin — Which architectural layer is this in (domain, integration, transport, persistence, UI, config, …)? Is the bug inside that layer, or is it a leak across a boundary?
- Conceptual origin — Which design concept is being misapplied: separation of concerns, ownership, single source of truth, explicit-over-implicit, data-flow direction, invariant location?
Only once 4 and 5 are named is a fix admissible. The admissible fix is at the layer you named — not above, not below.
Red Flags — STOP, you are about to symptom-patch
These phrases, appearing in your own thinking or answer, mean you skipped layers 3–5:
- "Just make it tolerant to both shapes"
- "Add a
try/except so it doesn't blow up"
- "I'll clean up later / mark as tech debt / file a ticket"
- "It's a smaller change than fixing it properly"
- "The other callers still rely on the old shape" (have you grepped?)
- "Under time pressure this is good enough"
- "Widen the regex / threshold / whitelist"
- "Retry loop will handle the flakiness"
- "
@pytest.mark.skip / xfail for now"
- "Hardcode the value that works"
- "Cache so we don't hit the broken path"
- "LLM was probably bad, just retry"
When one fires: stop, re-enter at layer 2, walk up.
Anti-Pattern Catalog
| Anti-pattern |
Why wrong |
Do instead |
try/except around the failing call |
Hides contract violation; corrupted state flows downstream |
Find the caller that violates the input contract; fix at that boundary |
hasattr-or-subscript / duck-typing shim |
Normalizes two implicit contracts into one ambiguous one |
Pick ONE explicit type; convert at the boundary |
| Widening regex / threshold / whitelist until test passes |
Treats the test as the spec; real spec is the contract |
Ask what the rule intends; honor that |
| Retry loop around a flaky step |
Normalizes non-determinism |
Trace non-determinism to its source, remove it there |
| Hardcoding a "works for now" value |
Freezes one case, breaks for the next |
Derive the value from the contract that should produce it |
@pytest.mark.skip / xfail to unblock |
Converts a loss signal into silence |
Fix root cause; if the test is wrong, fix the test with written justification |
| Patching log/UI so the error doesn't show |
Error is still there, just invisible |
Surface is a read-only view; fix reality |
| Adding a compat shim "for existing callers" |
Usually there are NO existing callers |
Grep for callers first; fix the single real caller |
| "LLM / network / filesystem was flaky" |
Abdicates diagnosis |
Capture the exact payload; diagnose which contract its output violates |
How to apply — checklist
- Read the symptom verbatim and paste it into your answer.
- For each of layers 2–5, write one sentence. If you can't, read more code — do not guess.
- If a Red Flag fired during step 2, delete your draft fix and restart at layer 2.
- State the admissible fix at the layer you named in 4/5. Resist the urge to fix one layer higher (too shallow) or one lower (too invasive).
- Add a regression test at the layer of the root cause, not at the layer of the symptom. A diagnosed-as-contract bug gets a contract-level test, not another end-to-end retry.
- If time pressure is real, the minimal structural fix is almost always smaller than the "quick" symptom patch plus the follow-up cleanup it promises. Ship the structural fix.
Common Rationalizations
| Excuse |
Reality |
| "Deadline — clean up later" |
Tech debt from symptom patches compounds faster than any deadline saves. "Clean up later" almost never happens, and when it does it's 3× the original cost. |
| "Make it tolerant so both shapes work" |
Tolerance = two implicit contracts living together. Pick one explicit contract. |
| "Other callers might rely on old shape" |
Did you grep? 90% of the time there are none — you invented them. |
| "Retry handles the flakiness" |
Flakiness has a cause. Retry masks, it does not fix. You will see the same bug next week. |
| "Smaller diff is safer" |
A 1-line symptom patch is usually a 3–5 line structural fix. Measure diffs in future bugs prevented, not in lines changed today. |
| "The test is wrong" |
Sometimes true — but then write the correct test before you touch the code. Never delete a failing test without a replacement. |
Measured failure mode
On the fixture tests/fixtures/root-cause-by-layer/ (a contract-boundary TypeError under 15-minute deadline pressure), a subagent without this skill ships:
email = user.email if hasattr(user, "email") else user["email"]
…labeled "tolerant," tagged as an "interim fix to be cleaned up later." Two implicit contracts, one masked boundary, one deferred cleanup that never happens — the canonical symptom patch.
With this skill loaded, the same subagent names layer 4 (domain ↔ integration boundary leak) and layer 5 (implicit-contract violation / separation-of-concerns), then fixes at the boundary by making the transport-layer signature explicit (email: str) and moving the extraction to the caller. No shim, no try/except, no deferred cleanup.
Δloss ≈ 6 rubric violations removed. Full baseline and GREEN artifacts in the fixture directory.
1---2name: root-cause-by-layer3description: Use when encountering a bug, failing test, or unexpected behavior — especially under time pressure where try/except, retry loops, xfail, type-tolerance shims, "I'll clean up later," or compat-for-both-shapes feel like the fastest path. Forbids symptom patches until structural and conceptual origins are named.4---56# Root-Cause-by-Layer — inner loop (`∂L/∂code`), the gradient computer78## The Metaphor910**The pathologist at the autopsy table.** The visible symptom — the bleeding, the bruise, the collapsed lung — is never the disease. The disease lives in a failing organ, a contract between systems, a subtle pressure gradient two layers deep. A surgeon who sutures the bleeding without opening up dies a different patient next week. Five layers, always. Stop at layer 2 and the bug re-emerges in a different guise.1112## Overview1314In [Gradient Descent for Agents](../../docs/theory.md), this skill is **how you compute `∂L/∂code`**. Without the 5-layer ladder, any proposed edit is a random direction in parameter space — on average it raises generalization loss even when it happens to lower training loss. A bug is a **symptom**. The disease almost always lives at a layer boundary, a contract violation, or a conceptual mismatch — rarely on the line that threw the exception. Walk the 5-layer ladder before proposing any fix.1516**Core principle:** A change that turns the symptom green without a causal story at layers 3–5 is a **suppression, not a cure.** It re-emerges in a different guise within a few iterations.1718**Violating the letter of the layered analysis is violating the spirit.** "I know the cause already, I'll skip writing it out" is the most common skip — write it down anyway. The discipline is the externalization.1920## When to Use2122Any of these triggers means invoke this skill **before** proposing code:2324- A test is red or flaky and you want to ship a fix25- An exception, type error, assertion failure, or unexpected output26- A CI gate is rejecting and you're considering widening the rule27- An LLM/tool returned a malformed result and you're considering a retry/regex28- You catch yourself typing `try:`, `hasattr(`, `@pytest.mark.skip`, `# TODO clean up`, or a retry loop as the primary fix29- Deadline / sunk-cost pressure is telling you "just get it green"3031Do **not** use for: cosmetic edits, rename refactors, adding new features with no failing signal. This skill is for *diagnosing*, not *building*.3233## The Five Layers3435Apply in order. You do **not** have a root cause until you can answer **both 4 and 5** out loud.36371. **Symptom** — The exact error, failed assertion, rejected output, wrong number. Copy the raw text. Do not paraphrase.382. **Mechanism** — Which code path produced the symptom? Name the function, branch, and state that led to it.393. **Contract** — Which input/output shape, type, invariant, or documented promise did the mechanism violate? **Whose** contract — the caller's or the callee's? Is the contract explicit (signature/docstring/schema) or only implicit?404. **Structural origin** — Which architectural layer is this in (domain, integration, transport, persistence, UI, config, …)? Is the bug *inside* that layer, or is it a **leak across** a boundary?415. **Conceptual origin** — Which design concept is being misapplied: separation of concerns, ownership, single source of truth, explicit-over-implicit, data-flow direction, invariant location?4243Only once 4 and 5 are named is a fix admissible. The admissible fix is **at the layer you named** — not above, not below.4445## Red Flags — STOP, you are about to symptom-patch4647These phrases, appearing in your own thinking or answer, mean you skipped layers 3–5:4849- "Just make it tolerant to both shapes"50- "Add a `try/except` so it doesn't blow up"51- "I'll clean up later / mark as tech debt / file a ticket"52- "It's a smaller change than fixing it properly"53- "The other callers still rely on the old shape" *(have you grepped?)*54- "Under time pressure this is good enough"55- "Widen the regex / threshold / whitelist"56- "Retry loop will handle the flakiness"57- "`@pytest.mark.skip` / `xfail` for now"58- "Hardcode the value that works"59- "Cache so we don't hit the broken path"60- "LLM was probably bad, just retry"6162When one fires: stop, re-enter at layer 2, walk up.6364## Anti-Pattern Catalog6566| Anti-pattern | Why wrong | Do instead |67|---|---|---|68| `try/except` around the failing call | Hides contract violation; corrupted state flows downstream | Find the caller that violates the input contract; fix at that boundary |69| `hasattr`-or-subscript / duck-typing shim | Normalizes two implicit contracts into one ambiguous one | Pick ONE explicit type; convert at the boundary |70| Widening regex / threshold / whitelist until test passes | Treats the test as the spec; real spec is the contract | Ask what the rule *intends*; honor that |71| Retry loop around a flaky step | Normalizes non-determinism | Trace non-determinism to its source, remove it there |72| Hardcoding a "works for now" value | Freezes one case, breaks for the next | Derive the value from the contract that should produce it |73| `@pytest.mark.skip` / `xfail` to unblock | Converts a loss signal into silence | Fix root cause; if the test is wrong, fix the test **with** written justification |74| Patching log/UI so the error doesn't show | Error is still there, just invisible | Surface is a read-only view; fix reality |75| Adding a compat shim "for existing callers" | Usually there are NO existing callers | Grep for callers **first**; fix the single real caller |76| "LLM / network / filesystem was flaky" | Abdicates diagnosis | Capture the exact payload; diagnose which contract its output violates |7778## How to apply — checklist79801. Read the symptom **verbatim** and paste it into your answer.812. For each of layers 2–5, write one sentence. If you can't, read more code — do **not** guess.823. If a Red Flag fired during step 2, delete your draft fix and restart at layer 2.834. State the admissible fix at the layer you named in 4/5. Resist the urge to fix one layer higher (too shallow) or one lower (too invasive).845. Add a regression test **at the layer of the root cause**, not at the layer of the symptom. A diagnosed-as-contract bug gets a contract-level test, not another end-to-end retry.856. If time pressure is real, the minimal *structural* fix is almost always smaller than the "quick" symptom patch *plus* the follow-up cleanup it promises. Ship the structural fix.8687## Common Rationalizations8889| Excuse | Reality |90|---|---|91| "Deadline — clean up later" | Tech debt from symptom patches compounds faster than any deadline saves. "Clean up later" almost never happens, and when it does it's 3× the original cost. |92| "Make it tolerant so both shapes work" | Tolerance = two implicit contracts living together. Pick one explicit contract. |93| "Other callers might rely on old shape" | Did you `grep`? 90% of the time there are none — you invented them. |94| "Retry handles the flakiness" | Flakiness has a cause. Retry masks, it does not fix. You will see the same bug next week. |95| "Smaller diff is safer" | A 1-line symptom patch is usually a 3–5 line structural fix. Measure diffs in **future bugs prevented**, not in lines changed today. |96| "The test is wrong" | Sometimes true — but then write the *correct* test before you touch the code. Never delete a failing test without a replacement. |9798## Measured failure mode99100On the fixture `tests/fixtures/root-cause-by-layer/` (a contract-boundary `TypeError` under 15-minute deadline pressure), a subagent without this skill ships:101102```python103email = user.email if hasattr(user, "email") else user["email"]104```105106…labeled "tolerant," tagged as an "interim fix to be cleaned up later." Two implicit contracts, one masked boundary, one deferred cleanup that never happens — the canonical symptom patch.107108With this skill loaded, the same subagent names layer 4 (domain ↔ integration boundary leak) and layer 5 (implicit-contract violation / separation-of-concerns), then fixes at the boundary by making the transport-layer signature explicit (`email: str`) and moving the extraction to the caller. No shim, no `try/except`, no deferred cleanup.109110Δloss ≈ 6 rubric violations removed. Full baseline and GREEN artifacts in the fixture directory.