17 KiB
phase, plan, type, wave, depends_on, files_modified, autonomous, requirements, must_haves
| phase | plan | type | wave | depends_on | files_modified | autonomous | requirements | must_haves | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 01-session-discovery | 02 | execute | 2 |
|
|
true |
|
|
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.
<execution_context> @$HOME/.claude/get-shit-done/workflows/execute-plan.md @$HOME/.claude/get-shit-done/templates/summary.md </execution_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.mdFrom types.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:
func FindClaudeProcesses(procDir string) ([]Process, error)
func EncodePath(path string) string
Structure intermediaire pour le parsing JSONL :
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
nlignes completes ou qu'on atteint le debut du fichier. - Parser chaque ligne en
JSONLMessageviajson.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.CwdviaEncodePath(proc.Cwd)pour obtenir le nom du dossier. - Lister les
*.jsonldansclaudeDir/<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
sessionIdetgitBranchdepuis 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
messagesest vide, retournerUnknown. - Prendre le dernier message (index len-1).
- Si le timestamp du dernier message est > 60 secondes avant
now->Idle(constIdleThreshold = 60 * time.Second). - Si
type == "assistant"etmessage.stop_reason == "end_turn"->NeedsInput(per D-05 : Claude a fini, attend l'utilisateur). - Si
type == "assistant"etmessage.stop_reason == "tool_use":- Scanner
message.contentpour unContentBlockde type"tool_use". - Si
name == "AskUserQuestion"->NeedsInput(per D-05). - Sinon ->
Working(outil en cours d'execution, per D-05).
- Scanner
- Si
type == "progress"->Working(per D-05 : hook ou subagent en cours). - Si
type == "user"et un ContentBlock atype == "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 unContentBlockde type"text". - Extraire le champ
textde 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 :
{"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"}}
Constantes ANSI (per D-07, pas de lib externe) :
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
sessionsest 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 sinoColor == true) :- Working -> colorGreen + "Working"
- NeedsInput -> colorYellow + "Needs Input"
- Idle -> colorGray + "Idle"
- Unknown -> colorGray + "Unknown"
- Le
cwdest leSession.CwdPath. Afficher aussi leWorktreesi different du cwd. - Le
(branch)estSession.GitBranch. Si vide, omettre les parentheses. - Le
Previewest 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) :
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.
cd /home/pierre/Code/vibe/vmux && nix-shell --run "go test -v ./... && go build -o vmux ./..."
<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>
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.
<success_criteria>
vmux listaffiche 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>