From c11763472c46a3140ece41896b73efce3ce656ff Mon Sep 17 00:00:00 2001 From: Mathias Bergqvist Date: Tue, 12 May 2026 12:18:30 +0200 Subject: [PATCH] =?UTF-8?q?feat(plan7):=20retire=20supervisor=20pod=20?= =?UTF-8?q?=E2=80=94=20delete=20cmd/supervisor,=20tdd/spec=20skills,=20Doc?= =?UTF-8?q?kerfile?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Removes the supervisor binary and its two exclusive skill packages (tdd, spec) now that all functionality is covered by SKILL.md files, the routing pod, and the brain MCP. Routing pod reuses review/debug/retrospective/trainer skill packages which are intentionally preserved. Co-Authored-By: Claude Sonnet 4.6 --- Dockerfile | 50 ------- cmd/supervisor/main.go | 182 -------------------------- cmd/supervisor/main_test.go | 14 -- internal/skills/spec/handlers.go | 87 ------------ internal/skills/spec/handlers_test.go | 53 -------- internal/skills/spec/skill.go | 56 -------- internal/skills/tdd/handlers.go | 173 ------------------------ internal/skills/tdd/handlers_test.go | 97 -------------- internal/skills/tdd/skill.go | 86 ------------ 9 files changed, 798 deletions(-) delete mode 100644 Dockerfile delete mode 100644 cmd/supervisor/main.go delete mode 100644 cmd/supervisor/main_test.go delete mode 100644 internal/skills/spec/handlers.go delete mode 100644 internal/skills/spec/handlers_test.go delete mode 100644 internal/skills/spec/skill.go delete mode 100644 internal/skills/tdd/handlers.go delete mode 100644 internal/skills/tdd/handlers_test.go delete mode 100644 internal/skills/tdd/skill.go diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index ebbab6c..0000000 --- a/Dockerfile +++ /dev/null @@ -1,50 +0,0 @@ -# syntax=docker/dockerfile:1 - -# ── Build stage ─────────────────────────────────────────────────────────────── -FROM golang:1.26-bookworm AS builder - -ARG VERSION=dev -WORKDIR /src - -COPY go.mod go.sum ./ -RUN go mod download - -COPY . . -RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 \ - go build -trimpath -ldflags="-s -w -X main.version=${VERSION}" \ - -o /out/supervisor ./cmd/supervisor - -# ── Runtime stage ───────────────────────────────────────────────────────────── -# Node.js 22 slim — needed for claude CLI subprocess -FROM node:22-slim - -# Install claude CLI (provides the `claude` binary the supervisor shells out to) -RUN npm install -g @anthropic-ai/claude-code \ - && claude --version \ - && echo "claude CLI installed" - -# Copy supervisor binary -COPY --from=builder /out/supervisor /usr/local/bin/supervisor - -# Bake in config (models.yaml + skill discipline files) -COPY config/ /app/config/ - -# Run as non-root -RUN groupadd -r supervisor && useradd -r -g supervisor -d /app supervisor - -WORKDIR /app - -# brain/ is writable state — mount a PersistentVolume here -VOLUME /app/brain - -ENV SUPERVISOR_CONFIG_DIR=/app/config/supervisor -ENV SUPERVISOR_MODELS_FILE=/app/config/models.yaml -ENV SUPERVISOR_BRAIN_DIR=/app/brain -ENV SUPERVISOR_SESSIONS_DIR=/app/brain/sessions -ENV SUPERVISOR_PORT=3200 - -USER supervisor - -EXPOSE 3200 - -ENTRYPOINT ["/usr/local/bin/supervisor"] diff --git a/cmd/supervisor/main.go b/cmd/supervisor/main.go deleted file mode 100644 index 66232a7..0000000 --- a/cmd/supervisor/main.go +++ /dev/null @@ -1,182 +0,0 @@ -package main - -import ( - "context" - "log/slog" - "net/http" - "os" - - "github.com/mathiasbq/supervisor/internal/auth" - "github.com/mathiasbq/supervisor/internal/config" - iexec "github.com/mathiasbq/supervisor/internal/exec" - "github.com/mathiasbq/supervisor/internal/mcp" - "github.com/mathiasbq/supervisor/internal/registry" - "github.com/mathiasbq/supervisor/internal/skills/brain" - "github.com/mathiasbq/supervisor/internal/skills/org" - "github.com/mathiasbq/supervisor/internal/skills/retrospective" - skilldebug "github.com/mathiasbq/supervisor/internal/skills/debug" - "github.com/mathiasbq/supervisor/internal/skills/review" - "github.com/mathiasbq/supervisor/internal/skills/spec" - "github.com/mathiasbq/supervisor/internal/skills/trainer" - "github.com/mathiasbq/supervisor/internal/skills/sessionlog" - "github.com/mathiasbq/supervisor/internal/skills/tdd" - "github.com/mathiasbq/supervisor/internal/tier" -) - -func main() { - logger := slog.New(slog.NewJSONHandler(os.Stdout, nil)) - - cfg, err := config.Load() - if err != nil { - logger.Error("load config", "err", err) - os.Exit(1) - } - - models, err := config.LoadModels(cfg.ModelsFile) - if err != nil { - logger.Error("load models", "err", err) - os.Exit(1) - } - - protocolsPrompt, err := os.ReadFile(cfg.ConfigDir + "/protocols.md") - if err != nil { - logger.Error("read protocols.md", "path", cfg.ConfigDir+"/protocols.md", "err", err) - os.Exit(1) - } - - // prependProtocols prepends the shared protocols to a skill discipline file. - prependProtocols := func(skillPrompt []byte) string { - return string(protocolsPrompt) + "\n---\n\n" + string(skillPrompt) - } - - tddPrompt, err := os.ReadFile(cfg.ConfigDir + "/tdd.md") - if err != nil { - logger.Error("read tdd.md", "path", cfg.ConfigDir+"/tdd.md", "err", err) - os.Exit(1) - } - - retroPrompt, err := os.ReadFile(cfg.ConfigDir + "/retrospective.md") - if err != nil { - logger.Error("read retrospective.md", "path", cfg.ConfigDir+"/retrospective.md", "err", err) - os.Exit(1) - } - - reviewPrompt, err := os.ReadFile(cfg.ConfigDir + "/review.md") - if err != nil { - logger.Error("read review.md", "path", cfg.ConfigDir+"/review.md", "err", err) - os.Exit(1) - } - - debugPrompt, err := os.ReadFile(cfg.ConfigDir + "/debug.md") - if err != nil { - logger.Error("read debug.md", "path", cfg.ConfigDir+"/debug.md", "err", err) - os.Exit(1) - } - - specPrompt, err := os.ReadFile(cfg.ConfigDir + "/spec.md") - if err != nil { - logger.Error("read spec.md", "path", cfg.ConfigDir+"/spec.md", "err", err) - os.Exit(1) - } - - trainerReaderPrompt, err := os.ReadFile(cfg.ConfigDir + "/trainer-reader.md") - if err != nil { - logger.Error("read trainer-reader.md", "path", cfg.ConfigDir+"/trainer-reader.md", "err", err) - os.Exit(1) - } - trainerWriterPrompt, err := os.ReadFile(cfg.ConfigDir + "/trainer-writer.md") - if err != nil { - logger.Error("read trainer-writer.md", "path", cfg.ConfigDir+"/trainer-writer.md", "err", err) - os.Exit(1) - } - - litellm := iexec.NewLiteLLM(cfg.LiteLLMBaseURL, cfg.LiteLLMAPIKey, 0) - - tierFn := func(ctx context.Context) tier.Info { - return tier.Detect(ctx, "https://api.anthropic.com", cfg.LiteLLMBaseURL) - } - - reg := registry.New() - reg.Register(tdd.New(tdd.Config{ - SkillPrompt: prependProtocols(tddPrompt), - DefaultModel: models.ModelFor("tdd", ""), - CompleteFunc: litellm.Complete, - SessionsDir: cfg.SessionsDir, - IngestBaseURL: cfg.IngestBaseURL, - })) - reg.Register(brain.New(brain.Config{ - IngestBaseURL: cfg.IngestBaseURL, - IngestSvcURL: cfg.IngestSvcURL, - KBRetrievalURL: cfg.KBRetrievalURL, - })) - reg.Register(org.New(org.Config{ - TierFn: tierFn, - })) - reg.Register(sessionlog.New(sessionlog.Config{ - SessionsDir: cfg.SessionsDir, - })) - reg.Register(retrospective.New(retrospective.Config{ - SkillPrompt: prependProtocols(retroPrompt), - DefaultModel: models.ModelFor("retrospective", ""), - SessionsDir: cfg.SessionsDir, - CompleteFunc: litellm.Complete, - })) - reg.Register(review.New(review.Config{ - SkillPrompt: prependProtocols(reviewPrompt), - DefaultModel: models.ModelFor("review", ""), - CompleteFunc: litellm.Complete, - SessionsDir: cfg.SessionsDir, - IngestBaseURL: cfg.IngestBaseURL, - })) - reg.Register(skilldebug.New(skilldebug.Config{ - SkillPrompt: prependProtocols(debugPrompt), - DefaultModel: models.ModelFor("debug", ""), - CompleteFunc: litellm.Complete, - SessionsDir: cfg.SessionsDir, - IngestBaseURL: cfg.IngestBaseURL, - })) - reg.Register(spec.New(spec.Config{ - SkillPrompt: prependProtocols(specPrompt), - DefaultModel: models.ModelFor("spec", ""), - CompleteFunc: litellm.Complete, - SessionsDir: cfg.SessionsDir, - IngestBaseURL: cfg.IngestBaseURL, - })) - reg.Register(trainer.New(trainer.Config{ - ReaderPrompt: prependProtocols(trainerReaderPrompt), - WriterPrompt: prependProtocols(trainerWriterPrompt), - DefaultModel: models.ModelFor("trainer", ""), - CompleteFunc: litellm.Complete, - SessionsDir: cfg.SessionsDir, - BrainDir: cfg.BrainDir, - })) - - var validator *auth.Validator - if dexURL := os.Getenv("DEX_ISSUER_URL"); dexURL != "" { - audience := os.Getenv("MCP_AUDIENCE") - v, err := auth.NewValidator(dexURL, audience) - if err != nil { - logger.Error("build jwt validator", "err", err) - os.Exit(1) - } - validator = v - logger.Info("jwt auth enabled", "issuer", dexURL) - } - - srv := mcp.NewServer(reg, cfg.MCPAuthToken, validator) - mux := http.NewServeMux() - mux.Handle("/mcp", srv) - - if dexURL := os.Getenv("DEX_ISSUER_URL"); dexURL != "" { - resourceURL := os.Getenv("MCP_RESOURCE_URL") - mux.HandleFunc("GET /.well-known/oauth-protected-resource", - auth.ProtectedResourceHandler(resourceURL, dexURL)) - } - - addr := ":" + cfg.Port - logger.Info("supervisor starting", "addr", addr, "version", "v0.5.0") - if err := http.ListenAndServe(addr, mux); err != nil { - logger.Error("server stopped", "err", err) - os.Exit(1) - } -} diff --git a/cmd/supervisor/main_test.go b/cmd/supervisor/main_test.go deleted file mode 100644 index 87cf84c..0000000 --- a/cmd/supervisor/main_test.go +++ /dev/null @@ -1,14 +0,0 @@ -package main - -import ( - "os/exec" - "testing" -) - -func TestBinaryCompiles(t *testing.T) { - cmd := exec.Command("go", "build", "./...") - out, err := cmd.CombinedOutput() - if err != nil { - t.Fatalf("build failed: %s\n%s", err, out) - } -} diff --git a/internal/skills/spec/handlers.go b/internal/skills/spec/handlers.go deleted file mode 100644 index 471afd8..0000000 --- a/internal/skills/spec/handlers.go +++ /dev/null @@ -1,87 +0,0 @@ -// internal/skills/spec/handlers.go -package spec - -import ( - "context" - "encoding/json" - "fmt" - "time" - - "github.com/mathiasbq/supervisor/internal/brain" - "github.com/mathiasbq/supervisor/internal/session" -) - -type specArgs struct { - ProjectRoot string `json:"project_root"` - Requirements string `json:"requirements"` - OutputPath string `json:"output_path"` - Context string `json:"context"` - Model string `json:"model"` - SessionID string `json:"session_id"` -} - -// Handle dispatches the MCP tool call to the appropriate handler. -func (s *Skill) Handle(ctx context.Context, tool string, args json.RawMessage) (json.RawMessage, error) { - if tool != "spec" { - return nil, fmt.Errorf("unknown tool: %s", tool) - } - var a specArgs - if err := json.Unmarshal(args, &a); err != nil { - return nil, fmt.Errorf("parse args: %w", err) - } - if a.ProjectRoot == "" { - return nil, fmt.Errorf("project_root is required") - } - if a.Requirements == "" { - return nil, fmt.Errorf("requirements is required") - } - outputPath := a.OutputPath - if outputPath == "" { - outputPath = "docs/spec.md" - } - - model := a.Model - if model == "" { - model = s.cfg.DefaultModel - } - - brainCtx, _ := brain.Query(ctx, s.cfg.IngestBaseURL, a.Requirements+" "+a.Context, 3) - - task := fmt.Sprintf( - "phase: spec\nproject_root: %s\nrequirements: %s\noutput_path: %s\ncontext: %s\nmodel: %s", - a.ProjectRoot, a.Requirements, outputPath, a.Context, model, - ) - task = session.PrependHistory(s.cfg.SessionsDir, a.SessionID, "spec", task) - if brainCtx != "" { - task = brainCtx + "\n---\n\n" + task - } - - if s.cfg.CompleteFunc == nil { - return nil, fmt.Errorf("no executor configured") - } - t0 := time.Now() - text, dur, err := s.cfg.CompleteFunc(ctx, model, s.cfg.SkillPrompt, task) - if err != nil { - return nil, err - } - - if a.SessionID != "" && s.cfg.SessionsDir != "" { - msg := text - if len(msg) > 200 { - msg = msg[:200] - } - _ = session.Append(s.cfg.SessionsDir, a.SessionID, session.Entry{ - SessionID: a.SessionID, - Timestamp: time.Now(), - Skill: "spec", - Phase: "spec", - ProjectRoot: a.ProjectRoot, - FinalStatus: "ok", - ModelUsed: model, - DurationMs: time.Since(t0).Milliseconds(), - Message: msg, - }) - } - - return json.Marshal(map[string]any{"text": text, "model": model, "duration_ms": dur}) -} diff --git a/internal/skills/spec/handlers_test.go b/internal/skills/spec/handlers_test.go deleted file mode 100644 index 3e864d2..0000000 --- a/internal/skills/spec/handlers_test.go +++ /dev/null @@ -1,53 +0,0 @@ -// internal/skills/spec/handlers_test.go -package spec_test - -import ( - "context" - "encoding/json" - "testing" - - "github.com/mathiasbq/supervisor/internal/skills/spec" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestSpecToolRegistered(t *testing.T) { - sk := spec.New(spec.Config{SkillPrompt: "spec rules"}) - names := make([]string, 0) - for _, tool := range sk.Tools() { - names = append(names, tool.Name) - } - assert.Contains(t, names, "spec") -} - -func TestSpecRequiresProjectRoot(t *testing.T) { - sk := spec.New(spec.Config{SkillPrompt: "s"}) - _, err := sk.Handle(context.Background(), "spec", json.RawMessage(`{"requirements":"add login"}`)) - assert.ErrorContains(t, err, "project_root") -} - -func TestSpecRequiresRequirements(t *testing.T) { - sk := spec.New(spec.Config{SkillPrompt: "s"}) - _, err := sk.Handle(context.Background(), "spec", json.RawMessage(`{"project_root":"/tmp"}`)) - assert.ErrorContains(t, err, "requirements") -} - -func TestSpecCallsCompleteFunc(t *testing.T) { - var capturedTask string - fakeFn := func(_ context.Context, _, _, user string) (string, int64, error) { - capturedTask = user - return "# OAuth2 Login Spec\n\n## Overview\nImplement OAuth2 login flow.", 110, nil - } - - sk := spec.New(spec.Config{SkillPrompt: "spec rules", CompleteFunc: fakeFn, SessionsDir: t.TempDir()}) - out, err := sk.Handle(context.Background(), "spec", json.RawMessage( - `{"project_root":"/tmp/proj","requirements":"add OAuth2 login","output_path":"docs/login-spec.md"}`, - )) - require.NoError(t, err) - assert.Contains(t, capturedTask, "OAuth2 login") - assert.Contains(t, capturedTask, "docs/login-spec.md") - - var result map[string]any - require.NoError(t, json.Unmarshal(out, &result)) - assert.Contains(t, result["text"], "OAuth2 Login Spec") -} diff --git a/internal/skills/spec/skill.go b/internal/skills/spec/skill.go deleted file mode 100644 index 461b886..0000000 --- a/internal/skills/spec/skill.go +++ /dev/null @@ -1,56 +0,0 @@ -// internal/skills/spec/skill.go -package spec - -import ( - "context" - "encoding/json" - - "github.com/mathiasbq/supervisor/internal/registry" -) - -// CompleteFunc is the function used to call a local model. -type CompleteFunc func(ctx context.Context, model, system, user string) (string, int64, error) - -// Config holds dependencies for the spec skill. -type Config struct { - SkillPrompt string - DefaultModel string - CompleteFunc CompleteFunc - SessionsDir string - IngestBaseURL string -} - -// Skill implements the spec MCP tool. -type Skill struct{ cfg Config } - -// New creates a new spec Skill. -func New(cfg Config) *Skill { return &Skill{cfg: cfg} } - -// Name returns the skill identifier. -func (s *Skill) Name() string { return "spec" } - -// Tools returns the MCP tool definitions for this skill. -func (s *Skill) Tools() []registry.ToolDef { - schema := func(required []string, props map[string]any) json.RawMessage { - b, _ := json.Marshal(map[string]any{"type": "object", "required": required, "properties": props}) - return b - } - str := map[string]any{"type": "string"} - return []registry.ToolDef{ - { - Name: "spec", - Description: "Consult a local model to draft a structured implementation spec from requirements. Returns the spec text.", - InputSchema: schema( - []string{"project_root", "requirements"}, - map[string]any{ - "project_root": str, - "requirements": str, - "output_path": str, - "context": str, - "model": str, - "session_id": str, - }, - ), - }, - } -} diff --git a/internal/skills/tdd/handlers.go b/internal/skills/tdd/handlers.go deleted file mode 100644 index f897fc6..0000000 --- a/internal/skills/tdd/handlers.go +++ /dev/null @@ -1,173 +0,0 @@ -package tdd - -import ( - "context" - "encoding/json" - "fmt" - "time" - - "github.com/mathiasbq/supervisor/internal/brain" - "github.com/mathiasbq/supervisor/internal/session" -) - -func (s *Skill) Handle(ctx context.Context, tool string, args json.RawMessage) (json.RawMessage, error) { - switch tool { - case "tdd_red": - return s.handleRed(ctx, args) - case "tdd_green": - return s.handleGreen(ctx, args) - case "tdd_refactor": - return s.handleRefactor(ctx, args) - default: - return nil, fmt.Errorf("unknown tool: %s", tool) - } -} - -type redArgs struct { - ProjectRoot string `json:"project_root"` - Spec string `json:"spec"` - Model string `json:"model"` - TestCmd string `json:"test_cmd"` -} - -func (s *Skill) handleRed(ctx context.Context, raw json.RawMessage) (json.RawMessage, error) { - var args redArgs - if err := json.Unmarshal(raw, &args); err != nil { - return nil, fmt.Errorf("parse args: %w", err) - } - if args.ProjectRoot == "" { - return nil, fmt.Errorf("project_root is required") - } - if args.Spec == "" { - return nil, fmt.Errorf("spec is required") - } - brainCtx, _ := brain.Query(ctx, s.cfg.IngestBaseURL, args.Spec, 3) - - task := fmt.Sprintf( - "phase: red\nproject_root: %s\nspec: %s\nmodel: %s\ntest_cmd: %s", - args.ProjectRoot, args.Spec, s.resolveModel(args.Model), args.TestCmd, - ) - if brainCtx != "" { - task = brainCtx + "\n---\n\n" + task - } - return s.complete(ctx, s.resolveModel(args.Model), task) -} - -type greenArgs struct { - ProjectRoot string `json:"project_root"` - TestPath string `json:"test_path"` - Model string `json:"model"` - TestCmd string `json:"test_cmd"` - SessionID string `json:"session_id"` -} - -func (s *Skill) handleGreen(ctx context.Context, raw json.RawMessage) (json.RawMessage, error) { - var args greenArgs - if err := json.Unmarshal(raw, &args); err != nil { - return nil, fmt.Errorf("parse args: %w", err) - } - if args.ProjectRoot == "" { - return nil, fmt.Errorf("project_root is required") - } - if args.TestPath == "" { - return nil, fmt.Errorf("test_path is required") - } - task := fmt.Sprintf( - "phase: green\nproject_root: %s\ntest_path: %s\nmodel: %s\ntest_cmd: %s", - args.ProjectRoot, args.TestPath, s.resolveModel(args.Model), args.TestCmd, - ) - task = session.PrependHistory(s.cfg.SessionsDir, args.SessionID, "green", task) - - t0 := time.Now() - result, err := s.complete(ctx, s.resolveModel(args.Model), task) - if err != nil { - return nil, err - } - s.logEntry(args.SessionID, args.ProjectRoot, "tdd", "green", s.resolveModel(args.Model), t0, result) - return result, nil -} - -type refactorArgs struct { - ProjectRoot string `json:"project_root"` - TestPath string `json:"test_path"` - ImplPath string `json:"impl_path"` - Model string `json:"model"` - TestCmd string `json:"test_cmd"` - SessionID string `json:"session_id"` -} - -func (s *Skill) handleRefactor(ctx context.Context, raw json.RawMessage) (json.RawMessage, error) { - var args refactorArgs - if err := json.Unmarshal(raw, &args); err != nil { - return nil, fmt.Errorf("parse args: %w", err) - } - if args.ProjectRoot == "" { - return nil, fmt.Errorf("project_root is required") - } - if args.TestPath == "" { - return nil, fmt.Errorf("test_path is required") - } - if args.ImplPath == "" { - return nil, fmt.Errorf("impl_path is required") - } - task := fmt.Sprintf( - "phase: refactor\nproject_root: %s\ntest_path: %s\nimpl_path: %s\nmodel: %s\ntest_cmd: %s", - args.ProjectRoot, args.TestPath, args.ImplPath, s.resolveModel(args.Model), args.TestCmd, - ) - task = session.PrependHistory(s.cfg.SessionsDir, args.SessionID, "refactor", task) - - t0 := time.Now() - result, err := s.complete(ctx, s.resolveModel(args.Model), task) - if err != nil { - return nil, err - } - s.logEntry(args.SessionID, args.ProjectRoot, "tdd", "refactor", s.resolveModel(args.Model), t0, result) - return result, nil -} - -func (s *Skill) resolveModel(override string) string { - if override != "" { - return override - } - return s.cfg.DefaultModel -} - -// complete calls CompleteFunc and returns the text as JSON. -func (s *Skill) complete(ctx context.Context, model, task string) (json.RawMessage, error) { - if s.cfg.CompleteFunc == nil { - return nil, fmt.Errorf("no executor configured") - } - text, dur, err := s.cfg.CompleteFunc(ctx, model, s.cfg.SkillPrompt, task) - if err != nil { - return nil, err - } - return json.Marshal(map[string]any{"text": text, "model": model, "duration_ms": dur}) -} - -// logEntry writes a session.Entry for a completed phase if session_id is set. -func (s *Skill) logEntry(sessionID, projectRoot, skill, phase, model string, t0 time.Time, raw json.RawMessage) { - if sessionID == "" || s.cfg.SessionsDir == "" { - return - } - var msg string - var result struct { - Text string `json:"text"` - } - if err := json.Unmarshal(raw, &result); err == nil && len(result.Text) > 0 { - msg = result.Text - if len(msg) > 200 { - msg = msg[:200] - } - } - _ = session.Append(s.cfg.SessionsDir, sessionID, session.Entry{ - SessionID: sessionID, - Timestamp: time.Now(), - Skill: skill, - Phase: phase, - ProjectRoot: projectRoot, - FinalStatus: "ok", - ModelUsed: model, - DurationMs: time.Since(t0).Milliseconds(), - Message: msg, - }) -} diff --git a/internal/skills/tdd/handlers_test.go b/internal/skills/tdd/handlers_test.go deleted file mode 100644 index ab0f1d5..0000000 --- a/internal/skills/tdd/handlers_test.go +++ /dev/null @@ -1,97 +0,0 @@ -package tdd_test - -import ( - "context" - "encoding/json" - "testing" - - "github.com/mathiasbq/supervisor/internal/session" - "github.com/mathiasbq/supervisor/internal/skills/tdd" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestTDDSkillTools(t *testing.T) { - skill := tdd.New(tdd.Config{ - SkillPrompt: "tdd rules", - }) - tools := skill.Tools() - names := make([]string, len(tools)) - for i, tool := range tools { - names[i] = tool.Name - } - assert.ElementsMatch(t, []string{"tdd_red", "tdd_green", "tdd_refactor"}, names) -} - -func TestTDDSkillHandleUnknown(t *testing.T) { - skill := tdd.New(tdd.Config{SkillPrompt: "t"}) - _, err := skill.Handle(context.Background(), "tdd_unknown", json.RawMessage(`{}`)) - assert.ErrorContains(t, err, "unknown tool") -} - -func TestTDDRedRequiresProjectRoot(t *testing.T) { - skill := tdd.New(tdd.Config{SkillPrompt: "t"}) - _, err := skill.Handle(context.Background(), "tdd_red", json.RawMessage(`{"spec":"add two numbers"}`)) - assert.ErrorContains(t, err, "project_root") -} - -func TestTDDRedRequiresSpec(t *testing.T) { - skill := tdd.New(tdd.Config{SkillPrompt: "t"}) - _, err := skill.Handle(context.Background(), "tdd_red", json.RawMessage(`{"project_root":"/tmp/proj"}`)) - assert.ErrorContains(t, err, "spec") -} - -func TestTDDGreenInjectsSessionHistory(t *testing.T) { - sessDir := t.TempDir() - require.NoError(t, session.Append(sessDir, "sess-1", session.Entry{ - SessionID: "sess-1", Skill: "tdd", Phase: "red", FinalStatus: "pass", - FilePath: "internal/foo/foo_test.go", - Message: "wrote failing test for Foo", - })) - - var capturedTask string - fakeFn := func(_ context.Context, _, _, user string) (string, int64, error) { - capturedTask = user - return "here is my suggestion", 100, nil - } - - sk := tdd.New(tdd.Config{SkillPrompt: "tdd", CompleteFunc: fakeFn, SessionsDir: sessDir}) - _, err := sk.Handle(context.Background(), "tdd_green", json.RawMessage( - `{"project_root":"/tmp","test_path":"internal/foo/foo_test.go","test_cmd":"go test ./...","session_id":"sess-1"}`, - )) - require.NoError(t, err) - assert.Contains(t, capturedTask, "## Session history") - assert.Contains(t, capturedTask, "wrote failing test for Foo") -} - -func TestTDDGreenNoHistoryWhenSessionIDEmpty(t *testing.T) { - var capturedTask string - fakeFn := func(_ context.Context, _, _, user string) (string, int64, error) { - capturedTask = user - return "suggestion", 50, nil - } - - sk := tdd.New(tdd.Config{SkillPrompt: "tdd", CompleteFunc: fakeFn, SessionsDir: t.TempDir()}) - _, err := sk.Handle(context.Background(), "tdd_green", json.RawMessage( - `{"project_root":"/tmp","test_path":"internal/foo/foo_test.go"}`, - )) - require.NoError(t, err) - assert.NotContains(t, capturedTask, "## Session history") -} - -func TestTDDGreenReturnsTextJSON(t *testing.T) { - fakeFn := func(_ context.Context, _, _, _ string) (string, int64, error) { - return "write a func that adds two ints", 42, nil - } - - sk := tdd.New(tdd.Config{SkillPrompt: "tdd", CompleteFunc: fakeFn}) - raw, err := sk.Handle(context.Background(), "tdd_green", json.RawMessage( - `{"project_root":"/tmp","test_path":"foo_test.go"}`, - )) - require.NoError(t, err) - - var result map[string]any - require.NoError(t, json.Unmarshal(raw, &result)) - assert.Equal(t, "write a func that adds two ints", result["text"]) - assert.Equal(t, float64(42), result["duration_ms"]) -} diff --git a/internal/skills/tdd/skill.go b/internal/skills/tdd/skill.go deleted file mode 100644 index 5d99caf..0000000 --- a/internal/skills/tdd/skill.go +++ /dev/null @@ -1,86 +0,0 @@ -package tdd - -import ( - "context" - "encoding/json" - - "github.com/mathiasbq/supervisor/internal/registry" -) - -// CompleteFunc is the function used to call a local model. -type CompleteFunc func(ctx context.Context, model, system, user string) (string, int64, error) - -type Config struct { - SkillPrompt string - CompleteFunc CompleteFunc // nil = no executor (tests that don't reach execute()) - DefaultModel string - SessionsDir string // optional: path to brain/sessions/ for history injection - IngestBaseURL string // optional: base URL of ingestion server for brain context -} - -type Skill struct { - cfg Config -} - -func New(cfg Config) *Skill { - return &Skill{cfg: cfg} -} - -func (s *Skill) Name() string { return "tdd" } - -func (s *Skill) Tools() []registry.ToolDef { - schema := func(required []string, props map[string]any) json.RawMessage { - b, _ := json.Marshal(map[string]any{ - "type": "object", - "required": required, - "properties": props, - }) - return b - } - strProp := map[string]any{"type": "string"} - - return []registry.ToolDef{ - { - Name: "tdd_red", - Description: "Consult a local model for help writing a failing test for the described behavior.", - InputSchema: schema( - []string{"project_root", "spec"}, - map[string]any{ - "project_root": strProp, - "spec": strProp, - "model": strProp, - "test_cmd": strProp, - }, - ), - }, - { - Name: "tdd_green", - Description: "Consult a local model for implementation ideas to make the test at test_path pass.", - InputSchema: schema( - []string{"project_root", "test_path"}, - map[string]any{ - "project_root": strProp, - "test_path": strProp, - "model": strProp, - "test_cmd": strProp, - "session_id": strProp, - }, - ), - }, - { - Name: "tdd_refactor", - Description: "Consult a local model for refactoring suggestions for impl_path while keeping tests green.", - InputSchema: schema( - []string{"project_root", "test_path", "impl_path"}, - map[string]any{ - "project_root": strProp, - "test_path": strProp, - "impl_path": strProp, - "model": strProp, - "test_cmd": strProp, - "session_id": strProp, - }, - ), - }, - } -}