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]>
The deck predates v0.8.3, the network-node pivot, and the flash
pathology. Capture what breaks against the app/hardware (stale ADD
layer names, pre-correction schedule, Web-Serial connect flow), the
narrowed flow, and slide-by-slide target copy pinned to addLayers.ts
and Landing.tsx so deck and app cannot re-drift.
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]>
Measured on the board 2026-07-20. Same agent, same 708-token prompt,
phrasing the only variable:
"List the I2C devices on the bus." -> tool call, answered in 20s
"What sensors can you find on the bus?" -> no tool call at all, >200s
The first starter prompt shipped as the interrogative form. Teams typing a
question would have watched their board hang for minutes and concluded the
agent was broken. Now imperative, with the measurement recorded next to the
list so nobody "improves" it back into a question.
Also surfaces the asymmetry to students inline: it is a genuine failure mode
of small on-device models, and belongs in their Layer 3 rather than being
hidden.
Separately confirmed the agent does not hallucinate hardware: asked to list
I2C devices on a board with an empty bus, it ran the tool and reported the
bus numbers rather than inventing a sensor.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
TeamCard displayed team.stats.{calls,nominal,anomalous,critical}, but
nothing has called recordEvent since the simulator was removed, so every
card read "0 · 0 · 0 · 0" - in the pre-pivot classifier vocabulary.
Admin already had real per-team tallies from useCollective.counts (derived
from the node activity stream, and already used for totalFlashes); they
just were not passed down. Cards now show agent runs / flashes / errors,
with errors only rendered when non-zero.
Same root cause as the AddDocument telemetry removed in 5a5810a.
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]>
Test-file TeamSnapshot literals needed the new `domain` field, api.test used
L1: null (now a string), and LiveBoardFeed's NodeActivityKind map was missing the
`fallback` key. tsc --noEmit + vitest passed these (vitest doesn't type-check;
the no-emit config skipped the test files that tsc -b checks); the vite build did not.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
The board is now a Claude-powered ZeroClaw agent with expert skills that teams
talk to directly (its own dashboard / Telegram / voice). The frontend was built
on the older sensor+harness model; this repoints it.
- State/DTO: AddLayers remapped to the 5 design layers (Domain/Skills/Policies/
Harness/Loops); added session.domain (+ mirrored through TeamSnapshot, sync,
and the SQLite store with a guarded migration); dropped Harness/Provider/RunMode;
persist v2 migrate resets stale state.
- Removed the harness/sim/failover machinery (HarnessProviderSelect, HarnessTuner,
HarnessTomlPreview, ResiliencePanel, TriggerButtons, harness.ts, useSerial,
LiveFeed/serial classifier) and every sim-vs-live branch.
- Onboarding: EnvSetup rebuilt into 'Meet your node' — Open-your-node hero (new
reusable OpenYourNode CTA), say-hi-to-your-agent, a free-text DomainPicker
(domain drives L1-L5), Telegram/voice pointers, and the open->locked lockdown
policy step. Domain is required to proceed.
- Modules repointed: M1 Domain & events (L1), M2 Skills & Policies (L2/L3, skills
reference + actuation-gate framing), M3 Harness (reasoning+tiering) & Loops
(cadence) (L4/L5). BuildFlash reframed as a guided 'ask your node to build X'
that hands off to the node dashboard.
- Judge rubric -> Domain fit/Skills/Policies/Harness/Loops; Lecture + Landing
re-storied to the Claude-node + talk-to-your-node narrative.
Verified: web tsc + 181 tests, api tsc + 58 tests, dead-ref sweep clean.
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]>
The board self-registers its gateway URL (http://<lan-ip>:8080) but the API
withheld it from the browser — teams saw only an online boolean. The bearer
token is the only real secret; the URL is safe to expose. Return it in the
/claim response and the node status poll, thread it into device.nodeUrl, and
render an 'Open your node →' deep-link in EnvSetup's connected-device card so a
team can reach its own node's embedded web chat directly on the workshop LAN.
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]>
Fixes a latent bug and hardens claim recovery. Refactors the unclaimed pool
into a BoardRegistry that tracks claimedBy, so a board's binding survives its
own reboot and a team can get back onto its board after an error.
- Auto-heal: a claimed board that reboots re-announces with a fresh IP/token;
self-register now REFRESHES its node binding instead of dumping it back into
the unclaimed pool (previously the team's board went stale/offline + the kit
wrongly reappeared as unclaimed).
- Resume: re-claiming an already-claimed kit with the right code returns the
board's CANONICAL teamId + team snapshot (not a fresh identity). The client
adopts it via a new resumeTeam() action, restoring name/members/phases/stats
so a lost-browser re-claim doesn't clobber synced progress.
- Release: POST /claim/release (admin) frees a kit back to the pool + unbinds
its node, for mis-claims / reassignment. Wired to a small control in /admin.
API 56 tests green; web 220 green; tsc + eslint clean.
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]>
In live mode the sense loop runs on the board (ZeroClaw on the Uno Q),
so the browser observes it rather than reading frames over Web Serial.
- useNodeFeed(teamId, enabled): subscribes to the node's SSE activity
feed (openTeamActivity) and seeds liveness with a one-shot getNodeStatus
(the per-team stream only emits status on change). Inert in sim mode.
- LiveBoardFeed: shows the board's real perception->reason->act activity
+ a live tally (agent runs / flashes / errors) + online indicator.
- Module 1/2 are now mode-aware: sim keeps the synthetic IMU feed +
browser classification (the teaching sandbox); live shows LiveBoardFeed
from the actual board. Proceed gates on board online (M1) / real board
activity (M2) in live, unchanged in sim.
Note: this consumes the board's real activity stream (the signal the
Uno Q emits today). Streaming raw IMU frames for browser-side
classification in live mode would need a board-side sensor emitter
(ZeroClaw firmware) + an API frame relay — a separate piece.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Surfaces the self-register pool in the instructor console so you can tell
at a glance which boards are up but not yet claimed.
- api: GET /nodes/unclaimed (admin-only; kit ids, never secrets) +
broadcast unclaimed:update on self-register (added) and claim (removed).
The WS connect snapshot now carries the current unclaimed kit ids
(attachWs takes the pool), so a freshly opened console is populated
without waiting for a change.
- web: useCollective tracks `unclaimed` (snapshot + unclaimed:update, with
a REST getUnclaimed seed for the poll fallback); Admin shows an
"Unclaimed" stat + a live "Unclaimed boards" panel of kit chips.
Verified live: empty → self-register adds the kit → claim removes it;
judge code is 401 on the endpoint.
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]>
The API gates board self-registration on FLEET_SECRET; wire it through
docker-compose (host .env) alongside ADMIN_CODE/JUDGE_CODE and document
it in the deploy README.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Board-side half of "preloaded + self-register + claim", plus the QR flow.
- apess-selfregister.sh (on-board): pairs locally for a token, discovers
the LAN IP, and announces {kitId, url, token, claimCode} to APESS
/nodes/self-register (x-fleet-secret gated). Retries until APESS is up;
idempotent, safe on boot and on a timer.
- systemd/apess-selfregister.{service,timer}: self-register After the
daemon, re-announce every 5 min so a DHCP lease change can't strand a
board.
- apess-node.env.example: per-board identity (KIT_ID, CLAIM_CODE,
FLEET_SECRET, APESS_URL).
- gen-kit-codes.sh (host): mint per-kit 6-digit codes, write the
per-board env files, and emit the QR sticker CSV
(…/workshop?kit=KIT-NN&code=NNNNNN).
- web: BoardClaim accepts initialCode; TeamRegistration pre-fills it from
?code= so scanning the kit QR fills kit + code — one tap to claim.
- deploy/uno-q/README: documents the whole self-serve onboarding path.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Attendee-facing half of the "preloaded + self-register + claim" flow.
- BoardClaim wizard: three physical bring-up steps (power → boot →
enter code) + a claim-code field that calls POST /claim. On success
the board shows as claimed; ClaimError surfaces the server's message
(wrong code / not powered on / rate-limited).
- TeamRegistration: swaps the Web-Serial "connect device · 115200 baud"
step for the wizard, with a "use the simulator instead" escape hatch
(sim needs no board). serial.ts stays — Module1/2 still use it for the
simulated sense path.
- EnvSetup: live self-test now polls GET /nodes/:teamId/status (board
liveness) instead of reading Web-Serial frames; sim path unchanged.
- api client: claimBoard() + ClaimError + getNodeStatus().
- api: GET /nodes/:teamId/status (public per-team liveness).
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Adds the server side of the "preloaded + self-register + claim" board
onboarding so attendees bring a board online with no operator and no
admin code.
- claim.ts: an unclaimed pool keyed by kitId, holding {url, token,
claimCode}; single-use claim with a constant-time code compare and a
per-kit sliding-window rate limit (MAX_FAILS/WINDOW_MS) to blunt code
guessing.
- POST /nodes/self-register — a booting board announces itself; gated
by a shared FLEET_SECRET (baked into the image), not the admin code.
- POST /claim {kit, teamId, code} — public + rate-limited; validates
the claim code, moves the bearer token straight from the pool into
the node bridge (never touches the browser), binds the board to the
team without clobbering an existing name/members, and emits the
node:status + team:update the live feeds already consume.
- index.ts: wire the pool + FLEET_SECRET env.
22 new tests (claim unit + endpoint integration).
Co-Authored-By: Claude Opus 4.8 <[email protected]>
The landing page only routed attendees; /judge and /admin were
unlinked. Add a subtle "Staff sign-in ▾" disclosure in the header
linking to the judge and instructor consoles (both already gated by
AccessGate). Attendee hero is unchanged.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Front-end still told the old UNO R4 WiFi story (Cortex-M4 / Renesas
RA4M1 / ~6 MB binary / UART→ESP32-S3 / browser-direct Web Serial).
The actual workshop node is the dual-brain Uno Q: ZeroClaw runs on
the quad-A53 Linux side, hosts a local Qwen model, and flashes the
on-board STM32U585 MCU it drives.
- Landing: hero, hardware spec tile, kit pickup, Stack cards, prereqs
- Lecture: Movement 3 (ZeroClaw) rewritten to the Uno Q architecture
- serial.ts + callers: device identity strings uno-r4-wifi -> uno-q
(Web Serial transport mechanism unchanged)
- README stack line
- tests updated to match
Co-Authored-By: Claude Opus 4.8 <[email protected]>
`strings "$BIN" | grep -qm1 …` under `set -o pipefail` fails on a MATCH:
grep -q closes the pipe on first hit, `strings` dies with SIGPIPE, and
pipefail propagates the non-zero pipeline status — so the verify-gate
reported "peripheral tools MISSING" on a binary that actually had them.
Grep the binary directly with `grep -qa` (no pipe, no SIGPIPE).
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Adds deploy/uno-q/build-deploy.sh: cross-compiles ZeroClaw for the Uno Q
(aarch64) on macOS via cargo-zigbuild and deploys it to the board over
adb as a proper drop-in for /home/arduino/zeroclaw.
Bakes in the two things that bit us:
- `--features hardware` is MANDATORY, else the build silently drops the
10 Uno Q peripheral tools (GPIO bridge, flash, sysfs_led, camera,
network, i2cdetect). The script fails fast if the artifact lacks them.
- each adb step runs with `</dev/null` so the shell returns (a trailing
`setsid … &` otherwise hangs the adb call).
Flow: build (hardware, musl static) → verify tools present → push +
sha-check → stop supervisor + kill daemon → swap (keeps .prev.bak) →
relaunch supervisor → poll :8080/health. Also --build-only and
--rollback. Verified: the hardware cross-build produces a static aarch64
binary with the peripheral+flash tools and the streaming-fallback fix.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Prototype #1 of the APESS innovation set: make the Uno Q's unique story —
a real LLM running on the board — visible and demonstrable. A team can
inject a cloud outage and watch their agent keep reasoning on the on-board
Qwen, which is exactly ADD Layer 4 (failure modes) / Layer 5 (edge vs cloud)
made concrete instead of merely documented.
- New `fallback` NodeActivityKind (api + client, kept in sync). `mapNodeEvent`
now recognizes ZeroClaw's failover log lines ("ModelProvider call failed",
"Exhausted retries, trying next model") and surfaces them as a first-class
resilience signal — NOT swallowed by the generic error branch. Rendered in
a distinct rose in the board-activity feeds.
- ResiliencePanel (Module 2): a "Simulate cloud outage" button that routes a
prompt through the board's new `chaos` agent; streams the live failover and
shows a "survived" banner when a fallback is followed by a response. Sim mode
plays a deterministic failover so it demos with zero hardware.
- Board config: a `chaos` agent backed by a deliberately-dead cloud endpoint
(:9099) with `fallback = ["llamacpp.local"]`, so the outage is deterministic
and workshop-safe (no tunnel-hacking, no real cloud to kill).
Tests: api 34, front-end 191 (+ResiliencePanel), typecheck clean, build passes.
Live-on-board validation pending (board USB link down at commit time).
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
A board power-cycle exposed two bugs that left the node dead after boot:
- recover-uno-q.sh step 3 ran `pgrep -f zeroclaw-supervisor`, which matches
the pgrep's OWN shell (its args contain the supervisor path) — so it always
concluded "already running" and never launched the supervisor. Bracket the
pattern (`[z]eroclaw-supervisor.sh`) so only the real process matches.
- The supervisor's single-instance lock only checked that the locked pid was
alive. After a reboot the stale pid can be recycled by an unrelated process,
falsely blocking startup. Now require the live pid's /proc/<pid>/cmdline to
actually be a supervisor before deferring — otherwise treat the lock as stale.
- Also export a full PATH in the supervisor for cron's minimal @reboot env.
Validated on hardware: discriminator matches a real supervisor and rejects
init's pid; fixed recover detects the running supervisor without spawning a
duplicate; board comes back healthy (gateway + llama up).
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
The node bridge opened a board's /api/events stream once; when the board
dropped or its daemon restarted, the async reader just ended and never
reconnected. Registration still read `online` (set at register time), so a
board looked fine while its event stream was dead — participant prompts
appeared to vanish. Only a manual DELETE+POST re-register revived it.
Reproduced live: kill the on-board daemon → the feed went permanently blind.
- subscribeNodeEvents now reconnects with exponential backoff (1s→15s,
reset on a healthy connection) until aborted, and reports each connect/drop
via an onStatus callback.
- The bridge maps onStatus → a deduped node:status, so `online` tracks reality
and the collective + participant feeds get offline/online transitions. A node
that was offline at register is promoted once its stream connects.
Proven: unit tests for drop→reconnect, dedup, and offline-at-register
promotion; and live against the board — killing the daemon now yields
node:status false→true on the participant feed with no re-register.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
A sudden USB/adb drop breaks the Uno Q node in ways that don't self-heal:
adb tunnels vanish, held-shell services die, llama wedges (alive but not
serving), and flashes silently stop landing. systemd is the clean fix with
root — but the dev board's account is expired (no sudo) and has no user
session bus, so neither system nor user units run. These are the no-root
equivalents, both validated on hardware:
- zeroclaw-supervisor.sh — on-board watchdog. Polls the /health ENDPOINTS
(a wedged process passes pgrep but fails here) and restarts llama / the
daemon on death or wedge. Launches children with `setsid … exec` so they
survive the launching shell — the property `nohup … &` in adb shell lacks.
Startup grace avoids reaping llama mid-cold-load; single-instance lock;
no per-restart shell leak. Proven: kill -9 the daemon → auto-restarted.
Boot-persisted via `@reboot` crontab (no root).
- recover-uno-q.sh — host-side. After the board is back, re-does adb + both
tunnels (out :8080, back :8090 shim), ensures the supervisor is running,
and health-checks every hop by endpoint. Proven end-to-end after a
simulated tunnel drop.
README: new Resilience section documenting the no-root reality + both tools.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Stage E deeper — derive real signal from the board event stream:
- Collective reducer tallies per-team {calls, flashes, errors} from
node:activity (thinking→calls, flash→flashes, error→errors); exposed
as `counts` alongside the existing activity feed.
- Admin gains a room-wide "Flashes" pulse stat.
- Judge review card shows a live "Board evidence" panel — streams the
reviewed team's own board activity via its SSE feed, so scoring can
reference real on-device work, not just the submitted code.
Tests: front-end 182, typecheck clean, prod build passes.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Codify the verified board setup into a repeatable kit under deploy/uno-q/:
- config.template.toml — the tested node config: cloud / cloud+fallback /
on-board-Qwen providers, three agents (default / cloud / local) on the
hardware risk profile, gateway, peripherals. Secrets stripped; cloud
endpoint substituted at provision time.
- provision-uno-q.sh — one-shot adb-driven provisioner (no root): install
config, start llama-server + `zeroclaw daemon`, pair for a bearer token,
and POST /nodes to APESS.
- systemd/{zeroclaw-llama,zeroclaw-daemon}.service — production units
(daemon, not `gateway start`, so peripheral tools register).
- README.md — dev (adb) and production (systemd + LAN bind) runbooks.
The individual steps are hardware-verified: all three agent aliases resolve
and route correctly on the board (local→Qwen, cloud→cloud, default→cloud
with Qwen fallback). Full script orchestration + live phone-home land once
the api (with /nodes) is redeployed.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>