// Command antidriftd is the AntiDrift focus daemon: it serves a local web UI // and owns the commitment state machine. package main import ( "context" "fmt" "log" "os/exec" "runtime" "time" "antidrift/internal/ai" "antidrift/internal/enforce" "antidrift/internal/evidence" "antidrift/internal/knowledge" "antidrift/internal/session" "antidrift/internal/settings" "antidrift/internal/statusfile" "antidrift/internal/store" "antidrift/internal/tasks" "antidrift/internal/web" ) const addr = "localhost:7777" func main() { path, err := store.DefaultPath() if err != nil { log.Fatalf("resolve snapshot path: %v", err) } ctrl, err := session.New(path) if err != nil { log.Fatalf("load session: %v", err) } srv := web.NewServer(ctrl) srv.Init() // re-arm or expire a restored Active session // Resolve and load settings, seeding from the legacy ANTIDRIFT_* env vars on // first run. After first run the file is the sole source of truth; env is // ignored. A missing file is the first-run signal. settingsPath, err := settings.DefaultPath() if err != nil { log.Fatalf("resolve settings path: %v", err) } cfg, err := settings.Load(settingsPath) if err != nil { cfg = settings.SeedFromEnv() if err := settings.Save(settingsPath, cfg); err != nil { log.Printf("settings: could not write %s (continuing): %v", settingsPath, err) } else { log.Printf("settings: seeded %s from environment", settingsPath) } } // The knowledge source is a single file adapter whose path is driven entirely // by SetKnowledgePath, so startup and live settings edits share one code path. ctrl.SetKnowledge(knowledge.NewFileSource("")) // applyFn re-wires the running daemon from a Settings value: AI backend + // service (coach/drift/nudge/reviewer), tasks command, and knowledge path. It // validates the backend FIRST and mutates nothing on failure, so POST /settings // can reject an invalid backend atomically. applyFn := func(s settings.Settings) error { backend, err := ai.NewBackend(s.AIBackend) if err != nil { return fmt.Errorf("%w: %v", settings.ErrInvalidBackend, err) } svc := ai.NewService(backend) ctrl.SetCoach(svc) ctrl.SetDriftJudge(svc) ctrl.SetNudge(svc) ctrl.SetReviewer(svc) ctrl.SetTasks(tasks.NewMarvin(s.MarvinCmd)) ctrl.SetKnowledgePath(s.KnowledgePath) return nil } // Apply once at startup. A bad persisted backend should not be fatal: fall back // to claude (always valid), persist the correction, and re-apply. if err := applyFn(cfg); err != nil { log.Printf("settings: %v; falling back to claude backend", err) cfg.AIBackend = "claude" if err := settings.Save(settingsPath, cfg); err != nil { log.Printf("settings: could not persist claude fallback to %s: %v", settingsPath, err) } if err := applyFn(cfg); err != nil { log.Fatalf("settings: apply failed even with claude: %v", err) } } srv.SetSettings(settingsPath, cfg, applyFn) log.Printf("settings: ai=%s, marvin=%q, knowledge=%q", cfg.AIBackend, cfg.MarvinCmd, cfg.KnowledgePath) // Mirror runtime status to ~/.antidrift_status for a window-manager bar. // A misconfigured home dir degrades to no status file, not a failed start. if statusPath, err := statusfile.DefaultPath(); err != nil { log.Printf("status file disabled: %v", err) } else { go statusfile.NewWriter(statusPath, ctrl.State).Run(context.Background()) log.Printf("status: writing %s", statusPath) } ctrl.SetGuard(enforce.NewGuard()) log.Printf("enforce: window-minimize guard") src := evidence.NewSource() go src.Watch(context.Background(), ctrl.RecordWindow) go openBrowser("http://" + addr) log.Printf("antidriftd listening on http://%s", addr) if err := srv.Router().Run(addr); err != nil { log.Fatalf("server: %v", err) } } // openBrowser best-effort launches the default browser after a short delay so // the server is listening first. Failures are logged, not fatal. func openBrowser(url string) { time.Sleep(300 * time.Millisecond) var cmd string var args []string switch runtime.GOOS { case "linux": cmd, args = "xdg-open", []string{url} case "darwin": cmd, args = "open", []string{url} case "windows": cmd, args = "rundll32", []string{"url.dll,FileProtocolHandler", url} default: log.Printf("open browser manually: %s", url) return } if err := exec.Command(cmd, args...).Start(); err != nil { log.Printf("could not open browser (%v); visit %s", err, url) } }