diff --git a/cmd/bd/duplicate.go b/cmd/bd/duplicate.go index ea4f0d6a..db3542f9 100644 --- a/cmd/bd/duplicate.go +++ b/cmd/bd/duplicate.go @@ -112,13 +112,25 @@ func runDuplicate(cmd *cobra.Command, args []string) error { } // Update the duplicate issue with duplicate_of and close it - updates := map[string]interface{}{ - "duplicate_of": canonicalID, - "status": string(types.StatusClosed), - } - - if err := store.UpdateIssue(ctx, duplicateID, updates, actor); err != nil { - return fmt.Errorf("failed to mark as duplicate: %w", err) + closedStatus := string(types.StatusClosed) + if daemonClient != nil { + // Use RPC for daemon mode (bd-fu83) + _, err := daemonClient.Update(&rpc.UpdateArgs{ + ID: duplicateID, + DuplicateOf: &canonicalID, + Status: &closedStatus, + }) + if err != nil { + return fmt.Errorf("failed to mark as duplicate: %w", err) + } + } else { + updates := map[string]interface{}{ + "duplicate_of": canonicalID, + "status": closedStatus, + } + if err := store.UpdateIssue(ctx, duplicateID, updates, actor); err != nil { + return fmt.Errorf("failed to mark as duplicate: %w", err) + } } // Trigger auto-flush @@ -199,13 +211,25 @@ func runSupersede(cmd *cobra.Command, args []string) error { } // Update the old issue with superseded_by and close it - updates := map[string]interface{}{ - "superseded_by": newID, - "status": string(types.StatusClosed), - } - - if err := store.UpdateIssue(ctx, oldID, updates, actor); err != nil { - return fmt.Errorf("failed to mark as superseded: %w", err) + closedStatus := string(types.StatusClosed) + if daemonClient != nil { + // Use RPC for daemon mode (bd-fu83) + _, err := daemonClient.Update(&rpc.UpdateArgs{ + ID: oldID, + SupersededBy: &newID, + Status: &closedStatus, + }) + if err != nil { + return fmt.Errorf("failed to mark as superseded: %w", err) + } + } else { + updates := map[string]interface{}{ + "superseded_by": newID, + "status": closedStatus, + } + if err := store.UpdateIssue(ctx, oldID, updates, actor); err != nil { + return fmt.Errorf("failed to mark as superseded: %w", err) + } } // Trigger auto-flush diff --git a/cmd/bd/relate.go b/cmd/bd/relate.go index eb71db9b..9eac1e0e 100644 --- a/cmd/bd/relate.go +++ b/cmd/bd/relate.go @@ -122,20 +122,44 @@ func runRelate(cmd *cobra.Command, args []string) error { // Add id2 to issue1's relates_to if not already present if !contains(issue1.RelatesTo, id2) { newRelatesTo1 := append(issue1.RelatesTo, id2) - if err := store.UpdateIssue(ctx, id1, map[string]interface{}{ - "relates_to": formatRelatesTo(newRelatesTo1), - }, actor); err != nil { - return fmt.Errorf("failed to update %s: %w", id1, err) + formattedRelatesTo1 := formatRelatesTo(newRelatesTo1) + if daemonClient != nil { + // Use RPC for daemon mode (bd-fu83) + _, err := daemonClient.Update(&rpc.UpdateArgs{ + ID: id1, + RelatesTo: &formattedRelatesTo1, + }) + if err != nil { + return fmt.Errorf("failed to update %s: %w", id1, err) + } + } else { + if err := store.UpdateIssue(ctx, id1, map[string]interface{}{ + "relates_to": formattedRelatesTo1, + }, actor); err != nil { + return fmt.Errorf("failed to update %s: %w", id1, err) + } } } // Add id1 to issue2's relates_to if not already present if !contains(issue2.RelatesTo, id1) { newRelatesTo2 := append(issue2.RelatesTo, id1) - if err := store.UpdateIssue(ctx, id2, map[string]interface{}{ - "relates_to": formatRelatesTo(newRelatesTo2), - }, actor); err != nil { - return fmt.Errorf("failed to update %s: %w", id2, err) + formattedRelatesTo2 := formatRelatesTo(newRelatesTo2) + if daemonClient != nil { + // Use RPC for daemon mode (bd-fu83) + _, err := daemonClient.Update(&rpc.UpdateArgs{ + ID: id2, + RelatesTo: &formattedRelatesTo2, + }) + if err != nil { + return fmt.Errorf("failed to update %s: %w", id2, err) + } + } else { + if err := store.UpdateIssue(ctx, id2, map[string]interface{}{ + "relates_to": formattedRelatesTo2, + }, actor); err != nil { + return fmt.Errorf("failed to update %s: %w", id2, err) + } } } @@ -232,18 +256,42 @@ func runUnrelate(cmd *cobra.Command, args []string) error { // Remove id2 from issue1's relates_to newRelatesTo1 := remove(issue1.RelatesTo, id2) - if err := store.UpdateIssue(ctx, id1, map[string]interface{}{ - "relates_to": formatRelatesTo(newRelatesTo1), - }, actor); err != nil { - return fmt.Errorf("failed to update %s: %w", id1, err) + formattedRelatesTo1 := formatRelatesTo(newRelatesTo1) + if daemonClient != nil { + // Use RPC for daemon mode (bd-fu83) + _, err := daemonClient.Update(&rpc.UpdateArgs{ + ID: id1, + RelatesTo: &formattedRelatesTo1, + }) + if err != nil { + return fmt.Errorf("failed to update %s: %w", id1, err) + } + } else { + if err := store.UpdateIssue(ctx, id1, map[string]interface{}{ + "relates_to": formattedRelatesTo1, + }, actor); err != nil { + return fmt.Errorf("failed to update %s: %w", id1, err) + } } // Remove id1 from issue2's relates_to newRelatesTo2 := remove(issue2.RelatesTo, id1) - if err := store.UpdateIssue(ctx, id2, map[string]interface{}{ - "relates_to": formatRelatesTo(newRelatesTo2), - }, actor); err != nil { - return fmt.Errorf("failed to update %s: %w", id2, err) + formattedRelatesTo2 := formatRelatesTo(newRelatesTo2) + if daemonClient != nil { + // Use RPC for daemon mode (bd-fu83) + _, err := daemonClient.Update(&rpc.UpdateArgs{ + ID: id2, + RelatesTo: &formattedRelatesTo2, + }) + if err != nil { + return fmt.Errorf("failed to update %s: %w", id2, err) + } + } else { + if err := store.UpdateIssue(ctx, id2, map[string]interface{}{ + "relates_to": formattedRelatesTo2, + }, actor); err != nil { + return fmt.Errorf("failed to update %s: %w", id2, err) + } } // Trigger auto-flush diff --git a/internal/rpc/protocol.go b/internal/rpc/protocol.go index 55591ed2..aae6aed8 100644 --- a/internal/rpc/protocol.go +++ b/internal/rpc/protocol.go @@ -99,6 +99,10 @@ type UpdateArgs struct { Sender *string `json:"sender,omitempty"` // Who sent this (for messages) Ephemeral *bool `json:"ephemeral,omitempty"` // Can be bulk-deleted when closed RepliesTo *string `json:"replies_to,omitempty"` // Issue ID for conversation threading + // Graph link fields (bd-fu83) + RelatesTo *string `json:"relates_to,omitempty"` // JSON array of related issue IDs + DuplicateOf *string `json:"duplicate_of,omitempty"` // Canonical issue ID if duplicate + SupersededBy *string `json:"superseded_by,omitempty"` // Replacement issue ID if obsolete } // CloseArgs represents arguments for the close operation diff --git a/internal/rpc/server_issues_epics.go b/internal/rpc/server_issues_epics.go index 3bcbca3b..17136231 100644 --- a/internal/rpc/server_issues_epics.go +++ b/internal/rpc/server_issues_epics.go @@ -87,6 +87,16 @@ func updatesFromArgs(a UpdateArgs) map[string]interface{} { if a.RepliesTo != nil { u["replies_to"] = *a.RepliesTo } + // Graph link fields (bd-fu83) + if a.RelatesTo != nil { + u["relates_to"] = *a.RelatesTo + } + if a.DuplicateOf != nil { + u["duplicate_of"] = *a.DuplicateOf + } + if a.SupersededBy != nil { + u["superseded_by"] = *a.SupersededBy + } return u }