Initial commit: Bzzz P2P Task Coordination System with Antennae meta-discussion layer
- libp2p networking with mDNS discovery - GitHub Issues integration for atomic task management - PubSub messaging for coordination and meta-discussion - Hypercore-inspired distributed logging - Tested P2P network formation between WALNUT and IRONWOOD nodes 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
381
github/client.go
Normal file
381
github/client.go
Normal file
@@ -0,0 +1,381 @@
|
||||
package github
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/google/go-github/v57/github"
|
||||
"golang.org/x/oauth2"
|
||||
)
|
||||
|
||||
// Client wraps the GitHub API client for Bzzz task management
|
||||
type Client struct {
|
||||
client *github.Client
|
||||
ctx context.Context
|
||||
config *Config
|
||||
}
|
||||
|
||||
// Config holds GitHub integration configuration
|
||||
type Config struct {
|
||||
AccessToken string
|
||||
Owner string // GitHub organization/user
|
||||
Repository string // Repository for task coordination
|
||||
|
||||
// Task management settings
|
||||
TaskLabel string // Label for Bzzz tasks
|
||||
InProgressLabel string // Label for tasks in progress
|
||||
CompletedLabel string // Label for completed tasks
|
||||
|
||||
// Branch management
|
||||
BaseBranch string // Base branch for task branches
|
||||
BranchPrefix string // Prefix for task branches
|
||||
}
|
||||
|
||||
// NewClient creates a new GitHub client for Bzzz integration
|
||||
func NewClient(ctx context.Context, config *Config) (*Client, error) {
|
||||
if config.AccessToken == "" {
|
||||
return nil, fmt.Errorf("GitHub access token is required")
|
||||
}
|
||||
|
||||
if config.Owner == "" || config.Repository == "" {
|
||||
return nil, fmt.Errorf("GitHub owner and repository are required")
|
||||
}
|
||||
|
||||
// Set defaults
|
||||
if config.TaskLabel == "" {
|
||||
config.TaskLabel = "bzzz-task"
|
||||
}
|
||||
if config.InProgressLabel == "" {
|
||||
config.InProgressLabel = "in-progress"
|
||||
}
|
||||
if config.CompletedLabel == "" {
|
||||
config.CompletedLabel = "completed"
|
||||
}
|
||||
if config.BaseBranch == "" {
|
||||
config.BaseBranch = "main"
|
||||
}
|
||||
if config.BranchPrefix == "" {
|
||||
config.BranchPrefix = "bzzz/task-"
|
||||
}
|
||||
|
||||
// Create OAuth2 token source
|
||||
ts := oauth2.StaticTokenSource(
|
||||
&oauth2.Token{AccessToken: config.AccessToken},
|
||||
)
|
||||
tc := oauth2.NewClient(ctx, ts)
|
||||
|
||||
client := &Client{
|
||||
client: github.NewClient(tc),
|
||||
ctx: ctx,
|
||||
config: config,
|
||||
}
|
||||
|
||||
// Verify access to repository
|
||||
if err := client.verifyAccess(); err != nil {
|
||||
return nil, fmt.Errorf("failed to verify GitHub access: %w", err)
|
||||
}
|
||||
|
||||
return client, nil
|
||||
}
|
||||
|
||||
// verifyAccess checks if we can access the configured repository
|
||||
func (c *Client) verifyAccess() error {
|
||||
_, _, err := c.client.Repositories.Get(c.ctx, c.config.Owner, c.config.Repository)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot access repository %s/%s: %w",
|
||||
c.config.Owner, c.config.Repository, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Task represents a Bzzz task as a GitHub issue
|
||||
type Task struct {
|
||||
ID int64 `json:"id"`
|
||||
Number int `json:"number"`
|
||||
Title string `json:"title"`
|
||||
Description string `json:"description"`
|
||||
State string `json:"state"` // open, closed
|
||||
Labels []string `json:"labels"`
|
||||
Assignee string `json:"assignee"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
|
||||
// Bzzz-specific fields
|
||||
TaskType string `json:"task_type"`
|
||||
Priority int `json:"priority"`
|
||||
Requirements []string `json:"requirements"`
|
||||
Deliverables []string `json:"deliverables"`
|
||||
Context map[string]interface{} `json:"context"`
|
||||
}
|
||||
|
||||
// CreateTask creates a new GitHub issue for a Bzzz task
|
||||
func (c *Client) CreateTask(task *Task) (*Task, error) {
|
||||
// Prepare issue request
|
||||
issue := &github.IssueRequest{
|
||||
Title: &task.Title,
|
||||
Body: github.String(c.formatTaskBody(task)),
|
||||
Labels: &[]string{
|
||||
c.config.TaskLabel,
|
||||
fmt.Sprintf("priority-%d", task.Priority),
|
||||
fmt.Sprintf("type-%s", task.TaskType),
|
||||
},
|
||||
}
|
||||
|
||||
// Create the issue
|
||||
createdIssue, _, err := c.client.Issues.Create(
|
||||
c.ctx,
|
||||
c.config.Owner,
|
||||
c.config.Repository,
|
||||
issue,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create GitHub issue: %w", err)
|
||||
}
|
||||
|
||||
// Convert back to our Task format
|
||||
return c.issueToTask(createdIssue), nil
|
||||
}
|
||||
|
||||
// ClaimTask atomically assigns a task to an agent
|
||||
func (c *Client) ClaimTask(issueNumber int, agentID string) (*Task, error) {
|
||||
// Get current issue state
|
||||
issue, _, err := c.client.Issues.Get(
|
||||
c.ctx,
|
||||
c.config.Owner,
|
||||
c.config.Repository,
|
||||
issueNumber,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get issue: %w", err)
|
||||
}
|
||||
|
||||
// Check if already assigned
|
||||
if issue.Assignee != nil {
|
||||
return nil, fmt.Errorf("task already assigned to %s", issue.Assignee.GetLogin())
|
||||
}
|
||||
|
||||
// Attempt atomic assignment using GitHub's native assignment
|
||||
issueRequest := &github.IssueRequest{
|
||||
Assignee: &agentID,
|
||||
}
|
||||
|
||||
// Add in-progress label
|
||||
currentLabels := make([]string, 0, len(issue.Labels)+1)
|
||||
for _, label := range issue.Labels {
|
||||
currentLabels = append(currentLabels, label.GetName())
|
||||
}
|
||||
currentLabels = append(currentLabels, c.config.InProgressLabel)
|
||||
issueRequest.Labels = ¤tLabels
|
||||
|
||||
// Update the issue
|
||||
updatedIssue, _, err := c.client.Issues.Edit(
|
||||
c.ctx,
|
||||
c.config.Owner,
|
||||
c.config.Repository,
|
||||
issueNumber,
|
||||
issueRequest,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to claim task: %w", err)
|
||||
}
|
||||
|
||||
// Create a task branch
|
||||
if err := c.createTaskBranch(issueNumber, agentID); err != nil {
|
||||
// Log error but don't fail the claim
|
||||
fmt.Printf("⚠️ Failed to create task branch: %v\n", err)
|
||||
}
|
||||
|
||||
return c.issueToTask(updatedIssue), nil
|
||||
}
|
||||
|
||||
// CompleteTask marks a task as completed and creates a pull request
|
||||
func (c *Client) CompleteTask(issueNumber int, agentID string, results map[string]interface{}) error {
|
||||
// Update issue labels
|
||||
issue, _, err := c.client.Issues.Get(
|
||||
c.ctx,
|
||||
c.config.Owner,
|
||||
c.config.Repository,
|
||||
issueNumber,
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get issue: %w", err)
|
||||
}
|
||||
|
||||
// Remove in-progress label, add completed label
|
||||
newLabels := make([]string, 0, len(issue.Labels))
|
||||
for _, label := range issue.Labels {
|
||||
labelName := label.GetName()
|
||||
if labelName != c.config.InProgressLabel {
|
||||
newLabels = append(newLabels, labelName)
|
||||
}
|
||||
}
|
||||
newLabels = append(newLabels, c.config.CompletedLabel)
|
||||
|
||||
// Add completion comment
|
||||
comment := &github.IssueComment{
|
||||
Body: github.String(c.formatCompletionComment(agentID, results)),
|
||||
}
|
||||
|
||||
_, _, err = c.client.Issues.CreateComment(
|
||||
c.ctx,
|
||||
c.config.Owner,
|
||||
c.config.Repository,
|
||||
issueNumber,
|
||||
comment,
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to add completion comment: %w", err)
|
||||
}
|
||||
|
||||
// Update labels
|
||||
issueRequest := &github.IssueRequest{
|
||||
Labels: &newLabels,
|
||||
State: github.String("closed"),
|
||||
}
|
||||
|
||||
_, _, err = c.client.Issues.Edit(
|
||||
c.ctx,
|
||||
c.config.Owner,
|
||||
c.config.Repository,
|
||||
issueNumber,
|
||||
issueRequest,
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to update issue: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ListAvailableTasks returns unassigned Bzzz tasks
|
||||
func (c *Client) ListAvailableTasks() ([]*Task, error) {
|
||||
// Search for open issues with Bzzz task label and no assignee
|
||||
opts := &github.IssueListByRepoOptions{
|
||||
State: "open",
|
||||
Labels: []string{c.config.TaskLabel},
|
||||
Assignee: "none",
|
||||
Sort: "created",
|
||||
Direction: "desc",
|
||||
ListOptions: github.ListOptions{PerPage: 50},
|
||||
}
|
||||
|
||||
issues, _, err := c.client.Issues.ListByRepo(
|
||||
c.ctx,
|
||||
c.config.Owner,
|
||||
c.config.Repository,
|
||||
opts,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to list issues: %w", err)
|
||||
}
|
||||
|
||||
tasks := make([]*Task, 0, len(issues))
|
||||
for _, issue := range issues {
|
||||
tasks = append(tasks, c.issueToTask(issue))
|
||||
}
|
||||
|
||||
return tasks, nil
|
||||
}
|
||||
|
||||
// createTaskBranch creates a new branch for task work
|
||||
func (c *Client) createTaskBranch(issueNumber int, agentID string) error {
|
||||
branchName := fmt.Sprintf("%s%d-%s", c.config.BranchPrefix, issueNumber, agentID)
|
||||
|
||||
// Get the base branch reference
|
||||
baseRef, _, err := c.client.Git.GetRef(
|
||||
c.ctx,
|
||||
c.config.Owner,
|
||||
c.config.Repository,
|
||||
"refs/heads/"+c.config.BaseBranch,
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get base branch: %w", err)
|
||||
}
|
||||
|
||||
// Create new branch
|
||||
newRef := &github.Reference{
|
||||
Ref: github.String("refs/heads/" + branchName),
|
||||
Object: &github.GitObject{
|
||||
SHA: baseRef.Object.SHA,
|
||||
},
|
||||
}
|
||||
|
||||
_, _, err = c.client.Git.CreateRef(
|
||||
c.ctx,
|
||||
c.config.Owner,
|
||||
c.config.Repository,
|
||||
newRef,
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create branch: %w", err)
|
||||
}
|
||||
|
||||
fmt.Printf("🌿 Created task branch: %s\n", branchName)
|
||||
return nil
|
||||
}
|
||||
|
||||
// formatTaskBody formats task details into GitHub issue body
|
||||
func (c *Client) formatTaskBody(task *Task) string {
|
||||
body := fmt.Sprintf("**Task Type:** %s\n", task.TaskType)
|
||||
body += fmt.Sprintf("**Priority:** %d\n", task.Priority)
|
||||
body += fmt.Sprintf("\n**Description:**\n%s\n", task.Description)
|
||||
|
||||
if len(task.Requirements) > 0 {
|
||||
body += "\n**Requirements:**\n"
|
||||
for _, req := range task.Requirements {
|
||||
body += fmt.Sprintf("- %s\n", req)
|
||||
}
|
||||
}
|
||||
|
||||
if len(task.Deliverables) > 0 {
|
||||
body += "\n**Deliverables:**\n"
|
||||
for _, deliverable := range task.Deliverables {
|
||||
body += fmt.Sprintf("- %s\n", deliverable)
|
||||
}
|
||||
}
|
||||
|
||||
body += "\n---\n*This task is managed by Bzzz P2P Task Coordination System*"
|
||||
return body
|
||||
}
|
||||
|
||||
// formatCompletionComment formats task completion results
|
||||
func (c *Client) formatCompletionComment(agentID string, results map[string]interface{}) string {
|
||||
comment := fmt.Sprintf("✅ **Task completed by agent: %s**\n\n", agentID)
|
||||
comment += fmt.Sprintf("**Completion time:** %s\n\n", time.Now().Format(time.RFC3339))
|
||||
|
||||
if len(results) > 0 {
|
||||
comment += "**Results:**\n"
|
||||
for key, value := range results {
|
||||
comment += fmt.Sprintf("- **%s:** %v\n", key, value)
|
||||
}
|
||||
}
|
||||
|
||||
return comment
|
||||
}
|
||||
|
||||
// issueToTask converts a GitHub issue to a Bzzz task
|
||||
func (c *Client) issueToTask(issue *github.Issue) *Task {
|
||||
task := &Task{
|
||||
ID: issue.GetID(),
|
||||
Number: issue.GetNumber(),
|
||||
Title: issue.GetTitle(),
|
||||
Description: issue.GetBody(),
|
||||
State: issue.GetState(),
|
||||
CreatedAt: issue.GetCreatedAt().Time,
|
||||
UpdatedAt: issue.GetUpdatedAt().Time,
|
||||
}
|
||||
|
||||
// Extract labels
|
||||
task.Labels = make([]string, 0, len(issue.Labels))
|
||||
for _, label := range issue.Labels {
|
||||
task.Labels = append(task.Labels, label.GetName())
|
||||
}
|
||||
|
||||
// Extract assignee
|
||||
if issue.Assignee != nil {
|
||||
task.Assignee = issue.Assignee.GetLogin()
|
||||
}
|
||||
|
||||
return task
|
||||
}
|
||||
207
github/integration.go
Normal file
207
github/integration.go
Normal file
@@ -0,0 +1,207 @@
|
||||
package github
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/deepblackcloud/bzzz/pubsub"
|
||||
)
|
||||
|
||||
// Integration handles the integration between GitHub tasks and Bzzz P2P coordination
|
||||
type Integration struct {
|
||||
client *Client
|
||||
pubsub *pubsub.PubSub
|
||||
ctx context.Context
|
||||
config *IntegrationConfig
|
||||
}
|
||||
|
||||
// IntegrationConfig holds configuration for GitHub-Bzzz integration
|
||||
type IntegrationConfig struct {
|
||||
PollInterval time.Duration // How often to check for new tasks
|
||||
MaxTasks int // Maximum tasks to process simultaneously
|
||||
AgentID string // This agent's identifier
|
||||
Capabilities []string // What types of tasks this agent can handle
|
||||
}
|
||||
|
||||
// NewIntegration creates a new GitHub-Bzzz integration
|
||||
func NewIntegration(ctx context.Context, client *Client, ps *pubsub.PubSub, config *IntegrationConfig) *Integration {
|
||||
if config.PollInterval == 0 {
|
||||
config.PollInterval = 30 * time.Second
|
||||
}
|
||||
if config.MaxTasks == 0 {
|
||||
config.MaxTasks = 3
|
||||
}
|
||||
|
||||
return &Integration{
|
||||
client: client,
|
||||
pubsub: ps,
|
||||
ctx: ctx,
|
||||
config: config,
|
||||
}
|
||||
}
|
||||
|
||||
// Start begins the GitHub-Bzzz integration
|
||||
func (i *Integration) Start() {
|
||||
fmt.Printf("🔗 Starting GitHub-Bzzz integration for agent: %s\n", i.config.AgentID)
|
||||
|
||||
// Start task polling
|
||||
go i.pollForTasks()
|
||||
|
||||
// Start listening for P2P task announcements
|
||||
go i.listenForTaskAnnouncements()
|
||||
}
|
||||
|
||||
// pollForTasks periodically checks GitHub for available tasks
|
||||
func (i *Integration) pollForTasks() {
|
||||
ticker := time.NewTicker(i.config.PollInterval)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-i.ctx.Done():
|
||||
return
|
||||
case <-ticker.C:
|
||||
if err := i.checkAndClaimTasks(); err != nil {
|
||||
fmt.Printf("❌ Error checking tasks: %v\n", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// checkAndClaimTasks looks for available tasks and claims suitable ones
|
||||
func (i *Integration) checkAndClaimTasks() error {
|
||||
// Get available tasks
|
||||
tasks, err := i.client.ListAvailableTasks()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to list tasks: %w", err)
|
||||
}
|
||||
|
||||
if len(tasks) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
fmt.Printf("📋 Found %d available tasks\n", len(tasks))
|
||||
|
||||
// Filter tasks based on capabilities
|
||||
suitableTasks := i.filterSuitableTasks(tasks)
|
||||
|
||||
if len(suitableTasks) == 0 {
|
||||
fmt.Printf("⚠️ No suitable tasks for agent capabilities: %v\n", i.config.Capabilities)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Claim the highest priority suitable task
|
||||
task := suitableTasks[0] // Assuming sorted by priority
|
||||
claimedTask, err := i.client.ClaimTask(task.Number, i.config.AgentID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to claim task %d: %w", task.Number, err)
|
||||
}
|
||||
|
||||
fmt.Printf("✋ Claimed task #%d: %s\n", claimedTask.Number, claimedTask.Title)
|
||||
|
||||
// Announce the claim over P2P
|
||||
if err := i.announceTaskClaim(claimedTask); err != nil {
|
||||
fmt.Printf("⚠️ Failed to announce task claim: %v\n", err)
|
||||
}
|
||||
|
||||
// Start working on the task
|
||||
go i.executeTask(claimedTask)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// filterSuitableTasks filters tasks based on agent capabilities
|
||||
func (i *Integration) filterSuitableTasks(tasks []*Task) []*Task {
|
||||
suitable := make([]*Task, 0)
|
||||
|
||||
for _, task := range tasks {
|
||||
// Check if this agent can handle this task type
|
||||
if i.canHandleTaskType(task.TaskType) {
|
||||
suitable = append(suitable, task)
|
||||
}
|
||||
}
|
||||
|
||||
return suitable
|
||||
}
|
||||
|
||||
// canHandleTaskType checks if this agent can handle the given task type
|
||||
func (i *Integration) canHandleTaskType(taskType string) bool {
|
||||
for _, capability := range i.config.Capabilities {
|
||||
if capability == taskType || capability == "general" {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// announceTaskClaim announces a task claim over the P2P network
|
||||
func (i *Integration) announceTaskClaim(task *Task) error {
|
||||
data := map[string]interface{}{
|
||||
"task_id": task.Number,
|
||||
"task_title": task.Title,
|
||||
"task_type": task.TaskType,
|
||||
"agent_id": i.config.AgentID,
|
||||
"claimed_at": time.Now().Unix(),
|
||||
"github_url": fmt.Sprintf("https://github.com/%s/%s/issues/%d",
|
||||
i.client.config.Owner, i.client.config.Repository, task.Number),
|
||||
}
|
||||
|
||||
return i.pubsub.PublishBzzzMessage(pubsub.TaskClaim, data)
|
||||
}
|
||||
|
||||
// executeTask simulates task execution
|
||||
func (i *Integration) executeTask(task *Task) {
|
||||
fmt.Printf("🚀 Starting execution of task #%d: %s\n", task.Number, task.Title)
|
||||
|
||||
// Announce task progress
|
||||
progressData := map[string]interface{}{
|
||||
"task_id": task.Number,
|
||||
"agent_id": i.config.AgentID,
|
||||
"status": "started",
|
||||
"timestamp": time.Now().Unix(),
|
||||
}
|
||||
|
||||
if err := i.pubsub.PublishBzzzMessage(pubsub.TaskProgress, progressData); err != nil {
|
||||
fmt.Printf("⚠️ Failed to announce task progress: %v\n", err)
|
||||
}
|
||||
|
||||
// Simulate work (in a real implementation, this would be actual task execution)
|
||||
workDuration := time.Duration(30+task.Priority*10) * time.Second
|
||||
fmt.Printf("⏳ Working on task for %v...\n", workDuration)
|
||||
time.Sleep(workDuration)
|
||||
|
||||
// Complete the task
|
||||
results := map[string]interface{}{
|
||||
"status": "completed",
|
||||
"execution_time": workDuration.String(),
|
||||
"agent_id": i.config.AgentID,
|
||||
"deliverables": []string{"Implementation completed", "Tests passed", "Documentation updated"},
|
||||
}
|
||||
|
||||
if err := i.client.CompleteTask(task.Number, i.config.AgentID, results); err != nil {
|
||||
fmt.Printf("❌ Failed to complete task #%d: %v\n", task.Number, err)
|
||||
return
|
||||
}
|
||||
|
||||
// Announce completion over P2P
|
||||
completionData := map[string]interface{}{
|
||||
"task_id": task.Number,
|
||||
"agent_id": i.config.AgentID,
|
||||
"completed_at": time.Now().Unix(),
|
||||
"results": results,
|
||||
}
|
||||
|
||||
if err := i.pubsub.PublishBzzzMessage(pubsub.TaskComplete, completionData); err != nil {
|
||||
fmt.Printf("⚠️ Failed to announce task completion: %v\n", err)
|
||||
}
|
||||
|
||||
fmt.Printf("✅ Completed task #%d: %s\n", task.Number, task.Title)
|
||||
}
|
||||
|
||||
// listenForTaskAnnouncements listens for task announcements from other agents
|
||||
func (i *Integration) listenForTaskAnnouncements() {
|
||||
// This would integrate with the pubsub message handlers
|
||||
// For now, it's a placeholder that demonstrates the pattern
|
||||
fmt.Printf("👂 Listening for task announcements from other agents...\n")
|
||||
}
|
||||
Reference in New Issue
Block a user