fix(autoimport): enable cold-start bootstrap for read-only commands (#977)

After devcontainer restart (cold-start), `bd --no-daemon show` failed to
find beads because:
1. Read-only commands skipped auto-import
2. Newly created DB had no issue_prefix set, causing import to fail

This fix enables seamless cold-start recovery by:
- Allowing read-only commands (show, list, etc.) to auto-bootstrap when
  JSONL exists but DB doesn't
- Setting needsBootstrap flag when falling back from read-only to
  read-write mode for missing DB
- Auto-detecting and setting issue_prefix from JSONL during auto-import
  when DB is uninitialized

Fixes: gt-b09

Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
Josh Nichols
2026-01-09 15:38:18 -05:00
committed by GitHub
parent edbfd5dc96
commit 5d1a8c2428
3 changed files with 96 additions and 25 deletions

View File

@@ -102,6 +102,39 @@ func canonicalizeIfRelative(path string) string {
return path
}
// detectPrefixFromJSONL extracts the issue prefix from JSONL data.
// Returns empty string if prefix cannot be detected.
// Used by cold-start bootstrap to initialize the database (GH#b09).
func detectPrefixFromJSONL(jsonlData []byte) string {
// Parse first issue to extract prefix from its ID
scanner := bufio.NewScanner(bytes.NewReader(jsonlData))
for scanner.Scan() {
line := scanner.Text()
if line == "" {
continue
}
var issue struct {
ID string `json:"id"`
}
if err := json.Unmarshal([]byte(line), &issue); err != nil {
continue
}
if issue.ID == "" {
continue
}
// Extract prefix from ID (e.g., "gt-abc" -> "gt", "test-001" -> "test")
if idx := strings.Index(issue.ID, "-"); idx > 0 {
return issue.ID[:idx]
}
// No hyphen - use whole ID as prefix
return issue.ID
}
return ""
}
// autoImportIfNewer checks if JSONL content changed (via hash) and imports if so
// Hash-based comparison is git-proof (mtime comparison fails after git pull).
// Uses collision detection to prevent silently overwriting local changes.
@@ -152,6 +185,34 @@ func autoImportIfNewer() {
debug.Logf("auto-import triggered (hash changed)")
// Check if database needs initialization (GH#b09 - cold-start bootstrap)
// If issue_prefix is not set, the DB is uninitialized and import will fail.
// Auto-detect and set the prefix to enable seamless cold-start recovery.
// Note: Use global store directly as cmdCtx.Store may not be synced yet (GH#b09)
if store != nil {
prefix, prefixErr := store.GetConfig(ctx, "issue_prefix")
if prefixErr != nil || prefix == "" {
// Database needs initialization - detect prefix from JSONL or directory
detectedPrefix := detectPrefixFromJSONL(jsonlData)
if detectedPrefix == "" {
// Fallback: detect from directory name
beadsDir := filepath.Dir(jsonlPath)
parentDir := filepath.Dir(beadsDir)
detectedPrefix = filepath.Base(parentDir)
if detectedPrefix == "." || detectedPrefix == "/" {
detectedPrefix = "bd"
}
}
detectedPrefix = strings.TrimRight(detectedPrefix, "-")
if setErr := store.SetConfig(ctx, "issue_prefix", detectedPrefix); setErr != nil {
fmt.Fprintf(os.Stderr, "Auto-import: failed to initialize database prefix: %v\n", setErr)
return
}
debug.Logf("auto-import: initialized database with prefix '%s'", detectedPrefix)
}
}
// Check for Git merge conflict markers
// Only match if they appear as standalone lines (not embedded in JSON strings)
lines := bytes.Split(jsonlData, []byte("\n"))