docs(02): phase 2 plans
This commit is contained in:
265
.planning/phases/02-daemon-et-i3-bridge/02-03-PLAN.md
Normal file
265
.planning/phases/02-daemon-et-i3-bridge/02-03-PLAN.md
Normal file
@@ -0,0 +1,265 @@
|
||||
---
|
||||
phase: 02-daemon-et-i3-bridge
|
||||
plan: 03
|
||||
type: execute
|
||||
wave: 2
|
||||
depends_on: ["02-01", "02-02"]
|
||||
files_modified:
|
||||
- client.go
|
||||
- client_test.go
|
||||
- main.go
|
||||
- display.go
|
||||
- display_test.go
|
||||
- daemon.go
|
||||
autonomous: false
|
||||
requirements: [DISC-04, I3-01, I3-02, STATE-04]
|
||||
|
||||
must_haves:
|
||||
truths:
|
||||
- "vmux list affiche workspace, label et temps d'attente pour chaque session"
|
||||
- "vmux switch <query> bascule vers le workspace i3 de la session matchee"
|
||||
- "vmux label <session> <texte> attribue un label humain"
|
||||
- "vmux stop arrete le daemon proprement"
|
||||
- "Le daemon demarre automatiquement si absent quand on lance une commande"
|
||||
artifacts:
|
||||
- path: "client.go"
|
||||
provides: "Client Unix socket CLI -> daemon"
|
||||
exports: ["Client", "EnsureDaemon"]
|
||||
- path: "main.go"
|
||||
provides: "Dispatch sous-commandes list/switch/label/stop/daemon"
|
||||
contains: "case \"switch\""
|
||||
- path: "display.go"
|
||||
provides: "Affichage enrichi avec workspace, label, temps d'attente"
|
||||
contains: "WaitingSince"
|
||||
key_links:
|
||||
- from: "client.go"
|
||||
to: "daemon.go"
|
||||
via: "Unix socket ~/.vmux/vmux.sock"
|
||||
pattern: "net.Dial.*unix"
|
||||
- from: "main.go"
|
||||
to: "client.go"
|
||||
via: "Dispatch sous-commandes vers Client"
|
||||
pattern: "client\\."
|
||||
- from: "daemon.go"
|
||||
to: "i3bridge.go"
|
||||
via: "Switch handler appelle FuzzyMatch + SwitchToWorkspace"
|
||||
pattern: "FuzzyMatch|SwitchToWorkspace"
|
||||
- from: "daemon.go"
|
||||
to: "workspace.go"
|
||||
via: "workspaceResolver dans la boucle de poll"
|
||||
pattern: "ResolveWorkspace|BuildTerminalWorkspaceMap"
|
||||
---
|
||||
|
||||
<objective>
|
||||
CLI complet : client socket, sous-commandes (list/switch/label/stop), autostart daemon, affichage enrichi.
|
||||
|
||||
Purpose: L'utilisateur peut interagir avec vmux via les sous-commandes. Tout est cable de bout en bout.
|
||||
Output: client.go (communication socket), main.go refactore, display.go enrichi, daemon.go complete avec switch + workspace.
|
||||
</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/phases/02-daemon-et-i3-bridge/02-CONTEXT.md
|
||||
@.planning/phases/02-daemon-et-i3-bridge/02-RESEARCH.md
|
||||
@.planning/phases/02-daemon-et-i3-bridge/02-01-SUMMARY.md
|
||||
@.planning/phases/02-daemon-et-i3-bridge/02-02-SUMMARY.md
|
||||
|
||||
<interfaces>
|
||||
<!-- A lire depuis les fichiers reels apres execution de 02-01 et 02-02 -->
|
||||
|
||||
From protocol.go (plan 02-01):
|
||||
```go
|
||||
type Request struct { Action string `json:"action"`; 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; SessionID string; Cwd string; GitBranch string; State string; Preview string; Workspace string; Label string; WaitingSince *time.Time }
|
||||
type SwitchArgs struct { Query string `json:"query"` }
|
||||
type LabelArgs struct { SessionID string `json:"session_id"`; Label string `json:"label"` }
|
||||
```
|
||||
|
||||
From daemon.go (plan 02-01):
|
||||
```go
|
||||
type Daemon struct { registry *SessionRegistry; labels *LabelStore; sockPath string; ... }
|
||||
func NewDaemon(sockPath, procDir, claudeDir string, labels *LabelStore) *Daemon
|
||||
func (d *Daemon) Start() error
|
||||
```
|
||||
|
||||
From i3bridge.go (plan 02-02):
|
||||
```go
|
||||
func FuzzyMatch(query string, sessions []SessionInfo) *SessionInfo
|
||||
func SwitchToWorkspace(commander I3Commander, wsName string) error
|
||||
type RealI3Commander struct{}
|
||||
```
|
||||
|
||||
From workspace.go (plan 02-02):
|
||||
```go
|
||||
func ResolveWorkspace(procDir string, claudePID int, terminalWorkspaces map[int]string) string
|
||||
func BuildTerminalWorkspaceMap(tree I3TreeProvider, x11 X11PIDResolver) (map[int]string, error)
|
||||
```
|
||||
</interfaces>
|
||||
</context>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 1: Client socket + autostart + daemon switch handler + workspace wiring</name>
|
||||
<files>client.go, client_test.go, daemon.go</files>
|
||||
<read_first>daemon.go, protocol.go, i3bridge.go, workspace.go</read_first>
|
||||
<action>
|
||||
1. Creer client.go (per D-01, D-03):
|
||||
```go
|
||||
type Client struct { sockPath string }
|
||||
func NewClient(sockPath string) *Client
|
||||
func (c *Client) Send(req Request) (*Response, error)
|
||||
// net.Dial("unix", sockPath), encoder req JSON, decoder response JSON
|
||||
```
|
||||
|
||||
```go
|
||||
func EnsureDaemon(sockPath string) error
|
||||
// Tenter net.DialTimeout. Si echec: lancer os.Executable() + "daemon" en background.
|
||||
// Retry 50ms x 20 (per RESEARCH Pattern 2). Detacher avec Setsid.
|
||||
```
|
||||
|
||||
2. Completer daemon.go : ajouter le handler "switch" (per D-07, I3-02):
|
||||
```go
|
||||
case "switch":
|
||||
var args SwitchArgs
|
||||
json.Unmarshal(req.Args, &args)
|
||||
sessions := r.registry.List()
|
||||
match := FuzzyMatch(args.Query, sessions)
|
||||
if match == nil { return Response{Error: "no session matching..."} }
|
||||
if match.Workspace == "" { return Response{Error: "no workspace for session..."} }
|
||||
err := SwitchToWorkspace(d.i3commander, match.Workspace)
|
||||
```
|
||||
|
||||
3. Cabler le workspace resolution dans la boucle de poll du daemon:
|
||||
- Au demarrage du daemon, initialiser BuildTerminalWorkspaceMap (i3 GetTree + X11).
|
||||
- Si i3/X11 indisponible: log warning, continuer sans workspace (per Pitfall 4, 5).
|
||||
- Dans scanOnce: appeler ResolveWorkspace(procDir, pid, terminalMap) pour chaque session.
|
||||
- Rafraichir la terminalMap a chaque scan (les fenetres peuvent bouger de workspace).
|
||||
|
||||
4. Tests:
|
||||
- TestClientSendReceive: Demarrer un daemon dans goroutine, creer Client, envoyer "list", verifier Response
|
||||
- TestEnsureDaemonAlreadyRunning: Socket actif -> EnsureDaemon retourne nil immediatement
|
||||
</action>
|
||||
<verify>
|
||||
<automated>nix-shell --run "go test -run 'TestClient|TestEnsureDaemon' -v -race ./..."</automated>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- grep -q 'type Client struct' client.go
|
||||
- grep -q 'func EnsureDaemon' client.go
|
||||
- grep -q 'case "switch"' daemon.go
|
||||
- grep -q 'FuzzyMatch' daemon.go
|
||||
- grep -q 'ResolveWorkspace\|BuildTerminalWorkspaceMap' daemon.go
|
||||
- grep -q 'TestClientSendReceive' client_test.go
|
||||
</acceptance_criteria>
|
||||
<done>Client envoie des requetes au daemon via socket, EnsureDaemon autostart le daemon, le handler switch utilise FuzzyMatch + SwitchToWorkspace, la boucle de poll resout les workspaces.</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 2: main.go sous-commandes + display enrichi</name>
|
||||
<files>main.go, display.go, display_test.go</files>
|
||||
<read_first>main.go, display.go, display_test.go, client.go, protocol.go</read_first>
|
||||
<action>
|
||||
1. Refactorer main.go pour supporter les sous-commandes (per D-03, D-07, D-08):
|
||||
```go
|
||||
func main() {
|
||||
// Parse --no-color global
|
||||
// Dispatch sur os.Args:
|
||||
// "list" -> EnsureDaemon + Client.Send(list) + DisplaySessionInfos
|
||||
// "switch" -> EnsureDaemon + Client.Send(switch, query)
|
||||
// "label" -> EnsureDaemon + Client.Send(label, session_id, texte)
|
||||
// "stop" -> Client.Send(stop)
|
||||
// "daemon" -> NewDaemon + Start (mode foreground, lance par autostart)
|
||||
// default -> usage
|
||||
}
|
||||
```
|
||||
sockPath = `~/.vmux/vmux.sock` (per D-01).
|
||||
Pour "label": args[2] = session_id, args[3:] = label text (join avec espace).
|
||||
Pour "switch": args[2] = query string.
|
||||
|
||||
2. Etendre display.go pour afficher les champs enrichis:
|
||||
```go
|
||||
// DisplaySessionInfos affiche les sessions recues du daemon (SessionInfo, pas Session).
|
||||
func DisplaySessionInfos(w io.Writer, sessions []SessionInfo, noColor bool, now time.Time)
|
||||
```
|
||||
Format par session:
|
||||
```
|
||||
[Needs Input] /home/pierre/Code/vibe/vmux (feat/auth) [ws:3] "review MR !456" (depuis 3 min)
|
||||
preview line 1
|
||||
preview line 2
|
||||
```
|
||||
- `[ws:N]` seulement si Workspace non vide (per D-05)
|
||||
- `"label"` entre guillemets seulement si Label non vide (per D-08)
|
||||
- `(depuis X min)` seulement si WaitingSince non nil (per D-09, STATE-04)
|
||||
- Calculer la duree relative : now.Sub(*WaitingSince). Afficher "< 1 min", "3 min", "1 h 5 min".
|
||||
|
||||
3. Mise a jour display_test.go:
|
||||
- TestDisplayWithWorkspace: session avec Workspace "3" -> contient "[ws:3]"
|
||||
- TestDisplayWithLabel: session avec Label "review MR" -> contient "\"review MR\""
|
||||
- TestDisplayWithWaitingSince: session avec WaitingSince 3 min ago -> contient "depuis 3 min"
|
||||
- TestDisplayWithoutOptionalFields: session sans workspace/label/waiting -> pas de [ws:], pas de guillemets, pas de "depuis"
|
||||
</action>
|
||||
<verify>
|
||||
<automated>nix-shell --run "go test -run 'TestDisplay' -v ./..." && nix-shell --run "go build -o /dev/null ./..."</automated>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- grep -q 'case "switch"' main.go
|
||||
- grep -q 'case "label"' main.go
|
||||
- grep -q 'case "stop"' main.go
|
||||
- grep -q 'case "daemon"' main.go
|
||||
- grep -q 'EnsureDaemon' main.go
|
||||
- grep -q 'func DisplaySessionInfos' display.go
|
||||
- grep -q 'WaitingSince' display.go
|
||||
- grep -q 'TestDisplayWithWorkspace' display_test.go
|
||||
- grep -q 'TestDisplayWithLabel' display_test.go
|
||||
- grep -q 'TestDisplayWithWaitingSince' display_test.go
|
||||
- nix-shell --run "go build -o /dev/null ./..." (compile sans erreur)
|
||||
</acceptance_criteria>
|
||||
<done>main.go dispatch list/switch/label/stop/daemon. DisplaySessionInfos affiche workspace, label et temps d'attente. Le binaire compile.</done>
|
||||
</task>
|
||||
|
||||
<task type="checkpoint:human-verify" gate="blocking">
|
||||
<name>Task 3: Verification manuelle du workflow complet</name>
|
||||
<what-built>
|
||||
Daemon vmuxd complet avec CLI multi-commandes : list (avec workspace, label, temps d'attente), switch (fuzzy match), label, stop, autostart.
|
||||
</what-built>
|
||||
<how-to-verify>
|
||||
1. Builder : `nix-shell --run "go build -o vmux ./..."`
|
||||
2. Lancer `./vmux list` (doit autostart le daemon et afficher les sessions avec workspaces)
|
||||
3. Verifier que chaque session affiche son workspace i3 ([ws:N])
|
||||
4. Attribuer un label : `./vmux label <session-id> "test label"`
|
||||
5. Relancer `./vmux list` et verifier que le label apparait
|
||||
6. Tester le switch : `./vmux switch <partie-du-nom>` (doit changer de workspace)
|
||||
7. Verifier que les sessions en attente affichent "depuis X min"
|
||||
8. Arreter : `./vmux stop`
|
||||
9. Verifier que le socket est supprime : `ls ~/.vmux/vmux.sock` (doit echouer)
|
||||
</how-to-verify>
|
||||
<resume-signal>Type "approved" ou decris les problemes constates</resume-signal>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<verification>
|
||||
nix-shell --run "go test -v -race ./..."
|
||||
nix-shell --run "go build -o vmux ./..."
|
||||
./vmux list
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- vmux list affiche workspace, label et temps d'attente (DISC-04, I3-01, STATE-04)
|
||||
- vmux switch bascule vers le bon workspace i3 (I3-02)
|
||||
- vmux label persiste le label (DISC-04)
|
||||
- vmux stop arrete le daemon (D-03)
|
||||
- Autostart fonctionne (D-03)
|
||||
- Le binaire compile et tous les tests passent
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
After completion, create `.planning/phases/02-daemon-et-i3-bridge/02-03-SUMMARY.md`
|
||||
</output>
|
||||
Reference in New Issue
Block a user