Fix critical issues breaking task execution cycle
- Fix branch name validation by hashing peer IDs using SHA256 - Fix Hive API claiming error by using correct 'task_number' parameter - Improve console app display with 300% wider columns and adaptive width - Add GitHub CLI integration to sandbox with token authentication - Enhance system prompt with collaboration guidelines and help escalation - Fix sandbox lifecycle to preserve work even if PR creation fails 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -7,6 +7,15 @@ RUN apt-get update && apt-get install -y \
|
||||
git \
|
||||
curl \
|
||||
tree \
|
||||
wget \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Install GitHub CLI
|
||||
RUN curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg | dd of=/usr/share/keyrings/githubcli-archive-keyring.gpg \
|
||||
&& chmod go+r /usr/share/keyrings/githubcli-archive-keyring.gpg \
|
||||
&& echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" | tee /etc/apt/sources.list.d/github-cli.list > /dev/null \
|
||||
&& apt-get update \
|
||||
&& apt-get install -y gh \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Create a non-root user for the agent to run as
|
||||
|
||||
@@ -245,7 +245,7 @@ class BzzzMonitor:
|
||||
def draw_p2p_status(self):
|
||||
"""Draw P2P network status section"""
|
||||
print(f"{Colors.BOLD}{Colors.BRIGHT_GREEN}P2P Network Status{Colors.RESET}")
|
||||
print("━" * 30)
|
||||
print("━" * min(80, self.terminal_width - 10))
|
||||
|
||||
# Current peers
|
||||
peer_color = Colors.BRIGHT_GREEN if self.current_peers > 0 else Colors.BRIGHT_RED
|
||||
@@ -265,7 +265,7 @@ class BzzzMonitor:
|
||||
def draw_agent_activity(self):
|
||||
"""Draw agent activity section"""
|
||||
print(f"{Colors.BOLD}{Colors.BRIGHT_YELLOW}Agent Activity{Colors.RESET}")
|
||||
print("━" * 30)
|
||||
print("━" * min(80, self.terminal_width - 10))
|
||||
|
||||
if not self.availability_history:
|
||||
print(f"{Colors.DIM}No recent agent activity{Colors.RESET}")
|
||||
@@ -307,7 +307,7 @@ class BzzzMonitor:
|
||||
def draw_coordination_channels(self):
|
||||
"""Draw real coordination channel statistics"""
|
||||
print(f"{Colors.BOLD}{Colors.BRIGHT_MAGENTA}Coordination Channels{Colors.RESET}")
|
||||
print("━" * 50)
|
||||
print("━" * min(120, self.terminal_width - 10))
|
||||
|
||||
self.update_message_rates()
|
||||
|
||||
@@ -331,7 +331,7 @@ class BzzzMonitor:
|
||||
def draw_coordination_status(self):
|
||||
"""Draw coordination activity section"""
|
||||
print(f"{Colors.BOLD}{Colors.BRIGHT_CYAN}System Status{Colors.RESET}")
|
||||
print("━" * 30)
|
||||
print("━" * min(80, self.terminal_width - 10))
|
||||
|
||||
# Total coordination stats
|
||||
total_coord_msgs = (self.channel_stats['bzzz_coordination']['messages'] +
|
||||
@@ -350,7 +350,7 @@ class BzzzMonitor:
|
||||
def draw_recent_activity(self):
|
||||
"""Draw recent activity log with compact timestamps"""
|
||||
print(f"{Colors.BOLD}{Colors.BRIGHT_WHITE}Recent Activity{Colors.RESET}")
|
||||
print("━" * 50)
|
||||
print("━" * min(120, self.terminal_width - 10))
|
||||
|
||||
# Combine and sort recent activities
|
||||
all_activities = []
|
||||
@@ -379,7 +379,7 @@ class BzzzMonitor:
|
||||
continue # These go to errors
|
||||
else:
|
||||
msg = msg.split(': ', 1)[-1] if ': ' in msg else msg
|
||||
msg = msg[:60] + "..." if len(msg) > 60 else msg
|
||||
msg = msg[:180] + "..." if len(msg) > 180 else msg
|
||||
|
||||
all_activities.append({
|
||||
'time': activity['timestamp'],
|
||||
@@ -397,7 +397,7 @@ class BzzzMonitor:
|
||||
err_msg = "GitHub verification failed (expected)"
|
||||
else:
|
||||
err_msg = err_msg.split(': ', 1)[-1] if ': ' in err_msg else err_msg
|
||||
err_msg = err_msg[:60] + "..." if len(err_msg) > 60 else err_msg
|
||||
err_msg = err_msg[:180] + "..." if len(err_msg) > 180 else err_msg
|
||||
|
||||
all_activities.append({
|
||||
'time': error['timestamp'],
|
||||
@@ -453,7 +453,7 @@ class BzzzMonitor:
|
||||
|
||||
def draw_footer(self):
|
||||
"""Draw footer with controls"""
|
||||
print("━" * 50)
|
||||
print("━" * min(120, self.terminal_width - 10))
|
||||
print(f"{Colors.DIM}Press Ctrl+C to exit | Refresh rate: {self.refresh_rate}s{Colors.RESET}")
|
||||
|
||||
def run(self):
|
||||
|
||||
@@ -13,19 +13,27 @@ import (
|
||||
|
||||
const maxIterations = 10 // Prevents infinite loops
|
||||
|
||||
// ExecuteTaskResult contains the result of task execution
|
||||
type ExecuteTaskResult struct {
|
||||
BranchName string
|
||||
Sandbox *sandbox.Sandbox
|
||||
}
|
||||
|
||||
// ExecuteTask manages the entire lifecycle of a task using a sandboxed environment.
|
||||
func ExecuteTask(ctx context.Context, task *types.EnhancedTask, hlog *logging.HypercoreLog) (string, error) {
|
||||
// Returns sandbox reference so it can be destroyed after PR creation
|
||||
func ExecuteTask(ctx context.Context, task *types.EnhancedTask, hlog *logging.HypercoreLog) (*ExecuteTaskResult, error) {
|
||||
// 1. Create the sandbox environment
|
||||
sb, err := sandbox.CreateSandbox(ctx, "") // Use default image for now
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to create sandbox: %w", err)
|
||||
return nil, fmt.Errorf("failed to create sandbox: %w", err)
|
||||
}
|
||||
defer sb.DestroySandbox()
|
||||
// NOTE: Do NOT defer destroy here - let caller handle it
|
||||
|
||||
// 2. Clone the repository inside the sandbox
|
||||
cloneCmd := fmt.Sprintf("git clone %s .", task.GitURL)
|
||||
if _, err := sb.RunCommand(cloneCmd); err != nil {
|
||||
return "", fmt.Errorf("failed to clone repository in sandbox: %w", err)
|
||||
sb.DestroySandbox() // Clean up on error
|
||||
return nil, fmt.Errorf("failed to clone repository in sandbox: %w", err)
|
||||
}
|
||||
hlog.Append(logging.TaskProgress, map[string]interface{}{"task_id": task.Number, "status": "cloned repo"})
|
||||
|
||||
@@ -35,7 +43,8 @@ func ExecuteTask(ctx context.Context, task *types.EnhancedTask, hlog *logging.Hy
|
||||
// a. Generate the next command based on the task and previous output
|
||||
nextCommand, err := generateNextCommand(ctx, task, lastCommandOutput)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to generate next command: %w", err)
|
||||
sb.DestroySandbox() // Clean up on error
|
||||
return nil, fmt.Errorf("failed to generate next command: %w", err)
|
||||
}
|
||||
|
||||
hlog.Append(logging.TaskProgress, map[string]interface{}{
|
||||
@@ -65,34 +74,56 @@ func ExecuteTask(ctx context.Context, task *types.EnhancedTask, hlog *logging.Hy
|
||||
// 4. Create a new branch and commit the changes
|
||||
branchName := fmt.Sprintf("bzzz-task-%d", task.Number)
|
||||
if _, err := sb.RunCommand(fmt.Sprintf("git checkout -b %s", branchName)); err != nil {
|
||||
return "", fmt.Errorf("failed to create branch: %w", err)
|
||||
sb.DestroySandbox() // Clean up on error
|
||||
return nil, fmt.Errorf("failed to create branch: %w", err)
|
||||
}
|
||||
if _, err := sb.RunCommand("git add ."); err != nil {
|
||||
return "", fmt.Errorf("failed to add files: %w", err)
|
||||
sb.DestroySandbox() // Clean up on error
|
||||
return nil, fmt.Errorf("failed to add files: %w", err)
|
||||
}
|
||||
commitCmd := fmt.Sprintf("git commit -m 'feat: resolve task #%d'", task.Number)
|
||||
if _, err := sb.RunCommand(commitCmd); err != nil {
|
||||
return "", fmt.Errorf("failed to commit changes: %w", err)
|
||||
sb.DestroySandbox() // Clean up on error
|
||||
return nil, fmt.Errorf("failed to commit changes: %w", err)
|
||||
}
|
||||
|
||||
// 5. Push the new branch
|
||||
if _, err := sb.RunCommand(fmt.Sprintf("git push origin %s", branchName)); err != nil {
|
||||
return "", fmt.Errorf("failed to push branch: %w", err)
|
||||
sb.DestroySandbox() // Clean up on error
|
||||
return nil, fmt.Errorf("failed to push branch: %w", err)
|
||||
}
|
||||
|
||||
hlog.Append(logging.TaskProgress, map[string]interface{}{"task_id": task.Number, "status": "pushed changes"})
|
||||
return branchName, nil
|
||||
return &ExecuteTaskResult{
|
||||
BranchName: branchName,
|
||||
Sandbox: sb,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// generateNextCommand uses the LLM to decide the next command to execute.
|
||||
func generateNextCommand(ctx context.Context, task *types.EnhancedTask, lastOutput string) (string, error) {
|
||||
prompt := fmt.Sprintf(
|
||||
"You are an AI developer in a sandboxed shell environment. Your goal is to solve the following GitHub issue:\n\n"+
|
||||
"You are an AI developer agent in the Bzzz P2P distributed development network, working in a sandboxed shell environment.\n\n"+
|
||||
"TASK DETAILS:\n"+
|
||||
"Title: %s\nDescription: %s\n\n"+
|
||||
"You can only interact with the system by issuing shell commands. "+
|
||||
"The previous command output was:\n---\n%s\n---\n"+
|
||||
"Based on this, what is the single next shell command you should run? "+
|
||||
"If you believe the task is complete and ready for a pull request, respond with 'TASK_COMPLETE'.",
|
||||
"CAPABILITIES & RESOURCES:\n"+
|
||||
"- You can issue shell commands to solve this GitHub issue\n"+
|
||||
"- You are part of a collaborative P2P mesh with other AI agents\n"+
|
||||
"- If stuck, you can ask for help by using keywords: 'stuck', 'help', 'clarification needed', 'manual intervention'\n"+
|
||||
"- Complex problems automatically escalate to human experts via N8N webhooks\n"+
|
||||
"- You have access to git, build tools, editors, and development utilities\n"+
|
||||
"- GitHub CLI (gh) is available for creating PRs: use 'gh pr create --title \"title\" --body \"description\"'\n"+
|
||||
"- GitHub authentication is configured automatically\n"+
|
||||
"- Work is preserved even if issues occur - your changes are committed and pushed\n\n"+
|
||||
"COLLABORATION GUIDELINES:\n"+
|
||||
"- Use clear, descriptive commit messages\n"+
|
||||
"- Break complex problems into smaller steps\n"+
|
||||
"- Ask for help early if you encounter unfamiliar technologies\n"+
|
||||
"- Document your reasoning in commands where helpful\n\n"+
|
||||
"PREVIOUS OUTPUT:\n---\n%s\n---\n\n"+
|
||||
"Based on this context, what is the single next shell command you should run?\n"+
|
||||
"If you believe the task is complete and ready for a pull request, respond with 'TASK_COMPLETE'.\n"+
|
||||
"If you need help, include relevant keywords in your response.",
|
||||
task.Title, task.Description, lastOutput,
|
||||
)
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ package github
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
@@ -321,9 +322,16 @@ func (c *Client) ListAvailableTasks() ([]*Task, error) {
|
||||
return tasks, nil
|
||||
}
|
||||
|
||||
// hashAgentID creates a short hash of the agent ID for safe branch naming
|
||||
func hashAgentID(agentID string) string {
|
||||
hash := sha256.Sum256([]byte(agentID))
|
||||
return fmt.Sprintf("%x", hash[:8]) // Use first 8 bytes (16 hex chars)
|
||||
}
|
||||
|
||||
// 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)
|
||||
hashedAgentID := hashAgentID(agentID)
|
||||
branchName := fmt.Sprintf("%s%d-%s", c.config.BranchPrefix, issueNumber, hashedAgentID)
|
||||
|
||||
// Get the base branch reference
|
||||
baseRef, _, err := c.client.Git.GetRef(
|
||||
|
||||
@@ -313,18 +313,33 @@ func (hi *HiveIntegration) executeTask(task *types.EnhancedTask, repoClient *Rep
|
||||
fmt.Printf("🚀 Starting execution of task #%d in sandbox...\n", task.Number)
|
||||
|
||||
// The executor now handles the entire iterative process.
|
||||
branchName, err := executor.ExecuteTask(hi.ctx, task, hi.hlog)
|
||||
result, err := executor.ExecuteTask(hi.ctx, task, hi.hlog)
|
||||
if err != nil {
|
||||
fmt.Printf("❌ Failed to execute task #%d: %v\n", task.Number, err)
|
||||
hi.hlog.Append(logging.TaskFailed, map[string]interface{}{"task_id": task.Number, "reason": "task execution failed in sandbox"})
|
||||
return
|
||||
}
|
||||
|
||||
// Ensure sandbox cleanup happens regardless of PR creation success/failure
|
||||
defer result.Sandbox.DestroySandbox()
|
||||
|
||||
// Create a pull request
|
||||
pr, err := repoClient.Client.CreatePullRequest(task.Number, branchName, hi.config.AgentID)
|
||||
pr, err := repoClient.Client.CreatePullRequest(task.Number, result.BranchName, hi.config.AgentID)
|
||||
if err != nil {
|
||||
fmt.Printf("❌ Failed to create pull request for task #%d: %v\n", task.Number, err)
|
||||
hi.hlog.Append(logging.TaskFailed, map[string]interface{}{"task_id": task.Number, "reason": "failed to create pull request"})
|
||||
fmt.Printf("📝 Note: Branch '%s' has been pushed to repository and work is preserved\n", result.BranchName)
|
||||
|
||||
// Escalate PR creation failure to humans via N8N webhook
|
||||
escalationReason := fmt.Sprintf("Failed to create pull request: %v. Task execution completed successfully and work is preserved in branch '%s', but PR creation failed.", err, result.BranchName)
|
||||
hi.requestAssistance(task, escalationReason, fmt.Sprintf("bzzz/meta/issue/%d", task.Number))
|
||||
|
||||
hi.hlog.Append(logging.TaskFailed, map[string]interface{}{
|
||||
"task_id": task.Number,
|
||||
"reason": "failed to create pull request",
|
||||
"branch_name": result.BranchName,
|
||||
"work_preserved": true,
|
||||
"escalated": true,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -49,9 +49,9 @@ type ActiveRepositoriesResponse struct {
|
||||
|
||||
// TaskClaimRequest represents a task claim request to Hive
|
||||
type TaskClaimRequest struct {
|
||||
TaskID int `json:"task_id"`
|
||||
AgentID string `json:"agent_id"`
|
||||
ClaimedAt int64 `json:"claimed_at"`
|
||||
TaskNumber int `json:"task_number"`
|
||||
AgentID string `json:"agent_id"`
|
||||
ClaimedAt int64 `json:"claimed_at"`
|
||||
}
|
||||
|
||||
// TaskStatusUpdate represents a task status update to Hive
|
||||
@@ -133,9 +133,9 @@ func (c *HiveClient) ClaimTask(ctx context.Context, projectID, taskID int, agent
|
||||
url := fmt.Sprintf("%s/api/bzzz/projects/%d/claim", c.BaseURL, projectID)
|
||||
|
||||
claimRequest := TaskClaimRequest{
|
||||
TaskID: taskID,
|
||||
AgentID: agentID,
|
||||
ClaimedAt: time.Now().Unix(),
|
||||
TaskNumber: taskID,
|
||||
AgentID: agentID,
|
||||
ClaimedAt: time.Now().Unix(),
|
||||
}
|
||||
|
||||
jsonData, err := json.Marshal(claimRequest)
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/docker/docker/client"
|
||||
@@ -53,6 +54,16 @@ func CreateSandbox(ctx context.Context, taskImage string) (*Sandbox, error) {
|
||||
return nil, fmt.Errorf("failed to create temp dir for sandbox: %w", err)
|
||||
}
|
||||
|
||||
// Read GitHub token for authentication
|
||||
githubToken := os.Getenv("BZZZ_GITHUB_TOKEN")
|
||||
if githubToken == "" {
|
||||
// Try to read from file
|
||||
tokenBytes, err := os.ReadFile("/home/tony/AI/secrets/passwords_and_tokens/gh-token")
|
||||
if err == nil {
|
||||
githubToken = strings.TrimSpace(string(tokenBytes))
|
||||
}
|
||||
}
|
||||
|
||||
// Define container configuration
|
||||
containerConfig := &container.Config{
|
||||
Image: taskImage,
|
||||
@@ -60,6 +71,10 @@ func CreateSandbox(ctx context.Context, taskImage string) (*Sandbox, error) {
|
||||
OpenStdin: true,
|
||||
WorkingDir: "/home/agent/work",
|
||||
User: "agent",
|
||||
Env: []string{
|
||||
"GITHUB_TOKEN=" + githubToken,
|
||||
"GH_TOKEN=" + githubToken,
|
||||
},
|
||||
}
|
||||
|
||||
// Define host configuration (e.g., volume mounts, resource limits)
|
||||
|
||||
Reference in New Issue
Block a user