Merge polecat/Bullet: ephemeral polecat model (gt-7ik)
Resolved conflict in spawn.go by keeping --polecat flag feature.
This commit is contained in:
@@ -226,9 +226,12 @@ func getManager(role Role) string {
|
||||
case RoleMayor, RoleWitness:
|
||||
return "daemon/"
|
||||
case RolePolecat, RoleRefinery:
|
||||
// Would need rig context to determine witness address
|
||||
// For now, use a placeholder pattern
|
||||
return "<rig>/witness"
|
||||
// Detect rig from environment or working directory
|
||||
rigName := detectRigName()
|
||||
if rigName != "" {
|
||||
return rigName + "/witness"
|
||||
}
|
||||
return "witness/" // fallback
|
||||
case RoleCrew:
|
||||
return "human" // Crew is human-managed
|
||||
default:
|
||||
@@ -236,6 +239,41 @@ func getManager(role Role) string {
|
||||
}
|
||||
}
|
||||
|
||||
// detectRigName detects the rig name from environment or directory context.
|
||||
func detectRigName() string {
|
||||
// Check environment variable first
|
||||
if rig := os.Getenv("GT_RIG"); rig != "" {
|
||||
return rig
|
||||
}
|
||||
|
||||
// Try to detect from tmux session name (format: gt-<rig>-<polecat>)
|
||||
out, err := exec.Command("tmux", "display-message", "-p", "#{session_name}").Output()
|
||||
if err == nil {
|
||||
sessionName := strings.TrimSpace(string(out))
|
||||
if strings.HasPrefix(sessionName, "gt-") {
|
||||
parts := strings.SplitN(sessionName, "-", 3)
|
||||
if len(parts) >= 2 {
|
||||
return parts[1]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Try to detect from working directory
|
||||
cwd, err := os.Getwd()
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
// Look for "polecats" in path: .../rig/polecats/polecat/...
|
||||
if idx := strings.Index(cwd, "/polecats/"); idx != -1 {
|
||||
// Extract rig name from path before /polecats/
|
||||
rigPath := cwd[:idx]
|
||||
return filepath.Base(rigPath)
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
// sendHandoffMail sends a handoff message to ourselves for the successor to read.
|
||||
func sendHandoffMail(role Role, townRoot string) error {
|
||||
// Determine our address
|
||||
@@ -286,19 +324,26 @@ func sendLifecycleRequest(manager string, role Role, action HandoffAction, townR
|
||||
return nil
|
||||
}
|
||||
|
||||
// Get polecat name for identification
|
||||
polecatName := detectPolecatName()
|
||||
rigName := detectRigName()
|
||||
|
||||
subject := fmt.Sprintf("LIFECYCLE: %s requesting %s", role, action)
|
||||
body := fmt.Sprintf(`Lifecycle request from %s.
|
||||
|
||||
Action: %s
|
||||
Rig: %s
|
||||
Polecat: %s
|
||||
Time: %s
|
||||
|
||||
Please verify state and execute lifecycle action.
|
||||
`, role, action, time.Now().Format(time.RFC3339))
|
||||
`, role, action, rigName, polecatName, time.Now().Format(time.RFC3339))
|
||||
|
||||
// Send via bd mail (syntax: bd mail send <recipient> -s <subject> -m <body>)
|
||||
cmd := exec.Command("bd", "mail", "send", manager,
|
||||
"-s", subject,
|
||||
"-m", body,
|
||||
"--type", "task", // Mark as task requiring action
|
||||
)
|
||||
cmd.Dir = townRoot
|
||||
|
||||
@@ -309,6 +354,45 @@ Please verify state and execute lifecycle action.
|
||||
return nil
|
||||
}
|
||||
|
||||
// detectPolecatName detects the polecat name from environment or directory context.
|
||||
func detectPolecatName() string {
|
||||
// Check environment variable first
|
||||
if polecat := os.Getenv("GT_POLECAT"); polecat != "" {
|
||||
return polecat
|
||||
}
|
||||
|
||||
// Try to detect from tmux session name (format: gt-<rig>-<polecat>)
|
||||
out, err := exec.Command("tmux", "display-message", "-p", "#{session_name}").Output()
|
||||
if err == nil {
|
||||
sessionName := strings.TrimSpace(string(out))
|
||||
if strings.HasPrefix(sessionName, "gt-") {
|
||||
parts := strings.SplitN(sessionName, "-", 3)
|
||||
if len(parts) >= 3 {
|
||||
return parts[2]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Try to detect from working directory
|
||||
cwd, err := os.Getwd()
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
// Look for "polecats" in path: .../rig/polecats/polecat/...
|
||||
if idx := strings.Index(cwd, "/polecats/"); idx != -1 {
|
||||
// Extract polecat name from path after /polecats/
|
||||
remainder := cwd[idx+len("/polecats/"):]
|
||||
// Take first component
|
||||
if slashIdx := strings.Index(remainder, "/"); slashIdx != -1 {
|
||||
return remainder[:slashIdx]
|
||||
}
|
||||
return remainder
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
// setRequestingState updates state.json to indicate we're requesting lifecycle action.
|
||||
func setRequestingState(role Role, action HandoffAction, townRoot string) error {
|
||||
// Determine state file location based on role
|
||||
|
||||
@@ -40,11 +40,11 @@ var polecatListCmd = &cobra.Command{
|
||||
Short: "List polecats in a rig",
|
||||
Long: `List polecats in a rig or all rigs.
|
||||
|
||||
Output:
|
||||
- Name
|
||||
- State (idle/active/working/done/stuck)
|
||||
- Current issue (if any)
|
||||
- Session status (running/stopped)
|
||||
In the ephemeral model, polecats exist only while working. The list shows
|
||||
all currently active polecats with their states:
|
||||
- working: Actively working on an issue
|
||||
- done: Completed work, waiting for cleanup
|
||||
- stuck: Needs assistance
|
||||
|
||||
Examples:
|
||||
gt polecat list gastown
|
||||
@@ -85,10 +85,13 @@ Example:
|
||||
|
||||
var polecatWakeCmd = &cobra.Command{
|
||||
Use: "wake <rig>/<polecat>",
|
||||
Short: "Mark polecat as active (ready for work)",
|
||||
Long: `Mark polecat as active (ready for work).
|
||||
Short: "(Deprecated) Resume a polecat to working state",
|
||||
Long: `Resume a polecat to working state.
|
||||
|
||||
Transitions: idle → active
|
||||
DEPRECATED: In the ephemeral model, polecats are created fresh for each task
|
||||
via 'gt spawn'. This command is kept for backward compatibility.
|
||||
|
||||
Transitions: done → working
|
||||
|
||||
Example:
|
||||
gt polecat wake gastown/Toast`,
|
||||
@@ -98,11 +101,14 @@ Example:
|
||||
|
||||
var polecatSleepCmd = &cobra.Command{
|
||||
Use: "sleep <rig>/<polecat>",
|
||||
Short: "Mark polecat as idle (not available)",
|
||||
Long: `Mark polecat as idle (not available).
|
||||
Short: "(Deprecated) Mark polecat as done",
|
||||
Long: `Mark polecat as done.
|
||||
|
||||
Transitions: active → idle
|
||||
Fails if session is running (stop first).
|
||||
DEPRECATED: In the ephemeral model, polecats use 'gt handoff' when complete,
|
||||
which triggers automatic cleanup by the Witness. This command is kept for
|
||||
backward compatibility.
|
||||
|
||||
Transitions: working → done
|
||||
|
||||
Example:
|
||||
gt polecat sleep gastown/Toast`,
|
||||
@@ -259,11 +265,11 @@ func runPolecatList(cmd *cobra.Command, args []string) error {
|
||||
}
|
||||
|
||||
if len(allPolecats) == 0 {
|
||||
fmt.Println("No polecats found.")
|
||||
fmt.Println("No active polecats found.")
|
||||
return nil
|
||||
}
|
||||
|
||||
fmt.Printf("%s\n\n", style.Bold.Render("Polecats"))
|
||||
fmt.Printf("%s\n\n", style.Bold.Render("Active Polecats"))
|
||||
for _, p := range allPolecats {
|
||||
// Session indicator
|
||||
sessionStatus := style.Dim.Render("○")
|
||||
@@ -271,9 +277,15 @@ func runPolecatList(cmd *cobra.Command, args []string) error {
|
||||
sessionStatus = style.Success.Render("●")
|
||||
}
|
||||
|
||||
// Normalize state for display (legacy idle/active → working)
|
||||
displayState := p.State
|
||||
if p.State == polecat.StateIdle || p.State == polecat.StateActive {
|
||||
displayState = polecat.StateWorking
|
||||
}
|
||||
|
||||
// State color
|
||||
stateStr := string(p.State)
|
||||
switch p.State {
|
||||
stateStr := string(displayState)
|
||||
switch displayState {
|
||||
case polecat.StateWorking:
|
||||
stateStr = style.Info.Render(stateStr)
|
||||
case polecat.StateStuck:
|
||||
@@ -351,6 +363,9 @@ func runPolecatRemove(cmd *cobra.Command, args []string) error {
|
||||
}
|
||||
|
||||
func runPolecatWake(cmd *cobra.Command, args []string) error {
|
||||
fmt.Println(style.Warning.Render("DEPRECATED: Use 'gt spawn' to create fresh polecats instead"))
|
||||
fmt.Println()
|
||||
|
||||
rigName, polecatName, err := parseAddress(args[0])
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -365,11 +380,14 @@ func runPolecatWake(cmd *cobra.Command, args []string) error {
|
||||
return fmt.Errorf("waking polecat: %w", err)
|
||||
}
|
||||
|
||||
fmt.Printf("%s Polecat %s is now active.\n", style.SuccessPrefix, polecatName)
|
||||
fmt.Printf("%s Polecat %s is now working.\n", style.SuccessPrefix, polecatName)
|
||||
return nil
|
||||
}
|
||||
|
||||
func runPolecatSleep(cmd *cobra.Command, args []string) error {
|
||||
fmt.Println(style.Warning.Render("DEPRECATED: Use 'gt handoff' from within a polecat session instead"))
|
||||
fmt.Println()
|
||||
|
||||
rigName, polecatName, err := parseAddress(args[0])
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -385,14 +403,14 @@ func runPolecatSleep(cmd *cobra.Command, args []string) error {
|
||||
sessMgr := session.NewManager(t, r)
|
||||
running, _ := sessMgr.IsRunning(polecatName)
|
||||
if running {
|
||||
return fmt.Errorf("session is running. Stop it first with: gt session stop %s/%s", rigName, polecatName)
|
||||
return fmt.Errorf("session is running. Use 'gt handoff' from the polecat session, or stop it with: gt session stop %s/%s", rigName, polecatName)
|
||||
}
|
||||
|
||||
if err := mgr.Sleep(polecatName); err != nil {
|
||||
return fmt.Errorf("sleeping polecat: %w", err)
|
||||
return fmt.Errorf("marking polecat as done: %w", err)
|
||||
}
|
||||
|
||||
fmt.Printf("%s Polecat %s is now idle.\n", style.SuccessPrefix, polecatName)
|
||||
fmt.Printf("%s Polecat %s is now done.\n", style.SuccessPrefix, polecatName)
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user