This file provides persistent project context for AI coding agents (Antigravity, Cursor, Claude Code). It is loaded automatically at the start of every session. Cursor and Antigravity read it directly; Claude Code reads it through the one-line CLAUDE.md shim. The human-facing map of the repository is docs/REPOSITORY.md; the contributor rules are CONTRIBUTING.md, and the hard rules below are the same rules.
Patternflow is an open-source hardware instrument: four rotary encoders controlling generative light patterns on a 128×64 LED matrix, powered by an ESP32-S3. It is an open-source reinterpretation of Nam June Paik's Participation TV (1963). The project is multi-domain, encompassing Arduino-based firmware, KiCad/Blender hardware designs, a Next.js web ecosystem, and comprehensive documentation.
It is standalone (not Eurorack), powered from a 5 V power bank through a screw terminal, and it is not an analog video synthesizer. Sound arrives through the Audio edition (on-board microphone, OSC, MIDI), not the default firmware.
docs/— contracts and long-form docs:EDITIONS.md(the firmware seam — read it before touching firmware),rest-api.md/osc-spec.md/midi-spec.md/mqtt-spec.md/audio-ws-spec.md(wire contracts integrations are built against, each with a folder of vectors beside it),contract-vectors.md(the vectors' format),pfst-v2-spec.md(show files, andpfst-v2-vectors/, the first vectors),panel-compatibility.md,RELEASING.md,SERVICES.md(production server ops), the assembly map (docs/assembly/), the legacy v2.x build guide (docs/legacy/BUILD_GUIDE_v2.md),AUDIO_GUIDE.md(sound into the panel), the manifesto, investigations, and media (build-guide images indocs/build-guide/). The vector folders arerest-api-vectors/,osc-vectors/,midi-vectors/,mqtt-vectors/andaudio-ws-vectors/. Each vector is a JSON file that quotes one sentence of its spec insaysand gives the steps that show a panel doing it. It follows the spec, not the firmware: where the firmware disagrees, the vector keeps the spec and the disagreement goes to an issue. An edit that rewords a quoted sentence fails its vector until the vector is brought along. Root-level guides:BUILD_GUIDE.md(the v3 board: v3.9, and v3.0 the same way),PATTERN_GUIDE.md(community → deck → Pattern Lab → device),FEATURE_GUIDE.md(writing a firmware feature or edition). The live roadmap is patternflow.work/journal and the/roadmappage.firmware/— PlatformIO/Arduino code for the ESP32-S3. Main sketch folder:firmware/patternflow/withpatternflow.ino,config.h(pin mappings, brightness, limits, LED calibration),net_config.h(Wi-Fi / Improv / OTA / self-update defaults and the lane scale — core only; a feature's own tunables live infeatures/<name>/<name>_config.h, and settings TUNE a feature, they never add one; per-device secrets in gitignoredpatternflow_secrets.h), andpattern_registry.h(function-pointer pattern table; compiles in Origin alone — the rest of the curated set ships as the Basics pack). Shared engine code lives infirmware/patternflow/src/(core_display.hHUB75 driver,core_encoders.h, canvas/color/math/noise helpers, Wi-Fi/OTA/web-console modules, the hotspot incore_hotspot.h) — core only: OSC, audio, MQTT, weather, shows, MIDI and the clock all live infeatures/.core_display.hdrives the panel throughsrc/hub75/, ESP32-HUB75-MatrixPanel-DMA vendored at tag 3.0.13. ItsUPSTREAM.mdnames the upstream commit and each commit taken from upstream since, andpatternflow.patchholds every line Patternflow changed (VENDORED.mdsays why). An edit undersrc/hub75/rebuilds the patch with the command inUPSTREAM.md, never by hand: upstream ends some lines in spaces, and an editor that trims them breaks the hunks.firmware/patternflow/abi/is the frozen host ⇄ module contract. Curated patterns live infirmware/patternflow/presets/(preset_origin.h,preset_wave_saw.h, dated presets), each using its own pattern namespace; user patterns are.pfmmodules uploaded to the device over Wi-Fi, built fromfirmware/modules/viafirmware/toolchain/build_module.py(the oldcustom1.h–custom3.hroot slots are gone). Features attach throughfirmware/patternflow/features/(namedaddons/until 2026-08-30; legacy spellings are shimmed for out-of-tree bundles — see docs/EDITIONS.md) — a directory and a descriptor of function pointers, with the core naming none of them (see its README).firmware/bundles/names firmwares built from the same tree (the shelf editionsaudioandperformance, plusclockandmidi, compositions the tree keeps buildable but does not publish; the default build has no bundle): two files each, saying which features compile in and what the build calls itself;firmware/bundles/build.shbuilds the default or a named one,shelf.shstages a publishable image. Device console pages are plain HTML infirmware/patternflow/console/, spliced into*_index.hbyfirmware/toolchain/console_pages.py. Tooling lives infirmware/toolchain/(repo-level scripts) andfirmware/patternflow/toolchain/(PlatformIOextra_scripts, which must stay sketch-relative). Also:firmware/encoder_test/(standalone encoder diagnostic) andfirmware/CUSTOM_PATTERNS.md(pattern authoring guide).hardware/— Hardware designs.case/(Blender source insource/, STLs by printer bed size,knobs/,legacy_v2/for the v2.x board,legacy_lasercut/for the v1/v2 acrylic drawings, andremixes/— community enclosures, one folder each, whose README is its license header),pcb/(KiCad 10.0 source, Gerbers per revision, schematic PDF),bom/(bom_v3.9.csv— the BOM source of truth; the guide's table is derived from it.bom_v3.0.csvis kept for the previous board).web/— Next.js site at patternflow.work: landing page (/,/pattern,/build,/insideare tabs of one view), the interactive guide (/guideis a hub over/guide/build,/guide/play,/guide/makeand/guide/audio, each with a/kotwin; one 3D stage incomponents/guide/, the firmware simulator inlib/guide/), Pattern Lab (/pattern-lab), community (/community/**, SQLite + Drizzle + Better Auth, only on the Pi deployment), browser flasher (no page of its own: the Flash Patternflow button on/patternand in the Play guide, installingpublic/flash/manifest.jsonand the images inpublic/flash/bin/; the bare/flashredirects to/guide/play#flash), the edition shelf (/editions;/variantsredirects there), the feature catalogue (/features, reels per feature), device update handoff (/update), journal, roadmap.lib/pattern/is the pattern runtime and annotations,lib/lab/the Pattern Lab,lib/community/the community,lib/ai/the Gemini client. Architecture doc:web/ARCHITECTURE.md. The JS presets inweb/src/lib/presets/are the source of truth for firmware preset headers.shared/—constants.json, the values the firmware and the site both carry: the panel geometriesPANEL_GEOMETRYpicks from, the knob count, the encoder's 24 detents a turn, the bus maximum and the button timings. Two files are generated from it,firmware/patternflow/src/core_shared.h(each key withPF_SHARED_in front) andweb/src/lib/shared/constants.ts, and neither is edited by hand. It exists because the lab counted 20 detents a turn, against the encoder's 24, until 31 July 2026.tools/— desktop-side helpers:patternflow-audio-extension(the browser audio-react extension — also the authoring source the device's/audio-inconsole page is assembled from),patternflow-audio-android(phone capture app),rtpmidi-probe,case-models(builds the /build preview's per-case 3D models,web/public/cases/<id>/model.glb, from the case files in the repo; run by hand, not by the site build), andvirtual-panel.virtual-panelis the one that is not a client: a Patternflow with no board, the guide'sDeviceSim(web/src/lib/guide/deviceSim.ts) behind the ports the firmware listens on, with the five contracts intransports/. It runs each pattern's JS twin, never a.pfmmodule. The vector suites replay against it, so a change toDeviceSimis a change to the panel they test. It is a test fixture, and Wi-Fi and firmware updates are not part of it.integrations/— host-software integrations, each with its own README.integrations/ableton/is the Max for Live bridge (knobs → Live parameters over OSC). The Home Assistant integration left this repository on 2026-09-03; its author maintains it separately. Integrations are built against the contracts indocs/(rest-api.md,osc-spec.md,midi-spec.md), not against the firmware source.docs/rest-api.mdalso has the table for choosing between HTTP, OSC and MQTT, and the rules that make the device's single-connection web server easy to knock over.
- Founders boards (#001–#005) are private. The KiCad project in
hardware/pcb/kicad/is the public v3.9 board (silkscreen "PATTERNFLOW … v3.9"). Never commit founders artifacts to this repo. hardware/bom/bom_v3.9.csvis the BOM source of truth. The parts table inBUILD_GUIDE.mdand the schematic inhardware/pcb/schematic.pdfmust match it. If you change one, check the other two. (bom_v3.0.csvstays for people holding a v3.0 board; it is not the source of truth.)- License split is strict: firmware and web code = MIT; hardware designs (PCB, case STLs, Blender source) = CC-BY-SA 4.0. Two separate license files at root:
LICENSE-MITandLICENSE-CC-BY-SA. Do not merge them; the rootLICENSEis a pointer to both so GitHub's detector sees both, not a third license. - Brand naming: body text = "Patternflow", physical engravings (PCB silkscreen, future case engravings) = "PATTERNFLOW", filenames and URLs = lowercase "patternflow". Never mix these in a single context.
- Known issues of the current board are documented in
BUILD_GUIDE.mdsection 10; the v2.0 fixes and leftovers are indocs/legacy/BUILD_GUIDE_v2.mdsection 10. Reference those sections instead of restating the issues. - The board has exactly one power input:
J4, the screw terminal. Never describe USB-C as a power option. v3.9 removed theUSB1footprint and itsR1/R2CC pull-downs outright, after a USB-C-powered v3.0 board ran for 20–30 minutes and then smoked at a connector pin (#221) — the failure is delayed, so "it seems to work" is not evidence. On v3.9 the footprint does not exist; on a v3.0 board already in someone's hands,USB1/R1/R2stay unpopulated. Release notes and changelog entries that record the hold as it stood are history — update current-state docs, leave the record alone. - The firmware core is not the place to put a feature.
firmware/patternflow/patternflow.inoandfirmware/patternflow/src/are the device itself — panel, encoders, pattern loader, Wi-Fi, sleep, console — and every published firmware compiles them unchanged. They change for a bug or a real improvement to the device, and nothing else; a feature bends to fit the core, never the reverse. Features attach through the hooks infirmware/patternflow/features/pf_feature.h, and the core names none of them — no#include, no#if PF_<FEATURE>_ENABLED, not even a feature's name in a string on the screen or in a console page. Before editing the core, ask whether the edit would still be correct on a build that does not contain the feature you are working on; if not, it belongs infeatures/. The full reasoning isdocs/EDITIONS.md, and the header comment ofpatternflow.inolists the regressions this rule exists because of. - During a cleanup window (see
docs/rfc-cleanup.md) no PR changes behaviour; moves and renames carry a redirect or an updated link map.
- Web dev server:
npm run dev(inside theweb/directory) - Web production build:
npm run build(inside theweb/directory); the smoke suites arenpm run check:*, and CI runs the onesnpm run check:cichains together. - Web lint and typecheck:
npm run lintandnpm run typecheck(insideweb/). CI runs lint with--max-warnings=0, so one warning fails a pull request the way an error does. The rules ineslint.config.mjsstay as they are. An exception is aneslint-disable-next-linecomment for one rule, with its reason after--.typecheckalso readstools/virtual-panel/, and lint does not. - Shared constants: change
shared/constants.json, then runnpm run gen:shared(insideweb/) and commit the two files it writes.npm run check:sharedfails while either is stale. It also reads the two copies the generator cannot write, the 1000 in the frozenabi/pf_params.hand the 128 and 64 infirmware/toolchain/build_module.py, and fails when they no longer match the JSON. - Drift checks:
npm run check:drift(insideweb/) runs each file inweb/scripts/drift/, and each compares two things the tree keeps by hand, such ashardware/bom/bom_v3.9.csvand the build guide's parts table.npm run check:<name>runs one:links-absolute,bom,bom-schematic,features,editions,presets,roadmap,changelogorshared.check:ciends with it, and.github/workflows/drift.ymlruns it on every pull request withPF_DRIFT_STRICT=1. At a desk a check skips a side it cannot read (a tag the clone lacks, nopdftotext, noghlogin); in that workflow the skip fails. One check has no script of its own:markersfails on aTODOorFIXMEin first-party code, and runs alone asnpx tsx scripts/drift.ts markers. - Contract vectors:
npm run check:rest-vectors(insideweb/) gives each file indocs/rest-api-vectors/a fresh virtual panel on free ports and prints what the vector expects beside what the panel did.check:osc-vectors,check:midi-vectors,check:mqtt-vectorsandcheck:audio-vectorsdo the same for their folders. The panel's clock stands still between steps, so a 500 ms release costs no real time.check:virtual-panelruns each preset with a JS twin on a panel of its own for two seconds of panel time. A thrown error fails it. The six are files inweb/scripts/drift/, socheck:driftanddrift.ymlrun them with the drift checks. - Virtual panel:
npm run virtual-panel -- --port http=8080(insideweb/) starts one to talk to by hand. HTTP is on 80 as on the device, and Linux keeps ports below 1024 from a process that is not root, so the flag moves it;--port '*=0'takes a free port for each and prints them.--features mqttcomposes a feature in by its directory name infirmware/patternflow/features/.GET /frame.pngis the frame as the LEDs would show it, and/vp/is a hand and the clock. The firmware has neither.tools/virtual-panel/README.mdhas the rest. - Locked text:
npm run check:human-lock(insideweb/) fails when a word changes in the maintainer's own writing, the regionsdocs/rfc-cleanup.md§2.9 lists plus each path indocs/.human-written. Markup and link paths in them may change. Text moved word for word passes, and the move needs the maintainer's approval on the PR. A typo is reported in the PR, not fixed.drift.ymlruns it on pull requests against the base commit; at a desk the base is the merge-base withorigin/dev, or withorigin/mainin a clone that has nodev. - Voice lint:
npm run check:voice(insideweb/) reads the lines a branch adds since that same base and printsfile:linefor each banned word fromdocs/voice-banned.txt, list of three, opener that says what the text is about, or closing summary. Run it by hand before opening a PR that adds prose; CI does not run it. Files given as arguments are read whole, and-reads a PR description from stdin. - Firmware compilation:
./firmware/bundles/build.sh(PlatformIO, bundled toolchain — not the Arduino IDE; the IDE path cannot build the editions' library set). That builds the default: the device with no features. Named editions are./firmware/bundles/build.sh audio,performance,clockandmidi(the last builds in its own PlatformIO env, named inbundles/midi/env, because its USB port must be the OTG controller — a build flag, not a setting)../firmware/bundles/build.sh allbuilds every composition and scans each binary for per-feature marker strings, proving each image carries exactly its features — run it before pushing anything that touches firmware. - Changing anything in the firmware core means building every composition —
build.sh allis that rule as one command. A hook change that compiles against the default is not tested; the default has no features to break. The boundary rule itself is enforced byfirmware/toolchain/check_boundaries.pyin CI: core referencing a feature namespace, including fromfeatures/, branching on a feature flag, or naming a feature in a core console page fails the build.check_presets.py(also CI) keepsweb/src/lib/presets/andfirmware/patternflow/presets/in step. - Console pages: edit
firmware/patternflow/console/*.html, preview withpython firmware/toolchain/console_serve.py, thenpython firmware/toolchain/console_pages.py build(CI checks the headers stayed in sync). - KiCad exports: Export Gerbers from
hardware/pcb/kicad/patternflow.kicad_pcb. Export STLs fromhardware/case/source/patternflow_case.blend.
- Project: v3.11.0 (current, released 2026-10-04), using unified semantic versioning across firmware, hardware, web, and docs — the firmware reports it as
PF_IMPROV_FW_VERSIONinnet_config.h, andCHANGELOG.mdgets a section per release. Current hardware is the v3.9 board — the v3.0 board with the USB-C footprint removed, same enclosure, pin map and guide; every v3.x release has been firmware/web on it. - Editions carry their own version line, independent of the project version (
PF_VARIANT_VERSIONinfirmware/bundles/<name>/overrides.h): Audio v0.7.0, Performance v0.4.0 at the time of writing. The shelf (web/src/app/editions/editions-data.ts) and the flasher manifest (web/public/flash/manifest.json) name the images that are live; only those images are kept inweb/public/flash/bin/— older ones live on their release tags. The one exception is a try-out image: a frozen copy of a composition that is not on the shelf, named by the features page (tryOutinweb/src/app/features/features-data.ts) so people can install it to try, never bumped with the core, and retired only by a newer try-out of the same name.clockandmidiare bundles the tree keeps building (CI compiles every composition) but does not put on the shelf: the clock is a feature, and the MIDI bundle is the composition that proves the USB-OTG build; each has a try-out image (clock-v0.1.5,midi-v0.1.0). Neither is in theEDITIONStuples ofrelease.py/check_versions.py; an edition joins those, the cards and the line above only when it goes on the shelf, and its maintainer cuts it withrelease.py edition <name> vA.B.C(docs/RELEASING.md). - Firmware source lives in
firmware/patternflow/; use release tags for versioning instead of encoding the release in the folder name. Tags arevX.Y.Z. - Conventions: filenames lowercase with underscores; commit messages start with the area (
firmware:,web:,docs:,hardware:) then a short present-tense summary.
- New users:
README.md→BUILD_GUIDE.md - Contributors:
CONTRIBUTING.md→docs/REPOSITORY.md→docs/EDITIONS.md(firmware) ·web/ARCHITECTURE.md(web) - Version history:
CHANGELOG.md
Everything an agent writes that a person will read — docs, README sections, release notes, console page copy, PR descriptions, comments — must read as if the maintainer wrote it. Not "polished". Written.
Voice reference: docs/voice.md describes the project voice. Read it before writing anything longer than a sentence. Match its register (Korean: 해요체 or 합니다체 as the file already uses; English: plain, first person where the project already uses it), its sentence length, and its habit of naming the specific thing — the part number, the detent count, the date — instead of the category.
Do not:
- Open with what the document is about ("This document describes…", "이 문서는 …을 설명합니다"). Start with the thing.
- Close with a summary that repeats the body, or with "In conclusion" / "결론적으로".
- Turn prose into bullet lists, or bold phrases for emphasis. Bullets are for lists of things.
- Use these words or their Korean equivalents: seamless, robust, leverage, comprehensive, streamline, empower, cutting-edge, ensure, utilize, delve, "it's important to note", "here's", 효율적으로, 원활하게, 강력한, 최적화된, 혁신적인, 다양한, 전반적으로, 참고로, 살펴보겠습니다.
- Write in threes ("fast, simple, and reliable"). One reason is a reason; three is a pattern.
- Join clauses with em dashes (—). End the sentence and start another.
- Hedge every claim. Say it, or leave it out.
- Add emoji, decorative headers, "Note:" callouts, or a "Generated by" line.
- Mention that an AI, an agent or a tool wrote or helped write the text. Ever.
- Reformat an existing human-written paragraph into another structure.
Do:
- Prefer one specific example over a general statement.
- Vary sentence length. A short one can stand alone.
- Keep the project's own terms (core, feature, edition, bundle, preset, module) exactly as the existing docs use them.
- For PR descriptions and commits: imperative first line, one line of why, nothing else. No "This PR introduces…".
- When unsure of tone, write less.
Any new or rewritten text over 20 lines goes through web/scripts/voice-lint.ts (banned phrases, triplets, opener and closer patterns) before the PR is opened, and a person reads it before merge.