@team-agent/installer 0.5.109 → 0.5.111
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/README.md +33 -0
- package/README.zh.md +30 -0
- package/crates/team-agent/src/cli/adapters.rs +234 -73
- package/crates/team-agent/src/cli/emit.rs +494 -201
- package/crates/team-agent/src/cli/leader.rs +4 -9
- package/crates/team-agent/src/cli/leaders.rs +90 -29
- package/crates/team-agent/src/cli/mod.rs +348 -161
- package/crates/team-agent/src/cli/models.rs +80 -24
- package/crates/team-agent/src/cli/named_address.rs +114 -25
- package/crates/team-agent/src/cli/route.rs +212 -0
- package/crates/team-agent/src/cli/send/presentation.rs +60 -41
- package/crates/team-agent/src/cli/send/resolve.rs +25 -18
- package/crates/team-agent/src/cli/send.rs +4 -4
- package/crates/team-agent/src/cli/spec.rs +56 -106
- package/crates/team-agent/src/cli/tests/base.rs +189 -82
- package/crates/team-agent/src/cli/tests/lane_c.rs +1 -2
- package/crates/team-agent/src/cli/tests/named_address.rs +19 -22
- package/crates/team-agent/src/cli/tests/status_send.rs +15 -10
- package/crates/team-agent/src/cli/tests/verb_install_skill.rs +2 -2
- package/crates/team-agent/src/cli/types.rs +22 -13
- package/crates/team-agent/src/leader/registry.rs +3 -3
- package/crates/team-agent/src/leader/start.rs +5 -1
- package/crates/team-agent/src/lifecycle/launch/spawn.rs +2 -0
- package/crates/team-agent/src/lifecycle/restart/common.rs +2 -0
- package/crates/team-agent/src/lifecycle/tests/cli_argv_routing_red.rs +1457 -0
- package/crates/team-agent/src/lifecycle/tests/pi_dual_entry_red.rs +4 -9
- package/crates/team-agent/src/lifecycle/tests/stale_team_saveconflict_contract.rs +3 -1
- package/crates/team-agent/src/lifecycle/tests.rs +1 -0
- package/crates/team-agent/src/provider/argv_route.rs +409 -0
- package/crates/team-agent/src/provider/mod.rs +3 -2
- package/package.json +4 -4
- package/skills/team-agent/SKILL.md +32 -0
|
@@ -16,8 +16,16 @@ use serde_json::json;
|
|
|
16
16
|
fn provider_args_cases() {
|
|
17
17
|
// (old case-id, input, Python golden); values[1:] of a lone "--" is empty.
|
|
18
18
|
const CASES: &[(&str, &[&str], &[&str])] = &[
|
|
19
|
-
(
|
|
20
|
-
|
|
19
|
+
(
|
|
20
|
+
"provider_args_strips_leading_dashdash",
|
|
21
|
+
&["--", "-x"],
|
|
22
|
+
&["-x"],
|
|
23
|
+
),
|
|
24
|
+
(
|
|
25
|
+
"provider_args_keeps_when_no_leading_dashdash",
|
|
26
|
+
&["-x", "-y"],
|
|
27
|
+
&["-x", "-y"],
|
|
28
|
+
),
|
|
21
29
|
("provider_args_empty_is_empty", &[], &[]),
|
|
22
30
|
("provider_args_lone_dashdash_yields_empty", &["--"], &[]),
|
|
23
31
|
];
|
|
@@ -36,41 +44,73 @@ fn provider_args_cases() {
|
|
|
36
44
|
fn leader_launcher_args_success_cases() {
|
|
37
45
|
// (old case-id, argv, complete expected structure), including default false flags.
|
|
38
46
|
let cases: &[(&str, &[&str], LeaderLauncherArgs)] = &[
|
|
39
|
-
(
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
47
|
+
(
|
|
48
|
+
"leader_launcher_args_empty_all_default",
|
|
49
|
+
&[],
|
|
50
|
+
LeaderLauncherArgs::default(),
|
|
51
|
+
),
|
|
52
|
+
(
|
|
53
|
+
"leader_launcher_args_attach_and_confirm",
|
|
54
|
+
&["--attach", "--confirm"],
|
|
55
|
+
LeaderLauncherArgs {
|
|
56
|
+
attach_existing: true,
|
|
57
|
+
confirm_attach: true,
|
|
58
|
+
..Default::default()
|
|
59
|
+
},
|
|
60
|
+
),
|
|
45
61
|
// --attach-existing is an alias of --attach.
|
|
46
|
-
(
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
62
|
+
(
|
|
63
|
+
"leader_launcher_args_attach_existing_alias",
|
|
64
|
+
&["--attach-existing"],
|
|
65
|
+
LeaderLauncherArgs {
|
|
66
|
+
attach_existing: true,
|
|
67
|
+
..Default::default()
|
|
68
|
+
},
|
|
69
|
+
),
|
|
70
|
+
(
|
|
71
|
+
"leader_launcher_args_external_leader_opt_out",
|
|
72
|
+
&["--external-leader", "--", "--model", "opus"],
|
|
73
|
+
LeaderLauncherArgs {
|
|
74
|
+
provider_args: vec!["--model".to_string(), "opus".to_string()],
|
|
75
|
+
external_leader: true,
|
|
76
|
+
..Default::default()
|
|
77
|
+
},
|
|
78
|
+
),
|
|
79
|
+
(
|
|
80
|
+
"leader_launcher_args_attach_session_spaced",
|
|
81
|
+
&["--attach-session", "mysess"],
|
|
82
|
+
LeaderLauncherArgs {
|
|
83
|
+
attach_session: Some("mysess".to_string()),
|
|
84
|
+
..Default::default()
|
|
85
|
+
},
|
|
86
|
+
),
|
|
87
|
+
(
|
|
88
|
+
"leader_launcher_args_attach_session_equals",
|
|
89
|
+
&["--attach-session=mysess"],
|
|
90
|
+
LeaderLauncherArgs {
|
|
91
|
+
attach_session: Some("mysess".to_string()),
|
|
92
|
+
..Default::default()
|
|
93
|
+
},
|
|
94
|
+
),
|
|
63
95
|
// Known Team Agent launcher flags after `--` are rejected by the error table.
|
|
64
|
-
(
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
96
|
+
(
|
|
97
|
+
"leader_launcher_args_dashdash_passthrough_strips_separator",
|
|
98
|
+
&["--attach", "--", "-x", "--provider-confirm"],
|
|
99
|
+
LeaderLauncherArgs {
|
|
100
|
+
provider_args: vec!["-x".to_string(), "--provider-confirm".to_string()],
|
|
101
|
+
attach_existing: true,
|
|
102
|
+
..Default::default()
|
|
103
|
+
},
|
|
104
|
+
),
|
|
105
|
+
(
|
|
106
|
+
"leader_launcher_args_unknown_tokens_collect_as_provider_args",
|
|
107
|
+
&["foo", "--attach", "bar"],
|
|
108
|
+
LeaderLauncherArgs {
|
|
109
|
+
provider_args: vec!["foo".to_string(), "bar".to_string()],
|
|
110
|
+
attach_existing: true,
|
|
111
|
+
..Default::default()
|
|
112
|
+
},
|
|
113
|
+
),
|
|
74
114
|
];
|
|
75
115
|
for (id, input, expected) in cases {
|
|
76
116
|
let argv: Vec<String> = input.iter().map(|arg| (*arg).to_string()).collect();
|
|
@@ -97,7 +137,10 @@ fn leader_launcher_args_error_cases() {
|
|
|
97
137
|
for (id, input, expected) in CASES {
|
|
98
138
|
let argv: Vec<String> = input.iter().map(|arg| (*arg).to_string()).collect();
|
|
99
139
|
let err = leader_launcher_args(&argv).expect_err(id);
|
|
100
|
-
assert!(
|
|
140
|
+
assert!(
|
|
141
|
+
err.to_string().contains(*expected),
|
|
142
|
+
"{id}: unexpected error: {err}"
|
|
143
|
+
);
|
|
101
144
|
}
|
|
102
145
|
}
|
|
103
146
|
|
|
@@ -110,14 +153,31 @@ fn send_target_cases() {
|
|
|
110
153
|
// (old case-id, targets, target, complete expected enum).
|
|
111
154
|
let cases: &[(&str, Option<&str>, Option<&str>, MessageTarget)] = &[
|
|
112
155
|
(
|
|
113
|
-
"send_target_fanout_strips_and_filters_empty",
|
|
156
|
+
"send_target_fanout_strips_and_filters_empty",
|
|
157
|
+
Some("a, b ,,c"),
|
|
158
|
+
None,
|
|
114
159
|
MessageTarget::Fanout(vec!["a".to_string(), "b".to_string(), "c".to_string()]),
|
|
115
160
|
),
|
|
116
|
-
(
|
|
161
|
+
(
|
|
162
|
+
"send_target_single_target",
|
|
163
|
+
None,
|
|
164
|
+
Some("agent_x"),
|
|
165
|
+
MessageTarget::Single("agent_x".to_string()),
|
|
166
|
+
),
|
|
117
167
|
// A bare "*" means broadcast to the whole team.
|
|
118
|
-
(
|
|
168
|
+
(
|
|
169
|
+
"send_target_broadcast_star",
|
|
170
|
+
None,
|
|
171
|
+
Some("*"),
|
|
172
|
+
MessageTarget::Broadcast,
|
|
173
|
+
),
|
|
119
174
|
// targets="" is falsy in Python, so it falls through to args.target.
|
|
120
|
-
(
|
|
175
|
+
(
|
|
176
|
+
"send_target_empty_targets_falls_through_to_target",
|
|
177
|
+
Some(""),
|
|
178
|
+
Some("fallback"),
|
|
179
|
+
MessageTarget::Single("fallback".to_string()),
|
|
180
|
+
),
|
|
121
181
|
];
|
|
122
182
|
for (id, targets, target, expected) in cases {
|
|
123
183
|
assert_eq!(send_target(*targets, *target), *expected, "{id}");
|
|
@@ -133,18 +193,52 @@ fn send_target_cases() {
|
|
|
133
193
|
fn classify_agent_bucket_cases() {
|
|
134
194
|
// (old case-id, all original (raw, health) inputs, expected bucket).
|
|
135
195
|
const CASES: &[(&str, &[(&str, &str)], SummaryBucket)] = &[
|
|
136
|
-
(
|
|
137
|
-
|
|
138
|
-
|
|
196
|
+
(
|
|
197
|
+
"classify_failed_takes_priority",
|
|
198
|
+
&[("failed", ""), ("error", ""), ("running", "error")],
|
|
199
|
+
SummaryBucket::Failed,
|
|
200
|
+
),
|
|
201
|
+
(
|
|
202
|
+
"classify_stopped",
|
|
203
|
+
&[("stopped", ""), ("done", ""), ("running", "done")],
|
|
204
|
+
SummaryBucket::Stopped,
|
|
205
|
+
),
|
|
206
|
+
(
|
|
207
|
+
"classify_busy",
|
|
208
|
+
&[("busy", ""), ("", "running"), ("", "working")],
|
|
209
|
+
SummaryBucket::Busy,
|
|
210
|
+
),
|
|
139
211
|
// hstatus==idle precedes the raw==running branch.
|
|
140
|
-
(
|
|
141
|
-
|
|
212
|
+
(
|
|
213
|
+
"classify_hstatus_idle_beats_raw_running",
|
|
214
|
+
&[("running", "idle")],
|
|
215
|
+
SummaryBucket::Idle,
|
|
216
|
+
),
|
|
217
|
+
(
|
|
218
|
+
"classify_pure_running",
|
|
219
|
+
&[("running", "")],
|
|
220
|
+
SummaryBucket::Running,
|
|
221
|
+
),
|
|
142
222
|
// bug-071/077/085: unmatched and blocked/stuck/missing are Unknown, NEVER idle.
|
|
143
|
-
(
|
|
223
|
+
(
|
|
224
|
+
"classify_blocked_and_unmatched_are_unknown_never_idle",
|
|
225
|
+
&[
|
|
226
|
+
("blocked", ""),
|
|
227
|
+
("stuck", ""),
|
|
228
|
+
("", "missing"),
|
|
229
|
+
("weird_value", ""),
|
|
230
|
+
("", ""),
|
|
231
|
+
],
|
|
232
|
+
SummaryBucket::Unknown,
|
|
233
|
+
),
|
|
144
234
|
];
|
|
145
235
|
for (id, inputs, expected) in CASES {
|
|
146
236
|
for &(raw, health) in inputs.iter() {
|
|
147
|
-
assert_eq!(
|
|
237
|
+
assert_eq!(
|
|
238
|
+
classify_agent_bucket(raw, health),
|
|
239
|
+
*expected,
|
|
240
|
+
"{id}: raw={raw:?}, health={health:?}"
|
|
241
|
+
);
|
|
148
242
|
}
|
|
149
243
|
}
|
|
150
244
|
}
|
|
@@ -395,7 +489,7 @@ fn cli_error_payload_plain_runtime() {
|
|
|
395
489
|
assert_eq!(payload.error, "some other error");
|
|
396
490
|
assert_eq!(
|
|
397
491
|
payload.action,
|
|
398
|
-
"
|
|
492
|
+
"先运行 team-agent doctor --workspace . 检查所选队伍,或查看此处列出的错误日志。"
|
|
399
493
|
);
|
|
400
494
|
assert_eq!(payload.log, "/tmp/y.log");
|
|
401
495
|
assert_eq!(payload.reason, None);
|
|
@@ -417,19 +511,14 @@ fn cli_error_payload_tmux_conflict_quick_start_enrichment() {
|
|
|
417
511
|
// context reset is only through restart --allow-fresh with explicit consent.
|
|
418
512
|
assert_eq!(
|
|
419
513
|
payload.action,
|
|
420
|
-
"
|
|
421
|
-
To resume it use `team-agent restart`. \
|
|
422
|
-
If recovery is impossible, use `team-agent restart --allow-fresh` only after explicit context-loss consent. \
|
|
423
|
-
Only if you want a separate team, change `name:` in TEAM.md and run quick-start again. \
|
|
424
|
-
Never terminate existing tmux sessions from quick-start."
|
|
425
|
-
);
|
|
426
|
-
assert_eq!(
|
|
427
|
-
payload.next_actions,
|
|
428
|
-
Some(vec![
|
|
429
|
-
"If this is your existing team, resume it with `team-agent restart`.".to_string(),
|
|
430
|
-
"If you want a separate team, change `name:` in TEAM.md and run `team-agent quick-start` again.".to_string(),
|
|
431
|
-
])
|
|
514
|
+
"终端会话 `my-team` 已存在,可能是你的已有队伍。恢复用 team-agent restart;只有明确同意丢弃旧对话才用 --allow-fresh。另建队伍请修改 TEAM.md 的 name 后再 quick-start;不要关闭已有队伍凑成功。"
|
|
432
515
|
);
|
|
516
|
+
let next = payload.next_actions.as_ref().unwrap();
|
|
517
|
+
assert_eq!(next.len(), 2);
|
|
518
|
+
assert!(next[0].contains("team-agent restart"));
|
|
519
|
+
assert!(
|
|
520
|
+
next[1].contains("TEAM.md") && next[1].contains("name") && next[1].contains("quick-start")
|
|
521
|
+
);
|
|
433
522
|
}
|
|
434
523
|
|
|
435
524
|
#[test]
|
|
@@ -440,16 +529,11 @@ fn cli_error_payload_tmux_conflict_non_quick_start_enrichment() {
|
|
|
440
529
|
assert_eq!(payload.session_name.as_deref(), Some("my-team"));
|
|
441
530
|
assert_eq!(
|
|
442
531
|
payload.action,
|
|
443
|
-
"
|
|
444
|
-
Do not terminate existing tmux sessions from startup; \
|
|
445
|
-
use a different team name or runtime.session_name and start again."
|
|
446
|
-
);
|
|
447
|
-
assert_eq!(
|
|
448
|
-
payload.next_actions,
|
|
449
|
-
Some(vec![
|
|
450
|
-
"Use a different team name or runtime.session_name before starting again.".to_string()
|
|
451
|
-
])
|
|
532
|
+
"终端会话 `my-team` 已存在,可能属于运行中的队伍。不要关闭它;请使用另一个队伍名称后再启动。"
|
|
452
533
|
);
|
|
534
|
+
let next = payload.next_actions.as_ref().unwrap();
|
|
535
|
+
assert_eq!(next.len(), 1);
|
|
536
|
+
assert!(next[0].contains("队伍") && next[0].contains("名称"));
|
|
453
537
|
}
|
|
454
538
|
|
|
455
539
|
#[test]
|
|
@@ -641,19 +725,42 @@ fn cmd_status_summary_with_agent_rejected() {
|
|
|
641
725
|
// =========================================================================
|
|
642
726
|
|
|
643
727
|
#[test]
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
let
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
let
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
728
|
+
#[serial_test::serial(env)]
|
|
729
|
+
fn cmd_leader_passthrough_help_is_pure_human_guidance() {
|
|
730
|
+
let env = crate::cli::hermetic_test_support::HermeticTestEnv::enter("launcher-help-unit");
|
|
731
|
+
let cwd = env.workspace("empty");
|
|
732
|
+
let tools = [
|
|
733
|
+
"pi", "codex", "claude", "copilot", "grok", "agent", "cursor",
|
|
734
|
+
];
|
|
735
|
+
let bin = env.root().join("canaries");
|
|
736
|
+
std::fs::create_dir(&bin).unwrap();
|
|
737
|
+
#[cfg(unix)]
|
|
738
|
+
let _path = {
|
|
739
|
+
use std::os::unix::fs::PermissionsExt;
|
|
740
|
+
for tool in tools {
|
|
741
|
+
let path = bin.join(tool);
|
|
742
|
+
std::fs::write(&path, "#!/bin/sh\nprintf called > \"$0.called\"\nexit 0\n").unwrap();
|
|
743
|
+
std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o755)).unwrap();
|
|
744
|
+
}
|
|
745
|
+
env.with_env("PATH", &bin.to_string_lossy())
|
|
746
|
+
};
|
|
747
|
+
for provider in ["pi", "codex", "claude", "copilot", "grok", "cursor"] {
|
|
748
|
+
for alias in ["-h", "--help"] {
|
|
749
|
+
let result = cmd_leader_passthrough(provider, &[alias.into()], &cwd).unwrap();
|
|
750
|
+
assert_eq!(result.exit, ExitCode::Ok);
|
|
751
|
+
assert!(
|
|
752
|
+
matches!(&result.output, CmdOutput::Human(text) if text.contains(provider) && text.contains("用法") && text.contains("Examples") && text.contains("下一步"))
|
|
753
|
+
);
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
assert_eq!(std::fs::read_dir(&cwd).unwrap().count(), 0);
|
|
757
|
+
assert!(env.registry_entries().is_empty());
|
|
758
|
+
for tool in tools {
|
|
759
|
+
assert!(
|
|
760
|
+
!bin.join(format!("{tool}.called")).exists(),
|
|
761
|
+
"native tool invoked"
|
|
762
|
+
);
|
|
763
|
+
}
|
|
657
764
|
}
|
|
658
765
|
|
|
659
766
|
#[test]
|
|
@@ -257,8 +257,7 @@ fn inbox_excludes_leader_stage_result_notifications() {
|
|
|
257
257
|
// the deterministic check sub-shapes (run_id is a random uuid; not value-locked). ────────────────
|
|
258
258
|
#[test]
|
|
259
259
|
fn comms_selftest_golden_boundary_scope_and_check_shapes() {
|
|
260
|
-
let boundary = "
|
|
261
|
-
Does NOT perform live runtime message round-trip. (zero token, zero pollution)";
|
|
260
|
+
let boundary = "这里只检查终端连接是否一致,不实际发送消息,也不证明队友已回复。";
|
|
262
261
|
let ws = tmp_workspace();
|
|
263
262
|
let v = diagnose_port::comms_selftest(&ws, None, None).expect("comms_selftest");
|
|
264
263
|
let obj = v.as_object().expect("comms dict");
|
|
@@ -248,12 +248,9 @@ fn resolve_qualified_name_prefers_unique_state_pane_over_same_window_idle_pane()
|
|
|
248
248
|
pane("team-a", "qa", "%idle"),
|
|
249
249
|
]);
|
|
250
250
|
|
|
251
|
-
let resolved =
|
|
252
|
-
&ws,
|
|
253
|
-
|
|
254
|
-
&transport,
|
|
255
|
-
)
|
|
256
|
-
.expect("a qualified worker address should use its unique state pane");
|
|
251
|
+
let resolved =
|
|
252
|
+
resolve_name_with_transport(&ws, &format!("{}::team-a/qa", ws.display()), &transport)
|
|
253
|
+
.expect("a qualified worker address should use its unique state pane");
|
|
257
254
|
assert_eq!(resolved.pane_id, "%worker");
|
|
258
255
|
assert_eq!(resolved.state_pane_id.as_deref(), Some("%worker"));
|
|
259
256
|
assert!(!resolved.state_pane_stale);
|
|
@@ -271,11 +268,17 @@ fn resolve_qualified_name_state_pane_must_uniquely_match_scoped_candidates() {
|
|
|
271
268
|
.as_object_mut()
|
|
272
269
|
.unwrap()
|
|
273
270
|
.remove("pane_id");
|
|
274
|
-
vec![
|
|
271
|
+
vec![
|
|
272
|
+
pane("team-a", "qa", "%worker"),
|
|
273
|
+
pane("team-a", "qa", "%idle"),
|
|
274
|
+
]
|
|
275
275
|
}
|
|
276
276
|
"stale" => {
|
|
277
277
|
team["agents"]["qa"]["pane_id"] = json!("%stale");
|
|
278
|
-
vec![
|
|
278
|
+
vec![
|
|
279
|
+
pane("team-a", "qa", "%worker"),
|
|
280
|
+
pane("team-a", "qa", "%idle"),
|
|
281
|
+
]
|
|
279
282
|
}
|
|
280
283
|
"cross-session" => vec![
|
|
281
284
|
pane("old-team-a", "qa", "%worker"),
|
|
@@ -293,12 +296,9 @@ fn resolve_qualified_name_state_pane_must_uniquely_match_scoped_candidates() {
|
|
|
293
296
|
};
|
|
294
297
|
seed_state(&ws, state_with_teams(json!({"team-a": team})));
|
|
295
298
|
let transport = OfflineTransport::new().with_targets(targets);
|
|
296
|
-
let err =
|
|
297
|
-
&ws,
|
|
298
|
-
|
|
299
|
-
&transport,
|
|
300
|
-
)
|
|
301
|
-
.expect_err("non-unique state pane evidence must remain fail-closed");
|
|
299
|
+
let err =
|
|
300
|
+
resolve_name_with_transport(&ws, &format!("{}::team-a/qa", ws.display()), &transport)
|
|
301
|
+
.expect_err("non-unique state pane evidence must remain fail-closed");
|
|
302
302
|
assert_eq!(err.kind, NamedAddressErrorKind::NameAmbiguous, "{scenario}");
|
|
303
303
|
let _ = std::fs::remove_dir_all(&ws);
|
|
304
304
|
}
|
|
@@ -313,11 +313,8 @@ fn resolve_qualified_name_canonicalizes_legacy_session_alias() {
|
|
|
313
313
|
"teamdir": worker_team("team-displayname", "qa", "%1", "qa")
|
|
314
314
|
})),
|
|
315
315
|
);
|
|
316
|
-
let transport =
|
|
317
|
-
"team-displayname",
|
|
318
|
-
"qa",
|
|
319
|
-
"%1",
|
|
320
|
-
)]);
|
|
316
|
+
let transport =
|
|
317
|
+
OfflineTransport::new().with_targets(vec![pane("team-displayname", "qa", "%1")]);
|
|
321
318
|
|
|
322
319
|
let resolved = resolve_name_with_transport(&ws, "displayname/qa", &transport)
|
|
323
320
|
.expect("qualified legacy alias must resolve through the canonical selector");
|
|
@@ -380,21 +377,21 @@ fn send_to_name_mutual_exclusion() {
|
|
|
380
377
|
let args = named_send_args(&cwd, Some("team-a/qa"), Some("%1"), None, &["hello"]);
|
|
381
378
|
let err = cmd_send(&args).unwrap_err();
|
|
382
379
|
assert!(
|
|
383
|
-
matches!(err, CliError::Usage(ref message) if message.contains("--to-name
|
|
380
|
+
matches!(err, CliError::Usage(ref message) if message.contains("--to-name/--pane/--to") && message.contains("不能同时")),
|
|
384
381
|
"expected --to-name mutual exclusion, got {err:?}"
|
|
385
382
|
);
|
|
386
383
|
|
|
387
384
|
let args = named_send_args(&cwd, Some("team-a/qa"), None, Some("worker"), &["hello"]);
|
|
388
385
|
let err = cmd_send(&args).unwrap_err();
|
|
389
386
|
assert!(
|
|
390
|
-
matches!(err, CliError::Usage(ref message) if message.contains("--to-name
|
|
387
|
+
matches!(err, CliError::Usage(ref message) if message.contains("--to-name/--pane/--to") && message.contains("不能同时")),
|
|
391
388
|
"expected --to-name/--to mutual exclusion, got {err:?}"
|
|
392
389
|
);
|
|
393
390
|
|
|
394
391
|
let args = named_send_args(&cwd, Some("team-a/qa"), None, None, &[]);
|
|
395
392
|
let err = cmd_send(&args).unwrap_err();
|
|
396
393
|
assert!(
|
|
397
|
-
matches!(err, CliError::Usage(ref message) if message == "
|
|
394
|
+
matches!(err, CliError::Usage(ref message) if message == "请填写任务内容;--to-name 不能发送空消息"),
|
|
398
395
|
"expected empty-message usage error, got {err:?}"
|
|
399
396
|
);
|
|
400
397
|
let _ = std::fs::remove_dir_all(&cwd);
|
|
@@ -645,7 +645,7 @@ fn cmd_send_joins_message_with_single_space() {
|
|
|
645
645
|
);
|
|
646
646
|
assert_eq!(
|
|
647
647
|
v.get("reminder").and_then(|reminder| reminder.as_str()),
|
|
648
|
-
Some("
|
|
648
|
+
Some("任务已收下,但还未确认送到队友对话。查看 team-agent status/doctor,等真实回复,不反复重发。")
|
|
649
649
|
);
|
|
650
650
|
}
|
|
651
651
|
}
|
|
@@ -654,22 +654,27 @@ fn cmd_send_joins_message_with_single_space() {
|
|
|
654
654
|
}
|
|
655
655
|
|
|
656
656
|
#[test]
|
|
657
|
-
fn
|
|
657
|
+
fn cmd_send_default_human_output_is_brief_without_false_delivered() {
|
|
658
658
|
let r = cmd_send(&queued_send_args_fixture(false)).expect("cmd_send returns CmdResult");
|
|
659
659
|
assert!(!r.as_json);
|
|
660
660
|
let text = emit(&r.output, r.as_json).expect("send should render human text");
|
|
661
661
|
let lines: Vec<_> = text.lines().collect();
|
|
662
662
|
assert_eq!(
|
|
663
663
|
lines.len(),
|
|
664
|
-
|
|
665
|
-
"
|
|
664
|
+
2,
|
|
665
|
+
"send must separate outcome and next action: {text}"
|
|
666
666
|
);
|
|
667
667
|
assert!(
|
|
668
|
-
lines[0].contains("
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
"
|
|
668
|
+
lines[0].contains("任务已收下") && lines[0].contains("还未送到队友的对话"),
|
|
669
|
+
"queued send must report its true pending outcome: {text}"
|
|
670
|
+
);
|
|
671
|
+
assert!(
|
|
672
|
+
lines[1].contains("下一步")
|
|
673
|
+
&& lines[1].contains("team-agent status")
|
|
674
|
+
&& lines[1].contains("doctor")
|
|
675
|
+
&& lines[1].contains("等真实回复")
|
|
676
|
+
&& lines[1].contains("不反复重发"),
|
|
677
|
+
"queued send must give honest next steps: {text}"
|
|
673
678
|
);
|
|
674
679
|
assert!(
|
|
675
680
|
!text.contains("delivered"),
|
|
@@ -946,7 +951,7 @@ fn cmd_send_unknown_task_surfaces_golden_error_envelope_not_silent() {
|
|
|
946
951
|
);
|
|
947
952
|
assert_eq!(
|
|
948
953
|
payload.action,
|
|
949
|
-
"
|
|
954
|
+
"先运行 team-agent doctor --workspace . 检查所选队伍,或查看此处列出的错误日志。"
|
|
950
955
|
);
|
|
951
956
|
let _ = std::fs::remove_dir_all(&ws);
|
|
952
957
|
}
|
|
@@ -62,8 +62,8 @@ fn install_skill_missing_source_is_usage_error() {
|
|
|
62
62
|
);
|
|
63
63
|
assert_eq!(
|
|
64
64
|
code,
|
|
65
|
-
ExitCode::
|
|
66
|
-
"
|
|
65
|
+
ExitCode::Usage,
|
|
66
|
+
"known missing --source must give pure Usage2 guidance"
|
|
67
67
|
);
|
|
68
68
|
let _ = std::fs::remove_dir_all(&ws);
|
|
69
69
|
}
|
|
@@ -41,6 +41,13 @@ pub enum CliError {
|
|
|
41
41
|
/// `peek` 缺 `--allow-raw-screen`)。对应 Python 抛 `TeamAgentError` 或 parser.error。
|
|
42
42
|
#[error("usage error: {0}")]
|
|
43
43
|
Usage(String),
|
|
44
|
+
/// Preserve the original refusal for audit while carrying safe human guidance.
|
|
45
|
+
#[error("usage error: {audit}")]
|
|
46
|
+
AddressRefusal {
|
|
47
|
+
audit: String,
|
|
48
|
+
error: String,
|
|
49
|
+
action: String,
|
|
50
|
+
},
|
|
44
51
|
/// state 解析失败(歧义/未找到 team 等)。透传 step 5。
|
|
45
52
|
#[error("{0}")]
|
|
46
53
|
State(#[from] crate::state::StateError),
|
|
@@ -76,7 +83,12 @@ impl CliError {
|
|
|
76
83
|
let mut payload = CliErrorPayload {
|
|
77
84
|
ok: false,
|
|
78
85
|
error: error.clone(),
|
|
79
|
-
action:
|
|
86
|
+
action: if super::spec::command_spec(command).is_some() {
|
|
87
|
+
"先运行 team-agent doctor --workspace . 检查所选队伍,或查看此处列出的错误日志。"
|
|
88
|
+
} else {
|
|
89
|
+
"run `team-agent doctor` or inspect the log path shown here"
|
|
90
|
+
}
|
|
91
|
+
.to_string(),
|
|
80
92
|
log: log_path.to_string_lossy().to_string(),
|
|
81
93
|
reason: None,
|
|
82
94
|
session_name: None,
|
|
@@ -87,30 +99,27 @@ impl CliError {
|
|
|
87
99
|
payload.session_name = Some(session.clone());
|
|
88
100
|
if command == "quick-start" {
|
|
89
101
|
payload.action = format!(
|
|
90
|
-
"
|
|
102
|
+
"终端会话 `{session}` 已存在,可能是你的已有队伍。恢复用 team-agent restart;只有明确同意丢弃旧对话才用 --allow-fresh。另建队伍请修改 TEAM.md 的 name 后再 quick-start;不要关闭已有队伍凑成功。"
|
|
91
103
|
);
|
|
92
104
|
payload.next_actions = Some(vec![
|
|
93
|
-
"
|
|
94
|
-
"
|
|
105
|
+
"如果是自己的已有队伍,使用 team-agent restart 恢复。".to_string(),
|
|
106
|
+
"如要另建队伍,修改 TEAM.md 的 name 后再运行 team-agent quick-start。"
|
|
107
|
+
.to_string(),
|
|
95
108
|
]);
|
|
96
109
|
} else {
|
|
97
110
|
payload.action = format!(
|
|
98
|
-
"
|
|
111
|
+
"终端会话 `{session}` 已存在,可能属于运行中的队伍。不要关闭它;请使用另一个队伍名称后再启动。"
|
|
99
112
|
);
|
|
100
|
-
payload.next_actions = Some(vec![
|
|
101
|
-
"Use a different team name or runtime.session_name before starting again."
|
|
102
|
-
.to_string(),
|
|
103
|
-
]);
|
|
113
|
+
payload.next_actions = Some(vec!["请使用另一个队伍名称后再启动。".to_string()]);
|
|
104
114
|
}
|
|
105
115
|
} else if error.contains("Team Agent launcher flag")
|
|
106
116
|
&& error.contains("must appear before --")
|
|
107
117
|
{
|
|
108
|
-
payload.action =
|
|
109
|
-
"
|
|
110
|
-
);
|
|
118
|
+
payload.action =
|
|
119
|
+
String::from("将主控启动选项放在 -- 前面;-- 后面只能是工具自己的参数。");
|
|
111
120
|
} else if error.contains("managed launcher refuses a different ambient tmux server") {
|
|
112
121
|
payload.action = String::from(
|
|
113
|
-
"
|
|
122
|
+
"先退出当前 tmux 客户端再重试;只有明确需要嵌套连接时才在 -- 前加 --allow-nested-attach。",
|
|
114
123
|
);
|
|
115
124
|
}
|
|
116
125
|
payload
|
|
@@ -816,7 +816,7 @@ mod tests {
|
|
|
816
816
|
}
|
|
817
817
|
|
|
818
818
|
#[test]
|
|
819
|
-
#[serial]
|
|
819
|
+
#[serial(env)]
|
|
820
820
|
fn prune_removes_team_proven_terminal() {
|
|
821
821
|
let (_home_guard, _home, workspace) = test_home("terminal");
|
|
822
822
|
write_state(
|
|
@@ -834,7 +834,7 @@ mod tests {
|
|
|
834
834
|
}
|
|
835
835
|
|
|
836
836
|
#[test]
|
|
837
|
-
#[serial]
|
|
837
|
+
#[serial(env)]
|
|
838
838
|
fn prune_removes_missing_key_only_from_valid_teams_map() {
|
|
839
839
|
let (_home_guard, _home, workspace) = test_home("missing-key");
|
|
840
840
|
write_state(&workspace, json!({"teams": {"other": {"status": "alive"}}}));
|
|
@@ -850,7 +850,7 @@ mod tests {
|
|
|
850
850
|
}
|
|
851
851
|
|
|
852
852
|
#[test]
|
|
853
|
-
#[serial]
|
|
853
|
+
#[serial(env)]
|
|
854
854
|
fn prune_keeps_null_nonobject_and_legacy_teams_shapes() {
|
|
855
855
|
for (label, state) in [
|
|
856
856
|
("null", json!({"teams": null})),
|
|
@@ -291,7 +291,7 @@ fn leader_start_plan_with_ambient_authority(
|
|
|
291
291
|
),
|
|
292
292
|
_ => None,
|
|
293
293
|
};
|
|
294
|
-
let provider_argv = if provider == Provider::Pi && mode != LeaderStartMode::AttachExisting {
|
|
294
|
+
let mut provider_argv = if provider == Provider::Pi && mode != LeaderStartMode::AttachExisting {
|
|
295
295
|
let parsed = crate::lifecycle::launch::pi_mcp::parse_pi_leader_args(provider_args)
|
|
296
296
|
.map_err(|error| LeaderError::Start(error.to_string()))?;
|
|
297
297
|
let mcp_config = adapter
|
|
@@ -329,6 +329,10 @@ fn leader_start_plan_with_ambient_authority(
|
|
|
329
329
|
} else {
|
|
330
330
|
provider_command_argv(provider, provider_args)
|
|
331
331
|
};
|
|
332
|
+
if mode != LeaderStartMode::AttachExisting {
|
|
333
|
+
crate::provider::argv_route::apply(provider, &mut provider_argv)
|
|
334
|
+
.map_err(|error| LeaderError::Start(error.to_string()))?;
|
|
335
|
+
}
|
|
332
336
|
let argv = start_argv(
|
|
333
337
|
mode,
|
|
334
338
|
provider,
|
|
@@ -330,6 +330,8 @@ pub(super) fn spawn_agents(
|
|
|
330
330
|
ensure_grok_login_and_folder_trust(workspace)?;
|
|
331
331
|
apply_grok_mcp_overlay(workspace, &mcp_config)?;
|
|
332
332
|
}
|
|
333
|
+
crate::provider::argv_route::apply(provider, &mut plan.argv)
|
|
334
|
+
.map_err(|error| LifecycleError::Provider(error.to_string()))?;
|
|
333
335
|
let spawn_epoch = u64::try_from(started.len()).unwrap_or(u64::MAX);
|
|
334
336
|
let spawned_at = spawn_timestamp_for_agent(u32::try_from(spawn_epoch).unwrap_or(u32::MAX));
|
|
335
337
|
// E6 层1 实证3 + 诊断留痕:落最终 worker argv(spawn 前的真实形态)。
|