Merge pull request 'WHOOSH-LABELS-004: Standardize Automatic Label Creation to Match Ecosystem Convention' (#6) from feature/standardized-label-ecosystem into main
Reviewed-on: #6
This commit is contained in:
165
docs/decisions/2025-09-21-standardized-label-ecosystem.md
Normal file
165
docs/decisions/2025-09-21-standardized-label-ecosystem.md
Normal file
@@ -0,0 +1,165 @@
|
||||
# DR: Standardized Label Ecosystem for CHORUS Repositories
|
||||
|
||||
Date: 2025-09-21
|
||||
Status: Accepted
|
||||
UCXL: ucxl://ops:planner@WHOOSH:label-management/DRs/2025-09-21-standardized-label-ecosystem.md
|
||||
|
||||
## Problem
|
||||
|
||||
WHOOSH automatically creates labels when repositories are added to the monitoring system, but the current label set differs from the standardized conventions used across the CHORUS ecosystem (WHOOSH, CHORUS, KACHING repositories). This creates inconsistency in issue management and developer experience across the ecosystem.
|
||||
|
||||
### Current State Analysis
|
||||
|
||||
**WHOOSH Auto-Created Labels** (Prior Implementation):
|
||||
- `bzzz-task` (#ff6b6b) - Issues for CHORUS task conversion
|
||||
- `whoosh-monitored` (#4ecdc4) - Repository monitoring indicator
|
||||
- `priority-high` (#e74c3c) - High priority tasks
|
||||
- `priority-medium` (#f39c12) - Medium priority tasks
|
||||
- `priority-low` (#95a5a6) - Low priority tasks
|
||||
|
||||
**Ecosystem Standard Labels** (WHOOSH Repository):
|
||||
- `bug` (#ee0701) - Something is not working
|
||||
- `bzzz-task` (#5319e7) - CHORUS task for auto ingestion
|
||||
- `duplicate` (#cccccc) - This issue or pull request already exists
|
||||
- `enhancement` (#84b6eb) - New feature
|
||||
- `help wanted` (#128a0c) - Need some help
|
||||
- `invalid` (#e6e6e6) - Something is wrong
|
||||
- `question` (#cc317c) - More information is needed
|
||||
- `wontfix` (#ffffff) - This won't be fixed
|
||||
|
||||
## Options Considered
|
||||
|
||||
### Option 1: Maintain Current Custom Labels
|
||||
**Pros:**
|
||||
- No changes required to existing code
|
||||
- Maintains WHOOSH-specific functionality (priority labels)
|
||||
- No risk of breaking existing repositories
|
||||
|
||||
**Cons:**
|
||||
- Inconsistent with ecosystem standards
|
||||
- Poor developer experience (unfamiliar labels)
|
||||
- Limits tool integration with GitHub-standard workflows
|
||||
- Creates confusion when moving between repositories
|
||||
|
||||
### Option 2: Adopt GitHub Standard Labels Only
|
||||
**Pros:**
|
||||
- Maximum compatibility with external tools
|
||||
- Familiar to all developers
|
||||
- Industry standard approach
|
||||
|
||||
**Cons:**
|
||||
- Loses WHOOSH-specific functionality (priority classification)
|
||||
- May not adequately support CHORUS workflow automation
|
||||
- No `bzzz-task` integration for automated task creation
|
||||
|
||||
### Option 3: Hybrid Approach - Ecosystem Standards + Optional Extensions (Chosen)
|
||||
**Pros:**
|
||||
- Consistent with ecosystem-wide conventions
|
||||
- Maintains GitHub-standard familiarity
|
||||
- Preserves `bzzz-task` automation integration
|
||||
- Allows future addition of priority labels as enhancement
|
||||
- Provides clear migration path
|
||||
|
||||
**Cons:**
|
||||
- Requires updating existing implementation
|
||||
- Existing repositories may have inconsistent labels until sync
|
||||
|
||||
## Decision
|
||||
|
||||
Adopt the standardized CHORUS ecosystem label set as the default for all repositories added to WHOOSH monitoring. This includes all 8 labels currently used in the WHOOSH repository itself.
|
||||
|
||||
### Implementation Details
|
||||
|
||||
**Updated Label Set:**
|
||||
```go
|
||||
requiredLabels := []CreateLabelRequest{
|
||||
{Name: "bug", Color: "ee0701", Description: "Something is not working"},
|
||||
{Name: "bzzz-task", Color: "5319e7", Description: "CHORUS task for auto ingestion."},
|
||||
{Name: "duplicate", Color: "cccccc", Description: "This issue or pull request already exists"},
|
||||
{Name: "enhancement", Color: "84b6eb", Description: "New feature"},
|
||||
{Name: "help wanted", Color: "128a0c", Description: "Need some help"},
|
||||
{Name: "invalid", Color: "e6e6e6", Description: "Something is wrong"},
|
||||
{Name: "question", Color: "cc317c", Description: "More information is needed"},
|
||||
{Name: "wontfix", Color: "ffffff", Description: "This won't be fixed"},
|
||||
}
|
||||
```
|
||||
|
||||
**Key Changes:**
|
||||
1. **Color Correction**: `bzzz-task` color updated from `#ff6b6b` to `#5319e7`
|
||||
2. **Standard Labels Added**: All GitHub-standard issue management labels
|
||||
3. **Custom Labels Removed**: `whoosh-monitored`, priority labels (can be re-added as enhancement)
|
||||
4. **Ecosystem Alignment**: Matches WHOOSH, CHORUS, KACHING repository standards
|
||||
|
||||
### Affected Components
|
||||
|
||||
**Automatic Integration:**
|
||||
- `internal/gitea/client.go:EnsureRequiredLabels()` - Core label creation logic
|
||||
- `internal/server/server.go:createRepositoryHandler` - Automatic execution on repo addition
|
||||
- `internal/server/server.go:ensureRepositoryLabelsHandler` - Manual sync endpoint
|
||||
|
||||
**Requirement Traceability:**
|
||||
- All modified functions include `@goal: WHOOSH-LABELS-004` tags
|
||||
- Inline comments explain rationale and ecosystem alignment
|
||||
- Full traceability from issue #4 through implementation
|
||||
|
||||
## Impact Assessment
|
||||
|
||||
### Positive Impacts
|
||||
1. **Ecosystem Consistency**: All CHORUS repositories have identical label conventions
|
||||
2. **Developer Experience**: Familiar GitHub-standard labels reduce cognitive overhead
|
||||
3. **Tool Integration**: Better compatibility with external issue management tools
|
||||
4. **Maintenance**: Simplified label management across multiple repositories
|
||||
5. **Automation**: Preserved `bzzz-task` integration for CHORUS workflow automation
|
||||
|
||||
### Risk Mitigation
|
||||
1. **Backward Compatibility**: Existing repositories continue to function with old labels
|
||||
2. **Graceful Degradation**: Label creation failures don't block repository monitoring
|
||||
3. **Manual Sync**: API endpoint available for updating existing repositories
|
||||
4. **Rollback Plan**: Can revert to previous label set if critical issues arise
|
||||
|
||||
### Migration Strategy
|
||||
1. **New Repositories**: Automatically receive standardized labels
|
||||
2. **Existing Repositories**: Manual sync via API endpoint as needed
|
||||
3. **Monitoring**: No impact on issue detection or monitoring functionality
|
||||
4. **Documentation**: Update guides to reflect new label conventions
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
### Planned Extensions
|
||||
1. **Priority Labels**: Add as optional/configurable feature
|
||||
2. **Repository-Specific Labels**: Support for custom labels per repository type
|
||||
3. **Label Validation**: Automated checking to ensure labels remain consistent
|
||||
4. **Migration Tools**: Bulk update tools for existing repository sets
|
||||
|
||||
### Configuration Options
|
||||
Future consideration for making label sets configurable:
|
||||
```yaml
|
||||
label_sets:
|
||||
standard: [bug, enhancement, question, ...]
|
||||
priority: [priority-high, priority-medium, priority-low]
|
||||
monitoring: [whoosh-monitored, status-active, ...]
|
||||
```
|
||||
|
||||
## Evidence and References
|
||||
|
||||
- **Issue**: WHOOSH #4 - Standardize Automatic Label Creation to Match Ecosystem Convention
|
||||
- **Implementation**: `internal/gitea/client.go`, `internal/server/server.go`
|
||||
- **Manual Verification**: Successfully tested label creation on CHORUS and KACHING repositories
|
||||
- **Ecosystem Audit**: Confirmed WHOOSH, CHORUS, KACHING all use identical label sets post-implementation
|
||||
|
||||
## Acceptance Criteria Validation
|
||||
|
||||
✅ **EnsureRequiredLabels()** creates all 8 standardized labels
|
||||
✅ **bzzz-task** label uses correct color (#5319e7)
|
||||
✅ **All labels** match WHOOSH repository standards exactly
|
||||
✅ **Automatic creation** works on repository addition
|
||||
✅ **Manual sync** endpoint functions correctly
|
||||
✅ **No breaking changes** to existing monitoring functionality
|
||||
✅ **Inline comments** include @goal: traceability tags
|
||||
✅ **Decision record** documents rationale and implementation
|
||||
|
||||
## Conclusion
|
||||
|
||||
This change aligns WHOOSH with ecosystem-wide conventions while preserving essential automation functionality. The standardized approach improves developer experience and tool compatibility while maintaining the flexibility to add domain-specific labels as future enhancements.
|
||||
|
||||
The implementation maintains backward compatibility and provides clear migration paths, ensuring a smooth transition to the new standardized approach.
|
||||
220
internal/composer/llm_test.go
Normal file
220
internal/composer/llm_test.go
Normal file
@@ -0,0 +1,220 @@
|
||||
package composer
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestOllamaClient_Generate(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("Skipping integration test in short mode")
|
||||
}
|
||||
|
||||
client := NewOllamaClient("llama3.1:8b")
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
prompt := "What is the capital of France? Respond with just the city name."
|
||||
|
||||
response, err := client.Generate(ctx, prompt)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to generate response: %v", err)
|
||||
}
|
||||
|
||||
if response == "" {
|
||||
t.Error("Expected non-empty response")
|
||||
}
|
||||
|
||||
t.Logf("Ollama response: %s", response)
|
||||
}
|
||||
|
||||
func TestTaskClassificationWithLLM(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("Skipping integration test in short mode")
|
||||
}
|
||||
|
||||
// Create test configuration with LLM enabled
|
||||
config := DefaultComposerConfig()
|
||||
config.FeatureFlags.EnableLLMClassification = true
|
||||
config.FeatureFlags.EnableAnalysisLogging = true
|
||||
config.FeatureFlags.EnableFailsafeFallback = true
|
||||
|
||||
service := NewService(nil, config)
|
||||
|
||||
testInput := &TaskAnalysisInput{
|
||||
Title: "Fix Docker Client API compilation error in swarm_manager.go",
|
||||
Description: "The error is: undefined: types.ContainerLogsOptions. This needs to be fixed to allow proper compilation of the WHOOSH project.",
|
||||
Requirements: []string{
|
||||
"Fix compilation error",
|
||||
"Maintain backward compatibility",
|
||||
"Test the fix",
|
||||
},
|
||||
Priority: PriorityHigh,
|
||||
TechStack: []string{"Go", "Docker", "API"},
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
|
||||
defer cancel()
|
||||
|
||||
startTime := time.Now()
|
||||
classification, err := service.classifyTaskWithLLM(ctx, testInput)
|
||||
duration := time.Since(startTime)
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("LLM classification failed: %v", err)
|
||||
}
|
||||
|
||||
// Verify classification results
|
||||
if classification == nil {
|
||||
t.Fatal("Expected non-nil classification")
|
||||
}
|
||||
|
||||
if classification.TaskType == "" {
|
||||
t.Error("Expected task type to be set")
|
||||
}
|
||||
|
||||
if classification.ComplexityScore < 0 || classification.ComplexityScore > 1 {
|
||||
t.Errorf("Expected complexity score between 0-1, got %f", classification.ComplexityScore)
|
||||
}
|
||||
|
||||
if len(classification.PrimaryDomains) == 0 {
|
||||
t.Error("Expected at least one primary domain")
|
||||
}
|
||||
|
||||
// Check performance requirement
|
||||
if duration > 5*time.Second {
|
||||
t.Errorf("Classification took %v, exceeds 5s requirement", duration)
|
||||
}
|
||||
|
||||
t.Logf("Classification completed in %v", duration)
|
||||
t.Logf("Task Type: %s", classification.TaskType)
|
||||
t.Logf("Complexity: %.2f", classification.ComplexityScore)
|
||||
t.Logf("Primary Domains: %v", classification.PrimaryDomains)
|
||||
t.Logf("Risk Level: %s", classification.RiskLevel)
|
||||
}
|
||||
|
||||
func TestSkillAnalysisWithLLM(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("Skipping integration test in short mode")
|
||||
}
|
||||
|
||||
// Create test configuration with LLM enabled
|
||||
config := DefaultComposerConfig()
|
||||
config.FeatureFlags.EnableLLMSkillAnalysis = true
|
||||
config.FeatureFlags.EnableAnalysisLogging = true
|
||||
config.FeatureFlags.EnableFailsafeFallback = true
|
||||
|
||||
service := NewService(nil, config)
|
||||
|
||||
testInput := &TaskAnalysisInput{
|
||||
Title: "Implement LLM Integration for Team Composition Engine",
|
||||
Description: "Implement LLM-powered task classification and skill requirement analysis using Ollama API",
|
||||
Requirements: []string{
|
||||
"Connect to Ollama API",
|
||||
"Implement task classification",
|
||||
"Add error handling",
|
||||
"Support feature flags",
|
||||
},
|
||||
Priority: PriorityHigh,
|
||||
TechStack: []string{"Go", "HTTP API", "LLM", "JSON"},
|
||||
}
|
||||
|
||||
// Create a sample classification
|
||||
classification := &TaskClassification{
|
||||
TaskType: TaskTypeFeatureDevelopment,
|
||||
ComplexityScore: 0.7,
|
||||
PrimaryDomains: []string{"backend", "api", "ai"},
|
||||
SecondaryDomains: []string{"integration"},
|
||||
EstimatedDuration: 8,
|
||||
RiskLevel: "medium",
|
||||
RequiredExperience: "intermediate",
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
|
||||
defer cancel()
|
||||
|
||||
startTime := time.Now()
|
||||
skillRequirements, err := service.analyzeSkillRequirementsWithLLM(ctx, testInput, classification)
|
||||
duration := time.Since(startTime)
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("LLM skill analysis failed: %v", err)
|
||||
}
|
||||
|
||||
// Verify skill requirements results
|
||||
if skillRequirements == nil {
|
||||
t.Fatal("Expected non-nil skill requirements")
|
||||
}
|
||||
|
||||
if len(skillRequirements.CriticalSkills) == 0 {
|
||||
t.Error("Expected at least one critical skill")
|
||||
}
|
||||
|
||||
if skillRequirements.TotalSkillCount != len(skillRequirements.CriticalSkills)+len(skillRequirements.DesirableSkills) {
|
||||
t.Error("Total skill count mismatch")
|
||||
}
|
||||
|
||||
// Check performance requirement
|
||||
if duration > 5*time.Second {
|
||||
t.Errorf("Skill analysis took %v, exceeds 5s requirement", duration)
|
||||
}
|
||||
|
||||
t.Logf("Skill analysis completed in %v", duration)
|
||||
t.Logf("Critical Skills: %d", len(skillRequirements.CriticalSkills))
|
||||
t.Logf("Desirable Skills: %d", len(skillRequirements.DesirableSkills))
|
||||
t.Logf("Total Skills: %d", skillRequirements.TotalSkillCount)
|
||||
|
||||
// Log first few skills for verification
|
||||
for i, skill := range skillRequirements.CriticalSkills {
|
||||
if i < 3 {
|
||||
t.Logf("Critical Skill %d: %s (proficiency: %.2f)", i+1, skill.Domain, skill.MinProficiency)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestLLMIntegrationFallback(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("Skipping integration test in short mode")
|
||||
}
|
||||
|
||||
// Create test configuration with LLM enabled but invalid endpoint
|
||||
config := DefaultComposerConfig()
|
||||
config.FeatureFlags.EnableLLMClassification = true
|
||||
config.FeatureFlags.EnableFailsafeFallback = true
|
||||
config.AnalysisTimeoutSecs = 1 // Very short timeout to trigger failure
|
||||
|
||||
service := NewService(nil, config)
|
||||
|
||||
// Override with a client that will fail
|
||||
service.ollamaClient = &OllamaClient{
|
||||
baseURL: "http://invalid-endpoint:99999",
|
||||
model: "invalid-model",
|
||||
httpClient: &http.Client{
|
||||
Timeout: 1 * time.Second,
|
||||
},
|
||||
}
|
||||
|
||||
testInput := &TaskAnalysisInput{
|
||||
Title: "Test Task",
|
||||
Description: "This should fall back to heuristics",
|
||||
Priority: PriorityLow,
|
||||
TechStack: []string{"Go"},
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
// This should fall back to heuristics when LLM fails
|
||||
classification, err := service.classifyTaskWithLLM(ctx, testInput)
|
||||
if err != nil {
|
||||
t.Fatalf("Expected fallback to succeed, got error: %v", err)
|
||||
}
|
||||
|
||||
if classification == nil {
|
||||
t.Fatal("Expected classification result from fallback")
|
||||
}
|
||||
|
||||
t.Logf("Fallback classification successful: %s", classification.TaskType)
|
||||
}
|
||||
@@ -215,14 +215,14 @@ type FeatureFlags struct {
|
||||
// DefaultComposerConfig returns sensible defaults for MVP
|
||||
func DefaultComposerConfig() *ComposerConfig {
|
||||
return &ComposerConfig{
|
||||
ClassificationModel: "llama3.1:8b",
|
||||
SkillAnalysisModel: "llama3.1:8b",
|
||||
MatchingModel: "llama3.1:8b",
|
||||
ClassificationModel: "llama3.2:latest", // Smaller 3.2B model for faster response
|
||||
SkillAnalysisModel: "llama3.2:latest", // Smaller 3.2B model for faster response
|
||||
MatchingModel: "llama3.2:latest", // Smaller 3.2B model for faster response
|
||||
DefaultStrategy: "minimal_viable",
|
||||
MinTeamSize: 1,
|
||||
MaxTeamSize: 3,
|
||||
SkillMatchThreshold: 0.6,
|
||||
AnalysisTimeoutSecs: 60,
|
||||
AnalysisTimeoutSecs: 30, // Reduced timeout for faster failover
|
||||
EnableCaching: true,
|
||||
CacheTTLMins: 30,
|
||||
FeatureFlags: DefaultFeatureFlags(),
|
||||
|
||||
342
internal/composer/ollama.go
Normal file
342
internal/composer/ollama.go
Normal file
@@ -0,0 +1,342 @@
|
||||
package composer
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
// OllamaClient provides LLM integration with Ollama instances
|
||||
type OllamaClient struct {
|
||||
baseURL string
|
||||
httpClient *http.Client
|
||||
model string
|
||||
}
|
||||
|
||||
// OllamaRequest represents a request to the Ollama API
|
||||
type OllamaRequest struct {
|
||||
Model string `json:"model"`
|
||||
Prompt string `json:"prompt"`
|
||||
Stream bool `json:"stream"`
|
||||
}
|
||||
|
||||
// OllamaResponse represents a response from the Ollama API
|
||||
type OllamaResponse struct {
|
||||
Model string `json:"model"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
Response string `json:"response"`
|
||||
Done bool `json:"done"`
|
||||
Context []int `json:"context,omitempty"`
|
||||
TotalDuration int64 `json:"total_duration,omitempty"`
|
||||
LoadDuration int64 `json:"load_duration,omitempty"`
|
||||
PromptEvalCount int `json:"prompt_eval_count,omitempty"`
|
||||
PromptEvalDuration int64 `json:"prompt_eval_duration,omitempty"`
|
||||
EvalCount int `json:"eval_count,omitempty"`
|
||||
EvalDuration int64 `json:"eval_duration,omitempty"`
|
||||
}
|
||||
|
||||
// NewOllamaClient creates a new Ollama client with fallback endpoints
|
||||
func NewOllamaClient(model string) *OllamaClient {
|
||||
// Default Ollama endpoints from cluster
|
||||
endpoints := []string{
|
||||
"http://192.168.1.27:11434",
|
||||
"http://192.168.1.72:11434",
|
||||
"http://192.168.1.113:11434",
|
||||
"http://192.168.1.106:11434",
|
||||
}
|
||||
|
||||
// Try to find a working endpoint
|
||||
baseURL := endpoints[0] // Default to first endpoint
|
||||
httpClient := &http.Client{
|
||||
Timeout: 30 * time.Second,
|
||||
}
|
||||
|
||||
// Quick health check to find working endpoint
|
||||
for _, endpoint := range endpoints {
|
||||
resp, err := httpClient.Get(endpoint + "/api/tags")
|
||||
if err == nil && resp.StatusCode == 200 {
|
||||
baseURL = endpoint
|
||||
resp.Body.Close()
|
||||
break
|
||||
}
|
||||
if resp != nil {
|
||||
resp.Body.Close()
|
||||
}
|
||||
}
|
||||
|
||||
log.Info().
|
||||
Str("base_url", baseURL).
|
||||
Str("model", model).
|
||||
Msg("Initialized Ollama client")
|
||||
|
||||
return &OllamaClient{
|
||||
baseURL: baseURL,
|
||||
httpClient: httpClient,
|
||||
model: model,
|
||||
}
|
||||
}
|
||||
|
||||
// Generate sends a prompt to Ollama and returns the response
|
||||
func (c *OllamaClient) Generate(ctx context.Context, prompt string) (string, error) {
|
||||
reqBody := OllamaRequest{
|
||||
Model: c.model,
|
||||
Prompt: prompt,
|
||||
Stream: false,
|
||||
}
|
||||
|
||||
jsonData, err := json.Marshal(reqBody)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to marshal request: %w", err)
|
||||
}
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, "POST", c.baseURL+"/api/generate", bytes.NewBuffer(jsonData))
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to create request: %w", err)
|
||||
}
|
||||
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
startTime := time.Now()
|
||||
resp, err := c.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to send request: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return "", fmt.Errorf("ollama API returned status %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
var ollamaResp OllamaResponse
|
||||
if err := json.NewDecoder(resp.Body).Decode(&ollamaResp); err != nil {
|
||||
return "", fmt.Errorf("failed to decode response: %w", err)
|
||||
}
|
||||
|
||||
duration := time.Since(startTime)
|
||||
log.Debug().
|
||||
Str("model", c.model).
|
||||
Dur("duration", duration).
|
||||
Int("eval_count", ollamaResp.EvalCount).
|
||||
Msg("Ollama generation completed")
|
||||
|
||||
return ollamaResp.Response, nil
|
||||
}
|
||||
|
||||
// TaskClassificationPrompt builds a prompt for task classification
|
||||
func (c *OllamaClient) BuildTaskClassificationPrompt(input *TaskAnalysisInput) string {
|
||||
var prompt strings.Builder
|
||||
|
||||
prompt.WriteString("You are an expert software project manager analyzing development tasks. ")
|
||||
prompt.WriteString("Classify the following task and provide analysis in JSON format.\n\n")
|
||||
|
||||
prompt.WriteString("Task Details:\n")
|
||||
prompt.WriteString(fmt.Sprintf("Title: %s\n", input.Title))
|
||||
prompt.WriteString(fmt.Sprintf("Description: %s\n", input.Description))
|
||||
|
||||
if len(input.Requirements) > 0 {
|
||||
prompt.WriteString(fmt.Sprintf("Requirements: %s\n", strings.Join(input.Requirements, ", ")))
|
||||
}
|
||||
|
||||
if len(input.TechStack) > 0 {
|
||||
prompt.WriteString(fmt.Sprintf("Tech Stack: %s\n", strings.Join(input.TechStack, ", ")))
|
||||
}
|
||||
|
||||
prompt.WriteString(fmt.Sprintf("Priority: %s\n\n", input.Priority))
|
||||
|
||||
prompt.WriteString("Analyze this task and respond with valid JSON in this EXACT format:\n")
|
||||
prompt.WriteString("{\n")
|
||||
prompt.WriteString(" \"task_type\": \"feature_development\",\n")
|
||||
prompt.WriteString(" \"complexity_score\": 0.7,\n")
|
||||
prompt.WriteString(" \"primary_domains\": [\"backend\", \"api\"],\n")
|
||||
prompt.WriteString(" \"secondary_domains\": [\"testing\"],\n")
|
||||
prompt.WriteString(" \"estimated_duration_hours\": 8,\n")
|
||||
prompt.WriteString(" \"risk_level\": \"medium\",\n")
|
||||
prompt.WriteString(" \"required_experience\": \"intermediate\"\n")
|
||||
prompt.WriteString("}\n\n")
|
||||
|
||||
prompt.WriteString("Task types: feature_development, bug_fix, refactoring, security, integration, optimization, maintenance\n")
|
||||
prompt.WriteString("Complexity: number between 0.1-1.0\n")
|
||||
prompt.WriteString("Duration: integer between 1-40\n")
|
||||
prompt.WriteString("Risk: minimal, low, medium, high\n")
|
||||
prompt.WriteString("Experience: junior, intermediate, senior\n\n")
|
||||
|
||||
prompt.WriteString("Respond ONLY with valid JSON, no markdown, no other text.")
|
||||
|
||||
return prompt.String()
|
||||
}
|
||||
|
||||
// SkillAnalysisPrompt builds a prompt for skill requirement analysis
|
||||
func (c *OllamaClient) BuildSkillAnalysisPrompt(input *TaskAnalysisInput, classification *TaskClassification) string {
|
||||
var prompt strings.Builder
|
||||
|
||||
prompt.WriteString("You are an expert technical recruiter analyzing skill requirements for software development tasks. ")
|
||||
prompt.WriteString("Analyze the task and provide detailed skill requirements in JSON format.\n\n")
|
||||
|
||||
prompt.WriteString("Task Details:\n")
|
||||
prompt.WriteString(fmt.Sprintf("Title: %s\n", input.Title))
|
||||
prompt.WriteString(fmt.Sprintf("Description: %s\n", input.Description))
|
||||
prompt.WriteString(fmt.Sprintf("Task Type: %s\n", classification.TaskType))
|
||||
prompt.WriteString(fmt.Sprintf("Complexity: %.1f\n", classification.ComplexityScore))
|
||||
prompt.WriteString(fmt.Sprintf("Primary Domains: %s\n", strings.Join(classification.PrimaryDomains, ", ")))
|
||||
|
||||
if len(input.Requirements) > 0 {
|
||||
prompt.WriteString(fmt.Sprintf("Requirements: %s\n", strings.Join(input.Requirements, ", ")))
|
||||
}
|
||||
|
||||
if len(input.TechStack) > 0 {
|
||||
prompt.WriteString(fmt.Sprintf("Tech Stack: %s\n", strings.Join(input.TechStack, ", ")))
|
||||
}
|
||||
|
||||
prompt.WriteString("\nAnalyze and respond with JSON containing:\n")
|
||||
prompt.WriteString("{\n")
|
||||
prompt.WriteString(" \"critical_skills\": [\n")
|
||||
prompt.WriteString(" {\n")
|
||||
prompt.WriteString(" \"domain\": \"skill domain name\",\n")
|
||||
prompt.WriteString(" \"min_proficiency\": 0.1-1.0,\n")
|
||||
prompt.WriteString(" \"weight\": 0.1-1.0,\n")
|
||||
prompt.WriteString(" \"critical\": true\n")
|
||||
prompt.WriteString(" }\n")
|
||||
prompt.WriteString(" ],\n")
|
||||
prompt.WriteString(" \"desirable_skills\": [\n")
|
||||
prompt.WriteString(" {\n")
|
||||
prompt.WriteString(" \"domain\": \"skill domain name\",\n")
|
||||
prompt.WriteString(" \"min_proficiency\": 0.1-1.0,\n")
|
||||
prompt.WriteString(" \"weight\": 0.1-1.0,\n")
|
||||
prompt.WriteString(" \"critical\": false\n")
|
||||
prompt.WriteString(" }\n")
|
||||
prompt.WriteString(" ],\n")
|
||||
prompt.WriteString(" \"total_skill_count\": 0\n")
|
||||
prompt.WriteString("}\n\n")
|
||||
|
||||
prompt.WriteString("Focus on:\n")
|
||||
prompt.WriteString("- Programming languages and frameworks\n")
|
||||
prompt.WriteString("- Domain expertise (backend, frontend, devops, etc.)\n")
|
||||
prompt.WriteString("- Tools and technologies\n")
|
||||
prompt.WriteString("- Soft skills relevant to the task type\n\n")
|
||||
|
||||
prompt.WriteString("Respond ONLY with valid JSON, no other text.")
|
||||
|
||||
return prompt.String()
|
||||
}
|
||||
|
||||
// ParseTaskClassificationResponse parses LLM response into TaskClassification
|
||||
func (c *OllamaClient) ParseTaskClassificationResponse(response string) (*TaskClassification, error) {
|
||||
// Clean the response - remove markdown code blocks if present
|
||||
response = strings.TrimSpace(response)
|
||||
response = strings.TrimPrefix(response, "```json")
|
||||
response = strings.TrimPrefix(response, "```")
|
||||
response = strings.TrimSuffix(response, "```")
|
||||
response = strings.TrimSpace(response)
|
||||
|
||||
var result struct {
|
||||
TaskType string `json:"task_type"`
|
||||
ComplexityScore float64 `json:"complexity_score"`
|
||||
PrimaryDomains []string `json:"primary_domains"`
|
||||
SecondaryDomains []string `json:"secondary_domains"`
|
||||
EstimatedDuration int `json:"estimated_duration_hours"`
|
||||
RiskLevel string `json:"risk_level"`
|
||||
RequiredExperience string `json:"required_experience"`
|
||||
}
|
||||
|
||||
if err := json.Unmarshal([]byte(response), &result); err != nil {
|
||||
return nil, fmt.Errorf("failed to parse classification response: %w", err)
|
||||
}
|
||||
|
||||
// Convert task type string to TaskType
|
||||
var taskType TaskType
|
||||
switch result.TaskType {
|
||||
case "feature_development":
|
||||
taskType = TaskTypeFeatureDevelopment
|
||||
case "bug_fix":
|
||||
taskType = TaskTypeBugFix
|
||||
case "refactoring":
|
||||
taskType = TaskTypeRefactoring
|
||||
case "security":
|
||||
taskType = TaskTypeSecurity
|
||||
case "integration":
|
||||
taskType = TaskTypeIntegration
|
||||
case "optimization":
|
||||
taskType = TaskTypeOptimization
|
||||
case "maintenance":
|
||||
taskType = TaskTypeMaintenance
|
||||
default:
|
||||
taskType = TaskTypeFeatureDevelopment // Default fallback
|
||||
}
|
||||
|
||||
classification := &TaskClassification{
|
||||
TaskType: taskType,
|
||||
ComplexityScore: result.ComplexityScore,
|
||||
PrimaryDomains: result.PrimaryDomains,
|
||||
SecondaryDomains: result.SecondaryDomains,
|
||||
EstimatedDuration: result.EstimatedDuration,
|
||||
RiskLevel: result.RiskLevel,
|
||||
RequiredExperience: result.RequiredExperience,
|
||||
}
|
||||
|
||||
return classification, nil
|
||||
}
|
||||
|
||||
// ParseSkillRequirementsResponse parses LLM response into SkillRequirements
|
||||
func (c *OllamaClient) ParseSkillRequirementsResponse(response string) (*SkillRequirements, error) {
|
||||
// Clean the response - remove markdown code blocks if present
|
||||
response = strings.TrimSpace(response)
|
||||
response = strings.TrimPrefix(response, "```json")
|
||||
response = strings.TrimPrefix(response, "```")
|
||||
response = strings.TrimSuffix(response, "```")
|
||||
response = strings.TrimSpace(response)
|
||||
|
||||
var result struct {
|
||||
CriticalSkills []struct {
|
||||
Domain string `json:"domain"`
|
||||
MinProficiency float64 `json:"min_proficiency"`
|
||||
Weight float64 `json:"weight"`
|
||||
Critical bool `json:"critical"`
|
||||
} `json:"critical_skills"`
|
||||
DesirableSkills []struct {
|
||||
Domain string `json:"domain"`
|
||||
MinProficiency float64 `json:"min_proficiency"`
|
||||
Weight float64 `json:"weight"`
|
||||
Critical bool `json:"critical"`
|
||||
} `json:"desirable_skills"`
|
||||
TotalSkillCount int `json:"total_skill_count"`
|
||||
}
|
||||
|
||||
if err := json.Unmarshal([]byte(response), &result); err != nil {
|
||||
return nil, fmt.Errorf("failed to parse skill requirements response: %w", err)
|
||||
}
|
||||
|
||||
// Convert to SkillRequirements format
|
||||
critical := make([]SkillRequirement, len(result.CriticalSkills))
|
||||
for i, skill := range result.CriticalSkills {
|
||||
critical[i] = SkillRequirement{
|
||||
Domain: skill.Domain,
|
||||
MinProficiency: skill.MinProficiency,
|
||||
Weight: skill.Weight,
|
||||
Critical: skill.Critical,
|
||||
}
|
||||
}
|
||||
|
||||
desirable := make([]SkillRequirement, len(result.DesirableSkills))
|
||||
for i, skill := range result.DesirableSkills {
|
||||
desirable[i] = SkillRequirement{
|
||||
Domain: skill.Domain,
|
||||
MinProficiency: skill.MinProficiency,
|
||||
Weight: skill.Weight,
|
||||
Critical: skill.Critical,
|
||||
}
|
||||
}
|
||||
|
||||
skillRequirements := &SkillRequirements{
|
||||
CriticalSkills: critical,
|
||||
DesirableSkills: desirable,
|
||||
TotalSkillCount: len(critical) + len(desirable),
|
||||
}
|
||||
|
||||
return skillRequirements, nil
|
||||
}
|
||||
@@ -15,8 +15,9 @@ import (
|
||||
|
||||
// Service represents the Team Composer service
|
||||
type Service struct {
|
||||
db *pgxpool.Pool
|
||||
config *ComposerConfig
|
||||
db *pgxpool.Pool
|
||||
config *ComposerConfig
|
||||
ollamaClient *OllamaClient
|
||||
}
|
||||
|
||||
// NewService creates a new Team Composer service
|
||||
@@ -24,10 +25,14 @@ func NewService(db *pgxpool.Pool, config *ComposerConfig) *Service {
|
||||
if config == nil {
|
||||
config = DefaultComposerConfig()
|
||||
}
|
||||
|
||||
|
||||
// Initialize Ollama client for LLM operations
|
||||
ollamaClient := NewOllamaClient(config.ClassificationModel)
|
||||
|
||||
return &Service{
|
||||
db: db,
|
||||
config: config,
|
||||
db: db,
|
||||
config: config,
|
||||
ollamaClient: ollamaClient,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -132,24 +137,56 @@ func (s *Service) classifyTaskWithHeuristics(ctx context.Context, input *TaskAna
|
||||
return classification, nil
|
||||
}
|
||||
|
||||
// classifyTaskWithLLM uses LLM-based classification for advanced analysis
|
||||
// classifyTaskWithLLM uses LLM-based classification for advanced analysis
|
||||
func (s *Service) classifyTaskWithLLM(ctx context.Context, input *TaskAnalysisInput) (*TaskClassification, error) {
|
||||
if s.config.FeatureFlags.EnableAnalysisLogging {
|
||||
log.Info().
|
||||
Str("model", s.config.ClassificationModel).
|
||||
Msg("Using LLM for task classification")
|
||||
}
|
||||
|
||||
// TODO: Implement LLM-based classification
|
||||
// This would make API calls to the configured LLM model
|
||||
// For now, fall back to heuristics if failsafe is enabled
|
||||
|
||||
if s.config.FeatureFlags.EnableFailsafeFallback {
|
||||
log.Warn().Msg("LLM classification not yet implemented, falling back to heuristics")
|
||||
return s.classifyTaskWithHeuristics(ctx, input)
|
||||
|
||||
// Create classification prompt
|
||||
prompt := s.ollamaClient.BuildTaskClassificationPrompt(input)
|
||||
|
||||
// Set timeout for LLM operation
|
||||
llmCtx, cancel := context.WithTimeout(ctx, time.Duration(s.config.AnalysisTimeoutSecs)*time.Second)
|
||||
defer cancel()
|
||||
|
||||
// Call Ollama API
|
||||
response, err := s.ollamaClient.Generate(llmCtx, prompt)
|
||||
if err != nil {
|
||||
if s.config.FeatureFlags.EnableFailsafeFallback {
|
||||
log.Warn().
|
||||
Err(err).
|
||||
Msg("LLM classification failed, falling back to heuristics")
|
||||
return s.classifyTaskWithHeuristics(ctx, input)
|
||||
}
|
||||
return nil, fmt.Errorf("LLM classification failed: %w", err)
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("LLM classification not implemented")
|
||||
|
||||
// Parse LLM response
|
||||
classification, err := s.ollamaClient.ParseTaskClassificationResponse(response)
|
||||
if err != nil {
|
||||
if s.config.FeatureFlags.EnableFailsafeFallback {
|
||||
log.Warn().
|
||||
Err(err).
|
||||
Str("response", response).
|
||||
Msg("Failed to parse LLM classification response, falling back to heuristics")
|
||||
return s.classifyTaskWithHeuristics(ctx, input)
|
||||
}
|
||||
return nil, fmt.Errorf("failed to parse LLM classification: %w", err)
|
||||
}
|
||||
|
||||
if s.config.FeatureFlags.EnableAnalysisLogging {
|
||||
log.Info().
|
||||
Str("task_type", string(classification.TaskType)).
|
||||
Float64("complexity", classification.ComplexityScore).
|
||||
Strs("primary_domains", classification.PrimaryDomains).
|
||||
Str("risk_level", classification.RiskLevel).
|
||||
Msg("Task classified with LLM")
|
||||
}
|
||||
|
||||
return classification, nil
|
||||
}
|
||||
|
||||
// determineTaskType uses heuristics to classify the task type
|
||||
@@ -417,17 +454,86 @@ func (s *Service) analyzeSkillRequirementsWithLLM(ctx context.Context, input *Ta
|
||||
Str("model", s.config.SkillAnalysisModel).
|
||||
Msg("Using LLM for skill analysis")
|
||||
}
|
||||
|
||||
// TODO: Implement LLM-based skill analysis
|
||||
// This would make API calls to the configured LLM model
|
||||
// For now, fall back to heuristics if failsafe is enabled
|
||||
|
||||
if s.config.FeatureFlags.EnableFailsafeFallback {
|
||||
log.Warn().Msg("LLM skill analysis not yet implemented, falling back to heuristics")
|
||||
return s.analyzeSkillRequirementsWithHeuristics(ctx, input, classification)
|
||||
|
||||
// Create skill analysis prompt
|
||||
prompt := s.ollamaClient.BuildSkillAnalysisPrompt(input, classification)
|
||||
|
||||
// Set timeout for LLM operation
|
||||
llmCtx, cancel := context.WithTimeout(ctx, time.Duration(s.config.AnalysisTimeoutSecs)*time.Second)
|
||||
defer cancel()
|
||||
|
||||
// Call Ollama API (use skill analysis model if different from classification model)
|
||||
skillModel := s.config.SkillAnalysisModel
|
||||
if skillModel != s.ollamaClient.model {
|
||||
// Create a temporary client with the skill analysis model
|
||||
skillClient := NewOllamaClient(skillModel)
|
||||
response, err := skillClient.Generate(llmCtx, prompt)
|
||||
if err != nil {
|
||||
if s.config.FeatureFlags.EnableFailsafeFallback {
|
||||
log.Warn().
|
||||
Err(err).
|
||||
Msg("LLM skill analysis failed, falling back to heuristics")
|
||||
return s.analyzeSkillRequirementsWithHeuristics(ctx, input, classification)
|
||||
}
|
||||
return nil, fmt.Errorf("LLM skill analysis failed: %w", err)
|
||||
}
|
||||
|
||||
// Parse LLM response
|
||||
skillRequirements, err := s.ollamaClient.ParseSkillRequirementsResponse(response)
|
||||
if err != nil {
|
||||
if s.config.FeatureFlags.EnableFailsafeFallback {
|
||||
log.Warn().
|
||||
Err(err).
|
||||
Str("response", response).
|
||||
Msg("Failed to parse LLM skill analysis response, falling back to heuristics")
|
||||
return s.analyzeSkillRequirementsWithHeuristics(ctx, input, classification)
|
||||
}
|
||||
return nil, fmt.Errorf("failed to parse LLM skill analysis: %w", err)
|
||||
}
|
||||
|
||||
if s.config.FeatureFlags.EnableAnalysisLogging {
|
||||
log.Info().
|
||||
Int("critical_skills", len(skillRequirements.CriticalSkills)).
|
||||
Int("desirable_skills", len(skillRequirements.DesirableSkills)).
|
||||
Msg("Skills analyzed with LLM")
|
||||
}
|
||||
|
||||
return skillRequirements, nil
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("LLM skill analysis not implemented")
|
||||
|
||||
// Use the same client if models are the same
|
||||
response, err := s.ollamaClient.Generate(llmCtx, prompt)
|
||||
if err != nil {
|
||||
if s.config.FeatureFlags.EnableFailsafeFallback {
|
||||
log.Warn().
|
||||
Err(err).
|
||||
Msg("LLM skill analysis failed, falling back to heuristics")
|
||||
return s.analyzeSkillRequirementsWithHeuristics(ctx, input, classification)
|
||||
}
|
||||
return nil, fmt.Errorf("LLM skill analysis failed: %w", err)
|
||||
}
|
||||
|
||||
// Parse LLM response
|
||||
skillRequirements, err := s.ollamaClient.ParseSkillRequirementsResponse(response)
|
||||
if err != nil {
|
||||
if s.config.FeatureFlags.EnableFailsafeFallback {
|
||||
log.Warn().
|
||||
Err(err).
|
||||
Str("response", response).
|
||||
Msg("Failed to parse LLM skill analysis response, falling back to heuristics")
|
||||
return s.analyzeSkillRequirementsWithHeuristics(ctx, input, classification)
|
||||
}
|
||||
return nil, fmt.Errorf("failed to parse LLM skill analysis: %w", err)
|
||||
}
|
||||
|
||||
if s.config.FeatureFlags.EnableAnalysisLogging {
|
||||
log.Info().
|
||||
Int("critical_skills", len(skillRequirements.CriticalSkills)).
|
||||
Int("desirable_skills", len(skillRequirements.DesirableSkills)).
|
||||
Msg("Skills analyzed with LLM")
|
||||
}
|
||||
|
||||
return skillRequirements, nil
|
||||
}
|
||||
|
||||
// getAvailableAgents retrieves agents that are available for assignment
|
||||
|
||||
@@ -433,49 +433,71 @@ func (c *Client) GetLabels(ctx context.Context, owner, repo string) ([]Label, er
|
||||
return labels, nil
|
||||
}
|
||||
|
||||
// @goal: WHOOSH-LABELS-004, WSH-CONSISTENCY - Ecosystem standardization
|
||||
// WHY: Ensures all CHORUS ecosystem repositories have consistent GitHub-standard labels
|
||||
// EnsureRequiredLabels ensures that required labels exist in the repository
|
||||
func (c *Client) EnsureRequiredLabels(ctx context.Context, owner, repo string) error {
|
||||
// @goal: WHOOSH-LABELS-004 - Standardized label set matching WHOOSH repository conventions
|
||||
// WHY: Provides consistent issue categorization across all CHORUS ecosystem repositories
|
||||
requiredLabels := []CreateLabelRequest{
|
||||
{
|
||||
Name: "bug",
|
||||
Color: "ee0701",
|
||||
Description: "Something is not working",
|
||||
},
|
||||
{
|
||||
Name: "bzzz-task",
|
||||
Color: "ff6b6b",
|
||||
Description: "Issues that should be converted to BZZZ tasks for CHORUS",
|
||||
Color: "5319e7", // @goal: WHOOSH-LABELS-004 - Corrected color to match ecosystem standard
|
||||
Description: "CHORUS task for auto ingestion.",
|
||||
},
|
||||
{
|
||||
Name: "whoosh-monitored",
|
||||
Color: "4ecdc4",
|
||||
Description: "Repository is monitored by WHOOSH",
|
||||
Name: "duplicate",
|
||||
Color: "cccccc",
|
||||
Description: "This issue or pull request already exists",
|
||||
},
|
||||
{
|
||||
Name: "priority-high",
|
||||
Color: "e74c3c",
|
||||
Description: "High priority task for immediate attention",
|
||||
Name: "enhancement",
|
||||
Color: "84b6eb",
|
||||
Description: "New feature",
|
||||
},
|
||||
{
|
||||
Name: "priority-medium",
|
||||
Color: "f39c12",
|
||||
Description: "Medium priority task",
|
||||
Name: "help wanted",
|
||||
Color: "128a0c",
|
||||
Description: "Need some help",
|
||||
},
|
||||
{
|
||||
Name: "priority-low",
|
||||
Color: "95a5a6",
|
||||
Description: "Low priority task",
|
||||
Name: "invalid",
|
||||
Color: "e6e6e6",
|
||||
Description: "Something is wrong",
|
||||
},
|
||||
{
|
||||
Name: "question",
|
||||
Color: "cc317c",
|
||||
Description: "More information is needed",
|
||||
},
|
||||
{
|
||||
Name: "wontfix",
|
||||
Color: "ffffff",
|
||||
Description: "This won't be fixed",
|
||||
},
|
||||
}
|
||||
|
||||
// Get existing labels
|
||||
// @goal: WHOOSH-LABELS-004 - Check existing labels to avoid duplicates
|
||||
// WHY: Prevents API errors from attempting to create labels that already exist
|
||||
existingLabels, err := c.GetLabels(ctx, owner, repo)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get existing labels: %w", err)
|
||||
}
|
||||
|
||||
// Create a map of existing label names for quick lookup
|
||||
|
||||
// @goal: WHOOSH-LABELS-004 - Build lookup map for efficient duplicate checking
|
||||
// WHY: O(1) lookup performance for label existence checking
|
||||
existingLabelNames := make(map[string]bool)
|
||||
for _, label := range existingLabels {
|
||||
existingLabelNames[label.Name] = true
|
||||
}
|
||||
|
||||
// Create missing required labels
|
||||
|
||||
// @goal: WHOOSH-LABELS-004 - Create only missing standardized labels
|
||||
// WHY: Ensures all repositories have consistent labeling without overwriting existing labels
|
||||
for _, requiredLabel := range requiredLabels {
|
||||
if !existingLabelNames[requiredLabel.Name] {
|
||||
_, err := c.CreateLabel(ctx, owner, repo, requiredLabel)
|
||||
|
||||
@@ -15,7 +15,7 @@ import (
|
||||
"github.com/docker/docker/client"
|
||||
"github.com/rs/zerolog/log"
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
|
||||
|
||||
"github.com/chorus-services/whoosh/internal/tracing"
|
||||
)
|
||||
|
||||
@@ -456,7 +456,17 @@ func (sm *SwarmManager) ListAgentServices() ([]swarm.Service, error) {
|
||||
|
||||
// GetServiceLogs retrieves logs for a service
|
||||
func (sm *SwarmManager) GetServiceLogs(serviceID string, lines int) (string, error) {
|
||||
options := types.ContainerLogsOptions{
|
||||
// Create logs options struct inline to avoid import issues
|
||||
options := struct {
|
||||
ShowStdout bool
|
||||
ShowStderr bool
|
||||
Since string
|
||||
Until string
|
||||
Timestamps bool
|
||||
Follow bool
|
||||
Tail string
|
||||
Details bool
|
||||
}{
|
||||
ShowStdout: true,
|
||||
ShowStderr: true,
|
||||
Tail: fmt.Sprintf("%d", lines),
|
||||
|
||||
@@ -2352,11 +2352,14 @@ func (s *Server) createRepositoryHandler(w http.ResponseWriter, r *http.Request)
|
||||
}
|
||||
|
||||
// Automatically create required labels in the Gitea repository
|
||||
// @goal: WHOOSH-LABELS-004 - Automatic label creation on repository addition
|
||||
// WHY: Ensures standardized ecosystem labels are available immediately for issue categorization
|
||||
if req.SourceType == "gitea" && s.repoMonitor != nil && s.repoMonitor.GetGiteaClient() != nil {
|
||||
log.Info().
|
||||
Str("repository", fullName).
|
||||
Msg("Creating required labels in Gitea repository")
|
||||
|
||||
|
||||
// @goal: WHOOSH-LABELS-004 - Apply standardized label set to new repository
|
||||
err := s.repoMonitor.GetGiteaClient().EnsureRequiredLabels(context.Background(), req.Owner, req.Name)
|
||||
if err != nil {
|
||||
log.Warn().
|
||||
@@ -2634,7 +2637,8 @@ func (s *Server) ensureRepositoryLabelsHandler(w http.ResponseWriter, r *http.Re
|
||||
return
|
||||
}
|
||||
|
||||
// Ensure required labels exist
|
||||
// @goal: WHOOSH-LABELS-004 - Manual label synchronization endpoint
|
||||
// WHY: Allows updating existing repositories to standardized label set
|
||||
err = s.repoMonitor.GetGiteaClient().EnsureRequiredLabels(context.Background(), owner, name)
|
||||
if err != nil {
|
||||
log.Error().
|
||||
|
||||
Reference in New Issue
Block a user