← Streetlight back to all nodes
← back to the diagram (midi-harness.html)

The Arc — three repos, one idea, getting simpler

The full story behind the front-page diagram. An ambitious first attempt (midi-coach) broke on a hidden flaw, the strip-back rebuild (midi-harness) is what the diagram shows, and the whole point was the public blog post. Source of truth: the two repos and the published post.

more complex ↑ time → midi-coach two streams: MIDI + voice byte-offset + Wispr/whisper aborted · clocks drifted May 27–29 midi-harness one stream: MIDI only delete the log each turn shipped · 46-word prompt May 29 → the blog post lessons, made public the whole point strip back write it up

Each step throws something away: midi-coach had two clocks and a byte-offset marker; midi-harness keeps one clock and uses file-deletion as the turn boundary; the blog post keeps only the lesson. The diagram on the front page is a zoom-in on the middle box.

01 · aborted midi-coach

The ambitious first attempt. A separate Go repo (~/Documents/midi-coach, git github.com/jakesimonds/MCP-Dash-Demo) tried to interleave two streams every turn: the MIDI notes Jake played and his Wispr-Flow voice dictation, time-aligned into a <spoken-and-played> block. It tracked progress with a byte-offset marker (.last-offset) instead of erasing the log.

The premise: Claude-as-piano-teacher. Jake narrates while he plays — "here comes a G octave… now a C octave… what do you see?" — and the hook slots the keys in where the words landed. A library/ of per-song references (a Bob Dylan PDF, a Brenda Earle Stokes voicing tutorial) gave the agent ground truth to check against.

It was abandoned because the sync was quietly broken — and worse, it looked like it worked. Jake's verdict after a deliberate test:

"Wispr is not working at all and I don't know that it ever has been."

From FINDINGS.md (2026-05-29) — the diagnostic that killed it

The G-octave / C-octave demo dropped the exact notes it was meant to show — the block held only the opening slice of the dictation, and the notes it did show weren't the octaves Jake described.
A turn where Jake said "this time I'm actually playing a note" came back words-only — zero MIDI clusters. A played note never reached the gate.
The hook's whisper re-transcription was regularly shorter than the clean Wispr message — two transcripts of the same speech that didn't match.
The meta-finding: the output always looked plausible, so nobody caught it being wrong until Jake played a known thing and looked for it. A block full of timestamped chords reads as "synced" whether the notes belong to those words or not. There was never any ground truth.

Root cause (FINDINGS.md, ranked): two independent clocks reconciled after the fact. Wispr recorded audio on its clock; the Go listener logged MIDI on wall-clock epoch; the gate kept only notes whose timestamp fell inside Wispr's clip window. Drift of a few seconds → the window missed the notes entirely. Architectural, not a tunable bug. The agreed fix was radical: kill the second stream.

Lives on only as a pointer in archive/midi-coach; the diagnostic itself is ~/Documents/midi-coach/FINDINGS.md. The repo was later repurposed, so its git history no longer reads as a music tool.

02 · shipped midi-harness — the strip-back

The 2026-05-29 rebuild as a fresh independent repo (~/Documents/midi-harness, public at github.com/jakesimonds/midi-harness). It dropped the voice stream and the byte-offset bookkeeping entirely. The one idea the front-page diagram captures: emptying the log every turn IS the "since last message" boundary — no offset marker, no time window, no second clock, no audio.

Two pure-stdlib Go binaries: cmd/listener (forever-running capture, re-opens the file per write so a deleted log is recreated by the next note) and cmd/hook (a UserPromptSubmit hook that reads the whole file, clusters notes into chords, emits a <played> block, then deletes the file). ~0ms hook startup. ~10 real sessions logged 2026-05-30 → 2026-06-01.

What Claude receives each turn (real hook output):

<played>
  C3 G3 C4 E4 x3
  F2 F3 A#3 D4
  G4
</played>
# notes only — no pitch-class brackets, no header.
# the format is explained once in the 46-word prompt,
# so each turn spends tokens only on the notes.

The listener controls (live in this node dir, wired to the local viewer's /api/run-script):

listener-start.sh

Kill-then-exec foreground start: cd ~/Documents/midi-harness, make build, kill any already-running listener (so events.jsonl never has two writers), then exec bin/listener — the viewer streams its stdout so notes scroll as you play.

listener-status.sh

Emits one JSON line for the node page's status pill. Matches on the binary path (pgrep -f), so it sees a listener started by anyone — this button, a terminal, or the repo's own SessionStart hook.

listener-stop.sh

A pkill on the same binary path — stops the listener no matter who started it.

Claude's second output — what-you-played.html, written ad hoc (no skill, no prompt line): a chord card over a fingered mini-keyboard. Here, C major as a 1–3–5 home-base shape.

C Major · 1–3–5 1 3 5 the home-base hand shape

Full current-state notes, the "does the sub-project see streetlight's harness?" answer, and the lineage all live in midi-harness.md. Drift committed 2026-06-11 as 7cb9815; session-drain.md is gitignored (local transcript paths).

1 · Two clocks reconciled after the fact will drift — and lie.

Notes got silently dropped, and the output always looked plausible so nobody caught it. The fix wasn't to reconcile better; it was to delete the second clock. One stream, one boundary.

2 · You can't tune a system prompt from inside the session it governs.

Claude Code loads CLAUDE.md once at session start and never re-reads it per turn — the editing session keeps running the OLD prompt and is a biased judge of its own edit. Edit, then test in a fresh session. See system-prompt-self-edit.

3 · One idea wearing three names IS miscommunication.

midi-coach, midi-harness-rebuild, and a same-named Rust-synth node all answered to "midi-harness." Naming sprawl made it hard to say which thing worked. Cleanest fix: let the live repo own the name.

4 · Plausible output without ground truth is the real trap.

An LLM block that reads as "synced" hides whether it actually is. You only catch it by playing a known thing and looking for it. Build the check before you trust the demo.

03 · the point the public blog post

The whole exercise was aimed at a public essay about treating an LLM as an instrument you learn to play — and the lessons from building a tiny harness to do it. The post is live; this node archives once it's out, so the AC is met.

Published post · jakesimonds.leaflet.pub

Midi Harness: An Experimental Stack

jakesimonds.leaflet.pub/3mnzjxem5q22z