Agent & tools·
`q` is a Strands agent that runs on the board. Its tools read the system, drive the LED matrix and talk to the MCU. Every tool returns `ok`, `err` or `refused`, the Fomo convention.
Where the credentials live·
Model credentials are the owner's own and never enter the repository. On the board they sit in /etc/q/env, mode 0600, owned by root, and are loaded by the systemd units.
CLI·
Installed as q by pyproject.toml (q = "q.cli:main"), on the board at /opt/q/bin/q. No arguments means doctor.
q doctor # 20 checks on the board, exit 0 when all pass; --json for the full report
q state # the same JSON as GET /api/state
q telemetry # flat JSON, same as GET /api/telemetry
q mcu # link, port, fw, sketch, socket
q mcu q/ping # any router method; q mcu <method> '<json params>'
q led # matrix + RGB state
q led text hello # scroll text on the matrix (q sketch)
q led glyph heart # a static glyph: ok x wifi tunnel heart q dot
q led glyph status # Wi-Fi bars, tunnel, doctor composed from live state
q led clear
q led rgb 0 32 0 # the RGB user LED, sysfs
q notes # Q's notebook; q notes add <text>; q notes rm <id>
Doctor·
q/doctor.py runs the same checks the CLI prints, the dashboard shows on the System card and the agent reads with q_doctor: router socket, MCU link, q sketch answering, CPU temperature readable and below 80 °C, Wi-Fi, the four units (q-dash, q-tunnel, q-tiny, arduino-router), RGB LED, root and home free space (1 GB floor), memory (300 MB floor), load, journal size (used blocks, 60 MB), cloudflared connections from its local /ready, and unit failures since this boot (red only while the last failure is under Q_DOCTOR_FAIL_RECENT_S, 15 min, old; a unit that failed earlier this boot and has run clean since keeps the count in the detail, marked recovered). A check that cannot run off the board is skipped, never counted as a failure. state.doctor carries the summary (cached 30 s); /api/telemetry carries doctor_ok, doctor_total, doctor_failing.
Measured on the board on 2026-09-09 (night lane): 17 of 18 pass in 1.5 s. The one failing row is honest: q-tunnel failed twice at the 20:07Z boot, before the unit ordering fix that landed afterwards; it clears at the next boot.
Notebook·
q/notes.py keeps small durable facts in ~/.q/notes.json (mode 0600, capped at 500). The agent writes with q_note, reads with q_notes, deletes with q_forget; the owner uses GET|POST /api/notes and DELETE /api/notes/{id}. The last five notes are quoted into the agent's system prompt, so a fact written tonight is known after a restart. Secrets do not belong in notes: they are quoted into prompts and readable through the API.
Agent tools·
q/agent.py exposes Strands tools, read through q.agent.tools() by the dashboard:
| tool | does |
|---|---|
q_state, q_doctor |
full state; the doctor report |
q_led_text, q_led_frame, q_led_glyph, q_led_clear, q_rgb |
the matrix and the RGB user LED |
q_pin(pin, level, mode, read) |
header GPIO D2 to D13: digitalWrite, pinMode, digitalRead. pin is 2, "D2" or an alias from ~/.q/pins.json; the answer is a sentence such as laser (D2) high. D0/D1 refused. Allowed at any time. |
q_mcu_status, q_mcu_call(method, params_json) |
the STM32 over the router |
q_note, q_notes, q_forget |
the notebook |
q_journal(unit, lines, since) |
this boot's journal for one unit or the kernel |
q_network |
interfaces, default route, DNS, Wi-Fi, tunnel connections |
q_top |
heaviest processes by CPU and memory |
q_shell(command) |
a shell as the arduino user, 15 s timeout, output capped |
q/ext/ (out-of-box lane) adds more, discovered at import time.
System prompt·
The dashboard builds the prompt per request (dashboard/agent.py, system_prompt()): a short identity plus live facts from q.board.state(): hostname, IP and SSID, tunnel, temperatures, load, memory, disk, the units, MCU firmware and sketch, what the USB-C port is doing, what is attached (only the matrix, the RGB LED and floating A0 to A5 unless a tool proves otherwise), the doctor summary and the recent notes. Verified on the board: asked with tools forbidden, the model answered hostname, IP and the doctor count correctly from the facts alone.
What the dashboard expects from q·
Decided by the supervisor and imported by dashboard/board.py: q.board.state(), q.led.text() / frame() / clear() / state(), q.mcu.status() / reset(), q.agent.tools(). The adapter needed no change when the package landed. Anything missing falls back to the simulator with source: "fake".