RIFF · developer tutorial · 2026-07-07 · code: scripts/fixlog.py · records: docs/fixlog/ · contract: docs/fixlog/CONTRIBUTING.md

The Fix Journal: How This Codebase Remembers Its Mistakes

Written so anyone new to the project can use it in five minutes. Short version: every bug we fix gets a little story written about it, a robot can search those stories, and every so often we turn the stories into rules. Result: the same bug never costs us a day twice.

The big idea, as a picture

🐛 Something breaksa caller hears silence
🔍 SEARCH FIRSThas this happened before?
🔧 Fix itroot cause, not band-aid
✍️ Write the storyFX-014-my-bug.md
🧠 Retroevery ~10 stories → rules

The two gold boxes are the habits this system enforces. Searching first means you inherit every past debugging session for free. Writing the story means the next person (human or AI) inherits yours.

A real example, start to finish

This actually happened on 2026-07-07. A caller pressed 0 to update their profile. The system said "I'll text you a code" — and no code ever arrived. Here's what the caller's phone showed:

the message that never came
SAY THIS 4 DIGIT CODE: 4827 …

Why? The first text (just a link, no code — that's on purpose) accidentally checked the internal box that says "a verification code has been sent." So when the caller asked for a real code, the system looked at the box, saw it checked, and skipped sending one. The caller was asked to read a code from a message that never existed. One checkbox, shared by two different features.

Step 1 — Search before touching anything

$ .venv/bin/python scripts/fixlog.py search "code text never arrived"

[semantic] top matches:
  FX-008  Texts 'sent' but never delivered (10DLC)
  FX-011  Verify chain skipped the real code (welcome text claimed verification)

Ten seconds in, you already know: this smells like FX-008 (a carrier problem) or FX-011 (a bookkeeping problem) — and each record tells you exactly how to check which one, because the last person wrote down their evidence.

Step 2 — The story that got written (anatomy of an entry)

# Verify chain skipped the real code ← title = the symptom family

**Symptom**: Pressed 0, said yes to "text you a code" — no code ever arrived. ← the caller's words
**Root cause**: the link-only welcome text set _caller_verification_sent=True
with an EMPTY code; the verify chain saw "already sent" and skipped. ← the mechanism
**Fix**: verification-sent now strictly means A CODE IS OUTSTANDING. ← what changed
**Why this way**: fixing the MEANING of the claim beats special-casing
the skip check — every future reader gets the truth. ← alternatives were weighed
**Shift-left gate**: tests/test_link_only_verification_isolation.py ← can't happen again
**Test**: the guard's exact path::name ← required on new entries
**Success**: pending — next live call, press 0, code arrives ← closed at retro
**Evidence**: call v3:ByaxLead, slot ledger, commit 84f0a649

Step 3 — The retro (where stories become rules)

Every ~10 entries (or before a deploy, or at a session wrap), whoever's working reads the new stories and asks: is there a pattern? Patterns get one line each in docs/fixlog/PRINCIPLES.md. Today's first retro produced nine. Three examples:

ruleborn fromplain English
Deterministic paths need deterministic audioFX-004, 006, 007 If a keypress can reach a state, that state's voice must be a pre-recorded file — keypresses mute the AI's mouth.
A claim slot must mean exactly what it saysFX-001, 002, 011 If a flag is called "verification sent," it better mean a code is really out there — lies in bookkeeping become bugs downstream.
Every fix ships its gateall 13 A fix without a test that would catch the regression is a fix you'll do twice.

How the rules are enforced (not the honor system)

habitenforced bywhat happens if skipped
Annotate every fix, all fieldstests/test_fixlog_schema.pyCI fails — the build is red until the story is complete
Search before changingAGENTS.md (read by every AI agent at session start) + persistent memorythe agent's own instructions call it mandatory
Retro on cadence10 entries · before deploy · session wrappending Success fields pile up and the schema test surfaces them

🧪 Try it yourself (2 minutes)

# 1. Search like a debugger would:
.venv/bin/python scripts/fixlog.py search "menu kept repeating"

# 2. Read the whole catalog:
.venv/bin/python scripts/fixlog.py list

# 3. Start a new entry when you fix something:
.venv/bin/python scripts/fixlog.py new my-bug-slug
#    → creates docs/fixlog/FX-014-my-bug-slug.md with the template

# 4. Prove your entry is complete:
.venv/bin/python -m pytest tests/test_fixlog_schema.py -q
Why this makes the AI (and you) faster over time. Debugging is mostly re-deriving what someone already knew. On 2026-07-07 one day of live phone testing produced thirteen root-caused mechanisms — sticky gate slots, silent dispatch states, carrier registration, bookkeeping lies. Re-deriving any one of them costs hours; searching costs ten seconds. The journal turns experience into a lookup table, and the retro turns the lookup table into instincts.
The one anti-rule. Don't write essays. An entry is 8 lines that a stranger can act on. If your root cause doesn't name a mechanism (a file, a slot, a guard), you haven't found it yet — keep digging before you write.