feat(beads): add attachment fields for pinned beads (gt-rana.1)
Implement AttachmentFields to track molecule attachments on pinned/handoff beads: - AttachedMolecule: root issue ID of attached molecule - AttachedAt: timestamp when attached API: - AttachMolecule(pinnedBeadID, moleculeID) - attach - DetachMolecule(pinnedBeadID) - detach - GetAttachment(pinnedBeadID) - query - ParseAttachmentFields(issue) - parse from description - FormatAttachmentFields(fields) - format for description Includes comprehensive tests for parsing and API. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -131,6 +131,44 @@ Example:
|
||||
RunE: runMoleculeProgress,
|
||||
}
|
||||
|
||||
var moleculeAttachCmd = &cobra.Command{
|
||||
Use: "attach <pinned-bead-id> <molecule-id>",
|
||||
Short: "Attach a molecule to a pinned bead",
|
||||
Long: `Attach a molecule to a pinned/handoff bead.
|
||||
|
||||
This records which molecule an agent is currently working on. The attachment
|
||||
is stored in the pinned bead's description and visible via 'bd show'.
|
||||
|
||||
Example:
|
||||
gt molecule attach gt-abc mol-xyz`,
|
||||
Args: cobra.ExactArgs(2),
|
||||
RunE: runMoleculeAttach,
|
||||
}
|
||||
|
||||
var moleculeDetachCmd = &cobra.Command{
|
||||
Use: "detach <pinned-bead-id>",
|
||||
Short: "Detach molecule from a pinned bead",
|
||||
Long: `Remove molecule attachment from a pinned/handoff bead.
|
||||
|
||||
This clears the attached_molecule and attached_at fields from the bead.
|
||||
|
||||
Example:
|
||||
gt molecule detach gt-abc`,
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: runMoleculeDetach,
|
||||
}
|
||||
|
||||
var moleculeAttachmentCmd = &cobra.Command{
|
||||
Use: "attachment <pinned-bead-id>",
|
||||
Short: "Show attachment status of a pinned bead",
|
||||
Long: `Show which molecule is attached to a pinned bead.
|
||||
|
||||
Example:
|
||||
gt molecule attachment gt-abc`,
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: runMoleculeAttachment,
|
||||
}
|
||||
|
||||
func init() {
|
||||
// List flags
|
||||
moleculeListCmd.Flags().BoolVar(&moleculeJSON, "json", false, "Output as JSON")
|
||||
@@ -154,6 +192,9 @@ func init() {
|
||||
// Progress flags
|
||||
moleculeProgressCmd.Flags().BoolVar(&moleculeJSON, "json", false, "Output as JSON")
|
||||
|
||||
// Attachment flags
|
||||
moleculeAttachmentCmd.Flags().BoolVar(&moleculeJSON, "json", false, "Output as JSON")
|
||||
|
||||
// Add subcommands
|
||||
moleculeCmd.AddCommand(moleculeListCmd)
|
||||
moleculeCmd.AddCommand(moleculeShowCmd)
|
||||
@@ -162,6 +203,9 @@ func init() {
|
||||
moleculeCmd.AddCommand(moleculeInstancesCmd)
|
||||
moleculeCmd.AddCommand(moleculeExportCmd)
|
||||
moleculeCmd.AddCommand(moleculeProgressCmd)
|
||||
moleculeCmd.AddCommand(moleculeAttachCmd)
|
||||
moleculeCmd.AddCommand(moleculeDetachCmd)
|
||||
moleculeCmd.AddCommand(moleculeAttachmentCmd)
|
||||
|
||||
rootCmd.AddCommand(moleculeCmd)
|
||||
}
|
||||
@@ -818,3 +862,120 @@ func extractMoleculeID(description string) string {
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func runMoleculeAttach(cmd *cobra.Command, args []string) error {
|
||||
pinnedBeadID := args[0]
|
||||
moleculeID := args[1]
|
||||
|
||||
workDir, err := findLocalBeadsDir()
|
||||
if err != nil {
|
||||
return fmt.Errorf("not in a beads workspace: %w", err)
|
||||
}
|
||||
|
||||
b := beads.New(workDir)
|
||||
|
||||
// Attach the molecule
|
||||
issue, err := b.AttachMolecule(pinnedBeadID, moleculeID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("attaching molecule: %w", err)
|
||||
}
|
||||
|
||||
attachment := beads.ParseAttachmentFields(issue)
|
||||
fmt.Printf("%s Attached %s to %s\n", style.Bold.Render("✓"), moleculeID, pinnedBeadID)
|
||||
if attachment != nil && attachment.AttachedAt != "" {
|
||||
fmt.Printf(" attached_at: %s\n", attachment.AttachedAt)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func runMoleculeDetach(cmd *cobra.Command, args []string) error {
|
||||
pinnedBeadID := args[0]
|
||||
|
||||
workDir, err := findLocalBeadsDir()
|
||||
if err != nil {
|
||||
return fmt.Errorf("not in a beads workspace: %w", err)
|
||||
}
|
||||
|
||||
b := beads.New(workDir)
|
||||
|
||||
// Check current attachment first
|
||||
attachment, err := b.GetAttachment(pinnedBeadID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("checking attachment: %w", err)
|
||||
}
|
||||
|
||||
if attachment == nil {
|
||||
fmt.Printf("%s No molecule attached to %s\n", style.Dim.Render("ℹ"), pinnedBeadID)
|
||||
return nil
|
||||
}
|
||||
|
||||
previousMolecule := attachment.AttachedMolecule
|
||||
|
||||
// Detach the molecule
|
||||
_, err = b.DetachMolecule(pinnedBeadID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("detaching molecule: %w", err)
|
||||
}
|
||||
|
||||
fmt.Printf("%s Detached %s from %s\n", style.Bold.Render("✓"), previousMolecule, pinnedBeadID)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func runMoleculeAttachment(cmd *cobra.Command, args []string) error {
|
||||
pinnedBeadID := args[0]
|
||||
|
||||
workDir, err := findLocalBeadsDir()
|
||||
if err != nil {
|
||||
return fmt.Errorf("not in a beads workspace: %w", err)
|
||||
}
|
||||
|
||||
b := beads.New(workDir)
|
||||
|
||||
// Get the issue
|
||||
issue, err := b.Show(pinnedBeadID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("getting issue: %w", err)
|
||||
}
|
||||
|
||||
attachment := beads.ParseAttachmentFields(issue)
|
||||
|
||||
if moleculeJSON {
|
||||
type attachmentOutput struct {
|
||||
IssueID string `json:"issue_id"`
|
||||
IssueTitle string `json:"issue_title"`
|
||||
Status string `json:"status"`
|
||||
AttachedMolecule string `json:"attached_molecule,omitempty"`
|
||||
AttachedAt string `json:"attached_at,omitempty"`
|
||||
}
|
||||
out := attachmentOutput{
|
||||
IssueID: issue.ID,
|
||||
IssueTitle: issue.Title,
|
||||
Status: issue.Status,
|
||||
}
|
||||
if attachment != nil {
|
||||
out.AttachedMolecule = attachment.AttachedMolecule
|
||||
out.AttachedAt = attachment.AttachedAt
|
||||
}
|
||||
enc := json.NewEncoder(os.Stdout)
|
||||
enc.SetIndent("", " ")
|
||||
return enc.Encode(out)
|
||||
}
|
||||
|
||||
// Human-readable output
|
||||
fmt.Printf("\n%s: %s\n", style.Bold.Render(issue.ID), issue.Title)
|
||||
fmt.Printf("Status: %s\n", issue.Status)
|
||||
|
||||
if attachment == nil || attachment.AttachedMolecule == "" {
|
||||
fmt.Printf("\n%s\n", style.Dim.Render("No molecule attached"))
|
||||
} else {
|
||||
fmt.Printf("\n%s\n", style.Bold.Render("Attached Molecule:"))
|
||||
fmt.Printf(" ID: %s\n", attachment.AttachedMolecule)
|
||||
if attachment.AttachedAt != "" {
|
||||
fmt.Printf(" Attached at: %s\n", attachment.AttachedAt)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user