huskies: merge 479_story_build_agent_mode_with_crdt_based_work_claiming
This commit is contained in:
+65
-9
@@ -3,6 +3,7 @@
|
||||
#![recursion_limit = "256"]
|
||||
|
||||
mod agent_log;
|
||||
mod agent_mode;
|
||||
mod agents;
|
||||
mod chat;
|
||||
mod config;
|
||||
@@ -46,6 +47,10 @@ struct CliArgs {
|
||||
path: Option<String>,
|
||||
/// Whether the `init` subcommand was given.
|
||||
init: bool,
|
||||
/// Whether the `agent` subcommand was given.
|
||||
agent: bool,
|
||||
/// Rendezvous WebSocket URL for agent mode (e.g. `ws://host:3001/crdt-sync`).
|
||||
rendezvous: Option<String>,
|
||||
}
|
||||
|
||||
/// Parse CLI arguments into `CliArgs`, or exit early for `--help` / `--version`.
|
||||
@@ -53,6 +58,8 @@ fn parse_cli_args(args: &[String]) -> Result<CliArgs, String> {
|
||||
let mut port: Option<u16> = None;
|
||||
let mut path: Option<String> = None;
|
||||
let mut init = false;
|
||||
let mut agent = false;
|
||||
let mut rendezvous: Option<String> = None;
|
||||
let mut i = 0;
|
||||
|
||||
while i < args.len() {
|
||||
@@ -82,9 +89,23 @@ fn parse_cli_args(args: &[String]) -> Result<CliArgs, String> {
|
||||
Err(_) => return Err(format!("invalid port value: '{val}'")),
|
||||
}
|
||||
}
|
||||
"--rendezvous" => {
|
||||
i += 1;
|
||||
if i >= args.len() {
|
||||
return Err("--rendezvous requires a value".to_string());
|
||||
}
|
||||
rendezvous = Some(args[i].clone());
|
||||
}
|
||||
a if a.starts_with("--rendezvous=") => {
|
||||
let val = &a["--rendezvous=".len()..];
|
||||
rendezvous = Some(val.to_string());
|
||||
}
|
||||
"init" => {
|
||||
init = true;
|
||||
}
|
||||
"agent" => {
|
||||
agent = true;
|
||||
}
|
||||
a if a.starts_with('-') => {
|
||||
return Err(format!("unknown option: {a}"));
|
||||
}
|
||||
@@ -98,17 +119,23 @@ fn parse_cli_args(args: &[String]) -> Result<CliArgs, String> {
|
||||
i += 1;
|
||||
}
|
||||
|
||||
Ok(CliArgs { port, path, init })
|
||||
if agent && rendezvous.is_none() {
|
||||
return Err("agent mode requires --rendezvous <URL>".to_string());
|
||||
}
|
||||
|
||||
Ok(CliArgs { port, path, init, agent, rendezvous })
|
||||
}
|
||||
|
||||
fn print_help() {
|
||||
println!("huskies [OPTIONS] [PATH]");
|
||||
println!("huskies init [OPTIONS] [PATH]");
|
||||
println!("huskies agent --rendezvous <URL> [OPTIONS] [PATH]");
|
||||
println!();
|
||||
println!("Serve a huskies project.");
|
||||
println!();
|
||||
println!("COMMANDS:");
|
||||
println!(" init Scaffold a new .huskies/ project and start the interactive setup wizard.");
|
||||
println!(" init Scaffold a new .huskies/ project and start the interactive setup wizard.");
|
||||
println!(" agent Run as a headless build agent — syncs CRDT state, claims and runs work.");
|
||||
println!();
|
||||
println!("ARGS:");
|
||||
println!(
|
||||
@@ -117,9 +144,11 @@ fn print_help() {
|
||||
);
|
||||
println!();
|
||||
println!("OPTIONS:");
|
||||
println!(" -h, --help Print this help and exit");
|
||||
println!(" -V, --version Print the version and exit");
|
||||
println!(" --port <PORT> Port to listen on (default: 3001). Persisted to project.toml.");
|
||||
println!(" -h, --help Print this help and exit");
|
||||
println!(" -V, --version Print the version and exit");
|
||||
println!(" --port <PORT> Port to listen on (default: 3001). Persisted to project.toml.");
|
||||
println!(" --rendezvous <URL> WebSocket URL of the rendezvous peer (agent mode only).");
|
||||
println!(" Example: ws://server:3001/crdt-sync");
|
||||
}
|
||||
|
||||
/// Resolve the optional positional path argument into an absolute `PathBuf`.
|
||||
@@ -169,6 +198,8 @@ async fn main() -> Result<(), std::io::Error> {
|
||||
};
|
||||
|
||||
let is_init = cli.init;
|
||||
let is_agent = cli.agent;
|
||||
let agent_rendezvous = cli.rendezvous.clone();
|
||||
let explicit_path = resolve_path_arg(cli.path.as_deref(), &cwd);
|
||||
|
||||
// Port resolution: CLI flag > project.toml (loaded later) > default.
|
||||
@@ -309,13 +340,36 @@ async fn main() -> Result<(), std::io::Error> {
|
||||
// (CRDT state layer is initialised above alongside the legacy pipeline.db.)
|
||||
|
||||
// Start the CRDT sync rendezvous client if configured in project.toml.
|
||||
if let Some(ref root) = *app_state.project_root.lock().unwrap()
|
||||
&& let Ok(cfg) = config::ProjectConfig::load(root)
|
||||
&& let Some(rendezvous_url) = cfg.rendezvous
|
||||
{
|
||||
// In agent mode, the --rendezvous flag overrides project.toml.
|
||||
let rendezvous_url_for_sync = if is_agent {
|
||||
agent_rendezvous.clone()
|
||||
} else {
|
||||
app_state
|
||||
.project_root
|
||||
.lock()
|
||||
.unwrap()
|
||||
.as_ref()
|
||||
.and_then(|root| config::ProjectConfig::load(root).ok())
|
||||
.and_then(|cfg| cfg.rendezvous)
|
||||
};
|
||||
if let Some(rendezvous_url) = rendezvous_url_for_sync {
|
||||
crdt_sync::spawn_rendezvous_client(rendezvous_url);
|
||||
}
|
||||
|
||||
// ── Agent mode: headless build agent ────────────────────────────────
|
||||
//
|
||||
// When `huskies agent --rendezvous <URL>` is invoked, skip the web UI,
|
||||
// chat bots, and HTTP server entirely. Instead, run a headless loop that:
|
||||
// 1. Syncs CRDT state with the rendezvous peer.
|
||||
// 2. Scans for unclaimed work and claims it via CRDT.
|
||||
// 3. Runs Claude Code locally for claimed stories.
|
||||
// 4. Pushes feature branches and reports completion via CRDT.
|
||||
if is_agent {
|
||||
let agent_root = app_state.project_root.lock().unwrap().clone();
|
||||
let rendezvous = agent_rendezvous.expect("agent mode requires --rendezvous");
|
||||
return agent_mode::run(agent_root, rendezvous, port).await;
|
||||
}
|
||||
|
||||
let workflow = Arc::new(std::sync::Mutex::new(WorkflowState::default()));
|
||||
|
||||
// Event bus: broadcast channel for pipeline lifecycle events.
|
||||
@@ -886,6 +940,8 @@ name = "coder"
|
||||
assert_eq!(result.port, None);
|
||||
assert_eq!(result.path, None);
|
||||
assert!(!result.init);
|
||||
assert!(!result.agent);
|
||||
assert_eq!(result.rendezvous, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user