ACTUATOR.md: hardware (TB6600 PUL=D4/DIR=D5), the stepper/stepper_status agent
tools + relay commands, the calibrated 0..9635 envelope, the safe daily bring-up,
the operating rules (park+zero before restart — a restart resets the MCU zero
while the envelope survives), mismatch recovery, recalibration, and the zigbuild
rebuild recipe. Linked from ONBOARDING.md.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Adds 'stepper' and 'stepper_status' to the risk profile allowed_tools +
auto_approve so the cloud agent can drive the linear actuator within its
calibrated soft limits.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
- connect-board.ps1 / .bat: PowerShell twin of connect-board.sh so teams on
Windows 11 attach the USB Uno Q to the local self-host stack (adb forward +
self-register; -Watch for auto-reattach; .bat bypasses execution policy)
- provision-wifi.sh: idempotent nmcli join over adb, venue WiFi baked as default
- provision-fleet.sh: run the WiFi step per board during fleet bring-up
- REQUIREMENTS.md: Windows 11 section (Docker+WSL2, native adb, USB driver),
'does adb see your board?' check, and correct the WiFi reality (board needs
venue WiFi for the cloud brain; we pre-join it)
Co-Authored-By: Claude Opus 4.8 <[email protected]>
- Mount each student's own App Lab files into the app container so the
agent can read/fix them: mount-user-workspace.sh binds ~/sketches (rw),
~/ArduinoApps/* (rw), ~/Arduino/libraries (ro) under /app/workspace via
a root oneshot ordered before arduino-app-cli.service (the /app bind is
rprivate, so binds must precede container start; App Lab has no app.yaml
volumes field). Wired into provision-node-app, provision-fleet (systemd,
best-effort sudo), and package-onboard-app (bundled under host-setup/).
- uno-q-hardware skill: document the mux-aware i2c_scan output format
(0x70:mux, 0x70.2=0x1d) and tell the agent its student's files live at
/app/workspace. See USER-WORKSPACE.md.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Student pre-workshop checklist for the confirmed model: each team runs the Docker
stack locally + board over USB, everything localhost, board auto-connects (no
code, no accounts). Lists the board-laptop prereqs (Docker, adb, git, disk,
pre-fetched bundle), the on-the-day flow, a pre-flight self-check, and an
instructor checklist for the remaining decisions.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
In the self-host model each team runs the stack locally with one board 1:1 over
USB, so the claim code (which only disambiguated boards in a shared pool) is
unnecessary. Add a LOCAL_MODE that auto-registers + auto-binds the board:
- API: `LOCAL_MODE` env + `GET /mode`; `POST /claim {local:true}` codelessly
binds the single board (`claimLocal`, rebinding a stale claim on a fresh
single-team stack); `POST /nodes/:team/disconnect` for the participant's own
release. Shared/LAN code path is unchanged.
- Frontend: `useLocalMode()` + `LocalBoardConnect` — detect → auto-bind →
"connected"; USB drop keeps the binding and auto-reconnects; explicit
Disconnect releases and waits for Reconnect. TeamRegistration swaps the code
card for it only when the API reports localMode.
- deploy/lan: compose sets `LOCAL_MODE=true` + `host.docker.internal:host-gateway`
(so the containerized API reaches the adb-forwarded board); `connect-board.sh`
forwards the tunnels and registers the board with the local stack (`--watch`
re-attaches on every reconnect).
Validated end-to-end through the Docker stack: mode → auto-bind → live status +
matrix mirror → disconnect → reconnect. 222 tests pass.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Add a single-app invariant step: set the boot default to the canonical app, stop
any stray *-main-1 container that would collide on :8080/:9999, and ensure the
canonical app is running. Prevents the duplicate-"ZeroClaw Node" collision from
recurring after a reboot/re-plug.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Replace the obsolete host-daemon recover.sh with the current App-Lab-node flow:
re-forward the adb tunnels (:8080 gateway, :9999 matrix relay), wait for the
daemon (start the app if needed), re-register the node with the local API, and
sync a fixed claim code onto the matrix so the board + API agree. Adds a
`--watch` mode and a launchd agent (com.redclaw.apess-board-recover.plist) that
runs it on every reconnect — so a re-plug heals itself, no manual step.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Restructure the workshop flow into the designer's cockpit: a persistent shell
(header + 5-step stepper + sticky instrument rail) wrapping the phase routes via
a React-Router layout route, so the rail stays mounted across navigation.
- Design system: IBM Plex Mono + Newsreader; the full cockpit token set (light
+ dark) in index.css; a working light/dark theme toggle (store `theme` +
useApplyTheme); a `switch` ui primitive.
- Shell: CockpitLayout, Stepper (forward-gated), PanelChrome helpers. Every
phase page restyled to the editorial panels + the WORKSHOP-FLOW fixes
(channels-after-bind, domain framing + L1 prefill, L2/L3 prefill at 3/3,
in-place submission finale). Store gains `tried` + `channels.saidHi` (v4).
- Live rail (CockpitRail): real node heartbeat + agent activity log + ADD
progress; sim telemetry (useTelemetry) for the waveform/accel/I2C behind a
seam, marked SIM.
- LED-matrix PIXEL MIRROR (real): the rail shows exactly what the physical
matrix displays — API GET /nodes/:team/matrix reads the board's framebuffer
off the :9999 relay (readMatrixFrame + the `matrixget` relay command);
useMatrixMirror polls it and unpacks the 104 bits.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
WORKSHOP-FLOW.md is the participant-journey / design-review doc: local-stack
launch, the module→ADD-layer map, each screen's job (does/sees/system), and the
open design questions. Mirrors the shared flow artifact.
ONBOARDING refresh: matrix tools now include matrix_count; Telegram ships
disabled (no more tokenless probe spam); drop the removed "Refine" step; cross-
link the two docs.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Adds the `count N` command to the host matrix-relay, allowlists + auto-approves
matrix_count for the default agent, and steers the led-matrix skill to use
matrix_count for "count to N once a second" instead of flashing a sketch (which
fails in the App Lab container). Pairs with the zeroclaw sketch/tool changes.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
A tokenless [channels.telegram.default] with enabled=true starts the Telegram
channel anyway; its getUpdates startup probe fails every 5s ("Startup probe:
API error : ; retrying in 5s") and that WARN leaks into the Module 2 chat via
the node activity feed. Ship enabled=false; the setup wizard flips it on with a
real bot_token when a team opts in (api/src/nodes.ts configureTelegram).
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Phase 2's "Open your agent" opens the node's :8080/ dashboard, but the
packager looked for web-dist (wrong) so it was skipped — the gateway then
served "Web dashboard not available". The built dashboard is at web/dist
(cargo xtask web build); carry that into the app's web-dist so :8080/ serves
it. Verified: dashboard + assets load from the board; the rebuilt import zip
includes it (~3.2M).
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Turnkey hosting for the distributable app: both web containers now mount
deploy/download/ read-only and nginx serves it at /download/<file>
(autoindex off, no-store). Drop dist/apess-onboard.zip into deploy/download/
and students fetch https://apess.redclaw.dev/download/apess-onboard.zip →
App Lab "Import an app" → Run. No image rebuild to update the artifact.
The zip carries a baked cloud token, so deploy/download/*.zip is gitignored
and the URL should be treated as a secret (workshop network / access-gated,
not a public link).
Co-Authored-By: Claude Opus 4.8 <[email protected]>
App Lab's own export/import mechanism is a zip whose top dir is the app
name. package-onboard-app.sh now emits dist/apess-onboard.zip alongside the
bundle — verified to round-trip through `arduino-app-cli app import` (a
mac-built zip imports cleanly; no board needed to package). Distribution is
now: build → host the zip on apess.redclaw.dev/download → student downloads
→ App Lab "Import an app" → Run. Replaces the unverified QR path. ONBOARDING
updated with the download/import steps + first-Run auto-install note.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Close the last onboarding gap: one shareable App Lab app a student imports
and Runs, no adb / no host install.
- package-onboard-app.sh: assemble a self-contained bundle — ZeroClaw binary
(matrix_text + i2c_scan), single-agent config, skills, responder sketch, and
the BAKED cloud token. Ships without .secret_key (each board mints its own)
or a team Telegram token; dist/ is gitignored.
- onboard-app/config.toml: the canonical packaged config (proven anthropic.max
single 'default' agent, matrix + i2c_scan allowlisted, Telegram-ready,
secrets stripped).
- ONBOARDING.md: the full flow — instructor packages once, student imports +
Runs, then the wizard. Notes APESS_URL (mDNS/per-team) + LAN reachability.
Validated on-hardware: a freshly-imported bundle mints its key, boots the
cloud agent, and runs the matrix + i2c_scan prompts in-container.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
- matrix-relay/relay.py: handle the `i2c` command → Bridge.call("i2c_scan").
- config.template: allow + auto-approve i2c_scan on the default risk profile.
- modulino skill: point "confirm a module is present" at i2c_scan (MCU Qwiic
bus) instead of the Linux i2cdetect.
Completes the container-native path: the three canned prompts (list I2C /
matrix pattern / scroll text) all run through the RouterBridge responder —
no /dev/i2c, no flash — so they work in the App Lab container node.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Make `default` the sole enabled agent in the template so a freshly
provisioned board matches the runtime: web, chat, Refine, and Telegram all
land on the same fully-loaded agent. cloud/local/chaos stay defined (for
opt-in demos of cloud-only / offline / outage) but ship disabled.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
The agent now drives the matrix in <1s via the resident responder instead of
compiling+flashing a sketch (~95s). Pieces:
- matrix-relay/: a tiny container (python-apps-base) running relay.py — a
TCP:9999 → RouterBridge relay. The ZeroClaw daemon runs on the host for
hardware access, but the RouterBridge python binding (arduino.app_utils)
only ships in the App Lab image, so JUST the relay runs containerized,
mounting the router socket + publishing :9999 to loopback.
- config.template.toml: add matrix_pattern + matrix_text to the default risk
profile's allowed_tools/auto_approve (the capability filter hides tools not
listed — this was why matrix_text wasn't exposed to the agent).
- skills/led-matrix: lead with "use matrix_text / matrix_pattern first"; only
flash for custom frames (flashing overwrites the responder). NOTE skills load
from each agent's workspace copy, not shared/skills — push to all workspaces.
- provision-host-daemon.sh: flash the responder once + start the relay.
Proven on-hardware: "show a heart" and "scroll GO CLAWS" both instant via the
agent, no flash.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
The App Lab container can't drive the board — proven on-hardware: I2C/SPI
blocked by the device cgroup + missing i2cdetect, no arduino-cli/Zephyr
toolchain for flashing, and /admin/reload refused (container NAT hides
loopback). Running the ZeroClaw daemon on the host fixes all of it: native
/dev, arduino-cli + toolchain, loopback reload.
- provision-host-daemon.sh: deploy the binary, write the cloud-token
env-file (raw token + the ZEROCLAW_…api_key config override), install
Arduino_RouterBridge (Zephyr core prereq), a udev rule for I2C/SPI perms,
and install+enable zeroclaw-daemon.service. Also tears down the App Lab
container + clears its default app so it won't grab :8080 on boot.
- zeroclaw-daemon.service: EnvironmentFile for the token (kept off the repo,
non-fatal if absent) + WorkingDirectory.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
The config template seeds channels.telegram.default disabled with an
empty token, so setting only the token leaves the listener off. The
wizard's apply now flips `enabled = true` alongside bot_token (two config
writes, one reload).
Provisioning: re-assert Telegram-readiness idempotently in
provision-node-app.sh — a board's telegram listener only starts if the
[channels.telegram.default] block exists AND agents.default subscribes to
`telegram.default`. The template ships both; the provisioner now patches
them back in if a carried config drifted, so every board is Telegram-ready
out of the box. (Verified the patcher against the template + a drifted
config: no-op when ready, additive when not, idempotent.)
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Judge site label: SubmissionSummary now carries `site` (from the
submissions.site column, populated in listSubmissions + upsertSubmission
and rippled through the submission:new broadcast). SubmissionList shows
it under the team name so a central judge can tell which team stack a
submission came from; hidden on single-fleet deploys where site is ''.
Central-mode deploy: the existing cloud deploy already carries
FLEET_SECRET and the instance-ingest routes are unconditional, so it IS
the central control plane — no build flag needed. Fixed the reporter's
CENTRAL_API default to the real api ORIGIN (apess-api.redclaw.dev, not
the web host + /api proxy — the reporter calls server-to-server).
Documented the wiring: deploy/.env.example + a "Central control plane"
section in deploy/README.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Phase 2 of the edge + control-plane architecture. A per-team local stack
can now mirror its state UP to a central instance so judges get a fleet
view — outbound-only, so it works from behind the room NAT.
Central ingest (same api/, central-mode):
- POST /instances/register + /instances/:id/heartbeat (fleet-secret
gated) + an `instances` table and GET /instances (admin) fleet read.
- PUT /teams/:id and POST /submissions accept an optional `site` tag;
`site` column added to teams + submissions (grouping/filtering).
- WS snapshot now carries instances; instance:update broadcast added.
Reporter sidecar (deploy/lan/reporter/, opt-in `federated` compose
profile): subscribes to the local WS feed and replays team/submission
writes up to central, namespaced by SITE_ID (ids never collide) and
site-tagged. Registers + heartbeats; failed writes queue in an in-memory
outbox and backfill on reconnect. Proven end-to-end (local→reporter→
central) before commit.
Also fixes a latent bug: listSubmissions selected snake_case columns but
mapped camelCase, so GET /submissions summaries were missing teamId/
teamName/submittedAt. Aliased the columns; added a regression assertion.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
A real power-cycle exposed a boot race: the old '@reboot zeroclaw-supervisor.sh' cron
started a host daemon that grabbed :8080 before the App Lab app, so the app's container
failed to bind and exited ('Failed to start default app'). The App Lab app + 'properties
set default' is the boot mechanism now, so the provisioner strips the cron and kills any
running supervisor. Verified the app then comes up clean on :8080.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Copy shared/skills into the app so the [skill_bundles.unoq] bundle loads all 12 Uno Q
skills onto every agent — agent now knows the board + how to drive its devices. Fixes
the cp-nesting gotcha (copy skills/ directly, not the parent). Verified: dashboard agent
lists led-matrix/uno-q-hardware/flashing/sketch-patterns/modulino/bridge/… as loaded.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
The gateway dashboard (:8080/) 503'd because web_dist_dir pointed at a host path
absent in the App Lab container. Provisioner now copies web-dist into /app and
repoints web_dist_dir=/app/web-dist so the dashboard serves. Also removed a stray
</content> line. Verified: :8080/ -> 200 (title ZeroClaw), whole node one app.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
A containerised App Lab node can't see its host LAN IP, but its self-register POST is
SNAT'd to the host, so /nodes/self-register now derives http://<source-ip>:<port> when
url is omitted (explicit url still wins; token defaults to open-lan). Adds
deploy/uno-q/provision-node-app.sh to install the ZeroClaw Node app + carry in its
runtime bits. Tests updated + derived-url coverage (64 pass).
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Workshop reliability: 'cloud' sets EVERY agent to anthropic.max (claude-sonnet-5) with
full tools (risk_profile=default) + full context (runtime=unoq) so nothing feels broken
and the on-board fallback is out of the way. 'offline' flips all to llamacpp.local with
lean tools (demo) + lean context (offline) so the 0.5B is usable — the fallback you
enable on purpose. Rewrites every [agents.*] block, restarts the daemon (token preserved
via the supervisor env), and verifies. Proven: default/sense/local all now sonnet +
matrix_pattern fires.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
ANTHROPIC_OAUTH_TOKEN + APESS_NODE_TOKEN added to cloud-providers/prod alongside
ELEVENLABS_API_KEY. Runbook secrets section is now a single Infisical pull for all
three (APESS_NODE_TOKEN vault key -> NODE_TOKEN env). Verified end-to-end.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Full demo-day runbook — secrets, pre-flight (recover.sh + voice proxy), the three
acts (web/Telegram/voice), mid-demo recovery, and gotchas. ELEVENLABS_API_KEY now
lives in Infisical on icarus (project cloud-providers, env prod, alongside the other
provider keys); runbook pulls it via the macbook-admin machine identity. Cloud token
and node token stay manual env exports for now.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
serve.py gains /tts (ElevenLabs synthesis) + /config (advertises tts mode). Client
plays the returned MP3 when ElevenLabs is on, else falls back to browser speechSynthesis;
barge-in stops in-flight audio. Default voice Sarah (free-tier usable); key read from
ELEVENLABS_API_KEY env, never committed. README documents the free-tier library-voice
402 gotcha. Verified: 200 audio/mpeg, 38KB MP3.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
recover.sh: re-tunnels, relaunches the supervisor with the cloud token in its
environment (env-only, read from ANTHROPIC_OAUTH_TOKEN — never on disk), starts the
matrix bridge app only if down, preserves a running llama to skip cold reload, and
verifies end-to-end (llama/daemon/bridge health + demo agent on claude-sonnet-5 +
matrix_pattern fires). Turns a mid-demo disconnect into a ~30s fix. Tokens are read
from the env, so the script carries no secrets.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
The /ws/chat path builds a fresh Agent that omits the dynamically-registered
peripheral tools (matrix_pattern), so the model improvises with shell/read and the
matrix never changes. Switch the voice client to serve.py, which serves the page and
proxies POST /webhook same-origin to the node gateway (bearer token server-side).
Browser does STT+TTS (Web Speech API, no keys); the agent runs on cloud sonnet and
fires matrix_pattern reliably. Validated end-to-end server-side (checker in 6.2s).
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Single-file client: browser-side STT + TTS (Web Speech API, no keys), drives the
lean demo agent over /ws/chat (?agent=demo, no auth on the workshop board) to change
the LED-matrix animation by voice. Push-to-talk, live transcript, spoken reply, tool
activity display. README covers the localhost-serve secure-context requirement and the
ElevenLabs/voice-duplex upgrade path.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Measured on the board 2026-07-20. Offline is limited by capability, not only
by speed:
simple tool call (i2cdetect, 708 tok) -> works, 20s, real answer
write+compile+flash a sketch (1997 tok) -> never completed in 450s, two
identical requests, no tool call
So with no network the node can sense and decide, but cannot author new
code - that needs the cloud model. This is the concrete degradation boundary
students are asked to state in ADD Layer 4: what survives an outage is the
loop over already-flashed firmware, not writing new firmware.
Also corrects the cold-load figure. The documented 3-5 minutes was measured
against the 1.1GB coder model; the 409MB qwen.gguf we standardised on came
up healthy in ~5s from a cold boot.
Notes that BuildFlash routes to the cloud agent, so the student build/flash
exercise does not sit behind this boundary.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Reviewed the official APESS 2026 programme against the app as shipped.
The structure held up; several published facts and one scoring bug did not.
Fixes a real scoring defect: AddReview (what judges read) still carried
pre-pivot layer titles - "Reasoning policy", "Action contract", "Failure
modes", "AI-native redesign" - so judges scored "Skills" under the heading
"Reasoning policy". Root cause was two duplicated title lists that drifted
after the domain-node reshape, so both surfaces now render from a single
ADD_LAYERS constant and cannot diverge again.
Also removes dead classifier telemetry from the graded artifact: the ADD
printed "N frames - N nominal - N anomalous - N critical", but nothing has
called recordEvent since the simulator was deleted, so those counters were
permanently zero - and the vocabulary predates the pivot.
Corrects the schedule. The app advertised the lecture at 14:00, inside the
hackathon block; the programme puts it at 10:45-12:15 as a separate morning
session, with the hackathon 14:00-19:00. Moving it out recovers an hour,
which the new timings spend on the sensor work rather than setup.
Reframes the session around designing for failure, per the workshop premise
and the school's "proactive resilient systems" theme:
L3 becomes Policies & failure - which way each failure fails, with the
governing rule that a fail-safe must never quietly report "nominal"
L4 becomes where each decision runs - the degradation path, cloud to
on-board to fully offline, not just the happy path
L5 gains what the loop does when a cycle fails - stale reads, missed
ticks, partial data
Students arrive having spent a week on their own sensor work with these
boards, so the hands-on now points the agent at hardware they already
wired (discover the bus, read it, act on a threshold) instead of only
scrolling text, and the framing invites the domain they are already
measuring. The domain stays free-text.
Adds backup-uno-q.sh: boards are reflashed on the day and a week of
student work is irreplaceable. Denylist rather than allowlist, because we
cannot know where a given team put their data; verifies the archive is
readable and non-trivial before reporting success. Deliberately keeps the
App Lab examples dir - stock, but exactly what someone would edit in place.
Verified end to end: byte-identical restore of a real sketch.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Replaces the never-executed "LiteRT-LM on UNO Q 4GB" spike plan with a
measured record of the local-fallback path we actually run.
Measured on board 65301572 (Qwen2.5-0.5B-Instruct, -c 8192, 4 threads),
using llama-server's own timings rather than wall clock:
prefill ~17-20 tok/s (linear), decode ~6-11 tok/s (degrades with KV)
warm prefix-cached tool call: 3.8s, 6/6 correct structured calls
Two findings that changed the deployment:
1. The board had drifted onto Qwen2.5-Coder-1.5B - larger and tuned for
the wrong task. Reverting to the repo's 0.5B made tool calls ~6x
faster (24s -> 3.8s) and freed ~700MB. The repo was right.
2. The harness, not the model, was the bottleneck. The default agent
profile sent a 4718-token prompt (~4.6 min prefill) and the client
cancelled before the model could answer. A lean runtime profile cuts
that to 706 tokens, lifts prefix-cache match 0.435 -> 0.966, and
completes a full agentic turn with a real tool call in 11s warm.
The ZeroClaw text parser was never at fault.
Prompt cost model for budgeting profiles: ~706 base (1 tool),
~244/additional tool, +315 for uno_q_flash (schema + flash imperative),
~53/skill in compact mode.
Also standardises context on -c 8192 across all three provisioning paths
(a 16k window costs ~16 min to fill at this speed and doubles KV for
nothing), and fixes stale references to the deleted src/lib/harness.ts.
Adds bench-prefill.sh and bench-tools.py as reproducible baselines.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Folds the last unmerged workshop work into main:
- ResiliencePanel — the failure-injection 'theater' UI (cloud outage → on-board
Qwen live demo) that visualizes the [agents.chaos] (custom.dead → llamacpp)
failover, plus Module2 integration + api/nodes support.
- build-deploy.sh — one-command aarch64-musl cross-build + adb deploy helper,
REFRESHED for 0.8.3: adds the web SPA prebuild step and the full feature set
(hardware, peripheral-rpi, embedded-web, gateway-voice-duplex) + the
embedded-web/web_dist_dir gotcha.
Config conflict resolved as a union: the branch's custom.dead + [agents.chaos]
demo blocks alongside this session's channels/voice/skills/risk-profile edits.
Verified: tsc clean, web 229 + api 58 tests green.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Commits the workshop inference pool (LiteLLM → boards: pooled coding plans,
per-attendee virtual keys, failover) — config + docs only, no secrets. Ignores
the vendored Uno-QClaw reference repo and the generated qr-sheet.html.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Vendors the fork's 11 granular UNO Q skills (bridge, flashing, led-matrix,
uno-q-hardware, sketch-patterns, modulino, linux-led, audio, vision, wireless,
arduino-app-lab) next to the comprehensive arduino-uno-q skill, and installs the
whole set into every agent's workspace on each board.
Why both: the comprehensive skill is the rich cloud reference (read_skill →
references); the granular skills are keyword-triggered and match the fork's eager
skill-inliner rules, so the on-board Qwen auto-inlines them (no read_skill
round-trip). flashing + led-matrix carry the exact uno_q_flash + frame-API /
ArduinoGraphics-not-installed detail that makes flashing reliable.
- push-skill.sh generalized: a single skill dir (has SKILL.md) OR a parent dir
installs every skill under it; provision-fleet now ships all of skills/.
- Verified on board 65301572: cloud/Sonnet-5 lists all 12 skills.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Vendors the comprehensive UNO Q skill (SKILL.md + 7 references/*.md) and installs
it into EVERY agent's workspace on each board, so agents know this board's
specifics (dual-brain arch, Bridge/RPC, pin tables, LED matrix + the
ArduinoGraphics-not-installed gotcha) instead of guessing generic Arduino.
Why per-agent workspace: ZeroClaw's read_skill returns only SKILL.md; the agent
reads references/*.md via the workspace-sandboxed file_read tool, so references
are only reachable under ~/.zeroclaw/agents/<alias>/workspace/skills/. A
shared/skills bundle surfaces the skill but its references get sandbox-blocked.
- push-skill.sh installs a SKILL.md+references skill into every agent workspace
(discovers aliases from the board); provision-fleet runs it per board.
- config.template risk profile now allows + auto-approves read_skill + file_read
so agents load skills without a human approver (webhook path is non-interactive).
- Flattened the folded 'description: >-' to single-line (ZeroClaw's frontmatter
parser is a flat scanner, not full YAML).
Verified on board 65301572 with cloud/Sonnet-5: discovered arduino-uno-q →
read_skill(SKILL.md) → file_read references/04-bridge-rpc.md → correct
board-specific answer citing the file.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Boards now boot LAN-open (host 0.0.0.0, allow_public_bind, require_pairing=false)
so a team reaches its node's embedded ZeroClaw dashboard + web chat directly by
IP with no token — the 'Open your node' path. Two more modalities ride the same
default (cloud-first) agent:
- Telegram: config seeds [channels.telegram.default] disabled + pre-bound to the
default agent; a team enables it from the dashboard by pasting a @BotFather
token. Dashboard writes only set pending_reload, and /admin/reload is
loopback-only, so a new zeroclaw-reload-watcher.sh (loopback) applies dashboard
config edits within seconds — no shell. provision-fleet pushes + launches it.
- Lockdown: zeroclaw-lockdown.sh flips require_pairing=true, reloads, and mints a
pair code (open -> locked harden step); documented as a board/adb action since
minting + reload are loopback-only.
- Voice: seeds a commented [channels.voice_duplex.default] + documents the
aarch64 build (embedded-web + gateway-voice-duplex); build/hardware work is
separate.
Onboarding scripts (apess-selfregister.sh, provision-uno-q.sh) now tolerate
open-boot: when the gateway mints no pair code, they announce with a placeholder
token (the open board ignores auth) instead of hard-failing.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Fixes the reachability gap: a cloud-hosted API can't reach boards behind the
workshop NAT, so live board features (claim status, Build & flash, module
feeds) fail. This stack runs web + API on one box on the workshop WiFi, where
the API CAN reach each board's LAN IP.
- deploy/lan/: docker-compose (web + internal API), nginx.lan.conf (SPA +
same-origin /api reverse-proxy with WS/SSE support), .env.example, README
(launch, point the fleet's APESS_URL at the box, run the room).
- Web is IP-agnostic: nginx serves the SPA and proxies /api on the same origin,
so no box IP is baked into the build.
- api.ts: API_BASE now supports a relative same-origin base ("/api"); wsUrl()
derives ws scheme+host from the page for it (absolute prod base unchanged).
- Dockerfile.web: VITE_API_BASE build arg (empty=prod default, "/api"=LAN).
Verified E2E with a real `docker compose up`: SPA 200, /api/healthz ok,
self-register→unclaimed round-trip through the proxy, /api/ws → 101 Switching
Protocols, and 0 hosted-URL refs in the /api build. Web 220 + api 56 green.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Folds the boot animation into the fleet pass — after onboarding each
board (env + self-register + timer/cron), provision-fleet now flashes the
default sketch (sketches/matrix_rain) via flash-sketch.sh. Best-effort: a
flash hiccup logs a warning but doesn't fail the board's onboarding.
- FLASH_DEFAULT=1 (default; 0 skips) and FLASH_SKETCH=<dir> knobs.
- Upfront guards for flash-sketch.sh + the sketch dir.
- provision() refactored to a single success tail (persistence → flash).
Verified on hardware: one pass reports both "ok — onboarding (cron)" and
"ok — boot animation (matrix_rain)".
Co-Authored-By: Claude Opus 4.8 <[email protected]>
- flash-sketch.sh: compile + flash a sketch dir onto a board's MCU over adb
(push → arduino-cli compile with TMPDIR=/tmp → arduino-flash @ 0x80F0000).
Defaults to sketches/matrix_rain; includes a fleet loop over adb devices.
DRYs up the recipe and makes "set the board default" one command.
- Designate matrix_rain as the boot animation boards ship with (the flashed
MCU sketch persists across power cycles). Documented in the sketches README
+ parent Files list.
Flashed matrix_rain to the board via the helper (821 bytes, verified E2E).
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Rounds out the sampler with the remaining effects:
- bloom — a filled diamond grows from the centre to full, then collapses.
- breathe — a dithered shimmer that swells and fades (density ramp via a
fixed per-pixel threshold, since the matrix can't truly fade).
- checker — a checkerboard that inverts on each beat.
Full cycle is now rain -> knight-rider -> comet -> ripple -> bloom ->
breathe -> sparkle -> checker -> wipe. Compiled + flashed E2E (2057 bytes).
Co-Authored-By: Claude Opus 4.8 <[email protected]>
A pixel that bounces off the walls trailing its last 5 cells (a diagonal
streak that bends on each bounce). Wired into the cycle between the
knight-rider sweep and ripple. Compiled + flashed E2E (1637 bytes).
Co-Authored-By: Claude Opus 4.8 <[email protected]>
There was only a scroll-text template; add frame-based animations for the
Uno Q's built-in 13x8 monochrome-blue matrix. Both compiled (arduino:zephyr
0.51.0) and flashed E2E on hardware (OpenOCD @ 0x80F0000, per boards.txt).
- matrix_rain/ — digital rain: staggered per-column drops + trails.
- matrix_effects/ — sampler cycling rain / knight-rider / ripple / sparkle
/ wipe.
- README: the grid->uint32[4] packing pattern (row-major, MSB-first), how to
add an effect, and the compile/flash recipe (TMPDIR=/tmp trap + the correct
0x80F0000 address vs QClaw's stale 0x8100000).
Note: the matrix is physically blue + on/off only — no colour/brightness in
software.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Two host-side helpers that close the operational gap between minting kit
codes and boards-in-hand.
- provision-fleet.sh: provision a whole fleet from fleet.csv (kit,serial).
Per board it pushes the per-kit apess-node.env + apess-selfregister.sh
over adb and enables the boot/refresh systemd timer (MODE=systemd, root)
or an equivalent cron (@reboot + every 5 min; the no-root default).
fleet.csv.example is the assignment template.
- gen-qr-sheet.sh: render the sticker CSV into a self-contained, printable
QR sheet (A4, ~9/page). QRs are baked in as inline SVG via qrencode, so
the HTML has zero external refs — prints offline. Each sticker: kit id,
QR of the claim URL (/workshop?kit=..&code=..), and the 6-digit code.
- .gitignore: never commit kit-codes/ (env files hold FLEET_SECRET +
claim codes) or fleet.csv.
- README: documents both, plus the one-command fleet path.
Verified: provision-fleet loop logic (header/comment/missing skips) and a
4-kit end-to-end sheet build (4 inline SVGs, 0 external refs).
Co-Authored-By: Claude Opus 4.8 <[email protected]>
verify-onboarding.sh runs the self-register + claim chain against a live
APESS API and asserts each HTTP status (correct/wrong fleet secret,
wrong code, unknown kit, successful claim, status, cleanup). Proven
end-to-end against a local API (7/7). bash 3.2-safe (no apostrophes in
${:?} messages, temp-file body capture).
API=… FLEET_SECRET=… [ADMIN_CODE=…] deploy/verify-onboarding.sh
Co-Authored-By: Claude Opus 4.8 <[email protected]>