mirror of
https://github.com/block/goose.git
synced 2026-07-17 12:56:20 +02:00
checkpoint
This commit is contained in:
@@ -62,14 +62,13 @@ pub fn register_node(repo: &str, worker_id: &str) -> Result<()> {
|
||||
// Get the issue number we just created
|
||||
let url = String::from_utf8_lossy(&output.stdout);
|
||||
url.split('/')
|
||||
.last()
|
||||
.next_back()
|
||||
.and_then(|s| s.trim().parse::<u32>().ok())
|
||||
.context("Failed to parse issue number from URL")?
|
||||
} else {
|
||||
issues[0]["number"]
|
||||
.as_u64()
|
||||
.context("Failed to get issue number")?
|
||||
as u32
|
||||
.context("Failed to get issue number")? as u32
|
||||
}
|
||||
} else {
|
||||
return Err(anyhow::anyhow!("Failed to list issues"));
|
||||
@@ -577,7 +576,7 @@ pub fn check_open_prs(repo: &str, task_issue_numbers: &[u32]) -> Result<Vec<serd
|
||||
if pr.get("isDraft").and_then(|v| v.as_bool()).unwrap_or(false) {
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
pr["task_issue"] = serde_json::json!(task_num);
|
||||
|
||||
// Avoid duplicates if a PR mentions multiple task issues
|
||||
@@ -679,7 +678,12 @@ pub fn create_task_issue(repo: &str, parent_issue: u32, title: &str, content: &s
|
||||
}
|
||||
|
||||
/// Create a planning issue from a file
|
||||
pub fn create_planning_issue(repo: &str, parent_issue: u32, title: &str, content: &str) -> Result<()> {
|
||||
pub fn create_planning_issue(
|
||||
repo: &str,
|
||||
parent_issue: u32,
|
||||
title: &str,
|
||||
content: &str,
|
||||
) -> Result<()> {
|
||||
// Make sure it doesn't have [task] prefix
|
||||
let final_title = if title.starts_with("[task]") {
|
||||
title.replace("[task]", "").trim().to_string()
|
||||
@@ -780,7 +784,12 @@ pub fn close_planning_issue(repo: &str, issue_number: u32) -> Result<()> {
|
||||
}
|
||||
|
||||
/// Create an evaluation issue
|
||||
pub fn create_evaluation_issue(repo: &str, parent_issue: u32, title: &str, content: &str) -> Result<()> {
|
||||
pub fn create_evaluation_issue(
|
||||
repo: &str,
|
||||
parent_issue: u32,
|
||||
title: &str,
|
||||
content: &str,
|
||||
) -> Result<()> {
|
||||
let body = format!("{}\n\nOriginal issue: #{}", content, parent_issue);
|
||||
|
||||
// Create the new issue
|
||||
@@ -819,6 +828,6 @@ pub fn add_help_wanted_label(repo: &str, issue_number: u32) -> Result<()> {
|
||||
if output.status.success() {
|
||||
println!("🔄 Re-added 'help wanted' label for another worker to try");
|
||||
}
|
||||
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -6,14 +6,20 @@ use crate::commands::goose_swarm::repo::{
|
||||
get_task_issue_numbers, is_issue_claimed, register_node, remove_help_wanted_label,
|
||||
unclaim_issue, GitHubIssue,
|
||||
};
|
||||
use crate::recipes::extract_from_cli::extract_recipe_info_from_cli;
|
||||
use crate::session::{build_session, SessionBuilderConfig};
|
||||
use anyhow::{Context, Result};
|
||||
use clap::Args;
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
use std::process::Command;
|
||||
use std::thread;
|
||||
use std::time::Duration;
|
||||
|
||||
// Embed recipe YAML files at compile time
|
||||
const PLAN_WORK_RECIPE: &str = include_str!("goose_swarm/plan_work.yaml");
|
||||
const SWARM_DRONE_RECIPE: &str = include_str!("goose_swarm/swarm_drone.yaml");
|
||||
const EVALUATE_RECIPE: &str = include_str!("goose_swarm/evaluate.yaml");
|
||||
|
||||
/// Orchestrate parallel work on GitHub issues using swarm intelligence
|
||||
#[derive(Args, Debug)]
|
||||
pub struct SwarmArgs {
|
||||
@@ -40,17 +46,69 @@ enum WorkType {
|
||||
None, // No work available
|
||||
}
|
||||
|
||||
/// Generate a unique worker ID
|
||||
/// Generate a unique worker ID with fun names
|
||||
fn generate_worker_id() -> String {
|
||||
let timestamp = chrono::Utc::now().format("%m%d");
|
||||
let random_suffix: String = (0..4)
|
||||
.map(|_| {
|
||||
let idx = rand::random::<usize>() % 26;
|
||||
(b'a' + idx as u8) as char
|
||||
})
|
||||
.collect();
|
||||
// Fun adjectives for the first word
|
||||
let adjectives = [
|
||||
"happy", "brave", "clever", "swift", "mighty", "gentle", "bright", "eager", "noble",
|
||||
"bold", "wise", "keen", "quick", "sharp", "calm", "cool", "smart", "lucky", "proud",
|
||||
"strong", "wild", "free", "grand", "true", "fair", "kind", "warm", "neat", "glad", "busy",
|
||||
"spry", "jolly",
|
||||
];
|
||||
|
||||
format!("thing-{}-{}", timestamp, random_suffix)
|
||||
// Fun nouns for the last word
|
||||
let nouns = [
|
||||
"fox", "owl", "bee", "cat", "dog", "bat", "ant", "elk", "emu", "jay", "hawk", "wolf",
|
||||
"bear", "lion", "deer", "duck", "crow", "dove", "fish", "frog", "goat", "hare", "moth",
|
||||
"newt", "orca", "puma", "quail", "raven", "seal", "swan", "toad", "vole", "wasp", "yak",
|
||||
"lynx", "mole", "otter",
|
||||
];
|
||||
|
||||
let adj_idx = rand::random::<usize>() % adjectives.len();
|
||||
let noun_idx = rand::random::<usize>() % nouns.len();
|
||||
let timestamp = chrono::Utc::now().format("%m%d");
|
||||
|
||||
format!("{}-{}-{}", adjectives[adj_idx], timestamp, nouns[noun_idx])
|
||||
}
|
||||
|
||||
/// Load or generate worker ID, persisting it to .node-id file
|
||||
fn get_or_create_worker_id(provided_id: Option<String>) -> Result<String> {
|
||||
// If explicitly provided, use that
|
||||
if let Some(id) = provided_id {
|
||||
return Ok(id);
|
||||
}
|
||||
|
||||
// Check for .node-id file in current directory
|
||||
let node_id_path = Path::new(".node-id");
|
||||
|
||||
if node_id_path.exists() {
|
||||
// Try to read existing ID
|
||||
match fs::read_to_string(node_id_path) {
|
||||
Ok(contents) => {
|
||||
let id = contents.trim().to_string();
|
||||
if !id.is_empty() {
|
||||
println!("📎 Loaded worker ID from .node-id: {}", id);
|
||||
return Ok(id);
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
println!("⚠️ Failed to read .node-id: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Generate new ID
|
||||
let new_id = generate_worker_id();
|
||||
|
||||
// Try to save it
|
||||
if let Err(e) = fs::write(node_id_path, &new_id) {
|
||||
println!("⚠️ Failed to save .node-id: {}", e);
|
||||
println!(" Worker ID will be regenerated on next run");
|
||||
} else {
|
||||
println!("💾 Saved worker ID to .node-id for future runs");
|
||||
}
|
||||
|
||||
Ok(new_id)
|
||||
}
|
||||
|
||||
/// Detect available work
|
||||
@@ -222,6 +280,7 @@ fn process_evaluation_issues(repo: &str, parent_issue: u32, issues_dir: &str) ->
|
||||
}
|
||||
|
||||
/// Execute planning work
|
||||
#[allow(clippy::too_many_lines)]
|
||||
async fn execute_planning_work(repo: &str, issue: &GitHubIssue, worker_id: &str) -> Result<()> {
|
||||
println!(
|
||||
"📋 Working on planning issue #{}: {}",
|
||||
@@ -272,11 +331,8 @@ async fn execute_planning_work(repo: &str, issue: &GitHubIssue, worker_id: &str)
|
||||
}
|
||||
|
||||
// Run the planning recipe to create tasks
|
||||
let recipe_path = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
|
||||
.join("src/commands/goose-swarm/plan_work.yaml");
|
||||
|
||||
println!("🎯 Running planning recipe to break down the issue...");
|
||||
run_recipe(recipe_path, params).await?;
|
||||
run_recipe(PLAN_WORK_RECIPE, params).await?;
|
||||
|
||||
// Process task files in tasks/ directory
|
||||
process_task_files(repo, issue.number, &tasks_dir)?;
|
||||
@@ -345,37 +401,44 @@ async fn execute_planning_work(repo: &str, issue: &GitHubIssue, worker_id: &str)
|
||||
];
|
||||
|
||||
// Run the evaluate recipe
|
||||
let eval_recipe_path = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
|
||||
.join("src/commands/goose-swarm/evaluate.yaml");
|
||||
|
||||
println!("🔮 Running evaluation recipe...");
|
||||
run_recipe(eval_recipe_path.clone(), eval_params.clone()).await?;
|
||||
run_recipe(EVALUATE_RECIPE, eval_params.clone()).await?;
|
||||
|
||||
// Process any new issues created by evaluate recipe
|
||||
process_evaluation_issues(repo, issue.number, &eval_issues_dir)?;
|
||||
|
||||
// Check if all PRs are closed/merged
|
||||
let remaining_open_prs = check_open_prs(repo, &task_issue_numbers)?;
|
||||
|
||||
|
||||
if !remaining_open_prs.is_empty() {
|
||||
println!("⚠️ {} PR(s) still open after evaluation. Running evaluation once more...", remaining_open_prs.len());
|
||||
|
||||
println!(
|
||||
"⚠️ {} PR(s) still open after evaluation. Running evaluation once more...",
|
||||
remaining_open_prs.len()
|
||||
);
|
||||
|
||||
// Run evaluation one more time to give it a chance to address open PRs
|
||||
run_recipe(eval_recipe_path, eval_params).await?;
|
||||
|
||||
run_recipe(EVALUATE_RECIPE, eval_params).await?;
|
||||
|
||||
// Process any new issues from second evaluation
|
||||
process_evaluation_issues(repo, issue.number, &eval_issues_dir)?;
|
||||
|
||||
|
||||
// Check PRs again
|
||||
let final_open_prs = check_open_prs(repo, &task_issue_numbers)?;
|
||||
|
||||
|
||||
if !final_open_prs.is_empty() {
|
||||
println!("📝 {} PR(s) still open. Closing them now...", final_open_prs.len());
|
||||
|
||||
println!(
|
||||
"📝 {} PR(s) still open. Closing them now...",
|
||||
final_open_prs.len()
|
||||
);
|
||||
|
||||
// Close all remaining open PRs
|
||||
for pr in final_open_prs {
|
||||
if let Some(pr_num) = pr.get("number").and_then(|n| n.as_u64()) {
|
||||
close_pr(repo, pr_num, "Automatically closed by Goose Swarm after evaluation phase.")?;
|
||||
close_pr(
|
||||
repo,
|
||||
pr_num,
|
||||
"Automatically closed by Goose Swarm after evaluation phase.",
|
||||
)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -434,10 +497,7 @@ async fn execute_task_work(repo: &str, issue: &GitHubIssue, worker_id: &str) ->
|
||||
}
|
||||
|
||||
// Run the worker recipe
|
||||
let recipe_path = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
|
||||
.join("src/commands/goose-swarm/swarm_drone.yaml");
|
||||
|
||||
run_recipe(recipe_path, params).await?;
|
||||
run_recipe(SWARM_DRONE_RECIPE, params).await?;
|
||||
|
||||
// Check if still on main branch (indicates failure)
|
||||
let output = Command::new("git")
|
||||
@@ -467,13 +527,46 @@ async fn execute_task_work(repo: &str, issue: &GitHubIssue, worker_id: &str) ->
|
||||
}
|
||||
|
||||
/// Run a recipe with given parameters
|
||||
async fn run_recipe(recipe_path: std::path::PathBuf, params: Vec<(String, String)>) -> Result<()> {
|
||||
// Extract recipe info and build input config
|
||||
let (input_config, recipe_info) = extract_recipe_info_from_cli(
|
||||
recipe_path.to_string_lossy().to_string(),
|
||||
params,
|
||||
Vec::new(),
|
||||
)?;
|
||||
async fn run_recipe(recipe_content: &str, params: Vec<(String, String)>) -> Result<()> {
|
||||
// Parse the YAML content directly
|
||||
let recipe: goose::recipe::Recipe =
|
||||
serde_yaml::from_str(recipe_content).context("Failed to parse recipe YAML")?;
|
||||
|
||||
// Build the prompt from recipe template and parameters
|
||||
let prompt = if let Some(mut template) = recipe.prompt.clone() {
|
||||
for (key, value) in ¶ms {
|
||||
let placeholder = format!("{{{{ {} }}}}", key);
|
||||
template = template.replace(&placeholder, value);
|
||||
}
|
||||
template
|
||||
} else {
|
||||
return Err(anyhow::anyhow!("Recipe does not have a prompt template"));
|
||||
};
|
||||
|
||||
// Convert recipe Settings to SessionSettings
|
||||
let session_settings = recipe
|
||||
.settings
|
||||
.clone()
|
||||
.map(|s| crate::session::SessionSettings {
|
||||
goose_provider: s.goose_provider,
|
||||
goose_model: s.goose_model,
|
||||
temperature: s.temperature,
|
||||
});
|
||||
|
||||
// Create input config from the recipe
|
||||
let input_config = crate::cli::InputConfig {
|
||||
contents: Some(prompt),
|
||||
extensions_override: recipe.extensions.clone(),
|
||||
additional_system_prompt: None,
|
||||
};
|
||||
|
||||
// Create recipe info from the recipe
|
||||
let recipe_info = crate::cli::RecipeInfo {
|
||||
session_settings,
|
||||
sub_recipes: recipe.sub_recipes.clone(),
|
||||
final_output_response: recipe.response.clone(),
|
||||
retry_config: recipe.retry.clone(),
|
||||
};
|
||||
|
||||
// Build and run the session
|
||||
let mut session = build_session(SessionBuilderConfig {
|
||||
@@ -512,7 +605,7 @@ async fn run_recipe(recipe_path: std::path::PathBuf, params: Vec<(String, String
|
||||
}
|
||||
|
||||
pub async fn run(args: SwarmArgs) -> Result<()> {
|
||||
let worker_id = args.worker_id.unwrap_or_else(generate_worker_id);
|
||||
let worker_id = get_or_create_worker_id(args.worker_id)?;
|
||||
|
||||
println!("🐝 Starting Goose Swarm");
|
||||
println!("📍 Repository: {}", args.repo);
|
||||
|
||||
Reference in New Issue
Block a user