Skip to content
Open
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
17 changes: 15 additions & 2 deletions src/commands/help.rs
Original file line number Diff line number Diff line change
Expand Up @@ -199,7 +199,7 @@ const LIST_HELP: &[HelpEntry] = &[
const SEND_HELP: &[HelpEntry] = &[
(" send @name -- message text", "Direct message"),
(" send @name1 @name2 -- message", "Multiple targets"),
(" send -- message text", "Broadcast to all"),
(" send --broadcast -- message", "Broadcast to all"),
(" send @name", "Message from stdin (pipe or heredoc)"),
(" send @name --file <path>", "Message from file"),
(
Expand All @@ -210,6 +210,16 @@ const SEND_HELP: &[HelpEntry] = &[
("", "Everything after -- is the message (no quotes needed)."),
("", "All flags must come before --."),
("", ""),
(
" --broadcast",
"Confirm broadcast to everyone (no @target)",
),
(
"",
" Required inside AI tool sessions; without it a no-@target",
),
("", " send is rejected and prints a preview instead."),
("", ""),
("Target matching:", ""),
(" @luna", "exact base name"),
(" @api-luna", "exact full name"),
Expand Down Expand Up @@ -260,7 +270,10 @@ const SEND_HELP: &[HelpEntry] = &[
" hcom send @luna @nova --intent request -- Can you help?",
"",
),
(" hcom send -- Broadcast message to everyone", ""),
(
" hcom send --broadcast -- Broadcast message to everyone",
"",
),
(" echo 'Complex message' | hcom send @luna", ""),
(" hcom send @luna <<'EOF'", ""),
(" Multi-line message with special chars", ""),
Expand Down
25 changes: 21 additions & 4 deletions src/commands/send.rs
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,11 @@ pub struct SendArgs {
#[arg(long)]
pub quiet: bool,

/// Explicitly confirm broadcasting to everyone (required for no-@target
/// sends from inside AI tools; --go no longer authorizes broadcasts)
#[arg(long)]
pub broadcast: bool,

// ── Inline bundle ──
/// Bundle title (creates inline bundle)
#[arg(long)]
Expand Down Expand Up @@ -368,8 +373,8 @@ fn print_broadcast_preview(db: &HcomDb, delivered_to: &[String]) {
println!("Recipients:\n {recipient_list}\n");
println!("Did you mean to send this to everyone?");
println!("Broadcasts can wake many terminals and spend many agents' context/tools.");
println!("\nAdd --go after send and run again to proceed:");
println!(" hcom send --go ...\n");
println!("\nIf you really mean everyone, add --broadcast and run again:");
println!(" hcom send --broadcast ...\n");
}

///
Expand Down Expand Up @@ -897,11 +902,14 @@ pub fn cmd_send(db: &HcomDb, args: &SendArgs, ctx: Option<&CommandContext>) -> i
}
};

// Broadcast from inside an AI tool requires an explicit --broadcast.
// Neither --go nor a small recipient count authorizes it: accidental
// no-@target sends (empty shell var, dropped arg) must fail loudly
// instead of waking every agent.
if is_inside_ai_tool()
&& !ctx.map(|c| c.go).unwrap_or(false)
&& !args.broadcast
&& preview_delivery.original_scope == MessageScope::Broadcast
&& !preview_delivery.is_thread_resolved
&& preview_delivery.delivered_to.len() > 3
{
print_broadcast_preview(db, &preview_delivery.delivered_to);
return 1;
Expand Down Expand Up @@ -1214,6 +1222,15 @@ mod tests {
assert_eq!(args.message, vec!["broadcast", "msg"]);
}

#[test]
fn parse_broadcast_flag_defaults_off() {
// Broadcast authorization is a dedicated opt-in flag, never implied.
let args = SendArgs::try_parse_from(["send", "--", "hello"]).unwrap();
assert!(!args.broadcast);
let args = SendArgs::try_parse_from(["send", "--broadcast", "--", "hello"]).unwrap();
assert!(args.broadcast);
}

#[test]
fn parse_with_intent_flag() {
let args =
Expand Down
58 changes: 50 additions & 8 deletions tests/cli_smoke.rs
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,26 @@ fn help_prints_and_exits_zero() {
);
}

#[test]
fn send_help_documents_broadcast_flag() {
// Regression guard: `hcom send --help` must describe the current
// broadcast mechanism (explicit --broadcast required from inside an AI
// tool session), not the pre-fix "send -- message text" / "-- Broadcast
// message to everyone" wording that no longer matches send.rs behavior.
let h = Hcom::new();
let (code, stdout, stderr) = h.run(["send", "--help"]);
assert_eq!(code, 0, "stdout={stdout} stderr={stderr}");
assert!(stdout.contains("--broadcast"), "stdout={stdout}");
assert!(
!stdout.contains("send -- message text"),
"stale no-flag broadcast usage still present: stdout={stdout}"
);
assert!(
!stdout.contains("hcom send -- Broadcast message to everyone"),
"stale broadcast example still present: stdout={stdout}"
);
}

#[test]
fn status_json_in_fresh_dir() {
let h = Hcom::new();
Expand Down Expand Up @@ -154,10 +174,12 @@ fn send_strips_redundant_trailing_name_from_auto_resolved_sender() {
}

#[test]
fn ai_tool_broadcast_to_many_requires_go_preview() {
fn ai_tool_broadcast_requires_explicit_broadcast_flag() {
let h = Hcom::new();
let sender = h.start();
for _ in 0..4 {
// Keep the recipient count below the old >3 preview threshold so this
// regression proves small accidental broadcasts are gated too.
for _ in 0..2 {
h.start();
}

Expand All @@ -179,9 +201,9 @@ fn ai_tool_broadcast_to_many_requires_go_preview() {
);
assert!(
stdout.contains("BROADCAST SEND PREVIEW")
&& stdout.contains("broadcast to 4 agents")
&& stdout.contains("broadcast to 2 agents")
&& stdout.contains("Did you mean to send this to everyone?")
&& stdout.contains("hcom send --go"),
&& stdout.contains("hcom send --broadcast"),
"stdout={stdout}"
);

Expand All @@ -191,24 +213,44 @@ fn ai_tool_broadcast_to_many_requires_go_preview() {
"preview must not send a message: events={events_out}"
);

// --go must no longer authorize a broadcast (regression guard: accidental
// no-@target sends with a habitual --go used to wake every agent).
let mut go_cmd = h.cmd();
go_cmd.env("CODEX_SANDBOX", "1").args([
"--go",
"send",
"--name",
&sender,
"--",
"confirmed broadcast",
"go does not mean everyone",
]);
let go_out = go_cmd.output().expect("spawn hcom --go send");
let go_code = go_out.status.code().unwrap_or(-1);
let go_stdout = String::from_utf8_lossy(&go_out.stdout);
let go_stderr = String::from_utf8_lossy(&go_out.stderr);
assert_eq!(go_code, 0, "stdout={go_stdout} stderr={go_stderr}");
assert_ne!(go_code, 0, "--go must still preview: stdout={go_stdout}");
assert!(
go_stdout.contains("Sent to:") || go_stdout.contains("Sent to 4 agents"),
go_stdout.contains("BROADCAST SEND PREVIEW"),
"stdout={go_stdout}"
);

let mut bc_cmd = h.cmd();
bc_cmd.env("CODEX_SANDBOX", "1").args([
"send",
"--broadcast",
"--name",
&sender,
"--",
"confirmed broadcast",
]);
let bc_out = bc_cmd.output().expect("spawn hcom send --broadcast");
let bc_code = bc_out.status.code().unwrap_or(-1);
let bc_stdout = String::from_utf8_lossy(&bc_out.stdout);
let bc_stderr = String::from_utf8_lossy(&bc_out.stderr);
assert_eq!(bc_code, 0, "stdout={bc_stdout} stderr={bc_stderr}");
assert!(
bc_stdout.contains("Sent to:") || bc_stdout.contains("Sent to 2 agents"),
"stdout={bc_stdout}"
);
}

#[test]
Expand Down
Loading