From bd0c1d75fd20f93f79f46e33289e6634557472b6 Mon Sep 17 00:00:00 2001 From: Mathias Bergqvist Date: Fri, 1 May 2026 13:04:40 +0200 Subject: [PATCH] feat(ingestion): implement session_log MCP tool Appends a JSON line to brainDir/sessions/.jsonl using the session package copied in Task 2. Required for upcoming pass-rate logging. Co-Authored-By: Claude Opus 4.7 (1M context) --- ingestion/internal/mcp/handlers.go | 41 +++++++++++++++++++++++++ ingestion/internal/mcp/handlers_test.go | 24 +++++++++++++++ ingestion/internal/mcp/server.go | 2 ++ 3 files changed, 67 insertions(+) diff --git a/ingestion/internal/mcp/handlers.go b/ingestion/internal/mcp/handlers.go index 4f07977..9c01bbb 100644 --- a/ingestion/internal/mcp/handlers.go +++ b/ingestion/internal/mcp/handlers.go @@ -6,11 +6,13 @@ import ( "fmt" "path/filepath" "strings" + "time" "github.com/mathiasbq/hyperguild/ingestion/internal/api" "github.com/mathiasbq/hyperguild/ingestion/internal/extract" "github.com/mathiasbq/hyperguild/ingestion/internal/pipeline" "github.com/mathiasbq/hyperguild/ingestion/internal/search" + "github.com/mathiasbq/hyperguild/ingestion/internal/session" ) // tools returns the tool descriptors. Handler bodies for each tool are filled @@ -198,6 +200,45 @@ func (s *Server) brainIngest(ctx context.Context, args json.RawMessage) (json.Ra return s.runIngest(ctx, a.Content, a.Source, a.DryRun) } +type sessionLogArgs struct { + SessionID string `json:"session_id"` + Skill string `json:"skill,omitempty"` + Phase string `json:"phase,omitempty"` + ProjectRoot string `json:"project_root,omitempty"` + FinalStatus string `json:"final_status,omitempty"` + FilePath string `json:"file_path,omitempty"` + ModelUsed string `json:"model_used,omitempty"` + DurationMs int64 `json:"duration_ms,omitempty"` + Message string `json:"message,omitempty"` +} + +func (s *Server) sessionLog(ctx context.Context, args json.RawMessage) (json.RawMessage, error) { + var a sessionLogArgs + if err := json.Unmarshal(args, &a); err != nil { + return nil, fmt.Errorf("parse args: %w", err) + } + if a.SessionID == "" { + return nil, fmt.Errorf("session_id is required") + } + entry := session.Entry{ + SessionID: a.SessionID, + Timestamp: time.Now().UTC(), + Skill: a.Skill, + Phase: a.Phase, + ProjectRoot: a.ProjectRoot, + FinalStatus: a.FinalStatus, + FilePath: a.FilePath, + ModelUsed: a.ModelUsed, + DurationMs: a.DurationMs, + Message: a.Message, + } + dir := filepath.Join(s.brainDir, "sessions") + if err := session.Append(dir, a.SessionID, entry); err != nil { + return nil, fmt.Errorf("append: %w", err) + } + return json.Marshal(map[string]string{"status": "ok", "session_id": a.SessionID}) +} + func (s *Server) runIngest(ctx context.Context, content, source string, dryRun bool) (json.RawMessage, error) { result, err := pipeline.Run(ctx, s.pipeline, s.brainDir, content, source, dryRun) if err != nil { diff --git a/ingestion/internal/mcp/handlers_test.go b/ingestion/internal/mcp/handlers_test.go index e248ac1..6d3fce7 100644 --- a/ingestion/internal/mcp/handlers_test.go +++ b/ingestion/internal/mcp/handlers_test.go @@ -170,3 +170,27 @@ func TestBrainIngestRequiresLLMConfigured(t *testing.T) { errObj := resp["error"].(map[string]any) assert.Contains(t, errObj["message"].(string), "LLM not configured") } + +func TestSessionLogAppends(t *testing.T) { + brainDir := t.TempDir() + srv := mcp.NewServer(brainDir, nil, nil) + + resp := toolCall(t, srv, "session_log", map[string]any{ + "session_id": "session-x", + "skill": "tdd", + "phase": "red", + "final_status": "ok", + }) + require.Nil(t, resp["error"]) + + got, err := os.ReadFile(filepath.Join(brainDir, "sessions", "session-x.jsonl")) + require.NoError(t, err) + assert.Contains(t, string(got), `"skill":"tdd"`) + assert.Contains(t, string(got), `"phase":"red"`) +} + +func TestSessionLogRequiresSessionID(t *testing.T) { + srv := mcp.NewServer(t.TempDir(), nil, nil) + resp := toolCall(t, srv, "session_log", map[string]any{"skill": "tdd"}) + require.NotNil(t, resp["error"]) +} diff --git a/ingestion/internal/mcp/server.go b/ingestion/internal/mcp/server.go index 9069e82..a787f7a 100644 --- a/ingestion/internal/mcp/server.go +++ b/ingestion/internal/mcp/server.go @@ -124,6 +124,8 @@ func (s *Server) handleCall(ctx context.Context, name string, args json.RawMessa return s.brainIngestRaw(ctx, args) case "brain_ingest": return s.brainIngest(ctx, args) + case "session_log": + return s.sessionLog(ctx, args) default: return nil, fmt.Errorf("unknown tool: %s", name) }