feat(daemon): add lifecycle support for refinery and crew

- Add refinery and crew patterns to identityToSession()
- Add refinery and crew handling to restartSession() with pre-sync
- Add refinery and crew to identityToStateFile()
- Fix gt refinery start to send gt prime after Claude starts
- New syncWorkspace() helper for agents with persistent clones
This commit is contained in:
Steve Yegge
2025-12-20 17:42:21 -08:00
parent 58cf789eee
commit a9d204aa22
3 changed files with 95 additions and 2 deletions

View File

@@ -177,6 +177,14 @@ func (d *Daemon) identityToSession(identity string) string {
if strings.HasSuffix(identity, "-witness") {
return "gt-" + identity
}
// Pattern: <rig>-refinery → gt-<rig>-refinery
if strings.HasSuffix(identity, "-refinery") {
return "gt-" + identity
}
// Pattern: <rig>-crew-<name> → gt-<rig>-crew-<name>
if strings.Contains(identity, "-crew-") {
return "gt-" + identity
}
// Unknown identity
return ""
}
@@ -187,19 +195,48 @@ func (d *Daemon) restartSession(sessionName, identity string) error {
// Determine working directory and startup command based on agent type
var workDir, startCmd string
var rigName string
var agentRole string
var needsPreSync bool
if identity == "mayor" {
workDir = d.config.TownRoot
startCmd = "exec claude --dangerously-skip-permissions"
agentRole = "coordinator"
} else if strings.HasSuffix(identity, "-witness") {
// Extract rig name: <rig>-witness → <rig>
rigName = strings.TrimSuffix(identity, "-witness")
workDir = d.config.TownRoot + "/" + rigName
startCmd = "exec claude --dangerously-skip-permissions"
agentRole = "witness"
} else if strings.HasSuffix(identity, "-refinery") {
// Extract rig name: <rig>-refinery → <rig>
rigName = strings.TrimSuffix(identity, "-refinery")
workDir = filepath.Join(d.config.TownRoot, rigName, "refinery", "rig")
startCmd = "exec claude --dangerously-skip-permissions"
agentRole = "refinery"
needsPreSync = true
} else if strings.Contains(identity, "-crew-") {
// Extract rig and crew name: <rig>-crew-<name> → <rig>, <name>
parts := strings.SplitN(identity, "-crew-", 2)
if len(parts) != 2 {
return fmt.Errorf("invalid crew identity format: %s", identity)
}
rigName = parts[0]
crewName := parts[1]
workDir = filepath.Join(d.config.TownRoot, rigName, "crew", crewName)
startCmd = "exec claude --dangerously-skip-permissions"
agentRole = "crew"
needsPreSync = true
} else {
return fmt.Errorf("don't know how to restart %s", identity)
}
// Pre-sync workspace for agents with git clones (refinery)
if needsPreSync {
d.logger.Printf("Pre-syncing workspace for %s at %s", identity, workDir)
d.syncWorkspace(workDir)
}
// Create session
if err := d.tmux.NewSession(sessionName, workDir); err != nil {
return fmt.Errorf("creating session: %w", err)
@@ -214,7 +251,7 @@ func (d *Daemon) restartSession(sessionName, identity string) error {
_ = d.tmux.ConfigureGasTownSession(sessionName, theme, "", "Mayor", "coordinator")
} else if rigName != "" {
theme := tmux.AssignTheme(rigName)
_ = d.tmux.ConfigureGasTownSession(sessionName, theme, rigName, "witness", "witness")
_ = d.tmux.ConfigureGasTownSession(sessionName, theme, rigName, agentRole, agentRole)
}
// Send startup command
@@ -230,6 +267,32 @@ func (d *Daemon) restartSession(sessionName, identity string) error {
return nil
}
// syncWorkspace syncs a git workspace before starting a new session.
// This ensures agents with persistent clones (like refinery) start with current code.
func (d *Daemon) syncWorkspace(workDir string) {
// Fetch latest from origin
fetchCmd := exec.Command("git", "fetch", "origin")
fetchCmd.Dir = workDir
if err := fetchCmd.Run(); err != nil {
d.logger.Printf("Warning: git fetch failed in %s: %v", workDir, err)
}
// Pull with rebase to incorporate changes
pullCmd := exec.Command("git", "pull", "--rebase", "origin", "main")
pullCmd.Dir = workDir
if err := pullCmd.Run(); err != nil {
d.logger.Printf("Warning: git pull failed in %s: %v", workDir, err)
// Don't fail - agent can handle conflicts
}
// Sync beads
bdCmd := exec.Command("bd", "sync")
bdCmd.Dir = workDir
if err := bdCmd.Run(); err != nil {
d.logger.Printf("Warning: bd sync failed in %s: %v", workDir, err)
}
}
// closeMessage marks a mail message as read by closing the beads issue.
func (d *Daemon) closeMessage(id string) error {
cmd := exec.Command("bd", "close", id)
@@ -289,6 +352,20 @@ func (d *Daemon) identityToStateFile(identity string) string {
rigName := strings.TrimSuffix(identity, "-witness")
return filepath.Join(d.config.TownRoot, rigName, "witness", "state.json")
}
// Pattern: <rig>-refinery → <townRoot>/<rig>/refinery/state.json
if strings.HasSuffix(identity, "-refinery") {
rigName := strings.TrimSuffix(identity, "-refinery")
return filepath.Join(d.config.TownRoot, rigName, "refinery", "state.json")
}
// Pattern: <rig>-crew-<name> → <townRoot>/<rig>/crew/<name>/state.json
if strings.Contains(identity, "-crew-") {
parts := strings.SplitN(identity, "-crew-", 2)
if len(parts) == 2 {
rigName := parts[0]
crewName := parts[1]
return filepath.Join(d.config.TownRoot, rigName, "crew", crewName, "state.json")
}
}
// Unknown identity - can't determine state file
return ""
}

View File

@@ -209,7 +209,6 @@ func (m *Manager) Start(foreground bool) error {
}
// Start Claude agent with full permissions (like polecats)
// The agent will run gt prime to load refinery context and start processing
command := "claude --dangerously-skip-permissions"
if err := t.SendKeys(sessionID, command); err != nil {
// Clean up the session on failure
@@ -217,6 +216,12 @@ func (m *Manager) Start(foreground bool) error {
return fmt.Errorf("starting Claude agent: %w", err)
}
// Prime the agent after Claude starts to load refinery context
if err := t.SendKeysDelayed(sessionID, "gt prime", 2000); err != nil {
// Warning only - don't fail startup
fmt.Printf("Warning: could not send prime command: %v\n", err)
}
return nil
}