---
phase: 02-daemon-et-i3-bridge
plan: 01
type: execute
wave: 1
depends_on: []
files_modified:
- types.go
- protocol.go
- protocol_test.go
- daemon.go
- daemon_test.go
autonomous: true
requirements: [DISC-04, STATE-04]
must_haves:
truths:
- "Le daemon maintient un registre des sessions a jour (poll toutes les 5s)"
- "Le daemon ecoute sur un Unix socket et repond aux requetes JSON"
- "Les labels sont persistes dans ~/.vmux/labels.json"
- "Le temps d'attente est tracke quand une session passe a NeedsInput"
artifacts:
- path: "protocol.go"
provides: "Types Request, Response, SessionInfo pour le protocole socket"
exports: ["Request", "Response", "SessionInfo"]
- path: "daemon.go"
provides: "SessionRegistry, LabelStore, boucle de poll, Unix socket server"
exports: ["SessionRegistry", "LabelStore", "StartDaemon"]
- path: "types.go"
provides: "Session enrichi avec Workspace, Label, WaitingSince"
contains: "Workspace string"
key_links:
- from: "daemon.go"
to: "proc.go"
via: "FindClaudeProcesses dans la boucle de poll"
pattern: "FindClaudeProcesses"
- from: "daemon.go"
to: "protocol.go"
via: "Request/Response JSON sur le socket"
pattern: "Request|Response"
---
Daemon vmuxd : registre des sessions, Unix socket server, labels persistants et tracking du temps d'attente.
Purpose: Transformer vmux d'un CLI one-shot en un daemon persistant qui maintient l'etat des sessions.
Output: protocol.go (types IPC), daemon.go (registre + socket + poll + labels), types.go enrichi.
@$HOME/.claude/get-shit-done/workflows/execute-plan.md
@$HOME/.claude/get-shit-done/templates/summary.md
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md
@.planning/phases/02-daemon-et-i3-bridge/02-CONTEXT.md
@.planning/phases/02-daemon-et-i3-bridge/02-RESEARCH.md
@.planning/phases/01-session-discovery/01-02-SUMMARY.md
From types.go:
```go
type SessionState int
const (Working SessionState = iota; NeedsInput; Idle; Unknown)
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
```
From session.go:
```go
func FindSessionForProcess(claudeDir string, proc Process) (string, []JSONLMessage, error)
func TailReadJSONL(path string, n int) ([]JSONLMessage, error)
```
From state.go:
```go
func DetectState(messages []JSONLMessage, now time.Time) SessionState
func ExtractPreview(messages []JSONLMessage) string
```
Task 1: Protocol types + SessionRegistry + LabelStore
protocol.go, protocol_test.go, daemon.go, daemon_test.go, types.go
types.go, proc.go, state.go, session.go
- TestRequestMarshal: Request{Action:"list"} se serialise/deserialise correctement en JSON
- TestResponseWithSessions: Response avec SessionInfo contenant Workspace, Label, WaitingSince
- TestRegistryUpdate: Ajouter une session, la retrouver dans List()
- TestRegistryWaitingSince: Session passant de Working a NeedsInput enregistre WaitingSince; re-passer a Working la reset
- TestRegistryRemoveStale: Sessions absentes du scan sont supprimees du registre
- TestLabelStoreSetGet: Set("session-id", "review MR") puis Get("session-id") retourne "review MR"
- TestLabelStorePersistence: Set() ecrit sur disque, nouveau LabelStore charge le fichier et retrouve le label
- TestLabelStoreLoadMissing: Charger un fichier inexistant retourne un store vide sans erreur
1. Enrichir Session dans types.go (per D-08, D-09):
- Ajouter `Workspace string`
- Ajouter `Label string`
- Ajouter `WaitingSince *time.Time`
2. Creer protocol.go avec les types IPC (per D-01):
```go
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"`
}
type SwitchArgs struct { Query string `json:"query"` }
type LabelArgs struct { SessionID string `json:"session_id"`; Label string `json:"label"` }
```
3. Creer daemon.go avec SessionRegistry (per D-02, D-09):
```go
type TrackedSession struct {
Info SessionInfo
PrevState string
WaitingSince time.Time
}
type SessionRegistry struct {
mu sync.RWMutex
sessions map[string]*TrackedSession
}
func NewRegistry() *SessionRegistry
func (r *SessionRegistry) Update(info SessionInfo) // track WaitingSince transitions
func (r *SessionRegistry) List() []SessionInfo
func (r *SessionRegistry) RemoveStale(activeIDs map[string]bool)
```
4. Creer LabelStore dans daemon.go (per D-08):
```go
type LabelStore struct {
mu sync.RWMutex
labels map[string]string
path string
}
func NewLabelStore(path string) (*LabelStore, error)
func (ls *LabelStore) Set(sessionID, label string) error
func (ls *LabelStore) Get(sessionID string) string
```
Persistence dans le fichier JSON. Load au demarrage, save apres chaque Set.
nix-shell --run "go test -run 'TestRequest|TestResponse|TestRegistry|TestLabelStore' -v ./..."
- grep -q 'Workspace string' types.go
- grep -q 'WaitingSince' types.go
- grep -q 'type Request struct' protocol.go
- grep -q 'type Response struct' protocol.go
- grep -q 'type SessionRegistry struct' daemon.go
- grep -q 'type LabelStore struct' daemon.go
- grep -q 'func.*LabelStore.*Set' daemon.go
- grep -q 'TestRegistryWaitingSince' daemon_test.go
- grep -q 'TestLabelStorePersistence' daemon_test.go
Protocol types compiles, Registry tracke les transitions WaitingSince, LabelStore persiste sur disque, tous les tests passent.
Task 2: Unix socket server + poll loop + stop handler
daemon.go, daemon_test.go
daemon.go, protocol.go, proc.go, session.go, state.go
1. Ajouter la boucle de poll dans daemon.go (per D-02):
```go
func (d *Daemon) scanOnce(procDir, claudeDir string, now time.Time) {
// FindClaudeProcesses -> FindSessionForProcess -> DetectState -> registry.Update
// resolveWorkspace via d.workspaceResolver (interface, nil-safe pour tests)
}
```
Le daemon fait un scan synchrone AVANT d'ecouter sur le socket (per Pitfall 3).
Ticker de 5 secondes pour les scans suivants.
2. Ajouter le socket server (per D-01):
```go
type Daemon struct {
registry *SessionRegistry
labels *LabelStore
sockPath string
procDir string
claudeDir string
workspaceResolver func(claudePID int) string // nil = pas de workspace
stopCh chan struct{}
}
func NewDaemon(sockPath, procDir, claudeDir string, labels *LabelStore) *Daemon
func (d *Daemon) Start() error // scan initial, listen, poll loop
func (d *Daemon) handleConnection(conn net.Conn) // dispatch Request.Action
```
Gestion du stale socket (per Pitfall 2): tenter net.Dial avant net.Listen, supprimer si stale.
PID file dans ~/.vmux/vmuxd.pid.
3. Handler pour chaque action:
- "list": registry.List() avec labels enrichis
- "label": labels.Set(args.SessionID, args.Label)
- "stop": fermer le listener, signal stopCh
4. Tests:
- TestDaemonStartStop: demarrer daemon dans goroutine avec tmpdir socket, envoyer "stop", verifier arret propre
- TestDaemonListOverSocket: demarrer daemon, envoyer "list", verifier la reponse JSON
- TestDaemonLabelOverSocket: envoyer "label", puis "list", verifier que le label apparait
Note: Le handler "switch" sera ajoute dans le plan 02-03 (depend de i3bridge du plan 02-02).
Le workspaceResolver est nil dans les tests de ce plan. Le plan 02-02 fournira l'implementation reelle.
nix-shell --run "go test -run 'TestDaemon' -v -race ./..."
- grep -q 'type Daemon struct' daemon.go
- grep -q 'func.*Daemon.*Start' daemon.go
- grep -q 'func.*Daemon.*handleConnection' daemon.go
- grep -q 'net.Listen.*unix' daemon.go
- grep -q 'TestDaemonStartStop' daemon_test.go
- grep -q 'TestDaemonListOverSocket' daemon_test.go
Le daemon demarre, ecoute sur un Unix socket, repond a list/label/stop, fait un scan initial synchrone et poll toutes les 5s. Tests avec -race passent.
nix-shell --run "go test -v -race ./..."
grep -q 'type Daemon struct' daemon.go
grep -q 'type LabelStore struct' daemon.go
grep -q 'type Request struct' protocol.go
- Protocol types (Request, Response, SessionInfo) definis et testes
- SessionRegistry tracke les transitions d'etat avec WaitingSince (STATE-04)
- LabelStore persiste les labels dans ~/.vmux/labels.json (DISC-04)
- Daemon ecoute sur Unix socket, repond aux requetes list/label/stop (D-01)
- Boucle de poll toutes les 5s avec scan initial synchrone (D-02)
- Tous les tests passent avec -race