chore(agent): phase roadmap from TODO.md, 5 phases

The four open items in TODO.md become an executable pipeline: 01 the
verification gate (host suites + sanitiser + both firmware envs, and the
hardware contract so no unit blocks on a board nobody is holding), 02 HTTPS
and wss:// with an identity the board generates itself into NVS, 03 session
takeover that always lands disarmed, 04 a responsive page whose safety
statements are pinned before they are trimmed, 05 the phone as a controller
(press-and-hold pad, soft keyboard, modifier latches over a new Q= frame).

TODO.md is cleared to its empty title; the items live in .agents/phases/ now.
AGENTS.md gains the five protocol rules and nothing else.

LLM-Generated: true
This commit is contained in:
2026-11-08 23:28:25 -05:00
parent ece0313d02
commit 56aa97d60d
28 changed files with 2033 additions and 0 deletions
@@ -0,0 +1,96 @@
# Phase 05 — The phone is a controller: soft keyboard and touch pointer
**Source:** `TODO.md` L5 — "Needs to be mobile compatible - click arm and then mobile
keyboard pops up. Mobile trackpad for touch trackpad."
**Story:** `n/a`
**Context:** `src/web_page.h` (the mouse block, `takeMouse()`/`flushMouse()`/`dropMouse()`,
`inPageField()`, `setArmed()`), `src/text.h` (`strokeFor()`), `src/proto.h`, `src/mouse.h`,
`src/config.h`, `tests/test_page.js`, README "Controller page" / "Safety model"
## Objective
Arm the bridge on a phone and the soft keyboard appears so you can actually type into the
target; press and hold the pad and the target's pointer moves, let go and every button is up.
Two capabilities the desktop gets from hardware — a physical keyboard and Pointer Lock — get
honest software equivalents, with the same guarantees restated in touch terms rather than
waived.
## The two equivalences, stated up front
| desktop guarantee | touch equivalent |
|---|---|
| Pointer Lock: the cursor cannot escape the tab, and `Esc` always frees it | **finger down on the pad is the capture, finger up is the release** — movement stops and every button lets go the instant the touch ends, and the Release button is 44 px and on screen. The capture cannot outlive the finger that asked for it. |
| Modifiers are physical keys held with a second hand | an **on-screen latch row** whose state is lit, restated in every frame, and cleared by a tap — never a sticky Ctrl that silently modifies the next six keys. |
Both are *more* conservative than the desktop case, not less: a touch capture ends when you
stop touching, which is stronger than a lock you have to remember to escape.
## What must not be traded away
- Movement is **relative**, one report per frame, deltas capped at ±127 and split so the net
displacement survives (invariant 7: splitting is the controller's job).
- No auto-click, no gesture, no macro, nothing scheduled. The button row is held-while-pressed.
- Capture requires armed **and** the board reporting `mouse:1`, and `E10`/`E11` still refuse
rather than ignore.
- Characters are typed by the board's own map. The page must **not** grow a second
character→usage table: the board owns `strokeFor()`, which is why the new `Q=` frame carries
*modifiers and hex bytes*, not usages. (`AGENTS.md`: when you add a table that decodes
something somebody else defined, test it — better still, do not add it.)
- Anything a touch session can leave behind (a latch, a held button, a focused field) dies in
the same funnel: disarm, blur, visibility change, link loss.
## Dependencies
- `04_controller_page_responsive` (todo) — the single-column layout, 44 px targets and
`touch-action:manipulation` are what make a touch pad usable; the folded prose is what makes
room for the latch row and the button row.
- `03_session_takeover` (todo) — `E12` handling and the `busy` pill, because a phone is exactly
the device that finds the laptop holding the bridge.
## Tasks
1. `01_touch_trackpad.md` — the pad: press-and-hold capture, relative movement from
`touchmove`, two-finger drag as the wheel, an explicit held-while-pressed button row, and
release on `touchend`/`touchcancel`/blur/disarm.
2. `02_soft_keyboard_capture.md` — ARM focuses a forwarding field so the soft keyboard pops;
printable characters leave as `P=0:<hex>` one per frame, named keys (Enter, Backspace, Tab,
arrows…) as a press/release pair; the field is cleared as soon as it is read.
3. `03_modifier_latch_q_frame.md` — the latch row and the `Q=<mods>:<hex>` frame that lets a
latched modifier type a character without the page owning a second character map. A real
four-place protocol change: `proto.h`, `web_page.h`, `relayctl.py`, README.
4. `04_mobile_tests_docs.md` — `test_page.js` touch/latch coverage, the named-key table pinned,
README's Controller page and Safety model rewritten for touch, `AGENTS.md`'s "three tables"
row, and the phone observation recorded.
## Testing & Quality
- `tests/test_page.js` — the shim already records document/window listeners and can `fire()`
them; extend it to dispatch `touchstart`/`touchmove`/`touchend`/`touchcancel` with multiple
touch points and `beforeinput` with `inputType`, then assert:
capture requires armed and `mouse:1`; one frame per report with nothing sent when idle; a
400×300-style drag split so no delta exceeds a signed byte and the net displacement is exact;
two-finger drag becomes wheel/pan not movement; a finger up releases every button **and**
stops movement; the button row's bits match `MOUSE_BTN`; the latch row's byte matches the
modifier names; a character becomes exactly one `P=0:` frame and a latched one becomes one
`Q=` frame; `E9`/`E10`/`E11` land in the log and cancel nothing they should not.
Every frame the page emits is parsed by the firmware's own parser — the suite rule that has
already caught mislabelled chords.
- `tests/test_proto.cpp`: `Q=` parses, bounds-checks (mods byte, cap on hex length), refuses
malformed input, round-trips.
- `tests/test_relayctl.py`: the tool builds `Q=` identically, and the button/name tables and
`MOUSE_BUTTONS` agreement extends to whatever the touch rows introduce.
- Coverage **>90%** on the changed page script and the new parser path.
- Device (best effort, one attempt): a phone on the board's AP — arm, keyboard appears, type a
word and `watch_target.py` shows the characters; hold the pad, move, lift, and the target
shows motion that stops and no button stuck.
## Completion Criteria
- [ ] Arm on a phone brings up the soft keyboard and typed characters land on the target.
- [ ] Hold the pad → the pointer moves; lift → motion stops and every button is up, observed
with `watch_target.py` or recorded as `skipped (no device)` with the exact commands.
- [ ] `Ctrl+C`-equivalent reachable with the latch row and lands as Ctrl+C on the target.
- [ ] `Q=` in the README wire table, `proto.h`, `web_page.h` and `relayctl.py`, with the tests
that pin all four agreeing.
- [ ] No second character→usage table anywhere; the named-key table that does exist is pinned.
- [ ] `bash .agents/validate.sh` green; README and `AGENTS.md` describe the touch model.