Add Claude settings templates for autonomous roles (gt-6957)

- Create internal/claude package with embedded settings templates
- settings-autonomous.json: gt prime && gt mail check --inject (SessionStart)
- settings-interactive.json: gt prime only (SessionStart)

- Update witness.go: EnsureSettings before session, remove broken gt prime injection
- Update refinery/manager.go: EnsureSettings before session, remove broken NudgeSession
- Update session/manager.go: EnsureSettings for polecats, remove broken issue injection

All autonomous roles (polecat, witness, refinery) now get proper SessionStart hooks
automatically when their sessions are created. No more timing-based gt prime injection.
This commit is contained in:
Steve Yegge
2025-12-22 17:51:15 -08:00
parent 588c0f5c42
commit 59d656470e
6 changed files with 183 additions and 24 deletions

View File

@@ -0,0 +1,40 @@
{
"enabledPlugins": {
"beads@beads-marketplace": false
},
"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"
}
]
}
]
}
}

View File

@@ -0,0 +1,40 @@
{
"enabledPlugins": {
"beads@beads-marketplace": false
},
"hooks": {
"SessionStart": [
{
"matcher": "",
"hooks": [
{
"type": "command",
"command": "gt prime"
}
]
}
],
"PreCompact": [
{
"matcher": "",
"hooks": [
{
"type": "command",
"command": "gt prime"
}
]
}
],
"UserPromptSubmit": [
{
"matcher": "",
"hooks": [
{
"type": "command",
"command": "gt mail check --inject"
}
]
}
]
}
}

View File

@@ -0,0 +1,80 @@
// Package claude provides Claude Code configuration management.
package claude
import (
"embed"
"fmt"
"os"
"path/filepath"
)
//go:embed config/*.json
var configFS embed.FS
// RoleType indicates whether a role is autonomous or interactive.
type RoleType string
const (
// Autonomous roles (polecat, witness, refinery) need mail in SessionStart
// because they may be triggered externally without user input.
Autonomous RoleType = "autonomous"
// Interactive roles (mayor, crew) wait for user input, so UserPromptSubmit
// handles mail injection.
Interactive RoleType = "interactive"
)
// RoleTypeFor returns the RoleType for a given role name.
func RoleTypeFor(role string) RoleType {
switch role {
case "polecat", "witness", "refinery":
return Autonomous
default:
return Interactive
}
}
// EnsureSettings ensures .claude/settings.json exists in the given directory.
// If the file doesn't exist, it copies the appropriate template based on role type.
// If the file already exists, it's left unchanged.
func EnsureSettings(workDir string, roleType RoleType) error {
claudeDir := filepath.Join(workDir, ".claude")
settingsPath := filepath.Join(claudeDir, "settings.json")
// If settings already exist, don't overwrite
if _, err := os.Stat(settingsPath); err == nil {
return nil
}
// Create .claude directory if needed
if err := os.MkdirAll(claudeDir, 0755); err != nil {
return fmt.Errorf("creating .claude directory: %w", err)
}
// Select template based on role type
var templateName string
switch roleType {
case Autonomous:
templateName = "config/settings-autonomous.json"
default:
templateName = "config/settings-interactive.json"
}
// Read template
content, err := configFS.ReadFile(templateName)
if err != nil {
return fmt.Errorf("reading template %s: %w", templateName, err)
}
// Write settings file
if err := os.WriteFile(settingsPath, content, 0644); err != nil {
return fmt.Errorf("writing settings: %w", err)
}
return nil
}
// EnsureSettingsForRole is a convenience function that combines RoleTypeFor and EnsureSettings.
func EnsureSettingsForRole(workDir, role string) error {
return EnsureSettings(workDir, RoleTypeFor(role))
}

View File

@@ -5,9 +5,9 @@ import (
"fmt"
"os"
"os/exec"
"time"
"github.com/spf13/cobra"
"github.com/steveyegge/gastown/internal/claude"
"github.com/steveyegge/gastown/internal/rig"
"github.com/steveyegge/gastown/internal/style"
"github.com/steveyegge/gastown/internal/tmux"
@@ -280,6 +280,11 @@ func ensureWitnessSession(rigName string, r *rig.Rig) (bool, error) {
return false, nil
}
// Ensure Claude settings exist (autonomous role needs mail in SessionStart)
if err := claude.EnsureSettingsForRole(r.Path, "witness"); err != nil {
return false, fmt.Errorf("ensuring Claude settings: %w", err)
}
// Create new tmux session
if err := t.NewSession(sessionName, r.Path); err != nil {
return false, fmt.Errorf("creating session: %w", err)
@@ -294,19 +299,12 @@ func ensureWitnessSession(rigName string, r *rig.Rig) (bool, error) {
_ = t.ConfigureGasTownSession(sessionName, theme, rigName, "witness", "witness")
// Launch Claude in a respawn loop
// NOTE: No gt prime injection needed - SessionStart hook handles it automatically
loopCmd := `while true; do echo "👁️ Starting Witness for ` + rigName + `..."; claude --dangerously-skip-permissions; echo ""; echo "Witness exited. Restarting in 2s... (Ctrl-C to stop)"; sleep 2; done`
if err := t.SendKeysDelayed(sessionName, loopCmd, 200); err != nil {
return false, fmt.Errorf("sending command: %w", err)
}
// Wait briefly then send gt prime to initialize context
// This runs after Claude starts up in the respawn loop
time.Sleep(3 * time.Second)
if err := t.SendKeys(sessionName, "gt prime"); err != nil {
// Non-fatal - Claude will still work, just without auto-priming
fmt.Printf("Warning: failed to send gt prime: %v\n", err)
}
return true, nil
}

View File

@@ -13,6 +13,7 @@ import (
"strings"
"time"
"github.com/steveyegge/gastown/internal/claude"
"github.com/steveyegge/gastown/internal/config"
"github.com/steveyegge/gastown/internal/mail"
"github.com/steveyegge/gastown/internal/rig"
@@ -189,6 +190,11 @@ func (m *Manager) Start(foreground bool) error {
refineryRigDir = m.workDir
}
// Ensure Claude settings exist (autonomous role needs mail in SessionStart)
if err := claude.EnsureSettingsForRole(refineryRigDir, "refinery"); err != nil {
return fmt.Errorf("ensuring Claude settings: %w", err)
}
if err := t.NewSession(sessionID, refineryRigDir); err != nil {
return fmt.Errorf("creating tmux session: %w", err)
}
@@ -227,20 +233,12 @@ func (m *Manager) Start(foreground bool) error {
}
// Wait for Claude to start (pane command changes from shell to node)
// NOTE: No gt prime injection needed - SessionStart hook handles it automatically
shells := []string{"bash", "zsh", "sh", "fish", "tcsh", "ksh"}
if err := t.WaitForCommand(sessionID, shells, 15*time.Second); err != nil {
fmt.Fprintf(m.output, "Warning: Timeout waiting for Claude to start: %v\n", err)
}
// Give Claude time to initialize after process starts
time.Sleep(500 * time.Millisecond)
// Prime the agent using NudgeSession for reliable delivery
if err := t.NudgeSession(sessionID, "run gt prime"); err != nil {
// Warning only - don't fail startup
fmt.Fprintf(m.output, "Warning: could not send prime command: %v\n", err)
}
return nil
}

View File

@@ -10,6 +10,7 @@ import (
"strings"
"time"
"github.com/steveyegge/gastown/internal/claude"
"github.com/steveyegge/gastown/internal/rig"
"github.com/steveyegge/gastown/internal/tmux"
)
@@ -118,6 +119,11 @@ func (m *Manager) Start(polecat string, opts StartOptions) error {
workDir = m.polecatDir(polecat)
}
// Ensure Claude settings exist (autonomous role needs mail in SessionStart)
if err := claude.EnsureSettingsForRole(workDir, "polecat"); err != nil {
return fmt.Errorf("ensuring Claude settings: %w", err)
}
// Create session
if err := m.tmux.NewSession(sessionID, workDir); err != nil {
return fmt.Errorf("creating session: %w", err)
@@ -149,12 +155,9 @@ func (m *Manager) Start(polecat string, opts StartOptions) error {
return fmt.Errorf("sending command: %w", err)
}
// If issue specified, wait a bit then inject it
if opts.Issue != "" {
time.Sleep(500 * time.Millisecond)
prompt := fmt.Sprintf("Work on issue: %s", opts.Issue)
_ = m.Inject(polecat, prompt) // Non-fatal error
}
// NOTE: No issue injection needed here. Work assignments are sent via mail
// before session start, and the SessionStart hook runs gt prime + mail check
// which shows the polecat its assignment.
return nil
}