docs(01-session-discovery): create phase plan

This commit is contained in:
Pierre Martin
2026-03-23 13:09:19 +01:00
parent 3faba89614
commit 4916a75011
3 changed files with 649 additions and 3 deletions

View File

@@ -28,11 +28,11 @@ Decimal phases appear between their surrounding integers in numeric order.
2. Chaque session affiche son cwd, worktree git et branche git 2. Chaque session affiche son cwd, worktree git et branche git
3. Chaque session affiche son etat : Working, Needs Input ou Idle 3. Chaque session affiche son etat : Working, Needs Input ou Idle
4. Chaque session affiche un apercu des dernieres lignes de sortie 4. Chaque session affiche un apercu des dernieres lignes de sortie
**Plans**: TBD **Plans**: 2 plans
Plans: Plans:
- [ ] 01-01: TBD - [ ] 01-01-PLAN.md — Scaffolding projet Go + detection des processus Claude via /proc
- [ ] 01-02: TBD - [ ] 01-02-PLAN.md — Parsing JSONL, heuristique d'etat et CLI vmux list
### Phase 2: Daemon et i3 Bridge ### Phase 2: Daemon et i3 Bridge
**Goal**: L'utilisateur peut switcher vers n'importe quelle session Claude Code en une action via son workspace i3 **Goal**: L'utilisateur peut switcher vers n'importe quelle session Claude Code en une action via son workspace i3

View File

@@ -0,0 +1,223 @@
---
phase: 01-session-discovery
plan: 01
type: execute
wave: 1
depends_on: []
files_modified:
- shell.nix
- go.mod
- types.go
- proc.go
- proc_test.go
autonomous: true
requirements:
- DISC-01
- DISC-02
must_haves:
truths:
- "vmux detecte les processus Claude Code actifs via /proc"
- "vmux lit le cwd de chaque processus via /proc/PID/cwd"
- "vmux encode le cwd en nom de dossier ~/.claude/projects/"
artifacts:
- path: "shell.nix"
provides: "Environnement de dev NixOS avec Go"
contains: "go"
- path: "go.mod"
provides: "Module Go"
contains: "module github.com/pieMusic/vmux"
- path: "types.go"
provides: "Types partages (Process, Session, SessionState)"
exports: ["Process", "Session", "SessionState", "Working", "NeedsInput", "Idle"]
- path: "proc.go"
provides: "Scan /proc pour les processus Claude"
exports: ["FindClaudeProcesses", "EncodePath"]
- path: "proc_test.go"
provides: "Tests unitaires pour proc.go"
contains: "TestFindClaudeProcesses"
key_links:
- from: "proc.go"
to: "/proc/*/cmdline"
via: "os.ReadFile + strings.Split sur null bytes"
pattern: "ReadFile.*cmdline"
- from: "proc.go"
to: "/proc/*/cwd"
via: "os.Readlink"
pattern: "Readlink.*cwd"
---
<objective>
Scaffolding du projet Go et detection des processus Claude Code via /proc.
Purpose: Poser les fondations (module Go, types, environnement NixOS) et implementer la brique de base : trouver les processus Claude actifs avec leur cwd.
Output: Un module Go fonctionnel avec `FindClaudeProcesses()` teste qui retourne les PID + cwd des processus Claude.
</objective>
<execution_context>
@$HOME/.claude/get-shit-done/workflows/execute-plan.md
@$HOME/.claude/get-shit-done/templates/summary.md
</execution_context>
<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md
@.planning/phases/01-session-discovery/01-CONTEXT.md
@.planning/phases/01-session-discovery/01-RESEARCH.md
</context>
<tasks>
<task type="auto" tdd="true">
<name>Task 1: Scaffolding projet + types partages</name>
<files>shell.nix, go.mod, types.go</files>
<read_first>
.planning/phases/01-session-discovery/01-RESEARCH.md
</read_first>
<action>
Creer 3 fichiers a la racine du projet :
**shell.nix** : environnement NixOS avec `go` et `gopls` :
```nix
{ pkgs ? import <nixpkgs> {} }:
pkgs.mkShell {
buildInputs = with pkgs; [ go gopls ];
}
```
**go.mod** : initialiser le module Go :
```
module github.com/pieMusic/vmux
go 1.25
```
**types.go** (package main) : definir les types partages utilises par tous les autres fichiers :
```go
package main
type SessionState int
const (
Working SessionState = iota
NeedsInput
Idle
Unknown
)
func (s SessionState) String() string // "Working", "Needs Input", "Idle", "Unknown"
type Process struct {
PID int
Cmd []string // arguments cmdline
Cwd string // readlink /proc/PID/cwd
}
type Session struct {
Process Process
SessionID string
GitBranch string
State SessionState
Preview string // dernieres lignes de sortie
CwdPath string // cwd du processus
Worktree string // git worktree (peut differer du cwd)
}
```
Le `SessionState` a 4 valeurs : `Working`, `NeedsInput`, `Idle`, `Unknown`.
`Unknown` est utilise quand aucun JSONL n'est trouve pour un processus (per D-04, pitfall 4 de la recherche).
</action>
<verify>
<automated>cd /home/pierre/Code/vibe/vmux && nix-shell --run "go build ./..."</automated>
</verify>
<acceptance_criteria>
- shell.nix contient `go` et `gopls` dans buildInputs
- go.mod contient `module github.com/pieMusic/vmux`
- types.go contient `type SessionState int`
- types.go contient `Working SessionState = iota`
- types.go contient `NeedsInput`
- types.go contient `Idle`
- types.go contient `Unknown`
- types.go contient `type Process struct`
- types.go contient `type Session struct`
- `go build ./...` compile sans erreur
</acceptance_criteria>
<done>Le projet compile. Les types Process, Session et SessionState sont definis et exportes.</done>
</task>
<task type="auto" tdd="true">
<name>Task 2: Scan /proc + encoding chemin + tests</name>
<files>proc.go, proc_test.go</files>
<read_first>
types.go
.planning/phases/01-session-discovery/01-RESEARCH.md
</read_first>
<behavior>
- TestFindClaudeProcesses: avec un faux /proc contenant 3 repertoires (2 processus "claude", 1 processus "bash"), retourne exactement 2 Process avec les bons PID, Cmd et Cwd.
- TestFindClaudeProcesses_PermissionDenied: un /proc/PID/cmdline illisible (permission denied) est ignore silencieusement, pas de crash.
- TestFindClaudeProcesses_EmptyProc: un /proc vide retourne un slice vide sans erreur.
- TestEncodePath: "/home/pierre/Code/vibe/vmux" -> "-home-pierre-Code-vibe-vmux"
- TestEncodePath_DotDir: "/home/pierre/.config/app" -> "-home-pierre--config-app" (les `.` sont remplaces par `-`, d'ou le double dash)
</behavior>
<action>
**proc.go** (package main) :
`FindClaudeProcesses(procDir string) ([]Process, error)` :
- Accepte le chemin vers /proc en parametre (permet le test avec un faux /proc). En production, passer "/proc".
- Lire `os.ReadDir(procDir)`, filtrer les entrees dont le nom est numerique (PID).
- Pour chaque PID, lire `procDir/PID/cmdline` via `os.ReadFile`. Le contenu est separe par des null bytes (`\x00`). Splitter avec `strings.Split(strings.TrimRight(data, "\x00"), "\x00")`.
- Si le premier argument (index 0) se termine par `claude` ou est exactement `claude`, c'est un processus Claude Code (per D-01).
- Lire le cwd via `os.Readlink(procDir/PID/cwd)` (per D-01).
- Ignorer silencieusement les erreurs de lecture (permission denied, processus disparu) : `continue` dans la boucle.
- Retourner un `[]Process` avec PID (int parse du nom de dossier), Cmd ([]string des arguments) et Cwd (string du readlink).
`EncodePath(path string) string` :
- Remplacer tous les `/` par `-` et tous les `.` par `-` (per RESEARCH Pattern 2, pitfall 6).
- Utiliser `strings.NewReplacer("/", "-", ".", "-").Replace(path)`.
**proc_test.go** (package main) :
Utiliser des fixtures temporaires pour simuler /proc (per strategie TDD de la recherche) :
- `createFakeProc(t *testing.T, entries map[int]fakeProcEntry) string` : cree un repertoire temporaire avec la structure /proc simulee.
- `fakeProcEntry` : struct avec `cmdline string` (contenu brut avec \x00) et `cwd string` (chemin cible du symlink).
- Creer des symlinks pour `cwd` (`os.Symlink`).
- Creer des fichiers pour `cmdline` (`os.WriteFile`).
5 tests decrits dans la section behavior.
</action>
<verify>
<automated>cd /home/pierre/Code/vibe/vmux && nix-shell --run "go test -v -run 'TestFindClaudeProcesses|TestEncodePath' ./..."</automated>
</verify>
<acceptance_criteria>
- proc.go contient `func FindClaudeProcesses(procDir string) ([]Process, error)`
- proc.go contient `func EncodePath(path string) string`
- proc_test.go contient `TestFindClaudeProcesses` qui cree un faux /proc
- proc_test.go contient `TestEncodePath`
- proc_test.go contient `TestEncodePath_DotDir`
- Tous les tests passent avec `go test -v -run 'TestFindClaudeProcesses|TestEncodePath' ./...`
- FindClaudeProcesses accepte un parametre procDir (pas de /proc en dur)
</acceptance_criteria>
<done>FindClaudeProcesses detecte les processus Claude dans un faux /proc. EncodePath convertit correctement les chemins. Tous les tests passent.</done>
</task>
</tasks>
<verification>
- `go build ./...` compile sans erreur
- `go test -v ./...` : tous les tests passent
- `go vet ./...` : pas de warning
</verification>
<success_criteria>
- Le projet Go est initialise avec shell.nix et go.mod
- Les types Process, Session, SessionState sont definis
- FindClaudeProcesses retourne les processus Claude depuis /proc
- EncodePath encode les chemins en noms de dossiers ~/.claude/projects/
- Tests unitaires couvrent les cas normaux et les cas limites
</success_criteria>
<output>
After completion, create `.planning/phases/01-session-discovery/01-01-SUMMARY.md`
</output>

View File

@@ -0,0 +1,423 @@
---
phase: 01-session-discovery
plan: 02
type: execute
wave: 2
depends_on:
- 01-01
files_modified:
- session.go
- session_test.go
- state.go
- state_test.go
- display.go
- display_test.go
- main.go
autonomous: true
requirements:
- DISC-03
- STATE-01
- STATE-02
must_haves:
truths:
- "vmux affiche la branche git de chaque session"
- "vmux detecte si une session travaille, attend input ou est idle"
- "vmux affiche un apercu des dernieres lignes de sortie"
- "vmux list affiche toutes les sessions avec leur etat, cwd, branche et apercu"
artifacts:
- path: "session.go"
provides: "Matching PID->JSONL, lecture metadonnees JSONL"
exports: ["FindSessionForProcess", "TailReadJSONL"]
- path: "state.go"
provides: "Heuristique d'etat basee sur le dernier message JSONL"
exports: ["DetectState", "ExtractPreview"]
- path: "display.go"
provides: "Formatage et affichage CLI avec couleurs"
exports: ["DisplaySessions"]
- path: "main.go"
provides: "Point d'entree CLI vmux list"
contains: "func main"
key_links:
- from: "session.go"
to: "~/.claude/projects/<encoded-cwd>/*.jsonl"
via: "EncodePath + filepath.Glob"
pattern: "EncodePath.*Glob"
- from: "state.go"
to: "session.go"
via: "TailReadJSONL fournit les messages au DetectState"
pattern: "DetectState.*JSONLMessage"
- from: "main.go"
to: "proc.go + session.go + state.go + display.go"
via: "Pipeline FindClaudeProcesses -> FindSessionForProcess -> DetectState -> DisplaySessions"
pattern: "FindClaudeProcesses.*FindSession.*DetectState.*Display"
---
<objective>
Parsing JSONL, heuristique d'etat et CLI `vmux list` fonctionnel.
Purpose: Completer le pipeline de detection : pour chaque processus Claude trouve en Plan 01, trouver le JSONL correspondant, determiner l'etat, extraire un apercu, et afficher le tout.
Output: `vmux list` fonctionnel qui affiche toutes les sessions Claude Code avec etat, cwd, branche git et apercu.
</objective>
<execution_context>
@$HOME/.claude/get-shit-done/workflows/execute-plan.md
@$HOME/.claude/get-shit-done/templates/summary.md
</execution_context>
<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md
@.planning/phases/01-session-discovery/01-CONTEXT.md
@.planning/phases/01-session-discovery/01-RESEARCH.md
@.planning/phases/01-session-discovery/01-01-SUMMARY.md
<interfaces>
<!-- Types et fonctions exportees par Plan 01. L'executeur doit les utiliser directement. -->
From types.go:
```go
package main
type SessionState int
const (
Working SessionState = iota
NeedsInput
Idle
Unknown
)
func (s SessionState) String() string
type Process struct {
PID int
Cmd []string
Cwd string
}
type Session struct {
Process Process
SessionID string
GitBranch string
State SessionState
Preview string
CwdPath string
Worktree string
}
```
From proc.go:
```go
func FindClaudeProcesses(procDir string) ([]Process, error)
func EncodePath(path string) string
```
</interfaces>
</context>
<tasks>
<task type="auto" tdd="true">
<name>Task 1: Matching PID->JSONL + tail-read + heuristique d'etat</name>
<files>session.go, session_test.go, state.go, state_test.go</files>
<read_first>
types.go
proc.go
.planning/phases/01-session-discovery/01-RESEARCH.md
</read_first>
<behavior>
- TestTailReadJSONL: fichier JSONL de 50 lignes, lire les 3 dernieres -> retourne exactement 3 messages JSON valides.
- TestTailReadJSONL_TruncatedLastLine: JSONL dont la derniere ligne est incomplete (pas de \n final) -> ignore la ligne tronquee, retourne les lignes completes precedentes.
- TestTailReadJSONL_EmptyFile: fichier vide -> retourne slice vide sans erreur.
- TestTailReadJSONL_SmallFile: fichier de 2 lignes, demander 10 -> retourne les 2 lignes.
- TestFindSessionForProcess: dossier ~/.claude/projects/ simule avec 2 fichiers JSONL, retourne celui avec le mtime le plus recent.
- TestFindSessionForProcess_NoMatch: aucun dossier correspondant -> retourne erreur.
- TestDetectState_EndTurnText: message assistant avec stop_reason="end_turn" et content type "text" -> NeedsInput.
- TestDetectState_ToolUseAskUser: message assistant avec stop_reason="tool_use" et tool name "AskUserQuestion" -> NeedsInput.
- TestDetectState_ToolUseOther: message assistant avec stop_reason="tool_use" et tool name "Read" -> Working.
- TestDetectState_Progress: message type="progress" avec data.type="agent_progress" -> Working.
- TestDetectState_ToolResult: message type="user" avec content type "tool_result" -> Working.
- TestDetectState_IdleThreshold: message timestamp > 60s dans le passe -> Idle.
- TestExtractPreview: message assistant avec content text "Voici le resultat..." -> retourne les 3 premieres lignes du texte.
</behavior>
<action>
**session.go** (package main) :
Structure intermediaire pour le parsing JSONL :
```go
type JSONLMessage struct {
Type string `json:"type"` // "assistant", "user", "progress", "system"
Timestamp string `json:"timestamp"` // ISO 8601
SessionID string `json:"sessionId"`
Cwd string `json:"cwd"`
GitBranch string `json:"gitBranch"`
Message *MessagePayload `json:"message,omitempty"`
Data *ProgressData `json:"data,omitempty"`
}
type MessagePayload struct {
Role string `json:"role"`
Content []ContentBlock `json:"content"`
StopReason string `json:"stop_reason"`
}
type ContentBlock struct {
Type string `json:"type"` // "text", "tool_use", "tool_result"
Text string `json:"text,omitempty"`
Name string `json:"name,omitempty"` // pour tool_use : nom de l'outil
}
type ProgressData struct {
Type string `json:"type"` // "agent_progress", "hook_progress"
}
```
`TailReadJSONL(path string, n int) ([]JSONLMessage, error)` :
- Ouvrir le fichier, `Seek(0, io.SeekEnd)` pour obtenir la taille.
- Lire des blocs de 8192 octets en reculant depuis la fin.
- Accumuler les lignes completes (terminees par `\n`). Ignorer la derniere ligne si elle ne se termine pas par `\n` (per pitfall 3 : race condition).
- Arreter quand on a `n` lignes completes ou qu'on atteint le debut du fichier.
- Parser chaque ligne en `JSONLMessage` via `json.Unmarshal`. Ignorer les lignes qui ne parsent pas (lignes vides, corruptions).
- Retourner les messages dans l'ordre chronologique (le plus ancien en premier).
`FindSessionForProcess(claudeDir string, proc Process) (string, []JSONLMessage, error)` :
- `claudeDir` = chemin vers `~/.claude/projects/` (parametre pour testabilite). En production : `os.Getenv("HOME") + "/.claude/projects/"`.
- Encoder `proc.Cwd` via `EncodePath(proc.Cwd)` pour obtenir le nom du dossier.
- Lister les `*.jsonl` dans `claudeDir/<encoded>/` (PAS les sous-dossiers, per pitfall 5 : exclure subagents/).
- Utiliser `filepath.Glob(filepath.Join(claudeDir, encoded, "*.jsonl"))` puis filtrer ceux qui sont dans un sous-dossier.
- Trier par mtime (le plus recent en premier). Prendre le premier = session active (per RESEARCH Pattern 5).
- Appeler `TailReadJSONL(path, 5)` sur le JSONL trouve.
- Extraire `sessionId` et `gitBranch` depuis la premiere entree retournee.
- Retourner le chemin JSONL, les messages, et nil. Si aucun JSONL trouve, retourner une erreur.
**state.go** (package main) :
`DetectState(messages []JSONLMessage, now time.Time) SessionState` :
- Si `messages` est vide, retourner `Unknown`.
- Prendre le dernier message (index len-1).
- Si le timestamp du dernier message est > 60 secondes avant `now` -> `Idle` (const `IdleThreshold = 60 * time.Second`).
- Si `type == "assistant"` et `message.stop_reason == "end_turn"` -> `NeedsInput` (per D-05 : Claude a fini, attend l'utilisateur).
- Si `type == "assistant"` et `message.stop_reason == "tool_use"` :
- Scanner `message.content` pour un `ContentBlock` de type `"tool_use"`.
- Si `name == "AskUserQuestion"` -> `NeedsInput` (per D-05).
- Sinon -> `Working` (outil en cours d'execution, per D-05).
- Si `type == "progress"` -> `Working` (per D-05 : hook ou subagent en cours).
- Si `type == "user"` et un ContentBlock a `type == "tool_result"` -> `Working` (resultat envoye, Claude va repondre).
- Sinon -> `Unknown`.
Le parametre `now time.Time` permet de tester le seuil Idle sans dependre de l'horloge.
`ExtractPreview(messages []JSONLMessage) string` :
- Parcourir les messages en ordre inverse pour trouver le dernier `type == "assistant"` avec un `ContentBlock` de type `"text"`.
- Extraire le champ `text` de ce ContentBlock.
- Tronquer a 3 lignes (split sur `\n`, prendre les 3 premieres, rejoin).
- Si le texte depasse 200 caracteres, tronquer a 200 et ajouter "...".
- Si aucun texte trouve, retourner "" (vide).
**session_test.go** et **state_test.go** :
Utiliser des fixtures JSONL dans des fichiers temporaires. Pour chaque test :
- Creer un `t.TempDir()`.
- Ecrire des lignes JSON valides representant les cas decrits dans behavior.
- Les fixtures JSONL doivent reproduire le format exact documente dans RESEARCH (voir section "Code Examples").
Exemple de fixture pour un message assistant end_turn :
```json
{"type":"assistant","timestamp":"2026-03-23T12:00:00Z","sessionId":"abc","cwd":"/tmp","gitBranch":"main","message":{"role":"assistant","content":[{"type":"text","text":"Done."}],"stop_reason":"end_turn"}}
```
</action>
<verify>
<automated>cd /home/pierre/Code/vibe/vmux && nix-shell --run "go test -v -run 'TestTailRead|TestFindSession|TestDetectState|TestExtractPreview' ./..."</automated>
</verify>
<acceptance_criteria>
- session.go contient `func TailReadJSONL(path string, n int) ([]JSONLMessage, error)`
- session.go contient `func FindSessionForProcess(claudeDir string, proc Process) (string, []JSONLMessage, error)`
- state.go contient `func DetectState(messages []JSONLMessage, now time.Time) SessionState`
- state.go contient `func ExtractPreview(messages []JSONLMessage) string`
- state.go contient `IdleThreshold` comme constante a 60 secondes
- session_test.go contient `TestTailReadJSONL` et `TestFindSessionForProcess`
- state_test.go contient `TestDetectState_EndTurnText`, `TestDetectState_ToolUseAskUser`, `TestDetectState_ToolUseOther`, `TestDetectState_IdleThreshold`
- state_test.go contient `TestExtractPreview`
- Tous les tests passent
</acceptance_criteria>
<done>TailReadJSONL lit les N derniers messages sans charger le fichier entier. FindSessionForProcess fait le matching PID->JSONL. DetectState applique l'heuristique d'etat. ExtractPreview extrait un apercu. Tous les tests passent.</done>
</task>
<task type="auto">
<name>Task 2: Affichage CLI + main.go + test d'integration</name>
<files>display.go, display_test.go, main.go</files>
<read_first>
types.go
proc.go
session.go
state.go
</read_first>
<action>
**display.go** (package main) :
Constantes ANSI (per D-07, pas de lib externe) :
```go
const (
colorReset = "\033[0m"
colorGreen = "\033[32m" // Working
colorYellow = "\033[33m" // Needs Input
colorRed = "\033[31m" // Needs Input (AskUserQuestion)
colorGray = "\033[90m" // Idle / Unknown
colorBold = "\033[1m"
)
```
`DisplaySessions(w io.Writer, sessions []Session, noColor bool)` :
- Si `sessions` est vide, ecrire "No active Claude Code sessions found.\n" et retourner.
- Pour chaque session, afficher un bloc :
```
[STATE] cwd (branch)
Preview text here...
```
- Le `[STATE]` est colore selon l'etat (sauf si `noColor == true`) :
- Working -> colorGreen + "Working"
- NeedsInput -> colorYellow + "Needs Input"
- Idle -> colorGray + "Idle"
- Unknown -> colorGray + "Unknown"
- Le `cwd` est le `Session.CwdPath`. Afficher aussi le `Worktree` si different du cwd.
- Le `(branch)` est `Session.GitBranch`. Si vide, omettre les parentheses.
- Le `Preview` est indente de 9 espaces (alignement avec le texte apres [STATE]). Chaque ligne du preview est prefixee de ces espaces.
- Separer les sessions par une ligne vide.
- Ecrire dans `w` (io.Writer) pour la testabilite. En production : `os.Stdout`.
`stateColor(state SessionState, noColor bool) string` : helper qui retourne le code ANSI pour un etat, ou "" si noColor.
**display_test.go** (package main) :
- `TestDisplaySessions_WithSessions` : 2 sessions (Working + NeedsInput), verifier que la sortie contient les cwds, branches et etats.
- `TestDisplaySessions_NoColor` : meme test avec noColor=true, verifier qu'aucun code ANSI n'est present (pas de `\033`).
- `TestDisplaySessions_Empty` : sessions vide -> "No active Claude Code sessions found."
Utiliser un `bytes.Buffer` comme io.Writer pour capturer la sortie.
**main.go** (package main) :
```go
func main() {
noColor := flag.Bool("no-color", false, "Disable colored output")
flag.Parse()
// Aussi detecter NO_COLOR env var (convention standard)
if os.Getenv("NO_COLOR") != "" {
*noColor = true
}
args := flag.Args()
if len(args) == 0 || args[0] != "list" {
fmt.Fprintf(os.Stderr, "Usage: vmux list [--no-color]\n")
os.Exit(1)
}
processes, err := FindClaudeProcesses("/proc")
if err != nil {
fmt.Fprintf(os.Stderr, "Error scanning processes: %v\n", err)
os.Exit(1)
}
claudeDir := filepath.Join(os.Getenv("HOME"), ".claude", "projects")
now := time.Now()
var sessions []Session
for _, proc := range processes {
jsonlPath, messages, err := FindSessionForProcess(claudeDir, proc)
if err != nil {
// Processus sans JSONL -> afficher avec etat Unknown (per pitfall 4)
sessions = append(sessions, Session{
Process: proc,
State: Unknown,
CwdPath: proc.Cwd,
})
continue
}
state := DetectState(messages, now)
preview := ExtractPreview(messages)
// Extraire metadonnees du JSONL
var sessionID, gitBranch, worktree string
for _, msg := range messages {
if msg.SessionID != "" {
sessionID = msg.SessionID
}
if msg.GitBranch != "" {
gitBranch = msg.GitBranch
}
}
// Worktree = git rev-parse --show-toplevel depuis le cwd
worktree = resolveWorktree(proc.Cwd)
_ = jsonlPath // utilise pour le debug futur
sessions = append(sessions, Session{
Process: proc,
SessionID: sessionID,
GitBranch: gitBranch,
State: state,
Preview: preview,
CwdPath: proc.Cwd,
Worktree: worktree,
})
}
DisplaySessions(os.Stdout, sessions, *noColor)
}
// resolveWorktree utilise git pour trouver le worktree.
// Retourne le cwd si git echoue (pas un repo git).
func resolveWorktree(cwd string) string {
cmd := exec.Command("git", "-C", cwd, "rev-parse", "--show-toplevel")
out, err := cmd.Output()
if err != nil {
return cwd
}
return strings.TrimSpace(string(out))
}
```
Imports necessaires : `flag`, `fmt`, `os`, `os/exec`, `path/filepath`, `strings`, `time`.
</action>
<verify>
<automated>cd /home/pierre/Code/vibe/vmux && nix-shell --run "go test -v ./... && go build -o vmux ./..."</automated>
</verify>
<acceptance_criteria>
- display.go contient `func DisplaySessions(w io.Writer, sessions []Session, noColor bool)`
- display.go contient les constantes ANSI colorGreen, colorYellow, colorGray
- display_test.go contient `TestDisplaySessions_WithSessions`
- display_test.go contient `TestDisplaySessions_NoColor`
- display_test.go contient `TestDisplaySessions_Empty`
- main.go contient `func main()` avec `flag.Parse()` et sous-commande "list"
- main.go contient `FindClaudeProcesses("/proc")`
- main.go contient `resolveWorktree` qui appelle `git rev-parse --show-toplevel`
- main.go gere le flag `--no-color` et la variable `NO_COLOR`
- `go build -o vmux ./...` produit un binaire vmux
- `go test -v ./...` : tous les tests passent (proc, session, state, display)
</acceptance_criteria>
<done>vmux list fonctionne : detecte les processus Claude, lit les JSONL, determine l'etat, affiche le resultat avec couleurs. Le binaire compile. Tous les tests passent.</done>
</task>
</tasks>
<verification>
- `go test -v -race ./...` : tous les tests passent sans race condition
- `go build -o vmux ./...` : le binaire compile
- `./vmux list` : affiche les sessions Claude Code actives sur le poste (test manuel)
- `./vmux list --no-color` : meme sortie sans codes ANSI
</verification>
<success_criteria>
- `vmux list` affiche toutes les sessions Claude Code actives (DISC-01)
- Chaque session affiche son cwd et worktree git (DISC-02)
- Chaque session affiche sa branche git (DISC-03)
- Chaque session affiche son etat Working/Needs Input/Idle (STATE-01)
- Chaque session affiche un apercu de la derniere sortie (STATE-02)
- Couleurs actives par defaut, desactivables avec --no-color (D-07)
</success_criteria>
<output>
After completion, create `.planning/phases/01-session-discovery/01-02-SUMMARY.md`
</output>