diff --git a/CONTRIBUTING.fr.md b/CONTRIBUTING.fr.md new file mode 100644 index 0000000..4a57245 --- /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.fr.md](SECURITY.fr.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/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/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..7d5fe44 100644 --- a/docs/compatibility.md +++ b/docs/compatibility.md @@ -1,28 +1,32 @@ -# 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 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 | -## 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, 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/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..d6c2515 --- /dev/null +++ b/docs/fr/compatibility.md @@ -0,0 +1,32 @@ +[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 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 | + +## 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, 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/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..af894cb --- /dev/null +++ b/docs/fr/installation.md @@ -0,0 +1,383 @@ +[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 +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 ». + +`--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. 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 : +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..2bdabec --- /dev/null +++ b/docs/fr/publishing-checklist.md @@ -0,0 +1,61 @@ +[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 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. +- [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. +- [ ] 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. +- [ ] 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..9e211a6 --- /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é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 +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..8f7e6d5 --- /dev/null +++ b/docs/fr/research/thread-status-feasibility.md @@ -0,0 +1,565 @@ +[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 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 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 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 +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** | **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 | +| 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..baaea09 --- /dev/null +++ b/docs/fr/scope-and-limitations.md @@ -0,0 +1,114 @@ +[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. **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 à + 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..bf4e1f4 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,138 @@ 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 +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" ``` -`--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é : +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. + +`--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. 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 +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 +313,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 +372,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..c34766f 100644 --- a/docs/publishing-checklist.md +++ b/docs/publishing-checklist.md @@ -1,57 +1,61 @@ -# 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é. +[English](publishing-checklist.md) · [Français](fr/publishing-checklist.md) + +# Publishing checklist + +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. + +## Public repository — done + +- [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. + +## Checks that apply to every contribution + +- [ ] `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. + +## Privacy + +- [ ] 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. + +## Gates for the Claude V1 preset + +- [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. +- [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 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. +- [ ] 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..00140fd 100644 --- a/docs/research/thread-status-feasibility.md +++ b/docs/research/thread-status-feasibility.md @@ -1,40 +1,44 @@ -# É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 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 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 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. + +## 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 +48,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** | **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 | +| 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 +187,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 +239,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 +281,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 +294,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 +414,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..397e6ab 100644 --- a/docs/scope-and-limitations.md +++ b/docs/scope-and-limitations.md @@ -1,107 +1,113 @@ -# 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. **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 + 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. 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).