mudra-skills 3.1.2 → 3.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -74,7 +74,7 @@
74
74
  "sprint",
75
75
  "charge"
76
76
  ],
77
- "pressure": [
77
+ "direct_pressure": [
78
78
  "slide",
79
79
  "volume",
80
80
  "size",
@@ -83,7 +83,31 @@
83
83
  "opacity",
84
84
  "brush",
85
85
  "zoom",
86
- "analog"
86
+ "analog",
87
+ "pressure",
88
+ "press harder",
89
+ "force"
90
+ ],
91
+ "pinch_pressure": [
92
+ "pinch",
93
+ "squeeze",
94
+ "pinch and hold",
95
+ "tap then squeeze",
96
+ "grab and scale",
97
+ "pinch-to-zoom",
98
+ "hold to charge"
99
+ ],
100
+ "imu_quaternion": [
101
+ "hand orientation",
102
+ "wrist orientation",
103
+ "absolute orientation",
104
+ "aim",
105
+ "point at",
106
+ "heading",
107
+ "which way the hand is pointing",
108
+ "roll pitch yaw",
109
+ "quaternion",
110
+ "1:1 rotation"
87
111
  ],
88
112
  "navigation": [
89
113
  "move",
@@ -98,10 +122,9 @@
98
122
  ],
99
123
  "imu_acc+imu_gyro": [
100
124
  "tilt",
101
- "orientation",
125
+ "shake",
126
+ "acceleration",
102
127
  "angle",
103
- "rotate",
104
- "3D",
105
128
  "balance",
106
129
  "level"
107
130
  ],
@@ -120,7 +143,9 @@
120
143
  "nerve"
121
144
  ]
122
145
  },
123
- "when_ambiguous": "If the user's concept could use either navigation or IMU, ask which fits better. Also ask when two motion modes (Pointer / Direction / IMU) map equally well — do not silently pick. Default to 'direction' when motion language is present but no clear winner."
146
+ "pressure_mode_rule": "There is no signal called 'pressure' — that was the old name for pinch_pressure. Pick exactly one of direct_pressure or pinch_pressure. direct_pressure is the NEW continuous ungated stream (always on, no gating; default). Requires firmware 6.0.12.11 and above. pinch_pressure is the ORIGINAL tap-to-release filtered stream (values stream only between tap and release) and works on older firmware. Choose pinch_pressure only for explicit commit-then-modulate interactions (grab-and-scale, pinch-to-zoom, hold-to-charge). Do not ask the user which mode — infer it.",
147
+ "orientation_rule": "For aiming, heading, pose gating, or 1:1 rotation of a mesh, use imu_quaternion. It is absolute and drift-free, needs no fusion on your side, belongs to no bundle, and is exempt from the Pointer/Direction/IMU motion-mode XOR — so it combines with navigation and nav_direction. Requires firmware 6.0.12.11 and above. Reach for imu_acc + imu_gyro only when the concept wants raw acceleration or shake.",
148
+ "when_ambiguous": "If the user's concept could use either navigation or IMU, ask which fits better. Also ask when two motion modes (Pointer / Direction / IMU) map equally well — do not silently pick. Default to 'direction' when motion language is present but no clear winner. Note: pure orientation is not an ambiguity — use imu_quaternion, which conflicts with nothing."
124
149
  },
125
150
  "creative_proposals": {
126
151
  "description": "When the user's concept maps to one signal, propose a complementary one that respects motion-mode exclusivity. Keep it to one sentence.",
@@ -177,9 +202,9 @@
177
202
  "Forward gesture.double_tap → onSelectEnd for release"
178
203
  ]
179
204
  },
180
- "pressure": {
181
- "description": "Finger pressure (0-100%)",
182
- "use_for": "Analog control (scale, zoom, opacity, intensity)",
205
+ "direct_pressure": {
206
+ "description": "Finger pressure 0–100, normalized 0–1. NEW continuous ungated stream — always on, no tap/release gating. The default pressure signal.",
207
+ "use_for": "Analog control (scale, zoom, opacity, intensity) that is live whenever the finger is pressing, with no gesture gate",
183
208
  "examples": [
184
209
  "Brush size",
185
210
  "Zoom level",
@@ -188,7 +213,7 @@
188
213
  "Mesh scale"
189
214
  ],
190
215
  "data_format": {
191
- "type": "pressure",
216
+ "type": "direct_pressure",
192
217
  "data": {
193
218
  "value": 50,
194
219
  "normalized": 0.5,
@@ -197,8 +222,70 @@
197
222
  "timestamp": 1234567890
198
223
  },
199
224
  "notes": [
225
+ "Finger pressure 0–100, normalized 0–1",
200
226
  "value: 0-100 integer",
201
- "normalized: 0.0-1.0 float (convenient for scale, hue, opacity)"
227
+ "normalized: 0.0-1.0 float (convenient for scale, hue, opacity)",
228
+ "Mutually exclusive with pinch_pressure — subscribe to exactly one",
229
+ "Requires firmware 6.0.12.11 and above.",
230
+ "'pressure' is not a valid signal name. That was the old name for pinch_pressure. direct_pressure is a new signal, not a rename."
231
+ ]
232
+ },
233
+ "pinch_pressure": {
234
+ "description": "Finger pressure 0–100, normalized 0–1. ORIGINAL tap-to-release filtered stream. Formerly named 'pressure'. Values stream only between tap and release: streaming starts on tap, the value falls off on release, and streaming stops until the next tap.",
235
+ "use_for": "Analog control that only begins once the user has committed with a pinch",
236
+ "examples": [
237
+ "Pinch a mesh, then squeeze to scale it",
238
+ "Pinch and hold to charge a throw",
239
+ "Pinch-to-zoom on a virtual screen",
240
+ "Grab-and-drag with force-sensitive resistance"
241
+ ],
242
+ "data_format": {
243
+ "type": "pinch_pressure",
244
+ "data": {
245
+ "value": 50,
246
+ "normalized": 0.5,
247
+ "timestamp": 1234567890
248
+ },
249
+ "timestamp": 1234567890
250
+ },
251
+ "notes": [
252
+ "Finger pressure 0–100, normalized 0–1",
253
+ "Identical payload to direct_pressure — only the acquisition window differs",
254
+ "Values stream only between tap and release; expect a run of frames bracketed by rest",
255
+ "Mutually exclusive with direct_pressure — subscribe to exactly one",
256
+ "Works on older firmware — not gated on 6.0.12.11",
257
+ "In XR this pairs naturally with a latched target: pinch to grab an object, squeeze to modulate it, release to commit"
258
+ ]
259
+ },
260
+ "imu_quaternion": {
261
+ "display_name": "Hand Orientation",
262
+ "description": "Absolute hand orientation as a stream of unit quaternions [w, x, y, z]",
263
+ "use_for": "Aiming, ray direction, 1:1 mesh rotation, heading, pose gating — no drift, no fusion needed",
264
+ "examples": [
265
+ "Aim a ray/reticle by pointing the hand",
266
+ "Rotate a model 1:1 with the wrist",
267
+ "Orient a virtual screen to face the hand",
268
+ "Palm-down / palm-up pose gate",
269
+ "Compass heading in a spatial HUD"
270
+ ],
271
+ "data_format": {
272
+ "type": "imu_quaternion",
273
+ "data": {
274
+ "values": [[0.7071, 0.0, 0.7071, 0.0], [0.706, 0.01, 0.708, 0.0]],
275
+ "frequency": 50,
276
+ "frequency_std": 0.4,
277
+ "timestamp": 1234567890
278
+ },
279
+ "timestamp": 1234567890
280
+ },
281
+ "notes": [
282
+ "Requires firmware 6.0.12.11 and above.",
283
+ "CRITICAL SHAPE DIFFERENCE: values is a LIST OF SAMPLES, each a 4-element [w, x, y, z] array. One nesting level deeper than imu_acc / imu_gyro, whose values is three flat per-axis arrays. Never assume the shapes match.",
284
+ "Read the latest sample per frame: const [w, x, y, z] = msg.data.values.at(-1)",
285
+ "Each sample is a unit quaternion (w² + x² + y² + z² ≈ 1.0). Scattered norms mean you are reading at the wrong depth.",
286
+ "three.js argument order differs: new THREE.Quaternion(x, y, z, w)",
287
+ "Prefer object.quaternion.slerp(incoming, 0.2) over a hard set, to absorb packet jitter",
288
+ "Standalone signal. NOT part of the imu_acc + imu_gyro + emg bundle and exempt from the Pointer/Direction/IMU motion-mode XOR — it combines with navigation and nav_direction."
202
289
  ]
203
290
  },
204
291
  "imu_acc": {
@@ -404,24 +491,41 @@
404
491
  }
405
492
  },
406
493
  "xor_rules": [
407
- "gesture XOR pressure (never both)",
494
+ "gesture XOR pressure (never both) — applies to direct_pressure and pinch_pressure alike",
495
+ "direct_pressure XOR pinch_pressure (exactly one pressure signal per app — they are mutually exclusive)",
408
496
  "navigation XOR nav_direction (never both)",
409
497
  "(navigation OR nav_direction) XOR imu_biometric_mode (never combine pointer/direction with the IMU+Biometric bundle)",
410
- "imu_acc, imu_gyro, emg are bundled — partial subscriptions are forbidden"
498
+ "imu_acc, imu_gyro, emg are bundled — partial subscriptions are forbidden",
499
+ "imu_quaternion is exempt from every rule above — it is standalone and combines with anything"
411
500
  ],
412
501
  "can_combine": [
413
- "gesture OR pressure",
502
+ "gesture OR one pressure mode (direct_pressure or pinch_pressure)",
414
503
  "button (with pointer/direction/imu_biometric/none)",
415
- "ONE OF: pointer_mode OR direction_mode OR imu_biometric_mode"
504
+ "ONE OF: pointer_mode OR direction_mode OR imu_biometric_mode",
505
+ "imu_quaternion + ANY of the above, including navigation and nav_direction"
416
506
  ],
417
507
  "cannot_combine": [
418
508
  {
419
509
  "signals": [
420
510
  "gesture",
421
- "pressure"
511
+ "direct_pressure"
512
+ ],
513
+ "reason": "XOR rule - pick one"
514
+ },
515
+ {
516
+ "signals": [
517
+ "gesture",
518
+ "pinch_pressure"
422
519
  ],
423
520
  "reason": "XOR rule - pick one"
424
521
  },
522
+ {
523
+ "signals": [
524
+ "direct_pressure",
525
+ "pinch_pressure"
526
+ ],
527
+ "reason": "Mutually exclusive — pick direct_pressure (new continuous stream) or pinch_pressure (original tap-to-release stream)"
528
+ },
425
529
  {
426
530
  "signals": [
427
531
  "navigation",
@@ -479,7 +583,7 @@
479
583
  "reason": "Button is part of pointer mode - cannot mix with direction mode"
480
584
  }
481
585
  ],
482
- "guidance": "Three mutually exclusive motion groups: (1) Pointer mode: navigation+button for continuous cursor/drag/pan, (2) Direction mode: nav_direction for discrete swipe-like gestures, (3) IMU+Biometric mode: imu_acc+imu_gyro+emg as an inseparable bundle for orientation/tilt/rotation/biometric. Pick ONE per app. gesture XOR pressure; button combines freely (subject to mode rules)."
586
+ "guidance": "Three mutually exclusive motion groups: (1) Pointer mode: navigation+button for continuous cursor/drag/pan, (2) Direction mode: nav_direction for discrete swipe-like gestures, (3) IMU+Biometric mode: imu_acc+imu_gyro+emg as an inseparable bundle for shake/acceleration/biometrics. Pick ONE per app. gesture XOR pressure, and direct_pressure XOR pinch_pressure; button combines freely (subject to mode rules). Hand Orientation (imu_quaternion) is a FOURTH, non-exclusive option that sits outside this scheme — if the app needs aiming or 1:1 rotation, add imu_quaternion to whichever motion mode you picked rather than treating it as a conflict. imu_quaternion: Requires firmware 6.0.12.11 and above. direct_pressure: Requires firmware 6.0.12.11 and above. pinch_pressure works on older firmware."
483
587
  },
484
588
  "websocket_api": {
485
589
  "url": "ws://127.0.0.1:8766",
@@ -524,7 +628,7 @@
524
628
  },
525
629
  "example": {
526
630
  "command": "unsubscribe",
527
- "signal": "pressure"
631
+ "signal": "direct_pressure"
528
632
  }
529
633
  },
530
634
  "get_subscriptions": {
@@ -590,10 +694,25 @@
590
694
  "reason": "Raw WebSocket has no mock fallback; MudraClient handles real + simulated paths transparently"
591
695
  },
592
696
  {
593
- "wrong": "{command: 'subscribe', signals: ['gesture', 'pressure']}",
697
+ "wrong": "{command: 'subscribe', signals: ['gesture', 'navigation']}",
594
698
  "correct": "Send separate subscribe commands — one per signal",
595
699
  "reason": "Parameter is 'signal' (singular), not 'signals'"
596
700
  },
701
+ {
702
+ "wrong": "{command: 'subscribe', signal: 'pressure'}",
703
+ "correct": "Subscribe to 'direct_pressure' (default) or 'pinch_pressure' — never both",
704
+ "reason": "'pressure' is the old name for pinch_pressure and is no longer a valid signal. The original tap-to-release stream is pinch_pressure; direct_pressure is a new continuous ungated stream. The server returns invalid_signal and the app silently receives nothing. The frame type mirrors the signal name, so handlers keyed on 'pressure' never fire either."
705
+ },
706
+ {
707
+ "wrong": "const [w, x, y, z] = msg.data.values // on an imu_quaternion frame",
708
+ "correct": "const [w, x, y, z] = msg.data.values.at(-1)",
709
+ "reason": "imu_quaternion values is a LIST of [w,x,y,z] samples, one nesting level deeper than imu_acc / imu_gyro. Read flat, you get whole samples where components belong and the rotation is nonsense."
710
+ },
711
+ {
712
+ "wrong": "Subscribing to imu_acc + imu_gyro + emg to derive where the hand is pointing",
713
+ "correct": "Subscribe to imu_quaternion alone",
714
+ "reason": "imu_quaternion is already fused and drift-free, and unlike the bundle it can coexist with navigation / nav_direction — so it does not force a motion-mode choice on the app."
715
+ },
597
716
  {
598
717
  "wrong": "Instantiating MudraClient inside init()",
599
718
  "correct": "Instantiate MudraClient at module scope, outside the xb.Script class",
@@ -814,14 +933,36 @@
814
933
  ]
815
934
  },
816
935
  "pressure_analog": {
817
- "description": "Analog control via finger pressure — brush size, zoom, opacity",
818
- "signal": "pressure",
936
+ "description": "NEW continuous ungated pressure stream — brush size, zoom, opacity. Requires firmware 6.0.12.11 and above.",
937
+ "signal": "direct_pressure",
819
938
  "implementation": [
820
939
  "Map normalized (0..1) to your range",
821
940
  "Smooth with rolling average (3-5 samples)",
822
941
  "Visual feedback: scale, color hue, bar indicator"
823
942
  ]
824
943
  },
944
+ "pinch_and_squeeze": {
945
+ "description": "ORIGINAL tap-to-release filtered stream — commit with a pinch, then modulate with force. Works on older firmware.",
946
+ "signal": "pinch_pressure",
947
+ "implementation": [
948
+ "Latch the target mesh on the first non-zero frame — that is the grab",
949
+ "Map normalized (0..1) to the modulated property while the pinch is held",
950
+ "Treat the return to ~0 as release — commit the final value and clear the latch",
951
+ "Visual feedback: highlight the latched mesh plus a force meter, so the user sees what they grabbed and how hard"
952
+ ]
953
+ },
954
+ "hand_orientation": {
955
+ "description": "Absolute hand orientation — aiming a ray, 1:1 mesh rotation, pose gating, heading. Requires firmware 6.0.12.11 and above.",
956
+ "signal": "imu_quaternion",
957
+ "implementation": [
958
+ "Read the latest sample each frame: const [w, x, y, z] = msg.data.values.at(-1)",
959
+ "Build the three.js quaternion with swapped argument order: new THREE.Quaternion(x, y, z, w)",
960
+ "Slerp toward it (factor ~0.2) rather than setting it hard, to absorb packet jitter",
961
+ "For aiming, apply the quaternion to a forward vector (0, 0, -1) and raycast along the result",
962
+ "Latch a reference quaternion on gesture.tap so the user can zero the pose in a comfortable arm position, then apply incoming rotations relative to it",
963
+ "Sanity check while developing: w² + x² + y² + z² should be ≈ 1.0"
964
+ ]
965
+ },
825
966
  "emg_time_series": {
826
967
  "description": "EMG visualization, muscle-activity overlays",
827
968
  "signal": "emg",
@@ -17,19 +17,36 @@ ws://127.0.0.1:8766
17
17
  Always construct the connection through `MudraClient` (Section 4).
18
18
  Never use raw `new WebSocket(...)`.
19
19
 
20
- ### Eight canonical signals
20
+ ### Ten canonical signals
21
21
 
22
22
  | Signal | Category | Description |
23
23
  |--------|----------|-------------|
24
24
  | `gesture` | Discrete | Hand gesture events (tap, double_tap, twist, double_twist) |
25
25
  | `button` | Discrete | Button hold / release |
26
- | `pressure` | Analog | Finger pressure 0–100, normalized 0–1 |
26
+ | `direct_pressure` | Analog | Finger pressure 0–100, normalized 0–1. **New** continuous ungated stream (always on). **Default.** Requires firmware 6.0.12.11 and above. |
27
+ | `pinch_pressure` | Analog | Finger pressure 0–100, normalized 0–1. **Original** tap-to-release filtered stream. Works on older firmware. |
27
28
  | `navigation` | Motion (Pointer) | Continuous delta_x / delta_y cursor movement |
28
29
  | `nav_direction` | Motion (Direction) | Discrete directional swipes: None, Right, Left, Up, Down, Roll Left, Roll Right |
29
30
  | `imu_acc` | Motion (IMU) | Accelerometer values [x, y, z] m/s², frequency 1125 Hz |
30
31
  | `imu_gyro` | Motion (IMU) | Gyroscope values [x, y, z] deg/s, frequency 1125 Hz |
32
+ | `imu_quaternion` | Orientation (standalone) | **Hand Orientation** — absolute unit quaternions. `values` is a **list** of `[w, x, y, z]` samples. Requires firmware 6.0.12.11 and above. |
31
33
  | `emg` | Biometric | 3 de-interleaved channel arrays [[ch1], [ch2], [ch3]] |
32
34
 
35
+ **`pressure` is not a signal name.** It was the old name for
36
+ `pinch_pressure`. Both signals are Finger pressure 0–100, normalized 0–1.
37
+ `direct_pressure` is a **new** continuous ungated
38
+ stream, not a split of the old signal. Sending `pressure` returns
39
+ `invalid_signal` and the app receives nothing. Subscribe to exactly one
40
+ pressure signal. Default to `direct_pressure`. Requires firmware 6.0.12.11 and above. Choose `pinch_pressure` only for explicit
41
+ commit-then-modulate interactions (grab-and-scale, pinch-to-zoom,
42
+ hold-to-charge). `pinch_pressure` works on older firmware.
43
+
44
+ **`imu_quaternion` is exempt from the motion-mode XOR** in Section 8 — it
45
+ is standalone and combines with any other signal, including `navigation`
46
+ and `nav_direction`. Prefer it over the IMU bundle whenever the app needs
47
+ absolute orientation (aiming, ray direction, 1:1 mesh rotation, pose
48
+ gating) rather than raw acceleration. Requires firmware 6.0.12.11 and above.
49
+
33
50
 
34
51
  ### Subscription handshake
35
52
 
@@ -38,11 +55,15 @@ Send one command per signal — never use plural `signals`, arrays, or batch com
38
55
  ```js
39
56
  // CORRECT
40
57
  ws.send(JSON.stringify({ command: 'subscribe', signal: 'gesture' }));
41
- ws.send(JSON.stringify({ command: 'subscribe', signal: 'pressure' }));
58
+ ws.send(JSON.stringify({ command: 'subscribe', signal: 'direct_pressure' }));
59
+ ws.send(JSON.stringify({ command: 'subscribe', signal: 'imu_quaternion' }));
42
60
 
43
61
  // WRONG — never do this
44
- ws.send(JSON.stringify({ command: 'subscribe', signals: ['gesture', 'pressure'] }));
45
- ws.send(JSON.stringify({ command: 'subscribe', signal: ['gesture', 'pressure'] }));
62
+ ws.send(JSON.stringify({ command: 'subscribe', signals: ['gesture', 'navigation'] }));
63
+ ws.send(JSON.stringify({ command: 'subscribe', signal: ['gesture', 'navigation'] }));
64
+
65
+ // WRONG — 'pressure' is the old name for pinch_pressure, not a signal
66
+ ws.send(JSON.stringify({ command: 'subscribe', signal: 'pressure' }));
46
67
  ```
47
68
 
48
69
  ### Full command surface
@@ -60,8 +81,8 @@ ws.send(JSON.stringify({ command: 'subscribe', signal: ['gesture', 'pressure'] }
60
81
  // button
61
82
  { type: 'button', data: { state: 'pressed'|'released', timestamp }, timestamp }
62
83
 
63
- // pressure
64
- { type: 'pressure', data: { value: 0–100, normalized: 0–1, timestamp }, timestamp }
84
+ // direct_pressure / pinch_pressure — same payload; pick exactly one
85
+ { type: 'direct_pressure'|'pinch_pressure', data: { value: 0–100, normalized: 0–1, timestamp }, timestamp }
65
86
 
66
87
  // navigation
67
88
  { type: 'navigation', data: { delta_x: number, delta_y: number, timestamp }, timestamp }
@@ -79,7 +100,7 @@ ws.send(JSON.stringify({ command: 'subscribe', signal: ['gesture', 'pressure'] }
79
100
  { type: 'emg', data: { values: [[ch1_samples], [ch2_samples], [ch3_samples]], frequency: number, frequency_std: number, timestamp }, timestamp }
80
101
 
81
102
  // status — response to get_status command
82
- { type: 'status', data: { device: { name, address, battery, charging, firmware, serial_number, hand, state, firmware_config: { target, active } }, subscriptions: { emg, imu_acc, imu_gyro, pressure, gesture, navigation, nav_direction, button } }, timestamp }
103
+ { type: 'status', data: { device: { name, address, battery, charging, firmware, serial_number, hand, state, firmware_config: { target, active } }, subscriptions: { emg, imu_acc, imu_gyro, imu_quaternion, direct_pressure, pinch_pressure, gesture, navigation, nav_direction, button } }, timestamp }
83
104
 
84
105
  // subscription_status — response to subscribe/unsubscribe
85
106
  { type: 'subscription_status', data: { signal: string, subscribed: boolean }, timestamp }
@@ -630,6 +651,20 @@ mudra.subscribe('emg'); // missing imu_acc and imu_gyro
630
651
  mudra.subscribe('imu_acc'); // missing imu_gyro and emg
631
652
  ```
632
653
 
654
+ ### Hand Orientation — `imu_quaternion` (standalone)
655
+
656
+ Exempt from every XOR in this section. It belongs to no bundle, requires
657
+ no motion mode, and combines with any other signal — including
658
+ `navigation` and `nav_direction`. Use it for aiming, ray direction, 1:1
659
+ mesh rotation, pose gating, and heading. Requires firmware 6.0.12.11 and above.
660
+
661
+ ### Pressure — `direct_pressure` vs `pinch_pressure`
662
+
663
+ Pick exactly one. There is no bare `pressure` signal.
664
+
665
+ - `direct_pressure` — Finger pressure 0–100, normalized 0–1. **New** continuous ungated stream (always on, no tap/release gating). Default. Requires firmware 6.0.12.11 and above.
666
+ - `pinch_pressure` — Finger pressure 0–100, normalized 0–1. **Original** tap-to-release filtered stream (the former `pressure` signal). Values stream only between tap and release. Works on older firmware.
667
+
633
668
  ### XOR rules (all non-negotiable)
634
669
 
635
670
  1. **Gesture ⊕ Pressure** — an app may use `gesture` OR `pressure`, never both.
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: mudra-preview
3
- version: 3.1.2
3
+ version: 3.2.0
4
4
  description: Generate a working Mudra Band interactive app preview as a single-file HTML. Use when the user describes a Mudra-controlled experience (gesture, pressure, navigation, IMU, EMG), wants to prototype a Mudra Link app, or asks to build/preview a Mudra app.
5
5
  ---
6
6
 
@@ -44,7 +44,7 @@ The canonical protocol is in `references/agent_protocol.json`.
44
44
 
45
45
  1. **Read the full instructions** from `references/prompt.md` inside the skill base directory. That file contains the complete protocol contract, signal compatibility rules, the Mode Toggle architecture (mandatory), build defaults, and sample catalog — follow all of it.
46
46
 
47
- 2. **Infer intent** from the user's description (or the args passed to this skill). Fill gaps with smart defaults. Ask only if there is genuine ambiguity (e.g., `gesture` vs `pressure`, `navigation` vs `nav_direction`, or directional motion vs IMU+Biometric bundle).
47
+ 2. **Infer intent** from the user's description (or the args passed to this skill). Fill gaps with smart defaults. Ask only if there is genuine ambiguity (e.g., `gesture` vs pressure, `navigation` vs `nav_direction`, or directional motion vs IMU+Biometric bundle). Choosing between `direct_pressure` and `pinch_pressure` is **not** an ambiguity — default to `direct_pressure` and state the choice in one clause.
48
48
 
49
49
  3. **Select the best-matching template** from `assets/` inside the skill base directory. Use the selection rule from `references/prompt.md` (motion mode → interaction pattern → signal overlap).
50
50
 
@@ -71,10 +71,12 @@ Every generated app MUST include a **compact, always-visible simulator panel** w
71
71
  | `gesture` | `Tap`, `2Tap`, `Twist`, `2Twist` |
72
72
  | `nav_direction` | `↑`, `↓`, `←`, `→`, `Roll L`, `Roll R` |
73
73
  | `navigation` | `↑`, `↓`, `←`, `→` (each click emits one delta event of ±8) |
74
- | `pressure` | Slider `0–100%` (or `−` / `+` buttons if horizontal space is tight) |
74
+ | `direct_pressure` | Slider `0–100%` (or `−` / `+` buttons if horizontal space is tight) |
75
+ | `pinch_pressure` | Slider `0–100%` plus a `Release` button that returns it to 0 — mirrors the pinch-hold-release arc |
75
76
  | `button` | `Press`, `Release` |
76
77
  | `imu_acc` | `Tilt X`, `Tilt Y`, `Tilt Z` (each fires a 5-frame burst at ±2 m/s²) |
77
78
  | `imu_gyro` | `Rot X`, `Rot Y`, `Rot Z` (each fires a 5-frame burst at ±10 deg/s) |
79
+ | `imu_quaternion` | `Yaw ±`, `Pitch ±`, `Roll ±`, `Reset` — each click rotates a held Euler state by 15°, converts to a unit quaternion, and emits it as a one-sample frame `{ values: [[w,x,y,z]] }` |
78
80
  | `emg` | `Spike` (injects a burst of elevated samples on all 3 channels) |
79
81
 
80
82
  **How each button must fire**
@@ -99,8 +101,22 @@ Unless the user explicitly asks for a different signal, every generated
99
101
  app MUST restrict itself to **at most these signals** (subject to the
100
102
  exclusivity rules below):
101
103
 
102
- 1. **One** of `pressure` **or** `gesture` — never both. `gesture` and
103
- `pressure` are mutually exclusive.
104
+ 1. **One** pressure signal **or** `gesture` — never both. `gesture` and
105
+ pressure are mutually exclusive.
106
+ - **Pressure mode rule**: there is no signal named `pressure`. Pick
107
+ **one** of `direct_pressure` or `pinch_pressure` — never both.
108
+ - `direct_pressure` → Finger pressure 0–100, normalized 0–1.
109
+ **New** continuous ungated stream — always on, no tap/release
110
+ gating. **Default.** Requires firmware 6.0.12.11 and above. Use
111
+ it whenever the user just says "pressure" or names an analog
112
+ synonym (volume, brush, throttle, zoom, intensity).
113
+ - `pinch_pressure` → Finger pressure 0–100, normalized 0–1. The
114
+ **original** tap-to-release filtered stream (formerly named
115
+ `pressure`). Values stream only between tap and release: streaming
116
+ starts on tap, the value falls off on release, and streaming
117
+ stops until the next tap. Works on older firmware. Use it only
118
+ when the interaction is explicitly commit-then-modulate:
119
+ grab-and-scale, pinch-to-zoom, pinch-and-hold-to-charge.
104
120
  - **Tap exclusivity rule** (within `gesture`): use `tap` OR `double_tap`
105
121
  — **never both together** unless the user explicitly names both (e.g.,
106
122
  "use single tap for X and double tap for Y").
@@ -120,9 +136,19 @@ Rules:
120
136
  - Drop any of the above if the concept does not need it (e.g., a pure
121
137
  tap-counter subscribes to `gesture` only and skips the directional
122
138
  signal).
123
- - **`gesture` and `pressure` are mutually exclusive** — pick one
139
+ - **`gesture` and pressure are mutually exclusive** — pick one
124
140
  interaction model per app. Tap/twist concepts → `gesture`. Analog
125
- concepts (volume, brush, throttle) → `pressure`.
141
+ concepts (volume, brush, throttle) → `direct_pressure`.
142
+ - **`imu_quaternion` (Hand Orientation) is standalone and free-combining.**
143
+ It streams absolute, drift-free orientation as unit quaternions and
144
+ belongs to no bundle — it combines with `navigation`, `nav_direction`,
145
+ `gesture`, `button`, and the pressure modes alike. Reach for it whenever
146
+ the concept needs aiming, heading, pose gating, or 1:1 rotation, instead
147
+ of subscribing to the IMU+Biometric bundle just to derive an angle.
148
+ Requires firmware 6.0.12.11 and above. **Its payload shape differs
149
+ from the other IMU signals** — `data.values` is a *list of samples*,
150
+ each `[w, x, y, z]`, not three per-axis arrays. Read the latest with
151
+ `values.at(-1)`.
126
152
  - `nav_direction` and `navigation` are **mutually exclusive per app** —
127
153
  pick the one that fits the interaction (discrete swipes →
128
154
  `nav_direction`; continuous cursor/scroll → `navigation`). Never wire
@@ -133,8 +159,8 @@ Rules:
133
159
  `nav_direction` — pick directional motion OR the IMU+Biometric bundle,
134
160
  never both.
135
161
  - Other gesture subtypes (`twist`, `double_twist`, etc.) and other
136
- signals (`button`, `imu_acc`, `imu_gyro`, `emg`) are **off by
137
- default**. Only include them when the user's prompt names them,
162
+ signals (`button`, `imu_acc`, `imu_gyro`, `emg`, `imu_quaternion`) are
163
+ **off by default**. Only include them when the user's prompt names them,
138
164
  names a synonym from the Signal Inference table in
139
165
  `references/prompt.md` § "Signal Inference Reference", or describes
140
166
  an interaction that genuinely cannot be expressed with the defaults
@@ -153,7 +179,9 @@ Rules:
153
179
  - Subscribe one signal per command: `{ "command": "subscribe", "signal": "<name>" }` — singular `signal`, never `signals`, never an array
154
180
  - Motion modes are mutually exclusive: Pointer (`navigation`+`button`) / Direction (`nav_direction`) / IMU+Biometric (`imu_acc`+`imu_gyro`+`emg`, always all three together)
155
181
  - IMU+Biometric bundle: `imu_acc`, `imu_gyro`, `emg` always subscribed together — never partially. The bundle is mutually exclusive with `navigation` and `nav_direction`.
156
- - `gesture` and `pressure` are mutually exclusive — never combine them
157
- - `button` combines freely with `gesture`, `pressure`, `emg`, `imu_acc`, `imu_gyro` (subject to the Pointer/Direction/IMU motion-mode XOR — `button` belongs to Pointer mode and never combines with `nav_direction`).
182
+ - Hand Orientation (`imu_quaternion`) is **outside** every motion-mode XOR — it is standalone and combines with any other signal, including `navigation` and `nav_direction`. Requires firmware 6.0.12.11 and above. Payload is `data.values` = list of `[w, x, y, z]` samples; read the latest with `values.at(-1)`.
183
+ - Pressure: pick exactly one. Both are Finger pressure 0–100, normalized 0–1. `direct_pressure` is the **new** continuous ungated stream (default). Requires firmware 6.0.12.11 and above. `pinch_pressure` is the **original** tap-to-release filtered stream and works on older firmware. There is no bare `pressure` signal.
184
+ - `gesture` and pressure are mutually exclusive — never combine them
185
+ - `button` combines freely with `gesture`, either pressure mode, `emg`, `imu_acc`, `imu_gyro`, `imu_quaternion` (subject to the Pointer/Direction/IMU motion-mode XOR — `button` belongs to Pointer mode and never combines with `nav_direction`).
158
186
  - **Navigation sensitivity is gentle by default**: keyboard `step = 3`, sim button `±3`, cursor multiplier `0.002`. Raise only when the prompt explicitly asks for fast/snappy movement. See `references/prompt.md` § "Navigation sensitivity defaults".
159
187
  - Canonical protocol JSON: `references/agent_protocol.json`