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)