Merge pull request #311 from rsnodgrass/feat/ux-system-import
feat(ui): import comprehensive UX system from beads
This commit is contained in:
@@ -28,6 +28,7 @@ func NewAgentBeadsCheck() *AgentBeadsCheck {
|
||||
BaseCheck: BaseCheck{
|
||||
CheckName: "agent-beads-exist",
|
||||
CheckDescription: "Verify agent beads exist for all agents",
|
||||
CheckCategory: CategoryRig,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ func NewBdDaemonCheck() *BdDaemonCheck {
|
||||
BaseCheck: BaseCheck{
|
||||
CheckName: "bd-daemon",
|
||||
CheckDescription: "Check if bd (beads) daemon is running",
|
||||
CheckCategory: CategoryInfrastructure,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@ func NewBeadsDatabaseCheck() *BeadsDatabaseCheck {
|
||||
BaseCheck: BaseCheck{
|
||||
CheckName: "beads-database",
|
||||
CheckDescription: "Verify beads database is properly initialized",
|
||||
CheckCategory: CategoryConfig,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -176,6 +177,7 @@ func NewPrefixConflictCheck() *PrefixConflictCheck {
|
||||
BaseCheck: BaseCheck{
|
||||
CheckName: "prefix-conflict",
|
||||
CheckDescription: "Check for duplicate beads prefixes across rigs",
|
||||
CheckCategory: CategoryConfig,
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -243,6 +245,7 @@ func NewPrefixMismatchCheck() *PrefixMismatchCheck {
|
||||
BaseCheck: BaseCheck{
|
||||
CheckName: "prefix-mismatch",
|
||||
CheckDescription: "Check for prefix mismatches between rigs.json and routes.jsonl",
|
||||
CheckCategory: CategoryConfig,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ func NewBootHealthCheck() *BootHealthCheck {
|
||||
BaseCheck: BaseCheck{
|
||||
CheckName: "boot-health",
|
||||
CheckDescription: "Check Boot watchdog health (the vet checks on the dog)",
|
||||
CheckCategory: CategoryInfrastructure,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ func NewBranchCheck() *BranchCheck {
|
||||
BaseCheck: BaseCheck{
|
||||
CheckName: "persistent-role-branches",
|
||||
CheckDescription: "Detect persistent roles not on main branch",
|
||||
CheckCategory: CategoryCleanup,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -213,6 +214,7 @@ func NewBeadsSyncOrphanCheck() *BeadsSyncOrphanCheck {
|
||||
BaseCheck: BaseCheck{
|
||||
CheckName: "beads-sync-orphans",
|
||||
CheckDescription: "Detect orphaned code on beads-sync branch",
|
||||
CheckCategory: CategoryCleanup,
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -338,6 +340,7 @@ func NewCloneDivergenceCheck() *CloneDivergenceCheck {
|
||||
BaseCheck: BaseCheck{
|
||||
CheckName: "clone-divergence",
|
||||
CheckDescription: "Detect emergency divergence between git clones",
|
||||
CheckCategory: CategoryCleanup,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,6 +50,7 @@ func NewClaudeSettingsCheck() *ClaudeSettingsCheck {
|
||||
BaseCheck: BaseCheck{
|
||||
CheckName: "claude-settings",
|
||||
CheckDescription: "Verify Claude settings.json files match expected templates",
|
||||
CheckCategory: CategoryConfig,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ func NewCommandsCheck() *CommandsCheck {
|
||||
BaseCheck: BaseCheck{
|
||||
CheckName: "commands-provisioned",
|
||||
CheckDescription: "Check .claude/commands/ is provisioned at town level",
|
||||
CheckCategory: CategoryConfig,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@ func NewSettingsCheck() *SettingsCheck {
|
||||
BaseCheck: BaseCheck{
|
||||
CheckName: "rig-settings",
|
||||
CheckDescription: "Check that rigs have settings/ directory",
|
||||
CheckCategory: CategoryConfig,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -105,6 +106,7 @@ func NewRuntimeGitignoreCheck() *RuntimeGitignoreCheck {
|
||||
BaseCheck: BaseCheck{
|
||||
CheckName: "runtime-gitignore",
|
||||
CheckDescription: "Check that .runtime/ directories are gitignored",
|
||||
CheckCategory: CategoryConfig,
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -194,6 +196,7 @@ func NewLegacyGastownCheck() *LegacyGastownCheck {
|
||||
BaseCheck: BaseCheck{
|
||||
CheckName: "legacy-gastown",
|
||||
CheckDescription: "Check for old .gastown/ directories that should be migrated",
|
||||
CheckCategory: CategoryConfig,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -281,6 +284,7 @@ func NewSessionHookCheck() *SessionHookCheck {
|
||||
BaseCheck: BaseCheck{
|
||||
CheckName: "session-hooks",
|
||||
CheckDescription: "Check that settings.json hooks use session-start.sh or --hook flag",
|
||||
CheckCategory: CategoryConfig,
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -549,6 +553,7 @@ func NewCustomTypesCheck() *CustomTypesCheck {
|
||||
BaseCheck: BaseCheck{
|
||||
CheckName: "beads-custom-types",
|
||||
CheckDescription: "Check that Gas Town custom types are registered with beads",
|
||||
CheckCategory: CategoryConfig,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -31,6 +31,7 @@ func NewCrashReportCheck() *CrashReportCheck {
|
||||
BaseCheck: BaseCheck{
|
||||
CheckName: "crash-reports",
|
||||
CheckDescription: "Check for recent macOS crash reports (tmux, Claude)",
|
||||
CheckCategory: CategoryCleanup,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,6 +32,7 @@ func NewCrewStateCheck() *CrewStateCheck {
|
||||
BaseCheck: BaseCheck{
|
||||
CheckName: "crew-state",
|
||||
CheckDescription: "Validate crew worker state.json files",
|
||||
CheckCategory: CategoryCleanup,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -239,6 +240,7 @@ func NewCrewWorktreeCheck() *CrewWorktreeCheck {
|
||||
BaseCheck: BaseCheck{
|
||||
CheckName: "crew-worktrees",
|
||||
CheckDescription: "Detect stale cross-rig worktrees in crew directories",
|
||||
CheckCategory: CategoryCleanup,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ func NewDaemonCheck() *DaemonCheck {
|
||||
BaseCheck: BaseCheck{
|
||||
CheckName: "daemon",
|
||||
CheckDescription: "Check if Gas Town daemon is running",
|
||||
CheckCategory: CategoryInfrastructure,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -27,6 +27,11 @@ func (d *Doctor) Checks() []Check {
|
||||
return d.checks
|
||||
}
|
||||
|
||||
// categoryGetter interface for checks that provide a category
|
||||
type categoryGetter interface {
|
||||
Category() string
|
||||
}
|
||||
|
||||
// Run executes all registered checks and returns a report.
|
||||
func (d *Doctor) Run(ctx *CheckContext) *Report {
|
||||
report := NewReport()
|
||||
@@ -37,6 +42,10 @@ func (d *Doctor) Run(ctx *CheckContext) *Report {
|
||||
if result.Name == "" {
|
||||
result.Name = check.Name()
|
||||
}
|
||||
// Set category from check if available
|
||||
if cg, ok := check.(categoryGetter); ok && result.Category == "" {
|
||||
result.Category = cg.Category()
|
||||
}
|
||||
report.Add(result)
|
||||
}
|
||||
|
||||
@@ -53,6 +62,10 @@ func (d *Doctor) Fix(ctx *CheckContext) *Report {
|
||||
if result.Name == "" {
|
||||
result.Name = check.Name()
|
||||
}
|
||||
// Set category from check if available
|
||||
if cg, ok := check.(categoryGetter); ok && result.Category == "" {
|
||||
result.Category = cg.Category()
|
||||
}
|
||||
|
||||
// Attempt fix if check failed and is fixable
|
||||
if result.Status != StatusOK && check.CanFix() {
|
||||
@@ -63,6 +76,10 @@ func (d *Doctor) Fix(ctx *CheckContext) *Report {
|
||||
if result.Name == "" {
|
||||
result.Name = check.Name()
|
||||
}
|
||||
// Set category again after re-run
|
||||
if cg, ok := check.(categoryGetter); ok && result.Category == "" {
|
||||
result.Category = cg.Category()
|
||||
}
|
||||
// Update message to indicate fix was applied
|
||||
if result.Status == StatusOK {
|
||||
result.Message = result.Message + " (fixed)"
|
||||
@@ -84,6 +101,12 @@ func (d *Doctor) Fix(ctx *CheckContext) *Report {
|
||||
type BaseCheck struct {
|
||||
CheckName string
|
||||
CheckDescription string
|
||||
CheckCategory string // Category for grouping (e.g., CategoryCore)
|
||||
}
|
||||
|
||||
// Category returns the check's category for grouping in output.
|
||||
func (b *BaseCheck) Category() string {
|
||||
return b.CheckCategory
|
||||
}
|
||||
|
||||
// Name returns the check name.
|
||||
|
||||
@@ -219,8 +219,12 @@ func TestReport_Print(t *testing.T) {
|
||||
if !bytes.Contains(buf.Bytes(), []byte("TestCheck")) {
|
||||
t.Error("Output should contain check name")
|
||||
}
|
||||
if !bytes.Contains(buf.Bytes(), []byte("2 checks")) {
|
||||
t.Error("Output should contain summary")
|
||||
// New summary format: "✓ N passed ⚠ N warnings ✖ N failed"
|
||||
if !bytes.Contains(buf.Bytes(), []byte("1 passed")) {
|
||||
t.Error("Output should contain summary with passed count")
|
||||
}
|
||||
if !bytes.Contains(buf.Bytes(), []byte("1 warnings")) {
|
||||
t.Error("Output should contain summary with warnings count")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -42,6 +42,7 @@ func NewEnvVarsCheck() *EnvVarsCheck {
|
||||
BaseCheck: BaseCheck{
|
||||
CheckName: "env-vars",
|
||||
CheckDescription: "Verify tmux session environment variables match expected values",
|
||||
CheckCategory: CategoryConfig,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ func NewFormulaCheck() *FormulaCheck {
|
||||
BaseCheck: BaseCheck{
|
||||
CheckName: "formulas",
|
||||
CheckDescription: "Check embedded formulas are up-to-date",
|
||||
CheckCategory: CategoryConfig,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ func NewGlobalStateCheck() *GlobalStateCheck {
|
||||
BaseCheck: BaseCheck{
|
||||
CheckName: "global-state",
|
||||
CheckDescription: "Validates Gas Town global state and shell integration",
|
||||
CheckCategory: CategoryCore,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,6 +32,7 @@ func NewHookAttachmentValidCheck() *HookAttachmentValidCheck {
|
||||
BaseCheck: BaseCheck{
|
||||
CheckName: "hook-attachment-valid",
|
||||
CheckDescription: "Verify attached molecules exist and are not closed",
|
||||
CheckCategory: CategoryHooks,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -207,6 +208,7 @@ func NewHookSingletonCheck() *HookSingletonCheck {
|
||||
BaseCheck: BaseCheck{
|
||||
CheckName: "hook-singleton",
|
||||
CheckDescription: "Ensure each agent has at most one handoff bead",
|
||||
CheckCategory: CategoryHooks,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -346,6 +348,7 @@ func NewOrphanedAttachmentsCheck() *OrphanedAttachmentsCheck {
|
||||
BaseCheck: BaseCheck{
|
||||
CheckName: "orphaned-attachments",
|
||||
CheckDescription: "Detect handoff beads for non-existent agents",
|
||||
CheckCategory: CategoryHooks,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,19 +9,19 @@ import (
|
||||
)
|
||||
|
||||
// IdentityCollisionCheck checks for agent identity collisions and stale locks.
|
||||
type IdentityCollisionCheck struct{}
|
||||
type IdentityCollisionCheck struct {
|
||||
BaseCheck
|
||||
}
|
||||
|
||||
// NewIdentityCollisionCheck creates a new identity collision check.
|
||||
func NewIdentityCollisionCheck() *IdentityCollisionCheck {
|
||||
return &IdentityCollisionCheck{}
|
||||
}
|
||||
|
||||
func (c *IdentityCollisionCheck) Name() string {
|
||||
return "identity-collision"
|
||||
}
|
||||
|
||||
func (c *IdentityCollisionCheck) Description() string {
|
||||
return "Check for agent identity collisions and stale locks"
|
||||
return &IdentityCollisionCheck{
|
||||
BaseCheck: BaseCheck{
|
||||
CheckName: "identity-collision",
|
||||
CheckDescription: "Check for agent identity collisions and stale locks",
|
||||
CheckCategory: CategoryInfrastructure,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (c *IdentityCollisionCheck) CanFix() bool {
|
||||
|
||||
@@ -27,6 +27,7 @@ func NewLifecycleHygieneCheck() *LifecycleHygieneCheck {
|
||||
BaseCheck: BaseCheck{
|
||||
CheckName: "lifecycle-hygiene",
|
||||
CheckDescription: "Check for stale lifecycle messages",
|
||||
CheckCategory: CategoryConfig,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -27,6 +27,7 @@ func NewOrphanSessionCheck() *OrphanSessionCheck {
|
||||
BaseCheck: BaseCheck{
|
||||
CheckName: "orphan-sessions",
|
||||
CheckDescription: "Detect orphaned tmux sessions",
|
||||
CheckCategory: CategoryCleanup,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -244,6 +245,7 @@ func NewOrphanProcessCheck() *OrphanProcessCheck {
|
||||
BaseCheck: BaseCheck{
|
||||
CheckName: "orphan-processes",
|
||||
CheckDescription: "Detect runtime processes outside tmux",
|
||||
CheckCategory: CategoryCleanup,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,6 +28,7 @@ func NewPatrolMoleculesExistCheck() *PatrolMoleculesExistCheck {
|
||||
BaseCheck: BaseCheck{
|
||||
CheckName: "patrol-molecules-exist",
|
||||
CheckDescription: "Check if patrol molecules exist for each rig",
|
||||
CheckCategory: CategoryPatrol,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -155,6 +156,7 @@ func NewPatrolHooksWiredCheck() *PatrolHooksWiredCheck {
|
||||
BaseCheck: BaseCheck{
|
||||
CheckName: "patrol-hooks-wired",
|
||||
CheckDescription: "Check if hooks trigger patrol execution",
|
||||
CheckCategory: CategoryPatrol,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -229,6 +231,7 @@ func NewPatrolNotStuckCheck() *PatrolNotStuckCheck {
|
||||
BaseCheck: BaseCheck{
|
||||
CheckName: "patrol-not-stuck",
|
||||
CheckDescription: "Check for stuck patrol wisps (>1h in_progress)",
|
||||
CheckCategory: CategoryPatrol,
|
||||
},
|
||||
stuckThreshold: DefaultStuckThreshold,
|
||||
}
|
||||
@@ -351,6 +354,7 @@ func NewPatrolPluginsAccessibleCheck() *PatrolPluginsAccessibleCheck {
|
||||
BaseCheck: BaseCheck{
|
||||
CheckName: "patrol-plugins-accessible",
|
||||
CheckDescription: "Check if plugin directories exist and are readable",
|
||||
CheckCategory: CategoryPatrol,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -420,6 +424,7 @@ func NewPatrolRolesHavePromptsCheck() *PatrolRolesHavePromptsCheck {
|
||||
BaseCheck: BaseCheck{
|
||||
CheckName: "patrol-roles-have-prompts",
|
||||
CheckDescription: "Check if internal/templates/roles/*.md.tmpl exist for each patrol role",
|
||||
CheckCategory: CategoryPatrol,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ func NewPreCheckoutHookCheck() *PreCheckoutHookCheck {
|
||||
BaseCheck: BaseCheck{
|
||||
CheckName: "pre-checkout-hook",
|
||||
CheckDescription: "Verify pre-checkout hook prevents branch switches",
|
||||
CheckCategory: CategoryHooks,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -42,6 +42,7 @@ func NewRepoFingerprintCheck() *RepoFingerprintCheck {
|
||||
BaseCheck: BaseCheck{
|
||||
CheckName: "repo-fingerprint",
|
||||
CheckDescription: "Verify beads database has valid repository fingerprint",
|
||||
CheckCategory: CategoryInfrastructure,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ func NewRigBeadsCheck() *RigBeadsCheck {
|
||||
BaseCheck: BaseCheck{
|
||||
CheckName: "rig-beads-exist",
|
||||
CheckDescription: "Verify rig identity beads exist for all rigs",
|
||||
CheckCategory: CategoryRig,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@ func NewRigIsGitRepoCheck() *RigIsGitRepoCheck {
|
||||
BaseCheck: BaseCheck{
|
||||
CheckName: "rig-is-git-repo",
|
||||
CheckDescription: "Verify rig has a valid mayor/rig git clone",
|
||||
CheckCategory: CategoryRig,
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -99,6 +100,7 @@ func NewGitExcludeConfiguredCheck() *GitExcludeConfiguredCheck {
|
||||
BaseCheck: BaseCheck{
|
||||
CheckName: "git-exclude-configured",
|
||||
CheckDescription: "Check .git/info/exclude has Gas Town directories",
|
||||
CheckCategory: CategoryRig,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -249,6 +251,7 @@ func NewHooksPathConfiguredCheck() *HooksPathConfiguredCheck {
|
||||
BaseCheck: BaseCheck{
|
||||
CheckName: "hooks-path-configured",
|
||||
CheckDescription: "Check core.hooksPath is set for all clones",
|
||||
CheckCategory: CategoryRig,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -371,6 +374,7 @@ func NewWitnessExistsCheck() *WitnessExistsCheck {
|
||||
BaseCheck: BaseCheck{
|
||||
CheckName: "witness-exists",
|
||||
CheckDescription: "Verify witness/ directory structure exists",
|
||||
CheckCategory: CategoryRig,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -477,6 +481,7 @@ func NewRefineryExistsCheck() *RefineryExistsCheck {
|
||||
BaseCheck: BaseCheck{
|
||||
CheckName: "refinery-exists",
|
||||
CheckDescription: "Verify refinery/ directory structure exists",
|
||||
CheckCategory: CategoryRig,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -582,6 +587,7 @@ func NewMayorCloneExistsCheck() *MayorCloneExistsCheck {
|
||||
BaseCheck: BaseCheck{
|
||||
CheckName: "mayor-clone-exists",
|
||||
CheckDescription: "Verify mayor/rig/ git clone exists",
|
||||
CheckCategory: CategoryRig,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -664,6 +670,7 @@ func NewPolecatClonesValidCheck() *PolecatClonesValidCheck {
|
||||
BaseCheck: BaseCheck{
|
||||
CheckName: "polecat-clones-valid",
|
||||
CheckDescription: "Verify polecat directories are valid git clones",
|
||||
CheckCategory: CategoryRig,
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -798,6 +805,7 @@ func NewBeadsConfigValidCheck() *BeadsConfigValidCheck {
|
||||
BaseCheck: BaseCheck{
|
||||
CheckName: "beads-config-valid",
|
||||
CheckDescription: "Verify beads configuration if .beads/ exists",
|
||||
CheckCategory: CategoryRig,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -893,6 +901,7 @@ func NewBeadsRedirectCheck() *BeadsRedirectCheck {
|
||||
BaseCheck: BaseCheck{
|
||||
CheckName: "beads-redirect",
|
||||
CheckDescription: "Verify rig-level beads redirect for tracked beads",
|
||||
CheckCategory: CategoryRig,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -1105,6 +1114,7 @@ func NewBareRepoRefspecCheck() *BareRepoRefspecCheck {
|
||||
BaseCheck: BaseCheck{
|
||||
CheckName: "bare-repo-refspec",
|
||||
CheckDescription: "Verify bare repo has correct refspec for worktrees",
|
||||
CheckCategory: CategoryRig,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ func NewRoutesCheck() *RoutesCheck {
|
||||
BaseCheck: BaseCheck{
|
||||
CheckName: "routes-config",
|
||||
CheckDescription: "Check beads routing configuration",
|
||||
CheckCategory: CategoryConfig,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@ func NewSparseCheckoutCheck() *SparseCheckoutCheck {
|
||||
BaseCheck: BaseCheck{
|
||||
CheckName: "sparse-checkout",
|
||||
CheckDescription: "Verify sparse checkout excludes Claude context files (.claude/, CLAUDE.md, etc.)",
|
||||
CheckCategory: CategoryRig,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ func NewStaleBinaryCheck() *StaleBinaryCheck {
|
||||
BaseCheck: BaseCheck{
|
||||
CheckName: "stale-binary",
|
||||
CheckDescription: "Check if gt binary is up to date with repo",
|
||||
CheckCategory: CategoryInfrastructure,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ func NewThemeCheck() *ThemeCheck {
|
||||
BaseCheck: BaseCheck{
|
||||
CheckName: "themes",
|
||||
CheckDescription: "Check tmux session theme configuration",
|
||||
CheckCategory: CategoryConfig,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ func NewLinkedPaneCheck() *LinkedPaneCheck {
|
||||
BaseCheck: BaseCheck{
|
||||
CheckName: "linked-panes",
|
||||
CheckDescription: "Detect tmux sessions sharing panes (causes crosstalk)",
|
||||
CheckCategory: CategoryInfrastructure,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ func NewTownGitCheck() *TownGitCheck {
|
||||
BaseCheck: BaseCheck{
|
||||
CheckName: "town-git",
|
||||
CheckDescription: "Verify town root is under version control",
|
||||
CheckCategory: CategoryCore,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ func NewTownRootBranchCheck() *TownRootBranchCheck {
|
||||
BaseCheck: BaseCheck{
|
||||
CheckName: "town-root-branch",
|
||||
CheckDescription: "Verify town root is on main branch",
|
||||
CheckCategory: CategoryCore,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
+136
-40
@@ -4,12 +4,34 @@ package doctor
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
"slices"
|
||||
"time"
|
||||
|
||||
"github.com/steveyegge/gastown/internal/style"
|
||||
"github.com/steveyegge/gastown/internal/ui"
|
||||
)
|
||||
|
||||
// Category constants for grouping checks
|
||||
const (
|
||||
CategoryCore = "Core"
|
||||
CategoryInfrastructure = "Infrastructure"
|
||||
CategoryRig = "Rig"
|
||||
CategoryPatrol = "Patrol"
|
||||
CategoryConfig = "Configuration"
|
||||
CategoryCleanup = "Cleanup"
|
||||
CategoryHooks = "Hooks"
|
||||
)
|
||||
|
||||
// CategoryOrder defines the display order for categories
|
||||
var CategoryOrder = []string{
|
||||
CategoryCore,
|
||||
CategoryInfrastructure,
|
||||
CategoryRig,
|
||||
CategoryPatrol,
|
||||
CategoryConfig,
|
||||
CategoryCleanup,
|
||||
CategoryHooks,
|
||||
}
|
||||
|
||||
// CheckStatus represents the result status of a health check.
|
||||
type CheckStatus int
|
||||
|
||||
@@ -55,11 +77,12 @@ func (ctx *CheckContext) RigPath() string {
|
||||
|
||||
// CheckResult represents the outcome of a health check.
|
||||
type CheckResult struct {
|
||||
Name string // Check name
|
||||
Status CheckStatus // Result status
|
||||
Message string // Primary result message
|
||||
Details []string // Additional information
|
||||
FixHint string // Suggestion if not auto-fixable
|
||||
Name string // Check name
|
||||
Status CheckStatus // Result status
|
||||
Message string // Primary result message
|
||||
Details []string // Additional information
|
||||
FixHint string // Suggestion if not auto-fixable
|
||||
Category string // Category for grouping (e.g., CategoryCore)
|
||||
}
|
||||
|
||||
// Check defines the interface for a health check.
|
||||
@@ -135,59 +158,132 @@ func (r *Report) IsHealthy() bool {
|
||||
}
|
||||
|
||||
// Print outputs the report to the given writer.
|
||||
// Matches bd doctor UX: grouped by category, semantic icons, warnings section.
|
||||
func (r *Report) Print(w io.Writer, verbose bool) {
|
||||
// Print individual check results
|
||||
// Print header with version placeholder (caller should set via PrintWithVersion)
|
||||
_, _ = fmt.Fprintln(w)
|
||||
|
||||
// Group checks by category
|
||||
checksByCategory := make(map[string][]*CheckResult)
|
||||
for _, check := range r.Checks {
|
||||
r.printCheck(w, check, verbose)
|
||||
cat := check.Category
|
||||
if cat == "" {
|
||||
cat = "Other"
|
||||
}
|
||||
checksByCategory[cat] = append(checksByCategory[cat], check)
|
||||
}
|
||||
|
||||
// Print summary (output errors non-actionable)
|
||||
_, _ = fmt.Fprintln(w)
|
||||
// Track warnings/errors for summary section
|
||||
var warnings []*CheckResult
|
||||
|
||||
// Print checks by category in defined order
|
||||
for _, category := range CategoryOrder {
|
||||
checks, exists := checksByCategory[category]
|
||||
if !exists || len(checks) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
// Print category header
|
||||
_, _ = fmt.Fprintln(w, ui.RenderCategory(category))
|
||||
|
||||
// Print each check in this category
|
||||
for _, check := range checks {
|
||||
r.printCheck(w, check, verbose)
|
||||
if check.Status != StatusOK {
|
||||
warnings = append(warnings, check)
|
||||
}
|
||||
}
|
||||
_, _ = fmt.Fprintln(w)
|
||||
}
|
||||
|
||||
// Print any checks without a category
|
||||
if otherChecks, exists := checksByCategory["Other"]; exists && len(otherChecks) > 0 {
|
||||
_, _ = fmt.Fprintln(w, ui.RenderCategory("Other"))
|
||||
for _, check := range otherChecks {
|
||||
r.printCheck(w, check, verbose)
|
||||
if check.Status != StatusOK {
|
||||
warnings = append(warnings, check)
|
||||
}
|
||||
}
|
||||
_, _ = fmt.Fprintln(w)
|
||||
}
|
||||
|
||||
// Print separator and summary
|
||||
_, _ = fmt.Fprintln(w, ui.RenderSeparator())
|
||||
r.printSummary(w)
|
||||
|
||||
// Print warnings/errors section with fixes
|
||||
r.printWarningsSection(w, warnings)
|
||||
}
|
||||
|
||||
// printCheck outputs a single check result (output errors non-actionable).
|
||||
// printCheck outputs a single check result with semantic styling.
|
||||
func (r *Report) printCheck(w io.Writer, check *CheckResult, verbose bool) {
|
||||
var prefix string
|
||||
var statusIcon string
|
||||
switch check.Status {
|
||||
case StatusOK:
|
||||
prefix = style.SuccessPrefix
|
||||
statusIcon = ui.RenderPassIcon()
|
||||
case StatusWarning:
|
||||
prefix = style.WarningPrefix
|
||||
statusIcon = ui.RenderWarnIcon()
|
||||
case StatusError:
|
||||
prefix = style.ErrorPrefix
|
||||
statusIcon = ui.RenderFailIcon()
|
||||
}
|
||||
|
||||
_, _ = fmt.Fprintf(w, "%s %s: %s\n", prefix, check.Name, check.Message)
|
||||
// Print check line: icon + name + muted message
|
||||
_, _ = fmt.Fprintf(w, " %s %s", statusIcon, check.Name)
|
||||
if check.Message != "" {
|
||||
_, _ = fmt.Fprintf(w, "%s", ui.RenderMuted(" "+check.Message))
|
||||
}
|
||||
_, _ = fmt.Fprintln(w)
|
||||
|
||||
// Print details in verbose mode or for non-OK results
|
||||
// Print details in verbose mode or for non-OK results (with tree connector)
|
||||
if len(check.Details) > 0 && (verbose || check.Status != StatusOK) {
|
||||
for _, detail := range check.Details {
|
||||
_, _ = fmt.Fprintf(w, " %s\n", detail)
|
||||
_, _ = fmt.Fprintf(w, " %s%s\n", ui.MutedStyle.Render(ui.TreeLast), ui.RenderMuted(detail))
|
||||
}
|
||||
}
|
||||
|
||||
// Print fix hint for errors/warnings
|
||||
if check.FixHint != "" && check.Status != StatusOK {
|
||||
_, _ = fmt.Fprintf(w, " %s %s\n", style.ArrowPrefix, check.FixHint)
|
||||
}
|
||||
}
|
||||
|
||||
// printSummary outputs the summary line (output errors non-actionable).
|
||||
// printSummary outputs the summary line with semantic icons.
|
||||
func (r *Report) printSummary(w io.Writer) {
|
||||
parts := []string{
|
||||
fmt.Sprintf("%d checks", r.Summary.Total),
|
||||
}
|
||||
|
||||
if r.Summary.OK > 0 {
|
||||
parts = append(parts, style.Success.Render(fmt.Sprintf("%d passed", r.Summary.OK)))
|
||||
}
|
||||
if r.Summary.Warnings > 0 {
|
||||
parts = append(parts, style.Warning.Render(fmt.Sprintf("%d warnings", r.Summary.Warnings)))
|
||||
}
|
||||
if r.Summary.Errors > 0 {
|
||||
parts = append(parts, style.Error.Render(fmt.Sprintf("%d errors", r.Summary.Errors)))
|
||||
}
|
||||
|
||||
_, _ = fmt.Fprintln(w, strings.Join(parts, ", "))
|
||||
summary := fmt.Sprintf("%s %d passed %s %d warnings %s %d failed",
|
||||
ui.RenderPassIcon(), r.Summary.OK,
|
||||
ui.RenderWarnIcon(), r.Summary.Warnings,
|
||||
ui.RenderFailIcon(), r.Summary.Errors,
|
||||
)
|
||||
_, _ = fmt.Fprintln(w, summary)
|
||||
}
|
||||
|
||||
// printWarningsSection outputs numbered warnings/errors sorted by severity.
|
||||
func (r *Report) printWarningsSection(w io.Writer, warnings []*CheckResult) {
|
||||
if len(warnings) == 0 {
|
||||
_, _ = fmt.Fprintln(w)
|
||||
_, _ = fmt.Fprintln(w, ui.RenderPass(ui.IconPass+" All checks passed"))
|
||||
return
|
||||
}
|
||||
|
||||
_, _ = fmt.Fprintln(w)
|
||||
_, _ = fmt.Fprintln(w, ui.RenderWarn(ui.IconWarn+" WARNINGS"))
|
||||
|
||||
// Sort by severity: errors first, then warnings
|
||||
slices.SortStableFunc(warnings, func(a, b *CheckResult) int {
|
||||
if a.Status == StatusError && b.Status != StatusError {
|
||||
return -1
|
||||
}
|
||||
if a.Status != StatusError && b.Status == StatusError {
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
})
|
||||
|
||||
for i, check := range warnings {
|
||||
line := fmt.Sprintf("%s: %s", check.Name, check.Message)
|
||||
if check.Status == StatusError {
|
||||
_, _ = fmt.Fprintf(w, " %s %s %s\n", ui.RenderFailIcon(), ui.RenderFail(fmt.Sprintf("%d.", i+1)), ui.RenderFail(line))
|
||||
} else {
|
||||
_, _ = fmt.Fprintf(w, " %s %s %s\n", ui.RenderWarnIcon(), ui.RenderWarn(fmt.Sprintf("%d.", i+1)), line)
|
||||
}
|
||||
if check.FixHint != "" {
|
||||
_, _ = fmt.Fprintf(w, " %s%s\n", ui.MutedStyle.Render(ui.TreeLast), check.FixHint)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,6 +28,7 @@ func NewWispGCCheck() *WispGCCheck {
|
||||
BaseCheck: BaseCheck{
|
||||
CheckName: "wisp-gc",
|
||||
CheckDescription: "Detect and clean orphaned wisps (>1h old)",
|
||||
CheckCategory: CategoryCleanup,
|
||||
},
|
||||
},
|
||||
threshold: 1 * time.Hour,
|
||||
|
||||
@@ -18,6 +18,7 @@ func NewTownConfigExistsCheck() *TownConfigExistsCheck {
|
||||
BaseCheck: BaseCheck{
|
||||
CheckName: "town-config-exists",
|
||||
CheckDescription: "Check that mayor/town.json exists",
|
||||
CheckCategory: CategoryCore,
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -53,6 +54,7 @@ func NewTownConfigValidCheck() *TownConfigValidCheck {
|
||||
BaseCheck: BaseCheck{
|
||||
CheckName: "town-config-valid",
|
||||
CheckDescription: "Check that mayor/town.json is valid with required fields",
|
||||
CheckCategory: CategoryCore,
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -130,6 +132,7 @@ func NewRigsRegistryExistsCheck() *RigsRegistryExistsCheck {
|
||||
BaseCheck: BaseCheck{
|
||||
CheckName: "rigs-registry-exists",
|
||||
CheckDescription: "Check that mayor/rigs.json exists",
|
||||
CheckCategory: CategoryCore,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -188,6 +191,7 @@ func NewRigsRegistryValidCheck() *RigsRegistryValidCheck {
|
||||
BaseCheck: BaseCheck{
|
||||
CheckName: "rigs-registry-valid",
|
||||
CheckDescription: "Check that registered rigs exist on disk",
|
||||
CheckCategory: CategoryCore,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -320,6 +324,7 @@ func NewMayorExistsCheck() *MayorExistsCheck {
|
||||
BaseCheck: BaseCheck{
|
||||
CheckName: "mayor-exists",
|
||||
CheckDescription: "Check that mayor/ directory exists with required files",
|
||||
CheckCategory: CategoryCore,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user