Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion cmd/bd/doctor.go
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,8 @@ Specific Check Mode (--check):
Run a specific check in detail. Available checks:
- artifacts: Detect and optionally clean beads classic artifacts
(stale JSONL, SQLite files, cruft .beads dirs). Use with --clean.
- conventions: Check for convention drift (lint warnings, stale
issues, orphaned issues). Advisory only - warns, never blocks.
- pollution: Detect and optionally clean test issues from database
- validate: Run focused data-integrity checks (duplicates, orphaned
deps, test pollution, git conflicts). Use with --fix to auto-repair.
Expand Down Expand Up @@ -172,6 +174,7 @@ Examples:
bd doctor --output diagnostics.json # Export diagnostics to file
bd doctor --check=artifacts # Show classic artifacts (JSONL, SQLite, cruft dirs)
bd doctor --check=artifacts --clean # Delete safe-to-delete artifacts (with confirmation)
bd doctor --check=conventions # Convention drift check (lint, stale, orphans)
bd doctor --check=pollution # Show potential test issues
bd doctor --check=pollution --clean # Delete test issues (with confirmation)
bd doctor --check=validate # Data-integrity checks only
Expand Down Expand Up @@ -238,8 +241,11 @@ Examples:
case "artifacts":
runArtifactsCheck(absPath, doctorClean, doctorYes)
return
case "conventions":
runConventionsCheck(absPath)
return
default:
FatalErrorWithHint(fmt.Sprintf("unknown check %q", doctorCheckFlag), "Available checks: artifacts, pollution, validate")
FatalErrorWithHint(fmt.Sprintf("unknown check %q", doctorCheckFlag), "Available checks: artifacts, conventions, pollution, validate")
}
}

Expand Down
200 changes: 200 additions & 0 deletions cmd/bd/doctor_conventions.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
package main

import (
"fmt"

"github.com/steveyegge/beads/internal/types"
"github.com/steveyegge/beads/internal/ui"
"github.com/steveyegge/beads/internal/validation"
)

// runConventionsCheck runs a composite conventions check: lint, stale, and orphans.
// All findings are advisory (warning, never error) - conventions are a choice.
func runConventionsCheck(path string) {
var checks []doctorCheck

checks = append(checks, runConventionsLint()...)
checks = append(checks, runConventionsStale()...)
checks = append(checks, runConventionsOrphans(path)...)

if jsonOutput {
overallOK := true
for _, c := range checks {
if c.Status != statusOK {
overallOK = false
break
}
}
outputJSON(struct {
Path string `json:"path"`
Checks []doctorCheck `json:"checks"`
OverallOK bool `json:"overall_ok"`
}{
Path: path,
Checks: checks,
OverallOK: overallOK,
})
return
}

// Human-readable output
fmt.Println()
fmt.Println(ui.RenderCategory("Conventions"))

var passCount, warnCount int
for _, c := range checks {
var statusIcon string
switch c.Status {
case statusOK:
statusIcon = ui.RenderPassIcon()
passCount++
case statusWarning:
statusIcon = ui.RenderWarnIcon()
warnCount++
}

fmt.Printf(" %s %s", statusIcon, c.Name)
if c.Message != "" {
fmt.Printf("%s", ui.RenderMuted(" "+c.Message))
}
fmt.Println()
if c.Detail != "" {
fmt.Printf(" %s%s\n", ui.MutedStyle.Render(ui.TreeLast), ui.RenderMuted(c.Detail))
}
if c.Fix != "" {
fmt.Printf(" %s\n", ui.RenderMuted("Fix: "+c.Fix))
}
}

fmt.Println()
fmt.Println(ui.RenderSeparator())
fmt.Printf("%s %d passed %s %d warnings\n",
ui.RenderPassIcon(), passCount,
ui.RenderWarnIcon(), warnCount,
)

if warnCount == 0 {
fmt.Println()
fmt.Printf("%s\n", ui.RenderPass("✓ All convention checks passed"))
}
}

// runConventionsLint checks open issues for missing template sections.
func runConventionsLint() []doctorCheck {
if store == nil {
return []doctorCheck{{
Name: "conventions.lint",
Status: statusWarning,
Message: "database not available",
Category: "Conventions",
}}
}

ctx := rootCtx
openStatus := types.StatusOpen
issues, err := store.SearchIssues(ctx, "", types.IssueFilter{Status: &openStatus})
if err != nil {
return []doctorCheck{{
Name: "conventions.lint",
Status: statusWarning,
Message: fmt.Sprintf("error reading issues: %v", err),
Category: "Conventions",
}}
}

warningCount := 0
for _, issue := range issues {
if err := validation.LintIssue(issue); err != nil {
warningCount++
}
}

if warningCount == 0 {
return []doctorCheck{{
Name: "conventions.lint",
Status: statusOK,
Message: fmt.Sprintf("all %d open issues pass template checks", len(issues)),
Category: "Conventions",
}}
}

return []doctorCheck{{
Name: "conventions.lint",
Status: statusWarning,
Message: fmt.Sprintf("%d of %d open issues missing recommended sections", warningCount, len(issues)),
Fix: "bd lint",
Category: "Conventions",
}}
}

// runConventionsStale checks for issues with no recent activity.
func runConventionsStale() []doctorCheck {
if store == nil {
return []doctorCheck{{
Name: "conventions.stale",
Status: statusWarning,
Message: "database not available",
Category: "Conventions",
}}
}

ctx := rootCtx
filter := types.StaleFilter{Days: 14, Limit: 100}
staleIssues, err := store.GetStaleIssues(ctx, filter)
if err != nil {
return []doctorCheck{{
Name: "conventions.stale",
Status: statusWarning,
Message: fmt.Sprintf("error checking stale issues: %v", err),
Category: "Conventions",
}}
}

if len(staleIssues) == 0 {
return []doctorCheck{{
Name: "conventions.stale",
Status: statusOK,
Message: "no issues inactive for 14+ days",
Category: "Conventions",
}}
}

return []doctorCheck{{
Name: "conventions.stale",
Status: statusWarning,
Message: fmt.Sprintf("%d issues inactive for 14+ days", len(staleIssues)),
Fix: "bd stale",
Category: "Conventions",
}}
}

// runConventionsOrphans checks for issues referenced in commits but still open.
func runConventionsOrphans(path string) []doctorCheck {
orphans, err := findOrphanedIssues(path)
if err != nil {
// Not an error - orphan detection may fail in non-git repos
return []doctorCheck{{
Name: "conventions.orphans",
Status: statusOK,
Message: "orphan check skipped (no git history)",
Category: "Conventions",
}}
}

if len(orphans) == 0 {
return []doctorCheck{{
Name: "conventions.orphans",
Status: statusOK,
Message: "no orphaned issues found",
Category: "Conventions",
}}
}

return []doctorCheck{{
Name: "conventions.orphans",
Status: statusWarning,
Message: fmt.Sprintf("%d issues referenced in commits but still open", len(orphans)),
Fix: "bd orphans",
Category: "Conventions",
}}
}
52 changes: 52 additions & 0 deletions cmd/bd/doctor_conventions_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package main

import (
"testing"
)

func TestConventionsLint_NoStore(t *testing.T) {
// Save and restore global store
origStore := store
store = nil
defer func() { store = origStore }()

checks := runConventionsLint()
if len(checks) != 1 {
t.Fatalf("expected 1 check, got %d", len(checks))
}
if checks[0].Status != statusWarning {
t.Errorf("expected warning status, got %s", checks[0].Status)
}
if checks[0].Name != "conventions.lint" {
t.Errorf("expected name conventions.lint, got %s", checks[0].Name)
}
}

func TestConventionsStale_NoStore(t *testing.T) {
origStore := store
store = nil
defer func() { store = origStore }()

checks := runConventionsStale()
if len(checks) != 1 {
t.Fatalf("expected 1 check, got %d", len(checks))
}
if checks[0].Status != statusWarning {
t.Errorf("expected warning status, got %s", checks[0].Status)
}
if checks[0].Name != "conventions.stale" {
t.Errorf("expected name conventions.stale, got %s", checks[0].Name)
}
}

func TestConventionsOrphans_NoGit(t *testing.T) {
// In a temp dir with no git history, orphan check should succeed gracefully
checks := runConventionsOrphans(t.TempDir())
if len(checks) != 1 {
t.Fatalf("expected 1 check, got %d", len(checks))
}
// Should be OK (skipped) since there's no git repo
if checks[0].Name != "conventions.orphans" {
t.Errorf("expected name conventions.orphans, got %s", checks[0].Name)
}
}
32 changes: 32 additions & 0 deletions cmd/bd/prime.go
Original file line number Diff line number Diff line change
Expand Up @@ -461,6 +461,38 @@ git push # Push to remote
### Project Health
- ` + "`bd stats`" + ` - Project statistics (open/closed/blocked counts)
- ` + "`bd doctor`" + ` - Check for issues (sync problems, missing hooks)
- ` + "`bd doctor --check=conventions`" + ` - Check for convention drift (lint, stale, orphans)

### Quality Tools
- ` + "`bd create --validate`" + ` - Check description has required sections
- ` + "`bd create --acceptance=\"criteria\"`" + ` - Set acceptance criteria (checked by --validate)
- ` + "`bd create --design=\"decisions\"`" + ` - Record design decisions
- ` + "`bd create --notes=\"context\"`" + ` - Add supplementary notes
- ` + "`bd config set validation.on-create warn`" + ` - Auto-validate on every create
- ` + "`bd lint`" + ` - Check existing issues for missing sections

### Lifecycle & Hygiene
- ` + "`bd defer <id> --until=\"date\"`" + ` - Defer work to a future date
- ` + "`bd supersede <id> --by=<new-id>`" + ` - Mark issue as superseded
- ` + "`bd close <id> --suggest-next`" + ` - Show newly unblocked issues after closing
- ` + "`bd stale`" + ` - Find issues with no recent activity
- ` + "`bd orphans`" + ` - Find issues with broken dependencies
- ` + "`bd preflight`" + ` - Pre-PR checks (lint, stale, orphans)
- ` + "`bd human <id>`" + ` - Flag for human decision (list/respond/dismiss)

### Structured Workflows
- ` + "`bd formula list`" + ` - See available workflow templates
- ` + "`bd mol pour <name>`" + ` - Start structured workflow from formula

## Close Reason Format
When closing non-trivial work, structure the reason:
` + "```" + `
Summary: [one line - what was done]
Change: [what changed and why]
Files: [key files modified]
Discovery: [what was learned that wasn't known at start]
` + "```" + `
Minimum: Summary + Change + Files. Discovery for non-trivial work.

## Common Workflows

Expand Down
16 changes: 16 additions & 0 deletions cmd/bd/testdata/prime_content.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# Test bd prime --full includes new sections
bd init --prefix test
bd prime --full
stdout 'Close Reason Format'
stdout 'Quality Tools'
stdout 'bd lint'
stdout 'bd formula list'
stdout 'bd human'
stdout 'Lifecycle & Hygiene'
stdout 'Structured Workflows'
stdout 'bd defer'
stdout 'bd supersede'
stdout 'bd stale'
stdout 'bd orphans'
stdout 'bd preflight'
stdout 'conventions'
11 changes: 11 additions & 0 deletions internal/templates/agents/defaults/beads-section.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,17 @@ bd close bd-42 --reason "Completed" --json
- `bd create "Found bug" --description="Details about what was found" -p 1 --deps discovered-from:<parent-id>`
5. **Complete**: `bd close <id> --reason "Done"`

### Quality
- Use `--acceptance` and `--design` fields when creating issues
- Use `--validate` to check description completeness
- Structure close reasons: Summary + Change + Files + Discovery

### Lifecycle
- `bd defer <id>` / `bd supersede <id>` for issue management
- `bd stale` / `bd orphans` / `bd lint` for hygiene
- `bd human <id>` to flag for human decisions
- `bd formula list` / `bd mol pour <name>` for structured workflows

### Auto-Sync

bd automatically syncs via Dolt:
Expand Down