fix(ready): prevent wisps from appearing in bd ready
Add multiple layers of defense against misclassified wisps: - Importer auto-detects -wisp- pattern and sets ephemeral flag - GetReadyWork excludes -wisp- IDs via SQL LIKE clause - Doctor check 26d detects misclassified wisps in JSONL This addresses recurring issue where wisps with missing ephemeral flag would pollute bd ready output after JSONL import. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
committed by
gastown/crew/dennis
parent
12c7bef159
commit
f703237c3d
File diff suppressed because one or more lines are too long
@@ -577,6 +577,11 @@ func runDiagnostics(path string) doctorResult {
|
|||||||
result.Checks = append(result.Checks, staleMQFilesCheck)
|
result.Checks = append(result.Checks, staleMQFilesCheck)
|
||||||
// Don't fail overall check for legacy MQ files, just warn
|
// Don't fail overall check for legacy MQ files, just warn
|
||||||
|
|
||||||
|
// Check 26d: Misclassified wisps (wisp-patterned IDs without ephemeral flag)
|
||||||
|
misclassifiedWispsCheck := convertDoctorCheck(doctor.CheckMisclassifiedWisps(path))
|
||||||
|
result.Checks = append(result.Checks, misclassifiedWispsCheck)
|
||||||
|
// Don't fail overall check for misclassified wisps, just warn
|
||||||
|
|
||||||
// Check 27: Expired tombstones (maintenance)
|
// Check 27: Expired tombstones (maintenance)
|
||||||
tombstonesExpiredCheck := convertDoctorCheck(doctor.CheckExpiredTombstones(path))
|
tombstonesExpiredCheck := convertDoctorCheck(doctor.CheckExpiredTombstones(path))
|
||||||
result.Checks = append(result.Checks, tombstonesExpiredCheck)
|
result.Checks = append(result.Checks, tombstonesExpiredCheck)
|
||||||
|
|||||||
@@ -445,3 +445,78 @@ func FixStaleMQFiles(path string) error {
|
|||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CheckMisclassifiedWisps detects wisp-patterned issues that lack the ephemeral flag.
|
||||||
|
// Issues with IDs containing "-wisp-" should always have Ephemeral=true.
|
||||||
|
// If they're in JSONL without the ephemeral flag, they'll pollute bd ready.
|
||||||
|
func CheckMisclassifiedWisps(path string) DoctorCheck {
|
||||||
|
beadsDir := resolveBeadsDir(filepath.Join(path, ".beads"))
|
||||||
|
jsonlPath := filepath.Join(beadsDir, "issues.jsonl")
|
||||||
|
|
||||||
|
if _, err := os.Stat(jsonlPath); os.IsNotExist(err) {
|
||||||
|
return DoctorCheck{
|
||||||
|
Name: "Misclassified Wisps",
|
||||||
|
Status: StatusOK,
|
||||||
|
Message: "N/A (no JSONL file)",
|
||||||
|
Category: CategoryMaintenance,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read JSONL and find wisp-patterned issues without ephemeral flag
|
||||||
|
file, err := os.Open(jsonlPath) // #nosec G304 - path constructed safely
|
||||||
|
if err != nil {
|
||||||
|
return DoctorCheck{
|
||||||
|
Name: "Misclassified Wisps",
|
||||||
|
Status: StatusOK,
|
||||||
|
Message: "N/A (unable to read JSONL)",
|
||||||
|
Category: CategoryMaintenance,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
var wispCount int
|
||||||
|
var wispIDs []string
|
||||||
|
decoder := json.NewDecoder(file)
|
||||||
|
|
||||||
|
for {
|
||||||
|
var issue types.Issue
|
||||||
|
if err := decoder.Decode(&issue); err != nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
// Skip deleted issues (tombstones)
|
||||||
|
if issue.DeletedAt != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// Look for wisp pattern without ephemeral flag
|
||||||
|
// These shouldn't be in JSONL at all (wisps are ephemeral)
|
||||||
|
if strings.Contains(issue.ID, "-wisp-") && !issue.Ephemeral {
|
||||||
|
wispCount++
|
||||||
|
if len(wispIDs) < 3 {
|
||||||
|
wispIDs = append(wispIDs, issue.ID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if wispCount == 0 {
|
||||||
|
return DoctorCheck{
|
||||||
|
Name: "Misclassified Wisps",
|
||||||
|
Status: StatusOK,
|
||||||
|
Message: "No misclassified wisps found",
|
||||||
|
Category: CategoryMaintenance,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
detail := fmt.Sprintf("Example: %v", wispIDs)
|
||||||
|
if wispCount > 3 {
|
||||||
|
detail += fmt.Sprintf(" (+%d more)", wispCount-3)
|
||||||
|
}
|
||||||
|
|
||||||
|
return DoctorCheck{
|
||||||
|
Name: "Misclassified Wisps",
|
||||||
|
Status: StatusWarning,
|
||||||
|
Message: fmt.Sprintf("%d wisp issue(s) in JSONL missing ephemeral flag", wispCount),
|
||||||
|
Detail: detail,
|
||||||
|
Fix: "Remove from JSONL: grep -v '\"id\":\"<id>\"' issues.jsonl > tmp && mv tmp issues.jsonl",
|
||||||
|
Category: CategoryMaintenance,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -99,6 +99,15 @@ func ImportIssues(ctx context.Context, dbPath string, store storage.Storage, iss
|
|||||||
issue.ContentHash = issue.ComputeContentHash()
|
issue.ContentHash = issue.ComputeContentHash()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Auto-detect wisps by ID pattern and set ephemeral flag
|
||||||
|
// This prevents orphaned wisp entries in JSONL from polluting bd ready
|
||||||
|
// Pattern: *-wisp-* indicates ephemeral patrol/workflow instances
|
||||||
|
for _, issue := range issues {
|
||||||
|
if strings.Contains(issue.ID, "-wisp-") && !issue.Ephemeral {
|
||||||
|
issue.Ephemeral = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Get or create SQLite store
|
// Get or create SQLite store
|
||||||
sqliteStore, needCloseStore, err := getOrCreateStore(ctx, dbPath, store)
|
sqliteStore, needCloseStore, err := getOrCreateStore(ctx, dbPath, store)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ func (s *SQLiteStorage) GetReadyWork(ctx context.Context, filter types.WorkFilte
|
|||||||
whereClauses := []string{
|
whereClauses := []string{
|
||||||
"i.pinned = 0", // Exclude pinned issues
|
"i.pinned = 0", // Exclude pinned issues
|
||||||
"(i.ephemeral = 0 OR i.ephemeral IS NULL)", // Exclude wisps
|
"(i.ephemeral = 0 OR i.ephemeral IS NULL)", // Exclude wisps
|
||||||
|
"i.id NOT LIKE '%-wisp-%'", // Defense in depth: exclude wisp IDs even if ephemeral flag missing
|
||||||
}
|
}
|
||||||
args := []interface{}{}
|
args := []interface{}{}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user