From dded59000890d4c3bc89648043c6f92b589bb032 Mon Sep 17 00:00:00 2001 From: Robin Wohlers-Reichel Date: Thu, 19 Feb 2026 18:18:07 +0100 Subject: [PATCH 1/3] agent: move HookNames from Agent to HookSupport interface HookNames is only relevant for agents that support hooks. Moving it to HookSupport makes the Agent interface narrower and the type assertion in hooks_cmd.go explicit. Co-Authored-By: Claude Opus 4.6 Entire-Checkpoint: 1cebb5656cd7 --- cmd/entire/cli/agent/agent.go | 10 +++++----- cmd/entire/cli/agent/agent_test.go | 2 +- cmd/entire/cli/hook_registry.go | 4 ++-- cmd/entire/cli/hooks_cmd.go | 5 ++++- cmd/entire/cli/lifecycle_test.go | 1 - 5 files changed, 12 insertions(+), 10 deletions(-) diff --git a/cmd/entire/cli/agent/agent.go b/cmd/entire/cli/agent/agent.go index 68473284b..ff03df0bd 100644 --- a/cmd/entire/cli/agent/agent.go +++ b/cmd/entire/cli/agent/agent.go @@ -43,11 +43,6 @@ type Agent interface { // --- Event Mapping --- - // HookNames returns the hook verbs this agent supports. - // These become subcommands under `entire hooks `. - // e.g., ["stop", "user-prompt-submit", "session-start", "session-end"] - HookNames() []string - // ParseHookEvent translates an agent-native hook into a normalized lifecycle Event. // Returns nil if the hook has no lifecycle significance (e.g., pass-through hooks). // This is the core contribution surface for new agent implementations. @@ -94,6 +89,11 @@ type Agent interface { type HookSupport interface { Agent + // HookNames returns the hook verbs this agent supports. + // These become subcommands under `entire hooks `. + // e.g., ["stop", "user-prompt-submit", "session-start", "session-end"] + HookNames() []string + // InstallHooks installs agent-specific hooks. // If localDev is true, hooks point to local development build. // If force is true, removes existing Entire hooks before installing. diff --git a/cmd/entire/cli/agent/agent_test.go b/cmd/entire/cli/agent/agent_test.go index 67a0eda59..2813bb63b 100644 --- a/cmd/entire/cli/agent/agent_test.go +++ b/cmd/entire/cli/agent/agent_test.go @@ -21,7 +21,6 @@ func (m *mockAgent) DetectPresence() (bool, error) { return false, nil } func (m *mockAgent) GetSessionID(_ *HookInput) string { return "" } func (m *mockAgent) ProtectedDirs() []string { return nil } -func (m *mockAgent) HookNames() []string { return nil } //nolint:nilnil // Mock implementation func (m *mockAgent) ParseHookEvent(_ string, _ io.Reader) (*Event, error) { return nil, nil } @@ -53,6 +52,7 @@ type mockHookSupport struct { var _ HookSupport = (*mockHookSupport)(nil) // Compile-time interface check +func (m *mockHookSupport) HookNames() []string { return nil } func (m *mockHookSupport) InstallHooks(_, _ bool) (int, error) { return 0, nil } func (m *mockHookSupport) UninstallHooks() error { return nil } func (m *mockHookSupport) AreHooksInstalled() bool { return false } diff --git a/cmd/entire/cli/hook_registry.go b/cmd/entire/cli/hook_registry.go index 2d3abc0e5..2e17439b7 100644 --- a/cmd/entire/cli/hook_registry.go +++ b/cmd/entire/cli/hook_registry.go @@ -44,9 +44,9 @@ func GetCurrentHookAgent() (agent.Agent, error) { return ag, nil } -// newAgentHooksCmd creates a hooks subcommand for an agent. +// newAgentHooksCmd creates a hooks subcommand for an agent that implements HookSupport. // It dynamically creates subcommands for each hook the agent supports. -func newAgentHooksCmd(agentName agent.AgentName, handler agent.Agent) *cobra.Command { +func newAgentHooksCmd(agentName agent.AgentName, handler agent.HookSupport) *cobra.Command { cmd := &cobra.Command{ Use: string(agentName), Short: handler.Description() + " hook handlers", diff --git a/cmd/entire/cli/hooks_cmd.go b/cmd/entire/cli/hooks_cmd.go index 76fc8a8a4..ae84913a8 100644 --- a/cmd/entire/cli/hooks_cmd.go +++ b/cmd/entire/cli/hooks_cmd.go @@ -21,12 +21,15 @@ func newHooksCmd() *cobra.Command { cmd.AddCommand(newHooksGitCmd()) // Dynamically add agent hook subcommands + // Each agent that implements HookSupport gets its own subcommand tree for _, agentName := range agent.List() { ag, err := agent.Get(agentName) if err != nil { continue } - cmd.AddCommand(newAgentHooksCmd(agentName, ag)) + if handler, ok := ag.(agent.HookSupport); ok { + cmd.AddCommand(newAgentHooksCmd(agentName, handler)) + } } return cmd diff --git a/cmd/entire/cli/lifecycle_test.go b/cmd/entire/cli/lifecycle_test.go index cc03dd98f..39b07ba0e 100644 --- a/cmd/entire/cli/lifecycle_test.go +++ b/cmd/entire/cli/lifecycle_test.go @@ -32,7 +32,6 @@ func (m *mockLifecycleAgent) Description() string { return "M func (m *mockLifecycleAgent) IsPreview() bool { return false } func (m *mockLifecycleAgent) DetectPresence() (bool, error) { return false, nil } func (m *mockLifecycleAgent) ProtectedDirs() []string { return nil } -func (m *mockLifecycleAgent) HookNames() []string { return nil } func (m *mockLifecycleAgent) GetSessionID(_ *agent.HookInput) string { return "" } //nolint:nilnil // Mock implementation From 0c50af595be058358c285cddeb7b5eb83c61e94c Mon Sep 17 00:00:00 2001 From: Robin Wohlers-Reichel Date: Thu, 19 Feb 2026 18:24:30 +0100 Subject: [PATCH 2/3] agent: move ParseHookEvent from Agent to HookSupport interface ParseHookEvent is only relevant for agents that support hooks. Moving it to HookSupport alongside HookNames keeps hook-related methods together and narrows the core Agent interface. Co-Authored-By: Claude Opus 4.6 Entire-Checkpoint: 475108d11c45 --- cmd/entire/cli/agent/agent.go | 12 +++++------- cmd/entire/cli/agent/agent_test.go | 15 ++++++++------- cmd/entire/cli/hook_registry.go | 7 ++++++- cmd/entire/cli/lifecycle_test.go | 6 ------ 4 files changed, 19 insertions(+), 21 deletions(-) diff --git a/cmd/entire/cli/agent/agent.go b/cmd/entire/cli/agent/agent.go index ff03df0bd..269814127 100644 --- a/cmd/entire/cli/agent/agent.go +++ b/cmd/entire/cli/agent/agent.go @@ -41,13 +41,6 @@ type Agent interface { // Examples: [".claude"] for Claude, [".gemini"] for Gemini. ProtectedDirs() []string - // --- Event Mapping --- - - // ParseHookEvent translates an agent-native hook into a normalized lifecycle Event. - // Returns nil if the hook has no lifecycle significance (e.g., pass-through hooks). - // This is the core contribution surface for new agent implementations. - ParseHookEvent(hookName string, stdin io.Reader) (*Event, error) - // --- Transcript Storage --- // ReadTranscript reads the raw transcript bytes for a session. @@ -94,6 +87,11 @@ type HookSupport interface { // e.g., ["stop", "user-prompt-submit", "session-start", "session-end"] HookNames() []string + // ParseHookEvent translates an agent-native hook into a normalized lifecycle Event. + // Returns nil if the hook has no lifecycle significance (e.g., pass-through hooks). + // This is the core contribution surface for new agent implementations. + ParseHookEvent(hookName string, stdin io.Reader) (*Event, error) + // InstallHooks installs agent-specific hooks. // If localDev is true, hooks point to local development build. // If force is true, removes existing Entire hooks before installing. diff --git a/cmd/entire/cli/agent/agent_test.go b/cmd/entire/cli/agent/agent_test.go index 2813bb63b..fdf204bb4 100644 --- a/cmd/entire/cli/agent/agent_test.go +++ b/cmd/entire/cli/agent/agent_test.go @@ -22,9 +22,7 @@ func (m *mockAgent) DetectPresence() (bool, error) { return false, nil } func (m *mockAgent) GetSessionID(_ *HookInput) string { return "" } func (m *mockAgent) ProtectedDirs() []string { return nil } -//nolint:nilnil // Mock implementation -func (m *mockAgent) ParseHookEvent(_ string, _ io.Reader) (*Event, error) { return nil, nil } -func (m *mockAgent) ReadTranscript(_ string) ([]byte, error) { return nil, nil } +func (m *mockAgent) ReadTranscript(_ string) ([]byte, error) { return nil, nil } func (m *mockAgent) ChunkTranscript(content []byte, _ int) ([][]byte, error) { return [][]byte{content}, nil } @@ -52,10 +50,13 @@ type mockHookSupport struct { var _ HookSupport = (*mockHookSupport)(nil) // Compile-time interface check -func (m *mockHookSupport) HookNames() []string { return nil } -func (m *mockHookSupport) InstallHooks(_, _ bool) (int, error) { return 0, nil } -func (m *mockHookSupport) UninstallHooks() error { return nil } -func (m *mockHookSupport) AreHooksInstalled() bool { return false } +func (m *mockHookSupport) HookNames() []string { return nil } + +//nolint:nilnil // Mock implementation +func (m *mockHookSupport) ParseHookEvent(_ string, _ io.Reader) (*Event, error) { return nil, nil } +func (m *mockHookSupport) InstallHooks(_, _ bool) (int, error) { return 0, nil } +func (m *mockHookSupport) UninstallHooks() error { return nil } +func (m *mockHookSupport) AreHooksInstalled() bool { return false } // mockFileWatcher implements both Agent and FileWatcher interfaces. type mockFileWatcher struct { diff --git a/cmd/entire/cli/hook_registry.go b/cmd/entire/cli/hook_registry.go index 2e17439b7..a24591a28 100644 --- a/cmd/entire/cli/hook_registry.go +++ b/cmd/entire/cli/hook_registry.go @@ -132,8 +132,13 @@ func newAgentHookVerbCmdWithLogging(agentName agent.AgentName, hookName string) return fmt.Errorf("failed to get agent %q: %w", agentName, agentErr) } + handler, ok := ag.(agent.HookSupport) + if !ok { + return fmt.Errorf("agent %q does not support hooks", agentName) + } + // Use cmd.InOrStdin() to support testing with cmd.SetIn() - event, parseErr := ag.ParseHookEvent(hookName, cmd.InOrStdin()) + event, parseErr := handler.ParseHookEvent(hookName, cmd.InOrStdin()) if parseErr != nil { return fmt.Errorf("failed to parse hook event: %w", parseErr) } diff --git a/cmd/entire/cli/lifecycle_test.go b/cmd/entire/cli/lifecycle_test.go index 39b07ba0e..a4645f331 100644 --- a/cmd/entire/cli/lifecycle_test.go +++ b/cmd/entire/cli/lifecycle_test.go @@ -2,7 +2,6 @@ package cli import ( "errors" - "io" "os" "path/filepath" "strings" @@ -34,11 +33,6 @@ func (m *mockLifecycleAgent) DetectPresence() (bool, error) { return fa func (m *mockLifecycleAgent) ProtectedDirs() []string { return nil } func (m *mockLifecycleAgent) GetSessionID(_ *agent.HookInput) string { return "" } -//nolint:nilnil // Mock implementation -func (m *mockLifecycleAgent) ParseHookEvent(_ string, _ io.Reader) (*agent.Event, error) { - return nil, nil -} - func (m *mockLifecycleAgent) ReadTranscript(_ string) ([]byte, error) { if m.transcriptErr != nil { return nil, m.transcriptErr From f3c86220f2bc54a395f7fadfd5e6caf1b57241cd Mon Sep 17 00:00:00 2001 From: Robin Wohlers-Reichel Date: Fri, 20 Feb 2026 10:10:36 +0100 Subject: [PATCH 3/3] agent: fix interface description --- cmd/entire/cli/agent/agent.go | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/cmd/entire/cli/agent/agent.go b/cmd/entire/cli/agent/agent.go index 269814127..94c74f6d7 100644 --- a/cmd/entire/cli/agent/agent.go +++ b/cmd/entire/cli/agent/agent.go @@ -11,12 +11,10 @@ import ( // Each agent implementation (Claude Code, Cursor, Aider, etc.) converts its // native format to the normalized types defined in this package. // -// The interface is organized into four groups: -// +// The interface is organized into three groups: // - Identity (5 methods): Name, Type, Description, DetectPresence, ProtectedDirs -// - Event Mapping (2 methods): HookNames, ParseHookEvent // - Transcript Storage (3 methods): ReadTranscript, ChunkTranscript, ReassembleTranscript -// - Legacy (8 methods): Will be moved to optional interfaces or removed in a future phase +// - Legacy (6 methods): Will be moved to optional interfaces or removed in a future phase type Agent interface { // --- Identity --- @@ -79,6 +77,10 @@ type Agent interface { // HookSupport is implemented by agents with lifecycle hooks. // This optional interface allows agents like Claude Code and Cursor to // install and manage hooks that notify Entire of agent events. +// +// The interface is organized into two groups: +// - Hook Mapping (2 methods): HookNames, ParseHookEvent +// - Hook Management (3 methods): InstallHooks, UninstallHooks, AreHooksInstalled type HookSupport interface { Agent