diff --git a/.planning/phases/01-session-discovery/01-RESEARCH.md b/.planning/phases/01-session-discovery/01-RESEARCH.md new file mode 100644 index 0000000..62323ac --- /dev/null +++ b/.planning/phases/01-session-discovery/01-RESEARCH.md @@ -0,0 +1,377 @@ +# Phase 1: Session Discovery - Research + +**Researched:** 2026-03-23 +**Domain:** Linux process inspection + Claude Code JSONL parsing +**Confidence:** HIGH + +## Summary + +La Phase 1 est un CLI one-shot (`vmux list`) qui scanne les processus Claude Code actifs via `/proc`, croise les PID avec les fichiers JSONL dans `~/.claude/projects/`, et affiche l'etat de chaque session. + +Le format JSONL de Claude Code est bien documente et stable. Chaque message contient des metadonnees riches (sessionId, cwd, gitBranch, version). L'heuristique d'etat se base sur le `stop_reason` du dernier message `assistant` et le nom de l'outil si `stop_reason=tool_use`. + +**Decouverte critique :** `sessions-index.json` n'est plus genere par les versions recentes de Claude Code (derniere MAJ: 2026-02-03). Seulement 13 des 91 dossiers projets en ont un. Il faut lire les metadonnees directement depuis le JSONL (chaque entree contient `sessionId`, `cwd`, `gitBranch`, `version`). + +**Recommandation principale :** Ne pas dependre de `sessions-index.json`. Parser la premiere et la derniere entree du JSONL pour obtenir toutes les informations necessaires. + + +## User Constraints (from CONTEXT.md) + +### Locked Decisions +- **D-01:** Scanner `/proc` pour trouver les PID des processus Claude Code actifs, puis enrichir avec les fichiers JSONL dans `~/.claude/projects/`. Un JSONL sans PID correspondant = session terminee, ignoree. +- **D-02:** Utiliser `sessions-index.json` (present dans chaque dossier projet sous `~/.claude/projects/`) comme source de metadonnees riches (sessionId, gitBranch, projectPath, summary, created, modified). Format verifie sur le poste. +- **D-03:** Le matching PID -> session se fait via le cwd du processus (`/proc/PID/cwd` -> readlink) croise avec le `projectPath` du sessions-index. +- **D-04:** Se baser uniquement sur le JSONL (tail-read de la derniere entree) pour determiner l'etat. Pas de lecture CPU en Phase 1. +- **D-05:** Heuristique d'etat basee sur le dernier message JSONL (voir details dans CONTEXT.md). +- **D-06:** Les JSONL peuvent faire > 100 Mo. Toujours tail-read, jamais charger le fichier entier. +- **D-07:** Couleurs activees par defaut pour l'etat (vert = Working, rouge/jaune = Needs Input, gris = Idle). + +### Claude's Discretion +- Format d'affichage : tableau compact ou blocs detailles, libre de proposer un format hybride. +- Gestion du `--no-color` pour la compatibilite pipe. + +### Deferred Ideas (OUT OF SCOPE) +None + + + +## Phase Requirements + +| ID | Description | Research Support | +|----|-------------|------------------| +| DISC-01 | vmux detecte automatiquement les processus Claude Code actifs sur le poste | Scan `/proc/*/cmdline` pour trouver les processus `claude`. Verifie: 6 processus actifs trouves. | +| DISC-02 | vmux identifie le cwd et le worktree git de chaque session | `readlink /proc/PID/cwd` donne le cwd. Worktree git via `git rev-parse --show-toplevel` ou parsing `.git`. | +| DISC-03 | vmux affiche le nom de la branche git de chaque session | Champ `gitBranch` present dans chaque entree JSONL. Alternative: `git -C branch --show-current`. | +| STATE-01 | vmux detecte l'etat de chaque session : travaille / attend input / idle | Heuristique basee sur `stop_reason` + `content[].type` du dernier message assistant. Voir section "Architecture Patterns". | +| STATE-02 | vmux affiche un apercu des dernieres lignes de sortie de chaque session | Champ `content[].text` du dernier message assistant type `text`. Aussi `summary` dans sessions-index.json quand disponible. | + + +## Standard Stack + +### Core +| Library | Version | Purpose | Why Standard | +|---------|---------|---------|--------------| +| Go | 1.25.7 | Langage | Deja utilise par piaire. Disponible via `nix-shell -p go`. | +| Go stdlib (os, encoding/json, path/filepath, fmt) | - | Tout le runtime | Aucune dependance externe necessaire pour Phase 1. /proc se lit avec os.ReadFile/os.Readlink. | + +### Supporting +| Library | Version | Purpose | When to Use | +|---------|---------|---------|-------------| +| Aucune | - | - | Phase 1 = stdlib uniquement. Pas de CLI framework, pas de TUI, pas de deps. | + +### Alternatives Considered +| Instead of | Could Use | Tradeoff | +|------------|-----------|----------| +| stdlib os pour /proc | prometheus/procfs | Over-engineered pour lire cmdline + cwd. API instable (WARNING dans leur doc). | +| stdlib flag | cobra/urfave | Over-engineered pour une seule commande `list`. flag suffit. | +| Reverse-read maison | hpcloud/tail | tail est pour le mode follow (Phase 2+). Pour un one-shot, un seek+read depuis la fin suffit. | + +**Installation:** +```bash +# Rien a installer. Go stdlib uniquement. +nix-shell -p go --run "go mod init github.com/pieMusic/vmux" +``` + +## Architecture Patterns + +### Recommended Project Structure +``` +vmux/ +├── main.go # Point d'entree, parsing des args +├── proc.go # Scan /proc pour les processus Claude +├── proc_test.go +├── session.go # Lecture JSONL + matching PID->session +├── session_test.go +├── state.go # Heuristique d'etat (Working/NeedsInput/Idle) +├── state_test.go +├── display.go # Formatage et affichage (couleurs, layout) +├── display_test.go +├── go.mod +└── shell.nix # Environnement de dev NixOS +``` + +### Pattern 1: Scan /proc pour les processus Claude +**What:** Lister tous les PID dont `/proc/PID/cmdline` contient `claude`. +**When to use:** Au demarrage de `vmux list`. +**Verification sur machine:** Les processus Claude Code apparaissent comme `claude` ou `claude -c` dans cmdline. + +```go +// Verifie sur la machine : /proc/PID/cmdline contient "claude\x00" ou "claude\x00-c\x00" +func findClaudeProcesses() ([]Process, error) { + entries, err := os.ReadDir("/proc") + // Pour chaque entree numerique, lire /proc/PID/cmdline + // Si le premier argument est "claude", c'est un processus Claude Code + // Lire aussi /proc/PID/cwd via os.Readlink pour obtenir le cwd +} +``` + +### Pattern 2: Encoding du chemin projet +**What:** Convertir un cwd en nom de dossier `~/.claude/projects/`. +**Verification:** `/home/pierre/Code/vibe/vmux` -> `-home-pierre-Code-vibe-vmux`. Les `/` et `.` sont remplaces par `-`. + +```go +func encodePath(cwd string) string { + // Remplacer / et . par - + result := strings.ReplaceAll(cwd, "/", "-") + result = strings.ReplaceAll(result, ".", "-") + return result +} +``` + +### Pattern 3: Tail-read JSONL (lecture depuis la fin) +**What:** Lire les N dernieres lignes d'un JSONL sans charger le fichier entier. +**When to use:** Pour chaque session, lire la derniere entree pour determiner l'etat. + +```go +func tailReadJSONL(path string, n int) ([]json.RawMessage, error) { + f, err := os.Open(path) + // Seek vers la fin du fichier + // Lire des blocs de ~8KB en arriere jusqu'a trouver n lignes completes + // Parser chaque ligne comme JSON +} +``` + +### Pattern 4: Heuristique d'etat +**What:** Determiner l'etat d'une session a partir du dernier message JSONL. +**Verification sur machine (resultats reels) :** + +| Dernier message | stop_reason | content_types | Etat | +|-----------------|-------------|---------------|------| +| `type=assistant` | `end_turn` | `['text']` | **Needs Input** (Claude a fini, attend l'utilisateur) | +| `type=assistant` | `tool_use` | `['tool_use']` avec `name=AskUserQuestion` | **Needs Input** (Claude pose une question) | +| `type=assistant` | `tool_use` | `['tool_use']` avec autre outil | **Working** (outil en cours d'execution) | +| `type=progress` | - | `data.type=agent_progress` | **Working** (subagent actif) | +| `type=progress` | - | `data.type=hook_progress` | **Working** (hook en cours) | +| `type=user` avec `content[].type=tool_result` | - | - | **Working** (resultat de tool envoye, Claude va repondre) | +| Aucun message recent (> seuil) | - | - | **Idle** | + +**Important :** le `stop_reason` est dans `message.stop_reason`, pas au top-level. + +### Pattern 5: Matching PID -> JSONL (REVISE par la recherche) +**What:** Trouver le bon JSONL pour un PID donne. +**Algorithme revise :** +1. Lire le cwd du PID via `readlink /proc/PID/cwd` +2. Encoder le cwd pour obtenir le nom du dossier dans `~/.claude/projects/` +3. Lister les fichiers `.jsonl` dans ce dossier (exclure le sous-dossier `subagents/`) +4. Prendre le fichier JSONL le plus recemment modifie (= session active) +5. **NE PAS dependre de `sessions-index.json`** (absent dans 85% des projets recents) + +### Anti-Patterns to Avoid +- **Charger un JSONL entier en memoire :** Les fichiers peuvent depasser 100 Mo. Toujours tail-read. +- **Dependre de sessions-index.json :** Obsolete depuis ~fev 2026. Seulement 13/91 projets en ont un. +- **Utiliser `ps aux` via exec :** Lire `/proc` directement est plus fiable et ne depend pas du format de sortie de `ps`. +- **Ignorer les subagents :** Le dossier `/subagents/` contient des JSONL de sous-agents. Ne pas les confondre avec les sessions principales. + +## Don't Hand-Roll + +| Problem | Don't Build | Use Instead | Why | +|---------|-------------|-------------|-----| +| Parsing des args CLI | Framework CLI (cobra, urfave) | `flag` stdlib | Une seule commande, pas besoin de framework | +| Couleurs terminal | Lib de couleurs (fatih/color) | Codes ANSI directs | 5 couleurs max, pas besoin d'une dependance | +| Lecture /proc | Lib procfs | `os.ReadFile` + `os.Readlink` | 3 fichiers a lire par PID, trivial | + +**Insight :** Phase 1 est volontairement zero-dependency. La complexite est dans la logique metier (heuristique d'etat, tail-read), pas dans les outils. + +## Common Pitfalls + +### Pitfall 1: sessions-index.json absent +**What goes wrong:** Le code essaie de lire sessions-index.json et echoue silencieusement, pas de metadonnees. +**Why it happens:** Claude Code ne genere plus ce fichier depuis ~fevrier 2026. +**How to avoid:** Lire les metadonnees directement depuis les entrees JSONL (chaque entree contient sessionId, cwd, gitBranch, version). +**Warning signs:** 85% des projets recents n'ont pas de sessions-index.json. + +### Pitfall 2: JSONL > 100 Mo charge en memoire +**What goes wrong:** OOM ou lenteur extreme pour une commande one-shot. +**Why it happens:** Les sessions longues generent des JSONL volumineux (max observe: 23 Mo pour un subagent). +**How to avoid:** Tail-read : `Seek(0, io.SeekEnd)` puis lire en arriere par blocs de 8-16 Ko. +**Warning signs:** Temps de reponse > 1s pour `vmux list`. + +### Pitfall 3: Race condition sur le JSONL +**What goes wrong:** Le JSONL est en cours d'ecriture par Claude Code pendant la lecture. +**Why it happens:** Fichier append-only, ecriture concurrente. +**How to avoid:** Lire la derniere ligne **complete** (terminee par `\n`). Ignorer une derniere ligne tronquee. +**Warning signs:** Erreur JSON parse sur la derniere ligne. + +### Pitfall 4: Processus Claude != session active +**What goes wrong:** Un PID Claude existe mais aucun JSONL recent ne correspond. +**Why it happens:** Claude Code peut etre en phase de demarrage, ou le dossier projet n'existe pas encore. +**How to avoid:** Afficher le processus avec un etat "Unknown" si aucun JSONL n'est trouve. +**Warning signs:** PID sans dossier encode correspondant dans `~/.claude/projects/`. + +### Pitfall 5: Confusion subagent / session principale +**What goes wrong:** Des JSONL de subagents sont affiches comme des sessions separees. +**Why it happens:** Structure `/subagents/agent-*.jsonl` dans le dossier projet. +**How to avoid:** Ne lister que les `*.jsonl` directement dans le dossier projet, pas dans les sous-dossiers. +**Warning signs:** Sessions fantomes qui apparaissent et disparaissent. + +### Pitfall 6: Double-dash dans l'encoding des chemins avec des points +**What goes wrong:** `/home/user/.config` s'encode en `-home-user--config` (double dash). Le code d'encoding rate la correspondance. +**Why it happens:** Les `.` sont remplaces par `-`, ce qui cree un `--` quand un dossier commence par `.`. +**How to avoid:** L'encoding est simple : remplacer `/` ET `.` par `-`. Verifie sur la machine. + +## Code Examples + +### Lecture de /proc/PID/cmdline +```go +// Source: verification directe sur /proc/13736/cmdline +func readCmdline(pid int) ([]string, error) { + data, err := os.ReadFile(fmt.Sprintf("/proc/%d/cmdline", pid)) + if err != nil { + return nil, err + } + // cmdline est separe par des null bytes + parts := strings.Split(strings.TrimRight(string(data), "\x00"), "\x00") + return parts, nil +} +``` + +### Structure d'un message JSONL assistant +```json +{ + "parentUuid": "...", + "isSidechain": false, + "message": { + "role": "assistant", + "content": [ + {"type": "text", "text": "..."}, + {"type": "tool_use", "name": "Read", "id": "toolu_..."} + ], + "stop_reason": "tool_use" + }, + "type": "assistant", + "uuid": "...", + "timestamp": "2026-03-23T11:38:18.236Z", + "cwd": "/home/pierre/Code/vibe/vmux", + "sessionId": "3a0e1bbb-...", + "version": "2.1.81", + "gitBranch": "HEAD" +} +``` + +### Structure d'un message progress (subagent) +```json +{ + "type": "progress", + "data": {"type": "agent_progress"}, + "timestamp": "2026-03-23T12:00:43.006Z", + "sessionId": "...", + "cwd": "..." +} +``` + +### Structure d'un message progress (hook) +```json +{ + "type": "progress", + "data": {"type": "hook_progress", "hookEvent": "...", "hookName": "..."}, + "timestamp": "..." +} +``` + +### Structure d'un message system (fin de tour) +```json +{ + "type": "system", + "subtype": "turn_duration", + "durationMs": 82049, + "timestamp": "...", + "sessionId": "...", + "cwd": "...", + "gitBranch": "HEAD" +} +``` + +## State of the Art + +| Old Approach | Current Approach | When Changed | Impact | +|--------------|------------------|--------------|--------| +| `sessions-index.json` pour les metadonnees | Metadonnees dans chaque entree JSONL | ~fev 2026 | Ne plus dependre de sessions-index.json | +| Claude Code v2.0.x | Claude Code v2.1.81 | mars 2026 | Champ `slug` ajoute, `agent_progress` pour les subagents | + +## 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-01 | Detecter les processus Claude actifs | unit | `go test -run TestFindClaudeProcesses -v` | Wave 0 | +| DISC-02 | Identifier cwd et worktree git | unit | `go test -run TestProcessCwd -v` | Wave 0 | +| DISC-03 | Afficher la branche git | unit | `go test -run TestGitBranch -v` | Wave 0 | +| STATE-01 | Detecter l'etat (Working/NeedsInput/Idle) | unit | `go test -run TestDetectState -v` | Wave 0 | +| STATE-02 | Afficher un apercu de la sortie | unit | `go test -run TestExtractPreview -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 +- [ ] `proc_test.go` -- couvre DISC-01, DISC-02 (testable avec des fixtures /proc simulees) +- [ ] `session_test.go` -- couvre DISC-03, STATE-02 (fixtures JSONL) +- [ ] `state_test.go` -- couvre STATE-01 (fixtures JSONL avec differents patterns) +- [ ] `display_test.go` -- couvre le formatage de sortie +- [ ] `go.mod` + `shell.nix` -- infrastructure de base + +### Strategy TDD +Les tests pour `/proc` et JSONL doivent utiliser des fixtures (fichiers temporaires) plutot que de lire le vrai `/proc`. Cela permet : +- Executer les tests en CI sans processus Claude actifs +- Tester les cas limites (JSONL corrompu, processus zombie, etc.) +- Reproductibilite totale + +## Environment Availability + +| Dependency | Required By | Available | Version | Fallback | +|------------|------------|-----------|---------|----------| +| Go | Build & run | oui | 1.25.7 (via nix-shell) | - | +| /proc filesystem | Process detection | oui | procfs Linux 6.19 | - | +| git | Branch detection | oui | standard NixOS | Fallback sur gitBranch du JSONL | + +**Missing dependencies with no fallback:** Aucune. + +## Open Questions + +1. **Seuil Idle** + - Ce qu'on sait : D-05 mentionne "Derniere activite > seuil -> Idle" sans definir le seuil. + - Ce qui est flou : 30s ? 60s ? 5min ? + - Recommandation : Commencer a 60s. Configurable plus tard. En Phase 1, un const suffit. + +2. **Sessions-index.json obsolete vs D-02** + - Ce qu'on sait : D-02 mentionne sessions-index.json comme source. La recherche montre qu'il est absent dans 85% des cas recents. + - Ce qui est flou : Faut-il quand meme le lire quand il existe (pour le champ `summary`) ? + - Recommandation : Lire sessions-index.json en bonus si present (pour `summary`), mais ne JAMAIS en dependre. Le JSONL est la source primaire. + +3. **Multiple sessions par cwd** + - Ce qu'on sait : Pas observe sur la machine actuelle (6 PID, 6 cwd differents). + - Ce qui est flou : Est-ce possible d'avoir 2 processus Claude dans le meme repertoire ? + - Recommandation : Prendre le JSONL le plus recemment modifie. Si 2 PID partagent le meme cwd, afficher 2 sessions separees pointant vers les memes JSONL (cas rare, accepter l'imprecision en v1). + +## Sources + +### Primary (HIGH confidence) +- Inspection directe de `/proc/*/cmdline`, `/proc/*/cwd` sur la machine (6 processus Claude actifs) +- Lecture directe de `~/.claude/projects/` : 91 dossiers projets, 13 avec sessions-index.json +- Analyse de fichiers JSONL reels (format des messages assistant, user, progress, system) +- Structure verifiee : `/subagents/agent-*.jsonl` pour les sous-agents + +### Secondary (MEDIUM confidence) +- Stack research (CLAUDE.md) : Go 1.25.7 verifie, Bubble Tea v2 pour phases futures + +### Tertiary (LOW confidence) +- Encoding exact des chemins (verifie sur ~10 exemples, peut-etre des cas speciaux non couverts avec des caracteres Unicode ou des espaces) + +## Metadata + +**Confidence breakdown:** +- Standard stack: HIGH - Go stdlib uniquement, verifie sur machine +- Architecture: HIGH - Format JSONL et /proc verifies par inspection directe +- Pitfalls: HIGH - sessions-index.json obsolete decouvert par inspection reelle (13/91) + +**Research date:** 2026-03-23 +**Valid until:** 2026-04-23 (format JSONL stable, /proc stable)