b1b807590c
main constructs the FileSource (default via ANTIDRIFT_KNOWLEDGE_FILE) and injects it. Adds POST /knowledge/path to repoint the profile file at runtime, and web tests asserting the planning payload carries the knowledge object (status + path, never the text) and that path selection reloads. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
106 lines
3.3 KiB
Go
106 lines
3.3 KiB
Go
// Command antidriftd is the AntiDrift focus daemon: it serves a local web UI
|
|
// and owns the commitment state machine.
|
|
package main
|
|
|
|
import (
|
|
"context"
|
|
"log"
|
|
"os"
|
|
"os/exec"
|
|
"runtime"
|
|
"time"
|
|
|
|
"antidrift/internal/ai"
|
|
"antidrift/internal/evidence"
|
|
"antidrift/internal/knowledge"
|
|
"antidrift/internal/session"
|
|
"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
|
|
|
|
// Wire the AI planning coach. Backend selectable via ANTIDRIFT_AI_BACKEND
|
|
// (claude default, or codex). Misconfiguration degrades to no coach rather
|
|
// than failing startup — manual planning still works.
|
|
if backend, err := ai.NewBackend(os.Getenv("ANTIDRIFT_AI_BACKEND")); err != nil {
|
|
log.Printf("ai disabled: %v", err)
|
|
} else {
|
|
svc := ai.NewService(backend)
|
|
ctrl.SetCoach(svc)
|
|
ctrl.SetDriftJudge(svc)
|
|
ctrl.SetNudge(svc)
|
|
log.Printf("ai: %s backend (coach + drift judge + nudge)", backend.Name())
|
|
}
|
|
|
|
// Wire the Tasks port: Amazing Marvin via ampy's `am` CLI. The command is
|
|
// overridable with ANTIDRIFT_MARVIN_CMD (e.g. "uv run am" or an absolute
|
|
// path). A missing or failing CLI degrades to no tasks panel at fetch time —
|
|
// planning still works.
|
|
ctrl.SetTasks(tasks.NewMarvin(os.Getenv("ANTIDRIFT_MARVIN_CMD")))
|
|
log.Printf("tasks: marvin adapter (am)")
|
|
|
|
// Wire the Knowledge port: a single profile file grounding the coach. The
|
|
// default path is overridable with ANTIDRIFT_KNOWLEDGE_FILE; unset falls back
|
|
// to ~/.antidrift/knowledge.md. A missing/unreadable file degrades to an
|
|
// ungrounded coach at load time — planning still works.
|
|
ctrl.SetKnowledge(knowledge.NewFileSource(os.Getenv("ANTIDRIFT_KNOWLEDGE_FILE")))
|
|
log.Printf("knowledge: file adapter")
|
|
|
|
// 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)
|
|
}
|
|
|
|
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)
|
|
}
|
|
}
|