Skip to content

Latest commit

 

History

History
80 lines (66 loc) · 21.7 KB

AGENTS.md

File metadata and controls

80 lines (66 loc) · 21.7 KB

Patternflow — AI Agent Context

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.

What this project is

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.

Repository map

  • 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, and pfst-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 in docs/build-guide/). The vector folders are rest-api-vectors/, osc-vectors/, midi-vectors/, mqtt-vectors/ and audio-ws-vectors/. Each vector is a JSON file that quotes one sentence of its spec in says and 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 /roadmap page.
  • firmware/ — PlatformIO/Arduino code for the ESP32-S3. Main sketch folder: firmware/patternflow/ with patternflow.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 in features/<name>/<name>_config.h, and settings TUNE a feature, they never add one; per-device secrets in gitignored patternflow_secrets.h), and pattern_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 in firmware/patternflow/src/ (core_display.h HUB75 driver, core_encoders.h, canvas/color/math/noise helpers, Wi-Fi/OTA/web-console modules, the hotspot in core_hotspot.h) — core only: OSC, audio, MQTT, weather, shows, MIDI and the clock all live in features/. core_display.h drives the panel through src/hub75/, ESP32-HUB75-MatrixPanel-DMA vendored at tag 3.0.13. Its UPSTREAM.md names the upstream commit and each commit taken from upstream since, and patternflow.patch holds every line Patternflow changed (VENDORED.md says why). An edit under src/hub75/ rebuilds the patch with the command in UPSTREAM.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 in firmware/patternflow/presets/ (preset_origin.h, preset_wave_saw.h, dated presets), each using its own pattern namespace; user patterns are .pfm modules uploaded to the device over Wi-Fi, built from firmware/modules/ via firmware/toolchain/build_module.py (the old custom1.h–custom3.h root slots are gone). Features attach through firmware/patternflow/features/ (named addons/ 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 editions audio and performance, plus clock and midi, 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.sh builds the default or a named one, shelf.sh stages a publishable image. Device console pages are plain HTML in firmware/patternflow/console/, spliced into *_index.h by firmware/toolchain/console_pages.py. Tooling lives in firmware/toolchain/ (repo-level scripts) and firmware/patternflow/toolchain/ (PlatformIO extra_scripts, which must stay sketch-relative). Also: firmware/encoder_test/ (standalone encoder diagnostic) and firmware/CUSTOM_PATTERNS.md (pattern authoring guide).
  • hardware/ — Hardware designs. case/ (Blender source in source/, STLs by printer bed size, knobs/, legacy_v2/ for the v2.x board, legacy_lasercut/ for the v1/v2 acrylic drawings, and remixes/ — 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.csv is kept for the previous board).
  • web/ — Next.js site at patternflow.work: landing page (/, /pattern, /build, /inside are tabs of one view), the interactive guide (/guide is a hub over /guide/build, /guide/play, /guide/make and /guide/audio, each with a /ko twin; one 3D stage in components/guide/, the firmware simulator in lib/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 /pattern and in the Play guide, installing public/flash/manifest.json and the images in public/flash/bin/; the bare /flash redirects to /guide/play#flash), the edition shelf (/editions; /variants redirects 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 in web/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 geometries PANEL_GEOMETRY picks 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 with PF_SHARED_ in front) and web/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-in console 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), and virtual-panel. virtual-panel is the one that is not a client: a Patternflow with no board, the guide's DeviceSim (web/src/lib/guide/deviceSim.ts) behind the ports the firmware listens on, with the five contracts in transports/. It runs each pattern's JS twin, never a .pfm module. The vector suites replay against it, so a change to DeviceSim is 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 in docs/ (rest-api.md, osc-spec.md, midi-spec.md), not against the firmware source. docs/rest-api.md also 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.

Hard rules (do not violate)

  1. 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.
  2. hardware/bom/bom_v3.9.csv is the BOM source of truth. The parts table in BUILD_GUIDE.md and the schematic in hardware/pcb/schematic.pdf must match it. If you change one, check the other two. (bom_v3.0.csv stays for people holding a v3.0 board; it is not the source of truth.)
  3. 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-MIT and LICENSE-CC-BY-SA. Do not merge them; the root LICENSE is a pointer to both so GitHub's detector sees both, not a third license.
  4. 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.
  5. Known issues of the current board are documented in BUILD_GUIDE.md section 10; the v2.0 fixes and leftovers are in docs/legacy/BUILD_GUIDE_v2.md section 10. Reference those sections instead of restating the issues.
  6. The board has exactly one power input: J4, the screw terminal. Never describe USB-C as a power option. v3.9 removed the USB1 footprint and its R1/R2 CC 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/R2 stay unpopulated. Release notes and changelog entries that record the hold as it stood are history — update current-state docs, leave the record alone.
  7. The firmware core is not the place to put a feature. firmware/patternflow/patternflow.ino and firmware/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 in firmware/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 in features/. The full reasoning is docs/EDITIONS.md, and the header comment of patternflow.ino lists the regressions this rule exists because of.
  8. During a cleanup window (see docs/rfc-cleanup.md) no PR changes behaviour; moves and renames carry a redirect or an updated link map.

Common commands

  • Web dev server: npm run dev (inside the web/ directory)
  • Web production build: npm run build (inside the web/ directory); the smoke suites are npm run check:*, and CI runs the ones npm run check:ci chains together.
  • Web lint and typecheck: npm run lint and npm run typecheck (inside web/). CI runs lint with --max-warnings=0, so one warning fails a pull request the way an error does. The rules in eslint.config.mjs stay as they are. An exception is an eslint-disable-next-line comment for one rule, with its reason after --. typecheck also reads tools/virtual-panel/, and lint does not.
  • Shared constants: change shared/constants.json, then run npm run gen:shared (inside web/) and commit the two files it writes. npm run check:shared fails while either is stale. It also reads the two copies the generator cannot write, the 1000 in the frozen abi/pf_params.h and the 128 and 64 in firmware/toolchain/build_module.py, and fails when they no longer match the JSON.
  • Drift checks: npm run check:drift (inside web/) runs each file in web/scripts/drift/, and each compares two things the tree keeps by hand, such as hardware/bom/bom_v3.9.csv and the build guide's parts table. npm run check:<name> runs one: links-absolute, bom, bom-schematic, features, editions, presets, roadmap, changelog or shared. check:ci ends with it, and .github/workflows/drift.yml runs it on every pull request with PF_DRIFT_STRICT=1. At a desk a check skips a side it cannot read (a tag the clone lacks, no pdftotext, no gh login); in that workflow the skip fails. One check has no script of its own: markers fails on a TODO or FIXME in first-party code, and runs alone as npx tsx scripts/drift.ts markers.
  • Contract vectors: npm run check:rest-vectors (inside web/) gives each file in docs/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-vectors and check:audio-vectors do the same for their folders. The panel's clock stands still between steps, so a 500 ms release costs no real time. check:virtual-panel runs 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 in web/scripts/drift/, so check:drift and drift.yml run them with the drift checks.
  • Virtual panel: npm run virtual-panel -- --port http=8080 (inside web/) 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 mqtt composes a feature in by its directory name in firmware/patternflow/features/. GET /frame.png is the frame as the LEDs would show it, and /vp/ is a hand and the clock. The firmware has neither. tools/virtual-panel/README.md has the rest.
  • Locked text: npm run check:human-lock (inside web/) fails when a word changes in the maintainer's own writing, the regions docs/rfc-cleanup.md §2.9 lists plus each path in docs/.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.yml runs it on pull requests against the base commit; at a desk the base is the merge-base with origin/dev, or with origin/main in a clone that has no dev.
  • Voice lint: npm run check:voice (inside web/) reads the lines a branch adds since that same base and prints file:line for each banned word from docs/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, clock and midi (the last builds in its own PlatformIO env, named in bundles/midi/env, because its USB port must be the OTG controller — a build flag, not a setting). ./firmware/bundles/build.sh all builds 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 all is 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 by firmware/toolchain/check_boundaries.py in CI: core referencing a feature namespace, including from features/, branching on a feature flag, or naming a feature in a core console page fails the build. check_presets.py (also CI) keeps web/src/lib/presets/ and firmware/patternflow/presets/ in step.
  • Console pages: edit firmware/patternflow/console/*.html, preview with python firmware/toolchain/console_serve.py, then python 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 from hardware/case/source/patternflow_case.blend.

Versioning

  • 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_VERSION in net_config.h, and CHANGELOG.md gets 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_VERSION in firmware/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 in web/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 (tryOut in web/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. clock and midi are 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 the EDITIONS tuples of release.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 with release.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 are vX.Y.Z.
  • Conventions: filenames lowercase with underscores; commit messages start with the area (firmware:, web:, docs:, hardware:) then a short present-tense summary.

Documentation entry points

  • 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

Writing voice (hard rule)

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.