docs(03): create phase plan for hook server

This commit is contained in:
Pierre Martin
2026-03-23 19:33:24 +01:00
parent f7228922cc
commit 005c6c1214
3 changed files with 465 additions and 3 deletions

View File

@@ -0,0 +1,218 @@
---
phase: 03-hook-server
plan: 01
type: execute
wave: 1
depends_on: []
files_modified:
- hook.go
- hook_test.go
- protocol.go
- protocol_test.go
autonomous: true
requirements: [STATE-03]
must_haves:
truths:
- "HookEvent struct parse le JSON de Claude Code hooks (session_id, hook_event_name, notification_type, etc.)"
- "processHookEvent mappe correctement Notification/permission_prompt vers WaitType=permission"
- "processHookEvent mappe correctement Notification/idle_prompt vers WaitType=idle"
- "processHookEvent mappe correctement Stop vers WaitType=question"
- "processHookEvent mappe correctement PostToolUse/PreToolUse vers State=Working et clear WaitType"
- "UpdateFromHook cree une entree si la session n'existe pas encore dans le registre"
- "SessionInfo contient le champ WaitType serialise en JSON"
- "HTTP handler POST /hook retourne 200 et met a jour le registre"
- "HTTP handler refuse les methodes non-POST (405)"
- "HTTP handler refuse les payloads invalides (400)"
- "HTTP handler limite la taille du body (MaxBytesReader 64KB)"
artifacts:
- path: "hook.go"
provides: "HookEvent struct, handleHook HTTP handler, processHookEvent, UpdateFromHook"
- path: "hook_test.go"
provides: "Tests unitaires pour tous les mappings hook event et le handler HTTP"
- path: "protocol.go"
provides: "SessionInfo avec champ WaitType"
- path: "protocol_test.go"
provides: "Test serialisation JSON de WaitType"
key_links:
- from: "hook.go"
to: "daemon.go"
via: "UpdateFromHook modifie le SessionRegistry"
pattern: "registry\\.UpdateFromHook"
- from: "hook.go"
to: "protocol.go"
via: "SessionInfo.WaitType"
pattern: "WaitType"
---
<objective>
Hook event processing core: types, mapping, handler HTTP, et mise a jour du registre.
Purpose: Poser toute la logique de traitement des events Claude Code hooks. Chaque type d'event (Notification, Stop, PostToolUse, PreToolUse) est mappe vers le bon State et WaitType.
Output: hook.go avec la logique, hook_test.go avec les tests, protocol.go enrichi avec WaitType.
</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/03-hook-server/03-RESEARCH.md
<interfaces>
<!-- Existing types the executor needs -->
From protocol.go:
```go
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"`
}
```
From daemon.go:
```go
type TrackedSession struct {
Info SessionInfo
PrevState string
}
type SessionRegistry struct {
mu sync.RWMutex
sessions map[string]*TrackedSession
}
func (r *SessionRegistry) Update(info SessionInfo)
func (r *SessionRegistry) List() []SessionInfo
func (r *SessionRegistry) RemoveStale(activeIDs map[string]bool)
```
From daemon_test.go:
```go
func newTestDaemon(t *testing.T) *Daemon
```
</interfaces>
</context>
<tasks>
<task type="auto" tdd="true">
<name>Task 1: HookEvent struct, processHookEvent mapping, UpdateFromHook, WaitType dans SessionInfo</name>
<files>hook.go, hook_test.go, protocol.go, protocol_test.go</files>
<read_first>
- protocol.go (SessionInfo actuel, a enrichir avec WaitType)
- daemon.go (SessionRegistry, TrackedSession, pattern Update)
- daemon_test.go (newTestDaemon helper, patterns de test)
- .planning/phases/03-hook-server/03-RESEARCH.md (payload JSON Claude Code, mapping events)
</read_first>
<behavior>
- TestProcessHookNotificationPermission: event Notification + notification_type=permission_prompt donne State="Needs Input", WaitType="permission"
- TestProcessHookNotificationIdle: event Notification + notification_type=idle_prompt donne State="Needs Input", WaitType="idle"
- TestProcessHookNotificationUnknown: event Notification + notification_type inconnu donne State="Needs Input", WaitType="question"
- TestProcessHookStop: event Stop donne State="Needs Input", WaitType="question"
- TestProcessHookPostToolUse: event PostToolUse donne State="Working", WaitType=""
- TestProcessHookPreToolUse: event PreToolUse donne State="Working", WaitType=""
- TestProcessHookIgnoresEmptySessionID: event avec session_id="" ne modifie pas le registre
- TestProcessHookIgnoresUnknownEvent: event avec hook_event_name inconnu ne modifie pas le registre
- TestUpdateFromHookCreatesNewEntry: session_id inconnu du registre cree une nouvelle entree
- TestUpdateFromHookSetsWaitingSince: transition Working vers NeedsInput met WaitingSince
- TestUpdateFromHookClearsWaitingSince: transition NeedsInput vers Working efface WaitingSince
- TestSessionInfoWaitTypeJSON: WaitType="permission" est serialise dans le JSON, WaitType="" est omis (omitempty)
</behavior>
<action>
1. Ajouter `WaitType string json:"wait_type,omitempty"` a SessionInfo dans protocol.go
2. Creer hook.go avec :
- HookEvent struct (session_id, transcript_path, cwd, hook_event_name, notification_type, message, title, last_assistant_message, stop_hook_active, tool_name). Tous les champs JSON avec tags corrects et omitempty pour les optionnels.
- `func (d *Daemon) processHookEvent(event HookEvent)` : switch sur event.HookEventName pour mapper vers state+waitType, puis appel a registry.UpdateFromHook. Ignorer session_id vide et events inconnus.
- `func (r *SessionRegistry) UpdateFromHook(sessionID, state, waitType, cwd string)` : lock, creer l'entree si absente, mettre a jour Info.SessionID/State/WaitType/Cwd, gerer la transition WaitingSince (meme logique que Update mais sans ecraser les autres champs).
3. Creer hook_test.go avec les 12 tests ci-dessus. Utiliser newTestDaemon pour les tests processHookEvent, NewRegistry directement pour les tests UpdateFromHook.
4. Ajouter un test dans protocol_test.go pour la serialisation JSON de WaitType.
NE PAS ajouter le handler HTTP dans cette tache (c'est la tache 2).
NE PAS modifier daemon.go (pas de startHookServer, pas de hookPort).
</action>
<verify>
<automated>nix-shell --run "go test -v -run 'TestProcessHook|TestUpdateFromHook|TestSessionInfoWaitType' ./..."</automated>
</verify>
<acceptance_criteria>
- grep -q 'WaitType.*string.*json:"wait_type' protocol.go
- grep -q 'type HookEvent struct' hook.go
- grep -q 'func.*Daemon.*processHookEvent' hook.go
- grep -q 'func.*SessionRegistry.*UpdateFromHook' hook.go
- grep -q 'TestProcessHookNotificationPermission' hook_test.go
- grep -q 'TestProcessHookStop' hook_test.go
- grep -q 'TestUpdateFromHookCreatesNewEntry' hook_test.go
- grep -q 'TestSessionInfoWaitTypeJSON' protocol_test.go
</acceptance_criteria>
<done>Les 12 tests passent. HookEvent parse le JSON Claude Code. processHookEvent mappe les 4 types d'events vers le bon State/WaitType. UpdateFromHook gere les nouvelles sessions et les transitions WaitingSince. SessionInfo.WaitType est serialise en JSON.</done>
</task>
<task type="auto" tdd="true">
<name>Task 2: HTTP handler POST /hook avec validation et protection</name>
<files>hook.go, hook_test.go</files>
<read_first>
- hook.go (HookEvent et processHookEvent crees par Task 1)
- daemon_test.go (newTestDaemon, patterns httptest)
- .planning/phases/03-hook-server/03-RESEARCH.md (handler pattern, MaxBytesReader)
</read_first>
<behavior>
- TestHandleHookPostOK: POST /hook avec payload Notification valide retourne 200 et met a jour le registre
- TestHandleHookMethodNotAllowed: GET /hook retourne 405
- TestHandleHookBadJSON: POST /hook avec body invalide retourne 400
- TestHandleHookBodyTooLarge: POST /hook avec body > 64KB retourne 400
</behavior>
<action>
1. Ajouter dans hook.go :
- `func (d *Daemon) handleHook(w http.ResponseWriter, r *http.Request)` : verifie Method==POST (sinon 405), applique http.MaxBytesReader(w, r.Body, 64*1024), decode JSON dans HookEvent (sinon 400), appelle processHookEvent, retourne 200 OK.
2. Ajouter les 4 tests dans hook_test.go. Utiliser httptest.NewRequest + httptest.NewRecorder pour tester le handler directement (pas besoin d'ouvrir un port).
3. Pour TestHandleHookPostOK, verifier que le registre contient la session avec le bon WaitType apres l'appel.
4. Pour TestHandleHookBodyTooLarge, generer un body de 65KB.
NE PAS demarrer de serveur HTTP dans cette tache. Le handler est teste unitairement via httptest.
</action>
<verify>
<automated>nix-shell --run "go test -v -run 'TestHandleHook' ./..."</automated>
</verify>
<acceptance_criteria>
- grep -q 'func.*Daemon.*handleHook' hook.go
- grep -q 'MaxBytesReader' hook.go
- grep -q 'StatusMethodNotAllowed' hook.go
- grep -q 'TestHandleHookPostOK' hook_test.go
- grep -q 'TestHandleHookMethodNotAllowed' hook_test.go
- grep -q 'TestHandleHookBadJSON' hook_test.go
- grep -q 'TestHandleHookBodyTooLarge' hook_test.go
</acceptance_criteria>
<done>Le handler HTTP /hook accepte les POST valides (200), rejette les non-POST (405), rejette le JSON invalide (400), limite la taille du body a 64KB. 4 tests passent.</done>
</task>
</tasks>
<verification>
nix-shell --run "go test -v -run 'TestProcessHook|TestUpdateFromHook|TestHandleHook|TestSessionInfoWaitType' ./..."
nix-shell --run "go vet ./..."
</verification>
<success_criteria>
- 16 tests passent (12 task 1 + 4 task 2)
- HookEvent struct couvre tous les champs du payload Claude Code
- processHookEvent mappe les 4 types d'events correctement
- UpdateFromHook gere les sessions inconnues et les transitions WaitingSince
- handleHook valide method, body size, et JSON
- SessionInfo.WaitType visible dans le JSON
- Aucune regression sur les tests existants
</success_criteria>
<output>
After completion, create `.planning/phases/03-hook-server/03-01-SUMMARY.md`
</output>