Implement handoffs using pinned beads (gt-cu7r)

Replace mail-based handoff system with pinned beads that persist
across sessions. This fixes the issue where handoff messages get
closed before successors can read them.

Changes:
- beads: Add StatusPinned constant and handoff functions:
  - HandoffBeadTitle() for well-known naming
  - FindHandoffBead() to locate role handoff bead
  - GetOrCreateHandoffBead() to ensure bead exists
  - UpdateHandoffContent() to set handoff message
  - ClearHandoffContent() to reset after reading

- cmd/handoff: Update to use pinned beads instead of mail
  - sendHandoffMail() now updates pinned bead content

- cmd/prime: Display handoff content on startup
  - outputHandoffContent() reads and shows handoff bead

- cmd/rig: Add reset command with --handoff flag
  - gt rig reset --handoff clears handoff content

Generated with Claude Code

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Steve Yegge
2025-12-19 01:51:14 -08:00
parent 717bc89132
commit 4edacde590
4 changed files with 196 additions and 31 deletions

View File

@@ -383,6 +383,88 @@ func (b *Beads) IsBeadsRepo() bool {
return err == nil || !errors.Is(err, ErrNotARepo)
}
// StatusPinned is the status for pinned beads that never get closed.
const StatusPinned = "pinned"
// HandoffBeadTitle returns the well-known title for a role's handoff bead.
func HandoffBeadTitle(role string) string {
return role + " Handoff"
}
// FindHandoffBead finds the pinned handoff bead for a role by title.
// Returns nil if not found (not an error).
func (b *Beads) FindHandoffBead(role string) (*Issue, error) {
issues, err := b.List(ListOptions{Status: StatusPinned, Priority: -1})
if err != nil {
return nil, fmt.Errorf("listing pinned issues: %w", err)
}
targetTitle := HandoffBeadTitle(role)
for _, issue := range issues {
if issue.Title == targetTitle {
return issue, nil
}
}
return nil, nil
}
// GetOrCreateHandoffBead returns the handoff bead for a role, creating it if needed.
func (b *Beads) GetOrCreateHandoffBead(role string) (*Issue, error) {
// Check if it exists
existing, err := b.FindHandoffBead(role)
if err != nil {
return nil, err
}
if existing != nil {
return existing, nil
}
// Create new handoff bead
issue, err := b.Create(CreateOptions{
Title: HandoffBeadTitle(role),
Type: "task",
Priority: 2,
Description: "", // Empty until first handoff
})
if err != nil {
return nil, fmt.Errorf("creating handoff bead: %w", err)
}
// Update to pinned status
status := StatusPinned
if err := b.Update(issue.ID, UpdateOptions{Status: &status}); err != nil {
return nil, fmt.Errorf("setting handoff bead to pinned: %w", err)
}
// Re-fetch to get updated status
return b.Show(issue.ID)
}
// UpdateHandoffContent updates the handoff bead's description with new content.
func (b *Beads) UpdateHandoffContent(role, content string) error {
issue, err := b.GetOrCreateHandoffBead(role)
if err != nil {
return err
}
return b.Update(issue.ID, UpdateOptions{Description: &content})
}
// ClearHandoffContent clears the handoff bead's description.
func (b *Beads) ClearHandoffContent(role string) error {
issue, err := b.FindHandoffBead(role)
if err != nil {
return err
}
if issue == nil {
return nil // Nothing to clear
}
empty := ""
return b.Update(issue.ID, UpdateOptions{Description: &empty})
}
// MRFields holds the structured fields for a merge-request issue.
// These fields are stored as key: value lines in the issue description.
type MRFields struct {