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:
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user