feat(plan7): retire supervisor pod — delete cmd/supervisor, tdd/spec skills, Dockerfile
All checks were successful
CI / Lint / Test / Vet (push) Successful in 10s
CI / Mirror to GitHub (push) Successful in 3s

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 <noreply@anthropic.com>
This commit is contained in:
Mathias Bergqvist
2026-05-12 12:18:30 +02:00
parent 189ff89c34
commit c11763472c
9 changed files with 0 additions and 798 deletions

View File

@@ -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"]

View File

@@ -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)
}
}

View File

@@ -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)
}
}

View File

@@ -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})
}

View File

@@ -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")
}

View File

@@ -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,
},
),
},
}
}

View File

@@ -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,
})
}

View File

@@ -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"])
}

View File

@@ -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,
},
),
},
}
}