package composer import ( "bytes" "context" "encoding/json" "fmt" "io" "net/http" "time" "github.com/google/uuid" "github.com/rs/zerolog/log" ) // SpecKitClient handles communication with the spec-kit service type SpecKitClient struct { baseURL string httpClient *http.Client config *SpecKitClientConfig } // SpecKitClientConfig contains configuration for the spec-kit client type SpecKitClientConfig struct { ServiceURL string `json:"service_url"` Timeout time.Duration `json:"timeout"` MaxRetries int `json:"max_retries"` RetryDelay time.Duration `json:"retry_delay"` EnableCircuitBreaker bool `json:"enable_circuit_breaker"` UserAgent string `json:"user_agent"` } // ProjectInitializeRequest for creating new spec-kit projects type ProjectInitializeRequest struct { ProjectName string `json:"project_name"` Description string `json:"description"` RepositoryURL string `json:"repository_url,omitempty"` ChorusMetadata map[string]interface{} `json:"chorus_metadata"` } // ProjectInitializeResponse from spec-kit service initialization type ProjectInitializeResponse struct { ProjectID string `json:"project_id"` BranchName string `json:"branch_name"` SpecFilePath string `json:"spec_file_path"` FeatureNumber string `json:"feature_number"` Status string `json:"status"` } // ConstitutionRequest for executing constitution phase type ConstitutionRequest struct { PrinciplesDescription string `json:"principles_description"` OrganizationContext map[string]interface{} `json:"organization_context"` } // ConstitutionResponse from constitution phase execution type ConstitutionResponse struct { Constitution ConstitutionData `json:"constitution"` FilePath string `json:"file_path"` Status string `json:"status"` } // ConstitutionData contains the structured constitution information type ConstitutionData struct { Principles []Principle `json:"principles"` Governance string `json:"governance"` Version string `json:"version"` RatifiedDate string `json:"ratified_date"` } // Principle represents a single principle in the constitution type Principle struct { Name string `json:"name"` Description string `json:"description"` } // SpecificationRequest for executing specification phase type SpecificationRequest struct { FeatureDescription string `json:"feature_description"` AcceptanceCriteria []string `json:"acceptance_criteria"` } // SpecificationResponse from specification phase execution type SpecificationResponse struct { Specification SpecificationData `json:"specification"` FilePath string `json:"file_path"` CompletenessScore float64 `json:"completeness_score"` ClarificationsNeeded []string `json:"clarifications_needed"` Status string `json:"status"` } // SpecificationData contains structured specification information type SpecificationData struct { FeatureName string `json:"feature_name"` UserScenarios []UserScenario `json:"user_scenarios"` FunctionalRequirements []Requirement `json:"functional_requirements"` Entities []Entity `json:"entities"` } // UserScenario represents a user story or scenario type UserScenario struct { PrimaryStory string `json:"primary_story"` AcceptanceScenarios []string `json:"acceptance_scenarios"` } // Requirement represents a functional requirement type Requirement struct { ID string `json:"id"` Requirement string `json:"requirement"` } // Entity represents a key business entity type Entity struct { Name string `json:"name"` Description string `json:"description"` } // PlanningRequest for executing planning phase type PlanningRequest struct { TechStack map[string]interface{} `json:"tech_stack"` ArchitecturePreferences map[string]interface{} `json:"architecture_preferences"` } // PlanningResponse from planning phase execution type PlanningResponse struct { Plan PlanData `json:"plan"` FilePath string `json:"file_path"` Status string `json:"status"` } // PlanData contains structured planning information type PlanData struct { TechStack map[string]interface{} `json:"tech_stack"` Architecture map[string]interface{} `json:"architecture"` Implementation map[string]interface{} `json:"implementation"` TestingStrategy map[string]interface{} `json:"testing_strategy"` } // TasksResponse from tasks phase execution type TasksResponse struct { Tasks TasksData `json:"tasks"` FilePath string `json:"file_path"` Status string `json:"status"` } // TasksData contains structured task information type TasksData struct { SetupTasks []Task `json:"setup_tasks"` CoreTasks []Task `json:"core_tasks"` IntegrationTasks []Task `json:"integration_tasks"` PolishTasks []Task `json:"polish_tasks"` } // Task represents a single implementation task type Task struct { ID string `json:"id"` Title string `json:"title"` Description string `json:"description"` Dependencies []string `json:"dependencies"` Parallel bool `json:"parallel"` EstimatedHours int `json:"estimated_hours"` } // ProjectStatusResponse contains current project status type ProjectStatusResponse struct { ProjectID string `json:"project_id"` CurrentPhase string `json:"current_phase"` PhasesCompleted []string `json:"phases_completed"` OverallProgress float64 `json:"overall_progress"` Artifacts []ArtifactInfo `json:"artifacts"` QualityMetrics map[string]float64 `json:"quality_metrics"` } // ArtifactInfo contains information about generated artifacts type ArtifactInfo struct { Type string `json:"type"` Path string `json:"path"` LastModified time.Time `json:"last_modified"` } // NewSpecKitClient creates a new spec-kit service client func NewSpecKitClient(config *SpecKitClientConfig) *SpecKitClient { if config == nil { config = &SpecKitClientConfig{ Timeout: 30 * time.Second, MaxRetries: 3, RetryDelay: 1 * time.Second, UserAgent: "WHOOSH-SpecKit-Client/1.0", } } return &SpecKitClient{ baseURL: config.ServiceURL, httpClient: &http.Client{ Timeout: config.Timeout, }, config: config, } } // InitializeProject creates a new spec-kit project func (c *SpecKitClient) InitializeProject( ctx context.Context, req *ProjectInitializeRequest, ) (*ProjectInitializeResponse, error) { log.Info(). Str("project_name", req.ProjectName). Str("council_id", fmt.Sprintf("%v", req.ChorusMetadata["council_id"])). Msg("Initializing spec-kit project") var response ProjectInitializeResponse err := c.makeRequest(ctx, "POST", "/v1/projects/initialize", req, &response) if err != nil { return nil, fmt.Errorf("failed to initialize project: %w", err) } log.Info(). Str("project_id", response.ProjectID). Str("branch_name", response.BranchName). Str("status", response.Status). Msg("Spec-kit project initialized successfully") return &response, nil } // ExecuteConstitution runs the constitution phase func (c *SpecKitClient) ExecuteConstitution( ctx context.Context, projectID string, req *ConstitutionRequest, ) (*ConstitutionResponse, error) { log.Info(). Str("project_id", projectID). Msg("Executing constitution phase") var response ConstitutionResponse url := fmt.Sprintf("/v1/projects/%s/constitution", projectID) err := c.makeRequest(ctx, "POST", url, req, &response) if err != nil { return nil, fmt.Errorf("failed to execute constitution phase: %w", err) } log.Info(). Str("project_id", projectID). Int("principles_count", len(response.Constitution.Principles)). Str("status", response.Status). Msg("Constitution phase completed") return &response, nil } // ExecuteSpecification runs the specification phase func (c *SpecKitClient) ExecuteSpecification( ctx context.Context, projectID string, req *SpecificationRequest, ) (*SpecificationResponse, error) { log.Info(). Str("project_id", projectID). Msg("Executing specification phase") var response SpecificationResponse url := fmt.Sprintf("/v1/projects/%s/specify", projectID) err := c.makeRequest(ctx, "POST", url, req, &response) if err != nil { return nil, fmt.Errorf("failed to execute specification phase: %w", err) } log.Info(). Str("project_id", projectID). Str("feature_name", response.Specification.FeatureName). Float64("completeness_score", response.CompletenessScore). Int("clarifications_needed", len(response.ClarificationsNeeded)). Str("status", response.Status). Msg("Specification phase completed") return &response, nil } // ExecutePlanning runs the planning phase func (c *SpecKitClient) ExecutePlanning( ctx context.Context, projectID string, req *PlanningRequest, ) (*PlanningResponse, error) { log.Info(). Str("project_id", projectID). Msg("Executing planning phase") var response PlanningResponse url := fmt.Sprintf("/v1/projects/%s/plan", projectID) err := c.makeRequest(ctx, "POST", url, req, &response) if err != nil { return nil, fmt.Errorf("failed to execute planning phase: %w", err) } log.Info(). Str("project_id", projectID). Str("status", response.Status). Msg("Planning phase completed") return &response, nil } // ExecuteTasks runs the tasks phase func (c *SpecKitClient) ExecuteTasks( ctx context.Context, projectID string, ) (*TasksResponse, error) { log.Info(). Str("project_id", projectID). Msg("Executing tasks phase") var response TasksResponse url := fmt.Sprintf("/v1/projects/%s/tasks", projectID) err := c.makeRequest(ctx, "POST", url, nil, &response) if err != nil { return nil, fmt.Errorf("failed to execute tasks phase: %w", err) } totalTasks := len(response.Tasks.SetupTasks) + len(response.Tasks.CoreTasks) + len(response.Tasks.IntegrationTasks) + len(response.Tasks.PolishTasks) log.Info(). Str("project_id", projectID). Int("total_tasks", totalTasks). Str("status", response.Status). Msg("Tasks phase completed") return &response, nil } // GetProjectStatus retrieves current project status func (c *SpecKitClient) GetProjectStatus( ctx context.Context, projectID string, ) (*ProjectStatusResponse, error) { log.Debug(). Str("project_id", projectID). Msg("Retrieving project status") var response ProjectStatusResponse url := fmt.Sprintf("/v1/projects/%s/status", projectID) err := c.makeRequest(ctx, "GET", url, nil, &response) if err != nil { return nil, fmt.Errorf("failed to get project status: %w", err) } return &response, nil } // ExecuteWorkflow executes a complete spec-kit workflow func (c *SpecKitClient) ExecuteWorkflow( ctx context.Context, req *SpecKitWorkflowRequest, ) (*SpecKitWorkflowResponse, error) { startTime := time.Now() log.Info(). Str("project_name", req.ProjectName). Strs("phases", req.WorkflowPhases). Msg("Starting complete spec-kit workflow execution") // Step 1: Initialize project initReq := &ProjectInitializeRequest{ ProjectName: req.ProjectName, Description: req.Description, RepositoryURL: req.RepositoryURL, ChorusMetadata: req.ChorusMetadata, } initResp, err := c.InitializeProject(ctx, initReq) if err != nil { return nil, fmt.Errorf("workflow initialization failed: %w", err) } projectID := initResp.ProjectID var artifacts []SpecKitArtifact phasesCompleted := []string{} // Execute each requested phase for _, phase := range req.WorkflowPhases { switch phase { case "constitution": constReq := &ConstitutionRequest{ PrinciplesDescription: "Create project principles focused on quality, testing, and performance", OrganizationContext: req.ChorusMetadata, } constResp, err := c.ExecuteConstitution(ctx, projectID, constReq) if err != nil { log.Error().Err(err).Str("phase", phase).Msg("Phase execution failed") continue } artifact := SpecKitArtifact{ Type: "constitution", Phase: phase, Content: map[string]interface{}{"constitution": constResp.Constitution}, FilePath: constResp.FilePath, CreatedAt: time.Now(), Quality: 0.95, // High quality for structured constitution } artifacts = append(artifacts, artifact) phasesCompleted = append(phasesCompleted, phase) case "specify": specReq := &SpecificationRequest{ FeatureDescription: req.Description, AcceptanceCriteria: []string{}, // Could be extracted from description } specResp, err := c.ExecuteSpecification(ctx, projectID, specReq) if err != nil { log.Error().Err(err).Str("phase", phase).Msg("Phase execution failed") continue } artifact := SpecKitArtifact{ Type: "specification", Phase: phase, Content: map[string]interface{}{"specification": specResp.Specification}, FilePath: specResp.FilePath, CreatedAt: time.Now(), Quality: specResp.CompletenessScore, } artifacts = append(artifacts, artifact) phasesCompleted = append(phasesCompleted, phase) case "plan": planReq := &PlanningRequest{ TechStack: map[string]interface{}{ "backend": "Go with chi framework", "frontend": "React with TypeScript", "database": "PostgreSQL", }, ArchitecturePreferences: map[string]interface{}{ "pattern": "microservices", "api_style": "REST", "testing": "TDD", }, } planResp, err := c.ExecutePlanning(ctx, projectID, planReq) if err != nil { log.Error().Err(err).Str("phase", phase).Msg("Phase execution failed") continue } artifact := SpecKitArtifact{ Type: "plan", Phase: phase, Content: map[string]interface{}{"plan": planResp.Plan}, FilePath: planResp.FilePath, CreatedAt: time.Now(), Quality: 0.90, // High quality for structured plan } artifacts = append(artifacts, artifact) phasesCompleted = append(phasesCompleted, phase) case "tasks": tasksResp, err := c.ExecuteTasks(ctx, projectID) if err != nil { log.Error().Err(err).Str("phase", phase).Msg("Phase execution failed") continue } artifact := SpecKitArtifact{ Type: "tasks", Phase: phase, Content: map[string]interface{}{"tasks": tasksResp.Tasks}, FilePath: tasksResp.FilePath, CreatedAt: time.Now(), Quality: 0.88, // Good quality for actionable tasks } artifacts = append(artifacts, artifact) phasesCompleted = append(phasesCompleted, phase) } } // Calculate quality metrics qualityMetrics := c.calculateQualityMetrics(artifacts) response := &SpecKitWorkflowResponse{ ProjectID: projectID, Status: "completed", PhasesCompleted: phasesCompleted, Artifacts: artifacts, QualityMetrics: qualityMetrics, ProcessingTime: time.Since(startTime), Metadata: req.ChorusMetadata, } log.Info(). Str("project_id", projectID). Int("phases_completed", len(phasesCompleted)). Int("artifacts_generated", len(artifacts)). Int64("total_time_ms", response.ProcessingTime.Milliseconds()). Msg("Complete spec-kit workflow execution finished") return response, nil } // GetTemplate retrieves workflow templates func (c *SpecKitClient) GetTemplate(ctx context.Context, templateType string) (map[string]interface{}, error) { var template map[string]interface{} url := fmt.Sprintf("/v1/templates/%s", templateType) err := c.makeRequest(ctx, "GET", url, nil, &template) if err != nil { return nil, fmt.Errorf("failed to get template: %w", err) } return template, nil } // GetAnalytics retrieves analytics data func (c *SpecKitClient) GetAnalytics( ctx context.Context, deploymentID uuid.UUID, timeRange string, ) (map[string]interface{}, error) { var analytics map[string]interface{} url := fmt.Sprintf("/v1/analytics?deployment_id=%s&time_range=%s", deploymentID.String(), timeRange) err := c.makeRequest(ctx, "GET", url, nil, &analytics) if err != nil { return nil, fmt.Errorf("failed to get analytics: %w", err) } return analytics, nil } // makeRequest handles HTTP requests with retries and error handling func (c *SpecKitClient) makeRequest( ctx context.Context, method, endpoint string, requestBody interface{}, responseBody interface{}, ) error { url := c.baseURL + endpoint var bodyReader io.Reader if requestBody != nil { jsonBody, err := json.Marshal(requestBody) if err != nil { return fmt.Errorf("failed to marshal request body: %w", err) } bodyReader = bytes.NewBuffer(jsonBody) } var lastErr error for attempt := 0; attempt <= c.config.MaxRetries; attempt++ { if attempt > 0 { select { case <-ctx.Done(): return ctx.Err() case <-time.After(c.config.RetryDelay * time.Duration(attempt)): } } req, err := http.NewRequestWithContext(ctx, method, url, bodyReader) if err != nil { lastErr = fmt.Errorf("failed to create request: %w", err) continue } req.Header.Set("Content-Type", "application/json") req.Header.Set("User-Agent", c.config.UserAgent) resp, err := c.httpClient.Do(req) if err != nil { lastErr = fmt.Errorf("request failed: %w", err) continue } defer resp.Body.Close() if resp.StatusCode >= 200 && resp.StatusCode < 300 { if responseBody != nil { if err := json.NewDecoder(resp.Body).Decode(responseBody); err != nil { return fmt.Errorf("failed to decode response: %w", err) } } return nil } // Read error response errorBody, _ := io.ReadAll(resp.Body) lastErr = fmt.Errorf("HTTP %d: %s", resp.StatusCode, string(errorBody)) // Don't retry on client errors (4xx) if resp.StatusCode >= 400 && resp.StatusCode < 500 { break } } return fmt.Errorf("request failed after %d attempts: %w", c.config.MaxRetries+1, lastErr) } // calculateQualityMetrics computes overall quality metrics from artifacts func (c *SpecKitClient) calculateQualityMetrics(artifacts []SpecKitArtifact) map[string]float64 { metrics := map[string]float64{} if len(artifacts) == 0 { return metrics } var totalQuality float64 for _, artifact := range artifacts { totalQuality += artifact.Quality metrics[artifact.Type+"_quality"] = artifact.Quality } metrics["overall_quality"] = totalQuality / float64(len(artifacts)) metrics["artifact_count"] = float64(len(artifacts)) metrics["completeness"] = float64(len(artifacts)) / 5.0 // 5 total possible phases return metrics }