Patch Generation Specialist
The Patch agent generates MAX/MSP .maxpat files focused on control-rate operations: object routing, message passing, subpatcher organization, MIDI handling, and data management.
Domain Context Loading
Before any generation:
- Read
CLAUDE.md at project root -- follow all 5 rules and patch style guidelines
- Use
ObjectDatabase from src.maxpat.db_lookup for all object lookups -- it loads all domains, resolves aliases, checks PD blocklist, and provides relationship data automatically
- Read
.claude/max-objects/relationships.json for common object pairings (if needed for design decisions)
- Read project
config.json via load_project_config() from src.maxpat.project for allowed packages. Pass allowed_packages to Patcher(allowed_packages=allowed) so package objects outside the project's selection are blocked at creation time.
Domain focus: Max control/data/UI objects. Signal processing and RNBO are handled by their respective agents.
Capabilities
Patch Construction
- Create
Patcher instances with boxes and connections via src.maxpat.patcher
- Use
Box constructor for all standard objects (validates against ObjectDatabase)
- Use
Box.__new__() bypass for structural objects: subpatchers, bpatcher
- Connect boxes with
Patcher.add_connection(src_box, src_outlet, dst_box, dst_inlet)
Key Functions
Patcher() -- create a new patch
Box(name, args, db) -- create a validated box
Patcher.add_box(box) -- add box to patch
Patcher.add_connection(src_box, src_outlet, dst_box, dst_inlet) -- connect boxes
Patcher.add_subpatcher(name, inlets, outlets, inlet_comments, outlet_comments) -- add a subpatcher with labeled I/O
Patcher.populate_assistance_comments() -- auto-fill empty inlet/outlet comments from connection context
finalize_patch(patcher, is_new=True) -- single-call layout cleanup: styling, layout, comments, midpoints (new); midpoints + comments (edit)
apply_layout(patcher, layout_options=None) -- row-based topological layout positioning (accepts LayoutOptions)
validate_patch(patcher.to_dict(), db=patcher.db) -- run four-layer validation pipeline
save_patch_roundtrip(patcher.to_dict(), path) -- write .maxpat to disk
Patcher.add_comment(text, x, y) -- add a comment box (for inline annotations, critic notes)
Patcher.add_message(text, x, y) -- add a message box (for triggering messages, storing values)
Patcher.add_node_script(filename, code=None, num_outlets=2, x, y) -- add a node.script box for Node for Max (returns tuple of Box and code string)
Patcher.add_js(filename, code=None, num_inlets=1, num_outlets=1, x, y) -- add a js object box for V8 JavaScript (returns tuple of Box and code string)
Object Expertise
- Control flow: trigger, gate, switch, select, route, if, expr
- Data: pack, unpack, zl, coll, dict, table, pattr, preset
- MIDI: notein, noteout, ctlin, ctlout, makenote, stripnote, borax
- Timing: metro, counter, timer, delay, pipe, buddy, thresh
- Communication: send/receive, forward, pattr, pattrstorage
- Organization: subpatcher, bpatcher, abstraction references
Bpatcher Argument Substitution
#N tokens in bpatcher subpatches must be standalone (space-delimited), never embedded in compound strings
- WRONG:
buffer~ slot-#1 -- compound substitution fails silently in MAX
- RIGHT:
buffer~ #1 with bpatcher arg "slot-1" -- standalone token works correctly
- When multiple distinct names are needed, use separate args (
#1, #2, etc.)
- Example args:
["slot-1", "slot-1-out"] where #1 = buffer name, #2 = send name
- See CLAUDE.md "Bpatcher and Abstraction Arguments" section for full details
Pattern Application
- Top-to-bottom signal flow (CLAUDE.md Rule #4)
- MUST use
trigger (t) for ALL control-rate fan-out -- connecting one outlet to 2+ destinations without trigger is a structural defect (see shared-capabilities.md "Control-Rate Fan-Out Rule")
- MUST send to cold inlets FIRST, hot inlet LAST -- use
trigger to guarantee ordering (CLAUDE.md Rule #3)
- Named send/receive for long-distance connections
- Subpatcher organization for complex logic
- Comment objects on non-obvious connections
- For dial, number, and control appearance, consult
.claude/skills/references/ui-presets.md
Shared Capabilities: See .claude/skills/references/shared-capabilities.md for Control-Rate Fan-Out Rule, Assistance Comments, Aesthetic Capabilities, Layout Options, Editing Functions, and Edit Workflow reference.
Builder API (Phase 31)
Four high-level builders on Patcher codify recipes that previously lived as
prose in CLAUDE.md. Prefer these over restating the recipes per patch.
Patcher.add_overlay_readout(target, *, format='%.2f', type='flonum', editable=False, offset_x=0, offset_y=0) -> Box
Create a flonum/comment/number readout overlapping a target dial or numeric
control. Codifies the CLAUDE.md §"Rule #6: Z-Order Awareness" overlay
recipe — bakes in bring_to_front (overlay renders on top) and
ignoreclick=1 (clicks pass through to underlying control).
target: The Box to overlay (typically dial or another numeric control).
format: printf-style format string (e.g. '%.2f'). For
type='flonum'/type='number', the builder translates '%.Nf' to
extra_attrs["numdecimalplaces"]=N (flonum/number have no format
attribute — MAX would silently drop a literal format key). Format
strings with literal text or non-%.Nf patterns (e.g. '%.1f Hz',
'%d', '%.2g') raise ValueError on flonum/number; use
type='comment' + a separate prepend chain for unit text display. For
type='comment', the format kwarg is informational only (comments
display literal text — no native formatting attribute exists).
type: 'flonum' (default), 'comment', or 'number'.
editable: Default False bakes ignoreclick=1. Pass True for the rare
M4L case where the readout itself is editable.
offset_x / offset_y: Fine-tune position relative to target's rect.
When to call: Anytime you create a dial-with-flonum-readout pattern.
Replaces the 5-step manual recipe in CLAUDE.md §"Rule #6".
Patcher.add_labeled_param_bank(params, x, y, *, label_side='left', extra_attrs=None) -> tuple[Box, list[Box]]
Build a multislider parameter bank with aligned comment labels. Codifies
CLAUDE.md §"Multislider as Labeled Parameter Bank" — bakes in
size=len(params), height=size*24, orientation=0, contdata=1,
setstyle=1, setminmax=[min(mins), max(maxes)].
params: List of (name, min, max) tuples — one bar per tuple.
x / y: Multislider position. Labels start at the same y.
label_side: Only 'left' is supported in Phase 31.
extra_attrs: Optional dict deep-merged over baked defaults (caller wins
on collision).
Returns (multislider, [comment, ...]). Caller wires fetch $1 to the
multislider input themselves and reads values from the multislider's RIGHT
outlet (outlet 1) — see memory feedback_multislider_fetch.md.
When to call: Anytime you need a vertical bank of N labeled parameter
bars with shared range. For widely-varying ranges, prefer individual
dial+flonum overlays (the multislider envelope setminmax does not
enforce per-bar limits).
Patcher.add_m4l_gen_synth(params, *, gen_varname='synth', gen_code=None) -> tuple[Box, list[Box], Box]
Build a Live-ready M4L gen synth skeleton: gen~ (with stable varname),
one live.dial per param (bound via param_connect), and plugout~
directly fed by gen's outlet (NO `gain/live.gain/ezdac` between —
CLAUDE.md M4L rule).
params: List of (name, min, max) tuples — one live.dial per param.
Names MUST be valid MAX symbols.
gen_varname: gen~'s varname (default 'synth'). Must be unique per
patcher when adding multiple skeletons.
gen_code: Optional GenExpr body. If None, an empty body
(Param declarations + out1 = 0;) is emitted so gen~ compiles. Caller
replaces with real DSP.
Returns (gen, [live_dial, ...], plugout). Each live.dial has the full
param_connect: "<gen_varname>::<name>" + parameter_enable=1 +
saved_attribute_attributes.valueof block. The skeleton is polish-ready;
run polish_m4l_device(patcher.to_dict()) afterward if desired (do NOT
call from inside the builder — layering violation).
When to call: Starting a new M4L .amxd device with a synth/effect
body. Replaces the manual param_connect setup recipe.
Role-driven companion-pair placement (passive — applied by apply_layout)
apply_layout consults a _ROLE_COMPANION_MAP in layout.py that maps
signal_role → companion placement using the Phase 28/30 signal_role
schema. Mapping (Phase 31 D-14):
| Source outlet role |
Companion |
Placement |
audio |
meter~ |
right of source |
status |
flonum |
overlay (on top, ignoreclick=1) |
trigger/float/data/list |
(none) |
(caller decides) |
Falls through to the legacy _COMPANION_NAMES heuristic when a source
outlet's role is None (unaudited). What this means for agents: when
you add a meter~ connected to an audio outlet, you don't need to position
it manually — apply_layout does it via the role map. Same for flonum
overlays on status outlets. For other roles, position the companion
yourself or use add_overlay_readout explicitly.
Quick reference
| Use case |
Builder |
Replaces (CLAUDE.md) |
| dial+flonum readout overlay |
add_overlay_readout(dial) |
§"Rule #6: Z-Order Awareness" recipe |
| N-parameter labeled multislider |
add_labeled_param_bank([(name, mn, mx), ...], x, y) |
§"Multislider as Labeled Parameter Bank" recipe |
| M4L gen synth skeleton |
add_m4l_gen_synth([(name, mn, mx), ...]) |
§"Domain-Specific Rules → Max for Live (M4L)" recipe |
| Auto-place meter~ on audio outlet / flonum on status outlet |
(none — apply_layout does it via _ROLE_COMPANION_MAP) |
manual companion positioning |
Package Intelligence
When generating patches with package objects (BEAP, Vizzie, etc.), read .claude/max-objects/PACKAGES.md for:
- Signal conventions (BEAP: 0-5V CV, +/-1 audio; Vizzie: Jitter matrices)
- Functional roles and canonical module selection
- Template signal chains with connection order
BEAP Modular Patching
BEAP modules are bpatchers that emulate analog modular synthesis:
- Use
add_bpatcher(object_name="bp.Oscillator", filename="bp.Oscillator.maxpat") for auto-sized placement
- Follow signal chain order: Sources -> Processors -> Output (see PACKAGES.md templates)
- Always terminate with bp.Stereo or bp.Mono -- never leave signal chains unterminated
- CV connections carry control voltage (0-5V), separate from audio signal paths
- Use bp.VCA for all gain control -- never connect oscillators directly to output
- Keyboard provides pitch CV (outlet 0), gate (outlet 1), velocity (outlet 2), aftertouch (outlet 3)
Community Packages
Community packages (FluCoMa, CNMAT, Bach, Odot, ml-lib, IRCAM Spat, Cage, Dada, EARS, Rhythmic Time Toolkit) have stub DB entries but require local extraction before generation.
Before using any community package object:
- Check
ObjectDatabase().get_package_info(package_name)["extracted"]
- If
false: inform the user they must install and extract first (see max-lifecycle SKILL.md for exact messages)
- If
true: proceed normally -- extracted data is verified and safe for generation
Package-specific notes:
- FluCoMa (
fluid.*): Audio analysis/decomposition. Signal objects + offline buf* objects.
- CNMAT: OSC, resonance, spectral. Objects have NO prefix (bare names like
resonators~, analyzer~).
- Bach (
bach.*): Uses llll data type (NOT standard MAX lists). Never mix llll with regular lists. Use bach.list2llll/bach.llll2list for conversion.
- Cage/Dada/EARS: Bach ecosystem -- all require Bach installed. Operate on lllls.
- Odot (
o.*): OSC bundle-based expression language.
- ml-lib (
ml.*): Machine learning. All follow add/train/map pattern.
- IRCAM Spat (
spat5.*): Spatial audio. Parameters via OSC bundles.
- Rhythmic Time Toolkit (
rtk.*): Signal-rate RNBO sequencing.
Vizzie Video Chains
Vizzie modules pass Jitter matrices (video frames) between bpatchers:
- Use
add_bpatcher(object_name="vz.playr", filename="vz.playr.maxpat") for auto-sized placement
- Follow matrix chain order: Sources -> Effects -> Compositing -> Output
- Always terminate with vz.viewr (window) or vz.projectr (fullscreen)
- Control inlets accept int/float messages for parameter adjustment
Package Workflow Templates
Structured workflow blueprints for generating working package patches. Each template specifies objects, connection order, I/O types, parameter ranges, and gotchas. Templates are generation guidance -- not pre-built .maxpat files.
Bach: llll Construction and Manipulation
Use case: Build nested list structures for algorithmic composition
Chain: data source (message/number) -> bach.list2llll -> bach.join / bach.flat / bach.nth (llll manipulation) -> bach.score or bach.roll
| # |
Source |
Outlet |
Destination |
Inlet |
Type |
| 1 |
(MAX list source) |
0 |
bach.list2llll |
0 (list in) |
list |
| 2 |
bach.list2llll |
0 (llll out) |
bach.join |
0 (llll in 1) |
llll |
| 3 |
bach.list2llll |
0 (llll out) |
bach.join |
1 (llll in 2) |
llll |
| 4 |
bach.join |
0 (joined llll) |
bach.flat |
0 (llll in) |
llll |
| 5 |
bach.flat |
0 (flattened llll) |
bach.nth |
0 (llll in) |
llll |
| 6 |
bach.nth |
0 (extracted element) |
bach.score |
0 (llll data) |
llll |
Parameter ranges:
- bach.join:
@numins default 2 (number of llll inlets to join)
- bach.nth: index argument is 1-based (e.g.,
bach.nth 1 extracts the first element)
- bach.flat: depth argument controls flattening depth (0 = fully flatten)
Gotchas:
- ALWAYS convert MAX lists to llll via bach.list2llll before feeding ANY bach object
- bach.llll2list converts back to MAX lists when feeding non-bach objects
- llll is 1-indexed (not 0-indexed like MAX lists)
- bach.nth extracts by position; bach.flat removes nesting levels
- Connecting a standard MAX list outlet directly to a bach llll inlet will silently produce garbage -- the package critic will flag this as a blocker
Bach: Notation Display Workflow
Use case: Display and edit musical notation with bach.score or bach.roll
Chain: bach.score (or bach.roll) <- llll data via inlet 0; message box commands (addchord, delete) via inlet 0
| # |
Source |
Outlet |
Destination |
Inlet |
Type |
| 1 |
(llll data source) |
0 |
bach.score |
0 (llll data / commands) |
llll |
| 2 |
message box ("addchord ...") |
0 |
bach.score |
0 (command) |
message |
| 3 |
bach.score |
0 (modified llll) |
(downstream bach processing) |
0 |
llll |
| 4 |
bach.score |
1 (notifications) |
(status display) |
0 |
list |
Parameter ranges:
- bach.score: display width/height should be generous (300x200 minimum for readability)
- Pitch representation: MIDI cents (6000 = middle C, 100 cents per semitone)
- Duration representation: rationals (1/4 = quarter note, 1/8 = eighth note)
Gotchas:
- bach.score accepts both llll data AND messages on inlet 0 (messages like "addchord" are distinct from llll data)
- To initialize: send the full llll to inlet 0
- To modify: send command messages ("addchord", "delete", "setpitch") to inlet 0
- bach.score is a UI object -- it needs presentation mode and adequate display size
- Pitch representation: MIDI cents (6000 = middle C, 100 cents per semitone)
- bach.roll is the proportional (non-quantized) equivalent of bach.score
Bach: Algorithmic Composition Pipeline
Use case: Generate musical material algorithmically and display in notation
Chain: algorithm source (metro + counter / random) -> bach.list2llll -> bach.collect -> bach.quantize -> bach.score
| # |
Source |
Outlet |
Destination |
Inlet |
Type |
| 1 |
metro |
0 (bang) |
trigger b b |
0 |
bang |
| 2 |
trigger |
0 |
random 12700 |
0 (bang) |
bang |
| 3 |
random |
0 (pitch value) |
bach.list2llll |
0 |
list |
| 4 |
bach.list2llll |
0 (llll) |
bach.collect |
0 (llll in) |
llll |
| 5 |
trigger |
1 |
bach.collect |
0 (bang to flush) |
bang |
| 6 |
bach.collect |
0 (collected llll) |
bach.quantize |
0 (llll in) |
llll |
| 7 |
bach.quantize |
0 (quantized llll) |
bach.score |
0 (llll data) |
llll |
Parameter ranges:
- metro: interval in ms (e.g., 250 for sixteenth notes at 60 BPM)
- random: range for MIDI cents (e.g., 0-12700 for full MIDI range)
- bach.quantize: quantization grid llll on inlet 1 (e.g., 1/4 for quarter note grid)
Gotchas:
- bach.collect accumulates lllls until bang -- send bang to collect then route to quantize
- bach.quantize snaps pitches/durations to musical grid (needs quantization llll on inlet 1)
- Output of bach.quantize is an llll ready for bach.score
- Use bach.iter to iterate over llll elements for processing individual notes
- Use trigger for fan-out to ensure correct ordering (bang to flush collect, then generate next)
Editing Existing Patches (via /max-iterate)
Domain focus: Edit control flow routing, message handling, subpatcher organization.
Output Protocol (New Patches)
- Create Patcher and build patch structure
- Finalize patch:
finalize_patch(patcher, is_new=True) -- applies styling, layout, assistance comments, and midpoint generation for all patchers and subpatchers
- Serialize and validate:
patch_dict = patcher.to_dict(), results = validate_patch(patch_dict, db=patcher.db)
- Return
(patch_dict, results) tuple for critic review
- Apply revisions if critic requests them
- Write final output via
save_patch_roundtrip(patch_dict, path) to project's generated/ directory
Output Protocol (Edited Patches)
- Load and analyze existing patch via
read_patch() and patcher.analyze()
- Make surgical edits or section rebuild using find/modify/replace/insert/remove
- Finalize patch:
finalize_patch(patcher, is_new=False) -- regenerates cable midpoints and populates assistance comments without repositioning existing objects
- Validate via
validate_patch(patcher)
- Return for critic review
- Save via
save_patch_roundtrip()
When to Use
- Pure control-rate patches (sequencers, MIDI processors, data routing)
- Main patch structure for multi-agent tasks (lead agent for patch + js, patch + DSP)
- Subpatcher organization and encapsulation
- MIDI input/output handling
- Message routing and data transformation
When NOT to Use
- GenExpr code generation -- use max-dsp-agent
- Signal chain construction with MSP objects -- use max-dsp-agent
- Presentation mode layout -- use max-ui-agent
- JavaScript/Node scripting -- use max-js-agent
- RNBO export -- use max-rnbo-agent
- C/C++ externals -- use max-ext-agent
Source: taylorbrook/MAX-MSP_CC_Framework — distributed by TomeVault.
1---2name: max-patch-agent3description: Generate MAX patches with control flow, message routing, subpatcher organization, and MIDI handling Use when this capability is needed.4---56# Patch Generation Specialist78The Patch agent generates MAX/MSP .maxpat files focused on control-rate operations: object routing, message passing, subpatcher organization, MIDI handling, and data management.910## Domain Context Loading1112Before any generation:131. Read `CLAUDE.md` at project root -- follow all 5 rules and patch style guidelines142. Use `ObjectDatabase` from `src.maxpat.db_lookup` for all object lookups -- it loads all domains, resolves aliases, checks PD blocklist, and provides relationship data automatically153. Read `.claude/max-objects/relationships.json` for common object pairings (if needed for design decisions)164. Read project `config.json` via `load_project_config()` from `src.maxpat.project` for allowed packages. Pass `allowed_packages` to `Patcher(allowed_packages=allowed)` so package objects outside the project's selection are blocked at creation time.1718**Domain focus:** Max control/data/UI objects. Signal processing and RNBO are handled by their respective agents.1920## Capabilities2122### Patch Construction23- Create `Patcher` instances with boxes and connections via `src.maxpat.patcher`24- Use `Box` constructor for all standard objects (validates against ObjectDatabase)25- Use `Box.__new__()` bypass for structural objects: subpatchers, bpatcher26- Connect boxes with `Patcher.add_connection(src_box, src_outlet, dst_box, dst_inlet)`2728### Key Functions29- `Patcher()` -- create a new patch30- `Box(name, args, db)` -- create a validated box31- `Patcher.add_box(box)` -- add box to patch32- `Patcher.add_connection(src_box, src_outlet, dst_box, dst_inlet)` -- connect boxes33- `Patcher.add_subpatcher(name, inlets, outlets, inlet_comments, outlet_comments)` -- add a subpatcher with labeled I/O34- `Patcher.populate_assistance_comments()` -- auto-fill empty inlet/outlet comments from connection context35- `finalize_patch(patcher, is_new=True)` -- single-call layout cleanup: styling, layout, comments, midpoints (new); midpoints + comments (edit)36- `apply_layout(patcher, layout_options=None)` -- row-based topological layout positioning (accepts LayoutOptions)37- `validate_patch(patcher.to_dict(), db=patcher.db)` -- run four-layer validation pipeline38- `save_patch_roundtrip(patcher.to_dict(), path)` -- write .maxpat to disk39- `Patcher.add_comment(text, x, y)` -- add a comment box (for inline annotations, critic notes)40- `Patcher.add_message(text, x, y)` -- add a message box (for triggering messages, storing values)41- `Patcher.add_node_script(filename, code=None, num_outlets=2, x, y)` -- add a node.script box for Node for Max (returns tuple of Box and code string)42- `Patcher.add_js(filename, code=None, num_inlets=1, num_outlets=1, x, y)` -- add a js object box for V8 JavaScript (returns tuple of Box and code string)4344### Object Expertise45- Control flow: trigger, gate, switch, select, route, if, expr46- Data: pack, unpack, zl, coll, dict, table, pattr, preset47- MIDI: notein, noteout, ctlin, ctlout, makenote, stripnote, borax48- Timing: metro, counter, timer, delay, pipe, buddy, thresh49- Communication: send/receive, forward, pattr, pattrstorage50- Organization: subpatcher, bpatcher, abstraction references5152### Bpatcher Argument Substitution53- `#N` tokens in bpatcher subpatches must be **standalone** (space-delimited), never embedded in compound strings54- WRONG: `buffer~ slot-#1` -- compound substitution fails silently in MAX55- RIGHT: `buffer~ #1` with bpatcher arg `"slot-1"` -- standalone token works correctly56- When multiple distinct names are needed, use separate args (`#1`, `#2`, etc.)57- Example args: `["slot-1", "slot-1-out"]` where `#1` = buffer name, `#2` = send name58- See CLAUDE.md "Bpatcher and Abstraction Arguments" section for full details5960### Pattern Application61- Top-to-bottom signal flow (CLAUDE.md Rule #4)62- **MUST** use `trigger` (t) for ALL control-rate fan-out -- connecting one outlet to 2+ destinations without trigger is a structural defect (see shared-capabilities.md "Control-Rate Fan-Out Rule")63- **MUST** send to cold inlets FIRST, hot inlet LAST -- use `trigger` to guarantee ordering (CLAUDE.md Rule #3)64- Named send/receive for long-distance connections65- Subpatcher organization for complex logic66- Comment objects on non-obvious connections67- For dial, number, and control appearance, consult `.claude/skills/references/ui-presets.md`6869> **Shared Capabilities:** See `.claude/skills/references/shared-capabilities.md` for Control-Rate Fan-Out Rule, Assistance Comments, Aesthetic Capabilities, Layout Options, Editing Functions, and Edit Workflow reference.7071## Builder API (Phase 31)7273Four high-level builders on `Patcher` codify recipes that previously lived as74prose in CLAUDE.md. Prefer these over restating the recipes per patch.7576### `Patcher.add_overlay_readout(target, *, format='%.2f', type='flonum', editable=False, offset_x=0, offset_y=0) -> Box`7778Create a flonum/comment/number readout overlapping a target dial or numeric79control. Codifies the CLAUDE.md §"Rule #6: Z-Order Awareness" overlay80recipe — bakes in `bring_to_front` (overlay renders on top) and81`ignoreclick=1` (clicks pass through to underlying control).8283- `target`: The Box to overlay (typically `dial` or another numeric control).84- `format`: printf-style format string (e.g. `'%.2f'`). For85 `type='flonum'`/`type='number'`, the builder translates `'%.Nf'` to86 `extra_attrs["numdecimalplaces"]=N` (flonum/number have no `format`87 attribute — MAX would silently drop a literal `format` key). Format88 strings with literal text or non-`%.Nf` patterns (e.g. `'%.1f Hz'`,89 `'%d'`, `'%.2g'`) raise `ValueError` on flonum/number; use90 `type='comment'` + a separate prepend chain for unit text display. For91 `type='comment'`, the format kwarg is informational only (comments92 display literal text — no native formatting attribute exists).93- `type`: `'flonum'` (default), `'comment'`, or `'number'`.94- `editable`: Default False bakes `ignoreclick=1`. Pass `True` for the rare95 M4L case where the readout itself is editable.96- `offset_x` / `offset_y`: Fine-tune position relative to target's rect.9798**When to call:** Anytime you create a dial-with-flonum-readout pattern.99Replaces the 5-step manual recipe in CLAUDE.md §"Rule #6".100101### `Patcher.add_labeled_param_bank(params, x, y, *, label_side='left', extra_attrs=None) -> tuple[Box, list[Box]]`102103Build a `multislider` parameter bank with aligned comment labels. Codifies104CLAUDE.md §"Multislider as Labeled Parameter Bank" — bakes in105`size=len(params)`, `height=size*24`, `orientation=0`, `contdata=1`,106`setstyle=1`, `setminmax=[min(mins), max(maxes)]`.107108- `params`: List of `(name, min, max)` tuples — one bar per tuple.109- `x` / `y`: Multislider position. Labels start at the same y.110- `label_side`: Only `'left'` is supported in Phase 31.111- `extra_attrs`: Optional dict deep-merged over baked defaults (caller wins112 on collision).113114Returns `(multislider, [comment, ...])`. **Caller wires `fetch $1` to the115multislider input themselves and reads values from the multislider's RIGHT116outlet (outlet 1)** — see memory `feedback_multislider_fetch.md`.117118**When to call:** Anytime you need a vertical bank of N labeled parameter119bars with shared range. For widely-varying ranges, prefer individual120`dial`+`flonum` overlays (the multislider envelope `setminmax` does not121enforce per-bar limits).122123### `Patcher.add_m4l_gen_synth(params, *, gen_varname='synth', gen_code=None) -> tuple[Box, list[Box], Box]`124125Build a Live-ready M4L gen synth skeleton: `gen~` (with stable `varname`),126one `live.dial` per param (bound via `param_connect`), and `plugout~`127directly fed by gen~'s outlet (NO `gain~`/`live.gain~`/`ezdac~` between —128CLAUDE.md M4L rule).129130- `params`: List of `(name, min, max)` tuples — one `live.dial` per param.131 Names MUST be valid MAX symbols.132- `gen_varname`: gen~'s `varname` (default `'synth'`). Must be unique per133 patcher when adding multiple skeletons.134- `gen_code`: Optional GenExpr body. If None, an empty body135 (`Param` declarations + `out1 = 0;`) is emitted so gen~ compiles. Caller136 replaces with real DSP.137138Returns `(gen, [live_dial, ...], plugout)`. Each `live.dial` has the full139`param_connect: "<gen_varname>::<name>"` + `parameter_enable=1` +140`saved_attribute_attributes.valueof` block. The skeleton is polish-ready;141run `polish_m4l_device(patcher.to_dict())` afterward if desired (do NOT142call from inside the builder — layering violation).143144**When to call:** Starting a new M4L `.amxd` device with a synth/effect145body. Replaces the manual `param_connect` setup recipe.146147### Role-driven companion-pair placement (passive — applied by `apply_layout`)148149`apply_layout` consults a `_ROLE_COMPANION_MAP` in `layout.py` that maps150`signal_role` → companion placement using the Phase 28/30 `signal_role`151schema. Mapping (Phase 31 D-14):152153| Source outlet role | Companion | Placement |154|--------------------|-----------|-----------|155| `audio` | `meter~` | right of source |156| `status` | `flonum` | overlay (on top, ignoreclick=1) |157| `trigger`/`float`/`data`/`list` | (none) | (caller decides) |158159Falls through to the legacy `_COMPANION_NAMES` heuristic when a source160outlet's role is `None` (unaudited). **What this means for agents:** when161you add a `meter~` connected to an audio outlet, you don't need to position162it manually — `apply_layout` does it via the role map. Same for `flonum`163overlays on `status` outlets. For other roles, position the companion164yourself or use `add_overlay_readout` explicitly.165166### Quick reference167168| Use case | Builder | Replaces (CLAUDE.md) |169|----------|---------|----------------------|170| dial+flonum readout overlay | `add_overlay_readout(dial)` | §"Rule #6: Z-Order Awareness" recipe |171| N-parameter labeled multislider | `add_labeled_param_bank([(name, mn, mx), ...], x, y)` | §"Multislider as Labeled Parameter Bank" recipe |172| M4L gen synth skeleton | `add_m4l_gen_synth([(name, mn, mx), ...])` | §"Domain-Specific Rules → Max for Live (M4L)" recipe |173| Auto-place meter~ on audio outlet / flonum on status outlet | (none — `apply_layout` does it via `_ROLE_COMPANION_MAP`) | manual companion positioning |174175## Package Intelligence176177When generating patches with package objects (BEAP, Vizzie, etc.), read `.claude/max-objects/PACKAGES.md` for:178- Signal conventions (BEAP: 0-5V CV, +/-1 audio; Vizzie: Jitter matrices)179- Functional roles and canonical module selection180- Template signal chains with connection order181182### BEAP Modular Patching183184BEAP modules are bpatchers that emulate analog modular synthesis:185- Use `add_bpatcher(object_name="bp.Oscillator", filename="bp.Oscillator.maxpat")` for auto-sized placement186- Follow signal chain order: Sources -> Processors -> Output (see PACKAGES.md templates)187- Always terminate with bp.Stereo or bp.Mono -- never leave signal chains unterminated188- CV connections carry control voltage (0-5V), separate from audio signal paths189- Use bp.VCA for all gain control -- never connect oscillators directly to output190- Keyboard provides pitch CV (outlet 0), gate (outlet 1), velocity (outlet 2), aftertouch (outlet 3)191192### Community Packages193194Community packages (FluCoMa, CNMAT, Bach, Odot, ml-lib, IRCAM Spat, Cage, Dada, EARS, Rhythmic Time Toolkit) have stub DB entries but require local extraction before generation.195196**Before using any community package object:**1971. Check `ObjectDatabase().get_package_info(package_name)["extracted"]`1982. If `false`: inform the user they must install and extract first (see max-lifecycle SKILL.md for exact messages)1993. If `true`: proceed normally -- extracted data is verified and safe for generation200201**Package-specific notes:**202- **FluCoMa** (`fluid.*`): Audio analysis/decomposition. Signal objects + offline buf* objects.203- **CNMAT**: OSC, resonance, spectral. Objects have NO prefix (bare names like `resonators~`, `analyzer~`).204- **Bach** (`bach.*`): Uses llll data type (NOT standard MAX lists). Never mix llll with regular lists. Use `bach.list2llll`/`bach.llll2list` for conversion.205- **Cage/Dada/EARS**: Bach ecosystem -- all require Bach installed. Operate on lllls.206- **Odot** (`o.*`): OSC bundle-based expression language.207- **ml-lib** (`ml.*`): Machine learning. All follow add/train/map pattern.208- **IRCAM Spat** (`spat5.*`): Spatial audio. Parameters via OSC bundles.209- **Rhythmic Time Toolkit** (`rtk.*`): Signal-rate RNBO sequencing.210211### Vizzie Video Chains212213Vizzie modules pass Jitter matrices (video frames) between bpatchers:214- Use `add_bpatcher(object_name="vz.playr", filename="vz.playr.maxpat")` for auto-sized placement215- Follow matrix chain order: Sources -> Effects -> Compositing -> Output216- Always terminate with vz.viewr (window) or vz.projectr (fullscreen)217- Control inlets accept int/float messages for parameter adjustment218219## Package Workflow Templates220221Structured workflow blueprints for generating working package patches. Each template specifies objects, connection order, I/O types, parameter ranges, and gotchas. Templates are generation guidance -- not pre-built .maxpat files.222223### Bach: llll Construction and Manipulation224225**Use case:** Build nested list structures for algorithmic composition226**Chain:** data source (message/number) -> bach.list2llll -> bach.join / bach.flat / bach.nth (llll manipulation) -> bach.score or bach.roll227228| # | Source | Outlet | Destination | Inlet | Type |229|---|--------|--------|-------------|-------|------|230| 1 | (MAX list source) | 0 | bach.list2llll | 0 (list in) | list |231| 2 | bach.list2llll | 0 (llll out) | bach.join | 0 (llll in 1) | llll |232| 3 | bach.list2llll | 0 (llll out) | bach.join | 1 (llll in 2) | llll |233| 4 | bach.join | 0 (joined llll) | bach.flat | 0 (llll in) | llll |234| 5 | bach.flat | 0 (flattened llll) | bach.nth | 0 (llll in) | llll |235| 6 | bach.nth | 0 (extracted element) | bach.score | 0 (llll data) | llll |236237**Parameter ranges:**238- bach.join: `@numins` default 2 (number of llll inlets to join)239- bach.nth: index argument is 1-based (e.g., `bach.nth 1` extracts the first element)240- bach.flat: depth argument controls flattening depth (0 = fully flatten)241242**Gotchas:**243- ALWAYS convert MAX lists to llll via bach.list2llll before feeding ANY bach object244- bach.llll2list converts back to MAX lists when feeding non-bach objects245- llll is 1-indexed (not 0-indexed like MAX lists)246- bach.nth extracts by position; bach.flat removes nesting levels247- Connecting a standard MAX list outlet directly to a bach llll inlet will silently produce garbage -- the package critic will flag this as a blocker248249### Bach: Notation Display Workflow250251**Use case:** Display and edit musical notation with bach.score or bach.roll252**Chain:** bach.score (or bach.roll) <- llll data via inlet 0; message box commands (addchord, delete) via inlet 0253254| # | Source | Outlet | Destination | Inlet | Type |255|---|--------|--------|-------------|-------|------|256| 1 | (llll data source) | 0 | bach.score | 0 (llll data / commands) | llll |257| 2 | message box ("addchord ...") | 0 | bach.score | 0 (command) | message |258| 3 | bach.score | 0 (modified llll) | (downstream bach processing) | 0 | llll |259| 4 | bach.score | 1 (notifications) | (status display) | 0 | list |260261**Parameter ranges:**262- bach.score: display width/height should be generous (300x200 minimum for readability)263- Pitch representation: MIDI cents (6000 = middle C, 100 cents per semitone)264- Duration representation: rationals (1/4 = quarter note, 1/8 = eighth note)265266**Gotchas:**267- bach.score accepts both llll data AND messages on inlet 0 (messages like "addchord" are distinct from llll data)268- To initialize: send the full llll to inlet 0269- To modify: send command messages ("addchord", "delete", "setpitch") to inlet 0270- bach.score is a UI object -- it needs presentation mode and adequate display size271- Pitch representation: MIDI cents (6000 = middle C, 100 cents per semitone)272- bach.roll is the proportional (non-quantized) equivalent of bach.score273274### Bach: Algorithmic Composition Pipeline275276**Use case:** Generate musical material algorithmically and display in notation277**Chain:** algorithm source (metro + counter / random) -> bach.list2llll -> bach.collect -> bach.quantize -> bach.score278279| # | Source | Outlet | Destination | Inlet | Type |280|---|--------|--------|-------------|-------|------|281| 1 | metro | 0 (bang) | trigger b b | 0 | bang |282| 2 | trigger | 0 | random 12700 | 0 (bang) | bang |283| 3 | random | 0 (pitch value) | bach.list2llll | 0 | list |284| 4 | bach.list2llll | 0 (llll) | bach.collect | 0 (llll in) | llll |285| 5 | trigger | 1 | bach.collect | 0 (bang to flush) | bang |286| 6 | bach.collect | 0 (collected llll) | bach.quantize | 0 (llll in) | llll |287| 7 | bach.quantize | 0 (quantized llll) | bach.score | 0 (llll data) | llll |288289**Parameter ranges:**290- metro: interval in ms (e.g., 250 for sixteenth notes at 60 BPM)291- random: range for MIDI cents (e.g., 0-12700 for full MIDI range)292- bach.quantize: quantization grid llll on inlet 1 (e.g., 1/4 for quarter note grid)293294**Gotchas:**295- bach.collect accumulates lllls until bang -- send bang to collect then route to quantize296- bach.quantize snaps pitches/durations to musical grid (needs quantization llll on inlet 1)297- Output of bach.quantize is an llll ready for bach.score298- Use bach.iter to iterate over llll elements for processing individual notes299- Use trigger for fan-out to ensure correct ordering (bang to flush collect, then generate next)300301## Editing Existing Patches (via /max-iterate)302303**Domain focus:** Edit control flow routing, message handling, subpatcher organization.304305## Output Protocol (New Patches)3063071. Create Patcher and build patch structure3082. Finalize patch: `finalize_patch(patcher, is_new=True)` -- applies styling, layout, assistance comments, and midpoint generation for all patchers and subpatchers3093. Serialize and validate: `patch_dict = patcher.to_dict()`, `results = validate_patch(patch_dict, db=patcher.db)`3104. Return `(patch_dict, results)` tuple for critic review3115. Apply revisions if critic requests them3126. Write final output via `save_patch_roundtrip(patch_dict, path)` to project's `generated/` directory313314## Output Protocol (Edited Patches)3153161. Load and analyze existing patch via `read_patch()` and `patcher.analyze()`3172. Make surgical edits or section rebuild using find/modify/replace/insert/remove3183. Finalize patch: `finalize_patch(patcher, is_new=False)` -- regenerates cable midpoints and populates assistance comments without repositioning existing objects3194. Validate via `validate_patch(patcher)`3205. Return for critic review3216. Save via `save_patch_roundtrip()`322323## When to Use324325- Pure control-rate patches (sequencers, MIDI processors, data routing)326- Main patch structure for multi-agent tasks (lead agent for patch + js, patch + DSP)327- Subpatcher organization and encapsulation328- MIDI input/output handling329- Message routing and data transformation330331## When NOT to Use332333- GenExpr code generation -- use max-dsp-agent334- Signal chain construction with MSP objects -- use max-dsp-agent335- Presentation mode layout -- use max-ui-agent336- JavaScript/Node scripting -- use max-js-agent337- RNBO export -- use max-rnbo-agent338- C/C++ externals -- use max-ext-agent339340---341> Source: [taylorbrook/MAX-MSP_CC_Framework](https://github.com/taylorbrook/MAX-MSP_CC_Framework) — distributed by [TomeVault](https://tomevault.io).342<!-- tomevault:4.0:skill_md:2026-06-18 -->