Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions crates/pu-cli/src/commands/spawn.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ pub async fn run(
agent_args: Option<String>,
plan_mode: bool,
no_trigger: bool,
trigger: Option<String>,
json: bool,
) -> Result<(), CliError> {
daemon_ctrl::ensure_daemon(socket).await?;
Expand Down Expand Up @@ -73,6 +74,7 @@ pub async fn run(
extra_args,
plan_mode,
no_trigger,
trigger,
},
)
.await?;
Expand Down
22 changes: 22 additions & 0 deletions crates/pu-cli/src/commands/trigger.rs
Original file line number Diff line number Diff line change
Expand Up @@ -111,3 +111,25 @@ pub async fn run_delete(
output::print_response(&resp, json)?;
Ok(())
}

pub async fn run_assign(
socket: &Path,
agent_id: &str,
trigger_name: &str,
json: bool,
) -> Result<(), CliError> {
daemon_ctrl::ensure_daemon(socket).await?;
let project_root = commands::cwd_string()?;
let resp = client::send_request(
socket,
&Request::AssignTrigger {
project_root,
agent_id: agent_id.to_string(),
trigger_name: trigger_name.to_string(),
},
)
.await?;
let resp = output::check_response(resp, json)?;
output::print_response(&resp, json)?;
Ok(())
}
21 changes: 20 additions & 1 deletion crates/pu-cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,9 @@ enum Commands {
/// Disable event triggers for this agent
#[arg(long)]
no_trigger: bool,
/// Bind an idle trigger to this agent (name of trigger in .pu/triggers/)
#[arg(long, conflicts_with = "no_trigger")]
trigger: Option<String>,
/// Output as JSON
#[arg(long)]
json: bool,
Expand Down Expand Up @@ -575,6 +578,16 @@ enum TriggerAction {
#[arg(long)]
json: bool,
},
/// Assign a trigger to an idle agent
Assign {
/// Agent ID
agent_id: String,
/// Trigger name
trigger_name: String,
/// Output as JSON
#[arg(long)]
json: bool,
},
}

#[tokio::main]
Expand Down Expand Up @@ -608,11 +621,12 @@ async fn main() {
agent_args,
plan,
no_trigger,
trigger,
json,
} => {
commands::spawn::run(
&socket, prompt, agent, name, base, root, worktree, template, file, command, vars,
no_auto, agent_args, plan, no_trigger, json,
no_auto, agent_args, plan, no_trigger, trigger, json,
)
.await
}
Expand Down Expand Up @@ -771,6 +785,11 @@ async fn main() {
TriggerAction::Delete { name, scope, json } => {
commands::trigger::run_delete(&socket, &name, &scope, json).await
}
TriggerAction::Assign {
agent_id,
trigger_name,
json,
} => commands::trigger::run_assign(&socket, &agent_id, &trigger_name, json).await,
},
Commands::Gate {
event,
Expand Down
12 changes: 12 additions & 0 deletions crates/pu-cli/src/output.rs
Original file line number Diff line number Diff line change
Expand Up @@ -253,6 +253,18 @@ pub fn print_response(response: &Response, json_mode: bool) -> Result<(), CliErr
Response::RenameResult { agent_id, name } => {
println!("Renamed agent {} to {}", agent_id.bold(), name.green());
}
Response::AssignTriggerResult {
agent_id,
trigger_name,
sequence_len,
} => {
println!(
"Assigned trigger {} to agent {} ({} steps)",
trigger_name.green(),
agent_id.bold(),
sequence_len
);
}
Response::CreateWorktreeResult { worktree_id } => {
println!("Created worktree {}", worktree_id.bold());
}
Expand Down
101 changes: 99 additions & 2 deletions crates/pu-core/src/protocol.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ use serde::{Deserialize, Serialize};

use crate::types::{AgentStatus, WorktreeEntry};

pub const PROTOCOL_VERSION: u32 = 4;
pub const PROTOCOL_VERSION: u32 = 5;

/// Serde helper: encode `Vec<u8>` as hex in JSON for binary PTY data.
mod hex_bytes {
Expand Down Expand Up @@ -66,6 +66,8 @@ pub enum Request {
plan_mode: bool,
#[serde(default)]
no_trigger: bool,
#[serde(default)]
trigger: Option<String>,
},
Status {
project_root: String,
Expand Down Expand Up @@ -127,6 +129,11 @@ pub enum Request {
agent_id: String,
name: String,
},
AssignTrigger {
project_root: String,
agent_id: String,
trigger_name: String,
},
CreateWorktree {
project_root: String,
#[serde(default)]
Expand Down Expand Up @@ -598,6 +605,11 @@ pub enum Response {
agent_id: String,
name: String,
},
AssignTriggerResult {
agent_id: String,
trigger_name: String,
sequence_len: u32,
},
CreateWorktreeResult {
worktree_id: String,
},
Expand Down Expand Up @@ -836,6 +848,7 @@ mod tests {
extra_args: vec![],
plan_mode: false,
no_trigger: false,
trigger: None,
};
let json = serde_json::to_string(&req).unwrap();
let parsed: Request = serde_json::from_str(&json).unwrap();
Expand Down Expand Up @@ -886,6 +899,7 @@ mod tests {
extra_args: vec![],
plan_mode: false,
no_trigger: false,
trigger: None,
};
let json = serde_json::to_string(&req).unwrap();
let parsed: Request = serde_json::from_str(&json).unwrap();
Expand Down Expand Up @@ -920,6 +934,7 @@ mod tests {
extra_args: vec!["--model".into(), "opus".into()],
plan_mode: false,
no_trigger: false,
trigger: None,
};
let json = serde_json::to_string(&req).unwrap();
let parsed: Request = serde_json::from_str(&json).unwrap();
Expand All @@ -946,6 +961,7 @@ mod tests {
extra_args: vec![],
plan_mode: true,
no_trigger: false,
trigger: None,
};
let json = serde_json::to_string(&req).unwrap();
let parsed: Request = serde_json::from_str(&json).unwrap();
Expand All @@ -955,6 +971,64 @@ mod tests {
}
}

#[test]
fn given_spawn_request_without_trigger_should_default_to_none() {
let json = r#"{"type":"spawn","project_root":"/test","prompt":"fix bug"}"#;
let req: Request = serde_json::from_str(json).unwrap();
match req {
Request::Spawn { trigger, .. } => assert!(trigger.is_none()),
_ => panic!("expected Spawn"),
}
}

#[test]
fn given_spawn_request_with_trigger_should_round_trip() {
let req = Request::Spawn {
project_root: "/test".into(),
prompt: "fix".into(),
agent: "claude".into(),
name: None,
base: None,
root: true,
worktree: None,
command: None,
no_auto: false,
extra_args: vec![],
plan_mode: false,
no_trigger: false,
trigger: Some("review-bot".into()),
};
let json = serde_json::to_string(&req).unwrap();
let parsed: Request = serde_json::from_str(&json).unwrap();
match parsed {
Request::Spawn { trigger, .. } => assert_eq!(trigger.unwrap(), "review-bot"),
_ => panic!("expected Spawn"),
}
}

#[test]
fn given_assign_trigger_request_should_round_trip() {
let req = Request::AssignTrigger {
project_root: "/test".into(),
agent_id: "ag-abc123".into(),
trigger_name: "review-bot".into(),
};
let json = serde_json::to_string(&req).unwrap();
let parsed: Request = serde_json::from_str(&json).unwrap();
match parsed {
Request::AssignTrigger {
project_root,
agent_id,
trigger_name,
} => {
assert_eq!(project_root, "/test");
assert_eq!(agent_id, "ag-abc123");
assert_eq!(trigger_name, "review-bot");
}
_ => panic!("expected AssignTrigger"),
}
}

#[test]
fn given_status_request_with_agent_id_should_round_trip() {
let req = Request::Status {
Expand Down Expand Up @@ -1262,7 +1336,7 @@ mod tests {

#[test]
fn given_protocol_version_should_be_current() {
assert_eq!(PROTOCOL_VERSION, 4);
assert_eq!(PROTOCOL_VERSION, 5);
}

// --- GridCommand round-trips ---
Expand Down Expand Up @@ -1528,6 +1602,29 @@ mod tests {
}
}

#[test]
fn given_assign_trigger_result_should_round_trip() {
let resp = Response::AssignTriggerResult {
agent_id: "ag-abc".into(),
trigger_name: "review-bot".into(),
sequence_len: 3,
};
let json = serde_json::to_string(&resp).unwrap();
let parsed: Response = serde_json::from_str(&json).unwrap();
match parsed {
Response::AssignTriggerResult {
agent_id,
trigger_name,
sequence_len,
} => {
assert_eq!(agent_id, "ag-abc");
assert_eq!(trigger_name, "review-bot");
assert_eq!(sequence_len, 3);
}
_ => panic!("expected AssignTriggerResult"),
}
}

// --- DeleteWorktree round-trips ---

#[test]
Expand Down
Loading
Loading