@team-agent/installer 0.5.86 → 0.5.91
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 +104 -164
- package/crates/team-agent/src/cli/emit.rs +68 -87
- package/crates/team-agent/src/cli/mod.rs +1 -1
- package/crates/team-agent/src/cli/spec.rs +1 -4
- package/crates/team-agent/src/cli/status_port/brief.rs +187 -7
- 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/types.rs +3 -16
- package/crates/team-agent/src/compiler.rs +4 -7
- 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/fake_worker.rs +4 -4
- package/crates/team-agent/src/lifecycle/launch/identity.rs +39 -21
- package/crates/team-agent/src/lifecycle/launch/spawn.rs +1 -1
- package/crates/team-agent/src/lifecycle/restart/common.rs +1 -1
- package/crates/team-agent/src/lifecycle/tests/b0_reader_hideone_audit_contract.rs +3 -3
- 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/model/enums.rs +17 -3
- package/crates/team-agent/src/model/spec.rs +8 -7
- package/crates/team-agent/src/provider/wire.rs +1 -1
- 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
package/Cargo.lock
CHANGED
package/Cargo.toml
CHANGED
|
@@ -314,7 +314,7 @@ fn warn_ignored_owner_team_id(team_dir: &std::path::Path) {
|
|
|
314
314
|
}
|
|
315
315
|
|
|
316
316
|
/// `cmd_status`(`commands.py:90`)。CLI status 统一为只读七字段 brief;
|
|
317
|
-
/// `--json` 与人读路径共享
|
|
317
|
+
/// `--json` 与人读路径共享 native tmux/process projection;`--summary`/`--detail` 保留参数兼容性但不暴露诊断。
|
|
318
318
|
#[cfg(test)]
|
|
319
319
|
pub(crate) fn cmd_status(args: &StatusArgs) -> Result<CmdResult, CliError> {
|
|
320
320
|
cmd_status_for_team(args, args.team.as_deref())
|
|
@@ -357,7 +357,7 @@ pub fn cmd_status_for_team(args: &StatusArgs, team: Option<&str>) -> Result<CmdR
|
|
|
357
357
|
}
|
|
358
358
|
}
|
|
359
359
|
// Status brief is deliberately independent of the legacy RuntimeSnapshot:
|
|
360
|
-
// it performs one
|
|
360
|
+
// it performs one native tmux/process sample and exposes exactly seven
|
|
361
361
|
// fields. `--summary` and `--detail` remain parser-compatible but do not
|
|
362
362
|
// re-enable history, runtime diagnostics, or reminder text.
|
|
363
363
|
if args.json {
|
|
@@ -429,83 +429,6 @@ pub fn cmd_sessions(args: &SessionsArgs) -> Result<CmdResult, CliError> {
|
|
|
429
429
|
))
|
|
430
430
|
}
|
|
431
431
|
|
|
432
|
-
/// `cmd_collect`(`parser.py:292`)。
|
|
433
|
-
#[cfg(test)]
|
|
434
|
-
pub(crate) fn cmd_collect(args: &CollectArgs) -> Result<CmdResult, CliError> {
|
|
435
|
-
cmd_collect_for_team(args, args.team.as_deref())
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
pub fn cmd_collect_for_team(args: &CollectArgs, team: Option<&str>) -> Result<CmdResult, CliError> {
|
|
439
|
-
let selected = match crate::state::selector::resolve_active_team(
|
|
440
|
-
&args.workspace,
|
|
441
|
-
team,
|
|
442
|
-
crate::state::selector::SelectorMode::RuntimeOnly,
|
|
443
|
-
) {
|
|
444
|
-
Ok(selected) => selected,
|
|
445
|
-
Err(error) => {
|
|
446
|
-
return Ok(CmdResult::from_json(
|
|
447
|
-
json!({
|
|
448
|
-
"ok": false,
|
|
449
|
-
"error": error.to_string(),
|
|
450
|
-
"workspace": args.workspace.to_string_lossy().to_string(),
|
|
451
|
-
}),
|
|
452
|
-
args.json,
|
|
453
|
-
));
|
|
454
|
-
}
|
|
455
|
-
};
|
|
456
|
-
let value = match if team.is_some() {
|
|
457
|
-
messaging::collect_for_team(
|
|
458
|
-
&selected.run_workspace,
|
|
459
|
-
args.result_file.as_deref(),
|
|
460
|
-
false,
|
|
461
|
-
Some(&selected.team_key),
|
|
462
|
-
)
|
|
463
|
-
} else {
|
|
464
|
-
messaging::collect(&selected.run_workspace, args.result_file.as_deref(), false)
|
|
465
|
-
} {
|
|
466
|
-
Ok(value) => value,
|
|
467
|
-
Err(error) => {
|
|
468
|
-
return Ok(CmdResult::from_json(
|
|
469
|
-
json!({
|
|
470
|
-
"ok": false,
|
|
471
|
-
"error": error.to_string(),
|
|
472
|
-
"workspace": selected.run_workspace.to_string_lossy().to_string(),
|
|
473
|
-
}),
|
|
474
|
-
args.json,
|
|
475
|
-
));
|
|
476
|
-
}
|
|
477
|
-
};
|
|
478
|
-
let results = value.get("results").cloned().unwrap_or_else(|| json!({}));
|
|
479
|
-
let ok = value.get("ok").and_then(Value::as_bool).unwrap_or(true);
|
|
480
|
-
Ok(CmdResult::from_json(
|
|
481
|
-
json!({
|
|
482
|
-
"collected": [],
|
|
483
|
-
"collected_results": value.get("collected_results").cloned().unwrap_or_else(|| json!([])),
|
|
484
|
-
"coordinator": {
|
|
485
|
-
"ok": false,
|
|
486
|
-
"status": "not_required",
|
|
487
|
-
},
|
|
488
|
-
"delivered_messages": value.get("delivered_messages").cloned().unwrap_or_else(|| json!([])),
|
|
489
|
-
"invalid_results": value.get("invalid_results").cloned().unwrap_or_else(|| json!([])),
|
|
490
|
-
"ok": ok,
|
|
491
|
-
"results": results,
|
|
492
|
-
"state_file": value
|
|
493
|
-
.get("state_file")
|
|
494
|
-
.cloned()
|
|
495
|
-
.unwrap_or_else(|| {
|
|
496
|
-
json!(selected
|
|
497
|
-
.spec_workspace
|
|
498
|
-
.as_deref()
|
|
499
|
-
.unwrap_or(&selected.run_workspace)
|
|
500
|
-
.join("team_state.md")
|
|
501
|
-
.to_string_lossy()
|
|
502
|
-
.to_string())
|
|
503
|
-
}),
|
|
504
|
-
}),
|
|
505
|
-
args.json,
|
|
506
|
-
))
|
|
507
|
-
}
|
|
508
|
-
|
|
509
432
|
pub fn cmd_results(args: &ResultsArgs) -> Result<CmdResult, CliError> {
|
|
510
433
|
let selected = match crate::state::selector::resolve_active_team(
|
|
511
434
|
&args.workspace,
|
|
@@ -846,14 +769,14 @@ fn run_fake_e2e(workspace: &Path) -> Result<Value, CliError> {
|
|
|
846
769
|
.mark(message_id, "delivered", None)
|
|
847
770
|
.map_err(crate::messaging::MessagingError::from)?;
|
|
848
771
|
}
|
|
849
|
-
let
|
|
772
|
+
let report = messaging::report_result(
|
|
850
773
|
workspace,
|
|
851
774
|
&json!({
|
|
852
775
|
"schema_version": "result_envelope_v1",
|
|
853
776
|
"task_id": "task_impl",
|
|
854
777
|
"agent_id": "fake_impl",
|
|
855
778
|
"status": "success",
|
|
856
|
-
"summary": "fake result
|
|
779
|
+
"summary": "fake result finalized",
|
|
857
780
|
"changes": [],
|
|
858
781
|
"tests": [],
|
|
859
782
|
"risks": [],
|
|
@@ -861,25 +784,29 @@ fn run_fake_e2e(workspace: &Path) -> Result<Value, CliError> {
|
|
|
861
784
|
"next_actions": [],
|
|
862
785
|
}),
|
|
863
786
|
)?;
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
.
|
|
870
|
-
|
|
871
|
-
|
|
787
|
+
let auto_finalized = report
|
|
788
|
+
.get("finalization_status")
|
|
789
|
+
.and_then(Value::as_str)
|
|
790
|
+
== Some("auto_finalized");
|
|
791
|
+
let shutdown = fake_shutdown(workspace)?;
|
|
792
|
+
let ok = launch.get("ok").and_then(Value::as_bool) == Some(true)
|
|
793
|
+
&& send.ok
|
|
794
|
+
&& auto_finalized
|
|
795
|
+
&& shutdown.get("ok").and_then(Value::as_bool) == Some(true);
|
|
796
|
+
return Ok(json!({
|
|
797
|
+
"ok": ok,
|
|
798
|
+
"launch": launch,
|
|
799
|
+
"send": send_value,
|
|
800
|
+
"report": report,
|
|
801
|
+
"shutdown": shutdown,
|
|
802
|
+
}));
|
|
872
803
|
}
|
|
873
804
|
let shutdown = fake_shutdown(workspace)?;
|
|
874
|
-
let ok = launch.get("ok").and_then(Value::as_bool) == Some(true)
|
|
875
|
-
&& send.ok
|
|
876
|
-
&& collected
|
|
877
|
-
&& shutdown.get("ok").and_then(Value::as_bool) == Some(true);
|
|
878
805
|
Ok(json!({
|
|
879
|
-
"ok":
|
|
806
|
+
"ok": false,
|
|
880
807
|
"launch": launch,
|
|
881
808
|
"send": send_value,
|
|
882
|
-
"
|
|
809
|
+
"report": null,
|
|
883
810
|
"shutdown": shutdown,
|
|
884
811
|
}))
|
|
885
812
|
}
|
|
@@ -1286,89 +1213,82 @@ pub fn cmd_inbox(args: &InboxArgs) -> Result<CmdResult, CliError> {
|
|
|
1286
1213
|
&selected.run_workspace,
|
|
1287
1214
|
&args.agent,
|
|
1288
1215
|
args.limit,
|
|
1289
|
-
args.since.as_deref(),
|
|
1290
|
-
args.json,
|
|
1291
1216
|
Some(&selected.team_key),
|
|
1292
1217
|
)?;
|
|
1293
1218
|
if args.json {
|
|
1294
1219
|
Ok(CmdResult::from_json(value, true))
|
|
1295
1220
|
} else {
|
|
1296
|
-
Ok(CmdResult::human(format_inbox_human(
|
|
1297
|
-
&selected.run_workspace,
|
|
1298
|
-
&args.agent,
|
|
1299
|
-
args.since.as_deref(),
|
|
1300
|
-
&value,
|
|
1301
|
-
Some(&selected.team_key),
|
|
1302
|
-
)?))
|
|
1221
|
+
Ok(CmdResult::human(format_inbox_human(&args.agent, &value)))
|
|
1303
1222
|
}
|
|
1304
1223
|
}
|
|
1305
1224
|
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
) ->
|
|
1225
|
+
const INBOX_LINE_LIMIT_BYTES: usize = 160;
|
|
1226
|
+
|
|
1227
|
+
fn clean_inbox_field(value: &str) -> String {
|
|
1228
|
+
value.chars().filter(|ch| !ch.is_control()).collect()
|
|
1229
|
+
}
|
|
1230
|
+
|
|
1231
|
+
fn truncate_inbox_line(line: &str) -> String {
|
|
1232
|
+
if line.len() <= INBOX_LINE_LIMIT_BYTES {
|
|
1233
|
+
return line.to_string();
|
|
1234
|
+
}
|
|
1235
|
+
let budget = INBOX_LINE_LIMIT_BYTES.saturating_sub("…".len());
|
|
1236
|
+
let mut end = 0;
|
|
1237
|
+
for (index, ch) in line.char_indices() {
|
|
1238
|
+
let next = index + ch.len_utf8();
|
|
1239
|
+
if next > budget {
|
|
1240
|
+
break;
|
|
1241
|
+
}
|
|
1242
|
+
end = next;
|
|
1243
|
+
}
|
|
1244
|
+
format!("{}…", &line[..end])
|
|
1245
|
+
}
|
|
1246
|
+
|
|
1247
|
+
fn format_inbox_human(agent: &str, value: &Value) -> String {
|
|
1313
1248
|
let messages = value
|
|
1314
1249
|
.get("messages")
|
|
1315
1250
|
.and_then(Value::as_array)
|
|
1316
1251
|
.cloned()
|
|
1317
1252
|
.unwrap_or_default();
|
|
1318
|
-
let mut lines = Vec::new();
|
|
1319
1253
|
if messages.is_empty() {
|
|
1320
|
-
|
|
1321
|
-
if let Some(since) = since {
|
|
1322
|
-
line.push_str(" since ");
|
|
1323
|
-
line.push_str(since);
|
|
1324
|
-
}
|
|
1325
|
-
lines.push(line);
|
|
1326
|
-
} else {
|
|
1327
|
-
lines.push(format!("{agent}: {} message(s)", messages.len()));
|
|
1328
|
-
for message in messages {
|
|
1329
|
-
let sender = message.get("sender").and_then(Value::as_str).unwrap_or("-");
|
|
1330
|
-
let content = message.get("content").and_then(Value::as_str).unwrap_or("");
|
|
1331
|
-
let status = message.get("status").and_then(Value::as_str).unwrap_or("-");
|
|
1332
|
-
let attempts = message
|
|
1333
|
-
.get("delivery_attempts")
|
|
1334
|
-
.and_then(Value::as_i64)
|
|
1335
|
-
.unwrap_or(0);
|
|
1336
|
-
let error = message.get("error").and_then(Value::as_str).unwrap_or("-");
|
|
1337
|
-
lines.push(format!(
|
|
1338
|
-
"- {sender}: {content} [status={status} attempts={attempts} error={error}]"
|
|
1339
|
-
));
|
|
1340
|
-
}
|
|
1254
|
+
return format!("{agent}: no messages");
|
|
1341
1255
|
}
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1256
|
+
messages
|
|
1257
|
+
.iter()
|
|
1258
|
+
.map(|message| {
|
|
1259
|
+
let id = clean_inbox_field(
|
|
1260
|
+
message
|
|
1261
|
+
.get("message_id")
|
|
1262
|
+
.and_then(Value::as_str)
|
|
1263
|
+
.unwrap_or("-"),
|
|
1264
|
+
);
|
|
1265
|
+
let sender = clean_inbox_field(
|
|
1266
|
+
message.get("sender").and_then(Value::as_str).unwrap_or("-"),
|
|
1267
|
+
);
|
|
1268
|
+
let recipient = clean_inbox_field(
|
|
1269
|
+
message
|
|
1270
|
+
.get("recipient")
|
|
1271
|
+
.and_then(Value::as_str)
|
|
1272
|
+
.unwrap_or("-"),
|
|
1273
|
+
);
|
|
1274
|
+
let status = clean_inbox_field(
|
|
1275
|
+
message.get("status").and_then(Value::as_str).unwrap_or("-"),
|
|
1276
|
+
);
|
|
1277
|
+
let time = clean_inbox_field(
|
|
1278
|
+
message
|
|
1279
|
+
.get("created_at")
|
|
1280
|
+
.and_then(Value::as_str)
|
|
1281
|
+
.unwrap_or("-"),
|
|
1282
|
+
);
|
|
1283
|
+
let summary = clean_inbox_field(
|
|
1284
|
+
message.get("summary").and_then(Value::as_str).unwrap_or(""),
|
|
1285
|
+
);
|
|
1286
|
+
truncate_inbox_line(&format!(
|
|
1287
|
+
"[{id}] [{sender} -> {recipient}] [{status}] [{time}] [{summary}]"
|
|
1288
|
+
))
|
|
1289
|
+
})
|
|
1290
|
+
.collect::<Vec<_>>()
|
|
1291
|
+
.join("\n")
|
|
1372
1292
|
}
|
|
1373
1293
|
|
|
1374
1294
|
/// `cmd_takeover`(`commands.py:152`)。
|
|
@@ -1629,11 +1549,31 @@ mod tests {
|
|
|
1629
1549
|
#![allow(clippy::unwrap_used)]
|
|
1630
1550
|
|
|
1631
1551
|
use super::{
|
|
1632
|
-
agent_pane_id, append_send_guidance, quickstart_human, send_command,
|
|
1552
|
+
agent_pane_id, append_send_guidance, format_inbox_human, quickstart_human, send_command,
|
|
1553
|
+
split_shell_argv,
|
|
1633
1554
|
};
|
|
1634
1555
|
use serde_json::{json, Value};
|
|
1635
1556
|
use std::path::{Path, PathBuf};
|
|
1636
1557
|
|
|
1558
|
+
#[test]
|
|
1559
|
+
fn inbox_human_output_sanitizes_controls_and_caps_each_line() {
|
|
1560
|
+
let value = json!({
|
|
1561
|
+
"messages": [{
|
|
1562
|
+
"message_id": "msg-\u{001b}[31m".repeat(20),
|
|
1563
|
+
"sender": "sender\t\u{001b}[2J".repeat(20),
|
|
1564
|
+
"recipient": "worker".repeat(20),
|
|
1565
|
+
"status": "delivered",
|
|
1566
|
+
"created_at": "2026-09-19T00:00:00Z",
|
|
1567
|
+
"summary": "中".repeat(120)
|
|
1568
|
+
}]
|
|
1569
|
+
});
|
|
1570
|
+
let output = format_inbox_human("worker", &value);
|
|
1571
|
+
for line in output.lines() {
|
|
1572
|
+
assert!(line.len() <= 160, "line is {} bytes: {line:?}", line.len());
|
|
1573
|
+
assert!(!line.chars().any(char::is_control), "controls leaked: {line:?}");
|
|
1574
|
+
}
|
|
1575
|
+
}
|
|
1576
|
+
|
|
1637
1577
|
// E13:happy 人类输出必须带 attach 块(此前 else 分支只打 summary 丢 attach_commands)。
|
|
1638
1578
|
#[test]
|
|
1639
1579
|
fn e13_quickstart_human_includes_attach_commands() {
|
|
@@ -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",
|
|
@@ -417,7 +411,7 @@ fn command_help(command: Option<&str>) -> String {
|
|
|
417
411
|
)
|
|
418
412
|
.to_string(),
|
|
419
413
|
Some("allow-peer-talk") => "usage: team-agent allow-peer-talk A B [--workspace WORKSPACE] [--json]".to_string(),
|
|
420
|
-
Some("status") => "usage: team-agent status [AGENT] [--workspace WORKSPACE] [--team TEAM] [--summary|--json] [--detail]\n\n输出七字段:name/provider/runtime_status/activity/health/session_name/tmux_command;人读与 --json
|
|
414
|
+
Some("status") => "usage: team-agent status [AGENT] [--workspace WORKSPACE] [--team TEAM] [--summary|--json] [--detail]\n\n输出七字段:name/provider/runtime_status/activity/health/session_name/tmux_command;人读与 --json 使用同一投影。缺少可靠定位或原生 tmux/process 采样时显示 unknown;tmux_command 可复制到对应目标。--summary/--detail 仅保留兼容性,不增加诊断字段。".to_string(),
|
|
421
415
|
Some("models") => "usage: team-agent models [--provider pi|cursor_agent] [QUERY|--search TEXT] [--json]\n\nLists exact provider model ids with case-insensitive multi-word search across provider, vendor, id, and display name. Cursor uses the local `agent --list-models` catalog.".to_string(),
|
|
422
416
|
Some("leaders") => "usage: team-agent leaders [QUERY|--search TEXT] [--all|--stale] [--json] | --prune [--dry-run] [--json]\n\nLists LIVE leaders by default. Use --all to include retained STALE entries, --stale to inspect only STALE entries, QUERY or --search TEXT to match workspace/team/name fields, and --prune to remove only entries proven terminal by canonical state. --dry-run is valid only with --prune.".to_string(),
|
|
423
417
|
Some("stop") => compat_hidden_help("stop", "usage: team-agent stop [--workspace WORKSPACE] [--team TEAM] [--keep-logs] [--json]"),
|
|
@@ -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,
|
|
@@ -1069,17 +1058,37 @@ fn quick_start_args(args: &[String], cwd: &Path) -> Result<QuickStartArgs, CliEr
|
|
|
1069
1058
|
));
|
|
1070
1059
|
}
|
|
1071
1060
|
let parsed = parse_args(args);
|
|
1072
|
-
let
|
|
1073
|
-
|
|
1074
|
-
.
|
|
1075
|
-
.
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1061
|
+
let explicit_workspace = parsed
|
|
1062
|
+
.workspace
|
|
1063
|
+
.as_deref()
|
|
1064
|
+
.map(|path| resolve_cli_path(cwd, path));
|
|
1065
|
+
let positional_agents_dir = parsed.positionals.first().map(PathBuf::from).map(|path| {
|
|
1066
|
+
if path.is_absolute() {
|
|
1067
|
+
path
|
|
1068
|
+
} else if let Some(workspace) = explicit_workspace.as_ref() {
|
|
1069
|
+
workspace.join(path)
|
|
1070
|
+
} else {
|
|
1071
|
+
cwd.join(path)
|
|
1072
|
+
}
|
|
1073
|
+
});
|
|
1074
|
+
let workspace = explicit_workspace
|
|
1075
|
+
.or_else(|| {
|
|
1076
|
+
let path = positional_agents_dir.as_ref()?;
|
|
1077
|
+
if let Some(run_workspace) = path
|
|
1078
|
+
.ancestors()
|
|
1079
|
+
.find(|ancestor| ancestor.file_name().and_then(|name| name.to_str()) == Some(".team"))
|
|
1080
|
+
.and_then(Path::parent)
|
|
1081
|
+
{
|
|
1082
|
+
return Some(run_workspace.to_path_buf());
|
|
1083
|
+
}
|
|
1084
|
+
path.join("TEAM.md").is_file().then(|| path.to_path_buf())
|
|
1085
|
+
})
|
|
1086
|
+
.unwrap_or_else(|| cwd.to_path_buf());
|
|
1087
|
+
// A positional team directory containing TEAM.md is a complete standalone
|
|
1088
|
+
// workspace. Use it as the runtime workspace unless --workspace explicitly
|
|
1089
|
+
// selects another root; this prevents host owner context/state leaking into
|
|
1090
|
+
// an independent quick-start target.
|
|
1091
|
+
let agents_dir = positional_agents_dir.unwrap_or_else(|| workspace.clone());
|
|
1083
1092
|
// 0.5.x Phase 1d Batch 2: validate the `--backend` literal up-front
|
|
1084
1093
|
// so users get a fast, actionable error instead of a downstream
|
|
1085
1094
|
// factory refusal. Accept the same literals as the factory
|
|
@@ -1418,12 +1427,19 @@ fn approvals_args(args: &[String], cwd: &Path) -> ApprovalsArgs {
|
|
|
1418
1427
|
}
|
|
1419
1428
|
|
|
1420
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
|
+
}
|
|
1421
1438
|
let parsed = parse_args(args);
|
|
1422
1439
|
Ok(InboxArgs {
|
|
1423
1440
|
agent: required_pos(&parsed, 0, "agent")?,
|
|
1424
1441
|
workspace: workspace(&parsed, cwd),
|
|
1425
|
-
limit: parsed.limit.unwrap_or(
|
|
1426
|
-
since: parsed.since,
|
|
1442
|
+
limit: parsed.limit.unwrap_or(3),
|
|
1427
1443
|
json: parsed.json,
|
|
1428
1444
|
team: parsed.team,
|
|
1429
1445
|
})
|
|
@@ -1828,18 +1844,6 @@ fn profile_args(args: &[String], cwd: &Path) -> Result<ProfileArgs, CliError> {
|
|
|
1828
1844
|
})
|
|
1829
1845
|
}
|
|
1830
1846
|
|
|
1831
|
-
fn collect_args(args: &[String], cwd: &Path) -> Result<CollectArgs, CliError> {
|
|
1832
|
-
let parsed = parse_args(args);
|
|
1833
|
-
let workspace = workspace(&parsed, cwd);
|
|
1834
|
-
refuse_if_multi_alive_team_missing_scope("collect", &workspace, parsed.team.as_deref())?;
|
|
1835
|
-
Ok(CollectArgs {
|
|
1836
|
-
workspace,
|
|
1837
|
-
result_file: parsed.result_file,
|
|
1838
|
-
json: parsed.json,
|
|
1839
|
-
team: parsed.team,
|
|
1840
|
-
})
|
|
1841
|
-
}
|
|
1842
|
-
|
|
1843
1847
|
fn results_args(args: &[String], cwd: &Path) -> Result<ResultsArgs, CliError> {
|
|
1844
1848
|
if args
|
|
1845
1849
|
.iter()
|
|
@@ -2329,15 +2333,11 @@ mod tests {
|
|
|
2329
2333
|
"--json",
|
|
2330
2334
|
][..],
|
|
2331
2335
|
),
|
|
2332
|
-
(
|
|
2333
|
-
"collect",
|
|
2334
|
-
&["--workspace", "--team", "--result-file", "--json"][..],
|
|
2335
|
-
),
|
|
2336
2336
|
("stuck-list", &["--workspace", "--team", "--json"][..]),
|
|
2337
2337
|
("approvals", &["--workspace", "--team", "--json"][..]),
|
|
2338
2338
|
(
|
|
2339
2339
|
"inbox",
|
|
2340
|
-
&["--workspace", "--team", "
|
|
2340
|
+
&["--workspace", "--team", "-n", "--limit", "--json"][..],
|
|
2341
2341
|
),
|
|
2342
2342
|
("sessions", &["--workspace", "--team", "--json"][..]),
|
|
2343
2343
|
("diagnose", &["--workspace", "--team", "--json"][..]),
|
|
@@ -2565,6 +2565,30 @@ mod tests {
|
|
|
2565
2565
|
let _ = std::fs::remove_dir_all(&ws);
|
|
2566
2566
|
}
|
|
2567
2567
|
|
|
2568
|
+
#[test]
|
|
2569
|
+
fn ux_quick_start_positional_team_dir_is_standalone_workspace() {
|
|
2570
|
+
let cwd = tmp_workspace();
|
|
2571
|
+
let ws = tmp_workspace();
|
|
2572
|
+
std::fs::write(ws.join("TEAM.md"), "# team\n").unwrap();
|
|
2573
|
+
let args = quick_start_args(&cli_argv(&[&ws.to_string_lossy()]), &cwd).unwrap();
|
|
2574
|
+
assert_eq!(args.workspace, ws);
|
|
2575
|
+
assert_eq!(args.agents_dir, ws);
|
|
2576
|
+
let _ = std::fs::remove_dir_all(&cwd);
|
|
2577
|
+
let _ = std::fs::remove_dir_all(&ws);
|
|
2578
|
+
}
|
|
2579
|
+
|
|
2580
|
+
#[test]
|
|
2581
|
+
fn ux_quick_start_team_dir_under_dot_team_uses_project_workspace() {
|
|
2582
|
+
let cwd = tmp_workspace();
|
|
2583
|
+
let team_dir = cwd.join(".team/current");
|
|
2584
|
+
std::fs::create_dir_all(&team_dir).unwrap();
|
|
2585
|
+
std::fs::write(team_dir.join("TEAM.md"), "# team\n").unwrap();
|
|
2586
|
+
let args = quick_start_args(&cli_argv(&[".team/current"]), &cwd).unwrap();
|
|
2587
|
+
assert_eq!(args.workspace, cwd);
|
|
2588
|
+
assert_eq!(args.agents_dir, team_dir);
|
|
2589
|
+
let _ = std::fs::remove_dir_all(&cwd);
|
|
2590
|
+
}
|
|
2591
|
+
|
|
2568
2592
|
// ── E8 (N38): 未知子命令 → 最近似建议(additive,不破坏 golden invalid-choice 行) ──
|
|
2569
2593
|
#[test]
|
|
2570
2594
|
fn e8_unknown_subcommand_suggests_nearest_known_command() {
|
|
@@ -2621,37 +2645,6 @@ mod tests {
|
|
|
2621
2645
|
);
|
|
2622
2646
|
}
|
|
2623
2647
|
|
|
2624
|
-
#[test]
|
|
2625
|
-
fn refuse_helper_passes_when_explicit_team_provided_even_in_multi_alive() {
|
|
2626
|
-
let ws = tmp_workspace();
|
|
2627
|
-
seed_two_alive_teams_in(&ws);
|
|
2628
|
-
assert!(
|
|
2629
|
-
refuse_if_multi_alive_team_missing_scope("collect", &ws, Some("alpha")).is_ok(),
|
|
2630
|
-
"explicit --team alpha must bypass the ambiguity gate"
|
|
2631
|
-
);
|
|
2632
|
-
}
|
|
2633
|
-
|
|
2634
|
-
#[test]
|
|
2635
|
-
fn refuse_helper_refuses_when_multi_alive_team_and_no_explicit_team() {
|
|
2636
|
-
let ws = tmp_workspace();
|
|
2637
|
-
seed_two_alive_teams_in(&ws);
|
|
2638
|
-
let err = refuse_if_multi_alive_team_missing_scope("collect", &ws, None)
|
|
2639
|
-
.expect_err("multi-alive-team must refuse without --team");
|
|
2640
|
-
let message = err.to_string();
|
|
2641
|
-
assert!(
|
|
2642
|
-
message.contains("multiple alive teams"),
|
|
2643
|
-
"refusal must name the ambiguity; got: {message}"
|
|
2644
|
-
);
|
|
2645
|
-
assert!(
|
|
2646
|
-
message.contains("alpha") && message.contains("beta"),
|
|
2647
|
-
"refusal must list candidate teams; got: {message}"
|
|
2648
|
-
);
|
|
2649
|
-
assert!(
|
|
2650
|
-
message.contains("collect"),
|
|
2651
|
-
"refusal must name the command for diagnostic clarity; got: {message}"
|
|
2652
|
-
);
|
|
2653
|
-
}
|
|
2654
|
-
|
|
2655
2648
|
#[test]
|
|
2656
2649
|
fn stuck_cancel_args_builder_refuses_on_multi_alive_team() {
|
|
2657
2650
|
let ws = tmp_workspace();
|
|
@@ -2664,18 +2657,6 @@ mod tests {
|
|
|
2664
2657
|
);
|
|
2665
2658
|
}
|
|
2666
2659
|
|
|
2667
|
-
#[test]
|
|
2668
|
-
fn collect_args_builder_refuses_on_multi_alive_team() {
|
|
2669
|
-
let ws = tmp_workspace();
|
|
2670
|
-
seed_two_alive_teams_in(&ws);
|
|
2671
|
-
let argv = cli_argv(&["--workspace", &ws.to_string_lossy()]);
|
|
2672
|
-
let err = collect_args(&argv, &ws).expect_err("must refuse");
|
|
2673
|
-
assert!(
|
|
2674
|
-
err.to_string().contains("multiple alive teams"),
|
|
2675
|
-
"collect args builder must surface the refusal; got: {err}"
|
|
2676
|
-
);
|
|
2677
|
-
}
|
|
2678
|
-
|
|
2679
2660
|
#[test]
|
|
2680
2661
|
fn shutdown_args_builder_refuses_on_multi_alive_team() {
|
|
2681
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;
|