feat(config): add validation.on-create and validation.on-sync config options (bd-t7jq)

Add .beads/config.yaml support for template validation settings:
- validation.on-create: warn|error|none (default: none)
- validation.on-sync: warn|error|none (default: none)

When set to "warn", issues missing required sections (based on type) show
warnings but operations proceed. When set to "error", operations fail.

Implementation:
- Add validation keys to YamlOnlyKeys in yaml_config.go
- Add defaults in config.go
- Wire up bd create to check validation.on-create config
- Wire up bd sync to run validation before export
- Add tests for config loading
- Update CONFIG.md documentation

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
topaz
2026-01-01 19:31:12 -08:00
committed by Steve Yegge
parent b73085962c
commit 65fb0c6d77
7 changed files with 164 additions and 2 deletions

View File

@@ -166,12 +166,26 @@ var createCmd = &cobra.Command{
estimatedMinutes = &est
}
// Validate template if --validate flag is set
// Validate template based on --validate flag or config
validateTemplate, _ := cmd.Flags().GetBool("validate")
if validateTemplate {
// Explicit --validate flag: fail on error
if err := validation.ValidateTemplate(types.IssueType(issueType), description); err != nil {
FatalError("%v", err)
}
} else {
// Check validation.on-create config (bd-t7jq)
validationMode := config.GetString("validation.on-create")
if validationMode == "error" || validationMode == "warn" {
if err := validation.ValidateTemplate(types.IssueType(issueType), description); err != nil {
if validationMode == "error" {
FatalError("%v", err)
} else {
// warn mode: print warning but proceed
fmt.Fprintf(os.Stderr, "%s %v\n", ui.RenderWarn("⚠"), err)
}
}
}
}
// Use global jsonOutput set by PersistentPreRun

View File

@@ -287,6 +287,11 @@ Use --merge to merge the sync branch back to main branch.`,
}
}
// Template validation before export (bd-t7jq)
if err := validateOpenIssuesForSync(ctx); err != nil {
FatalError("%v", err)
}
fmt.Println("→ Exporting pending changes to JSONL...")
if err := exportToJSONL(ctx, jsonlPath); err != nil {
FatalError("exporting: %v", err)

View File

@@ -10,8 +10,11 @@ import (
"slices"
"time"
"github.com/steveyegge/beads/internal/config"
"github.com/steveyegge/beads/internal/rpc"
"github.com/steveyegge/beads/internal/types"
"github.com/steveyegge/beads/internal/ui"
"github.com/steveyegge/beads/internal/validation"
)
// exportToJSONL exports the database to JSONL format
@@ -179,3 +182,63 @@ func exportToJSONL(ctx context.Context, jsonlPath string) error {
return nil
}
// validateOpenIssuesForSync validates all open issues against their templates
// before export, based on the validation.on-sync config setting.
// Returns an error if validation.on-sync is "error" and issues fail validation.
// Prints warnings if validation.on-sync is "warn".
// Does nothing if validation.on-sync is "none" (default).
func validateOpenIssuesForSync(ctx context.Context) error {
validationMode := config.GetString("validation.on-sync")
if validationMode == "none" || validationMode == "" {
return nil
}
// Ensure store is active
if err := ensureStoreActive(); err != nil {
return fmt.Errorf("failed to initialize store for validation: %w", err)
}
// Get all issues (excluding tombstones) and filter to open ones
allIssues, err := store.SearchIssues(ctx, "", types.IssueFilter{})
if err != nil {
return fmt.Errorf("failed to get issues for validation: %w", err)
}
// Filter to only open issues (not closed, not tombstones)
var issues []*types.Issue
for _, issue := range allIssues {
if issue.Status != types.StatusClosed && issue.Status != types.StatusTombstone {
issues = append(issues, issue)
}
}
// Validate each issue
var warnings []string
for _, issue := range issues {
if err := validation.LintIssue(issue); err != nil {
warnings = append(warnings, fmt.Sprintf("%s: %v", issue.ID, err))
}
}
if len(warnings) == 0 {
return nil
}
// Report based on mode
if validationMode == "error" {
fmt.Fprintf(os.Stderr, "%s Validation failed for %d issue(s):\n", ui.RenderFail("✗"), len(warnings))
for _, w := range warnings {
fmt.Fprintf(os.Stderr, " - %s\n", w)
}
return fmt.Errorf("template validation failed: %d issues missing required sections (set validation.on-sync: none or warn to proceed)", len(warnings))
}
// warn mode: print warnings but proceed
fmt.Fprintf(os.Stderr, "%s Validation warnings for %d issue(s):\n", ui.RenderWarn("⚠"), len(warnings))
for _, w := range warnings {
fmt.Fprintf(os.Stderr, " - %s\n", w)
}
return nil
}