From 58e41228e51609525a84df56999f1cd4d14be567 Mon Sep 17 00:00:00 2001 From: Thanh Chau <1320427+thannous@users.noreply.github.com> Date: Fri, 31 Jul 2026 16:24:28 +0200 Subject: [PATCH 1/2] docs: add bilingual project documentation --- CONTRIBUTING.fr.md | 142 +++ CONTRIBUTING.md | 185 ++-- LICENSING.fr.md | 56 ++ LICENSING.md | 62 +- SECURITY.fr.md | 79 ++ SECURITY.md | 129 ++- docs/codex-micro/configuration.md | 239 ++--- .../local-observation-2026-07-27.md | 70 +- docs/compatibility.md | 47 +- docs/fr/codex-micro/configuration.md | 142 +++ .../local-observation-2026-07-27.md | 49 + docs/fr/compatibility.md | 30 + docs/fr/getting-started.md | 122 +++ docs/fr/installation.md | 371 ++++++++ docs/fr/publishing-checklist.md | 59 ++ docs/fr/research/appsense-behavior.md | 135 +++ docs/fr/research/effort-wheel-calibration.md | 144 +++ docs/fr/research/hid-lighting-protocol.md | 117 +++ docs/fr/research/input-0.17.2-sharing.md | 116 +++ docs/fr/research/thread-status-feasibility.md | 562 +++++++++++ docs/fr/roadmap.md | 122 +++ docs/fr/scope-and-limitations.md | 109 +++ docs/fr/vision.md | 83 ++ docs/getting-started.md | 99 +- docs/installation.md | 426 +++++---- docs/publishing-checklist.md | 97 +- docs/research/appsense-behavior.md | 251 ++--- docs/research/effort-wheel-calibration.md | 248 +++-- docs/research/hid-lighting-protocol.md | 205 ++-- docs/research/input-0.17.2-sharing.md | 153 +-- docs/research/thread-status-feasibility.md | 877 +++++++++--------- docs/roadmap.md | 231 ++--- docs/scope-and-limitations.md | 205 ++-- docs/vision.md | 110 ++- 34 files changed, 4254 insertions(+), 1818 deletions(-) create mode 100644 CONTRIBUTING.fr.md create mode 100644 LICENSING.fr.md create mode 100644 SECURITY.fr.md create mode 100644 docs/fr/codex-micro/configuration.md create mode 100644 docs/fr/codex-micro/local-observation-2026-07-27.md create mode 100644 docs/fr/compatibility.md create mode 100644 docs/fr/getting-started.md create mode 100644 docs/fr/installation.md create mode 100644 docs/fr/publishing-checklist.md create mode 100644 docs/fr/research/appsense-behavior.md create mode 100644 docs/fr/research/effort-wheel-calibration.md create mode 100644 docs/fr/research/hid-lighting-protocol.md create mode 100644 docs/fr/research/input-0.17.2-sharing.md create mode 100644 docs/fr/research/thread-status-feasibility.md create mode 100644 docs/fr/roadmap.md create mode 100644 docs/fr/scope-and-limitations.md create mode 100644 docs/fr/vision.md diff --git a/CONTRIBUTING.fr.md b/CONTRIBUTING.fr.md new file mode 100644 index 0000000..0594197 --- /dev/null +++ b/CONTRIBUTING.fr.md @@ -0,0 +1,142 @@ +[English](CONTRIBUTING.md) · [Français](CONTRIBUTING.fr.md) + +# Contribuer + +## Statut du projet + +Le dépôt est public et expérimental. La licence MIT et le titulaire +`Thanh Chau` sont confirmés. Aucun preset ne devient installable ou stable sans +preuve reproductible. + +Une contribution peut améliorer une proposition, fournir une preuve matérielle, +ajouter un outil de sécurité ou introduire un vrai export officiel assaini. Son +niveau de preuve doit rester explicite pendant toute la revue. + +## Proposer un nouveau preset + +Commencer par le modèle GitHub « Proposition de preset » avec : + +- l'application ou le workflow cible ; +- le matériel, le système, la version d'Input et le firmware ; +- le mapping touche par touche ; +- le mode d'activation, notamment AppSense ; +- la méthode d'installation envisagée ; +- la sauvegarde et le retour arrière ; +- les actions sensibles volontairement exclues. + +Placer ensuite le preset dans `profiles//` selon +[`profiles/README.md`](profiles/README.md). + +## Architecture + +Chaque preset stable sépare : + +- `manifest.json` : identité, compatibilité, preuve et politique d'installation ; +- `mapping.json` : positions physiques, actions, couleur et activation ; +- `assets/` : visuels originaux ou redistribuables ; +- `artifacts/` : uniquement un export officiel assaini et vérifié ; +- `README.md` : installation, tests, limites et rollback. + +Les schémas communs se trouvent dans `profiles/schema/v1/`. La piste BLE reste +séparée sous `ble/`. + +## Préparer l'environnement + +Prérequis : Node.js 18 ou version ultérieure. Les dépendances de validation +sont épinglées par `package-lock.json`. + +```sh +npm ci --no-audit --no-fund +npm run check +git diff --check +``` + +Les contrôles exécutent : + +- le validateur du contrat Claude historique ; +- le validateur des manifestes et mappings ; +- la vérification des liens locaux ; +- les tests de sauvegarde, rollback, sanitation et idempotence sur fixtures. + +Ils ne modifient ni Input, ni le clavier, ni macOS. + +## Modifier ou ajouter un preset + +1. conserver `proposal-not-applied` sans preuve matérielle ; +2. protéger l'index `0` et ne jamais supposer qu'un identifiant local est + universel ; +3. exiger exactement un layer cible existant, hors index `0` ; +4. conserver son lien AppSense dans la copie locale sans le publier ; +5. laisser les contrôles non utilisés sans action ou réservés ; +6. exclure envoi, permissions, suppression, push, déploiement et commandes + destructrices ; +7. documenter versions, date, preuve et résultat négatif éventuel ; +8. exécuter `npm run check` et `git diff --check`. + +## Ajouter un export officiel + +Un fichier `*-layer.json` doit provenir de **Export layer** dans Work Louder +Input. Ne jamais fabriquer les objets internes à partir du manifeste. + +Avant commit : + +```sh +node scripts/input-layer.mjs inspect-export \ + --input "$HOME/Downloads/Mon-layer.json" \ + --json + +node scripts/input-layer.mjs sanitize-export \ + --input "$HOME/Downloads/Mon-layer.json" \ + --output profiles//artifacts/mon-layer.json +``` + +Le fichier public doit ensuite subir : + +1. import dans une configuration isolée ; +2. comparaison contrôle par contrôle au mapping ; +3. liaison du fichier au SHA-256 déclaré dans le manifeste ; +4. validation sémantique contre le mapping canonique ; +5. second import prouvant l'idempotence ou un refus propre ; +6. rollback par le profile d'origine ; +7. nouvelle exportation et comparaison des sommes/structures. + +La copie brute, le profile original, les captures privées, les chemins locaux, +ports, adresses Bluetooth, numéros de série, identifiants matériels et secrets +restent sous `.local/` et hors de Git. + +## Fournir une preuve matérielle + +Pour AppSense, documenter au minimum : + +- versions d'Input, firmware, macOS et application ; +- nom affiché et application détectée ; +- index de l'unique layer Claude et preuve que l'index `0` est intact ; +- résultat de chaque touche, du cadran et du joystick ; +- layer actif avec et sans focus Claude ; +- persistance après redémarrage ; +- autres liens AppSense préservés, sans publier leurs données privées ; +- restauration du profile original. + +Un résultat non concluant doit rester indiqué comme tel. + +## Piste BLE + +Une contribution BLE ne doit pas présenter Hardware Buddy comme compatible +sans preuve du service Nordic UART, de la coexistence HID + NUS, d'un firmware +restaurable et d'une stratégie de permissions sûre. Aucun firmware ou outil de +flash propriétaire ne doit être ajouté. + +## Checklist de revue + +- [ ] Modification limitée au besoin annoncé. +- [ ] Layer `0`, autres layers, profiles et liens AppSense préservés. +- [ ] Niveau de preuve exact. +- [ ] Sources officielles reliées à l'affirmation correspondante. +- [ ] Aucun secret, chemin privé ou identifiant matériel unique. +- [ ] `npm run check` réussi. +- [ ] `git diff --check` réussi. +- [ ] Sauvegarde et retour arrière documentés. +- [ ] Statut d'import confirmé par une preuve ou indiqué comme non vérifié. +- [ ] Aucun asset ou firmware propriétaire. + +Lire [SECURITY.md](SECURITY.md) avant de publier un rapport sensible. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 5c6ccc9..32f6ede 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,47 +1,49 @@ -# Contribuer +[English](CONTRIBUTING.md) · [Français](CONTRIBUTING.fr.md) -## Statut du projet +# Contributing -Le dépôt est public et expérimental. La licence MIT et le titulaire -`Thanh Chau` sont confirmés. Aucun preset ne devient installable ou stable sans -preuve reproductible. +## Project status -Une contribution peut améliorer une proposition, fournir une preuve matérielle, -ajouter un outil de sécurité ou introduire un vrai export officiel assaini. Son -niveau de preuve doit rester explicite pendant toute la revue. +The repository is public and experimental. The MIT licence and the holder +`Thanh Chau` are confirmed. No preset becomes installable or stable without +reproducible proof. -## Proposer un nouveau preset +A contribution can improve a proposal, provide hardware evidence, add a safety +tool, or introduce a real sanitised official export. Its level of proof must stay +explicit throughout the review. -Commencer par le modèle GitHub « Proposition de preset » avec : +## Proposing a new preset -- l'application ou le workflow cible ; -- le matériel, le système, la version d'Input et le firmware ; -- le mapping touche par touche ; -- le mode d'activation, notamment AppSense ; -- la méthode d'installation envisagée ; -- la sauvegarde et le retour arrière ; -- les actions sensibles volontairement exclues. +Start with the GitHub "Preset proposal" template, including: -Placer ensuite le preset dans `profiles//` selon +- the target application or workflow; +- the hardware, the operating system, the Input version and the firmware; +- the key-by-key mapping; +- the activation method, AppSense in particular; +- the intended installation method; +- the backup and the rollback; +- the sensitive actions deliberately excluded. + +Then place the preset in `profiles//` following [`profiles/README.md`](profiles/README.md). ## Architecture -Chaque preset stable sépare : +Every stable preset separates: -- `manifest.json` : identité, compatibilité, preuve et politique d'installation ; -- `mapping.json` : positions physiques, actions, couleur et activation ; -- `assets/` : visuels originaux ou redistribuables ; -- `artifacts/` : uniquement un export officiel assaini et vérifié ; -- `README.md` : installation, tests, limites et rollback. +- `manifest.json`: identity, compatibility, proof and installation policy; +- `mapping.json`: physical positions, actions, colour and activation; +- `assets/`: original or redistributable visuals; +- `artifacts/`: only a sanitised, verified official export; +- `README.md`: installation, tests, limits and rollback. -Les schémas communs se trouvent dans `profiles/schema/v1/`. La piste BLE reste -séparée sous `ble/`. +The common schemas live in `profiles/schema/v1/`. The BLE track stays separate +under `ble/`. -## Préparer l'environnement +## Preparing the environment -Prérequis : Node.js 18 ou version ultérieure. Les dépendances de validation -sont épinglées par `package-lock.json`. +Prerequisite: Node.js 18 or newer. The validation dependencies are pinned by +`package-lock.json`. ```sh npm ci --no-audit --no-fund @@ -49,92 +51,91 @@ npm run check git diff --check ``` -Les contrôles exécutent : +The checks run: -- le validateur du contrat Claude historique ; -- le validateur des manifestes et mappings ; -- la vérification des liens locaux ; -- les tests de sauvegarde, rollback, sanitation et idempotence sur fixtures. +- the historical Claude contract validator; +- the manifest and mapping validator; +- local link verification; +- the backup, rollback, sanitisation and idempotence tests on fixtures. -Ils ne modifient ni Input, ni le clavier, ni macOS. +They modify neither Input, nor the keyboard, nor macOS. -## Modifier ou ajouter un preset +## Modifying or adding a preset -1. conserver `proposal-not-applied` sans preuve matérielle ; -2. protéger l'index `0` et ne jamais supposer qu'un identifiant local est - universel ; -3. exiger exactement un layer cible existant, hors index `0` ; -4. conserver son lien AppSense dans la copie locale sans le publier ; -5. laisser les contrôles non utilisés sans action ou réservés ; -6. exclure envoi, permissions, suppression, push, déploiement et commandes - destructrices ; -7. documenter versions, date, preuve et résultat négatif éventuel ; -8. exécuter `npm run check` et `git diff --check`. +1. keep `proposal-not-applied` without hardware evidence; +2. protect index `0` and never assume a local identifier is universal; +3. require exactly one existing target layer, outside index `0`; +4. preserve its AppSense link in the local copy without publishing it; +5. leave unused controls without an action, or reserved; +6. exclude sending, permissions, deletion, push, deployment and destructive + commands; +7. document versions, date, proof and any negative result; +8. run `npm run check` and `git diff --check`. -## Ajouter un export officiel +## Adding an official export -Un fichier `*-layer.json` doit provenir de **Export layer** dans Work Louder -Input. Ne jamais fabriquer les objets internes à partir du manifeste. +A `*-layer.json` file must come from **Export layer** in Work Louder Input. Never +fabricate the internal objects from the manifest. -Avant commit : +Before committing: ```sh node scripts/input-layer.mjs inspect-export \ - --input "$HOME/Downloads/Mon-layer.json" \ + --input "$HOME/Downloads/My-layer.json" \ --json node scripts/input-layer.mjs sanitize-export \ - --input "$HOME/Downloads/Mon-layer.json" \ - --output profiles//artifacts/mon-layer.json + --input "$HOME/Downloads/My-layer.json" \ + --output profiles//artifacts/my-layer.json ``` -Le fichier public doit ensuite subir : +The public file must then go through: -1. import dans une configuration isolée ; -2. comparaison contrôle par contrôle au mapping ; -3. liaison du fichier au SHA-256 déclaré dans le manifeste ; -4. validation sémantique contre le mapping canonique ; -5. second import prouvant l'idempotence ou un refus propre ; -6. rollback par le profile d'origine ; -7. nouvelle exportation et comparaison des sommes/structures. +1. import into an isolated configuration; +2. control-by-control comparison against the mapping; +3. binding the file to the SHA-256 declared in the manifest; +4. semantic validation against the canonical mapping; +5. a second import proving idempotence, or a clean refusal; +6. rollback through the original profile; +7. a fresh export and comparison of sums and structures. -La copie brute, le profile original, les captures privées, les chemins locaux, -ports, adresses Bluetooth, numéros de série, identifiants matériels et secrets -restent sous `.local/` et hors de Git. +The raw copy, the original profile, private screenshots, local paths, ports, +Bluetooth addresses, serial numbers, hardware identifiers and secrets stay under +`.local/` and out of Git. -## Fournir une preuve matérielle +## Providing hardware evidence -Pour AppSense, documenter au minimum : +For AppSense, document at minimum: -- versions d'Input, firmware, macOS et application ; -- nom affiché et application détectée ; -- index de l'unique layer Claude et preuve que l'index `0` est intact ; -- résultat de chaque touche, du cadran et du joystick ; -- layer actif avec et sans focus Claude ; -- persistance après redémarrage ; -- autres liens AppSense préservés, sans publier leurs données privées ; -- restauration du profile original. +- the Input, firmware, macOS and application versions; +- the displayed name and the detected application; +- the index of the single Claude layer, and proof that index `0` is intact; +- the result for every key, the dial and the joystick; +- the active layer with and without Claude focused; +- persistence after a restart; +- other AppSense links preserved, without publishing their private data; +- restoration of the original profile. -Un résultat non concluant doit rester indiqué comme tel. +An inconclusive result must stay marked as such. -## Piste BLE +## BLE track -Une contribution BLE ne doit pas présenter Hardware Buddy comme compatible -sans preuve du service Nordic UART, de la coexistence HID + NUS, d'un firmware -restaurable et d'une stratégie de permissions sûre. Aucun firmware ou outil de -flash propriétaire ne doit être ajouté. +A BLE contribution must not present Hardware Buddy as compatible without evidence +of the Nordic UART service, of HID + NUS coexistence, of restorable firmware and +of a safe permission strategy. No proprietary firmware or flashing tool may be +added. -## Checklist de revue +## Review checklist -- [ ] Modification limitée au besoin annoncé. -- [ ] Layer `0`, autres layers, profiles et liens AppSense préservés. -- [ ] Niveau de preuve exact. -- [ ] Sources officielles reliées à l'affirmation correspondante. -- [ ] Aucun secret, chemin privé ou identifiant matériel unique. -- [ ] `npm run check` réussi. -- [ ] `git diff --check` réussi. -- [ ] Sauvegarde et retour arrière documentés. -- [ ] Statut d'import confirmé par une preuve ou indiqué comme non vérifié. -- [ ] Aucun asset ou firmware propriétaire. +- [ ] Change limited to the stated need. +- [ ] Layer `0`, other layers, profiles and AppSense links preserved. +- [ ] Exact level of proof. +- [ ] Official sources tied to the corresponding claim. +- [ ] No secret, private path or unique hardware identifier. +- [ ] `npm run check` passes. +- [ ] `git diff --check` passes. +- [ ] Backup and rollback documented. +- [ ] Import status confirmed by evidence, or marked as unverified. +- [ ] No proprietary asset or firmware. -Lire [SECURITY.md](SECURITY.md) avant de publier un rapport sensible. +Read [SECURITY.md](SECURITY.md) before publishing a sensitive report. diff --git a/LICENSING.fr.md b/LICENSING.fr.md new file mode 100644 index 0000000..c77461f --- /dev/null +++ b/LICENSING.fr.md @@ -0,0 +1,56 @@ +[English](LICENSING.md) · [Français](LICENSING.fr.md) + +# Licence du projet + +## Décision + +La licence **MIT** a été choisie par le propriétaire du projet. + +Le texte standard complet se trouve dans [`LICENSE`](LICENSE) et couvre le code +ainsi que la documentation du dépôt, sauf mention explicite contraire. + +## Titulaire du copyright + +Le propriétaire du projet a confirmé le titulaire suivant : + +```text +Thanh Chau +``` + +La notice installée est : + +```text +Copyright (c) 2026 Thanh Chau +``` + +## Effet de la licence MIT + +La licence MIT permet notamment l'utilisation, la copie, la modification, la +fusion, la publication, la distribution, la sous-licence et la vente de copies, +sous réserve de conserver la notice de copyright et la notice de permission. +Elle inclut une exclusion de garantie. + +Le texte de `LICENSE` fait foi ; ce résumé n'ajoute ni ne retire de condition. + +## Marques et contenus tiers + +La licence du projet ne donne aucun droit sur les marques Work Louder, +Codex Micro, Claude ou Anthropic. Le dépôt reste un projet indépendant et ne +doit pas inclure : + +- logos ou assets tiers sans autorisation ; +- firmware ou exports propriétaires ; +- captures contenant des identifiants privés ; +- texte tiers incompatible avec la licence ou la loi. + +## État de la décision + +- Licence : MIT. +- Titulaire : Thanh Chau. +- Année : 2026. +- Périmètre : code et documentation, sauf mention explicite contraire. + +Les portes de licence sont résolues. Les étapes Git et de publication restent +soumises à une autorisation séparée. + +Ce document ne constitue pas un avis juridique. diff --git a/LICENSING.md b/LICENSING.md index 6d9af9e..c161375 100644 --- a/LICENSING.md +++ b/LICENSING.md @@ -1,54 +1,56 @@ -# Licence du projet +[English](LICENSING.md) · [Français](LICENSING.fr.md) -## Décision +# Project licence -La licence **MIT** a été choisie par le propriétaire du projet. +## Decision -Le texte standard complet se trouve dans [`LICENSE`](LICENSE) et couvre le code -ainsi que la documentation du dépôt, sauf mention explicite contraire. +The **MIT** licence was chosen by the project owner. -## Titulaire du copyright +The complete standard text is in [`LICENSE`](LICENSE) and covers the code as well +as the repository's documentation, unless explicitly stated otherwise. -Le propriétaire du projet a confirmé le titulaire suivant : +## Copyright holder + +The project owner confirmed the following holder: ```text Thanh Chau ``` -La notice installée est : +The installed notice is: ```text Copyright (c) 2026 Thanh Chau ``` -## Effet de la licence MIT +## Effect of the MIT licence -La licence MIT permet notamment l'utilisation, la copie, la modification, la -fusion, la publication, la distribution, la sous-licence et la vente de copies, -sous réserve de conserver la notice de copyright et la notice de permission. -Elle inclut une exclusion de garantie. +The MIT licence permits, among other things, use, copying, modification, merging, +publication, distribution, sublicensing and the sale of copies, provided the +copyright notice and the permission notice are kept. It includes a disclaimer of +warranty. -Le texte de `LICENSE` fait foi ; ce résumé n'ajoute ni ne retire de condition. +The text of `LICENSE` governs; this summary adds no condition and removes none. -## Marques et contenus tiers +## Trademarks and third-party content -La licence du projet ne donne aucun droit sur les marques Work Louder, -Codex Micro, Claude ou Anthropic. Le dépôt reste un projet indépendant et ne -doit pas inclure : +The project's licence grants no right over the Work Louder, Codex Micro, Claude +or Anthropic trademarks. The repository remains an independent project and must +not include: -- logos ou assets tiers sans autorisation ; -- firmware ou exports propriétaires ; -- captures contenant des identifiants privés ; -- texte tiers incompatible avec la licence ou la loi. +- third-party logos or assets without permission; +- proprietary firmware or exports; +- screenshots containing private identifiers; +- third-party text incompatible with the licence or the law. -## État de la décision +## State of the decision -- Licence : MIT. -- Titulaire : Thanh Chau. -- Année : 2026. -- Périmètre : code et documentation, sauf mention explicite contraire. +- Licence: MIT. +- Holder: Thanh Chau. +- Year: 2026. +- Scope: code and documentation, unless explicitly stated otherwise. -Les portes de licence sont résolues. Les étapes Git et de publication restent -soumises à une autorisation séparée. +The licensing gates are resolved. The Git and publication steps remain subject to +separate authorisation. -Ce document ne constitue pas un avis juridique. +This document does not constitute legal advice. diff --git a/SECURITY.fr.md b/SECURITY.fr.md new file mode 100644 index 0000000..5320225 --- /dev/null +++ b/SECURITY.fr.md @@ -0,0 +1,79 @@ +[English](SECURITY.md) · [Français](SECURITY.fr.md) + +# Sécurité + +## Statut + +Le dépôt est public, mais le projet reste expérimental et ne possède pas encore +de version supportée ni de canal privé de signalement. Ne publiez jamais de +secret, de profil personnel ou de détail permettant d'exploiter une +vulnérabilité. En l'absence de canal privé, ouvrez seulement un signalement +minimal demandant un moyen de contact confidentiel. + +## Modèle de risque + +### Raccourcis HID + +Une touche peut envoyer, supprimer ou déclencher une action dans la mauvaise +application. Le profil par défaut exclut `Entrée` et toute décision de +permission. Les tests doivent utiliser un contexte sans enjeu et commencer par +une seule action à faible risque. + +### AppSense et focus + +Une mauvaise détection peut activer le mauvais layer. Il faut vérifier : + +- l'application détectée ; +- le layer actif avant chaque test ; +- le comportement après perte de focus ; +- les conflits avec les liens existants. + +Ne jamais corriger un conflit en réinitialisant tous les réglages. + +### BLE Hardware Buddy + +Le protocole permet de recevoir des informations de session et de répondre à +une demande de permission. Une implémentation défectueuse pourrait exposer des +extraits de conversation ou approuver une action involontairement. + +Tant que la piste n'est pas auditée : + +- aucune approbation ou décision automatique ; +- aucun identifiant de prompt conservé ; +- aucun journal contenant adresse, jeton ou code d'appairage ; +- aucun firmware flashable distribué ; +- aucun appairage présenté comme supporté ; +- retour à un état neutre après perte de connexion. + +## Données à ne pas collecter + +- adresse Bluetooth ou identifiant matériel unique ; +- numéro de série ; +- code d'appairage ; +- contenu des conversations Claude ; +- jeton, clé API ou secret local ; +- capture complète de réglages contenant des données personnelles. + +## Dépendances et scripts + +Le projet ne transmet pas les profils ou exports Work Louder à un service +distant. Le configurateur, le générateur et les validateurs les traitent +localement, sans télémétrie applicative. + +Une connexion réseau peut toutefois être utilisée par +`npm ci --no-audit --no-fund` pour télécharger depuis le registre configuré les +versions verrouillées dans `package-lock.json`. Lors de leur première +exécution, `npm run configure` et `npm run check` peuvent de même lancer +`npm ci --ignore-scripts` dans `prototype/` si les dépendances du GUI sont +absentes. Une fois installées, le traitement des profils reste local. Toute +nouvelle dépendance ou communication distante doit être justifiée, verrouillée, +documentée et auditée avant publication. + +## Sauvegarde et restauration + +Le retour arrière principal reste l'import du profile officiel d'origine dans +Input. La restauration brute du stockage est secondaire et exige plusieurs +confirmations explicites. Sa destination doit correspondre exactement à un +chemin Input détecté ou à `WORK_LOUDER_INPUT_USER_DATA`. L'outil refuse la +racine du système, le dossier utilisateur, le dépôt, les liens symboliques et +tout dossier non approuvé. diff --git a/SECURITY.md b/SECURITY.md index 67fa8e1..3ed71c0 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -1,77 +1,76 @@ -# Sécurité +[English](SECURITY.md) · [Français](SECURITY.fr.md) -## Statut +# Security -Le dépôt est public, mais le projet reste expérimental et ne possède pas encore -de version supportée ni de canal privé de signalement. Ne publiez jamais de -secret, de profil personnel ou de détail permettant d'exploiter une -vulnérabilité. En l'absence de canal privé, ouvrez seulement un signalement -minimal demandant un moyen de contact confidentiel. +## Status -## Modèle de risque +The repository is public, but the project stays experimental and does not yet +have a supported version or a private reporting channel. Never publish a secret, +a personal profile, or any detail that would make a vulnerability exploitable. +With no private channel available, open only a minimal report asking for a +confidential contact method. -### Raccourcis HID +## Risk model -Une touche peut envoyer, supprimer ou déclencher une action dans la mauvaise -application. Le profil par défaut exclut `Entrée` et toute décision de -permission. Les tests doivent utiliser un contexte sans enjeu et commencer par -une seule action à faible risque. +### HID shortcuts -### AppSense et focus +A key can send, delete or trigger an action in the wrong application. The default +profile excludes `Enter` and every permission decision. Tests must use a +low-stakes context and start with a single low-risk action. -Une mauvaise détection peut activer le mauvais layer. Il faut vérifier : +### AppSense and focus -- l'application détectée ; -- le layer actif avant chaque test ; -- le comportement après perte de focus ; -- les conflits avec les liens existants. +Faulty detection can activate the wrong layer. You have to check: -Ne jamais corriger un conflit en réinitialisant tous les réglages. +- the detected application; +- the active layer before each test; +- the behaviour after focus loss; +- conflicts with existing links. + +Never fix a conflict by resetting all settings. ### BLE Hardware Buddy -Le protocole permet de recevoir des informations de session et de répondre à -une demande de permission. Une implémentation défectueuse pourrait exposer des -extraits de conversation ou approuver une action involontairement. - -Tant que la piste n'est pas auditée : - -- aucune approbation ou décision automatique ; -- aucun identifiant de prompt conservé ; -- aucun journal contenant adresse, jeton ou code d'appairage ; -- aucun firmware flashable distribué ; -- aucun appairage présenté comme supporté ; -- retour à un état neutre après perte de connexion. - -## Données à ne pas collecter - -- adresse Bluetooth ou identifiant matériel unique ; -- numéro de série ; -- code d'appairage ; -- contenu des conversations Claude ; -- jeton, clé API ou secret local ; -- capture complète de réglages contenant des données personnelles. - -## Dépendances et scripts - -Le projet ne transmet pas les profils ou exports Work Louder à un service -distant. Le configurateur, le générateur et les validateurs les traitent -localement, sans télémétrie applicative. - -Une connexion réseau peut toutefois être utilisée par -`npm ci --no-audit --no-fund` pour télécharger depuis le registre configuré les -versions verrouillées dans `package-lock.json`. Lors de leur première -exécution, `npm run configure` et `npm run check` peuvent de même lancer -`npm ci --ignore-scripts` dans `prototype/` si les dépendances du GUI sont -absentes. Une fois installées, le traitement des profils reste local. Toute -nouvelle dépendance ou communication distante doit être justifiée, verrouillée, -documentée et auditée avant publication. - -## Sauvegarde et restauration - -Le retour arrière principal reste l'import du profile officiel d'origine dans -Input. La restauration brute du stockage est secondaire et exige plusieurs -confirmations explicites. Sa destination doit correspondre exactement à un -chemin Input détecté ou à `WORK_LOUDER_INPUT_USER_DATA`. L'outil refuse la -racine du système, le dossier utilisateur, le dépôt, les liens symboliques et -tout dossier non approuvé. +The protocol makes it possible to receive session information and to answer a +permission request. A faulty implementation could expose conversation excerpts or +approve an action unintentionally. + +Until the track is audited: + +- no automatic approval or decision; +- no prompt identifier retained; +- no log containing an address, token or pairing code; +- no flashable firmware distributed; +- no pairing presented as supported; +- return to a neutral state after a connection loss. + +## Data not to collect + +- Bluetooth address or unique hardware identifier; +- serial number; +- pairing code; +- the content of Claude conversations; +- token, API key or local secret; +- a full settings capture containing personal data. + +## Dependencies and scripts + +The project does not transmit Work Louder profiles or exports to a remote +service. The configurator, the generator and the validators process them locally, +with no application telemetry. + +A network connection may however be used by `npm ci --no-audit --no-fund` to +download, from the configured registry, the versions locked in +`package-lock.json`. On their first run, `npm run configure` and `npm run check` +may likewise run `npm ci --ignore-scripts` in `prototype/` if the GUI +dependencies are missing. Once installed, profile processing stays local. Any new +dependency or remote communication must be justified, locked, documented and +audited before publication. + +## Backup and restore + +The primary rollback remains importing the original official profile into Input. +Restoring the storage raw is secondary and requires several explicit +confirmations. Its destination must match exactly a detected Input path or +`WORK_LOUDER_INPUT_USER_DATA`. The tool refuses the system root, the home folder, +the repository, symbolic links, and any unapproved folder. diff --git a/docs/codex-micro/configuration.md b/docs/codex-micro/configuration.md index 15ce090..671d619 100644 --- a/docs/codex-micro/configuration.md +++ b/docs/codex-micro/configuration.md @@ -1,140 +1,141 @@ -# Configurer le Codex Micro pour Claude Desktop - -## Flux cible - -1. exporter le profile Input actif et créer une sauvegarde vérifiée ; -2. inventorier les layers sans publier les données privées ; -3. protéger intégralement le layer natif à l'index `0` ; -4. exiger exactement un layer `Claude` existant, hors index `0` ; -5. conserver son lien AppSense dans une copie locale du profile ; -6. appliquer le mapping physique documenté à cette copie ; -7. tester chaque contrôle, la perte de focus et le redémarrage ; -8. exporter le layer, l'assainir et vérifier son round-trip ; -9. restaurer le profile original en cas d'écart. - -Cette intégration utilise les raccourcis HID et AppSense. Elle ne dépend pas du -protocole expérimental Hardware Buddy. - -Le manifeste et le mapping V1 décrivent l'état observé du générateur. Le fichier -logique `macos.example.json` reste `proposal-not-applied` et ne doit pas être -importé tel quel. `npm run configure` fabrique uniquement un profile personnel -à partir de l'export officiel de l'utilisateur. - -## Préservation obligatoire - -Le preset et l'outil imposent les règles suivantes : - -- index `0` protégé, sans remplacement ni modification ; -- politique `exactly-one-existing-named-layer` ; -- six emplacements au maximum ; -- aucun autre profile, layer ou lien AppSense modifié ; -- absence ou doublon `Claude` refusé ; -- structure d'export inconnue refusée au lieu d'être interprétée ; -- sauvegarde et vérification SHA-256 avant la session réelle ; -- aucune écriture directe dans le stockage Input ; -- aucune écriture de configuration sur le périphérique — keymap, layers et - couleurs de layer passent exclusivement par le flux de profils Input. - -La dernière règle portait jusqu'ici sur toute écriture vers le périphérique. -Elle est restreinte aux écritures **persistantes** : l'envoi de rapports HID -d'éclairage volatils est désormais dans le périmètre, sous six conditions -cumulatives énoncées dans -[`docs/scope-and-limitations.md`](../scope-and-limitations.md). Cette -configuration-ci n'en dépend pas et reste réalisable sans écrire une seule fois -sur le périphérique. - -L'inventaire bloque la transformation si l'export officiel ne permet pas de -prouver la présence du layer protégé à l'index `0` et d'un unique layer -`Claude`. - -## Mapping physique V1 - -Orientation : vue du dessus, câble à l'opposé de l'utilisateur. - -| Contrôle | Position | Action | +[English](configuration.md) · [Français](../fr/codex-micro/configuration.md) + +# Configuring the Codex Micro for Claude Desktop + +## Target flow + +1. export the active Input profile and create a verified backup; +2. inventory the layers without publishing private data; +3. fully protect the native layer at index `0`; +4. require exactly one existing `Claude` layer, outside index `0`; +5. preserve its AppSense link in a local copy of the profile; +6. apply the documented physical mapping to that copy; +7. test every control, focus loss and a restart; +8. export the layer, sanitise it and verify its round-trip; +9. restore the original profile on any discrepancy. + +This integration uses HID shortcuts and AppSense. It does not depend on the +experimental Hardware Buddy protocol. + +The V1 manifest and mapping describe the observed state of the generator. The +logical file `macos.example.json` stays `proposal-not-applied` and must not be +imported as is. `npm run configure` only builds a personal profile from the +user's own official export. + +## Mandatory preservation + +The preset and the tool enforce the following rules: + +- index `0` protected, with no replacement and no modification; +- `exactly-one-existing-named-layer` policy; +- six slots at most; +- no other profile, layer or AppSense link modified; +- a missing or duplicated `Claude` refused; +- an unknown export structure refused rather than interpreted; +- backup and SHA-256 verification before the real session; +- no direct write into Input's storage; +- no configuration write to the device — keymap, layers and layer colours go + exclusively through the Input profile flow. + +The last rule used to cover every write to the device. It is now restricted to +**persistent** writes: sending volatile HID lighting reports is within scope, under +six cumulative conditions set out in +[`docs/scope-and-limitations.md`](../scope-and-limitations.md). This particular +configuration does not depend on that, and remains achievable without writing to +the device even once. + +The inventory blocks the transformation if the official export does not make it +possible to prove the presence of the protected layer at index `0` and of a +single `Claude` layer. + +## V1 physical mapping + +Orientation: seen from above, cable pointing away from the user. + +| Control | Position | Action | | --- | --- | --- | -| Touche 1 | rangée des quatre touches carrées, tout à gauche | `⌘N` — nouvelle conversation | -| Touche 2 | même rangée, deuxième | `⌘D` — mode vocal | -| Touche 3 | même rangée, troisième | `⌘⇧D` — afficher ou masquer le diff | -| Touche 4 | même rangée, tout à droite | `Esc` — annuler ou fermer selon le contexte | -| Molette cliquable | coin supérieur gauche | Effort Claude, horaire : `+1` ; antihoraire : `−1` ; clic configurable | -| Joystick sans clic | coin supérieur droit | flèches haut, droite, bas et gauche | +| Key 1 | row of four square keys, far left | `⌘N` — new conversation | +| Key 2 | same row, second | `⌘D` — voice mode | +| Key 3 | same row, third | `⌘⇧D` — show or hide the diff | +| Key 4 | same row, far right | `Esc` — cancel or close, depending on context | +| Clickable wheel | upper left corner | Claude Effort, clockwise: `+1`; counterclockwise: `−1`; press configurable | +| Non-clickable joystick | upper right corner | up, right, down and left arrows | -![Schéma physique du mapping](../../profiles/claude-shortcuts/assets/layout.svg) +![Physical mapping diagram](../../profiles/claude-shortcuts/assets/layout.svg) -Les identifiants internes `inputControlId` restent `null` jusqu'à leur relevé -dans Input sur le Codex Micro exact. Les positions ci-dessus sont donc une cible -physique lisible, pas une affirmation sur le schéma interne de l'application. +The internal `inputControlId` identifiers stay `null` until they are read in +Input on the exact Codex Micro. The positions above are therefore a readable +physical target, not a claim about the application's internal schema. -## Apparence +## Appearance -- nom du layer : `Claude` ; -- couleur proposée : `#D97757` ; -- autres contrôles : `no-action` ; -- capteur tactile : réservé au changement de layer ; -- appui du cadran : aucune action. +- layer name: `Claude`; +- proposed colour: `#D97757`; +- other controls: `no-action`; +- touch sensor: reserved for layer switching; +- dial press: no action. ## AppSense -Le lien cible uniquement : +The link targets only: ```text Claude com.anthropic.claudefordesktop ``` -Le lien doit exister avant l'export du profile. Le générateur conserve son -`linkedAppId` dans la copie locale, refuse son absence et ne crée jamais un -second lien. Les autres liens ne sont jamais modifiés. - -**Il n'existe aucun retour automatique à un état sûr après perte de focus.** -Mesuré sur matériel : AppSense n'est qu'un ensemble de règles application → -layer, et chaque règle est une transition aller. Quitter Claude pour une -application non liée laisse la carte sur le layer Claude, indéfiniment. Le layer -Claude doit donc être conçu comme si ses raccourcis pouvaient rester actifs -ailleurs. Voir [`docs/research/appsense-behavior.md`](../research/appsense-behavior.md). - -## Actions absentes par défaut - -- Retour/Entrée et envoi d'un message ; -- approbation, refus ou rejet d'une permission ; -- suppression ; -- `git push` ; -- déploiement ; -- terminal, shell ou commande destructive ; -- raccourcis globaux Saisie rapide et Dictée. - -Les raccourcis globaux sont volontairement hors du layer AppSense : ils doivent -rester utilisables quand une autre application est au premier plan. Sur la -configuration documentée, il s'agit du double appui sur Option pour la saisie -rapide et de Verr. Maj. pour la dictée globale. - -## Partage officiel observé - -Input `0.17.2` contient les flux **Export layer**, **Import layer**, **Export -Profile** et **Import Profile**. Un export de layer porte le suffixe -`*-layer.json` et contient l'enveloppe décrite dans +The link must exist before the profile is exported. The generator preserves its +`linkedAppId` in the local copy, refuses its absence, and never creates a second +link. Other links are never modified. + +**There is no automatic return to a safe state after focus loss.** Measured on +hardware: AppSense is only a set of application → layer rules, and every rule is +a one-way transition. Leaving Claude for an unlinked application leaves the board +on the Claude layer, indefinitely. The Claude layer must therefore be designed as +if its shortcuts could stay active elsewhere. See +[`docs/research/appsense-behavior.md`](../research/appsense-behavior.md). + +## Actions absent by default + +- Return/Enter and sending a message; +- approving, refusing or rejecting a permission; +- deletion; +- `git push`; +- deployment; +- terminal, shell or destructive commands; +- the Quick Entry and Dictation global shortcuts. + +Global shortcuts are deliberately outside the AppSense layer: they have to stay +usable when another application is in the foreground. On the documented setup, +those are the double press on Option for quick entry, and Caps Lock for global +dictation. + +## Official sharing, as observed + +Input `0.17.2` contains the **Export layer**, **Import layer**, **Export +Profile** and **Import Profile** flows. A layer export carries the +`*-layer.json` suffix and contains the envelope described in [`docs/research/input-0.17.2-sharing.md`](../research/input-0.17.2-sharing.md). -Le dépôt ne fabrique pas les objets internes `layer`, `actions` et groupes. Le -futur artefact doit provenir d'un vrai export du Codex Micro, être assaini, puis -réimporté sur une configuration isolée. +The repository does not fabricate the internal `layer`, `actions` and group +objects. The future artefact must come from a real Codex Micro export, be +sanitised, then re-imported into an isolated configuration. -## Validation restante +## Validation still outstanding -- [ ] export du profile réel et inventaire lisible ; -- [ ] positions et identifiants physiques vérifiés dans Input ; -- [x] transformation locale du layer existant sans modification de l'index `0` ; -- [x] conservation du lien AppSense dans le profile généré ; -- [ ] quatre touches, cadran et joystick testés ; -- [ ] contrôles inutilisés confirmés sans action ; -- [x] comportement après perte de focus : établi, il n'y a pas de retour ; -- [ ] persistance après redémarrage ; -- [ ] export/import du layer reproduit sur une copie isolée ; -- [ ] doublon refusé ou traité idempotemment ; -- [ ] profile original réimporté et périphérique vérifié. +- [ ] real profile export and readable inventory; +- [ ] positions and physical identifiers verified in Input; +- [x] local transformation of the existing layer without modifying index `0`; +- [x] AppSense link preserved in the generated profile; +- [ ] four keys, dial and joystick tested; +- [ ] unused controls confirmed as no-action; +- [x] behaviour after focus loss: established, there is no return; +- [ ] persistence after a restart; +- [ ] layer export/import reproduced on an isolated copy; +- [ ] duplicate refused or handled idempotently; +- [ ] original profile re-imported and device verified. ## Sources -- [Work Louder — Codex Micro, layers et AppSense](https://worklouder.cc/openai-micro-setup) -- [Claude — saisie rapide sur macOS](https://support.claude.com/en/articles/12626668-use-quick-entry-with-claude-desktop-on-mac) +- [Work Louder — Codex Micro, layers and AppSense](https://worklouder.cc/openai-micro-setup) +- [Claude — quick entry on macOS](https://support.claude.com/en/articles/12626668-use-quick-entry-with-claude-desktop-on-mac) diff --git a/docs/codex-micro/local-observation-2026-07-27.md b/docs/codex-micro/local-observation-2026-07-27.md index 2000559..6af9eab 100644 --- a/docs/codex-micro/local-observation-2026-07-27.md +++ b/docs/codex-micro/local-observation-2026-07-27.md @@ -1,47 +1,49 @@ -# Observation locale — 27 juillet 2026 +[English](local-observation-2026-07-27.md) · [Français](../fr/codex-micro/local-observation-2026-07-27.md) -Reconnaissance effectuée en lecture seule lors de l'initialisation précédente, -sans ouvrir les réglages système ni modifier Claude ou le périphérique. +# Local observation — 27 July 2026 -## Environnement observé +Read-only reconnaissance carried out during the previous initialisation, without +opening system settings and without modifying Claude or the device. -- macOS `26.5.2` (`25F84`), Apple Silicon `arm64` ; -- Claude Desktop présent dans `/Applications/Claude.app` ; -- bundle `com.anthropic.claudefordesktop` ; -- version Claude `1.24012.9` ; -- Work Louder Input `0.17.2` ; -- firmware Codex Micro `v0.4.1`, observé dans l'écran Setup d'Input. +## Environment observed -## Codex Micro observé +- macOS `26.5.2` (`25F84`), Apple Silicon `arm64`; +- Claude Desktop present in `/Applications/Claude.app`; +- bundle `com.anthropic.claudefordesktop`; +- Claude version `1.24012.9`; +- Work Louder Input `0.17.2`; +- Codex Micro firmware `v0.4.1`, seen in Input's Setup screen. -Le registre I/O macOS exposait un périphérique HID actif avec : +## Codex Micro observed -- produit `Codex Micro #1` ; -- fabricant `Work Louder` ; -- transport `Bluetooth Low Energy` ; -- Vendor ID `12346` ; -- Product ID `33632` ; +The macOS I/O registry exposed an active HID device with: + +- product `Codex Micro #1`; +- manufacturer `Work Louder`; +- transport `Bluetooth Low Energy`; +- Vendor ID `12346`; +- Product ID `33632`; - VersionNumber `24193`. -Les identifiants uniques, l'adresse Bluetooth et le numéro de série sont -volontairement exclus. +Unique identifiers, the Bluetooth address and the serial number are deliberately +excluded. -## Ce que cette observation prouve +## What this observation proves -- le Codex Micro est visible par macOS ; -- une connexion BLE HID est disponible ; -- le clavier peut en principe émettre des raccourcis standards ; -- la combinaison Input/firmware observée est `0.17.2` / `v0.4.1`. +- the Codex Micro is visible to macOS; +- a BLE HID connection is available; +- the keyboard can in principle emit standard shortcuts; +- the Input/firmware combination observed is `0.17.2` / `v0.4.1`. -## Ce qu'elle ne prouve pas +## What it does not prove -- la révision commerciale exacte du matériel ; -- le caractère modifiable ou publiquement redistribuable du firmware ; -- la présence du Nordic UART Service attendu par Claude ; -- la possibilité d'annoncer un nom commençant par `Claude` ; -- la coexistence HID + Hardware Buddy ; -- l'activation du mode développeur Claude ; -- un échange réel de messages Hardware Buddy. +- the exact commercial revision of the hardware; +- whether the firmware is modifiable or publicly redistributable; +- the presence of the Nordic UART Service that Claude expects; +- the ability to advertise a name starting with `Claude`; +- HID + Hardware Buddy coexistence; +- enabling Claude's developer mode; +- an actual exchange of Hardware Buddy messages. -`system_profiler` ne listait pas le périphérique pendant la reconnaissance. La -preuve provenait du registre HID I/O. Aucun scan GATT actif n'a été lancé. +`system_profiler` did not list the device during the reconnaissance. The evidence +came from the HID I/O registry. No active GATT scan was run. diff --git a/docs/compatibility.md b/docs/compatibility.md index 85788b8..1fff13c 100644 --- a/docs/compatibility.md +++ b/docs/compatibility.md @@ -1,28 +1,31 @@ -# Matrice de compatibilité +[English](compatibility.md) · [Français](fr/compatibility.md) -Cette matrice distingue l'environnement observé, l'analyse du mécanisme de -partage et les validations encore requises sur le matériel. +# Compatibility matrix -| Élément | Version / cible | Niveau de preuve | Résultat | +This matrix separates the observed environment, the analysis of the sharing +mechanism, and the validations still required on hardware. + +| Item | Version / target | Level of proof | Result | | --- | --- | --- | --- | -| Matériel | Work Louder Codex Micro | `hardware-observed` | molette cliquable supérieure gauche, joystick sans clic supérieur droit et touches principales observés ; checklist complète en attente | -| Work Louder Input | `0.17.3`, bundle `it.focusense.input-app` | profile réel et générateur validés | transformation locale du profile validée ; round-trip layer séparé en attente | -| Mécanisme historique | Input `0.17.2` | bundle officiel inspecté | import/export layer et profile observé statiquement | -| Firmware | `v0.4.1` | écran Setup observé localement | combinaison Input/firmware connue ; validation matérielle partielle | -| macOS | `26.5.2` arm64 | observation locale | application et HID observés | -| Claude Desktop | `1.24012.9`, bundle `com.anthropic.claudefordesktop` | bundle local et profil généré | `⌘N`, `⌘D`, `⌘⇧D` configurés ; `Esc` à valider contextuellement | -| AppSense | lien existant du layer Claude | export réel observé | `linkedAppId` conservé localement et interdit dans les artefacts publics ; perte de focus à tester | -| Sauvegarde CLI | Node.js `>=18` | tests automatisés | copie, manifeste SHA-256 et restauration testés sur une copie isolée | -| Artefact layer | `*-layer.json` | absent | doit provenir d'un export réel assaini et subir un round-trip | +| Hardware | Work Louder Codex Micro | `hardware-observed` | clickable wheel at the upper left, non-clickable joystick at the upper right and the main keys observed; full checklist pending | +| Work Louder Input | `0.17.3`, bundle `it.focusense.input-app` | real profile and generator validated | local profile transformation validated; separate layer round-trip pending | +| Historical mechanism | Input `0.17.2` | official bundle inspected | layer and profile import/export observed statically | +| Firmware | `v0.4.1` | Setup screen observed locally | Input/firmware combination known; hardware validation partial | +| macOS | `26.5.2` arm64 | local observation | application and HID observed | +| Claude Desktop | `1.24012.9`, bundle `com.anthropic.claudefordesktop` | local bundle and generated profile | `⌘N`, `⌘D`, `⌘⇧D` configured; `Esc` still to be validated in context | +| AppSense | existing link of the Claude layer | real export observed | `linkedAppId` preserved locally and forbidden in public artefacts; focus loss still to be tested | +| CLI backup | Node.js `>=18` | automated tests | copy, SHA-256 manifest and restore tested on an isolated copy | +| Layer artefact | `*-layer.json` | absent | must come from a real, sanitised export and go through a round-trip | -## Interprétation +## Reading the levels -- `proposal-not-applied` : fichiers logiques et outils disponibles, aucun layer - réel revendiqué ; -- `hardware-observed` : environnement matériel inventorié ; -- `manually-validated` : mapping et AppSense testés contrôle par contrôle ; -- `export-format-verified` : export, import isolé, doublon et rollback reproduits. +- `proposal-not-applied`: logical files and tools available, no real layer + claimed; +- `hardware-observed`: hardware environment inventoried; +- `manually-validated`: mapping and AppSense tested control by control; +- `export-format-verified`: export, isolated import, duplicate and rollback + reproduced. -Le preset Claude est `hardware-observed`. Il ne passera à -`manually-validated` qu'après la checklist complète des touches, de la molette, -du joystick, de la perte de focus, du redémarrage et du rollback. +The Claude preset is `hardware-observed`. It will only move to +`manually-validated` after the full checklist covering the keys, the wheel, the +joystick, focus loss, restart and rollback. diff --git a/docs/fr/codex-micro/configuration.md b/docs/fr/codex-micro/configuration.md new file mode 100644 index 0000000..bbb08da --- /dev/null +++ b/docs/fr/codex-micro/configuration.md @@ -0,0 +1,142 @@ +[English](../../codex-micro/configuration.md) · [Français](configuration.md) + +# Configurer le Codex Micro pour Claude Desktop + +## Flux cible + +1. exporter le profile Input actif et créer une sauvegarde vérifiée ; +2. inventorier les layers sans publier les données privées ; +3. protéger intégralement le layer natif à l'index `0` ; +4. exiger exactement un layer `Claude` existant, hors index `0` ; +5. conserver son lien AppSense dans une copie locale du profile ; +6. appliquer le mapping physique documenté à cette copie ; +7. tester chaque contrôle, la perte de focus et le redémarrage ; +8. exporter le layer, l'assainir et vérifier son round-trip ; +9. restaurer le profile original en cas d'écart. + +Cette intégration utilise les raccourcis HID et AppSense. Elle ne dépend pas du +protocole expérimental Hardware Buddy. + +Le manifeste et le mapping V1 décrivent l'état observé du générateur. Le fichier +logique `macos.example.json` reste `proposal-not-applied` et ne doit pas être +importé tel quel. `npm run configure` fabrique uniquement un profile personnel +à partir de l'export officiel de l'utilisateur. + +## Préservation obligatoire + +Le preset et l'outil imposent les règles suivantes : + +- index `0` protégé, sans remplacement ni modification ; +- politique `exactly-one-existing-named-layer` ; +- six emplacements au maximum ; +- aucun autre profile, layer ou lien AppSense modifié ; +- absence ou doublon `Claude` refusé ; +- structure d'export inconnue refusée au lieu d'être interprétée ; +- sauvegarde et vérification SHA-256 avant la session réelle ; +- aucune écriture directe dans le stockage Input ; +- aucune écriture de configuration sur le périphérique — keymap, layers et + couleurs de layer passent exclusivement par le flux de profils Input. + +La dernière règle portait jusqu'ici sur toute écriture vers le périphérique. +Elle est restreinte aux écritures **persistantes** : l'envoi de rapports HID +d'éclairage volatils est désormais dans le périmètre, sous six conditions +cumulatives énoncées dans +[`docs/scope-and-limitations.md`](../scope-and-limitations.md). Cette +configuration-ci n'en dépend pas et reste réalisable sans écrire une seule fois +sur le périphérique. + +L'inventaire bloque la transformation si l'export officiel ne permet pas de +prouver la présence du layer protégé à l'index `0` et d'un unique layer +`Claude`. + +## Mapping physique V1 + +Orientation : vue du dessus, câble à l'opposé de l'utilisateur. + +| Contrôle | Position | Action | +| --- | --- | --- | +| Touche 1 | rangée des quatre touches carrées, tout à gauche | `⌘N` — nouvelle conversation | +| Touche 2 | même rangée, deuxième | `⌘D` — mode vocal | +| Touche 3 | même rangée, troisième | `⌘⇧D` — afficher ou masquer le diff | +| Touche 4 | même rangée, tout à droite | `Esc` — annuler ou fermer selon le contexte | +| Molette cliquable | coin supérieur gauche | Effort Claude, horaire : `+1` ; antihoraire : `−1` ; clic configurable | +| Joystick sans clic | coin supérieur droit | flèches haut, droite, bas et gauche | + +![Schéma physique du mapping](../../../profiles/claude-shortcuts/assets/layout.svg) + +Les identifiants internes `inputControlId` restent `null` jusqu'à leur relevé +dans Input sur le Codex Micro exact. Les positions ci-dessus sont donc une cible +physique lisible, pas une affirmation sur le schéma interne de l'application. + +## Apparence + +- nom du layer : `Claude` ; +- couleur proposée : `#D97757` ; +- autres contrôles : `no-action` ; +- capteur tactile : réservé au changement de layer ; +- appui du cadran : aucune action. + +## AppSense + +Le lien cible uniquement : + +```text +Claude +com.anthropic.claudefordesktop +``` + +Le lien doit exister avant l'export du profile. Le générateur conserve son +`linkedAppId` dans la copie locale, refuse son absence et ne crée jamais un +second lien. Les autres liens ne sont jamais modifiés. + +**Il n'existe aucun retour automatique à un état sûr après perte de focus.** +Mesuré sur matériel : AppSense n'est qu'un ensemble de règles application → +layer, et chaque règle est une transition aller. Quitter Claude pour une +application non liée laisse la carte sur le layer Claude, indéfiniment. Le layer +Claude doit donc être conçu comme si ses raccourcis pouvaient rester actifs +ailleurs. Voir [`docs/research/appsense-behavior.md`](../research/appsense-behavior.md). + +## Actions absentes par défaut + +- Retour/Entrée et envoi d'un message ; +- approbation, refus ou rejet d'une permission ; +- suppression ; +- `git push` ; +- déploiement ; +- terminal, shell ou commande destructive ; +- raccourcis globaux Saisie rapide et Dictée. + +Les raccourcis globaux sont volontairement hors du layer AppSense : ils doivent +rester utilisables quand une autre application est au premier plan. Sur la +configuration documentée, il s'agit du double appui sur Option pour la saisie +rapide et de Verr. Maj. pour la dictée globale. + +## Partage officiel observé + +Input `0.17.2` contient les flux **Export layer**, **Import layer**, **Export +Profile** et **Import Profile**. Un export de layer porte le suffixe +`*-layer.json` et contient l'enveloppe décrite dans +[`docs/research/input-0.17.2-sharing.md`](../research/input-0.17.2-sharing.md). + +Le dépôt ne fabrique pas les objets internes `layer`, `actions` et groupes. Le +futur artefact doit provenir d'un vrai export du Codex Micro, être assaini, puis +réimporté sur une configuration isolée. + +## Validation restante + +- [ ] export du profile réel et inventaire lisible ; +- [ ] positions et identifiants physiques vérifiés dans Input ; +- [x] transformation locale du layer existant sans modification de l'index `0` ; +- [x] conservation du lien AppSense dans le profile généré ; +- [ ] quatre touches, cadran et joystick testés ; +- [ ] contrôles inutilisés confirmés sans action ; +- [x] comportement après perte de focus : établi, il n'y a pas de retour ; +- [ ] persistance après redémarrage ; +- [ ] export/import du layer reproduit sur une copie isolée ; +- [ ] doublon refusé ou traité idempotemment ; +- [ ] profile original réimporté et périphérique vérifié. + +## Sources + +- [Work Louder — Codex Micro, layers et AppSense](https://worklouder.cc/openai-micro-setup) +- [Claude — saisie rapide sur macOS](https://support.claude.com/en/articles/12626668-use-quick-entry-with-claude-desktop-on-mac) diff --git a/docs/fr/codex-micro/local-observation-2026-07-27.md b/docs/fr/codex-micro/local-observation-2026-07-27.md new file mode 100644 index 0000000..8a494c1 --- /dev/null +++ b/docs/fr/codex-micro/local-observation-2026-07-27.md @@ -0,0 +1,49 @@ +[English](../../codex-micro/local-observation-2026-07-27.md) · [Français](local-observation-2026-07-27.md) + +# Observation locale — 27 juillet 2026 + +Reconnaissance effectuée en lecture seule lors de l'initialisation précédente, +sans ouvrir les réglages système ni modifier Claude ou le périphérique. + +## Environnement observé + +- macOS `26.5.2` (`25F84`), Apple Silicon `arm64` ; +- Claude Desktop présent dans `/Applications/Claude.app` ; +- bundle `com.anthropic.claudefordesktop` ; +- version Claude `1.24012.9` ; +- Work Louder Input `0.17.2` ; +- firmware Codex Micro `v0.4.1`, observé dans l'écran Setup d'Input. + +## Codex Micro observé + +Le registre I/O macOS exposait un périphérique HID actif avec : + +- produit `Codex Micro #1` ; +- fabricant `Work Louder` ; +- transport `Bluetooth Low Energy` ; +- Vendor ID `12346` ; +- Product ID `33632` ; +- VersionNumber `24193`. + +Les identifiants uniques, l'adresse Bluetooth et le numéro de série sont +volontairement exclus. + +## Ce que cette observation prouve + +- le Codex Micro est visible par macOS ; +- une connexion BLE HID est disponible ; +- le clavier peut en principe émettre des raccourcis standards ; +- la combinaison Input/firmware observée est `0.17.2` / `v0.4.1`. + +## Ce qu'elle ne prouve pas + +- la révision commerciale exacte du matériel ; +- le caractère modifiable ou publiquement redistribuable du firmware ; +- la présence du Nordic UART Service attendu par Claude ; +- la possibilité d'annoncer un nom commençant par `Claude` ; +- la coexistence HID + Hardware Buddy ; +- l'activation du mode développeur Claude ; +- un échange réel de messages Hardware Buddy. + +`system_profiler` ne listait pas le périphérique pendant la reconnaissance. La +preuve provenait du registre HID I/O. Aucun scan GATT actif n'a été lancé. diff --git a/docs/fr/compatibility.md b/docs/fr/compatibility.md new file mode 100644 index 0000000..d352d22 --- /dev/null +++ b/docs/fr/compatibility.md @@ -0,0 +1,30 @@ +[English](../compatibility.md) · [Français](compatibility.md) + +# Matrice de compatibilité + +Cette matrice distingue l'environnement observé, l'analyse du mécanisme de +partage et les validations encore requises sur le matériel. + +| Élément | Version / cible | Niveau de preuve | Résultat | +| --- | --- | --- | --- | +| Matériel | Work Louder Codex Micro | `hardware-observed` | molette cliquable supérieure gauche, joystick sans clic supérieur droit et touches principales observés ; checklist complète en attente | +| Work Louder Input | `0.17.3`, bundle `it.focusense.input-app` | profile réel et générateur validés | transformation locale du profile validée ; round-trip layer séparé en attente | +| Mécanisme historique | Input `0.17.2` | bundle officiel inspecté | import/export layer et profile observé statiquement | +| Firmware | `v0.4.1` | écran Setup observé localement | combinaison Input/firmware connue ; validation matérielle partielle | +| macOS | `26.5.2` arm64 | observation locale | application et HID observés | +| Claude Desktop | `1.24012.9`, bundle `com.anthropic.claudefordesktop` | bundle local et profil généré | `⌘N`, `⌘D`, `⌘⇧D` configurés ; `Esc` à valider contextuellement | +| AppSense | lien existant du layer Claude | export réel observé | `linkedAppId` conservé localement et interdit dans les artefacts publics ; perte de focus à tester | +| Sauvegarde CLI | Node.js `>=18` | tests automatisés | copie, manifeste SHA-256 et restauration testés sur une copie isolée | +| Artefact layer | `*-layer.json` | absent | doit provenir d'un export réel assaini et subir un round-trip | + +## Interprétation + +- `proposal-not-applied` : fichiers logiques et outils disponibles, aucun layer + réel revendiqué ; +- `hardware-observed` : environnement matériel inventorié ; +- `manually-validated` : mapping et AppSense testés contrôle par contrôle ; +- `export-format-verified` : export, import isolé, doublon et rollback reproduits. + +Le preset Claude est `hardware-observed`. Il ne passera à +`manually-validated` qu'après la checklist complète des touches, de la molette, +du joystick, de la perte de focus, du redémarrage et du rollback. diff --git a/docs/fr/getting-started.md b/docs/fr/getting-started.md new file mode 100644 index 0000000..8c823ef --- /dev/null +++ b/docs/fr/getting-started.md @@ -0,0 +1,122 @@ +[English](../getting-started.md) · [Français](getting-started.md) + +# Guide de démarrage + +Ce guide ne modifie pas Input tant que `--apply` n'est pas utilisé. Même avec +`--apply`, l'outil ne clique pas dans l'interface et ne patche pas directement +la configuration d'Input. + +## 1. Contrôler le dépôt + +```sh +git status --short +npm ci --no-audit --no-fund +npm run check +``` + +Préserver toute modification sans rapport. Les données locales vont sous +`.local/`. + +## 2. Sonde générale en lecture seule + +```sh +./scripts/probe-macos.sh +node scripts/input-layer.mjs doctor --json +``` + +La première commande lit les versions et propriétés HID non uniques. La seconde +cherche Input, Claude et les emplacements de configuration candidats. + +Comparer avec +[`local-observation-2026-07-27.md`](codex-micro/local-observation-2026-07-27.md). + +## 3. Comprendre les fichiers V1 + +- [`manifest.json`](../../profiles/claude-shortcuts/manifest.json) : compatibilité, + preuve, préservation et installation ; +- [`mapping.json`](../../profiles/claude-shortcuts/mapping.json) : positions, + raccourcis, couleur et AppSense ; +- [`layout.svg`](../../profiles/claude-shortcuts/assets/layout.svg) : aperçu ; +- [`macos.example.json`](../../profiles/claude-shortcuts/macos.example.json) : + contrat logique historique aligné sur V1. + +Le manifeste communautaire n'est pas un export Input. Le parcours principal +transforme localement un export officiel `*-profile.json` d'Input `0.17.3`. +L'étude du format `*-layer.json` d'Input `0.17.2` reste une preuve historique. + +## 4. Exporter et sauvegarder avant toute modification + +Dans Input : + +1. vérifier qu'il existe exactement un layer `Claude`, hors index `0` ; +2. vérifier que ce layer est déjà lié à Claude Desktop avec AppSense ; +3. inventorier visuellement les autres profils, layers et liens ; +4. utiliser **Export Profile** ; +5. quitter Input. + +Puis : + +```sh +node scripts/input-layer.mjs backup \ + --profile-export "$HOME/Downloads/Mac-profile.json" \ + --json +``` + +La sauvegarde doit être vérifiée avant l'installation. + +## 5. Inventorier et simuler + +```sh +node scripts/input-layer.mjs inventory \ + --profile-export "$HOME/Downloads/Mac-profile.json" \ + --output .local/inventories/current.json \ + --json + +node scripts/input-layer.mjs install \ + --inventory .local/inventories/current.json \ + --profile-export "$HOME/Downloads/Mac-profile.json" \ + --dry-run \ + --json +``` + +Le plan doit protéger l'index `0`, sélectionner exactement le layer `Claude` +existant et refuser son absence ou sa duplication. + +## 6. Installation guidée + +Lire [`installation.md`](installation.md), puis seulement après revue : + +```sh +node scripts/input-layer.mjs install \ + --apply \ + --inventory .local/inventories/current.json \ + --profile-export "$HOME/Downloads/Mac-profile.json" \ + --open-input \ + --json +``` + +Générer ensuite le nouveau profile sans modifier la sauvegarde source : + +```sh +npm run build:profile -- \ + "$HOME/Downloads/Mac-profile.json" \ + "$HOME/Downloads/Claude-macOS-profile.json" +``` + +Importer `Claude-macOS-profile.json` avec **Add New**. Ne jamais utiliser +`Reset settings`, remplacer l'index `0` ou recréer un lien AppSense. + +## 7. Validation et retour arrière + +Tester AppSense, les quatre touches, le cadran, le joystick, la perte de focus +et le redémarrage. L'export de layer reste une validation de publication +optionnelle, distincte du profile généré. + +Le rollback principal consiste à réimporter le `*-profile.json` original dans +Input. La copie brute du stockage n'est qu'un recours secondaire explicite. + +## 8. Piste BLE séparée + +Lire [`ble/protocol.md`](../../ble/protocol.md) et +[`ble/feasibility.md`](../../ble/feasibility.md). Aucun résultat du preset HID ne +prouve la compatibilité Hardware Buddy. diff --git a/docs/fr/installation.md b/docs/fr/installation.md new file mode 100644 index 0000000..fe27475 --- /dev/null +++ b/docs/fr/installation.md @@ -0,0 +1,371 @@ +[English](../installation.md) · [Français](installation.md) + +# Générer et installer le profile Claude V1 + +Le parcours V1 transforme localement un export officiel d'Input `0.17.3`. Le +profile source doit contenir exactement un layer `Claude`, hors index `0` et +déjà lié à Claude Desktop avec AppSense. Le générateur modifie uniquement ce +layer dans une copie et produit un nouveau `*-profile.json`. + +Il ne clique jamais à votre place, ne lance pas `Reset settings`, ne flashe +aucun firmware et ne modifie aucun raccourci système. + +## Parcours GUI recommandé + +Depuis la racine du dépôt : + +```sh +npm run configure +``` + +La première ouverture peut préparer les dépendances verrouillées de +`prototype/`. Le configurateur s'ouvre ensuite localement dans le navigateur. + +1. exporter le profile actif depuis Work Louder Input ; +2. déposer ce `*-profile.json` dans le configurateur ; +3. vérifier l'unique layer `Claude`, son lien AppSense et l'index natif `0` ; +4. personnaliser les 13 switches, la rotation du cadran et le joystick ; seul + le capteur tactile de changement de layer reste réservé ; +5. télécharger `Claude-macOS-profile.json` ; +6. l'importer avec **Add New**, sans remplacer le profile source. + +La génération refuse un mauvais appareil, un layer Claude absent ou dupliqué, +une cible à l'index `0` et un lien AppSense manquant. Elle conserve le layer +natif, les autres layers, les autres profils et les autres liens AppSense. Le +reste de ce guide décrit le même parcours avec les contrôles CLI détaillés. + +## 1. Vérifier le dépôt + +Depuis la copie locale : + +```sh +cd claude-codex-micro +git status --short +npm ci --no-audit --no-fund +npm run check +``` + +Ne mettez pas de côté et ne supprimez pas les modifications sans rapport. Les +fichiers privés créés par les outils restent sous `.local/`, ignoré par Git. + +## 2. Inspecter l'environnement sans écriture + +```sh +node scripts/input-layer.mjs doctor --json +``` + +Vérifier notamment : + +- Input `0.17.3`, bundle `it.focusense.input-app` ; +- Claude, bundle `com.anthropic.claudefordesktop` ; +- le chemin de configuration Input détecté ; +- le firmware `v0.4.1` dans l'écran Setup d'Input ; +- le layer Codex natif à l'index `0`. + +`doctor` ne modifie rien. Si l'installation utilise un chemin personnalisé, +définir d'abord `WORK_LOUDER_INPUT_USER_DATA` sur ce chemin. Un +`--config-root` arbitraire est refusé. + +## 3. Exporter le profile original avec Input + +Cette étape est la sauvegarde de référence pour le keymap du périphérique. + +1. ouvrir Input et sélectionner le profile actuellement actif ; +2. vérifier qu'il existe exactement un layer `Claude`, hors index `0`, et qu'il + possède déjà le lien AppSense Claude ; +3. inventorier visuellement les autres profils, layers et liens ; +4. utiliser le menu du profile puis **Export Profile** ; +5. conserver le fichier `*-profile.json` dans un emplacement local ; +6. quitter complètement Input avant de copier sa configuration locale. + +Le profil exporté peut contenir des informations privées. Ne jamais le committer. + +## 4. Créer et vérifier la sauvegarde restaurable + +Exemple : + +```sh +node scripts/input-layer.mjs backup \ + --profile-export "$HOME/Downloads/Mac-profile.json" \ + --input-version 0.17.3 \ + --firmware-version v0.4.1 \ + --json +``` + +La commande : + +- refuse de continuer si Input tourne ; +- copie la configuration reconnue vers `.local/input-backups//` ; +- copie l'export officiel du profile ; +- exclut uniquement les caches jetables ; +- calcule une somme SHA-256 pour chaque fichier ; +- relit immédiatement la sauvegarde. + +Vérification indépendante : + +```sh +node scripts/input-layer.mjs verify-backup \ + --backup .local/input-backups/ \ + --json +``` + +Le résultat doit contenir `"ok": true`. + +## 5. Générer l'inventaire local + +```sh +node scripts/input-layer.mjs inventory \ + --profile-export "$HOME/Downloads/Mac-profile.json" \ + --output .local/inventories/current.json \ + --json +``` + +Contrôler dans le résultat : + +- `protectedLayerIndexes: [0]` ; +- le nom du layer natif à l'index `0` ; +- exactement un layer nommé `Claude`, avec un index supérieur à `0` ; +- la présence d'un champ AppSense candidat sur ce layer. + +L'outil ne publie pas les valeurs AppSense trouvées ; il signale uniquement les +chemins de champs candidats. L'inventaire détaillé reste local. + +## 6. Simuler l'installation + +```sh +node scripts/input-layer.mjs install \ + --inventory .local/inventories/current.json \ + --profile-export "$HOME/Downloads/Mac-profile.json" \ + --dry-run \ + --json +``` + +Le dry-run doit : + +- sélectionner l'unique layer `Claude` existant ; +- protéger l'index `0` ; +- refuser l'absence ou la duplication de `Claude` ; +- annoncer `guided-ui` et la transformation locale du profile. + +## 7. Préparer la session réelle + +Quitter Input, puis : + +```sh +node scripts/input-layer.mjs install \ + --apply \ + --inventory .local/inventories/current.json \ + --profile-export "$HOME/Downloads/Mac-profile.json" \ + --open-input \ + --json +``` + +Avant d'ouvrir Input, la commande crée et vérifie une nouvelle sauvegarde de +sécurité. Elle écrit ensuite un état de session sous `.local/sessions/`. Un +second lancement non restauré est refusé. + +## 8. Générer le profile local + +Input fermé, exécuter : + +```sh +npm run build:profile -- \ + "$HOME/Downloads/Mac-profile.json" \ + "$HOME/Downloads/Claude-macOS-profile.json" +``` + +Le fichier de sortie est créé sans écraser un fichier existant. Le générateur +refuse un mauvais appareil, un layer Claude absent ou dupliqué, un layer cible +à l'index `0` et un lien AppSense manquant. + +Le mapping généré est : + +| Position physique | Action | +| --- | --- | +| rangée des quatre touches carrées, gauche | `⌘N` | +| même rangée, deuxième | `⌘D` | +| même rangée, troisième | `⌘⇧D` | +| même rangée, droite | `Esc` | +| molette supérieure gauche, horaire / antihoraire | **Effort Claude** `+1` / `−1` | +| clic de la molette supérieure gauche | configurable séparément | +| joystick supérieur droit, sans clic | quatre flèches | + +La rotation de la molette est en mode **Effort Claude** par défaut. Chaque cran +envoie `⌘⇧E`, attend l'ouverture du sélecteur, puis `←` ou `→` et `Esc`. Il passe +donc au niveau d'effort disponible précédent ou suivant sans utiliser `Entrée`. +Les niveaux réellement proposés dépendent du modèle et de la version de Claude +Desktop. + +Le GUI permet de la remettre sur le défilement page par page, le défilement ligne +par ligne, le volume, ou de la désassigner. + +`⌘⇧E` est une bascule : le `Esc` final est obligatoire, sans lui le cran suivant +refermerait le sélecteur au lieu de l'ouvrir. + +La macro porte donc deux temporisations, pour un coût d'environ 90 ms par cran : + +- **80 ms** sur la libération de ⌘, le temps que le sélecteur apparaisse. En + dessous de 40 ms la flèche part avant que le sélecteur ait le focus et le + changement de niveau est perdu sans message d'erreur ; +- **10 ms** sur `Esc`, le temps que le sélecteur peigne le niveau atteint avant + de se refermer. Sans elle le sélecteur ne fait que clignoter et le niveau + choisi n'est jamais affiché. + +Ces temporisations sont exécutées par le firmware, et tourner vite pendant qu'une +macro est en cours peut faire perdre des crans. Le mode convient à des +ajustements de quelques niveaux, pas à un balayage continu. + +Le calibrage et ce qui reste non prouvé sont détaillés dans +[`docs/research/effort-wheel-calibration.md`](research/effort-wheel-calibration.md). + +Le schéma de référence est +[`profiles/claude-shortcuts/assets/layout.svg`](../../profiles/claude-shortcuts/assets/layout.svg). + +## 9. Importer sans recréer AppSense + +1. dans Input, choisir **Add New** ; +2. sélectionner `Claude-macOS-profile.json` ; +3. activer le nouveau profile ; +4. vérifier que le layer `Claude` conserve son lien AppSense ; +5. ne toucher à aucun autre lien AppSense. + +Le générateur conserve l'identifiant AppSense local sans le publier. Si le lien +est absent, revenir au profile original et le créer manuellement avant un +nouvel export. + +### Forcer un lien, ou en ajouter un second + +Deux options écrivent une référence AppSense au lieu de seulement reprendre celle +de la sauvegarde : + +```bash +node scripts/build-input-profile.mjs sauvegarde.json sortie.json --app-sense-id=0 --base-layer-app-sense-id=2 +``` + +`--app-sense-id=` force la référence du layer `Claude` et dispense d'en exiger +une dans la sauvegarde : c'est le cas d'usage « réparer un lien perdu ». + +`--base-layer-app-sense-id=` lie le layer natif à une **seconde** application. +C'est le seul moyen de quitter automatiquement le layer `Claude`, puisqu'AppSense +n'a pas de retour : la sortie est elle-même une entrée dans un autre layer lié. +Le keymap natif reste intact au keycode près, seul le lien est ajouté, et le +générateur refuse que les deux layers pointent vers la même entrée. + +**Ces options écrivent une référence, jamais une entrée.** Un fichier +`*-profile.json` ne transporte pas la table `linkedApps` : l'entrée visée doit +déjà exister sur la carte, créée une fois dans l'UI d'Input avec `Auto detect`. +Une référence vers une entrée absente s'importe **sans erreur** et laisse +AppSense mort sans le signaler. Relever les identifiants réels avant, dans +`~/Library/Logs/input/main.log`, où `sending device config :` est suivi du JSON +complet — et n'exécuter `Auto detect` qu'une seule fois par application, il ne +dédoublonne pas. + +Le GUI expose les deux mêmes réglages dans l'étape **Vérifier et générer**, section +« Liens AppSense ». Laisser les champs vides revient à ne pas passer les options : +les liens de la sauvegarde sont alors repris tels quels. + +Détail du comportement mesuré : +[`docs/research/appsense-behavior.md`](research/appsense-behavior.md). + +## 10. Validation matérielle + +Tester dans une conversation sans enjeu, une action à la fois : + +- [ ] Claude au premier plan active le layer existant, pas l'index `0` ; +- [ ] `⌘N` ouvre une nouvelle conversation ; +- [ ] `⌘D` active le mode vocal ; +- [ ] `⌘⇧D` affiche ou masque le diff ; +- [ ] `Esc` annule ou ferme uniquement le contexte attendu ; +- [ ] cadran horaire descend et antihoraire monte ; +- [ ] en mode Effort Claude, chaque cran change d'un seul niveau et referme le + sélecteur sans envoyer de prompt ; +- [ ] en mode Effort Claude, le niveau atteint est lisible avant la fermeture, et + un cran déclenché au retour d'une autre application le change bien : c'est + le cas défavorable, où l'échec est silencieux ; +- [ ] joystick émet les quatre flèches ; +- [ ] tous les contrôles non utilisés restent sans action dangereuse ; +- [ ] revenir dans Claude réactive le layer ; +- [ ] passer au Finder **laisse** le layer Claude actif — c'est le comportement + attendu, pas un défaut : AppSense n'a pas de retour, voir + [`docs/research/appsense-behavior.md`](research/appsense-behavior.md). + Vérifier plutôt qu'aucune action du layer n'est dangereuse hors de Claude ; +- [ ] quitter puis relancer Input conserve la configuration ; +- [ ] aucun autre profile, layer ou lien AppSense n'a changé. + +Ne tester ni Entrée, ni permission, ni suppression, ni push, ni déploiement. + +## 11. Capturer un éventuel artefact layer partageable + +Après validation, utiliser **Export layer** dans Input, puis : + +```sh +node scripts/input-layer.mjs sanitize-export \ + --input "$HOME/Downloads/Claude-layer.json" \ + --output profiles/claude-shortcuts/artifacts/claude-desktop-macos-layer.json +``` + +Le sanitizer refuse les chemins absolus, ports, adresses matérielles, secrets, +identifiants AppSense et appareils autres que `codex_micro`. Après sanitation, +enregistrer le SHA-256 exact dans `manifest.json`. Le validateur compare aussi +les actions, la molette et le joystick au mapping canonique. + +Avant de promouvoir le statut : importer cette copie dans une configuration +isolée, vérifier le mapping, tenter un second import et restaurer le profile +original. + +## 12. Retour arrière + +Simulation : + +```sh +node scripts/input-layer.mjs rollback \ + --backup .local/input-backups/ \ + --dry-run \ + --json +``` + +Méthode principale : + +1. ouvrir Input ; +2. utiliser **Import Profile** ; +3. sélectionner le `*-profile.json` dans le dossier + `official-profile-export` de la sauvegarde ; +4. remettre ce profile comme profile courant ; +5. vérifier le layer Codex, chaque autre layer et chaque lien AppSense ; +6. relancer Input et refaire le contrôle. + +Après le réimport officiel et la vérification du périphérique, la session peut +être marquée comme restaurée afin qu'une future installation ne soit plus +bloquée par l'état précédent : + +```sh +node scripts/input-layer.mjs rollback \ + --backup .local/input-backups/ \ + --session .local/sessions/claude-desktop-macos-codex-micro-v1.json \ + --apply \ + --confirm-official-profile-import \ + --json +``` + +Cette option enregistre une confirmation de l'utilisateur ; elle ne remplace +pas la vérification matérielle. + +La restauration brute du stockage applicatif reste un recours secondaire et +non une preuve de restauration du périphérique. Elle exige volontairement les +options explicites suivantes, Input fermé. `--config-root` doit correspondre +exactement à un chemin Input détecté ou à `WORK_LOUDER_INPUT_USER_DATA` : + +```sh +node scripts/input-layer.mjs rollback \ + --backup .local/input-backups/ \ + --apply \ + --restore-storage \ + --acknowledge-unverified-storage-restore \ + --session .local/sessions/claude-desktop-macos-codex-micro-v1.json \ + --json +``` + +Avant de recopier la sauvegarde, l'outil renomme atomiquement le dossier Input +courant en copie de sécurité `*.before-codex-restore-*`. En cas d'échec de la +copie, ce dossier est remis en place. Cette voie reste secondaire : ne jamais +utiliser `Reset settings` pour le retour arrière. diff --git a/docs/fr/publishing-checklist.md b/docs/fr/publishing-checklist.md new file mode 100644 index 0000000..6722344 --- /dev/null +++ b/docs/fr/publishing-checklist.md @@ -0,0 +1,59 @@ +[English](../publishing-checklist.md) · [Français](publishing-checklist.md) + +# Checklist de publication + +Le dépôt public et la publication d'un preset installable sont deux décisions +distinctes. Une proposition peut être utile et testée automatiquement sans être +présentée comme un layer matériel validé. + +## Dépôt public — effectué + +- [x] Licence MIT et copyright 2026 Thanh Chau. +- [x] Absence d'affiliation à Work Louder et Anthropic. +- [x] Aucun firmware, asset propriétaire ou export utilisateur brut. +- [x] `main` comme branche par défaut. +- [x] Modèles GitHub de contribution. + +## Contrôles applicables à chaque contribution + +- [ ] `npm run check` réussi. +- [ ] `npm ci --no-audit --no-fund` utilise le lockfile sans modifier l'arbre. +- [ ] `git diff --check` réussi. +- [ ] Sources externes vérifiées manuellement. +- [ ] Niveau de preuve exact dans le manifeste et le README. +- [ ] Layer natif à l'index `0` explicitement protégé. +- [ ] Aucun envoi, permission, suppression, push, déploiement ou commande + destructive mappé par défaut. +- [ ] BLE toujours indiqué comme non prouvé sans preuves GATT et firmware. + +## Confidentialité + +- [ ] Aucun secret, jeton, clé ou identifiant de compte. +- [ ] Aucune adresse Bluetooth, numéro de série, port ou identifiant matériel. +- [ ] Aucun chemin utilisateur absolu. +- [ ] Aucune capture ou log contenant des données personnelles. +- [ ] `.local/`, `work/` et `outputs/` ignorés par Git. +- [ ] Tout `*-layer.json` passé par `sanitize-export` et revu manuellement. + +## Portes du preset Claude V1 + +- [x] Flux officiels Import/Export layer et profile identifiés dans Input + `0.17.2`. +- [x] Manifeste, mapping, schémas et représentation visuelle publiables. +- [x] Sauvegarde, vérification SHA-256, dry-run, sélection unique et rollback + testés sur copies isolées. +- [x] Configuration locale réelle inventoriée sans publier les valeurs AppSense. +- [x] Export officiel du profile d'origine conservé hors Git. +- [x] Unique layer Claude confirmé ; index `0` comparé avant/après. +- [x] Profile Input `0.17.3` généré localement avec AppSense conservé. +- [ ] Positions physiques et identifiants Input vérifiés. +- [ ] AppSense, touches, cadran, joystick et perte de focus testés. +- [ ] Persistance après redémarrage d'Input vérifiée. +- [ ] Vrai `*-layer.json` exporté, assaini et ajouté avec sa somme SHA-256. +- [ ] SHA-256 déclaré identique et contenu conforme au mapping canonique. +- [ ] Import dans une configuration isolée et second import testés. +- [ ] Profile original réimporté et périphérique vérifié. +- [ ] Matrice de compatibilité mise à jour avec les résultats. + +Tant que les cases matérielles restent ouvertes, le preset conserve le statut +`hardware-observed` et ne peut pas être présenté comme entièrement validé. diff --git a/docs/fr/research/appsense-behavior.md b/docs/fr/research/appsense-behavior.md new file mode 100644 index 0000000..2cbd209 --- /dev/null +++ b/docs/fr/research/appsense-behavior.md @@ -0,0 +1,135 @@ +[English](../../research/appsense-behavior.md) · [Français](appsense-behavior.md) + +# AppSense — comportement réel mesuré sur Codex Micro + +## Verdict + +**AppSense n'a pas de retour.** C'est un ensemble de règles application → layer, +et chaque règle est une transition **aller**. Il n'existe ni layer par défaut, ni +repli, ni désactivation quand l'application liée perd le focus. + +Conséquence à retenir avant de concevoir un layer : quitter Claude pour une +application non liée laisse la carte sur le layer Claude, indéfiniment. Le layer +doit donc être sûr en dehors de Claude, puisqu'il y restera actif. + +## Le modèle, et ce qu'il explique + +| application au premier plan | règle | effet | +| --- | --- | --- | +| liée à un layer | trouvée | bascule vers ce layer | +| non liée | aucune | **rien ne se passe, la carte reste où elle est** | + +Un « aller-retour » entre deux applications n'est donc pas un aller suivi d'un +retour : c'est **deux allers**, qui exigent que les deux applications soient +liées chacune à son layer. Le layer d'indice `0` peut parfaitement être une +cible, contrairement à ce qu'on pourrait croire — mais seulement si une +application lui est explicitement liée. + +Mesures qui établissent le modèle, sur firmware `v0.4.1` et Input `0.17.3` : + +- depuis le layer de base, mettre Claude au premier plan bascule bien vers le + layer `Claude` — l'aller fonctionne ; +- avec `com.openai.codex` lié au layer de base, alterner Claude et ChatGPT fait + bien alterner les deux layers ; +- avec la même configuration, quitter Claude pour le Finder ne change **rien** : + le layer `Claude` reste actif. + +## Limite pratique + +Six layers au maximum, et un seul `linkedAppId` par layer : au plus **six +applications** peuvent déclencher une bascule. Toute autre application laisse la +carte sur le dernier layer activé. + +Pour un poste où l'on navigue entre plus d'applications que ça, il n'y a que le +capteur tactile, qui fait défiler les layers à la main. + +## Statut chez le fabricant + +Le repli attendu est une **fonctionnalité absente, pas un bug**. Elle est +demandée sur le board de feedback Work Louder en statut `Planned`, sans ETA, et +un administrateur l'a confirmé : + +> nous prévoyons de l'implémenter mais nous n'avons pas encore d'ETA + +Aucune note de version d'Input, de `0.11.0` à `0.18.0-rc.8`, ne mentionne +AppSense, le focus applicatif ou le changement de layer. Mettre Input à jour ne +change donc rien à ce comportement. + +Sources : +et . + +## Mécanique côté hôte, utile au diagnostic + +**AppSense est piloté par l'hôte.** Input observe l'application au premier plan +et pousse un appel JSON-RPC `host.focused_app` vers la carte ; le firmware +consulte alors sa table de liens et bascule. Deux conséquences : + +- **AppSense s'arrête net si Input n'est pas lancé.** Aucune bascule n'a plus + lieu, et la carte se figera sur son dernier layer. « Fermer Input » est donc le + pire contournement possible. +- La détection se fait par **sondage à 1000 ms**, via `osascript`, pas par + abonnement système. Une bascule peut donc prendre jusqu'à une seconde : ne pas + conclure trop vite lors d'un test. + +L'envoi est **inconditionnel** — Input ne consulte pas la table des liens avant +d'émettre, tout le filtrage est côté firmware — et il n'existe aucun message +signifiant « aucune application liée ». Le firmware ne renvoie jamais sur quel +layer il a basculé : la réponse à `host.focused_app` est toujours `null`. + +## Pièges rencontrés pendant l'investigation + +- **`Auto detect` ne dédoublonne pas par processus.** Chaque exécution crée une + nouvelle entrée `linkedApps`. Deux entrées pour la même application, et deux + layers les revendiquant dans deux profils différents, rendent le diagnostic + illisible. N'exécuter `Auto detect` qu'une fois par application. +- **Un fichier `*-profile.json` exporté ne transporte pas la table + `linkedApps`**, seulement les références `linkedAppId` posées sur les layers. + Un profil importé ne peut donc pas créer un lien : l'entrée cible doit déjà + exister, sinon la référence pend et le lien est silencieusement mort. +- **La copie locale `~/Library/Application Support/input/devices//keymap.json` + peut être en retard** sur ce qui a réellement été poussé. La source fiable est + `~/Library/Logs/input/main.log`, où `|device_keymap_service| sending device + config :` est suivi du JSON complet. +- **`device.status.layer_index` est 1-based**, Input le convertit par `r - 1`. + Un `layer_index: 2` désigne le layer d'indice `1`. +- Les erreurs `cannot send, no device connected` accompagnant chaque changement + de focus sont présentes dès le démarrage : Input instancie un client par + transport et seul celui du transport réel répond. Ce n'est pas la panne. + +## Contention avec l'application ChatGPT + +Les deux applications tiennent le même périphérique HID, ouvert en mode non +exclusif : les lectures sont diffusées aux deux, seules les écritures se +disputent, et **la dernière écriture gagne**. On le voit directement dans le log +d'Input, qui reçoit les réponses à des appels `v.oai.rgbcfg` et `v.oai.thstatus` +qu'il n'a jamais émis. + +Conséquence à connaître pour tout témoin visuel : **ChatGPT écrase l'underglow +des layers non-Codex** quel que soit le layer actif — bug confirmé, non corrigé +sur ce modèle. Le **backlight** est la zone qu'il laisse tranquille, donc le seul +indicateur de layer fiable tant que ChatGPT tourne. + +Ce que ChatGPT ne fait pas : il ne repositionne jamais le layer. Il n'est donc +pas la cause du layer collé. + +## Avertissement + +**Ne flasher aucun firmware depuis l'écran de récupération d'Input.** Il propose +Nomad, Knob, KnobF1, Creator Micro V2 et XYZ R2, n'offre **pas** de firmware +Codex Micro et **n'avertit pas** de l'incompatibilité. Un Codex Micro a été +briqué exactement comme ça, et aucun firmware de récupération officiel n'est +publié. + +Source : + +## Ce qui reste non établi + +- L'ordre dans lequel le firmware parcourt sa table de liens, et s'il cherche + dans le profil actif seulement ou dans tous les profils. Pendant + l'investigation, une configuration dont le layer de base référençait un + `linkedAppId` inexistant a semblé fonctionner là où une référence valide + échouait. L'écart n'a pas été reproduit et est vraisemblablement un artefact de + séquence de test, mais il n'est pas expliqué. +- La raison du refus d'import d'un profil à trois layers : rien n'est journalisé + côté processus principal, le motif est dans le log du renderer, accessible par + `Help > Download Logs`. diff --git a/docs/fr/research/effort-wheel-calibration.md b/docs/fr/research/effort-wheel-calibration.md new file mode 100644 index 0000000..8200061 --- /dev/null +++ b/docs/fr/research/effort-wheel-calibration.md @@ -0,0 +1,144 @@ +[English](../../research/effort-wheel-calibration.md) · [Français](effort-wheel-calibration.md) + +# Molette Effort Claude — calibrage de la macro + +## Verdict + +La macro du mode Effort porte deux temporisations, pour environ **90 ms par +cran** : `80 ms` sur la libération de ⌘ et `10 ms` sur `Esc`. L'étape de la +flèche reste volontairement à `0`. + +La première version de cette macro coûtait 900 ms par cran. Le facteur dix ne +vient pas d'avoir essayé plus de valeurs, mais d'avoir changé la **forme** de la +macro pour que chaque mesure devienne interprétable. + +## Pourquoi `Esc` est obligatoire + +`⌘⇧E` est une **bascule**, vérifié sur Claude Desktop : l'envoyer deux fois de +suite ouvre puis referme le sélecteur. + +Chaque cran doit donc refermer le sélecteur lui-même. Sans le `Esc` final, le +cran suivant refermerait le sélecteur au lieu de l'ouvrir, et le niveau serait +sauté. C'est aussi la cause la plus probable des niveaux perdus lors d'un +balayage rapide : deux macros qui se chevauchent désynchronisent la bascule. + +## Pourquoi la forme est asymétrique + +Input ne documente pas si le champ `delay` d'une étape s'applique **avant** ou +**après** cette étape, et aucune source ne permet de le trancher : l'application +n'exécute jamais les macros, elle écrit `keymap.json` sur la flash de la carte et +le firmware seul interprète le champ. + +Tant que l'attente était répartie sur deux étapes, les deux lectures donnaient au +sélecteur des durées différentes, et chaque essai mesurait donc autre chose que +ce qu'on croyait régler. En posant toute l'attente sur la libération de ⌘ et `0` +sur la flèche, les deux lectures deviennent équivalentes : + +| lecture | déroulé | attente reçue par le sélecteur | +| --- | --- | --- | +| « après » | ⌘ relâché, attente, flèche | la constante | +| « avant » | attente, ⌘ relâché, flèche | la constante | + +Le délai d'ouverture devient alors exactement égal à la constante. Ne pas +répartir cette attente sur les deux étapes : cela double ce délai sans rien +garantir de plus. Le coût total d'un cran inclut aussi les `10 ms` de retour +visuel portés par `Esc`. + +## Calibrage mesuré sur matériel + +Échelle testée sur Codex Micro, une valeur par touche, toutes en « effort +1 » +pour que le sens ne soit pas une variable : + +| attente d'ouverture | résultat | +| --- | --- | +| 120 ms | change le niveau | +| 80 ms | change le niveau | +| 60 ms | change le niveau | +| 40 ms | change le niveau | +| 20 ms | échoue | +| 0 ms | échoue | + +Le plancher est donc entre 20 et 40 ms. La valeur retenue, `80 ms`, double le +plancher mesuré. + +`40 ms` a été essayé en exploitation et jugé moins fiable qu'en test isolé. C'est +cohérent : le plancher a été mesuré sur un sélecteur déjà chaud, alors que le +temps de montage réel dépend de la charge du renderer. **Un plancher n'est pas +une valeur d'exploitation.** En dessous d'environ 100 ms la différence de latence +n'est pas perceptible, alors qu'un niveau perdu l'est immédiatement : la bonne +cible est la plus petite valeur qui ne rate jamais, pas la plus petite qui +marche. + +L'échec en dessous du plancher n'est pas bruyant. La flèche part avant que le +sélecteur ait le focus, et le changement est perdu sans message d'erreur. Toute +baisse doit donc être validée par plusieurs répétitions **et** par une première +ouverture à froid, au retour d'une autre application. + +## Pourquoi `Esc` porte 10 ms + +Sans attente sur cette étape, la flèche et `Esc` sont émis sans écart et Claude +les traite dans le même tour de boucle : le sélecteur s'ouvre et se referme sans +jamais peindre une image montrant le slider à son nouveau niveau. Le niveau +change bien, mais **à l'aveugle** — l'effet visible n'est qu'un clignotement. + +`10 ms` suffisent à laisser passer une image, et le niveau atteint devient +lisible. Cette attente est payée **après** que le niveau a changé : elle allonge +la macro sans retarder son effet. + +Conséquence méthodologique : les 300 ms que portait la première version de la +macro à cet endroit n'étaient pas du temps mort. Elles avaient été supprimées sur +le seul critère de la latence, ce qui a fait perdre le retour visuel sans que le +critère retenu puisse le détecter. + +## Sens de rotation + +La cellule d'encodeur d'indice `0` est **physiquement horaire**, confirmé sur +matériel. C'est l'inverse de ce que suggèrent les noms du gabarit d'usine, qui +nomme les trois cellules `KV_OAI_ENC_CC`, `KV_OAI_ENC_CW`, `KV_OAI_ENC_CLK`. + +Deux inversions se superposent, ce qui rend l'erreur facile : + +- le firmware délivre les deux événements de rotation permutés par rapport aux + noms de cellules du vendeur ; +- Input 0.17.3 permute en plus les libellés `CW` et `CCW` de son éditeur pour + tout encodeur à trois cellules, si bien que son interface contredit les noms de + keycodes de son propre gabarit par défaut. + +Le générateur écrit le JSON directement et contourne donc le second point. Ne pas +aligner `PHYSICAL_ENCODER_SLOTS` sur ce qu'affiche l'éditeur, ni sur les noms de +keycodes : cela inverse la molette. + +## Pourquoi le regroupement des crans a été écarté + +Faire coûter un seul cycle `⌘⇧E` / `Esc` à N crans demande trois choses : un +compteur persistant, une temporisation non bloquante, et l'émission de frappes +clavier. + +Le SDK MicroPython embarqué fournit les deux premières — un compteur de crans via +`EVENT.ENCODER`, et une temporisation approchée via le hook de frame — mais +**aucune API d'émission de frappes**. Ce SDK n'est de plus pas disponible sur le +Codex Micro : il est réservé au Nomad [E] v1, et Input ne présente pas d'onglet +Widgets pour ce modèle. Le format `keymap.json` n'offre pas d'alternative : ni +compteur, ni condition, ni bascule, le seul état retenu par le firmware étant le +layer et le profil actifs. + +Le regroupement exige donc un agent sur l'hôte. À 900 ms par cran il se +justifiait largement ; à 90 ms, cinq crans coûtent 450 ms contre environ 300 ms +pour un agent qui les regroupe, et l'écart ne paie plus un démon ni une +autorisation d'accessibilité. + +## Ce qui reste non prouvé + +- **La sémantique de `delay`.** Le comportement observé sur l'étape `Esc` indique + qu'elle s'applique avant son étape, puisque dans la lecture « après » ces 10 ms + seraient du temps mort en fin de macro et ne changeraient rien à l'affichage. + Ce n'est pas une preuve. Test décisif : porter `500 ms` sur l'étape `Esc`. Si le + sélecteur reste visiblement affiché une demi-seconde, c'est « avant » ; s'il se + referme aussitôt et que c'est la molette qui reste inerte, c'est « après ». +- **Le sort des événements d'encodeur pendant l'exécution d'une macro** : mis en + file ou perdus. Le firmware n'est pas du QMK — c'est un ESP32-S3 sous FreeRTOS + avec un firmware maison — donc l'hypothèse d'une boucle de scan gelée pendant + l'attente est infondée. +- **Le délai maximal accepté par le firmware.** L'interface d'Input plafonne la + saisie à 9999 ms, mais rien ne borne la valeur à l'import : un JSON écrit à la + main transmet ce qu'il veut. diff --git a/docs/fr/research/hid-lighting-protocol.md b/docs/fr/research/hid-lighting-protocol.md new file mode 100644 index 0000000..02297b3 --- /dev/null +++ b/docs/fr/research/hid-lighting-protocol.md @@ -0,0 +1,117 @@ +[English](../../research/hid-lighting-protocol.md) · [Français](hid-lighting-protocol.md) + +# Protocole d'éclairage HID du Codex Micro — format observé, confirmé à l'exécution + +## Verdict + +**Le canal d'éclairage par touche est ouvert.** Le cadrage rapporté est +confirmé à l'exécution, le transport non exclusif fonctionne avec `node-hid` +3.4.0, et une réimplémentation originale est livrée +(`scripts/lib/hid-frame.mjs`, `scripts/lib/hid-lighting.mjs`, +`scripts/lib/hid-device.mjs`, `scripts/lighting.mjs`). Chacun peut piloter la +couleur et l'effet des touches de son propre clavier, ainsi que les deux zones +globales, et écouter les événements touches et joystick. + +Mesures sur macOS `26.5.2` arm64, firmware Codex Micro `v0.4.1` (relevé par +`sys.version`), `node-hid` `3.4.0`. + +## Cadre légal, rappelé + +Ce document décrit un **format observé** : constantes, positions d'octets, +champs JSON. L'implémentation du dépôt est un code original écrit d'après ces +faits, pour interopérer avec un périphérique que son utilisateur possède. +Aucune ligne du SDK Work Louder (`UNLICENSED`, registre privé) n'est reprise +ni redistribuée ; les extraits lus localement pour établir les faits restent +sous `.local/`, ignoré par Git. + +## Matrice de preuve + +| Affirmation | État | Preuve | +| --- | --- | --- | +| Rapports de 64 octets, octet 0 = `0x06`, octet 1 = canal `2` (RPC), octet 2 = longueur, charge UTF-8 à l'octet 3 | **confirmé** | round-trip `sys.version` → `{"result":{"version":"v0.4.1"},"id":798,"method":"sys.version"}` | +| Charge utile de 61 octets par rapport, continuation multi-rapports au-delà | **confirmé** | poussées `thstatus` de ~140 octets (3 rapports) acquittées six fois pendant la sonde | +| L'octet 2 porte la longueur **du fragment**, sur chaque rapport | confirmé | cohérent avec l'accumulation par canal côté hôte ; l'hypothèse « longueur totale au premier rapport » (sonde Swift parallèle) est écartée | +| Canal 1 = journaux de débogage, canal 2 = RPC, messages terminés par saut de ligne | confirmé | lecture du format + réception fonctionnelle | +| Enveloppe de requête `{method, params, id}`, `id` entier dans `[0, 999)`, non-ASCII échappé en `\uXXXX` | confirmé | round-trips réussis | +| Réponse `{result, id, method}` ou `{error, id}` ; la méthode est renvoyée en écho | confirmé | réponses observées | +| Notification sans `id` : `{method, params}`, formes compactes `m`/`p`, `i` possibles | confirmé | format documenté, distribution implémentée | +| VID `0x303a`, PID `0x8360`, collection vendeur usage page `0xFF00` | confirmé | `hidutil list`, énumération `node-hid` | +| Ouverture **non exclusive** possible avec `node-hid` 3.4.0 (`HIDAsync.open(path, { nonExclusive: true })`) | **confirmé** | ouverture + round-trip réussis pendant que ChatGPT tient le même périphérique | +| `v.oai.thstatus` pilote chaque touche Agent : entrées `{id, c, b, e, s, sk, sa}`, champs omis inchangés | **confirmé** | six écritures acquittées, touches allumées une par une, extinction propre | +| `v.oai.rgbcfg` configure deux zones globales `{ambient, keys}` × `{e, b, s, m, c}` | confirmé (format) | lecture du format ; non exercé à l'écriture ici | +| Effets : `off=0, solid=1, snake=2, rainbow=3, breath=4, gradient=5, shallowBreath=6` | confirmé (format) | énumération documentée | +| Notifications `v.oai.hid` `{k, act, ag}` (touches) et `v.oai.rad` `{a, d}` (joystick) | confirmé (format) | types documentés ; écoute implémentée (`listen`) | +| Correspondance thread id ↔ touche physique : `[0..5]` dans l'ordre `key-9, key-10, key-5…key-8` | **confirmé sur matériel** | sonde une-touche-à-la-fois : les `id` 0 à 5 suivent l'ordre de `SLOT_CONTROLS`, rangée du haut puis la suivante, de gauche à droite | +| Une requête en vol, 50 ms entre appels, 10 s de garde par réponse | respecté | comportement du transport livré | + +## Le point transport, et l'erreur à ne pas reproduire + +`node-hid` embarque hidapi, qui ouvre **en mode exclusif par défaut** depuis +hidapi 0.14. Ouvrir avec `new HID.HID(path)` sans option échoue sur ce +périphérique tant qu'une autre application le tient — c'est l'échec mesuré par +la sonde parallèle (`scripts/lighting-probe.mjs`), qui concluait à tort que +`node-hid` ne pouvait pas ouvrir ce périphérique. + +`node-hid` expose pourtant bien l'option : `HIDAsync.open(path, { nonExclusive: true })` +appelle `hid_darwin_set_open_exclusive(0)` (`node_modules/node-hid/src/HIDAsync.cc:137`), +et l'ouverture non exclusive **fonctionne** — preuve par le round-trip +`sys.version`. L'ouverture non exclusive IOKit (`kIOHIDOptionsTypeNone`), que la +sonde Swift parallèle a validée, revient au même. + +Conséquence pratique : le transport Node suffit, pas besoin d'un binaire +auxiliaire. L'autorisation macOS « Surveillance des saisies » n'a pas été +requise pour l'ouverture non exclusive sur cette machine. + +## Concurrence d'écriture, stratégie livrée + +Le périphérique est ouvert en non exclusif par toutes les applications : les +lectures sont diffusées à tous, les écritures se disputent, **dernière +écriture gagnante**. L'app ChatGPT repousse `rgbcfg` puis `thstatus` toutes +les 35 à 40 secondes. + +Le signal de coexistence est gratuit : les réponses portent la méthode en +écho, et une réponse dont l'identifiant n'est pas le nôtre est forcément +celle d'un autre écrivain (ce sont ces « réponses orphelines » qu'Input +journalise en avertissement). Le mode `--hold` de `scripts/lighting.mjs` +s'appuie dessus : toute poussée étrangère détectée déclenche une +réapplication immédiate, avec un filet de sécurité périodique de 10 s. Sans +`--hold`, l'état posé est recouvert à la cadence de ChatGPT — comportement +attendu, affiché à l'utilisateur. + +## Composants livrés + +| Composant | Rôle | +| --- | --- | +| `scripts/lib/hid-frame.mjs` | cadrage pur : fragmentation, réassemblage par canal, accumulateur JSON-RPC (pur, testé) | +| `scripts/lib/hid-lighting.mjs` | paramètres `thstatus`/`rgbcfg`, palette d'états → six entrées (pur, testé) | +| `scripts/lib/hid-device.mjs` | transport `node-hid` : découverte, ouverture non exclusive, file cadencée, corrélation par id, détection d'écritures étrangères | +| `scripts/lighting.mjs` | CLI `list` / `probe` / `set` / `watch` / `listen` / `off`, option `--hold` | +| `tests/hid-frame.test.mjs`, `tests/hid-lighting.test.mjs` | 21 tests sans matériel | + +`watch` est le `DeviceAdapter` prévu par la feuille de route : il suit +`~/.claude/thread-status/slots.json` et pousse les couleurs d'état des six +emplacements à chaque changement. + +## Ce qui reste ouvert + +- La sémantique exacte de `sk` / `sa` (synchronisation de la couleur d'un + thread vers les zones touches / ambiante, dans un sens ou dans l'autre) : + non éprouvée, laissée à 0 par défaut. +- `v.oai.rgbcfg` à l'écriture : format confirmé, jamais envoyé ici. La méthode + décrit les deux zones d'un coup ; la CLI exige donc `--keys` et `--ambient` + ensemble. +- Si les identifiants de thread au-delà de 5 existent (autres touches) : + aucun indice, non exploré. +- La pérennité : le format est celui du firmware `v0.4.1` ; une mise à jour + peut le faire évoluer sans prévenir. + +## Sources + +- Format et énumérations : lus localement dans le bundle ChatGPT.app + (`@worklouder/device-kit-oai`, `@worklouder/wl-device-kit`) — lecture pour + documentation, aucune redistribution. +- Mesures d'exécution : cette machine, juillet 2026 (round-trip, sonde, + chaîne `watch` sur état synthétique). +- [`thread-status-feasibility.md`](thread-status-feasibility.md) — mesures + amont (roster, hooks, contention, réponses orphelines dans le log d'Input). +- [`appsense-behavior.md`](appsense-behavior.md) — contention et zones. diff --git a/docs/fr/research/input-0.17.2-sharing.md b/docs/fr/research/input-0.17.2-sharing.md new file mode 100644 index 0000000..e59bfbe --- /dev/null +++ b/docs/fr/research/input-0.17.2-sharing.md @@ -0,0 +1,116 @@ +[English](../../research/input-0.17.2-sharing.md) · [Français](input-0.17.2-sharing.md) + +# Work Louder Input 0.17.2 — mécanisme de partage observé + +## Verdict + +Input `0.17.2` possède un flux utilisateur officiel d'import et d'export au +niveau **layer** et **profile**. Les fichiers produits portent respectivement +les suffixes `*-layer.json` et `*-profile.json`. + +Cela justifie de privilégier l'import/export officiel plutôt qu'un patch direct +de la base locale. En revanche, le dépôt ne publie pas encore de fichier layer +Codex Micro : aucun export réel n'a été capturé puis réimporté sur le matériel. +Le statut reste donc `proposal-not-applied`. + +## Faits vérifiés + +### Documentation Work Louder + +La page officielle du Codex Micro indique : + +- six layers programmables au maximum ; +- un lien AppSense créé avec l'icône de lien, `Auto detect`, puis cinq secondes + de focus sur l'application cible ; +- `Reset settings` supprime les layers, profils et actions. + +Source : + +### Application distribuée + +L'archive officielle inspectée est : + +```text +https://github.com/worklouder/input-releases/releases/download/v0.17.2/input-0.17.2-arm64-mac.zip +``` + +Identité observée : + +- SHA-256 de l'archive : + `72bb2ccd2f0de0b21a61cd4006367a64e628010c3e7303e94f219cff5ad45d35` ; +- bundle macOS : `it.focusense.input-app` ; +- version courte et build : `0.17.2` ; +- package Electron : `input` `0.17.2`. + +L'analyse statique assainie du bundle expose les libellés suivants : + +- `Import layer` ; +- `Export layer` ; +- `Import Profile` ; +- `Export Profile` ; +- avertissement de langue différente ; +- refus d'un fichier créé pour un autre type de clavier. + +L'enveloppe JSON d'un export de layer contient les clés de premier niveau : + +```text +keyboard +language +layer +actions +multiactions +smartActions +actionGroups +multiactionGroups +smartActionGroups +``` + +L'export de profile remplace `layer` par `profile`. Le code packagé utilise +`JSON.stringify`, `Blob` et `URL.createObjectURL` pour l'export, puis +`FileReader`, `JSON.parse` et une copie structurée pour l'import. + +## Méthode reproductible + +Les workflows suivants téléchargent temporairement l'archive officielle, +extraient `app.asar`, calculent uniquement des métadonnées structurales, puis +suppriment les fichiers propriétaires : + +- `.github/workflows/inspect-input-0172.yml` ; +- `.github/workflows/inspect-input-0172-ast.yml`. + +Les artefacts CI contiennent uniquement : versions, sommes de contrôle, +libellés bornés, noms de clés, suffixes et résumés AST. Ils ne contiennent ni +code source packagé, ni image, ni firmware, ni identifiant local. + +## Décision d'architecture initiale + +Cette inspection avait conduit au plan initial suivant : + +1. sauvegarde par export officiel du profile **et** copie de la configuration + locale reconnue ; +2. inventaire depuis le `*-profile.json` officiel ; +3. sélection du premier emplacement libre après l'index protégé `0` ; +4. import du `*-layer.json` officiel lorsqu'un artefact réel aura été vérifié ; +5. procédure guidée manuelle tant que cet artefact manque ; +6. rollback principal par réimport du profile officiel d'origine ; +7. aucun patch direct de `input_storage.json`, Local Storage ou du périphérique + avant preuve supplémentaire. + +Ce plan est conservé comme historique, mais il n'est plus le parcours V1 +courant. La V1 actuelle transforme localement un export `*-profile.json` +Input `0.17.3` contenant exactement un layer `Claude` déjà lié avec AppSense. +L'import de layer reste une validation de publication optionnelle. + +## Points non encore prouvés + +- structure interne complète des objets `layer` et `actions` pour le Codex + Micro ; +- identifiants physiques affichés par Input pour chaque touche ; +- création puis import du layer Claude sur le périphérique réel ; +- persistance après redémarrage d'Input ; +- retour au layer précédent lors de la perte de focus ; +- restauration effective du keymap matériel par réimport de profile ; +- comportement d'un second import du même layer. + +Un vrai fichier `*-layer.json` ne doit être ajouté qu'après les tests décrits +dans `profiles/claude-shortcuts/artifacts/README.md`. diff --git a/docs/fr/research/thread-status-feasibility.md b/docs/fr/research/thread-status-feasibility.md new file mode 100644 index 0000000..a8a7fc0 --- /dev/null +++ b/docs/fr/research/thread-status-feasibility.md @@ -0,0 +1,562 @@ +[English](../../research/thread-status-feasibility.md) · [Français](thread-status-feasibility.md) + +# États des sessions Claude Code et touches Agent — mesures + +## Verdict + +**Les états sont disponibles officiellement, les LED fonctionnent sur le layer +`Claude`, et la navigation est résolue sur toutes les surfaces.** Trois +conclusions, dans cet ordre de solidité : + +1. Détecter « en cours / intervention / terminé / fermé » par session est un + problème résolu, avec deux mécanismes documentés et complémentaires. +2. Aller à la session depuis une touche est résolu sur les deux surfaces : focus + de fenêtre en AppleScript quand la session tourne dans un terminal, et + `claude://resume?session=` quand Claude Desktop l'héberge. Cette seconde + route n'est pas documentée. +3. Piloter les six LED **fonctionne, y compris sur le layer `Claude`**, à une + condition découverte tardivement : les six positions Agent de ce layer doivent + porter les keycodes `KV_OAI_AG00` à `KV_OAI_AG05`. Le prédicat du firmware est + le keycode, pas l'index du layer. + +Les trois briques sont réunies, et aucun raccourci Claude n'est sacrifié. Mais la +fonction a une condition d'usage : **l'app ChatGPT doit être quittée.** Elle +réécrit les six LED toutes les 35 à 40 secondes et intercepte les appuis sur les +touches Agent pour changer de thread Codex. Les deux moitiés de la fonction lui +sont donc disputées par la même application, et rien ne permet d'arbitrer. + +## Le modèle : deux sources, deux rôles + +| Source | Autorité sur | Nature | +| --- | --- | --- | +| `claude agents --json` | l'appartenance : qui occupe un emplacement | poll, officiel | +| hooks du plugin | l'état de chaque session | push, officiel | + +Cette séparation n'est pas esthétique, elle est nécessaire. Un hook manqué — +crash, `kill -9` — fige l'état à « en cours » indéfiniment, et seul le roster le +rattrape. Inversement le roster ne publie aucun état. Aucune des deux sources ne +suffit seule. + +Sortie réelle du roster, sur ce dépôt : + +```json +[ + { "pid": 47158, "cwd": "/Users/…/claude-codex-micro", "kind": "interactive", + "startedAt": 1785370945651, "sessionId": "81f2c88d-…", + "name": "claude-codex-micro-73" } +] +``` + +`--json` est explicitement prévu pour le script : « does not require a TTY ». +`--all` ajoute les sessions d'arrière-plan terminées, `--cwd` filtre par +répertoire. + +### Table des états + +| État | Événement | Champ décisif | Couleur | +| --- | --- | --- | --- | +| en cours | `UserPromptSubmit` | — | `#D97757` | +| intervention | `Notification` | `notification_type` ∈ {`permission_prompt`, `agent_needs_input`, `elicitation_dialog`} | `#C2483D` | +| au repos | `SessionStart`, `Notification` / `idle_prompt` | — | `#6D5A7D` | +| terminé | `Stop` | — | `#5B8C6F` | +| fermé | `SessionEnd`, ou absence du roster | `reason` | `#2F2927` | + +`notification_type` est le seul champ qui distingue « on t'attend » de « ça +travaille ». Les types `auth_success`, `elicitation_complete` et +`agent_completed` ne changent pas l'état : un témoin rouge doit signifier une +décision attendue, rien d'autre. + +## Matrice de preuve + +Mesuré sur macOS `26.5.2` arm64, Claude `1.24012.9`, Claude Code `2.1.219`. + +| Affirmation | État | Preuve | +| --- | --- | --- | +| `claude agents --json` liste les sessions vivantes avec `sessionId`, `pid`, `cwd`, `name` | confirmé | exécution, 3 sessions retournées | +| Un hook de plugin reçoit l'événement en JSON sur stdin | confirmé | plugin sonde, 4 événements capturés | +| Un hook hérite de `CLAUDE_PID`, `CLAUDE_CODE_SESSION_ID`, `CLAUDE_CODE_ENTRYPOINT` | confirmé | environnement relevé dans le hook | +| `CLAUDE_PID` est égal au `pid` du roster | confirmé | `47158` des deux côtés | +| `$CLAUDE_PLUGIN_ROOT` s'étend dans une commande de hook | confirmé | plugin `thread-status` chargé par `--plugin-dir` | +| `CLAUDE_PLUGIN_DATA` diffère selon le mode de chargement | confirmé | `…/data/-inline` avec `--plugin-dir` | +| Une session `claude -p` émet des hooks sans entrer au roster | confirmé | session `439b9985` absente de `agents --json` | +| `CLAUDE_CODE_HOST_SESSION_ID` **n'identifie pas** une session | confirmé | deux sessions distinctes partagent `local_f92b6e6a` | +| Le `tty` distingue terminal et Desktop | confirmé | `tty = ??` pour les trois sessions Desktop | +| Focus d'une fenêtre de terminal par `tty` en AppleScript | non testé de bout en bout | scripts compilés par `osacompile` ; aucune session en terminal disponible, iTerm2 absent de la machine | +| Route pour ouvrir une session Claude Code locale dans Desktop **par identifiant** | **réfuté** | `claude://resume?session=` ouvre la bonne session, vérifié sur machine ; la route est absente de la doc des deep links, qui ne cite que `claude://code/new` | +| `claude://resume` valide sa cible par une regex UUID stricte | confirmé | lu dans le handler : l'`uuid` est vérifié avant `importCliSession`, puis la navigation | +| `claude://resume` échoue quand le transcript est absent du disque | rapporté, non testé | chemin d'erreur `transcript_missing` du handler ; aucun compte rendu ne remonte à l'appelant, `open` sort en 0 dans tous les cas | +| Raccourcis de **cycle** entre sessions dans Claude Desktop | documentés | `Ctrl Tab` / `Ctrl Shift Tab` et `Cmd Shift ]` / `Cmd Shift [` — table des raccourcis du Code tab | +| Raccourci pour sélectionner une session par son rang | inexistant | `1`–`9` ne sélectionne que dans un menu ouvert, et il n'existe pas de menu de sessions | +| Repli pour une session Desktop sans identifiant UUID : activer l'application | implémenté | `open -b com.anthropic.claudefordesktop`, sans consentement Automation ; la session précise reste non sélectionnable | +| `claude --resume ` reprend une session fermée | documenté | doc de gestion des sessions | +| Le Codex Micro est un périphérique Espressif | confirmé | `hidutil list` : VID `0x303a`, PID `0x8360` | +| Le log d'Input ne peut pas livrer le protocole RGB | confirmé | 66 lignes `v.oai.*`, toutes des réponses, zéro requête | +| `v.oai.thstatus` est l'éclairage **par thread** | confirmé | commentaire et énumération lus dans le bundle ChatGPT | +| `v.oai.rgbcfg` ne couvre que deux zones globales | confirmé | même source, recoupé avec le modèle d'Input | +| Le SDK est privé et sans licence | confirmé | `@worklouder/device-kit-oai`, `UNLICENSED`, registre privé | +| Cadre HID 64 octets, `0x06` / canal `2` | **confirmé sur matériel** | requête envoyée, réponse `{"result":{"ok":1},"id":1,"method":"v.oai.thstatus"}` | +| Fragmentation : longueur totale dans le premier rapport | **réfuté** | corrélées par `id`, les charges de 101 octets ne reçoivent aucune réponse | +| Fragmentation : longueur du fragment dans chaque rapport | **confirmé sur matériel** | charges ~140 octets (3 rapports) acquittées avec `id` corrélé — `scripts/lib/hid-frame.mjs`, voir [`hid-lighting-protocol.md`](hid-lighting-protocol.md) | +| Les réponses sont diffusées à tous les lecteurs | confirmé | des accusés de l'app ChatGPT ont été pris pour les nôtres tant que l'`id` n'était pas vérifié | +| L'écriture par emplacement est acceptée | **confirmé sur matériel** | six poussées colorées acquittées avec `id` corrélé, touches allumées une par une — sonde `scripts/lighting.mjs` | +| Une écriture peut échouer en répétition rapide | observé | un `SetReport` sur ~60 refusé en `0xE00002BC` pendant la réassertion | +| Correspondance `id` → touche physique | **confirmé sur matériel** | sonde une-touche-à-la-fois : les `id` 0 à 5 suivent l'ordre de `SLOT_CONTROLS`, rangée du haut puis rangée suivante, de gauche à droite | +| Le rendu exige les keycodes `KV_OAI_AG00..05` sur les six positions | **confirmé sur matériel** | layer `Claude` avec `KC_NONE` : aucun rendu ; les mêmes six positions passées aux keycodes Agent : rendu immédiat des six couleurs | +| Le prédicat du firmware est l'index de layer 0 | **réfuté** | le rendu fonctionne sur le layer `Claude` d'index 1 dès que les keycodes y sont posés | +| Les touches Agent notifient l'hôte de leur appui | **confirmé sur matériel** | `v.oai.hid` reçu pour `AG00` à `AG05`, `act` 1 à l'appui et 0 au relâchement — `npm run lighting -- listen` | +| L'app ChatGPT intercepte aussi ces appuis | **confirmé sur matériel** | sur le layer `Claude`, appuyer sur une touche Agent fait basculer les threads Codex | +| Un raccourci global natif est nécessaire pour la navigation | **réfuté** | le clavier désigne l'emplacement sur le canal HID déjà ouvert ; ni `RegisterEventHotKey`, ni autorisation macOS | +| `device.status` renvoie un index de layer 1-based | rapporté, non revérifié | `layer_index: 1` = layer Codex, `2` = layer `Claude` ; Input applique `layer_index - 1` | +| Les keycodes `KV_OAI_AG00..05` sont assignables depuis Input | **réfuté** | une seule occurrence chacun dans l'`app.asar`, dans la définition câblée du layer natif : absents du sélecteur de touches | +| Un `{"ok":1}` garantit un rendu visible | **réfuté** | des accusés non corrélés provenaient de l'app ChatGPT | +| Une écriture mono-rapport s'affiche réellement | **confirmé sur matériel** | emplacement 0 passé au bleu puis éteint par la sonde, observé | +| Le rendu est reproductible | confirmé, après correction | l'intermittence initiale venait de la fragmentation fausse ; reproductible une fois les keycodes Agent posés | +| Le périphérique expose une collection vendor `0xff00` | confirmé | `HID.devices()` : quatre collections, `0x0001/0x06`, `0x000c/0x01`, `0x000c/0x02`, `0xff00/0x01` | +| `node-hid` **peut** ouvrir ce périphérique en non exclusif | **confirmé sur matériel** | `HIDAsync.open(path, { nonExclusive: true })` puis round-trip `sys.version` réussi ; l'échec antérieur venait d'une ouverture sans l'option (`new HID.HID(path)` ouvre en *seize*) | +| L'ouverture non exclusive par IOKit réussit | confirmé | `IOHIDDeviceOpen(kIOHIDOptionsTypeNone)` accepté là où `hid_open_path` échoue | +| « Surveillance des saisies » est requise | **indéterminé** | accordée dans le contexte où IOKit réussit : les deux causes ne sont pas séparables | +| Socket IPC privé de l'app Codex | présent | `~/.codex/ipc/ipc.sock`, `srw-------` | + +## Le point qui décide de la navigation + +`CLAUDE_CODE_HOST_SESSION_ID` ressemble à l'identifiant qui manquerait pour +adresser une session dans Claude Desktop. Ce n'en est pas un. Trois sessions +Desktop indépendantes, relevées par `ps eww` : + +```text +pid 81796 local_f92b6e6a-2b3c-4cdf-8e43-c58a3e3681a2 +pid 32491 local_f92b6e6a-2b3c-4cdf-8e43-c58a3e3681a2 ← même identifiant +pid 47158 local_0c92bab5-02f7-4d17-a748-d0717a6c526e +``` + +Deux sessions Claude Code distinctes, ni parentes ni filles, partagent la même +valeur. L'identifiant **regroupe** des sessions, il n'en désigne aucune. Il ne +peut donc pas servir de cible de navigation, et le compagnon ne fabrique aucune +URL à partir de lui. + +Conséquence directe, et c'est la décision produit du projet : + +| Surface | `tty` | Aller à la session | +| --- | --- | --- | +| Terminal | réel | `pid` → `tty` → focus de la fenêtre en AppleScript | +| Claude Desktop, IDE | `??` | `claude://resume?session=` | +| Session fermée | — | `claude --resume ` | + +Ce qui manquait n'était pas un identifiant, c'était la route. Le `sessionId` du +roster est exactement la cible que `claude://resume` attend — le +`hostSessionId`, lui, ne désigne toujours rien. **Les six emplacements sont donc +navigables quelle que soit la surface**, et le repli « activer l'application » +ne sert plus qu'aux sessions dont l'identifiant n'est pas un UUID. + +Deux réserves, portées par le code plutôt que par ce texte : la route n'est pas +documentée, et le handler ne rend pas compte de l'issue — `open` sort en 0 même +quand la reprise échoue faute de transcript sur le disque. Le focus par +AppleScript demande le consentement Automation de macOS, pas l'Accessibilité ; +le passage par `claude://` n'exige ni l'un ni l'autre. + +## Pièges rencontrés + +- **Une session `claude -p` ou un sous-agent émet des hooks sans jamais entrer au + roster.** Leur donner un emplacement remplirait les six touches de sessions + invisibles. Règle retenue : le roster arbitre l'appartenance, les hooks ne font + que poser un état sur un emplacement déjà ouvert. Les états orphelins sont mis + en attente dans une file bornée, et les abandons sont comptés. +- **`CLAUDE_CODE_CHILD_SESSION=1` ne filtre pas les sessions imbriquées.** La + session principale d'une fenêtre Desktop le porte aussi. +- **La sortie standard d'un hook `UserPromptSubmit` est injectée dans le contexte + de la conversation.** Un émetteur bavard finirait dans le prompt de + l'utilisateur. L'émetteur n'écrit rien sur stdout et sort toujours avec 0. +- **`CLAUDE_PLUGIN_DATA` n'est pas un chemin d'état stable** : il vaut + `…/data/-inline` sous `--plugin-dir` et `…/data/` après installation. + L'état vit donc sous `~/.claude/thread-status/`, surchargeable par + `CLAUDE_THREAD_STATUS_DIR`. +- **Le format des transcripts `.jsonl` est déclaré interne et change entre + versions.** Aucun composant ne les lit, y compris pour l'état. + +## Envoyer l'information au clavier : résultat négatif mesuré + +L'idée retenue jusqu'ici était de récupérer le protocole RGB en capturant +`~/Library/Logs/input/main.log` pendant que l'app ChatGPT anime les touches +Agent. **Cette voie est fermée**, et la mesure le montre sans ambiguïté. + +Le log contient bien 66 lignes `v.oai.*`, mais elles n'ont que deux formes : + +```text +|wl_device_comm| No resolver found for id: 475 response: + {"result":{"ok":1},"id":475,"method":"v.oai.thstatus"} + {"result":{"ok":1},"id":121,"method":"v.oai.rgbcfg"} +``` + +Zéro ligne porte des `params`. Ce sont des **réponses orphelines** : le +périphérique HID est ouvert en mode non exclusif, ses rapports d'entrée sont +diffusés à tous les lecteurs, et Input journalise en avertissement les réponses à +des appels qu'il n'a jamais émis. Le sens qui nous intéresse — hôte → clavier, +celui qui porte les couleurs et les états — n'y passe jamais. + +Ce que la mesure donne quand même : + +- les deux méthodes sont appelées **en couple**, `rgbcfg` puis `thstatus` environ + 70 ms plus tard, et le couple se répète toutes les 35 à 40 secondes ; +- le périphérique accuse par `{"ok":1}`, donc les appels aboutissent ; +- les méthodes propres à Input dans le même log sont `device.status`, + `host.focused_app`, `fs.list`, `fs.readbin`, `sys.version` — le namespace + `v.oai.*` est bien distinct et n'appartient pas à Input. + +Trois verrous subsistent, et ils sont indépendants : + +1. **Le format des requêtes est inconnu.** Le récupérer demande une capture du + bus USB (rapports de sortie de l'app ChatGPT vers le périphérique), pas une + lecture de log. +2. **La contention reste entière.** Dernière écriture gagnante, et l'app ChatGPT + repousse sa configuration toutes les 35 à 40 secondes : des couleurs écrites + par un tiers seraient recouvertes à cette cadence tant qu'elle tourne. +3. **Aucun canal sanctionné n'existe.** Pas de SDK Work Louder public, et + Hardware Buddy n'expose que des compteurs agrégés, sans identité de session. + +Le VID Espressif rapproche le matériel de la famille de l'exemple ESP32 publié +avec Hardware Buddy. Cela ne rend pas le firmware modifiable pour autant, et +l'interdiction de flasher reste entière : voir l'avertissement dans +[`appsense-behavior.md`](appsense-behavior.md). + +### Le modèle d'éclairage d'Input ne peut pas exprimer six couleurs + +Question suivante, plus intéressante : puisque l'app Work Louder pilote bien +l'éclairage, son propre canal suffirait-il ? **Non, et pour une raison de +structure, pas de documentation.** + +L'app ne modélise pas les méthodes `v.oai.*` : zéro occurrence de `v.oai`, +`rgbcfg` ou `thstatus` dans ses 226 Mo d'`app.asar`. Son éclairage voyage dans la +configuration du périphérique, sous la forme suivante — relevée dans +`~/Library/Application Support/input/devices//keymap.json` : + +```json +"layers": [{ + "id": …, "name": "Claude", "color": 16711680, "linkedAppId": …, + "lights": { + "backlight": { "effect": "solid", "brightness": 1, "speed": 0.5, "magic": 1, "color": 14251863 }, + "underglow": { "effect": "rainbow", "brightness": 1, "speed": 0.55, "magic": 1, "color": 16777215 } + }, + "layout": { "keymap": [["KC_NONE","KC_NONE"], …], "encoders": […], "joystick": {…} } +}] +``` + +`14251863` vaut `#D97757` : la couleur Claude du dépôt est déjà en place sur le +matériel. Et c'est tout ce que le format permet : + +- **deux zones par layer**, `backlight` et `underglow`, une seule couleur chacune ; +- `layout.keymap` est un tableau plat de chaînes de keycodes — aucun objet par + touche, donc **aucun emplacement où mettre une couleur par touche**. Une + recherche exhaustive de tout entier de type couleur hors `lights` ne retourne + rien. + +Conséquence : même en connaissant parfaitement le format d'Input, on ne peut pas +allumer six touches de six couleurs. Le canal par touche appartient exclusivement +au namespace privé `v.oai.*` de l'app ChatGPT, qu'Input ignore complètement. + +Ce que le canal d'Input **peut** faire, en revanche : un signal agrégé sur le +`backlight`, précisément la zone que l'app ChatGPT laisse tranquille d'après +[`appsense-behavior.md`](appsense-behavior.md). Une couleur pour « une session +attend une décision », une autre pour « au moins une travaille », une troisième +pour « tout est terminé ». Le format est connu, la couleur Claude y est déjà. + +Cette voie n'est pas ouverte pour autant : elle exige d'écrire la configuration du +périphérique en cours de session, ce que le périmètre du dépôt exclut +explicitement — « aucune écriture directe dans le stockage Input ou le +périphérique » — et elle entrerait en concurrence avec les propres poussées +d'Input. Elle demande donc une décision de périmètre, pas seulement du code. + +Réserve de mesure : la copie locale de `keymap.json` peut être en retard sur ce qui +a réellement été poussé au périphérique, et l'observation ci-dessus n'a pas été +recoupée avec une ligne `sending device config` — le log courant n'en contient +aucune. + +### Le canal par touche existe, et il est lisible localement + +L'app ChatGPT embarque le SDK privé qui parle au Codex Micro : +`@worklouder/device-kit-oai` `0.1.11` et `@worklouder/wl-device-kit`, dans +`/Applications/ChatGPT.app/Contents/Resources/app.asar`. Le fichier +`node_modules/@worklouder/device-kit-oai/dist/rpc_api_oai/rpc_api_oai.js` +contient l'énumération, en clair : + +```js +// Vendor specific. +VendorJsonRpcMethods["ThreadsLighting"] = "v.oai.thstatus"; /** Per-thread accent lighting. */ +VendorJsonRpcMethods["RgbConfig"] = "v.oai.rgbcfg"; /** Keys and ambient zone lighting. */ +``` + +Le commentaire tranche la question : `thstatus` est **l'éclairage par thread**, +`rgbcfg` ne couvre que les deux zones globales — ce qui recoupe exactement le +modèle à deux zones d'Input mesuré plus haut. La méthode d'envoi est documentée +dans le bundle : + +```js +// Only the thread id is required on each entry. […] optional color, brightness, +// effect, speed, and sync flags (brightness 0 = off through 1 = full on; +// speed 0 = stopped through 1 = fast). […] omit optional fields to leave those +// parameters unchanged on the device. +async sendThreadsLighting(threads) { + let minimized = threads.map((thread) => { return { id: thread.id, c: thread.color, … } }); +``` + +Donc : un tableau d'entrées, une par emplacement, chacune adressée par `id`, avec +`c` en entier `0xRRGGBB`, `b` et `s` normalisés de 0 à 1, et des drapeaux de +synchronisation. Les mises à jour partielles sont prévues. L'énumération des +effets est également en clair : `snake = 2`, `rainbow = 3`, `breath = 4`, +`gradient = 5`, `shallowBreath = 6`. + +Deux méthodes supplémentaires existent dans le même namespace et ne sont pas +étudiées ici : `v.oai.hid` et `v.oai.rad`. + +### Cadre HID confirmé sur matériel + +Le cadre rapporté est exact. Une requête no-op — une entrée réduite à `{"id":0}`, +qui d'après le SDK ne change aucune couleur — a été envoyée au périphérique et +acquittée : + +```text +envoi 0602367b226d6574686f64223a22762e… {"method":"v.oai.thstatus","params":[{"id":0}],"id":1} +réponse 0602367b22726573756c74223a7b226f… {"result":{"ok":1},"id":1,"method":"v.oai.thstatus"} +``` + +Rapports de 64 octets, octet 0 = report ID `0x06`, octet 1 = canal `0x02`, +octet 2 = longueur, charge utile UTF-8 à partir de l'octet 3, soit 61 octets par +rapport. **Le même cadre sert dans les deux sens.** + +**La fragmentation est confirmée, dans le schéma du SDK.** L'hypothèse +initiale — octet 2 du premier rapport portant la longueur totale — est réfutée +par la mesure. Le schéma qui fonctionne est celui lu dans le bundle : **l'octet +2 porte la longueur du fragment, sur chaque rapport**, la charge utile étant +découpée en fragments de 61 octets réassemblés par le périphérique. Preuve : +des poussées `thstatus` de ~140 octets (six entrées, trois rapports) ont été +acquittées avec l'`id` corrélé, et les touches se sont allumées une par une — +sonde `scripts/lighting.mjs`, cadrage `scripts/lib/hid-frame.mjs`. + +Le piège mérite d'être retenu, car il invalide silencieusement toute mesure : +**les réponses du périphérique sont diffusées à tous les lecteurs du HID**, et +l'app ChatGPT en provoque toutes les 35 à 40 secondes. Une sonde qui prend la +première réponse venue prend donc les accusés de ChatGPT pour les siens. Six +écritures avaient ainsi été déclarées réussies alors qu'aucune n'avait abouti — +ce qui explique l'absence de tout changement visible. Corrélées par le champ +`id`, les mêmes charges de 101 octets (schéma « longueur totale ») ne reçoivent +aucune réponse. + +Toute sonde sur ce périphérique doit donc vérifier l'`id` de la réponse. C'est la +leçon la plus coûteuse de cette campagne de mesure. + +Le routage se fait par le report ID, pas par la collection : IOKit énumère un +seul périphérique là où hidapi voit quatre collections, et `SetReport` avec +l'identifiant `0x06` atteint le bon canal. + +**`node-hid` fait ce travail sur macOS, à condition de demander le non exclusif.** +L'échec mesuré lors de la première campagne venait d'une ouverture +`new HID.HID(path)` **sans option** : hidapi ouvre alors en mode *seize*, refusé +tant qu'une autre application tient le périphérique. `node-hid` 3.4.0 expose +pourtant `hid_darwin_set_open_exclusive` — `src/HIDAsync.cc:137` — +via `HIDAsync.open(path, { nonExclusive: true })`, et le round-trip +`sys.version` réussit ainsi pendant que ChatGPT tient le même périphérique +(transport livré : `scripts/lib/hid-device.mjs`). L'ouverture non exclusive +IOKit (`kIOHIDOptionsTypeNone`), validée par la sonde Swift, revient au même ; +les deux voies fonctionnent. + +Reste rapporté et non revérifié : le garde-fou Electron, selon lequel +`codexMicro.updateLighting` n'accepterait les mises à jour que depuis le +`webContents` de la fenêtre principale de ChatGPT. + +### Acquitté n'est pas affiché + +Le périphérique acquitte `{"ok":1}` sur les six emplacements, avec l'entrée +canonique complète — `c`, `b`, `e`, `s`, `sk`, `sa` — et **rien ne change à +l'écran du clavier**. L'accusé porte donc sur la réception de l'appel, pas sur +son rendu. + +Trois hypothèses, non départagées, par ordre de conséquence pour le produit : + +1. **Dépendance au layer.** L'observation a été faite sur le layer `Claude`, dont + la configuration Input impose déjà un `backlight` solide `#D97757`. Si + l'éclairage par thread n'est rendu que sur le layer Codex natif, où les touches + Agent sont effectivement des touches Agent, alors la fonction est inatteignable + là où ce projet en a besoin. C'est l'hypothèse à écarter en premier. +2. **`sk` inversé.** `syncKeysLighting` peut signifier « propage cette couleur à + la zone des touches » aussi bien que « cet emplacement suit la zone des + touches » — la seconde lecture écraserait la couleur demandée. +3. **`rgbcfg` conditionne `thstatus`.** L'app ChatGPT envoie toujours les deux en + couple, `rgbcfg` puis `thstatus` 70 ms après. La zone des touches doit + peut-être être placée dans un mode qui autorise les accents. + +L'hypothèse 1 s'est révélée la bonne piste, mais pas pour la raison énoncée : +voir plus bas. Les hypothèses 2 et 3 n'ont jamais servi. + +### Le rendu marche, mais une fois + +Une charge mono-rapport — `{"method":"v.oai.thstatus","params":[{"id":0,"c":255,"e":1}]}`, +61 octets — a **réellement allumé la première touche en bleu**, puis l'a éteinte +à la restauration. La chaîne complète est donc démontrée : cadre, adressage par +emplacement, encodage de couleur, effet solide, extinction. + +Aux relances suivantes, le même appel ne produit plus rien. Et pendant +l'observation, les autres touches ont repris une teinte orange **une par une** : +l'app ChatGPT réassère son propre état. + +Un succès non reproductible, sur un périphérique où un second écrivain repeint +périodiquement, ne se lit pas comme un protocole défaillant. Il se lit comme une +**course perdue**. Le protocole est acquis ; ce qui manquait était le contrôle +exclusif de la surface d'affichage. + +### La contrainte de layer, et sa résolution + +**Résolu.** Le firmware ne rend l'éclairage par thread que sur les touches dont le +keycode est `KV_OAI_AG00` à `KV_OAI_AG05`. Ce n'est pas l'index du layer qui +compte : c'est que le firmware doit savoir quelle touche physique est +l'emplacement N, et le keycode est ce qui le lui dit. Sur un layer où ces +positions valent `KC_NONE`, il n'y a aucun emplacement à peindre. + +L'indice décisif se lisait dans le bundle d'Input, où le layer Codex natif est +défini exactement ainsi : + +```js +base: [ + [ {keycode:"KV_OAI_AG00"}, {keycode:"KV_OAI_AG01"} ], + [ {keycode:"KV_OAI_AG02"}, {keycode:"KV_OAI_AG03"}, {keycode:"KV_OAI_AG04"}, {keycode:"KV_OAI_AG05"} ], + … +] +``` + +Deux touches puis quatre — la géométrie exacte des six touches Agent, dans +l'ordre confirmé à l'œil. + +Ces keycodes n'apparaissent qu'**une seule fois** chacun dans les 226 Mo de +l'`app.asar` d'Input : ils sont absents de son sélecteur de touches, donc +inassignables depuis l'interface. D'où +[`scripts/enable-agent-keys.mjs`](../../../scripts/enable-agent-keys.mjs), qui les +écrit dans un export de profile à réimporter par le flux officiel **Import +Profile**, sans jamais toucher au layer d'index 0. + +Aucun raccourci Claude n'est perdu : ces six positions étaient `no-action`, et les +raccourcis vivent sur la rangée suivante. + +**Mais le coût n'est pas nul, et il n'est pas seulement théorique.** Ces keycodes +ne sont pas de simples marqueurs d'affichage : le firmware émet une notification +`v.oai.hid`, et **l'app ChatGPT y réagit en changeant de thread Codex**. Mesuré : +sur le layer `Claude`, appuyer sur une touche Agent fait basculer les threads +Codex. + +La même application contend donc les deux moitiés de la fonction : + +| Ressource | Ce que fait l'app ChatGPT | +| --- | --- | +| les six LED | réécrit sa configuration toutes les 35 à 40 s | +| les six appuis | intercepte et change de thread Codex | + +Il n'y a pas d'arbitrage possible : les notifications sont diffusées à tous les +lecteurs, et rien ne permet de demander à ChatGPT de se taire. **Quitter l'app +ChatGPT résout les deux d'un coup** — les écritures d'éclairage ne sont plus +recouvertes, et les appuis n'ont plus qu'un seul destinataire. + +C'est donc la condition d'usage réelle de la fonction, et elle doit être annoncée +comme telle : les six témoins Claude et l'app ChatGPT ne cohabitent pas. + +**Vérifié sur matériel** : après import, layer `Claude` actif, +`lighting set all #00FF00` allume bien les six touches en vert. + +### Les deux contournements qui avaient échoué + +Avant que l'explication par les keycodes soit trouvée, la théorie de travail était +que l'éclairage par thread ne rendait que sur le layer Codex natif, puisque sur le +layer `Claude` les écritures étaient acquittées sans que rien ne s'affiche. + +L'explication paraissait tenir au modèle d'éclairage d'Input mesuré plus haut : +chaque layer porte son propre `lights.backlight`, et celui du layer `Claude` est +un `solid` à `#D97757`. Le rendu du layer recouvrirait alors les accents par +thread, que le firmware ne compose peut-être que sur le layer qui porte le rôle +« touches Agent ». + +Deux contournements ont été essayés, du moins coûteux au plus coûteux : + +1. **Neutraliser la zone des touches à l'exécution**, par `v.oai.rgbcfg` avec un + effet `off` sur `keys`, puis pousser les accents. **Éprouvé, sans effet** : sur + le layer `Claude` rien ne s'affiche, et la même écriture rend normalement dès + que le layer Codex redevient actif. `rgbcfg` règle une zone globale, pas le + `backlight` propre au layer, qui l'emporte tant que ce layer est actif. +2. **Neutraliser le `backlight` du layer `Claude`**, à `off` ou en luminosité 0, + par l'app Input. **Éprouvé, sans effet** : le layer `Claude` reste noir, et la + même écriture applique la couleur sur les six touches dès que le layer Codex + redevient actif. + +Ces deux échecs pointaient dans la mauvaise direction : ils cherchaient ce qui +*recouvrait* les accents, alors que le firmware n'en composait aucun, faute de +keycode pour identifier les emplacements. + +### Ce qui bloque désormais n'est plus technique + +| Verrou | État | +| --- | --- | +| Connaître le protocole par touche | **levé**, lisible localement | +| Licence | `UNLICENSED`, paquet privé, registre GitHub Packages fermé | +| Concurrence d'écriture | entière : ChatGPT repousse toutes les 35 à 40 s | +| Canal sanctionné | inexistant, ni OpenAI ni Work Louder | + +La distinction utile pour un dépôt public : **documenter un format observé** est +ce que ce dépôt fait déjà pour Input ; **redistribuer le SDK ou son code** est +exclu par sa licence. Réimplémenter le format observé pour interopérer avec un +périphérique que l'on possède est la voie habituelle, et reste une décision à +prendre en connaissance de cause, pas un acquis. + +Reste que le verrou de concurrence n'est pas résolu par la connaissance du +format : deux écrivains sur un HID non exclusif, dernière écriture gagnante, et +l'app ChatGPT réémet périodiquement. Aucune stratégie de coexistence déterministe +n'a été identifiée — seulement des hypothèses non testées, dont retirer à ChatGPT +l'autorisation macOS de surveillance des saisies, ce qui désactiverait aussi ses +propres touches. + +## Ce qui est implémenté + +| Composant | Chemin | +| --- | --- | +| Plugin de hooks | [`thread-status/`](../../../thread-status/README.md) | +| Réducteur pur, six emplacements | `scripts/lib/thread-slots.mjs` | +| Compagnon `watch` / `status` / `focus` / `doctor` | `scripts/thread-status.mjs` | +| Cadrage HID et modèle d'éclairage | `scripts/lib/hid-frame.mjs`, `scripts/lib/hid-lighting.mjs` | +| Transport `node-hid` | `scripts/lib/hid-device.mjs` | +| CLI d'éclairage, `DeviceAdapter` | `scripts/lighting.mjs` | +| Keycodes Agent sur le layer `Claude` | `scripts/enable-agent-keys.mjs` | +| Tests | `tests/thread-slots.test.mjs`, `tests/hid-frame.test.mjs`, `tests/hid-lighting.test.mjs` | + +La sortie du compagnon est `~/.claude/thread-status/slots.json`. C'est la couture +que consomme le `DeviceAdapter` : `node scripts/lighting.mjs watch` suit ce +fichier et pousse les six couleurs d'état sur le clavier. + +Les touches physiques sont reliées. Les six touches Agent sont `key-9`, `key-10`, +`key-5`, `key-6`, `key-7`, `key-8` — voir `KEY_CONTROL_LOCATIONS` dans +`shared/input-profile.mjs` — et elles émettent `v.oai.hid` sur le canal HID déjà +ouvert, si bien que `npm run lighting -- watch --focus` route un appui vers +`focus ` sans raccourci global natif ni autorisation macOS. + +## Ce qui reste non établi + +- Le focus AppleScript par `tty` n'a pas été exercé de bout en bout : aucune + session Claude Code en terminal n'était disponible, et iTerm2 n'est pas installé + sur la machine de mesure. Seule Terminal.app pourrait être testée. +- La liste complète des valeurs de `CLAUDE_CODE_ENTRYPOINT`. Une seule est + observée : `claude-desktop`. +- Si `claude agents --json` inclut les sessions lancées par une extension d'IDE. +- Si `claude://resume` échoue, et comment, quand le transcript a disparu du + disque. Le handler porte un chemin d'erreur `transcript_missing`, mais `open` + sort en 0 dans tous les cas : rien ne remonte à l'appelant. +- La pérennité de la route non documentée `claude://resume`. Elle est vérifiée + sur Claude `1.24012.9` et peut changer à une mise à jour de l'application. +- Toute stratégie de coexistence avec l'app ChatGPT sur le même périphérique HID. + Aucune n'a été testée, et aucune ne paraît déterministe. +- `v.oai.hid` et `v.oai.rad`, présents dans le même namespace, non étudiés. +- Si Work Louder ou OpenAI accepteraient d'ouvrir le canal. C'est la seule voie + qui lèverait à la fois la licence, la concurrence et la pérennité. +- Le comportement du roster lorsque plus de six sessions vivent en parallèle du + point de vue de l'utilisateur : le débordement est compté et signalé, mais + l'ergonomie retenue n'est pas validée. + +## Sources + +- [Claude Code — hooks](https://code.claude.com/docs/en/hooks) +- [Claude Code — gestion des sessions](https://code.claude.com/docs/en/sessions) +- [Claude Code — création de plugins](https://code.claude.com/docs/en/plugins) +- [Claude Desktop — ouvrir avec un lien](https://support.claude.com/en/articles/14729294-open-claude-desktop-with-a-link) +- [Anthropic — Hardware Buddy BLE Protocol](https://github.com/anthropics/claude-desktop-buddy/blob/main/REFERENCE.md) diff --git a/docs/fr/roadmap.md b/docs/fr/roadmap.md new file mode 100644 index 0000000..2c0fc9f --- /dev/null +++ b/docs/fr/roadmap.md @@ -0,0 +1,122 @@ +[English](../roadmap.md) · [Français](roadmap.md) + +# Feuille de route + +## État actuel + +Le dépôt public contient désormais : + +- un manifeste communautaire et un mapping physique Claude ; +- des schémas réutilisables pour les futurs presets ; +- une représentation SVG originale ; +- un outil d'inventaire, sauvegarde, dry-run, sanitation et rollback ; +- un générateur local de profile Input `0.17.3` qui conserve AppSense ; +- des tests transactionnels sur copies isolées ; +- une analyse reproductible du format de partage d'Input `0.17.2` ; +- une piste BLE séparée, explicitement non fonctionnelle. + +Le vrai export officiel Claude `*-layer.json` et la validation matérielle +complète restent manquants. Le preset conserve donc le statut +`hardware-observed`. + +## V1 — preset Claude vérifié + +Objectif : obtenir un premier layer reproductible sans modifier le layer Codex +natif. + +- [ ] exécuter `git status --short` dans la copie locale et préserver les + modifications sans rapport ; +- [x] exporter le profile Input réel et inventorier profils, layers, actions et + liens AppSense ; +- [x] fournir une sauvegarde locale vérifiée et un rollback transactionnel ; +- [x] identifier les flux officiels Import/Export layer et profile d'Input + `0.17.2` ; +- [x] définir le manifeste, le mapping physique, la couleur et les exclusions ; +- [x] protéger l'index `0`, exiger un unique layer Claude existant et conserver + son AppSense ; +- [x] générer localement un nouveau profile Input `0.17.3` ; +- [ ] vérifier les positions, les touches, le cadran et le joystick ; +- [ ] vérifier AppSense, la perte de focus et les liens concurrents ; +- [ ] vérifier la persistance après redémarrage d'Input ; +- [ ] exporter et assainir le vrai `*-layer.json` ; +- [ ] tester l'import sur une configuration isolée et le second import ; +- [ ] restaurer le profile d'origine et vérifier le périphérique ; +- [ ] promouvoir le niveau de preuve et sortir la PR du mode brouillon. + +La V1 est terminée uniquement si une autre personne peut reproduire le résultat +sans identifiant local ni remplacement implicite d'un layer. + +## V2 — portabilité généralisée + +Les fondations minimales sont déjà présentes, mais ne sont pas déclarées +stables : + +- [x] manifeste commun et schémas V1 ; +- [x] simulation, sauvegarde, sanitation et tests de rollback sur fixtures ; +- [x] sélection d'un unique layer existant et refus de l'absence/duplication ; +- [x] aperçu visuel du mapping ; +- [ ] prise en charge d'un artefact officiel vérifié ; +- [ ] comparaison structurelle avant/après depuis de vrais exports ; +- [ ] détection des incompatibilités Input/firmware ; +- [ ] journal de validation matérielle signé par versions et sommes de contrôle ; +- [ ] automatisation du rollback officiel si Input expose un canal supporté. + +Aucun patch direct du stockage Input ne deviendra le chemin normal tant que son +format et son effet sur le périphérique ne sont pas prouvés. + +## V3 — catalogue communautaire + +- ajouter un preset « Claude Code » (terminal) en premier candidat naturel ; +- ajouter des presets IDE, navigateur, recherche, Figma et Framer ; +- indexer les presets par application, plateforme et compatibilité ; +- utiliser les modèles GitHub de proposition et de pull request ; +- exiger une méthode de sauvegarde et de retour arrière ; +- publier les résultats négatifs et incompatibilités connus ; +- permettre plusieurs représentations physiques sans identifiants locaux. + +## V4 — expérience simplifiée + +- catalogue lisible depuis une interface dédiée ; +- aperçu interactif du clavier ; +- comparaison avant/après ; +- installation guidée avec consentement explicite ; +- mises à jour versionnées sans écraser les personnalisations locales. + +## Piste parallèle : Hardware Buddy + +La recherche BLE reste indépendante. Elle ne rejoint la feuille de route +principale que si le service Nordic UART, la coexistence HID et une procédure +de restauration sûre sont démontrés sur le Codex Micro exact. + +## Piste parallèle : statut des sessions Claude Code + +Une seconde piste couvre les six touches Agent : afficher l'état des sessions +Claude Code locales et sauter à la bonne session. Contrairement à Hardware Buddy, +ses deux briques centrales reposent sur des mécanismes documentés — le roster +`claude agents --json` et les hooks — et sont implémentées : + +- [x] plugin de hooks et journal publiable ([`thread-status/`](../../thread-status/README.md)) ; +- [x] réducteur à six emplacements, testé sans matériel ; +- [x] compagnon `watch` / `status` / `focus` / `doctor` ; +- [ ] focus d'une session hébergée par un terminal, vérifié de bout en bout ; +- [x] appui d'une touche Agent relié à `focus ` : les touches émettent + `v.oai.hid` avec `k` valant `AG00` à `AG05`, donc aucun raccourci global natif + n'est nécessaire — `npm run lighting -- watch --focus` ; +- [x] couche `DeviceAdapter` : le protocole d'éclairage est confirmé sur + matériel et `node scripts/lighting.mjs watch` pousse les couleurs d'état — + voir [`hid-lighting-protocol.md`](research/hid-lighting-protocol.md) ; +- [x] navigation vers une session hébergée par Claude Desktop : + `claude://resume?session=` l'ouvre par son identifiant. La route n'est + pas documentée et le handler ne rend pas compte de l'issue : une session dont + le transcript a disparu du disque échoue en silence. + +Les deux limites historiques sont levées. Le protocole des LED par touche est +confirmé sur matériel et implémenté — y compris sur le layer `Claude`, à +condition que ses six positions Agent portent les keycodes `KV_OAI_AG00` à +`KV_OAI_AG05`, ce que pose +[`scripts/enable-agent-keys.mjs`](../../scripts/enable-agent-keys.mjs). Ce qui +maintient cette piste hors de la V1 est désormais sa dépendance à une route non +documentée et à l'arrêt de l'app ChatGPT, non un problème irrésolu. Mesures et +bornes dans +[`docs/research/thread-status-feasibility.md`](research/thread-status-feasibility.md) +et [`docs/research/hid-lighting-protocol.md`](research/hid-lighting-protocol.md). diff --git a/docs/fr/scope-and-limitations.md b/docs/fr/scope-and-limitations.md new file mode 100644 index 0000000..0fe3433 --- /dev/null +++ b/docs/fr/scope-and-limitations.md @@ -0,0 +1,109 @@ +[English](../scope-and-limitations.md) · [Français](scope-and-limitations.md) + +# Périmètre et limitations + +## Dans le périmètre V1 + +- conventions sûres pour une bibliothèque de presets ; +- manifeste et mapping physique Claude ; +- protection du layer Codex natif à l'index `0` ; +- sélection d'un unique layer `Claude` existant après inventaire ; +- conservation locale de son lien AppSense Claude Desktop ; +- génération locale d'un nouveau `*-profile.json` Input `0.17.3` ; +- export officiel du profile comme sauvegarde principale ; +- copie locale vérifiée par SHA-256 ; +- dry-run, état de session, refus de doublon et rollback guidé ; +- sanitation fail-closed d'un vrai export de layer ; +- tests transactionnels sur copies isolées ; +- analyse séparée de Hardware Buddy. + +## Non effectué sur le matériel dans cette branche + +- test des identifiants physiques ; +- import d'un `*-layer.json` Codex Micro ; +- persistance après redémarrage ; +- test de perte de focus ; +- restauration du keymap matériel. + +## Toujours hors périmètre + +- `Reset settings` ; +- suppression d'un profile ou layer existant ; +- modification de raccourcis système ; +- flash ou redistribution du firmware ; +- approbation de permissions depuis le clavier ; +- action destructive, push ou déploiement ; +- publication d'un export brut ou d'un identifiant matériel ; +- présentation de Hardware Buddy comme fonctionnel sans preuve ; +- redistribution du SDK `@worklouder/device-kit-oai` ou de son code. + +## Amendement : écriture volatile de l'éclairage + +Jusqu'ici le dépôt s'interdisait toute écriture directe sur le périphérique. Cet +amendement ouvre **un cas précis et un seul** : l'envoi de rapports HID de sortie +portant l'état lumineux d'exécution. + +La distinction qui fonde l'amendement est la persistance, pas la nature du canal : + +| Écriture | Statut | +| --- | --- | +| rapport HID d'éclairage, volatile, perdu à la déconnexion | **dans le périmètre**, sous conditions | +| configuration du périphérique, keymap, layers, couleurs de layer | hors périmètre, inchangé | +| stockage applicatif d'Input | hors périmètre, inchangé | +| firmware | hors périmètre, inchangé | + +Conditions cumulatives, toutes requises : + +1. **Opt-in explicite.** Aucune écriture par défaut, jamais au premier lancement. +2. **Volatile uniquement.** Rien qui survive à une déconnexion du périphérique. +3. **Implémentation originale.** Le format observé est documenté ; le SDK + propriétaire n'est ni copié, ni redistribué, ni empaqueté. +4. **Restauration en sortie.** Interruption, arrêt ou exception laissent + l'éclairage dans un état neutre, jamais figé sur un état faux. +5. **Concurrence documentée.** L'app ChatGPT réémet toutes les 35 à 40 secondes, + la dernière écriture gagne, et aucune coexistence déterministe n'est promise. +6. **Réversibilité par abstention.** Ne pas lancer l'outil suffit à revenir à + l'état d'origine ; il n'y a rien à désinstaller côté matériel. + +Ce que l'amendement ne change pas : le remappage des touches continue de passer +exclusivement par le flux de profils Input, et la capture de frappes reste hors +périmètre. + +Base retenue pour la réimplémentation : interopérabilité avec un périphérique que +l'utilisateur possède, code original, aucune redistribution. Voir +[`research/thread-status-feasibility.md`](research/thread-status-feasibility.md) +pour les mesures qui établissent le format. + +## Matrice de confiance + +| Affirmation | État | Preuve | +| --- | --- | --- | +| Codex Micro visible comme HID BLE | confirmé localement | observation I/O du 27 juillet 2026 | +| Input `0.17.3` installé | confirmé localement | bundle et profile réel | +| Firmware `v0.4.1` installé | confirmé localement | écran Setup | +| Six layers et AppSense | confirmé par Work Louder | documentation constructeur | +| Import/export de layer et profile | observé dans Input `0.17.2` | analyse assainie du package officiel | +| Enveloppe `*-layer.json` | observée statiquement | AST de la fonction d'export | +| Artefact Claude importable | non disponible | export réel requis | +| Lien AppSense Claude préservé | confirmé dans le profile généré | tests du générateur et export réel | +| Retour au layer précédent | non testé | test matériel requis | +| Sauvegarde/rollback de l'outil | validé sur fixture | tests Node isolés | +| Restauration réelle du périphérique | non testée | import profile + contrôle matériel requis | +| Nordic UART / Hardware Buddy | inconnu | preuves GATT et firmware requises | + +## Compatibilité + +L'observation actuelle concerne macOS `26.5.2` arm64, Claude `1.24012.9`, +Input `0.17.3` et firmware `v0.4.1`. L'analyse du mécanisme de partage +`0.17.2` reste historique. Cette combinaison n'est pas une plage de +compatibilité garantie. + +Voir [`compatibility.md`](compatibility.md). + +## Limite du rollback local + +La configuration Input copiée peut contenir des métadonnées utiles à +l'application, mais le keymap est également écrit sur le périphérique. Par +conséquent, la restauration principale est le flux officiel **Import Profile**. +La restauration brute du dossier applicatif exige un consentement supplémentaire +et ne suffit pas à promouvoir le preset. diff --git a/docs/fr/vision.md b/docs/fr/vision.md new file mode 100644 index 0000000..fe0d8b8 --- /dev/null +++ b/docs/fr/vision.md @@ -0,0 +1,83 @@ +[English](../vision.md) · [Français](vision.md) + +# Vision du projet + +## Problème + +Work Louder Input permet de personnaliser le Codex Micro, mais une +configuration utile reste difficile à transmettre : elle dépend de la version +d'Input, du firmware, des layers déjà présents et de la manière dont +l'application identifie les logiciels avec AppSense. + +Une capture d'écran ou une liste de raccourcis ne suffit pas. Un preset +partageable doit aussi expliquer sa compatibilité, préserver l'existant, +prouver son niveau de validation et offrir un retour arrière. + +## Objectif + +Construire une bibliothèque communautaire de configurations Codex Micro +compréhensibles, testables et, lorsque le format le permet, installables. + +Le parcours cible est : + +1. choisir un preset pour une application ou un workflow ; +2. vérifier la compatibilité matérielle et logicielle ; +3. sauvegarder la configuration Input courante ; +4. simuler ou inspecter le changement ; +5. installer ou reproduire uniquement le layer demandé ; +6. vérifier chaque contrôle ; +7. restaurer la sauvegarde en cas de problème. + +## Premier cas de référence + +Claude Desktop sur macOS sert de premier cas complet : + +- activation automatique du layer avec AppSense ; +- raccourcis courants et réversibles ; +- molette pour le défilement ; +- joystick pour la navigation ; +- exclusion par défaut de l'envoi, des permissions et des actions + destructrices ; le GUI personnel peut proposer l'envoi uniquement sur choix + explicite. + +Ce premier preset doit définir les conventions réutilisables par les futurs +layers IDE, navigateur, création graphique ou workflows spécialisés. + +## Principes + +### Préserver le clavier natif + +Le layer Codex fourni avec le matériel reste intact. Un preset communautaire +utilise un emplacement libre ou demande une décision explicite avant tout +remplacement. + +### Publier le niveau de preuve + +Chaque artefact porte un statut : + +- `proposal-not-applied` : spécification uniquement ; +- `hardware-observed` : environnement inventorié ; +- `manually-validated` : mapping testé sur le matériel déclaré ; +- `export-format-verified` : installation et restauration reproduites. + +### Minimiser les écritures + +Un outil d'installation doit proposer une simulation, sauvegarder avant +écriture et appliquer un delta ciblé. Il ne doit jamais dépendre de Reset +Settings. + +### Protéger la confidentialité + +Les chemins utilisateur, ports, numéros de série, adresses Bluetooth, captures +privées et exports complets restent hors du dépôt. + +### Écarter les actions conséquentes + +Les presets publics n'incluent pas par défaut l'envoi d'un message, +l'approbation d'une permission, une suppression, un push ou un déploiement. + +## Hors objectif + +Le projet ne redistribue pas de firmware Work Louder, ne promet pas une +compatibilité non testée et ne présente pas la piste BLE Hardware Buddy comme +fonctionnelle sans preuve reproductible. diff --git a/docs/getting-started.md b/docs/getting-started.md index e777920..5cc856e 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -1,10 +1,12 @@ -# Guide de démarrage +[English](getting-started.md) · [Français](fr/getting-started.md) -Ce guide ne modifie pas Input tant que `--apply` n'est pas utilisé. Même avec -`--apply`, l'outil ne clique pas dans l'interface et ne patche pas directement -la configuration d'Input. +# Getting started -## 1. Contrôler le dépôt +This guide does not modify Input until `--apply` is used. Even with `--apply`, +the tool does not click in the interface and does not patch Input's +configuration directly. + +## 1. Check the repository ```sh git status --short @@ -12,47 +14,46 @@ npm ci --no-audit --no-fund npm run check ``` -Préserver toute modification sans rapport. Les données locales vont sous -`.local/`. +Preserve any unrelated changes. Local data goes under `.local/`. -## 2. Sonde générale en lecture seule +## 2. Read-only general probe ```sh ./scripts/probe-macos.sh node scripts/input-layer.mjs doctor --json ``` -La première commande lit les versions et propriétés HID non uniques. La seconde -cherche Input, Claude et les emplacements de configuration candidats. +The first command reads versions and non-unique HID properties. The second looks +for Input, Claude and the candidate configuration locations. -Comparer avec +Compare against [`local-observation-2026-07-27.md`](codex-micro/local-observation-2026-07-27.md). -## 3. Comprendre les fichiers V1 +## 3. Understand the V1 files -- [`manifest.json`](../profiles/claude-shortcuts/manifest.json) : compatibilité, - preuve, préservation et installation ; -- [`mapping.json`](../profiles/claude-shortcuts/mapping.json) : positions, - raccourcis, couleur et AppSense ; -- [`layout.svg`](../profiles/claude-shortcuts/assets/layout.svg) : aperçu ; -- [`macos.example.json`](../profiles/claude-shortcuts/macos.example.json) : - contrat logique historique aligné sur V1. +- [`manifest.json`](../profiles/claude-shortcuts/manifest.json): compatibility, + proof, preservation and installation; +- [`mapping.json`](../profiles/claude-shortcuts/mapping.json): positions, + shortcuts, colour and AppSense; +- [`layout.svg`](../profiles/claude-shortcuts/assets/layout.svg): preview; +- [`macos.example.json`](../profiles/claude-shortcuts/macos.example.json): + historical logical contract, aligned with V1. -Le manifeste communautaire n'est pas un export Input. Le parcours principal -transforme localement un export officiel `*-profile.json` d'Input `0.17.3`. -L'étude du format `*-layer.json` d'Input `0.17.2` reste une preuve historique. +The community manifest is not an Input export. The main path transforms an +official Input `0.17.3` `*-profile.json` export locally. The study of the Input +`0.17.2` `*-layer.json` format remains historical evidence. -## 4. Exporter et sauvegarder avant toute modification +## 4. Export and back up before any change -Dans Input : +In Input: -1. vérifier qu'il existe exactement un layer `Claude`, hors index `0` ; -2. vérifier que ce layer est déjà lié à Claude Desktop avec AppSense ; -3. inventorier visuellement les autres profils, layers et liens ; -4. utiliser **Export Profile** ; -5. quitter Input. +1. check that exactly one `Claude` layer exists, outside index `0`; +2. check that this layer is already linked to Claude Desktop with AppSense; +3. visually inventory the other profiles, layers and links; +4. use **Export Profile**; +5. quit Input. -Puis : +Then: ```sh node scripts/input-layer.mjs backup \ @@ -60,9 +61,9 @@ node scripts/input-layer.mjs backup \ --json ``` -La sauvegarde doit être vérifiée avant l'installation. +The backup must be verified before installing. -## 5. Inventorier et simuler +## 5. Inventory and simulate ```sh node scripts/input-layer.mjs inventory \ @@ -77,12 +78,12 @@ node scripts/input-layer.mjs install \ --json ``` -Le plan doit protéger l'index `0`, sélectionner exactement le layer `Claude` -existant et refuser son absence ou sa duplication. +The plan must protect index `0`, select exactly the existing `Claude` layer, and +refuse both its absence and its duplication. -## 6. Installation guidée +## 6. Guided installation -Lire [`installation.md`](installation.md), puis seulement après revue : +Read [`installation.md`](installation.md), then — only after review: ```sh node scripts/input-layer.mjs install \ @@ -93,7 +94,7 @@ node scripts/input-layer.mjs install \ --json ``` -Générer ensuite le nouveau profile sans modifier la sauvegarde source : +Then generate the new profile without modifying the source backup: ```sh npm run build:profile -- \ @@ -101,20 +102,20 @@ npm run build:profile -- \ "$HOME/Downloads/Claude-macOS-profile.json" ``` -Importer `Claude-macOS-profile.json` avec **Add New**. Ne jamais utiliser -`Reset settings`, remplacer l'index `0` ou recréer un lien AppSense. +Import `Claude-macOS-profile.json` with **Add New**. Never use +`Reset settings`, never replace index `0`, and never recreate an AppSense link. -## 7. Validation et retour arrière +## 7. Validation and rollback -Tester AppSense, les quatre touches, le cadran, le joystick, la perte de focus -et le redémarrage. L'export de layer reste une validation de publication -optionnelle, distincte du profile généré. +Test AppSense, the four keys, the dial, the joystick, focus loss and a restart. +The layer export stays an optional publication validation, distinct from the +generated profile. -Le rollback principal consiste à réimporter le `*-profile.json` original dans -Input. La copie brute du stockage n'est qu'un recours secondaire explicite. +The primary rollback is re-importing the original `*-profile.json` into Input. +Copying the storage raw is only an explicit secondary fallback. -## 8. Piste BLE séparée +## 8. Separate BLE track -Lire [`ble/protocol.md`](../ble/protocol.md) et -[`ble/feasibility.md`](../ble/feasibility.md). Aucun résultat du preset HID ne -prouve la compatibilité Hardware Buddy. +Read [`ble/protocol.md`](../ble/protocol.md) and +[`ble/feasibility.md`](../ble/feasibility.md). No result from the HID preset +proves Hardware Buddy compatibility. diff --git a/docs/installation.md b/docs/installation.md index eddc697..55ff59f 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -1,40 +1,42 @@ -# Générer et installer le profile Claude V1 +[English](installation.md) · [Français](fr/installation.md) -Le parcours V1 transforme localement un export officiel d'Input `0.17.3`. Le -profile source doit contenir exactement un layer `Claude`, hors index `0` et -déjà lié à Claude Desktop avec AppSense. Le générateur modifie uniquement ce -layer dans une copie et produit un nouveau `*-profile.json`. +# Generating and installing the Claude V1 profile -Il ne clique jamais à votre place, ne lance pas `Reset settings`, ne flashe -aucun firmware et ne modifie aucun raccourci système. +The V1 path transforms an official Input `0.17.3` export locally. The source +profile must contain exactly one `Claude` layer, outside index `0` and already +linked to Claude Desktop with AppSense. The generator only modifies that layer, +in a copy, and produces a new `*-profile.json`. -## Parcours GUI recommandé +It never clicks on your behalf, never runs `Reset settings`, never flashes +firmware, and never modifies a system shortcut. -Depuis la racine du dépôt : +## Recommended GUI path + +From the root of the repository: ```sh npm run configure ``` -La première ouverture peut préparer les dépendances verrouillées de -`prototype/`. Le configurateur s'ouvre ensuite localement dans le navigateur. +The first launch may prepare `prototype/`'s locked dependencies. The +configurator then opens locally in the browser. -1. exporter le profile actif depuis Work Louder Input ; -2. déposer ce `*-profile.json` dans le configurateur ; -3. vérifier l'unique layer `Claude`, son lien AppSense et l'index natif `0` ; -4. personnaliser les 13 switches, la rotation du cadran et le joystick ; seul - le capteur tactile de changement de layer reste réservé ; -5. télécharger `Claude-macOS-profile.json` ; -6. l'importer avec **Add New**, sans remplacer le profile source. +1. export the active profile from Work Louder Input; +2. drop that `*-profile.json` into the configurator; +3. check the single `Claude` layer, its AppSense link and the native index `0`; +4. customise the 13 switches, the dial rotation and the joystick; only the + layer-switching touch sensor stays reserved; +5. download `Claude-macOS-profile.json`; +6. import it with **Add New**, without replacing the source profile. -La génération refuse un mauvais appareil, un layer Claude absent ou dupliqué, -une cible à l'index `0` et un lien AppSense manquant. Elle conserve le layer -natif, les autres layers, les autres profils et les autres liens AppSense. Le -reste de ce guide décrit le même parcours avec les contrôles CLI détaillés. +Generation refuses a wrong device, a missing or duplicated Claude layer, a target +at index `0`, and a missing AppSense link. It preserves the native layer, the +other layers, the other profiles and the other AppSense links. The rest of this +guide describes the same path with the detailed CLI checks. -## 1. Vérifier le dépôt +## 1. Check the repository -Depuis la copie locale : +From the local copy: ```sh cd claude-codex-micro @@ -43,44 +45,44 @@ npm ci --no-audit --no-fund npm run check ``` -Ne mettez pas de côté et ne supprimez pas les modifications sans rapport. Les -fichiers privés créés par les outils restent sous `.local/`, ignoré par Git. +Do not stash or delete unrelated changes. Private files created by the tools stay +under `.local/`, which Git ignores. -## 2. Inspecter l'environnement sans écriture +## 2. Inspect the environment without writing ```sh node scripts/input-layer.mjs doctor --json ``` -Vérifier notamment : +Check in particular: -- Input `0.17.3`, bundle `it.focusense.input-app` ; -- Claude, bundle `com.anthropic.claudefordesktop` ; -- le chemin de configuration Input détecté ; -- le firmware `v0.4.1` dans l'écran Setup d'Input ; -- le layer Codex natif à l'index `0`. +- Input `0.17.3`, bundle `it.focusense.input-app`; +- Claude, bundle `com.anthropic.claudefordesktop`; +- the detected Input configuration path; +- firmware `v0.4.1` in Input's Setup screen; +- the native Codex layer at index `0`. -`doctor` ne modifie rien. Si l'installation utilise un chemin personnalisé, -définir d'abord `WORK_LOUDER_INPUT_USER_DATA` sur ce chemin. Un -`--config-root` arbitraire est refusé. +`doctor` modifies nothing. If the installation uses a custom path, set +`WORK_LOUDER_INPUT_USER_DATA` to that path first. An arbitrary `--config-root` +is refused. -## 3. Exporter le profile original avec Input +## 3. Export the original profile with Input -Cette étape est la sauvegarde de référence pour le keymap du périphérique. +This step is the reference backup for the device's keymap. -1. ouvrir Input et sélectionner le profile actuellement actif ; -2. vérifier qu'il existe exactement un layer `Claude`, hors index `0`, et qu'il - possède déjà le lien AppSense Claude ; -3. inventorier visuellement les autres profils, layers et liens ; -4. utiliser le menu du profile puis **Export Profile** ; -5. conserver le fichier `*-profile.json` dans un emplacement local ; -6. quitter complètement Input avant de copier sa configuration locale. +1. open Input and select the currently active profile; +2. check that exactly one `Claude` layer exists, outside index `0`, and that it + already has the Claude AppSense link; +3. visually inventory the other profiles, layers and links; +4. use the profile menu, then **Export Profile**; +5. keep the `*-profile.json` file in a local location; +6. quit Input completely before copying its local configuration. -Le profil exporté peut contenir des informations privées. Ne jamais le committer. +The exported profile can contain private information. Never commit it. -## 4. Créer et vérifier la sauvegarde restaurable +## 4. Create and verify the restorable backup -Exemple : +Example: ```sh node scripts/input-layer.mjs backup \ @@ -90,26 +92,26 @@ node scripts/input-layer.mjs backup \ --json ``` -La commande : +The command: -- refuse de continuer si Input tourne ; -- copie la configuration reconnue vers `.local/input-backups//` ; -- copie l'export officiel du profile ; -- exclut uniquement les caches jetables ; -- calcule une somme SHA-256 pour chaque fichier ; -- relit immédiatement la sauvegarde. +- refuses to continue if Input is running; +- copies the recognised configuration to `.local/input-backups//`; +- copies the official profile export; +- excludes only disposable caches; +- computes a SHA-256 sum for every file; +- reads the backup back immediately. -Vérification indépendante : +Independent verification: ```sh node scripts/input-layer.mjs verify-backup \ - --backup .local/input-backups/ \ + --backup .local/input-backups/ \ --json ``` -Le résultat doit contenir `"ok": true`. +The result must contain `"ok": true`. -## 5. Générer l'inventaire local +## 5. Generate the local inventory ```sh node scripts/input-layer.mjs inventory \ @@ -118,17 +120,17 @@ node scripts/input-layer.mjs inventory \ --json ``` -Contrôler dans le résultat : +Check in the result: -- `protectedLayerIndexes: [0]` ; -- le nom du layer natif à l'index `0` ; -- exactement un layer nommé `Claude`, avec un index supérieur à `0` ; -- la présence d'un champ AppSense candidat sur ce layer. +- `protectedLayerIndexes: [0]`; +- the name of the native layer at index `0`; +- exactly one layer named `Claude`, with an index above `0`; +- the presence of a candidate AppSense field on that layer. -L'outil ne publie pas les valeurs AppSense trouvées ; il signale uniquement les -chemins de champs candidats. L'inventaire détaillé reste local. +The tool does not publish the AppSense values it finds; it only reports the +candidate field paths. The detailed inventory stays local. -## 6. Simuler l'installation +## 6. Simulate the installation ```sh node scripts/input-layer.mjs install \ @@ -138,16 +140,16 @@ node scripts/input-layer.mjs install \ --json ``` -Le dry-run doit : +The dry run must: -- sélectionner l'unique layer `Claude` existant ; -- protéger l'index `0` ; -- refuser l'absence ou la duplication de `Claude` ; -- annoncer `guided-ui` et la transformation locale du profile. +- select the single existing `Claude` layer; +- protect index `0`; +- refuse a missing or duplicated `Claude`; +- announce `guided-ui` and the local transformation of the profile. -## 7. Préparer la session réelle +## 7. Prepare the real session -Quitter Input, puis : +Quit Input, then: ```sh node scripts/input-layer.mjs install \ @@ -158,13 +160,13 @@ node scripts/input-layer.mjs install \ --json ``` -Avant d'ouvrir Input, la commande crée et vérifie une nouvelle sauvegarde de -sécurité. Elle écrit ensuite un état de session sous `.local/sessions/`. Un -second lancement non restauré est refusé. +Before opening Input, the command creates and verifies a new safety backup. It +then writes a session state under `.local/sessions/`. A second, unrestored run is +refused. -## 8. Générer le profile local +## 8. Generate the local profile -Input fermé, exécuter : +With Input closed, run: ```sh npm run build:profile -- \ @@ -172,129 +174,127 @@ npm run build:profile -- \ "$HOME/Downloads/Claude-macOS-profile.json" ``` -Le fichier de sortie est créé sans écraser un fichier existant. Le générateur -refuse un mauvais appareil, un layer Claude absent ou dupliqué, un layer cible -à l'index `0` et un lien AppSense manquant. +The output file is created without overwriting an existing one. The generator +refuses a wrong device, a missing or duplicated Claude layer, a target layer at +index `0`, and a missing AppSense link. -Le mapping généré est : +The generated mapping is: -| Position physique | Action | +| Physical position | Action | | --- | --- | -| rangée des quatre touches carrées, gauche | `⌘N` | -| même rangée, deuxième | `⌘D` | -| même rangée, troisième | `⌘⇧D` | -| même rangée, droite | `Esc` | -| molette supérieure gauche, horaire / antihoraire | **Effort Claude** `+1` / `−1` | -| clic de la molette supérieure gauche | configurable séparément | -| joystick supérieur droit, sans clic | quatre flèches | - -La rotation de la molette est en mode **Effort Claude** par défaut. Chaque cran -envoie `⌘⇧E`, attend l'ouverture du sélecteur, puis `←` ou `→` et `Esc`. Il passe -donc au niveau d'effort disponible précédent ou suivant sans utiliser `Entrée`. -Les niveaux réellement proposés dépendent du modèle et de la version de Claude -Desktop. - -Le GUI permet de la remettre sur le défilement page par page, le défilement ligne -par ligne, le volume, ou de la désassigner. - -`⌘⇧E` est une bascule : le `Esc` final est obligatoire, sans lui le cran suivant -refermerait le sélecteur au lieu de l'ouvrir. - -La macro porte donc deux temporisations, pour un coût d'environ 90 ms par cran : - -- **80 ms** sur la libération de ⌘, le temps que le sélecteur apparaisse. En - dessous de 40 ms la flèche part avant que le sélecteur ait le focus et le - changement de niveau est perdu sans message d'erreur ; -- **10 ms** sur `Esc`, le temps que le sélecteur peigne le niveau atteint avant - de se refermer. Sans elle le sélecteur ne fait que clignoter et le niveau - choisi n'est jamais affiché. - -Ces temporisations sont exécutées par le firmware, et tourner vite pendant qu'une -macro est en cours peut faire perdre des crans. Le mode convient à des -ajustements de quelques niveaux, pas à un balayage continu. - -Le calibrage et ce qui reste non prouvé sont détaillés dans +| row of four square keys, left | `⌘N` | +| same row, second | `⌘D` | +| same row, third | `⌘⇧D` | +| same row, right | `Esc` | +| upper-left wheel, clockwise / counterclockwise | **Claude Effort** `+1` / `−1` | +| upper-left wheel press | configurable separately | +| upper-right joystick, no press | four arrows | + +Wheel rotation is in **Claude Effort** mode by default. Each notch sends `⌘⇧E`, +waits for the picker to open, then `←` or `→` and `Esc`. It therefore moves to +the previous or next available effort level without using `Enter`. The levels +actually offered depend on the model and on the Claude Desktop version. + +The GUI lets you set it back to page-by-page scrolling, line-by-line scrolling, +volume, or unassign it. + +`⌘⇧E` is a toggle: the final `Esc` is mandatory — without it the next notch would +close the picker instead of opening it. + +The macro therefore carries two delays, for a cost of roughly 90ms per notch: + +- **80ms** on the ⌘ release, the time for the picker to appear. Below 40ms the + arrow leaves before the picker has focus and the level change is lost with no + error message; +- **10ms** on `Esc`, the time for the picker to paint the level reached before + closing. Without it the picker only flickers and the chosen level is never + displayed. + +These delays are executed by the firmware, and turning fast while a macro is +running can lose notches. The mode suits adjustments of a few levels, not a +continuous sweep. + +The calibration and what remains unproven are detailed in [`docs/research/effort-wheel-calibration.md`](research/effort-wheel-calibration.md). -Le schéma de référence est +The reference diagram is [`profiles/claude-shortcuts/assets/layout.svg`](../profiles/claude-shortcuts/assets/layout.svg). -## 9. Importer sans recréer AppSense +## 9. Import without recreating AppSense -1. dans Input, choisir **Add New** ; -2. sélectionner `Claude-macOS-profile.json` ; -3. activer le nouveau profile ; -4. vérifier que le layer `Claude` conserve son lien AppSense ; -5. ne toucher à aucun autre lien AppSense. +1. in Input, choose **Add New**; +2. select `Claude-macOS-profile.json`; +3. activate the new profile; +4. check that the `Claude` layer keeps its AppSense link; +5. do not touch any other AppSense link. -Le générateur conserve l'identifiant AppSense local sans le publier. Si le lien -est absent, revenir au profile original et le créer manuellement avant un -nouvel export. +The generator preserves the local AppSense identifier without publishing it. If +the link is missing, go back to the original profile and create it by hand before +a new export. -### Forcer un lien, ou en ajouter un second +### Forcing a link, or adding a second one -Deux options écrivent une référence AppSense au lieu de seulement reprendre celle -de la sauvegarde : +Two options write an AppSense reference instead of only carrying over the one +from the backup: ```bash -node scripts/build-input-profile.mjs sauvegarde.json sortie.json --app-sense-id=0 --base-layer-app-sense-id=2 +node scripts/build-input-profile.mjs backup.json output.json --app-sense-id=0 --base-layer-app-sense-id=2 ``` -`--app-sense-id=` force la référence du layer `Claude` et dispense d'en exiger -une dans la sauvegarde : c'est le cas d'usage « réparer un lien perdu ». - -`--base-layer-app-sense-id=` lie le layer natif à une **seconde** application. -C'est le seul moyen de quitter automatiquement le layer `Claude`, puisqu'AppSense -n'a pas de retour : la sortie est elle-même une entrée dans un autre layer lié. -Le keymap natif reste intact au keycode près, seul le lien est ajouté, et le -générateur refuse que les deux layers pointent vers la même entrée. - -**Ces options écrivent une référence, jamais une entrée.** Un fichier -`*-profile.json` ne transporte pas la table `linkedApps` : l'entrée visée doit -déjà exister sur la carte, créée une fois dans l'UI d'Input avec `Auto detect`. -Une référence vers une entrée absente s'importe **sans erreur** et laisse -AppSense mort sans le signaler. Relever les identifiants réels avant, dans -`~/Library/Logs/input/main.log`, où `sending device config :` est suivi du JSON -complet — et n'exécuter `Auto detect` qu'une seule fois par application, il ne -dédoublonne pas. - -Le GUI expose les deux mêmes réglages dans l'étape **Vérifier et générer**, section -« Liens AppSense ». Laisser les champs vides revient à ne pas passer les options : -les liens de la sauvegarde sont alors repris tels quels. - -Détail du comportement mesuré : +`--app-sense-id=` forces the `Claude` layer's reference and removes the need +to require one in the backup: this is the "repair a lost link" use case. + +`--base-layer-app-sense-id=` links the native layer to a **second** +application. That is the only way to leave the `Claude` layer automatically, +since AppSense has no return path: the exit is itself an entry into another +linked layer. The native keymap stays intact down to the keycode, only the link +is added, and the generator refuses to have both layers point at the same entry. + +**These options write a reference, never an entry.** A `*-profile.json` does not +carry the `linkedApps` table: the target entry must already exist on the board, +created once in Input's UI with `Auto detect`. A reference to a missing entry +imports **without error** and leaves AppSense dead without saying so. Read the +real identifiers first, in `~/Library/Logs/input/main.log`, where +`sending device config :` is followed by the full JSON — and run `Auto detect` +only once per application, since it does not deduplicate. + +The GUI exposes the same two settings in the **Verify and generate** step, under +"AppSense links". Leaving the fields empty is the same as not passing the +options: the backup's links are then carried over as they are. + +Details of the measured behaviour: [`docs/research/appsense-behavior.md`](research/appsense-behavior.md). -## 10. Validation matérielle - -Tester dans une conversation sans enjeu, une action à la fois : - -- [ ] Claude au premier plan active le layer existant, pas l'index `0` ; -- [ ] `⌘N` ouvre une nouvelle conversation ; -- [ ] `⌘D` active le mode vocal ; -- [ ] `⌘⇧D` affiche ou masque le diff ; -- [ ] `Esc` annule ou ferme uniquement le contexte attendu ; -- [ ] cadran horaire descend et antihoraire monte ; -- [ ] en mode Effort Claude, chaque cran change d'un seul niveau et referme le - sélecteur sans envoyer de prompt ; -- [ ] en mode Effort Claude, le niveau atteint est lisible avant la fermeture, et - un cran déclenché au retour d'une autre application le change bien : c'est - le cas défavorable, où l'échec est silencieux ; -- [ ] joystick émet les quatre flèches ; -- [ ] tous les contrôles non utilisés restent sans action dangereuse ; -- [ ] revenir dans Claude réactive le layer ; -- [ ] passer au Finder **laisse** le layer Claude actif — c'est le comportement - attendu, pas un défaut : AppSense n'a pas de retour, voir +## 10. Hardware validation + +Test in a low-stakes conversation, one action at a time: + +- [ ] Claude in the foreground activates the existing layer, not index `0`; +- [ ] `⌘N` opens a new conversation; +- [ ] `⌘D` activates voice mode; +- [ ] `⌘⇧D` shows or hides the diff; +- [ ] `Esc` cancels or closes only the expected context; +- [ ] clockwise dial goes down and counterclockwise goes up; +- [ ] in Claude Effort mode, each notch changes exactly one level and closes the + picker without sending a prompt; +- [ ] in Claude Effort mode, the level reached is readable before the picker + closes, and a notch triggered when coming back from another application + does change it: that is the unfavourable case, where failure is silent; +- [ ] the joystick emits the four arrows; +- [ ] every unused control stays free of dangerous actions; +- [ ] coming back into Claude re-activates the layer; +- [ ] switching to the Finder **leaves** the Claude layer active — that is the + expected behaviour, not a defect: AppSense has no return path, see [`docs/research/appsense-behavior.md`](research/appsense-behavior.md). - Vérifier plutôt qu'aucune action du layer n'est dangereuse hors de Claude ; -- [ ] quitter puis relancer Input conserve la configuration ; -- [ ] aucun autre profile, layer ou lien AppSense n'a changé. + Check instead that no action on the layer is dangerous outside Claude; +- [ ] quitting and relaunching Input preserves the configuration; +- [ ] no other profile, layer or AppSense link has changed. -Ne tester ni Entrée, ni permission, ni suppression, ni push, ni déploiement. +Do not test Enter, permissions, deletion, push or deployment. -## 11. Capturer un éventuel artefact layer partageable +## 11. Capturing a shareable layer artefact, if any -Après validation, utiliser **Export layer** dans Input, puis : +After validation, use **Export layer** in Input, then: ```sh node scripts/input-layer.mjs sanitize-export \ @@ -302,60 +302,58 @@ node scripts/input-layer.mjs sanitize-export \ --output profiles/claude-shortcuts/artifacts/claude-desktop-macos-layer.json ``` -Le sanitizer refuse les chemins absolus, ports, adresses matérielles, secrets, -identifiants AppSense et appareils autres que `codex_micro`. Après sanitation, -enregistrer le SHA-256 exact dans `manifest.json`. Le validateur compare aussi -les actions, la molette et le joystick au mapping canonique. +The sanitiser refuses absolute paths, ports, hardware addresses, secrets, +AppSense identifiers and devices other than `codex_micro`. After sanitisation, +record the exact SHA-256 in `manifest.json`. The validator also compares the +actions, the wheel and the joystick against the canonical mapping. -Avant de promouvoir le statut : importer cette copie dans une configuration -isolée, vérifier le mapping, tenter un second import et restaurer le profile -original. +Before promoting the status: import that copy into an isolated configuration, +verify the mapping, attempt a second import, and restore the original profile. -## 12. Retour arrière +## 12. Rollback -Simulation : +Simulation: ```sh node scripts/input-layer.mjs rollback \ - --backup .local/input-backups/ \ + --backup .local/input-backups/ \ --dry-run \ --json ``` -Méthode principale : +Primary method: -1. ouvrir Input ; -2. utiliser **Import Profile** ; -3. sélectionner le `*-profile.json` dans le dossier - `official-profile-export` de la sauvegarde ; -4. remettre ce profile comme profile courant ; -5. vérifier le layer Codex, chaque autre layer et chaque lien AppSense ; -6. relancer Input et refaire le contrôle. +1. open Input; +2. use **Import Profile**; +3. select the `*-profile.json` in the backup's `official-profile-export` folder; +4. set that profile back as the current one; +5. check the Codex layer, every other layer and every AppSense link; +6. relaunch Input and repeat the check. -Après le réimport officiel et la vérification du périphérique, la session peut -être marquée comme restaurée afin qu'une future installation ne soit plus -bloquée par l'état précédent : +After the official re-import and the device check, the session can be marked as +restored, so that a future installation is no longer blocked by the previous +state: ```sh node scripts/input-layer.mjs rollback \ - --backup .local/input-backups/ \ + --backup .local/input-backups/ \ --session .local/sessions/claude-desktop-macos-codex-micro-v1.json \ --apply \ --confirm-official-profile-import \ --json ``` -Cette option enregistre une confirmation de l'utilisateur ; elle ne remplace -pas la vérification matérielle. +That option records a confirmation from the user; it does not replace the +hardware check. -La restauration brute du stockage applicatif reste un recours secondaire et -non une preuve de restauration du périphérique. Elle exige volontairement les -options explicites suivantes, Input fermé. `--config-root` doit correspondre -exactement à un chemin Input détecté ou à `WORK_LOUDER_INPUT_USER_DATA` : +Restoring the application storage raw remains a secondary fallback and not proof +that the device was restored. It deliberately requires the following explicit +options, with Input closed. `--config-root` must match exactly a detected Input +path or `WORK_LOUDER_INPUT_USER_DATA`: ```sh node scripts/input-layer.mjs rollback \ - --backup .local/input-backups/ \ + --backup .local/input-backups/ \ --apply \ --restore-storage \ --acknowledge-unverified-storage-restore \ @@ -363,7 +361,7 @@ node scripts/input-layer.mjs rollback \ --json ``` -Avant de recopier la sauvegarde, l'outil renomme atomiquement le dossier Input -courant en copie de sécurité `*.before-codex-restore-*`. En cas d'échec de la -copie, ce dossier est remis en place. Cette voie reste secondaire : ne jamais -utiliser `Reset settings` pour le retour arrière. +Before copying the backup back, the tool atomically renames the current Input +folder to a safety copy named `*.before-codex-restore-*`. If the copy fails, that +folder is put back. This path stays secondary: never use `Reset settings` to roll +back. diff --git a/docs/publishing-checklist.md b/docs/publishing-checklist.md index 9d855a5..a412415 100644 --- a/docs/publishing-checklist.md +++ b/docs/publishing-checklist.md @@ -1,57 +1,60 @@ -# Checklist de publication +[English](publishing-checklist.md) · [Français](fr/publishing-checklist.md) -Le dépôt public et la publication d'un preset installable sont deux décisions -distinctes. Une proposition peut être utile et testée automatiquement sans être -présentée comme un layer matériel validé. +# Publishing checklist -## Dépôt public — effectué +Making the repository public and publishing an installable preset are two +distinct decisions. A proposal can be useful and automatically tested without +being presented as a validated hardware layer. -- [x] Licence MIT et copyright 2026 Thanh Chau. -- [x] Absence d'affiliation à Work Louder et Anthropic. -- [x] Aucun firmware, asset propriétaire ou export utilisateur brut. -- [x] `main` comme branche par défaut. -- [x] Modèles GitHub de contribution. +## Public repository — done -## Contrôles applicables à chaque contribution +- [x] MIT licence and copyright 2026 Thanh Chau. +- [x] No affiliation with Work Louder or Anthropic. +- [x] No firmware, proprietary asset or raw user export. +- [x] `main` as the default branch. +- [x] GitHub contribution templates. -- [ ] `npm run check` réussi. -- [ ] `npm ci --no-audit --no-fund` utilise le lockfile sans modifier l'arbre. -- [ ] `git diff --check` réussi. -- [ ] Sources externes vérifiées manuellement. -- [ ] Niveau de preuve exact dans le manifeste et le README. -- [ ] Layer natif à l'index `0` explicitement protégé. -- [ ] Aucun envoi, permission, suppression, push, déploiement ou commande - destructive mappé par défaut. -- [ ] BLE toujours indiqué comme non prouvé sans preuves GATT et firmware. +## Checks that apply to every contribution -## Confidentialité +- [ ] `npm run check` passes. +- [ ] `npm ci --no-audit --no-fund` uses the lockfile without changing the tree. +- [ ] `git diff --check` passes. +- [ ] External sources verified by hand. +- [ ] Exact level of proof in the manifest and the README. +- [ ] Native layer at index `0` explicitly protected. +- [ ] No send, permission, deletion, push, deployment or destructive command + mapped by default. +- [ ] BLE still marked as unproven without GATT and firmware evidence. -- [ ] Aucun secret, jeton, clé ou identifiant de compte. -- [ ] Aucune adresse Bluetooth, numéro de série, port ou identifiant matériel. -- [ ] Aucun chemin utilisateur absolu. -- [ ] Aucune capture ou log contenant des données personnelles. -- [ ] `.local/`, `work/` et `outputs/` ignorés par Git. -- [ ] Tout `*-layer.json` passé par `sanitize-export` et revu manuellement. +## Privacy -## Portes du preset Claude V1 +- [ ] No secret, token, key or account identifier. +- [ ] No Bluetooth address, serial number, port or hardware identifier. +- [ ] No absolute user path. +- [ ] No screenshot or log containing personal data. +- [ ] `.local/`, `work/` and `outputs/` ignored by Git. +- [ ] Every `*-layer.json` run through `sanitize-export` and reviewed by hand. -- [x] Flux officiels Import/Export layer et profile identifiés dans Input +## Gates for the Claude V1 preset + +- [x] Official layer and profile Import/Export flows identified in Input `0.17.2`. -- [x] Manifeste, mapping, schémas et représentation visuelle publiables. -- [x] Sauvegarde, vérification SHA-256, dry-run, sélection unique et rollback - testés sur copies isolées. -- [x] Configuration locale réelle inventoriée sans publier les valeurs AppSense. -- [x] Export officiel du profile d'origine conservé hors Git. -- [x] Unique layer Claude confirmé ; index `0` comparé avant/après. -- [x] Profile Input `0.17.3` généré localement avec AppSense conservé. -- [ ] Positions physiques et identifiants Input vérifiés. -- [ ] AppSense, touches, cadran, joystick et perte de focus testés. -- [ ] Persistance après redémarrage d'Input vérifiée. -- [ ] Vrai `*-layer.json` exporté, assaini et ajouté avec sa somme SHA-256. -- [ ] SHA-256 déclaré identique et contenu conforme au mapping canonique. -- [ ] Import dans une configuration isolée et second import testés. -- [ ] Profile original réimporté et périphérique vérifié. -- [ ] Matrice de compatibilité mise à jour avec les résultats. - -Tant que les cases matérielles restent ouvertes, le preset conserve le statut -`hardware-observed` et ne peut pas être présenté comme entièrement validé. +- [x] Manifest, mapping, schemas and visual representation publishable. +- [x] Backup, SHA-256 verification, dry run, single selection and rollback + tested on isolated copies. +- [x] Real local configuration inventoried without publishing the AppSense + values. +- [x] Official export of the original profile kept out of Git. +- [x] Single Claude layer confirmed; index `0` compared before and after. +- [x] Input `0.17.3` profile generated locally with AppSense preserved. +- [ ] Physical positions and Input identifiers verified. +- [ ] AppSense, keys, dial, joystick and focus loss tested. +- [ ] Persistence after an Input restart verified. +- [ ] Real `*-layer.json` exported, sanitised and added with its SHA-256 sum. +- [ ] Declared SHA-256 identical and content matching the canonical mapping. +- [ ] Import into an isolated configuration and a second import tested. +- [ ] Original profile re-imported and device verified. +- [ ] Compatibility matrix updated with the results. + +As long as the hardware boxes stay open, the preset keeps the +`hardware-observed` status and cannot be presented as fully validated. diff --git a/docs/research/appsense-behavior.md b/docs/research/appsense-behavior.md index fcdc0b5..ccba3af 100644 --- a/docs/research/appsense-behavior.md +++ b/docs/research/appsense-behavior.md @@ -1,133 +1,134 @@ -# AppSense — comportement réel mesuré sur Codex Micro +[English](appsense-behavior.md) · [Français](../fr/research/appsense-behavior.md) + +# AppSense — real behaviour measured on the Codex Micro ## Verdict -**AppSense n'a pas de retour.** C'est un ensemble de règles application → layer, -et chaque règle est une transition **aller**. Il n'existe ni layer par défaut, ni -repli, ni désactivation quand l'application liée perd le focus. +**AppSense has no return path.** It is a set of application → layer rules, and +every rule is a **one-way** transition. There is no default layer, no fallback, +and no deactivation when the linked application loses focus. -Conséquence à retenir avant de concevoir un layer : quitter Claude pour une -application non liée laisse la carte sur le layer Claude, indéfiniment. Le layer -doit donc être sûr en dehors de Claude, puisqu'il y restera actif. +The consequence to keep in mind before designing a layer: leaving Claude for an +unlinked application leaves the board on the Claude layer, indefinitely. The +layer therefore has to be safe outside Claude, since that is where it will stay +active. -## Le modèle, et ce qu'il explique +## The model, and what it explains -| application au premier plan | règle | effet | +| foreground application | rule | effect | | --- | --- | --- | -| liée à un layer | trouvée | bascule vers ce layer | -| non liée | aucune | **rien ne se passe, la carte reste où elle est** | - -Un « aller-retour » entre deux applications n'est donc pas un aller suivi d'un -retour : c'est **deux allers**, qui exigent que les deux applications soient -liées chacune à son layer. Le layer d'indice `0` peut parfaitement être une -cible, contrairement à ce qu'on pourrait croire — mais seulement si une -application lui est explicitement liée. - -Mesures qui établissent le modèle, sur firmware `v0.4.1` et Input `0.17.3` : - -- depuis le layer de base, mettre Claude au premier plan bascule bien vers le - layer `Claude` — l'aller fonctionne ; -- avec `com.openai.codex` lié au layer de base, alterner Claude et ChatGPT fait - bien alterner les deux layers ; -- avec la même configuration, quitter Claude pour le Finder ne change **rien** : - le layer `Claude` reste actif. - -## Limite pratique - -Six layers au maximum, et un seul `linkedAppId` par layer : au plus **six -applications** peuvent déclencher une bascule. Toute autre application laisse la -carte sur le dernier layer activé. - -Pour un poste où l'on navigue entre plus d'applications que ça, il n'y a que le -capteur tactile, qui fait défiler les layers à la main. - -## Statut chez le fabricant - -Le repli attendu est une **fonctionnalité absente, pas un bug**. Elle est -demandée sur le board de feedback Work Louder en statut `Planned`, sans ETA, et -un administrateur l'a confirmé : - -> nous prévoyons de l'implémenter mais nous n'avons pas encore d'ETA - -Aucune note de version d'Input, de `0.11.0` à `0.18.0-rc.8`, ne mentionne -AppSense, le focus applicatif ou le changement de layer. Mettre Input à jour ne -change donc rien à ce comportement. - -Sources : -et . - -## Mécanique côté hôte, utile au diagnostic - -**AppSense est piloté par l'hôte.** Input observe l'application au premier plan -et pousse un appel JSON-RPC `host.focused_app` vers la carte ; le firmware -consulte alors sa table de liens et bascule. Deux conséquences : - -- **AppSense s'arrête net si Input n'est pas lancé.** Aucune bascule n'a plus - lieu, et la carte se figera sur son dernier layer. « Fermer Input » est donc le - pire contournement possible. -- La détection se fait par **sondage à 1000 ms**, via `osascript`, pas par - abonnement système. Une bascule peut donc prendre jusqu'à une seconde : ne pas - conclure trop vite lors d'un test. - -L'envoi est **inconditionnel** — Input ne consulte pas la table des liens avant -d'émettre, tout le filtrage est côté firmware — et il n'existe aucun message -signifiant « aucune application liée ». Le firmware ne renvoie jamais sur quel -layer il a basculé : la réponse à `host.focused_app` est toujours `null`. - -## Pièges rencontrés pendant l'investigation - -- **`Auto detect` ne dédoublonne pas par processus.** Chaque exécution crée une - nouvelle entrée `linkedApps`. Deux entrées pour la même application, et deux - layers les revendiquant dans deux profils différents, rendent le diagnostic - illisible. N'exécuter `Auto detect` qu'une fois par application. -- **Un fichier `*-profile.json` exporté ne transporte pas la table - `linkedApps`**, seulement les références `linkedAppId` posées sur les layers. - Un profil importé ne peut donc pas créer un lien : l'entrée cible doit déjà - exister, sinon la référence pend et le lien est silencieusement mort. -- **La copie locale `~/Library/Application Support/input/devices//keymap.json` - peut être en retard** sur ce qui a réellement été poussé. La source fiable est - `~/Library/Logs/input/main.log`, où `|device_keymap_service| sending device - config :` est suivi du JSON complet. -- **`device.status.layer_index` est 1-based**, Input le convertit par `r - 1`. - Un `layer_index: 2` désigne le layer d'indice `1`. -- Les erreurs `cannot send, no device connected` accompagnant chaque changement - de focus sont présentes dès le démarrage : Input instancie un client par - transport et seul celui du transport réel répond. Ce n'est pas la panne. - -## Contention avec l'application ChatGPT - -Les deux applications tiennent le même périphérique HID, ouvert en mode non -exclusif : les lectures sont diffusées aux deux, seules les écritures se -disputent, et **la dernière écriture gagne**. On le voit directement dans le log -d'Input, qui reçoit les réponses à des appels `v.oai.rgbcfg` et `v.oai.thstatus` -qu'il n'a jamais émis. - -Conséquence à connaître pour tout témoin visuel : **ChatGPT écrase l'underglow -des layers non-Codex** quel que soit le layer actif — bug confirmé, non corrigé -sur ce modèle. Le **backlight** est la zone qu'il laisse tranquille, donc le seul -indicateur de layer fiable tant que ChatGPT tourne. - -Ce que ChatGPT ne fait pas : il ne repositionne jamais le layer. Il n'est donc -pas la cause du layer collé. - -## Avertissement - -**Ne flasher aucun firmware depuis l'écran de récupération d'Input.** Il propose -Nomad, Knob, KnobF1, Creator Micro V2 et XYZ R2, n'offre **pas** de firmware -Codex Micro et **n'avertit pas** de l'incompatibilité. Un Codex Micro a été -briqué exactement comme ça, et aucun firmware de récupération officiel n'est -publié. - -Source : - -## Ce qui reste non établi - -- L'ordre dans lequel le firmware parcourt sa table de liens, et s'il cherche - dans le profil actif seulement ou dans tous les profils. Pendant - l'investigation, une configuration dont le layer de base référençait un - `linkedAppId` inexistant a semblé fonctionner là où une référence valide - échouait. L'écart n'a pas été reproduit et est vraisemblablement un artefact de - séquence de test, mais il n'est pas expliqué. -- La raison du refus d'import d'un profil à trois layers : rien n'est journalisé - côté processus principal, le motif est dans le log du renderer, accessible par +| linked to a layer | found | switches to that layer | +| not linked | none | **nothing happens, the board stays where it is** | + +A "round trip" between two applications is therefore not one trip out and one +back: it is **two trips out**, which requires both applications to be linked, +each to its own layer. The layer at index `0` can perfectly well be a target, +contrary to what one might assume — but only if an application is explicitly +linked to it. + +Measurements that establish the model, on firmware `v0.4.1` and Input `0.17.3`: + +- from the base layer, bringing Claude to the foreground does switch to the + `Claude` layer — the trip out works; +- with `com.openai.codex` linked to the base layer, alternating between Claude + and ChatGPT does alternate the two layers; +- with the same configuration, leaving Claude for the Finder changes **nothing**: + the `Claude` layer stays active. + +## Practical limit + +Six layers at most, and a single `linkedAppId` per layer: at most **six +applications** can trigger a switch. Any other application leaves the board on +the last activated layer. + +For a machine where you move between more applications than that, there is only +the touch sensor, which cycles the layers by hand. + +## Status at the vendor + +The expected fallback is a **missing feature, not a bug**. It is requested on the +Work Louder feedback board with status `Planned`, no ETA, and an administrator +confirmed it: + +> we plan to implement it but we don't have an ETA yet + +No Input release note, from `0.11.0` to `0.18.0-rc.8`, mentions AppSense, +application focus or layer switching. Updating Input therefore changes nothing +about this behaviour. + +Sources: +and . + +## Host-side mechanics, useful for diagnosis + +**AppSense is driven by the host.** Input watches the foreground application and +pushes a `host.focused_app` JSON-RPC call to the board; the firmware then +consults its link table and switches. Two consequences: + +- **AppSense stops dead if Input is not running.** No switch happens any more, + and the board freezes on its last layer. "Close Input" is therefore the worst + possible workaround. +- Detection is done by **polling at 1000ms**, through `osascript`, not by a + system subscription. A switch can therefore take up to a second: do not + conclude too quickly during a test. + +The send is **unconditional** — Input does not consult the link table before +emitting, all the filtering is on the firmware side — and there is no message +meaning "no linked application". The firmware never reports which layer it +switched to: the response to `host.focused_app` is always `null`. + +## Traps hit during the investigation + +- **`Auto detect` does not deduplicate by process.** Each run creates a new + `linkedApps` entry. Two entries for the same application, and two layers + claiming them across two different profiles, make diagnosis unreadable. Run + `Auto detect` only once per application. +- **An exported `*-profile.json` does not carry the `linkedApps` table**, only + the `linkedAppId` references set on the layers. An imported profile therefore + cannot create a link: the target entry must already exist, otherwise the + reference dangles and the link is silently dead. +- **The local copy + `~/Library/Application Support/input/devices//keymap.json` can lag** + behind what was actually pushed. The reliable source is + `~/Library/Logs/input/main.log`, where `|device_keymap_service| sending device + config :` is followed by the full JSON. +- **`device.status.layer_index` is 1-based**, and Input converts it with `r - 1`. + A `layer_index: 2` designates the layer at index `1`. +- The `cannot send, no device connected` errors accompanying every focus change + are present from startup: Input instantiates one client per transport and only + the one on the real transport answers. That is not the failure. + +## Contention with the ChatGPT application + +Both applications hold the same HID device, opened non-exclusively: reads are +broadcast to both, only writes compete, and **the last write wins**. You can see +it directly in Input's log, which receives responses to `v.oai.rgbcfg` and +`v.oai.thstatus` calls it never made. + +A consequence to know about for any visual indicator: **ChatGPT overwrites the +underglow of non-Codex layers** whatever the active layer — a confirmed bug, not +fixed on this model. The **backlight** is the zone it leaves alone, and therefore +the only reliable layer indicator while ChatGPT is running. + +What ChatGPT does not do: it never repositions the layer. It is therefore not the +cause of the stuck layer. + +## Warning + +**Do not flash any firmware from Input's recovery screen.** It offers Nomad, +Knob, KnobF1, Creator Micro V2 and XYZ R2, does **not** offer Codex Micro +firmware, and does **not** warn about the incompatibility. A Codex Micro was +bricked exactly that way, and no official recovery firmware is published. + +Source: + +## What is still unestablished + +- The order in which the firmware walks its link table, and whether it searches + the active profile only or every profile. During the investigation, a + configuration whose base layer referenced a non-existent `linkedAppId` seemed + to work where a valid reference failed. The discrepancy was not reproduced and + is most likely an artefact of the test sequence, but it is not explained. +- The reason a three-layer profile is refused on import: nothing is logged on the + main process side, the cause is in the renderer log, reachable through `Help > Download Logs`. diff --git a/docs/research/effort-wheel-calibration.md b/docs/research/effort-wheel-calibration.md index cdd0f6b..7b58d72 100644 --- a/docs/research/effort-wheel-calibration.md +++ b/docs/research/effort-wheel-calibration.md @@ -1,142 +1,140 @@ -# Molette Effort Claude — calibrage de la macro +[English](effort-wheel-calibration.md) · [Français](../fr/research/effort-wheel-calibration.md) + +# Claude Effort wheel — macro calibration ## Verdict -La macro du mode Effort porte deux temporisations, pour environ **90 ms par -cran** : `80 ms` sur la libération de ⌘ et `10 ms` sur `Esc`. L'étape de la -flèche reste volontairement à `0`. +The Effort mode macro carries two delays, for roughly **90ms per notch**: `80ms` +on the ⌘ release and `10ms` on `Esc`. The arrow step deliberately stays at `0`. -La première version de cette macro coûtait 900 ms par cran. Le facteur dix ne -vient pas d'avoir essayé plus de valeurs, mais d'avoir changé la **forme** de la -macro pour que chaque mesure devienne interprétable. +The first version of this macro cost 900ms per notch. The factor of ten does not +come from trying more values, but from changing the **shape** of the macro so +that each measurement became interpretable. -## Pourquoi `Esc` est obligatoire +## Why `Esc` is mandatory -`⌘⇧E` est une **bascule**, vérifié sur Claude Desktop : l'envoyer deux fois de -suite ouvre puis referme le sélecteur. +`⌘⇧E` is a **toggle**, verified on Claude Desktop: sending it twice in a row +opens then closes the picker. -Chaque cran doit donc refermer le sélecteur lui-même. Sans le `Esc` final, le -cran suivant refermerait le sélecteur au lieu de l'ouvrir, et le niveau serait -sauté. C'est aussi la cause la plus probable des niveaux perdus lors d'un -balayage rapide : deux macros qui se chevauchent désynchronisent la bascule. +Every notch therefore has to close the picker itself. Without the final `Esc`, +the next notch would close the picker instead of opening it, and the level would +be skipped. That is also the most likely cause of levels lost during a fast +sweep: two overlapping macros desynchronise the toggle. -## Pourquoi la forme est asymétrique +## Why the shape is asymmetric -Input ne documente pas si le champ `delay` d'une étape s'applique **avant** ou -**après** cette étape, et aucune source ne permet de le trancher : l'application -n'exécute jamais les macros, elle écrit `keymap.json` sur la flash de la carte et -le firmware seul interprète le champ. +Input does not document whether a step's `delay` field applies **before** or +**after** that step, and no source settles it: the application never executes the +macros, it writes `keymap.json` to the board's flash and the firmware alone +interprets the field. -Tant que l'attente était répartie sur deux étapes, les deux lectures donnaient au -sélecteur des durées différentes, et chaque essai mesurait donc autre chose que -ce qu'on croyait régler. En posant toute l'attente sur la libération de ⌘ et `0` -sur la flèche, les deux lectures deviennent équivalentes : +As long as the wait was split across two steps, the two readings gave the picker +different durations, and every attempt was therefore measuring something other +than what you thought you were tuning. Putting all the wait on the ⌘ release and +`0` on the arrow makes both readings equivalent: -| lecture | déroulé | attente reçue par le sélecteur | +| reading | sequence | wait the picker receives | | --- | --- | --- | -| « après » | ⌘ relâché, attente, flèche | la constante | -| « avant » | attente, ⌘ relâché, flèche | la constante | +| "after" | ⌘ released, wait, arrow | the constant | +| "before" | wait, ⌘ released, arrow | the constant | -Le délai d'ouverture devient alors exactement égal à la constante. Ne pas -répartir cette attente sur les deux étapes : cela double ce délai sans rien -garantir de plus. Le coût total d'un cran inclut aussi les `10 ms` de retour -visuel portés par `Esc`. +The opening delay then becomes exactly equal to the constant. Do not split this +wait across both steps: that doubles the delay and guarantees nothing more. The +total cost of a notch also includes the `10ms` of visual feedback carried by +`Esc`. -## Calibrage mesuré sur matériel +## Calibration measured on hardware -Échelle testée sur Codex Micro, une valeur par touche, toutes en « effort +1 » -pour que le sens ne soit pas une variable : +Scale tested on the Codex Micro, one value per key, all on "effort +1" so that +direction is not a variable: -| attente d'ouverture | résultat | +| opening wait | result | | --- | --- | -| 120 ms | change le niveau | -| 80 ms | change le niveau | -| 60 ms | change le niveau | -| 40 ms | change le niveau | -| 20 ms | échoue | -| 0 ms | échoue | - -Le plancher est donc entre 20 et 40 ms. La valeur retenue, `80 ms`, double le -plancher mesuré. - -`40 ms` a été essayé en exploitation et jugé moins fiable qu'en test isolé. C'est -cohérent : le plancher a été mesuré sur un sélecteur déjà chaud, alors que le -temps de montage réel dépend de la charge du renderer. **Un plancher n'est pas -une valeur d'exploitation.** En dessous d'environ 100 ms la différence de latence -n'est pas perceptible, alors qu'un niveau perdu l'est immédiatement : la bonne -cible est la plus petite valeur qui ne rate jamais, pas la plus petite qui -marche. - -L'échec en dessous du plancher n'est pas bruyant. La flèche part avant que le -sélecteur ait le focus, et le changement est perdu sans message d'erreur. Toute -baisse doit donc être validée par plusieurs répétitions **et** par une première -ouverture à froid, au retour d'une autre application. - -## Pourquoi `Esc` porte 10 ms - -Sans attente sur cette étape, la flèche et `Esc` sont émis sans écart et Claude -les traite dans le même tour de boucle : le sélecteur s'ouvre et se referme sans -jamais peindre une image montrant le slider à son nouveau niveau. Le niveau -change bien, mais **à l'aveugle** — l'effet visible n'est qu'un clignotement. - -`10 ms` suffisent à laisser passer une image, et le niveau atteint devient -lisible. Cette attente est payée **après** que le niveau a changé : elle allonge -la macro sans retarder son effet. - -Conséquence méthodologique : les 300 ms que portait la première version de la -macro à cet endroit n'étaient pas du temps mort. Elles avaient été supprimées sur -le seul critère de la latence, ce qui a fait perdre le retour visuel sans que le -critère retenu puisse le détecter. - -## Sens de rotation - -La cellule d'encodeur d'indice `0` est **physiquement horaire**, confirmé sur -matériel. C'est l'inverse de ce que suggèrent les noms du gabarit d'usine, qui -nomme les trois cellules `KV_OAI_ENC_CC`, `KV_OAI_ENC_CW`, `KV_OAI_ENC_CLK`. - -Deux inversions se superposent, ce qui rend l'erreur facile : - -- le firmware délivre les deux événements de rotation permutés par rapport aux - noms de cellules du vendeur ; -- Input 0.17.3 permute en plus les libellés `CW` et `CCW` de son éditeur pour - tout encodeur à trois cellules, si bien que son interface contredit les noms de - keycodes de son propre gabarit par défaut. - -Le générateur écrit le JSON directement et contourne donc le second point. Ne pas -aligner `PHYSICAL_ENCODER_SLOTS` sur ce qu'affiche l'éditeur, ni sur les noms de -keycodes : cela inverse la molette. - -## Pourquoi le regroupement des crans a été écarté - -Faire coûter un seul cycle `⌘⇧E` / `Esc` à N crans demande trois choses : un -compteur persistant, une temporisation non bloquante, et l'émission de frappes -clavier. - -Le SDK MicroPython embarqué fournit les deux premières — un compteur de crans via -`EVENT.ENCODER`, et une temporisation approchée via le hook de frame — mais -**aucune API d'émission de frappes**. Ce SDK n'est de plus pas disponible sur le -Codex Micro : il est réservé au Nomad [E] v1, et Input ne présente pas d'onglet -Widgets pour ce modèle. Le format `keymap.json` n'offre pas d'alternative : ni -compteur, ni condition, ni bascule, le seul état retenu par le firmware étant le -layer et le profil actifs. - -Le regroupement exige donc un agent sur l'hôte. À 900 ms par cran il se -justifiait largement ; à 90 ms, cinq crans coûtent 450 ms contre environ 300 ms -pour un agent qui les regroupe, et l'écart ne paie plus un démon ni une -autorisation d'accessibilité. - -## Ce qui reste non prouvé - -- **La sémantique de `delay`.** Le comportement observé sur l'étape `Esc` indique - qu'elle s'applique avant son étape, puisque dans la lecture « après » ces 10 ms - seraient du temps mort en fin de macro et ne changeraient rien à l'affichage. - Ce n'est pas une preuve. Test décisif : porter `500 ms` sur l'étape `Esc`. Si le - sélecteur reste visiblement affiché une demi-seconde, c'est « avant » ; s'il se - referme aussitôt et que c'est la molette qui reste inerte, c'est « après ». -- **Le sort des événements d'encodeur pendant l'exécution d'une macro** : mis en - file ou perdus. Le firmware n'est pas du QMK — c'est un ESP32-S3 sous FreeRTOS - avec un firmware maison — donc l'hypothèse d'une boucle de scan gelée pendant - l'attente est infondée. -- **Le délai maximal accepté par le firmware.** L'interface d'Input plafonne la - saisie à 9999 ms, mais rien ne borne la valeur à l'import : un JSON écrit à la - main transmet ce qu'il veut. +| 120ms | changes the level | +| 80ms | changes the level | +| 60ms | changes the level | +| 40ms | changes the level | +| 20ms | fails | +| 0ms | fails | + +The floor is therefore between 20 and 40ms. The value chosen, `80ms`, doubles the +measured floor. + +`40ms` was tried in real use and judged less reliable than in isolated testing. +That is consistent: the floor was measured on an already-warm picker, whereas the +real mount time depends on the renderer's load. **A floor is not a production +value.** Below roughly 100ms the difference in latency is not perceptible, +whereas a lost level is noticed immediately: the right target is the smallest +value that never misses, not the smallest one that works. + +Failure below the floor is quiet. The arrow leaves before the picker has focus, +and the change is lost with no error message. Any reduction must therefore be +validated over several repetitions **and** on a cold first open, coming back from +another application. + +## Why `Esc` carries 10ms + +Without a wait on that step, the arrow and `Esc` are emitted with no gap and +Claude handles them in the same loop turn: the picker opens and closes without +ever painting a frame showing the slider at its new level. The level does change, +but **blind** — the visible effect is only a flicker. + +`10ms` is enough to let one frame through, and the level reached becomes +readable. This wait is paid **after** the level has changed: it lengthens the +macro without delaying its effect. + +Methodological consequence: the 300ms the first version of the macro carried at +that spot were not dead time. They had been removed on the sole criterion of +latency, which lost the visual feedback without the chosen criterion being able +to detect it. + +## Direction of rotation + +The encoder cell at index `0` is **physically clockwise**, confirmed on hardware. +That is the opposite of what the factory template's names suggest, since it names +the three cells `KV_OAI_ENC_CC`, `KV_OAI_ENC_CW`, `KV_OAI_ENC_CLK`. + +Two inversions stack, which makes the mistake easy: + +- the firmware delivers the two rotation events swapped relative to the vendor's + cell names; +- Input 0.17.3 additionally swaps the `CW` and `CCW` labels in its editor for any + three-cell encoder, so its interface contradicts the keycode names of its own + default template. + +The generator writes the JSON directly and therefore bypasses the second point. +Do not align `PHYSICAL_ENCODER_SLOTS` with what the editor displays, nor with the +keycode names: that inverts the wheel. + +## Why grouping notches was ruled out + +Making a single `⌘⇧E` / `Esc` cycle cover N notches requires three things: a +persistent counter, a non-blocking delay, and the ability to emit keystrokes. + +The embedded MicroPython SDK provides the first two — a notch counter through +`EVENT.ENCODER`, and an approximate delay through the frame hook — but **no +keystroke-emitting API**. That SDK is moreover not available on the Codex Micro: +it is reserved for the Nomad [E] v1, and Input shows no Widgets tab for this +model. The `keymap.json` format offers no alternative: no counter, no condition, +no toggle, the only state the firmware retains being the active layer and +profile. + +Grouping therefore requires an agent on the host. At 900ms per notch it was amply +justified; at 90ms, five notches cost 450ms against roughly 300ms for an agent +that groups them, and the gap no longer pays for a daemon or an accessibility +permission. + +## What is still unproven + +- **The semantics of `delay`.** The behaviour observed on the `Esc` step suggests + it applies before its step, since under the "after" reading these 10ms would be + dead time at the end of the macro and would change nothing on screen. That is + not a proof. Decisive test: put `500ms` on the `Esc` step. If the picker stays + visibly displayed for half a second, it is "before"; if it closes immediately + and it is the wheel that stays inert, it is "after". +- **What happens to encoder events while a macro runs**: queued or lost. The + firmware is not QMK — it is an ESP32-S3 under FreeRTOS with in-house firmware — + so the hypothesis of a scan loop frozen during the wait is unfounded. +- **The maximum delay the firmware accepts.** Input's interface caps the input at + 9999ms, but nothing bounds the value on import: a hand-written JSON passes + whatever it likes. diff --git a/docs/research/hid-lighting-protocol.md b/docs/research/hid-lighting-protocol.md index e151ef5..ebfce73 100644 --- a/docs/research/hid-lighting-protocol.md +++ b/docs/research/hid-lighting-protocol.md @@ -1,118 +1,113 @@ -# Protocole d'éclairage HID du Codex Micro — format observé, confirmé à l'exécution +[English](hid-lighting-protocol.md) · [Français](../fr/research/hid-lighting-protocol.md) + +# Codex Micro HID lighting protocol — observed format, confirmed at runtime ## Verdict -**Le canal d'éclairage par touche est ouvert.** Le cadrage rapporté est -confirmé à l'exécution, le transport non exclusif fonctionne avec `node-hid` -3.4.0, et une réimplémentation originale est livrée -(`scripts/lib/hid-frame.mjs`, `scripts/lib/hid-lighting.mjs`, -`scripts/lib/hid-device.mjs`, `scripts/lighting.mjs`). Chacun peut piloter la -couleur et l'effet des touches de son propre clavier, ainsi que les deux zones -globales, et écouter les événements touches et joystick. +**The per-key lighting channel is open.** The reported framing is confirmed at +runtime, the non-exclusive transport works with `node-hid` 3.4.0, and an original +reimplementation ships with the repository (`scripts/lib/hid-frame.mjs`, +`scripts/lib/hid-lighting.mjs`, `scripts/lib/hid-device.mjs`, +`scripts/lighting.mjs`). Anyone can drive the colour and effect of their own +keyboard's keys, plus the two global zones, and listen to key and joystick +events. -Mesures sur macOS `26.5.2` arm64, firmware Codex Micro `v0.4.1` (relevé par -`sys.version`), `node-hid` `3.4.0`. +Measurements on macOS `26.5.2` arm64, Codex Micro firmware `v0.4.1` (read +through `sys.version`), `node-hid` `3.4.0`. -## Cadre légal, rappelé +## Legal framing, restated -Ce document décrit un **format observé** : constantes, positions d'octets, -champs JSON. L'implémentation du dépôt est un code original écrit d'après ces -faits, pour interopérer avec un périphérique que son utilisateur possède. -Aucune ligne du SDK Work Louder (`UNLICENSED`, registre privé) n'est reprise -ni redistribuée ; les extraits lus localement pour établir les faits restent -sous `.local/`, ignoré par Git. +This document describes an **observed format**: constants, byte positions, JSON +fields. The repository's implementation is original code written from those +facts, to interoperate with a device its user owns. Not a line of the Work Louder +SDK (`UNLICENSED`, private registry) is reused or redistributed; the extracts +read locally to establish the facts stay under `.local/`, which Git ignores. -## Matrice de preuve +## Evidence matrix -| Affirmation | État | Preuve | +| Claim | State | Proof | | --- | --- | --- | -| Rapports de 64 octets, octet 0 = `0x06`, octet 1 = canal `2` (RPC), octet 2 = longueur, charge UTF-8 à l'octet 3 | **confirmé** | round-trip `sys.version` → `{"result":{"version":"v0.4.1"},"id":798,"method":"sys.version"}` | -| Charge utile de 61 octets par rapport, continuation multi-rapports au-delà | **confirmé** | poussées `thstatus` de ~140 octets (3 rapports) acquittées six fois pendant la sonde | -| L'octet 2 porte la longueur **du fragment**, sur chaque rapport | confirmé | cohérent avec l'accumulation par canal côté hôte ; l'hypothèse « longueur totale au premier rapport » (sonde Swift parallèle) est écartée | -| Canal 1 = journaux de débogage, canal 2 = RPC, messages terminés par saut de ligne | confirmé | lecture du format + réception fonctionnelle | -| Enveloppe de requête `{method, params, id}`, `id` entier dans `[0, 999)`, non-ASCII échappé en `\uXXXX` | confirmé | round-trips réussis | -| Réponse `{result, id, method}` ou `{error, id}` ; la méthode est renvoyée en écho | confirmé | réponses observées | -| Notification sans `id` : `{method, params}`, formes compactes `m`/`p`, `i` possibles | confirmé | format documenté, distribution implémentée | -| VID `0x303a`, PID `0x8360`, collection vendeur usage page `0xFF00` | confirmé | `hidutil list`, énumération `node-hid` | -| Ouverture **non exclusive** possible avec `node-hid` 3.4.0 (`HIDAsync.open(path, { nonExclusive: true })`) | **confirmé** | ouverture + round-trip réussis pendant que ChatGPT tient le même périphérique | -| `v.oai.thstatus` pilote chaque touche Agent : entrées `{id, c, b, e, s, sk, sa}`, champs omis inchangés | **confirmé** | six écritures acquittées, touches allumées une par une, extinction propre | -| `v.oai.rgbcfg` configure deux zones globales `{ambient, keys}` × `{e, b, s, m, c}` | confirmé (format) | lecture du format ; non exercé à l'écriture ici | -| Effets : `off=0, solid=1, snake=2, rainbow=3, breath=4, gradient=5, shallowBreath=6` | confirmé (format) | énumération documentée | -| Notifications `v.oai.hid` `{k, act, ag}` (touches) et `v.oai.rad` `{a, d}` (joystick) | confirmé (format) | types documentés ; écoute implémentée (`listen`) | -| Correspondance thread id ↔ touche physique : `[0..5]` dans l'ordre `key-9, key-10, key-5…key-8` | **provisoire** | sonde visuelle exécutée ; à figer après confirmation de l'utilisateur | -| Une requête en vol, 50 ms entre appels, 10 s de garde par réponse | respecté | comportement du transport livré | - -## Le point transport, et l'erreur à ne pas reproduire - -`node-hid` embarque hidapi, qui ouvre **en mode exclusif par défaut** depuis -hidapi 0.14. Ouvrir avec `new HID.HID(path)` sans option échoue sur ce -périphérique tant qu'une autre application le tient — c'est l'échec mesuré par -la sonde parallèle (`scripts/lighting-probe.mjs`), qui concluait à tort que -`node-hid` ne pouvait pas ouvrir ce périphérique. - -`node-hid` expose pourtant bien l'option : `HIDAsync.open(path, { nonExclusive: true })` -appelle `hid_darwin_set_open_exclusive(0)` (`node_modules/node-hid/src/HIDAsync.cc:137`), -et l'ouverture non exclusive **fonctionne** — preuve par le round-trip -`sys.version`. L'ouverture non exclusive IOKit (`kIOHIDOptionsTypeNone`), que la -sonde Swift parallèle a validée, revient au même. - -Conséquence pratique : le transport Node suffit, pas besoin d'un binaire -auxiliaire. L'autorisation macOS « Surveillance des saisies » n'a pas été -requise pour l'ouverture non exclusive sur cette machine. - -## Concurrence d'écriture, stratégie livrée - -Le périphérique est ouvert en non exclusif par toutes les applications : les -lectures sont diffusées à tous, les écritures se disputent, **dernière -écriture gagnante**. L'app ChatGPT repousse `rgbcfg` puis `thstatus` toutes -les 35 à 40 secondes. - -Le signal de coexistence est gratuit : les réponses portent la méthode en -écho, et une réponse dont l'identifiant n'est pas le nôtre est forcément -celle d'un autre écrivain (ce sont ces « réponses orphelines » qu'Input -journalise en avertissement). Le mode `--hold` de `scripts/lighting.mjs` -s'appuie dessus : toute poussée étrangère détectée déclenche une -réapplication immédiate, avec un filet de sécurité périodique de 10 s. Sans -`--hold`, l'état posé est recouvert à la cadence de ChatGPT — comportement -attendu, affiché à l'utilisateur. - -## Composants livrés - -| Composant | Rôle | +| 64-byte reports, byte 0 = `0x06`, byte 1 = channel `2` (RPC), byte 2 = length, UTF-8 payload from byte 3 | **confirmed** | `sys.version` round-trip → `{"result":{"version":"v0.4.1"},"id":798,"method":"sys.version"}` | +| 61-byte payload per report, multi-report continuation beyond that | **confirmed** | `thstatus` pushes of ~140 bytes (3 reports) acknowledged six times during the probe | +| Byte 2 carries the length **of the chunk**, on every report | confirmed | consistent with per-channel accumulation on the host side; the "total length in the first report" hypothesis (parallel Swift probe) is ruled out | +| Channel 1 = debug logs, channel 2 = RPC, messages terminated by a newline | confirmed | format read + working reception | +| Request envelope `{method, params, id}`, integer `id` in `[0, 999)`, non-ASCII escaped as `\uXXXX` | confirmed | successful round-trips | +| Response `{result, id, method}` or `{error, id}`; the method is echoed back | confirmed | responses observed | +| Notification without `id`: `{method, params}`, compact forms `m`/`p`, `i` possible | confirmed | format documented, dispatch implemented | +| VID `0x303a`, PID `0x8360`, vendor collection usage page `0xFF00` | confirmed | `hidutil list`, `node-hid` enumeration | +| **Non-exclusive** open possible with `node-hid` 3.4.0 (`HIDAsync.open(path, { nonExclusive: true })`) | **confirmed** | open + round-trip succeeded while ChatGPT held the same device | +| `v.oai.thstatus` drives each Agent key: entries `{id, c, b, e, s, sk, sa}`, omitted fields unchanged | **confirmed** | six writes acknowledged, keys lit one by one, clean turn-off | +| `v.oai.rgbcfg` configures two global zones `{ambient, keys}` × `{e, b, s, m, c}` | confirmed (format) | format read; not exercised for writing here | +| Effects: `off=0, solid=1, snake=2, rainbow=3, breath=4, gradient=5, shallowBreath=6` | confirmed (format) | documented enumeration | +| Notifications `v.oai.hid` `{k, act, ag}` (keys) and `v.oai.rad` `{a, d}` (joystick) | confirmed (format) | types documented; listening implemented (`listen`) | +| Thread id ↔ physical key mapping: `[0..5]` in the order `key-9, key-10, key-5…key-8` | **confirmed on hardware** | one-key-at-a-time probe: ids 0 to 5 follow the order of `SLOT_CONTROLS`, top row then the next, left to right | +| One request in flight, 50ms between calls, a 10s guard per response | honoured | behaviour of the shipped transport | + +## The transport point, and the mistake not to repeat + +`node-hid` bundles hidapi, which opens **exclusively by default** since hidapi +0.14. Opening with `new HID.HID(path)` and no option fails on this device while +another application holds it — that is the failure measured by the parallel probe +(`scripts/lighting-probe.mjs`), which wrongly concluded that `node-hid` could not +open this device. + +`node-hid` does expose the option, though: +`HIDAsync.open(path, { nonExclusive: true })` calls +`hid_darwin_set_open_exclusive(0)` (`node_modules/node-hid/src/HIDAsync.cc:137`), +and the non-exclusive open **works** — proven by the `sys.version` round-trip. +The IOKit non-exclusive open (`kIOHIDOptionsTypeNone`), which the parallel Swift +probe validated, amounts to the same thing. + +Practical consequence: the Node transport is enough, no helper binary needed. The +macOS "Input Monitoring" permission was not required for the non-exclusive open +on this machine. + +## Write contention, shipped strategy + +The device is opened non-exclusively by every application: reads are broadcast to +all, writes compete, **last write wins**. The ChatGPT app pushes `rgbcfg` then +`thstatus` again every 35 to 40 seconds. + +The coexistence signal is free: responses echo the method, and a response whose +id is not ours necessarily belongs to another writer (these are the "orphan +responses" that Input logs as warnings). The `--hold` mode of +`scripts/lighting.mjs` builds on that: any detected foreign push triggers an +immediate reapply, with a periodic 10s safety net. Without `--hold`, the state +set here is overwritten at ChatGPT's cadence — expected behaviour, and shown to +the user. + +## Shipped components + +| Component | Role | | --- | --- | -| `scripts/lib/hid-frame.mjs` | cadrage pur : fragmentation, réassemblage par canal, accumulateur JSON-RPC (pur, testé) | -| `scripts/lib/hid-lighting.mjs` | paramètres `thstatus`/`rgbcfg`, palette d'états → six entrées (pur, testé) | -| `scripts/lib/hid-device.mjs` | transport `node-hid` : découverte, ouverture non exclusive, file cadencée, corrélation par id, détection d'écritures étrangères | -| `scripts/lighting.mjs` | CLI `list` / `probe` / `set` / `watch` / `listen` / `off`, option `--hold` | -| `tests/hid-frame.test.mjs`, `tests/hid-lighting.test.mjs` | 21 tests sans matériel | - -`watch` est le `DeviceAdapter` prévu par la feuille de route : il suit -`~/.claude/thread-status/slots.json` et pousse les couleurs d'état des six -emplacements à chaque changement. - -## Ce qui reste ouvert - -- **Confirmation visuelle du mapping** thread id ↔ touche (table provisoire - `[0..5]` dans `SLOT_THREAD_IDS`). La sonde allume les touches une par une ; - toute divergence observée se corrige dans cette table. -- La sémantique exacte de `sk` / `sa` (synchronisation de la couleur d'un - thread vers les zones touches / ambiante, dans un sens ou dans l'autre) : - non éprouvée, laissée à 0 par défaut. -- `v.oai.rgbcfg` à l'écriture : format confirmé, jamais envoyé ici. La méthode - décrit les deux zones d'un coup ; la CLI exige donc `--keys` et `--ambient` - ensemble. -- Si les identifiants de thread au-delà de 5 existent (autres touches) : - aucun indice, non exploré. -- La pérennité : le format est celui du firmware `v0.4.1` ; une mise à jour - peut le faire évoluer sans prévenir. +| `scripts/lib/hid-frame.mjs` | pure framing: fragmentation, per-channel reassembly, JSON-RPC accumulator (pure, tested) | +| `scripts/lib/hid-lighting.mjs` | `thstatus`/`rgbcfg` parameters, state palette → six entries (pure, tested) | +| `scripts/lib/hid-device.mjs` | `node-hid` transport: discovery, non-exclusive open, paced queue, correlation by id, foreign-write detection | +| `scripts/lighting.mjs` | `list` / `probe` / `set` / `watch` / `listen` / `off` CLI, `--hold` option | +| `tests/hid-frame.test.mjs`, `tests/hid-lighting.test.mjs` | 21 tests without hardware | + +`watch` is the `DeviceAdapter` the roadmap called for: it follows +`~/.claude/thread-status/slots.json` and pushes the state colours of the six +slots on every change. + +## What is still open + +- The exact semantics of `sk` / `sa` (syncing a thread's colour towards the key + or ambient zones, in either direction): not exercised, left at 0 by default. +- `v.oai.rgbcfg` for writing: format confirmed, never sent here. The method + describes both zones at once, so the CLI requires `--keys` and `--ambient` + together. +- Whether thread ids beyond 5 exist (other keys): no clue, not explored. +- Longevity: this is the format of firmware `v0.4.1`; an update may change it + without notice. ## Sources -- Format et énumérations : lus localement dans le bundle ChatGPT.app - (`@worklouder/device-kit-oai`, `@worklouder/wl-device-kit`) — lecture pour - documentation, aucune redistribution. -- Mesures d'exécution : cette machine, juillet 2026 (round-trip, sonde, - chaîne `watch` sur état synthétique). -- [`thread-status-feasibility.md`](thread-status-feasibility.md) — mesures - amont (roster, hooks, contention, réponses orphelines dans le log d'Input). -- [`appsense-behavior.md`](appsense-behavior.md) — contention et zones. +- Format and enumerations: read locally in the ChatGPT.app bundle + (`@worklouder/device-kit-oai`, `@worklouder/wl-device-kit`) — read for + documentation, no redistribution. +- Runtime measurements: this machine, July 2026 (round-trip, probe, `watch` + chain on synthetic state). +- [`thread-status-feasibility.md`](thread-status-feasibility.md) — upstream + measurements (roster, hooks, contention, orphan responses in Input's log). +- [`appsense-behavior.md`](appsense-behavior.md) — contention and zones. diff --git a/docs/research/input-0.17.2-sharing.md b/docs/research/input-0.17.2-sharing.md index 69e2b03..10d7103 100644 --- a/docs/research/input-0.17.2-sharing.md +++ b/docs/research/input-0.17.2-sharing.md @@ -1,55 +1,57 @@ -# Work Louder Input 0.17.2 — mécanisme de partage observé +[English](input-0.17.2-sharing.md) · [Français](../fr/research/input-0.17.2-sharing.md) + +# Work Louder Input 0.17.2 — observed sharing mechanism ## Verdict -Input `0.17.2` possède un flux utilisateur officiel d'import et d'export au -niveau **layer** et **profile**. Les fichiers produits portent respectivement -les suffixes `*-layer.json` et `*-profile.json`. +Input `0.17.2` has an official user flow for importing and exporting at both the +**layer** and **profile** level. The files it produces carry the `*-layer.json` +and `*-profile.json` suffixes respectively. -Cela justifie de privilégier l'import/export officiel plutôt qu'un patch direct -de la base locale. En revanche, le dépôt ne publie pas encore de fichier layer -Codex Micro : aucun export réel n'a été capturé puis réimporté sur le matériel. -Le statut reste donc `proposal-not-applied`. +That justifies preferring the official import/export over patching the local +database directly. The repository, however, does not publish a Codex Micro layer +file yet: no real export has been captured and re-imported on hardware. The +status therefore stays `proposal-not-applied`. -## Faits vérifiés +## Verified facts -### Documentation Work Louder +### Work Louder documentation -La page officielle du Codex Micro indique : +The official Codex Micro page states: -- six layers programmables au maximum ; -- un lien AppSense créé avec l'icône de lien, `Auto detect`, puis cinq secondes - de focus sur l'application cible ; -- `Reset settings` supprime les layers, profils et actions. +- six programmable layers at most; +- an AppSense link created with the link icon, `Auto detect`, then five seconds + of focus on the target application; +- `Reset settings` deletes layers, profiles and actions. -Source : +Source: -### Application distribuée +### Distributed application -L'archive officielle inspectée est : +The official archive inspected is: ```text https://github.com/worklouder/input-releases/releases/download/v0.17.2/input-0.17.2-arm64-mac.zip ``` -Identité observée : +Identity observed: -- SHA-256 de l'archive : - `72bb2ccd2f0de0b21a61cd4006367a64e628010c3e7303e94f219cff5ad45d35` ; -- bundle macOS : `it.focusense.input-app` ; -- version courte et build : `0.17.2` ; -- package Electron : `input` `0.17.2`. +- SHA-256 of the archive: + `72bb2ccd2f0de0b21a61cd4006367a64e628010c3e7303e94f219cff5ad45d35`; +- macOS bundle: `it.focusense.input-app`; +- short version and build: `0.17.2`; +- Electron package: `input` `0.17.2`. -L'analyse statique assainie du bundle expose les libellés suivants : +The sanitised static analysis of the bundle exposes the following labels: -- `Import layer` ; -- `Export layer` ; -- `Import Profile` ; -- `Export Profile` ; -- avertissement de langue différente ; -- refus d'un fichier créé pour un autre type de clavier. +- `Import layer`; +- `Export layer`; +- `Import Profile`; +- `Export Profile`; +- a different-language warning; +- refusal of a file created for another keyboard type. -L'enveloppe JSON d'un export de layer contient les clés de premier niveau : +The JSON envelope of a layer export contains the top-level keys: ```text keyboard @@ -63,52 +65,51 @@ multiactionGroups smartActionGroups ``` -L'export de profile remplace `layer` par `profile`. Le code packagé utilise -`JSON.stringify`, `Blob` et `URL.createObjectURL` pour l'export, puis -`FileReader`, `JSON.parse` et une copie structurée pour l'import. +The profile export replaces `layer` with `profile`. The packaged code uses +`JSON.stringify`, `Blob` and `URL.createObjectURL` for the export, then +`FileReader`, `JSON.parse` and a structured clone for the import. -## Méthode reproductible +## Reproducible method -Les workflows suivants téléchargent temporairement l'archive officielle, -extraient `app.asar`, calculent uniquement des métadonnées structurales, puis -suppriment les fichiers propriétaires : +The following workflows temporarily download the official archive, extract +`app.asar`, compute structural metadata only, then delete the proprietary files: -- `.github/workflows/inspect-input-0172.yml` ; +- `.github/workflows/inspect-input-0172.yml`; - `.github/workflows/inspect-input-0172-ast.yml`. -Les artefacts CI contiennent uniquement : versions, sommes de contrôle, -libellés bornés, noms de clés, suffixes et résumés AST. Ils ne contiennent ni -code source packagé, ni image, ni firmware, ni identifiant local. - -## Décision d'architecture initiale - -Cette inspection avait conduit au plan initial suivant : - -1. sauvegarde par export officiel du profile **et** copie de la configuration - locale reconnue ; -2. inventaire depuis le `*-profile.json` officiel ; -3. sélection du premier emplacement libre après l'index protégé `0` ; -4. import du `*-layer.json` officiel lorsqu'un artefact réel aura été vérifié ; -5. procédure guidée manuelle tant que cet artefact manque ; -6. rollback principal par réimport du profile officiel d'origine ; -7. aucun patch direct de `input_storage.json`, Local Storage ou du périphérique - avant preuve supplémentaire. - -Ce plan est conservé comme historique, mais il n'est plus le parcours V1 -courant. La V1 actuelle transforme localement un export `*-profile.json` -Input `0.17.3` contenant exactement un layer `Claude` déjà lié avec AppSense. -L'import de layer reste une validation de publication optionnelle. - -## Points non encore prouvés - -- structure interne complète des objets `layer` et `actions` pour le Codex - Micro ; -- identifiants physiques affichés par Input pour chaque touche ; -- création puis import du layer Claude sur le périphérique réel ; -- persistance après redémarrage d'Input ; -- retour au layer précédent lors de la perte de focus ; -- restauration effective du keymap matériel par réimport de profile ; -- comportement d'un second import du même layer. - -Un vrai fichier `*-layer.json` ne doit être ajouté qu'après les tests décrits -dans `profiles/claude-shortcuts/artifacts/README.md`. +The CI artefacts contain only: versions, checksums, bounded labels, key names, +suffixes and AST summaries. They contain no packaged source code, no image, no +firmware and no local identifier. + +## Initial architecture decision + +This inspection had led to the following initial plan: + +1. back up through the official profile export **and** a copy of the recognised + local configuration; +2. inventory from the official `*-profile.json`; +3. select the first free slot after the protected index `0`; +4. import the official `*-layer.json` once a real artefact has been verified; +5. a manual guided procedure as long as that artefact is missing; +6. primary rollback by re-importing the original official profile; +7. no direct patching of `input_storage.json`, Local Storage or the device + before further proof. + +This plan is kept as history, but it is no longer the current V1 path. Today's V1 +transforms an Input `0.17.3` `*-profile.json` export locally, one that contains +exactly one `Claude` layer already linked with AppSense. The layer import remains +an optional publication validation. + +## Points not yet proven + +- the complete internal structure of the `layer` and `actions` objects for the + Codex Micro; +- the physical identifiers Input displays for each key; +- creating then importing the Claude layer on the real device; +- persistence after an Input restart; +- returning to the previous layer on focus loss; +- effective restoration of the hardware keymap by re-importing a profile; +- the behaviour of a second import of the same layer. + +A real `*-layer.json` file must only be added after the tests described in +`profiles/claude-shortcuts/artifacts/README.md`. diff --git a/docs/research/thread-status-feasibility.md b/docs/research/thread-status-feasibility.md index 29fd65e..6d9d426 100644 --- a/docs/research/thread-status-feasibility.md +++ b/docs/research/thread-status-feasibility.md @@ -1,40 +1,43 @@ -# États des sessions Claude Code et touches Agent — mesures +[English](thread-status-feasibility.md) · [Français](../fr/research/thread-status-feasibility.md) + +# Claude Code session states and Agent keys — measurements ## Verdict -**Les états sont disponibles officiellement, les LED fonctionnent sur le layer -`Claude`, et seule la navigation reste partiellement ouverte.** Trois conclusions, -dans cet ordre de solidité : - -1. Détecter « en cours / intervention / terminé / fermé » par session est un - problème résolu, avec deux mécanismes documentés et complémentaires. -2. Aller à la session depuis une touche est résolu **si la session tourne dans un - terminal**, et sans route connue si elle est hébergée par Claude Desktop ou un - IDE. -3. Piloter les six LED **fonctionne, y compris sur le layer `Claude`**, à une - condition découverte tardivement : les six positions Agent de ce layer doivent - porter les keycodes `KV_OAI_AG00` à `KV_OAI_AG05`. Le prédicat du firmware est - le keycode, pas l'index du layer. - -Les trois briques sont réunies, et aucun raccourci Claude n'est sacrifié. Mais la -fonction a une condition d'usage : **l'app ChatGPT doit être quittée.** Elle -réécrit les six LED toutes les 35 à 40 secondes et intercepte les appuis sur les -touches Agent pour changer de thread Codex. Les deux moitiés de la fonction lui -sont donc disputées par la même application, et rien ne permet d'arbitrer. - -## Le modèle : deux sources, deux rôles - -| Source | Autorité sur | Nature | +**The states are available officially, the LEDs work on the `Claude` layer, and +navigation is solved on every surface.** Three conclusions, in decreasing order +of solidity: + +1. Detecting "running / needs you / done / closed" per session is a solved + problem, with two documented and complementary mechanisms. +2. Going to a session from a key is solved on both surfaces: window focus over + AppleScript when the session runs in a terminal, and + `claude://resume?session=` when Claude Desktop hosts it. The second + route is not documented. +3. Driving the six LEDs **works, including on the `Claude` layer**, on one + condition discovered late: the six Agent positions of that layer must carry + the `KV_OAI_AG00` to `KV_OAI_AG05` keycodes. The firmware's predicate is the + keycode, not the layer index. + +All three building blocks are in place, and no Claude shortcut is sacrificed. But +the feature has a condition of use: **the ChatGPT app must be quit.** It rewrites +the six LEDs every 35 to 40 seconds and intercepts Agent key presses to switch +Codex thread. Both halves of the feature are therefore contended by the same +application, and nothing can arbitrate. + +## The model: two sources, two roles + +| Source | Authority over | Nature | | --- | --- | --- | -| `claude agents --json` | l'appartenance : qui occupe un emplacement | poll, officiel | -| hooks du plugin | l'état de chaque session | push, officiel | +| `claude agents --json` | membership: who occupies a slot | poll, official | +| plugin hooks | the state of each session | push, official | -Cette séparation n'est pas esthétique, elle est nécessaire. Un hook manqué — -crash, `kill -9` — fige l'état à « en cours » indéfiniment, et seul le roster le -rattrape. Inversement le roster ne publie aucun état. Aucune des deux sources ne -suffit seule. +This separation is not cosmetic, it is necessary. A missed hook — crash, +`kill -9` — pins the state to "running" indefinitely, and only the roster catches +it. Conversely the roster publishes no state at all. Neither source is sufficient +on its own. -Sortie réelle du roster, sur ce dépôt : +Real roster output, on this repository: ```json [ @@ -44,139 +47,138 @@ Sortie réelle du roster, sur ce dépôt : ] ``` -`--json` est explicitement prévu pour le script : « does not require a TTY ». -`--all` ajoute les sessions d'arrière-plan terminées, `--cwd` filtre par -répertoire. +`--json` is explicitly meant for scripting: "does not require a TTY". `--all` +adds finished background sessions, `--cwd` filters by directory. -### Table des états +### State table -| État | Événement | Champ décisif | Couleur | +| State | Event | Deciding field | Colour | | --- | --- | --- | --- | -| en cours | `UserPromptSubmit` | — | `#D97757` | -| intervention | `Notification` | `notification_type` ∈ {`permission_prompt`, `agent_needs_input`, `elicitation_dialog`} | `#C2483D` | -| au repos | `SessionStart`, `Notification` / `idle_prompt` | — | `#6D5A7D` | -| terminé | `Stop` | — | `#5B8C6F` | -| fermé | `SessionEnd`, ou absence du roster | `reason` | `#2F2927` | +| running | `UserPromptSubmit` | — | `#D97757` | +| needs you | `Notification` | `notification_type` ∈ {`permission_prompt`, `agent_needs_input`, `elicitation_dialog`} | `#C2483D` | +| idle | `SessionStart`, `Notification` / `idle_prompt` | — | `#6D5A7D` | +| done | `Stop` | — | `#5B8C6F` | +| closed | `SessionEnd`, or absence from the roster | `reason` | `#2F2927` | -`notification_type` est le seul champ qui distingue « on t'attend » de « ça -travaille ». Les types `auth_success`, `elicitation_complete` et -`agent_completed` ne changent pas l'état : un témoin rouge doit signifier une -décision attendue, rien d'autre. +`notification_type` is the only field that separates "you are being waited for" +from "it is working". The `auth_success`, `elicitation_complete` and +`agent_completed` types do not change the state: a red light must mean a decision +is expected, and nothing else. -## Matrice de preuve +## Evidence matrix -Mesuré sur macOS `26.5.2` arm64, Claude `1.24012.9`, Claude Code `2.1.219`. +Measured on macOS `26.5.2` arm64, Claude `1.24012.9`, Claude Code `2.1.219`. -| Affirmation | État | Preuve | +| Claim | State | Proof | | --- | --- | --- | -| `claude agents --json` liste les sessions vivantes avec `sessionId`, `pid`, `cwd`, `name` | confirmé | exécution, 3 sessions retournées | -| Un hook de plugin reçoit l'événement en JSON sur stdin | confirmé | plugin sonde, 4 événements capturés | -| Un hook hérite de `CLAUDE_PID`, `CLAUDE_CODE_SESSION_ID`, `CLAUDE_CODE_ENTRYPOINT` | confirmé | environnement relevé dans le hook | -| `CLAUDE_PID` est égal au `pid` du roster | confirmé | `47158` des deux côtés | -| `$CLAUDE_PLUGIN_ROOT` s'étend dans une commande de hook | confirmé | plugin `thread-status` chargé par `--plugin-dir` | -| `CLAUDE_PLUGIN_DATA` diffère selon le mode de chargement | confirmé | `…/data/-inline` avec `--plugin-dir` | -| Une session `claude -p` émet des hooks sans entrer au roster | confirmé | session `439b9985` absente de `agents --json` | -| `CLAUDE_CODE_HOST_SESSION_ID` **n'identifie pas** une session | confirmé | deux sessions distinctes partagent `local_f92b6e6a` | -| Le `tty` distingue terminal et Desktop | confirmé | `tty = ??` pour les trois sessions Desktop | -| Focus d'une fenêtre de terminal par `tty` en AppleScript | non testé de bout en bout | scripts compilés par `osacompile` ; aucune session en terminal disponible, iTerm2 absent de la machine | -| Route pour ouvrir une session Claude Code locale dans Desktop **par identifiant** | **réfuté** | `claude://resume?session=` ouvre la bonne session, vérifié sur machine ; la route est absente de la doc des deep links, qui ne cite que `claude://code/new` | -| `claude://resume` valide sa cible par une regex UUID stricte | confirmé | lu dans le handler : l'`uuid` est vérifié avant `importCliSession`, puis la navigation | -| `claude://resume` échoue quand le transcript est absent du disque | rapporté, non testé | chemin d'erreur `transcript_missing` du handler ; aucun compte rendu ne remonte à l'appelant, `open` sort en 0 dans tous les cas | -| Raccourcis de **cycle** entre sessions dans Claude Desktop | documentés | `Ctrl Tab` / `Ctrl Shift Tab` et `Cmd Shift ]` / `Cmd Shift [` — table des raccourcis du Code tab | -| Raccourci pour sélectionner une session par son rang | inexistant | `1`–`9` ne sélectionne que dans un menu ouvert, et il n'existe pas de menu de sessions | -| Repli pour une session Desktop sans identifiant UUID : activer l'application | implémenté | `open -b com.anthropic.claudefordesktop`, sans consentement Automation ; la session précise reste non sélectionnable | -| `claude --resume ` reprend une session fermée | documenté | doc de gestion des sessions | -| Le Codex Micro est un périphérique Espressif | confirmé | `hidutil list` : VID `0x303a`, PID `0x8360` | -| Le log d'Input ne peut pas livrer le protocole RGB | confirmé | 66 lignes `v.oai.*`, toutes des réponses, zéro requête | -| `v.oai.thstatus` est l'éclairage **par thread** | confirmé | commentaire et énumération lus dans le bundle ChatGPT | -| `v.oai.rgbcfg` ne couvre que deux zones globales | confirmé | même source, recoupé avec le modèle d'Input | -| Le SDK est privé et sans licence | confirmé | `@worklouder/device-kit-oai`, `UNLICENSED`, registre privé | -| Cadre HID 64 octets, `0x06` / canal `2` | **confirmé sur matériel** | requête envoyée, réponse `{"result":{"ok":1},"id":1,"method":"v.oai.thstatus"}` | -| Fragmentation : longueur totale dans le premier rapport | **réfuté** | corrélées par `id`, les charges de 101 octets ne reçoivent aucune réponse | -| Fragmentation : longueur du fragment dans chaque rapport | **confirmé sur matériel** | charges ~140 octets (3 rapports) acquittées avec `id` corrélé — `scripts/lib/hid-frame.mjs`, voir [`hid-lighting-protocol.md`](hid-lighting-protocol.md) | -| Les réponses sont diffusées à tous les lecteurs | confirmé | des accusés de l'app ChatGPT ont été pris pour les nôtres tant que l'`id` n'était pas vérifié | -| L'écriture par emplacement est acceptée | **confirmé sur matériel** | six poussées colorées acquittées avec `id` corrélé, touches allumées une par une — sonde `scripts/lighting.mjs` | -| Une écriture peut échouer en répétition rapide | observé | un `SetReport` sur ~60 refusé en `0xE00002BC` pendant la réassertion | -| Correspondance `id` → touche physique | **confirmé sur matériel** | sonde une-touche-à-la-fois : les `id` 0 à 5 suivent l'ordre de `SLOT_CONTROLS`, rangée du haut puis rangée suivante, de gauche à droite | -| Le rendu exige les keycodes `KV_OAI_AG00..05` sur les six positions | **confirmé sur matériel** | layer `Claude` avec `KC_NONE` : aucun rendu ; les mêmes six positions passées aux keycodes Agent : rendu immédiat des six couleurs | -| Le prédicat du firmware est l'index de layer 0 | **réfuté** | le rendu fonctionne sur le layer `Claude` d'index 1 dès que les keycodes y sont posés | -| Les touches Agent notifient l'hôte de leur appui | **confirmé sur matériel** | `v.oai.hid` reçu pour `AG00` à `AG05`, `act` 1 à l'appui et 0 au relâchement — `npm run lighting -- listen` | -| L'app ChatGPT intercepte aussi ces appuis | **confirmé sur matériel** | sur le layer `Claude`, appuyer sur une touche Agent fait basculer les threads Codex | -| Un raccourci global natif est nécessaire pour la navigation | **réfuté** | le clavier désigne l'emplacement sur le canal HID déjà ouvert ; ni `RegisterEventHotKey`, ni autorisation macOS | -| `device.status` renvoie un index de layer 1-based | rapporté, non revérifié | `layer_index: 1` = layer Codex, `2` = layer `Claude` ; Input applique `layer_index - 1` | -| Les keycodes `KV_OAI_AG00..05` sont assignables depuis Input | **réfuté** | une seule occurrence chacun dans l'`app.asar`, dans la définition câblée du layer natif : absents du sélecteur de touches | -| Un `{"ok":1}` garantit un rendu visible | **réfuté** | des accusés non corrélés provenaient de l'app ChatGPT | -| Une écriture mono-rapport s'affiche réellement | **confirmé sur matériel** | emplacement 0 passé au bleu puis éteint par la sonde, observé | -| Le rendu est reproductible | confirmé, après correction | l'intermittence initiale venait de la fragmentation fausse ; reproductible une fois les keycodes Agent posés | -| Le périphérique expose une collection vendor `0xff00` | confirmé | `HID.devices()` : quatre collections, `0x0001/0x06`, `0x000c/0x01`, `0x000c/0x02`, `0xff00/0x01` | -| `node-hid` **peut** ouvrir ce périphérique en non exclusif | **confirmé sur matériel** | `HIDAsync.open(path, { nonExclusive: true })` puis round-trip `sys.version` réussi ; l'échec antérieur venait d'une ouverture sans l'option (`new HID.HID(path)` ouvre en *seize*) | -| L'ouverture non exclusive par IOKit réussit | confirmé | `IOHIDDeviceOpen(kIOHIDOptionsTypeNone)` accepté là où `hid_open_path` échoue | -| « Surveillance des saisies » est requise | **indéterminé** | accordée dans le contexte où IOKit réussit : les deux causes ne sont pas séparables | -| Socket IPC privé de l'app Codex | présent | `~/.codex/ipc/ipc.sock`, `srw-------` | - -## Le point qui décide de la navigation - -`CLAUDE_CODE_HOST_SESSION_ID` ressemble à l'identifiant qui manquerait pour -adresser une session dans Claude Desktop. Ce n'en est pas un. Trois sessions -Desktop indépendantes, relevées par `ps eww` : +| `claude agents --json` lists live sessions with `sessionId`, `pid`, `cwd`, `name` | confirmed | run, 3 sessions returned | +| A plugin hook receives the event as JSON on stdin | confirmed | plugin probe, 4 events captured | +| A hook inherits `CLAUDE_PID`, `CLAUDE_CODE_SESSION_ID`, `CLAUDE_CODE_ENTRYPOINT` | confirmed | environment read inside the hook | +| `CLAUDE_PID` equals the roster's `pid` | confirmed | `47158` on both sides | +| `$CLAUDE_PLUGIN_ROOT` expands inside a hook command | confirmed | `thread-status` plugin loaded with `--plugin-dir` | +| `CLAUDE_PLUGIN_DATA` differs with the loading mode | confirmed | `…/data/-inline` with `--plugin-dir` | +| A `claude -p` session emits hooks without entering the roster | confirmed | session `439b9985` absent from `agents --json` | +| `CLAUDE_CODE_HOST_SESSION_ID` **does not identify** a session | confirmed | two distinct sessions share `local_f92b6e6a` | +| The `tty` separates terminal from Desktop | confirmed | `tty = ??` for the three Desktop sessions | +| Focusing a terminal window by `tty` in AppleScript | not tested end to end | scripts compiled by `osacompile`; no terminal session available, iTerm2 absent from the machine | +| A route to open a local Claude Code session in Desktop **by id** | **refuted** | `claude://resume?session=` opens the right session, verified on this machine; the route is absent from the deep-link documentation, which only mentions `claude://code/new` | +| `claude://resume` validates its target against a strict UUID regex | confirmed | read in the handler: the `uuid` is checked before `importCliSession`, then navigation | +| `claude://resume` fails when the transcript is absent from disk | reported, not tested | the handler's `transcript_missing` error path; nothing is reported back to the caller, `open` exits 0 either way | +| **Cycling** shortcuts between sessions in Claude Desktop | documented | `Ctrl Tab` / `Ctrl Shift Tab` and `Cmd Shift ]` / `Cmd Shift [` — Code tab shortcut table | +| A shortcut to select a session by its rank | non-existent | `1`–`9` only selects inside an open menu, and there is no session menu | +| Fallback for a Desktop session with no UUID id: activate the application | implemented | `open -b com.anthropic.claudefordesktop`, no Automation consent; the specific session stays unselectable | +| `claude --resume ` resumes a closed session | documented | session management documentation | +| The Codex Micro is an Espressif device | confirmed | `hidutil list`: VID `0x303a`, PID `0x8360` | +| Input's log cannot deliver the RGB protocol | confirmed | 66 `v.oai.*` lines, all responses, zero requests | +| `v.oai.thstatus` is the **per-thread** lighting | confirmed | comment and enumeration read in the ChatGPT bundle | +| `v.oai.rgbcfg` only covers two global zones | confirmed | same source, cross-checked with Input's model | +| The SDK is private and unlicensed | confirmed | `@worklouder/device-kit-oai`, `UNLICENSED`, private registry | +| 64-byte HID frame, `0x06` / channel `2` | **confirmed on hardware** | request sent, response `{"result":{"ok":1},"id":1,"method":"v.oai.thstatus"}` | +| Fragmentation: total length in the first report | **refuted** | correlated by `id`, the 101-byte payloads receive no response | +| Fragmentation: chunk length in every report | **confirmed on hardware** | ~140-byte payloads (3 reports) acknowledged with a correlated `id` — `scripts/lib/hid-frame.mjs`, see [`hid-lighting-protocol.md`](hid-lighting-protocol.md) | +| Responses are broadcast to every reader | confirmed | acknowledgements from the ChatGPT app were taken for ours until the `id` was checked | +| Per-slot writing is accepted | **confirmed on hardware** | six coloured pushes acknowledged with a correlated `id`, keys lit one by one — `scripts/lighting.mjs` probe | +| A write can fail on rapid repetition | observed | one `SetReport` in ~60 refused with `0xE00002BC` during reassertion | +| `id` → physical key mapping | **confirmed on hardware** | one-key-at-a-time probe: ids 0 to 5 follow the order of `SLOT_CONTROLS`, top row then the next, left to right | +| Rendering requires the `KV_OAI_AG00..05` keycodes on the six positions | **confirmed on hardware** | `Claude` layer with `KC_NONE`: no rendering; the same six positions given the Agent keycodes: all six colours render immediately | +| The firmware's predicate is layer index 0 | **refuted** | rendering works on the `Claude` layer at index 1 as soon as the keycodes are set there | +| Agent keys notify the host of their press | **confirmed on hardware** | `v.oai.hid` received for `AG00` to `AG05`, `act` 1 on press and 0 on release — `npm run lighting -- listen` | +| The ChatGPT app also intercepts those presses | **confirmed on hardware** | on the `Claude` layer, pressing an Agent key switches Codex threads | +| A native global shortcut is required for navigation | **refuted** | the keyboard names the slot on the HID channel already open; neither `RegisterEventHotKey` nor a macOS permission | +| `device.status` returns a 1-based layer index | reported, not re-verified | `layer_index: 1` = Codex layer, `2` = `Claude` layer; Input applies `layer_index - 1` | +| The `KV_OAI_AG00..05` keycodes are assignable from Input | **refuted** | a single occurrence each in the `app.asar`, inside the hard-wired definition of the native layer: absent from the key picker | +| An `{"ok":1}` guarantees a visible render | **refuted** | uncorrelated acknowledgements came from the ChatGPT app | +| A single-report write actually shows up | **confirmed on hardware** | slot 0 turned blue then off by the probe, observed | +| Rendering is reproducible | confirmed, after correction | the initial flakiness came from the wrong fragmentation; reproducible once the Agent keycodes are set | +| The device exposes a `0xff00` vendor collection | confirmed | `HID.devices()`: four collections, `0x0001/0x06`, `0x000c/0x01`, `0x000c/0x02`, `0xff00/0x01` | +| `node-hid` **can** open this device non-exclusively | **confirmed on hardware** | `HIDAsync.open(path, { nonExclusive: true })` then a successful `sys.version` round-trip; the earlier failure came from opening without the option (`new HID.HID(path)` opens in *seize* mode) | +| A non-exclusive IOKit open succeeds | confirmed | `IOHIDDeviceOpen(kIOHIDOptionsTypeNone)` accepted where `hid_open_path` fails | +| "Input Monitoring" is required | **undetermined** | granted in the context where IOKit succeeds: the two causes cannot be separated | +| Private IPC socket of the Codex app | present | `~/.codex/ipc/ipc.sock`, `srw-------` | + +## The point that decides navigation + +`CLAUDE_CODE_HOST_SESSION_ID` looks like the identifier that would be missing to +address a session in Claude Desktop. It is not one. Three independent Desktop +sessions, read with `ps eww`: ```text pid 81796 local_f92b6e6a-2b3c-4cdf-8e43-c58a3e3681a2 -pid 32491 local_f92b6e6a-2b3c-4cdf-8e43-c58a3e3681a2 ← même identifiant +pid 32491 local_f92b6e6a-2b3c-4cdf-8e43-c58a3e3681a2 ← same identifier pid 47158 local_0c92bab5-02f7-4d17-a748-d0717a6c526e ``` -Deux sessions Claude Code distinctes, ni parentes ni filles, partagent la même -valeur. L'identifiant **regroupe** des sessions, il n'en désigne aucune. Il ne -peut donc pas servir de cible de navigation, et le compagnon ne fabrique aucune -URL à partir de lui. +Two distinct Claude Code sessions, neither parent nor child of one another, share +the same value. The identifier **groups** sessions, it designates none of them. +It therefore cannot serve as a navigation target, and the companion builds no URL +from it. -Conséquence directe, et c'est la décision produit du projet : +Direct consequence, and this is the product decision of the project: -| Surface | `tty` | Aller à la session | +| Surface | `tty` | Going to the session | | --- | --- | --- | -| Terminal | réel | `pid` → `tty` → focus de la fenêtre en AppleScript | +| Terminal | real | `pid` → `tty` → window focus in AppleScript | | Claude Desktop, IDE | `??` | `claude://resume?session=` | -| Session fermée | — | `claude --resume ` | - -Ce qui manquait n'était pas un identifiant, c'était la route. Le `sessionId` du -roster est exactement la cible que `claude://resume` attend — le -`hostSessionId`, lui, ne désigne toujours rien. **Les six emplacements sont donc -navigables quelle que soit la surface**, et le repli « activer l'application » -ne sert plus qu'aux sessions dont l'identifiant n'est pas un UUID. - -Deux réserves, portées par le code plutôt que par ce texte : la route n'est pas -documentée, et le handler ne rend pas compte de l'issue — `open` sort en 0 même -quand la reprise échoue faute de transcript sur le disque. Le focus par -AppleScript demande le consentement Automation de macOS, pas l'Accessibilité ; -le passage par `claude://` n'exige ni l'un ni l'autre. - -## Pièges rencontrés - -- **Une session `claude -p` ou un sous-agent émet des hooks sans jamais entrer au - roster.** Leur donner un emplacement remplirait les six touches de sessions - invisibles. Règle retenue : le roster arbitre l'appartenance, les hooks ne font - que poser un état sur un emplacement déjà ouvert. Les états orphelins sont mis - en attente dans une file bornée, et les abandons sont comptés. -- **`CLAUDE_CODE_CHILD_SESSION=1` ne filtre pas les sessions imbriquées.** La - session principale d'une fenêtre Desktop le porte aussi. -- **La sortie standard d'un hook `UserPromptSubmit` est injectée dans le contexte - de la conversation.** Un émetteur bavard finirait dans le prompt de - l'utilisateur. L'émetteur n'écrit rien sur stdout et sort toujours avec 0. -- **`CLAUDE_PLUGIN_DATA` n'est pas un chemin d'état stable** : il vaut - `…/data/-inline` sous `--plugin-dir` et `…/data/` après installation. - L'état vit donc sous `~/.claude/thread-status/`, surchargeable par +| Closed session | — | `claude --resume ` | + +What was missing was not an identifier, it was the route. The roster's +`sessionId` is exactly the target `claude://resume` expects — the +`hostSessionId`, meanwhile, still designates nothing. **All six slots are +therefore navigable whatever the surface**, and the "activate the application" +fallback now only serves sessions whose id is not a UUID. + +Two reservations, carried by the code rather than by this text: the route is not +documented, and the handler reports nothing back — `open` exits 0 even when the +resume fails for a transcript missing from disk. Focusing over AppleScript +requires macOS Automation consent, not Accessibility; going through `claude://` +requires neither. + +## Traps encountered + +- **A `claude -p` session or a subagent emits hooks without ever entering the + roster.** Giving them a slot would fill all six keys with invisible sessions. + Rule retained: the roster arbitrates membership, the hooks only set a state on + a slot that is already open. Orphan states are held in a bounded pending queue, + and drops are counted. +- **`CLAUDE_CODE_CHILD_SESSION=1` does not filter nested sessions.** The main + session of a Desktop window carries it too. +- **The standard output of a `UserPromptSubmit` hook is injected into the + conversation context.** A chatty emitter would end up in the user's prompt. The + emitter writes nothing to stdout and always exits 0. +- **`CLAUDE_PLUGIN_DATA` is not a stable state path**: it is + `…/data/-inline` under `--plugin-dir` and `…/data/` once installed. + The state therefore lives under `~/.claude/thread-status/`, overridable with `CLAUDE_THREAD_STATUS_DIR`. -- **Le format des transcripts `.jsonl` est déclaré interne et change entre - versions.** Aucun composant ne les lit, y compris pour l'état. +- **The `.jsonl` transcript format is declared internal and changes between + versions.** No component reads them, including for state. -## Envoyer l'information au clavier : résultat négatif mesuré +## Sending the information to the keyboard: measured negative result -L'idée retenue jusqu'ici était de récupérer le protocole RGB en capturant -`~/Library/Logs/input/main.log` pendant que l'app ChatGPT anime les touches -Agent. **Cette voie est fermée**, et la mesure le montre sans ambiguïté. +The idea retained until then was to recover the RGB protocol by capturing +`~/Library/Logs/input/main.log` while the ChatGPT app animates the Agent keys. +**That path is closed**, and the measurement shows it unambiguously. -Le log contient bien 66 lignes `v.oai.*`, mais elles n'ont que deux formes : +The log does contain 66 `v.oai.*` lines, but they have only two shapes: ```text |wl_device_comm| No resolver found for id: 475 response: @@ -184,47 +186,46 @@ Le log contient bien 66 lignes `v.oai.*`, mais elles n'ont que deux formes : {"result":{"ok":1},"id":121,"method":"v.oai.rgbcfg"} ``` -Zéro ligne porte des `params`. Ce sont des **réponses orphelines** : le -périphérique HID est ouvert en mode non exclusif, ses rapports d'entrée sont -diffusés à tous les lecteurs, et Input journalise en avertissement les réponses à -des appels qu'il n'a jamais émis. Le sens qui nous intéresse — hôte → clavier, -celui qui porte les couleurs et les états — n'y passe jamais. - -Ce que la mesure donne quand même : - -- les deux méthodes sont appelées **en couple**, `rgbcfg` puis `thstatus` environ - 70 ms plus tard, et le couple se répète toutes les 35 à 40 secondes ; -- le périphérique accuse par `{"ok":1}`, donc les appels aboutissent ; -- les méthodes propres à Input dans le même log sont `device.status`, - `host.focused_app`, `fs.list`, `fs.readbin`, `sys.version` — le namespace - `v.oai.*` est bien distinct et n'appartient pas à Input. - -Trois verrous subsistent, et ils sont indépendants : - -1. **Le format des requêtes est inconnu.** Le récupérer demande une capture du - bus USB (rapports de sortie de l'app ChatGPT vers le périphérique), pas une - lecture de log. -2. **La contention reste entière.** Dernière écriture gagnante, et l'app ChatGPT - repousse sa configuration toutes les 35 à 40 secondes : des couleurs écrites - par un tiers seraient recouvertes à cette cadence tant qu'elle tourne. -3. **Aucun canal sanctionné n'existe.** Pas de SDK Work Louder public, et - Hardware Buddy n'expose que des compteurs agrégés, sans identité de session. - -Le VID Espressif rapproche le matériel de la famille de l'exemple ESP32 publié -avec Hardware Buddy. Cela ne rend pas le firmware modifiable pour autant, et -l'interdiction de flasher reste entière : voir l'avertissement dans +Not one line carries `params`. These are **orphan responses**: the HID device is +opened non-exclusively, its input reports are broadcast to every reader, and +Input logs, as warnings, the responses to calls it never made. The direction we +care about — host → keyboard, the one carrying colours and states — never appears +there. + +What the measurement gives anyway: + +- both methods are called **as a pair**, `rgbcfg` then `thstatus` roughly 70ms + later, and the pair repeats every 35 to 40 seconds; +- the device acknowledges with `{"ok":1}`, so the calls do land; +- Input's own methods in the same log are `device.status`, `host.focused_app`, + `fs.list`, `fs.readbin`, `sys.version` — the `v.oai.*` namespace is clearly + distinct and does not belong to Input. + +Three locks remained at that point, and they were independent: + +1. **The request format is unknown.** Recovering it requires a USB bus capture + (output reports from the ChatGPT app to the device), not reading a log. +2. **Contention is untouched.** Last write wins, and the ChatGPT app pushes its + configuration again every 35 to 40 seconds: colours written by a third party + would be overwritten at that cadence while it runs. +3. **No sanctioned channel exists.** No public Work Louder SDK, and Hardware + Buddy only exposes aggregate counters, with no session identity. + +The Espressif VID puts the hardware in the same family as the ESP32 example +published with Hardware Buddy. That does not make the firmware modifiable, and +the ban on flashing stands in full: see the warning in [`appsense-behavior.md`](appsense-behavior.md). -### Le modèle d'éclairage d'Input ne peut pas exprimer six couleurs +### Input's lighting model cannot express six colours -Question suivante, plus intéressante : puisque l'app Work Louder pilote bien -l'éclairage, son propre canal suffirait-il ? **Non, et pour une raison de -structure, pas de documentation.** +The next question, and a more interesting one: since the Work Louder app does +drive the lighting, would its own channel be enough? **No, and for a structural +reason, not a documentation one.** -L'app ne modélise pas les méthodes `v.oai.*` : zéro occurrence de `v.oai`, -`rgbcfg` ou `thstatus` dans ses 226 Mo d'`app.asar`. Son éclairage voyage dans la -configuration du périphérique, sous la forme suivante — relevée dans -`~/Library/Application Support/input/devices//keymap.json` : +The app does not model the `v.oai.*` methods: zero occurrences of `v.oai`, +`rgbcfg` or `thstatus` in its 226 MB `app.asar`. Its lighting travels inside the +device configuration, in the following shape — read from +`~/Library/Application Support/input/devices//keymap.json`: ```json "layers": [{ @@ -237,43 +238,41 @@ configuration du périphérique, sous la forme suivante — relevée dans }] ``` -`14251863` vaut `#D97757` : la couleur Claude du dépôt est déjà en place sur le -matériel. Et c'est tout ce que le format permet : - -- **deux zones par layer**, `backlight` et `underglow`, une seule couleur chacune ; -- `layout.keymap` est un tableau plat de chaînes de keycodes — aucun objet par - touche, donc **aucun emplacement où mettre une couleur par touche**. Une - recherche exhaustive de tout entier de type couleur hors `lights` ne retourne - rien. - -Conséquence : même en connaissant parfaitement le format d'Input, on ne peut pas -allumer six touches de six couleurs. Le canal par touche appartient exclusivement -au namespace privé `v.oai.*` de l'app ChatGPT, qu'Input ignore complètement. - -Ce que le canal d'Input **peut** faire, en revanche : un signal agrégé sur le -`backlight`, précisément la zone que l'app ChatGPT laisse tranquille d'après -[`appsense-behavior.md`](appsense-behavior.md). Une couleur pour « une session -attend une décision », une autre pour « au moins une travaille », une troisième -pour « tout est terminé ». Le format est connu, la couleur Claude y est déjà. - -Cette voie n'est pas ouverte pour autant : elle exige d'écrire la configuration du -périphérique en cours de session, ce que le périmètre du dépôt exclut -explicitement — « aucune écriture directe dans le stockage Input ou le -périphérique » — et elle entrerait en concurrence avec les propres poussées -d'Input. Elle demande donc une décision de périmètre, pas seulement du code. - -Réserve de mesure : la copie locale de `keymap.json` peut être en retard sur ce qui -a réellement été poussé au périphérique, et l'observation ci-dessus n'a pas été -recoupée avec une ligne `sending device config` — le log courant n'en contient -aucune. - -### Le canal par touche existe, et il est lisible localement - -L'app ChatGPT embarque le SDK privé qui parle au Codex Micro : -`@worklouder/device-kit-oai` `0.1.11` et `@worklouder/wl-device-kit`, dans -`/Applications/ChatGPT.app/Contents/Resources/app.asar`. Le fichier +`14251863` is `#D97757`: the repository's Claude colour is already in place on +the hardware. And that is all the format allows: + +- **two zones per layer**, `backlight` and `underglow`, one colour each; +- `layout.keymap` is a flat array of keycode strings — no per-key object, and + therefore **nowhere to put a per-key colour**. An exhaustive search for any + colour-shaped integer outside `lights` returns nothing. + +Consequence: even with a perfect knowledge of Input's format, you cannot light +six keys in six colours. The per-key channel belongs exclusively to the ChatGPT +app's private `v.oai.*` namespace, which Input ignores completely. + +What Input's channel **can** do, on the other hand: an aggregate signal on the +`backlight`, precisely the zone the ChatGPT app leaves alone according to +[`appsense-behavior.md`](appsense-behavior.md). One colour for "a session is +waiting on a decision", another for "at least one is working", a third for +"everything is done". The format is known, and the Claude colour is already +there. + +That path is not open for all that: it requires writing the device configuration +during a session, which the repository's scope explicitly excludes — "no direct +write into Input's storage or the device" — and it would compete with Input's own +pushes. It therefore calls for a scope decision, not only code. + +Measurement caveat: the local copy of `keymap.json` can lag behind what was +actually pushed to the device, and the observation above was not cross-checked +against a `sending device config` line — the current log contains none. + +### The per-key channel exists, and it is readable locally + +The ChatGPT app bundles the private SDK that talks to the Codex Micro: +`@worklouder/device-kit-oai` `0.1.11` and `@worklouder/wl-device-kit`, inside +`/Applications/ChatGPT.app/Contents/Resources/app.asar`. The file `node_modules/@worklouder/device-kit-oai/dist/rpc_api_oai/rpc_api_oai.js` -contient l'énumération, en clair : +contains the enumeration, in plain text: ```js // Vendor specific. @@ -281,10 +280,9 @@ VendorJsonRpcMethods["ThreadsLighting"] = "v.oai.thstatus"; /** Per-thread acce VendorJsonRpcMethods["RgbConfig"] = "v.oai.rgbcfg"; /** Keys and ambient zone lighting. */ ``` -Le commentaire tranche la question : `thstatus` est **l'éclairage par thread**, -`rgbcfg` ne couvre que les deux zones globales — ce qui recoupe exactement le -modèle à deux zones d'Input mesuré plus haut. La méthode d'envoi est documentée -dans le bundle : +The comment settles the question: `thstatus` is the **per-thread** lighting, +`rgbcfg` only covers the two global zones — which matches Input's two-zone model +measured above exactly. The sending method is documented in the bundle: ```js // Only the thread id is required on each entry. […] optional color, brightness, @@ -295,125 +293,117 @@ async sendThreadsLighting(threads) { let minimized = threads.map((thread) => { return { id: thread.id, c: thread.color, … } }); ``` -Donc : un tableau d'entrées, une par emplacement, chacune adressée par `id`, avec -`c` en entier `0xRRGGBB`, `b` et `s` normalisés de 0 à 1, et des drapeaux de -synchronisation. Les mises à jour partielles sont prévues. L'énumération des -effets est également en clair : `snake = 2`, `rainbow = 3`, `breath = 4`, -`gradient = 5`, `shallowBreath = 6`. +So: an array of entries, one per slot, each addressed by `id`, with `c` as a +`0xRRGGBB` integer, `b` and `s` normalised from 0 to 1, and sync flags. Partial +updates are provided for. The effect enumeration is in plain text too: +`snake = 2`, `rainbow = 3`, `breath = 4`, `gradient = 5`, `shallowBreath = 6`. -Deux méthodes supplémentaires existent dans le même namespace et ne sont pas -étudiées ici : `v.oai.hid` et `v.oai.rad`. +Two further methods exist in the same namespace and are not studied here: +`v.oai.hid` and `v.oai.rad`. -### Cadre HID confirmé sur matériel +### HID frame confirmed on hardware -Le cadre rapporté est exact. Une requête no-op — une entrée réduite à `{"id":0}`, -qui d'après le SDK ne change aucune couleur — a été envoyée au périphérique et -acquittée : +The reported frame is correct. A no-op request — an entry reduced to `{"id":0}`, +which according to the SDK changes no colour — was sent to the device and +acknowledged: ```text -envoi 0602367b226d6574686f64223a22762e… {"method":"v.oai.thstatus","params":[{"id":0}],"id":1} -réponse 0602367b22726573756c74223a7b226f… {"result":{"ok":1},"id":1,"method":"v.oai.thstatus"} +sent 0602367b226d6574686f64223a22762e… {"method":"v.oai.thstatus","params":[{"id":0}],"id":1} +response 0602367b22726573756c74223a7b226f… {"result":{"ok":1},"id":1,"method":"v.oai.thstatus"} ``` -Rapports de 64 octets, octet 0 = report ID `0x06`, octet 1 = canal `0x02`, -octet 2 = longueur, charge utile UTF-8 à partir de l'octet 3, soit 61 octets par -rapport. **Le même cadre sert dans les deux sens.** - -**La fragmentation est confirmée, dans le schéma du SDK.** L'hypothèse -initiale — octet 2 du premier rapport portant la longueur totale — est réfutée -par la mesure. Le schéma qui fonctionne est celui lu dans le bundle : **l'octet -2 porte la longueur du fragment, sur chaque rapport**, la charge utile étant -découpée en fragments de 61 octets réassemblés par le périphérique. Preuve : -des poussées `thstatus` de ~140 octets (six entrées, trois rapports) ont été -acquittées avec l'`id` corrélé, et les touches se sont allumées une par une — -sonde `scripts/lighting.mjs`, cadrage `scripts/lib/hid-frame.mjs`. - -Le piège mérite d'être retenu, car il invalide silencieusement toute mesure : -**les réponses du périphérique sont diffusées à tous les lecteurs du HID**, et -l'app ChatGPT en provoque toutes les 35 à 40 secondes. Une sonde qui prend la -première réponse venue prend donc les accusés de ChatGPT pour les siens. Six -écritures avaient ainsi été déclarées réussies alors qu'aucune n'avait abouti — -ce qui explique l'absence de tout changement visible. Corrélées par le champ -`id`, les mêmes charges de 101 octets (schéma « longueur totale ») ne reçoivent -aucune réponse. - -Toute sonde sur ce périphérique doit donc vérifier l'`id` de la réponse. C'est la -leçon la plus coûteuse de cette campagne de mesure. - -Le routage se fait par le report ID, pas par la collection : IOKit énumère un -seul périphérique là où hidapi voit quatre collections, et `SetReport` avec -l'identifiant `0x06` atteint le bon canal. - -**`node-hid` fait ce travail sur macOS, à condition de demander le non exclusif.** -L'échec mesuré lors de la première campagne venait d'une ouverture -`new HID.HID(path)` **sans option** : hidapi ouvre alors en mode *seize*, refusé -tant qu'une autre application tient le périphérique. `node-hid` 3.4.0 expose -pourtant `hid_darwin_set_open_exclusive` — `src/HIDAsync.cc:137` — -via `HIDAsync.open(path, { nonExclusive: true })`, et le round-trip -`sys.version` réussit ainsi pendant que ChatGPT tient le même périphérique -(transport livré : `scripts/lib/hid-device.mjs`). L'ouverture non exclusive -IOKit (`kIOHIDOptionsTypeNone`), validée par la sonde Swift, revient au même ; -les deux voies fonctionnent. - -Reste rapporté et non revérifié : le garde-fou Electron, selon lequel -`codexMicro.updateLighting` n'accepterait les mises à jour que depuis le -`webContents` de la fenêtre principale de ChatGPT. - -### Acquitté n'est pas affiché - -Le périphérique acquitte `{"ok":1}` sur les six emplacements, avec l'entrée -canonique complète — `c`, `b`, `e`, `s`, `sk`, `sa` — et **rien ne change à -l'écran du clavier**. L'accusé porte donc sur la réception de l'appel, pas sur -son rendu. - -Trois hypothèses, non départagées, par ordre de conséquence pour le produit : - -1. **Dépendance au layer.** L'observation a été faite sur le layer `Claude`, dont - la configuration Input impose déjà un `backlight` solide `#D97757`. Si - l'éclairage par thread n'est rendu que sur le layer Codex natif, où les touches - Agent sont effectivement des touches Agent, alors la fonction est inatteignable - là où ce projet en a besoin. C'est l'hypothèse à écarter en premier. -2. **`sk` inversé.** `syncKeysLighting` peut signifier « propage cette couleur à - la zone des touches » aussi bien que « cet emplacement suit la zone des - touches » — la seconde lecture écraserait la couleur demandée. -3. **`rgbcfg` conditionne `thstatus`.** L'app ChatGPT envoie toujours les deux en - couple, `rgbcfg` puis `thstatus` 70 ms après. La zone des touches doit - peut-être être placée dans un mode qui autorise les accents. - -Ces hypothèses ne se départagent pas par la lecture : elles demandent une -observation du clavier par une personne, un essai à la fois. - -### Le rendu marche, mais une fois - -Une charge mono-rapport — `{"method":"v.oai.thstatus","params":[{"id":0,"c":255,"e":1}]}`, -61 octets — a **réellement allumé la première touche en bleu**, puis l'a éteinte -à la restauration. La chaîne complète est donc démontrée : cadre, adressage par -emplacement, encodage de couleur, effet solide, extinction. - -Aux relances suivantes, le même appel ne produit plus rien. Et pendant -l'observation, les autres touches ont repris une teinte orange **une par une** : -l'app ChatGPT réassère son propre état. - -Un succès non reproductible, sur un périphérique où un second écrivain repeint -périodiquement, ne se lit pas comme un protocole défaillant. Il se lit comme une -**course perdue**. Le protocole est acquis ; ce qui manque est le contrôle -exclusif de la surface d'affichage. - -Le test qui tranche est le plus simple possible : quitter l'app ChatGPT et -relancer. S'il devient reproductible, le diagnostic est clos et la contention est -le seul obstacle restant — celui-là même que ce document annonçait comme non -résoluble de façon déterministe. - -### La contrainte de layer, et sa résolution - -**Résolu.** Le firmware ne rend l'éclairage par thread que sur les touches dont le -keycode est `KV_OAI_AG00` à `KV_OAI_AG05`. Ce n'est pas l'index du layer qui -compte : c'est que le firmware doit savoir quelle touche physique est -l'emplacement N, et le keycode est ce qui le lui dit. Sur un layer où ces -positions valent `KC_NONE`, il n'y a aucun emplacement à peindre. - -L'indice décisif se lisait dans le bundle d'Input, où le layer Codex natif est -défini exactement ainsi : +64-byte reports, byte 0 = report ID `0x06`, byte 1 = channel `0x02`, byte 2 = +length, UTF-8 payload from byte 3, so 61 bytes per report. **The same frame +serves in both directions.** + +**Fragmentation is confirmed, in the SDK's scheme.** The initial hypothesis — +byte 2 of the first report carrying the total length — is refuted by measurement. +The scheme that works is the one read in the bundle: **byte 2 carries the chunk +length, on every report**, the payload being split into 61-byte chunks +reassembled by the device. Proof: `thstatus` pushes of ~140 bytes (six entries, +three reports) were acknowledged with a correlated `id`, and the keys lit one by +one — `scripts/lighting.mjs` probe, framing in `scripts/lib/hid-frame.mjs`. + +The trap is worth remembering, because it silently invalidates any measurement: +**the device's responses are broadcast to every HID reader**, and the ChatGPT app +provokes some every 35 to 40 seconds. A probe that takes the first response it +sees therefore takes ChatGPT's acknowledgements for its own. Six writes had been +declared successful that way while none had landed — which explains the absence +of any visible change. Correlated by the `id` field, the same 101-byte payloads +("total length" scheme) receive no response at all. + +Any probe on this device must therefore check the response's `id`. That is the +most expensive lesson of this measurement campaign. + +Routing is done by report ID, not by collection: IOKit enumerates a single device +where hidapi sees four collections, and `SetReport` with identifier `0x06` +reaches the right channel. + +**`node-hid` does this job on macOS, provided you ask for non-exclusive.** The +failure measured during the first campaign came from a `new HID.HID(path)` open +**without options**: hidapi then opens in *seize* mode, refused while another +application holds the device. `node-hid` 3.4.0 does expose +`hid_darwin_set_open_exclusive` — `src/HIDAsync.cc:137` — through +`HIDAsync.open(path, { nonExclusive: true })`, and the `sys.version` round-trip +succeeds that way while ChatGPT holds the same device (shipped transport: +`scripts/lib/hid-device.mjs`). The IOKit non-exclusive open +(`kIOHIDOptionsTypeNone`), validated by the Swift probe, amounts to the same; +both routes work. + +Still reported and not re-verified: the Electron guard, according to which +`codexMicro.updateLighting` would only accept updates from the `webContents` of +ChatGPT's main window. + +### Acknowledged is not displayed + +The device acknowledged `{"ok":1}` on all six slots, with the full canonical +entry — `c`, `b`, `e`, `s`, `sk`, `sa` — and **nothing changed on the keyboard**. +The acknowledgement therefore covers receiving the call, not rendering it. + +Three hypotheses were on the table at that point, in order of consequence for the +product: + +1. **Layer dependency.** The observation was made on the `Claude` layer, whose + Input configuration already forces a solid `#D97757` `backlight`. If + per-thread lighting only renders on the native Codex layer, where the Agent + keys really are Agent keys, then the feature is unreachable where this project + needs it. That was the hypothesis to rule out first. +2. **`sk` inverted.** `syncKeysLighting` could mean "propagate this colour to the + key zone" just as well as "this slot follows the key zone" — the second + reading would overwrite the requested colour. +3. **`rgbcfg` gates `thstatus`.** The ChatGPT app always sends the two as a pair, + `rgbcfg` then `thstatus` 70ms later. The key zone may need to be put into a + mode that allows accents. + +Hypothesis 1 turned out to be the right track, but not for the reason stated: see +below. Hypotheses 2 and 3 were never needed. + +### Rendering works, but once + +A single-report payload — +`{"method":"v.oai.thstatus","params":[{"id":0,"c":255,"e":1}]}`, 61 bytes — +**actually lit the first key blue**, then turned it off on restore. The complete +chain was therefore demonstrated: frame, addressing by slot, colour encoding, +solid effect, turn-off. + +On subsequent runs, the same call produced nothing. And during the observation, +the other keys took on an orange tint **one by one**: the ChatGPT app reasserting +its own state. + +A non-reproducible success, on a device where a second writer periodically +repaints, does not read as a failing protocol. It reads as a **lost race**. The +protocol was acquired; what was missing was exclusive control of the display +surface. + +### The layer constraint, and its resolution + +**Resolved.** The firmware only renders per-thread lighting on keys whose keycode +is `KV_OAI_AG00` to `KV_OAI_AG05`. It is not the layer index that matters: the +firmware has to know which physical key is slot N, and the keycode is what tells +it. On a layer where those positions are `KC_NONE`, there is no slot to paint. + +The decisive clue was in Input's bundle, where the native Codex layer is defined +exactly like this: ```js base: [ @@ -423,139 +413,140 @@ base: [ ] ``` -Deux touches puis quatre — la géométrie exacte des six touches Agent, dans -l'ordre confirmé à l'œil. +Two keys then four — the exact geometry of the six Agent keys, in the order +confirmed by eye. -Ces keycodes n'apparaissent qu'**une seule fois** chacun dans les 226 Mo de -l'`app.asar` d'Input : ils sont absents de son sélecteur de touches, donc -inassignables depuis l'interface. D'où -[`scripts/enable-agent-keys.mjs`](../../scripts/enable-agent-keys.mjs), qui les -écrit dans un export de profile à réimporter par le flux officiel **Import -Profile**, sans jamais toucher au layer d'index 0. +These keycodes appear **only once** each in Input's 226 MB `app.asar`: they are +absent from its key picker, and therefore unassignable from the interface. Hence +[`scripts/enable-agent-keys.mjs`](../../scripts/enable-agent-keys.mjs), which +writes them into a profile export to be re-imported through the official **Import +Profile** flow, without ever touching the layer at index 0. -Aucun raccourci Claude n'est perdu : ces six positions étaient `no-action`, et les -raccourcis vivent sur la rangée suivante. +No Claude shortcut is lost: those six positions were `no-action`, and the +shortcuts live on the next row. -**Mais le coût n'est pas nul, et il n'est pas seulement théorique.** Ces keycodes -ne sont pas de simples marqueurs d'affichage : le firmware émet une notification -`v.oai.hid`, et **l'app ChatGPT y réagit en changeant de thread Codex**. Mesuré : -sur le layer `Claude`, appuyer sur une touche Agent fait basculer les threads -Codex. +**But the cost is not zero, and it is not merely theoretical.** These keycodes +are not simple display markers: the firmware emits a `v.oai.hid` notification, +and **the ChatGPT app reacts to it by switching Codex thread**. Measured: on the +`Claude` layer, pressing an Agent key switches Codex threads. -La même application contend donc les deux moitiés de la fonction : +The same application therefore contends for both halves of the feature: -| Ressource | Ce que fait l'app ChatGPT | +| Resource | What the ChatGPT app does | | --- | --- | -| les six LED | réécrit sa configuration toutes les 35 à 40 s | -| les six appuis | intercepte et change de thread Codex | - -Il n'y a pas d'arbitrage possible : les notifications sont diffusées à tous les -lecteurs, et rien ne permet de demander à ChatGPT de se taire. **Quitter l'app -ChatGPT résout les deux d'un coup** — les écritures d'éclairage ne sont plus -recouvertes, et les appuis n'ont plus qu'un seul destinataire. +| the six LEDs | rewrites its configuration every 35 to 40s | +| the six presses | intercepts them and switches Codex thread | -C'est donc la condition d'usage réelle de la fonction, et elle doit être annoncée -comme telle : les six témoins Claude et l'app ChatGPT ne cohabitent pas. +There is no arbitration possible: notifications are broadcast to every reader, +and nothing can ask ChatGPT to be quiet. **Quitting the ChatGPT app solves both +at once** — lighting writes are no longer overwritten, and presses have a single +recipient. -**Vérifié sur matériel** : après import, layer `Claude` actif, -`lighting set all #00FF00` allume bien les six touches en vert. +That is therefore the real condition of use of the feature, and it has to be +announced as such: the six Claude lights and the ChatGPT app do not coexist. -### Les deux contournements qui avaient échoué +**Verified on hardware**: after import, with the `Claude` layer active, +`lighting set all #00FF00` does light all six keys green. -Le protocole fonctionne, mais **l'éclairage par thread ne rend que sur le layer -Codex natif**. Sur le layer `Claude`, les écritures sont acquittées et rien ne -s'affiche. +### The two workarounds that had failed -C'est la contrainte la plus lourde du projet, car les deux besoins s'excluent : -le layer `Claude` existe pour porter les raccourcis Claude, et c'est précisément -là que les couleurs d'état seraient utiles. +Before the keycode explanation was found, the working theory was that per-thread +lighting only rendered on the native Codex layer, because on the `Claude` layer +the writes were acknowledged and nothing appeared. -L'explication probable tient au modèle d'éclairage d'Input mesuré plus haut : -chaque layer porte son propre `lights.backlight`, et celui du layer `Claude` est -un `solid` à `#D97757`. Le rendu du layer recouvrirait alors les accents par -thread, que le firmware ne compose peut-être que sur le layer qui porte le rôle -« touches Agent ». +The probable explanation seemed to lie in Input's lighting model measured above: +each layer carries its own `lights.backlight`, and the `Claude` layer's is a +`solid` at `#D97757`. The layer's rendering would then cover the per-thread +accents, which the firmware might only compose on the layer that carries the +"Agent keys" role. -Deux contournements, du moins coûteux au plus coûteux : +Two workarounds were tried, from least to most costly: -1. **Neutraliser la zone des touches à l'exécution**, par `v.oai.rgbcfg` avec un - effet `off` sur `keys`, puis pousser les accents. **Éprouvé, sans effet** : sur - le layer `Claude` rien ne s'affiche, et la même écriture rend normalement dès - que le layer Codex redevient actif. `rgbcfg` règle une zone globale, pas le - `backlight` propre au layer, qui l'emporte tant que ce layer est actif. -2. **Neutraliser le `backlight` du layer `Claude`**, à `off` ou en luminosité 0, - par l'app Input. **Éprouvé, sans effet** : le layer `Claude` reste noir, et la - même écriture applique la couleur sur les six touches dès que le layer Codex - redevient actif. +1. **Neutralising the key zone at runtime**, with `v.oai.rgbcfg` and an `off` + effect on `keys`, then pushing the accents. **Tried, no effect**: on the + `Claude` layer nothing appears, and the same write renders normally as soon as + the Codex layer becomes active again. `rgbcfg` sets a global zone, not the + layer's own `backlight`, which wins while that layer is active. +2. **Neutralising the `Claude` layer's `backlight`**, to `off` or brightness 0, + through the Input app. **Tried, no effect**: the `Claude` layer stays black, + and the same write applies the colour to all six keys as soon as the Codex + layer becomes active again. -Ces deux échecs pointaient dans la mauvaise direction : ils cherchaient ce qui -*recouvrait* les accents, alors que le firmware n'en composait aucun, faute de -keycode pour identifier les emplacements. +Both failures pointed in the wrong direction: they looked for what was *covering* +the accents, whereas the firmware was composing none, for lack of a keycode to +identify the slots. -### Ce qui bloque désormais n'est plus technique +### What blocks now is no longer technical -| Verrou | État | +| Lock | State | | --- | --- | -| Connaître le protocole par touche | **levé**, lisible localement | -| Licence | `UNLICENSED`, paquet privé, registre GitHub Packages fermé | -| Concurrence d'écriture | entière : ChatGPT repousse toutes les 35 à 40 s | -| Canal sanctionné | inexistant, ni OpenAI ni Work Louder | - -La distinction utile pour un dépôt public : **documenter un format observé** est -ce que ce dépôt fait déjà pour Input ; **redistribuer le SDK ou son code** est -exclu par sa licence. Réimplémenter le format observé pour interopérer avec un -périphérique que l'on possède est la voie habituelle, et reste une décision à -prendre en connaissance de cause, pas un acquis. - -Reste que le verrou de concurrence n'est pas résolu par la connaissance du -format : deux écrivains sur un HID non exclusif, dernière écriture gagnante, et -l'app ChatGPT réémet périodiquement. Aucune stratégie de coexistence déterministe -n'a été identifiée — seulement des hypothèses non testées, dont retirer à ChatGPT -l'autorisation macOS de surveillance des saisies, ce qui désactiverait aussi ses -propres touches. - -## Ce qui est implémenté - -| Composant | Chemin | +| Knowing the per-key protocol | **lifted**, readable locally | +| Licence | `UNLICENSED`, private package, closed GitHub Packages registry | +| Write contention | untouched: ChatGPT pushes again every 35 to 40s | +| Sanctioned channel | non-existent, neither OpenAI nor Work Louder | + +The useful distinction for a public repository: **documenting an observed format** +is what this repository already does for Input; **redistributing the SDK or its +code** is excluded by its licence. Reimplementing the observed format to +interoperate with a device you own is the usual route, and remains a decision to +take knowingly, not a given. + +The contention lock is not solved by knowing the format: two writers on a +non-exclusive HID, last write wins, and the ChatGPT app re-emits periodically. No +deterministic coexistence strategy has been identified — only untested +hypotheses, among them removing ChatGPT's macOS input-monitoring permission, +which would also disable its own keys. + +## What is implemented + +| Component | Path | | --- | --- | -| Plugin de hooks | [`thread-status/`](../../thread-status/README.md) | -| Réducteur pur, six emplacements | `scripts/lib/thread-slots.mjs` | -| Compagnon `watch` / `status` / `focus` / `doctor` | `scripts/thread-status.mjs` | -| Tests | `tests/thread-slots.test.mjs` | - -La sortie du compagnon est `~/.claude/thread-status/slots.json`. C'est la couture -prévue pour un futur `DeviceAdapter` : tant que le protocole RGB n'est pas mesuré, -la chaîne s'arrête à ce fichier. - -Les touches physiques ne sont pas encore reliées. Les six touches Agent sont -`key-9`, `key-10`, `key-5`, `key-6`, `key-7`, `key-8` — voir -`KEY_CONTROL_LOCATIONS` dans `shared/input-profile.mjs`. Le raccourci global qui -appellerait `focus ` demande une API native (`RegisterEventHotKey`), qui -n'exige pas l'Accessibilité mais sort du périmètre d'un script Node. - -## Ce qui reste non établi - -- Le focus AppleScript par `tty` n'a pas été exercé de bout en bout : aucune - session Claude Code en terminal n'était disponible, et iTerm2 n'est pas installé - sur la machine de mesure. Seule Terminal.app pourrait être testée. -- La liste complète des valeurs de `CLAUDE_CODE_ENTRYPOINT`. Une seule est - observée : `claude-desktop`. -- Si `claude agents --json` inclut les sessions lancées par une extension d'IDE. -- Le cadre HID exact, non revérifié ici. Une capture passive du bus USB n'est plus - nécessaire pour obtenir le format, seulement pour le confirmer à l'exécution. -- Toute stratégie de coexistence avec l'app ChatGPT sur le même périphérique HID. - Aucune n'a été testée, et aucune ne paraît déterministe. -- `v.oai.hid` et `v.oai.rad`, présents dans le même namespace, non étudiés. -- Si Work Louder ou OpenAI accepteraient d'ouvrir le canal. C'est la seule voie - qui lèverait à la fois la licence, la concurrence et la pérennité. -- Le comportement du roster lorsque plus de six sessions vivent en parallèle du - point de vue de l'utilisateur : le débordement est compté et signalé, mais - l'ergonomie retenue n'est pas validée. +| Hooks plugin | [`thread-status/`](../../thread-status/README.md) | +| Pure reducer, six slots | `scripts/lib/thread-slots.mjs` | +| `watch` / `status` / `focus` / `doctor` companion | `scripts/thread-status.mjs` | +| HID framing and lighting model | `scripts/lib/hid-frame.mjs`, `scripts/lib/hid-lighting.mjs` | +| `node-hid` transport | `scripts/lib/hid-device.mjs` | +| Lighting CLI, `DeviceAdapter` | `scripts/lighting.mjs` | +| Agent keycodes on the `Claude` layer | `scripts/enable-agent-keys.mjs` | +| Tests | `tests/thread-slots.test.mjs`, `tests/hid-frame.test.mjs`, `tests/hid-lighting.test.mjs` | + +The companion's output is `~/.claude/thread-status/slots.json`. It is the seam +the `DeviceAdapter` consumes: `node scripts/lighting.mjs watch` follows that file +and pushes the six state colours to the keyboard. + +The physical keys are wired: the six Agent keys are `key-9`, `key-10`, `key-5`, +`key-6`, `key-7`, `key-8` — see `KEY_CONTROL_LOCATIONS` in +`shared/input-profile.mjs` — and they emit `v.oai.hid` on the HID channel already +open, so `npm run lighting -- watch --focus` routes a press to `focus ` with +no native global shortcut and no macOS permission. + +## What is still unestablished + +- AppleScript focus by `tty` has not been exercised end to end: no terminal + Claude Code session was available, and iTerm2 is not installed on the + measurement machine. Only Terminal.app could be tested. +- The complete list of `CLAUDE_CODE_ENTRYPOINT` values. Only one is observed: + `claude-desktop`. +- Whether `claude agents --json` includes sessions started by an IDE extension. +- Whether `claude://resume` fails, and how, when the transcript is missing from + disk. The handler has a `transcript_missing` error path, but `open` exits 0 + regardless, so nothing surfaces to the caller. +- How long the undocumented `claude://resume` route will last. It is verified on + Claude `1.24012.9` and can change with an application update. +- Any coexistence strategy with the ChatGPT app on the same HID device. None has + been tested, and none looks deterministic. +- `v.oai.hid` and `v.oai.rad`, present in the same namespace, not studied. +- Whether Work Louder or OpenAI would agree to open the channel. That is the only + route that would lift the licence, the contention and the longevity questions + at once. +- The roster's behaviour, from the user's point of view, when more than six + sessions are alive: overflow is counted and reported, but the ergonomics + retained are not validated. ## Sources - [Claude Code — hooks](https://code.claude.com/docs/en/hooks) -- [Claude Code — gestion des sessions](https://code.claude.com/docs/en/sessions) -- [Claude Code — création de plugins](https://code.claude.com/docs/en/plugins) -- [Claude Desktop — ouvrir avec un lien](https://support.claude.com/en/articles/14729294-open-claude-desktop-with-a-link) +- [Claude Code — session management](https://code.claude.com/docs/en/sessions) +- [Claude Code — plugin creation](https://code.claude.com/docs/en/plugins) +- [Claude Desktop — open with a link](https://support.claude.com/en/articles/14729294-open-claude-desktop-with-a-link) - [Anthropic — Hardware Buddy BLE Protocol](https://github.com/anthropics/claude-desktop-buddy/blob/main/REFERENCE.md) diff --git a/docs/roadmap.md b/docs/roadmap.md index 84e2a70..6deeb2a 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -1,115 +1,118 @@ -# Feuille de route - -## État actuel - -Le dépôt public contient désormais : - -- un manifeste communautaire et un mapping physique Claude ; -- des schémas réutilisables pour les futurs presets ; -- une représentation SVG originale ; -- un outil d'inventaire, sauvegarde, dry-run, sanitation et rollback ; -- un générateur local de profile Input `0.17.3` qui conserve AppSense ; -- des tests transactionnels sur copies isolées ; -- une analyse reproductible du format de partage d'Input `0.17.2` ; -- une piste BLE séparée, explicitement non fonctionnelle. - -Le vrai export officiel Claude `*-layer.json` et la validation matérielle -complète restent manquants. Le preset conserve donc le statut -`hardware-observed`. - -## V1 — preset Claude vérifié - -Objectif : obtenir un premier layer reproductible sans modifier le layer Codex -natif. - -- [ ] exécuter `git status --short` dans la copie locale et préserver les - modifications sans rapport ; -- [x] exporter le profile Input réel et inventorier profils, layers, actions et - liens AppSense ; -- [x] fournir une sauvegarde locale vérifiée et un rollback transactionnel ; -- [x] identifier les flux officiels Import/Export layer et profile d'Input - `0.17.2` ; -- [x] définir le manifeste, le mapping physique, la couleur et les exclusions ; -- [x] protéger l'index `0`, exiger un unique layer Claude existant et conserver - son AppSense ; -- [x] générer localement un nouveau profile Input `0.17.3` ; -- [ ] vérifier les positions, les touches, le cadran et le joystick ; -- [ ] vérifier AppSense, la perte de focus et les liens concurrents ; -- [ ] vérifier la persistance après redémarrage d'Input ; -- [ ] exporter et assainir le vrai `*-layer.json` ; -- [ ] tester l'import sur une configuration isolée et le second import ; -- [ ] restaurer le profile d'origine et vérifier le périphérique ; -- [ ] promouvoir le niveau de preuve et sortir la PR du mode brouillon. - -La V1 est terminée uniquement si une autre personne peut reproduire le résultat -sans identifiant local ni remplacement implicite d'un layer. - -## V2 — portabilité généralisée - -Les fondations minimales sont déjà présentes, mais ne sont pas déclarées -stables : - -- [x] manifeste commun et schémas V1 ; -- [x] simulation, sauvegarde, sanitation et tests de rollback sur fixtures ; -- [x] sélection d'un unique layer existant et refus de l'absence/duplication ; -- [x] aperçu visuel du mapping ; -- [ ] prise en charge d'un artefact officiel vérifié ; -- [ ] comparaison structurelle avant/après depuis de vrais exports ; -- [ ] détection des incompatibilités Input/firmware ; -- [ ] journal de validation matérielle signé par versions et sommes de contrôle ; -- [ ] automatisation du rollback officiel si Input expose un canal supporté. - -Aucun patch direct du stockage Input ne deviendra le chemin normal tant que son -format et son effet sur le périphérique ne sont pas prouvés. - -## V3 — catalogue communautaire - -- ajouter un preset « Claude Code » (terminal) en premier candidat naturel ; -- ajouter des presets IDE, navigateur, recherche, Figma et Framer ; -- indexer les presets par application, plateforme et compatibilité ; -- utiliser les modèles GitHub de proposition et de pull request ; -- exiger une méthode de sauvegarde et de retour arrière ; -- publier les résultats négatifs et incompatibilités connus ; -- permettre plusieurs représentations physiques sans identifiants locaux. - -## V4 — expérience simplifiée - -- catalogue lisible depuis une interface dédiée ; -- aperçu interactif du clavier ; -- comparaison avant/après ; -- installation guidée avec consentement explicite ; -- mises à jour versionnées sans écraser les personnalisations locales. - -## Piste parallèle : Hardware Buddy - -La recherche BLE reste indépendante. Elle ne rejoint la feuille de route -principale que si le service Nordic UART, la coexistence HID et une procédure -de restauration sûre sont démontrés sur le Codex Micro exact. - -## Piste parallèle : statut des sessions Claude Code - -Une seconde piste couvre les six touches Agent : afficher l'état des sessions -Claude Code locales et sauter à la bonne session. Contrairement à Hardware Buddy, -ses deux briques centrales reposent sur des mécanismes documentés — le roster -`claude agents --json` et les hooks — et sont implémentées : - -- [x] plugin de hooks et journal publiable ([`thread-status/`](../thread-status/README.md)) ; -- [x] réducteur à six emplacements, testé sans matériel ; -- [x] compagnon `watch` / `status` / `focus` / `doctor` ; -- [ ] focus d'une session hébergée par un terminal, vérifié de bout en bout ; -- [x] appui d'une touche Agent relié à `focus ` : les touches émettent - `v.oai.hid` avec `k` valant `AG00` à `AG05`, donc aucun raccourci global natif - n'est nécessaire — `npm run lighting -- watch --focus` ; -- [x] couche `DeviceAdapter` : le protocole d'éclairage est confirmé sur - matériel et `node scripts/lighting.mjs watch` pousse les couleurs d'état — - voir [`hid-lighting-protocol.md`](research/hid-lighting-protocol.md). - -Une seule limite la maintient hors de la V1 : aucune route n'adresse une -session Claude Code hébergée par Claude Desktop. Le protocole des LED par -touche, seconde limite historique, est désormais confirmé sur matériel et -implémenté — y compris sur le layer `Claude`, à condition que ses six positions -Agent portent les keycodes `KV_OAI_AG00` à `KV_OAI_AG05`, ce que pose -[`scripts/enable-agent-keys.mjs`](../scripts/enable-agent-keys.mjs). Mesures et -bornes dans +[English](roadmap.md) · [Français](fr/roadmap.md) + +# Roadmap + +## Current state + +The public repository now contains: + +- a community manifest and a Claude physical mapping; +- reusable schemas for future presets; +- an original SVG representation; +- an inventory, backup, dry-run, sanitisation and rollback tool; +- a local Input `0.17.3` profile generator that preserves AppSense; +- transactional tests on isolated copies; +- a reproducible analysis of the Input `0.17.2` sharing format; +- a separate BLE track, explicitly not working. + +The real official Claude `*-layer.json` export and full hardware validation are +still missing. The preset therefore keeps the `hardware-observed` status. + +## V1 — verified Claude preset + +Goal: obtain a first reproducible layer without modifying the native Codex +layer. + +- [ ] run `git status --short` in the local copy and preserve unrelated changes; +- [x] export the real Input profile and inventory profiles, layers, actions and + AppSense links; +- [x] provide a verified local backup and a transactional rollback; +- [x] identify the official layer and profile Import/Export flows of Input + `0.17.2`; +- [x] define the manifest, the physical mapping, the colour and the exclusions; +- [x] protect index `0`, require a single existing Claude layer and preserve its + AppSense; +- [x] generate a new Input `0.17.3` profile locally; +- [ ] verify the positions, the keys, the dial and the joystick; +- [ ] verify AppSense, focus loss and competing links; +- [ ] verify persistence after an Input restart; +- [ ] export and sanitise the real `*-layer.json`; +- [ ] test the import on an isolated configuration, and the second import; +- [ ] restore the original profile and verify the device; +- [ ] promote the level of proof and take the PR out of draft. + +V1 is finished only if another person can reproduce the result without a local +identifier and without implicitly replacing a layer. + +## V2 — general portability + +The minimal foundations are already present, but are not declared stable: + +- [x] common manifest and V1 schemas; +- [x] simulation, backup, sanitisation and rollback tests on fixtures; +- [x] selection of a single existing layer, refusing absence and duplication; +- [x] visual preview of the mapping; +- [ ] support for a verified official artefact; +- [ ] structural before/after comparison from real exports; +- [ ] detection of Input/firmware incompatibilities; +- [ ] hardware validation log signed by versions and checksums; +- [ ] automation of the official rollback if Input exposes a supported channel. + +Patching Input's storage directly will not become the normal path until its +format and its effect on the device are proven. + +## V3 — community catalogue + +- add a "Claude Code" (terminal) preset as the natural first candidate; +- add IDE, browser, search, Figma and Framer presets; +- index presets by application, platform and compatibility; +- use the GitHub proposal and pull request templates; +- require a backup and rollback method; +- publish known negative results and incompatibilities; +- allow several physical representations without local identifiers. + +## V4 — simplified experience + +- catalogue readable from a dedicated interface; +- interactive keyboard preview; +- before/after comparison; +- guided installation with explicit consent; +- versioned updates that do not overwrite local customisations. + +## Parallel track: Hardware Buddy + +The BLE research stays independent. It only joins the main roadmap if the Nordic +UART service, HID coexistence and a safe restore procedure are demonstrated on +the exact Codex Micro. + +## Parallel track: Claude Code session status + +A second track covers the six Agent keys: showing the state of local Claude Code +sessions and jumping to the right one. Unlike Hardware Buddy, both of its core +building blocks rest on documented mechanisms — the `claude agents --json` +roster and the hooks — and are implemented: + +- [x] hooks plugin and publishable journal + ([`thread-status/`](../thread-status/README.md)); +- [x] six-slot reducer, tested without hardware; +- [x] `watch` / `status` / `focus` / `doctor` companion; +- [ ] focusing a terminal-hosted session, verified end to end; +- [x] Agent key press wired to `focus `: the keys emit `v.oai.hid` with `k` + set to `AG00` through `AG05`, so no native global shortcut is needed — + `npm run lighting -- watch --focus`; +- [x] `DeviceAdapter` layer: the lighting protocol is confirmed on hardware and + `node scripts/lighting.mjs watch` pushes the state colours — see + [`hid-lighting-protocol.md`](research/hid-lighting-protocol.md); +- [x] navigating to a session hosted by Claude Desktop: + `claude://resume?session=` opens it by id. The route is not documented + and the handler reports nothing back, so a session whose transcript has left + the disk fails silently. + +Both historical blockers are now lifted. The per-key LED protocol is confirmed +on hardware and implemented — including on the `Claude` layer, provided its six +Agent positions carry the `KV_OAI_AG00` to `KV_OAI_AG05` keycodes, which +[`scripts/enable-agent-keys.mjs`](../scripts/enable-agent-keys.mjs) sets. What +keeps this track out of V1 is now its dependency on an undocumented route and on +quitting the ChatGPT app, not an unsolved problem. Measurements and bounds in [`docs/research/thread-status-feasibility.md`](research/thread-status-feasibility.md) -et [`docs/research/hid-lighting-protocol.md`](research/hid-lighting-protocol.md). +and [`docs/research/hid-lighting-protocol.md`](research/hid-lighting-protocol.md). diff --git a/docs/scope-and-limitations.md b/docs/scope-and-limitations.md index 77e422d..b146598 100644 --- a/docs/scope-and-limitations.md +++ b/docs/scope-and-limitations.md @@ -1,107 +1,108 @@ -# Périmètre et limitations - -## Dans le périmètre V1 - -- conventions sûres pour une bibliothèque de presets ; -- manifeste et mapping physique Claude ; -- protection du layer Codex natif à l'index `0` ; -- sélection d'un unique layer `Claude` existant après inventaire ; -- conservation locale de son lien AppSense Claude Desktop ; -- génération locale d'un nouveau `*-profile.json` Input `0.17.3` ; -- export officiel du profile comme sauvegarde principale ; -- copie locale vérifiée par SHA-256 ; -- dry-run, état de session, refus de doublon et rollback guidé ; -- sanitation fail-closed d'un vrai export de layer ; -- tests transactionnels sur copies isolées ; -- analyse séparée de Hardware Buddy. - -## Non effectué sur le matériel dans cette branche - -- test des identifiants physiques ; -- import d'un `*-layer.json` Codex Micro ; -- persistance après redémarrage ; -- test de perte de focus ; -- restauration du keymap matériel. - -## Toujours hors périmètre - -- `Reset settings` ; -- suppression d'un profile ou layer existant ; -- modification de raccourcis système ; -- flash ou redistribution du firmware ; -- approbation de permissions depuis le clavier ; -- action destructive, push ou déploiement ; -- publication d'un export brut ou d'un identifiant matériel ; -- présentation de Hardware Buddy comme fonctionnel sans preuve ; -- redistribution du SDK `@worklouder/device-kit-oai` ou de son code. - -## Amendement : écriture volatile de l'éclairage - -Jusqu'ici le dépôt s'interdisait toute écriture directe sur le périphérique. Cet -amendement ouvre **un cas précis et un seul** : l'envoi de rapports HID de sortie -portant l'état lumineux d'exécution. - -La distinction qui fonde l'amendement est la persistance, pas la nature du canal : - -| Écriture | Statut | +[English](scope-and-limitations.md) · [Français](fr/scope-and-limitations.md) + +# Scope and limitations + +## In scope for V1 + +- safe conventions for a preset library; +- Claude manifest and physical mapping; +- protection of the native Codex layer at index `0`; +- selection of a single existing `Claude` layer after inventory; +- local preservation of its Claude Desktop AppSense link; +- local generation of a new Input `0.17.3` `*-profile.json`; +- official profile export as the primary backup; +- local copy verified by SHA-256; +- dry run, session state, duplicate refusal and guided rollback; +- fail-closed sanitisation of a real layer export; +- transactional tests on isolated copies; +- separate analysis of Hardware Buddy. + +## Not done on hardware in this branch + +- testing the physical identifiers; +- importing a Codex Micro `*-layer.json`; +- persistence across a restart; +- focus-loss test; +- restoring the hardware keymap. + +## Always out of scope + +- `Reset settings`; +- deleting an existing profile or layer; +- modifying system shortcuts; +- flashing or redistributing firmware; +- approving permissions from the keyboard; +- destructive actions, pushes or deployments; +- publishing a raw export or a hardware identifier; +- presenting Hardware Buddy as working without proof; +- redistributing the `@worklouder/device-kit-oai` SDK or its code. + +## Amendment: volatile lighting writes + +Until now the repository ruled out any direct write to the device. This +amendment opens **one precise case, and one only**: sending HID output reports +carrying the runtime lighting state. + +The distinction the amendment rests on is persistence, not the nature of the +channel: + +| Write | Status | | --- | --- | -| rapport HID d'éclairage, volatile, perdu à la déconnexion | **dans le périmètre**, sous conditions | -| configuration du périphérique, keymap, layers, couleurs de layer | hors périmètre, inchangé | -| stockage applicatif d'Input | hors périmètre, inchangé | -| firmware | hors périmètre, inchangé | - -Conditions cumulatives, toutes requises : - -1. **Opt-in explicite.** Aucune écriture par défaut, jamais au premier lancement. -2. **Volatile uniquement.** Rien qui survive à une déconnexion du périphérique. -3. **Implémentation originale.** Le format observé est documenté ; le SDK - propriétaire n'est ni copié, ni redistribué, ni empaqueté. -4. **Restauration en sortie.** Interruption, arrêt ou exception laissent - l'éclairage dans un état neutre, jamais figé sur un état faux. -5. **Concurrence documentée.** L'app ChatGPT réémet toutes les 35 à 40 secondes, - la dernière écriture gagne, et aucune coexistence déterministe n'est promise. -6. **Réversibilité par abstention.** Ne pas lancer l'outil suffit à revenir à - l'état d'origine ; il n'y a rien à désinstaller côté matériel. - -Ce que l'amendement ne change pas : le remappage des touches continue de passer -exclusivement par le flux de profils Input, et la capture de frappes reste hors -périmètre. - -Base retenue pour la réimplémentation : interopérabilité avec un périphérique que -l'utilisateur possède, code original, aucune redistribution. Voir +| HID lighting report, volatile, lost on disconnect | **in scope**, under conditions | +| device configuration, keymap, layers, layer colours | out of scope, unchanged | +| Input's application storage | out of scope, unchanged | +| firmware | out of scope, unchanged | + +Cumulative conditions, all required: + +1. **Explicit opt-in.** No write by default, never on first launch. +2. **Volatile only.** Nothing that survives a device disconnect. +3. **Original implementation.** The observed format is documented; the + proprietary SDK is neither copied, nor redistributed, nor bundled. +4. **Restore on exit.** An interrupt, a shutdown or an exception leaves the + lighting in a neutral state, never frozen on a false one. +5. **Documented contention.** The ChatGPT app re-emits every 35 to 40 seconds, + the last write wins, and no deterministic coexistence is promised. +6. **Reversible by abstention.** Not running the tool is enough to return to the + original state; there is nothing to uninstall on the hardware side. + +What the amendment does not change: key remapping still goes exclusively through +the Input profile flow, and keystroke capture stays out of scope. + +Basis retained for the reimplementation: interoperability with a device the user +owns, original code, no redistribution. See [`research/thread-status-feasibility.md`](research/thread-status-feasibility.md) -pour les mesures qui établissent le format. +for the measurements that establish the format. -## Matrice de confiance +## Confidence matrix -| Affirmation | État | Preuve | +| Claim | State | Proof | | --- | --- | --- | -| Codex Micro visible comme HID BLE | confirmé localement | observation I/O du 27 juillet 2026 | -| Input `0.17.3` installé | confirmé localement | bundle et profile réel | -| Firmware `v0.4.1` installé | confirmé localement | écran Setup | -| Six layers et AppSense | confirmé par Work Louder | documentation constructeur | -| Import/export de layer et profile | observé dans Input `0.17.2` | analyse assainie du package officiel | -| Enveloppe `*-layer.json` | observée statiquement | AST de la fonction d'export | -| Artefact Claude importable | non disponible | export réel requis | -| Lien AppSense Claude préservé | confirmé dans le profile généré | tests du générateur et export réel | -| Retour au layer précédent | non testé | test matériel requis | -| Sauvegarde/rollback de l'outil | validé sur fixture | tests Node isolés | -| Restauration réelle du périphérique | non testée | import profile + contrôle matériel requis | -| Nordic UART / Hardware Buddy | inconnu | preuves GATT et firmware requises | - -## Compatibilité - -L'observation actuelle concerne macOS `26.5.2` arm64, Claude `1.24012.9`, -Input `0.17.3` et firmware `v0.4.1`. L'analyse du mécanisme de partage -`0.17.2` reste historique. Cette combinaison n'est pas une plage de -compatibilité garantie. - -Voir [`compatibility.md`](compatibility.md). - -## Limite du rollback local - -La configuration Input copiée peut contenir des métadonnées utiles à -l'application, mais le keymap est également écrit sur le périphérique. Par -conséquent, la restauration principale est le flux officiel **Import Profile**. -La restauration brute du dossier applicatif exige un consentement supplémentaire -et ne suffit pas à promouvoir le preset. +| Codex Micro visible as a BLE HID | confirmed locally | I/O observation of 27 July 2026 | +| Input `0.17.3` installed | confirmed locally | bundle and real profile | +| Firmware `v0.4.1` installed | confirmed locally | Setup screen | +| Six layers and AppSense | confirmed by Work Louder | vendor documentation | +| Layer and profile import/export | observed in Input `0.17.2` | sanitised analysis of the official package | +| `*-layer.json` envelope | observed statically | AST of the export function | +| Importable Claude artefact | not available | real export required | +| Claude AppSense link preserved | confirmed in the generated profile | generator tests and real export | +| Return to the previous layer | not tested | hardware test required | +| Tool backup/rollback | validated on a fixture | isolated Node tests | +| Real device restore | not tested | profile import + hardware check required | +| Nordic UART / Hardware Buddy | unknown | GATT and firmware evidence required | + +## Compatibility + +The current observation covers macOS `26.5.2` arm64, Claude `1.24012.9`, +Input `0.17.3` and firmware `v0.4.1`. The analysis of the `0.17.2` sharing +mechanism remains historical. This combination is not a guaranteed compatibility +range. + +See [`compatibility.md`](compatibility.md). + +## Limit of the local rollback + +The copied Input configuration can carry metadata useful to the application, but +the keymap is also written to the device. Consequently, the primary restore path +is the official **Import Profile** flow. Restoring the application folder raw +requires additional consent and is not enough to promote the preset. diff --git a/docs/vision.md b/docs/vision.md index 6b85037..77789d9 100644 --- a/docs/vision.md +++ b/docs/vision.md @@ -1,81 +1,79 @@ -# Vision du projet +[English](vision.md) · [Français](fr/vision.md) -## Problème +# Project vision -Work Louder Input permet de personnaliser le Codex Micro, mais une -configuration utile reste difficile à transmettre : elle dépend de la version -d'Input, du firmware, des layers déjà présents et de la manière dont -l'application identifie les logiciels avec AppSense. +## Problem -Une capture d'écran ou une liste de raccourcis ne suffit pas. Un preset -partageable doit aussi expliquer sa compatibilité, préserver l'existant, -prouver son niveau de validation et offrir un retour arrière. +Work Louder Input lets you customise the Codex Micro, but a useful configuration +stays hard to pass on: it depends on the Input version, the firmware, the layers +already present, and the way the application identifies software with AppSense. -## Objectif +A screenshot or a list of shortcuts is not enough. A shareable preset also has to +explain its compatibility, preserve what is already there, prove its level of +validation, and offer a way back. -Construire une bibliothèque communautaire de configurations Codex Micro -compréhensibles, testables et, lorsque le format le permet, installables. +## Goal -Le parcours cible est : +Build a community library of Codex Micro configurations that are +understandable, testable and — where the format allows it — installable. -1. choisir un preset pour une application ou un workflow ; -2. vérifier la compatibilité matérielle et logicielle ; -3. sauvegarder la configuration Input courante ; -4. simuler ou inspecter le changement ; -5. installer ou reproduire uniquement le layer demandé ; -6. vérifier chaque contrôle ; -7. restaurer la sauvegarde en cas de problème. +The target journey is: -## Premier cas de référence +1. pick a preset for an application or a workflow; +2. check hardware and software compatibility; +3. back up the current Input configuration; +4. simulate or inspect the change; +5. install or reproduce only the layer that was asked for; +6. verify every control; +7. restore the backup if anything goes wrong. -Claude Desktop sur macOS sert de premier cas complet : +## First reference case -- activation automatique du layer avec AppSense ; -- raccourcis courants et réversibles ; -- molette pour le défilement ; -- joystick pour la navigation ; -- exclusion par défaut de l'envoi, des permissions et des actions - destructrices ; le GUI personnel peut proposer l'envoi uniquement sur choix - explicite. +Claude Desktop on macOS serves as the first complete case: -Ce premier preset doit définir les conventions réutilisables par les futurs -layers IDE, navigateur, création graphique ou workflows spécialisés. +- automatic layer activation with AppSense; +- common, reversible shortcuts; +- wheel for scrolling; +- joystick for navigation; +- sending, permissions and destructive actions excluded by default; the personal + GUI may offer sending only on an explicit choice. -## Principes +This first preset has to set the conventions that future IDE, browser, graphics +or specialised-workflow layers can reuse. -### Préserver le clavier natif +## Principles -Le layer Codex fourni avec le matériel reste intact. Un preset communautaire -utilise un emplacement libre ou demande une décision explicite avant tout -remplacement. +### Preserve the native keyboard -### Publier le niveau de preuve +The Codex layer shipped with the hardware stays intact. A community preset uses +a free slot, or asks for an explicit decision before any replacement. -Chaque artefact porte un statut : +### Publish the level of proof -- `proposal-not-applied` : spécification uniquement ; -- `hardware-observed` : environnement inventorié ; -- `manually-validated` : mapping testé sur le matériel déclaré ; -- `export-format-verified` : installation et restauration reproduites. +Every artefact carries a status: -### Minimiser les écritures +- `proposal-not-applied`: specification only; +- `hardware-observed`: environment inventoried; +- `manually-validated`: mapping tested on the declared hardware; +- `export-format-verified`: installation and restore reproduced. -Un outil d'installation doit proposer une simulation, sauvegarder avant -écriture et appliquer un delta ciblé. Il ne doit jamais dépendre de Reset -Settings. +### Minimise writes -### Protéger la confidentialité +An installation tool must offer a dry run, back up before writing, and apply a +targeted delta. It must never depend on Reset Settings. -Les chemins utilisateur, ports, numéros de série, adresses Bluetooth, captures -privées et exports complets restent hors du dépôt. +### Protect privacy -### Écarter les actions conséquentes +User paths, ports, serial numbers, Bluetooth addresses, private screenshots and +full exports stay out of the repository. -Les presets publics n'incluent pas par défaut l'envoi d'un message, -l'approbation d'une permission, une suppression, un push ou un déploiement. +### Keep consequential actions out -## Hors objectif +Public presets do not include, by default, sending a message, approving a +permission, a deletion, a push or a deployment. -Le projet ne redistribue pas de firmware Work Louder, ne promet pas une -compatibilité non testée et ne présente pas la piste BLE Hardware Buddy comme -fonctionnelle sans preuve reproductible. +## Out of scope + +The project does not redistribute Work Louder firmware, does not promise +untested compatibility, and does not present the BLE Hardware Buddy track as +working without reproducible proof. From cfb486802298649f546df7c9909f86b0130b74ce Mon Sep 17 00:00:00 2001 From: Thanh Chau <1320427+thannous@users.noreply.github.com> Date: Fri, 31 Jul 2026 16:41:07 +0200 Subject: [PATCH 2/2] docs: address bilingual review feedback --- CONTRIBUTING.fr.md | 2 +- README.md | 28 +++++++++---------- docs/compatibility.md | 5 ++-- docs/fr/compatibility.md | 6 ++-- docs/fr/installation.md | 18 ++++++++++-- docs/fr/publishing-checklist.md | 8 ++++-- docs/fr/research/effort-wheel-calibration.md | 2 +- docs/fr/research/thread-status-feasibility.md | 17 ++++++----- docs/fr/scope-and-limitations.md | 9 ++++-- docs/installation.md | 17 +++++++++-- docs/publishing-checklist.md | 7 +++-- docs/research/thread-status-feasibility.md | 19 +++++++------ docs/scope-and-limitations.md | 9 ++++-- profiles/claude-shortcuts/README.md | 2 +- thread-status/README.md | 2 +- 15 files changed, 97 insertions(+), 54 deletions(-) diff --git a/CONTRIBUTING.fr.md b/CONTRIBUTING.fr.md index 0594197..4a57245 100644 --- a/CONTRIBUTING.fr.md +++ b/CONTRIBUTING.fr.md @@ -139,4 +139,4 @@ flash propriétaire ne doit être ajouté. - [ ] Statut d'import confirmé par une preuve ou indiqué comme non vérifié. - [ ] Aucun asset ou firmware propriétaire. -Lire [SECURITY.md](SECURITY.md) avant de publier un rapport sensible. +Lire [SECURITY.fr.md](SECURITY.fr.md) avant de publier un rapport sensible. diff --git a/README.md b/README.md index b5dbd39..dcd1757 100644 --- a/README.md +++ b/README.md @@ -158,7 +158,7 @@ Une personne doit pouvoir : À terme, le catalogue pourra accueillir des layers IDE, navigateur, recherche, Figma, Framer, applications Adobe et workflows communautaires. Lire la -[vision](docs/vision.md) et la [feuille de route](docs/roadmap.md). +[vision](docs/fr/vision.md) et la [feuille de route](docs/fr/roadmap.md). ### Résultat V1 actuel @@ -233,7 +233,7 @@ actif. - bundle Claude `com.anthropic.claudefordesktop` ; - bundle Input `it.focusense.input-app`. -Voir la [matrice de compatibilité](docs/compatibility.md) pour distinguer les +Voir la [matrice de compatibilité](docs/fr/compatibility.md) pour distinguer les faits, tests de fixture et validations matérielles manquantes. ### Configurateur graphique @@ -319,7 +319,7 @@ npm run build:profile -- \ Importer ensuite `Claude-macOS-profile.json` avec **Add New** dans Input. Le fichier source reste inchangé et aucune donnée n'est téléversée. -Lire le [guide d'installation et de retour arrière](docs/installation.md) avant +Lire le [guide d'installation et de retour arrière](docs/fr/installation.md) avant `--apply`. ### Format de partage @@ -330,7 +330,7 @@ Input `0.17.2` expose un flux officiel au niveau layer et profile : - `*-profile.json` : même enveloppe avec `profile`. La preuve et ses limites sont documentées dans -[`docs/research/input-0.17.2-sharing.md`](docs/research/input-0.17.2-sharing.md). +[`docs/fr/research/input-0.17.2-sharing.md`](docs/fr/research/input-0.17.2-sharing.md). Le manifeste communautaire n'imite pas ce format. Le parcours principal transforme localement un vrai `*-profile.json`. Un éventuel artefact layer @@ -412,20 +412,20 @@ node scripts/lighting.mjs off ``` Protocole, mesures et bornes : -[`docs/research/hid-lighting-protocol.md`](docs/research/hid-lighting-protocol.md). +[`docs/fr/research/hid-lighting-protocol.md`](docs/fr/research/hid-lighting-protocol.md). ### Liens de documentation -- [Vision](docs/vision.md) -- [Feuille de route](docs/roadmap.md) -- [Installation et rollback](docs/installation.md) -- [Compatibilité](docs/compatibility.md) -- [Mécanisme Input 0.17.2](docs/research/input-0.17.2-sharing.md) -- [Protocole d'éclairage HID du Codex Micro](docs/research/hid-lighting-protocol.md) +- [Vision](docs/fr/vision.md) +- [Feuille de route](docs/fr/roadmap.md) +- [Installation et rollback](docs/fr/installation.md) +- [Compatibilité](docs/fr/compatibility.md) +- [Mécanisme Input 0.17.2](docs/fr/research/input-0.17.2-sharing.md) +- [Protocole d'éclairage HID du Codex Micro](docs/fr/research/hid-lighting-protocol.md) - [Conventions des presets](profiles/README.md) - [Preset Claude](profiles/claude-shortcuts/README.md) -- [Contribution](CONTRIBUTING.md) -- [Sécurité](SECURITY.md) +- [Contribution](CONTRIBUTING.fr.md) +- [Sécurité](SECURITY.fr.md) ### Sources principales @@ -437,4 +437,4 @@ Protocole, mesures et bornes : ### Licence MIT, copyright 2026 Thanh Chau. Voir [LICENSE](LICENSE) et -[LICENSING.md](LICENSING.md). +[LICENSING.fr.md](LICENSING.fr.md). diff --git a/docs/compatibility.md b/docs/compatibility.md index 1fff13c..7d5fe44 100644 --- a/docs/compatibility.md +++ b/docs/compatibility.md @@ -13,7 +13,7 @@ mechanism, and the validations still required on hardware. | Firmware | `v0.4.1` | Setup screen observed locally | Input/firmware combination known; hardware validation partial | | macOS | `26.5.2` arm64 | local observation | application and HID observed | | Claude Desktop | `1.24012.9`, bundle `com.anthropic.claudefordesktop` | local bundle and generated profile | `⌘N`, `⌘D`, `⌘⇧D` configured; `Esc` still to be validated in context | -| AppSense | existing link of the Claude layer | real export observed | `linkedAppId` preserved locally and forbidden in public artefacts; focus loss still to be tested | +| AppSense | existing link of the Claude layer | real export and hardware behaviour observed | `linkedAppId` preserved locally and forbidden in public artefacts; focus loss measured: an unlinked application does not return to the native layer | | CLI backup | Node.js `>=18` | automated tests | copy, SHA-256 manifest and restore tested on an isolated copy | | Layer artefact | `*-layer.json` | absent | must come from a real, sanitised export and go through a round-trip | @@ -28,4 +28,5 @@ mechanism, and the validations still required on hardware. The Claude preset is `hardware-observed`. It will only move to `manually-validated` after the full checklist covering the keys, the wheel, the -joystick, focus loss, restart and rollback. +joystick, restart and rollback. The unsafe focus-loss behaviour is already +measured; its absence from that list is not a claim that AppSense is safe. diff --git a/docs/fr/compatibility.md b/docs/fr/compatibility.md index d352d22..d6c2515 100644 --- a/docs/fr/compatibility.md +++ b/docs/fr/compatibility.md @@ -13,7 +13,7 @@ partage et les validations encore requises sur le matériel. | Firmware | `v0.4.1` | écran Setup observé localement | combinaison Input/firmware connue ; validation matérielle partielle | | macOS | `26.5.2` arm64 | observation locale | application et HID observés | | Claude Desktop | `1.24012.9`, bundle `com.anthropic.claudefordesktop` | bundle local et profil généré | `⌘N`, `⌘D`, `⌘⇧D` configurés ; `Esc` à valider contextuellement | -| AppSense | lien existant du layer Claude | export réel observé | `linkedAppId` conservé localement et interdit dans les artefacts publics ; perte de focus à tester | +| AppSense | lien existant du layer Claude | export réel et comportement matériel observés | `linkedAppId` conservé localement et interdit dans les artefacts publics ; perte de focus mesurée : une application non liée ne ramène pas au layer natif | | Sauvegarde CLI | Node.js `>=18` | tests automatisés | copie, manifeste SHA-256 et restauration testés sur une copie isolée | | Artefact layer | `*-layer.json` | absent | doit provenir d'un export réel assaini et subir un round-trip | @@ -27,4 +27,6 @@ partage et les validations encore requises sur le matériel. Le preset Claude est `hardware-observed`. Il ne passera à `manually-validated` qu'après la checklist complète des touches, de la molette, -du joystick, de la perte de focus, du redémarrage et du rollback. +du joystick, du redémarrage et du rollback. Le comportement dangereux après +perte de focus est déjà mesuré ; son absence de cette liste ne signifie pas +qu'AppSense est sûr. diff --git a/docs/fr/installation.md b/docs/fr/installation.md index fe27475..af894cb 100644 --- a/docs/fr/installation.md +++ b/docs/fr/installation.md @@ -239,9 +239,18 @@ Deux options écrivent une référence AppSense au lieu de seulement reprendre c de la sauvegarde : ```bash -node scripts/build-input-profile.mjs sauvegarde.json sortie.json --app-sense-id=0 --base-layer-app-sense-id=2 +CLAUDE_APP_SENSE_ID="" +RETURN_APP_SENSE_ID="" +node scripts/build-input-profile.mjs sauvegarde.json sortie.json \ + --app-sense-id="$CLAUDE_APP_SENSE_ID" \ + --base-layer-app-sense-id="$RETURN_APP_SENSE_ID" ``` +Relever ces deux valeurs dans la configuration actuelle du périphérique et les +vérifier avant d'exécuter la commande. Les marqueurs sont volontairement non +numériques : une commande copiée échoue ainsi au lieu de lier silencieusement +les mauvaises applications. + `--app-sense-id=` force la référence du layer `Claude` et dispense d'en exiger une dans la sauvegarde : c'est le cas d'usage « réparer un lien perdu ». @@ -257,8 +266,11 @@ déjà exister sur la carte, créée une fois dans l'UI d'Input avec `Auto detec Une référence vers une entrée absente s'importe **sans erreur** et laisse AppSense mort sans le signaler. Relever les identifiants réels avant, dans `~/Library/Logs/input/main.log`, où `sending device config :` est suivi du JSON -complet — et n'exécuter `Auto detect` qu'une seule fois par application, il ne -dédoublonne pas. +complet. Garder ce journal en local et ne jamais le coller dans une issue : il +peut contenir des adresses du périphérique, des identifiants, des jetons et +d'autres paramètres privés. Ne copier que les nombres `linkedAppId` requis et +masquer toute valeur sensible avant de partager un extrait. N'exécuter +`Auto detect` qu'une seule fois par application : il ne dédoublonne pas. Le GUI expose les deux mêmes réglages dans l'étape **Vérifier et générer**, section « Liens AppSense ». Laisser les champs vides revient à ne pas passer les options : diff --git a/docs/fr/publishing-checklist.md b/docs/fr/publishing-checklist.md index 6722344..2bdabec 100644 --- a/docs/fr/publishing-checklist.md +++ b/docs/fr/publishing-checklist.md @@ -37,8 +37,9 @@ présentée comme un layer matériel validé. ## Portes du preset Claude V1 -- [x] Flux officiels Import/Export layer et profile identifiés dans Input - `0.17.2`. +- [x] Flux historiques Import/Export layer et profile identifiés par inspection + statique d'Input `0.17.2` ; ce n'est pas une preuve de round-trip actuelle en + `0.17.3`. - [x] Manifeste, mapping, schémas et représentation visuelle publiables. - [x] Sauvegarde, vérification SHA-256, dry-run, sélection unique et rollback testés sur copies isolées. @@ -47,7 +48,8 @@ présentée comme un layer matériel validé. - [x] Unique layer Claude confirmé ; index `0` comparé avant/après. - [x] Profile Input `0.17.3` généré localement avec AppSense conservé. - [ ] Positions physiques et identifiants Input vérifiés. -- [ ] AppSense, touches, cadran, joystick et perte de focus testés. +- [ ] Activation AppSense, touches, cadran et joystick testés de bout en bout ; + le comportement dangereux après perte de focus est déjà mesuré. - [ ] Persistance après redémarrage d'Input vérifiée. - [ ] Vrai `*-layer.json` exporté, assaini et ajouté avec sa somme SHA-256. - [ ] SHA-256 déclaré identique et contenu conforme au mapping canonique. diff --git a/docs/fr/research/effort-wheel-calibration.md b/docs/fr/research/effort-wheel-calibration.md index 8200061..9e211a6 100644 --- a/docs/fr/research/effort-wheel-calibration.md +++ b/docs/fr/research/effort-wheel-calibration.md @@ -14,7 +14,7 @@ macro pour que chaque mesure devienne interprétable. ## Pourquoi `Esc` est obligatoire -`⌘⇧E` est une **bascule**, vérifié sur Claude Desktop : l'envoyer deux fois de +`⌘⇧E` est une **bascule**, vérifiée sur Claude Desktop : l'envoyer deux fois de suite ouvre puis referme le sélecteur. Chaque cran doit donc refermer le sélecteur lui-même. Sans le `Esc` final, le diff --git a/docs/fr/research/thread-status-feasibility.md b/docs/fr/research/thread-status-feasibility.md index a8a7fc0..8f7e6d5 100644 --- a/docs/fr/research/thread-status-feasibility.md +++ b/docs/fr/research/thread-status-feasibility.md @@ -5,21 +5,24 @@ ## Verdict **Les états sont disponibles officiellement, les LED fonctionnent sur le layer -`Claude`, et la navigation est résolue sur toutes les surfaces.** Trois +`Claude`, et la navigation est vérifiée dans Claude Desktop ; le focus du +terminal est implémenté, mais pas encore testé de bout en bout.** Trois conclusions, dans cet ordre de solidité : 1. Détecter « en cours / intervention / terminé / fermé » par session est un problème résolu, avec deux mécanismes documentés et complémentaires. -2. Aller à la session depuis une touche est résolu sur les deux surfaces : focus - de fenêtre en AppleScript quand la session tourne dans un terminal, et - `claude://resume?session=` quand Claude Desktop l'héberge. Cette seconde - route n'est pas documentée. +2. Aller à la session depuis une touche est vérifié par + `claude://resume?session=` quand Claude Desktop l'héberge. Cette route + n'est pas documentée. Le focus de fenêtre du terminal par AppleScript est + implémenté et compile, mais attend encore un test de bout en bout avec une + session terminal active. 3. Piloter les six LED **fonctionne, y compris sur le layer `Claude`**, à une condition découverte tardivement : les six positions Agent de ce layer doivent porter les keycodes `KV_OAI_AG00` à `KV_OAI_AG05`. Le prédicat du firmware est le keycode, pas l'index du layer. -Les trois briques sont réunies, et aucun raccourci Claude n'est sacrifié. Mais la +Les trois briques sont implémentées, le focus du terminal attend encore une +validation de bout en bout, et aucun raccourci Claude n'est sacrifié. Mais la fonction a une condition d'usage : **l'app ChatGPT doit être quittée.** Elle réécrit les six LED toutes les 35 à 40 secondes et intercepte les appuis sur les touches Agent pour changer de thread Codex. Les deux moitiés de la fonction lui @@ -82,7 +85,7 @@ Mesuré sur macOS `26.5.2` arm64, Claude `1.24012.9`, Claude Code `2.1.219`. | `CLAUDE_CODE_HOST_SESSION_ID` **n'identifie pas** une session | confirmé | deux sessions distinctes partagent `local_f92b6e6a` | | Le `tty` distingue terminal et Desktop | confirmé | `tty = ??` pour les trois sessions Desktop | | Focus d'une fenêtre de terminal par `tty` en AppleScript | non testé de bout en bout | scripts compilés par `osacompile` ; aucune session en terminal disponible, iTerm2 absent de la machine | -| Route pour ouvrir une session Claude Code locale dans Desktop **par identifiant** | **réfuté** | `claude://resume?session=` ouvre la bonne session, vérifié sur machine ; la route est absente de la doc des deep links, qui ne cite que `claude://code/new` | +| Route pour ouvrir une session Claude Code locale dans Desktop **par identifiant** | **confirmé sur cette machine** | `claude://resume?session=` ouvre la bonne session, vérifié sur machine ; la route est absente de la doc des deep links, qui ne cite que `claude://code/new` | | `claude://resume` valide sa cible par une regex UUID stricte | confirmé | lu dans le handler : l'`uuid` est vérifié avant `importCliSession`, puis la navigation | | `claude://resume` échoue quand le transcript est absent du disque | rapporté, non testé | chemin d'erreur `transcript_missing` du handler ; aucun compte rendu ne remonte à l'appelant, `open` sort en 0 dans tous les cas | | Raccourcis de **cycle** entre sessions dans Claude Desktop | documentés | `Ctrl Tab` / `Ctrl Shift Tab` et `Cmd Shift ]` / `Cmd Shift [` — table des raccourcis du Code tab | diff --git a/docs/fr/scope-and-limitations.md b/docs/fr/scope-and-limitations.md index 0fe3433..baaea09 100644 --- a/docs/fr/scope-and-limitations.md +++ b/docs/fr/scope-and-limitations.md @@ -58,8 +58,13 @@ Conditions cumulatives, toutes requises : 2. **Volatile uniquement.** Rien qui survive à une déconnexion du périphérique. 3. **Implémentation originale.** Le format observé est documenté ; le SDK propriétaire n'est ni copié, ni redistribué, ni empaqueté. -4. **Restauration en sortie.** Interruption, arrêt ou exception laissent - l'éclairage dans un état neutre, jamais figé sur un état faux. +4. **Limite de sortie documentée et neutralisation explicite.** + `lighting-probe.mjs --map` éteint les six emplacements après un balayage + normal et sur `SIGINT` ; `lighting.mjs off` le fait à la demande. Les chemins + longs `set --hold` et `watch` ferment leur session HID sur `SIGINT`, mais ne + neutralisent pas le dernier état volatil, et les arrêts ou exceptions ne sont + pas couverts. Déconnecter le périphérique ou exécuter + `npm run lighting -- off` lorsqu'un état neutre est requis. 5. **Concurrence documentée.** L'app ChatGPT réémet toutes les 35 à 40 secondes, la dernière écriture gagne, et aucune coexistence déterministe n'est promise. 6. **Réversibilité par abstention.** Ne pas lancer l'outil suffit à revenir à diff --git a/docs/installation.md b/docs/installation.md index 55ff59f..bf4e1f4 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -238,9 +238,17 @@ Two options write an AppSense reference instead of only carrying over the one from the backup: ```bash -node scripts/build-input-profile.mjs backup.json output.json --app-sense-id=0 --base-layer-app-sense-id=2 +CLAUDE_APP_SENSE_ID="" +RETURN_APP_SENSE_ID="" +node scripts/build-input-profile.mjs backup.json output.json \ + --app-sense-id="$CLAUDE_APP_SENSE_ID" \ + --base-layer-app-sense-id="$RETURN_APP_SENSE_ID" ``` +Resolve both values from the current device configuration and verify them before +running the command. The placeholders are intentionally non-numeric so a copied +command fails instead of silently linking the wrong applications. + `--app-sense-id=` forces the `Claude` layer's reference and removes the need to require one in the backup: this is the "repair a lost link" use case. @@ -255,8 +263,11 @@ carry the `linkedApps` table: the target entry must already exist on the board, created once in Input's UI with `Auto detect`. A reference to a missing entry imports **without error** and leaves AppSense dead without saying so. Read the real identifiers first, in `~/Library/Logs/input/main.log`, where -`sending device config :` is followed by the full JSON — and run `Auto detect` -only once per application, since it does not deduplicate. +`sending device config :` is followed by the full JSON. Keep that log local and +never paste it into an issue: it can contain device addresses, identifiers, +tokens and other private parameters. Copy only the required `linkedAppId` +numbers, and redact every sensitive value before sharing an excerpt. Run +`Auto detect` only once per application, since it does not deduplicate. The GUI exposes the same two settings in the **Verify and generate** step, under "AppSense links". Leaving the fields empty is the same as not passing the diff --git a/docs/publishing-checklist.md b/docs/publishing-checklist.md index a412415..c34766f 100644 --- a/docs/publishing-checklist.md +++ b/docs/publishing-checklist.md @@ -37,8 +37,8 @@ being presented as a validated hardware layer. ## Gates for the Claude V1 preset -- [x] Official layer and profile Import/Export flows identified in Input - `0.17.2`. +- [x] Historical layer and profile Import/Export flows identified by static + inspection of Input `0.17.2`; this is not current `0.17.3` round-trip proof. - [x] Manifest, mapping, schemas and visual representation publishable. - [x] Backup, SHA-256 verification, dry run, single selection and rollback tested on isolated copies. @@ -48,7 +48,8 @@ being presented as a validated hardware layer. - [x] Single Claude layer confirmed; index `0` compared before and after. - [x] Input `0.17.3` profile generated locally with AppSense preserved. - [ ] Physical positions and Input identifiers verified. -- [ ] AppSense, keys, dial, joystick and focus loss tested. +- [ ] AppSense activation, keys, dial and joystick tested end to end; unsafe + focus-loss behaviour is already measured. - [ ] Persistence after an Input restart verified. - [ ] Real `*-layer.json` exported, sanitised and added with its SHA-256 sum. - [ ] Declared SHA-256 identical and content matching the canonical mapping. diff --git a/docs/research/thread-status-feasibility.md b/docs/research/thread-status-feasibility.md index 6d9d426..00140fd 100644 --- a/docs/research/thread-status-feasibility.md +++ b/docs/research/thread-status-feasibility.md @@ -5,22 +5,23 @@ ## Verdict **The states are available officially, the LEDs work on the `Claude` layer, and -navigation is solved on every surface.** Three conclusions, in decreasing order -of solidity: +navigation is verified in Claude Desktop; terminal focus is implemented but not +yet tested end to end.** Three conclusions, in decreasing order of solidity: 1. Detecting "running / needs you / done / closed" per session is a solved problem, with two documented and complementary mechanisms. -2. Going to a session from a key is solved on both surfaces: window focus over - AppleScript when the session runs in a terminal, and - `claude://resume?session=` when Claude Desktop hosts it. The second - route is not documented. +2. Going to a session from a key is verified through + `claude://resume?session=` when Claude Desktop hosts it. The route is + not documented. Terminal window focus over AppleScript is implemented and + compiles, but still lacks an end-to-end test with a live terminal session. 3. Driving the six LEDs **works, including on the `Claude` layer**, on one condition discovered late: the six Agent positions of that layer must carry the `KV_OAI_AG00` to `KV_OAI_AG05` keycodes. The firmware's predicate is the keycode, not the layer index. -All three building blocks are in place, and no Claude shortcut is sacrificed. But -the feature has a condition of use: **the ChatGPT app must be quit.** It rewrites +All three building blocks are implemented, with terminal focus still awaiting +end-to-end validation, and no Claude shortcut is sacrificed. But the feature +has a condition of use: **the ChatGPT app must be quit.** It rewrites the six LEDs every 35 to 40 seconds and intercepts Agent key presses to switch Codex thread. Both halves of the feature are therefore contended by the same application, and nothing can arbitrate. @@ -81,7 +82,7 @@ Measured on macOS `26.5.2` arm64, Claude `1.24012.9`, Claude Code `2.1.219`. | `CLAUDE_CODE_HOST_SESSION_ID` **does not identify** a session | confirmed | two distinct sessions share `local_f92b6e6a` | | The `tty` separates terminal from Desktop | confirmed | `tty = ??` for the three Desktop sessions | | Focusing a terminal window by `tty` in AppleScript | not tested end to end | scripts compiled by `osacompile`; no terminal session available, iTerm2 absent from the machine | -| A route to open a local Claude Code session in Desktop **by id** | **refuted** | `claude://resume?session=` opens the right session, verified on this machine; the route is absent from the deep-link documentation, which only mentions `claude://code/new` | +| A route to open a local Claude Code session in Desktop **by id** | **confirmed on this machine** | `claude://resume?session=` opens the right session, verified on this machine; the route is absent from the deep-link documentation, which only mentions `claude://code/new` | | `claude://resume` validates its target against a strict UUID regex | confirmed | read in the handler: the `uuid` is checked before `importCliSession`, then navigation | | `claude://resume` fails when the transcript is absent from disk | reported, not tested | the handler's `transcript_missing` error path; nothing is reported back to the caller, `open` exits 0 either way | | **Cycling** shortcuts between sessions in Claude Desktop | documented | `Ctrl Tab` / `Ctrl Shift Tab` and `Cmd Shift ]` / `Cmd Shift [` — Code tab shortcut table | diff --git a/docs/scope-and-limitations.md b/docs/scope-and-limitations.md index b146598..397e6ab 100644 --- a/docs/scope-and-limitations.md +++ b/docs/scope-and-limitations.md @@ -59,8 +59,13 @@ Cumulative conditions, all required: 2. **Volatile only.** Nothing that survives a device disconnect. 3. **Original implementation.** The observed format is documented; the proprietary SDK is neither copied, nor redistributed, nor bundled. -4. **Restore on exit.** An interrupt, a shutdown or an exception leaves the - lighting in a neutral state, never frozen on a false one. +4. **Documented exit boundary and explicit neutralisation.** + `lighting-probe.mjs --map` turns the six slots off after a normal sweep and + on `SIGINT`; `lighting.mjs off` does so on demand. The long-running + `set --hold` and `watch` paths close their HID session on `SIGINT`, but do + not neutralise the last volatile state, and shutdown or exception paths are + not covered. Disconnect the device or run `npm run lighting -- off` when a + neutral state is required. 5. **Documented contention.** The ChatGPT app re-emits every 35 to 40 seconds, the last write wins, and no deterministic coexistence is promised. 6. **Reversible by abstention.** Not running the tool is enough to return to the diff --git a/profiles/claude-shortcuts/README.md b/profiles/claude-shortcuts/README.md index ebe1f2d..abf59a0 100644 --- a/profiles/claude-shortcuts/README.md +++ b/profiles/claude-shortcuts/README.md @@ -110,4 +110,4 @@ node scripts/validate-presets.mjs node --test ``` -Lire ensuite [`docs/installation.md`](../../docs/installation.md). +Lire ensuite [`docs/fr/installation.md`](../../docs/fr/installation.md). diff --git a/thread-status/README.md b/thread-status/README.md index 2e966e2..1c170c2 100644 --- a/thread-status/README.md +++ b/thread-status/README.md @@ -65,4 +65,4 @@ Deux options, au choix : faire échouer une session. Détail des mesures et de ce qui reste ouvert : -[`docs/research/thread-status-feasibility.md`](../docs/research/thread-status-feasibility.md). +[`docs/fr/research/thread-status-feasibility.md`](../docs/fr/research/thread-status-feasibility.md).