docs(02): phase 2 plans
This commit is contained in:
269
.planning/phases/02-daemon-et-i3-bridge/02-01-PLAN.md
Normal file
269
.planning/phases/02-daemon-et-i3-bridge/02-01-PLAN.md
Normal file
@@ -0,0 +1,269 @@
|
||||
---
|
||||
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>
|
||||
Reference in New Issue
Block a user