Commit Graph
96 Commits
Author SHA1 Message Date
Omar SobhandClaude Opus 4.8 7780903278 feat(module2): chat with your agent — three canned prompts, then Layers 2/3
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]>
2026-07-22 01:39:25 -07:00
Omar SobhandClaude Opus 4.8 c2ef338888 refactor(module1): drop live board feed + actor map; focus on Layer 1
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]>
2026-07-22 01:20:08 -07:00
Omar SobhandClaude Opus 4.8 f6979a685a refactor(workshop): domain is name-only; Module 1 carries it read-only
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]>
2026-07-22 01:17:17 -07:00
Omar SobhandClaude Opus 4.8 e0bcf51b08 refactor(workshop): "meet your agent" framing for Phase 2
- 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]>
2026-07-22 01:01:34 -07:00
Omar SobhandClaude Opus 4.8 66723ebd35 feat(telegram): enable the channel on apply + guarantee Telegram-ready boards
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]>
2026-07-22 00:38:29 -07:00
Omar SobhandClaude Opus 4.8 a4d06ebddb fix(telegram): apply bot token to the real alias + in-container reload path
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]>
2026-07-22 00:33:51 -07:00
Omar SobhandClaude Opus 4.8 59aa2365c2 Merge feat/local-demo-phase1: local-demo landing + Phase 1 rework + live Telegram setup
Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-22 00:24:11 -07:00
Omar SobhandClaude Opus 4.8 063116507b feat(workshop): local-demo landing + consolidated Phase 1 + live Telegram setup
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]>
2026-07-22 00:24:05 -07:00
Omar SobhandClaude Opus 4.8 6386884842 Merge feat/central-mode-and-judge-site: judge site tag + central-mode deploy wiring
Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-21 23:38:40 -07:00
Omar SobhandClaude Opus 4.8 0adeb75236 feat(federation): site tag in judge queue + central-mode deploy wiring
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]>
2026-07-21 23:34:39 -07:00
Omar SobhandClaude Opus 4.8 49d75111f1 Merge feat/workshop-failure-first-alignment: edge + control-plane federation
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]>
2026-07-21 23:28:53 -07:00
Omar SobhandClaude Opus 4.8 45cc3a1f85 feat(fleet): central fleet dashboard — instances → teams grouped by site
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]>
2026-07-21 23:24:22 -07:00
Omar SobhandClaude Opus 4.8 5ef14655b9 feat(federation): reporter sidecar + central ingest (edge → control plane)
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]>
2026-07-21 23:18:03 -07:00
Omar SobhandClaude Opus 4.8 60416cb8c7 feat(api): anchor board identity on durable hwId + persist the registry
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]>
2026-07-21 23:08:50 -07:00
Omar SobhandClaude Opus 4.8 d71899520e fix(provision): remove legacy @reboot supervisor cron (App Lab owns boot)
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]>
2026-07-21 20:18:32 -07:00
Omar SobhandClaude Opus 4.8 410e5aba68 fix(provision): carry hardware skills into the node app (skill_bundles)
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]>
2026-07-21 18:54:38 -07:00
Omar SobhandClaude Opus 4.8 bedb1b4adc fix(provision): carry web-dist into the app + repoint web_dist_dir; drop stray tag
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]>
2026-07-21 18:17:30 -07:00
Omar SobhandClaude Opus 4.8 40bee45842 feat(node): self-register derives node url from source IP; App Lab provisioner
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]>
2026-07-21 13:07:32 -07:00
Omar SobhandClaude Opus 4.8 8001e100c3 feat(refine): AI Refine on domain (Phase 2) + wand on ADD layers
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]>
2026-07-21 10:16:18 -07:00
Omar SobhandClaude Opus 4.8 9d44931752 feat(uno-q): agent-mode.sh — flip all agents cloud/offline
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]>
2026-07-21 09:58:05 -07:00
Omar SobhandClaude Opus 4.8 cefddb245c feat(phase2): real say-hi chat + Connected node state on Meet-your-node
- 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]>
2026-07-21 09:26:50 -07:00
Omar SobhandClaude Opus 4.8 fc5f66021f feat(reg): correct Phase 1 for pre-deployed devices + matrix-code binding
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]>
2026-07-21 06:18:09 -07:00
Omar SobhandClaude Opus 4.8 1b31f6db1b feat(reg): inline MemberFields (+ add-row) replacing the chip input
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]>
2026-07-21 06:08:28 -07:00
Omar SobhandClaude Opus 4.8 1642fbce39 docs(demo): all three demo secrets now vaulted in Infisical
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]>
2026-07-21 05:12:34 -07:00
Omar SobhandClaude Opus 4.8 eb8c41b14b docs(demo): add DEMO-RUNBOOK.md; ElevenLabs key sourced from Infisical
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]>
2026-07-21 05:06:07 -07:00
Omar SobhandClaude Opus 4.8 413230255e feat(voice): ElevenLabs TTS in serve.py (server-side, key env-only)
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]>
2026-07-21 04:58:54 -07:00
Omar SobhandClaude Opus 4.8 3fef8c4d23 feat(uno-q): one-command demo-node recovery after USB drop
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]>
2026-07-21 04:51:43 -07:00
Omar SobhandClaude Opus 4.8 b09068b60c feat(voice): same-origin proxy client reusing the reliable /webhook path
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]>
2026-07-21 04:43:24 -07:00
Omar SobhandClaude Opus 4.8 1c762e51ba feat(voice): browser voice client for talking to the on-board agent
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]>
2026-07-20 21:54:10 -07:00
Omar SobhandClaude Opus 4.8 62e4a3b688 docs(deck): revision guide reconciling the lecture deck to shipped reality
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]>
2026-07-20 15:28:17 -07:00
Omar SobhandClaude Opus 4.8 37a54521e8 docs(uno-q): record the offline capability boundary
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]>
2026-07-20 09:09:48 -07:00
Omar SobhandClaude Opus 4.8 e27fad46e2 fix(workshop): starter prompts must be imperative, not interrogative
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]>
2026-07-20 08:45:03 -07:00
Omar SobhandClaude Opus 4.8 32750b0d9d fix(admin): show real node tallies on team cards, not dead counters
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]>
2026-07-20 04:06:30 -07:00
Omar SobhandClaude Opus 4.8 5a5810a1c8 feat(workshop): align to the real programme; failure-first framing
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]>
2026-07-19 17:26:04 -07:00
Omar SobhandClaude Opus 4.8 a23a525aef docs(uno-q): measure on-board inference; retire the LiteRT spike
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]>
2026-07-19 15:39:10 -07:00
Omar SobhandClaude Opus 4.8 e2ce67f069 fix(web): production-build type errors (tsc -b caught what --noEmit missed)
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]>
2026-07-16 15:44:20 -07:00
Omar SobhandClaude Opus 4.8 6d94833bb9 feat(web): reshape workshop from 'tune a harness' to 'design a domain node'
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]>
2026-07-16 15:33:38 -07:00
Omar SobhandClaude Opus 4.8 3be44ec215 Merge feat/uno-q-cross-build-deploy: resilience theater UI + cross-build helper
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]>
2026-07-16 13:12:01 -07:00
Omar SobhandClaude Opus 4.8 766f2946af chore: add workshop-llm LiteLLM proxy deploy + ignore vendored/generated files
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]>
2026-07-16 11:42:05 -07:00
Omar SobhandClaude Opus 4.8 62c435c648 feat(uno-q): bundle the granular skill set alongside arduino-uno-q on every node
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]>
2026-07-16 09:32:11 -07:00
Omar SobhandClaude Opus 4.8 d2135a1938 feat(uno-q): ship the arduino-uno-q expert skill on every node by default
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]>
2026-07-16 09:14:00 -07:00
Omar SobhandClaude Opus 4.8 7233a4b1c0 feat(uno-q): LAN-open boot + Telegram self-serve + guided lockdown modalities
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]>
2026-07-15 16:35:28 -07:00
Omar SobhandClaude Opus 4.8 af9cae0abc feat(onboarding): surface board LAN URL so teams open their node's ZeroClaw web UI
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]>
2026-07-15 16:26:15 -07:00
Omar SobhandClaude Opus 4.8 95c947be5b feat(deploy): LAN stack — run APESS in the room so the API can reach the boards
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]>
2026-07-08 17:25:06 -07:00
Omar SobhandClaude Opus 4.8 2c18e67c1b feat(onboarding): board re-attach — auto-heal reboots, resume lost browsers, instructor release
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]>
2026-07-08 17:16:14 -07:00
Omar SobhandClaude Opus 4.8 4d9d984b04 feat(uno-q): provision-fleet flashes the default boot animation
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]>
2026-07-08 15:46:27 -07:00
Omar SobhandClaude Opus 4.8 a2ccf519d4 feat(uno-q): flash-sketch helper + matrix_rain as the board boot default
- 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]>
2026-07-08 15:40:40 -07:00
Omar SobhandClaude Opus 4.8 5f2456e5cb feat(uno-q): add bloom, breathe, checker effects to the matrix sampler
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]>
2026-07-08 14:51:40 -07:00
Omar SobhandClaude Opus 4.8 45a8feb8b4 feat(uno-q): add comet effect to the matrix sampler
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]>
2026-07-08 14:49:52 -07:00
Omar SobhandClaude Opus 4.8 2683c64c34 feat(uno-q): LED-matrix animation sketches (blue 13x8)
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]>
2026-07-08 14:47:53 -07:00