$npx -y skills add UnpaidAttention/fable5-methodology --skill debugging-methodologyStep-by-step procedure for diagnosing and fixing failures — reproduce, read, hypothesize, bisect, fix, regression-test. Trigger this whenever something does not work as expected — a bug report, a failing or flaky test, a crash, an error message, wrong output, "it worked yesterday
| 1 | # Debugging Methodology |
| 2 | |
| 3 | Fix causes, not symptoms. The procedure is sequential — skipped steps are where the time goes. |
| 4 | |
| 5 | ## Step 1: Reproduce (mandatory, no exceptions) |
| 6 | |
| 7 | 1. Make the failure happen on demand with the shortest command available. Capture the exact |
| 8 | output. A fix applied to an unreproduced bug is a guess wearing a fix's clothes. |
| 9 | 2. Can't reproduce? Then the task is information-gathering, not fixing: collect failing inputs, |
| 10 | versions, environment, logs, timing. Do not theorize past the evidence you have. |
| 11 | 3. Shrink it: smallest input, fewest components. Every component removed is a component |
| 12 | exonerated. |
| 13 | |
| 14 | ## Step 2: Read before theorizing |
| 15 | |
| 16 | 1. Read the WHOLE error message and the WHOLE stack trace, literally. `KeyError: 'user_id'` |
| 17 | means that key was absent in that dict at that line — start there, not at your favorite |
| 18 | suspect. |
| 19 | 2. Read the failing function completely, then read the code that produced the data it consumed. |
| 20 | 3. Sweep the boring environmental causes FIRST — they are frequent: wrong directory, stale |
| 21 | build/cache, unsaved file, wrong branch, wrong env/port, version mismatch, leftover process |
| 22 | holding a port. |
| 23 | |
| 24 | ## Step 3: Hypothesize — falsifiably |
| 25 | |
| 26 | Write the hypothesis as a sentence that an observation could prove false: |
| 27 | > "The handler receives None because the middleware short-circuits before auth populates it." |
| 28 | |
| 29 | If you can't phrase it falsifiably, you have a mood, not a hypothesis — go back to Step 2. |
| 30 | |
| 31 | ## Step 4: Test ONE hypothesis with the cheapest observation |
| 32 | |
| 33 | 1. Choose the cheapest observation that distinguishes true/false: a print/log of the actual |
| 34 | value at the suspect boundary, a debugger breakpoint, a 3-line unit test, `git bisect` when |
| 35 | the failure is "used to work". |
| 36 | 2. Prefer observing state over re-reading code: code says what SHOULD happen; instrumentation |
| 37 | says what DID. |
| 38 | 3. One change → run → observe → conclude. NEVER two speculative changes in one step: if it |
| 39 | passes you won't know why; if it fails you won't know what to revert. |
| 40 | 4. Binary-search the pipeline: output of A→B→C→D wrong? Probe at B/C first, not linearly. |
| 41 | 5. Remove your instrumentation once it has answered. |
| 42 | |
| 43 | ## Step 5: Know when to abandon a hypothesis |
| 44 | |
| 45 | Abandon — do not patch around — when: |
| 46 | - The observation came back false. State what you now know; derive the next hypothesis from |
| 47 | that evidence, not from the discard pile. |
| 48 | - A fix "works" but you cannot explain the bug's mechanism. Unexplained fixes are bugs in |
| 49 | hiding: keep digging, or explicitly deliver it labeled "empirical fix, mechanism unconfirmed". |
| 50 | - You're adding a second workaround on top of a first. Two workarounds = the real cause is |
| 51 | upstream. Revert both, go upstream. |
| 52 | |
| 53 | ## Step 6: The 3-strike rule |
| 54 | |
| 55 | After 3 failed fix attempts on the same failure: |
| 56 | 1. STOP. Revert to last known-good (`git stash` / `git checkout -- .`). |
| 57 | 2. Write down: what fails, exact error, what's ruled out, what each attempt changed and what |
| 58 | happened. |
| 59 | 3. Either form a genuinely new hypothesis FROM that write-up, or deliver the write-up itself |
| 60 | as the current state. "Here's what I've ruled out" is a deliverable; a fourth blind patch |
| 61 | is not. |
| 62 | |
| 63 | ## Step 7: After the fix |
| 64 | |
| 65 | 1. Add a regression test that fails on the pre-fix code (verify against the broken version if |
| 66 | cheap — stash the fix, run, unstash). |
| 67 | 2. Grep for siblings: the same bug usually lives wherever the same idiom was copy-pasted. |
| 68 | 3. Run the full relevant suite, not just the new test. Re-run AFTER your final edit — a pass |
| 69 | before the last tweak proves nothing. |
| 70 | |
| 71 | ## Worked example |
| 72 | |
| 73 | Symptom: nightly report job produces an empty CSV since Tuesday. |
| 74 | |
| 75 | 1. Reproduce: run the job locally against Monday's snapshot → non-empty. Against Tuesday's → |
| 76 | empty. Reproduced and narrowed to data-dependent in one step. |
| 77 | 2. Read: no error raised. The query returns 0 rows; the writer wrote headers only. |
| 78 | 3. Hypothesis: "The date filter excludes all rows because Tuesday's rows have timezone-aware |
| 79 | timestamps and the filter compares naive datetimes." |
| 80 | 4. Cheapest observation: print one row's timestamp and the filter bound → `2026-07-01T00:00:00+00:00` |
| 81 | vs naive bound. Confirmed on first probe. |
| 82 | 5. Fix at the origin (normalize at the ingestion boundary), not the crash site (not a |
| 83 | band-aid in the report query). |
| 84 | 6. Regression test: report over a snapshot containing aware timestamps must be non-empty. |
| 85 | Grep for other naive `datetime.no |