From 3c9be8d49c61039412625e1c836cf8fabfa5b807 Mon Sep 17 00:00:00 2001 From: seanmartinsmith <114885497+seanmartinsmith@users.noreply.github.com> Date: Tue, 17 Mar 2026 14:25:21 -0400 Subject: [PATCH 1/2] feat: surface quality, lifecycle, and workflow commands in prime and template Dogfooding on 85+ beads showed agents only see ~19 of 100+ commands via bd prime. 26% of close reasons were terse one-liners, --acceptance was never used, and --validate was unknown. This adds visibility for features that already exist but were invisible to agents. Prime output (CLI mode only) now includes: - Close reason format template (Summary/Change/Files/Discovery) - Quality tools section (--validate, --acceptance, --design, bd lint) - Lifecycle & hygiene commands (defer, supersede, stale, orphans, etc.) - Structured workflows (bd formula list, bd mol pour) beads-section.md template (full profile) gets matching condensed sections for non-hook agents (Codex, Factory, Mux, OpenCode). MCP mode and beads-section-minimal.md are unchanged. --- cmd/bd/prime.go | 32 +++++++++++++++++++ cmd/bd/testdata/prime_content.txt | 16 ++++++++++ .../agents/defaults/beads-section.md | 11 +++++++ 3 files changed, 59 insertions(+) create mode 100644 cmd/bd/testdata/prime_content.txt diff --git a/cmd/bd/prime.go b/cmd/bd/prime.go index 724ee01be1..410f41b730 100644 --- a/cmd/bd/prime.go +++ b/cmd/bd/prime.go @@ -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 --until=\"date\"`" + ` - Defer work to a future date +- ` + "`bd supersede --by=`" + ` - Mark issue as superseded +- ` + "`bd close --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 `" + ` - Flag for human decision (list/respond/dismiss) + +### Structured Workflows +- ` + "`bd formula list`" + ` - See available workflow templates +- ` + "`bd mol pour `" + ` - 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 diff --git a/cmd/bd/testdata/prime_content.txt b/cmd/bd/testdata/prime_content.txt new file mode 100644 index 0000000000..576864eff3 --- /dev/null +++ b/cmd/bd/testdata/prime_content.txt @@ -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' diff --git a/internal/templates/agents/defaults/beads-section.md b/internal/templates/agents/defaults/beads-section.md index 3ad07daa89..026be860d1 100644 --- a/internal/templates/agents/defaults/beads-section.md +++ b/internal/templates/agents/defaults/beads-section.md @@ -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:` 5. **Complete**: `bd close --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 ` / `bd supersede ` for issue management +- `bd stale` / `bd orphans` / `bd lint` for hygiene +- `bd human ` to flag for human decisions +- `bd formula list` / `bd mol pour ` for structured workflows + ### Auto-Sync bd automatically syncs via Dolt: From 1ff8e287a8265a4f0a0bcba73819f54a0151f2ae Mon Sep 17 00:00:00 2001 From: seanmartinsmith <114885497+seanmartinsmith@users.noreply.github.com> Date: Tue, 17 Mar 2026 14:25:50 -0400 Subject: [PATCH 2/2] feat: add doctor --check=conventions for convention drift detection Composite check that runs lint, stale, and orphan detection in one pass. Advisory only - warns but never blocks, consistent with the "quality is a choice" philosophy. Three sub-checks: - conventions.lint: open issues missing recommended template sections - conventions.stale: issues inactive for 14+ days - conventions.orphans: issues referenced in commits but still open Each sub-check returns a doctorCheck following the existing pattern. Supports both human-readable and --json output. --- cmd/bd/doctor.go | 8 +- cmd/bd/doctor_conventions.go | 200 ++++++++++++++++++++++++++++++ cmd/bd/doctor_conventions_test.go | 52 ++++++++ 3 files changed, 259 insertions(+), 1 deletion(-) create mode 100644 cmd/bd/doctor_conventions.go create mode 100644 cmd/bd/doctor_conventions_test.go diff --git a/cmd/bd/doctor.go b/cmd/bd/doctor.go index 556e555a48..23928bd5eb 100644 --- a/cmd/bd/doctor.go +++ b/cmd/bd/doctor.go @@ -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. @@ -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 @@ -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") } } diff --git a/cmd/bd/doctor_conventions.go b/cmd/bd/doctor_conventions.go new file mode 100644 index 0000000000..8cecd9afb6 --- /dev/null +++ b/cmd/bd/doctor_conventions.go @@ -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", + }} +} diff --git a/cmd/bd/doctor_conventions_test.go b/cmd/bd/doctor_conventions_test.go new file mode 100644 index 0000000000..bc5f76ac18 --- /dev/null +++ b/cmd/bd/doctor_conventions_test.go @@ -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) + } +}