skip to content
graham@mccarthy:~$

The Build Was Green and the Lick Was Unplayable

graham_m·2026-08-21·7 min read
The Build Was Green and the Lick Was Unplayable

I've been trying to get better at guitar for years in the way most intermediate players do: a tab site in one tab, a scale diagram in another, a metronome app on my phone, and a vague sense that I'm running the same box I already know. What I wanted was one place that held all of it — scales, voicings, chords, the shapes and the reasons — plus enough gamification to turn twenty minutes a day into a habit instead of an intention.

So I built it. Fretstart is a daily practice coach for blues and rock lead: it tells you what to play today, shows it on the neck, keeps a metronome and a backing groove running, records you, and keeps a streak. It went from an empty folder to live on a domain in seven days, 175 commits, built the same way I built Multiplay — AI running the loop, me holding the merge button.

The interesting part isn't that week. It's what the week taught me about a category of bug I'd never had to think about before.

The code was fine. The music was wrong.

A few days in, the build was green. TypeScript clean, lint clean, everything rendering. And the app was teaching things that were false.

A lick was written on the B string, which put it out of key, while the prose beside it described the G string. A descending run chained pull-offs across strings, which no hand can play. The library's 12-bar page described a plain blues while the diagram directly above it rendered a quick change. Every one of those compiled perfectly, rendered beautifully, and would have taught me something wrong for a week before I noticed.

That's a different failure mode than I'm used to. Normally the artifact tells you when it's broken. Here the artifact is a claim about music, and nothing in a JavaScript toolchain has an opinion about whether a claim about music is true.

So the fix was to give the toolchain an opinion. check:content is a script that treats the musical content as data with invariants and asserts them on every build:

phrases       284 notes, 14 legato, all in the key they declare
licks/solos   16 phrases: length, source weeks, box-1 claims
transposable  every phrase moves to another key; fewest is 6 of 12
library       12 prose claims checked against the interval arrays
12-bar grid   I IV I I IV IV I I V IV I V  (14 prose claims checked)
library prose 21 fret/note claims checked against the tuning
box windows   13 library diagrams frame the shape their page describes

The line I'd steal for any content-heavy product is the fourth one. Every factual claim in the prose is checked against the data structure the diagram is drawn from, so a page physically cannot contradict its own picture. Prose and facts live in separate modules for exactly that reason: the copy is authored, everything factual is derived, and the checker sits in between calling out drift.

Alongside it, 579 conventional tests cover the logic where a bug is invisible until someone loses a streak: day boundaries, the lapse rule, the guest-to-account handoff. The rule I ended up writing down is that a new test has to be broken once to prove it fails. A test that has never failed proves nothing.

Gamification only works if the numbers are honest

The streak was the whole reason I built this, and it's also the thing most likely to make the app a liar.

The sixth commit in the repo deleted a stat from the prototype: it tracked "clean tempo" progress, and there was no honest way to measure it, so it went. A lapsed streak now displays zero the moment it lapses, rather than the stale number sitting in the database until your next session writes over it. Neither of those makes the app feel better. Both of them make the number mean something, and a number that doesn't mean anything can't build a habit.

The bigger version of the same problem: the plan originally ran sixteen weeks, and on day 113 it quietly served week 16 again, forever. The most committed user hits that wall first, which is exactly backwards. Fixing it honestly meant authoring weeks 17 through 32 — a whole second phase about repertoire, playing with other people, and building a set — across both the blues and rock tracks. 32 weeks × 7 days × 2 tracks is 448 authored days.

The payoff mechanic I like most is the quietest. The curriculum asks for a recording on day 1 and the last day of every fourth week, out to day 224. Those takes are never pruned to make room, and when you open the comparison it defaults to the widest span you have. Progress on an instrument is invisible day to day. Hearing day 1 played back against day 224 is the only proof that isn't a number I made up.

The difference between a planner and a coach

The feature I didn't plan is the one that justifies the app existing.

The curriculum kept telling me to check my bends. "A bend is only in tune if you check it." "Hear it before you pull it." Week 2 of both tracks is entirely about bending in tune, and the app's contribution was telling me to listen harder — leaving the checking to the ear that is itself the thing being trained.

Meanwhile the app already had pitch detection sitting in the tuner. Wiring it to the bend was a few hundred lines. It listens to several attempts and reports the pattern rather than the last one, because one good bend proves nothing: "reliably 30 cents flat" is a fixable habit, "landing somewhere different every time" is a hand problem that needs a different fix entirely. Fifteen cents counts as in tune.

That's the line between a practice planner and a practice coach, and it's the one thing here a video course structurally cannot do, because a video can't hear you.

What a green build still doesn't catch

Five audit rounds later — 146 issues filed, 140 closed — the traps that survived longest were all the silent kind:

  • Postgres taught me the same lesson three times. revoke ... from public does not revoke from anon. PUBLIC is its own grantee, and Supabase grants new tables and functions to each role by name, so a migration that revokes from public and grants to one role leaves the other's grant untouched. The grant line reads like the whole story and isn't.
  • A daily reminder that never alerted anyone. tag and renotify look like one setting and are two. Shipped with the default renotify: false, each day's reminder silently replaced yesterday's in the tray. Nothing on the server can see this: the push service accepts it, the subscription is healthy, the notification is genuinely created. It just never says so.
  • A migration in the repo is not a migration in the database. One sat unapplied while three later ones were written on top of it, so the page-view function didn't exist and every arrival was silently dropped for the entire life of the feature. The repo is not the record. The database is.

None of those show up in a test run. All of them show up in a query.

What isn't done

Six issues are still open, and the honest ones are worth naming. Weeks 17 to 32 have never been read by a guitarist other than me. /today ships more JavaScript on first load than it should, including all 32 weeks of curriculum. Ads are fully built and switched off, and stay off until the published pages draw around 1,000 sessions a month — written down as a decision so it doesn't become a drift the day somebody sets an env var. And there is exactly one account on the thing, which is mine.

That last one is fine. I built this to make myself practice, and I'm on the streak.

Try it at fretstart.com — the 32-week path and the scale and chord library are readable without an account.