gt sling --force: return displaced work to ready pool (gt-o40t)
When slinging with --force to an agent with occupied hook, the displaced molecule is now returned to the ready pool rather than silently orphaned. Changes: - Modified checkHookCollision to take force param and return displaced ID - Added releaseDisplacedWork helper to unpin and release displaced molecules - Updated all 5 sling handlers (polecat, crew, witness, refinery, mayor) Behavior: - Without --force: still errors "hook already occupied by X" - With --force: prints warning, releases old work, proceeds with new sling 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -350,10 +350,15 @@ func slingToPolecat(townRoot string, target *SlingTarget, thing *SlingThing) err
|
|||||||
polecatExists := err == nil
|
polecatExists := err == nil
|
||||||
|
|
||||||
if polecatExists {
|
if polecatExists {
|
||||||
// Check for existing work on hook (unless --force)
|
// Check for existing work on hook
|
||||||
if !slingForce {
|
displacedID, err := checkHookCollision(polecatAddress, r.Path, slingForce)
|
||||||
if err := checkHookCollision(polecatAddress, r.Path); err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
}
|
||||||
|
if displacedID != "" {
|
||||||
|
fmt.Printf("%s Displaced %s back to ready pool\n", style.Warning.Render("⚠"), displacedID)
|
||||||
|
if err := releaseDisplacedWork(r.Path, displacedID); err != nil {
|
||||||
|
fmt.Printf(" %s could not release: %v\n", style.Dim.Render("Warning:"), err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -595,10 +600,15 @@ func slingToCrew(townRoot string, target *SlingTarget, thing *SlingThing) error
|
|||||||
return fmt.Errorf("crew member '%s' not found at %s", target.Name, crewPath)
|
return fmt.Errorf("crew member '%s' not found at %s", target.Name, crewPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for existing work on hook (unless --force)
|
// Check for existing work on hook
|
||||||
if !slingForce {
|
displacedID, err := checkHookCollision(crewAddress, beadsPath, slingForce)
|
||||||
if err := checkHookCollision(crewAddress, beadsPath); err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
}
|
||||||
|
if displacedID != "" {
|
||||||
|
fmt.Printf("%s Displaced %s back to ready pool\n", style.Warning.Render("⚠"), displacedID)
|
||||||
|
if err := releaseDisplacedWork(beadsPath, displacedID); err != nil {
|
||||||
|
fmt.Printf(" %s could not release: %v\n", style.Dim.Render("Warning:"), err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -610,7 +620,6 @@ func slingToCrew(townRoot string, target *SlingTarget, thing *SlingThing) error
|
|||||||
// Process the thing based on its kind
|
// Process the thing based on its kind
|
||||||
var issueID string
|
var issueID string
|
||||||
var moleculeCtx *MoleculeContext
|
var moleculeCtx *MoleculeContext
|
||||||
var err error
|
|
||||||
|
|
||||||
switch thing.Kind {
|
switch thing.Kind {
|
||||||
case "proto":
|
case "proto":
|
||||||
@@ -683,10 +692,15 @@ func slingToWitness(townRoot string, target *SlingTarget, thing *SlingThing) err
|
|||||||
style.Dim.Render("Note:"))
|
style.Dim.Render("Note:"))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for existing work on hook (unless --force)
|
// Check for existing work on hook
|
||||||
if !slingForce {
|
displacedID, err := checkHookCollision(witnessAddress, beadsPath, slingForce)
|
||||||
if err := checkHookCollision(witnessAddress, beadsPath); err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
}
|
||||||
|
if displacedID != "" {
|
||||||
|
fmt.Printf("%s Displaced %s back to ready pool\n", style.Warning.Render("⚠"), displacedID)
|
||||||
|
if err := releaseDisplacedWork(beadsPath, displacedID); err != nil {
|
||||||
|
fmt.Printf(" %s could not release: %v\n", style.Dim.Render("Warning:"), err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -698,7 +712,6 @@ func slingToWitness(townRoot string, target *SlingTarget, thing *SlingThing) err
|
|||||||
// Process the thing
|
// Process the thing
|
||||||
var issueID string
|
var issueID string
|
||||||
var moleculeCtx *MoleculeContext
|
var moleculeCtx *MoleculeContext
|
||||||
var err error
|
|
||||||
|
|
||||||
switch thing.Kind {
|
switch thing.Kind {
|
||||||
case "proto":
|
case "proto":
|
||||||
@@ -741,10 +754,15 @@ func slingToRefinery(townRoot string, target *SlingTarget, thing *SlingThing) er
|
|||||||
beadsPath := filepath.Join(townRoot, target.Rig)
|
beadsPath := filepath.Join(townRoot, target.Rig)
|
||||||
refineryAddress := fmt.Sprintf("%s/refinery", target.Rig)
|
refineryAddress := fmt.Sprintf("%s/refinery", target.Rig)
|
||||||
|
|
||||||
// Check for existing work on hook (unless --force)
|
// Check for existing work on hook
|
||||||
if !slingForce {
|
displacedID, err := checkHookCollision(refineryAddress, beadsPath, slingForce)
|
||||||
if err := checkHookCollision(refineryAddress, beadsPath); err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
}
|
||||||
|
if displacedID != "" {
|
||||||
|
fmt.Printf("%s Displaced %s back to ready pool\n", style.Warning.Render("⚠"), displacedID)
|
||||||
|
if err := releaseDisplacedWork(beadsPath, displacedID); err != nil {
|
||||||
|
fmt.Printf(" %s could not release: %v\n", style.Dim.Render("Warning:"), err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -756,7 +774,6 @@ func slingToRefinery(townRoot string, target *SlingTarget, thing *SlingThing) er
|
|||||||
// Process the thing
|
// Process the thing
|
||||||
var issueID string
|
var issueID string
|
||||||
var moleculeCtx *MoleculeContext
|
var moleculeCtx *MoleculeContext
|
||||||
var err error
|
|
||||||
|
|
||||||
switch thing.Kind {
|
switch thing.Kind {
|
||||||
case "proto":
|
case "proto":
|
||||||
@@ -802,10 +819,15 @@ func slingToMayor(townRoot string, target *SlingTarget, thing *SlingThing) error
|
|||||||
beadsPath := townRoot
|
beadsPath := townRoot
|
||||||
mayorAddress := "mayor/"
|
mayorAddress := "mayor/"
|
||||||
|
|
||||||
// Check for existing work on hook (unless --force)
|
// Check for existing work on hook
|
||||||
if !slingForce {
|
displacedID, err := checkHookCollision(mayorAddress, beadsPath, slingForce)
|
||||||
if err := checkHookCollision(mayorAddress, beadsPath); err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
}
|
||||||
|
if displacedID != "" {
|
||||||
|
fmt.Printf("%s Displaced %s back to ready pool\n", style.Warning.Render("⚠"), displacedID)
|
||||||
|
if err := releaseDisplacedWork(beadsPath, displacedID); err != nil {
|
||||||
|
fmt.Printf(" %s could not release: %v\n", style.Dim.Render("Warning:"), err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -817,7 +839,6 @@ func slingToMayor(townRoot string, target *SlingTarget, thing *SlingThing) error
|
|||||||
// Process the thing
|
// Process the thing
|
||||||
var issueID string
|
var issueID string
|
||||||
var moleculeCtx *MoleculeContext
|
var moleculeCtx *MoleculeContext
|
||||||
var err error
|
|
||||||
|
|
||||||
switch thing.Kind {
|
switch thing.Kind {
|
||||||
case "proto":
|
case "proto":
|
||||||
@@ -1000,7 +1021,10 @@ func spawnMoleculeOnIssue(beadsPath string, thing *SlingThing, assignee string)
|
|||||||
}
|
}
|
||||||
|
|
||||||
// checkHookCollision checks if the agent's hook already has work.
|
// checkHookCollision checks if the agent's hook already has work.
|
||||||
func checkHookCollision(agentAddress, beadsPath string) error {
|
// If force is true and hook is occupied, returns the displaced molecule ID.
|
||||||
|
// If force is false and hook is occupied, returns an error.
|
||||||
|
// Returns ("", nil) if hook is empty.
|
||||||
|
func checkHookCollision(agentAddress, beadsPath string, force bool) (string, error) {
|
||||||
// Parse agent address to get the role for handoff bead lookup
|
// Parse agent address to get the role for handoff bead lookup
|
||||||
parts := strings.Split(agentAddress, "/")
|
parts := strings.Split(agentAddress, "/")
|
||||||
var role string
|
var role string
|
||||||
@@ -1014,19 +1038,42 @@ func checkHookCollision(agentAddress, beadsPath string) error {
|
|||||||
handoff, err := b.FindHandoffBead(role)
|
handoff, err := b.FindHandoffBead(role)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// Can't check, assume OK
|
// Can't check, assume OK
|
||||||
return nil
|
return "", nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if handoff == nil {
|
if handoff == nil {
|
||||||
// No handoff bead exists, no collision
|
// No handoff bead exists, no collision
|
||||||
return nil
|
return "", nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if there's an attached molecule
|
// Check if there's an attached molecule
|
||||||
attachment := beads.ParseAttachmentFields(handoff)
|
attachment := beads.ParseAttachmentFields(handoff)
|
||||||
if attachment != nil && attachment.AttachedMolecule != "" {
|
if attachment != nil && attachment.AttachedMolecule != "" {
|
||||||
return fmt.Errorf("hook already occupied by %s\nUse --force to re-sling",
|
if !force {
|
||||||
attachment.AttachedMolecule)
|
return "", fmt.Errorf("hook already occupied by %s\nUse --force to re-sling",
|
||||||
|
attachment.AttachedMolecule)
|
||||||
|
}
|
||||||
|
// Force mode: return the displaced molecule ID
|
||||||
|
return attachment.AttachedMolecule, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// releaseDisplacedWork returns displaced work to the ready pool.
|
||||||
|
// It unpins the molecule and sets status back to open with cleared assignee.
|
||||||
|
func releaseDisplacedWork(beadsPath, displacedID string) error {
|
||||||
|
b := beads.New(beadsPath)
|
||||||
|
|
||||||
|
// Unpin the molecule
|
||||||
|
if err := b.Unpin(displacedID); err != nil {
|
||||||
|
// Non-fatal, continue with release
|
||||||
|
fmt.Printf(" %s could not unpin %s: %v\n", style.Dim.Render("Note:"), displacedID, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Release: set status=open, clear assignee
|
||||||
|
if err := b.ReleaseWithReason(displacedID, "displaced by new sling"); err != nil {
|
||||||
|
return fmt.Errorf("releasing displaced work: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
Reference in New Issue
Block a user