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,81 @@
# Task 01 — Touch pad: the capture is the finger
**Phase:** `05_mobile_controller` · **Source:** `TODO.md:5` — "Needs to be mobile compatible
— … **Mobile trackpad for touch trackpad.**"
**Story:** `n/a`
## Objective
`#mPad` becomes a touch pad: press and hold it and the target's pointer moves with your
finger, drag two fingers to scroll, and the moment the touch ends the movement stops and every
button lets go. Pointer Lock does not exist for touch, so the capture is the touch itself.
## Work
1. `src/web_page.h`, the mouse block:
- `touchstart` on `#mPad`: requires `armed` and `mouseAvailable()` (`s.mouse === 1`) — the
same two gates as `takeMouse()`, and the same `E10`/`E11` refusal path if the board
disagrees. Sets `mTouchActive`, records the anchor point, and takes **no** button.
- `touchmove`: compute the delta from the previous sample, feed the existing movement
pipeline (the code behind `flushMouse()`) so the ±127 cap and the multi-frame split are
the ones already tested — do not write a second splitter. Coalesce per report the way
`mousemove` does, and drop movement when there is no link rather than queueing it
(the page's existing rule: a queue of pointer motion delivered after you stopped moving
is the pointer version of a stuck key).
- Two-touch drag → wheel/pan through the same quantiser the desktop `wheel` handler uses
(notches with the remainder carried), not movement. `touchmove` with two fingers must not
also emit dx/dy.
- `touchend`/`touchcancel`, and `blur`/`visibilitychange`/disarm/link loss: end the capture,
`dropMouse()` (which releases every button through the funnel), reset the anchor. `touchcancel`
is the browser taking the gesture away — incoming call, notification shade, a palm — and it
must land exactly like lifting the finger.
- A visible button row beside the pad: **Left / Right / Middle**, held while pressed
(`touchstart` down, `touchend` up), carrying the existing `MOUSE_BTN` bits. Tap on the pad
itself never clicks. The row is 44 px and shows which bits are down, so "left is held" is
a thing you can see before you ask what is stuck.
- The pad's label changes with state (`not captured` / `captured — move to move, lift to
release`) since a phone has no cursor to indicate either.
2. `refreshMouseUi()` / `paintMouse()` / `mouseAvailable()` — teach them the touch state so
the pills say what is true. `mCaptured` (Pointer Lock) and `mTouchActive` are different
states that must render distinctly and never both claim the pointer.
3. Keep the desktop path byte-for-byte identical: `mousemove`, the Pointer Lock request,
`Esc`, `wheel` on a mouse — phase 04's assertions and today's `test_page.js` mouse checks
must stay green without edit.
4. `tests/test_page.js` — extend the shim for multi-point touch and add: capture requires
armed + `mouse:1`; a drag produces one `M=` per flush with no idle frames; a large drag
splits with the net displacement exact; two fingers produce wheel/pan and no dx/dy;
`touchend` and `touchcancel` each release every button and stop movement; button-row bits
equal `MOUSE_BTN`; every emitted frame parses with the firmware's own parser.
5. `README.md` — Controller page: the pad row of the element table describes the touch
behaviour; "What capture does and does not do" gains the touch sentence (this is
elaborated properly in task 04 — here, only what this task changes).
- ASSUMPTION: one finger moves, two fingers scroll, and clicking is an explicit held button
rather than a tap. Tap-to-click on a pad whose whole job is movement produces clicks the
operator did not ask for, and on a remote keyboard a click the target did not expect is the
worst class of surprise.
- ASSUMPTION: no "tap-and-hold to drag" (holding a button while moving). It needs a second
simultaneous gesture and any mis-timing strands a button on the target; the button row gives
the same capability with visible state.
- ASSUMPTION: scale of finger-delta to HID delta is 1:1 (one CSS pixel → one report unit) at
first. If it feels slow on hardware, the tuning knob goes in `config.h`-style naming on the
page side with the measured value in the comment — the board must not invent a pointer speed
of its own.
## Testing & Quality
- `node tests/test_page.js`: the new touch checks pass and the existing mouse checks are
unmodified (prove it: the diff of that test file adds, it does not change, mouse assertions).
- `bash .agents/validate.sh` green; `test_proto.cpp`, `test_mouse.cpp`, `test_output.cpp`
untouched.
- Device (best effort, one attempt): a phone on the board's AP — movement lands, lift stops
it, and `watch_target.py` shows no stuck button across 20 press/release cycles.
## Completion Criteria
- [ ] Press-and-hold on the pad moves the target's pointer; lifting stops it and releases
every button (observed, or recorded `skipped (no device)` with the commands).
- [ ] Two-finger drag scrolls and never moves the pointer.
- [ ] Left/Right/Middle are held-while-pressed and visible; tap on the pad clicks nothing.
- [ ] Desktop Pointer Lock path unchanged and still covered by the pre-existing checks.
- [ ] Full test suite green.