From 35f8e197ee41b95e85258ec39653b7e57302c9a7 Mon Sep 17 00:00:00 2001 From: Steve Yegge Date: Sat, 27 Dec 2025 22:17:03 -0800 Subject: [PATCH] feat: add --parent flag to bd update for reparenting issues (bd-cj2e) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Allows reparenting issues to a different epic/parent: bd update bd-xyz --parent=bd-epic Implementation: - Add Parent field to UpdateArgs in protocol.go - Handle reparenting in RPC handler (server_issues_epics.go) - Add --parent flag to updateCmd with both daemon and direct mode support - Remove old parent-child dependency before adding new one - Pass empty string to remove parent entirely 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- cmd/bd/show.go | 54 ++++++++++++++++++++++++- internal/rpc/protocol.go | 2 + internal/rpc/server_issues_epics.go | 61 ++++++++++++++++++++++++++++- 3 files changed, 114 insertions(+), 3 deletions(-) diff --git a/cmd/bd/show.go b/cmd/bd/show.go index d80cb9a5..560e49dd 100644 --- a/cmd/bd/show.go +++ b/cmd/bd/show.go @@ -635,6 +635,10 @@ var updateCmd = &cobra.Command{ setLabels, _ := cmd.Flags().GetStringSlice("set-labels") updates["set_labels"] = setLabels } + if cmd.Flags().Changed("parent") { + parent, _ := cmd.Flags().GetString("parent") + updates["parent"] = parent + } if cmd.Flags().Changed("type") { issueType, _ := cmd.Flags().GetString("type") // Validate issue type @@ -726,6 +730,9 @@ var updateCmd = &cobra.Command{ if issueType, ok := updates["issue_type"].(string); ok { updateArgs.IssueType = &issueType } + if parent, ok := updates["parent"].(string); ok { + updateArgs.Parent = &parent + } resp, err := daemonClient.Update(updateArgs) if err != nil { @@ -771,7 +778,7 @@ var updateCmd = &cobra.Command{ // Apply regular field updates if any regularUpdates := make(map[string]interface{}) for k, v := range updates { - if k != "add_labels" && k != "remove_labels" && k != "set_labels" { + if k != "add_labels" && k != "remove_labels" && k != "set_labels" && k != "parent" { regularUpdates[k] = v } } @@ -800,6 +807,50 @@ var updateCmd = &cobra.Command{ } } + // Handle parent reparenting (bd-cj2e) + if newParent, ok := updates["parent"].(string); ok { + // Validate new parent exists (unless empty string to remove parent) + if newParent != "" { + parentIssue, err := store.GetIssue(ctx, newParent) + if err != nil { + fmt.Fprintf(os.Stderr, "Error getting parent %s: %v\n", newParent, err) + continue + } + if parentIssue == nil { + fmt.Fprintf(os.Stderr, "Error: parent issue %s not found\n", newParent) + continue + } + } + + // Find and remove existing parent-child dependency + deps, err := store.GetDependencyRecords(ctx, id) + if err != nil { + fmt.Fprintf(os.Stderr, "Error getting dependencies for %s: %v\n", id, err) + continue + } + for _, dep := range deps { + if dep.Type == types.DepParentChild { + if err := store.RemoveDependency(ctx, id, dep.DependsOnID, actor); err != nil { + fmt.Fprintf(os.Stderr, "Error removing old parent dependency: %v\n", err) + } + break + } + } + + // Add new parent-child dependency (if not removing parent) + if newParent != "" { + newDep := &types.Dependency{ + IssueID: id, + DependsOnID: newParent, + Type: types.DepParentChild, + } + if err := store.AddDependency(ctx, newDep, actor); err != nil { + fmt.Fprintf(os.Stderr, "Error adding parent dependency: %v\n", err) + continue + } + } + } + // Run update hook (bd-kwro.8) updatedIssue, _ := store.GetIssue(ctx, id) if updatedIssue != nil && hookRunner != nil { @@ -1496,6 +1547,7 @@ func init() { updateCmd.Flags().StringSlice("add-label", nil, "Add labels (repeatable)") updateCmd.Flags().StringSlice("remove-label", nil, "Remove labels (repeatable)") updateCmd.Flags().StringSlice("set-labels", nil, "Set labels, replacing all existing (repeatable)") + updateCmd.Flags().String("parent", "", "New parent issue ID (reparents the issue, use empty string to remove parent)") rootCmd.AddCommand(updateCmd) editCmd.Flags().Bool("title", false, "Edit the title") diff --git a/internal/rpc/protocol.go b/internal/rpc/protocol.go index e6099c94..becf0700 100644 --- a/internal/rpc/protocol.go +++ b/internal/rpc/protocol.go @@ -124,6 +124,8 @@ type UpdateArgs struct { SupersededBy *string `json:"superseded_by,omitempty"` // Replacement issue ID if obsolete // Pinned field (bd-iea) Pinned *bool `json:"pinned,omitempty"` // If true, issue is a persistent context marker + // Reparenting field (bd-cj2e) + Parent *string `json:"parent,omitempty"` // New parent issue ID (reparents the issue) } // CloseArgs represents arguments for the close operation diff --git a/internal/rpc/server_issues_epics.go b/internal/rpc/server_issues_epics.go index 6998f4d6..6763e77e 100644 --- a/internal/rpc/server_issues_epics.go +++ b/internal/rpc/server_issues_epics.go @@ -466,8 +466,65 @@ func (s *Server) handleUpdate(req *Request) Response { } } - // Emit mutation event for event-driven daemon (only if any updates or label operations were performed) - if len(updates) > 0 || len(updateArgs.SetLabels) > 0 || len(updateArgs.AddLabels) > 0 || len(updateArgs.RemoveLabels) > 0 { + // Handle reparenting (bd-cj2e) + if updateArgs.Parent != nil { + newParentID := *updateArgs.Parent + + // Validate new parent exists (unless empty string to remove parent) + if newParentID != "" { + newParent, err := store.GetIssue(ctx, newParentID) + if err != nil { + return Response{ + Success: false, + Error: fmt.Sprintf("failed to get new parent: %v", err), + } + } + if newParent == nil { + return Response{ + Success: false, + Error: fmt.Sprintf("parent issue %s not found", newParentID), + } + } + } + + // Find and remove existing parent-child dependency + deps, err := store.GetDependencyRecords(ctx, updateArgs.ID) + if err != nil { + return Response{ + Success: false, + Error: fmt.Sprintf("failed to get dependencies: %v", err), + } + } + for _, dep := range deps { + if dep.Type == types.DepParentChild { + if err := store.RemoveDependency(ctx, updateArgs.ID, dep.DependsOnID, actor); err != nil { + return Response{ + Success: false, + Error: fmt.Sprintf("failed to remove old parent dependency: %v", err), + } + } + break // Only one parent-child dependency expected + } + } + + // Add new parent-child dependency (if not removing parent) + if newParentID != "" { + newDep := &types.Dependency{ + IssueID: updateArgs.ID, + DependsOnID: newParentID, + Type: types.DepParentChild, + } + if err := store.AddDependency(ctx, newDep, actor); err != nil { + return Response{ + Success: false, + Error: fmt.Sprintf("failed to add parent dependency: %v", err), + } + } + } + } + + // Emit mutation event for event-driven daemon (only if any updates or label/parent operations were performed) + if len(updates) > 0 || len(updateArgs.SetLabels) > 0 || len(updateArgs.AddLabels) > 0 || len(updateArgs.RemoveLabels) > 0 || updateArgs.Parent != nil { // Check if this was a status change - emit rich MutationStatus event if updateArgs.Status != nil && *updateArgs.Status != string(issue.Status) { s.emitRichMutation(MutationEvent{