diff --git a/cmd/bd/doctor.go b/cmd/bd/doctor.go index cc25bfd2..c2b617e1 100644 --- a/cmd/bd/doctor.go +++ b/cmd/bd/doctor.go @@ -48,13 +48,14 @@ type doctorResult struct { } var ( - doctorFix bool - doctorYes bool - doctorInteractive bool // bd-3xl: per-fix confirmation mode - doctorDryRun bool // bd-a5z: preview fixes without applying - doctorOutput string // bd-9cc: export diagnostics to file - perfMode bool - checkHealthMode bool + doctorFix bool + doctorYes bool + doctorInteractive bool // bd-3xl: per-fix confirmation mode + doctorDryRun bool // bd-a5z: preview fixes without applying + doctorOutput string // bd-9cc: export diagnostics to file + doctorFixChildParent bool // bd-cuek: opt-in fix for child→parent deps + perfMode bool + checkHealthMode bool ) // ConfigKeyHintsDoctor is the config key for suppressing doctor hints @@ -104,6 +105,7 @@ Examples: bd doctor --fix # Automatically fix issues (with confirmation) bd doctor --fix --yes # Automatically fix issues (no confirmation) bd doctor --fix -i # Confirm each fix individually (bd-3xl) + bd doctor --fix --fix-child-parent # Also fix child→parent deps (opt-in) bd doctor --dry-run # Preview what --fix would do without making changes bd doctor --perf # Performance diagnostics bd doctor --output diagnostics.json # Export diagnostics to file`, @@ -182,6 +184,7 @@ func init() { doctorCmd.Flags().BoolVarP(&doctorYes, "yes", "y", false, "Skip confirmation prompt (for non-interactive use)") doctorCmd.Flags().BoolVarP(&doctorInteractive, "interactive", "i", false, "Confirm each fix individually (bd-3xl)") doctorCmd.Flags().BoolVar(&doctorDryRun, "dry-run", false, "Preview fixes without making changes (bd-a5z)") + doctorCmd.Flags().BoolVar(&doctorFixChildParent, "fix-child-parent", false, "Remove child→parent dependencies (opt-in, bd-cuek)") } // previewFixes shows what would be fixed without applying changes (bd-a5z) @@ -401,6 +404,11 @@ func applyFixList(path string, fixes []doctorCheck) { case "Orphaned Dependencies": err = fix.OrphanedDependencies(path) case "Child-Parent Dependencies": + // bd-cuek: Requires explicit opt-in flag (destructive, may remove intentional deps) + if !doctorFixChildParent { + fmt.Printf(" ⚠ Child→parent deps require explicit opt-in: bd doctor --fix --fix-child-parent\n") + continue + } err = fix.ChildParentDependencies(path) case "Duplicate Issues": // No auto-fix: duplicates require user review diff --git a/cmd/bd/doctor/fix/validation.go b/cmd/bd/doctor/fix/validation.go index e48aec03..1b5beb0d 100644 --- a/cmd/bd/doctor/fix/validation.go +++ b/cmd/bd/doctor/fix/validation.go @@ -162,8 +162,9 @@ func OrphanedDependencies(path string) error { return nil } -// ChildParentDependencies removes child→parent dependencies (anti-pattern). -// This fixes the deadlock where children depend on their parent epic. +// ChildParentDependencies removes child→parent blocking dependencies. +// These often indicate a modeling mistake (deadlock: child waits for parent, parent waits for children). +// Requires explicit opt-in via --fix-child-parent flag since some workflows may use these intentionally. func ChildParentDependencies(path string) error { if err := validateBeadsWorkspace(path); err != nil { return err diff --git a/cmd/bd/doctor/validation.go b/cmd/bd/doctor/validation.go index 8509bcf6..c484fc97 100644 --- a/cmd/bd/doctor/validation.go +++ b/cmd/bd/doctor/validation.go @@ -308,8 +308,9 @@ func CheckTestPollution(path string) DoctorCheck { } } -// CheckChildParentDependencies detects the child→parent dependency anti-pattern. -// This creates a deadlock: child can't start (parent open), parent can't close (children not done). +// CheckChildParentDependencies detects child→parent blocking dependencies. +// These often indicate a modeling mistake (deadlock: child waits for parent, parent waits for children). +// However, they may be intentional in some workflows, so removal requires explicit opt-in. func CheckChildParentDependencies(path string) DoctorCheck { beadsDir := filepath.Join(path, ".beads") dbPath := filepath.Join(beadsDir, beads.CanonicalDatabaseName) @@ -374,9 +375,9 @@ func CheckChildParentDependencies(path string) DoctorCheck { return DoctorCheck{ Name: "Child-Parent Dependencies", Status: "warning", - Message: fmt.Sprintf("%d child→parent dependency anti-pattern(s) detected", len(badDeps)), + Message: fmt.Sprintf("%d child→parent dependency detected (may cause deadlock)", len(badDeps)), Detail: detail, - Fix: "Run 'bd doctor --fix' to remove child→parent dependencies", + Fix: "Run 'bd doctor --fix --fix-child-parent' to remove (if unintentional)", Category: CategoryMetadata, } }