@team-agent/installer 0.5.89 → 0.5.92
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Cargo.lock +1 -1
- package/Cargo.toml +1 -1
- package/crates/team-agent/src/cli/adapters.rs +145 -172
- package/crates/team-agent/src/cli/diagnose.rs +78 -39
- package/crates/team-agent/src/cli/emit.rs +12 -75
- package/crates/team-agent/src/cli/mod.rs +38 -10
- package/crates/team-agent/src/cli/spec.rs +1 -4
- package/crates/team-agent/src/cli/status_port/inbox.rs +42 -17
- package/crates/team-agent/src/cli/tests/lane_c.rs +34 -26
- package/crates/team-agent/src/cli/tests/main_preserved.rs +4 -70
- package/crates/team-agent/src/cli/tests/missing_subcommands.rs +0 -68
- package/crates/team-agent/src/cli/triage.rs +183 -0
- package/crates/team-agent/src/cli/types.rs +3 -16
- package/crates/team-agent/src/coordinator/health.rs +41 -1
- package/crates/team-agent/src/coordinator/tests/tick_core.rs +5 -6
- package/crates/team-agent/src/coordinator/tick.rs +1 -16
- package/crates/team-agent/src/coordinator/types.rs +1 -1
- package/crates/team-agent/src/db/message_store.rs +10 -5
- package/crates/team-agent/src/db/migration.rs +36 -15
- package/crates/team-agent/src/fake_worker.rs +4 -4
- package/crates/team-agent/src/lifecycle/tests/harvest2_a_batch_red.rs +2 -2
- package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +0 -10
- package/crates/team-agent/src/lifecycle/tests/phase_golden.rs +2 -9
- package/crates/team-agent/src/lifecycle/tests/upgrade_compat_0211_red.rs +1 -26
- package/crates/team-agent/src/main.rs +7 -2
- package/crates/team-agent/src/mcp_server/mod.rs +1 -1
- package/crates/team-agent/src/messaging/mod.rs +2 -2
- package/crates/team-agent/src/messaging/results.rs +234 -386
- package/crates/team-agent/src/messaging/tests/runtime.rs +91 -135
- package/crates/team-agent/src/messaging/watchers.rs +1 -1
- package/crates/team-agent/src/redaction.rs +5 -1
- package/crates/team-agent/src/topology.rs +17 -11
- package/package.json +4 -4
- package/skills/team-agent/SKILL.md +3 -3
- package/skills/team-agent/command-coverage.json +2 -9
- package/skills/team-agent/references/team-agent-operator.md +661 -0
|
@@ -223,10 +223,6 @@ fn dispatch(command: &str, args: &[String], cwd: &Path) -> Result<ExitCode, CliE
|
|
|
223
223
|
"validate" => cmd_validate(&validate_args(args, cwd)).map(emit_result),
|
|
224
224
|
"install-skill" => cmd_install_skill(&install_skill_args(args)?).map(emit_result),
|
|
225
225
|
"profile" => cmd_profile(&profile_args(args, cwd)?).map(emit_result),
|
|
226
|
-
"collect" => {
|
|
227
|
-
cmd_collect_for_team(&collect_args(args, cwd)?, parse_args(args).team.as_deref())
|
|
228
|
-
.map(emit_result)
|
|
229
|
-
}
|
|
230
226
|
"results" => cmd_results(&results_args(args, cwd)?).map(emit_result),
|
|
231
227
|
"wait" => cmd_wait(&wait_args(args, cwd)?).map(emit_result),
|
|
232
228
|
"diagnose" => cmd_diagnose(&diagnose_args(args, cwd)).map(emit_result),
|
|
@@ -275,7 +271,6 @@ const DISPATCH_COMMANDS: &[&str] = &[
|
|
|
275
271
|
"validate",
|
|
276
272
|
"install-skill",
|
|
277
273
|
"profile",
|
|
278
|
-
"collect",
|
|
279
274
|
"results",
|
|
280
275
|
"wait",
|
|
281
276
|
"diagnose",
|
|
@@ -328,7 +323,6 @@ pub(crate) fn default_help() -> String {
|
|
|
328
323
|
"quick-start",
|
|
329
324
|
"send",
|
|
330
325
|
"status",
|
|
331
|
-
"collect",
|
|
332
326
|
"results",
|
|
333
327
|
"models",
|
|
334
328
|
"leaders",
|
|
@@ -441,14 +435,13 @@ fn command_help(command: Option<&str>) -> String {
|
|
|
441
435
|
Some("attach-app-server-leader") => "usage: team-agent attach-app-server-leader [--workspace WORKSPACE] [--team TEAM] --socket unix:///path.sock --thread-id THREAD_ID [--json]".to_string(),
|
|
442
436
|
Some("identity") => "usage: team-agent identity [--workspace WORKSPACE] [--team TEAM] [--json]".to_string(),
|
|
443
437
|
Some("approvals") => "usage: team-agent approvals [AGENT] [--workspace WORKSPACE] [--team TEAM] [--json]".to_string(),
|
|
444
|
-
Some("inbox") => "usage: team-agent inbox AGENT [
|
|
438
|
+
Some("inbox") => "usage: team-agent inbox AGENT [-n N|--limit N] [--workspace WORKSPACE] [--team TEAM] [--json]".to_string(),
|
|
445
439
|
Some("doctor") => "usage: team-agent doctor [SPEC] [--workspace WORKSPACE] [--team TEAM] [--gate orphans|comms] [--comms] [--fix] [--fix-schema] [--cleanup-orphans] [--confirm] [--json]".to_string(),
|
|
446
440
|
Some("watch") => "usage: team-agent watch [--workspace WORKSPACE] [--team TEAM]".to_string(),
|
|
447
441
|
Some("sessions") => "usage: team-agent sessions [--workspace WORKSPACE] [--team TEAM] [--json]".to_string(),
|
|
448
442
|
Some("validate") => "usage: team-agent validate [SPEC] [--json]".to_string(),
|
|
449
443
|
Some("install-skill") => "usage: team-agent install-skill (--source DIR | --uninstall) [--target codex|claude|copilot|all] [--dest DIR] [--dry-run] [--json]".to_string(),
|
|
450
444
|
Some("profile") => "usage: team-agent profile COMMAND NAME [--workspace WORKSPACE] [--team TEAM] [--auth-mode MODE] [--proxy-mode direct|inherit] [--json]".to_string(),
|
|
451
|
-
Some("collect") => "usage: team-agent collect [--workspace WORKSPACE] [--team TEAM] [--result-file FILE] [--json]".to_string(),
|
|
452
445
|
Some("results") => "usage: team-agent results --case CASE_ID [--workspace WORKSPACE] [--team TEAM] [--json]".to_string(),
|
|
453
446
|
Some("wait") => "usage: team-agent wait --task TASK [--workspace WORKSPACE] [--json]".to_string(),
|
|
454
447
|
Some("diagnose") => "usage: team-agent diagnose [--workspace WORKSPACE] [--team TEAM] [--json]".to_string(),
|
|
@@ -842,7 +835,6 @@ struct ParsedArgs {
|
|
|
842
835
|
confirm: bool,
|
|
843
836
|
alert_type: Option<String>,
|
|
844
837
|
limit: Option<usize>,
|
|
845
|
-
since: Option<String>,
|
|
846
838
|
gate: Option<String>,
|
|
847
839
|
comms: bool,
|
|
848
840
|
fix: bool,
|
|
@@ -856,7 +848,6 @@ struct ParsedArgs {
|
|
|
856
848
|
tail: Option<usize>,
|
|
857
849
|
head: Option<usize>,
|
|
858
850
|
search: Option<String>,
|
|
859
|
-
result_file: Option<PathBuf>,
|
|
860
851
|
file: Option<PathBuf>,
|
|
861
852
|
result: Option<String>,
|
|
862
853
|
real: bool,
|
|
@@ -936,10 +927,9 @@ fn parse_args(args: &[String]) -> ParsedArgs {
|
|
|
936
927
|
"--from-spec" => parsed.from_spec = true,
|
|
937
928
|
"--confirm" => parsed.confirm = true,
|
|
938
929
|
"--alert-type" => parsed.alert_type = next_arg(args, &mut i),
|
|
939
|
-
"--limit" => {
|
|
930
|
+
"-n" | "--limit" => {
|
|
940
931
|
parsed.limit = next_arg(args, &mut i).and_then(|v| v.parse::<usize>().ok())
|
|
941
932
|
}
|
|
942
|
-
"--since" => parsed.since = next_arg(args, &mut i),
|
|
943
933
|
"--gate" => parsed.gate = next_arg(args, &mut i),
|
|
944
934
|
"--comms" => parsed.comms = true,
|
|
945
935
|
"--fix" => parsed.fix = true,
|
|
@@ -955,7 +945,6 @@ fn parse_args(args: &[String]) -> ParsedArgs {
|
|
|
955
945
|
"--tail" => parsed.tail = next_arg(args, &mut i).and_then(|v| v.parse::<usize>().ok()),
|
|
956
946
|
"--head" => parsed.head = next_arg(args, &mut i).and_then(|v| v.parse::<usize>().ok()),
|
|
957
947
|
"--search" => parsed.search = next_arg(args, &mut i),
|
|
958
|
-
"--result-file" => parsed.result_file = next_arg(args, &mut i).map(PathBuf::from),
|
|
959
948
|
"--file" => parsed.file = next_arg(args, &mut i).map(PathBuf::from),
|
|
960
949
|
"--result" => parsed.result = next_arg(args, &mut i),
|
|
961
950
|
"--real" => parsed.real = true,
|
|
@@ -1438,12 +1427,19 @@ fn approvals_args(args: &[String], cwd: &Path) -> ApprovalsArgs {
|
|
|
1438
1427
|
}
|
|
1439
1428
|
|
|
1440
1429
|
fn inbox_args(args: &[String], cwd: &Path) -> Result<InboxArgs, CliError> {
|
|
1430
|
+
if args
|
|
1431
|
+
.iter()
|
|
1432
|
+
.any(|arg| arg == "--since" || arg.starts_with("--since="))
|
|
1433
|
+
{
|
|
1434
|
+
return Err(CliError::Usage(
|
|
1435
|
+
"inbox only supports -n/--limit; --since is not supported".to_string(),
|
|
1436
|
+
));
|
|
1437
|
+
}
|
|
1441
1438
|
let parsed = parse_args(args);
|
|
1442
1439
|
Ok(InboxArgs {
|
|
1443
1440
|
agent: required_pos(&parsed, 0, "agent")?,
|
|
1444
1441
|
workspace: workspace(&parsed, cwd),
|
|
1445
|
-
limit: parsed.limit.unwrap_or(
|
|
1446
|
-
since: parsed.since,
|
|
1442
|
+
limit: parsed.limit.unwrap_or(3),
|
|
1447
1443
|
json: parsed.json,
|
|
1448
1444
|
team: parsed.team,
|
|
1449
1445
|
})
|
|
@@ -1848,18 +1844,6 @@ fn profile_args(args: &[String], cwd: &Path) -> Result<ProfileArgs, CliError> {
|
|
|
1848
1844
|
})
|
|
1849
1845
|
}
|
|
1850
1846
|
|
|
1851
|
-
fn collect_args(args: &[String], cwd: &Path) -> Result<CollectArgs, CliError> {
|
|
1852
|
-
let parsed = parse_args(args);
|
|
1853
|
-
let workspace = workspace(&parsed, cwd);
|
|
1854
|
-
refuse_if_multi_alive_team_missing_scope("collect", &workspace, parsed.team.as_deref())?;
|
|
1855
|
-
Ok(CollectArgs {
|
|
1856
|
-
workspace,
|
|
1857
|
-
result_file: parsed.result_file,
|
|
1858
|
-
json: parsed.json,
|
|
1859
|
-
team: parsed.team,
|
|
1860
|
-
})
|
|
1861
|
-
}
|
|
1862
|
-
|
|
1863
1847
|
fn results_args(args: &[String], cwd: &Path) -> Result<ResultsArgs, CliError> {
|
|
1864
1848
|
if args
|
|
1865
1849
|
.iter()
|
|
@@ -2349,15 +2333,11 @@ mod tests {
|
|
|
2349
2333
|
"--json",
|
|
2350
2334
|
][..],
|
|
2351
2335
|
),
|
|
2352
|
-
(
|
|
2353
|
-
"collect",
|
|
2354
|
-
&["--workspace", "--team", "--result-file", "--json"][..],
|
|
2355
|
-
),
|
|
2356
2336
|
("stuck-list", &["--workspace", "--team", "--json"][..]),
|
|
2357
2337
|
("approvals", &["--workspace", "--team", "--json"][..]),
|
|
2358
2338
|
(
|
|
2359
2339
|
"inbox",
|
|
2360
|
-
&["--workspace", "--team", "
|
|
2340
|
+
&["--workspace", "--team", "-n", "--limit", "--json"][..],
|
|
2361
2341
|
),
|
|
2362
2342
|
("sessions", &["--workspace", "--team", "--json"][..]),
|
|
2363
2343
|
("diagnose", &["--workspace", "--team", "--json"][..]),
|
|
@@ -2665,37 +2645,6 @@ mod tests {
|
|
|
2665
2645
|
);
|
|
2666
2646
|
}
|
|
2667
2647
|
|
|
2668
|
-
#[test]
|
|
2669
|
-
fn refuse_helper_passes_when_explicit_team_provided_even_in_multi_alive() {
|
|
2670
|
-
let ws = tmp_workspace();
|
|
2671
|
-
seed_two_alive_teams_in(&ws);
|
|
2672
|
-
assert!(
|
|
2673
|
-
refuse_if_multi_alive_team_missing_scope("collect", &ws, Some("alpha")).is_ok(),
|
|
2674
|
-
"explicit --team alpha must bypass the ambiguity gate"
|
|
2675
|
-
);
|
|
2676
|
-
}
|
|
2677
|
-
|
|
2678
|
-
#[test]
|
|
2679
|
-
fn refuse_helper_refuses_when_multi_alive_team_and_no_explicit_team() {
|
|
2680
|
-
let ws = tmp_workspace();
|
|
2681
|
-
seed_two_alive_teams_in(&ws);
|
|
2682
|
-
let err = refuse_if_multi_alive_team_missing_scope("collect", &ws, None)
|
|
2683
|
-
.expect_err("multi-alive-team must refuse without --team");
|
|
2684
|
-
let message = err.to_string();
|
|
2685
|
-
assert!(
|
|
2686
|
-
message.contains("multiple alive teams"),
|
|
2687
|
-
"refusal must name the ambiguity; got: {message}"
|
|
2688
|
-
);
|
|
2689
|
-
assert!(
|
|
2690
|
-
message.contains("alpha") && message.contains("beta"),
|
|
2691
|
-
"refusal must list candidate teams; got: {message}"
|
|
2692
|
-
);
|
|
2693
|
-
assert!(
|
|
2694
|
-
message.contains("collect"),
|
|
2695
|
-
"refusal must name the command for diagnostic clarity; got: {message}"
|
|
2696
|
-
);
|
|
2697
|
-
}
|
|
2698
|
-
|
|
2699
2648
|
#[test]
|
|
2700
2649
|
fn stuck_cancel_args_builder_refuses_on_multi_alive_team() {
|
|
2701
2650
|
let ws = tmp_workspace();
|
|
@@ -2708,18 +2657,6 @@ mod tests {
|
|
|
2708
2657
|
);
|
|
2709
2658
|
}
|
|
2710
2659
|
|
|
2711
|
-
#[test]
|
|
2712
|
-
fn collect_args_builder_refuses_on_multi_alive_team() {
|
|
2713
|
-
let ws = tmp_workspace();
|
|
2714
|
-
seed_two_alive_teams_in(&ws);
|
|
2715
|
-
let argv = cli_argv(&["--workspace", &ws.to_string_lossy()]);
|
|
2716
|
-
let err = collect_args(&argv, &ws).expect_err("must refuse");
|
|
2717
|
-
assert!(
|
|
2718
|
-
err.to_string().contains("multiple alive teams"),
|
|
2719
|
-
"collect args builder must surface the refusal; got: {err}"
|
|
2720
|
-
);
|
|
2721
|
-
}
|
|
2722
|
-
|
|
2723
2660
|
#[test]
|
|
2724
2661
|
fn shutdown_args_builder_refuses_on_multi_alive_team() {
|
|
2725
2662
|
let ws = tmp_workspace();
|
|
@@ -51,7 +51,7 @@ use crate::model::ids::{TaskId, TeamKey};
|
|
|
51
51
|
pub(crate) const COMMS_BOUNDARY_TEXT: &str = "validates live pane binding consistency and zero-token comms contracts. Does NOT perform live runtime message round-trip. (zero token, zero pollution)";
|
|
52
52
|
pub(crate) const QUICK_START_REMINDER: &str = "Reminder: Do not inspect raw worker terminal output during normal operation. Use team-agent status / inbox / collect instead. Wait for report_result.";
|
|
53
53
|
pub(crate) const SEND_REMINDER: &str = "Message delivered. Wait for the worker to report_result. Do not poll the worker terminal with capture-pane.";
|
|
54
|
-
pub(crate) const STATUS_REMINDER: &str = "
|
|
54
|
+
pub(crate) const STATUS_REMINDER: &str = "Results are finalized automatically; use --watch-result or team-agent inbox AGENT -n 3 for transport fallback. Do not capture-pane worker terminals.";
|
|
55
55
|
|
|
56
56
|
pub mod adapters;
|
|
57
57
|
pub mod attach_app_server_leader;
|
|
@@ -66,6 +66,7 @@ pub mod profile;
|
|
|
66
66
|
pub mod send;
|
|
67
67
|
pub(crate) mod spec;
|
|
68
68
|
pub mod status;
|
|
69
|
+
pub(crate) mod triage;
|
|
69
70
|
pub mod types;
|
|
70
71
|
|
|
71
72
|
pub use adapters::*;
|
|
@@ -5050,17 +5051,44 @@ pub mod diagnose_port {
|
|
|
5050
5051
|
return;
|
|
5051
5052
|
}
|
|
5052
5053
|
for (idx, line) in text.lines().enumerate() {
|
|
5053
|
-
|
|
5054
|
-
|
|
5055
|
-
|
|
5056
|
-
|
|
5057
|
-
|
|
5058
|
-
|
|
5059
|
-
|
|
5060
|
-
|
|
5061
|
-
|
|
5054
|
+
let Some(key) = secret_assignment_key(line) else {
|
|
5055
|
+
continue;
|
|
5056
|
+
};
|
|
5057
|
+
let rel = path.strip_prefix(root).unwrap_or(path);
|
|
5058
|
+
findings.push(json!({
|
|
5059
|
+
"path": rel.to_string_lossy().to_string(),
|
|
5060
|
+
"line": idx.saturating_add(1),
|
|
5061
|
+
"rule": "api_key_assignment",
|
|
5062
|
+
"match_excerpt": format!("{key}=<redacted>"),
|
|
5063
|
+
}));
|
|
5062
5064
|
}
|
|
5063
5065
|
}
|
|
5066
|
+
|
|
5067
|
+
fn secret_assignment_key(line: &str) -> Option<&'static str> {
|
|
5068
|
+
let line = line.trim_start();
|
|
5069
|
+
let line = match line.strip_prefix("export") {
|
|
5070
|
+
Some(rest) if rest.chars().next().is_some_and(char::is_whitespace) => rest.trim_start(),
|
|
5071
|
+
_ => line,
|
|
5072
|
+
};
|
|
5073
|
+
["OPENAI_API_KEY", "ANTHROPIC_API_KEY"]
|
|
5074
|
+
.into_iter()
|
|
5075
|
+
.find(|key| {
|
|
5076
|
+
line.strip_prefix(*key)
|
|
5077
|
+
.and_then(|rest| rest.strip_prefix('='))
|
|
5078
|
+
.is_some_and(|rhs| {
|
|
5079
|
+
let rhs = rhs.trim();
|
|
5080
|
+
let rhs = rhs
|
|
5081
|
+
.strip_prefix('"')
|
|
5082
|
+
.and_then(|value| value.strip_suffix('"'))
|
|
5083
|
+
.or_else(|| {
|
|
5084
|
+
rhs.strip_prefix('\'')
|
|
5085
|
+
.and_then(|value| value.strip_suffix('\''))
|
|
5086
|
+
})
|
|
5087
|
+
.unwrap_or(rhs);
|
|
5088
|
+
!rhs.is_empty()
|
|
5089
|
+
})
|
|
5090
|
+
})
|
|
5091
|
+
}
|
|
5064
5092
|
///
|
|
5065
5093
|
/// `run_comms_selftest`(`--comms`/`--gate comms`)。**纯 state-read,零 token**(MUST-NOT-13)。
|
|
5066
5094
|
#[cfg(test)]
|
|
@@ -76,7 +76,6 @@ pub(crate) enum DispatchKind {
|
|
|
76
76
|
Validate,
|
|
77
77
|
InstallSkill,
|
|
78
78
|
Profile,
|
|
79
|
-
Collect,
|
|
80
79
|
Results,
|
|
81
80
|
Wait,
|
|
82
81
|
Diagnose,
|
|
@@ -123,7 +122,6 @@ pub(crate) const ALL_DISPATCH_KINDS: &[DispatchKind] = &[
|
|
|
123
122
|
DispatchKind::Validate,
|
|
124
123
|
DispatchKind::InstallSkill,
|
|
125
124
|
DispatchKind::Profile,
|
|
126
|
-
DispatchKind::Collect,
|
|
127
125
|
DispatchKind::Results,
|
|
128
126
|
DispatchKind::Wait,
|
|
129
127
|
DispatchKind::Diagnose,
|
|
@@ -170,7 +168,6 @@ pub(crate) const COMMAND_SPECS: &[CommandSpec] = &[
|
|
|
170
168
|
CommandSpec { name: "send", tier: CommandTier::Core, category: CommandCategory::Daily, kind: CommandKind::Dispatch(DispatchKind::Send), summary: "persist a message for an in-team short name or fully-qualified logical recipient", usage: "usage: team-agent send TO MESSAGE... [--workspace WORKSPACE] [--team TEAM] [--mailbox] [--json]\nTO forms are co-equal: an in-team short name (for example `team-agent send reviewer \"Review\"`) or `<workspace>::<team>/<agent>`.", default_help: true, command_help: true, suggestion_index: true, token_usage: TokenUsage::Conditional, alias_of: None, sunset: Some("next compatibility release"), action: Some("use positional logical TO and the returned message id"), governance: None },
|
|
171
169
|
CommandSpec { name: "status", tier: CommandTier::Core, category: CommandCategory::Daily, kind: CommandKind::Dispatch(DispatchKind::Status), summary: "show current team status", usage: "usage: team-agent status [AGENT] [--workspace WORKSPACE] [--team TEAM] [--summary|--json] [--detail]", default_help: true, command_help: true, suggestion_index: true, token_usage: TokenUsage::No, alias_of: None, sunset: None, action: None, governance: None },
|
|
172
170
|
CommandSpec { name: "models", tier: CommandTier::Core, category: CommandCategory::Observe, kind: CommandKind::Dispatch(DispatchKind::Models), summary: "list exact provider model ids", usage: "usage: team-agent models [--provider pi|cursor_agent] [QUERY|--search TEXT] [--json]", default_help: true, command_help: true, suggestion_index: true, token_usage: TokenUsage::No, alias_of: None, sunset: None, action: None, governance: None },
|
|
173
|
-
CommandSpec { name: "collect", tier: CommandTier::Core, category: CommandCategory::Daily, kind: CommandKind::Dispatch(DispatchKind::Collect), summary: "collect reported results", usage: "usage: team-agent collect [--workspace WORKSPACE] [--team TEAM] [--result-file FILE] [--json]", default_help: true, command_help: true, suggestion_index: true, token_usage: TokenUsage::No, alias_of: None, sunset: None, action: None, governance: None },
|
|
174
171
|
CommandSpec { name: "results", tier: CommandTier::Core, category: CommandCategory::Daily, kind: CommandKind::Dispatch(DispatchKind::Results), summary: "read reported results for a case", usage: "usage: team-agent results --case CASE_ID [--workspace WORKSPACE] [--team TEAM] [--json]", default_help: true, command_help: true, suggestion_index: true, token_usage: TokenUsage::No, alias_of: None, sunset: None, action: None, governance: None },
|
|
175
172
|
CommandSpec { name: "wait", tier: CommandTier::Core, category: CommandCategory::Daily, kind: CommandKind::Dispatch(DispatchKind::Wait), summary: "block until a task result is stored", usage: "usage: team-agent wait --task TASK [--workspace WORKSPACE] [--json]", default_help: false, command_help: true, suggestion_index: true, token_usage: TokenUsage::No, alias_of: None, sunset: None, action: None, governance: None },
|
|
176
173
|
CommandSpec { name: "restart", tier: CommandTier::Core, category: CommandCategory::TeamLifecycle, kind: CommandKind::Dispatch(DispatchKind::Restart), summary: "restart the selected team", usage: "usage: team-agent restart [WORKSPACE] [--team TEAM] [--allow-fresh] [--session-converge-deadline SECONDS] [--json]", default_help: true, command_help: true, suggestion_index: true, token_usage: TokenUsage::Conditional, alias_of: None, sunset: None, action: None, governance: None },
|
|
@@ -208,7 +205,7 @@ pub(crate) const COMMAND_SPECS: &[CommandSpec] = &[
|
|
|
208
205
|
CommandSpec { name: "approvals", tier: CommandTier::Secondary, category: CommandCategory::Observe, kind: CommandKind::Dispatch(DispatchKind::Approvals), summary: "inspect provider approval state", usage: "usage: team-agent approvals [AGENT] [--workspace WORKSPACE] [--team TEAM] [--json]", default_help: false, command_help: true, suggestion_index: false, token_usage: TokenUsage::No, alias_of: None, sunset: None, action: None, governance: Some(GovernanceNote { decision: "secondary", reason: "approval diagnostics" }) },
|
|
209
206
|
CommandSpec { name: "profile", tier: CommandTier::Secondary, category: CommandCategory::Setup, kind: CommandKind::Dispatch(DispatchKind::Profile), summary: "manage provider profiles", usage: "usage: team-agent profile COMMAND NAME [--workspace WORKSPACE] [--team TEAM] [--auth-mode MODE] [--proxy-mode direct|inherit] [--json]", default_help: false, command_help: true, suggestion_index: false, token_usage: TokenUsage::No, alias_of: None, sunset: None, action: None, governance: Some(GovernanceNote { decision: "secondary", reason: "provider/profile setup" }) },
|
|
210
207
|
CommandSpec { name: "install-skill", tier: CommandTier::Secondary, category: CommandCategory::Setup, kind: CommandKind::Dispatch(DispatchKind::InstallSkill), summary: "install or uninstall Team Agent skills", usage: "usage: team-agent install-skill (--source DIR | --uninstall) [--target codex|claude|copilot|all] [--dest DIR] [--dry-run] [--json]", default_help: false, command_help: true, suggestion_index: false, token_usage: TokenUsage::No, alias_of: None, sunset: None, action: None, governance: Some(GovernanceNote { decision: "secondary", reason: "capability setup" }) },
|
|
211
|
-
CommandSpec { name: "inbox", tier: CommandTier::Secondary, category: CommandCategory::Observe, kind: CommandKind::Dispatch(DispatchKind::Inbox), summary: "inspect
|
|
208
|
+
CommandSpec { name: "inbox", tier: CommandTier::Secondary, category: CommandCategory::Observe, kind: CommandKind::Dispatch(DispatchKind::Inbox), summary: "inspect recent message summaries", usage: "usage: team-agent inbox AGENT [-n N|--limit N] [--workspace WORKSPACE] [--team TEAM] [--json]", default_help: false, command_help: true, suggestion_index: false, token_usage: TokenUsage::No, alias_of: None, sunset: None, action: None, governance: None },
|
|
212
209
|
|
|
213
210
|
CommandSpec { name: "identity", tier: CommandTier::DevInternal, category: CommandCategory::Dev, kind: CommandKind::Dispatch(DispatchKind::Identity), summary: "inspect identity metadata", usage: "usage: team-agent identity [--workspace WORKSPACE] [--team TEAM] [--json]", default_help: false, command_help: true, suggestion_index: false, token_usage: TokenUsage::No, alias_of: None, sunset: None, action: None, governance: None },
|
|
214
211
|
CommandSpec { name: "watch", tier: CommandTier::DevInternal, category: CommandCategory::Dev, kind: CommandKind::Dispatch(DispatchKind::Watch), summary: "watch event logs", usage: "usage: team-agent watch [--workspace WORKSPACE] [--team TEAM]", default_help: false, command_help: true, suggestion_index: false, token_usage: TokenUsage::No, alias_of: None, sunset: None, action: None, governance: None },
|
|
@@ -1,37 +1,62 @@
|
|
|
1
1
|
//!
|
|
2
2
|
use super::*;
|
|
3
3
|
|
|
4
|
+
const SUMMARY_LIMIT: usize = 120;
|
|
5
|
+
|
|
6
|
+
fn clean_text(text: &str) -> String {
|
|
7
|
+
text.chars().filter(|ch| !ch.is_control()).collect()
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
fn compact_field(value: Option<&Value>) -> Value {
|
|
11
|
+
value
|
|
12
|
+
.and_then(Value::as_str)
|
|
13
|
+
.map(|text| Value::String(clean_text(text)))
|
|
14
|
+
.unwrap_or(Value::Null)
|
|
15
|
+
}
|
|
16
|
+
|
|
4
17
|
pub fn inbox(
|
|
5
18
|
workspace: &Path,
|
|
6
19
|
agent: &str,
|
|
7
20
|
limit: usize,
|
|
8
|
-
since: Option<&str>,
|
|
9
|
-
as_json: bool,
|
|
10
21
|
owner_team_id: Option<&str>,
|
|
11
22
|
) -> Result<Value, CliError> {
|
|
12
|
-
let _ = as_json;
|
|
13
23
|
let store = crate::message_store::MessageStore::open(workspace)
|
|
14
24
|
.map_err(|e| CliError::Runtime(e.to_string()))?;
|
|
15
|
-
let
|
|
25
|
+
let messages = store
|
|
16
26
|
.inbox(agent, limit, owner_team_id)
|
|
17
|
-
.map_err(|e| CliError::Runtime(e.to_string()))
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
.get("created_at")
|
|
22
|
-
.and_then(Value::as_str)
|
|
23
|
-
.and_then(parse_rfc3339)
|
|
24
|
-
.is_some_and(|created| created >= cutoff)
|
|
25
|
-
});
|
|
26
|
-
}
|
|
27
|
+
.map_err(|e| CliError::Runtime(e.to_string()))?
|
|
28
|
+
.into_iter()
|
|
29
|
+
.map(compact_message)
|
|
30
|
+
.collect::<Vec<_>>();
|
|
27
31
|
Ok(json!({
|
|
28
32
|
"ok": true,
|
|
29
33
|
"agent_id": agent,
|
|
30
34
|
"messages": messages,
|
|
31
|
-
"since": since,
|
|
32
35
|
}))
|
|
33
36
|
}
|
|
34
37
|
|
|
35
|
-
|
|
36
|
-
|
|
38
|
+
fn compact_message(message: Value) -> Value {
|
|
39
|
+
json!({
|
|
40
|
+
"message_id": compact_field(message.get("message_id")),
|
|
41
|
+
"sender": compact_field(message.get("sender")),
|
|
42
|
+
"recipient": compact_field(message.get("recipient")),
|
|
43
|
+
"status": compact_field(message.get("status")),
|
|
44
|
+
"created_at": compact_field(message.get("created_at")),
|
|
45
|
+
"summary": compact_summary(message.get("content").and_then(Value::as_str).unwrap_or("")),
|
|
46
|
+
})
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
fn compact_summary(content: &str) -> String {
|
|
50
|
+
let summary = content
|
|
51
|
+
.lines()
|
|
52
|
+
.map(|line| clean_text(line).trim().to_string())
|
|
53
|
+
.find(|line| !line.is_empty())
|
|
54
|
+
.unwrap_or_default();
|
|
55
|
+
let mut chars = summary.chars();
|
|
56
|
+
let truncated = chars.by_ref().take(SUMMARY_LIMIT).collect::<String>();
|
|
57
|
+
if chars.next().is_some() {
|
|
58
|
+
format!("{truncated}…")
|
|
59
|
+
} else {
|
|
60
|
+
truncated
|
|
61
|
+
}
|
|
37
62
|
}
|
|
@@ -211,36 +211,44 @@ fn approvals_golden_shape_has_waiting_count_and_scan_not_agent() {
|
|
|
211
211
|
let _ = std::fs::remove_dir_all(&ws);
|
|
212
212
|
}
|
|
213
213
|
|
|
214
|
-
// ── inbox:
|
|
215
|
-
// `since` (not `agent`/`limit`). RUST mod.rs:136-144 stub {ok,agent,limit,messages}. RED. ──────────
|
|
214
|
+
// ── inbox: compact fallback projection ───────────────────────────────────────────────
|
|
216
215
|
#[test]
|
|
217
|
-
fn
|
|
216
|
+
fn inbox_golden_shape_is_compact_and_scoped_to_agent() {
|
|
218
217
|
let ws = tmp_workspace();
|
|
219
|
-
let v = status_port::inbox(&ws, "alpha",
|
|
218
|
+
let v = status_port::inbox(&ws, "alpha", 3, None).expect("inbox");
|
|
220
219
|
let obj = v.as_object().expect("inbox dict");
|
|
220
|
+
assert_eq!(v["agent_id"], json!("alpha"));
|
|
221
221
|
let order: Vec<&str> = obj.keys().map(String::as_str).collect();
|
|
222
|
-
assert_eq!(
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
222
|
+
assert_eq!(order, vec!["ok", "agent_id", "messages"]);
|
|
223
|
+
let _ = std::fs::remove_dir_all(&ws);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
#[test]
|
|
227
|
+
fn inbox_excludes_leader_stage_result_notifications() {
|
|
228
|
+
let ws = tmp_workspace();
|
|
229
|
+
let store = crate::message_store::MessageStore::open(&ws).unwrap();
|
|
230
|
+
let message_id = store
|
|
231
|
+
.create_message(
|
|
232
|
+
None,
|
|
233
|
+
"leader",
|
|
234
|
+
"worker",
|
|
235
|
+
"Task t reported success; Result id: res-1",
|
|
236
|
+
None,
|
|
237
|
+
false,
|
|
238
|
+
None,
|
|
239
|
+
)
|
|
240
|
+
.unwrap();
|
|
241
|
+
let conn = crate::db::schema::open_db(store.db_path()).unwrap();
|
|
242
|
+
conn.execute(
|
|
243
|
+
"update messages set presentation = ?1 where message_id = ?2",
|
|
244
|
+
rusqlite::params![
|
|
245
|
+
json!({"sink": "leader", "class": "stage_result"}).to_string(),
|
|
246
|
+
message_id,
|
|
247
|
+
],
|
|
248
|
+
)
|
|
249
|
+
.unwrap();
|
|
250
|
+
let value = status_port::inbox(&ws, "leader", 3, None).expect("inbox");
|
|
251
|
+
assert!(value["messages"].as_array().unwrap().is_empty());
|
|
244
252
|
let _ = std::fs::remove_dir_all(&ws);
|
|
245
253
|
}
|
|
246
254
|
|
|
@@ -208,7 +208,7 @@ fn inbox_returns_stored_message_for_recipient() {
|
|
|
208
208
|
let mid = store
|
|
209
209
|
.create_message(None, "leader", "w1", "hello w1", None, true, None)
|
|
210
210
|
.unwrap();
|
|
211
|
-
let v = status_port::inbox(&ws, "w1", 20, None
|
|
211
|
+
let v = status_port::inbox(&ws, "w1", 20, None).expect("inbox");
|
|
212
212
|
let messages = v["messages"].as_array().expect("messages array");
|
|
213
213
|
assert_eq!(
|
|
214
214
|
messages.len(),
|
|
@@ -223,30 +223,12 @@ fn inbox_returns_stored_message_for_recipient() {
|
|
|
223
223
|
);
|
|
224
224
|
assert_eq!(m["recipient"], json!("w1"));
|
|
225
225
|
assert_eq!(m["sender"], json!("leader"));
|
|
226
|
-
assert_eq!(m["
|
|
226
|
+
assert_eq!(m["summary"], json!("hello w1"));
|
|
227
227
|
assert_eq!(
|
|
228
228
|
m["status"],
|
|
229
229
|
json!("accepted"),
|
|
230
230
|
"create_message persists status='accepted'"
|
|
231
231
|
);
|
|
232
|
-
// NULL owner_team_id semantics: status.inbox() calls MessageStore.inbox(agent) with
|
|
233
|
-
// owner_team_id=None (no team clause), so a NULL-owner message MUST surface for its recipient.
|
|
234
|
-
assert_eq!(
|
|
235
|
-
m["owner_team_id"],
|
|
236
|
-
json!(null),
|
|
237
|
-
"the stored message's owner_team_id is NULL and still returned"
|
|
238
|
-
);
|
|
239
|
-
// byte-faithful raw-row columns: requires_ack is the 0/1 INT; artifact_refs the literal text "[]".
|
|
240
|
-
assert_eq!(
|
|
241
|
-
m["requires_ack"],
|
|
242
|
-
json!(1),
|
|
243
|
-
"requires_ack is the 0/1 int, not a bool"
|
|
244
|
-
);
|
|
245
|
-
assert_eq!(
|
|
246
|
-
m["artifact_refs"],
|
|
247
|
-
json!("[]"),
|
|
248
|
-
"artifact_refs is the raw text column, not parsed"
|
|
249
|
-
);
|
|
250
232
|
let _ = std::fs::remove_dir_all(&ws);
|
|
251
233
|
}
|
|
252
234
|
// ── BUG-2 (match scope) — inbox(agent) returns rows where sender==agent OR recipient==agent, and
|
|
@@ -265,11 +247,11 @@ fn inbox_matches_sender_or_recipient_and_excludes_others() {
|
|
|
265
247
|
store
|
|
266
248
|
.create_message(None, "leader", "w2", "unrelated to w2", None, true, None)
|
|
267
249
|
.unwrap();
|
|
268
|
-
let v = status_port::inbox(&ws, "w1", 20, None
|
|
250
|
+
let v = status_port::inbox(&ws, "w1", 20, None).expect("inbox");
|
|
269
251
|
let messages = v["messages"].as_array().expect("messages array");
|
|
270
252
|
let mut contents: Vec<String> = messages
|
|
271
253
|
.iter()
|
|
272
|
-
.map(|m| m["
|
|
254
|
+
.map(|m| m["summary"].as_str().unwrap().to_string())
|
|
273
255
|
.collect();
|
|
274
256
|
contents.sort();
|
|
275
257
|
assert_eq!(
|
|
@@ -691,54 +673,6 @@ fn remove_agent_from_spec_refusal_is_not_success_envelope() {
|
|
|
691
673
|
);
|
|
692
674
|
}
|
|
693
675
|
#[test]
|
|
694
|
-
fn collect_uncollected_result_marks_db_and_outputs_result() {
|
|
695
|
-
let ws = tmp_workspace();
|
|
696
|
-
seed_collect_state(&ws);
|
|
697
|
-
seed_uncollected_result(&ws, "res_collect_red");
|
|
698
|
-
let out = json_output(
|
|
699
|
-
cmd_collect(&CollectArgs {
|
|
700
|
-
workspace: ws.clone(),
|
|
701
|
-
result_file: None,
|
|
702
|
-
json: true,
|
|
703
|
-
team: None,
|
|
704
|
-
})
|
|
705
|
-
.unwrap(),
|
|
706
|
-
);
|
|
707
|
-
assert_eq!(out["ok"], json!(true));
|
|
708
|
-
assert_eq!(
|
|
709
|
-
out["collected_results"][0]["result_id"],
|
|
710
|
-
json!("res_collect_red")
|
|
711
|
-
);
|
|
712
|
-
assert_eq!(out["collected_results"][0]["scope"], json!("task"));
|
|
713
|
-
assert_eq!(
|
|
714
|
-
out["results"],
|
|
715
|
-
json!({"total": 1, "uncollected": 0, "collected": 1, "invalid": 0, "by_status": {}})
|
|
716
|
-
);
|
|
717
|
-
let store = crate::message_store::MessageStore::open(&ws).unwrap();
|
|
718
|
-
let conn = crate::db::schema::open_db(store.db_path()).unwrap();
|
|
719
|
-
let status: String = conn
|
|
720
|
-
.query_row(
|
|
721
|
-
"select status from results where result_id = 'res_collect_red'",
|
|
722
|
-
[],
|
|
723
|
-
|row| row.get(0),
|
|
724
|
-
)
|
|
725
|
-
.unwrap();
|
|
726
|
-
assert_eq!(status, "collected");
|
|
727
|
-
let state = read_state(&ws);
|
|
728
|
-
assert_eq!(state["tasks"][0]["status"], json!("done"));
|
|
729
|
-
assert_eq!(
|
|
730
|
-
state["tasks"][0]["accepted_result_id"],
|
|
731
|
-
json!("res_collect_red")
|
|
732
|
-
);
|
|
733
|
-
assert!(
|
|
734
|
-
read_events(&ws)
|
|
735
|
-
.iter()
|
|
736
|
-
.any(|e| e["event"] == json!("collect.result")
|
|
737
|
-
&& e["result_id"] == json!("res_collect_red")),
|
|
738
|
-
"collect must emit collect.result for the stored result"
|
|
739
|
-
);
|
|
740
|
-
}
|
|
741
|
-
#[test]
|
|
742
676
|
fn stuck_cancel_persists_suppression_and_stuck_list_reads_state() {
|
|
743
677
|
let ws = tmp_workspace();
|
|
744
678
|
seed_collect_state(&ws);
|
|
@@ -275,36 +275,6 @@ fn seed_team_spec(ws: &std::path::Path) {
|
|
|
275
275
|
std::fs::write(ws.join("team.spec.yaml"), spec).unwrap();
|
|
276
276
|
}
|
|
277
277
|
|
|
278
|
-
fn seed_collect_runtime_state(ws: &std::path::Path) {
|
|
279
|
-
crate::state::persist::save_runtime_state(
|
|
280
|
-
ws,
|
|
281
|
-
&json!({
|
|
282
|
-
"active_team_key": "fake-e2e",
|
|
283
|
-
"team_dir": ws.to_string_lossy().to_string(),
|
|
284
|
-
"spec_path": ws.join("team.spec.yaml").to_string_lossy().to_string(),
|
|
285
|
-
"session_name": "team-agent-fake-e2e",
|
|
286
|
-
"leader": {"id": "leader"},
|
|
287
|
-
"agents": {
|
|
288
|
-
"fake_impl": {
|
|
289
|
-
"status": "running",
|
|
290
|
-
"provider": "fake",
|
|
291
|
-
"role": "implementation_engineer",
|
|
292
|
-
"window": "fake_impl",
|
|
293
|
-
"owner_team_id": "fake-e2e"
|
|
294
|
-
}
|
|
295
|
-
},
|
|
296
|
-
"tasks": [{
|
|
297
|
-
"id": "task_impl",
|
|
298
|
-
"title": "Fake implementation",
|
|
299
|
-
"type": "implementation",
|
|
300
|
-
"status": "pending",
|
|
301
|
-
"assignee": "fake_impl"
|
|
302
|
-
}]
|
|
303
|
-
}),
|
|
304
|
-
)
|
|
305
|
-
.unwrap();
|
|
306
|
-
}
|
|
307
|
-
|
|
308
278
|
// ── sessions ── golden cli/parser.py:230 `cmd_sessions` -> runtime.sessions(ws). EXIT 0.
|
|
309
279
|
// `team-agent sessions --workspace <ws> --json` on an empty ws ->
|
|
310
280
|
// {"ok":true,"sessions":[],"workspace":"<ws>"} (--json sort_keys). RED: unrouted -> Error.
|
|
@@ -324,44 +294,6 @@ fn dispatch_routes_sessions_subcommand() {
|
|
|
324
294
|
let _ = std::fs::remove_dir_all(&ws);
|
|
325
295
|
}
|
|
326
296
|
|
|
327
|
-
// ── collect ── golden parser.py:292 `cmd_collect` -> runtime.collect(ws). With a valid
|
|
328
|
-
// team.spec.yaml present and nothing to collect -> EXIT 0, golden:
|
|
329
|
-
// {"collected":[],"collected_results":[],"coordinator":{"ok":false,"status":"not_required"},
|
|
330
|
-
// "delivered_messages":[],"invalid_results":[],"ok":true,"results":{...},"state_file":"<ws>/team_state.md"}
|
|
331
|
-
// RED: unrouted -> Error.
|
|
332
|
-
#[test]
|
|
333
|
-
#[serial(env)]
|
|
334
|
-
fn dispatch_routes_collect_with_spec() {
|
|
335
|
-
let _env = EnvUnsetGuard::unset(&[
|
|
336
|
-
"TEAM_AGENT_WORKSPACE",
|
|
337
|
-
"TEAM_AGENT_TEAM_ID",
|
|
338
|
-
"TEAM_AGENT_OWNER_TEAM_ID",
|
|
339
|
-
"TEAM_AGENT_ACTIVE_TEAM",
|
|
340
|
-
"TEAM_AGENT_ID",
|
|
341
|
-
"TEAM_AGENT_LEADER_PANE_ID",
|
|
342
|
-
"TEAM_AGENT_LEADER_SESSION_UUID",
|
|
343
|
-
"TEAM_AGENT_LEADER_SESSION_UUID_OVERRIDE",
|
|
344
|
-
"TEAM_AGENT_LEADER_PROVIDER",
|
|
345
|
-
"TMUX",
|
|
346
|
-
"TMUX_PANE",
|
|
347
|
-
]);
|
|
348
|
-
let ws = tmp_workspace();
|
|
349
|
-
let _cleanup = WorkspaceCleanup(ws.clone());
|
|
350
|
-
seed_team_spec(&ws);
|
|
351
|
-
seed_collect_runtime_state(&ws);
|
|
352
|
-
let code = run(
|
|
353
|
-
&cli_argv(&["collect", "--workspace", &ws.to_string_lossy(), "--json"]),
|
|
354
|
-
&ws,
|
|
355
|
-
);
|
|
356
|
-
assert_eq!(
|
|
357
|
-
code,
|
|
358
|
-
ExitCode::Ok,
|
|
359
|
-
"`collect` must ROUTE to cmd_collect (parser.py:292); with a valid spec golden exits 0 \
|
|
360
|
-
{{collected,collected_results,coordinator,delivered_messages,invalid_results,ok,results,state_file}}; \
|
|
361
|
-
today -> unknown-subcommand Error"
|
|
362
|
-
);
|
|
363
|
-
}
|
|
364
|
-
|
|
365
297
|
// ── diagnose ── attached is host-registry authority, not state.json self-sign.
|
|
366
298
|
// Seed leader_receiver=attached + a ~/.team-agent/leaders/ row for this workspace
|
|
367
299
|
// + NO session_name + NO agents -> issues=[] -> EXIT 0.
|