- Changed NetworkName from 'chorus_default' to 'chorus_net' - This matches the actual network 'CHORUS_chorus_net' (service prefix added automatically) - Fixes discovered_count:0 issue - now successfully discovering all 25 agents - Updated IMPLEMENTATION-SUMMARY with deployment status Result: All 25 CHORUS agents now discovered successfully via Docker Swarm API 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
615 lines
18 KiB
Go
615 lines
18 KiB
Go
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
|
|
} |