Recast the cockpit shell: the phase stepper becomes a collapsible LEFT sidebar
(brand + vertical stepper + theme), the center is the editorial content, and the
right pane on Team Registration is a native three.js scene — the Meridian Tower
in a procedural Turin/Po-valley landscape — that slides from night to day the
moment the board connects.
- TowerScene.tsx: the .dc.html scene ported to a native React component (no
iframe), [email protected] via npm, day/night driven by a `night` prop (lerped),
StrictMode-safe WebGL teardown. Code-split so three.js loads only when the
scene renders. Sky textures in public/skyscraper/.
- CockpitLayout: left sidebar + content + tower/rail right pane; tower stretches
to the content height. Stepper is now a vertical, collapsible list.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Move sections to where they read better:
- Phase 1 (Team registration): drop the "Your Agent" block.
- Phase 2 (Meet your agent): host the "Your Agent" section (say-hi / Telegram /
voice) AND the agent chat under it; remove "Pick your domain".
- Module 1 (Domain & events): "Pick your domain" moves here (no more read-only
carry) alongside the ADD Layer 1 card.
- Module 2 (Skills & policies): the chat is gone; the L2/L3 ADD cards show
directly (still auto-prefilled if the three prompts ran in Meet your agent).
Also: OpenYourNode opens the browser-reachable board URL. In self-host/USB mode
the stored url is container-facing (host.docker.internal) which the browser
can't resolve — derive the current host on :8080 instead. Tests updated.
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]>
- WaveformCanvas: a ResizeObserver keeps the drawing buffer matched to the
element size (was measured once at mount), so the trace is centered and peaks
never clip; taller 100px window; clamp to ±45% height.
- useTelemetry: signed oscilloscope signal (idle wave at rest, decaying ring on
impact, bounded ±0.95) instead of a one-sided magnitude that overshot the box.
- useMatrixMirror: poll 8 → 15 fps. The board renders at ~12 fps and a matrix_get
round-trip is only ~15 ms, so 8 fps under-sampled (stutter); 15 fps oversamples
cleanly, well under the ~68 fps ceiling.
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]>
The Module 2 chat showed lifecycle labels ("Agent started / Running i2c_scan /
Agent finished") but never the actual answer. mapNodeEvent dropped the
`tool_call_result` log line, and the observability `agent_end` event is slow and
content-free (the native tool path leaves the final text empty). The real reply
lives in tool_call_result.attributes.output — e.g. i2c_scan →
"No I2C devices responded on the bus." Map it to a `response` activity (or
`error` on a failed tool), so the chat shows the outcome. Falls back to a
"<tool> ✓" marker when a tool returns no output (e.g. a matrix write).
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]>
public/download/ holds a dev-server copy of apess-onboard.zip (baked cloud
token) so the "Download the board app" button works against the Vite dev
server. Never commit it.
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]>
Replace BoardClaim's old `curl | bash` setup command with a download
button (→ /download/apess-onboard.zip, served same-origin by the web
container) and rewrite the steps to the App Lab flow: download → Import an
app → Run → type the code the matrix scrolls. Works on prod and the LAN box
(relative /download URL). Added a test asserting the link + download attr.
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]>
The Zustand store persisted to sessionStorage, which is tab-volatile — a
hard reload / new tab / closing the tab dropped the whole session,
including the board binding (device.connected, nodeUrl, teamId). The
workshop runs on the team's own laptop, so switch the store (and the
offline outbox) to localStorage: the binding + progress now survive a
refresh, hard reload, navigation, and tab close.
Clearing is now explicit only: a new `disconnect()` action drops the board
binding (keeping team/domain/phases/ADD so they can re-bind and resume),
surfaced as a "Disconnect" button on the claimed-board card. `reset()`
still wipes everything.
Tests: add an in-memory localStorage to setupTests (Node 22's disabled
experimental localStorage shadows jsdom's) + clear it per test.
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]>
Say-hi, the module chat, and Refine each hard-coded the 'cloud' agent
while Telegram ran on 'demo' — different agents for web vs Telegram, easy
to get wrong. Introduce a single `AGENT = 'default'` in api.ts and use it
everywhere. 'default' is the node's fallback agent (the one used when no
alias is given), fully loaded with the cloud model + all skills + all
tools — so no call site can pick a different or missing agent.
Board side (config): 'default' is now the sole enabled agent and owns the
telegram.default channel; 'cloud' and 'demo' are disabled and any stray
request for them falls back to 'default'.
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]>
Module 2 is now a chat straight to the agent instead of Build & flash.
Removed the "Open your node" links and the Live board feed card.
New AgentChat component: three imperative canned prompts —
- List the I2C devices on the bus
- Count to 100 and print the value once a second in the LED matrix
- Scroll GO CLAWS on the LED matrix
Each sends to the agent (fire-and-forget, cloud) and streams its
activity (tools/flash/reply) back into the transcript. A prompt is marked
done on its first terminal step (flash/response); an error resets it to
retry. Prompts run one at a time.
Once all three have run successfully, Module 2 reveals "What's next" —
the ADD Layer 2 (Skills) + Layer 3 (Policies & failure) capture — and
Proceed gates on all-three-tried AND L2 + L3 filled.
Note: BuildFlash / LiveBoardFeed / ActorMap are now orphaned (kept for
possible reuse).
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Module 1 is now: domain (carried, read-only) → ADD Layer 1 (domain &
events) → proceed. Removed the Live board feed and Actor map cards. The
node feed hook stays (its online signal still gates Proceed), just no
longer rendered.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Phase 2 "Pick your domain": drop the Refine button + the four-dimension
generation machinery — just capture the domain name and save it for the
modules ahead (DomainPicker is now a single input).
Module 1: show the domain the team named on the previous screen as
read-only ("Your domain" carried over), and remove the "Open your node"
inline link + the editable domain field.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
- Phase 1 proceed button → "Meet your agent".
- Phase strip label + Phase 2 heading: "Meet your node" → "Meet your agent".
- Phase 2 intro now describes the APESS agent (Claude on the edge,
drives the board's devices, on-board fallback).
- "Open your node" card → "Open your agent to explore" (same local
ZeroClaw dashboard link); Pick-your-domain card kept below it.
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]>
Verified against a live board: the open workshop board accepts a remote
config write but refuses a remote /admin/reload (only loopback is
allowed, and the gateway runs inside a container so even host→localhost
is non-loopback). And the board's telegram channel is provisioned under
alias `default`, not `main`.
- bridge configureTelegram: write channels.telegram.default.bot_token
(was .main), and make the remote /admin/reload best-effort — never
fail on its rejection; the board applies the reload itself.
Companion change in zeroclaw firmware (main.py reload_watcher): the
in-container supervisor polls /api/config/reload-status and triggers the
loopback /admin/reload when a change is pending. Proven E2E on the live
board — remote PUT → pending_reload → watcher reload within ~3s, channel
back up.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Reframes the app for the local single-team demo and reworks onboarding.
Landing (local-demo chrome):
- Drop Lecture / staff sign-in / programme / stat tiles / bottom CTA.
- Hero → "an APESS agent on the edge"; CTA → "Start the workshop".
- Presenter keeps redclaw.dev + [email protected] contact.
Phase strip:
- Colour by position off the active phase (earlier=done, current=active,
later=pending) so a stale/persisted flag can't light up a future phase
green (fixes "phase 2 green while on phase 1").
Phase 1 (Team registration) — consolidated onboarding:
- After the board connects, reveal Say hi (moved from Phase 2), Extra
channels (Telegram wizard), and a Voice enable/disable toggle.
- Phase 2 "Meet your node" thins to Open-your-node + Pick-your-domain;
removed the Lock-it-down card. New session `channels {telegram, voice}`
(persist v3, non-destructive migration).
Live Telegram apply (browser → APESS API → board ZeroClaw gateway):
- Wizard Finish pushes the bot token to the running node and restarts it:
PUT /api/config/prop channels.telegram.main.bot_token (gateway
auto-creates the alias + enc2-encrypts the secret) then POST
/admin/reload (in-place subsystem reload → the Telegram channel starts).
- api: bridge configureTelegram + POST /nodes/:teamId/telegram; client
configureTelegram(); wizard shows applying/error and only saves on
success. Remote /admin/reload needs the board's gateway.allow_remote_admin
(+ pairing); on a fully-open board the reload step 403s.
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]>
Board hwId identity (Phase 1), reporter sidecar + central ingest
(Phase 2), and the central fleet dashboard (Phase 3), plus the earlier
workshop failure-first alignment + refine features.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Phase 3 of the edge + control-plane architecture (frontend). The
instructor dashboard becomes fleet-aware: when local instances have
phoned home (central mode), it groups teams under the instance that
reported them, each with an online/offline dot (from heartbeat
last-seen) + team count; otherwise it renders the existing flat grid
unchanged (single-fleet deploys are unaffected).
- src/types.ts: mirror the api — `site?` on TeamSnapshot/SubmissionDTO,
new InstanceDTO, `instance:update` event + `instances` on snapshot.
- api.ts: getInstances(code) (GET /instances; [] on single-fleet).
- useCollective: instances in state + reducer (snapshot seeds, instance:
update upserts, an instance-less snapshot preserves known instances),
polled in the REST seed without dropping the team seed on failure.
- Admin: FleetView (grouped) vs TeamGrid (flat), an Instances stat
(live/total). Shared TeamGrid extracted from the old inline grid.
Judging already works centrally (submission ids are site-namespaced by
the reporter); deeper site-grouping in Judge + Landing-as-distribution
are deferred (plan's "refined as we go").
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]>
The board registry keyed on kitId, so a board that changed IP, was
re-provisioned under a new label, or came up on a wiped laptop looked
like a brand-new board — breaking claim resume and auto-heal.
Key the registry on hwId (WiFi MAC + eMMC serial, survives OS reflash;
machine-id does not). kitId is now a display label only. announce()
takes hwId and upserts by it, preserving claimedBy across IP/label
changes. /nodes/self-register accepts hwId (falls back to kitId for
older nodes). Persist boards in a new sqlite `boards` table keyed by
hwId and seed the in-memory registry from it on boot, so recognition
survives an API restart.
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]>
Phase 2 'Pick your domain' gets a Refine button that asks the team's node (cloud
sonnet) to draft all four design dimensions from the typed domain — Skills/Policies/
Harness/Loops → ADD layers L2-L5. Each card generates behind the scenes, turns green
when ready, and opens a modal with its draft (failure-first framed prompts). Drafts
land in the session store for later modules.
AddLayerForm (incl. Module 1's L1 Domain & events) gets a 🪄 Refine wand that reformats
+ structures the author's notes in place (meaning preserved) for submission.
Both reuse a new askNode() over the blocking say-hi/promptAndWait path. New reusable
Modal. Proven against the board: Skills draft for 'stress fractures' in 5.6s. Tests +196.
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]>
- Open your node: shows a 'Connected' badge (+ board name) once the board is claimed.
- Say hi: replaces the liveness-poll self-test with an actual round-trip to the
agent on the team's own board — sends a greeting, shows the agent's reply, and
only unlocks once it answers. New blocking /nodes/:teamId/say-hi route (greeting
never flashes, so no hang risk) + bridge.sayHi + client sayHi(). The /prompt route
stays fire-and-forget for the flash path.
- Pick your domain already captures to the store and syncs to the server DB via
useCollectiveSync -> pushTeam, available to later steps/judging.
- Proceed gates on connected + agent-replied + domain. Tests updated + say-hi
coverage (route + page).
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Teams already have their boards, so drop the kit pickup + sticker claim. New flow:
run the setup script on your board -> it self-registers and scrolls a code on its
LED matrix -> enter that code to bind the node to your team.
- api: BoardRegistry.claimByCode() binds the unique board whose claimCode matches,
no kit needed; /claim accepts code-first ({teamId, code[, members]}) and keeps the
legacy kit path. Snapshot/response use the resolved board's kitId.
- client: ClaimInput.kit optional, carries members.
- BoardClaim: setup-script command + copy, code from the matrix, 'Bind board'.
- TeamRegistration: remove KitSelector + QR-kit preselect; card is now 'Your board'.
- Tests updated + code-first coverage (registry + HTTP).
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Phase 1 members now entered as inline text rows with a '+ Add member' affordance and
per-row remove, per the pre-deployed-devices onboarding rework. Parent store still
receives only trimmed, non-empty names. Caps at 5. MemberChips retained (unused) for
now. Tests updated.
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]>