Files
serumalt/docs/archive/JANITOR_TASKS_2026-05.md
T
biggy facd5c5b5e chore: move build output under build/ and scripts into scripts/
Layout:
- scripts/ holds the four build scripts plus the new clean.sh
- cmake/ holds mingw64-toolchain.cmake
- the scripts default to build/linux, build/macos, build/windows, build/harness
- docs/ holds the audit documents, with the round 1 task list archived

Cleanup:
- scripts/clean.sh removes generated output, with --dry-run, --staging,
  --legacy and --all, and refuses tracked, protected or outside paths
- the Windows VST3 manifest now comes from cmake/moduleinfo.json.in, so its
  version, vendor and class IDs follow cmake/Plugin.cmake instead of a
  hand-written heredoc that could drift
- deleted the unused Source/GUI/Slider, which no code constructed
- fixed the hidden double processBlock overload: three warning lines to zero
- unslopped the build scripts, the two .cjs helpers and the CMake comments
- .gitignore covers the upstream JUCE Projucer and DemoRunner binaries

Verified: five scripts pass bash -n; the harness passes CTest with zero
warning lines; the Linux VST3 and Standalone link; the Windows cross-build
passes its x86-64, runtime-import and entry-point checks; each configure
exposes only its own targets; and a full build leaves Source/ and
third_party/JUCE unchanged. The generated manifest is identical to the
previous hand-written one.
2026-10-06 09:06:53 +02:00

33 KiB

Prepared janitor tasks

Prepared from the current working tree on branch main, using the graft context graph and a read-only verification pass. All 14 tasks were then dispatched to janitors-coder agents on the user's confirmation and completed. See the Execution status section at the end of this file for what each agent changed and the one remaining limitation.

How to read this file

Each task is a handoff for one janitors-coder agent. The shape follows the janitor skill: observed problem, desired behavior, file locations, acceptance check, and conflicts.

Task types are Defect fix when current behavior is wrong for the user, and Cleanup when the change should preserve behavior. Fixes that change what the user sees or hears are marked Risk.

Baseline at preparation time. A clean harness build and CTest pass: BUILD_DIR=/tmp/serumalt-check ./build_harness.sh --run reaches 100% tests passed. The build also reports first-party compiler warnings (task JT-05).

Ownership before dispatch

The worktree is dirty. These files have uncommitted edits and should be committed or frozen before any task touches them:

Source/FXProcessor.cpp        Source/PluginEditor.cpp
Source/SynthVoice.cpp         Source/SynthVoice.h
README.md                     AGENTS.md
AUDIT_REPORT.md               opencode.json
Source/AGENT.md -> AGENTS.md  (staged rename)
Source/*/AGENT.md -> AGENTS.md (staged renames)

The Source/FXProcessor.cpp edit repairs code that does not compile at HEAD (HEAD declares a two-dimensional units array in FXProcessor.h but indexes it as one-dimensional in FXProcessor.cpp). Commit it first. Do not let two agents edit the same file at once.

Task index

Id Title Type Priority Primary files
JT-01 Remove the unused look-and-feel Cleanup P1 Source/PluginEditor.h, Source/GUI/
JT-02 Make the LFO preview use the real LFO shape Defect fix P1 Source/LFO.{h,cpp}, Source/GUI/LFODisplay.{h,cpp}
JT-03 Give macros one set of names Defect fix P1 Source/MacroControls.*, Source/PluginProcessor.cpp, Source/PluginEditor.cpp
JT-04 Report rejected mod and macro inserts Defect fix P1 Source/PluginEditor.cpp
JT-05 Drive first-party compiler warnings to zero Cleanup P2 several Source/ files
JT-06 Use FXProcessor::fxTypeName for the FX type list Cleanup P2 Source/PluginEditor.cpp
JT-07 Remove unused members and dead tracking Cleanup P2 Source/PluginEditor.h, Source/Filter.*, Source/Engine.*
JT-08 Remove dead parameters and discarded computations Cleanup P2 Source/SubOscillator.*, Source/Oscillator.*, Source/Wavetable.cpp, Source/Filter.*, Source/FilterBank.*
JT-09 De-duplicate the test render loop Cleanup P2 Source/Tests/TestMain.cpp
JT-10 Unify pi and two-pi constants Cleanup P2 Source/EffectUnits/*, Source/Wavetable.cpp, Source/SubOscillator.h, Source/GUI/LFODisplay.cpp, Source/EffectUnits/Biquad.h
JT-11 Share the envelope curve and mark the filter preview Cleanup P2 Source/Envelope.*, Source/GUI/EnvelopeDisplay.cpp, Source/GUI/FilterDisplay.cpp
JT-12 Reuse paramIds:: in PluginProcessor Cleanup P3 Source/PluginProcessor.cpp
JT-13 Guard the mono-buffer and capacity assumptions Hardening P3 Source/FXProcessor.cpp, Source/SynthVoice.cpp
JT-14 Fix stale guidance and remaining prose slop Cleanup P3 AGENTS.md files, .claude/, build_windows.sh, AUDIT_REPORT.md

JT-01 Remove the unused look-and-feel

Type. Cleanup. Priority. P1. Risk. None visible. The class was never installed, so deleting it changes nothing on screen. This outcome was chosen by the user on 2026-05-10: keep JUCE defaults.

Observed problem. Source/PluginEditor.h:39 declares SerumLookAndFeel laf, and no source file installs it. A repo-wide search finds no setLookAndFeel or setDefaultLookAndFeel call, and git log -S setLookAndFeel shows none ever existed. Every control draws with JUCE's default look-and-feel, and the custom drawRotarySlider and drawLinearSlider in Source/GUI/SerumLookAndFeel.h have never run. The class and its member are dead, and Source/GUI/AGENTS.md:19 still requires controls to be drawn through it. Source/GUI/Knob.h:11 repeats the claim.

Desired behavior. JUCE's default look-and-feel stays in use, the dead class and member are gone, and the guidance matches the code.

Work. Delete Source/GUI/SerumLookAndFeel.h. Remove the include at Source/PluginEditor.h:5 and the laf member at Source/PluginEditor.h:39. Rewrite the comment at Source/GUI/Knob.h:11 so it no longer says the knob is styled by SerumLookAndFeel. In Source/GUI/AGENTS.md, drop SerumLookAndFeel.h from Owned files and remove the "Draw all controls through SerumLookAndFeel" rule. The header is header-only and absent from SERUMALT_SOURCES, so CMake needs no change.

Files in scope. Source/PluginEditor.h, Source/GUI/SerumLookAndFeel.h (delete), Source/GUI/Knob.h, Source/GUI/AGENTS.md.

Acceptance. BUILD_DIR=/tmp/serumalt-check ./build_harness.sh --run passes. grep -rn "SerumLookAndFeel" Source/ returns nothing. The GUI is unchanged, because the class was never installed.


JT-02 Make the LFO preview use the real LFO shape

Type. Defect fix. Priority. P1. Risk. Changes the LFO display. The audio path should not change.

Observed problem. Source/GUI/LFODisplay.cpp:23-52 re-implements every LFO shape. The audio path implements the same shapes in Source/LFO.cpp:65-96. Two problems follow.

  1. Sample and hold disagrees. The display computes a fixed multiplicative hash at Source/GUI/LFODisplay.cpp:31-32 (((step * 2654435761u) & 0xffffu) / 32768.0f - 1.0f), while the audio returns holdValue, which comes from juce::Random (Source/LFO.cpp:13,79,131-133). The preview shows a sequence the synth never plays.
  2. The remaining shapes can drift. Triangle, saw, square, step and freehand are copied at LFODisplay.cpp:26-28,34-52 and LFO.cpp:73,75,77,80-92.

The display also hardcodes 64 at Source/GUI/LFODisplay.cpp:8,9,10,17, while LFO.h:16 already exposes static constexpr int kShapePoints = 64;.

Desired behavior. One function decides the value of a shape, and both the audio render and the display call it. The preview then cannot disagree with the sound.

Work. Extract the shape switch from LFO::shapeValue (Source/LFO.cpp:65-96) into a shared, pure function that takes phase, shape, step count and the shape buffer, for example float lfoShapeValue (float phase, LfoShape shape, int steps, const float* data, int dataSize). Call it from both LFO::shapeValue and LFODisplay::valueAt. Replace the hardcoded 64 in LFODisplay with LFO::kShapePoints. For sample and hold, the preview should not invent values; either have the display read the live hold value through Engine::getLfos() (Source/Engine.h:42), or draw the S&H shape as a clearly labelled schematic and stop pretending it is the played sequence.

Files in scope. Source/LFO.h, Source/LFO.cpp, Source/GUI/LFODisplay.h, Source/GUI/LFODisplay.cpp.

Acceptance. BUILD_DIR=/tmp/serumalt-check ./build_harness.sh --run passes. A focused check confirms the display and LFO::shapeValue return the same value for the same phase and shape, including sample and hold. State which S&H approach you took.


JT-03 Give macros one set of names

Type. Defect fix. Priority. P1. Risk. The macro parameter display names change in the host. Parameter IDs do not change, so saved projects still load.

Observed problem. The same four macros have two names. Source/MacroControls.cpp:30-40 returns "Energy", "Width", "Drive" and "Atmosphere". The APVTS parameters are named "Macro 1" through "Macro 4" at Source/PluginProcessor.cpp:168-171, and the assignment combo repeats "Macro 1" through "Macro 4" at Source/PluginEditor.cpp:686. The macro panels and the macro list use MacroControls::macroName (Source/PluginEditor.cpp:668-669,948). A user sees "Energy" on the panel and "Macro 1" in the host automation lane and in the assign combo.

Desired behavior. One source of truth for the four macro names.

Work. Make MacroControls::macroName the source of truth. Use it to build the APVTS display names in PluginProcessor.cpp and the macroAssignIndex items in PluginEditor.cpp. Leave the parameter IDs (ids::macro1 and friends) and the normalized values unchanged.

Files in scope. Source/MacroControls.h, Source/MacroControls.cpp, Source/PluginProcessor.cpp, Source/PluginEditor.cpp.

Acceptance. BUILD_DIR=/tmp/serumalt-check ./build_harness.sh --run passes. The host automation names, the assign combo and the panel labels show the same four strings. grep -rn '"Macro 1"' Source/ returns only Source/Params.cpp:22, which is the stable ModSource serialisation token used by saved state and must not change.


JT-04 Report rejected mod and macro inserts

Type. Defect fix. Priority. P1. Risk. Small UI change, no audio change.

Observed problem. ModulationMatrix::addConnection and MacroControls::addAssignment return bool and reject inserts past kMaxConnections or kMaxAssignments (Source/ModulationMatrix.cpp:6-12, Source/MacroControls.cpp:6-13). The editor ignores both results at Source/PluginEditor.cpp:550 and Source/PluginEditor.cpp:716, so a click that fails looks identical to a click that worked. The list does not change and no message appears.

Desired behavior. A rejected insert is visible to the user.

Work. Check the returned value at both call sites. The smallest useful fix is to disable or grey the Add buttons when the relevant count reaches its cap, and to keep the list text and selection unchanged on rejection. A status message is also acceptable. Do not change the caps.

Files in scope. Source/PluginEditor.cpp. Read the caps from ModulationMatrix::kMaxConnections and MacroControls::kMaxAssignments; do not hardcode the numbers.

Acceptance. BUILD_DIR=/tmp/serumalt-check ./build_harness.sh --run passes. With the connection or assignment count at its cap, the Add control is visibly unavailable and clicking it changes nothing.


JT-05 Drive first-party compiler warnings to zero

Type. Cleanup. Priority. P2.

Observed problem. The harness build compiles with -Wall -Wextra plus JUCE's recommended warning flags and reports more than fifty warning lines from files under Source/. Representative sites, all current:

  • Source/SynthVoice.cpp:131,138 sign conversion from int to size_t.
  • Source/GUI/LFODisplay.cpp:11,12,16,32 sign conversion and a float equality.
  • Source/PluginEditor.cpp:88 fmtSlope is defined and never used.
  • Source/Oscillator.cpp:15 unused parameter freqHz.
  • Source/GUI/Slider.cpp:6 a local named formatter shadows a member.
  • Source/GUI/Knob.cpp:77 a local named cursor shadows a member.
  • Source/Params.cpp:8,33,58,97, Source/Oscillator.cpp:85,108, Source/LFO.cpp:68, Source/Filter.cpp:57, Source/FXProcessor.cpp:53, Source/GUI/FilterDisplay.cpp:13, Source/GUI/LFODisplay.cpp:23 -Wswitch-enum cases.
  • -Wfloat-equal comparisons in Source/GUI/EnvelopeDisplay.cpp:8, Source/GUI/FilterDisplay.cpp:40, Source/GUI/WaveformDisplay.h:18, Source/Filter.cpp:99, Source/LFO.cpp:43, Source/Oscillator.cpp:28,29, Source/MacroControls.cpp:82, Source/PluginProcessor.cpp:409.

Desired behavior. The harness build reports no warnings from Source/. Warnings from third_party/JUCE are out of scope.

Work. List the warnings with:

BUILD_DIR=/tmp/serumalt-check ./build_harness.sh 2>&1 | grep 'warning:' | grep -v third_party

Fix the unused function, the unused parameter and the two shadowed locals by deletion or rename. Fix the sign-conversion sites with explicit (size_t) or (juce::uint32) casts, matching the surrounding style. For -Wswitch-enum, name every enumerator explicitly. For -Wfloat-equal, check the meaning of each comparison first. Some are deliberate cache keys that compare a stored parameter against the current one. If the exact comparison is intended, keep the behavior and make the intent explicit with a small named helper or a narrow, commented suppression. Do not silently weaken a change-detection check.

Files in scope. The Source/ files named above and any other first-party file that the command reports.

Acceptance. The command above prints nothing from Source/. BUILD_DIR=/tmp/serumalt-check ./build_harness.sh --run passes.


JT-06 Use FXProcessor::fxTypeName for the FX type list

Type. Cleanup. Priority. P2.

Observed problem. FXProcessor::fxTypeName is declared at Source/FXProcessor.h:47 and defined at Source/FXProcessor.cpp:51-67 with no callers. The same ten labels are written again as a literal juce::StringArray at Source/PluginEditor.cpp:604-605. The two lists must stay in the same order as the FxType enum, and nothing enforces that.

Desired behavior. The FX type combo is built from the one label function.

Work. In PluginEditor::buildFxTab, replace the literal array with a loop over (int) FxType::Count that adds FXProcessor::fxTypeName (t). Confirm the combo indices still line up with FxType values, since the combo index is used directly as the parameter value (Source/PluginEditor.cpp:246 area and the FX type attachment).

Files in scope. Source/PluginEditor.cpp, Source/FXProcessor.h.

Acceptance. BUILD_DIR=/tmp/serumalt-check ./build_harness.sh --run passes. The combo shows Off through Reverb in enum order and selecting a type still sets the matching slot type. FXProcessor::fxTypeName now has a caller.

Conflict. Source/FXProcessor.cpp and Source/PluginEditor.cpp both have uncommitted edits. Commit or freeze them first.


JT-07 Remove unused members and dead tracking

Type. Cleanup. Priority. P2.

Observed problem. Four unused declarations remain:

  1. Source/PluginEditor.h:66 declares std::vector<juce::Component*> oscAComponents, oscBComponents;. Neither name appears anywhere else in Source/.
  2. Filter::svfLow and Filter::svfHigh are declared at Source/Filter.h:53,55 and defined at Source/Filter.cpp:108,130. Each name occurs twice, at the declaration and the definition. The sibling svfBand is used at Source/Filter.cpp:222.
  3. Engine::getActiveVoiceCount (Source/Engine.h:46) has no callers, so activeVoiceCount (Source/Engine.h:70) is written at Source/Engine.cpp:59 and Source/Engine.cpp:363 and never read. The per-block tally at Source/Engine.cpp:360-362 exists only to feed it.
  4. PluginEditor.cpp:88 fmtSlope is unused, which the compiler also reports (task JT-05 overlaps here; one agent should own both).

Desired behavior. No unused storage and no work that feeds nothing.

Work. Delete the two PluginEditor vectors. Delete svfLow and svfHigh from the header and the implementation. Delete the getter, the atomic field, the reset() store and the per-block tally, unless you can point to an intended caller and wire it up instead. Delete fmtSlope unless a knob needs it; the slope readout currently uses a different formatter.

Files in scope. Source/PluginEditor.h, Source/Filter.h, Source/Filter.cpp, Source/Engine.h, Source/Engine.cpp. Optionally Source/PluginEditor.cpp for fmtSlope.

Acceptance. BUILD_DIR=/tmp/serumalt-check ./build_harness.sh --run passes and grep -rn "oscAComponents\|oscBComponents\|svfLow\|svfHigh\|getActiveVoiceCount\|activeVoiceCount" Source/ returns nothing.


JT-08 Remove dead parameters and discarded computations

Type. Cleanup. Priority. P2.

Observed problem. Four sites compute or accept values that nothing uses:

  1. Source/SubOscillator.cpp:9-11 computes mult from octaveShift, then discards it and freqHz with (void) casts. Only phase = 0.0; has an effect. The octave shift is already baked into freqHz by the voice (Source/SynthVoice.cpp:200,212). The only caller passes -1 for the shift (Source/SynthVoice.cpp:58).
  2. Source/Oscillator.cpp:15 accepts freqHz only to feed jassert (freqHz > 0.0). In a release build the assert compiles out and the parameter is unused.
  3. Source/Wavetable.cpp:238 and :244 compute const float u = ... and discard it with (void) u at :240 and :246, while :248 immediately recomputes the same expression as tt, which is the value actually used.
  4. Source/Filter::prepare accepts maxBlockSize and discards it at Source/Filter.cpp:28. FilterBank::prepare forwards it to four Filter objects (Source/FilterBank.cpp:8-11), so the parameter is dead end to end.

Desired behavior. Signatures and locals describe what the code uses.

Work. Reduce SubOscillator::noteOn to the work it does, which is resetting phase, and update the one caller. Drop freqHz from Oscillator::noteOn and update the two callers at Source/SynthVoice.cpp:170-171. Delete the two u locals and both (void) u casts in Wavetable.cpp, keeping tt. Drop maxBlockSize from Filter::prepare, FilterBank::prepare and their headers, and update the FilterBank call at Source/SynthVoice.cpp:20. The FXUnit interface keeps its prepare (double, int) shape because all units share it, and the units that ignore the block size already leave the parameter unnamed, which is the existing convention. See Source/EffectUnits/AGENTS.md and Source/synth/AGENTS.md.

Files in scope. Source/SubOscillator.h, Source/SubOscillator.cpp, Source/Oscillator.h, Source/Oscillator.cpp, Source/Wavetable.cpp, Source/Filter.h, Source/Filter.cpp, Source/FilterBank.h, Source/FilterBank.cpp, Source/SynthVoice.cpp.

Acceptance. BUILD_DIR=/tmp/serumalt-check ./build_harness.sh --run passes. grep -rn "(void)" Source/SubOscillator.cpp Source/Wavetable.cpp Source/Filter.cpp returns nothing. The sub oscillator still tracks the voice frequency, because that frequency is applied in SynthVoice::render.

Conflict. Source/SynthVoice.cpp has uncommitted edits.


JT-09 De-duplicate the test render loop

Type. Cleanup. Priority. P2.

Observed problem. Source/Tests/TestMain.cpp repeats the same render loop twice. The init-preset block at lines 49-74 and the per-preset block at lines 76-105 do the same work: build three note-ons, render 240 blocks of 512 samples, release at block 180, accumulate a NaN flag and a peak, compare against 0.001f, print, and count failures. The RAVE block at lines 107-130 is a smaller variant.

Desired behavior. One helper runs a preset and returns whether it passed.

Work. Extract a function, for example bool runPreset (serum::SerumAltAudioProcessor&, const juce::String& label, double maxPeakFloor), and call it for the init case and for each factory preset. Keep the output format and the exit code behavior. Keep the rule from Source/Tests/AGENTS.md: check non-silence across the whole render, not per block, because a release can produce a legitimately silent block.

Files in scope. Source/Tests/TestMain.cpp.

Acceptance. BUILD_DIR=/tmp/serumalt-check ./build_harness.sh --run passes and still prints one PASS line per preset.


JT-10 Unify pi and two-pi constants

Type. Cleanup. Priority. P2.

Observed problem. Source/synth/AGENTS.md and Source/EffectUnits/AGENTS.md require juce::MathConstants. Raw literals and local constants remain:

  • Source/SubOscillator.h:26 kTwoPi = 6.28318530717958647692.
  • Source/Wavetable.cpp:21 twoPi = 6.28318530717958647692.
  • Source/Wavetable.cpp:156 kPi = 3.14159265358979323846f.
  • Source/EffectUnits/Chorus.cpp:40,41 two-pi literals, :46 a pi literal.
  • Source/EffectUnits/Flanger.cpp:38,39 two-pi literals.
  • Source/EffectUnits/Phaser.cpp:30,31 two-pi literals.
  • Source/GUI/LFODisplay.cpp:25 6.2831853f.
  • Source/EffectUnits/Biquad.h:31,45,60,75,90 write 2.0 * juce::MathConstants<double>::pi, which should be twoPi.

Already converted and correct: Source/Oscillator.h:72, Source/Filter.cpp:8, Source/LFO.cpp:71, Source/Oscillator.cpp:63 (half pi, leave it).

Desired behavior. One spelling for each constant, taken from juce::MathConstants.

Work. Replace each literal with juce::MathConstants<double>::twoPi or juce::MathConstants<float>::twoPi as the surrounding precision requires, and pi where only pi is meant. Remove the now-unused local constants. Keep the per-sample DSP value types unchanged, so an f-suffixed literal becomes juce::MathConstants<float>::twoPi and a double literal becomes the double form.

Files in scope. Source/SubOscillator.h, Source/SubOscillator.cpp, Source/Wavetable.cpp, Source/EffectUnits/Chorus.cpp, Source/EffectUnits/Flanger.cpp, Source/EffectUnits/Phaser.cpp, Source/EffectUnits/Biquad.h, Source/GUI/LFODisplay.cpp.

Acceptance. BUILD_DIR=/tmp/serumalt-check ./build_harness.sh --run passes. grep -rn "6.28318530717958647692\|3.14159265358979323846\|6.2831853f" Source/ returns nothing. The harness reports no NaN and preset peaks match the values from the unmodified build.


JT-11 Share the envelope curve and mark the filter preview

Type. Cleanup. Priority. P2.

Observed problem. Two smaller duplications remain between DSP and display.

  1. Envelope curve. Source/Envelope.cpp:44-45 computes attackShape = 0.3 + curve * 2.7 and decayShape = 3.0 - curve * 2.7. Source/GUI/EnvelopeDisplay.cpp:22-23 repeats both formulas with f suffixes.
  2. Filter preview. Source/GUI/FilterDisplay.cpp:6-36 re-implements a magnitude response that does not match the DSP in Source/Filter.cpp:87-88. The display uses q = 0.6f + res * 14.0f, while the DSP uses k = clamp (2.0 * (1.0 - res), 0.05, 2.0). The header calls the view approximate (Source/GUI/FilterDisplay.h:11,17), but the re-implementation site has no comment at all.

Desired behavior. The envelope preview cannot drift from the envelope DSP, and the filter preview states at the code that it is schematic.

Work. Add one shared helper for the curve mapping, for example attackShapeFor (float curve) and decayShapeFor (float curve) in Source/Envelope.h, and call it from both Envelope::setParams and EnvelopeDisplay::paint. Add a short comment at the top of FilterDisplay::magnitude that says the response is a schematic preview and does not model the DSP, the drive, the slope or modulation, and point to Source/Filter.cpp. Do not try to make the preview numerically match the nonlinear DSP.

Files in scope. Source/Envelope.h, Source/Envelope.cpp, Source/GUI/EnvelopeDisplay.cpp, Source/GUI/FilterDisplay.cpp.

Acceptance. BUILD_DIR=/tmp/serumalt-check ./build_harness.sh --run passes. grep -n "2.7" Source/Envelope.h Source/Envelope.cpp Source/GUI/EnvelopeDisplay.cpp returns the two uses in the shared helper only, and nothing in the two callers. FilterDisplay.cpp carries the approximate comment.


JT-12 Reuse paramIds:: in PluginProcessor

Type. Cleanup. Priority. P3.

Observed problem. Source/Params.h:229-269 defines the paramIds:: indexed groups. Source/Engine.cpp and Source/PluginEditor.cpp now iterate those groups. Source/PluginProcessor.cpp still builds its own arrays that mirror the same groups:

  • :102-106 five envelope arrays.
  • :121-127 seven LFO arrays.
  • :142-155 FX type, mix and parameter arrays.

The arrays reference ids::, so no string literal is duplicated. The structure is.

Desired behavior. The parameter layout walks the shared groups instead of re-declaring parallel arrays.

Work. Replace the local arrays with paramIds::envAttack and the other groups, or iterate them directly. Keep the parameter names and defaults exactly as they are, because they define the host-visible layout and the saved-state order. Do not change parameter IDs.

Files in scope. Source/PluginProcessor.cpp.

Acceptance. BUILD_DIR=/tmp/serumalt-check ./build_harness.sh --run passes. The parameter count and order are unchanged. A saved state from before the change still loads.


JT-13 Guard the mono-buffer and capacity assumptions

Type. Hardening. Priority. P3. Risk. No behavior change for the current 2-channel, 64-sample path.

Observed problem. FXProcessor::process reads and writes channel 1 unconditionally (Source/FXProcessor.cpp:92,98). It is safe today only because Engine always hands it a 2-channel view (Source/Engine.cpp:307). A future one-channel caller reads a null channel. Similarly, FXProcessor::process and SynthVoice::render trust the sample count to fit the buffers sized in prepare (Source/FXProcessor.cpp:38, Source/SynthVoice.cpp:23) with no assert. The engine's 64-sample segmentation (Source/Engine.cpp:270) is the only thing keeping that true.

Desired behavior. The boundaries are explicit, and a debug build catches a violation instead of corrupting memory.

Work. In FXProcessor::process, handle buffer.getNumChannels() < 2 by processing only the channels present, or by returning early with a jassert. Add jassert (n <= wet.getNumSamples()) and jassert (numSamples <= scratch.getNumSamples()) at the top of the two render functions. Do not add allocation and do not change the audio-thread behavior for valid input.

Files in scope. Source/FXProcessor.cpp, Source/SynthVoice.cpp.

Acceptance. BUILD_DIR=/tmp/serumalt-check ./build_harness.sh --run passes. A debug build asserts when SynthVoice::render is called with numSamples > scratch.getNumSamples().

Conflict. Both files have uncommitted edits.


JT-14 Fix stale guidance and remaining prose slop

Type. Cleanup. Priority. P3.

Observed problem. Two kinds of documentation drift.

Guidance that contradicts the code:

  • Source/params/AGENTS.md says to use the ids:: namespace for every parameter ID and never to re-declare one outside Params.h. It does not mention the paramIds:: indexed groups at Source/Params.h:229, which the engine and editor now use.
  • Source/synth/AGENTS.md says "Wavetables are prebuilt during Engine construction" but its example still ends with // in prepareToPlay. The real call is Source/Engine.cpp:22 in the constructor.
  • Source/engine/AGENTS.md shows a "GOOD" example that calls mixBuffer.clear() in processBlock. The code instead builds a non-owning view per segment (Source/Engine.cpp:307). Update the example to match.

Prose that still violates the unslop rules:

  • .claude/skills/graft/SKILL.md carries em dashes, an emoji at line 129, boldface across whole sentences, and colon connectors. This is the largest remaining prose problem.
  • build_windows.sh:3 has an em dash, and :58-59 uses a colon as a connector.
  • AUDIT_REPORT.md:123,141,247,320,322 use colons as mid-sentence connectors, and :286 reads "very sparingly".
  • .claude/helpers/graft-hooks.cjs:67 and .claude/helpers/graft-statusline.cjs:67 have an em dash in a trailing comment.
  • Root AGENTS.md has nine em dashes inside the generated <!-- graft:start --> block. That block is produced by graft init from a Graft template, so editing it in place will be overwritten on the next graft init. Fix the template source, or leave the block and note it as generated.

Desired behavior. Guidance matches the code, and prose reads plainly.

Work. Apply the fixes above. For each unslop edit, replace em dashes with a comma or a full stop, split colon connectors into two sentences, remove the emoji, and unbold body text. Keep literal technical terms such as "harness", "vector" and "surface" when they name a real thing. Do not edit third_party/JUCE.

Files in scope. Source/params/AGENTS.md, Source/synth/AGENTS.md, Source/engine/AGENTS.md, .claude/skills/graft/SKILL.md, .claude/helpers/graft-hooks.cjs, .claude/helpers/graft-statusline.cjs, build_windows.sh, AUDIT_REPORT.md, and root AGENTS.md.

Acceptance. bash -n passes for build_*.sh. grep -rn "—" AGENTS.md Source/*/AGENTS.md build_windows.sh .claude/skills/graft/SKILL.md returns nothing outside the generated graft block. A build and harness run still pass, since only comments and prose changed.

Conflict. AGENTS.md, AUDIT_REPORT.md and the Source/**/AGENTS.md files have uncommitted edits. Commit or freeze them first.


Considered and rejected

These came up during verification and are deliberately not tasks.

  • Rename Source/RAVEButton.{h,cpp} to match class RaveController. Both Source/modulation/AGENTS.md:19 and root AGENTS.md say to keep the filenames unless a rename is in scope.
  • Rewrite AUDIT_REPORT.md. Its own header and root AGENTS.md mark it as a historical snapshot, so it is not a pending instruction. Only its prose is touched, in JT-14.
  • Move PluginEditor, FXProcessor, Resources and RAVEButton into subdirectories. The proposed moves in AUDIT_REPORT.md are suggestions, and root AGENTS.md says the current layout is intentional until a move is part of a requested task.
  • Make ModulationMatrix::connections and MacroControls::assignments private and force all mutation through locked members. Every current caller already holds Engine::getControlLock(), and the change is a wide interface refactor, not cleanup. Revisit only if a caller appears outside the lock.
  • Make the lazy WavetableLibrary::getTable build branch unreachable. The constructor prebuilds every index (Source/Engine.cpp:22), so the branch is dead today. JT-13 covers the defensive asserts; deleting the lazy path is a separate design decision.
  • Add tests. AGENTS.md says to add test infrastructure only when requested.

Proposed dispatch order

Dispatch one task at a time per file cluster. The conflicts column above names the files with uncommitted edits.

  • Batch 1, independent files: JT-02, JT-09, JT-11, JT-13.
  • Batch 2, editor cluster, serial: JT-01, then JT-03, then JT-04, then JT-06.
  • Batch 3, dead-code cluster, serial across SynthVoice and Filter: JT-08, then JT-07.
  • Batch 4, wide edits, serial: JT-05, then JT-10, then JT-12, then JT-14.

Each agent must run the harness before handing back:

BUILD_DIR=/tmp/serumalt-check ./build_harness.sh --run

Execution status

All 14 tasks were dispatched and completed. Final consolidated verification on the frozen tree:

BUILD_DIR=/tmp/jt-final CMAKE_BUILD_PARALLEL_LEVEL=8 ./build_harness.sh --run

Result: exit 0, 1/1 Test #1: SerumAltTest ... Passed, 100% tests passed, and zero first-party compiler warnings in Source/ (down from 55 warning lines).

Id Result Note
JT-01 Done Source/GUI/SerumLookAndFeel.h deleted. The class was never installed, so the GUI is unchanged.
JT-02 Done One lfoShapeValue function now serves LFO and LFODisplay. The sample-and-hold preview is a labelled schematic and no longer invents a sequence.
JT-03 Done Macro names come from MacroControls::macroName in the APVTS, the assign combo, the panels and the list. Only the Params.cpp serialisation tokens keep the old strings.
JT-04 Done Add and Assign grey out at kMaxConnections and kMaxAssignments, and rejected inserts no longer look like success.
JT-05 Done 55 first-party warning lines to 0. New Source/ExactCompare.h names the deliberate exact float comparisons instead of weakening them to an epsilon.
JT-06 Done The FX type combo is built from FXProcessor::fxTypeName; the duplicate literal list is gone.
JT-07 Done Removed the unused oscAComponents/oscBComponents, Filter::svfLow/svfHigh, Engine::getActiveVoiceCount and the per-block tally, and the unused fmtSlope.
JT-08 Done Removed the dead SubOscillator octave math, the assert-only Oscillator::noteOn frequency, the discarded Wavetable locals, and the unused maxBlockSize chain through Filter and FilterBank.
JT-09 Done The duplicated test loops are one runPreset helper. Output format and peak values are unchanged.
JT-10 Done Raw pi and two-pi literals replaced with juce::MathConstants. The only remaining match is the intentional "BAD" example in Source/synth/AGENTS.md.
JT-11 Done The envelope curve mapping lives in Envelope.h and both callers use it. FilterDisplay::magnitude now states that it is a schematic preview.
JT-12 Done PluginProcessor::createParameterLayout uses the paramIds:: groups. A layout comparison confirmed all 96 (id, name, default) entries are identical and in the same order.
JT-13 Done FXProcessor::process returns early on a mono buffer, and capacity asserts guard FXProcessor::process and SynthVoice::render.
JT-14 Done, one limitation Guidance and prose fixed across the AGENTS.md files, .claude/skills/graft/SKILL.md, build_windows.sh, AUDIT_REPORT.md and the two helper scripts. Nine em dashes remain inside the generated <!-- graft:start --> block in root AGENTS.md.

The JT-14 limitation. The graft block in root AGENTS.md is generated by the installed @nanonets/graft package, whose template carries the em dashes. graft init would reproduce them, so the block was left as generated. Removing them needs a change to the package template at /home/biggy/Documents/Coding/_skills/Graft/src/hosts/instructions.ts and a reinstall, which is outside this repository.

Other notes.

  • The pre-existing uncommitted work in the tree was preserved throughout. It includes the FXProcessor per-slot repair, the SynthVoice::getModulationValue addition, the editor control-lock line, and the AGENT.md to AGENTS.md renames. HEAD does not compile without the FXProcessor repair, so commit the current tree before treating any single task as an isolated diff.
  • Every task ran its own harness build in /tmp, so no build_* directory in the repository was modified and third_party/JUCE was not touched.
  • No task changed APVTS parameter IDs, so saved projects stay compatible.