@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
package/Cargo.lock
CHANGED
package/Cargo.toml
CHANGED
|
@@ -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,
|
|
@@ -577,7 +500,7 @@ pub fn cmd_diagnose(args: &DiagnoseArgs) -> Result<CmdResult, CliError> {
|
|
|
577
500
|
.join(".team")
|
|
578
501
|
.join("logs")
|
|
579
502
|
.join("events.jsonl");
|
|
580
|
-
return Ok(
|
|
503
|
+
return Ok(crate::cli::triage::report(
|
|
581
504
|
json!({
|
|
582
505
|
"event_log": event_log.to_string_lossy().to_string(),
|
|
583
506
|
"issues": issues,
|
|
@@ -595,9 +518,10 @@ pub fn cmd_diagnose(args: &DiagnoseArgs) -> Result<CmdResult, CliError> {
|
|
|
595
518
|
"suggested_repairs": suggested_repairs,
|
|
596
519
|
}),
|
|
597
520
|
args.json,
|
|
521
|
+
"diagnose",
|
|
598
522
|
));
|
|
599
523
|
}
|
|
600
|
-
let selected = crate::state::selector::
|
|
524
|
+
let selected = crate::state::selector::resolve_active_team_readonly(
|
|
601
525
|
&args.workspace,
|
|
602
526
|
args.team.as_deref(),
|
|
603
527
|
crate::state::selector::SelectorMode::RuntimeOnly,
|
|
@@ -632,7 +556,7 @@ pub fn cmd_diagnose(args: &DiagnoseArgs) -> Result<CmdResult, CliError> {
|
|
|
632
556
|
Some(selected.team_key.as_str()),
|
|
633
557
|
);
|
|
634
558
|
let ok = issues.as_array().is_some_and(Vec::is_empty);
|
|
635
|
-
Ok(
|
|
559
|
+
Ok(crate::cli::triage::report(
|
|
636
560
|
json!({
|
|
637
561
|
"event_log": event_log.to_string_lossy().to_string(),
|
|
638
562
|
"issues": issues,
|
|
@@ -650,6 +574,7 @@ pub fn cmd_diagnose(args: &DiagnoseArgs) -> Result<CmdResult, CliError> {
|
|
|
650
574
|
"suggested_repairs": suggested_repairs,
|
|
651
575
|
}),
|
|
652
576
|
args.json,
|
|
577
|
+
"diagnose",
|
|
653
578
|
))
|
|
654
579
|
}
|
|
655
580
|
|
|
@@ -846,14 +771,14 @@ fn run_fake_e2e(workspace: &Path) -> Result<Value, CliError> {
|
|
|
846
771
|
.mark(message_id, "delivered", None)
|
|
847
772
|
.map_err(crate::messaging::MessagingError::from)?;
|
|
848
773
|
}
|
|
849
|
-
let
|
|
774
|
+
let report = messaging::report_result(
|
|
850
775
|
workspace,
|
|
851
776
|
&json!({
|
|
852
777
|
"schema_version": "result_envelope_v1",
|
|
853
778
|
"task_id": "task_impl",
|
|
854
779
|
"agent_id": "fake_impl",
|
|
855
780
|
"status": "success",
|
|
856
|
-
"summary": "fake result
|
|
781
|
+
"summary": "fake result finalized",
|
|
857
782
|
"changes": [],
|
|
858
783
|
"tests": [],
|
|
859
784
|
"risks": [],
|
|
@@ -861,25 +786,29 @@ fn run_fake_e2e(workspace: &Path) -> Result<Value, CliError> {
|
|
|
861
786
|
"next_actions": [],
|
|
862
787
|
}),
|
|
863
788
|
)?;
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
.
|
|
870
|
-
|
|
871
|
-
|
|
789
|
+
let auto_finalized = report
|
|
790
|
+
.get("finalization_status")
|
|
791
|
+
.and_then(Value::as_str)
|
|
792
|
+
== Some("auto_finalized");
|
|
793
|
+
let shutdown = fake_shutdown(workspace)?;
|
|
794
|
+
let ok = launch.get("ok").and_then(Value::as_bool) == Some(true)
|
|
795
|
+
&& send.ok
|
|
796
|
+
&& auto_finalized
|
|
797
|
+
&& shutdown.get("ok").and_then(Value::as_bool) == Some(true);
|
|
798
|
+
return Ok(json!({
|
|
799
|
+
"ok": ok,
|
|
800
|
+
"launch": launch,
|
|
801
|
+
"send": send_value,
|
|
802
|
+
"report": report,
|
|
803
|
+
"shutdown": shutdown,
|
|
804
|
+
}));
|
|
872
805
|
}
|
|
873
806
|
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
807
|
Ok(json!({
|
|
879
|
-
"ok":
|
|
808
|
+
"ok": false,
|
|
880
809
|
"launch": launch,
|
|
881
810
|
"send": send_value,
|
|
882
|
-
"
|
|
811
|
+
"report": null,
|
|
883
812
|
"shutdown": shutdown,
|
|
884
813
|
}))
|
|
885
814
|
}
|
|
@@ -1286,89 +1215,82 @@ pub fn cmd_inbox(args: &InboxArgs) -> Result<CmdResult, CliError> {
|
|
|
1286
1215
|
&selected.run_workspace,
|
|
1287
1216
|
&args.agent,
|
|
1288
1217
|
args.limit,
|
|
1289
|
-
args.since.as_deref(),
|
|
1290
|
-
args.json,
|
|
1291
1218
|
Some(&selected.team_key),
|
|
1292
1219
|
)?;
|
|
1293
1220
|
if args.json {
|
|
1294
1221
|
Ok(CmdResult::from_json(value, true))
|
|
1295
1222
|
} 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
|
-
)?))
|
|
1223
|
+
Ok(CmdResult::human(format_inbox_human(&args.agent, &value)))
|
|
1303
1224
|
}
|
|
1304
1225
|
}
|
|
1305
1226
|
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
) ->
|
|
1227
|
+
const INBOX_LINE_LIMIT_BYTES: usize = 160;
|
|
1228
|
+
|
|
1229
|
+
fn clean_inbox_field(value: &str) -> String {
|
|
1230
|
+
value.chars().filter(|ch| !ch.is_control()).collect()
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1233
|
+
fn truncate_inbox_line(line: &str) -> String {
|
|
1234
|
+
if line.len() <= INBOX_LINE_LIMIT_BYTES {
|
|
1235
|
+
return line.to_string();
|
|
1236
|
+
}
|
|
1237
|
+
let budget = INBOX_LINE_LIMIT_BYTES.saturating_sub("…".len());
|
|
1238
|
+
let mut end = 0;
|
|
1239
|
+
for (index, ch) in line.char_indices() {
|
|
1240
|
+
let next = index + ch.len_utf8();
|
|
1241
|
+
if next > budget {
|
|
1242
|
+
break;
|
|
1243
|
+
}
|
|
1244
|
+
end = next;
|
|
1245
|
+
}
|
|
1246
|
+
format!("{}…", &line[..end])
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1249
|
+
fn format_inbox_human(agent: &str, value: &Value) -> String {
|
|
1313
1250
|
let messages = value
|
|
1314
1251
|
.get("messages")
|
|
1315
1252
|
.and_then(Value::as_array)
|
|
1316
1253
|
.cloned()
|
|
1317
1254
|
.unwrap_or_default();
|
|
1318
|
-
let mut lines = Vec::new();
|
|
1319
1255
|
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
|
-
}
|
|
1341
|
-
}
|
|
1342
|
-
let pending = uncollected_result_count(workspace, owner_team_id)?;
|
|
1343
|
-
let mut note = "final results are not in inbox; use team-agent collect".to_string();
|
|
1344
|
-
if pending > 0 {
|
|
1345
|
-
note.push_str(&format!(" ({pending} uncollected result(s) pending)"));
|
|
1346
|
-
}
|
|
1347
|
-
lines.push(note);
|
|
1348
|
-
Ok(lines.join("\n"))
|
|
1349
|
-
}
|
|
1350
|
-
|
|
1351
|
-
fn uncollected_result_count(
|
|
1352
|
-
workspace: &Path,
|
|
1353
|
-
owner_team_id: Option<&str>,
|
|
1354
|
-
) -> Result<i64, CliError> {
|
|
1355
|
-
let store = crate::message_store::MessageStore::open(workspace)
|
|
1356
|
-
.map_err(|e| CliError::Runtime(e.to_string()))?;
|
|
1357
|
-
let conn = crate::db::schema::open_db(store.db_path())
|
|
1358
|
-
.map_err(|e| CliError::Runtime(e.to_string()))?;
|
|
1359
|
-
match owner_team_id {
|
|
1360
|
-
Some(team) => conn.query_row(
|
|
1361
|
-
"select count(*) from results where status not in ('collected', 'invalid') and owner_team_id = ?1",
|
|
1362
|
-
[team],
|
|
1363
|
-
|row| row.get::<_, i64>(0),
|
|
1364
|
-
),
|
|
1365
|
-
None => conn.query_row(
|
|
1366
|
-
"select count(*) from results where status not in ('collected', 'invalid')",
|
|
1367
|
-
[],
|
|
1368
|
-
|row| row.get::<_, i64>(0),
|
|
1369
|
-
),
|
|
1256
|
+
return format!("{agent}: no messages");
|
|
1370
1257
|
}
|
|
1371
|
-
|
|
1258
|
+
messages
|
|
1259
|
+
.iter()
|
|
1260
|
+
.map(|message| {
|
|
1261
|
+
let id = clean_inbox_field(
|
|
1262
|
+
message
|
|
1263
|
+
.get("message_id")
|
|
1264
|
+
.and_then(Value::as_str)
|
|
1265
|
+
.unwrap_or("-"),
|
|
1266
|
+
);
|
|
1267
|
+
let sender = clean_inbox_field(
|
|
1268
|
+
message.get("sender").and_then(Value::as_str).unwrap_or("-"),
|
|
1269
|
+
);
|
|
1270
|
+
let recipient = clean_inbox_field(
|
|
1271
|
+
message
|
|
1272
|
+
.get("recipient")
|
|
1273
|
+
.and_then(Value::as_str)
|
|
1274
|
+
.unwrap_or("-"),
|
|
1275
|
+
);
|
|
1276
|
+
let status = clean_inbox_field(
|
|
1277
|
+
message.get("status").and_then(Value::as_str).unwrap_or("-"),
|
|
1278
|
+
);
|
|
1279
|
+
let time = clean_inbox_field(
|
|
1280
|
+
message
|
|
1281
|
+
.get("created_at")
|
|
1282
|
+
.and_then(Value::as_str)
|
|
1283
|
+
.unwrap_or("-"),
|
|
1284
|
+
);
|
|
1285
|
+
let summary = clean_inbox_field(
|
|
1286
|
+
message.get("summary").and_then(Value::as_str).unwrap_or(""),
|
|
1287
|
+
);
|
|
1288
|
+
truncate_inbox_line(&format!(
|
|
1289
|
+
"[{id}] [{sender} -> {recipient}] [{status}] [{time}] [{summary}]"
|
|
1290
|
+
))
|
|
1291
|
+
})
|
|
1292
|
+
.collect::<Vec<_>>()
|
|
1293
|
+
.join("\n")
|
|
1372
1294
|
}
|
|
1373
1295
|
|
|
1374
1296
|
/// `cmd_takeover`(`commands.py:152`)。
|
|
@@ -1593,15 +1515,21 @@ pub fn cmd_doctor(args: &DoctorArgs) -> Result<CmdResult, CliError> {
|
|
|
1593
1515
|
args.team.as_deref(),
|
|
1594
1516
|
Some("comms"),
|
|
1595
1517
|
)?;
|
|
1518
|
+
let result = CmdResult::from_json(value, args.json);
|
|
1596
1519
|
if !args.json {
|
|
1597
|
-
let json_tail =
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1520
|
+
let json_tail = match &result.output {
|
|
1521
|
+
CmdOutput::Json(value) => serde_json::to_string_pretty(&sort_json(value))?,
|
|
1522
|
+
_ => String::new(),
|
|
1523
|
+
};
|
|
1524
|
+
return Ok(CmdResult {
|
|
1525
|
+
output: CmdOutput::Human(format!("{COMMS_BOUNDARY_TEXT}\n{json_tail}")),
|
|
1526
|
+
..result
|
|
1527
|
+
});
|
|
1601
1528
|
}
|
|
1602
|
-
return Ok(
|
|
1529
|
+
return Ok(result);
|
|
1603
1530
|
}
|
|
1604
|
-
let
|
|
1531
|
+
let default_report = args.gate.is_none() && !args.cleanup_orphans && !args.fix_schema;
|
|
1532
|
+
let mut value = if matches!(args.gate, Some(DoctorGate::Orphans)) {
|
|
1605
1533
|
crate::diagnose::orphans::orphan_gate_json(&args.workspace, args.fix, args.confirm)?
|
|
1606
1534
|
} else if args.cleanup_orphans {
|
|
1607
1535
|
crate::diagnose::orphans::cleanup_orphans_json(&args.workspace, args.confirm)?
|
|
@@ -1621,7 +1549,32 @@ pub fn cmd_doctor(args: &DoctorArgs) -> Result<CmdResult, CliError> {
|
|
|
1621
1549
|
);
|
|
1622
1550
|
value
|
|
1623
1551
|
};
|
|
1624
|
-
|
|
1552
|
+
if default_report {
|
|
1553
|
+
let base_ok = value.get("ok").and_then(Value::as_bool).unwrap_or(false);
|
|
1554
|
+
let has_blocking_issues = value
|
|
1555
|
+
.get("issues")
|
|
1556
|
+
.and_then(Value::as_array)
|
|
1557
|
+
.is_some_and(|issues| {
|
|
1558
|
+
issues.iter().any(|issue| {
|
|
1559
|
+
let id = issue
|
|
1560
|
+
.as_str()
|
|
1561
|
+
.or_else(|| issue.get("id").and_then(Value::as_str));
|
|
1562
|
+
id != Some("leader_not_attached")
|
|
1563
|
+
})
|
|
1564
|
+
});
|
|
1565
|
+
if let Some(object) = value.as_object_mut() {
|
|
1566
|
+
object.insert(
|
|
1567
|
+
"ok".to_string(),
|
|
1568
|
+
Value::Bool(base_ok && !has_blocking_issues),
|
|
1569
|
+
);
|
|
1570
|
+
}
|
|
1571
|
+
}
|
|
1572
|
+
let result = CmdResult::from_json(value, args.json);
|
|
1573
|
+
Ok(if default_report && !args.json {
|
|
1574
|
+
crate::cli::triage::human_result(result, "doctor")
|
|
1575
|
+
} else {
|
|
1576
|
+
result
|
|
1577
|
+
})
|
|
1625
1578
|
}
|
|
1626
1579
|
|
|
1627
1580
|
#[cfg(test)]
|
|
@@ -1629,11 +1582,31 @@ mod tests {
|
|
|
1629
1582
|
#![allow(clippy::unwrap_used)]
|
|
1630
1583
|
|
|
1631
1584
|
use super::{
|
|
1632
|
-
agent_pane_id, append_send_guidance, quickstart_human, send_command,
|
|
1585
|
+
agent_pane_id, append_send_guidance, format_inbox_human, quickstart_human, send_command,
|
|
1586
|
+
split_shell_argv,
|
|
1633
1587
|
};
|
|
1634
1588
|
use serde_json::{json, Value};
|
|
1635
1589
|
use std::path::{Path, PathBuf};
|
|
1636
1590
|
|
|
1591
|
+
#[test]
|
|
1592
|
+
fn inbox_human_output_sanitizes_controls_and_caps_each_line() {
|
|
1593
|
+
let value = json!({
|
|
1594
|
+
"messages": [{
|
|
1595
|
+
"message_id": "msg-\u{001b}[31m".repeat(20),
|
|
1596
|
+
"sender": "sender\t\u{001b}[2J".repeat(20),
|
|
1597
|
+
"recipient": "worker".repeat(20),
|
|
1598
|
+
"status": "delivered",
|
|
1599
|
+
"created_at": "2026-09-19T00:00:00Z",
|
|
1600
|
+
"summary": "中".repeat(120)
|
|
1601
|
+
}]
|
|
1602
|
+
});
|
|
1603
|
+
let output = format_inbox_human("worker", &value);
|
|
1604
|
+
for line in output.lines() {
|
|
1605
|
+
assert!(line.len() <= 160, "line is {} bytes: {line:?}", line.len());
|
|
1606
|
+
assert!(!line.chars().any(char::is_control), "controls leaked: {line:?}");
|
|
1607
|
+
}
|
|
1608
|
+
}
|
|
1609
|
+
|
|
1637
1610
|
// E13:happy 人类输出必须带 attach 块(此前 else 分支只打 summary 丢 attach_commands)。
|
|
1638
1611
|
#[test]
|
|
1639
1612
|
fn e13_quickstart_human_includes_attach_commands() {
|
|
@@ -96,12 +96,6 @@ pub(crate) fn classify_tmux_server_error(error_text: &str) -> &'static str {
|
|
|
96
96
|
pub(crate) fn diagnose_runtime(state: &Value, backend: &dyn Transport) -> (Value, Value) {
|
|
97
97
|
let mut issues = Vec::new();
|
|
98
98
|
let mut repairs = Vec::new();
|
|
99
|
-
for issue in crate::topology::diagnose_topology_issues(state, backend) {
|
|
100
|
-
if let Some(id) = crate::topology::issue_id(&issue) {
|
|
101
|
-
repairs.push(topology_repair_hint(id));
|
|
102
|
-
}
|
|
103
|
-
issues.push(issue);
|
|
104
|
-
}
|
|
105
99
|
|
|
106
100
|
if state
|
|
107
101
|
.get("leader_receiver")
|
|
@@ -118,41 +112,63 @@ pub(crate) fn diagnose_runtime(state: &Value, backend: &dyn Transport) -> (Value
|
|
|
118
112
|
));
|
|
119
113
|
}
|
|
120
114
|
|
|
121
|
-
|
|
115
|
+
// Probe the session before any topology enumeration. A missing session is
|
|
116
|
+
// terminal for all window/pane/socket probes; repeating those tmux calls
|
|
117
|
+
// only pays the dead-socket timeout again on every layer. The persisted
|
|
118
|
+
// endpoint/socket split is state-only, so preserve that fact before the
|
|
119
|
+
// short-circuit without probing either dead endpoint.
|
|
120
|
+
let persisted_socket_conflict = crate::topology::endpoint_socket_conflict(state);
|
|
121
|
+
let session_unavailable = state
|
|
122
122
|
.get("session_name")
|
|
123
123
|
.and_then(Value::as_str)
|
|
124
124
|
.filter(|s| !s.is_empty())
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
Err(error) => {
|
|
139
|
-
// 0.5.39 Slice 1 (tmux-server-death-locate §11.1 B):
|
|
140
|
-
// when the transport error stderr matches "server exited
|
|
141
|
-
// unexpectedly", the physical layer that disappeared is
|
|
142
|
-
// the tmux server itself — not just this team's session.
|
|
143
|
-
// Surface `tmux_server_crashed` so the user's next
|
|
144
|
-
// action is a coordinator/host-level recovery, not a
|
|
145
|
-
// per-agent `restart <agent>`.
|
|
146
|
-
let error_str = error.to_string();
|
|
147
|
-
let issue_id = classify_tmux_server_error(&error_str);
|
|
148
|
-
issues.push(json!(issue_id));
|
|
149
|
-
let mut hint = recovery_hint(session_name, issue_id, "team-agent diagnose");
|
|
150
|
-
if let Some(obj) = hint.as_object_mut() {
|
|
151
|
-
obj.insert("reason".to_string(), Value::String(error_str));
|
|
125
|
+
.is_some_and(|session_name| {
|
|
126
|
+
match backend.has_session(&crate::transport::SessionName::new(
|
|
127
|
+
session_name.to_string(),
|
|
128
|
+
)) {
|
|
129
|
+
Ok(true) => false,
|
|
130
|
+
Ok(false) => {
|
|
131
|
+
issues.push(json!("tmux_session_missing"));
|
|
132
|
+
repairs.push(recovery_hint(
|
|
133
|
+
session_name,
|
|
134
|
+
"tmux_session_missing",
|
|
135
|
+
"team-agent restart",
|
|
136
|
+
));
|
|
137
|
+
true
|
|
152
138
|
}
|
|
153
|
-
|
|
139
|
+
Err(error) => {
|
|
140
|
+
// 0.5.39 Slice 1 (tmux-server-death-locate §11.1 B):
|
|
141
|
+
// when the transport error stderr matches "server exited
|
|
142
|
+
// unexpectedly", the physical layer that disappeared is
|
|
143
|
+
// the tmux server itself — not just this team's session.
|
|
144
|
+
let error_str = error.to_string();
|
|
145
|
+
let issue_id = classify_tmux_server_error(&error_str);
|
|
146
|
+
issues.push(json!(issue_id));
|
|
147
|
+
let mut hint = recovery_hint(session_name, issue_id, "team-agent diagnose");
|
|
148
|
+
if let Some(obj) = hint.as_object_mut() {
|
|
149
|
+
obj.insert("reason".to_string(), Value::String(error_str));
|
|
150
|
+
}
|
|
151
|
+
repairs.push(hint);
|
|
152
|
+
true
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
if session_unavailable {
|
|
158
|
+
if let Some(issue) = persisted_socket_conflict {
|
|
159
|
+
if let Some(id) = crate::topology::issue_id(&issue) {
|
|
160
|
+
repairs.push(topology_repair_hint(id));
|
|
154
161
|
}
|
|
162
|
+
issues.push(issue);
|
|
163
|
+
}
|
|
164
|
+
return (Value::Array(issues), Value::Array(repairs));
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
for issue in crate::topology::diagnose_topology_issues(state, backend) {
|
|
168
|
+
if let Some(id) = crate::topology::issue_id(&issue) {
|
|
169
|
+
repairs.push(topology_repair_hint(id));
|
|
155
170
|
}
|
|
171
|
+
issues.push(issue);
|
|
156
172
|
}
|
|
157
173
|
|
|
158
174
|
if leader_receiver_attached(state) {
|
|
@@ -624,6 +640,17 @@ pub(crate) fn append_registry_channel_unbound_to_report(
|
|
|
624
640
|
repairs.push(recovery_hint(&team_key, issue_id, repair));
|
|
625
641
|
object.insert("issues".to_string(), Value::Array(issues));
|
|
626
642
|
object.insert("suggested_repairs".to_string(), Value::Array(repairs));
|
|
643
|
+
let is_healthy_unattached_host =
|
|
644
|
+
class == crate::lifecycle::launch::LeaderBindingClass::Unbound
|
|
645
|
+
&& object.get("ok").and_then(Value::as_bool) == Some(true)
|
|
646
|
+
&& object
|
|
647
|
+
.get("profile_smoke")
|
|
648
|
+
.and_then(|profile| profile.get("checks"))
|
|
649
|
+
.and_then(Value::as_array)
|
|
650
|
+
.is_some_and(|checks| !checks.is_empty());
|
|
651
|
+
if !is_healthy_unattached_host {
|
|
652
|
+
object.insert("ok".to_string(), Value::Bool(false));
|
|
653
|
+
}
|
|
627
654
|
}
|
|
628
655
|
|
|
629
656
|
pub(crate) fn append_selected_live_leader_workspace_mismatch(
|
|
@@ -652,7 +679,7 @@ fn selected_live_leader_workspace_mismatch(
|
|
|
652
679
|
workspace: &std::path::Path,
|
|
653
680
|
team: Option<&str>,
|
|
654
681
|
) -> Option<(Value, Value)> {
|
|
655
|
-
let selected = crate::state::selector::
|
|
682
|
+
let selected = crate::state::selector::resolve_active_team_readonly(
|
|
656
683
|
workspace,
|
|
657
684
|
team,
|
|
658
685
|
crate::state::selector::SelectorMode::RuntimeOnly,
|
|
@@ -766,8 +793,10 @@ fn append_coordinator_health_issue(
|
|
|
766
793
|
issues: &mut Value,
|
|
767
794
|
repairs: &mut Value,
|
|
768
795
|
) {
|
|
796
|
+
// Diagnose must observe coordinator state without initializing or rewriting
|
|
797
|
+
// `.team/runtime/team.db`, including when the file is missing or empty.
|
|
769
798
|
let workspace = crate::coordinator::WorkspacePath::new(workspace.to_path_buf());
|
|
770
|
-
let health = crate::coordinator::
|
|
799
|
+
let health = crate::coordinator::coordinator_health_read_only(&workspace);
|
|
771
800
|
let Some(id) = coordinator_issue_id(state, &health) else {
|
|
772
801
|
return;
|
|
773
802
|
};
|
|
@@ -1910,14 +1939,24 @@ pub(crate) fn provider_doctor_checks() -> Value {
|
|
|
1910
1939
|
] {
|
|
1911
1940
|
let adapter = crate::provider::get_adapter(provider);
|
|
1912
1941
|
let name = provider_wire(provider);
|
|
1913
|
-
let
|
|
1942
|
+
let fake = matches!(provider, crate::provider::Provider::Fake);
|
|
1943
|
+
let (auth, version) = if fake {
|
|
1944
|
+
(crate::provider::AuthHintStatus::Present, "fake".to_string())
|
|
1945
|
+
} else {
|
|
1946
|
+
(
|
|
1947
|
+
crate::provider::AuthHintStatus::Unknown,
|
|
1948
|
+
"unknown".to_string(),
|
|
1949
|
+
)
|
|
1950
|
+
};
|
|
1914
1951
|
providers.insert(
|
|
1915
1952
|
name.to_string(),
|
|
1916
1953
|
json!({
|
|
1917
|
-
"auth":
|
|
1954
|
+
"auth": auth,
|
|
1918
1955
|
"command": provider_command(provider),
|
|
1919
1956
|
"installed": adapter.is_installed(),
|
|
1920
1957
|
"version": version,
|
|
1958
|
+
"probe_status": "not_run",
|
|
1959
|
+
"provider_probe_status": "not_run",
|
|
1921
1960
|
}),
|
|
1922
1961
|
);
|
|
1923
1962
|
}
|