From 50d9643db1effa2327c7c851edb3736c39a37b74 Mon Sep 17 00:00:00 2001 From: slit Date: Fri, 2 Jan 2026 12:11:29 -0800 Subject: [PATCH] feat(doctor): Add repo-fingerprint check for beads database (gt-nrgm5) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add repo-fingerprint check to gt doctor that verifies beads databases have valid repository fingerprints. Missing or empty fingerprints can cause daemon startup failures and sync issues. The check: - Uses bd doctor --json to check fingerprint status - Runs on town-level and rig-level beads directories - Can fix by running bd migrate --update-repo-id - Restarts daemon after migration if it was running 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- internal/cmd/doctor.go | 2 + internal/doctor/repo_fingerprint_check.go | 199 ++++++++++++++++++++++ 2 files changed, 201 insertions(+) create mode 100644 internal/doctor/repo_fingerprint_check.go diff --git a/internal/cmd/doctor.go b/internal/cmd/doctor.go index 86086fb7..0fb01cc4 100644 --- a/internal/cmd/doctor.go +++ b/internal/cmd/doctor.go @@ -34,6 +34,7 @@ Workspace checks: Infrastructure checks: - daemon Check if daemon is running (fixable) + - repo-fingerprint Check database has valid repo fingerprint (fixable) - boot-health Check Boot watchdog health (vet mode) Cleanup checks (fixable): @@ -99,6 +100,7 @@ func runDoctor(cmd *cobra.Command, args []string) error { // Register built-in checks d.Register(doctor.NewTownGitCheck()) d.Register(doctor.NewDaemonCheck()) + d.Register(doctor.NewRepoFingerprintCheck()) d.Register(doctor.NewBootHealthCheck()) d.Register(doctor.NewBeadsDatabaseCheck()) d.Register(doctor.NewBdDaemonCheck()) diff --git a/internal/doctor/repo_fingerprint_check.go b/internal/doctor/repo_fingerprint_check.go new file mode 100644 index 00000000..d55b86a9 --- /dev/null +++ b/internal/doctor/repo_fingerprint_check.go @@ -0,0 +1,199 @@ +package doctor + +import ( + "bytes" + "encoding/json" + "fmt" + "os" + "os/exec" + "path/filepath" + "time" + + "github.com/steveyegge/gastown/internal/beads" + "github.com/steveyegge/gastown/internal/daemon" +) + +// bdDoctorResult represents the JSON output from bd doctor --json. +type bdDoctorResult struct { + Checks []bdDoctorCheck `json:"checks"` +} + +// bdDoctorCheck represents a single check result from bd doctor. +type bdDoctorCheck struct { + Name string `json:"name"` + Status string `json:"status"` + Message string `json:"message"` + Detail string `json:"detail,omitempty"` + Fix string `json:"fix,omitempty"` +} + +// RepoFingerprintCheck verifies that beads databases have valid repository fingerprints. +// A missing or mismatched fingerprint can cause daemon startup failures and sync issues. +type RepoFingerprintCheck struct { + FixableCheck + needsMigration bool // Cached during Run for use in Fix + beadsDir string // Beads directory that needs migration +} + +// NewRepoFingerprintCheck creates a new repo fingerprint check. +func NewRepoFingerprintCheck() *RepoFingerprintCheck { + return &RepoFingerprintCheck{ + FixableCheck: FixableCheck{ + BaseCheck: BaseCheck{ + CheckName: "repo-fingerprint", + CheckDescription: "Verify beads database has valid repository fingerprint", + }, + }, + } +} + +// Run checks if beads databases have valid repo fingerprints. +func (c *RepoFingerprintCheck) Run(ctx *CheckContext) *CheckResult { + // Reset cached state + c.needsMigration = false + c.beadsDir = "" + + // Check town-level beads + townBeadsDir := filepath.Join(ctx.TownRoot, ".beads") + if _, err := os.Stat(townBeadsDir); err == nil { + result := c.checkBeadsDir(filepath.Dir(townBeadsDir), "town") + if result.Status != StatusOK { + return result + } + } + + // Check rig-level beads if specified + if ctx.RigName != "" { + rigBeadsDir := beads.ResolveBeadsDir(ctx.RigPath()) + if _, err := os.Stat(rigBeadsDir); err == nil { + result := c.checkBeadsDir(filepath.Dir(rigBeadsDir), "rig "+ctx.RigName) + if result.Status != StatusOK { + return result + } + } + } + + return &CheckResult{ + Name: c.Name(), + Status: StatusOK, + Message: "Repository fingerprints verified", + } +} + +// checkBeadsDir checks a single beads directory for repo fingerprint using bd doctor. +func (c *RepoFingerprintCheck) checkBeadsDir(workDir, location string) *CheckResult { + // Run bd doctor --json to get fingerprint status + cmd := exec.Command("bd", "doctor", "--json") + cmd.Dir = workDir + var stdout, stderr bytes.Buffer + cmd.Stdout = &stdout + cmd.Stderr = &stderr + + // bd doctor exits with non-zero if there are warnings, so ignore exit code + _ = cmd.Run() + + // Parse JSON output + var result bdDoctorResult + if err := json.Unmarshal(stdout.Bytes(), &result); err != nil { + // If we can't parse bd doctor output, skip this check + return &CheckResult{ + Name: c.Name(), + Status: StatusOK, + Message: fmt.Sprintf("Skipped %s (bd doctor unavailable)", location), + } + } + + // Find the Repo Fingerprint check + for _, check := range result.Checks { + if check.Name == "Repo Fingerprint" { + switch check.Status { + case "ok": + return &CheckResult{ + Name: c.Name(), + Status: StatusOK, + Message: fmt.Sprintf("Fingerprint verified in %s (%s)", location, check.Message), + } + case "warning": + c.needsMigration = true + c.beadsDir = filepath.Join(workDir, ".beads") + return &CheckResult{ + Name: c.Name(), + Status: StatusWarning, + Message: fmt.Sprintf("Fingerprint issue in %s: %s", location, check.Message), + Details: func() []string { + if check.Detail != "" { + return []string{check.Detail} + } + return nil + }(), + FixHint: "Run 'gt doctor --fix' or 'bd migrate --update-repo-id'", + } + case "error": + c.needsMigration = true + c.beadsDir = filepath.Join(workDir, ".beads") + return &CheckResult{ + Name: c.Name(), + Status: StatusError, + Message: fmt.Sprintf("Fingerprint error in %s: %s", location, check.Message), + Details: func() []string { + if check.Detail != "" { + return []string{check.Detail} + } + return nil + }(), + FixHint: "Run 'gt doctor --fix' or 'bd migrate --update-repo-id'", + } + } + } + } + + // Fingerprint check not found in bd doctor output - skip + return &CheckResult{ + Name: c.Name(), + Status: StatusOK, + Message: fmt.Sprintf("Fingerprint check not applicable for %s", location), + } +} + +// Fix runs bd migrate --update-repo-id and restarts the daemon. +func (c *RepoFingerprintCheck) Fix(ctx *CheckContext) error { + if !c.needsMigration || c.beadsDir == "" { + return nil + } + + // Run bd migrate --update-repo-id + cmd := exec.Command("bd", "migrate", "--update-repo-id") + cmd.Dir = filepath.Dir(c.beadsDir) // Parent of .beads directory + var stderr bytes.Buffer + cmd.Stderr = &stderr + if err := cmd.Run(); err != nil { + return fmt.Errorf("bd migrate --update-repo-id failed: %v: %s", err, stderr.String()) + } + + // Restart daemon if running + running, _, err := daemon.IsRunning(ctx.TownRoot) + if err == nil && running { + // Stop daemon + stopCmd := exec.Command("gt", "daemon", "stop") + stopCmd.Dir = ctx.TownRoot + _ = stopCmd.Run() // Ignore errors + + // Wait a moment + time.Sleep(500 * time.Millisecond) + + // Start daemon + startCmd := exec.Command("gt", "daemon", "run") + startCmd.Dir = ctx.TownRoot + startCmd.Stdin = nil + startCmd.Stdout = nil + startCmd.Stderr = nil + if err := startCmd.Start(); err != nil { + return fmt.Errorf("failed to restart daemon: %w", err) + } + + // Wait for daemon to initialize + time.Sleep(300 * time.Millisecond) + } + + return nil +}