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:
anthonyrawlins
2025-07-14 22:06:50 +10:00
parent 588e561e9d
commit d1d61c063b
7 changed files with 111 additions and 33 deletions

View File

@@ -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

View File

@@ -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):

View File

@@ -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,
)

View File

@@ -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(

View File

@@ -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
}

View File

@@ -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)

View File

@@ -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)