Files
2026-03-23 13:04:40 +01:00

378 lines
18 KiB
Markdown

# 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>
## 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
</user_constraints>
<phase_requirements>
## 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 <cwd> 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. |
</phase_requirements>
## 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 `<session-uuid>/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 `<uuid>/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 : `<uuid>/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)