From 94997bd619e1be4564467774647f7dc50d40a490 Mon Sep 17 00:00:00 2001 From: beads/crew/wolf Date: Tue, 6 Jan 2026 18:51:38 -0800 Subject: [PATCH] fix(sync): use inline import for --import-only with redirect (bd-ysal) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When running `bd sync --import-only` from a directory with `.beads/redirect`, the subprocess-based import could fail to update staleness metadata correctly because the subprocess might resolve paths differently than the parent process. The fix uses inline import (calling importIssuesCore directly) instead of spawning a subprocess. This ensures: 1. The same store and dbPath are used throughout 2. Path resolution is consistent with the parent process 3. Staleness metadata is updated correctly in the redirected database 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- cmd/bd/sync.go | 3 +- cmd/bd/sync_import.go | 91 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 93 insertions(+), 1 deletion(-) diff --git a/cmd/bd/sync.go b/cmd/bd/sync.go index 669747c1..b720a4d7 100644 --- a/cmd/bd/sync.go +++ b/cmd/bd/sync.go @@ -110,12 +110,13 @@ Use --merge to merge the sync branch back to main branch.`, } // If import-only mode, just import and exit + // Use inline import to avoid subprocess path resolution issues with .beads/redirect (bd-ysal) if importOnly { if dryRun { fmt.Println("→ [DRY RUN] Would import from JSONL") } else { fmt.Println("→ Importing from JSONL...") - if err := importFromJSONL(ctx, jsonlPath, renameOnImport, noGitHistory); err != nil { + if err := importFromJSONLInline(ctx, jsonlPath, renameOnImport, noGitHistory); err != nil { FatalError("importing: %v", err) } fmt.Println("✓ Import complete") diff --git a/cmd/bd/sync_import.go b/cmd/bd/sync_import.go index 6bf87fe2..e76690e0 100644 --- a/cmd/bd/sync_import.go +++ b/cmd/bd/sync_import.go @@ -1,10 +1,16 @@ package main import ( + "bufio" "context" + "encoding/json" "fmt" "os" "os/exec" + "time" + + "github.com/steveyegge/beads/internal/debug" + "github.com/steveyegge/beads/internal/types" ) // importFromJSONL imports the JSONL file by running the import command @@ -55,6 +61,91 @@ func importFromJSONL(ctx context.Context, jsonlPath string, renameOnImport bool, return nil } +// importFromJSONLInline imports the JSONL file directly without spawning a subprocess. +// This avoids path resolution issues when running from directories with .beads/redirect. +// The parent process's store and dbPath are used, ensuring consistent path resolution. +// (bd-ysal fix) +func importFromJSONLInline(ctx context.Context, jsonlPath string, renameOnImport bool, noGitHistory bool) error { + // Verify we have an active store + if store == nil { + return fmt.Errorf("no database store available for inline import") + } + + // Read and parse the JSONL file + // #nosec G304 - jsonlPath is from findJSONLPath() which uses trusted paths + f, err := os.Open(jsonlPath) + if err != nil { + return fmt.Errorf("failed to open JSONL file: %w", err) + } + defer func() { _ = f.Close() }() + + var allIssues []*types.Issue + scanner := bufio.NewScanner(f) + lineNum := 0 + + for scanner.Scan() { + lineNum++ + line := scanner.Text() + if line == "" { + continue + } + + var issue types.Issue + if err := json.Unmarshal([]byte(line), &issue); err != nil { + return fmt.Errorf("error parsing line %d: %w", lineNum, err) + } + issue.SetDefaults() + allIssues = append(allIssues, &issue) + } + + if err := scanner.Err(); err != nil { + return fmt.Errorf("error reading JSONL: %w", err) + } + + // Import using shared logic + opts := ImportOptions{ + RenameOnImport: renameOnImport, + } + result, err := importIssuesCore(ctx, dbPath, store, allIssues, opts) + if err != nil { + return fmt.Errorf("import failed: %w", err) + } + + // Update staleness metadata (same as import.go lines 386-411) + // This is critical: without this, CheckStaleness will still report stale + if currentHash, hashErr := computeJSONLHash(jsonlPath); hashErr == nil { + if err := store.SetMetadata(ctx, "jsonl_content_hash", currentHash); err != nil { + debug.Logf("Warning: failed to update jsonl_content_hash: %v", err) + } + if err := store.SetJSONLFileHash(ctx, currentHash); err != nil { + debug.Logf("Warning: failed to update jsonl_file_hash: %v", err) + } + importTime := time.Now().Format(time.RFC3339Nano) + if err := store.SetMetadata(ctx, "last_import_time", importTime); err != nil { + debug.Logf("Warning: failed to update last_import_time: %v", err) + } + } else { + debug.Logf("Warning: failed to compute JSONL hash: %v", hashErr) + } + + // Update database mtime + if err := TouchDatabaseFile(dbPath, jsonlPath); err != nil { + debug.Logf("Warning: failed to update database mtime: %v", err) + } + + // Print summary + fmt.Fprintf(os.Stderr, "Import complete: %d created, %d updated", result.Created, result.Updated) + if result.Unchanged > 0 { + fmt.Fprintf(os.Stderr, ", %d unchanged", result.Unchanged) + } + if result.Skipped > 0 { + fmt.Fprintf(os.Stderr, ", %d skipped", result.Skipped) + } + fmt.Fprintf(os.Stderr, "\n") + + return nil +} + // resolveNoGitHistoryForFromMain returns the resolved noGitHistory value for sync operations. // When syncing from main (--from-main), noGitHistory is forced to true to prevent creating // incorrect deletion records for locally-created beads that don't exist on main.