feat(wisp): add misclassified wisp detection and defense-in-depth filtering (#833)

Add CheckMisclassifiedWisps doctor check to detect issues that should be
marked as wisps but aren't. This catches merge-requests, patrol molecules,
and operational work that lacks the wisp:true flag.

Add defense-in-depth wisp filtering to gt ready command. While bd ready
should already filter wisps, this provides an additional layer to ensure
ephemeral operational work doesn't leak into the ready work display.

Changes:
- New doctor check: misclassified-wisps (fixable, CategoryCleanup)
- gt ready now filters wisps from issues.jsonl in addition to scaffolds
- Detects wisp patterns: merge-request type, patrol labels, mol-* IDs

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Steve Yegge
2026-01-20 20:20:49 -08:00
committed by GitHub
parent e591f2ae25
commit 7c2f9687ec
3 changed files with 269 additions and 2 deletions
+1
View File
@@ -139,6 +139,7 @@ func runDoctor(cmd *cobra.Command, args []string) error {
d.Register(doctor.NewZombieSessionCheck())
d.Register(doctor.NewOrphanProcessCheck())
d.Register(doctor.NewWispGCCheck())
d.Register(doctor.NewCheckMisclassifiedWisps())
d.Register(doctor.NewBranchCheck())
d.Register(doctor.NewBeadsSyncOrphanCheck())
d.Register(doctor.NewCloneDivergenceCheck())
+62 -2
View File
@@ -1,6 +1,7 @@
package cmd
import (
"bufio"
"encoding/json"
"fmt"
"os"
@@ -132,7 +133,10 @@ func runReady(cmd *cobra.Command, args []string) error {
} else {
// Filter out formula scaffolds (gt-579)
formulaNames := getFormulaNames(townBeadsPath)
src.Issues = filterFormulaScaffolds(issues, formulaNames)
filtered := filterFormulaScaffolds(issues, formulaNames)
// Defense-in-depth: also filter wisps that shouldn't appear in ready work
wispIDs := getWispIDs(townBeadsPath)
src.Issues = filterWisps(filtered, wispIDs)
}
sources = append(sources, src)
}()
@@ -156,7 +160,10 @@ func runReady(cmd *cobra.Command, args []string) error {
} else {
// Filter out formula scaffolds (gt-579)
formulaNames := getFormulaNames(rigBeadsPath)
src.Issues = filterFormulaScaffolds(issues, formulaNames)
filtered := filterFormulaScaffolds(issues, formulaNames)
// Defense-in-depth: also filter wisps that shouldn't appear in ready work
wispIDs := getWispIDs(rigBeadsPath)
src.Issues = filterWisps(filtered, wispIDs)
}
sources = append(sources, src)
}(r)
@@ -346,3 +353,56 @@ func filterFormulaScaffolds(issues []*beads.Issue, formulaNames map[string]bool)
}
return filtered
}
// getWispIDs reads the issues.jsonl and returns a set of IDs that are wisps.
// Wisps are ephemeral issues (wisp: true flag) that shouldn't appear in ready work.
// This is a defense-in-depth exclusion - bd ready should already filter wisps,
// but we double-check at the display layer to ensure operational work doesn't leak.
func getWispIDs(beadsPath string) map[string]bool {
beadsDir := beads.ResolveBeadsDir(beadsPath)
issuesPath := filepath.Join(beadsDir, "issues.jsonl")
file, err := os.Open(issuesPath)
if err != nil {
return nil // No issues file
}
defer file.Close()
wispIDs := make(map[string]bool)
scanner := bufio.NewScanner(file)
for scanner.Scan() {
line := scanner.Text()
if line == "" {
continue
}
var issue struct {
ID string `json:"id"`
Wisp bool `json:"wisp"`
}
if err := json.Unmarshal([]byte(line), &issue); err != nil {
continue
}
if issue.Wisp {
wispIDs[issue.ID] = true
}
}
return wispIDs
}
// filterWisps removes wisp issues from the list.
// Wisps are ephemeral operational work that shouldn't appear in ready work.
func filterWisps(issues []*beads.Issue, wispIDs map[string]bool) []*beads.Issue {
if wispIDs == nil || len(wispIDs) == 0 {
return issues
}
filtered := make([]*beads.Issue, 0, len(issues))
for _, issue := range issues {
if !wispIDs[issue.ID] {
filtered = append(filtered, issue)
}
}
return filtered
}