docs(02): phase 2 plans
This commit is contained in:
242
.planning/phases/02-daemon-et-i3-bridge/02-02-PLAN.md
Normal file
242
.planning/phases/02-daemon-et-i3-bridge/02-02-PLAN.md
Normal file
@@ -0,0 +1,242 @@
|
||||
---
|
||||
phase: 02-daemon-et-i3-bridge
|
||||
plan: 02
|
||||
type: execute
|
||||
wave: 1
|
||||
depends_on: []
|
||||
files_modified:
|
||||
- workspace.go
|
||||
- workspace_test.go
|
||||
- i3bridge.go
|
||||
- i3bridge_test.go
|
||||
- go.mod
|
||||
- go.sum
|
||||
autonomous: true
|
||||
requirements: [I3-01, I3-02]
|
||||
|
||||
must_haves:
|
||||
truths:
|
||||
- "Un PID Claude Code est resolu vers son workspace i3 via la chaine PPID"
|
||||
- "Le fuzzy match trouve une session par label, branche ou cwd"
|
||||
- "Le switch vers un workspace i3 fonctionne via i3 IPC RunCommand"
|
||||
- "Si i3 ou X11 est absent, le mapping retourne vide sans erreur"
|
||||
artifacts:
|
||||
- path: "workspace.go"
|
||||
provides: "Resolution PID -> workspace via PPID chain + X11 _NET_WM_PID"
|
||||
exports: ["ReadPPID", "ResolveWorkspace", "BuildTerminalWorkspaceMap"]
|
||||
- path: "i3bridge.go"
|
||||
provides: "Interface I3Client, fuzzy match, switch workspace"
|
||||
exports: ["I3Client", "FuzzyMatch", "SwitchToWorkspace"]
|
||||
key_links:
|
||||
- from: "workspace.go"
|
||||
to: "/proc/PID/status"
|
||||
via: "ReadPPID lit le PPid"
|
||||
pattern: "ReadPPID"
|
||||
- from: "i3bridge.go"
|
||||
to: "go.i3wm.org/i3/v4"
|
||||
via: "GetTree, RunCommand"
|
||||
pattern: "i3\\.GetTree|i3\\.RunCommand"
|
||||
---
|
||||
|
||||
<objective>
|
||||
i3 bridge : mapping PID -> workspace via PPID chain + X11, fuzzy match pour switch, abstraction i3 IPC.
|
||||
|
||||
Purpose: Permettre a vmux de savoir dans quel workspace i3 se trouve chaque session Claude Code et d'y switcher.
|
||||
Output: workspace.go (PPID chain walk + X11 PID resolution), i3bridge.go (interface i3, fuzzy match, switch).
|
||||
</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
|
||||
|
||||
<interfaces>
|
||||
<!-- Types existants necessaires -->
|
||||
|
||||
From types.go:
|
||||
```go
|
||||
type Process struct { PID int; Cmd []string; Cwd string }
|
||||
```
|
||||
|
||||
From protocol.go (cree par plan 02-01, mais ce plan n'en depend pas):
|
||||
```go
|
||||
type SessionInfo struct {
|
||||
PID int `json:"pid"`
|
||||
Workspace string `json:"workspace"`
|
||||
Label string `json:"label,omitempty"`
|
||||
GitBranch string `json:"git_branch"`
|
||||
Cwd string `json:"cwd"`
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
<!-- Le plan 02-01 cree un champ workspaceResolver func(int) string dans Daemon.
|
||||
Ce plan fournit l'implementation reelle de ce resolver. -->
|
||||
</interfaces>
|
||||
</context>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto" tdd="true">
|
||||
<name>Task 1: PPID chain walk + workspace resolution</name>
|
||||
<files>workspace.go, workspace_test.go</files>
|
||||
<read_first>proc.go, proc_test.go</read_first>
|
||||
<behavior>
|
||||
- TestReadPPID: Lire le PPid depuis un faux /proc/PID/status -> retourne le bon PPID
|
||||
- TestReadPPIDMissing: Fichier inexistant -> erreur
|
||||
- TestResolveWorkspace: Claude PID 100 -> PPID 50 -> PPID 10 (dans terminalMap) -> retourne "workspace 3"
|
||||
- TestResolveWorkspaceNotFound: Aucun ancetre dans terminalMap -> retourne ""
|
||||
- TestResolveWorkspaceMaxDepth: Chaine > 20 niveaux -> retourne "" (securite, per Pitfall 7)
|
||||
- TestBuildTerminalWorkspaceMapUnit: Avec un mock i3 tree + mock X11 PID resolver, retourne la bonne map
|
||||
</behavior>
|
||||
<action>
|
||||
1. Creer workspace.go:
|
||||
|
||||
```go
|
||||
// ReadPPID lit le PPid depuis /proc/PID/status. procDir injectable pour les tests.
|
||||
func ReadPPID(procDir string, pid int) (int, error)
|
||||
```
|
||||
Parser la ligne "PPid:\tNNN" du fichier status.
|
||||
|
||||
```go
|
||||
// ResolveWorkspace remonte la chaine PPID depuis claudePID jusqu'a trouver
|
||||
// un PID connu dans terminalWorkspaces. Max 20 niveaux (per Pitfall 7).
|
||||
func ResolveWorkspace(procDir string, claudePID int, terminalWorkspaces map[int]string) string
|
||||
```
|
||||
|
||||
```go
|
||||
// X11PIDResolver abstrait la lecture de _NET_WM_PID pour testabilite.
|
||||
type X11PIDResolver interface {
|
||||
GetPID(windowID uint32) (int, error)
|
||||
}
|
||||
|
||||
// I3TreeProvider abstrait i3.GetTree() pour testabilite.
|
||||
type I3TreeProvider interface {
|
||||
GetTree() (*i3.Tree, error)
|
||||
}
|
||||
|
||||
// BuildTerminalWorkspaceMap construit la map terminalPID -> workspaceName.
|
||||
// Utilise i3 GetTree + X11 _NET_WM_PID (per D-04 corrige par RESEARCH).
|
||||
func BuildTerminalWorkspaceMap(tree I3TreeProvider, x11 X11PIDResolver) (map[int]string, error)
|
||||
```
|
||||
Walk recursif de l'arbre i3 : pour chaque node avec Window > 0, appeler x11.GetPID(node.Window).
|
||||
Tracker le workspace courant via node.Type == i3.WorkspaceNode.
|
||||
|
||||
2. Creer l'implementation reelle X11 avec xgbutil (per RESEARCH):
|
||||
```go
|
||||
type RealX11Resolver struct { xu *xgbutil.XUtil }
|
||||
func NewRealX11Resolver() (*RealX11Resolver, error) // verifie $DISPLAY (per Pitfall 5)
|
||||
func (r *RealX11Resolver) GetPID(windowID uint32) (int, error) // ewmh.WmPidGet
|
||||
func (r *RealX11Resolver) Close()
|
||||
```
|
||||
|
||||
3. Tests avec fake /proc (meme pattern que proc_test.go):
|
||||
- Creer des faux fichiers /proc/PID/status avec PPid
|
||||
- Mock I3TreeProvider et X11PIDResolver pour tester BuildTerminalWorkspaceMap
|
||||
- ReadPPID et ResolveWorkspace testes avec le filesystem reel (tmpdir)
|
||||
|
||||
4. Ajouter go.i3wm.org/i3/v4 dans go.mod (per D-06):
|
||||
```bash
|
||||
nix-shell --run "go get go.i3wm.org/i3/v4@latest"
|
||||
```
|
||||
Et BurntSushi/xgbutil pour ewmh (dependance transitive mais import explicite necessaire).
|
||||
</action>
|
||||
<verify>
|
||||
<automated>nix-shell --run "go test -run 'TestReadPPID|TestResolveWorkspace|TestBuildTerminal' -v ./..."</automated>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- grep -q 'func ReadPPID' workspace.go
|
||||
- grep -q 'func ResolveWorkspace' workspace.go
|
||||
- grep -q 'func BuildTerminalWorkspaceMap' workspace.go
|
||||
- grep -q 'type X11PIDResolver interface' workspace.go
|
||||
- grep -q 'TestResolveWorkspace' workspace_test.go
|
||||
- grep -q 'TestReadPPID' workspace_test.go
|
||||
- grep -q 'go.i3wm.org/i3/v4' go.mod
|
||||
</acceptance_criteria>
|
||||
<done>ReadPPID lit le PPid depuis /proc, ResolveWorkspace remonte la chaine PPID avec limite de 20, BuildTerminalWorkspaceMap abstrait i3+X11 derriere des interfaces testables. Tous les tests passent.</done>
|
||||
</task>
|
||||
|
||||
<task type="auto" tdd="true">
|
||||
<name>Task 2: Fuzzy match + switch workspace + i3 client interface</name>
|
||||
<files>i3bridge.go, i3bridge_test.go</files>
|
||||
<read_first>workspace.go</read_first>
|
||||
<behavior>
|
||||
- TestFuzzyMatchByLabel: sessions avec labels, query "review" matche la session avec label "review MR !456"
|
||||
- TestFuzzyMatchByBranch: query "auth" matche la session sur branche "feat/auth-flow"
|
||||
- TestFuzzyMatchByCwd: query "vmux" matche la session avec cwd "/home/pierre/Code/vibe/vmux"
|
||||
- TestFuzzyMatchPriority: session avec label "auth" ET autre session avec branche "auth" -> le label gagne
|
||||
- TestFuzzyMatchNoResult: query "inexistant" retourne nil
|
||||
- TestFuzzyMatchCaseInsensitive: query "AUTH" matche "auth" dans la branche
|
||||
- TestSwitchToWorkspace: Mock i3 RunCommand, verifier que "workspace number 3" est envoye
|
||||
</behavior>
|
||||
<action>
|
||||
1. Creer i3bridge.go:
|
||||
|
||||
```go
|
||||
// FuzzyMatch trouve la premiere session matchant query dans : label > branche > cwd.
|
||||
// Case-insensitive. Retourne nil si aucun match (per D-07).
|
||||
func FuzzyMatch(query string, sessions []SessionInfo) *SessionInfo
|
||||
```
|
||||
Priorite : label > GitBranch > Cwd. strings.Contains + strings.ToLower.
|
||||
|
||||
```go
|
||||
// I3Commander abstrait i3.RunCommand pour testabilite.
|
||||
type I3Commander interface {
|
||||
RunCommand(command string) ([]i3.CommandResult, error)
|
||||
}
|
||||
|
||||
// SwitchToWorkspace bascule vers le workspace indique via i3 IPC (per D-06).
|
||||
func SwitchToWorkspace(commander I3Commander, wsName string) error
|
||||
```
|
||||
Envoie `workspace number <wsName>`. Verifie le Success du resultat.
|
||||
|
||||
```go
|
||||
// RealI3Commander utilise go.i3wm.org/i3/v4 directement.
|
||||
type RealI3Commander struct{}
|
||||
func (c RealI3Commander) RunCommand(cmd string) ([]i3.CommandResult, error)
|
||||
```
|
||||
|
||||
2. Tests :
|
||||
- FuzzyMatch : pure logique, pas de mock i3. Construire des []SessionInfo en dur.
|
||||
- SwitchToWorkspace : mock I3Commander qui enregistre la commande recue.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>nix-shell --run "go test -run 'TestFuzzyMatch|TestSwitchToWorkspace' -v ./..."</automated>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- grep -q 'func FuzzyMatch' i3bridge.go
|
||||
- grep -q 'func SwitchToWorkspace' i3bridge.go
|
||||
- grep -q 'type I3Commander interface' i3bridge.go
|
||||
- grep -q 'TestFuzzyMatchByLabel' i3bridge_test.go
|
||||
- grep -q 'TestFuzzyMatchPriority' i3bridge_test.go
|
||||
- grep -q 'TestSwitchToWorkspace' i3bridge_test.go
|
||||
</acceptance_criteria>
|
||||
<done>FuzzyMatch cherche dans label > branche > cwd (case-insensitive), SwitchToWorkspace envoie la commande i3 IPC. Tests couvrent les 3 niveaux de priorite, le no-match et le case-insensitive.</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<verification>
|
||||
nix-shell --run "go test -v -race ./..."
|
||||
grep -q 'func ResolveWorkspace' workspace.go
|
||||
grep -q 'func FuzzyMatch' i3bridge.go
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- Mapping PID -> workspace via PPID chain fonctionnel et teste (I3-01)
|
||||
- Fuzzy match sur label/branche/cwd avec priorite correcte (I3-02)
|
||||
- Switch workspace via i3 IPC abstrait derriere une interface testable (I3-02)
|
||||
- Fallback gracieux si i3/X11 absent (per Pitfall 4, 5)
|
||||
- go.i3wm.org/i3/v4 ajoute dans go.mod (D-06)
|
||||
- Tous les tests passent avec -race
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
After completion, create `.planning/phases/02-daemon-et-i3-bridge/02-02-SUMMARY.md`
|
||||
</output>
|
||||
Reference in New Issue
Block a user