@team-agent/installer 0.5.94 → 0.5.97
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 +381 -157
- package/crates/team-agent/src/cli/diagnose.rs +40 -10
- package/crates/team-agent/src/cli/emit.rs +6 -4
- package/crates/team-agent/src/cli/mod.rs +114 -30
- package/crates/team-agent/src/cli/spec.rs +2 -2
- package/crates/team-agent/src/cli/tests/base.rs +20 -10
- package/crates/team-agent/src/cli/tests/divergence.rs +12 -21
- package/crates/team-agent/src/cli/tests/main_preserved.rs +6 -1
- package/crates/team-agent/src/cli/tests/missing_subcommands.rs +20 -11
- package/crates/team-agent/src/cli/tests/shutdown_kill_plan.rs +43 -0
- package/crates/team-agent/src/cli/triage.rs +31 -31
- package/crates/team-agent/src/cli/types.rs +3 -1
- package/crates/team-agent/src/lifecycle/restart/rebuild.rs +26 -0
- package/crates/team-agent/src/lifecycle/tests/claude_profile_launch_red.rs +10 -4
- package/crates/team-agent/src/lifecycle/tests/restart_build_before_destroy_0540_contract.rs +128 -11
- package/package.json +4 -4
package/Cargo.lock
CHANGED
package/Cargo.toml
CHANGED
|
@@ -482,100 +482,22 @@ pub fn cmd_allow_peer_talk(args: &AllowPeerTalkArgs) -> Result<CmdResult, CliErr
|
|
|
482
482
|
Ok(CmdResult::from_json(value, args.json))
|
|
483
483
|
}
|
|
484
484
|
|
|
485
|
-
/// `
|
|
485
|
+
/// `diagnose` is the permanent compatibility spelling for `doctor`.
|
|
486
|
+
/// Keep this wrapper free of probing/reporting logic so direct Rust callers and
|
|
487
|
+
/// the CLI both converge on the canonical handler.
|
|
486
488
|
pub fn cmd_diagnose(args: &DiagnoseArgs) -> Result<CmdResult, CliError> {
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
.
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
.workspace
|
|
500
|
-
.join(".team")
|
|
501
|
-
.join("logs")
|
|
502
|
-
.join("events.jsonl");
|
|
503
|
-
return Ok(crate::cli::triage::report(
|
|
504
|
-
json!({
|
|
505
|
-
"event_log": event_log.to_string_lossy().to_string(),
|
|
506
|
-
"issues": issues,
|
|
507
|
-
"ok": false,
|
|
508
|
-
"providers": provider_doctor_checks(),
|
|
509
|
-
"runtime": {
|
|
510
|
-
"workspace": args.workspace.to_string_lossy().to_string(),
|
|
511
|
-
"team_key": team_hint,
|
|
512
|
-
"session_name": Value::Null,
|
|
513
|
-
"leader_receiver": Value::Null,
|
|
514
|
-
"agent_count": 0,
|
|
515
|
-
"message_count": 0,
|
|
516
|
-
"result_count": 0,
|
|
517
|
-
},
|
|
518
|
-
"suggested_repairs": suggested_repairs,
|
|
519
|
-
}),
|
|
520
|
-
args.json,
|
|
521
|
-
"diagnose",
|
|
522
|
-
));
|
|
523
|
-
}
|
|
524
|
-
let selected = crate::state::selector::resolve_active_team_readonly(
|
|
525
|
-
&args.workspace,
|
|
526
|
-
args.team.as_deref(),
|
|
527
|
-
crate::state::selector::SelectorMode::RuntimeOnly,
|
|
528
|
-
)
|
|
529
|
-
.map_err(|e| CliError::Runtime(e.to_string()))?;
|
|
530
|
-
let state = selected.state;
|
|
531
|
-
let event_log = selected
|
|
532
|
-
.run_workspace
|
|
533
|
-
.join(".team")
|
|
534
|
-
.join("logs")
|
|
535
|
-
.join("events.jsonl");
|
|
536
|
-
// 0.5.x Phase 1d Batch 3: factory-routed diagnose backend so
|
|
537
|
-
// conpty teams get accurate `has_session` / capture from the shim
|
|
538
|
-
// rather than a workspace tmux fallback (which would always miss
|
|
539
|
-
// the ConPTY pane universe).
|
|
540
|
-
let backend: Box<dyn crate::transport::Transport> =
|
|
541
|
-
match crate::transport_factory::resolve_read_only_transport(
|
|
542
|
-
&selected.run_workspace,
|
|
543
|
-
Some(&state),
|
|
544
|
-
crate::transport_factory::TransportPurpose::Diagnose,
|
|
545
|
-
) {
|
|
546
|
-
Ok(r) => r.backend,
|
|
547
|
-
Err(_) => Box::new(crate::tmux_backend::TmuxBackend::for_workspace(
|
|
548
|
-
&selected.run_workspace,
|
|
549
|
-
)),
|
|
550
|
-
};
|
|
551
|
-
let (issues, suggested_repairs) =
|
|
552
|
-
diagnose_runtime_for_workspace(
|
|
553
|
-
&selected.run_workspace,
|
|
554
|
-
&state,
|
|
555
|
-
backend.as_ref(),
|
|
556
|
-
Some(selected.team_key.as_str()),
|
|
557
|
-
);
|
|
558
|
-
let ok = issues.as_array().is_some_and(Vec::is_empty);
|
|
559
|
-
Ok(crate::cli::triage::report(
|
|
560
|
-
json!({
|
|
561
|
-
"event_log": event_log.to_string_lossy().to_string(),
|
|
562
|
-
"issues": issues,
|
|
563
|
-
"ok": ok,
|
|
564
|
-
"providers": provider_doctor_checks(),
|
|
565
|
-
"runtime": {
|
|
566
|
-
"workspace": args.workspace.to_string_lossy().to_string(),
|
|
567
|
-
"team_key": selected.team_key,
|
|
568
|
-
"session_name": state.get("session_name").cloned().unwrap_or(Value::Null),
|
|
569
|
-
"leader_receiver": state.get("leader_receiver").cloned().unwrap_or(Value::Null),
|
|
570
|
-
"agent_count": state.get("agents").and_then(Value::as_object).map_or(0, serde_json::Map::len),
|
|
571
|
-
"message_count": count_dir_entries(&selected.run_workspace.join(".team").join("messages")),
|
|
572
|
-
"result_count": count_dir_entries(&selected.run_workspace.join(".team").join("results")),
|
|
573
|
-
},
|
|
574
|
-
"suggested_repairs": suggested_repairs,
|
|
575
|
-
}),
|
|
576
|
-
args.json,
|
|
577
|
-
"diagnose",
|
|
578
|
-
))
|
|
489
|
+
cmd_doctor(&DoctorArgs {
|
|
490
|
+
spec: None,
|
|
491
|
+
workspace: args.workspace.clone(),
|
|
492
|
+
gate: None,
|
|
493
|
+
comms: false,
|
|
494
|
+
team: args.team.clone(),
|
|
495
|
+
fix: false,
|
|
496
|
+
fix_schema: false,
|
|
497
|
+
cleanup_orphans: false,
|
|
498
|
+
confirm: false,
|
|
499
|
+
json: args.json,
|
|
500
|
+
})
|
|
579
501
|
}
|
|
580
502
|
|
|
581
503
|
/// `cmd_preflight`(`parser.py:160`)。
|
|
@@ -1493,88 +1415,390 @@ pub fn cmd_acknowledge_idle(args: &AcknowledgeIdleArgs) -> Result<CmdResult, Cli
|
|
|
1493
1415
|
))
|
|
1494
1416
|
}
|
|
1495
1417
|
|
|
1496
|
-
/// `
|
|
1497
|
-
///
|
|
1498
|
-
/// 非 gate:`--fix-schema`→`fix_schema`;schema drift→注入;`--cleanup-orphans`→`cleanup_orphans`;
|
|
1499
|
-
/// else→`diagnose_port::doctor(spec)` + schema 注入。返回 `dict | str`(comms 人读 = boundary+json)。
|
|
1418
|
+
/// `doctor` is the single health/diagnostic pipeline. `diagnose` enters here
|
|
1419
|
+
/// through the compatibility wrapper above; it never gets a second facts engine.
|
|
1500
1420
|
pub fn cmd_doctor(args: &DoctorArgs) -> Result<CmdResult, CliError> {
|
|
1501
|
-
if args.fix && args.gate.is_none() {
|
|
1502
|
-
return Err(CliError::Runtime("--fix requires --gate".to_string()));
|
|
1503
|
-
}
|
|
1504
|
-
// swallow batch 3 ①: an unknown gate refuses explicitly (Python commands.py:234-235
|
|
1505
|
-
// `unknown doctor gate`), never an empty default-doctor green.
|
|
1506
1421
|
if let Some(DoctorGate::Unknown(raw)) = &args.gate {
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1422
|
+
let value = json!({
|
|
1423
|
+
"ok": false,
|
|
1424
|
+
"issues": ["unknown_gate"],
|
|
1425
|
+
"suggested_repairs": [{"action": "use --gate orphans or --gate comms", "issue": "unknown_gate"}],
|
|
1426
|
+
"status": "unknown_gate",
|
|
1427
|
+
"gate": raw,
|
|
1428
|
+
});
|
|
1429
|
+
return Ok(crate::cli::triage::report(value, args.json, "doctor"));
|
|
1430
|
+
}
|
|
1431
|
+
if args.fix && args.gate.is_none() {
|
|
1432
|
+
let value = json!({
|
|
1433
|
+
"ok": false,
|
|
1434
|
+
"error": "--fix requires --gate",
|
|
1435
|
+
"issues": ["fix_requires_gate"],
|
|
1436
|
+
"suggested_repairs": [{"issue": "fix_requires_gate", "action": "add --gate orphans|comms"}],
|
|
1437
|
+
});
|
|
1438
|
+
return Ok(crate::cli::triage::report(value, args.json, "doctor"));
|
|
1511
1439
|
}
|
|
1512
|
-
|
|
1513
|
-
|
|
1440
|
+
|
|
1441
|
+
let explicit_comms = args.comms || matches!(args.gate, Some(DoctorGate::Comms));
|
|
1442
|
+
let default_report = !explicit_comms
|
|
1443
|
+
&& args.gate.is_none()
|
|
1444
|
+
&& !args.cleanup_orphans
|
|
1445
|
+
&& !args.fix_schema;
|
|
1446
|
+
let mut value = if explicit_comms {
|
|
1447
|
+
crate::diagnose::comms::doctor_comms_json(
|
|
1514
1448
|
&args.workspace,
|
|
1515
1449
|
args.team.as_deref(),
|
|
1516
1450
|
Some("comms"),
|
|
1517
|
-
)
|
|
1518
|
-
|
|
1519
|
-
if !args.json {
|
|
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
|
-
});
|
|
1528
|
-
}
|
|
1529
|
-
return Ok(result);
|
|
1530
|
-
}
|
|
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)) {
|
|
1451
|
+
)?
|
|
1452
|
+
} else if matches!(args.gate, Some(DoctorGate::Orphans)) {
|
|
1533
1453
|
crate::diagnose::orphans::orphan_gate_json(&args.workspace, args.fix, args.confirm)?
|
|
1534
1454
|
} else if args.cleanup_orphans {
|
|
1535
1455
|
crate::diagnose::orphans::cleanup_orphans_json(&args.workspace, args.confirm)?
|
|
1536
1456
|
} else if args.fix_schema {
|
|
1537
1457
|
diagnose_port::fix_schema(&args.workspace)?
|
|
1538
1458
|
} else {
|
|
1539
|
-
let
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
)
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1459
|
+
let value = diagnose_port::doctor(&args.workspace, args.spec.as_deref())?;
|
|
1460
|
+
unified_default_doctor_report(args, value)
|
|
1461
|
+
};
|
|
1462
|
+
|
|
1463
|
+
if explicit_comms {
|
|
1464
|
+
if let Some(object) = value.as_object_mut() {
|
|
1465
|
+
// The comms probe uses a random run id internally for its disposable
|
|
1466
|
+
// fixture; the public doctor/diagnose report must be alias-stable.
|
|
1467
|
+
object.insert("run_id".to_string(), Value::String("doctor".to_string()));
|
|
1468
|
+
}
|
|
1469
|
+
}
|
|
1470
|
+
if !default_report {
|
|
1471
|
+
finalize_doctor_report(&mut value, false);
|
|
1472
|
+
}
|
|
1473
|
+
let result = crate::cli::triage::report(value, args.json, "doctor");
|
|
1474
|
+
Ok(result)
|
|
1475
|
+
}
|
|
1476
|
+
|
|
1477
|
+
fn unified_default_doctor_report(args: &DoctorArgs, mut value: Value) -> Value {
|
|
1478
|
+
let team_key = args
|
|
1479
|
+
.team
|
|
1480
|
+
.as_deref()
|
|
1481
|
+
.filter(|team| !team.is_empty())
|
|
1482
|
+
.unwrap_or("current");
|
|
1483
|
+
let requested_workspace = args.workspace.to_string_lossy().to_string();
|
|
1484
|
+
let mut runtime = json!({
|
|
1485
|
+
"status": "not_present",
|
|
1486
|
+
"workspace": requested_workspace,
|
|
1487
|
+
"run_workspace": Value::Null,
|
|
1488
|
+
"team_key": team_key,
|
|
1489
|
+
"session_name": Value::Null,
|
|
1490
|
+
"leader_receiver": Value::Null,
|
|
1491
|
+
"agent_count": 0,
|
|
1492
|
+
"message_count": 0,
|
|
1493
|
+
"result_count": 0,
|
|
1494
|
+
});
|
|
1495
|
+
// Resolve explicit selectors and nested team directories before deciding
|
|
1496
|
+
// that runtime inspection is inapplicable.
|
|
1497
|
+
let selected = crate::state::selector::resolve_active_team_readonly(
|
|
1498
|
+
&args.workspace,
|
|
1499
|
+
args.team.as_deref(),
|
|
1500
|
+
crate::state::selector::SelectorMode::RuntimeOnly,
|
|
1501
|
+
);
|
|
1502
|
+
let run_workspace = selected.as_ref().map(|selected| selected.run_workspace.clone())
|
|
1503
|
+
.unwrap_or_else(|_| crate::model::paths::canonical_run_workspace(&args.workspace)
|
|
1504
|
+
.unwrap_or_else(|_| args.workspace.clone()));
|
|
1505
|
+
let runtime_present = selected.as_ref().is_ok_and(|selected| {
|
|
1506
|
+
crate::cli::diagnose::workspace_has_existing_team_runtime(
|
|
1507
|
+
&selected.run_workspace,
|
|
1508
|
+
&selected.team_key,
|
|
1509
|
+
)
|
|
1510
|
+
});
|
|
1511
|
+
let db_present = crate::model::paths::runtime_dir(&run_workspace).join("team.db").exists();
|
|
1512
|
+
// One read-only observation supplies both issue classification and details.
|
|
1513
|
+
// A physical database must be checked even before state.json is created.
|
|
1514
|
+
let health = (runtime_present || db_present).then(|| {
|
|
1515
|
+
crate::coordinator::coordinator_health_read_only(
|
|
1516
|
+
&crate::coordinator::WorkspacePath::new(run_workspace.clone()),
|
|
1517
|
+
)
|
|
1518
|
+
});
|
|
1519
|
+
match selected {
|
|
1520
|
+
Ok(selected) => {
|
|
1521
|
+
if let Some(health) = health.as_ref().filter(|_| runtime_present) {
|
|
1522
|
+
let state = selected.state;
|
|
1523
|
+
let backend: Box<dyn crate::transport::Transport> =
|
|
1524
|
+
match crate::transport_factory::resolve_read_only_transport(
|
|
1525
|
+
&selected.run_workspace,
|
|
1526
|
+
Some(&state),
|
|
1527
|
+
crate::transport_factory::TransportPurpose::Diagnose,
|
|
1528
|
+
) {
|
|
1529
|
+
Ok(resolved) => resolved.backend,
|
|
1530
|
+
Err(_) => Box::new(crate::tmux_backend::TmuxBackend::for_workspace(
|
|
1531
|
+
&selected.run_workspace,
|
|
1532
|
+
)),
|
|
1533
|
+
};
|
|
1534
|
+
let (issues, repairs) = crate::cli::diagnose::diagnose_runtime_for_workspace(
|
|
1535
|
+
&selected.run_workspace,
|
|
1536
|
+
&state,
|
|
1537
|
+
backend.as_ref(),
|
|
1538
|
+
Some(selected.team_key.as_str()),
|
|
1539
|
+
health,
|
|
1540
|
+
);
|
|
1541
|
+
merge_report_array(&mut value, "issues", &issues);
|
|
1542
|
+
merge_report_array(&mut value, "suggested_repairs", &repairs);
|
|
1543
|
+
let run_workspace = selected.run_workspace.to_string_lossy().to_string();
|
|
1544
|
+
runtime = json!({
|
|
1545
|
+
"status": "present",
|
|
1546
|
+
"workspace": args.workspace.to_string_lossy().to_string(),
|
|
1547
|
+
"run_workspace": run_workspace,
|
|
1548
|
+
"team_key": selected.team_key,
|
|
1549
|
+
"session_name": state.get("session_name").cloned().unwrap_or(Value::Null),
|
|
1550
|
+
"leader_receiver": state.get("leader_receiver").cloned().unwrap_or(Value::Null),
|
|
1551
|
+
"agent_count": state.get("agents").and_then(Value::as_object).map_or(0, serde_json::Map::len),
|
|
1552
|
+
"message_count": count_dir_entries(&selected.run_workspace.join(".team").join("messages")),
|
|
1553
|
+
"result_count": count_dir_entries(&selected.run_workspace.join(".team").join("results")),
|
|
1554
|
+
});
|
|
1555
|
+
}
|
|
1556
|
+
}
|
|
1557
|
+
Err(error) => {
|
|
1558
|
+
runtime["status"] = Value::String("unresolved".to_string());
|
|
1559
|
+
append_issue_repair(
|
|
1560
|
+
&mut value,
|
|
1561
|
+
"runtime_selection_failed",
|
|
1562
|
+
format!("unable to select runtime team: {error}"),
|
|
1563
|
+
);
|
|
1564
|
+
}
|
|
1565
|
+
}
|
|
1566
|
+
if let Some(health) = health {
|
|
1567
|
+
if db_present && !health.schema.ok {
|
|
1568
|
+
append_issue_repair(
|
|
1569
|
+
&mut value,
|
|
1570
|
+
"coordinator_schema_incompatible",
|
|
1571
|
+
"team-agent doctor --fix-schema --json".to_string(),
|
|
1572
|
+
);
|
|
1573
|
+
}
|
|
1574
|
+
value["coordinator"] = diagnose_port::coordinator_health_value(health);
|
|
1575
|
+
}
|
|
1576
|
+
if let Some(object) = value.as_object_mut() {
|
|
1577
|
+
object.insert("runtime".to_string(), runtime);
|
|
1578
|
+
object.insert("event_log".to_string(), Value::String(
|
|
1579
|
+
run_workspace.join(".team").join("logs").join("events.jsonl")
|
|
1580
|
+
.to_string_lossy().to_string(),
|
|
1581
|
+
));
|
|
1582
|
+
}
|
|
1583
|
+
finalize_doctor_report(&mut value, true);
|
|
1584
|
+
value
|
|
1585
|
+
}
|
|
1586
|
+
|
|
1587
|
+
fn merge_report_array(report: &mut Value, key: &str, incoming: &Value) {
|
|
1588
|
+
let Some(items) = incoming.as_array() else {
|
|
1589
|
+
return;
|
|
1590
|
+
};
|
|
1591
|
+
let Some(object) = report.as_object_mut() else {
|
|
1592
|
+
return;
|
|
1593
|
+
};
|
|
1594
|
+
let target = object
|
|
1595
|
+
.entry(key.to_string())
|
|
1596
|
+
.or_insert_with(|| Value::Array(Vec::new()));
|
|
1597
|
+
if !target.is_array() {
|
|
1598
|
+
*target = Value::Array(Vec::new());
|
|
1599
|
+
}
|
|
1600
|
+
let target = target
|
|
1601
|
+
.as_array_mut()
|
|
1602
|
+
.expect("report array normalized before merge");
|
|
1603
|
+
for item in items {
|
|
1604
|
+
let identity = report_item_identity(item);
|
|
1605
|
+
if !target.iter().any(|existing| report_item_identity(existing) == identity) {
|
|
1606
|
+
target.push(item.clone());
|
|
1607
|
+
}
|
|
1608
|
+
}
|
|
1609
|
+
}
|
|
1610
|
+
|
|
1611
|
+
fn report_item_identity(item: &Value) -> String {
|
|
1612
|
+
if let Some(text) = item.as_str() {
|
|
1613
|
+
return text.to_string();
|
|
1614
|
+
}
|
|
1615
|
+
let primary = ["id", "code", "issue", "action", "reason", "message"]
|
|
1616
|
+
.iter()
|
|
1617
|
+
.find_map(|key| item.get(*key).and_then(Value::as_str));
|
|
1618
|
+
let scope = ["workspace", "team", "agent", "path", "rule"]
|
|
1619
|
+
.iter()
|
|
1620
|
+
.filter_map(|key| item.get(*key).and_then(Value::as_str))
|
|
1621
|
+
.collect::<Vec<_>>();
|
|
1622
|
+
let line = item.get("line").and_then(Value::as_u64);
|
|
1623
|
+
if primary.is_some() || !scope.is_empty() || line.is_some() {
|
|
1624
|
+
if scope.is_empty() && line.is_none() {
|
|
1625
|
+
return primary.unwrap_or("object").to_string();
|
|
1626
|
+
}
|
|
1627
|
+
return format!(
|
|
1628
|
+
"{}|{}|{}",
|
|
1629
|
+
primary.unwrap_or("object"),
|
|
1630
|
+
scope.join("|"),
|
|
1631
|
+
line.map_or_else(String::new, |line| line.to_string())
|
|
1549
1632
|
);
|
|
1550
|
-
|
|
1633
|
+
}
|
|
1634
|
+
item.to_string()
|
|
1635
|
+
}
|
|
1636
|
+
|
|
1637
|
+
fn append_issue_repair(report: &mut Value, issue: &str, repair: String) {
|
|
1638
|
+
merge_report_array(report, "issues", &json!([issue]));
|
|
1639
|
+
merge_report_array(
|
|
1640
|
+
report,
|
|
1641
|
+
"suggested_repairs",
|
|
1642
|
+
&json!([{"issue": issue, "action": repair}]),
|
|
1643
|
+
);
|
|
1644
|
+
}
|
|
1645
|
+
|
|
1646
|
+
fn finalize_doctor_report(report: &mut Value, default_report: bool) {
|
|
1647
|
+
let Some(object) = report.as_object_mut() else {
|
|
1648
|
+
*report = json!({
|
|
1649
|
+
"ok": false,
|
|
1650
|
+
"issues": ["invalid_doctor_report"],
|
|
1651
|
+
"suggested_repairs": [{"issue": "invalid_doctor_report"}],
|
|
1652
|
+
});
|
|
1653
|
+
return;
|
|
1551
1654
|
};
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1655
|
+
let runtime_status = object
|
|
1656
|
+
.get("runtime")
|
|
1657
|
+
.and_then(|runtime| runtime.get("status"))
|
|
1658
|
+
.and_then(Value::as_str)
|
|
1659
|
+
.unwrap_or("not_run")
|
|
1660
|
+
.to_string();
|
|
1661
|
+
let mut issues = object
|
|
1662
|
+
.remove("issues")
|
|
1663
|
+
.and_then(|value| value.as_array().cloned())
|
|
1664
|
+
.unwrap_or_default();
|
|
1665
|
+
let mut repairs = object
|
|
1666
|
+
.remove("suggested_repairs")
|
|
1667
|
+
.and_then(|value| value.as_array().cloned())
|
|
1668
|
+
.unwrap_or_default();
|
|
1669
|
+
let mut add = |issue: Value, repair: Option<Value>| {
|
|
1670
|
+
if !issues.iter().any(|existing| report_item_identity(existing) == report_item_identity(&issue)) {
|
|
1671
|
+
issues.push(issue);
|
|
1672
|
+
}
|
|
1673
|
+
if let Some(repair) = repair {
|
|
1674
|
+
if !repairs.iter().any(|existing| report_item_identity(existing) == report_item_identity(&repair)) {
|
|
1675
|
+
repairs.push(repair);
|
|
1676
|
+
}
|
|
1677
|
+
}
|
|
1678
|
+
};
|
|
1679
|
+
|
|
1680
|
+
if !default_report && object.get("ok").and_then(Value::as_bool) == Some(false) {
|
|
1681
|
+
let status = object
|
|
1682
|
+
.get("status")
|
|
1683
|
+
.and_then(Value::as_str)
|
|
1684
|
+
.or_else(|| object.get("reason").and_then(Value::as_str))
|
|
1685
|
+
.unwrap_or("doctor_failed");
|
|
1686
|
+
let issue = if status == "failed" {
|
|
1687
|
+
object
|
|
1688
|
+
.get("gate")
|
|
1689
|
+
.and_then(Value::as_str)
|
|
1690
|
+
.map_or_else(|| "doctor_failed".to_string(), |gate| format!("{gate}_failed"))
|
|
1691
|
+
} else if status.ends_with("_failed") || status == "refused" {
|
|
1692
|
+
status.to_string()
|
|
1693
|
+
} else {
|
|
1694
|
+
format!("{status}_failed")
|
|
1695
|
+
};
|
|
1696
|
+
let repair = object
|
|
1697
|
+
.get("action")
|
|
1698
|
+
.or_else(|| object.get("next_action"))
|
|
1699
|
+
.or_else(|| object.get("reason"))
|
|
1700
|
+
.cloned();
|
|
1701
|
+
add(Value::String(issue), repair.map(|value| json!({"action": value})));
|
|
1702
|
+
}
|
|
1703
|
+
if object
|
|
1704
|
+
.get("workspace")
|
|
1705
|
+
.and_then(Value::as_str)
|
|
1706
|
+
.is_some_and(|workspace| !std::path::Path::new(workspace).is_dir())
|
|
1707
|
+
{
|
|
1708
|
+
add(
|
|
1709
|
+
json!("invalid_workspace"),
|
|
1710
|
+
Some(json!({"issue": "invalid_workspace", "action": "use an existing workspace"})),
|
|
1711
|
+
);
|
|
1712
|
+
}
|
|
1713
|
+
if let Some(profile) = object.get("profile_smoke") {
|
|
1714
|
+
if profile.get("ok").and_then(Value::as_bool) == Some(false)
|
|
1715
|
+
&& profile.get("status").and_then(Value::as_str) != Some("legacy_team_invalid")
|
|
1716
|
+
{
|
|
1717
|
+
add(
|
|
1718
|
+
json!("profile_smoke_failed"),
|
|
1719
|
+
profile
|
|
1720
|
+
.get("next_action")
|
|
1721
|
+
.cloned()
|
|
1722
|
+
.map(|action| json!({"issue": "profile_smoke_failed", "action": action})),
|
|
1723
|
+
);
|
|
1724
|
+
}
|
|
1725
|
+
}
|
|
1726
|
+
if let Some(grok) = object.get("grok_slot") {
|
|
1727
|
+
if grok.get("readable").and_then(Value::as_bool) == Some(false)
|
|
1728
|
+
|| grok.get("consistent").and_then(Value::as_bool) == Some(false)
|
|
1729
|
+
{
|
|
1730
|
+
add(
|
|
1731
|
+
json!("grok_slot_mismatch"),
|
|
1732
|
+
grok.get("reason").cloned().map(|reason| {
|
|
1733
|
+
json!({"issue": "grok_slot_mismatch", "action": reason})
|
|
1734
|
+
}),
|
|
1735
|
+
);
|
|
1736
|
+
}
|
|
1737
|
+
}
|
|
1738
|
+
if let Some(findings) = object
|
|
1739
|
+
.get("secret_scan")
|
|
1740
|
+
.and_then(|scan| scan.get("findings"))
|
|
1741
|
+
.and_then(Value::as_array)
|
|
1742
|
+
{
|
|
1743
|
+
for finding in findings {
|
|
1744
|
+
let Some(finding_object) = finding.as_object() else {
|
|
1745
|
+
continue;
|
|
1746
|
+
};
|
|
1747
|
+
let Some(rule) = finding_object.get("rule").and_then(Value::as_str) else {
|
|
1748
|
+
continue;
|
|
1749
|
+
};
|
|
1750
|
+
let path = finding_object
|
|
1751
|
+
.get("path")
|
|
1752
|
+
.and_then(Value::as_str)
|
|
1753
|
+
.unwrap_or("unknown");
|
|
1754
|
+
let line = finding_object
|
|
1755
|
+
.get("line")
|
|
1756
|
+
.and_then(Value::as_u64)
|
|
1757
|
+
.unwrap_or(0);
|
|
1758
|
+
let issue = json!({
|
|
1759
|
+
"id": "secret_scan_finding",
|
|
1760
|
+
"rule": rule,
|
|
1761
|
+
"path": path,
|
|
1762
|
+
"line": line,
|
|
1564
1763
|
});
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1764
|
+
add(
|
|
1765
|
+
issue,
|
|
1766
|
+
Some(json!({
|
|
1767
|
+
"issue": "secret_scan_finding",
|
|
1768
|
+
"rule": rule,
|
|
1769
|
+
"path": path,
|
|
1770
|
+
"line": line,
|
|
1771
|
+
"action": format!("remove secret finding at {path}:{line}"),
|
|
1772
|
+
})),
|
|
1569
1773
|
);
|
|
1570
1774
|
}
|
|
1571
1775
|
}
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
}
|
|
1776
|
+
if runtime_status == "unresolved" {
|
|
1777
|
+
add(
|
|
1778
|
+
json!("runtime_selection_failed"),
|
|
1779
|
+
Some(json!({"issue": "runtime_selection_failed", "action": "select an existing Team runtime"})),
|
|
1780
|
+
);
|
|
1781
|
+
}
|
|
1782
|
+
if runtime_status == "present"
|
|
1783
|
+
&& object
|
|
1784
|
+
.get("coordinator")
|
|
1785
|
+
.and_then(|coordinator| coordinator.get("ok"))
|
|
1786
|
+
.and_then(Value::as_bool)
|
|
1787
|
+
== Some(false)
|
|
1788
|
+
{
|
|
1789
|
+
let reason = object
|
|
1790
|
+
.get("coordinator")
|
|
1791
|
+
.and_then(|coordinator| coordinator.get("metadata_mismatch_reason"))
|
|
1792
|
+
.cloned()
|
|
1793
|
+
.unwrap_or_else(|| json!("coordinator unavailable"));
|
|
1794
|
+
add(
|
|
1795
|
+
json!("coordinator_unavailable"),
|
|
1796
|
+
Some(json!({"issue": "coordinator_unavailable", "action": reason})),
|
|
1797
|
+
);
|
|
1798
|
+
}
|
|
1799
|
+
object.insert("issues".to_string(), Value::Array(issues.clone()));
|
|
1800
|
+
object.insert("suggested_repairs".to_string(), Value::Array(repairs));
|
|
1801
|
+
object.insert("ok".to_string(), Value::Bool(issues.is_empty()));
|
|
1578
1802
|
}
|
|
1579
1803
|
|
|
1580
1804
|
#[cfg(test)]
|