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