fix: bd update supports cross-rig bead updates via prefix routing (gt-wq1wb)

The bd update command now checks needsRouting() before attempting daemon
RPC resolution. When an issue ID (like hq-eggh5) routes to a different
beads directory, the update bypasses the daemon and uses direct mode with
the routed storage.

This enables polecats in gastown to update HQ beads (hq-* prefix) and
vice versa. The fix mirrors the routing pattern already used by bd show.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
gastown/polecats/nux
2025-12-31 13:22:52 -08:00
committed by Steve Yegge
parent eb0cc50ce6
commit 628ab911a0
2 changed files with 985 additions and 880 deletions

File diff suppressed because one or more lines are too long

View File

@@ -143,10 +143,17 @@ create, update, show, or close operation).`,
ctx := rootCtx
// Resolve partial IDs first
// Resolve partial IDs first, checking for cross-rig routing
var resolvedIDs []string
var routedArgs []string // IDs that need cross-repo routing (bypass daemon)
if daemonClient != nil {
// In daemon mode, resolve via RPC - but check routing first
for _, id := range args {
// Check if this ID needs routing to a different beads directory
if needsRouting(id) {
routedArgs = append(routedArgs, id)
continue
}
resolveArgs := &rpc.ResolveIDArgs{ID: id}
resp, err := daemonClient.ResolveID(resolveArgs)
if err != nil {
@@ -252,6 +259,104 @@ create, update, show, or close operation).`,
}
}
// Handle routed IDs via direct mode (bypass daemon)
for _, id := range routedArgs {
result, err := resolveAndGetIssueWithRouting(ctx, store, id)
if err != nil {
if result != nil {
result.Close()
}
fmt.Fprintf(os.Stderr, "Error resolving %s: %v\n", id, err)
continue
}
if result == nil || result.Issue == nil {
if result != nil {
result.Close()
}
fmt.Fprintf(os.Stderr, "Issue %s not found\n", id)
continue
}
issue := result.Issue
issueStore := result.Store
if err := validateIssueUpdatable(id, issue); err != nil {
fmt.Fprintf(os.Stderr, "%s\n", err)
result.Close()
continue
}
// Handle claim operation atomically
if claimFlag {
if issue.Assignee != "" {
fmt.Fprintf(os.Stderr, "Error claiming %s: already claimed by %s\n", id, issue.Assignee)
result.Close()
continue
}
claimUpdates := map[string]interface{}{
"assignee": actor,
"status": "in_progress",
}
if err := issueStore.UpdateIssue(ctx, result.ResolvedID, claimUpdates, actor); err != nil {
fmt.Fprintf(os.Stderr, "Error claiming %s: %v\n", id, err)
result.Close()
continue
}
}
// 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" && k != "parent" {
regularUpdates[k] = v
}
}
if len(regularUpdates) > 0 {
if err := issueStore.UpdateIssue(ctx, result.ResolvedID, regularUpdates, actor); err != nil {
fmt.Fprintf(os.Stderr, "Error updating %s: %v\n", id, err)
result.Close()
continue
}
}
// Handle label operations
var setLabels, addLabels, removeLabels []string
if v, ok := updates["set_labels"].([]string); ok {
setLabels = v
}
if v, ok := updates["add_labels"].([]string); ok {
addLabels = v
}
if v, ok := updates["remove_labels"].([]string); ok {
removeLabels = v
}
if len(setLabels) > 0 || len(addLabels) > 0 || len(removeLabels) > 0 {
if err := applyLabelUpdates(ctx, issueStore, result.ResolvedID, actor, setLabels, addLabels, removeLabels); err != nil {
fmt.Fprintf(os.Stderr, "Error updating labels for %s: %v\n", id, err)
result.Close()
continue
}
}
// Run update hook
updatedIssue, _ := issueStore.GetIssue(ctx, result.ResolvedID)
if updatedIssue != nil && hookRunner != nil {
hookRunner.Run(hooks.EventUpdate, updatedIssue)
}
if jsonOutput {
if updatedIssue != nil {
updatedIssues = append(updatedIssues, updatedIssue)
}
} else {
fmt.Printf("%s Updated issue: %s\n", ui.RenderPass("✓"), result.ResolvedID)
}
if firstUpdatedID == "" {
firstUpdatedID = result.ResolvedID
}
result.Close()
}
if jsonOutput && len(updatedIssues) > 0 {
outputJSON(updatedIssues)
}