Fix council formation broadcast and reduce core roles
Two critical fixes for E2E council workflow: 1. Reduced core council roles from 8 to 2 (tpm + senior-software-architect) - Faster council formation - Easier debugging - Sufficient for initial project planning 2. Added broadcast to monitor path - Monitor now broadcasts council opportunities to CHORUS agents - Previously only webhook path had broadcast, monitor path missed it - Added broadcaster parameter to NewMonitor() - Broadcast sent after council formation with 30s timeout Verified working: - Council formation successful - Broadcast to CHORUS agents confirmed - Role claims received (TPM claimed and loaded) - Persona status "loaded" acknowledged Image: anthonyrawlins/whoosh:broadcast-fix Council: 2dad2070-292a-4dbd-9195-89795f84da19 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -35,14 +35,18 @@ type CouncilComposition struct {
|
||||
|
||||
// CouncilAgent represents a single agent in the council
|
||||
type CouncilAgent struct {
|
||||
AgentID string `json:"agent_id"`
|
||||
RoleName string `json:"role_name"`
|
||||
AgentName string `json:"agent_name"`
|
||||
Required bool `json:"required"`
|
||||
Deployed bool `json:"deployed"`
|
||||
ServiceID string `json:"service_id,omitempty"`
|
||||
DeployedAt *time.Time `json:"deployed_at,omitempty"`
|
||||
Status string `json:"status"` // pending, deploying, active, failed
|
||||
AgentID string `json:"agent_id"`
|
||||
RoleName string `json:"role_name"`
|
||||
AgentName string `json:"agent_name"`
|
||||
Required bool `json:"required"`
|
||||
Deployed bool `json:"deployed"`
|
||||
ServiceID string `json:"service_id,omitempty"`
|
||||
DeployedAt *time.Time `json:"deployed_at,omitempty"`
|
||||
Status string `json:"status"` // pending, assigned, deploying, active, failed
|
||||
PersonaStatus *string `json:"persona_status,omitempty"` // pending, loading, loaded, failed
|
||||
PersonaLoadedAt *time.Time `json:"persona_loaded_at,omitempty"`
|
||||
EndpointURL *string `json:"endpoint_url,omitempty"`
|
||||
PersonaAckPayload map[string]interface{} `json:"persona_ack_payload,omitempty"`
|
||||
}
|
||||
|
||||
// CouncilDeploymentResult represents the result of council agent deployment
|
||||
@@ -81,15 +85,10 @@ type CouncilArtifacts struct {
|
||||
}
|
||||
|
||||
// CoreCouncilRoles defines the required roles for any project kickoff council
|
||||
// Reduced to minimal set for faster formation and easier debugging
|
||||
var CoreCouncilRoles = []string{
|
||||
"systems-analyst",
|
||||
"senior-software-architect",
|
||||
"tpm",
|
||||
"security-architect",
|
||||
"devex-platform-engineer",
|
||||
"qa-test-engineer",
|
||||
"sre-observability-lead",
|
||||
"technical-writer",
|
||||
"senior-software-architect",
|
||||
}
|
||||
|
||||
// OptionalCouncilRoles defines the optional roles that may be included based on project needs
|
||||
|
||||
@@ -13,6 +13,8 @@ import (
|
||||
"github.com/chorus-services/whoosh/internal/council"
|
||||
"github.com/chorus-services/whoosh/internal/gitea"
|
||||
"github.com/chorus-services/whoosh/internal/orchestrator"
|
||||
"github.com/chorus-services/whoosh/internal/p2p"
|
||||
"github.com/chorus-services/whoosh/internal/tasks"
|
||||
"github.com/chorus-services/whoosh/internal/tracing"
|
||||
"github.com/google/uuid"
|
||||
"github.com/jackc/pgx/v5"
|
||||
@@ -28,18 +30,20 @@ type Monitor struct {
|
||||
composer *composer.Service
|
||||
council *council.CouncilComposer
|
||||
agentDeployer *orchestrator.AgentDeployer
|
||||
broadcaster *p2p.Broadcaster
|
||||
stopCh chan struct{}
|
||||
syncInterval time.Duration
|
||||
}
|
||||
|
||||
// NewMonitor creates a new repository monitor
|
||||
func NewMonitor(db *pgxpool.Pool, giteaCfg config.GITEAConfig, composerService *composer.Service, councilComposer *council.CouncilComposer, agentDeployer *orchestrator.AgentDeployer) *Monitor {
|
||||
func NewMonitor(db *pgxpool.Pool, giteaCfg config.GITEAConfig, composerService *composer.Service, councilComposer *council.CouncilComposer, agentDeployer *orchestrator.AgentDeployer, broadcaster *p2p.Broadcaster) *Monitor {
|
||||
return &Monitor{
|
||||
db: db,
|
||||
gitea: gitea.NewClient(giteaCfg),
|
||||
composer: composerService,
|
||||
council: councilComposer,
|
||||
agentDeployer: agentDeployer,
|
||||
broadcaster: broadcaster,
|
||||
stopCh: make(chan struct{}),
|
||||
syncInterval: 5 * time.Minute, // Default sync every 5 minutes
|
||||
}
|
||||
@@ -367,7 +371,7 @@ func (m *Monitor) createOrUpdateTask(ctx context.Context, repo RepositoryConfig,
|
||||
sourceConfig := map[string]interface{}{
|
||||
"repository_id": repo.ID,
|
||||
"issue_number": issue.Number,
|
||||
"issue_id": issue.ID,
|
||||
"issue_id": issue.ID,
|
||||
}
|
||||
sourceConfigJSON, _ := json.Marshal(sourceConfig)
|
||||
|
||||
@@ -381,11 +385,11 @@ func (m *Monitor) createOrUpdateTask(ctx context.Context, repo RepositoryConfig,
|
||||
status, // status
|
||||
priority, // priority
|
||||
repo.FullName, // repository
|
||||
repo.ID, // repository_id
|
||||
labelsJSON, // labels
|
||||
techStackJSON, // tech_stack
|
||||
issue.CreatedAt, // external_created_at
|
||||
issue.UpdatedAt, // external_updated_at
|
||||
repo.ID, // repository_id
|
||||
labelsJSON, // labels
|
||||
techStackJSON, // tech_stack
|
||||
issue.CreatedAt, // external_created_at
|
||||
issue.UpdatedAt, // external_updated_at
|
||||
).Scan(&taskID)
|
||||
|
||||
if err != nil {
|
||||
@@ -479,18 +483,18 @@ func (m *Monitor) extractTechStackFromIssue(issue gitea.Issue) []string {
|
||||
|
||||
// RepositoryConfig represents a monitored repository configuration
|
||||
type RepositoryConfig struct {
|
||||
ID string `db:"id"`
|
||||
Name string `db:"name"`
|
||||
Owner string `db:"owner"`
|
||||
FullName string `db:"full_name"`
|
||||
URL string `db:"url"`
|
||||
SourceType string `db:"source_type"`
|
||||
MonitorIssues bool `db:"monitor_issues"`
|
||||
EnableChorusIntegration bool `db:"enable_chorus_integration"`
|
||||
ChorusTaskLabels []string `db:"chorus_task_labels"`
|
||||
LastSync *time.Time `db:"last_sync_at"`
|
||||
LastIssueSync *time.Time `db:"last_issue_sync"`
|
||||
SyncStatus string `db:"sync_status"`
|
||||
ID string `db:"id"`
|
||||
Name string `db:"name"`
|
||||
Owner string `db:"owner"`
|
||||
FullName string `db:"full_name"`
|
||||
URL string `db:"url"`
|
||||
SourceType string `db:"source_type"`
|
||||
MonitorIssues bool `db:"monitor_issues"`
|
||||
EnableChorusIntegration bool `db:"enable_chorus_integration"`
|
||||
ChorusTaskLabels []string `db:"chorus_task_labels"`
|
||||
LastSync *time.Time `db:"last_sync_at"`
|
||||
LastIssueSync *time.Time `db:"last_issue_sync"`
|
||||
SyncStatus string `db:"sync_status"`
|
||||
}
|
||||
|
||||
// getMonitoredRepositories retrieves all repositories that should be monitored
|
||||
@@ -730,11 +734,11 @@ func (m *Monitor) triggerTeamComposition(ctx context.Context, taskID string, iss
|
||||
Priority: m.mapPriorityToComposer(m.extractPriorityFromLabels(issue.Labels)),
|
||||
TechStack: techStack,
|
||||
Metadata: map[string]interface{}{
|
||||
"task_id": taskID,
|
||||
"issue_id": issue.ID,
|
||||
"issue_number": issue.Number,
|
||||
"task_id": taskID,
|
||||
"issue_id": issue.ID,
|
||||
"issue_number": issue.Number,
|
||||
"repository_id": repo.ID,
|
||||
"external_url": issue.HTMLURL,
|
||||
"external_url": issue.HTMLURL,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -785,6 +789,80 @@ func (m *Monitor) triggerTeamComposition(ctx context.Context, taskID string, iss
|
||||
Msg("🚀 Task successfully assigned to team")
|
||||
}
|
||||
|
||||
// TriggerTeamCompositionForCouncil runs team composition for the task associated with a council once the council is active.
|
||||
func (m *Monitor) TriggerTeamCompositionForCouncil(ctx context.Context, taskID string) {
|
||||
logger := log.With().Str("task_id", taskID).Logger()
|
||||
logger.Info().Msg("🔁 Triggering team composition for council task")
|
||||
|
||||
if m.composer == nil {
|
||||
logger.Warn().Msg("Composer service unavailable; cannot trigger team composition")
|
||||
return
|
||||
}
|
||||
|
||||
taskUUID, err := uuid.Parse(taskID)
|
||||
if err != nil {
|
||||
logger.Error().Err(err).Msg("Invalid task ID format; skipping team composition")
|
||||
return
|
||||
}
|
||||
|
||||
// Load task details so we can build the analysis input
|
||||
taskService := tasks.NewService(m.db)
|
||||
task, err := taskService.GetTask(ctx, taskUUID)
|
||||
if err != nil {
|
||||
logger.Error().Err(err).Msg("Failed to load task for council team composition")
|
||||
return
|
||||
}
|
||||
|
||||
analysisInput := &composer.TaskAnalysisInput{
|
||||
Title: task.Title,
|
||||
Description: task.Description,
|
||||
Repository: task.Repository,
|
||||
Requirements: task.Requirements,
|
||||
Priority: m.mapPriorityToComposer(string(task.Priority)),
|
||||
TechStack: task.TechStack,
|
||||
Metadata: map[string]interface{}{
|
||||
"task_id": task.ID.String(),
|
||||
"source_type": string(task.SourceType),
|
||||
"source_config": task.SourceConfig,
|
||||
"labels": task.Labels,
|
||||
},
|
||||
}
|
||||
|
||||
// Perform team composition analysis
|
||||
result, err := m.composer.AnalyzeAndComposeTeam(ctx, analysisInput)
|
||||
if err != nil {
|
||||
logger.Error().Err(err).Msg("Team composition analysis failed for council task")
|
||||
return
|
||||
}
|
||||
|
||||
logger.Info().
|
||||
Str("team_id", result.TeamComposition.TeamID.String()).
|
||||
Int("estimated_size", result.TeamComposition.EstimatedSize).
|
||||
Float64("confidence", result.TeamComposition.ConfidenceScore).
|
||||
Msg("✅ Council task team composition analysis completed")
|
||||
|
||||
team, err := m.composer.CreateTeam(ctx, result.TeamComposition, analysisInput)
|
||||
if err != nil {
|
||||
logger.Error().Err(err).Msg("Failed to create team for council task")
|
||||
return
|
||||
}
|
||||
|
||||
if err := m.assignTaskToTeam(ctx, taskID, team.ID.String()); err != nil {
|
||||
logger.Error().Err(err).Str("team_id", team.ID.String()).Msg("Failed to assign council task to team")
|
||||
}
|
||||
|
||||
// Optionally deploy agents for the team if our orchestrator is available
|
||||
if m.agentDeployer != nil {
|
||||
repo := RepositoryConfig{FullName: task.Repository}
|
||||
go m.deployTeamAgents(ctx, taskID, team, result.TeamComposition, repo)
|
||||
}
|
||||
|
||||
logger.Info().
|
||||
Str("team_id", team.ID.String()).
|
||||
Str("team_name", team.Name).
|
||||
Msg("🚀 Council task assigned to team")
|
||||
}
|
||||
|
||||
// deployTeamAgents deploys Docker containers for the assigned team agents
|
||||
func (m *Monitor) deployTeamAgents(ctx context.Context, taskID string, team *composer.Team, teamComposition *composer.TeamComposition, repo RepositoryConfig) {
|
||||
log.Info().
|
||||
@@ -810,9 +888,9 @@ func (m *Monitor) deployTeamAgents(ctx context.Context, taskID string, team *com
|
||||
IssueDescription: team.Description, // TODO: Extract actual issue description
|
||||
Repository: repo.FullName,
|
||||
TechStack: []string{"go", "docker", "ai"}, // TODO: Extract from analysis
|
||||
Requirements: []string{}, // TODO: Extract from issue
|
||||
Priority: "medium", // TODO: Extract from team data
|
||||
ExternalURL: "", // TODO: Add issue URL
|
||||
Requirements: []string{}, // TODO: Extract from issue
|
||||
Priority: "medium", // TODO: Extract from team data
|
||||
ExternalURL: "", // TODO: Add issue URL
|
||||
Metadata: map[string]interface{}{
|
||||
"task_type": "development",
|
||||
},
|
||||
@@ -820,6 +898,15 @@ func (m *Monitor) deployTeamAgents(ctx context.Context, taskID string, team *com
|
||||
DeploymentMode: "immediate",
|
||||
}
|
||||
|
||||
// Check if agent deployment is available (Docker enabled)
|
||||
if m.agentDeployer == nil {
|
||||
log.Info().
|
||||
Str("task_id", taskID).
|
||||
Str("team_id", team.ID.String()).
|
||||
Msg("Docker disabled - team assignment completed without agent deployment")
|
||||
return
|
||||
}
|
||||
|
||||
// Deploy all agents for this team
|
||||
deploymentResult, err := m.agentDeployer.DeployTeamAgents(deploymentRequest)
|
||||
if err != nil {
|
||||
@@ -933,20 +1020,20 @@ func (m *Monitor) triggerCouncilFormation(ctx context.Context, taskID string, is
|
||||
|
||||
// Create council formation request
|
||||
councilRequest := &council.CouncilFormationRequest{
|
||||
ProjectName: projectName,
|
||||
Repository: repo.FullName,
|
||||
ProjectBrief: issue.Body,
|
||||
TaskID: taskUUID,
|
||||
IssueID: issue.ID,
|
||||
ExternalURL: issue.HTMLURL,
|
||||
ProjectName: projectName,
|
||||
Repository: repo.FullName,
|
||||
ProjectBrief: issue.Body,
|
||||
TaskID: taskUUID,
|
||||
IssueID: issue.ID,
|
||||
ExternalURL: issue.HTMLURL,
|
||||
Metadata: map[string]interface{}{
|
||||
"task_id": taskID,
|
||||
"issue_id": issue.ID,
|
||||
"issue_number": issue.Number,
|
||||
"repository_id": repo.ID,
|
||||
"created_by": issue.User.Login,
|
||||
"labels": m.extractLabelNames(issue.Labels),
|
||||
"milestone": m.extractMilestone(issue),
|
||||
"task_id": taskID,
|
||||
"issue_id": issue.ID,
|
||||
"issue_number": issue.Number,
|
||||
"repository_id": repo.ID,
|
||||
"created_by": issue.User.Login,
|
||||
"labels": m.extractLabelNames(issue.Labels),
|
||||
"milestone": m.extractMilestone(issue),
|
||||
},
|
||||
}
|
||||
|
||||
@@ -974,6 +1061,61 @@ func (m *Monitor) triggerCouncilFormation(ctx context.Context, taskID string, is
|
||||
Int("optional_agents", len(composition.OptionalAgents)).
|
||||
Msg("✅ Council composition formed")
|
||||
|
||||
// Broadcast council opportunity to CHORUS agents
|
||||
if m.broadcaster != nil {
|
||||
go func() {
|
||||
// Build council opportunity for broadcast
|
||||
coreRoles := make([]p2p.CouncilRole, len(composition.CoreAgents))
|
||||
for i, agent := range composition.CoreAgents {
|
||||
coreRoles[i] = p2p.CouncilRole{
|
||||
RoleName: agent.RoleName,
|
||||
AgentName: agent.AgentName,
|
||||
Required: agent.Required,
|
||||
Description: fmt.Sprintf("Core role: %s", agent.AgentName),
|
||||
}
|
||||
}
|
||||
|
||||
optionalRoles := make([]p2p.CouncilRole, len(composition.OptionalAgents))
|
||||
for i, agent := range composition.OptionalAgents {
|
||||
optionalRoles[i] = p2p.CouncilRole{
|
||||
RoleName: agent.RoleName,
|
||||
AgentName: agent.AgentName,
|
||||
Required: agent.Required,
|
||||
Description: fmt.Sprintf("Optional role: %s", agent.AgentName),
|
||||
}
|
||||
}
|
||||
|
||||
opportunity := &p2p.CouncilOpportunity{
|
||||
CouncilID: composition.CouncilID,
|
||||
ProjectName: projectName,
|
||||
Repository: repo.FullName,
|
||||
ProjectBrief: issue.Body,
|
||||
CoreRoles: coreRoles,
|
||||
OptionalRoles: optionalRoles,
|
||||
UCXLAddress: fmt.Sprintf("ucxl://team:council@project:%s:council/councils/%s", strings.ReplaceAll(projectName, " ", "-"), composition.CouncilID.String()),
|
||||
FormationDeadline: time.Now().Add(24 * time.Hour),
|
||||
CreatedAt: composition.CreatedAt,
|
||||
Metadata: councilRequest.Metadata,
|
||||
}
|
||||
|
||||
broadcastCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
if err := m.broadcaster.BroadcastCouncilOpportunity(broadcastCtx, opportunity); err != nil {
|
||||
log.Error().
|
||||
Err(err).
|
||||
Str("council_id", composition.CouncilID.String()).
|
||||
Msg("Failed to broadcast council opportunity to CHORUS agents")
|
||||
} else {
|
||||
log.Info().
|
||||
Str("council_id", composition.CouncilID.String()).
|
||||
Int("core_roles", len(coreRoles)).
|
||||
Int("optional_roles", len(optionalRoles)).
|
||||
Msg("📡 Successfully broadcast council opportunity to CHORUS agents")
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// Deploy council agents if agent deployer is available
|
||||
if m.agentDeployer != nil {
|
||||
go m.deployCouncilAgents(ctx, taskID, composition, councilRequest, repo)
|
||||
@@ -1019,24 +1161,42 @@ func (m *Monitor) deployCouncilAgents(ctx context.Context, taskID string, compos
|
||||
|
||||
// Create council deployment request
|
||||
deploymentRequest := &orchestrator.CouncilDeploymentRequest{
|
||||
CouncilID: composition.CouncilID,
|
||||
ProjectName: composition.ProjectName,
|
||||
CouncilID: composition.CouncilID,
|
||||
ProjectName: composition.ProjectName,
|
||||
CouncilComposition: composition,
|
||||
ProjectContext: &orchestrator.CouncilProjectContext{
|
||||
ProjectName: composition.ProjectName,
|
||||
Repository: request.Repository,
|
||||
ProjectBrief: request.ProjectBrief,
|
||||
Constraints: request.Constraints,
|
||||
TechLimits: request.TechLimits,
|
||||
ProjectName: composition.ProjectName,
|
||||
Repository: request.Repository,
|
||||
ProjectBrief: request.ProjectBrief,
|
||||
Constraints: request.Constraints,
|
||||
TechLimits: request.TechLimits,
|
||||
ComplianceNotes: request.ComplianceNotes,
|
||||
Targets: request.Targets,
|
||||
ExternalURL: request.ExternalURL,
|
||||
Targets: request.Targets,
|
||||
ExternalURL: request.ExternalURL,
|
||||
},
|
||||
DeploymentMode: "immediate",
|
||||
}
|
||||
|
||||
// Deploy the council agents
|
||||
result, err := m.agentDeployer.DeployCouncilAgents(deploymentRequest)
|
||||
// Check if agent deployment is available (Docker enabled)
|
||||
if m.agentDeployer == nil {
|
||||
log.Info().
|
||||
Str("council_id", composition.CouncilID.String()).
|
||||
Msg("Docker disabled - council formation completed without agent deployment")
|
||||
|
||||
// Update council status to active since formation is complete
|
||||
m.council.UpdateCouncilStatus(ctx, composition.CouncilID, "active")
|
||||
|
||||
span.SetAttributes(
|
||||
attribute.String("deployment.status", "skipped"),
|
||||
attribute.String("deployment.reason", "docker_disabled"),
|
||||
attribute.Int("deployment.deployed_agents", 0),
|
||||
attribute.Int("deployment.errors", 0),
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
// Assign the council agents to available CHORUS agents instead of deploying new services
|
||||
result, err := m.agentDeployer.AssignCouncilAgents(deploymentRequest)
|
||||
if err != nil {
|
||||
tracing.SetSpanError(span, err)
|
||||
log.Error().
|
||||
@@ -1114,4 +1274,3 @@ func (m *Monitor) extractMilestone(issue gitea.Issue) string {
|
||||
// For now, return empty string to avoid build issues
|
||||
return ""
|
||||
}
|
||||
|
||||
|
||||
325
internal/p2p/broadcaster.go
Normal file
325
internal/p2p/broadcaster.go
Normal file
@@ -0,0 +1,325 @@
|
||||
package p2p
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
// Broadcaster handles P2P broadcasting of opportunities and events to CHORUS agents
|
||||
type Broadcaster struct {
|
||||
discovery *Discovery
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
}
|
||||
|
||||
// NewBroadcaster creates a new P2P broadcaster
|
||||
func NewBroadcaster(discovery *Discovery) *Broadcaster {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
|
||||
return &Broadcaster{
|
||||
discovery: discovery,
|
||||
ctx: ctx,
|
||||
cancel: cancel,
|
||||
}
|
||||
}
|
||||
|
||||
// Close shuts down the broadcaster
|
||||
func (b *Broadcaster) Close() error {
|
||||
b.cancel()
|
||||
return nil
|
||||
}
|
||||
|
||||
// CouncilOpportunity represents a council formation opportunity for agents to claim
|
||||
type CouncilOpportunity struct {
|
||||
CouncilID uuid.UUID `json:"council_id"`
|
||||
ProjectName string `json:"project_name"`
|
||||
Repository string `json:"repository"`
|
||||
ProjectBrief string `json:"project_brief"`
|
||||
CoreRoles []CouncilRole `json:"core_roles"`
|
||||
OptionalRoles []CouncilRole `json:"optional_roles"`
|
||||
UCXLAddress string `json:"ucxl_address"`
|
||||
FormationDeadline time.Time `json:"formation_deadline"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
Metadata map[string]interface{} `json:"metadata"`
|
||||
}
|
||||
|
||||
// CouncilRole represents a role within a council that can be claimed
|
||||
type CouncilRole struct {
|
||||
RoleName string `json:"role_name"`
|
||||
AgentName string `json:"agent_name"`
|
||||
Required bool `json:"required"`
|
||||
RequiredSkills []string `json:"required_skills"`
|
||||
Description string `json:"description"`
|
||||
}
|
||||
|
||||
// RoleCounts provides claimed vs total counts for a role category
|
||||
type RoleCounts struct {
|
||||
Total int `json:"total"`
|
||||
Claimed int `json:"claimed"`
|
||||
}
|
||||
|
||||
// PersonaCounts captures persona readiness across council roles.
|
||||
type PersonaCounts struct {
|
||||
Total int `json:"total"`
|
||||
Loaded int `json:"loaded"`
|
||||
CoreLoaded int `json:"core_loaded"`
|
||||
}
|
||||
|
||||
// CouncilStatusUpdate notifies agents about council staffing progress
|
||||
type CouncilStatusUpdate struct {
|
||||
CouncilID uuid.UUID `json:"council_id"`
|
||||
ProjectName string `json:"project_name"`
|
||||
Status string `json:"status"`
|
||||
Message string `json:"message,omitempty"`
|
||||
Timestamp time.Time `json:"timestamp"`
|
||||
CoreRoles RoleCounts `json:"core_roles"`
|
||||
Optional RoleCounts `json:"optional_roles"`
|
||||
Personas PersonaCounts `json:"personas,omitempty"`
|
||||
BriefDispatched bool `json:"brief_dispatched"`
|
||||
}
|
||||
|
||||
// BroadcastCouncilOpportunity broadcasts a council formation opportunity to all available CHORUS agents
|
||||
func (b *Broadcaster) BroadcastCouncilOpportunity(ctx context.Context, opportunity *CouncilOpportunity) error {
|
||||
log.Info().
|
||||
Str("council_id", opportunity.CouncilID.String()).
|
||||
Str("project_name", opportunity.ProjectName).
|
||||
Int("core_roles", len(opportunity.CoreRoles)).
|
||||
Int("optional_roles", len(opportunity.OptionalRoles)).
|
||||
Msg("📡 Broadcasting council opportunity to CHORUS agents")
|
||||
|
||||
// Get all discovered agents
|
||||
agents := b.discovery.GetAgents()
|
||||
|
||||
if len(agents) == 0 {
|
||||
log.Warn().Msg("No CHORUS agents discovered to broadcast opportunity to")
|
||||
return fmt.Errorf("no agents available to receive broadcast")
|
||||
}
|
||||
|
||||
successCount := 0
|
||||
errorCount := 0
|
||||
|
||||
// Broadcast to each agent
|
||||
for _, agent := range agents {
|
||||
err := b.sendOpportunityToAgent(ctx, agent, opportunity)
|
||||
if err != nil {
|
||||
log.Error().
|
||||
Err(err).
|
||||
Str("agent_id", agent.ID).
|
||||
Str("endpoint", agent.Endpoint).
|
||||
Msg("Failed to send opportunity to agent")
|
||||
errorCount++
|
||||
continue
|
||||
}
|
||||
successCount++
|
||||
}
|
||||
|
||||
log.Info().
|
||||
Int("success_count", successCount).
|
||||
Int("error_count", errorCount).
|
||||
Int("total_agents", len(agents)).
|
||||
Msg("✅ Council opportunity broadcast completed")
|
||||
|
||||
if successCount == 0 {
|
||||
return fmt.Errorf("failed to broadcast to any agents")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// sendOpportunityToAgent sends a council opportunity to a specific CHORUS agent
|
||||
func (b *Broadcaster) sendOpportunityToAgent(ctx context.Context, agent *Agent, opportunity *CouncilOpportunity) error {
|
||||
// Construct the agent's opportunity endpoint
|
||||
// CHORUS agents should expose /api/v1/opportunities endpoint to receive opportunities
|
||||
opportunityURL := fmt.Sprintf("%s/api/v1/opportunities/council", agent.Endpoint)
|
||||
|
||||
// Marshal opportunity to JSON
|
||||
payload, err := json.Marshal(opportunity)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal opportunity: %w", err)
|
||||
}
|
||||
|
||||
// Create HTTP request
|
||||
req, err := http.NewRequestWithContext(ctx, "POST", opportunityURL, bytes.NewBuffer(payload))
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create request: %w", err)
|
||||
}
|
||||
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("X-WHOOSH-Broadcast", "council-opportunity")
|
||||
req.Header.Set("X-Council-ID", opportunity.CouncilID.String())
|
||||
|
||||
// Send request with timeout
|
||||
client := &http.Client{
|
||||
Timeout: 10 * time.Second,
|
||||
}
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to send opportunity to agent: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusAccepted {
|
||||
return fmt.Errorf("agent returned non-success status: %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
log.Debug().
|
||||
Str("agent_id", agent.ID).
|
||||
Str("council_id", opportunity.CouncilID.String()).
|
||||
Int("status_code", resp.StatusCode).
|
||||
Msg("Successfully sent council opportunity to agent")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// BroadcastAgentAssignment notifies an agent that they've been assigned to a council role
|
||||
func (b *Broadcaster) BroadcastAgentAssignment(ctx context.Context, agentID string, assignment *AgentAssignment) error {
|
||||
// Find the agent
|
||||
agents := b.discovery.GetAgents()
|
||||
var targetAgent *Agent
|
||||
|
||||
for _, agent := range agents {
|
||||
if agent.ID == agentID {
|
||||
targetAgent = agent
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if targetAgent == nil {
|
||||
return fmt.Errorf("agent %s not found in discovery", agentID)
|
||||
}
|
||||
|
||||
// Send assignment to agent
|
||||
assignmentURL := fmt.Sprintf("%s/api/v1/assignments/council", targetAgent.Endpoint)
|
||||
|
||||
payload, err := json.Marshal(assignment)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal assignment: %w", err)
|
||||
}
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, "POST", assignmentURL, bytes.NewBuffer(payload))
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create request: %w", err)
|
||||
}
|
||||
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("X-WHOOSH-Broadcast", "council-assignment")
|
||||
|
||||
client := &http.Client{Timeout: 10 * time.Second}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to send assignment to agent: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusAccepted {
|
||||
return fmt.Errorf("agent returned non-success status: %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
log.Info().
|
||||
Str("agent_id", agentID).
|
||||
Str("council_id", assignment.CouncilID.String()).
|
||||
Str("role", assignment.RoleName).
|
||||
Msg("✅ Successfully notified agent of council assignment")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// BroadcastCouncilStatusUpdate notifies all discovered agents about council staffing status
|
||||
func (b *Broadcaster) BroadcastCouncilStatusUpdate(ctx context.Context, update *CouncilStatusUpdate) error {
|
||||
log.Info().
|
||||
Str("council_id", update.CouncilID.String()).
|
||||
Str("status", update.Status).
|
||||
Msg("📢 Broadcasting council status update to CHORUS agents")
|
||||
|
||||
agents := b.discovery.GetAgents()
|
||||
if len(agents) == 0 {
|
||||
log.Warn().Str("council_id", update.CouncilID.String()).Msg("No CHORUS agents discovered for council status update")
|
||||
return fmt.Errorf("no agents available to receive council status update")
|
||||
}
|
||||
|
||||
successCount := 0
|
||||
errorCount := 0
|
||||
|
||||
for _, agent := range agents {
|
||||
if err := b.sendCouncilStatusToAgent(ctx, agent, update); err != nil {
|
||||
log.Error().
|
||||
Err(err).
|
||||
Str("agent_id", agent.ID).
|
||||
Str("council_id", update.CouncilID.String()).
|
||||
Msg("Failed to send council status update to agent")
|
||||
errorCount++
|
||||
continue
|
||||
}
|
||||
successCount++
|
||||
}
|
||||
|
||||
log.Info().
|
||||
Str("council_id", update.CouncilID.String()).
|
||||
Int("success_count", successCount).
|
||||
Int("error_count", errorCount).
|
||||
Int("total_agents", len(agents)).
|
||||
Msg("✅ Council status update broadcast completed")
|
||||
|
||||
if successCount == 0 {
|
||||
return fmt.Errorf("failed to broadcast council status update to any agents")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *Broadcaster) sendCouncilStatusToAgent(ctx context.Context, agent *Agent, update *CouncilStatusUpdate) error {
|
||||
statusURL := fmt.Sprintf("%s/api/v1/councils/status", agent.Endpoint)
|
||||
|
||||
payload, err := json.Marshal(update)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal council status update: %w", err)
|
||||
}
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, "POST", statusURL, bytes.NewBuffer(payload))
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create council status request: %w", err)
|
||||
}
|
||||
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("X-WHOOSH-Broadcast", "council-status")
|
||||
req.Header.Set("X-Council-ID", update.CouncilID.String())
|
||||
|
||||
client := &http.Client{Timeout: 10 * time.Second}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to send council status to agent: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusAccepted {
|
||||
return fmt.Errorf("agent returned non-success status: %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
log.Debug().
|
||||
Str("agent_id", agent.ID).
|
||||
Str("council_id", update.CouncilID.String()).
|
||||
Int("status_code", resp.StatusCode).
|
||||
Msg("Successfully sent council status update to agent")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// AgentAssignment represents an assignment of an agent to a council role
|
||||
type AgentAssignment struct {
|
||||
CouncilID uuid.UUID `json:"council_id"`
|
||||
ProjectName string `json:"project_name"`
|
||||
RoleName string `json:"role_name"`
|
||||
UCXLAddress string `json:"ucxl_address"`
|
||||
ProjectBrief string `json:"project_brief"`
|
||||
Repository string `json:"repository"`
|
||||
AssignedAt time.Time `json:"assigned_at"`
|
||||
Metadata map[string]interface{} `json:"metadata"`
|
||||
}
|
||||
@@ -179,8 +179,8 @@ func NewServer(cfg *config.Config, db *database.DB) (*Server, error) {
|
||||
log.Warn().Msg("🐳 Docker integration disabled - scaling system and council agent deployment unavailable")
|
||||
}
|
||||
|
||||
// Initialize repository monitor with team composer, council composer, and agent deployer
|
||||
repoMonitor := monitor.NewMonitor(db.Pool, cfg.GITEA, teamComposer, councilComposer, agentDeployer)
|
||||
// Initialize repository monitor with team composer, council composer, agent deployer, and broadcaster
|
||||
repoMonitor := monitor.NewMonitor(db.Pool, cfg.GITEA, teamComposer, councilComposer, agentDeployer, p2pBroadcaster)
|
||||
|
||||
s := &Server{
|
||||
config: cfg,
|
||||
|
||||
Reference in New Issue
Block a user