diff --git a/internal/cmd/deacon.go b/internal/cmd/deacon.go index f79f4abc..edf4370b 100644 --- a/internal/cmd/deacon.go +++ b/internal/cmd/deacon.go @@ -169,6 +169,11 @@ func startDeaconSession(t *tmux.Tmux) error { return fmt.Errorf("creating deacon directory: %w", err) } + // Ensure deacon has patrol hooks (idempotent) + if err := ensurePatrolHooks(deaconDir); err != nil { + fmt.Printf("%s Warning: Could not create deacon hooks: %v\n", style.Dim.Render("⚠"), err) + } + // Create session in deacon directory fmt.Println("Starting Deacon session...") if err := t.NewSession(DeaconSessionName, deaconDir); err != nil { @@ -388,3 +393,60 @@ func runDeaconTriggerPending(cmd *cobra.Command, args []string) error { return nil } +// ensurePatrolHooks creates .claude/settings.json with hooks for patrol roles. +// This is idempotent - if hooks already exist, it does nothing. +func ensurePatrolHooks(workspacePath string) error { + settingsPath := filepath.Join(workspacePath, ".claude", "settings.json") + + // Check if already exists + if _, err := os.Stat(settingsPath); err == nil { + return nil // Already exists + } + + claudeDir := filepath.Join(workspacePath, ".claude") + if err := os.MkdirAll(claudeDir, 0755); err != nil { + return fmt.Errorf("creating .claude dir: %w", err) + } + + // Standard patrol hooks + hooksJSON := `{ + "hooks": { + "SessionStart": [ + { + "matcher": "", + "hooks": [ + { + "type": "command", + "command": "gt prime && gt mail check --inject" + } + ] + } + ], + "PreCompact": [ + { + "matcher": "", + "hooks": [ + { + "type": "command", + "command": "gt prime" + } + ] + } + ], + "UserPromptSubmit": [ + { + "matcher": "", + "hooks": [ + { + "type": "command", + "command": "gt mail check --inject" + } + ] + } + ] + } +} +` + return os.WriteFile(settingsPath, []byte(hooksJSON), 0600) +} + diff --git a/internal/rig/manager.go b/internal/rig/manager.go index 20cb4b74..43abe5d8 100644 --- a/internal/rig/manager.go +++ b/internal/rig/manager.go @@ -256,6 +256,11 @@ func (m *Manager) AddRig(opts AddRigOptions) (*Rig, error) { if err := m.createRoleCLAUDEmd(refineryRigPath, "refinery", opts.Name, ""); err != nil { return nil, fmt.Errorf("creating refinery CLAUDE.md: %w", err) } + // Create refinery hooks for patrol triggering (at refinery/ level, not rig/) + refineryPath := filepath.Dir(refineryRigPath) + if err := m.createPatrolHooks(refineryPath); err != nil { + fmt.Printf(" Warning: Could not create refinery hooks: %v\n", err) + } // Clone repository for default crew workspace crewPath := filepath.Join(rigPath, "crew", opts.CrewName) @@ -275,6 +280,10 @@ func (m *Manager) AddRig(opts AddRigOptions) (*Rig, error) { if err := os.MkdirAll(witnessPath, 0755); err != nil { return nil, fmt.Errorf("creating witness dir: %w", err) } + // Create witness hooks for patrol triggering + if err := m.createPatrolHooks(witnessPath); err != nil { + fmt.Printf(" Warning: Could not create witness hooks: %v\n", err) + } // Create polecats directory (empty) polecatsPath := filepath.Join(rigPath, "polecats") @@ -573,6 +582,58 @@ func (m *Manager) createRoleCLAUDEmd(workspacePath string, role string, rigName return os.WriteFile(claudePath, []byte(content), 0644) } +// createPatrolHooks creates .claude/settings.json with hooks for patrol roles. +// These hooks trigger gt prime on session start and inject mail, enabling +// autonomous patrol execution for Witness and Refinery roles. +func (m *Manager) createPatrolHooks(workspacePath string) error { + claudeDir := filepath.Join(workspacePath, ".claude") + if err := os.MkdirAll(claudeDir, 0755); err != nil { + return fmt.Errorf("creating .claude dir: %w", err) + } + + // Standard patrol hooks - same as deacon + hooksJSON := `{ + "hooks": { + "SessionStart": [ + { + "matcher": "", + "hooks": [ + { + "type": "command", + "command": "gt prime && gt mail check --inject" + } + ] + } + ], + "PreCompact": [ + { + "matcher": "", + "hooks": [ + { + "type": "command", + "command": "gt prime" + } + ] + } + ], + "UserPromptSubmit": [ + { + "matcher": "", + "hooks": [ + { + "type": "command", + "command": "gt mail check --inject" + } + ] + } + ] + } +} +` + settingsPath := filepath.Join(claudeDir, "settings.json") + return os.WriteFile(settingsPath, []byte(hooksJSON), 0600) +} + // seedPatrolMolecules creates patrol molecule prototypes in the rig's beads database. // These molecules define the work loops for Deacon, Witness, and Refinery roles. func (m *Manager) seedPatrolMolecules(rigPath string) error {