feat: swarm worker spawning, mail routing improvements, beads sync
This commit is contained in:
@@ -173,12 +173,14 @@ func runSpawn(cmd *cobra.Command, args []string) error {
|
||||
if err := sessMgr.Start(polecatName, session.StartOptions{}); err != nil {
|
||||
return fmt.Errorf("starting session: %w", err)
|
||||
}
|
||||
// Wait for claude to initialize
|
||||
time.Sleep(2 * time.Second)
|
||||
// Wait for Claude to fully initialize (needs 4-5s for prompt)
|
||||
fmt.Printf("Waiting for Claude to initialize...\n")
|
||||
time.Sleep(5 * time.Second)
|
||||
}
|
||||
|
||||
// Inject initial context
|
||||
context := buildSpawnContext(issue, spawnMessage)
|
||||
fmt.Printf("Injecting work assignment...\n")
|
||||
if err := sessMgr.Inject(polecatName, context); err != nil {
|
||||
return fmt.Errorf("injecting context: %w", err)
|
||||
}
|
||||
|
||||
+73
-1
@@ -11,9 +11,12 @@ import (
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/steveyegge/gastown/internal/config"
|
||||
"github.com/steveyegge/gastown/internal/git"
|
||||
"github.com/steveyegge/gastown/internal/polecat"
|
||||
"github.com/steveyegge/gastown/internal/rig"
|
||||
"github.com/steveyegge/gastown/internal/session"
|
||||
"github.com/steveyegge/gastown/internal/style"
|
||||
"github.com/steveyegge/gastown/internal/swarm"
|
||||
"github.com/steveyegge/gastown/internal/tmux"
|
||||
"github.com/steveyegge/gastown/internal/workspace"
|
||||
)
|
||||
|
||||
@@ -291,13 +294,14 @@ func runSwarmCreate(cmd *cobra.Command, args []string) error {
|
||||
func runSwarmStart(cmd *cobra.Command, args []string) error {
|
||||
swarmID := args[0]
|
||||
|
||||
// Find the swarm
|
||||
// Find the swarm and its rig
|
||||
rigs, _, err := getAllRigs()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var store *SwarmStore
|
||||
var foundRig *rig.Rig
|
||||
|
||||
for _, r := range rigs {
|
||||
s, err := LoadSwarmStore(r.Path)
|
||||
@@ -307,6 +311,7 @@ func runSwarmStart(cmd *cobra.Command, args []string) error {
|
||||
|
||||
if _, exists := s.Swarms[swarmID]; exists {
|
||||
store = s
|
||||
foundRig = r
|
||||
break
|
||||
}
|
||||
}
|
||||
@@ -329,6 +334,73 @@ func runSwarmStart(cmd *cobra.Command, args []string) error {
|
||||
}
|
||||
|
||||
fmt.Printf("%s Swarm %s started\n", style.Bold.Render("✓"), swarmID)
|
||||
|
||||
// Spawn sessions for workers with tasks
|
||||
if len(sw.Workers) > 0 && len(sw.Tasks) > 0 {
|
||||
fmt.Printf("\nSpawning workers...\n")
|
||||
if err := spawnSwarmWorkers(foundRig, sw); err != nil {
|
||||
fmt.Printf("Warning: failed to spawn some workers: %v\n", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// spawnSwarmWorkers spawns sessions for swarm workers with task assignments.
|
||||
func spawnSwarmWorkers(r *rig.Rig, sw *swarm.Swarm) error {
|
||||
t := tmux.NewTmux()
|
||||
sessMgr := session.NewManager(t, r)
|
||||
polecatGit := git.NewGit(r.Path)
|
||||
polecatMgr := polecat.NewManager(r, polecatGit)
|
||||
|
||||
// Pair workers with tasks (round-robin if more tasks than workers)
|
||||
workerIdx := 0
|
||||
for i, task := range sw.Tasks {
|
||||
if task.State != swarm.TaskPending {
|
||||
continue
|
||||
}
|
||||
|
||||
if workerIdx >= len(sw.Workers) {
|
||||
break // No more workers
|
||||
}
|
||||
|
||||
worker := sw.Workers[workerIdx]
|
||||
workerIdx++
|
||||
|
||||
// Assign task to worker in swarm state
|
||||
sw.Tasks[i].Assignee = worker
|
||||
sw.Tasks[i].State = swarm.TaskAssigned
|
||||
|
||||
// Update polecat state
|
||||
if err := polecatMgr.AssignIssue(worker, task.IssueID); err != nil {
|
||||
fmt.Printf(" Warning: couldn't assign %s to %s: %v\n", task.IssueID, worker, err)
|
||||
continue
|
||||
}
|
||||
|
||||
// Check if already running
|
||||
running, _ := sessMgr.IsRunning(worker)
|
||||
if running {
|
||||
fmt.Printf(" %s already running, injecting task...\n", worker)
|
||||
} else {
|
||||
fmt.Printf(" Starting %s...\n", worker)
|
||||
if err := sessMgr.Start(worker, session.StartOptions{}); err != nil {
|
||||
fmt.Printf(" Warning: couldn't start %s: %v\n", worker, err)
|
||||
continue
|
||||
}
|
||||
// Wait for Claude to initialize
|
||||
time.Sleep(5 * time.Second)
|
||||
}
|
||||
|
||||
// Inject work assignment
|
||||
context := fmt.Sprintf("[SWARM] You are part of swarm %s.\n\nAssigned task: %s\nTitle: %s\n\nWork on this task. When complete, commit and signal DONE.",
|
||||
sw.ID, task.IssueID, task.Title)
|
||||
if err := sessMgr.Inject(worker, context); err != nil {
|
||||
fmt.Printf(" Warning: couldn't inject to %s: %v\n", worker, err)
|
||||
} else {
|
||||
fmt.Printf(" %s → %s ✓\n", worker, task.IssueID)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -72,8 +72,8 @@ func (m *Mailbox) List() ([]*Message, error) {
|
||||
}
|
||||
|
||||
func (m *Mailbox) listBeads() ([]*Message, error) {
|
||||
// bd mail inbox --json
|
||||
cmd := exec.Command("bd", "mail", "inbox", "--json")
|
||||
// bd message inbox --json
|
||||
cmd := exec.Command("bd", "message", "inbox", "--json")
|
||||
cmd.Dir = m.workDir
|
||||
cmd.Env = append(cmd.Environ(), "BD_IDENTITY="+m.identity)
|
||||
|
||||
@@ -173,7 +173,7 @@ func (m *Mailbox) Get(id string) (*Message, error) {
|
||||
}
|
||||
|
||||
func (m *Mailbox) getBeads(id string) (*Message, error) {
|
||||
cmd := exec.Command("bd", "mail", "read", id, "--json")
|
||||
cmd := exec.Command("bd", "message", "read", id, "--json")
|
||||
cmd.Dir = m.workDir
|
||||
|
||||
var stdout, stderr bytes.Buffer
|
||||
@@ -221,7 +221,7 @@ func (m *Mailbox) MarkRead(id string) error {
|
||||
}
|
||||
|
||||
func (m *Mailbox) markReadBeads(id string) error {
|
||||
cmd := exec.Command("bd", "mail", "ack", id)
|
||||
cmd := exec.Command("bd", "message", "ack", id)
|
||||
cmd.Dir = m.workDir
|
||||
|
||||
var stderr bytes.Buffer
|
||||
|
||||
@@ -25,25 +25,24 @@ func NewRouter(workDir string) *Router {
|
||||
}
|
||||
}
|
||||
|
||||
// Send delivers a message via beads mail.
|
||||
// Send delivers a message via beads message.
|
||||
func (r *Router) Send(msg *Message) error {
|
||||
// Convert addresses to beads identities
|
||||
toIdentity := addressToIdentity(msg.To)
|
||||
fromIdentity := addressToIdentity(msg.From)
|
||||
|
||||
// Build command: bd mail send <recipient> -s <subject> -m <body> --identity <sender>
|
||||
args := []string{"mail", "send", toIdentity,
|
||||
// Build command: bd message send <recipient> <body> -s <subject>
|
||||
args := []string{"message", "send", toIdentity, msg.Body,
|
||||
"-s", msg.Subject,
|
||||
"-m", msg.Body,
|
||||
"--identity", fromIdentity,
|
||||
}
|
||||
|
||||
// Add --urgent flag for high priority
|
||||
// Add importance flag for high priority
|
||||
if msg.Priority == PriorityHigh {
|
||||
args = append(args, "--urgent")
|
||||
args = append(args, "--importance", "high")
|
||||
}
|
||||
|
||||
cmd := exec.Command("bd", args...)
|
||||
cmd.Env = append(cmd.Environ(), "BEADS_AGENT_NAME="+fromIdentity)
|
||||
cmd.Dir = r.workDir
|
||||
|
||||
var stderr bytes.Buffer
|
||||
|
||||
@@ -105,7 +105,7 @@ func TestRemoveNotFound(t *testing.T) {
|
||||
}
|
||||
m := NewManager(r, git.NewGit(root))
|
||||
|
||||
err := m.Remove("nonexistent")
|
||||
err := m.Remove("nonexistent", false)
|
||||
if err != ErrPolecatNotFound {
|
||||
t.Errorf("Remove = %v, want ErrPolecatNotFound", err)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package session
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/steveyegge/gastown/internal/rig"
|
||||
@@ -36,8 +38,17 @@ func TestPolecatDir(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestHasPolecat(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
// hasPolecat checks filesystem, so create actual directories
|
||||
for _, name := range []string{"Toast", "Cheedo"} {
|
||||
if err := os.MkdirAll(filepath.Join(root, "polecats", name), 0755); err != nil {
|
||||
t.Fatalf("mkdir: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
r := &rig.Rig{
|
||||
Name: "gastown",
|
||||
Path: root,
|
||||
Polecats: []string{"Toast", "Cheedo"},
|
||||
}
|
||||
m := NewManager(tmux.NewTmux(), r)
|
||||
|
||||
@@ -311,10 +311,11 @@ func (m *Manager) loadTasksFromBeads(epicID string) ([]SwarmTask, error) {
|
||||
return nil, fmt.Errorf("epic not found: %s", epicID)
|
||||
}
|
||||
|
||||
// Extract parent-child dependents as tasks
|
||||
// Extract dependents as tasks (issues that depend on/are blocked by this epic)
|
||||
// Accept both "parent-child" and "blocks" relationships
|
||||
var tasks []SwarmTask
|
||||
for _, dep := range issues[0].Dependents {
|
||||
if dep.DependencyType != "parent-child" {
|
||||
if dep.DependencyType != "parent-child" && dep.DependencyType != "blocks" {
|
||||
continue
|
||||
}
|
||||
|
||||
|
||||
@@ -49,6 +49,14 @@ Town ({{ .TownRoot }})
|
||||
- `bd create --title="Found bug" --type=bug` - File new issue
|
||||
- `bd create --title="Need feature" --type=task` - File new task
|
||||
|
||||
### Agent UX: File Issues for CLI Surprises
|
||||
If you guess how a `gt` or `bd` command should work and it fails, file a bead!
|
||||
Example: If `gt session capture rig/polecat 50` fails but `-n 50` works, file:
|
||||
```
|
||||
bd create --title="gt session capture: Support positional line count" --type=task --priority=1
|
||||
```
|
||||
Agent-friendly UX is critical. Your guesses reveal what's intuitive.
|
||||
|
||||
### Completion
|
||||
- `gt done` - Signal work ready for merge queue
|
||||
- `bd sync` - Sync beads changes
|
||||
|
||||
Reference in New Issue
Block a user