--- 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 After completion, create `.planning/phases/02-daemon-et-i3-bridge/02-01-SUMMARY.md`