@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
package/Cargo.lock
CHANGED
package/Cargo.toml
CHANGED
package/README.md
CHANGED
|
@@ -66,6 +66,39 @@ The lead and each teammate independently choose which CLI to use:
|
|
|
66
66
|
|
|
67
67
|
In other words: let Claude be the lead for planning and review, let Codex teammates handle bulk implementation. Mix and match whatever subscriptions you have.
|
|
68
68
|
|
|
69
|
+
## Native CLI argv routing (opt-in)
|
|
70
|
+
|
|
71
|
+
Configure literal startup arguments globally, separately for each provider, from any directory:
|
|
72
|
+
|
|
73
|
+
```sh
|
|
74
|
+
team-agent route set pi -- --mode rpc
|
|
75
|
+
team-agent route enable
|
|
76
|
+
team-agent route status --json
|
|
77
|
+
team-agent route show pi --json
|
|
78
|
+
team-agent route add pi -- --verbose
|
|
79
|
+
team-agent route clear pi
|
|
80
|
+
team-agent route disable
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
The default is OFF. `~/.team-agent/argv-routing.json` stores the switch and mappings;
|
|
84
|
+
`TEAM_AGENT_CLI_ARGV_ROUTING=1|true|on` or `0|false|off` overrides the persisted switch
|
|
85
|
+
(case-insensitive, surrounding whitespace accepted). Invalid overrides safely disable routing.
|
|
86
|
+
`enable`/`disable` preserve mappings; `set` replaces, `add` appends, and `clear` removes one mapping.
|
|
87
|
+
Commands report both persisted and effective states, including an active environment override.
|
|
88
|
+
|
|
89
|
+
Provider keys: `claude`, `codex`, `copilot`, `gemini_cli`, `grok`, `cursor_agent`, `pi`.
|
|
90
|
+
Claude/Claude Code share one mapping; `claude_code`/`claude-code` and `agent`/`cursor` are accepted aliases.
|
|
91
|
+
Everything after the first `--` is literal argv data, including `--help`, `--json`, empty strings
|
|
92
|
+
and `{workspace}`; no shell expansion is performed. Arguments are inserted immediately after
|
|
93
|
+
the native executable, before its original arguments. Do not store secrets or override framework-managed
|
|
94
|
+
session/MCP/permission arguments; routing does not bypass existing identity or authentication checks.
|
|
95
|
+
|
|
96
|
+
Only the next actual native Leader/worker launch uses the current mapping, including restart,
|
|
97
|
+
start/reset/add/clone, Pi new-seat fork and remove rollback. Running/attached processes, dry runs,
|
|
98
|
+
model/version/auth probes and other helper processes are unchanged. Enabled invalid configuration
|
|
99
|
+
fails before native spawn; `TEAM_AGENT_CLI_ARGV_ROUTING=off` is an emergency override, not a file repair.
|
|
100
|
+
Pi `--mode rpc` is passed through as startup argv; it does **not** create an RPC host or change Team Agent's transport.
|
|
101
|
+
|
|
69
102
|
## FAQ
|
|
70
103
|
|
|
71
104
|
**What do I need?**
|
package/README.zh.md
CHANGED
|
@@ -66,6 +66,36 @@ team-agent claude
|
|
|
66
66
|
|
|
67
67
|
也就是说:你可以让 Claude 当队长出方案、做评审,让 Codex 队友批量干实现。你手上有哪几家的订阅,就能怎么搭着用。
|
|
68
68
|
|
|
69
|
+
## 原生 CLI 参数路由(默认关闭)
|
|
70
|
+
|
|
71
|
+
在任意目录按 Provider 全局配置字面启动参数,无需工作区或已登录的 CLI:
|
|
72
|
+
|
|
73
|
+
```sh
|
|
74
|
+
team-agent route set pi -- --mode rpc
|
|
75
|
+
team-agent route enable
|
|
76
|
+
team-agent route status --json
|
|
77
|
+
team-agent route show pi --json
|
|
78
|
+
team-agent route add pi -- --verbose
|
|
79
|
+
team-agent route clear pi
|
|
80
|
+
team-agent route disable
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
默认 OFF;开关与映射保存于 `~/.team-agent/argv-routing.json`。
|
|
84
|
+
环境变量 `TEAM_AGENT_CLI_ARGV_ROUTING=1|true|on` 或 `0|false|off` 优先于持久化开关
|
|
85
|
+
(忽略大小写和两侧空白),无效值安全关闭。`enable/disable` 不清映射,`set` 精确替换、
|
|
86
|
+
`add` 按序追加、`clear` 删除一个映射。结果同时报告持久化态、实际有效态与环境覆盖。
|
|
87
|
+
|
|
88
|
+
Provider 键为 `claude`、`codex`、`copilot`、`gemini_cli`、`grok`、`cursor_agent`、`pi`。
|
|
89
|
+
Claude/Claude Code 共用映射;支持 `claude_code/claude-code` 和 `agent/cursor` 别名。
|
|
90
|
+
第一个 `--` 后全部是字面 argv,包括 `--help`、`--json`、空字符串、`{workspace}`,不会二次 shell 展开。
|
|
91
|
+
注入位置为原生 executable 之后、原有参数之前。不要保存密码/token,或覆盖框架管理的
|
|
92
|
+
session/MCP/权限参数;既有身份、认证与安全检查不变。
|
|
93
|
+
|
|
94
|
+
下一次真实 Leader/worker 启动读取当前配置,包括 restart、start/reset/add/clone、Pi 新席 fork
|
|
95
|
+
及 remove 失败回滚复活。运行中或仅 attach 的进程、dry-run、model/version/auth 探测与其他辅助进程不变。
|
|
96
|
+
已开启但无效的配置会在原生 spawn 前报错;可用 `TEAM_AGENT_CLI_ARGV_ROUTING=off` 紧急停用,
|
|
97
|
+
但这不会修复坏文件。Pi `--mode rpc` 只是启动参数,不会自动建立 RPC 宿主或改变 Team Agent 的传输。
|
|
98
|
+
|
|
69
99
|
## 常见问题
|
|
70
100
|
|
|
71
101
|
**需要什么前提?**
|
|
@@ -16,6 +16,9 @@ pub fn cmd_quick_start(args: &QuickStartArgs) -> Result<CmdResult, CliError> {
|
|
|
16
16
|
args.yes,
|
|
17
17
|
args.backend.as_deref(),
|
|
18
18
|
)?;
|
|
19
|
+
if let Some(result) = quick_start_config_guidance(&mut value, args) {
|
|
20
|
+
return Ok(result);
|
|
21
|
+
}
|
|
19
22
|
append_send_guidance(&mut value, &args.workspace, args.team_id.as_deref());
|
|
20
23
|
let readiness = value.get("readiness").and_then(Value::as_object);
|
|
21
24
|
let all_resumable_have_session = readiness
|
|
@@ -46,6 +49,29 @@ pub fn cmd_quick_start(args: &QuickStartArgs) -> Result<CmdResult, CliError> {
|
|
|
46
49
|
if args.json && status.as_deref() == Some("pending_tool_load") {
|
|
47
50
|
result.exit = ExitCode::Ok;
|
|
48
51
|
}
|
|
52
|
+
if !args.json {
|
|
53
|
+
if let CmdOutput::Json(value) = &result.output {
|
|
54
|
+
let explanation = if value.get("ok").and_then(Value::as_bool) == Some(false) {
|
|
55
|
+
format!(
|
|
56
|
+
"启动未完成:{}",
|
|
57
|
+
value
|
|
58
|
+
.get("error")
|
|
59
|
+
.and_then(Value::as_str)
|
|
60
|
+
.unwrap_or("队伍尚未准备好接收任务")
|
|
61
|
+
)
|
|
62
|
+
} else {
|
|
63
|
+
"队伍尚未准备好接收任务;请等待工具完成启动。".to_string()
|
|
64
|
+
};
|
|
65
|
+
let mut doctor = format!(
|
|
66
|
+
"team-agent doctor --workspace {}",
|
|
67
|
+
shell_quote(&args.workspace.to_string_lossy())
|
|
68
|
+
);
|
|
69
|
+
if let Some(team) = args.team_id.as_deref() {
|
|
70
|
+
doctor.push_str(&format!(" --team {}", shell_quote(team)));
|
|
71
|
+
}
|
|
72
|
+
result.output = CmdOutput::Human(format!("{explanation}\n下一步:运行 {doctor},按体检提示处理;用 status 查看状态,不反复起队或重发任务。"));
|
|
73
|
+
}
|
|
74
|
+
}
|
|
49
75
|
Ok(result)
|
|
50
76
|
} else {
|
|
51
77
|
// E13:happy 人类路径必须带 attach_commands(json 路径 cli/mod.rs:1775 已有)。
|
|
@@ -53,13 +79,99 @@ pub fn cmd_quick_start(args: &QuickStartArgs) -> Result<CmdResult, CliError> {
|
|
|
53
79
|
}
|
|
54
80
|
}
|
|
55
81
|
|
|
82
|
+
// Project only an actual compiler rejection; never reject a valid lifecycle fallback.
|
|
83
|
+
fn quick_start_config_guidance(value: &mut Value, args: &QuickStartArgs) -> Option<CmdResult> {
|
|
84
|
+
if value.get("ok").and_then(Value::as_bool) != Some(false) {
|
|
85
|
+
return None;
|
|
86
|
+
}
|
|
87
|
+
let error = value.get("error")?.as_str()?;
|
|
88
|
+
let compile = error.strip_prefix("spec compile failed: ")?;
|
|
89
|
+
let detail = compile
|
|
90
|
+
.strip_prefix("validation error: ")
|
|
91
|
+
.unwrap_or(compile);
|
|
92
|
+
let team_path = args.agents_dir.join("TEAM.md");
|
|
93
|
+
let agents_path = args.agents_dir.join("agents");
|
|
94
|
+
let missing_team = detail == format!("{}: missing TEAM.md", team_path.display())
|
|
95
|
+
&& team_path.try_exists().ok() == Some(false);
|
|
96
|
+
let missing_roles = detail == format!("{}: missing agents directory", agents_path.display())
|
|
97
|
+
|| detail == format!("{}: no role docs found", agents_path.display());
|
|
98
|
+
let missing_field = detail.split_once(": ").is_some_and(|(path, reason)| {
|
|
99
|
+
Path::new(path).parent() == Some(agents_path.as_path())
|
|
100
|
+
&& (reason.contains("missing required field") || reason.contains("is required"))
|
|
101
|
+
});
|
|
102
|
+
if !missing_team && !(team_path.is_file() && (missing_roles || missing_field)) {
|
|
103
|
+
return None;
|
|
104
|
+
}
|
|
105
|
+
let role_path = agents_path.join("worker.md");
|
|
106
|
+
let mut retry = format!(
|
|
107
|
+
"team-agent quick-start {} --workspace {}",
|
|
108
|
+
shell_quote(&args.agents_dir.to_string_lossy()),
|
|
109
|
+
shell_quote(&args.workspace.to_string_lossy())
|
|
110
|
+
);
|
|
111
|
+
if let Some(team) = args.team_id.as_deref() {
|
|
112
|
+
retry.push_str(&format!(" --team {}", shell_quote(team)));
|
|
113
|
+
}
|
|
114
|
+
if let Some(name) = args.name.as_deref() {
|
|
115
|
+
retry.push_str(&format!(" --name {}", shell_quote(name)));
|
|
116
|
+
}
|
|
117
|
+
if let Some(backend) = args.backend.as_deref() {
|
|
118
|
+
retry.push_str(&format!(" --backend {}", shell_quote(backend)));
|
|
119
|
+
}
|
|
120
|
+
if args.yes {
|
|
121
|
+
retry.push_str(" --yes");
|
|
122
|
+
}
|
|
123
|
+
if args.detail {
|
|
124
|
+
retry.push_str(" --detail");
|
|
125
|
+
}
|
|
126
|
+
if args.json {
|
|
127
|
+
retry.push_str(" --json");
|
|
128
|
+
}
|
|
129
|
+
let explanation = if missing_team {
|
|
130
|
+
format!("这里还没有队伍配置:{}。没有启动任何队友。请自行创建下面两个文件;本命令没有写入这些文件。", team_path.display())
|
|
131
|
+
} else {
|
|
132
|
+
format!("队友配置尚不完整:{detail}。请补齐角色目录或必需字段;下面仅给安全示例,不覆盖已有文件。")
|
|
133
|
+
};
|
|
134
|
+
let next = format!("确认 Pi 已安装并登录;运行 team-agent models --provider pi 核对模型。\n运行 team-agent pi,在该主控的命令行/工具上下文再次执行:\n{retry}");
|
|
135
|
+
let mut templates =
|
|
136
|
+
vec![serde_json::json!({"path": role_path, "content": super::emit::WORKER_TEMPLATE})];
|
|
137
|
+
let mut human = explanation.clone();
|
|
138
|
+
if missing_team {
|
|
139
|
+
templates.insert(
|
|
140
|
+
0,
|
|
141
|
+
serde_json::json!({"path": team_path, "content": super::emit::TEAM_TEMPLATE}),
|
|
142
|
+
);
|
|
143
|
+
human.push_str(&format!(
|
|
144
|
+
"\n\n{}:\n{}",
|
|
145
|
+
team_path.display(),
|
|
146
|
+
super::emit::TEAM_TEMPLATE
|
|
147
|
+
));
|
|
148
|
+
}
|
|
149
|
+
human.push_str(&format!(
|
|
150
|
+
"\n\n{}:\n{}\n下一步:\n{next}",
|
|
151
|
+
role_path.display(),
|
|
152
|
+
super::emit::WORKER_TEMPLATE
|
|
153
|
+
));
|
|
154
|
+
value["action"] = serde_json::json!(explanation);
|
|
155
|
+
value["next_actions"] =
|
|
156
|
+
serde_json::json!(["team-agent models --provider pi", "team-agent pi", retry]);
|
|
157
|
+
value["templates"] = serde_json::json!(templates);
|
|
158
|
+
if args.json {
|
|
159
|
+
Some(CmdResult::from_json(value.clone(), true))
|
|
160
|
+
} else {
|
|
161
|
+
let mut result = CmdResult::human(&human);
|
|
162
|
+
result.exit = ExitCode::Error;
|
|
163
|
+
Some(result)
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
56
167
|
/// E13:quick-start "team 起了" 人类输出 = summary + attach 块。所有成功出口共用(别每分支手拷)。
|
|
57
168
|
/// attach_commands 缺/空 → 只 summary(向后兼容)。
|
|
58
169
|
fn quickstart_human(value: &Value) -> String {
|
|
59
|
-
let summary = value
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
170
|
+
let summary = if value.get("summary").and_then(Value::as_str) == Some("existing runtime") {
|
|
171
|
+
"发现已有队伍;请查看返回的连接方式,不会另建同名队伍。"
|
|
172
|
+
} else {
|
|
173
|
+
"队伍已启动;下面是连接方式和派发任务的命令。"
|
|
174
|
+
};
|
|
63
175
|
let attach: Vec<&str> = value
|
|
64
176
|
.get("attach_commands")
|
|
65
177
|
.and_then(Value::as_array)
|
|
@@ -72,14 +184,14 @@ fn quickstart_human(value: &Value) -> String {
|
|
|
72
184
|
.unwrap_or_default();
|
|
73
185
|
let mut out = String::from(summary);
|
|
74
186
|
if !attach.is_empty() {
|
|
75
|
-
out.push_str("\n\
|
|
187
|
+
out.push_str("\n\n连接队伍:");
|
|
76
188
|
for cmd in attach {
|
|
77
189
|
out.push_str("\n ");
|
|
78
190
|
out.push_str(cmd);
|
|
79
191
|
}
|
|
80
192
|
}
|
|
81
193
|
if !sends.is_empty() {
|
|
82
|
-
out.push_str("\n\
|
|
194
|
+
out.push_str("\n\n派发任务(请替换任务内容):");
|
|
83
195
|
for cmd in sends {
|
|
84
196
|
out.push_str("\n ");
|
|
85
197
|
out.push_str(cmd);
|
|
@@ -146,7 +258,7 @@ pub(crate) fn send_command(agent: &str, workspace: &Path, team: Option<&str>) ->
|
|
|
146
258
|
let mut command = format!(
|
|
147
259
|
"team-agent send {} {} --workspace {}",
|
|
148
260
|
shell_quote(agent),
|
|
149
|
-
shell_quote("
|
|
261
|
+
shell_quote("请完成任务并把答案回复给 leader。"),
|
|
150
262
|
shell_quote(workspace)
|
|
151
263
|
);
|
|
152
264
|
if let Some(team) = team {
|
|
@@ -156,7 +268,7 @@ pub(crate) fn send_command(agent: &str, workspace: &Path, team: Option<&str>) ->
|
|
|
156
268
|
Some(command)
|
|
157
269
|
}
|
|
158
270
|
|
|
159
|
-
fn shell_quote(value: &str) -> String {
|
|
271
|
+
pub(crate) fn shell_quote(value: &str) -> String {
|
|
160
272
|
if value.bytes().all(|byte| {
|
|
161
273
|
byte.is_ascii_alphanumeric() || matches!(byte, b'/' | b'.' | b'_' | b'-' | b':')
|
|
162
274
|
}) {
|
|
@@ -551,10 +663,8 @@ fn run_fake_e2e(workspace: &Path) -> Result<Value, CliError> {
|
|
|
551
663
|
"next_actions": [],
|
|
552
664
|
}),
|
|
553
665
|
)?;
|
|
554
|
-
let auto_finalized =
|
|
555
|
-
.get("finalization_status")
|
|
556
|
-
.and_then(Value::as_str)
|
|
557
|
-
== Some("auto_finalized");
|
|
666
|
+
let auto_finalized =
|
|
667
|
+
report.get("finalization_status").and_then(Value::as_str) == Some("auto_finalized");
|
|
558
668
|
let shutdown = fake_shutdown(workspace)?;
|
|
559
669
|
let ok = launch.get("ok").and_then(Value::as_bool) == Some(true)
|
|
560
670
|
&& send.ok
|
|
@@ -1015,27 +1125,24 @@ fn format_inbox_human(agent: &str, value: &Value) -> String {
|
|
|
1015
1125
|
.and_then(Value::as_str)
|
|
1016
1126
|
.unwrap_or("-"),
|
|
1017
1127
|
);
|
|
1018
|
-
let sender =
|
|
1019
|
-
message.get("sender").and_then(Value::as_str).unwrap_or("-")
|
|
1020
|
-
);
|
|
1128
|
+
let sender =
|
|
1129
|
+
clean_inbox_field(message.get("sender").and_then(Value::as_str).unwrap_or("-"));
|
|
1021
1130
|
let recipient = clean_inbox_field(
|
|
1022
1131
|
message
|
|
1023
1132
|
.get("recipient")
|
|
1024
1133
|
.and_then(Value::as_str)
|
|
1025
1134
|
.unwrap_or("-"),
|
|
1026
1135
|
);
|
|
1027
|
-
let status =
|
|
1028
|
-
message.get("status").and_then(Value::as_str).unwrap_or("-")
|
|
1029
|
-
);
|
|
1136
|
+
let status =
|
|
1137
|
+
clean_inbox_field(message.get("status").and_then(Value::as_str).unwrap_or("-"));
|
|
1030
1138
|
let time = clean_inbox_field(
|
|
1031
1139
|
message
|
|
1032
1140
|
.get("created_at")
|
|
1033
1141
|
.and_then(Value::as_str)
|
|
1034
1142
|
.unwrap_or("-"),
|
|
1035
1143
|
);
|
|
1036
|
-
let summary =
|
|
1037
|
-
message.get("summary").and_then(Value::as_str).unwrap_or("")
|
|
1038
|
-
);
|
|
1144
|
+
let summary =
|
|
1145
|
+
clean_inbox_field(message.get("summary").and_then(Value::as_str).unwrap_or(""));
|
|
1039
1146
|
truncate_inbox_line(&format!(
|
|
1040
1147
|
"[{id}] [{sender} -> {recipient}] [{status}] [{time}] [{summary}]"
|
|
1041
1148
|
))
|
|
@@ -1054,8 +1161,7 @@ pub fn cmd_takeover(args: &TakeoverArgs) -> Result<CmdResult, CliError> {
|
|
|
1054
1161
|
|
|
1055
1162
|
/// `cmd_claim_leader`(`commands.py:156`)。
|
|
1056
1163
|
pub fn cmd_claim_leader(args: &ClaimLeaderArgs) -> Result<CmdResult, CliError> {
|
|
1057
|
-
let mut value =
|
|
1058
|
-
leader_port::claim_leader(&args.workspace, args.team.as_deref(), args.confirm)?;
|
|
1164
|
+
let mut value = leader_port::claim_leader(&args.workspace, args.team.as_deref(), args.confirm)?;
|
|
1059
1165
|
if !args.detail {
|
|
1060
1166
|
leader_port::compact_lease_value(&mut value);
|
|
1061
1167
|
}
|
|
@@ -1104,10 +1210,7 @@ pub fn cmd_start_agent(args: &StartAgentArgs) -> Result<CmdResult, CliError> {
|
|
|
1104
1210
|
)?;
|
|
1105
1211
|
if value.get("agent_ids").is_none() {
|
|
1106
1212
|
if let Some(object) = value.as_object_mut() {
|
|
1107
|
-
object.insert(
|
|
1108
|
-
"agent_ids".to_string(),
|
|
1109
|
-
json!([args.agent.as_str()]),
|
|
1110
|
-
);
|
|
1213
|
+
object.insert("agent_ids".to_string(), json!([args.agent.as_str()]));
|
|
1111
1214
|
}
|
|
1112
1215
|
}
|
|
1113
1216
|
append_send_guidance(&mut value, &args.workspace, args.team.as_deref());
|
|
@@ -1147,10 +1250,7 @@ pub fn cmd_add_agent(args: &AddAgentArgs) -> Result<CmdResult, CliError> {
|
|
|
1147
1250
|
)?;
|
|
1148
1251
|
if value.get("agent_ids").is_none() {
|
|
1149
1252
|
if let Some(object) = value.as_object_mut() {
|
|
1150
|
-
object.insert(
|
|
1151
|
-
"agent_ids".to_string(),
|
|
1152
|
-
json!([args.agent.as_str()]),
|
|
1153
|
-
);
|
|
1253
|
+
object.insert("agent_ids".to_string(), json!([args.agent.as_str()]));
|
|
1154
1254
|
}
|
|
1155
1255
|
}
|
|
1156
1256
|
append_send_guidance(&mut value, &args.workspace, args.team.as_deref());
|
|
@@ -1222,10 +1322,8 @@ pub fn cmd_doctor(args: &DoctorArgs) -> Result<CmdResult, CliError> {
|
|
|
1222
1322
|
}
|
|
1223
1323
|
|
|
1224
1324
|
let explicit_comms = args.comms || matches!(args.gate, Some(DoctorGate::Comms));
|
|
1225
|
-
let default_report =
|
|
1226
|
-
&& args.gate.is_none()
|
|
1227
|
-
&& !args.cleanup_orphans
|
|
1228
|
-
&& !args.fix_schema;
|
|
1325
|
+
let default_report =
|
|
1326
|
+
!explicit_comms && args.gate.is_none() && !args.cleanup_orphans && !args.fix_schema;
|
|
1229
1327
|
let mut value = if explicit_comms {
|
|
1230
1328
|
crate::diagnose::comms::doctor_comms_json(
|
|
1231
1329
|
&args.workspace,
|
|
@@ -1282,22 +1380,28 @@ fn unified_default_doctor_report(args: &DoctorArgs, mut value: Value) -> Value {
|
|
|
1282
1380
|
args.team.as_deref(),
|
|
1283
1381
|
crate::state::selector::SelectorMode::RuntimeOnly,
|
|
1284
1382
|
);
|
|
1285
|
-
let run_workspace = selected
|
|
1286
|
-
.
|
|
1287
|
-
|
|
1383
|
+
let run_workspace = selected
|
|
1384
|
+
.as_ref()
|
|
1385
|
+
.map(|selected| selected.run_workspace.clone())
|
|
1386
|
+
.unwrap_or_else(|_| {
|
|
1387
|
+
crate::model::paths::canonical_run_workspace(&args.workspace)
|
|
1388
|
+
.unwrap_or_else(|_| args.workspace.clone())
|
|
1389
|
+
});
|
|
1288
1390
|
let runtime_present = selected.as_ref().is_ok_and(|selected| {
|
|
1289
1391
|
crate::cli::diagnose::workspace_has_existing_team_runtime(
|
|
1290
1392
|
&selected.run_workspace,
|
|
1291
1393
|
&selected.team_key,
|
|
1292
1394
|
)
|
|
1293
1395
|
});
|
|
1294
|
-
let db_present = crate::model::paths::runtime_dir(&run_workspace)
|
|
1396
|
+
let db_present = crate::model::paths::runtime_dir(&run_workspace)
|
|
1397
|
+
.join("team.db")
|
|
1398
|
+
.exists();
|
|
1295
1399
|
// One read-only observation supplies both issue classification and details.
|
|
1296
1400
|
// A physical database must be checked even before state.json is created.
|
|
1297
1401
|
let health = (runtime_present || db_present).then(|| {
|
|
1298
|
-
crate::coordinator::coordinator_health_read_only(
|
|
1299
|
-
|
|
1300
|
-
)
|
|
1402
|
+
crate::coordinator::coordinator_health_read_only(&crate::coordinator::WorkspacePath::new(
|
|
1403
|
+
run_workspace.clone(),
|
|
1404
|
+
))
|
|
1301
1405
|
});
|
|
1302
1406
|
match selected {
|
|
1303
1407
|
Ok(selected) => {
|
|
@@ -1358,10 +1462,17 @@ fn unified_default_doctor_report(args: &DoctorArgs, mut value: Value) -> Value {
|
|
|
1358
1462
|
}
|
|
1359
1463
|
if let Some(object) = value.as_object_mut() {
|
|
1360
1464
|
object.insert("runtime".to_string(), runtime);
|
|
1361
|
-
object.insert(
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1465
|
+
object.insert(
|
|
1466
|
+
"event_log".to_string(),
|
|
1467
|
+
Value::String(
|
|
1468
|
+
run_workspace
|
|
1469
|
+
.join(".team")
|
|
1470
|
+
.join("logs")
|
|
1471
|
+
.join("events.jsonl")
|
|
1472
|
+
.to_string_lossy()
|
|
1473
|
+
.to_string(),
|
|
1474
|
+
),
|
|
1475
|
+
);
|
|
1365
1476
|
}
|
|
1366
1477
|
finalize_doctor_report(&mut value, true);
|
|
1367
1478
|
value
|
|
@@ -1385,7 +1496,10 @@ fn merge_report_array(report: &mut Value, key: &str, incoming: &Value) {
|
|
|
1385
1496
|
.expect("report array normalized before merge");
|
|
1386
1497
|
for item in items {
|
|
1387
1498
|
let identity = report_item_identity(item);
|
|
1388
|
-
if !target
|
|
1499
|
+
if !target
|
|
1500
|
+
.iter()
|
|
1501
|
+
.any(|existing| report_item_identity(existing) == identity)
|
|
1502
|
+
{
|
|
1389
1503
|
target.push(item.clone());
|
|
1390
1504
|
}
|
|
1391
1505
|
}
|
|
@@ -1450,11 +1564,17 @@ fn finalize_doctor_report(report: &mut Value, default_report: bool) {
|
|
|
1450
1564
|
.and_then(|value| value.as_array().cloned())
|
|
1451
1565
|
.unwrap_or_default();
|
|
1452
1566
|
let mut add = |issue: Value, repair: Option<Value>| {
|
|
1453
|
-
if !issues
|
|
1567
|
+
if !issues
|
|
1568
|
+
.iter()
|
|
1569
|
+
.any(|existing| report_item_identity(existing) == report_item_identity(&issue))
|
|
1570
|
+
{
|
|
1454
1571
|
issues.push(issue);
|
|
1455
1572
|
}
|
|
1456
1573
|
if let Some(repair) = repair {
|
|
1457
|
-
if !repairs
|
|
1574
|
+
if !repairs
|
|
1575
|
+
.iter()
|
|
1576
|
+
.any(|existing| report_item_identity(existing) == report_item_identity(&repair))
|
|
1577
|
+
{
|
|
1458
1578
|
repairs.push(repair);
|
|
1459
1579
|
}
|
|
1460
1580
|
}
|
|
@@ -1467,10 +1587,10 @@ fn finalize_doctor_report(report: &mut Value, default_report: bool) {
|
|
|
1467
1587
|
.or_else(|| object.get("reason").and_then(Value::as_str))
|
|
1468
1588
|
.unwrap_or("doctor_failed");
|
|
1469
1589
|
let issue = if status == "failed" {
|
|
1470
|
-
object
|
|
1471
|
-
.
|
|
1472
|
-
|
|
1473
|
-
|
|
1590
|
+
object.get("gate").and_then(Value::as_str).map_or_else(
|
|
1591
|
+
|| "doctor_failed".to_string(),
|
|
1592
|
+
|gate| format!("{gate}_failed"),
|
|
1593
|
+
)
|
|
1474
1594
|
} else if status.ends_with("_failed") || status == "refused" {
|
|
1475
1595
|
status.to_string()
|
|
1476
1596
|
} else {
|
|
@@ -1481,7 +1601,10 @@ fn finalize_doctor_report(report: &mut Value, default_report: bool) {
|
|
|
1481
1601
|
.or_else(|| object.get("next_action"))
|
|
1482
1602
|
.or_else(|| object.get("reason"))
|
|
1483
1603
|
.cloned();
|
|
1484
|
-
add(
|
|
1604
|
+
add(
|
|
1605
|
+
Value::String(issue),
|
|
1606
|
+
repair.map(|value| json!({"action": value})),
|
|
1607
|
+
);
|
|
1485
1608
|
}
|
|
1486
1609
|
if object
|
|
1487
1610
|
.get("workspace")
|
|
@@ -1512,9 +1635,9 @@ fn finalize_doctor_report(report: &mut Value, default_report: bool) {
|
|
|
1512
1635
|
{
|
|
1513
1636
|
add(
|
|
1514
1637
|
json!("grok_slot_mismatch"),
|
|
1515
|
-
grok.get("reason")
|
|
1516
|
-
|
|
1517
|
-
|
|
1638
|
+
grok.get("reason")
|
|
1639
|
+
.cloned()
|
|
1640
|
+
.map(|reason| json!({"issue": "grok_slot_mismatch", "action": reason})),
|
|
1518
1641
|
);
|
|
1519
1642
|
}
|
|
1520
1643
|
}
|
|
@@ -1559,7 +1682,9 @@ fn finalize_doctor_report(report: &mut Value, default_report: bool) {
|
|
|
1559
1682
|
if runtime_status == "unresolved" {
|
|
1560
1683
|
add(
|
|
1561
1684
|
json!("runtime_selection_failed"),
|
|
1562
|
-
Some(
|
|
1685
|
+
Some(
|
|
1686
|
+
json!({"issue": "runtime_selection_failed", "action": "select an existing Team runtime"}),
|
|
1687
|
+
),
|
|
1563
1688
|
);
|
|
1564
1689
|
}
|
|
1565
1690
|
if runtime_status == "present"
|
|
@@ -1610,7 +1735,10 @@ mod tests {
|
|
|
1610
1735
|
let output = format_inbox_human("worker", &value);
|
|
1611
1736
|
for line in output.lines() {
|
|
1612
1737
|
assert!(line.len() <= 160, "line is {} bytes: {line:?}", line.len());
|
|
1613
|
-
assert!(
|
|
1738
|
+
assert!(
|
|
1739
|
+
!line.chars().any(char::is_control),
|
|
1740
|
+
"controls leaked: {line:?}"
|
|
1741
|
+
);
|
|
1614
1742
|
}
|
|
1615
1743
|
}
|
|
1616
1744
|
|
|
@@ -1625,9 +1753,12 @@ mod tests {
|
|
|
1625
1753
|
],
|
|
1626
1754
|
});
|
|
1627
1755
|
let out = quickstart_human(&value);
|
|
1628
|
-
assert!(out.contains("team started"), "must keep summary; got {out}");
|
|
1629
1756
|
assert!(
|
|
1630
|
-
out.contains("
|
|
1757
|
+
out.contains("队伍已启动"),
|
|
1758
|
+
"must show startup summary; got {out}"
|
|
1759
|
+
);
|
|
1760
|
+
assert!(
|
|
1761
|
+
out.contains("连接队伍:"),
|
|
1631
1762
|
"must render attach block; got {out}"
|
|
1632
1763
|
);
|
|
1633
1764
|
assert!(
|
|
@@ -1643,14 +1774,20 @@ mod tests {
|
|
|
1643
1774
|
#[test]
|
|
1644
1775
|
fn reminders_only_recommend_supported_result_commands() {
|
|
1645
1776
|
let reminder = crate::cli::QUICK_START_REMINDER;
|
|
1646
|
-
assert!(reminder.contains("
|
|
1777
|
+
assert!(reminder.contains("team-agent status"));
|
|
1778
|
+
assert!(reminder.contains("team-agent inbox"));
|
|
1647
1779
|
for text in [
|
|
1648
1780
|
reminder,
|
|
1649
1781
|
crate::cli::STATUS_REMINDER,
|
|
1650
1782
|
crate::cli::SEND_REMINDER,
|
|
1651
1783
|
&quickstart_human(&json!({"summary": "team started"})),
|
|
1652
1784
|
] {
|
|
1653
|
-
|
|
1785
|
+
for hidden in ["collect", "team-agent results", "team-agent wait"] {
|
|
1786
|
+
assert!(
|
|
1787
|
+
!text.contains(hidden),
|
|
1788
|
+
"private/removed CLI command leaked: {text}"
|
|
1789
|
+
);
|
|
1790
|
+
}
|
|
1654
1791
|
}
|
|
1655
1792
|
}
|
|
1656
1793
|
|
|
@@ -1659,13 +1796,19 @@ mod tests {
|
|
|
1659
1796
|
let value = json!({"summary": "quick-start complete"});
|
|
1660
1797
|
assert_eq!(
|
|
1661
1798
|
quickstart_human(&value),
|
|
1662
|
-
format!(
|
|
1799
|
+
format!(
|
|
1800
|
+
"队伍已启动;下面是连接方式和派发任务的命令。\n{}",
|
|
1801
|
+
crate::cli::QUICK_START_REMINDER
|
|
1802
|
+
)
|
|
1663
1803
|
);
|
|
1664
|
-
//
|
|
1804
|
+
// 空数组也只显示人类摘要与下一步,不凭空编造连接方式。
|
|
1665
1805
|
let value2 = json!({"summary": "s", "attach_commands": []});
|
|
1666
1806
|
assert_eq!(
|
|
1667
1807
|
quickstart_human(&value2),
|
|
1668
|
-
format!(
|
|
1808
|
+
format!(
|
|
1809
|
+
"队伍已启动;下面是连接方式和派发任务的命令。\n{}",
|
|
1810
|
+
crate::cli::QUICK_START_REMINDER
|
|
1811
|
+
)
|
|
1669
1812
|
);
|
|
1670
1813
|
}
|
|
1671
1814
|
|
|
@@ -1684,7 +1827,7 @@ mod tests {
|
|
|
1684
1827
|
"team-agent",
|
|
1685
1828
|
"send",
|
|
1686
1829
|
"worker name; echo unsafe",
|
|
1687
|
-
"
|
|
1830
|
+
"请完成任务并把答案回复给 leader。",
|
|
1688
1831
|
"--workspace",
|
|
1689
1832
|
"/tmp/my workspace",
|
|
1690
1833
|
"--team",
|
|
@@ -1695,11 +1838,15 @@ mod tests {
|
|
|
1695
1838
|
|
|
1696
1839
|
#[test]
|
|
1697
1840
|
fn send_guidance_is_copyable_and_preserves_explicit_scope() {
|
|
1698
|
-
let command = send_command(
|
|
1699
|
-
|
|
1841
|
+
let command = send_command(
|
|
1842
|
+
"worker name; echo unsafe",
|
|
1843
|
+
Path::new("/tmp/my workspace"),
|
|
1844
|
+
Some("team-a"),
|
|
1845
|
+
)
|
|
1846
|
+
.unwrap();
|
|
1700
1847
|
assert_eq!(
|
|
1701
1848
|
command,
|
|
1702
|
-
"team-agent send 'worker name; echo unsafe'
|
|
1849
|
+
"team-agent send 'worker name; echo unsafe' '请完成任务并把答案回复给 leader。' --workspace '/tmp/my workspace' --team team-a"
|
|
1703
1850
|
);
|
|
1704
1851
|
}
|
|
1705
1852
|
|
|
@@ -1720,7 +1867,10 @@ mod tests {
|
|
|
1720
1867
|
fn quick_start_guidance_lists_every_known_agent_without_guessing() {
|
|
1721
1868
|
let mut value = json!({"ok": true, "agent_ids": ["sol", "luna"]});
|
|
1722
1869
|
append_send_guidance(&mut value, Path::new("/tmp/ws"), Some("team"));
|
|
1723
|
-
let commands = value
|
|
1870
|
+
let commands = value
|
|
1871
|
+
.get("send_commands")
|
|
1872
|
+
.and_then(|v| v.as_array())
|
|
1873
|
+
.unwrap();
|
|
1724
1874
|
assert_eq!(commands.len(), 2);
|
|
1725
1875
|
assert!(commands[0].as_str().unwrap().contains("send sol"));
|
|
1726
1876
|
assert!(commands[1].as_str().unwrap().contains("send luna"));
|
|
@@ -1747,8 +1897,19 @@ mod tests {
|
|
|
1747
1897
|
.unwrap();
|
|
1748
1898
|
assert!(command.contains("send worker"));
|
|
1749
1899
|
assert!(command.contains("--team team-a"));
|
|
1750
|
-
|
|
1751
|
-
|
|
1900
|
+
assert_eq!(
|
|
1901
|
+
split_shell_argv(command),
|
|
1902
|
+
[
|
|
1903
|
+
"team-agent",
|
|
1904
|
+
"send",
|
|
1905
|
+
"worker",
|
|
1906
|
+
"请完成任务并把答案回复给 leader。",
|
|
1907
|
+
"--workspace",
|
|
1908
|
+
"/tmp/ws",
|
|
1909
|
+
"--team",
|
|
1910
|
+
"team-a",
|
|
1911
|
+
]
|
|
1912
|
+
);
|
|
1752
1913
|
}
|
|
1753
1914
|
|
|
1754
1915
|
#[test]
|