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)
|
||||
// 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)
|
||||
tombstonesExpiredCheck := convertDoctorCheck(doctor.CheckExpiredTombstones(path))
|
||||
result.Checks = append(result.Checks, tombstonesExpiredCheck)
|
||||
|
||||
@@ -445,3 +445,78 @@ func FixStaleMQFiles(path string) error {
|
||||
|
||||
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()
|
||||
}
|
||||
|
||||
// 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
|
||||
sqliteStore, needCloseStore, err := getOrCreateStore(ctx, dbPath, store)
|
||||
if err != nil {
|
||||
|
||||
@@ -19,6 +19,7 @@ func (s *SQLiteStorage) GetReadyWork(ctx context.Context, filter types.WorkFilte
|
||||
whereClauses := []string{
|
||||
"i.pinned = 0", // Exclude pinned issues
|
||||
"(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{}{}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user