From d1d61c063bdc3ef76720739417158a91224e4569 Mon Sep 17 00:00:00 2001 From: anthonyrawlins Date: Mon, 14 Jul 2025 22:06:50 +1000 Subject: [PATCH] Fix critical issues breaking task execution cycle MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- Dockerfile.sandbox | 9 ++++++ cmd/bzzz-monitor.py | 16 +++++----- executor/executor.go | 61 ++++++++++++++++++++++++++++---------- github/client.go | 10 ++++++- github/hive_integration.go | 21 +++++++++++-- pkg/hive/client.go | 12 ++++---- sandbox/sandbox.go | 15 ++++++++++ 7 files changed, 111 insertions(+), 33 deletions(-) diff --git a/Dockerfile.sandbox b/Dockerfile.sandbox index 40ddf666..74d6c7f8 100644 --- a/Dockerfile.sandbox +++ b/Dockerfile.sandbox @@ -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 diff --git a/cmd/bzzz-monitor.py b/cmd/bzzz-monitor.py index 580358b5..d52959e4 100755 --- a/cmd/bzzz-monitor.py +++ b/cmd/bzzz-monitor.py @@ -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): diff --git a/executor/executor.go b/executor/executor.go index 3c06ee7a..638dd36b 100644 --- a/executor/executor.go +++ b/executor/executor.go @@ -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, ) diff --git a/github/client.go b/github/client.go index ecf70ba6..07c4476b 100644 --- a/github/client.go +++ b/github/client.go @@ -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( diff --git a/github/hive_integration.go b/github/hive_integration.go index f13bd49e..3effaecc 100644 --- a/github/hive_integration.go +++ b/github/hive_integration.go @@ -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 } diff --git a/pkg/hive/client.go b/pkg/hive/client.go index f7b5c327..7736a6e3 100644 --- a/pkg/hive/client.go +++ b/pkg/hive/client.go @@ -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) diff --git a/sandbox/sandbox.go b/sandbox/sandbox.go index 1ef3701b..dab4e80e 100644 --- a/sandbox/sandbox.go +++ b/sandbox/sandbox.go @@ -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)