Break down monolithic beads.go and mail.go into smaller, single-purpose files: beads package: - beads_agent.go: Agent-related bead operations - beads_delegation.go: Delegation bead handling - beads_dog.go: Dog pool operations - beads_merge_slot.go: Merge slot management - beads_mr.go: Merge request operations - beads_redirect.go: Redirect bead handling - beads_rig.go: Rig bead operations - beads_role.go: Role bead management cmd package: - mail_announce.go: Announcement subcommand - mail_check.go: Mail check subcommand - mail_identity.go: Identity management - mail_inbox.go: Inbox operations - mail_queue.go: Queue subcommand - mail_search.go: Search functionality - mail_send.go: Send subcommand - mail_thread.go: Thread operations Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
134 lines
3.8 KiB
Go
134 lines
3.8 KiB
Go
// Package beads provides merge slot management for serialized conflict resolution.
|
|
package beads
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"strings"
|
|
)
|
|
|
|
// MergeSlotStatus represents the result of checking a merge slot.
|
|
type MergeSlotStatus struct {
|
|
ID string `json:"id"`
|
|
Available bool `json:"available"`
|
|
Holder string `json:"holder,omitempty"`
|
|
Waiters []string `json:"waiters,omitempty"`
|
|
Error string `json:"error,omitempty"`
|
|
}
|
|
|
|
// MergeSlotCreate creates the merge slot bead for the current rig.
|
|
// The slot is used for serialized conflict resolution in the merge queue.
|
|
// Returns the slot ID if successful.
|
|
func (b *Beads) MergeSlotCreate() (string, error) {
|
|
out, err := b.run("merge-slot", "create", "--json")
|
|
if err != nil {
|
|
return "", fmt.Errorf("creating merge slot: %w", err)
|
|
}
|
|
|
|
var result struct {
|
|
ID string `json:"id"`
|
|
Status string `json:"status"`
|
|
}
|
|
if err := json.Unmarshal(out, &result); err != nil {
|
|
return "", fmt.Errorf("parsing merge-slot create output: %w", err)
|
|
}
|
|
|
|
return result.ID, nil
|
|
}
|
|
|
|
// MergeSlotCheck checks the availability of the merge slot.
|
|
// Returns the current status including holder and waiters if held.
|
|
func (b *Beads) MergeSlotCheck() (*MergeSlotStatus, error) {
|
|
out, err := b.run("merge-slot", "check", "--json")
|
|
if err != nil {
|
|
// Check if slot doesn't exist
|
|
if strings.Contains(err.Error(), "not found") {
|
|
return &MergeSlotStatus{Error: "not found"}, nil
|
|
}
|
|
return nil, fmt.Errorf("checking merge slot: %w", err)
|
|
}
|
|
|
|
var status MergeSlotStatus
|
|
if err := json.Unmarshal(out, &status); err != nil {
|
|
return nil, fmt.Errorf("parsing merge-slot check output: %w", err)
|
|
}
|
|
|
|
return &status, nil
|
|
}
|
|
|
|
// MergeSlotAcquire attempts to acquire the merge slot for exclusive access.
|
|
// If holder is empty, defaults to BD_ACTOR environment variable.
|
|
// If addWaiter is true and the slot is held, the requester is added to the waiters queue.
|
|
// Returns the acquisition result.
|
|
func (b *Beads) MergeSlotAcquire(holder string, addWaiter bool) (*MergeSlotStatus, error) {
|
|
args := []string{"merge-slot", "acquire", "--json"}
|
|
if holder != "" {
|
|
args = append(args, "--holder="+holder)
|
|
}
|
|
if addWaiter {
|
|
args = append(args, "--wait")
|
|
}
|
|
|
|
out, err := b.run(args...)
|
|
if err != nil {
|
|
// Parse the output even on error - it may contain useful info
|
|
var status MergeSlotStatus
|
|
if jsonErr := json.Unmarshal(out, &status); jsonErr == nil {
|
|
return &status, nil
|
|
}
|
|
return nil, fmt.Errorf("acquiring merge slot: %w", err)
|
|
}
|
|
|
|
var status MergeSlotStatus
|
|
if err := json.Unmarshal(out, &status); err != nil {
|
|
return nil, fmt.Errorf("parsing merge-slot acquire output: %w", err)
|
|
}
|
|
|
|
return &status, nil
|
|
}
|
|
|
|
// MergeSlotRelease releases the merge slot after conflict resolution completes.
|
|
// If holder is provided, it verifies the slot is held by that holder before releasing.
|
|
func (b *Beads) MergeSlotRelease(holder string) error {
|
|
args := []string{"merge-slot", "release", "--json"}
|
|
if holder != "" {
|
|
args = append(args, "--holder="+holder)
|
|
}
|
|
|
|
out, err := b.run(args...)
|
|
if err != nil {
|
|
return fmt.Errorf("releasing merge slot: %w", err)
|
|
}
|
|
|
|
var result struct {
|
|
Released bool `json:"released"`
|
|
Error string `json:"error,omitempty"`
|
|
}
|
|
if err := json.Unmarshal(out, &result); err != nil {
|
|
return fmt.Errorf("parsing merge-slot release output: %w", err)
|
|
}
|
|
|
|
if !result.Released && result.Error != "" {
|
|
return fmt.Errorf("slot release failed: %s", result.Error)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// MergeSlotEnsureExists creates the merge slot if it doesn't exist.
|
|
// This is idempotent - safe to call multiple times.
|
|
func (b *Beads) MergeSlotEnsureExists() (string, error) {
|
|
// Check if slot exists first
|
|
status, err := b.MergeSlotCheck()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
if status.Error == "not found" {
|
|
// Create it
|
|
return b.MergeSlotCreate()
|
|
}
|
|
|
|
return status.ID, nil
|
|
}
|