diff --git a/.planning/phases/02-daemon-et-i3-bridge/02-RESEARCH.md b/.planning/phases/02-daemon-et-i3-bridge/02-RESEARCH.md
new file mode 100644
index 0000000..9a10767
--- /dev/null
+++ b/.planning/phases/02-daemon-et-i3-bridge/02-RESEARCH.md
@@ -0,0 +1,637 @@
+# Phase 2: Daemon et i3 Bridge - Research
+
+**Researched:** 2026-03-23
+**Domain:** Go daemon (Unix socket IPC) + i3 IPC + X11 PID resolution
+**Confidence:** HIGH
+
+## Summary
+
+La Phase 2 transforme vmux d'un CLI one-shot en un daemon persistant (vmuxd) qui maintient un registre des sessions a jour. Le daemon communique avec le CLI via un Unix socket JSON. L'integration i3 permet de mapper chaque session Claude Code a son workspace et de switcher en une action.
+
+**Decouverte critique :** i3 GetTree() ne contient PAS de champ PID dans ses nodes. Le contexte (D-04) suppose que "i3 GetTree() donne le PID de chaque fenetre terminal", ce qui est faux. Le champ `Window` (X11 window ID) est present, et il faut lire `_NET_WM_PID` via X11 pour obtenir le PID du terminal. La bonne nouvelle : `go.i3wm.org/i3/v4` depend deja de `BurntSushi/xgbutil` qui expose `ewmh.WmPidGet()`. Aucune dependance supplementaire.
+
+L'algorithme de mapping est : i3 GetTree() -> Node.Window -> `ewmh.WmPidGet()` -> terminal PID -> walk up /proc PPID chain depuis chaque Claude PID jusqu'a trouver un terminal PID connu.
+
+**Recommandation principale :** Utiliser `go.i3wm.org/i3/v4` + `BurntSushi/xgbutil/ewmh` (deja en transitive dep) pour le mapping PID-workspace. Pas de shell-out vers xdotool/xprop.
+
+
+## User Constraints (from CONTEXT.md)
+
+### Locked Decisions
+- **D-01:** vmuxd communique avec le CLI via un Unix socket (`~/.vmux/vmux.sock`). Protocole JSON sur le socket.
+- **D-02:** vmuxd poll /proc + JSONL toutes les 5 secondes pour mettre a jour le registre des sessions. La Phase 3 (hooks) reduira la latence a < 1s.
+- **D-03:** Autostart : `vmux list` (ou tout autre commande) demarre vmuxd automatiquement s'il ne tourne pas. `vmux stop` pour arreter proprement.
+- **D-04:** Mapping session -> workspace via le PID. i3 GetTree() donne le PID de chaque fenetre terminal ; on croise avec le PID du processus Claude Code pour trouver le workspace. **NOTE RECHERCHE : i3 GetTree() n'a PAS de champ PID. Utiliser Node.Window + _NET_WM_PID via X11. Meme resultat final.**
+- **D-05:** Si plusieurs sessions Claude Code sont dans le meme workspace i3, elles sont toutes listees avec le meme workspace.
+- **D-06:** Utiliser `go.i3wm.org/i3/v4` (lib officielle Go pour i3 IPC). Premiere dependance externe du projet. API : GetTree, GetWorkspaces, RunCommand.
+- **D-07:** `vmux switch` utilise du fuzzy match : `vmux switch auth` matche la premiere session contenant "auth" dans branche git, label ou cwd.
+- **D-08:** `vmux label "texte"` attribue un label humain. Le label est stocke en memoire dans vmuxd (persiste sur disque dans `~/.vmux/labels.json` pour survivre aux redemarrages).
+- **D-09:** Le daemon track les transitions d'etat. Quand une session passe a NeedsInput, il enregistre le timestamp. `vmux list` affiche "depuis X min".
+
+### Claude's Discretion
+- Format de communication JSON sur le Unix socket (structure des requetes/reponses)
+- Gestion du PID file pour le daemon
+- Gestion des erreurs de connexion i3 IPC (fallback gracieux si i3 non disponible)
+
+### Deferred Ideas (OUT OF SCOPE)
+None
+
+
+
+## Phase Requirements
+
+| ID | Description | Research Support |
+|----|-------------|------------------|
+| DISC-04 | vmux permet d'associer un label humain a une session | Labels stockes en memoire + persistes dans `~/.vmux/labels.json`. Protocole socket : requete `label` avec session_id + texte. |
+| I3-01 | vmux mappe chaque session a son workspace i3 | Algorithme : i3 GetTree -> Node.Window -> ewmh.WmPidGet -> terminal PID -> PPID chain matching. Verifie sur machine : 6 Claude PIDs correctement mappes a leurs workspaces. |
+| I3-02 | vmux permet de switcher vers le workspace i3 d'une session en une action | `i3.RunCommand("workspace number N")` via go.i3wm.org/i3/v4. Fuzzy match sur label/branche/cwd pour trouver la session. |
+| STATE-04 | vmux affiche le temps ecoule depuis que la session attend | Daemon track les transitions d'etat avec timestamp. Quand state passe a NeedsInput, enregistre `WaitingSince`. Affichage "depuis X min" calcule par le CLI. |
+
+
+## Project Constraints (from CLAUDE.md)
+
+- NixOS : dependances via `nix-shell`
+- Code simple, expressif, bien concu
+- Commentaires uniquement pour le "Pourquoi"
+- TDD Chicago School
+- Ne jamais modifier les .env
+- Francais avec accents dans les communications
+
+## Standard Stack
+
+### Core
+| Library | Version | Purpose | Why Standard |
+|---------|---------|---------|--------------|
+| Go | 1.25.7 | Langage | Deja utilise en Phase 1. Disponible via `nix-shell -p go`. |
+| go.i3wm.org/i3/v4 | v4.24.0 | i3 IPC (GetTree, GetWorkspaces, RunCommand) | Lib officielle Go pour i3. Couvre 100% du protocole IPC. Retry transparent sur restart i3. |
+| BurntSushi/xgbutil/ewmh | v0.0.0-20190907113008 | Lecture _NET_WM_PID des fenetres X11 | Dependance transitive de go.i3wm.org/i3/v4. Pas d'ajout explicite. `ewmh.WmPidGet(xu, win)` retourne le PID. |
+| Go stdlib (net, encoding/json, os, sync) | - | Unix socket, JSON protocol, /proc, concurrence | Daemon et IPC ne necessitent aucune lib externe. |
+
+### Supporting
+| Library | Version | Purpose | When to Use |
+|---------|---------|---------|-------------|
+| BurntSushi/xgb | v0.0.0-20210121224620 | Connexion X11 bas niveau | Dependance transitive. Necessaire pour initialiser xgbutil. |
+
+### Alternatives Considered
+| Instead of | Could Use | Tradeoff |
+|------------|-----------|----------|
+| BurntSushi/xgbutil pour _NET_WM_PID | exec xdotool | 37ms pour 14 fenetres, acceptable mais shell-out inutile puisque xgbutil est deja en dep transitive |
+| stdlib net (Unix socket) | gRPC / protobuf | Over-engineered pour un daemon local avec 4 requetes |
+| stdlib flag avec sous-commandes manuelles | cobra | Over-engineered, 4 sous-commandes max |
+| PID file maison | github.com/nightlyone/lockfile | Trop simple pour justifier une dep (un os.WriteFile + flock) |
+
+**Installation:**
+```bash
+nix-shell -p go --run "go get go.i3wm.org/i3/v4@v4.24.0"
+```
+
+**Version verification:** `go.i3wm.org/i3/v4 v4.24.0` correspond a i3 4.24 installe sur la machine. `BurntSushi/xgb` et `xgbutil` sont des dependances transitives, pas d'ajout explicite dans go.mod.
+
+## Architecture Patterns
+
+### Recommended Project Structure
+```
+vmux/
+├── main.go # Point d'entree CLI, dispatch sous-commandes, autostart daemon
+├── daemon.go # vmuxd : boucle de poll, registre en memoire, Unix socket server
+├── daemon_test.go
+├── client.go # Client Unix socket (CLI -> daemon)
+├── client_test.go
+├── protocol.go # Types requete/reponse JSON du socket
+├── protocol_test.go
+├── i3bridge.go # Integration i3 : GetTree, PID resolution, RunCommand
+├── i3bridge_test.go
+├── workspace.go # Mapping PID -> workspace via PPID chain + _NET_WM_PID
+├── workspace_test.go
+├── proc.go # (Phase 1, inchange)
+├── proc_test.go
+├── session.go # (Phase 1, inchange)
+├── session_test.go
+├── state.go # (Phase 1, inchange)
+├── state_test.go
+├── display.go # Etendu : workspace, label, temps d'attente
+├── display_test.go
+├── types.go # Enrichi : Workspace, Label, WaitingSince dans Session
+├── go.mod
+├── go.sum
+└── shell.nix
+```
+
+### Pattern 1: Daemon avec Unix Socket
+**What:** vmuxd ecoute sur `~/.vmux/vmux.sock`, recoit des requetes JSON, repond avec le registre.
+**When to use:** Toutes les commandes CLI (`list`, `switch`, `label`, `stop`).
+
+```go
+// Protocole JSON simple : une requete = un objet JSON, une reponse = un objet JSON
+// Delimiter : chaque message termine par \n (JSON Lines sur le socket)
+
+type Request struct {
+ Action string `json:"action"` // "list", "switch", "label", "stop"
+ Args json.RawMessage `json:"args,omitempty"`
+}
+
+type Response struct {
+ OK bool `json:"ok"`
+ Error string `json:"error,omitempty"`
+ Sessions []SessionInfo `json:"sessions,omitempty"`
+}
+
+type SessionInfo struct {
+ PID int `json:"pid"`
+ SessionID string `json:"session_id"`
+ Cwd string `json:"cwd"`
+ GitBranch string `json:"git_branch"`
+ State string `json:"state"`
+ Preview string `json:"preview"`
+ Workspace string `json:"workspace"`
+ Label string `json:"label,omitempty"`
+ WaitingSince *time.Time `json:"waiting_since,omitempty"`
+}
+```
+
+### Pattern 2: Autostart Daemon
+**What:** Le CLI tente de se connecter au socket. Si echec, il lance vmuxd en arriere-plan puis re-essaie.
+**When to use:** A chaque commande CLI.
+
+```go
+func ensureDaemon(sockPath string) error {
+ conn, err := net.DialTimeout("unix", sockPath, 500*time.Millisecond)
+ if err == nil {
+ conn.Close()
+ return nil // daemon deja actif
+ }
+ // Lancer le daemon en background
+ exe, _ := os.Executable()
+ cmd := exec.Command(exe, "daemon")
+ cmd.Stdout = nil
+ cmd.Stderr = nil
+ cmd.SysProcAttr = &syscall.SysProcAttr{Setsid: true} // detacher du terminal
+ if err := cmd.Start(); err != nil {
+ return fmt.Errorf("cannot start daemon: %w", err)
+ }
+ // Attendre que le socket soit pret (retry avec backoff)
+ for i := 0; i < 20; i++ {
+ time.Sleep(50 * time.Millisecond)
+ conn, err := net.DialTimeout("unix", sockPath, 200*time.Millisecond)
+ if err == nil {
+ conn.Close()
+ return nil
+ }
+ }
+ return fmt.Errorf("daemon did not start in time")
+}
+```
+
+### Pattern 3: PID -> Workspace Resolution (CORRIGE vs D-04)
+**What:** Mapper un PID Claude Code a son workspace i3.
+**Algorithme verifie sur la machine :**
+
+```
+1. i3.GetTree() -> arbre de nodes
+2. Pour chaque node avec Window > 0 :
+ a. ewmh.WmPidGet(xu, xproto.Window(node.Window)) -> terminal PID
+ b. Stocker dans map[terminalPID]workspaceName
+3. Pour chaque Claude PID :
+ a. Lire /proc/PID/status -> PPid
+ b. Remonter la chaine PPID jusqu'a trouver un terminalPID dans la map
+ c. Le workspace correspondant est le workspace de la session
+```
+
+**Verification reelle sur la machine :**
+| Claude PID | Terminal PID (ancestor) | Workspace |
+|------------|------------------------|-----------|
+| 10466 | 10138 | 1 |
+| 13736 | 11864 | 10 |
+| 148021 | 147724 | 2 |
+| 16030 | 15281 | 10 |
+| 179544 | 178345 | 3 |
+| 87142 | 86863 | 3 |
+
+Chaine typique : `claude -> zsh -> .sakura-wrapped -> i3`
+
+```go
+func resolveWorkspace(claudePID int, terminalWorkspaces map[int]string) string {
+ pid := claudePID
+ for i := 0; i < 20; i++ { // max 20 ancestors (securite)
+ if ws, ok := terminalWorkspaces[pid]; ok {
+ return ws
+ }
+ ppid, err := readPPID(pid)
+ if err != nil || ppid <= 1 {
+ return "" // pas de workspace trouve
+ }
+ pid = ppid
+ }
+ return ""
+}
+
+func readPPID(pid int) (int, error) {
+ data, err := os.ReadFile(fmt.Sprintf("/proc/%d/status", pid))
+ // Parse "PPid:\t" line
+}
+```
+
+### Pattern 4: Workspace Switch via i3 IPC
+**What:** Basculer vers le workspace d'une session.
+
+```go
+func switchToWorkspace(wsName string) error {
+ results, err := i3.RunCommand(fmt.Sprintf("workspace number %s", wsName))
+ if err != nil {
+ return err
+ }
+ if len(results) > 0 && !results[0].Success {
+ return fmt.Errorf("i3: %s", results[0].Error)
+ }
+ return nil
+}
+```
+
+### Pattern 5: Fuzzy Match pour Switch
+**What:** `vmux switch auth` matche la premiere session contenant "auth" dans label, branche, ou cwd.
+**Ordre de priorite :** label > branche git > dernier segment du cwd.
+
+```go
+func fuzzyMatch(query string, sessions []SessionInfo) *SessionInfo {
+ query = strings.ToLower(query)
+ // Priorite 1: label contient query
+ for _, s := range sessions {
+ if strings.Contains(strings.ToLower(s.Label), query) {
+ return &s
+ }
+ }
+ // Priorite 2: branche git contient query
+ for _, s := range sessions {
+ if strings.Contains(strings.ToLower(s.GitBranch), query) {
+ return &s
+ }
+ }
+ // Priorite 3: cwd contient query
+ for _, s := range sessions {
+ if strings.Contains(strings.ToLower(s.Cwd), query) {
+ return &s
+ }
+ }
+ return nil
+}
+```
+
+### Pattern 6: Labels Persistence
+**What:** Labels persistes dans `~/.vmux/labels.json`.
+
+```go
+// Format: map[sessionID]string
+type LabelStore struct {
+ mu sync.RWMutex
+ labels map[string]string
+ path string
+}
+
+func (ls *LabelStore) Set(sessionID, label string) error {
+ ls.mu.Lock()
+ defer ls.mu.Unlock()
+ ls.labels[sessionID] = label
+ return ls.save()
+}
+
+func (ls *LabelStore) save() error {
+ data, _ := json.MarshalIndent(ls.labels, "", " ")
+ return os.WriteFile(ls.path, data, 0o644)
+}
+```
+
+### Pattern 7: Tracking des Transitions d'Etat
+**What:** Le daemon enregistre le timestamp quand une session passe a NeedsInput.
+
+```go
+type SessionRegistry struct {
+ mu sync.RWMutex
+ sessions map[string]*TrackedSession // key = sessionID
+}
+
+type TrackedSession struct {
+ SessionInfo
+ PrevState string
+ WaitingSince time.Time // set quand state passe a NeedsInput
+}
+
+func (r *SessionRegistry) Update(info SessionInfo) {
+ r.mu.Lock()
+ defer r.mu.Unlock()
+ existing, ok := r.sessions[info.SessionID]
+ if !ok {
+ r.sessions[info.SessionID] = &TrackedSession{SessionInfo: info}
+ return
+ }
+ if info.State == "Needs Input" && existing.PrevState != "Needs Input" {
+ existing.WaitingSince = time.Now()
+ }
+ existing.PrevState = existing.State
+ existing.SessionInfo = info
+ if info.State == "Needs Input" {
+ info.WaitingSince = &existing.WaitingSince
+ }
+}
+```
+
+### Anti-Patterns to Avoid
+- **i3 GetTree() sans _NET_WM_PID :** Le Node i3 n'a PAS de champ PID. Il faut passer par X11 pour resoudre Window -> PID.
+- **Relancer le daemon a chaque commande :** Utiliser autostart (connect, si echec start, re-connect).
+- **Socket HTTP/REST :** Over-engineered. JSON Lines sur Unix socket suffit (une ligne = une requete, une ligne = une reponse).
+- **Polling i3 GetTree a chaque scan :** Coupler le scan /proc avec le scan i3 dans la meme boucle de 5s pour avoir une vue coherente.
+- **Stocker les labels par PID :** Les PID changent a chaque redemarrage de Claude Code. Utiliser le sessionID (UUID stable).
+
+## Don't Hand-Roll
+
+| Problem | Don't Build | Use Instead | Why |
+|---------|-------------|-------------|-----|
+| i3 IPC protocol | Parser binaire i3 IPC | `go.i3wm.org/i3/v4` | Protocole binaire complexe avec magic bytes, types, longueurs. Retry transparent integre. |
+| X11 window properties | Shell-out vers xprop/xdotool | `BurntSushi/xgbutil/ewmh.WmPidGet()` | Deja en dep transitive. Pure Go, pas de fork/exec. |
+| Process tree walking | Lib procfs | `os.ReadFile("/proc/PID/status")` | Une seule ligne a parser (PPid). Trivial. |
+| JSON encoding/decoding | Lib serialisation | `encoding/json` stdlib | JSON Lines est trivial. |
+| CLI sub-commands | Framework CLI (cobra) | `os.Args` manual dispatch | 5 sous-commandes max (`list`, `switch`, `label`, `stop`, `daemon`). Un switch/case suffit. |
+
+**Insight :** La seule vraie complexite externe est i3 IPC (protocole binaire) et X11 (protocole bas niveau). Les deux sont couverts par les dependances de `go.i3wm.org/i3/v4`.
+
+## Common Pitfalls
+
+### Pitfall 1: i3 Node n'a pas de PID
+**What goes wrong:** Le code suppose que `Node.PID` existe (comme dit dans D-04) et panic/compile error.
+**Why it happens:** Confusion courante. i3 ne stocke pas le PID dans l'arbre. Le PID est une propriete X11 du client window.
+**How to avoid:** Utiliser `Node.Window` (X11 window ID) + `ewmh.WmPidGet()` pour obtenir le terminal PID.
+**Warning signs:** Champ inexistant dans la struct Node du Go package.
+
+### Pitfall 2: Socket file residuel apres crash
+**What goes wrong:** vmuxd ne demarre pas car `~/.vmux/vmux.sock` existe deja (bind: address already in use).
+**Why it happens:** Crash ou kill -9 sans cleanup.
+**How to avoid:** Avant `net.Listen("unix", path)`, tenter un `net.Dial("unix", path)`. Si echec -> supprimer le fichier stale. Si succes -> daemon deja actif.
+**Warning signs:** "address already in use" au demarrage.
+
+### Pitfall 3: Race condition daemon/CLI
+**What goes wrong:** Le CLI envoie une requete avant que le daemon ait fini son premier scan.
+**Why it happens:** Autostart : le daemon vient de demarrer, le registre est vide.
+**How to avoid:** Le daemon fait un scan synchrone AVANT d'ecouter sur le socket. Le CLI attend que le socket soit pret (retry 50ms x 20).
+**Warning signs:** `vmux list` retourne 0 sessions juste apres le premier lancement.
+
+### Pitfall 4: i3 non disponible (SSH, TTY, Wayland)
+**What goes wrong:** `i3.GetTree()` echoue car i3 IPC n'est pas accessible.
+**Why it happens:** vmux est execute dans un environnement sans i3 (SSH, console, Sway/Wayland).
+**How to avoid:** Attraper l'erreur de `GetTree()`, mettre workspace="" pour toutes les sessions. `vmux list` fonctionne toujours, sans colonne workspace. `vmux switch` retourne une erreur claire.
+**Warning signs:** Erreur au premier scan.
+
+### Pitfall 5: xgbutil necessita DISPLAY
+**What goes wrong:** `xgbutil.NewConn()` echoue si `$DISPLAY` n'est pas set.
+**Why it happens:** Meme situation que pitfall 4 : pas de X11.
+**How to avoid:** Verifier `$DISPLAY` avant d'initialiser xgbutil. Si absent, desactiver le mapping workspace (fallback gracieux).
+**Warning signs:** Erreur "cannot open display" au demarrage du daemon.
+
+### Pitfall 6: Labels orphelins dans labels.json
+**What goes wrong:** labels.json grossit indefiniment avec des sessionIDs de sessions terminees.
+**Why it happens:** Les sessions Claude Code sont ephemeres, les sessionIDs changent.
+**How to avoid:** Au chargement de labels.json, ne garder que les labels dont le sessionID correspond a une session active. Ou purger periodiquement (ex: garder les 100 derniers).
+**Warning signs:** labels.json de plusieurs Mo apres des semaines d'utilisation.
+
+### Pitfall 7: PPID chain walk infinie
+**What goes wrong:** La boucle de walk PPID ne termine pas (process circulaire theorique).
+**Why it happens:** Bug kernel theorique ou processus zombie.
+**How to avoid:** Limiter a 20 iterations. PID 1 (init) est toujours le point d'arret.
+**Warning signs:** CPU spike pendant le scan.
+
+## Code Examples
+
+### Initialisation X11 pour _NET_WM_PID
+```go
+// Source: BurntSushi/xgbutil/ewmh package (dep transitive de go.i3wm.org/i3/v4)
+import (
+ "github.com/BurntSushi/xgb/xproto"
+ "github.com/BurntSushi/xgbutil"
+ "github.com/BurntSushi/xgbutil/ewmh"
+ "go.i3wm.org/i3/v4"
+)
+
+func buildTerminalWorkspaceMap() (map[int]string, error) {
+ xu, err := xgbutil.NewConn()
+ if err != nil {
+ return nil, fmt.Errorf("X11 connection failed: %w", err)
+ }
+ defer xu.Conn().Close()
+
+ tree, err := i3.GetTree()
+ if err != nil {
+ return nil, fmt.Errorf("i3 GetTree failed: %w", err)
+ }
+
+ result := make(map[int]string)
+ var walk func(node *i3.Node, wsName string)
+ walk = func(node *i3.Node, wsName string) {
+ if node.Type == i3.WorkspaceNode {
+ wsName = node.Name
+ }
+ if node.Window > 0 {
+ pid, err := ewmh.WmPidGet(xu, xproto.Window(node.Window))
+ if err == nil && pid > 0 {
+ result[int(pid)] = wsName
+ }
+ }
+ for _, child := range node.Nodes {
+ walk(child, wsName)
+ }
+ for _, child := range node.FloatingNodes {
+ walk(child, wsName)
+ }
+ }
+ walk(tree.Root, "")
+
+ return result, nil
+}
+```
+
+### Lecture du PPID depuis /proc
+```go
+// Source: verification directe sur /proc//status
+func readPPID(pid int) (int, error) {
+ data, err := os.ReadFile(fmt.Sprintf("/proc/%d/status", pid))
+ if err != nil {
+ return 0, err
+ }
+ for _, line := range strings.Split(string(data), "\n") {
+ if strings.HasPrefix(line, "PPid:") {
+ fields := strings.Fields(line)
+ if len(fields) >= 2 {
+ return strconv.Atoi(fields[1])
+ }
+ }
+ }
+ return 0, fmt.Errorf("PPid not found for PID %d", pid)
+}
+```
+
+### Unix Socket Server (daemon)
+```go
+// Source: Go stdlib net package
+func startDaemonSocket(sockPath string) (net.Listener, error) {
+ // Cleanup stale socket
+ if _, err := net.DialTimeout("unix", sockPath, 200*time.Millisecond); err != nil {
+ os.Remove(sockPath) // stale, supprimer
+ }
+
+ os.MkdirAll(filepath.Dir(sockPath), 0o700)
+ return net.Listen("unix", sockPath)
+}
+
+func serveDaemon(listener net.Listener, registry *SessionRegistry) {
+ for {
+ conn, err := listener.Accept()
+ if err != nil {
+ return // listener ferme
+ }
+ go handleConnection(conn, registry)
+ }
+}
+
+func handleConnection(conn net.Conn, registry *SessionRegistry) {
+ defer conn.Close()
+ decoder := json.NewDecoder(conn)
+ encoder := json.NewEncoder(conn)
+
+ var req Request
+ if err := decoder.Decode(&req); err != nil {
+ return
+ }
+
+ switch req.Action {
+ case "list":
+ sessions := registry.List()
+ encoder.Encode(Response{OK: true, Sessions: sessions})
+ case "switch":
+ // ... fuzzy match + i3.RunCommand
+ case "label":
+ // ... set label
+ case "stop":
+ encoder.Encode(Response{OK: true})
+ // signal shutdown
+ }
+}
+```
+
+### Switch workspace via i3
+```go
+// Source: go.i3wm.org/i3/v4 documentation (pkg.go.dev)
+func switchToWorkspace(wsName string) error {
+ cmd := fmt.Sprintf("workspace number %s", wsName)
+ results, err := i3.RunCommand(cmd)
+ if err != nil {
+ if i3.IsUnsuccessful(err) {
+ return fmt.Errorf("i3 switch failed: %v", results[0].Error)
+ }
+ return err
+ }
+ return nil
+}
+```
+
+## State of the Art
+
+| Old Approach | Current Approach | When Changed | Impact |
+|--------------|------------------|--------------|--------|
+| BurntSushi/xgb (original) | jezek/xgb (maintained fork) | 2021 | jezek/xgb est le fork actif. Mais go.i3wm.org/i3/v4 utilise encore BurntSushi/xgb. |
+| i3 IPC v4.0 (basique) | i3 IPC v4.24 (complet) | 2024 | Memes APIs core. GetTree, GetWorkspaces, RunCommand stables depuis des annees. |
+
+## Validation Architecture
+
+### Test Framework
+| Property | Value |
+|----------|-------|
+| Framework | Go testing (stdlib) |
+| Config file | Aucun (convention Go par defaut) |
+| Quick run command | `go test ./...` |
+| Full suite command | `go test -v -race ./...` |
+
+### Phase Requirements -> Test Map
+| Req ID | Behavior | Test Type | Automated Command | File Exists? |
+|--------|----------|-----------|-------------------|-------------|
+| DISC-04 | Label humain sur une session | unit | `go test -run TestLabelStore -v` | Wave 0 |
+| I3-01 | Mapping session -> workspace i3 | unit | `go test -run TestResolveWorkspace -v` | Wave 0 |
+| I3-02 | Switch vers workspace i3 d'une session | unit+integration | `go test -run TestFuzzyMatch -v` | Wave 0 |
+| STATE-04 | Temps ecoule depuis attente | unit | `go test -run TestWaitingSince -v` | Wave 0 |
+
+### Sampling Rate
+- **Per task commit:** `go test ./...`
+- **Per wave merge:** `go test -v -race ./...`
+- **Phase gate:** Full suite green before `/gsd:verify-work`
+
+### Wave 0 Gaps
+- [ ] `daemon_test.go` -- poll loop, registre, autostart logic (testable avec mock socket)
+- [ ] `client_test.go` -- requetes/reponses sur socket (testable avec mock server)
+- [ ] `protocol_test.go` -- serialisation JSON des requetes/reponses
+- [ ] `workspace_test.go` -- couvre I3-01 : PPID chain walk (testable avec fake /proc + mock i3 tree)
+- [ ] `i3bridge_test.go` -- couvre I3-02 : fuzzy match (pure logique, pas de mock)
+
+### Strategy TDD
+Le daemon est testable en isolant les couches :
+- **Registre** : pure logique, pas de I/O. Tester les transitions d'etat, le tracking WaitingSince.
+- **Protocol** : serialisation/deserialisation JSON. Tester les request/response structs.
+- **Workspace resolution** : `readPPID` mockable via un fake /proc. Le mapping i3 mockable via une interface.
+- **Client/Server** : Tester avec un vrai Unix socket dans un tmpdir.
+- **i3 bridge** : Abstraire derriere une interface (`type I3Client interface { GetTree() ... }`). Mocker en test.
+
+L'integration i3 reelle (GetTree, RunCommand) sera testee manuellement sur la machine. Les tests unitaires couvrent toute la logique sans dependre de i3.
+
+## Environment Availability
+
+| Dependency | Required By | Available | Version | Fallback |
+|------------|------------|-----------|---------|----------|
+| Go | Build & run | oui | 1.25.7 (via nix-shell) | - |
+| i3 | Workspace mapping + switch | oui | 4.24 | Fallback gracieux : vmux fonctionne sans workspace si i3 absent |
+| X11 (DISPLAY) | _NET_WM_PID resolution | oui | X11 actif (i3 tourne) | Meme fallback : pas de workspace sans X11 |
+| /proc filesystem | Process detection + PPID chain | oui | procfs Linux 6.19 | - |
+| xdotool | Non utilise (pure Go via xgbutil) | oui | 3.20211022.1 | - |
+
+**Missing dependencies with no fallback:** Aucune.
+
+**Missing dependencies with fallback:**
+- i3 + X11 : si absents, vmux fonctionne sans les colonnes workspace et sans `vmux switch`. Toutes les autres fonctionnalites (list, label, temps d'attente) restent operationnelles.
+
+## Open Questions
+
+1. **Concurrence X11 + i3 IPC dans le daemon**
+ - Ce qu'on sait : `go.i3wm.org/i3/v4` est concurrent-safe. `xgbutil` n'est PAS concurrent-safe explicitement.
+ - Ce qui est flou : Le daemon poll dans une goroutine et repond aux requetes dans d'autres. La connexion X11 doit-elle etre re-ouverte a chaque scan ou partagee ?
+ - Recommandation : Ouvrir la connexion X11 une fois au demarrage du daemon. N'utiliser `ewmh.WmPidGet` que dans la goroutine de scan (jamais depuis les handlers de requetes). Le scan produit une snapshot `map[int]string` atomiquement remplacee via `sync.RWMutex`.
+
+2. **Sous-commande `daemon` visible ou cachee ?**
+ - Ce qu'on sait : D-03 dit autostart implicite.
+ - Ce qui est flou : L'utilisateur peut-il lancer `vmux daemon` manuellement ?
+ - Recommandation : Sous-commande cachee (non affichee dans l'aide). Utile pour le debug (`vmux daemon --foreground`).
+
+3. **Nettoyage des sessions disparues**
+ - Ce qu'on sait : Le daemon poll toutes les 5s. Si un processus Claude disparait, il ne sera plus dans /proc.
+ - Ce qui est flou : Faut-il garder l'entree quelques secondes pour eviter le flicker ?
+ - Recommandation : Supprimer immediatement du registre. Si le PID n'est plus dans /proc, la session est terminee.
+
+## Sources
+
+### Primary (HIGH confidence)
+- [go.i3wm.org/i3/v4 pkg.go.dev](https://pkg.go.dev/go.i3wm.org/i3/v4) - API complete : Node struct (pas de PID), GetTree, GetWorkspaces, RunCommand, Subscribe
+- [BurntSushi/xgbutil/ewmh source](https://github.com/BurntSushi/xgbutil/blob/master/ewmh/ewmh.go) - `WmPidGet(xu, win)` pour _NET_WM_PID
+- Inspection directe machine : 6 Claude PIDs mappes a leurs workspaces via PPID chain
+- i3-msg -t get_tree : confirme absence du champ `pid` dans les nodes (14 windows verifiees)
+
+### Secondary (MEDIUM confidence)
+- [i3 IPC documentation](https://i3wm.org/docs/ipc.html) - Protocole IPC officiel
+- [Go Unix Domain Sockets (Eli Bendersky)](https://eli.thegreenplace.net/2019/unix-domain-sockets-in-go/) - Pattern daemon Go
+- [go-i3 GitHub](https://github.com/i3/go-i3) - Repo officiel de la lib Go i3
+
+### Tertiary (LOW confidence)
+- Comportement de `xgbutil` en multi-thread : documentation limitee, recommandation basee sur les bonnes pratiques Go (single writer)
+
+## Metadata
+
+**Confidence breakdown:**
+- Standard stack: HIGH - go.i3wm.org/i3/v4 v4.24.0 verifie, BurntSushi/xgbutil confirme en dep transitive
+- Architecture: HIGH - Algorithme PID->workspace verifie sur 6 sessions reelles
+- Pitfalls: HIGH - Absence de PID dans Node i3 decouverte par inspection directe, pas par supposition
+- Protocol: MEDIUM - Design du protocole socket base sur des patterns Go standard, pas de source specifique
+
+**Research date:** 2026-03-23
+**Valid until:** 2026-04-23 (i3 IPC stable depuis des annees, /proc stable)