1. 02 Aug, 2026 7 commits
    • fix(take_5_drops): the choir was muffled by a 1 kHz Moog filter, not by the mix · 516e3f30
      PLN, mid-run: "i hear no d7? or its muffed like '# att 0.2' or sth?" — then,
      after experimenting: "tried adding 'arp up' before the chord, its now a set of 4
      muffed short notes LOL. but d7 still muffed".
      
      Read the SynthDef instead of guessing the synth (CLAUDE.md: never infer a
      sound's role from its name, validate). SCLOrkSynths/SynthDefs/bass/moogBass.scd:
      
          arg ... cutoff = 1000, gain = 2.0, att, dec, sus, rel, chorus = 0.7;
          filter = MoogFF.ar(in: osc, freq: cutoff * (1.0 + (0.5*filterenv)), gain: gain)
      
      A 4-pole Moog lowpass parked at 1 kHz, on a SynthDef whose own metadata reads
      `category: \bass, tags: [\pitched, \bass]`. Every harmonic above 1 kHz is gone at
      24 dB/oct. A "CHOEUR GENERATIF" voiced on a bass synth cannot be anything but
      mud, and no amount of gain, room or crush was ever going to fix it. `# cutoff
      3500` opens it — 2500 warm pad, 3500 choir, 6000 buzzy.
      
      TWO TRAPS in this SynthDef, now written into the file where the next reader
      will hit them:
        * it DECLARES att/dec/sus/rel and then ignores them — the envelope is a
          hardcoded Env.adsr(0.001, 0.3, 0.9, 0.2, amp). That is why PLN's `# sus 2`
          did nothing and he commented it out. Shape this voice with legato.
        * `gain` in this SynthDef is the MoogFF filter RESONANCE, not a level.
      
      Three more real defects in the same orbit, all found by tooling, all fixed:
        * legato 2 -> 1.2. His new arp makes quarter-length notes; 2x overlap smeared
          them into each other. That is the "4 muffed short notes".
        * d7 had NO cut group at all, so held voices piled up — on a synth that also
          carries its own 7-tap chorus. Now `cut 7`.
        * crushbus restored, on slot 71 rather than d4's 41. The shared-bus case that
          became PV011 twenty minutes ago.
      
      And PV004 caught one PLN had not mentioned: d11 (THE FLUTIST) and d12 (THE
      SLEEPWALKER) both sat on `cut 11`, truncating each other instead of layering.
      d12 now owns cut 12.
      
      Also in this commit: mafia_sans_serif's d7, whose diagnosis is written up in
      b8344ac1. take_5_drops now 0 errors / 0 warnings, silent-eval --seeded ok.
      PLN (Algolia) authored
    • feat(set): a note is not a track — and the dead buttons PLN asked about · 5e5a37ad
      THE PHANTOM. PLN drafts in backlog.md, and he drafts in LIST ITEMS. Mid-run he
      wrote himself a deliberation:
      
        -- Q: This intro is quite raptech. jazz intro? quand on decolle opening? reflect
      
      and quand_on_decolle silently joined the OPAL set. The resolver's "codename
      carries extra words" branch is a subset test, so {quand, on, decolle} being 3 of
      those 13 words was a match. It then flowed downstream exactly as designed — into
      the .txt, the preload plan, the remap scope and the gig gate. The pipe worked
      perfectly, on garbage.
      
      Two guards, because either alone is too clever:
        * _NOTE_RE skips items marked Q:/NB:/NOTE:/TODO:/FIXME:/IDEA:, and PRINTS what
          it skipped. A note dropped silently and a track dropped silently look
          identical from inside the parser, and only one of them is survivable.
        * the extra-words branch is bounded to 4 decoration words. Real entries spend
          at most 4 ("[129] Desire [TODO BASS ETC!]"); that note spent 10.
      
      An UNMARKED prose line now RAISES instead of resolving. That is deliberate and
      tested: red gate, PLN adds "Q:" or deletes the line. A phantom track goes to the
      venue. Loud beats clever.
      
      (He has since re-added quand_on_decolle under an explicit ## SOUNDCHECK heading,
      as a real entry. It arrived carrying the full off-by-one-column corpus pattern —
      11 moves — now migrated. That is the pipe working on something real.)
      
      DEAD BUTTONS. PLN, playing: "not seeing effects on button 44 nor 43?" on
      do_it_right, "no buttons 42 43?" on ouais_je_funk. They were not broken, they
      were never mapped. Now:
        do_it_right   ^43  d3 hats — HIS OWN TODO implemented verbatim: "an opening
                           legato or att to make it slowly go from hard dry h to an
                           open ho? as in take 5 drops". Held, legato walks 0.4->2.5
                           and hpf 7k->2.5k over 8 cycles; released, it snaps back.
        do_it_right   ^44  d4 bass — octave down, DROP TO THE CELLAR
        ouais_je_funk ^42  d2 clap — the clap had no gate at all. Handclap roll.
      
      d4's crush/octersub land on ^52/^32 exactly as PLN asked. That required
      REORDERING the two lines, not renumbering them: migrate-columns assigns knobs in
      order of declaration, so the file now carries a comment saying the order is
      load-bearing. Fighting the tool by hand would just be undone next run.
      
      Set: 15 tracks, 0 moves pending, silent-eval --seeded 15/15, pvlint 0 errors,
      setlist tests all green (now pinning lime_tacos OUT and the phantom absent).
      PLN (Algolia) authored
    • feat(pvlint): PV014 + PV011-as-error — and they immediately found mafia's acid · b8344ac1
      PLN, at rehearsal, on take_5_drops' d7 coming out muffled:
      
        "i saw also its crushbus was 41, a d7 with sub 4 is sus -- and two effects on
         same bus is a redflag as it means fighting for bus! so we gotta flag warning
         any 'bus nXY on dX' as sus (e.g. bus 41 (d4's) on d7 here), and any shared
         bus (^41 on d4 and d7) as PV error"
      
      Two rules, exactly as specified.
      
      PV014 (warning) — a bus slot numbered for a different orbit. The convention is
      <orbit><slot>: d4 owns 41/42, d10 owns 101/102, and a bare `bus 4` on d4 is the
      same idea with one slot. MEASURED before encoding it rather than assumed: 894
      of 984 bus references under live/ conform, 90.9%. A real convention, not a
      tidiness preference. Mis-numbering is *how you accidentally share* a bus, so
      this rule sits upstream of PV011 and catches the cause, not the symptom.
      
      PV011 promoted warning -> error. It shipped as a warning on the theory that
      sharing a bus is legitimate glue. Rehearsal disproved that theory.
      
      The asymmetry with PV008 is deliberate and now documented in both rules and
      pinned by a test, so no future refactor quietly aligns them: yesterday PLN
      DEMOTED a shared button to a warning ("its ok when two percs are linked"),
      today he PROMOTED a shared bus to an error. A shared button is one hand doing
      two things on purpose and you hear it instantly. A shared bus is two orbits
      fighting over one effect instance with the winner decided by event density —
      you don't hear a mistake, you hear a slightly wrong sound and blame the synth.
      Surprises you can hear are warnings; surprises you cannot are errors.
      
      THE PAYOFF, first run, in a track still in the set. mafia_sans_serif's d7 was
      already carrying PLN's own note "FIXME ACID WEIRD NOT GOOD". It had THREE
      collisions with d4's moog bass, none visible in the code:
      
          # cut 4        every acid note truncated the bass, and vice versa
          # crushbus 41  one crush instance, ^35 and ^32 fighting over it
          # octerbus 42  one octer instance, ^55 and ^52 fighting over it
      
      d7 now owns cut 7 and slots 71/72. That is a diagnosis for a FIXME he wrote
      without knowing the cause, produced by a rule written twenty minutes earlier.
      
      Set is 0 errors / 9 warnings, silent-eval --seeded 14/14 ok, pvlint 68/68.
      PLN (Algolia) authored
    • feat(set): the remap was never systemic — it read the STALE setlist (#46, #123) · 71bb9bc2
      PLN, mid-run: 'wait bass was also on 89 not yet 76 whyyy i thought we were
      systemic :((' — and he was right to be annoyed. The answer is embarrassing and
      exactly the bug we already fixed one layer up.
      
      tools/migrate-columns.py hardcoded SETLIST = armada/setlist_opal2026.txt. That
      .txt was 13 tracks while backlog.md was 15. So when the remap ran, the two
      tracks PLN had just added were the only two it never saw — ouais_je_funk and
      lime_tacos kept their pre-remap CCs and he found out by reaching for a control
      that wasn't there. Same shape as #120/#121: a generated artifact consumed as if
      it were the source. resolve() now calls setlist.tracks(), i.e. backlog.md.
      
      Re-planned against the real set: 22 pending moves across 4 tracks, 11 of 15
      already clean. Then PLN cut lime_tacos ('actually weak, will have more fun on
      ouais funk and perfect') — removed from backlog.md, and the 14-track setlist
      plus the plan regenerated themselves from it. That is the pipe working.
      
      Applied, 10 moves:
        wap        d4  octer/octersubsub  ^52 -> ^32 (B4)
        piment     d7  modIndex/octersub  swapped onto B7/C7 in declaration order
        ouais      d3  snare gate         ^44 -> ^43 (E3)
        ouais      d4  crush ^53->^52 (C4), octersub -> ^32 (B4)
        ouais      d5  width/crush        swapped onto B5/C5
        ouais      d5  superimpose        ^57 -> ^89 (F5, gestures live on row F)
        ouais      d7  UPDOWN             ^55 -> ^35 (B7)
      
      d4's 'ply 2' had been authored on a KNOB (^56) and the migrator dutifully gave
      it d4's B4 slot, which pushed the octersub into an overflow FIXME. But PLN had
      just asked for exactly the opposite layout — 'd4 52 a crush? 32 a octersub
      maybe?' — so ply moved to ^44 (E4, a gate on the gate row, free now that d3's
      snare vacated it) and both effects got their proper knobs. No FIXME left.
      
      Verified: migrate-columns --plan reports 0 moves, silent-eval --seeded 14/14 ok.
      PLN (Algolia) authored
    • fix(do_it_right): an inline TODO split gM3 into `gM` + `3` — the whole block was dead · c9c8bc39
      PLN typed a TODO comment into the middle of `d12 $ gF3 $ gM3`, landing the `3`
      after the `--`. The line became `d12 $ gF3 $ gM`, and `gM` does not exist:
      
          Variable not in scope: gM :: Pattern ValueMap -> Pattern ValueMap
          Perhaps use one of these: 'gM1', 'gM2', 'gM3'
      
      One parse error silences the WHOLE do-block, so this was not 'd12 is quiet' —
      it was 'do_it_right makes no sound at all', found by silent-eval --seeded while
      he was mid-run. Comment moved above the line, where it cannot swallow code.
      
      The TODO itself is preserved verbatim: he wants more of ^18/^19 on this lead.
      PLN (Algolia) authored
    • docs(morning): Sunday brief — 15 tracks, 10 of them changed, and what to hear · 583dc5a2
      J-6, the one-before-last KRK rehearsal. Per-track watch list for the 1h30 run,
      because ten of the fifteen tracks have something in them PLN has never heard:
      seven new d10 risers, the take_5_drops d5 structure fix, ouais_je_funk's d10
      finally mutable, lime_tacos' cymbals relocated to d12, and two tracks that were
      not in any tool's idea of the set until yesterday.
      
      Also carries the fresh ghost matrix (9 of 14 transitions, 18 orphans) recomputed
      on the real 15-track order — and the pleasant surprise that adding the two
      missing tracks produced the longest clean stretch in the set,
      piment -> ouais -> lime -> perfect.
      PLN (Algolia) authored
    • feat(lime_tacos): cymbales sol d6 -> d12, keeping the shared ^44 percussion link · 97fb85ed
      PLN: "for lime if d11 taken lets push d6 to free d12 :)" — d11 is held by
      "Menace de retour", and d10 is now the safe riser, so d12 was the free slot.
      
      The move is genuinely one token: d6 appears exactly once in the file, carries no
      `cut 6` and no orbit-bound crushbus, so nothing had to follow it. ^44 stays
      shared with d3 DELIBERATELY — that is the idiom PLN ruled correct: one press
      re-rhythms hats and cymbals together.
      
      Side effect worth knowing at the desk: PV008 only inspects orbits 1-8, so with
      the cymbals on d12 the shared-button warning stops firing entirely. The link is
      unchanged; only the lint's field of view moved. Do not read the green as the
      coupling having gone away.
      
      WATCH ON THE RUN: Ardour Tidal 12 sits at +6.0 dB and Tidal 06 at -3.8 dB, so
      the cymbals land ~9.8 dB hotter than before. Ride Tidal 12 down rather than
      re-gaining the pattern until PLN has heard it.
      
      Verified: silent-eval --seeded ok (every declared orbit emits), pvlint clean.
      PLN (Algolia) authored
  2. 01 Aug, 2026 15 commits
    • fix(pvlint): PV008 is a WARNING — linked percussion is an idiom, not a defect · 6981bd15
      PLN, on lime_tacos' ^44 driving both d3 (hats) and d6 (cymbales sol):
      
          "lime tacos its ok when two percs are linked, e.g. here, change rythm of
           both 3 and 6. [...] im fine with this, not a NOGO, just a WARN?"
      
      He is right, and the interesting part is WHY the rule was wrong rather than that
      it was. PV008 was earned on a real bug — the 2026-07-29 phase-2 remap left d1
      holding ^42 after d2 was given ^42, and perfect.tidal shipped one button firing
      two orbits. That bug is real and this rule is how it was found.
      
      But the accidental case and the deliberate one are TEXTUALLY IDENTICAL. Two
      orbits, one button CC, no way to tell from the source whether PLN meant it. The
      rule had quietly encoded "one button drives one orbit" as an invariant of his
      style, when it is only a hypothesis about it — and the hypothesis was wrong. He
      groups percussion on purpose: one gesture re-rhythms the whole group.
      
      A check that cannot distinguish intent must not hold the gig gate shut. It
      reports; PLN decides. So: severity error -> warning, and the detail now states
      both readings and what distinguishes them (deliberate percussion grouping vs a
      control inherited from a remap) instead of assuming the second.
      
      Effect: tools/gig-up.sh goes NO-GO -> GO. This was the last blocking failure, and
      it was blocking on a correctly-written track.
      
      Added test_pv008_is_a_warning_not_an_error, which quotes PLN's ruling, so nobody
      "tightens" this back to error without meeting the argument first. pvlint 63/63.
      PLN (Algolia) authored
    • chore(tools): actually record the exec bit on setlist.py + its test · efb1aff7
      Third time. tools/gig-up.sh's 'tools executable' check (added yesterday for exactly
      this) is why it surfaced: setlist.py went into 04fc8c7c with content but mode 100644,
      so a fresh clone would get 'permission denied' from a HARD gate check. chmod +x
      touches the working tree; git stores the bit separately and needs
      `git update-index --chmod=+x`, and even then a pathspec-limited commit can leave
      HEAD at 100644 while the index says 100755 — which is what happened here.
      
      Verify with `git ls-tree HEAD <file>`, never `ls -l`.
      PLN (Algolia) authored
    • feat(set): d10 is the riser — 7 tracks get PLN's safe idiom, and d5 stops flattening (#33, #99) · 2376e431
      PLN: "can you add a safe riser on all set tracks that dont have one, and consider
      moving the d10 to d9 (eg a synmenace iirc) to have d10 riser always?"
      
      THE CONVENTION. d10 = riser, so the gesture lives on the same column in every track
      and his hands stop having to remember per-track. It was already true in three tracks
      without anyone naming it — do_it_right (mask "<f!4 t f!3>"), take_5_drops (the
      "A FAIR RISER" comment on d10) and vague_de_crime, whose line #33 designates as THE
      safe riser because a 1-in-8 mask cannot run away:
      
          d10 $ gF1 $ gM3 $ mask "<t f!7>" $ "risers:8"
      
      Copied verbatim into the 7 tracks where d10 was free: bombe_dj, wap, lime_tacos,
      you_my_sunshine, mafia_sans_serif, desire, electric_hammer. Inserted contiguous with
      the existing d-blocks, because all three pre-existing risers live inside the main
      block rather than standalone — a riser in its own block would need a separate
      ctrl+enter, which is not the gesture. 10 of 15 tracks now have it.
      
      WHY THE RELOCATION IS NOT IN THIS COMMIT. "Consider" did real work here. The two
      tracks PLN was remembering are piment_bresilien (d10 = Menace, d9 free) and
      ouais_je_funk (d10 = SynMenace, only d12 free) — but moving an orbit is not a
      one-line move, because orbit N is simultaneously the LCXL column, the Ardour track
      and the cut/crushbus group:
      
          piment's d10 block carries  -- A6 = d10's effect  on its crushbus CC ^18
          d9's fx CC is ^17, d12's is ^20      -> the CC must move with the sound
          Ardour Tidal 10 = -2.2 dB, Tidal 09 = +6.0 dB, Tidal 12 = +6.0 dB
                                               -> the synth lands +8.2 dB louder
      
      And the argument that settles it: perfect, gimme_acid and the_revolution declare all
      12 orbits, so d10 is never free there and the convention caps at 12/15 no matter what.
      Relocating buys 2 tracks for two CC remaps, a cut-group change and two synths jumping
      +8.2 dB — on material PLN has not rehearsed since, 7 days out. Deferred by choice, not
      forgotten; his call to overrule.
      
      #99, d5 of take_5_drops: `>|` takes structure from the RIGHT, so `slow 4` note
      patterns were REPLACING the slice rhythm with one event per four cycles — the
      "steals structure" symptom, confirmed. Now `|>`, which keeps the left structure and
      takes only the note value. Note line 35 immediately above was already doing it that
      way, so this makes the block self-consistent rather than inventing an idiom. Marked
      TODO @pln verify as asked.
      
      Also ouais_je_funk d10 read `gF3 $ gF3` — the same helper twice, so the orbit had no
      family mute and NOTHING on the surface could drop it (pvlint PV013). Now `gF3 $ gM3`.
      Also TODO @pln verify.
      
      VALIDATION: all 15 setlist tracks still compile (gig-up "setlist compiles" green,
      silent-eval cold-queries every dN), preload still covers the set at 53/53 banks, and
      pvlint added zero new findings — still the one pre-existing error, lime_tacos' ^44
      driving both d3 and d6. That one is left alone deliberately: ^44 is neither orbit's
      home gate (d3 = ^43, d6 = ^58/^90), so fixing it means rebinding a button PLN's hands
      already know, which is a taste call and not lint's to make.
      PLN (Algolia) authored
    • feat(gig): the backlog IS the setlist — one parser, and the set was 15 not 13 (#123) · 04fc8c7c
      PLN's ruling yesterday: "set SSOT is the backlog entry, always. if in the backlog
      i add a track or reorder them, its the truth." Then today, the part that decided
      the design: "if i added new ones, id not need to tell you to update, it should
      flow downstream, ditto if remove one."
      
      So this is not a resolution, it is a pipe.
      
      WHAT WAS ACTUALLY WRONG. Applying the ruling for the first time found the backlog
      listing 15 tracks while every tool in the repo believed 13:
      
          missing everywhere:  ouais_je_funk (120)   lime_tacos (110)
          wrong order:         backlog has you_my_sunshine (166) BEFORE mafia_sans_serif
      
      ouais_je_funk was not a stale wishlist entry — PLN committed a kick rework to it
      in 23f60e76, yesterday. And the drift had already cost him something audible:
      
          preload warmed   50 banks   (13 tracks)
          the set needs    53 banks   (15 tracks)
          would cold-load  dr  gfunk_bass  h2ogmcy
      
      Three banks read from disk on first play. That is the same mechanism PLN heard as
      crackle on the take_5_drops transition (#120) — a bug we thought was fixed, still
      live, just smaller. check-preload.sh reported "ok" the whole time, because it asked
      set-coherence, which was one of the wrong answers. The freshness check had
      inherited the staleness it was built to catch.
      
      ROOT CAUSE: eight tools each hardcoding `SETLIST = armada/setlist_opal2026.txt`,
      and one human keeping that file in sync by hand.
      
      THE SHAPE, and why it is not "make all eight import a module". The .txt stays
      exactly where it is, same format, so all eight consumers are untouched — this is
      J-7 to a gig and a refactor that breaks a tool costs more than it buys. What
      changes is its STATUS: generated, never hand-edited, with a HARD gig-up check
      that it still matches the backlog. One parser, one artifact, one answer at a time,
      and a gate that fails loudly the moment they drift.
      
        tools/setlist.py         the parser. Resolves PLN's freeform codenames
                                 ("Ceci n'est pas Une Bombe" -> bombe_dj, "Le shifteur
                                 marteau ? to finish!" -> electric_hammer) via aliases +
                                 token containment, and raises rather than guessing.
        gig-up --converge        regenerates the setlist BEFORE preload. That order is
                                 load-bearing: preload reads the setlist, so doing it the
                                 other way warms the old set.
        gig-up "setlist vs backlog"   HARD, and placed before every check that reads the
                                 setlist — a stale list makes "setlist compiles",
                                 "preload covers set" and "transition ghosts" all answer
                                 about a set PLN is not playing.
      
      WHAT THE END-OF-SECTION DETECTION TAUGHT ME. First attempt whitelisted the
      headings that count as part of the set ({FINALE, Encore, ...}) and died on line
      one against "## Livecoding Techno DNB Nu-jazz". PLN's headings are prose; any
      whitelist is a list of the ones he happened to write that day. The working rule
      ignores heading semantics entirely: run to the next top-level heading, take the
      list items. Non-track content does not survive that filter — "> Sunset Forest",
      "1h30 - 20h30-22h", "??TRANSITION???" and the trailing url: block are none of
      them list items.
      
      VALIDATION, and specifically not "it returns the 15 tracks currently in the
      backlog" — a hardcoded list passes that too. tools/tests/test_setlist.py MUTATES
      a synthetic backlog and asserts the output moved: add a track (appears, in PLN's
      position, not appended), remove one (disappears), swap two lines (output swaps),
      unresolvable name / duplicate / missing section (all raise). 18/18.
      Then the gate itself, mutated: dropped a line from the .txt -> caught; swapped
      two lines, same membership -> caught as "same tracks, DIFFERENT ORDER".
      
      AND IT PAID OFF IMMEDIATELY. With the real 15 in the setlist, pvlint went red on
      two tracks that had been invisible to every tool until today: an unmutable orbit
      in ouais_je_funk (gF with no gM3 — nothing on the surface can drop it) and button
      ^44 driving both d3 and d6 in lime_tacos (one press, two orbits). Neither is new
      breakage; both were simply never looked at. Left for PLN — they are musical calls.
      
      The 53 banks are warm on the rig now: 54/54 banks OK in 7.0 s.
      PLN (Algolia) authored
    • live(desire): drop the lpf sweep, open the room, push crushbus · fbe427ea
      Reworking d4's texture: the lpf range sweep comes out (resonance stays), crushbus
      goes 16->3.5 on ^52, and room 0.4 / sz 0.5 / dry 1.1 opens the tail.
      PLN (Algolia) authored
    • docs(tasks): archive #115/#120/#121 — the day every layer saw the failure and none acted · 0d203a02
      Three closed tasks from the 2026-08-01 session, written for a cold reader. The
      through-line worth mining later: the rig SENSES everything and ACTUATES nothing.
      scsynth died and sclang logged it, the Bridge warned 63 times, gig-up saw it and
      printed GO — and separately, PLN's ears found a preload bug that four green tools
      had missed. Plus the session's sharpest lesson: two of my own fixes (a watchdog, a
      chmod) were themselves broken in exactly the way they were meant to catch.
      PLN (Algolia) authored
    • live(wip): ouais_je_funk kick rework + octave drop, crushbus on desire · 23f60e76
      PLN's own rehearsal edits, 2026-08-01, saved before a break. Committed as their own
      human commit so nothing is lost — IN PROGRESS, not a finished arrangement.
      
      * ouais_je_funk — the kick becomes gate-driven instead of static:
        "[jazz,kick:5]*4" -> midiOn "^41" / midiOff "^41" variants over a layered
        "[techno:0,808bd:2,909,kick:4]" with gain 1.5 # lpf 400. Old "kick:5" kept
        commented rather than deleted.
        Also: the lead drops an octave — note "<b b b g>@7 <cs6!3 fs6>" is now
        ("..." - 12); the superimpose gate moves ^18 -> ^20; crushbus 101 added on ^18;
        and both d11 arpeggio blocks (pinkyup/thumbupdown, diverge/disconverge) are
        commented out rather than removed.
      * desire — crushbus 41 (range 16 2.5 "^52").
      
      Left as PLN saved it. He resumes rehearsing Saturday afternoon.
      PLN (Algolia) authored
    • feat(gig): gig-up --converge, and the gate finally checks its own hands (#121, #122) · 8d307401
      --converge is the CLI big red button: bring the rig to ready, then let the existing
      checks prove it. Scope is SERVICES, not apps — it starts systemd units it owns and
      REPORTS Pulsar/Ardour with the command instead of launching them, because a script
      that opens windows under someone is a script they stop trusting (and Ardour has a
      session + crash-recovery dialog, #20). Launching apps belongs to the Bridge (#116).
      
      Safe at any moment: every action is conditional on the thing being broken, so a
      healthy rig converges to a no-op — verified, scsynth PID unchanged. It handles the
      exact 2026-08-01 failure explicitly: unit ACTIVE but audio server DEAD needs a
      RESTART, since `start` is a no-op on an active unit. Distinguishing those two is the
      whole lesson. Preload is fixed BEFORE SuperDirt boots, or the warm would not happen
      until the next restart. And it waits for scsynth to appear rather than guessing —
      guessing is what made the first watchdog flap.
      
      === THE THIRD TIME, AND THIS TIME IT WAS US ===
      
      gig-up's first run (1219c704) found check-boot.sh and check-tracks.sh at mode 644 —
      never runnable as `tools/check-tracks.sh`. That fix turned out to be FAKE: the chmod
      only ever touched the working tree. Git has recorded 100644 for both from that day to
      this, so every fresh clone still got a broken pre-gig gate.
      
      And gig-up.sh had the same bug, from birth. Committed 100644 by 1219c704 and again by
      6ab09eaa. It ran only because the local working tree happened to carry the bit; a
      `git checkout master` materialised it at 644 and it died with "permission denied".
      
      That failure was nearly invisible, which is the real story: the caller filtered its
      output through sed, so a run that never executed LOOKED like a clean no-op, and I
      reported "no-op confirmed" about a script that had not run. A false green produced by
      the absence of a program.
      
      So the gate now checks its own hands — "tools executable", HARD, verifying BOTH the
      on-disk bit and what git records. It found four real problems on its first run:
          disk=x  git=100644  check-boot.sh          <- day-one "fix" never committed
          disk=x  git=100644  check-tracks.sh        <- same
          disk=-  git=100644  sc-watchdog.sh         <- not executable AT ALL
          disk=-  git=100644  tests/test-sc-watchdog.sh
      The third is the one that matters: parvagues-sc-watchdog.service had been ENABLED AT
      BOOT with an ExecStart it could not execute. It was stuck `activating`, restart-
      looping silently. The supervisor I added to catch silent failure was itself failing
      silently, and nothing would have said so until an audio server died at a venue and
      did not come back. All six now 100755 on disk and in the index; watchdog restarted
      and confirmed logging.
      
      Same family as the day-one finding: a thing verified once, in a context that no
      longer holds. The new twist is that "chmod +x" is not a fix — recording it in git is.
      
      Also honest-ing the recovery toast (#122). PLN reported needing a ctrl+enter in
      Pulsar to resume after a recovery. Likely ICMP port-unreachable poisoning Tidal's
      connected UDP socket (ECONNREFUSED on next send, stream stops until re-evaluated).
      Until #122 tests it, the toast says "if the music does not resume on its own,
      ctrl+enter in Pulsar" rather than claiming a full recovery — a rig that is alive and
      silent is indistinguishable on stage from the original fault.
      
      Validation: 16/16 green on the live rig (9 cold + 7 live). Converge no-op verified on
      a healthy rig; converge ACTION verified by stopping the watchdog and watching it come
      back, with scsynth untouched throughout.
      PLN (Algolia) authored
    • feat(gig): gig-up --live — prove a RIG exists, not just a set (#121, #120) · 6ab09eaa
      gig-up proved the set COMPILES and said nothing about whether anything could make
      a sound. scsynth was read only to colour a banner (:106,:115) and to skip --audio
      (:202) — it never failed. This morning the audio server had been dead for two days
      and the gate would still have printed GO.
      
      --live is a MODE, not the default. Cold-by-default remains right: cold checks are
      safe mid-set and must work on a train. Severity is chosen per link, deliberately:
      
        HARD  scsynth alive / sclang :57120 / SC -> Ardour (>=24 pw links)
              no audio server, no listener, or no path to the DAW = there is no gig.
        SOFT  Tidal :6010, LCXL -> SC, SuperDirt warmed, perf mode
              all legitimately absent ten minutes before doors; a gate that reddens for
              them is a gate PLN stops reading.
      
      Every check reads the thing that MAKES SOUND — a process, a socket, a real pw-link —
      never `systemctl is-active`. That distinction is the whole reason this exists: the
      unit reported active/running for two days while the rig was mute, because MainPID is
      sclang and scsynth is its child.
      
      ALSO, and it came from PLN's ears: "preload covers set", a HARD cold check.
      
      He heard crackles moving do_it_right -> take_5_drops. Measured in that window: 0
      SuperDirt lates, 0 pipewire xruns, 17 lazy soundfile reads — all take5:*. Disk I/O,
      not CPU. The preload MECHANISM was healthy and said so ("47/47 banks OK in 1.9 s");
      it was warming the wrong list. preload.scd was dated Jul 28, header "10 track(s)
      scanned", while the set is 13 — because setlist_samples.py reads setlist_opal2026.txt
      (10 tracks) and gig-up/cheat-sheet trust set-coherence.setlist_tracks() (13). The
      missing three are do_it_right, take_5_drops, electric_hammer: exactly the pair he was
      transitioning between. #120 tracks which source becomes canonical — not guessed here.
      
      Two properties made it invisible, and they are the transferable lesson: preload.scd
      is GITIGNORED (correctly — generated), so no diff ever showed it drifting, and
      NOTHING COMPARED IT TO THE SET. A generated artifact with no freshness check is a
      stale artifact waiting for the worst possible moment; its natural debut was a cold
      boot at the venue, on the first play of every track added since Jul 28. It only
      surfaced at a desk because SuperDirt had been up since Jul 28 with those banks cached
      from a first play weeks earlier, and a restart cleared the cache.
      
      tools/check-preload.sh compares BANK SETS (not bytes — the generator stamps a header
      and orders banks, so a byte diff would cry wolf), borrows set-coherence's parser
      rather than adding a fourth definition of "the setlist", and has --fix.
      
      Validation, all of it adversarial:
       * check-preload mutation-tested — restore the Jul-28 plan and it names the 7 banks
         that would crackle: daft fbreak120 hammer popkick realclaps superfreak take5.
         hammer and superfreak are independently confirmed: they appear as lazy loads in
         this morning's journal, before any of this was written.
       * --live on the live rig: 7/7 green after fixing MY OWN check (below).
       * THE GATE BITES: run under a PATH shim whose pgrep reports scsynth absent ->
         NO-GO, exit 1, correct fix hint, real rig never touched. That is precisely the
         state that printed GO this morning.
       * cold run unchanged: still GO, live chain not executed.
      
      One check was wrong before it was right, in the session's recurring shape. "SuperDirt
      warmed" first warned about a rig that was perfectly warm, because
      `journalctl --user -u parvagues-sc` returns lines from a PREVIOUS instance on this
      box — it handed back Jul 30 output while the current instance had already logged
      "51/51 banks OK". Now filtered by SyslogIdentifier and keyed to the unit's own
      ActiveEnterTimestamp, so it asks "did THIS boot warm?" rather than "was there ever a
      PRELOAD line", which any previous boot would satisfy forever.
      PLN (Algolia) authored
    • Merge branch 'claude/sc-watchdog' · 03faf72b
      PLN (Algolia) authored
    • live(opal): octersub on, thin bombe_dj's kick stack, flag do_it_right variations · e60cec77
      PLN's own live-session edits, made at the desk on 2026-08-01 while rehearsing the
      OPAL set. Committed separately from the session's tooling work so the musical
      changes stay legible on their own.
      
      * wap — uncomment octersubsubbus 42 / octerbus 41, both on range 0 1.4 "^53".
        The sub layer is live again. (This is the `octersub` want noted in the
        2026-07-31 set-coherence report.)
      * bombe_dj — "[808bd:1,808bd:11]" -> "[808bd:11]". One kick sample instead of two
        stacked, on the 1/4/6/9/11 positions.
      * do_it_right — mark d3's gM2/gF1 line with a TODO about buttons and variations.
      PLN (Algolia) authored
    • fix(rig): the test paged a human about a unit that did not exist (#115) · 0c553fd6
      Minutes after test-sc-watchdog exited, PLN got a STICKY desktop notification:
      
          "3 restarts in 10 min. Giving up — this needs your eyes.
           journalctl --user -u scwd-test-1269280.service -n 50"
      
      scwd-test-1269280.service was the test's own FAKE transient unit. By the time the
      notification was read, it had been torn down — so the suggested command returns
      nothing, about a failure that never happened, while the real rig was playing fine.
      
      Two defects in one message. The test could reach a real notification daemon at all;
      and the give-up path is deliberately urgency=critical/sticky, which is correct for a
      real outage and actively harmful for a fake one — it cannot be dismissed by waiting.
      
      Fix: SCWD_NO_NOTIFY=1, honoured by notify() and set by the harness. A test must never
      be able to page a human about something that is not real.
      
      Re-run green: 7 passed, 0 failed, and silent. The log still shows the rate limiter
      walking (0 prior) -> (1 prior) -> (2 prior) -> GIVING UP, so suppressing the toast
      did not suppress the evidence — the assertion reads the log, not the notification.
      
      Same family as the flap it was written to catch: a supervisory tool whose side
      effects escape the scope it was reasoning about.
      PLN (Algolia) authored
    • feat(rig): the audio server can die alone — supervise scsynth, not sclang (#115) · b978a2fa
      The rig went silent overnight and NOTHING recovered it. Seven layers watched it
      happen. From the journal:
      
          10:37:36  PM: suspend entry (s2idle)
          10:48:38  systemd-coredump: scsynth terminated abnormally
          10:48:38  sclang: "Server 'localhost' exited with exit code 0."
          11:18:47  Bridge: "scsynth process not found"   x63, to the journal only
      
      scsynth is a CHILD of sclang and systemd's MainPID is sclang, so the unit reported
      `active (running)` for two days while there was no audio server at all. A green unit
      is not sound.
      
      Two findings behind it:
      
      * `parvagues-sc.service` was DISABLED. Six user units start with the session —
        gig-log, bridge, perf-tray, tidal-ardour-autoroute, lcxl-leds-watch,
        midi-autoconnect — and the only one that produces audio was not among them. It had
        been alive since Jul 28 purely as a manual leftover, which is why its death was
        permanent. Now enabled, and symlinked to the repo like the others so it cannot
        drift from the tracked copy.
      * The d8346672 fix (QT_QPA_PLATFORM=offscreen) worked exactly as designed: sclang
        survived the suspend perfectly. Suspend killed scsynth instead. The mitigation had
        been applied one layer too shallow — and because sclang stayed up, the unit never
        failed, so no Restart= setting of any kind would have fired.
      
      So: Restart=on-failure (bounded by StartLimitBurst=3/5min, honouring the unit's own
      "audio gear should never flap" comment) for the sclang case, plus an external
      watchdog for the scsynth case. Deliberately outside the boot path — a bug in
      start_and_midi.scd is gig-fatal, a bug in a watchdog is merely annoying. Folding
      this into an sclang ServerQuit handler is the right long-term shape; not seven days
      before a gig.
      
      THE FIRST VERSION FLAPPED ON PLN'S DESK, and the bug is the interesting part.
      The unit had `PartOf=parvagues-sc.service`, so that stopping SuperDirt would stop
      the watchdog. But PartOf propagates RESTARTS. Three lines, 90ms apart:
      
          11:35:46.265  sc-watchdog[1102323]: scsynth GONE — restarting
          11:35:46.321  systemd: parvagues-sc.service: Consumed 1min CPU time
          11:35:46.354  sc-watchdog[1123545]: watching scsynth        <- NEW PID
      
      The watchdog killed itself issuing the restart, Restart=always revived it, and its
      in-memory rate-limit counter reset to zero. MAX_RESTARTS was unreachable BY
      CONSTRUCTION: the state enforcing the limit was destroyed by the action it was
      limiting. It would not have stopped at four notifications; it would have gone
      forever. A supervisor must outlive its supervisee.
      
      Fixed three ways, defence in depth:
        - no PartOf on the watchdog unit (the actual fix)
        - restart log persisted to $XDG_RUNTIME_DIR, so being killed cannot launder it
        - await the server's return (up to 100s > TimeoutStopSec=90s) instead of sleeping
          a guessed 25s and judging a boot that was still in flight
      
      Also: every notification was urgency=critical, which Plasma treats as never-expire,
      so recovery buried the screen in sticky popups. Stickiness is now the signal —
      normal+timeout for "handled itself", critical+sticky only for "this needs your
      hands".
      
      Validation. tools/tests/test-sc-watchdog.sh runs the real script against a FAKE unit
      holding a FAKE child server, reproducing the green-unit-no-sound topology exactly.
      7/7 pass in ~90s and scsynth's PID is unchanged across the run — it touches no real
      audio. It pins the flap directly: "watchdog survived issuing the restart", and the
      log shows (0 prior) -> (1 prior) -> (2 prior) -> GIVING UP. Plus a 25s --dry-run
      against the live unit: zero false positives on a healthy rig.
      
      That testability is the real lesson. v1 could only be exercised by killing the
      actual audio server five days before a gig, so its flap was found by a human hearing
      four notifications instead of by a test. UNIT and PROC are now injectable.
      
      The watchdog is committed but NOT enabled: it needs one real-rig recovery test
      before it earns a place in the boot set.
      PLN (Algolia) authored
    • docs(tasks): archive #44/#114 — the orchestrator as adversarial user · 29ce99d3
      Two entries. #44's lesson is the one worth mining later: wiring six working
      checks into one caller found two of them dead, because every check had been
      verified once, interactively, by a human who supplied the missing piece without
      noticing. A human types `zsh tools/foo.sh` out of habit and never learns the
      file is not executable. An orchestrator has no habits.
      
      #114 records that the overnight constraint list did real work rather than being
      ceremony: "never write to a setlist .tidal" is what kept three ranked-"needed"
      tasks off the queue, and drawing that boundary first is why the night was
      productive instead of risky.
      PLN (Algolia) authored
  3. 31 Jul, 2026 9 commits
    • docs(morning): fold in the real corpus number — 51+21 of 694 live/ tracks · f849295c
      The sweep landed after the brief was written. Replaces the placeholder with the
      measured figure and, more importantly, with the right DENOMINATOR: study/ is
      documentation prose, so 79+30 of 799 overstates it the same way the harness bug
      did. Also surfaces the CRASH class to him directly, since those are the ones
      worth his time and the ones I deliberately did not touch.
      PLN (Algolia) authored
    • docs(corpus): the real compile-failure count, with an honest denominator (#112) · 4eb3cf01
      The clean re-run after the harness fixes. Three numbers have now been attached
      to this question and the first two were both about the tool:
      
          #112 said        14   (a baseline taken during the mute migration)
          pre-fix run     128   (the column-0 `$` artifact, ~49 of them fake)
          clean run        79 BROKEN + 30 CRASH, out of 799 files
      
      And 799 is the wrong denominator too. Split by scope:
      
          OPAL setlist        13 files    0 BROKEN   0 CRASH
          live/              694 files   51 BROKEN  21 CRASH   ~10%
          study/              59 files   13 BROKEN   3 CRASH
          sandbox/blocks/test 21 files    5 BROKEN   3 CRASH
      
      `study/` is Tidal DOCUMENTATION with inline examples — prose that was never
      meant to compile as a track. Counting it inflates the figure the same way the
      harness bug did, just more quietly. The number worth quoting is 51+21 of 694.
      
      THE GIG IS UNAFFECTED. Zero setlist tracks in either category, and gig-up is GO.
      
      The CRASH class is new and it is the valuable one. These tracks COMPILE, then
      throw when queried — invalid mini-notation inside a string, which is exactly why
      the type checker waved them through:
      
          "[d d] <d d d*2 d*4>]"     unbalanced bracket
          "~ ulgab?:1"               `?` before `:`
          "[0 .. 7 0]"               a range with a trailing element
          "<0.75 .. 0.8 0.8 .. 0.65>"  a sequence on the left of `..`
      
      Every one would fail live, because the scheduler queries every cycle. Until this
      run they were all reported as "FAIL — 0 orbit(s) SILENT".
      
      Deliberately NOT fixed here. None are in the set, and each one needs a decision
      about what PLN meant — "[0 .. 7 0]" has at least two readings and only he knows
      which. Triage is the deliverable; the edits are his call.
      
      Also records a counting caveat rather than hiding it: silent-eval prints a
      track's STEM, not its path, so stems that exist in several directories are
      marked as ambiguous instead of guessed. The real fix is repo-relative paths in
      silent-eval's output — noted on #112, not silently papered over.
      PLN (Algolia) authored
    • docs(morning): Saturday brief — the gate is GO, and #112's number was the tool's fault · caac31cb
      Leads with 'nothing needs your hands', because the previous two mornings led
      with a fader he had to raise and that framing sets the whole read. Then the two
      things that shipped, then the one finding that changes something he was already
      told: #112's '14 tracks do not compile' was a harness artifact, and the harness
      was calling his own dominant writing style a parse error.
      
      Deliberately does NOT quote a corpus-wide count. The clean re-run was still
      going; the number lands on the task instead. Quoting an unmeasured figure is
      exactly how #112 got a wrong one.
      PLN (Algolia) authored
    • test(silent-eval): pin the three bugs, and prove the pins actually bite (#112) · ee7b8b8c
      The previous commit fixed three bugs in the tool whose entire job is catching
      failures that make no noise. That makes its own failure mode the worst one
      available: saying OK about a track that does not work, or BROKEN about a track
      that does. It was doing BOTH, corpus-wide — and it stayed hidden because the 13
      setlist tracks, the only ones anyone ever pointed it at, happened to dodge all
      three.
      
      Seven tests, all structural — no ghc, no rig, 0.06s:
      
        1. a column-0 `$` is a CONTINUATION (PLN's dominant corpus style), plus a
           guard that indenting does not FLATTEN relative structure below it, plus
           one for the ordinary inline style — which gets a test precisely because
           dodging the bug is what let the bug live.
        2. a non-zero exit with no SILENT line is a CRASH, and its stderr survives
           into the detail, because stderr is the only thing that says why.
        3. the exit code answers "is anything wrong", never "is anything silent".
      
      MUTATION-CHECKED, because green tests on their own prove nothing. Each fix was
      individually reverted and the suite re-run:
      
        remove the continuation indent  -> test_dollar_at_column_zero_is_indented FAILS
        key the return to silence only  -> test_exit_code_is_not_keyed_to_silence_alone FAILS
        both restored                   -> 7 passed
      
      Test 3 is the one that matters. When crashes were split out of the silence
      count, the return value was briefly left keyed to `silent_only`, so a corpus
      where every single track failed to compile would have printed "OK — every
      declared orbit emits events" and exited 0. That is a false green from the gate,
      and a false green here is indistinguishable from a working set right up until
      the downbeat.
      
      Full suite: 229 passed.
      PLN (Algolia) authored
    • feat(gig): the surface card — generated from the grid, not copied from it (#16) · 3d91272e
      #16 was ranked "bonus, can be 80/20ed". The cheat-sheet IS the 80/20: at OPAL
      he is alone in a field with a laptop and a 48-cell control surface. The FOH
      chain and the go-bag he can hold in his head. Twelve columns of CC numbers he
      cannot.
      
      GENERATED, NOT AUTHORED. A hand-written card is a copy of tools/lcxl_grid.py,
      and a copy drifts silently the moment the grid changes — which it did twice
      this fortnight. This reads the grid and the setlist directly, so a card that
      disagrees with the rig is impossible by construction. `--md` rewrites
      docs/GIG-CHEATSHEET.md; regenerate rather than edit.
      
      THE EMERGENCY BOX GOES FIRST
      The moment you reach for this page is the moment something is wrong. A card
      that opens with a reference table makes you read past your own panic to get to
      the answer. So the top of the page is eight failure modes and their remedy,
      each one a symptom he has actually hit: LEDs dark but knobs alive (USB OUT
      endpoint stall — replug), a track making no sound (it did not compile; one
      block, one error), an orbit that will not die (it is the previous track's
      pattern), mixer silent while Tidal looks fine (a fader; the desk wins on touch).
      
      TWO LAYOUT BUGS THE FIRST RENDER CAUGHT
        * the title bar was hand-padded and off by one — now ljust'd like every
          other line, so it cannot drift again
        * gF3 and gM3 hold 7 and 8 orbits, which overran a side-by-side two-column
          family layout and silently misaligned the block. Stacked instead.
      
      AND ONE COLUMN DELETED FOR SAYING NOTHING
      The set table first carried a "roles" column. Every track in the set carries
      kick+perc+bass+lead, so it printed the identical string 13 times and pushed the
      page past 80 columns. Replaced with the orbits each track does NOT declare —
      which is the fact worth having on stage, because an undeclared orbit is not
      silent, it keeps playing whatever the previous track left there. That column is
      the transition ghost list (#77), per track, at a glance. It also shows `perfect`
      declaring all 12, i.e. the one track in the set that cannot inherit a ghost.
      
      Verified: 0 lines over 80 columns, renders from a clean checkout, and the
      family map printed matches lcxl_grid.filter_family/mute_family exactly.
      PLN (Algolia) authored
    • fix(silent-eval): the harness called 128 working tracks broken, and a crash "silence" (#112) · d280acf0
      #112 said "14 corpus tracks do not compile". Running the harness over the whole
      corpus said 128. Neither number was about the music.
      
      BUG 1 — column-0 `$` continuations reported as parse errors
        PLN writes a lot of the corpus like this:
      
            d1
            $ whenmod 128 129 (…)
            $ s "<k k*2 <k*2 k> k>"
      
        Pulsar sends a block wrapped in GHCi's `:{ … :}`, which suspends the layout
        rule, so that `$` at column 0 is a continuation and the track plays fine.
        `orbit_bindings` rewrites `dN` into `name = idcp` and emits the block into a
        real MODULE, where a column-0 `$` opens a NEW top-level declaration — a hard
        parse error. The harness then printed, in these words:
      
             THE TRACK DOES NOT COMPILE — it will fail live too.
      
        about code that has never once failed live. The intent to indent was already
        written in the comment above the line ("becomes `name = idcp` + `  $ x`");
        the indent itself was never applied. Two spaces on lines[1:] restores it and
        preserves every relative indent below.
      
        Why it stayed hidden: ZERO of the 13 setlist tracks use that style, and the
        setlist is what anyone actually runs. The harness was green on everything it
        was ever pointed at. Same shape as the chmod bug in the previous commit — a
        thing verified once, in a context that no longer covers the corpus.
      
      BUG 2 — a crashed probe reported as "FAIL — 0 orbit(s) SILENT"
        The Haskell probe sets its failure flag ONLY when it prints a SILENT line, so
        a non-zero exit with no SILENT line never measured silence at all — it threw
        while QUERYING. `check_track` mapped any non-zero return to "SILENT" and
        discarded stderr, so the one string that said what went wrong was thrown away
        and replaced by a self-contradicting verdict. New CRASH verdict keeps stderr.
      
        It immediately paid for itself. `live/chill/dub.tidal` compiles, plays three
        orbits, then throws:
      
            Syntax error in sequence:
              "<0.75 .. 0.8 0.8 .. 0.65>"
                           ^
      
        A real mini-notation bug, precisely located — and one that WOULD fail live,
        since the scheduler queries every cycle. It had been sitting behind a message
        that said zero orbits were silent.
      
      BUG 3 (latent, found while fixing 2) — a false green in the exit code
        Splitting the summary so crashes stop being counted as silence revealed that
        the return was keyed to the silence count alone. A corpus where every single
        track failed to compile would have printed "OK — every declared orbit emits
        events" and exited 0. The exit code now answers "is anything wrong", which is
        the only question a gate may answer — this is the one tool whose entire job
        is catching the failure that makes no noise.
      
      Verified: setlist still 13/13 green and gig-up still GO (so no regression on
      the path that matters); dub now reports THROWS WHEN QUERIED and exits 1;
      previously-"broken" tracks compile. The corpus-wide count is deliberately NOT
      restated here — it needs a clean re-run, and quoting a number I have not
      re-measured is how #112 got a wrong one in the first place.
      PLN (Algolia) authored
    • feat(gig): one command says GO or NO-GO — and it caught two bugs on its first run (#44) · 1219c704
      A fortnight of debugging produced a shelf of good checks: check-boot,
      check-mix, silent-eval, fix-mute-roles, pvlint, orphan-orbits. Every one of
      them worked. Every one of them was also a command PLN would have to REMEMBER —
      and on Sat 8 Aug at ~19:00 he is in a field, alone, with a soundcheck window
      and no assistant. A check you have to remember under pressure is a check you
      do not run.
      
      So gig-up.sh contains no new checking logic at all. It is a running order, an
      exit code, and a decision about which failures are GO-BLOCKING. Its whole value
      is that there is now one thing to type.
      
      WHAT IS HARD vs SOFT, and why the line is drawn there
        HARD  boot helpers    one parse error in BootTidal.hs silences EVERY track
                              at once, invisibly, while a stale ghci holds the old
                              definitions. First check for that reason.
        HARD  ardour faders   earned it three sessions running, a DIFFERENT set of
                              tracks each time: 07-28 had 05/06/08/10 at -inf,
                              07-30 had 06/10/12, 07-31 had 10. Three recurrences is
                              not bad luck, it is a property of MIDI-learned faders
                              that drift from the saved session in both directions.
        HARD  compiles / mute map / pvlint
        SOFT  transition ghosts   real (#77) and known-open. A gate that goes red for
                                  work you have consciously deferred is a gate that
                                  gets ignored.
        SOFT  LCXL present        legitimately unplugged at a kitchen table. At the
                                  venue, a warn here means the set has no hands.
      
      TWO BUGS FOUND BY THE FIRST RUN — which is the argument for building it
        1. check-boot.sh and check-tracks.sh were mode 644. NOT EXECUTABLE. The
           pre-gig audio gate has never once been runnable as `tools/check-tracks.sh`
           since it was written; it only ever ran when someone typed `zsh tools/...`.
           Every green memory of it is a green memory of a different invocation.
        2. pvlint's `--setlist` is a MODIFIER ("these paths are ordered"), not a
           mode — it still requires paths. `python3 -m pvlint --setlist` exits 2 on
           an argparse error, which from the outside looks exactly like a lint
           failure. gig-up now borrows set-coherence's setlist_tracks() rather than
           teaching a fourth file to parse the setlist.
      
      Both are the same shape and it is this rig's signature failure: a thing that
      was verified once, in a context that no longer holds. Assembling the checks
      into one caller is what re-executed them under fresh conditions.
      
      DESIGN NOTES
        * Cold by default. Makes no sound, sends no MIDI, writes no .tidal. Safe to
          run mid-set, at 3am, or backstage. The audio gate is --audio, and it skips
          itself if the cold gate is already red — booting 13 tracks to confirm what
          a typecheck just told you is 10 wasted minutes at a venue.
        * Never touches CC 77-84 (Ardour track gains; 77 down is total silence) and
          never sweeps 93 (gPanic).
        * Scope is the SETLIST, not the corpus. 14 corpus tracks do not compile
          (#112) and must never make the gig gate red.
        * Failures print FIRST and carry their fix command inline, because the read
          order in a field is "what is broken / what do I type".
        * GO states its own limit every time: check-mix reads the SAVED session, so
          green means "Ardour will BOOT with this fader up", never "the mixer IS
          correct". Saying it in those words is how the -inf bug stops hiding behind
          a clean report.
      
      Verified: NO-GO path observed for real (2 failures, both genuine, both fixed);
      GO path 7/7 green; --help, --quiet and unknown-flag paths exercised.
      PLN (Algolia) authored
    • feat(tools): set-coherence — 108 controls sit idle under his hand, and three of… · a32c6d2c
      feat(tools): set-coherence — 108 controls sit idle under his hand, and three of them are systematic (#113)
      
      PLN, 2026-07-31, having walked the OPAL set by eye:
      
        "when i went across set i saw some unmapped effects/button, a 'set coherence
         mapping review' and quick wins suggestions (bass octersub, lead slice, etc etc)
         could be good ways for you to fill-in suggest"
      
      He was right, and the number is larger than "some": **108 idle slots across 117
      declared orbits in the 13 setlist tracks.**
      
      ## Why this needed a tool rather than a read-through
      
      Absence has no line number. The surface is a fixed grid — each orbit owns a known set
      of cells (effect knob, maybe a second, gate button, maybe a second) — so a track that
      declares d5 and never references ^33 leaves d5's knob inert for that track's whole
      duration. You cannot grep for a control that isn't there; you can only compute it, by
      subtracting what the track USES from what the orbit OWNS.
      
      The tool reports both directions of the same coherence question, deliberately:
        IDLE     orbit owns a slot the track never uses   -> unused affordance (new)
        STARVED  track drives more controls than it owns  -> already covered by PV008
                                                             and the FIXME(#54)/(#94) markers
      Fixing one can create the other, which is why they belong in one report.
      
      ## Three systematic findings, not a flat list
      
      1. **The drums' own effect knobs are the deadest controls on the board.**
         d2.fx (B2 ^30) idle in 12/13 tracks, d1.fx (B1 ^29) 9/13, d3.fx (B3 ^31) 8/13.
         d1-d3 own exactly ONE per-orbit knob each — their row-C cells are the family
         filters — so this is not a spare slot going unused, it is their only knob. The
         KICK has no per-orbit effect knob bound anywhere in the entire set.
      
      2. **Row A (d9-d12 effects, A5-A8) is ~90% idle.** d11 6/6, d12 5/5, d10 6/7, d9 5/8.
         That is #98's proposal (pool A5-A8 in order of appearance) with its supporting
         numbers: four knobs almost never used, on exactly the orbits that overflow.
      
      3. **d6 has gates but NO knobs, 5 of 5 tracks.** Fallout from the d7->d6
         consolidation: ^58/^90 came along, ^34/^54 did not. And vague_de_crime's d6
         references nothing at all — no knob, no gate.
      
      ## Suggestions are grounded, not invented
      
      Measured what PLN actually binds to a per-orbit effect knob across the set — perc
      legato(4); bass lpf(3), gain(1), bandf(1); lead width(2), modIndex, filterRange,
      cutoff, resonance; kick nothing — and the suggestion table echoes that idiom before
      extending it (memory feedback_self_reference_rhythm_study: extract the grammar from
      his own tracks rather than importing generic Tidal advice).
      
      Worth recording because it inverts the ask: his two named wants sit OUTSIDE his
      measured idiom. He binds lpf to bass knobs and never octersub; slice lives on his
      buttons, not his knobs. So "bass octersub, lead slice" are additions he is reaching
      for, not corrections of drift — and saying so is more useful than pretending the
      corpus already implied them.
      
      ## Deliberately not applied
      Every suggestion changes how a track sounds and he is rehearsing on these files this
      weekend. Output is docs/2026-07-31-set-coherence.md to skim and cherry-pick. The tool
      also never suggests binding CC 77-84 / 13-16 (Ardour track gains), 93 (gPanic), or the
      family cells 49/50/51 + 73/74/75.
      
      Also confirms the #94 remap held: "borrowed" CCs — ones an orbit drives without owning
      — are 0 or 1 across every track in the set.
      PLN (Algolia) authored
  4. 30 Jul, 2026 7 commits
    • fix(gimme_acid): the EXPLOSIF knob boots at an extreme — diagnosed, not silently retuned (#111) · 91b8b958
      `# width (range 0.2 0.8 "^33") -- /!\ FIXME EXPLOSIF /!\` in gimme_acid's acid synth.
      PLN has had a hazard marker on that line for a while. The cause is mechanical and
      checkable without any audio:
      
      ^33 is an A/B knob, so BootTidal's #55 seed gives it 0. The seed policy states its own
      convention — "range <neutral> <extreme>, so 0 == neutral for effect knobs" — but 0.2 is
      NOT neutral for `width`: the preset reference PLN pasted two lines above the call says
      nominal is width = 0.51. So the line boots the synth at an extreme and the knob sweeps
      it THROUGH nominal rather than away from it, which is a good candidate for whatever
      "explosif" means to his ears.
      
      This is the exact shape BootTidal.hs already documents as an authoring bug, using
      piment_bresilien's `legato (range 0.05 2 "^52")` as its worked example, and its stated
      remedy is to fix the range in the TRACK rather than special-case the seed.
      
      Deliberately NOT applied. The candidate is `range 0.51 0.8 "^33"` — boots at nominal,
      knob only adds — but it costs the 0.2-0.51 downward half of his sweep, and that is a
      decision about how the track sounds. The analysis goes in the file next to his marker
      so the next reader inherits the diagnosis instead of re-deriving it; the sound stays
      his.
      
      Did remove one unambiguous thing: `# filterRange (range 0 8 "^53")` appeared TWICE in
      consecutive lines, same value, so the second was a no-op paste.
      
      MORNING.md rewritten for today. Leads with the finding that actually matters at J-7:
      check-mix.py reports Tidal 06/10/12 at -inf dB in the SAVED Ardour session, and
      cross-referencing those against the orbits each setlist track declares shows **9 of 13
      tracks would lose at least one orbit on a relaunch** (#62). The live faders are
      probably fine — PLN has been playing — which is precisely the bug: the fix is not
      persisted, so it dies with the process. Ctrl+S is the whole remedy, and it is his
      hands, not mine: the D row is MIDI-learned to CC 77-84 and writing from outside
      desyncs it from the physical desk.
      
      Also flagged in MORNING.md rather than guessed: yesterday's file said "OPAL Day-5" and
      today's framing was "J-7". There is no OPAL 2026 entry in Web/www/content/lives/2026/,
      which CLAUDE.md names as the canonical source for gig metadata, so the countdown is
      unresolvable from here and is asked rather than invented.
      PLN (Algolia) authored
    • refactor(corpus): 917 lines move the kick onto its own mute button — every… · 27c1581e
      refactor(corpus): 917 lines move the kick onto its own mute button — every track, not just the set (#105, #70)
      
      PLN, mid-migration, when I had scoped this to the 13 setlist tracks:
      
        "good, and not only in the set am i right? did we not do the whole refact on the
         other .tidal files on my repo?"
      
      He is right, and the reason is muscle memory: his fingers do not know which folder a
      track lives in. A half-migrated corpus is worse than either extreme, because it makes
      the surface unpredictable exactly when he reaches for an old track mid-set.
      
      ## What moved
      
      913 lines across 213 files, plus 7 orbits given a mute they never had:
      
           505+4  gM1 -> gM2      percs vacating the kick's button (the BULK of the work —
                                  not the kick itself, which is only 133 lines)
          127+1   gM2 -> gM1      the kick, arriving
            66+4  gMute2 -> gM1   same, in the legacy long spelling
              64  gM1 -> gM3
              58  gMute3 -> gM3   spelling normalisation only
           41+2   gM2 -> gM3
            12+2  gMute2 -> gM2 / gMute1 -> gM2 / gMute1 -> gM1 / gM3 -> gM2  the tail
               7  FILL            orbits with NO mute at all, in the SET only
      
      Only the setlist was filled. PLN: "ensure we have that consistently in all set, and
      flag in other non-set tracks for me to see." Outside the set a missing gate may be a
      composition choice, so docs/2026-07-30-orbits-without-a-mute.md REPORTS the rest and
      writes nothing — split into the cut that makes 2397 findings actionable:
      
          A · partial  110 files   321 blocks   uses family mutes but missed some
                                                <- the oversight he meant
          B · never    354 files  2076 blocks   no orbit has one; predates the convention
                                                <- a style era, not a mistake
      
      ## Also, in this diff: #70
      
      `live/midi/nova/dnb/liquid/you_my_sunshine.tidal` had d5 (the voice) and d11
      (`no_sunshine:4/4` chopped) both on `# cut 5`. A cut group is monophonic, so every
      d11 event killed the vocal mid-word. That is ALSO the `-- FIXME VOICE OFF` PLN had
      left on d5 line 59: one root cause wearing two labels, found by pvlint PV004 on
      2026-07-28 and confirmed by reading the file today. d11 moves to `# cut 11`, matching
      the group-N-equals-orbit-N convention d3/d6/d8/d9 already follow. The comment says
      how to revert in one line if the vocal was meant to be monophonic across both layers.
      
      ## How this was verified, given it touches 213 files PLN has to play in 7 days
      
      1. **Convergence** — `--apply` twice; second pass proposes 0 rewrites and 0 fills.
      2. **Structural proof, not spot-checks** — diffed every changed line against HEAD and
         asserted the two sides are identical once mute tokens and whitespace are stripped.
         908 of 917 changed lines are provably mute-plumbing-only; the other 9 are listed
         and each accounted for (4 fills onto head lines with no `$`, 3 trailing-whitespace,
         1 FIXME annotation, 1 the cut 5->11 fix). Nothing else in the corpus moved.
      3. **Compile-invariance argument** — gM1/gM2/gM3 are the same type and arity in
         BootTidal, so substituting one for another cannot change whether a file compiles.
         Only the 7 fills alter expression structure, and all 7 are in the set.
      4. **silent-eval --seeded on the set: 13/13 ok**, before and after.
      5. Suite: 222 passed.
      
      THE HONEST LIMIT, stated because a green check here is misleading: every button CC
      seeds to 0 at boot and gMute<N> is `midiOn "^7N" (mask "f*16")`, so all three mutes
      are equally inert until pressed. silent-eval therefore proves nothing got silenced at
      boot and NOTHING about behaviour under a press — which is the only thing this change
      alters. The real gate is PLN's ear, or pv-at (#74).
      
      NOTE FOR PLN: 213 files changed on disk. Anything open in Pulsar now has a stale
      buffer, and Pulsar saves the BUFFER — reload before evaluating or your next Ctrl-S
      silently reverts the migration in that file.
      PLN (Algolia) authored
    • feat(surface): the mutes group by ROLE, the filters by BLOC — two maps, one author (#105) · 72943be9
      PLN settled the gM question on 2026-07-30, and the answer was not the one this task
      had assumed for a day:
      
        "I want the kick, now on d1/fader1, to have its mute on F1 botrow button. I want
         the C1 knob to djf all percs, C2 only bass djf, C3 melodies DJF. however i want
         the mutes consistent across all all tracks: m1 d1 / m2 other percs / m3 all
         bass+melodics"
      
      ## The thing I had wrong
      
      #105 was framed as "align gM<N> to gF<N>" — make the mute index mirror the filter
      index so an orbit's knob and button share a column. I measured 35 mismatches in the
      set under that invariant and was ready to fix them.
      
      That invariant is WRONG, and the measurement was answering the wrong question. The
      filters and the mutes are two different gestures:
      
        FILTERS want the rhythm section as ONE BLOC     — you sweep the drums together
        MUTES want the kick SPLIT OUT on its own button — you drop the kick alone
      
      So gF groups {d1,d2,d3,d8} and gM splits d1 from {d2,d3,d8}. Under the real map the
      edit count is 55, not 35 — and `d4: gF2+gM3`, which the mirror-invariant called a
      mismatch in 11 tracks, is CORRECT. Had I shipped the "alignment" I would have moved
      11 correct lines and left the kick sharing a button with the hats.
      
      Recorded as tools/lcxl_grid.py `_FILTER_FAMILY` / `_MUTE_FAMILY` — in the ONE
      authored grid (#97), with his words above it, so the asymmetry reads as deliberate
      rather than as drift. `as_dict()` now exports `orbit_family` so the HUD can say which
      knob AND which button own an orbit; `--generate` rewrote tools/lcxl_grid.json and the
      HUD package's own copy (outside this repo, in Tools/pulsar-parvagues-hud).
      
      ## tools/fix-mute-roles.py
      
      Convergent migrator: `--apply` rewrites, `--check` exits 1 on drift (a pre-gig gate),
      `--fill` gives a mute to a block that has none, `--report-missing` writes a markdown
      report instead. Reads the map from lcxl_grid rather than restating it.
      
      ## PV012 / PV013, so it cannot re-drift
      
      PV012 flags an orbit whose gM does not match its ROLE — deliberately NOT "gM index
      != gF index", and there is a regression test asserting `d2 $ gF1 $ gM2` is clean,
      because a rule written to the mirror invariant would flag that valid line. PV013
      flags an orbit with no family mute at all: PLN's "no mute is oversight!" — nothing on
      the surface can drop such an orbit, so taking it out means editing live.
      
      ## PV013 immediately caught a bug in the migrator that wrote it
      
      Minutes after PV013 existed, it flagged `d5` in you_my_sunshine — an orbit the
      migrator had walked straight past. Cause: fix-mute-roles had its OWN orbit regex
      requiring `^dN $`, and that file's head line is `d5  -- The Voice of Love`, no `$`,
      with the gates on a commented continuation. pvlint's regex allows `dN` followed by
      `$`, `--`, or EOL. Two parsers disagreeing about what an orbit IS is the same class
      of bug as two copies of the grid, so the migrator now imports pvlint's parser and
      owns none of its own. Re-running found 14 more rewrites and 7 more missing mutes it
      had silently skipped — the miss was under-application, not corruption, but it would
      have left exactly the inconsistency this task exists to remove.
      
      Also fixes #109: the orphan-orbits hand-measurement for you_my_sunshine still
      expected d7, which PLN moved to d6 in ce887b78. Re-derived by reading the file's
      column-0 declarations, not by pasting the parser's output — a hand measurement that
      quotes the thing it checks is a tautology.
      
      Suite: 222 passed, 0 failed (8 new PV012/PV013 tests).
      PLN (Algolia) authored
    • perf(set): PLN's in-flight edits — the controls come back onto their own orbits · ce887b78
      Twelve files PLN had been working live, committed as his own change before any
      tooling touches them. Not a refactor: this is the set moving under his hands.
      
      The through-line is the #94 remap paying off. The remap had COMMENTED OUT every
      control line that fired across columns — honest, but it left orbits playing with
      their hands tied. He is now restoring them onto the CCs the orbit actually owns:
      
        gimme_acid  d1's kick variations were disabled because they rode ^42, which the
                    remap gave to d2. Restored on ^41, d1's own gate — so the kick keeps
                    its midiOn/midiOff variation without borrowing a neighbour's button.
        perfect     d5's SUPERSTAR chain was 14 commented lines. Rebuilt live on ^53/^33
                    with a nested `midiOff "^89"` mask, so the slice logic is gated by a
                    button instead of hardcoded.
        desire      the compile blocker is gone: `# pan 0.42plz /se` (line 46) is fixed.
                    That one typo, in a file with no blank lines — hence ONE do-block —
                    was killing every orbit in the track. Found by silent-eval learning to
                    blame the track instead of itself; `silent-eval --seeded` now reports
                    13/13 ok, the first time the whole OPAL setlist has built cold.
      
      Also: d7→d6 in you_my_sunshine (with ^91→^90, ^59→^58) — the same consolidation he
      made in do_it_right, freeing an orbit slot. This invalidates a hand-measured fixture
      in tools/tests/test_orphan_orbits.py; the parser is right, the fixture is stale.
      
      Orbit naming throughout (BASSE DU 3e cercle, Synth lapin speed, SuperStars, Breaks
      divins), gain tuning, and backlog notes. Structural changes stay in the next commit
      so this one reads as pure performance work.
      PLN (Algolia) authored
    • docs(tasks): archive #93/#106/#107/#31 to the structured completed log · 8c3bea37
      Four tasks closed 2026-07-29 evening: the silent-eval harness fix (and the real bug
      it uncovered), the take-pack sidecar+archive tool, and the desire rhythm study.
      Written for a cold reader per the archive's own convention — commit hashes are in
      git log, this is the narrative and the numbers that don't show up in a diff.
      PLN (Algolia) authored
  5. 29 Jul, 2026 2 commits
    • docs(morning): rewrite the review checklist — last night's version was stale · 9ec69a35
      The existing MORNING.md led with a warning about ctrl+enter fading to silence over
      4 bars (#79) — fixed and confirmed by ear hours ago ("no sample goes oblivion!! 👏").
      Leading with a solved problem buries what actually needs review tonight.
      
      Replaced with five things worth his eyes/ears, ordered by urgency: the desire
      compile break (#108, a real gig blocker), the corrected take-94 mix (v2, the trim
      threshold fix), silent-eval now covering all 13 setlist tracks, the slop renderer's
      first real output (not yet good enough to send, flagged as such), and the four
      desire bass-rhythm alternatives to A/B. Everything else from the session is
      mechanical and needs no listening, so it's kept to one short list at the bottom
      rather than mixed in with what actually needs his judgment.
      PLN (Algolia) authored
    • study(desire): the bass is a metronome with a stutter — four rhythmic alternatives (#31) · f2467db7
      PLN: "i gotta solve desire bassline is not great rythmics coulld be cooler more nto
      more ete_a_mauerpark or insouciance or haunted_house i guess."
      
      Read the three references he named and extracted what they share. The diagnosis turns
      out to be mechanical rather than a matter of taste. desire's d4 is
      
          note (scale "aeolian" "0 . 0*<1 2> . <0 3> . 0*<1 1 4>" + 2 - "[24,36]")
      
      — four EQUAL groups, the same degree in nearly all of them, never a rest, never
      displaced. The only variable is density, applied to a repeated note. That is a
      metronome with a stutter, and no amount of filter movement makes it a riff.
      
      The six mechanisms both references use and desire has none of:
       1. uneven durations via `@` (insouciance 5:1:1:1 of 8; mauerpark 1:1:14 of 16) —
          one long note holds the harmony, short notes carry the motion;
       2. the pitch pattern IS the rhythm pattern, so a new note is a new onset, instead
          of a static degree with density stacked on top;
       3. calage — `(0.125 <~)` displaces the whole line so it pushes against the kick
          rather than doubling it;
       4. accent follows rhythm — `|* gain ("1@5 0.95 0.98 0.95")` carries the SAME
          weighting as the notes;
       5. space — rests inside the phrase and whole bars masked out;
       6. the phrase is longer than the bar (4-bar alternation, plus `slow 2`).
      
      Four proposals in study/desire_bass_rhythm.tidal. A, B and C each isolate ONE
      mechanism so what he hears is attributable; D combines all six. Pitch design is held
      constant everywhere — aeolian, +2 root, "[24,36]" double — because the key was
      measured at corr=0.903 and is not what he is unhappy with.
      
      All four verified through silent-eval --seeded: they compile and every one emits
      events cold. Measured onsets per bar: B 3.0, A 4.0, C 4.5, D 5.75, against ~5.5 for
      what desire has today. D is deliberately DENSER than the current line and still
      reads as a riff — which is the control that shows the problem was never "too many
      notes", it was where they fall and how long they last.
      
      desire.tidal is untouched: it is in his dirty working tree, and it also does not
      currently compile at all (#108).
      PLN (Algolia) authored