270 lines
10 KiB
Markdown
270 lines
10 KiB
Markdown
---
|
|
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"
|
|
---
|
|
|
|
<objective>
|
|
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.
|
|
</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/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
|
|
|
|
<interfaces>
|
|
<!-- Types Phase 1 que le daemon reutilise -->
|
|
|
|
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
|
|
```
|
|
</interfaces>
|
|
</context>
|
|
|
|
<tasks>
|
|
|
|
<task type="auto" tdd="true">
|
|
<name>Task 1: Protocol types + SessionRegistry + LabelStore</name>
|
|
<files>protocol.go, protocol_test.go, daemon.go, daemon_test.go, types.go</files>
|
|
<read_first>types.go, proc.go, state.go, session.go</read_first>
|
|
<behavior>
|
|
- 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
|
|
</behavior>
|
|
<action>
|
|
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.
|
|
</action>
|
|
<verify>
|
|
<automated>nix-shell --run "go test -run 'TestRequest|TestResponse|TestRegistry|TestLabelStore' -v ./..."</automated>
|
|
</verify>
|
|
<acceptance_criteria>
|
|
- 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
|
|
</acceptance_criteria>
|
|
<done>Protocol types compiles, Registry tracke les transitions WaitingSince, LabelStore persiste sur disque, tous les tests passent.</done>
|
|
</task>
|
|
|
|
<task type="auto">
|
|
<name>Task 2: Unix socket server + poll loop + stop handler</name>
|
|
<files>daemon.go, daemon_test.go</files>
|
|
<read_first>daemon.go, protocol.go, proc.go, session.go, state.go</read_first>
|
|
<action>
|
|
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.
|
|
</action>
|
|
<verify>
|
|
<automated>nix-shell --run "go test -run 'TestDaemon' -v -race ./..."</automated>
|
|
</verify>
|
|
<acceptance_criteria>
|
|
- 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
|
|
</acceptance_criteria>
|
|
<done>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.</done>
|
|
</task>
|
|
|
|
</tasks>
|
|
|
|
<verification>
|
|
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
|
|
</verification>
|
|
|
|
<success_criteria>
|
|
- 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
|
|
</success_criteria>
|
|
|
|
<output>
|
|
After completion, create `.planning/phases/02-daemon-et-i3-bridge/02-01-SUMMARY.md`
|
|
</output>
|