feat: add tier and session_log MCP tools

Adds two new MCP skill packages:
- internal/skills/org: exposes the tier tool, calling an injected TierFn
  for testability; returns current operating tier as structured JSON
- internal/skills/sessionlog: exposes the session_log tool, appending
  structured JSONL entries to brain/sessions/{session_id}.jsonl; requires
  session_id, wraps internal/session.Append

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Mathias Bergqvist
2026-04-17 20:40:50 +02:00
parent e610e253ef
commit 9cfce8f700
6 changed files with 237 additions and 0 deletions

View File

@@ -0,0 +1,54 @@
// internal/skills/sessionlog/handlers.go
package sessionlog
import (
"context"
"encoding/json"
"fmt"
"time"
"github.com/mathiasbq/supervisor/internal/session"
)
type logArgs struct {
SessionID string `json:"session_id"`
Skill string `json:"skill"`
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"`
}
// Handle dispatches the session_log tool call.
func (s *Skill) Handle(ctx context.Context, tool string, args json.RawMessage) (json.RawMessage, error) {
if tool != "session_log" {
return nil, fmt.Errorf("unknown sessionlog tool: %s", tool)
}
var a logArgs
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,
}
if err := session.Append(s.cfg.SessionsDir, a.SessionID, entry); err != nil {
return nil, fmt.Errorf("append session log: %w", err)
}
b, _ := json.Marshal(map[string]string{"status": "ok", "session_id": a.SessionID})
return b, nil
}

View File

@@ -0,0 +1,44 @@
// internal/skills/sessionlog/handlers_test.go
package sessionlog_test
import (
"context"
"encoding/json"
"os"
"path/filepath"
"testing"
"github.com/mathiasbq/supervisor/internal/skills/sessionlog"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestHandle_SessionLog_AppendsEntry(t *testing.T) {
dir := t.TempDir()
s := sessionlog.New(sessionlog.Config{SessionsDir: dir})
args, _ := json.Marshal(map[string]any{
"session_id": "sess-abc",
"skill": "tdd_green",
"final_status": "pass",
"model_used": "ollama/qwen3",
"duration_ms": 3000,
})
out, err := s.Handle(context.Background(), "session_log", args)
require.NoError(t, err)
var result map[string]string
require.NoError(t, json.Unmarshal(out, &result))
assert.Equal(t, "ok", result["status"])
// Verify file written
data, err := os.ReadFile(filepath.Join(dir, "sess-abc.jsonl"))
require.NoError(t, err)
assert.Contains(t, string(data), "tdd_green")
}
func TestHandle_SessionLog_RequiresSessionID(t *testing.T) {
s := sessionlog.New(sessionlog.Config{SessionsDir: t.TempDir()})
args, _ := json.Marshal(map[string]any{"skill": "tdd_red"})
_, err := s.Handle(context.Background(), "session_log", args)
assert.Error(t, err)
}

View File

@@ -0,0 +1,49 @@
// internal/skills/sessionlog/skill.go
package sessionlog
import (
"encoding/json"
"github.com/mathiasbq/supervisor/internal/registry"
)
// Config holds sessionlog skill configuration.
type Config struct {
SessionsDir string // path to brain/sessions/
}
// Skill implements registry.Skill for the session_log tool.
type Skill struct {
cfg Config
}
// New constructs a sessionlog Skill.
func New(cfg Config) *Skill { return &Skill{cfg: cfg} }
// Name returns the skill name.
func (s *Skill) Name() string { return "sessionlog" }
// Tools returns the MCP tool definitions.
func (s *Skill) Tools() []registry.ToolDef {
return []registry.ToolDef{
{
Name: "session_log",
Description: "Append a structured entry to the current session log. Call after each skill invocation completes to record what happened for retrospective and training data extraction.",
InputSchema: json.RawMessage(`{
"type": "object",
"required": ["session_id"],
"properties": {
"session_id": {"type": "string"},
"skill": {"type": "string"},
"phase": {"type": "string"},
"project_root": {"type": "string"},
"final_status": {"type": "string"},
"file_path": {"type": "string"},
"model_used": {"type": "string"},
"duration_ms": {"type": "integer"},
"message": {"type": "string"}
}
}`),
},
}
}