A playbook for AI agents collaborating on stackchan-kai. Read this once
per session before reaching for code. CLAUDE.md is the shared
human+agent guide; this file is agent-specific.
- Stack:
no_std+allocRust on ESP32-S3 (CoreS3), embassy executor, defmt logging over USB-Serial-JTAG. - Domain model:
Entity+Director+Modifier/Skilltraits instackchan-core(pure, host-testable). Firmware composes a fixed modifier stack at 30 FPS inrender_task. - Cross-task communication: typed
Signal<RawMutex, T>channels. Sensors publish viasignal(); consumers drain viatry_take(). Latest-wins semantics throughout. - Tests:
stackchan-simruns modifiers againstFakeClockfor deterministic golden assertions. Firmware-side tests use on-device benches (examples/*_bench.rs).
Most useful sessions fit one of these shapes. Recognising the shape early keeps the work scoped.
- Read the relevant modifier in
crates/stackchan-core/src/modifiers/. - Add or change behavior; update the unit tests in the same file.
- Add a sim-level integration test in
crates/stackchan-sim/src/lib.rs. just check— gates pass before flashing.- Optional:
just fmrto confirm on hardware.
Skills are predicate-fired capabilities that write mind.intent /
mind.attention / voice / events; modifiers in later phases
translate that into face / motor.
- Read
crates/stackchan-core/src/skill.rsfor the trait and pick any existing impl incrates/stackchan-core/src/skills/as a starting point. - Add the new file under
crates/stackchan-core/src/skills/, re-export frommod.rs. - Register in
render_taskviadirector.add_skill(&mut x). - If the skill's intent needs a visible response, add or extend a
modifier (
Phase::Motionfor pose,Phase::Expressionfor face) that readsmind.intent/mind.attention.
- Read the crate's README to understand current scope.
- Add registers, driver methods, or init steps; keep
embedded-hal-asyncboundary clean. - Unit-test what you can on host (packet construction, register encoding).
- Add or update a
crates/stackchan-firmware/examples/<chip>_bench.rsthat exercises the new path on hardware. just <chip>-benchto verify on device.
For servo trim calibration specifically, tools/bench-trim parses the
just bench defmt output (just bench | tee /tmp/scfmr.log →
bench-trim --input /tmp/scfmr.log) and prints suggested
head.pan_trim_deg / head.tilt_trim_deg for STACKCHAN.RON instead of
grepping deltas by hand. For camera lens calibration, tools/lens-calibration
parses just tracker-bench output the same way (just tracker-bench | tee /tmp/scfmr.log → lens-calibration --input /tmp/scfmr.log) and flags the
tracker.flip_x / tracker.flip_y mounting flags when the head chases away
from motion.
- Identify the
Signal<…, T>channel(s) the new feature needs. - Producer goes in a per-peripheral task (
src/<chip>.rs); consumer reads inrender_task. - Update
Entity(or a sub-component likeFace/Motor/Perception) if the feature surfaces persistent state; remember to extendFace::frame_eqonly if it's pixel-affecting. cargo +esp clippy --release -- -D warningsfrom the firmware crate.- Hardware-verify boot and runtime via
just fmr.
- CLAUDE.md is shared (humans + agents). AGENTS.md is agent-only.
docs/is for cross-cutting reference (e.g.,docs/errors.md,docs/http.md). - Per-crate READMEs document API + gotchas — the pre-commit hook reminds you to review them when source changes.
- justfile recipes are the project's idiomatic invocation surface — prefer adding a recipe to documenting a long invocation in prose.
- Wire format: parsers + validators in
crates/stackchan-net/src/{config,http_command,http_parse,bare_json}.rs. Host-testable; unit tests live beside the parser. - Handler:
crates/stackchan-firmware/src/net/http.rsmatches requests by(method, path); each route is a handler function. - Persisted state rides the RON schema (
stackchan_net::config::Config) throughPUT /settings's atomic writeback — no parallel persistence paths. - Operator-driven routes show up in the dashboard: source lives under
web/src/(Vite + Solid), and the firmware's HTTP responder embeds the builtweb/dist/index.html.gzviainclude_bytes!(seecrates/stackchan-firmware/src/net/respond.rs). Runjust web-buildafter touchingweb/src/—just check-firmwarechains through it automatically. just check-firmware && just clippy-firmware && just build-firmware, then curl smoke after flashing.- Document the route in
docs/http.md(the canonical reference).
- Ask vs assume: ask when the change spans multiple PRs, when a public API surface changes, or when a doc rewrite is implied. Otherwise assume and proceed; the user can course-correct.
- One PR per feature: the recent network arc (#134–#168) shows the cadence — a large new surface broken into thematically tight PRs, capped with a self-audit (#159) and a string of refactor lifts that pull duplicated firmware helpers up into
stackchan-net. Greptile reviews are tighter on small PRs. - Hardware verification: required for any firmware-touching PR. Skip only for pure host-side changes (sim tests, doc updates, host crate refactors).
- Memory writes: save hardware quirks, gotchas, and corrections — but never current task state. Use the
feedbacktype for behavioral preferences andprojectfor unit-specific or repo-specific facts.
- Signal drain pattern:
if let Some(x) = SOME_SIGNAL.try_take() { entity.perception.x = Some(x); }— non-blocking, drops misses (the producer's next signal overwrites), runs once per render tick. SSE fan-out is the exception: it usesembassy_sync::pubsub::PubSubChannelbecause it has multiple consumers. frame_eqgate: the render task short-circuits LCD blits when no pixel-affecting field changed. NewFacefields default to excluded fromframe_equnless they affect drawing.- Per-modifier state: modifiers own their state (timers, RNG, pending transitions).
update(&mut self, &mut Entity)is the only mutation surface; time flows in viaentity.tick.now. - Errors: typed across the workspace via
thiserror(host) ordefmt::Formatderives (firmware). Seedocs/errors.md.
The boot log lives at /tmp/scfmr.log after just fmr runs in tmux. Key
anchors to grep for:
grep -E "stackchan-firmware v|boot complete|panic|ERROR" /tmp/scfmr.logA clean boot ends with boot complete — idle heartbeat around 1.4 s.
The 1 Hz head: cmd=… actual=… and periodic audio: DmaError(Late)
warnings are expected. To watch only state changes:
tail -f /tmp/scfmr.log | grep -vE "head: cmd|DmaError\(Late\)|audio: RMS"For compile-time filtering, set DEFMT_LOG before flashing:
DEFMT_LOG=info,stackchan_firmware::head=warn just fmrThese are pre-existing on Andy's specific kit — see memory note on unit hardware:
BMM150: not reachable on main I²C — likely wired to BMI270 AUX— by design on this revision.BMI270: init attempt 1/3 failed (I2c(I2c(Timeout))); retrying— known timing wobble, succeeds on retry.audio: DMA pop error ("DmaError(Late)"); publishing silence and resyncing— periodic (every ~2 s); the resync logic recovers automatically.FT6336U: vendor ID 0x01— non-canonical but register-compatible; the touch driver works.
The auto-memory store at
/home/andy/.ccs/instances/home/projects/-var-home-andy-Git-stackchan-kai/memory/
holds accumulated session-spanning context. Read MEMORY.md first; it
indexes feedback (preferences), project (repo-specific facts), and
reference entries.
Highlights to know at session start:
- USB-Serial-JTAG wedges on rapid back-to-back
espflashinvocations — preferjust fmr, usejust reattachto pick up without reset. - ES7210 has no documented chip-ID register and needs MCLK to answer I²C. AW88298 uses 16-bit big-endian registers.
- HTTP control plane lives in
crates/stackchan-firmware/src/net/; wire formats + RON config schema are instackchan-net. Auth is bearer-token, LAN-only by design — no TLS / CSRF / rate limit in v0.x (seedocs/http.mdsecurity section). The firmware boots offline if the SD card is absent. - The user opts out of proactive
/scheduleoffers — don't end replies with "want me to schedule a follow-up?" pitches. - Forward-looking PRDs and v2.x vision content go to the user's Obsidian vault, not the public repo. Repo docs stay tactical and present-tense.
Report blocking state explicitly: what completed, what's blocking, what was attempted, what's needed from the user. Don't loop on the same failing command.