@team-agent/installer 0.5.92 → 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.
Files changed (32) hide show
  1. package/Cargo.lock +1 -1
  2. package/Cargo.toml +1 -1
  3. package/crates/team-agent/src/cli/adapters.rs +381 -157
  4. package/crates/team-agent/src/cli/diagnose.rs +40 -10
  5. package/crates/team-agent/src/cli/emit.rs +6 -4
  6. package/crates/team-agent/src/cli/mod.rs +483 -470
  7. package/crates/team-agent/src/cli/models.rs +1 -0
  8. package/crates/team-agent/src/cli/spec.rs +2 -2
  9. package/crates/team-agent/src/cli/tests/base.rs +20 -10
  10. package/crates/team-agent/src/cli/tests/divergence.rs +12 -21
  11. package/crates/team-agent/src/cli/tests/main_preserved.rs +6 -1
  12. package/crates/team-agent/src/cli/tests/missing_subcommands.rs +20 -11
  13. package/crates/team-agent/src/cli/tests/shutdown_kill_plan.rs +197 -30
  14. package/crates/team-agent/src/cli/triage.rs +31 -31
  15. package/crates/team-agent/src/cli/types.rs +3 -1
  16. package/crates/team-agent/src/coordinator/health.rs +92 -30
  17. package/crates/team-agent/src/coordinator/tick.rs +62 -8
  18. package/crates/team-agent/src/coordinator/types.rs +2 -0
  19. package/crates/team-agent/src/kill_audit.rs +41 -2
  20. package/crates/team-agent/src/lifecycle/launch/quick_start.rs +48 -14
  21. package/crates/team-agent/src/lifecycle/launch/quick_start_transport.rs +4 -8
  22. package/crates/team-agent/src/lifecycle/launch.rs +17 -0
  23. package/crates/team-agent/src/lifecycle/restart/rebuild.rs +26 -0
  24. package/crates/team-agent/src/lifecycle/tests/claude_profile_launch_red.rs +10 -4
  25. package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +10 -7
  26. package/crates/team-agent/src/lifecycle/tests/phase_golden.rs +91 -4
  27. package/crates/team-agent/src/lifecycle/tests/restart_build_before_destroy_0540_contract.rs +128 -11
  28. package/crates/team-agent/src/platform/process.rs +4 -45
  29. package/crates/team-agent/src/tmux_backend.rs +113 -3
  30. package/crates/team-agent/src/transport/test_support.rs +30 -0
  31. package/crates/team-agent/src/transport.rs +41 -0
  32. package/package.json +4 -4
@@ -902,6 +902,7 @@ mod tests {
902
902
 
903
903
  #[cfg(unix)]
904
904
  #[test]
905
+ #[serial_test::serial(env)]
905
906
  fn command_boundary_connects_runner_parser_search_and_contract() {
906
907
  let path = fixture(
907
908
  "printf 'provider model\\nopenai-codex gpt-5.6-sol\\nopenai-codex gpt-5.6-luna\\n'",
@@ -176,7 +176,7 @@ pub(crate) const COMMAND_SPECS: &[CommandSpec] = &[
176
176
  CommandSpec { name: "start-agent", tier: CommandTier::Core, category: CommandCategory::WorkerLifecycle, kind: CommandKind::Dispatch(DispatchKind::StartAgent), summary: "start an existing worker", usage: "usage: team-agent start-agent AGENT [--workspace WORKSPACE] [--team TEAM] [--force] [--allow-fresh] [--no-display] [--json]", default_help: true, command_help: true, suggestion_index: true, token_usage: TokenUsage::Conditional, alias_of: None, sunset: None, action: None, governance: None },
177
177
  CommandSpec { name: "stop-agent", tier: CommandTier::Core, category: CommandCategory::WorkerLifecycle, kind: CommandKind::Dispatch(DispatchKind::StopAgent), summary: "stop a worker", usage: "usage: team-agent stop-agent AGENT [--workspace WORKSPACE] [--team TEAM] [--json]", default_help: true, command_help: true, suggestion_index: true, token_usage: TokenUsage::No, alias_of: None, sunset: None, action: None, governance: None },
178
178
  CommandSpec { name: "reset-agent", tier: CommandTier::Core, category: CommandCategory::WorkerLifecycle, kind: CommandKind::Dispatch(DispatchKind::ResetAgent), summary: "reset a worker session", usage: "usage: team-agent reset-agent AGENT [--workspace WORKSPACE] [--team TEAM] [--discard-session] [--no-display] [--json]", default_help: true, command_help: true, suggestion_index: true, token_usage: TokenUsage::Conditional, alias_of: None, sunset: None, action: None, governance: None },
179
- CommandSpec { name: "diagnose", tier: CommandTier::Core, category: CommandCategory::Observe, kind: CommandKind::Dispatch(DispatchKind::Diagnose), summary: "inspect this workspace runtime and suggested repairs", usage: "usage: team-agent diagnose [--workspace WORKSPACE] [--team TEAM] [--json]", default_help: true, command_help: true, suggestion_index: true, token_usage: TokenUsage::No, alias_of: None, sunset: None, action: None, governance: None },
179
+ CommandSpec { name: "diagnose", tier: CommandTier::CompatHidden, category: CommandCategory::Compat, kind: CommandKind::Dispatch(DispatchKind::Diagnose), summary: "compatibility alias for doctor", usage: "usage: team-agent doctor [SPEC] [--workspace WORKSPACE] [--team TEAM] [--gate orphans|comms] [--comms] [--fix] [--fix-schema] [--cleanup-orphans] [--confirm] [--json]", default_help: false, command_help: true, suggestion_index: false, token_usage: TokenUsage::No, alias_of: Some("doctor"), sunset: None, action: None, governance: None },
180
180
  CommandSpec { name: "claim-leader", tier: CommandTier::Guided, category: CommandCategory::GuidedRecovery, kind: CommandKind::Dispatch(DispatchKind::ClaimLeader), summary: "bind this pane when diagnose reports rebind_required", usage: "usage: team-agent claim-leader [--workspace WORKSPACE] [--team TEAM] [--confirm] [--json]", default_help: true, command_help: true, suggestion_index: true, token_usage: TokenUsage::No, alias_of: None, sunset: None, action: None, governance: None },
181
181
  CommandSpec { name: "takeover", tier: CommandTier::Guided, category: CommandCategory::GuidedRecovery, kind: CommandKind::Dispatch(DispatchKind::Takeover), summary: "replace a live leader binding with explicit consent", usage: "usage: team-agent takeover [--workspace WORKSPACE] [--team TEAM] [--confirm] [--json]", default_help: true, command_help: true, suggestion_index: true, token_usage: TokenUsage::No, alias_of: None, sunset: None, action: None, governance: None },
182
182
  CommandSpec { name: "attach-leader", tier: CommandTier::Guided, category: CommandCategory::GuidedRecovery, kind: CommandKind::Dispatch(DispatchKind::AttachLeader), summary: "bind an external leader pane", usage: "usage: team-agent attach-leader [--workspace WORKSPACE] [--team TEAM] [--pane PANE] [--provider PROVIDER] [--confirm] [--json]", default_help: true, command_help: true, suggestion_index: true, token_usage: TokenUsage::No, alias_of: None, sunset: None, action: None, governance: None },
@@ -196,7 +196,7 @@ pub(crate) const COMMAND_SPECS: &[CommandSpec] = &[
196
196
  CommandSpec { name: "stuck-cancel", tier: CommandTier::CompatHidden, category: CommandCategory::Compat, kind: CommandKind::Dispatch(DispatchKind::StuckCancel), summary: "legacy stuck alert suppression", usage: "usage: team-agent stuck-cancel AGENT [--workspace WORKSPACE] [--alert-type stuck|idle_fallback|cross_worker_deadlock|all] [--json]", default_help: false, command_help: true, suggestion_index: false, token_usage: TokenUsage::No, alias_of: None, sunset: Some("C2"), action: Some("use the concrete notification action or `team-agent diagnose`"), governance: None },
197
197
  CommandSpec { name: "acknowledge-idle", tier: CommandTier::CompatHidden, category: CommandCategory::Compat, kind: CommandKind::Dispatch(DispatchKind::AcknowledgeIdle), summary: "legacy idle acknowledgement", usage: "usage: team-agent acknowledge-idle [--workspace WORKSPACE] [--team TEAM] [--json]", default_help: false, command_help: true, suggestion_index: false, token_usage: TokenUsage::No, alias_of: None, sunset: Some("C2"), action: Some("use the concrete idle notification action or `team-agent diagnose`"), governance: None },
198
198
  CommandSpec { name: "leaders", tier: CommandTier::Secondary, category: CommandCategory::Observe, kind: CommandKind::Dispatch(DispatchKind::Leaders), summary: "list live, stale, or ambiguous host leaders", usage: "usage: team-agent leaders [QUERY|--search TEXT] [--all|--stale] [--json] | --prune [--dry-run] [--json]", default_help: false, command_help: true, suggestion_index: false, token_usage: TokenUsage::No, alias_of: None, sunset: None, action: None, governance: None },
199
- CommandSpec { name: "doctor", tier: CommandTier::Secondary, category: CommandCategory::Setup, kind: CommandKind::Dispatch(DispatchKind::Doctor), summary: "run host maintenance checks", usage: "usage: team-agent doctor [SPEC] [--workspace WORKSPACE] [--team TEAM] [--gate orphans|comms] [--comms] [--fix] [--fix-schema] [--cleanup-orphans] [--confirm] [--json]", default_help: false, command_help: true, suggestion_index: false, token_usage: TokenUsage::No, alias_of: None, sunset: None, action: None, governance: None },
199
+ CommandSpec { name: "doctor", tier: CommandTier::Core, category: CommandCategory::Observe, kind: CommandKind::Dispatch(DispatchKind::Doctor), summary: "inspect environment and Team runtime health", usage: "usage: team-agent doctor [SPEC] [--workspace WORKSPACE] [--team TEAM] [--gate orphans|comms] [--comms] [--fix] [--fix-schema] [--cleanup-orphans] [--confirm] [--json]", default_help: true, command_help: true, suggestion_index: true, token_usage: TokenUsage::No, alias_of: None, sunset: None, action: None, governance: None },
200
200
  CommandSpec { name: "attach-app-server-leader", tier: CommandTier::Guided, category: CommandCategory::GuidedRecovery, kind: CommandKind::Dispatch(DispatchKind::AttachAppServerLeader), summary: "bind an app-server leader receiver", usage: "usage: team-agent attach-app-server-leader [--workspace WORKSPACE] [--team TEAM] --socket unix:///path.sock --thread-id THREAD_ID [--json]", default_help: false, command_help: true, suggestion_index: false, token_usage: TokenUsage::No, alias_of: None, sunset: None, action: None, governance: None },
201
201
  CommandSpec { name: "remove-agent", tier: CommandTier::Secondary, category: CommandCategory::WorkerLifecycle, kind: CommandKind::Dispatch(DispatchKind::RemoveAgent), summary: "remove a worker", usage: "usage: team-agent remove-agent AGENT [--workspace WORKSPACE] [--team TEAM] [--from-spec] [--confirm] [--force] [--json]", default_help: false, command_help: true, suggestion_index: false, token_usage: TokenUsage::No, alias_of: None, sunset: None, action: None, governance: None },
202
202
  CommandSpec { name: "fork-agent", tier: CommandTier::Secondary, category: CommandCategory::WorkerLifecycle, kind: CommandKind::Dispatch(DispatchKind::ForkAgent), summary: "fork a worker", usage: "usage: team-agent fork-agent SOURCE_AGENT --as AGENT [--label LABEL] [--workspace WORKSPACE] [--team TEAM] [--no-display] [--json]", default_help: false, command_help: true, suggestion_index: false, token_usage: TokenUsage::Conditional, alias_of: None, sunset: None, action: None, governance: None },
@@ -671,13 +671,12 @@ fn exit_code_numeric() {
671
671
  }
672
672
 
673
673
  // =========================================================================
674
- // cmd_doctor 分派(commands.py:218-260):--fix 缺 gate -> Usage err
674
+ // cmd_doctor: --fix without --gate is a structured failure in both renderers.
675
675
  // =========================================================================
676
676
 
677
677
  #[test]
678
- fn cmd_doctor_fix_without_gate_is_usage_error() {
679
- // commands.py:220-221: --fix and not gate -> TeamAgentError("--fix requires --gate")
680
- let args = DoctorArgs {
678
+ fn cmd_doctor_fix_without_gate_reports_usage_failure() {
679
+ let mut args = DoctorArgs {
681
680
  spec: None,
682
681
  workspace: PathBuf::from("."),
683
682
  gate: None,
@@ -689,12 +688,23 @@ fn cmd_doctor_fix_without_gate_is_usage_error() {
689
688
  confirm: false,
690
689
  json: false,
691
690
  };
692
- let err = cmd_doctor(&args).unwrap_err();
693
- let msg = err.to_string();
694
- assert!(
695
- msg.contains("--fix requires --gate"),
696
- "expected '--fix requires --gate', got: {msg}"
697
- );
691
+ let human = cmd_doctor(&args).expect("structured usage failure");
692
+ assert_eq!(human.exit, ExitCode::Error);
693
+ let CmdOutput::Human(text) = human.output else {
694
+ panic!("expected human triage");
695
+ };
696
+ assert!(text.contains("fix_requires_gate"), "{text}");
697
+ assert!(text.contains("add --gate orphans|comms"), "{text}");
698
+
699
+ args.json = true;
700
+ let result = cmd_doctor(&args).expect("structured JSON usage failure");
701
+ assert_eq!(result.exit, ExitCode::Error);
702
+ let CmdOutput::Json(value) = result.output else {
703
+ panic!("expected JSON usage failure");
704
+ };
705
+ assert_eq!(value["ok"], false);
706
+ assert_eq!(value["error"], "--fix requires --gate");
707
+ assert_eq!(value["issues"], json!(["fix_requires_gate"]));
698
708
  }
699
709
 
700
710
  // =========================================================================
@@ -457,14 +457,10 @@ fn red_send_target_none_is_not_broadcast() {
457
457
  assert_eq!(send_target(None, Some("*")), MessageTarget::Broadcast);
458
458
  }
459
459
 
460
- // ---- #23: cmd_doctor comms (human) returns COMMS_BOUNDARY_TEXT + sorted indented JSON ----
461
- // golden: for --comms WITHOUT --json, cmd_doctor returns the STRING
462
- // f"{COMMS_BOUNDARY_TEXT}\n{json.dumps(result, indent=2, ensure_ascii=False, sort_keys=True)}".
463
- // Rust always does CmdResult::from_json -> CmdOutput::Json (wrong shape).
460
+ // Unified doctor human output is triage only, including the comms gate.
464
461
  #[test]
465
- fn red_cmd_doctor_comms_human_is_boundary_text_plus_sorted_json() {
466
- 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)";
467
- let args = DoctorArgs {
462
+ fn cmd_doctor_comms_human_is_compact_triage_without_boundary_prefix() {
463
+ let mut args = DoctorArgs {
468
464
  spec: None,
469
465
  workspace: PathBuf::from("."),
470
466
  gate: None,
@@ -479,21 +475,16 @@ fn red_cmd_doctor_comms_human_is_boundary_text_plus_sorted_json() {
479
475
  let result = cmd_doctor(&args).expect("comms doctor returns CmdResult");
480
476
  let text = match result.output {
481
477
  CmdOutput::Human(s) => s,
482
- other => panic!(
483
- "comms WITHOUT --json MUST be a Human boundary-text + JSON string, got {other:?}"
484
- ),
478
+ other => panic!("comms WITHOUT --json must be human triage, got {other:?}"),
485
479
  };
486
- assert!(
487
- text.starts_with(&format!("{COMMS_BOUNDARY_TEXT}\n")),
488
- "comms human output MUST start with COMMS_BOUNDARY_TEXT then a newline (golden commands.py:231); got: {text:?}"
489
- );
490
- // the tail is the selftest result rendered as sort_keys+indent=2 JSON (parseable, sorted).
491
- let json_tail = text
492
- .strip_prefix(&format!("{COMMS_BOUNDARY_TEXT}\n"))
493
- .unwrap();
494
- let parsed: Value = serde_json::from_str(json_tail)
495
- .expect("comms human tail MUST be indent=2 sort_keys JSON of the selftest result");
496
- assert!(parsed.is_object(), "comms selftest JSON tail is an object");
480
+ args.json = true;
481
+ let CmdOutput::Json(report) = cmd_doctor(&args).unwrap().output else {
482
+ panic!("--json must preserve the structured report");
483
+ };
484
+ assert_eq!(text, crate::cli::triage::render("doctor", &report));
485
+ assert!(text.starts_with("doctor:"));
486
+ assert_eq!(text.lines().count(), 1 + report["issues"].as_array().unwrap().len()
487
+ + report["suggested_repairs"].as_array().unwrap().len());
497
488
  }
498
489
 
499
490
  // ---- #13 / #27 (P2): run() must NOT treat 'claude_code' as a passthrough trigger ----
@@ -368,12 +368,17 @@ fn ux_doctor_secret_scan_findings_name_the_exact_trigger() {
368
368
  .pointer("/secret_scan/findings/0")
369
369
  .and_then(serde_json::Value::as_object)
370
370
  .expect("secret-scan must report the concrete trigger");
371
- for key in ["path", "line", "rule", "match_excerpt"] {
371
+ for key in ["path", "line", "rule"] {
372
372
  assert!(
373
373
  finding.contains_key(key),
374
374
  "secret-scan finding missing `{key}`: {finding:?}"
375
375
  );
376
376
  }
377
+ assert_eq!(finding["path"], "leaky-role.md");
378
+ assert_eq!(finding["line"], 1);
379
+ assert_eq!(finding["rule"], "api_key_assignment");
380
+ assert!(!finding.contains_key("match_excerpt"));
381
+ assert!(!value.to_string().contains("sk-test-red-contract"));
377
382
  let _ = std::fs::remove_dir_all(&ws);
378
383
  }
379
384
  #[test]
@@ -299,7 +299,7 @@ fn dispatch_routes_sessions_subcommand() {
299
299
  // + NO session_name + NO agents -> issues=[] -> EXIT 0.
300
300
  #[test]
301
301
  #[serial(env)]
302
- fn dispatch_routes_diagnose_healthy_leader() {
302
+ fn dispatch_routes_diagnose_checks_runtime_beyond_healthy_leader() {
303
303
  let _env = EnvUnsetGuard::unset(&[
304
304
  "TMUX",
305
305
  "TMUX_PANE",
@@ -332,17 +332,26 @@ fn dispatch_routes_diagnose_healthy_leader() {
332
332
  pane_pid: None,
333
333
  leader_env: Default::default(),
334
334
  }]);
335
- let code = crate::transport_factory::with_leader_endpoint_transport(endpoint, transport, || {
336
- run(
337
- &cli_argv(&["diagnose", "--workspace", &ws.to_string_lossy(), "--json"]),
338
- &ws,
339
- )
335
+ crate::transport_factory::with_leader_endpoint_transport(endpoint, transport, || {
336
+ for command in ["doctor", "diagnose"] {
337
+ let code = run(
338
+ &cli_argv(&[command, "--workspace", &ws.to_string_lossy(), "--json"]),
339
+ &ws,
340
+ );
341
+ assert_eq!(code, ExitCode::Error, "healthy binding alone cannot hide missing coordinator");
342
+ }
343
+ let result = cmd_diagnose(&DiagnoseArgs {
344
+ workspace: ws.clone(), json: true, team: None,
345
+ }).expect("diagnostic report");
346
+ let CmdOutput::Json(report) = result.output else {
347
+ panic!("expected JSON diagnostic report");
348
+ };
349
+ assert_eq!(report["coordinator"]["ok"], false, "{report}");
350
+ let issues = report["issues"].to_string();
351
+ for binding_failure in ["leader_not_attached", "leader_receiver_unbound", "leader_workspace_mismatch"] {
352
+ assert!(!issues.contains(binding_failure), "healthy binding regressed: {report}");
353
+ }
340
354
  });
341
- assert_eq!(
342
- code,
343
- ExitCode::Ok,
344
- "`diagnose` must be Ok when canonical owner/receiver, production registry, and live align; got {code:?}"
345
- );
346
355
  let _ = std::fs::remove_dir_all(&ws);
347
356
  }
348
357