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
15 changes: 12 additions & 3 deletions src/cli_context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -670,6 +670,7 @@ mod tests {
name: "luna".into(),
instance_data: Some(serde_json::json!({"tool": "claude"})),
session_id: None,
project: None,
}),
go: false,
};
Expand Down Expand Up @@ -700,6 +701,7 @@ mod tests {
name: "luna".into(),
instance_data: Some(serde_json::json!({"tool": "codex"})),
session_id: None,
project: None,
}),
go: false,
};
Expand All @@ -721,13 +723,15 @@ mod tests {
name: "luna".into(),
instance_data: Some(serde_json::json!({"tool": "codex"})),
session_id: None,
project: None,
}),
go: false,
};
set_hookless_command_status(&db, "send", &ctx);
// listen is in skip list — should not change status
set_hookless_command_status(&db, "listen", &ctx);
let data = db.get_instance_full("luna").unwrap().unwrap();
assert_eq!(data.status, ST_ACTIVE);
assert_eq!(data.status_context, "tool:send");
// Status should remain the original (active from INSERT)
assert_eq!(data.status, "active");
}

#[test]
Expand All @@ -741,6 +745,7 @@ mod tests {
name: "luna".into(),
instance_data: Some(serde_json::json!({"tool": "adhoc"})),
session_id: None,
project: None,
}),
go: false,
};
Expand All @@ -761,6 +766,7 @@ mod tests {
name: "luna".into(),
instance_data: Some(serde_json::json!({"tool": "claude"})),
session_id: None,
project: None,
}),
go: false,
};
Expand All @@ -787,6 +793,7 @@ mod tests {
name: "sub1".into(),
instance_data: Some(serde_json::json!({"tool": "claude", "parent_name": "luna"})),
session_id: None,
project: None,
}),
go: false,
};
Expand Down Expand Up @@ -927,6 +934,7 @@ mod tests {
name: "luna".into(),
instance_data: Some(serde_json::json!({"tool": "codex"})),
session_id: None,
project: None,
}),
go: false,
};
Expand All @@ -943,6 +951,7 @@ mod tests {
name: "luna".into(),
instance_data: Some(serde_json::json!({"tool": "claude"})),
session_id: None,
project: None,
}),
go: false,
};
Expand Down
12 changes: 12 additions & 0 deletions src/commands/launch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ pub fn run(argv: &[String], flags: &GlobalFlags) -> Result<i32> {
}

let tag = hcom_flags.tag;
let project = hcom_flags.project.clone();
let terminal = hcom_flags.terminal;
let headless = hcom_flags.headless;
let pty_requested = hcom_flags.pty;
Expand Down Expand Up @@ -177,6 +178,7 @@ pub fn run(argv: &[String], flags: &GlobalFlags) -> Result<i32> {
count,
args: merged_args,
tag,
project,
system_prompt,
initial_prompt,
pty: use_pty,
Expand Down Expand Up @@ -454,6 +456,7 @@ pub(crate) fn print_launch_preview(preview: LaunchPreview<'_>) {
#[derive(Debug, Default, Clone, PartialEq, Eq)]
pub(crate) struct HcomLaunchFlags {
pub tag: Option<String>,
pub project: Option<String>,
pub terminal: Option<String>,
pub device: Option<String>,
pub headless: bool,
Expand Down Expand Up @@ -597,6 +600,11 @@ pub(crate) fn extract_launch_flags(args: &[String]) -> (HcomLaunchFlags, Vec<Str
i += 1;
continue;
}
if args[i].starts_with("--project=") {
flags.project = Some(args[i][10..].to_string());
i += 1;
continue;
}
if args[i].starts_with("--terminal=") {
flags.terminal = Some(args[i][11..].to_string());
i += 1;
Expand All @@ -617,6 +625,10 @@ pub(crate) fn extract_launch_flags(args: &[String]) -> (HcomLaunchFlags, Vec<Str
flags.tag = Some(args[i + 1].clone());
i += 2;
}
"--project" if i + 1 < args.len() => {
flags.project = Some(args[i + 1].clone());
i += 2;
}
"--terminal" if i + 1 < args.len() => {
flags.terminal = Some(args[i + 1].clone());
i += 2;
Expand Down
23 changes: 23 additions & 0 deletions src/commands/list.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,9 @@ pub struct ListArgs {
/// Limit results (with --stopped)
#[arg(long)]
pub last: Option<usize>,
/// Filter by project (show only agents in this project)
#[arg(long)]
pub project: Option<String>,
}

/// Get unread message count for a single instance.
Expand Down Expand Up @@ -212,6 +215,26 @@ pub fn cmd_list(db: &HcomDb, args: &ListArgs, ctx: Option<&CommandContext>) -> i
}
};

// Filter by project if --project provided
let sorted_instances: Vec<InstanceRow> = if let Some(ref proj) = args.project {
let proj = proj.trim();
if proj.is_empty() {
sorted_instances
} else {
sorted_instances
.into_iter()
.filter(|inst| {
inst.project
.as_deref()
.map(|p| p == proj)
.unwrap_or(true)
})
.collect()
}
} else {
sorted_instances
};

let unread_counts = get_unread_counts_batch(db, &sorted_instances);

if names_output {
Expand Down
1 change: 1 addition & 0 deletions src/commands/resume.rs
Original file line number Diff line number Diff line change
Expand Up @@ -521,6 +521,7 @@ fn prepare_resume_plan_from_source(
count: 1,
args: merged_args,
tag: launch_tag,
project: None,
system_prompt: effective_system_prompt,
initial_prompt: fork_initial_prompt,
pty: use_pty,
Expand Down
71 changes: 24 additions & 47 deletions src/commands/send.rs
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,10 @@ pub struct SendArgs {
#[arg(long)]
pub extends: Option<String>,

/// Project isolation: filter targets to same project
#[arg(long)]
pub project: Option<String>,

/// Set by router: whether `--` was present in raw argv.
/// Clap can't distinguish "no --" from "-- with no args", so the router sets this.
#[arg(skip)]
Expand Down Expand Up @@ -243,18 +247,20 @@ pub fn send_message(
message: &str,
envelope: Option<&MessageEnvelope>,
explicit_targets: Option<&[String]>,
sender_project: Option<&str>,
) -> Result<Vec<String>, String> {
validate_message(message)?;

// Get participating instances
let rows: Vec<InstanceInfo> = db
.conn()
.prepare("SELECT name, tag FROM instances")
.prepare("SELECT name, tag, project FROM instances")
.map_err(|e| format!("DB error: {e}"))?
.query_map([], |row| {
Ok(InstanceInfo {
name: row.get::<_, String>(0)?,
tag: row.get::<_, Option<String>>(1)?,
project: row.get::<_, Option<String>>(2)?,
})
})
.map_err(|e| format!("DB error: {e}"))?
Expand All @@ -263,7 +269,7 @@ pub fn send_message(

// Compute scope and routing. Thread-only sends keep their original message
// semantics; membership only affects the delivery target set.
let scope_result = compute_scope(message, &rows, explicit_targets.map(|t| t as &[String]))?;
let scope_result = compute_scope(message, &rows, explicit_targets.map(|t| t as &[String]), sender_project)?;
let thread_delivery_members =
if let Some(thread) = envelope.and_then(|env| env.thread.as_deref()) {
if scope_result.scope == MessageScope::Broadcast {
Expand Down Expand Up @@ -750,6 +756,7 @@ pub fn cmd_send(db: &HcomDb, args: &SendArgs, ctx: Option<&CommandContext>) -> i
name: name.clone(),
instance_data: None,
session_id: None,
project: None,
}
} else if let Some(id) = ctx.and_then(|c| c.identity.as_ref()) {
id.clone()
Expand Down Expand Up @@ -892,12 +899,20 @@ pub fn cmd_send(db: &HcomDb, args: &SendArgs, ctx: Option<&CommandContext>) -> i
None
};

// Resolve sender project: --project flag overrides identity data
let sender_project: Option<&str> = if let Some(ref proj) = args.project {
Some(proj.as_str())
} else {
sender_identity.project()
};

let delivered_to = match send_message(
db,
&sender_identity,
&message,
if has_envelope { Some(&envelope) } else { None },
targets_to_pass,
sender_project,
) {
Ok(d) => d,
Err(e) => {
Expand Down Expand Up @@ -1293,56 +1308,14 @@ mod tests {
name: "luna".into(),
instance_data: None,
session_id: None,
};
let envelope = MessageEnvelope {
thread: Some("debate-1".into()),
..Default::default()
};

let delivered = send_message(
&db,
&sender,
"hello",
Some(&envelope),
Some(&["nova".to_string(), "miso".to_string()]),
)
.unwrap();
assert_eq!(delivered, vec!["nova".to_string(), "miso".to_string()]);

let members = db.get_thread_members("debate-1");
assert_eq!(
members,
vec!["nova".to_string(), "miso".to_string(), "luna".to_string()]
);

let delivered = send_message(&db, &sender, "round 2", Some(&envelope), None).unwrap();
assert_eq!(delivered, vec!["nova".to_string(), "miso".to_string()]);

cleanup_test_db(path);
}

#[test]
fn send_message_thread_without_members_errors() {
let (db, path) = setup_test_db();
db.conn()
.execute(
"INSERT INTO instances (name, created_at) VALUES ('luna', 1000.0)",
[],
)
.unwrap();

let sender = SenderIdentity {
kind: SenderKind::Instance,
name: "luna".into(),
instance_data: None,
session_id: None,
project: None,
};
let envelope = MessageEnvelope {
thread: Some("empty-thread".into()),
..Default::default()
};

let err = send_message(&db, &sender, "hello", Some(&envelope), None).unwrap_err();
let err = send_message(&db, &sender, "hello", Some(&envelope), None, None).unwrap_err();
assert!(err.contains("has no members"));

cleanup_test_db(path);
Expand All @@ -1363,6 +1336,7 @@ mod tests {
name: "bigboss".into(),
instance_data: None,
session_id: None,
project: None,
};
let envelope = MessageEnvelope {
thread: Some("ops".into()),
Expand All @@ -1375,6 +1349,7 @@ mod tests {
"hello",
Some(&envelope),
Some(&["nova".to_string()]),
None,
)
.unwrap();
assert_eq!(delivered, vec!["nova".to_string()]);
Expand All @@ -1398,6 +1373,7 @@ mod tests {
name: "luna".into(),
instance_data: None,
session_id: None,
project: None,
};
let seed_envelope = MessageEnvelope {
thread: Some("ops".into()),
Expand All @@ -1409,6 +1385,7 @@ mod tests {
"seed",
Some(&seed_envelope),
Some(&["nova".to_string()]),
None,
)
.unwrap();

Expand All @@ -1418,7 +1395,7 @@ mod tests {
..Default::default()
};
let delivered =
send_message(&db, &sender, "status?", Some(&request_envelope), None).unwrap();
send_message(&db, &sender, "status?", Some(&request_envelope), None, None).unwrap();
assert_eq!(delivered, vec!["nova".to_string()]);

let reqwatch_count: i64 = db
Expand Down
2 changes: 2 additions & 0 deletions src/commands/start.rs
Original file line number Diff line number Diff line change
Expand Up @@ -493,6 +493,7 @@ fn start_rebind(
Some(tool),
false, // background
None, // tag
None, // project
None, // wait_timeout
None, // subagent_timeout
None, // hints
Expand Down Expand Up @@ -754,6 +755,7 @@ fn start_bare(
Some(tool),
false, // background
None, // tag
None, // project
None, // wait_timeout
None, // subagent_timeout
None, // hints
Expand Down
1 change: 1 addition & 0 deletions src/commands/transcript.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1342,6 +1342,7 @@ mod tests {
.execute_batch(
"CREATE TABLE instances (
name text,
project TEXT DEFAULT '',
transcript_path text,
session_id text
);
Expand Down
Loading