Add substring ID matching for all bd commands
- Enhanced ResolvePartialID to handle: - Bare hashes: 07b8c8 → bd-07b8c8 - Prefix without hyphen: bd07b8c8 → bd-07b8c8 - Full IDs: bd-07b8c8 (unchanged) - Substring matching: 07b → finds bd-07b8c8 - Added RPC support: - New OpResolveID operation - handleResolveID server handler - ResolveID client method - Updated all commands to resolve IDs: - show, update, close, reopen - dep (add, remove, tree) - label (add, remove, list) - Works in both daemon and direct modes Fixes bd-0591c3
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
{"id":"bd-0134cc5a","content_hash":"d45c0e44c01c5855f14f07693bd800f4bfeac3084e10ceb17970ff54c58f6a40","title":"Fix auto-import creating duplicates instead of updating issues","description":"ROOT CAUSE: server_export_import_auto.go line 221 uses ResolveCollisions: true for ALL auto-imports. This is wrong.\n\nProblem:\n- ResolveCollisions is for branch merges (different issues with same ID)\n- Auto-import should UPDATE existing issues, not create duplicates\n- Every git pull creates NEW duplicate issues with different IDs\n- Two agents ping-pong creating endless duplicates\n\nEvidence:\n- 31 duplicate groups found (bd duplicates)\n- bd-236-246 are duplicates of bd-224-235\n- Both agents keep pulling and creating more duplicates\n- JSONL file grows endlessly with duplicates\n\nThe Fix:\nChange checkAndAutoImportIfStale in server_export_import_auto.go:\n- Remove ResolveCollisions: true (line 221)\n- Use normal import logic that updates existing issues by ID\n- Only use ResolveCollisions for explicit bd import --resolve-collisions\n\nImpact: Critical - makes beads unusable for multi-agent workflows","acceptance_criteria":"- Auto-import does NOT create duplicates when pulling git changes\n- Existing issues are updated in-place by ID match\n- No ping-pong commits between agents\n- Test: two agents updating same issue should NOT create duplicates\n- bd duplicates shows 0 groups after fix","status":"closed","priority":0,"issue_type":"bug","created_at":"2025-10-27T21:48:57.733846-07:00","updated_at":"2025-10-30T17:05:26.022586-07:00","closed_at":"2025-10-27T22:26:40.627239-07:00"}
|
||||
{"id":"bd-0447029c","title":"bd find-duplicates - AI-powered duplicate detection","description":"Find semantically duplicate issues.\n\nApproaches:\n1. Mechanical: Exact title/description matching\n2. Embeddings: Cosine similarity (cheap, scalable)\n3. AI: LLM-based semantic comparison (expensive, accurate)\n\nUses embeddings by default for \u003e100 issues.\n\nFiles: cmd/bd/find_duplicates.go (new)","status":"closed","priority":1,"issue_type":"task","created_at":"2025-10-29T16:43:28.182327-07:00","updated_at":"2025-10-30T17:05:25.998219-07:00","closed_at":"2025-10-29T16:15:10.64719-07:00"}
|
||||
{"id":"bd-0591c3","content_hash":"558b48309ef0ab7e45e576075858b8693847512010ec74e07b2dadc397140bfa","title":"bd show should accept partial IDs and auto-add prefix","description":"Agent tried `bd show 667565` (hash only, no prefix) and got \"Issue wy667565 not found\". Then tried `bd show wy-667565` which worked.\n\nCommands should:\n1. Accept bare hash (e.g., \"667565\" → \"wy-667565\")\n2. Accept prefix without hyphen (e.g., \"wy667565\" → \"wy-667565\")\n3. Support substring matching for partial hashes (e.g., \"6675\" → find matches)\n\nThis affects show, update, close, dep, label commands.","status":"open","priority":1,"issue_type":"bug","created_at":"2025-10-30T19:05:17.694824-07:00","updated_at":"2025-10-30T19:05:17.694824-07:00"}
|
||||
{"id":"bd-0591c3","content_hash":"b0bd9523f023c8f152b15abfcea955e70dac97b65726cf4780f0a32198acb96e","title":"bd show should accept partial IDs and auto-add prefix","description":"Agent tried `bd show 667565` (hash only, no prefix) and got \"Issue wy667565 not found\". Then tried `bd show wy-667565` which worked.\n\nCommands should:\n1. Accept bare hash (e.g., \"667565\" → \"wy-667565\")\n2. Accept prefix without hyphen (e.g., \"wy667565\" → \"wy-667565\")\n3. Support substring matching for partial hashes (e.g., \"6675\" → find matches)\n\nThis affects show, update, close, dep, label commands.","status":"closed","priority":1,"issue_type":"bug","created_at":"2025-10-30T19:05:17.694824-07:00","updated_at":"2025-10-30T19:14:41.792597-07:00","closed_at":"2025-10-30T19:14:41.792601-07:00"}
|
||||
{"id":"bd-0650a73b","content_hash":"a596aa8d6114d4938471e181ebc30da5d0315f74fd711a92dbbb83f5d0e7af88","title":"Create cmd/bd/daemon_debouncer.go (~60 LOC)","description":"Implement Debouncer to batch rapid events into single action. Default 500ms, configurable via BEADS_DEBOUNCE_MS. Thread-safe with mutex.","status":"closed","priority":1,"issue_type":"task","created_at":"2025-10-28T16:20:02.431118-07:00","updated_at":"2025-10-30T17:05:26.039318-07:00","closed_at":"2025-10-28T12:03:35.614191-07:00"}
|
||||
{"id":"bd-06aec0c3","content_hash":"330e69cf6ca40209948559b453ed5242c15a71b5c949a858ad6854488b12dca2","title":"Integration Testing","description":"Verify cache removal doesn't break any workflows","acceptance_criteria":"- All test cases pass\n- No stale data observed\n- Performance is same or better\n- MCP works as before\n\nTest cases:\n1. Basic daemon operations (bd daemon --stop, bd daemon, bd list, bd create, bd show)\n2. Auto-import/export cycle (edit beads.jsonl externally, bd list auto-imports)\n3. Git workflow (git pull updates beads.jsonl, bd list shows pulled issues)\n4. Concurrent operations (multiple bd commands simultaneously)\n5. Daemon health (bd daemon --health, bd daemon --metrics)\n6. MCP operations (test MCP server with multiple repos, verify project switching)","status":"closed","priority":1,"issue_type":"task","created_at":"2025-10-28T10:50:15.126668-07:00","updated_at":"2025-10-30T17:05:26.030232-07:00","closed_at":"2025-10-28T10:49:20.471129-07:00"}
|
||||
{"id":"bd-07b8c8","content_hash":"6d2f495c5f33b476936e25159e007c4ca46f089e2e24944899b78103dae6c392","title":"Replace filesystem discovery with daemon registry","description":"Current design recursively scans home directory for bd.sock files, causing indefinite hangs. Need daemons to register themselves in a central location (e.g., ~/.beads/registry.json or socket in known location) that bd daemons list can query instantly without filesystem traversal.","status":"closed","priority":0,"issue_type":"bug","created_at":"2025-10-30T18:29:41.173371-07:00","updated_at":"2025-10-30T18:37:01.317949-07:00","closed_at":"2025-10-30T18:37:01.317949-07:00","dependencies":[{"issue_id":"bd-07b8c8","depends_on_id":"bd-acb971c7","type":"discovered-from","created_at":"2025-10-30T18:29:41.174947-07:00","created_by":"stevey"}]}
|
||||
|
||||
148
cmd/bd/dep.go
148
cmd/bd/dep.go
@@ -26,11 +26,46 @@ var depAddCmd = &cobra.Command{
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
depType, _ := cmd.Flags().GetString("type")
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
// Resolve partial IDs first
|
||||
var fromID, toID string
|
||||
if daemonClient != nil {
|
||||
resolveArgs := &rpc.ResolveIDArgs{ID: args[0]}
|
||||
resp, err := daemonClient.ResolveID(resolveArgs)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error resolving issue ID %s: %v\n", args[0], err)
|
||||
os.Exit(1)
|
||||
}
|
||||
fromID = string(resp.Data)
|
||||
|
||||
resolveArgs = &rpc.ResolveIDArgs{ID: args[1]}
|
||||
resp, err = daemonClient.ResolveID(resolveArgs)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error resolving dependency ID %s: %v\n", args[1], err)
|
||||
os.Exit(1)
|
||||
}
|
||||
toID = string(resp.Data)
|
||||
} else {
|
||||
var err error
|
||||
fromID, err = utils.ResolvePartialID(ctx, store, args[0])
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error resolving issue ID %s: %v\n", args[0], err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
toID, err = utils.ResolvePartialID(ctx, store, args[1])
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error resolving dependency ID %s: %v\n", args[1], err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
// If daemon is running, use RPC
|
||||
if daemonClient != nil {
|
||||
depArgs := &rpc.DepAddArgs{
|
||||
FromID: args[0],
|
||||
ToID: args[1],
|
||||
FromID: fromID,
|
||||
ToID: toID,
|
||||
DepType: depType,
|
||||
}
|
||||
|
||||
@@ -52,23 +87,9 @@ var depAddCmd = &cobra.Command{
|
||||
}
|
||||
|
||||
// Direct mode
|
||||
ctx := context.Background()
|
||||
|
||||
fullFromID, err := utils.ResolvePartialID(ctx, store, args[0])
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error resolving issue ID %s: %v\n", args[0], err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
fullToID, err := utils.ResolvePartialID(ctx, store, args[1])
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error resolving dependency ID %s: %v\n", args[1], err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
dep := &types.Dependency{
|
||||
IssueID: fullFromID,
|
||||
DependsOnID: fullToID,
|
||||
IssueID: fromID,
|
||||
DependsOnID: toID,
|
||||
Type: types.DependencyType(depType),
|
||||
}
|
||||
|
||||
@@ -108,8 +129,8 @@ var depAddCmd = &cobra.Command{
|
||||
if jsonOutput {
|
||||
outputJSON(map[string]interface{}{
|
||||
"status": "added",
|
||||
"issue_id": fullFromID,
|
||||
"depends_on_id": fullToID,
|
||||
"issue_id": fromID,
|
||||
"depends_on_id": toID,
|
||||
"type": depType,
|
||||
})
|
||||
return
|
||||
@@ -117,7 +138,7 @@ var depAddCmd = &cobra.Command{
|
||||
|
||||
green := color.New(color.FgGreen).SprintFunc()
|
||||
fmt.Printf("%s Added dependency: %s depends on %s (%s)\n",
|
||||
green("✓"), fullFromID, fullToID, depType)
|
||||
green("✓"), fromID, toID, depType)
|
||||
},
|
||||
}
|
||||
|
||||
@@ -126,11 +147,46 @@ var depRemoveCmd = &cobra.Command{
|
||||
Short: "Remove a dependency",
|
||||
Args: cobra.ExactArgs(2),
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
ctx := context.Background()
|
||||
|
||||
// Resolve partial IDs first
|
||||
var fromID, toID string
|
||||
if daemonClient != nil {
|
||||
resolveArgs := &rpc.ResolveIDArgs{ID: args[0]}
|
||||
resp, err := daemonClient.ResolveID(resolveArgs)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error resolving issue ID %s: %v\n", args[0], err)
|
||||
os.Exit(1)
|
||||
}
|
||||
fromID = string(resp.Data)
|
||||
|
||||
resolveArgs = &rpc.ResolveIDArgs{ID: args[1]}
|
||||
resp, err = daemonClient.ResolveID(resolveArgs)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error resolving dependency ID %s: %v\n", args[1], err)
|
||||
os.Exit(1)
|
||||
}
|
||||
toID = string(resp.Data)
|
||||
} else {
|
||||
var err error
|
||||
fromID, err = utils.ResolvePartialID(ctx, store, args[0])
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error resolving issue ID %s: %v\n", args[0], err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
toID, err = utils.ResolvePartialID(ctx, store, args[1])
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error resolving dependency ID %s: %v\n", args[1], err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
// If daemon is running, use RPC
|
||||
if daemonClient != nil {
|
||||
depArgs := &rpc.DepRemoveArgs{
|
||||
FromID: args[0],
|
||||
ToID: args[1],
|
||||
FromID: fromID,
|
||||
ToID: toID,
|
||||
}
|
||||
|
||||
resp, err := daemonClient.RemoveDependency(depArgs)
|
||||
@@ -146,24 +202,13 @@ var depRemoveCmd = &cobra.Command{
|
||||
|
||||
green := color.New(color.FgGreen).SprintFunc()
|
||||
fmt.Printf("%s Removed dependency: %s no longer depends on %s\n",
|
||||
green("✓"), args[0], args[1])
|
||||
green("✓"), fromID, toID)
|
||||
return
|
||||
}
|
||||
|
||||
// Direct mode
|
||||
ctx := context.Background()
|
||||
|
||||
fullFromID, err := utils.ResolvePartialID(ctx, store, args[0])
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error resolving issue ID %s: %v\n", args[0], err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
fullToID, err := utils.ResolvePartialID(ctx, store, args[1])
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error resolving dependency ID %s: %v\n", args[1], err)
|
||||
os.Exit(1)
|
||||
}
|
||||
fullFromID := fromID
|
||||
fullToID := toID
|
||||
|
||||
if err := store.RemoveDependency(ctx, fullFromID, fullToID, actor); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
||||
@@ -193,6 +238,27 @@ var depTreeCmd = &cobra.Command{
|
||||
Short: "Show dependency tree",
|
||||
Args: cobra.ExactArgs(1),
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
ctx := context.Background()
|
||||
|
||||
// Resolve partial ID first
|
||||
var fullID string
|
||||
if daemonClient != nil {
|
||||
resolveArgs := &rpc.ResolveIDArgs{ID: args[0]}
|
||||
resp, err := daemonClient.ResolveID(resolveArgs)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error resolving issue ID %s: %v\n", args[0], err)
|
||||
os.Exit(1)
|
||||
}
|
||||
fullID = string(resp.Data)
|
||||
} else {
|
||||
var err error
|
||||
fullID, err = utils.ResolvePartialID(ctx, store, args[0])
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error resolving %s: %v\n", args[0], err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
// If daemon is running but doesn't support this command, use direct storage
|
||||
if daemonClient != nil && store == nil {
|
||||
var err error
|
||||
@@ -212,14 +278,6 @@ var depTreeCmd = &cobra.Command{
|
||||
fmt.Fprintf(os.Stderr, "Error: --max-depth must be >= 1\n")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
fullID, err := utils.ResolvePartialID(ctx, store, args[0])
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error resolving %s: %v\n", args[0], err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
tree, err := store.GetDependencyTree(ctx, fullID, maxDepth, showAllPaths, reverse)
|
||||
if err != nil {
|
||||
|
||||
@@ -81,20 +81,31 @@ var labelAddCmd = &cobra.Command{
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
issueIDs, label := parseLabelArgs(args)
|
||||
|
||||
// Resolve partial IDs if in direct mode
|
||||
if daemonClient == nil {
|
||||
ctx := context.Background()
|
||||
resolvedIDs := make([]string, 0, len(issueIDs))
|
||||
for _, id := range issueIDs {
|
||||
fullID, err := utils.ResolvePartialID(ctx, store, id)
|
||||
// Resolve partial IDs
|
||||
ctx := context.Background()
|
||||
resolvedIDs := make([]string, 0, len(issueIDs))
|
||||
for _, id := range issueIDs {
|
||||
var fullID string
|
||||
var err error
|
||||
|
||||
if daemonClient != nil {
|
||||
resolveArgs := &rpc.ResolveIDArgs{ID: id}
|
||||
resp, err := daemonClient.ResolveID(resolveArgs)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error resolving %s: %v\n", id, err)
|
||||
continue
|
||||
}
|
||||
fullID = string(resp.Data)
|
||||
} else {
|
||||
fullID, err = utils.ResolvePartialID(ctx, store, id)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error resolving %s: %v\n", id, err)
|
||||
continue
|
||||
}
|
||||
resolvedIDs = append(resolvedIDs, fullID)
|
||||
}
|
||||
issueIDs = resolvedIDs
|
||||
resolvedIDs = append(resolvedIDs, fullID)
|
||||
}
|
||||
issueIDs = resolvedIDs
|
||||
|
||||
processBatchLabelOperation(issueIDs, label, "added",
|
||||
func(issueID, lbl string) error {
|
||||
@@ -115,20 +126,31 @@ var labelRemoveCmd = &cobra.Command{
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
issueIDs, label := parseLabelArgs(args)
|
||||
|
||||
// Resolve partial IDs if in direct mode
|
||||
if daemonClient == nil {
|
||||
ctx := context.Background()
|
||||
resolvedIDs := make([]string, 0, len(issueIDs))
|
||||
for _, id := range issueIDs {
|
||||
fullID, err := utils.ResolvePartialID(ctx, store, id)
|
||||
// Resolve partial IDs
|
||||
ctx := context.Background()
|
||||
resolvedIDs := make([]string, 0, len(issueIDs))
|
||||
for _, id := range issueIDs {
|
||||
var fullID string
|
||||
var err error
|
||||
|
||||
if daemonClient != nil {
|
||||
resolveArgs := &rpc.ResolveIDArgs{ID: id}
|
||||
resp, err := daemonClient.ResolveID(resolveArgs)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error resolving %s: %v\n", id, err)
|
||||
continue
|
||||
}
|
||||
fullID = string(resp.Data)
|
||||
} else {
|
||||
fullID, err = utils.ResolvePartialID(ctx, store, id)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error resolving %s: %v\n", id, err)
|
||||
continue
|
||||
}
|
||||
resolvedIDs = append(resolvedIDs, fullID)
|
||||
}
|
||||
issueIDs = resolvedIDs
|
||||
resolvedIDs = append(resolvedIDs, fullID)
|
||||
}
|
||||
issueIDs = resolvedIDs
|
||||
|
||||
processBatchLabelOperation(issueIDs, label, "removed",
|
||||
func(issueID, lbl string) error {
|
||||
@@ -146,21 +168,29 @@ var labelListCmd = &cobra.Command{
|
||||
Short: "List labels for an issue",
|
||||
Args: cobra.ExactArgs(1),
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
issueID := args[0]
|
||||
|
||||
ctx := context.Background()
|
||||
var labels []string
|
||||
|
||||
// Resolve partial ID if in direct mode
|
||||
if daemonClient == nil {
|
||||
fullID, err := utils.ResolvePartialID(ctx, store, issueID)
|
||||
// Resolve partial ID first
|
||||
var issueID string
|
||||
if daemonClient != nil {
|
||||
resolveArgs := &rpc.ResolveIDArgs{ID: args[0]}
|
||||
resp, err := daemonClient.ResolveID(resolveArgs)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error resolving %s: %v\n", issueID, err)
|
||||
fmt.Fprintf(os.Stderr, "Error resolving issue ID %s: %v\n", args[0], err)
|
||||
os.Exit(1)
|
||||
}
|
||||
issueID = string(resp.Data)
|
||||
} else {
|
||||
var err error
|
||||
issueID, err = utils.ResolvePartialID(ctx, store, args[0])
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error resolving %s: %v\n", args[0], err)
|
||||
os.Exit(1)
|
||||
}
|
||||
issueID = fullID
|
||||
}
|
||||
|
||||
var labels []string
|
||||
|
||||
// Use daemon if available
|
||||
if daemonClient != nil {
|
||||
resp, err := daemonClient.Show(&rpc.ShowArgs{ID: issueID})
|
||||
|
||||
@@ -24,11 +24,33 @@ This is more explicit than 'bd update --status open' and emits a Reopened event.
|
||||
reason, _ := cmd.Flags().GetString("reason")
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
// Resolve partial IDs first
|
||||
var resolvedIDs []string
|
||||
if daemonClient != nil {
|
||||
for _, id := range args {
|
||||
resolveArgs := &rpc.ResolveIDArgs{ID: id}
|
||||
resp, err := daemonClient.ResolveID(resolveArgs)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error resolving ID %s: %v\n", id, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
resolvedIDs = append(resolvedIDs, string(resp.Data))
|
||||
}
|
||||
} else {
|
||||
var err error
|
||||
resolvedIDs, err = utils.ResolvePartialIDs(ctx, store, args)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
reopenedIssues := []*types.Issue{}
|
||||
|
||||
// If daemon is running, use RPC
|
||||
if daemonClient != nil {
|
||||
for _, id := range args {
|
||||
for _, id := range resolvedIDs {
|
||||
openStatus := string(types.StatusOpen)
|
||||
updateArgs := &rpc.UpdateArgs{
|
||||
ID: id,
|
||||
|
||||
132
cmd/bd/show.go
132
cmd/bd/show.go
@@ -20,10 +20,35 @@ var showCmd = &cobra.Command{
|
||||
Short: "Show issue details",
|
||||
Args: cobra.MinimumNArgs(1),
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
ctx := context.Background()
|
||||
|
||||
// Resolve partial IDs first
|
||||
var resolvedIDs []string
|
||||
if daemonClient != nil {
|
||||
// In daemon mode, resolve via RPC
|
||||
for _, id := range args {
|
||||
resolveArgs := &rpc.ResolveIDArgs{ID: id}
|
||||
resp, err := daemonClient.ResolveID(resolveArgs)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error resolving ID %s: %v\n", id, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
resolvedIDs = append(resolvedIDs, string(resp.Data))
|
||||
}
|
||||
} else {
|
||||
// In direct mode, resolve via storage
|
||||
var err error
|
||||
resolvedIDs, err = utils.ResolvePartialIDs(ctx, store, args)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
// If daemon is running, use RPC
|
||||
if daemonClient != nil {
|
||||
allDetails := []interface{}{}
|
||||
for idx, id := range args {
|
||||
for idx, id := range resolvedIDs {
|
||||
showArgs := &rpc.ShowArgs{ID: id}
|
||||
resp, err := daemonClient.Show(showArgs)
|
||||
if err != nil {
|
||||
@@ -158,22 +183,15 @@ var showCmd = &cobra.Command{
|
||||
}
|
||||
|
||||
// Direct mode
|
||||
ctx := context.Background()
|
||||
allDetails := []interface{}{}
|
||||
for idx, id := range args {
|
||||
fullID, err := utils.ResolvePartialID(ctx, store, id)
|
||||
for idx, id := range resolvedIDs {
|
||||
issue, err := store.GetIssue(ctx, id)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error resolving %s: %v\n", id, err)
|
||||
continue
|
||||
}
|
||||
|
||||
issue, err := store.GetIssue(ctx, fullID)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error fetching %s: %v\n", fullID, err)
|
||||
fmt.Fprintf(os.Stderr, "Error fetching %s: %v\n", id, err)
|
||||
continue
|
||||
}
|
||||
if issue == nil {
|
||||
fmt.Fprintf(os.Stderr, "Issue %s not found\n", fullID)
|
||||
fmt.Fprintf(os.Stderr, "Issue %s not found\n", id)
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -360,10 +378,33 @@ var updateCmd = &cobra.Command{
|
||||
return
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
// Resolve partial IDs first
|
||||
var resolvedIDs []string
|
||||
if daemonClient != nil {
|
||||
for _, id := range args {
|
||||
resolveArgs := &rpc.ResolveIDArgs{ID: id}
|
||||
resp, err := daemonClient.ResolveID(resolveArgs)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error resolving ID %s: %v\n", id, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
resolvedIDs = append(resolvedIDs, string(resp.Data))
|
||||
}
|
||||
} else {
|
||||
var err error
|
||||
resolvedIDs, err = utils.ResolvePartialIDs(ctx, store, args)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
// If daemon is running, use RPC
|
||||
if daemonClient != nil {
|
||||
updatedIssues := []*types.Issue{}
|
||||
for _, id := range args {
|
||||
for _, id := range resolvedIDs {
|
||||
updateArgs := &rpc.UpdateArgs{ID: id}
|
||||
|
||||
// Map updates to RPC args
|
||||
@@ -416,28 +457,21 @@ var updateCmd = &cobra.Command{
|
||||
}
|
||||
|
||||
// Direct mode
|
||||
ctx := context.Background()
|
||||
updatedIssues := []*types.Issue{}
|
||||
for _, id := range args {
|
||||
fullID, err := utils.ResolvePartialID(ctx, store, id)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error resolving %s: %v\n", id, err)
|
||||
continue
|
||||
}
|
||||
|
||||
if err := store.UpdateIssue(ctx, fullID, updates, actor); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error updating %s: %v\n", fullID, err)
|
||||
continue
|
||||
}
|
||||
for _, id := range resolvedIDs {
|
||||
if err := store.UpdateIssue(ctx, id, updates, actor); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error updating %s: %v\n", id, err)
|
||||
continue
|
||||
}
|
||||
|
||||
if jsonOutput {
|
||||
issue, _ := store.GetIssue(ctx, fullID)
|
||||
if jsonOutput {
|
||||
issue, _ := store.GetIssue(ctx, id)
|
||||
if issue != nil {
|
||||
updatedIssues = append(updatedIssues, issue)
|
||||
}
|
||||
} else {
|
||||
green := color.New(color.FgGreen).SprintFunc()
|
||||
fmt.Printf("%s Updated issue: %s\n", green("✓"), fullID)
|
||||
fmt.Printf("%s Updated issue: %s\n", green("✓"), id)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -658,10 +692,33 @@ var closeCmd = &cobra.Command{
|
||||
reason = "Closed"
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
// Resolve partial IDs first
|
||||
var resolvedIDs []string
|
||||
if daemonClient != nil {
|
||||
for _, id := range args {
|
||||
resolveArgs := &rpc.ResolveIDArgs{ID: id}
|
||||
resp, err := daemonClient.ResolveID(resolveArgs)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error resolving ID %s: %v\n", id, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
resolvedIDs = append(resolvedIDs, string(resp.Data))
|
||||
}
|
||||
} else {
|
||||
var err error
|
||||
resolvedIDs, err = utils.ResolvePartialIDs(ctx, store, args)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
// If daemon is running, use RPC
|
||||
if daemonClient != nil {
|
||||
closedIssues := []*types.Issue{}
|
||||
for _, id := range args {
|
||||
for _, id := range resolvedIDs {
|
||||
closeArgs := &rpc.CloseArgs{
|
||||
ID: id,
|
||||
Reason: reason,
|
||||
@@ -690,27 +747,20 @@ var closeCmd = &cobra.Command{
|
||||
}
|
||||
|
||||
// Direct mode
|
||||
ctx := context.Background()
|
||||
closedIssues := []*types.Issue{}
|
||||
for _, id := range args {
|
||||
fullID, err := utils.ResolvePartialID(ctx, store, id)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error resolving %s: %v\n", id, err)
|
||||
continue
|
||||
}
|
||||
|
||||
if err := store.CloseIssue(ctx, fullID, reason, actor); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error closing %s: %v\n", fullID, err)
|
||||
for _, id := range resolvedIDs {
|
||||
if err := store.CloseIssue(ctx, id, reason, actor); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error closing %s: %v\n", id, err)
|
||||
continue
|
||||
}
|
||||
if jsonOutput {
|
||||
issue, _ := store.GetIssue(ctx, fullID)
|
||||
issue, _ := store.GetIssue(ctx, id)
|
||||
if issue != nil {
|
||||
closedIssues = append(closedIssues, issue)
|
||||
}
|
||||
} else {
|
||||
green := color.New(color.FgGreen).SprintFunc()
|
||||
fmt.Printf("%s Closed %s: %s\n", green("✓"), fullID, reason)
|
||||
fmt.Printf("%s Closed %s: %s\n", green("✓"), id, reason)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -255,6 +255,11 @@ func (c *Client) Show(args *ShowArgs) (*Response, error) {
|
||||
return c.Execute(OpShow, args)
|
||||
}
|
||||
|
||||
// ResolveID resolves a partial issue ID to a full ID via the daemon
|
||||
func (c *Client) ResolveID(args *ResolveIDArgs) (*Response, error) {
|
||||
return c.Execute(OpResolveID, args)
|
||||
}
|
||||
|
||||
// Ready gets ready work via the daemon
|
||||
func (c *Client) Ready(args *ReadyArgs) (*Response, error) {
|
||||
return c.Execute(OpReady, args)
|
||||
|
||||
@@ -25,6 +25,7 @@ const (
|
||||
OpCommentList = "comment_list"
|
||||
OpCommentAdd = "comment_add"
|
||||
OpBatch = "batch"
|
||||
OpResolveID = "resolve_id"
|
||||
|
||||
OpCompact = "compact"
|
||||
OpCompactStats = "compact_stats"
|
||||
@@ -104,6 +105,11 @@ type ShowArgs struct {
|
||||
ID string `json:"id"`
|
||||
}
|
||||
|
||||
// ResolveIDArgs represents arguments for the resolve_id operation
|
||||
type ResolveIDArgs struct {
|
||||
ID string `json:"id"`
|
||||
}
|
||||
|
||||
// ReadyArgs represents arguments for the ready operation
|
||||
type ReadyArgs struct {
|
||||
Assignee string `json:"assignee,omitempty"`
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/steveyegge/beads/internal/types"
|
||||
"github.com/steveyegge/beads/internal/utils"
|
||||
)
|
||||
|
||||
// normalizeLabels trims whitespace, removes empty strings, and deduplicates labels
|
||||
@@ -319,6 +320,31 @@ func (s *Server) handleList(req *Request) Response {
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) handleResolveID(req *Request) Response {
|
||||
var args ResolveIDArgs
|
||||
if err := json.Unmarshal(req.Args, &args); err != nil {
|
||||
return Response{
|
||||
Success: false,
|
||||
Error: fmt.Sprintf("invalid resolve_id args: %v", err),
|
||||
}
|
||||
}
|
||||
|
||||
ctx := s.reqCtx(req)
|
||||
resolvedID, err := utils.ResolvePartialID(ctx, s.storage, args.ID)
|
||||
if err != nil {
|
||||
return Response{
|
||||
Success: false,
|
||||
Error: fmt.Sprintf("failed to resolve ID: %v", err),
|
||||
}
|
||||
}
|
||||
|
||||
data, _ := json.Marshal(resolvedID)
|
||||
return Response{
|
||||
Success: true,
|
||||
Data: data,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) handleShow(req *Request) Response {
|
||||
var showArgs ShowArgs
|
||||
if err := json.Unmarshal(req.Args, &showArgs); err != nil {
|
||||
|
||||
@@ -168,6 +168,8 @@ func (s *Server) handleRequest(req *Request) Response {
|
||||
resp = s.handleList(req)
|
||||
case OpShow:
|
||||
resp = s.handleShow(req)
|
||||
case OpResolveID:
|
||||
resp = s.handleResolveID(req)
|
||||
case OpReady:
|
||||
resp = s.handleReady(req)
|
||||
case OpStats:
|
||||
|
||||
@@ -29,32 +29,47 @@ func ParseIssueID(input string, prefix string) string {
|
||||
// ResolvePartialID resolves a potentially partial issue ID to a full ID.
|
||||
// Supports:
|
||||
// - Full IDs: "bd-a3f8e9" or "a3f8e9" → "bd-a3f8e9"
|
||||
// - Partial IDs: "a3f8" → "bd-a3f8e9" (if unique match, requires hash IDs)
|
||||
// - Without hyphen: "bda3f8e9" or "wya3f8e9" → "bd-a3f8e9"
|
||||
// - Partial IDs: "a3f8" → "bd-a3f8e9" (if unique match)
|
||||
// - Hierarchical: "a3f8e9.1" → "bd-a3f8e9.1"
|
||||
//
|
||||
// Returns an error if:
|
||||
// - No issue found matching the ID
|
||||
// - Multiple issues match (ambiguous prefix)
|
||||
//
|
||||
// Note: Partial ID matching (shorter prefixes) requires hash-based IDs (bd-165).
|
||||
// For now, this primarily handles prefix-optional input (bd-a3f8e9 vs a3f8e9).
|
||||
func ResolvePartialID(ctx context.Context, store storage.Storage, input string) (string, error) {
|
||||
// Get the configured prefix
|
||||
prefix, err := store.GetConfig(ctx, "issue_prefix")
|
||||
if err != nil || prefix == "" {
|
||||
prefix = "bd-"
|
||||
prefix = "bd"
|
||||
}
|
||||
|
||||
// Ensure the input has the prefix
|
||||
parsedID := ParseIssueID(input, prefix)
|
||||
// Ensure prefix has hyphen for ID format
|
||||
prefixWithHyphen := prefix
|
||||
if !strings.HasSuffix(prefix, "-") {
|
||||
prefixWithHyphen = prefix + "-"
|
||||
}
|
||||
|
||||
// Normalize input:
|
||||
// 1. If it has the full prefix with hyphen (bd-a3f8e9), use as-is
|
||||
// 2. Otherwise, add prefix with hyphen (handles both bare hashes and prefix-without-hyphen cases)
|
||||
|
||||
var normalizedID string
|
||||
|
||||
if strings.HasPrefix(input, prefixWithHyphen) {
|
||||
// Already has prefix with hyphen: "bd-a3f8e9"
|
||||
normalizedID = input
|
||||
} else {
|
||||
// Bare hash or prefix without hyphen: "a3f8e9", "07b8c8", "bda3f8e9" → all get prefix with hyphen added
|
||||
normalizedID = prefixWithHyphen + input
|
||||
}
|
||||
|
||||
// First try exact match
|
||||
_, err = store.GetIssue(ctx, parsedID)
|
||||
if err == nil {
|
||||
return parsedID, nil
|
||||
issue, err := store.GetIssue(ctx, normalizedID)
|
||||
if err == nil && issue != nil {
|
||||
return normalizedID, nil
|
||||
}
|
||||
|
||||
// If exact match failed, try prefix search
|
||||
// If exact match failed, try substring search
|
||||
filter := types.IssueFilter{}
|
||||
|
||||
issues, err := store.SearchIssues(ctx, "", filter)
|
||||
@@ -62,9 +77,14 @@ func ResolvePartialID(ctx context.Context, store storage.Storage, input string)
|
||||
return "", fmt.Errorf("failed to search issues: %w", err)
|
||||
}
|
||||
|
||||
// Extract the hash part for substring matching
|
||||
hashPart := strings.TrimPrefix(normalizedID, prefix)
|
||||
|
||||
var matches []string
|
||||
for _, issue := range issues {
|
||||
if strings.HasPrefix(issue.ID, parsedID) {
|
||||
issueHash := strings.TrimPrefix(issue.ID, prefix)
|
||||
// Check if the issue hash contains the input hash as substring
|
||||
if strings.Contains(issueHash, hashPart) {
|
||||
matches = append(matches, issue.ID)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user