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]>
This commit is contained in:
Omar Sobh
2026-07-19 17:26:04 -07:00
co-authored by Claude Opus 4.8
parent a23a525aef
commit 5a5810a1c8
13 changed files with 235 additions and 73 deletions
+109
View File
@@ -0,0 +1,109 @@
#!/usr/bin/env bash
# Back up a student's Uno Q before reflashing it for the APESS workshop.
#
# ./backup-uno-q.sh <adb-serial> [team-label] [outdir]
#
# Students arrive having spent a week doing their own sensor work on these
# boards. THAT WORK IS IRREPLACEABLE — this runs before provision-uno-q.sh and
# must succeed before anything is overwritten.
#
# Strategy: DENYLIST, not allowlist. We do not know where a given team put
# their data (App Lab project dir, a loose CSV, a sketch folder), so we capture
# all of /home/arduino and exclude only what we can reproduce ourselves:
# the GGUF models, the Arduino core cache, our llama/zeroclaw binaries, the
# embedded SPA, and build output. On a reference board that leaves ~1-2 MB;
# a board with a week of logged data will be larger but still quick.
#
# The archive is VERIFIED READABLE before the script reports success — an
# unverified backup is not a backup.
set -euo pipefail
SERIAL="${1:?usage: backup-uno-q.sh <adb-serial> [team-label] [outdir]}"
LABEL="${2:-$SERIAL}"
OUTDIR="${3:-./backups}"
a() { adb -s "$SERIAL" "$@"; }
STAMP="$(date -u +%Y%m%dT%H%M%SZ)"
NAME="unoq-${LABEL}-${STAMP}"
REMOTE_TAR="/tmp/${NAME}.tar.gz"
LOCAL_TAR="${OUTDIR}/${NAME}.tar.gz"
LOCAL_MANIFEST="${OUTDIR}/${NAME}.manifest.txt"
# Reproducible — ours, or regenerable. Everything else is theirs and is kept.
#
# DELIBERATELY NOT EXCLUDED: ~/.local/share/arduino-app-cli/examples (~37M).
# Those are Arduino's stock App Lab examples, so in principle reproducible —
# but several of them (real-time-accelerometer, anomaly-detection,
# air-quality-monitoring) are exactly what a student doing sensor work would
# open and then edit IN PLACE. Excluding them would silently discard a week of
# work to save 25MB. We keep them. A backup is insurance, not a size contest.
EXCLUDES=(
'--exclude=./models' # 1.5G of GGUF weights — we push these
'--exclude=./.arduino15' # ~570M Arduino cores/index — arduino-cli refetches
'--exclude=./llama' # llama.cpp binaries — we push these
'--exclude=./web-dist' # embedded SPA build — we push this
'--exclude=./.cache' # regenerable
'--exclude=./lost+found'
'--exclude=./zeroclaw' # the daemon binary...
'--exclude=./zeroclaw.*' # ...and its .bak copies
'--exclude=./llama8083.log'
'--exclude=./zc-daemon.log'
'--exclude=./zc-supervisor.log'
'--exclude=*/build' # sketch build output — recompiled on demand
)
mkdir -p "$OUTDIR"
echo "==> [1/5] board reachable?"
a shell 'echo ok' >/dev/null 2>&1 || { echo " FAIL: board $SERIAL not reachable over adb"; exit 1; }
a shell 'test -d /home/arduino' >/dev/null 2>&1 || { echo " FAIL: /home/arduino missing"; exit 1; }
echo " $SERIAL ok"
echo "==> [2/5] inventory what will be captured"
a shell '
cd /home/arduino || exit 1
echo " user-work directories:"
for d in sketches Arduino ArduinoApps .arduino-bricks; do
if [ -e "$d" ]; then printf " %-18s %s\n" "$d" "$(du -sh "$d" 2>/dev/null | cut -f1)"; fi
done
echo " data-shaped files (top 10 by size):"
find . -maxdepth 4 \( -name "*.csv" -o -name "*.tsv" -o -name "*.dat" -o -name "*.json" -o -name "*.txt" -o -name "*.py" \) \
-not -path "./.arduino15/*" -not -path "./.cache/*" -not -path "*/build/*" -not -path "./.zeroclaw/*" \
-printf " %s\t%p\n" 2>/dev/null | sort -rn | head -10
echo " (none found)"
' 2>/dev/null || true
echo "==> [3/5] archive on board"
a shell "cd /home/arduino && tar czf '$REMOTE_TAR' ${EXCLUDES[*]} . 2>/dev/null; echo done" >/dev/null
SIZE="$(a shell "du -h '$REMOTE_TAR' 2>/dev/null | cut -f1" | tr -d '\r\n ')"
echo " archive: $SIZE"
echo "==> [4/5] pull + verify"
a pull "$REMOTE_TAR" "$LOCAL_TAR" >/dev/null 2>&1 || { echo " FAIL: could not pull archive"; exit 1; }
# An unverified backup is not a backup: list the archive and require real content.
if ! tar tzf "$LOCAL_TAR" > "$LOCAL_MANIFEST" 2>/dev/null; then
echo " FAIL: archive is not readable — DO NOT REFLASH THIS BOARD"
exit 1
fi
ENTRIES="$(wc -l < "$LOCAL_MANIFEST" | tr -d ' ')"
if [ "$ENTRIES" -lt 5 ]; then
echo " FAIL: archive has only $ENTRIES entries — suspiciously empty, DO NOT REFLASH"
exit 1
fi
echo " verified: $ENTRIES entries, manifest at $LOCAL_MANIFEST"
echo "==> [5/5] clean up board temp"
a shell "rm -f '$REMOTE_TAR'" >/dev/null 2>&1 || true
cat <<EOF
BACKUP OK — safe to reflash $SERIAL
archive : $LOCAL_TAR ($SIZE)
manifest: $LOCAL_MANIFEST
restore (after reflash):
adb -s $SERIAL push $LOCAL_TAR /tmp/restore.tar.gz
adb -s $SERIAL shell 'cd /home/arduino && tar xzf /tmp/restore.tar.gz && rm /tmp/restore.tar.gz'
EOF
+7 -17
View File
@@ -1,5 +1,6 @@
import { type ReactNode } from 'react' import { type ReactNode } from 'react'
import { useSession } from '@/store/session' import { useSession } from '@/store/session'
import { ADD_LAYERS } from '@/lib/addLayers'
function LayerBlock({ n, title, body }: { n: number; title: string; body: ReactNode }) { function LayerBlock({ n, title, body }: { n: number; title: string; body: ReactNode }) {
return ( return (
@@ -20,7 +21,6 @@ export function AddDocument() {
const team = useSession((s) => s.team) const team = useSession((s) => s.team)
const add = useSession((s) => s.add) const add = useSession((s) => s.add)
const domain = useSession((s) => s.domain) const domain = useSession((s) => s.domain)
const stats = useSession((s) => s.stats)
return ( return (
<article <article
@@ -38,23 +38,13 @@ export function AddDocument() {
</div> </div>
</header> </header>
<LayerBlock n={1} title="Domain & events" body={add.L1} /> {ADD_LAYERS.map((l) => (
<LayerBlock n={2} title="Skills" body={add.L2} /> <LayerBlock key={l.key} n={l.n} title={l.title} body={add[l.key]} />
<LayerBlock n={3} title="Policies" body={add.L3} /> ))}
<LayerBlock n={4} title="Harness" body={add.L4} />
<LayerBlock n={5} title="Loops" body={add.L5} />
<div className="grid sm:grid-cols-2 gap-4 border-t border-border pt-4 break-inside-avoid"> <div className="border-t border-border pt-4 break-inside-avoid space-y-1">
<div className="space-y-1"> <div className="font-mono text-[10px] uppercase tracking-widest text-muted-foreground">Domain</div>
<div className="font-mono text-[10px] uppercase tracking-widest text-muted-foreground">Domain</div> <div className="font-mono text-[11px]">{domain || '—'}</div>
<div className="font-mono text-[11px]">{domain || '—'}</div>
</div>
<div className="space-y-1">
<div className="font-mono text-[10px] uppercase tracking-widest text-muted-foreground">Session</div>
<div className="font-mono text-[11px]">
{stats.calls} frames · {stats.nominal} nominal · {stats.anomalous} anomalous · {stats.critical} critical
</div>
</div>
</div> </div>
</article> </article>
) )
+3 -10
View File
@@ -1,11 +1,5 @@
import type { SubmissionDTO } from '@/types' import type { SubmissionDTO } from '@/types'
import { ADD_LAYERS } from '@/lib/addLayers'
const LAYERS: { key: 'L2' | 'L3' | 'L4' | 'L5'; title: string }[] = [
{ key: 'L2', title: 'Reasoning policy' },
{ key: 'L3', title: 'Action contract' },
{ key: 'L4', title: 'Failure modes' },
{ key: 'L5', title: 'AI-native redesign' },
]
function Block({ n, title, body }: { n: number; title: string; body: string }) { function Block({ n, title, body }: { n: number; title: string; body: string }) {
return ( return (
@@ -31,9 +25,8 @@ export function AddReview({ submission }: AddReviewProps) {
<div className="font-mono text-[10px] uppercase tracking-widest text-muted-foreground">{submission.code}</div> <div className="font-mono text-[10px] uppercase tracking-widest text-muted-foreground">{submission.code}</div>
<h3 className="text-lg font-bold tracking-tight">{submission.teamName || submission.teamId}</h3> <h3 className="text-lg font-bold tracking-tight">{submission.teamName || submission.teamId}</h3>
</header> </header>
<Block n={1} title="Domain & events" body={submission.add.L1} /> {ADD_LAYERS.map((l) => (
{LAYERS.map((l, i) => ( <Block key={l.key} n={l.n} title={l.title} body={submission.add[l.key]} />
<Block key={l.key} n={i + 2} title={l.title} body={submission.add[l.key]} />
))} ))}
</article> </article>
) )
+45 -16
View File
@@ -14,6 +14,19 @@ interface Entry {
label: string label: string
} }
/**
* Starter prompts. Deliberately sensor-agnostic — teams wired different devices
* during their own sensing week, so the agent discovers what is actually on the
* bus rather than being told. The last one needs no sensor at all, as a fallback
* for hardware that will not enumerate.
*/
const EXAMPLE_PROMPTS = [
'What sensors can you find on the I2C bus?',
'Read my sensor and print a value once a second',
'Light the matrix red when the reading crosses a threshold I set',
'Scroll GO CLAWS on the LED matrix',
]
const KIND_DOT: Record<NodeActivityKind, string> = { const KIND_DOT: Record<NodeActivityKind, string> = {
thinking: 'bg-muted-foreground', thinking: 'bg-muted-foreground',
tool: 'bg-amber', tool: 'bg-amber',
@@ -68,26 +81,42 @@ export function BuildFlash() {
<CardContent className="space-y-4"> <CardContent className="space-y-4">
<div className="flex items-start justify-between gap-4"> <div className="flex items-start justify-between gap-4">
<p className="text-sm text-muted-foreground leading-relaxed max-w-md"> <p className="text-sm text-muted-foreground leading-relaxed max-w-md">
Ask your node to build something — it uses its skills, writes a sketch, and flashes the MCU. Point the agent at the sensor you already have wired. It finds the device, writes a sketch using
The conversation lives in the node; watch it happen there while the activity feed streams here. its skills, and flashes the MCU — sense, decide, act, on your own hardware. The conversation lives
in the node; the activity feed streams here.
</p> </p>
<OpenYourNode variant="inline" className="shrink-0 mt-0.5" /> <OpenYourNode variant="inline" className="shrink-0 mt-0.5" />
</div> </div>
<div className="flex gap-2"> <div className="space-y-2">
<Input <div className="flex flex-wrap gap-1.5">
aria-label="Prompt your board" {EXAMPLE_PROMPTS.map((ex) => (
placeholder="e.g. scroll the message GO CLAWS on the LED matrix" <button
value={prompt} key={ex}
onChange={(e) => setPrompt(e.target.value)} type="button"
onKeyDown={(e) => { onClick={() => setPrompt(ex)}
if (e.key === 'Enter') void run() className="font-mono text-[10px] px-2 py-1 rounded border border-border bg-background hover:bg-muted text-muted-foreground hover:text-foreground transition-colors text-left"
}} >
className="font-mono text-sm" {ex}
/> </button>
<Button onClick={() => void run()} disabled={busy || !prompt.trim()}> ))}
{busy ? 'Working…' : 'Send'} </div>
</Button>
<div className="flex gap-2">
<Input
aria-label="Prompt your board"
placeholder="Ask your node to read your sensor and act on it…"
value={prompt}
onChange={(e) => setPrompt(e.target.value)}
onKeyDown={(e) => {
if (e.key === 'Enter') void run()
}}
className="font-mono text-sm"
/>
<Button onClick={() => void run()} disabled={busy || !prompt.trim()}>
{busy ? 'Working…' : 'Send'}
</Button>
</div>
</div> </div>
<ul data-testid="activity-log" className="space-y-1.5 min-h-[3rem]"> <ul data-testid="activity-log" className="space-y-1.5 min-h-[3rem]">
+2 -1
View File
@@ -34,7 +34,8 @@ export function DomainPicker() {
onChange={(e) => setDomain(e.target.value)} onChange={(e) => setDomain(e.target.value)}
/> />
<p className="font-mono text-[10px] text-muted-foreground leading-relaxed"> <p className="font-mono text-[10px] text-muted-foreground leading-relaxed">
Name the domain your node is for and the events it senses. This frames everything you design next. Name the domain your node is for and the events it senses — ideally the one you have been measuring
already. This frames everything you design next.
</p> </p>
</div> </div>
+5 -5
View File
@@ -24,10 +24,10 @@ describe('PhaseStrip', () => {
expect(phases).toHaveLength(5) expect(phases).toHaveLength(5)
expect(phases.map((p) => p.textContent)).toEqual([ expect(phases.map((p) => p.textContent)).toEqual([
expect.stringContaining('Team reg'), expect.stringContaining('Team reg'),
expect.stringContaining('Env setup'), expect.stringContaining('Meet your node'),
expect.stringContaining('Module 1'), expect.stringContaining('Module 1'),
expect.stringContaining('Module 2'), expect.stringContaining('Module 2'),
expect.stringContaining('ADD · submit'), expect.stringContaining('Module 3'),
]) ])
}) })
@@ -42,7 +42,7 @@ describe('PhaseStrip', () => {
useSession.getState().completePhase('setup') useSession.getState().completePhase('setup')
renderAt('/workshop/module1', 'm1') renderAt('/workshop/module1', 'm1')
expect(screen.getByText('Team reg').closest('a')).toHaveAttribute('data-state', 'done') expect(screen.getByText('Team reg').closest('a')).toHaveAttribute('data-state', 'done')
expect(screen.getByText('Env setup').closest('a')).toHaveAttribute('data-state', 'done') expect(screen.getByText('Meet your node').closest('a')).toHaveAttribute('data-state', 'done')
expect(screen.getByText('Module 1').closest('a')).toHaveAttribute('data-state', 'active') expect(screen.getByText('Module 1').closest('a')).toHaveAttribute('data-state', 'active')
expect(screen.getByText('Module 2').closest('a')).toHaveAttribute('data-state', 'pending') expect(screen.getByText('Module 2').closest('a')).toHaveAttribute('data-state', 'pending')
}) })
@@ -50,9 +50,9 @@ describe('PhaseStrip', () => {
it('links each phase to its workshop sub-route', () => { it('links each phase to its workshop sub-route', () => {
renderAt('/workshop') renderAt('/workshop')
expect(screen.getByText('Team reg').closest('a')).toHaveAttribute('href', '/workshop') expect(screen.getByText('Team reg').closest('a')).toHaveAttribute('href', '/workshop')
expect(screen.getByText('Env setup').closest('a')).toHaveAttribute('href', '/workshop/setup') expect(screen.getByText('Meet your node').closest('a')).toHaveAttribute('href', '/workshop/setup')
expect(screen.getByText('Module 1').closest('a')).toHaveAttribute('href', '/workshop/module1') expect(screen.getByText('Module 1').closest('a')).toHaveAttribute('href', '/workshop/module1')
expect(screen.getByText('Module 2').closest('a')).toHaveAttribute('href', '/workshop/module2') expect(screen.getByText('Module 2').closest('a')).toHaveAttribute('href', '/workshop/module2')
expect(screen.getByText('ADD · submit').closest('a')).toHaveAttribute('href', '/workshop/add') expect(screen.getByText('Module 3').closest('a')).toHaveAttribute('href', '/workshop/add')
}) })
}) })
+2 -2
View File
@@ -10,10 +10,10 @@ interface PhaseMeta {
const PHASES: PhaseMeta[] = [ const PHASES: PhaseMeta[] = [
{ key: 'reg', name: 'Team reg', to: '/workshop' }, { key: 'reg', name: 'Team reg', to: '/workshop' },
{ key: 'setup', name: 'Env setup', to: '/workshop/setup' }, { key: 'setup', name: 'Meet your node', to: '/workshop/setup' },
{ key: 'm1', name: 'Module 1', to: '/workshop/module1' }, { key: 'm1', name: 'Module 1', to: '/workshop/module1' },
{ key: 'm2', name: 'Module 2', to: '/workshop/module2' }, { key: 'm2', name: 'Module 2', to: '/workshop/module2' },
{ key: 'add', name: 'ADD · submit', to: '/workshop/add' }, { key: 'add', name: 'Module 3', to: '/workshop/add' },
] ]
export interface PhaseStripProps { export interface PhaseStripProps {
+19
View File
@@ -0,0 +1,19 @@
import type { AddLayers } from '@/store/session'
/**
* The five ADD layers, in order — the single source of truth for their numbers
* and titles.
*
* These titles were previously duplicated in AddDocument (what the student
* writes) and AddReview (what the judge reads). The two drifted after the
* domain-node reshape, so judges were scoring "Skills" under the heading
* "Reasoning policy". Both now render from this list; add a layer here and
* both surfaces follow.
*/
export const ADD_LAYERS: { key: keyof AddLayers; n: number; title: string }[] = [
{ key: 'L1', n: 1, title: 'Domain & events' },
{ key: 'L2', n: 2, title: 'Skills' },
{ key: 'L3', n: 3, title: 'Policies' },
{ key: 'L4', n: 4, title: 'Harness' },
{ key: 'L5', n: 5, title: 'Loops' },
]
+19 -6
View File
@@ -68,15 +68,28 @@ export function AddBuilder() {
<div className="grid lg:grid-cols-2 gap-6 print:hidden"> <div className="grid lg:grid-cols-2 gap-6 print:hidden">
<AddLayerForm <AddLayerForm
layer="L4" layer="L4"
title="ADD · Layer 4 — Harness (reasoning + tiering)" title="ADD · Layer 4 — Harness (where each decision runs)"
description="How it reasons — Claude via the Max token, on-board Qwen as the offline fallback — and when it escalates to a home hub, phone, or cloud." description="Which decisions run on the board, which escalate to the cloud — and what still works with no network at all. Describe the degradation path, not just the happy path."
placeholder="Reason locally with on-board Qwen; escalate ambiguous calls to Claude via the Max token; fall back to the home hub when offline." placeholder={
'Routine checks: on-board model, no network needed.\n' +
'Ambiguous or high-consequence calls: escalate to the cloud model.\n\n' +
'Degradation path:\n' +
'• cloud slow or rate-limited → fall back on-board, note reduced confidence\n' +
'• no network at all → keep sensing, logging and safing locally; queue anything that needs escalation\n' +
'• on-board model unavailable → stop actuating, alert, keep recording'
}
/> />
<AddLayerForm <AddLayerForm
layer="L5" layer="L5"
title="ADD · Layer 5 — Loops (autonomous cadence)" title="ADD · Layer 5 — Loops (cadence, and what happens when a cycle fails)"
description="How it monitors its domain over time — cron / heartbeat — and reports by exception." description="How often it checks, what it reports by exception — and how the loop behaves when a cycle fails: stale readings, missed ticks, partial data."
placeholder="Heartbeat every 30s; sample the IMU each minute; report only on anomaly; nightly cron summary." placeholder={
'Heartbeat every 30 s; sample the sensor each minute; report only on exception; daily summary.\n\n' +
'When a cycle fails:\n' +
'• missed tick → skip, do not back-fill invented data\n' +
'• partial data → report what is missing, not an average of what is left\n' +
'• N consecutive failures → escalate to a human and stop acting on the readings'
}
/> />
</div> </div>
+1 -1
View File
@@ -21,7 +21,7 @@ describe('Landing', () => {
it('exposes the four PRD success-metric stats', () => { it('exposes the four PRD success-metric stats', () => {
renderLanding() renderLanding()
expect(screen.getByText(/5h 30m/i)).toBeInTheDocument() expect(screen.getByText(/14:00 – 19:00/i)).toBeInTheDocument()
expect(screen.getByText(/15 teams/i)).toBeInTheDocument() expect(screen.getByText(/15 teams/i)).toBeInTheDocument()
expect(screen.getAllByText(/Arduino Uno Q/i).length).toBeGreaterThan(0) expect(screen.getAllByText(/Arduino Uno Q/i).length).toBeGreaterThan(0)
}) })
+12 -11
View File
@@ -11,12 +11,13 @@ interface ProgrammeRow {
} }
const PROGRAMME: ProgrammeRow[] = [ const PROGRAMME: ProgrammeRow[] = [
{ time: '13:00', title: 'Arrival & kit pickup', desc: 'Teams collect Arduino Uno Q boards + sensor kits.', tags: ['setup'] }, { time: '10:45', title: 'Lecture · Agentic design thinking', desc: 'Separate morning session — the five layers, and designing for failure.', tags: ['lecture'] },
{ time: '14:00', title: 'Lecture · 5 movements', desc: 'Domain & events, skills, policies, harness, loops.', tags: ['lecture'] }, { time: '14:00', title: 'Arrival & registration', desc: 'Boards backed up and reflashed for the workshop while you register.', tags: ['setup'] },
{ time: '15:00', title: 'Module 1 · Domain & Skills', desc: 'Pick a domain, name its events, draft the skill library — Layers 1 + 2.', tags: ['build'] }, { time: '14:25', title: 'Meet your node', desc: 'The board you already know — now carrying an agent that can drive your devices.', tags: ['setup'] },
{ time: '16:15', title: 'Module 2 · Policies & Harness', desc: 'Set the actuation gate, tier the reasoning, talk to your node — Layers 3 + 4.', tags: ['build'] }, { time: '14:45', title: 'Module 1 · Domain & events', desc: 'Your sensors, your data, the events that matter — Layer 1.', tags: ['build'] },
{ time: '17:45', title: 'ADD builder & submit', desc: 'Design the autonomous loops, PDF export, submission — Layer 5.', tags: ['add'] }, { time: '16:10', title: 'Module 2 · Skills & policies', desc: 'Drive a real sensor, enumerate the failure states, set the actuation gate — Layers 2 + 3.', tags: ['build'] },
{ time: '19:00', title: 'Judging & award', desc: 'Demartino panel reviews ADDs; RedClaw Systems award announced.', tags: ['judge'] }, { time: '17:40', title: 'Module 3 · Harness, loops & submit', desc: 'How it degrades, how often it runs, then submit — Layers 4 + 5.', tags: ['add'] },
{ time: '19:00', title: 'Judging & award', desc: 'Panel reviews the Agent Design Documents; RedClaw Systems award announced.', tags: ['judge'] },
] ]
const STACK = [ const STACK = [
@@ -80,9 +81,9 @@ export function Landing() {
<span className="text-primary">a Claude agent on the edge</span> <span className="text-primary">a Claude agent on the edge</span>
</h1> </h1>
<p className="text-base md:text-lg text-muted-foreground leading-relaxed max-w-2xl mx-auto"> <p className="text-base md:text-lg text-muted-foreground leading-relaxed max-w-2xl mx-auto">
Pick a domain, then engineer the skills, policies, harness, and loops of a Claude-powered ZeroClaw node Your Uno Q already senses. Today it gets an agent — one you talk to, one that drives your devices, and
on an Arduino Uno Q — one you talk to and one that works on its own. Complete a five-layer Agent Design one that keeps working when things break. Design its skills, policies, harness and loops around the
Document in a single 5.5-hour session. failure states you find, and leave with a five-layer Agent Design Document.
</p> </p>
<div className="flex flex-wrap gap-3 justify-center pt-4"> <div className="flex flex-wrap gap-3 justify-center pt-4">
<Button asChild size="lg"> <Button asChild size="lg">
@@ -95,7 +96,7 @@ export function Landing() {
</div> </div>
<div className="max-w-4xl mx-auto mt-16 grid grid-cols-2 md:grid-cols-4 gap-px bg-border rounded-md overflow-hidden text-center"> <div className="max-w-4xl mx-auto mt-16 grid grid-cols-2 md:grid-cols-4 gap-px bg-border rounded-md overflow-hidden text-center">
{[ {[
['Duration', '5h 30m'], ['Hackathon', '14:00 – 19:00'],
['Teams', '15 teams'], ['Teams', '15 teams'],
['Per team', '3–5 students'], ['Per team', '3–5 students'],
['Hardware', 'Arduino Uno Q · 4 GB'], ['Hardware', 'Arduino Uno Q · 4 GB'],
@@ -112,7 +113,7 @@ export function Landing() {
<div className="max-w-4xl mx-auto space-y-6"> <div className="max-w-4xl mx-auto space-y-6">
<div> <div>
<p className="font-mono text-[10px] tracking-widest uppercase text-primary mb-2">Programme</p> <p className="font-mono text-[10px] tracking-widest uppercase text-primary mb-2">Programme</p>
<h2 className="text-2xl md:text-3xl font-bold tracking-tight">Five and a half hours · seven moves</h2> <h2 className="text-2xl md:text-3xl font-bold tracking-tight">Lecture at 10:45 · build from 14:00</h2>
</div> </div>
<div data-testid="programme" className="border border-border rounded-lg overflow-hidden bg-card divide-y divide-border"> <div data-testid="programme" className="border border-border rounded-lg overflow-hidden bg-card divide-y divide-border">
{PROGRAMME.map((row) => ( {PROGRAMME.map((row) => (
+1 -1
View File
@@ -21,7 +21,7 @@ const MOVEMENTS: Movement[] = [
thesis: 'An agent is a system that closes the loop between sensing the world and acting on it — without a human in the middle. First you choose the world.', thesis: 'An agent is a system that closes the loop between sensing the world and acting on it — without a human in the middle. First you choose the world.',
body: [ body: [
'Most embedded software is reactive plumbing: read a sensor, threshold it, toggle a pin. An agent is different in kind, not degree — it holds a goal, forms a belief about its environment, and chooses an action it expects to advance that goal.', 'Most embedded software is reactive plumbing: read a sensor, threshold it, toggle a pin. An agent is different in kind, not degree — it holds a goal, forms a belief about its environment, and chooses an action it expects to advance that goal.',
'Today you design a domain node: pick a domain — a workshop, a greenhouse, a stairwell, a bike — and name the events it must notice and answer for. The board becomes an expert in that world, and everything downstream — its skills, its policies, its cadence — is justified by the events you name here.', 'Today you design a domain node. Pick the domain you are already working in — whatever you have been measuring this fortnight — and name the events it must notice and answer for. The board becomes an expert in that world, and everything downstream — its skills, its policies, its cadence — is justified by the events you name here. You have the data; you already know which events matter and which are noise.',
], ],
takeaways: [ takeaways: [
'Agency = goal + perception + decision + action, closed in a loop', 'Agency = goal + perception + decision + action, closed in a loop',
+10 -3
View File
@@ -95,9 +95,16 @@ export function Module2() {
/> />
<AddLayerForm <AddLayerForm
layer="L3" layer="L3"
title="ADD · Layer 3 — Policies" title="ADD · Layer 3 — Policies & failure"
description="The actuation gate: what may it do autonomously vs. need approval, and where is the e-stop?" description="The actuation gate — and what happens when things break. For each failure you can name, which way does it fail? A fail-safe must never quietly report 'normal'."
placeholder="Autonomous: log + alert. Needs approval: drive the damper. E-stop: operator can halt actuation at any time." placeholder={
'Autonomous: log + alert. Needs approval: drive the actuator. E-stop: operator halts actuation at any time.\n\n' +
'Failure states → response:\n' +
'• sensor disconnected / stuck value / drifting → mark UNKNOWN, never "nominal"\n' +
'• reading older than 60 s → treat as no reading\n' +
'• cloud unreachable → decide on-board, flag reduced confidence\n' +
'• agent unsure → escalate to a human, do not actuate'
}
/> />
</div> </div>