Wire the knowledge file adapter and a runtime path selector
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>
This commit is contained in:
@@ -12,6 +12,7 @@ import (
|
||||
|
||||
"antidrift/internal/ai"
|
||||
"antidrift/internal/evidence"
|
||||
"antidrift/internal/knowledge"
|
||||
"antidrift/internal/session"
|
||||
"antidrift/internal/statusfile"
|
||||
"antidrift/internal/store"
|
||||
@@ -54,6 +55,13 @@ func main() {
|
||||
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 {
|
||||
|
||||
@@ -58,6 +58,7 @@ func (s *Server) Router() *gin.Engine {
|
||||
r.POST("/end", s.handleEnd)
|
||||
r.POST("/refocus", s.handleRefocus)
|
||||
r.POST("/ontask", s.handleOnTask)
|
||||
r.POST("/knowledge/path", s.handleKnowledgePath)
|
||||
return r
|
||||
}
|
||||
|
||||
@@ -124,6 +125,24 @@ func (s *Server) handleCommitment(c *gin.Context) {
|
||||
s.respond(c, err)
|
||||
}
|
||||
|
||||
type knowledgePathRequest struct {
|
||||
Path string `json:"path"`
|
||||
}
|
||||
|
||||
// handleKnowledgePath repoints the profile file at runtime (session-only). It
|
||||
// mutates config, not commitment state, so it never returns a transition error;
|
||||
// it just sets the path, re-loads, and broadcasts the refreshed state.
|
||||
func (s *Server) handleKnowledgePath(c *gin.Context) {
|
||||
var req knowledgePathRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid json"})
|
||||
return
|
||||
}
|
||||
s.ctrl.SetKnowledgePath(req.Path)
|
||||
s.broadcast()
|
||||
c.Data(http.StatusOK, "application/json", []byte(s.stateJSON()))
|
||||
}
|
||||
|
||||
func (s *Server) handleComplete(c *gin.Context) {
|
||||
s.cancelExpiry()
|
||||
s.respond(c, s.ctrl.Complete())
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
"antidrift/internal/ai"
|
||||
"antidrift/internal/domain"
|
||||
"antidrift/internal/evidence"
|
||||
"antidrift/internal/knowledge"
|
||||
"antidrift/internal/session"
|
||||
"antidrift/internal/tasks"
|
||||
|
||||
@@ -108,7 +109,7 @@ type stubCoach struct {
|
||||
err error
|
||||
}
|
||||
|
||||
func (s stubCoach) Coach(ctx context.Context, intent string) (ai.Proposal, error) {
|
||||
func (s stubCoach) Coach(ctx context.Context, intent, grounding string) (ai.Proposal, error) {
|
||||
return s.prop, s.err
|
||||
}
|
||||
|
||||
@@ -281,3 +282,57 @@ func TestCommitmentCarriesAllowedClassesAndRoutesWork(t *testing.T) {
|
||||
t.Fatalf("refocus while Active: want 200, got %d", w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
type stubSource struct {
|
||||
profile knowledge.Profile
|
||||
}
|
||||
|
||||
func (s stubSource) Load(ctx context.Context, path string) (knowledge.Profile, error) {
|
||||
if path != "" {
|
||||
return knowledge.Profile{Text: "from " + path, Path: path}, nil
|
||||
}
|
||||
return s.profile, nil
|
||||
}
|
||||
|
||||
func TestPlanningStatePayloadCarriesKnowledge(t *testing.T) {
|
||||
s := newTestServer(t)
|
||||
s.ctrl.SetKnowledge(stubSource{profile: knowledge.Profile{Text: "I value small diffs.", Path: "/home/u/.antidrift/knowledge.md"}})
|
||||
r := s.Router()
|
||||
if w := post(t, r, "/planning", ""); w.Code != http.StatusOK {
|
||||
t.Fatalf("/planning code %d", w.Code)
|
||||
}
|
||||
deadline := time.Now().Add(2 * time.Second)
|
||||
for time.Now().Before(deadline) {
|
||||
if kv := s.ctrl.State().Knowledge; kv != nil && kv.Status == "ready" {
|
||||
break
|
||||
}
|
||||
time.Sleep(5 * time.Millisecond)
|
||||
}
|
||||
js := s.stateJSON()
|
||||
if !strings.Contains(js, `"knowledge"`) || !strings.Contains(js, `"status":"ready"`) {
|
||||
t.Fatalf("planning payload missing knowledge object: %s", js)
|
||||
}
|
||||
if strings.Contains(js, "small diffs") {
|
||||
t.Fatalf("profile text must NOT cross the wire: %s", js)
|
||||
}
|
||||
}
|
||||
|
||||
func TestKnowledgePathSelectionReloads(t *testing.T) {
|
||||
s := newTestServer(t)
|
||||
s.ctrl.SetKnowledge(stubSource{profile: knowledge.Profile{Path: "/default"}})
|
||||
r := s.Router()
|
||||
if w := post(t, r, "/planning", ""); w.Code != http.StatusOK {
|
||||
t.Fatalf("/planning code %d", w.Code)
|
||||
}
|
||||
if w := post(t, r, "/knowledge/path", `{"path":"/custom/profile.md"}`); w.Code != http.StatusOK {
|
||||
t.Fatalf("/knowledge/path code %d body %s", w.Code, w.Body.String())
|
||||
}
|
||||
deadline := time.Now().Add(2 * time.Second)
|
||||
for time.Now().Before(deadline) {
|
||||
if kv := s.ctrl.State().Knowledge; kv != nil && kv.Path == "/custom/profile.md" {
|
||||
return
|
||||
}
|
||||
time.Sleep(5 * time.Millisecond)
|
||||
}
|
||||
t.Fatalf("path selection did not reload: %+v", s.ctrl.State().Knowledge)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user