@team-agent/installer 0.5.111 → 0.5.113

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 (73) hide show
  1. package/Cargo.lock +1 -1
  2. package/Cargo.toml +1 -1
  3. package/crates/team-agent/src/cli/adapters.rs +93 -779
  4. package/crates/team-agent/src/cli/diagnose.rs +2 -424
  5. package/crates/team-agent/src/cli/emit.rs +158 -373
  6. package/crates/team-agent/src/cli/leaders.rs +32 -32
  7. package/crates/team-agent/src/cli/mod.rs +8 -15
  8. package/crates/team-agent/src/cli/named_address.rs +17 -16
  9. package/crates/team-agent/src/cli/route.rs +4 -4
  10. package/crates/team-agent/src/cli/send/presentation.rs +19 -18
  11. package/crates/team-agent/src/cli/send/resolve.rs +11 -11
  12. package/crates/team-agent/src/cli/send.rs +4 -4
  13. package/crates/team-agent/src/cli/spec.rs +29 -32
  14. package/crates/team-agent/src/cli/status.rs +7 -7
  15. package/crates/team-agent/src/cli/tests/base.rs +6 -6
  16. package/crates/team-agent/src/cli/tests/divergence.rs +3 -2
  17. package/crates/team-agent/src/cli/tests/lane_c.rs +1 -1
  18. package/crates/team-agent/src/cli/tests/leader_watch.rs +2 -86
  19. package/crates/team-agent/src/cli/tests/main_preserved.rs +15 -140
  20. package/crates/team-agent/src/cli/tests/missing_subcommands.rs +1 -258
  21. package/crates/team-agent/src/cli/tests/mod.rs +0 -2
  22. package/crates/team-agent/src/cli/tests/named_address.rs +3 -3
  23. package/crates/team-agent/src/cli/tests/status_send.rs +7 -7
  24. package/crates/team-agent/src/cli/triage.rs +53 -2
  25. package/crates/team-agent/src/cli/types.rs +12 -116
  26. package/crates/team-agent/src/compiler/tests.rs +12 -0
  27. package/crates/team-agent/src/compiler.rs +0 -9
  28. package/crates/team-agent/src/coordinator/health.rs +3 -283
  29. package/crates/team-agent/src/coordinator/mod.rs +0 -5
  30. package/crates/team-agent/src/coordinator/tests/basics.rs +0 -9
  31. package/crates/team-agent/src/coordinator/tests/mod.rs +0 -1
  32. package/crates/team-agent/src/coordinator/tests/tick_core.rs +0 -17
  33. package/crates/team-agent/src/coordinator/types.rs +1 -21
  34. package/crates/team-agent/src/db/message_store.rs +0 -42
  35. package/crates/team-agent/src/db/migration.rs +49 -9
  36. package/crates/team-agent/src/db/schema.rs +27 -20
  37. package/crates/team-agent/src/leader/owner_bind.rs +0 -24
  38. package/crates/team-agent/src/leader/start.rs +5 -0
  39. package/crates/team-agent/src/leader/tests/byte_findings.rs +0 -20
  40. package/crates/team-agent/src/leader/tests/identity.rs +21 -32
  41. package/crates/team-agent/src/leader/tests/lease_claim.rs +0 -55
  42. package/crates/team-agent/src/lifecycle/launch/identity.rs +172 -13
  43. package/crates/team-agent/src/lifecycle/launch/quick_start.rs +325 -14
  44. package/crates/team-agent/src/lifecycle/launch.rs +2 -2
  45. package/crates/team-agent/src/lifecycle/profile_launch.rs +58 -46
  46. package/crates/team-agent/src/lifecycle/restart/rebuild.rs +97 -52
  47. package/crates/team-agent/src/lifecycle/restart.rs +1 -1
  48. package/crates/team-agent/src/lifecycle/tests/restart_session_capture_red.rs +2 -51
  49. package/crates/team-agent/src/lifecycle/tests/stale_team_saveconflict_contract.rs +3 -2
  50. package/crates/team-agent/src/lifecycle/tests/status_credential_redaction_contract.rs +23 -23
  51. package/crates/team-agent/src/lifecycle/tests/subprep_codex_trust_roundtrip_red.rs +29 -55
  52. package/crates/team-agent/src/messaging/mod.rs +0 -2
  53. package/crates/team-agent/src/messaging/results.rs +0 -58
  54. package/crates/team-agent/src/messaging/tests/runtime.rs +0 -46
  55. package/crates/team-agent/src/provider/adapter.rs +8 -59
  56. package/crates/team-agent/src/provider/adapters/claude.rs +6 -14
  57. package/crates/team-agent/src/provider/adapters/codex.rs +8 -18
  58. package/crates/team-agent/src/provider/adapters/copilot.rs +5 -22
  59. package/crates/team-agent/src/provider/adapters/cursor_agent.rs +8 -45
  60. package/crates/team-agent/src/provider/adapters/grok.rs +7 -31
  61. package/crates/team-agent/src/provider/adapters/pi.rs +7 -18
  62. package/crates/team-agent/src/provider/command_helpers.rs +45 -0
  63. package/crates/team-agent/src/provider/mod.rs +1 -0
  64. package/crates/team-agent/src/provider/model_catalog.rs +21 -28
  65. package/crates/team-agent/src/provider/tests/adapter.rs +70 -0
  66. package/crates/team-agent/src/provider/tests/command_helpers.rs +55 -0
  67. package/crates/team-agent/src/state/repository.rs +1 -5
  68. package/package.json +4 -4
  69. package/templates/team_state.md +1 -1
  70. package/crates/team-agent/src/cli/tests/peer_allow.rs +0 -164
  71. package/crates/team-agent/src/cli/tests/verb_validate.rs +0 -195
  72. package/crates/team-agent/src/coordinator/tests/watch.rs +0 -223
  73. package/crates/team-agent/src/messaging/peers.rs +0 -34
@@ -41,6 +41,71 @@ use crate::lifecycle::lock::{acquire_agent_lifecycle_lock, LifecycleLockRequest}
41
41
  // `pane_verify_ms`, `startup_prompt_handler_ms`, `tmux_start_mode`) so
42
42
  // bounded-concurrency spawn can be justified with real numbers.
43
43
 
44
+ /// Stable wire labels for launch/restart timing events, not readiness states.
45
+ #[derive(Clone, Copy, Debug, Eq, PartialEq)]
46
+ pub(crate) enum LifecyclePhase {
47
+ ResolveContext,
48
+ CompileSpec,
49
+ PlanClassification,
50
+ Teardown,
51
+ SpawnAll,
52
+ SaveState,
53
+ CoordinatorStart,
54
+ ReadinessWait,
55
+ Completed,
56
+ }
57
+
58
+ impl LifecyclePhase {
59
+ pub(crate) fn as_str(self) -> &'static str {
60
+ match self {
61
+ Self::ResolveContext => "resolve_context",
62
+ Self::CompileSpec => "compile_spec",
63
+ Self::PlanClassification => "plan_classification",
64
+ Self::Teardown => "teardown",
65
+ Self::SpawnAll => "spawn_all",
66
+ Self::SaveState => "save_state",
67
+ Self::CoordinatorStart => "coordinator_start",
68
+ Self::ReadinessWait => "readiness_wait",
69
+ Self::Completed => "completed",
70
+ }
71
+ }
72
+
73
+ /// Pure event formatting shared by launch/restart; no clock, I/O or readiness policy.
74
+ pub(crate) fn event_fields(self, elapsed_ms: u64) -> serde_json::Value {
75
+ serde_json::json!({
76
+ "phase": self.as_str(),
77
+ "elapsed_ms": elapsed_ms,
78
+ })
79
+ }
80
+ }
81
+
82
+ #[test]
83
+ fn lifecycle_phase_wire_labels_remain_stable() {
84
+ for (phase, wire) in [
85
+ (LifecyclePhase::ResolveContext, "resolve_context"),
86
+ (LifecyclePhase::CompileSpec, "compile_spec"),
87
+ (LifecyclePhase::PlanClassification, "plan_classification"),
88
+ (LifecyclePhase::Teardown, "teardown"),
89
+ (LifecyclePhase::SpawnAll, "spawn_all"),
90
+ (LifecyclePhase::SaveState, "save_state"),
91
+ (LifecyclePhase::CoordinatorStart, "coordinator_start"),
92
+ (LifecyclePhase::ReadinessWait, "readiness_wait"),
93
+ (LifecyclePhase::Completed, "completed"),
94
+ ] {
95
+ assert_eq!(phase.as_str(), wire);
96
+ for elapsed_ms in [0, 123, u64::MAX] {
97
+ assert_eq!(phase.event_fields(elapsed_ms), serde_json::json!({
98
+ "phase": wire,
99
+ "elapsed_ms": elapsed_ms,
100
+ }));
101
+ assert_eq!(
102
+ serde_json::to_string(&phase.event_fields(elapsed_ms)).expect("phase JSON"),
103
+ format!("{{\"phase\":\"{wire}\",\"elapsed_ms\":{elapsed_ms}}}"),
104
+ );
105
+ }
106
+ }
107
+ }
108
+
44
109
  pub(crate) struct RestartPhaseTimer {
45
110
  started_at: std::time::Instant,
46
111
  }
@@ -71,15 +136,9 @@ impl RestartPhaseTimer {
71
136
  /// phase: 阶段名
72
137
  /// returns: 写事件失败被吞掉,不影响主流程
73
138
  /// ---
74
- pub(crate) fn emit(&self, workspace: &Path, kind: &'static str, phase: &'static str) {
139
+ pub(crate) fn emit(&self, workspace: &Path, kind: &'static str, phase: LifecyclePhase) {
75
140
  let event_log = crate::event_log::EventLog::new(workspace);
76
- let _ = event_log.write(
77
- kind,
78
- serde_json::json!({
79
- "phase": phase,
80
- "elapsed_ms": self.elapsed_ms(),
81
- }),
82
- );
141
+ let _ = event_log.write(kind, phase.event_fields(self.elapsed_ms()));
83
142
  }
84
143
  }
85
144
 
@@ -305,7 +364,7 @@ fn restart_with_selected_team_and_transport(
305
364
  // resolved a context; downstream phases are emitted with monotonic
306
365
  // `elapsed_ms` for at-a-glance latency triage.
307
366
  let phase_timer = RestartPhaseTimer::start();
308
- phase_timer.emit(&selected.run_workspace, "restart.phase", "resolve_context");
367
+ phase_timer.emit(&selected.run_workspace, "restart.phase", LifecyclePhase::ResolveContext);
309
368
  let lifecycle_lock = acquire_agent_lifecycle_lock(LifecycleLockRequest {
310
369
  workspace: &selected.run_workspace,
311
370
  operation: "restart",
@@ -345,7 +404,7 @@ fn restart_with_selected_team_and_transport(
345
404
  // 写 runtime spec。角色缺(TEAM.md/agents 不在)→ 显式拒(列缺哪些),旧 spec 原地保留不删不用。
346
405
  let spec =
347
406
  rebuild_runtime_spec_from_roles(&selected.run_workspace, &selected.team_key, &state)?;
348
- phase_timer.emit(&selected.run_workspace, "restart.phase", "compile_spec");
407
+ phase_timer.emit(&selected.run_workspace, "restart.phase", LifecyclePhase::CompileSpec);
349
408
  // 重建后 spec_workspace 恒为 runtime spec 的父目录(.team/runtime/<team_key>/)。
350
409
  let runtime_spec =
351
410
  crate::model::paths::runtime_spec_path(&selected.run_workspace, &selected.team_key);
@@ -441,7 +500,7 @@ fn restart_with_selected_team_and_transport(
441
500
  phase_timer.emit(
442
501
  &selected.run_workspace,
443
502
  "restart.phase",
444
- "plan_classification",
503
+ LifecyclePhase::PlanClassification,
445
504
  );
446
505
  write_restart_resume_decision_events(
447
506
  &selected.run_workspace,
@@ -558,8 +617,8 @@ fn restart_with_selected_team_and_transport(
558
617
  &topology_authority_agent_ids,
559
618
  )?;
560
619
  }
561
- phase_timer.emit(&selected.run_workspace, "restart.phase", "teardown");
562
- phase_timer.emit(&selected.run_workspace, "restart.phase", "spawn_all");
620
+ phase_timer.emit(&selected.run_workspace, "restart.phase", LifecyclePhase::Teardown);
621
+ phase_timer.emit(&selected.run_workspace, "restart.phase", LifecyclePhase::SpawnAll);
563
622
  // 0.5.40 Slice 3 (tmux-server-death-locate §7 Slice 3): when the
564
623
  // pre-spawn teardown was DEFERRED (live worker session with
565
624
  // authoritative running agents), snapshot the pre-spawn agent rows
@@ -582,8 +641,8 @@ fn restart_with_selected_team_and_transport(
582
641
  // tmux session is created deterministically; the remaining independent
583
642
  // decisions run their `transport.spawn_into` in parallel via a thread
584
643
  // scope with concurrency capped at `min(4, workers-1)`. Every spawn
585
- // result is collected in-memory; `verify_spawned_agent_live` +
586
- // `mark_agent_respawned` are then applied in ORIGINAL PLAN ORDER so
644
+ // result is collected in-memory; `mark_agent_respawned` is then
645
+ // applied in ORIGINAL PLAN ORDER so
587
646
  // persisted `spawn_epoch` / `spawned_at` / `pane_id` / `window` stay
588
647
  // deterministic regardless of thread completion order. Failure
589
648
  // aggregation stays equivalent to serial: per-agent errors go into
@@ -837,17 +896,14 @@ fn restart_with_selected_team_and_transport(
837
896
  agent.get("dangerously_skip_permissions"),
838
897
  Some(serde_json::Value::Bool(true))
839
898
  );
840
- if let Err(error) = verify_spawned_agent_live(&decision.agent_id, &spawn, transport)
841
- .and_then(|_| {
842
- mark_agent_respawned(
843
- &mut state,
844
- &decision.agent_id,
845
- decision.restart_mode,
846
- &spawn,
847
- transport,
848
- launched_bypass,
849
- )
850
- })
899
+ if let Err(error) = mark_agent_respawned(
900
+ &mut state,
901
+ &decision.agent_id,
902
+ decision.restart_mode,
903
+ &spawn,
904
+ transport,
905
+ launched_bypass,
906
+ )
851
907
  {
852
908
  let error = error.to_string();
853
909
  mark_agent_restart_failed(&mut state, decision, &error);
@@ -1026,7 +1082,7 @@ fn restart_with_selected_team_and_transport(
1026
1082
  &capture_backfill_skip_agent_ids,
1027
1083
  &topology_authority_agent_ids,
1028
1084
  )?;
1029
- phase_timer.emit(&selected.run_workspace, "restart.phase", "save_state");
1085
+ phase_timer.emit(&selected.run_workspace, "restart.phase", LifecyclePhase::SaveState);
1030
1086
  if fatal_resume_failure {
1031
1087
  let attach_commands = Vec::new();
1032
1088
  let next_actions = restart_failure_next_actions(&failed_agents);
@@ -1304,10 +1360,10 @@ fn restart_with_selected_team_and_transport(
1304
1360
  phase_timer.emit(
1305
1361
  &selected.run_workspace,
1306
1362
  "restart.phase",
1307
- "coordinator_start",
1363
+ LifecyclePhase::CoordinatorStart,
1308
1364
  );
1309
1365
  let coordinator_started = coordinator.ok;
1310
- phase_timer.emit(&selected.run_workspace, "restart.phase", "readiness_wait");
1366
+ phase_timer.emit(&selected.run_workspace, "restart.phase", LifecyclePhase::ReadinessWait);
1311
1367
  wait_restart_readiness_or_timeout(
1312
1368
  &selected.run_workspace,
1313
1369
  &state,
@@ -1331,7 +1387,7 @@ fn restart_with_selected_team_and_transport(
1331
1387
  let mut next_actions = Vec::new();
1332
1388
  if !failed_agents.is_empty() {
1333
1389
  next_actions.extend(restart_failure_next_actions(&failed_agents));
1334
- phase_timer.emit(&selected.run_workspace, "restart.phase", "completed");
1390
+ phase_timer.emit(&selected.run_workspace, "restart.phase", LifecyclePhase::Completed);
1335
1391
  write_restart_completed_event(
1336
1392
  &selected.run_workspace,
1337
1393
  &successful_agents,
@@ -1360,7 +1416,7 @@ fn restart_with_selected_team_and_transport(
1360
1416
  leader_bind_reason,
1361
1417
  });
1362
1418
  }
1363
- phase_timer.emit(&selected.run_workspace, "restart.phase", "completed");
1419
+ phase_timer.emit(&selected.run_workspace, "restart.phase", LifecyclePhase::Completed);
1364
1420
  write_restart_completed_event(
1365
1421
  &selected.run_workspace,
1366
1422
  &successful_agents,
@@ -1852,14 +1908,6 @@ fn yes_no(value: bool) -> &'static str {
1852
1908
  }
1853
1909
  }
1854
1910
 
1855
- fn verify_spawned_agent_live(
1856
- _agent_id: &AgentId,
1857
- _spawn: &SpawnedAgentWindow,
1858
- _transport: &dyn crate::transport::Transport,
1859
- ) -> Result<(), LifecycleError> {
1860
- Ok(())
1861
- }
1862
-
1863
1911
  /// 0.3.30 Bug 1: restart success path auto-attach.
1864
1912
  /// When `TMUX_PANE` is present in the caller's env, treat the restart as if
1865
1913
  /// the user had also run `attach-leader` from that pane. Mirrors quick-start's
@@ -3088,8 +3136,8 @@ fn run_bounded_parallel_worker_spawns(
3088
3136
  outcomes
3089
3137
  }
3090
3138
 
3091
- /// 0.5.38 Step 2: serial post-spawn stage. Runs verify_spawned_agent_live +
3092
- /// mark_agent_respawned in plan order, emits worker.spawn_timing, and
3139
+ /// 0.5.38 Step 2: serial post-spawn stage. Runs mark_agent_respawned
3140
+ /// in plan order, emits worker.spawn_timing, and
3093
3141
  /// updates the successful/failed agent lists exactly like the pre-0.5.38
3094
3142
  /// serial loop. Keeping this serial guarantees deterministic persisted
3095
3143
  /// state (`spawn_epoch`, `spawned_at`, etc.) regardless of parallel
@@ -3111,17 +3159,14 @@ fn apply_marked_respawn(
3111
3159
  launched_bypass: bool,
3112
3160
  ) {
3113
3161
  let verify_start = std::time::Instant::now();
3114
- if let Err(error) =
3115
- verify_spawned_agent_live(&decision.agent_id, spawn, transport).and_then(|_| {
3116
- mark_agent_respawned(
3117
- state,
3118
- &decision.agent_id,
3119
- decision.restart_mode,
3120
- spawn,
3121
- transport,
3122
- launched_bypass,
3123
- )
3124
- })
3162
+ if let Err(error) = mark_agent_respawned(
3163
+ state,
3164
+ &decision.agent_id,
3165
+ decision.restart_mode,
3166
+ spawn,
3167
+ transport,
3168
+ launched_bypass,
3169
+ )
3125
3170
  {
3126
3171
  let error = error.to_string();
3127
3172
  mark_agent_restart_failed(state, decision, &error);
@@ -100,7 +100,7 @@ pub(crate) use rebuild::restart_with_transport_with_session_convergence_deadline
100
100
  // timer + worker timing writer so `lifecycle::launch::launch_with_transport_in_workspace`
101
101
  // can emit the same instrumentation event family with `source="launch"`.
102
102
  pub(crate) use rebuild::{
103
- provider_wire_from_state, write_worker_spawn_timing_event, RestartPhaseTimer,
103
+ provider_wire_from_state, write_worker_spawn_timing_event, LifecyclePhase, RestartPhaseTimer,
104
104
  };
105
105
  pub use rebuild::{restart, restart_with_session_convergence_deadline};
106
106
  pub(crate) use rebuild::{
@@ -21,7 +21,6 @@ use std::sync::Mutex;
21
21
 
22
22
  use serde_json::{json, Value};
23
23
  use serial_test::serial;
24
- use team_agent::cli::{cmd_wait_ready, CmdOutput, WaitReadyArgs};
25
24
  use team_agent::lifecycle::{
26
25
  classify_restart_plan, restart_with_transport, RestartReport, ResumeDecision, StartMode,
27
26
  };
@@ -448,48 +447,7 @@ fn restart_refuses_first_send_at_null_running_resumable_session_incomplete_when_
448
447
  }
449
448
 
450
449
  #[test]
451
- #[ignore = "real-machine: provider session filesystem + restart/shutdown lifecycle convergence"]
452
- #[serial(env)]
453
- fn wait_ready_reports_session_capture_incomplete_for_running_resumable_worker_without_session() {
454
- let fixture = RestartFixture::new("wait-ready-session-incomplete", "codex");
455
- seed_running_state_without_session(&fixture.team, "codex", &fixture.spawn_cwd);
456
- let mut state = load_runtime_state(&fixture.team).unwrap();
457
- let mcp_config = fixture.team.join(".team/runtime/worker_a.mcp.json");
458
- std::fs::create_dir_all(mcp_config.parent().unwrap()).unwrap();
459
- std::fs::write(&mcp_config, "{}").unwrap();
460
- state["agents"]["worker_a"]["pane_id"] = json!("%1");
461
- state["agents"]["worker_a"]["mcp_config"] = json!(mcp_config.to_string_lossy().to_string());
462
- save_runtime_state(&fixture.team, &state).unwrap();
463
-
464
- let out = json_result(
465
- cmd_wait_ready(&WaitReadyArgs {
466
- workspace: fixture.team.clone(),
467
- timeout: 0.0,
468
- json: true,
469
- team: None,
470
- })
471
- .expect("wait-ready should return structured JSON"),
472
- );
473
-
474
- assert_eq!(
475
- out["ok"],
476
- json!(false),
477
- "wait-ready cannot report ready while a running Codex/Claude worker is missing captured session context; out={out}"
478
- );
479
- assert_eq!(
480
- out["reason"],
481
- json!("session_capture_incomplete"),
482
- "readiness reason must distinguish missing resumable session from generic workers_not_ready; out={out}"
483
- );
484
- assert!(
485
- out["readiness"]["session_capture_incomplete"].as_bool() == Some(true)
486
- && out["readiness"]["all_resumable_have_session"].as_bool() == Some(false),
487
- "readiness truth table must include all_resumable_have_session and session_capture_incomplete; out={out}"
488
- );
489
- }
490
-
491
- #[test]
492
- fn status_quick_start_and_wait_ready_surfaces_include_session_capture_completeness() {
450
+ fn status_and_quick_start_surfaces_include_session_capture_completeness() {
493
451
  let diagnose =
494
452
  std::fs::read_to_string(concat!(env!("CARGO_MANIFEST_DIR"), "/src/cli/diagnose.rs"))
495
453
  .unwrap();
@@ -500,7 +458,7 @@ fn status_quick_start_and_wait_ready_surfaces_include_session_capture_completene
500
458
  .unwrap();
501
459
 
502
460
  for (name, source) in [
503
- ("wait-ready", diagnose.as_str()),
461
+ ("status readiness formula", diagnose.as_str()),
504
462
  ("status --json", status.as_str()),
505
463
  ("quick-start readiness", launch.as_str()),
506
464
  ("quick-start --json", adapters.as_str()),
@@ -945,13 +903,6 @@ fn read_events(team: &Path) -> String {
945
903
  std::fs::read_to_string(team.join(".team/logs/events.jsonl")).unwrap_or_default()
946
904
  }
947
905
 
948
- fn json_result(result: team_agent::cli::CmdResult) -> Value {
949
- match result.output {
950
- CmdOutput::Json(value) => value,
951
- other => panic!("expected JSON command output, got {other:?}"),
952
- }
953
- }
954
-
955
906
  fn assert_no_agent_attribution_ambiguous<const N: usize>(
956
907
  state: &Value,
957
908
  expected_agents: [&str; N],
@@ -165,14 +165,15 @@ fn purge_agent_help_and_dispatch_are_consistent() {
165
165
  output_text(&case.run(["purge-agent", "ghost", "--workspace", case.ws(), "--json"]));
166
166
  if help.contains("purge-agent") {
167
167
  assert!(
168
- !command.contains("invalid choice") && !command.contains("unknown subcommand") && !command.contains("Commands:"),
168
+ !command.contains("invalid choice") && !command.contains("unknown subcommand")
169
+ && !command.contains("Unknown command:") && !command.contains("Commands:"),
169
170
  "RED6: help exposes purge-agent, so dispatch must be real or the command must be removed from help; help={help} command={command}"
170
171
  );
171
172
  } else {
172
173
  assert!(
173
174
  command.contains("unknown subcommand")
174
175
  || command.contains("invalid choice")
175
- || command.contains("没有这个操作"),
176
+ || command.contains("Unknown command:"),
176
177
  "RED6: if purge-agent is not implemented, help and dispatch must agree it is absent; help={help} command={command}"
177
178
  );
178
179
  }
@@ -23,7 +23,7 @@ use std::time::{Duration, Instant};
23
23
 
24
24
  use serde_json::{json, Value};
25
25
  use serial_test::serial;
26
- use team_agent::cli::{cmd_preflight, emit, CmdOutput, PreflightArgs};
26
+ use team_agent::cli::{cmd_doctor, emit, CmdOutput, DoctorArgs};
27
27
  use team_agent::event_log::EventLog;
28
28
  use team_agent::message_store::MessageStore;
29
29
  use team_agent::state::persist::save_runtime_state;
@@ -334,11 +334,10 @@ fn red6_cli_restart_reports_and_top_level_error_logs_are_redacted() {
334
334
  env.scrub_tmux();
335
335
  env.assert_no_real_tmux();
336
336
  let workspace = env.workspace("red6");
337
- let spec = write_invalid_validate_spec(&workspace, &marker);
338
337
  let mut combined = String::new();
339
338
  for json_mode in [true, false] {
340
339
  let mut command = Command::new(crate::lifecycle::tests::test_binary_path());
341
- command.arg("validate").arg(&spec);
340
+ command.arg("profile").arg(credential_url(&marker)).arg("synthetic-profile");
342
341
  if json_mode {
343
342
  command.arg("--json");
344
343
  }
@@ -351,7 +350,7 @@ fn red6_cli_restart_reports_and_top_level_error_logs_are_redacted() {
351
350
  .expect("run top-level CLI error boundary");
352
351
  assert!(
353
352
  !output.status.success(),
354
- "invalid spec must reach CLI error"
353
+ "invalid profile subcommand must reach CLI error"
355
354
  );
356
355
  combined.push_str(&String::from_utf8_lossy(&output.stdout));
357
356
  combined.push_str(&String::from_utf8_lossy(&output.stderr));
@@ -391,15 +390,30 @@ fn red7_pane_and_profile_scrubbers_converge_without_losing_existing_policy() {
391
390
  let env = hermetic_guard::HermeticTestEnv::enter("redact-red7");
392
391
  let pane_policy_preserved = pane_excerpt_uses_shared_redaction(&env, &marker);
393
392
  let team = write_profile_fixture(env.root(), server.base_url(), &profile_token);
394
- let result = cmd_preflight(&PreflightArgs { team, json: true })
393
+ let result = cmd_doctor(&DoctorArgs {
394
+ spec: Some(team.join("TEAM.md")),
395
+ workspace: team,
396
+ gate: None,
397
+ comms: false,
398
+ team: None,
399
+ fix: false,
400
+ fix_schema: false,
401
+ cleanup_orphans: false,
402
+ confirm: false,
403
+ json: true,
404
+ })
395
405
  .expect("profile smoke returns a diagnostic report");
396
406
  let report = match result.output {
397
407
  CmdOutput::Json(value) => value,
398
- other => panic!("expected JSON preflight output, got {other:?}"),
408
+ other => panic!("expected JSON doctor output, got {other:?}"),
399
409
  };
400
- let text = report.to_string();
401
- let profile_policy_preserved = !text.contains(&marker)
402
- && !text.contains(&profile_token)
410
+ let report_text = report.to_string();
411
+ let text = report
412
+ .get("profile_smoke")
413
+ .unwrap_or_else(|| panic!("doctor must expose profile_smoke; report={report}"))
414
+ .to_string();
415
+ let profile_policy_preserved = !report_text.contains(&marker)
416
+ && !report_text.contains(&profile_token)
403
417
  && !text.contains(tail_sentinel)
404
418
  && text.contains(REDACTED)
405
419
  && text.contains("proxy.invalid:8443/path")
@@ -555,20 +569,6 @@ fn run_mcp_status_frames(
555
569
  .collect()
556
570
  }
557
571
 
558
- fn write_invalid_validate_spec(workspace: &Path, marker: &str) -> PathBuf {
559
- let path = workspace.join("invalid-provider.spec.yaml");
560
- fs::write(
561
- &path,
562
- format!(
563
- "version: 1\nteam:\n id: current\n name: current\n session_name: team-redaction-contract\n workspace: \"{}\"\nleader:\n provider: \"{}\"\nagents:\n - id: worker\n provider: fake\n model: fake\n role: Worker\n window: worker\ntasks: []\n",
564
- workspace.display(),
565
- credential_url(marker)
566
- ),
567
- )
568
- .unwrap();
569
- path
570
- }
571
-
572
572
  fn read_cli_error_logs(workspace: &Path) -> String {
573
573
  let dir = workspace.join(".team/logs");
574
574
  fs::read_dir(dir)
@@ -92,15 +92,10 @@ const SUBROOT_FULL_TRUST_PANE_AFTER_QUICKSTART: &str = r#"> You are in /private/
92
92
  │ directory: /private/tmp/…/teamdir │
93
93
  ╰───────────────────────────────────────╯
94
94
 
95
-
96
95
  › Find and fix a bug in @filename
97
96
 
98
97
  gpt-5.5 default · /private/tmp/ta-subroot-SUB-PROBE-1-trust-startup.uL9Boc/te…
99
98
 
100
-
101
-
102
-
103
-
104
99
  "#;
105
100
 
106
101
  fn tmp_dir(tag: &str) -> TempWorkspace {
@@ -419,8 +414,7 @@ fn contract_a_own_workspace_trust_is_actionable_not_ready_and_foreign_is_pending
419
414
  }
420
415
 
421
416
  #[test]
422
- fn contract_a_probe5_quick_start_ready_is_invalid_while_actionable_trust_remains() {
423
- use team_agent::cli::{cmd_wait_ready, CmdOutput, WaitReadyArgs};
417
+ fn contract_a_runtime_readiness_is_false_while_actionable_trust_remains() {
424
418
  use team_agent::provider::{classify_codex_startup_screen, StartupScreenDecision};
425
419
 
426
420
  assert_eq!(
@@ -429,67 +423,47 @@ fn contract_a_probe5_quick_start_ready_is_invalid_while_actionable_trust_remains
429
423
  "fixture sanity: probe5 pane text is an actionable own-workspace trust prompt"
430
424
  );
431
425
 
432
- let ws = tmp_dir("a-wait-ready-trust");
426
+ let ws = tmp_dir("a-runtime-readiness-trust");
433
427
  let mcp_config = ws.join(".team").join("runtime").join("mcp").join("w1.json");
434
428
  std::fs::create_dir_all(mcp_config.parent().unwrap()).unwrap();
435
429
  std::fs::write(&mcp_config, "{}").unwrap();
436
- team_agent::state::persist::save_runtime_state(
437
- &ws,
438
- &json!({
439
- "session_name": "team-subprep",
440
- "agents": {
441
- "w1": {
442
- "provider": "codex",
443
- "status": "awaiting_trust_prompt",
444
- "startup_prompt_status": "awaiting_trust_prompt",
445
- "startup_prompts": "awaiting_trust_prompt",
446
- "pane_id": "%7",
447
- "window": "w1",
448
- "mcp_config": mcp_config.to_string_lossy().to_string(),
449
- "task_prompt_delivered": true
450
- }
430
+ // Keep every other readiness gate satisfied; an unattached leader would
431
+ // make ready=false independently of the trust prompt under test.
432
+ let mut state = json!({
433
+ "session_name": "team-subprep",
434
+ "tmux_session_present": true,
435
+ "leader_receiver": {"status": "attached", "pane_id": "%9"},
436
+ "agents": {
437
+ "w1": {
438
+ "provider": "codex",
439
+ "status": "awaiting_trust_prompt",
440
+ "startup_prompt_status": "awaiting_trust_prompt",
441
+ "startup_prompts": "awaiting_trust_prompt",
442
+ "pane_id": "%7",
443
+ "window": "w1",
444
+ "mcp_config": mcp_config.to_string_lossy().to_string(),
445
+ "task_prompt_delivered": true
451
446
  }
452
- }),
453
- )
454
- .unwrap();
455
-
456
- let result = cmd_wait_ready(&WaitReadyArgs {
457
- workspace: ws.to_path_buf(),
458
- timeout: 0.0,
459
- json: true,
460
- team: None,
461
- })
462
- .expect("wait-ready SUT should return shaped JSON");
463
- let value = match result.output {
464
- CmdOutput::Json(value) => value,
465
- other => panic!("wait-ready --json must return JSON output, got {other:?}"),
466
- };
447
+ }
448
+ });
467
449
 
450
+ let value = crate::cli::diagnose::wait_readiness(&state);
468
451
  assert_eq!(
469
- value["ok"],
470
- json!(false),
471
- "Contract A: quick-start/wait-ready readiness must not report ok=true while actionable trust remains; got {value}"
472
- );
473
- assert_eq!(
474
- value["status"],
475
- json!("pending"),
476
- "Contract A: actionable trust prompt should keep readiness pending, not ready; got {value}"
477
- );
478
- assert_eq!(
479
- value["reason"],
480
- json!("awaiting_trust_prompt"),
481
- "Contract A: not-ready reason must identify the startup trust boundary; got {value}"
482
- );
483
- assert_eq!(
484
- value["readiness"]["awaiting_trust_prompt"],
452
+ value["awaiting_trust_prompt"],
485
453
  json!(true),
486
454
  "Contract A: ready logic must surface awaiting_trust_prompt=true; got {value}"
487
455
  );
488
456
  assert_eq!(
489
- value["readiness"]["ready"],
457
+ value["ready"],
490
458
  json!(false),
491
459
  "Contract A: ready must stay false until the actionable trust prompt is cleared; got {value}"
492
460
  );
461
+ state["agents"]["w1"]["status"] = json!("running");
462
+ state["agents"]["w1"]["startup_prompt_status"] = json!("complete");
463
+ state["agents"]["w1"]["startup_prompts"] = json!("complete");
464
+ let cleared = crate::cli::diagnose::wait_readiness(&state);
465
+ assert_eq!(cleared["awaiting_trust_prompt"], json!(false));
466
+ assert_eq!(cleared["ready"], json!(true), "positive control: {cleared}");
493
467
  }
494
468
 
495
469
  #[test]
@@ -67,7 +67,6 @@ pub mod delivery;
67
67
  pub mod helpers;
68
68
  pub mod leader_channel;
69
69
  pub mod leader_receiver;
70
- pub mod peers;
71
70
  pub mod persist;
72
71
  pub mod presentation;
73
72
  pub mod results;
@@ -96,7 +95,6 @@ pub use leader_receiver::{
96
95
  mirror_peer_message_to_leader, send_to_leader_receiver,
97
96
  send_to_leader_receiver_with_presentation,
98
97
  };
99
- pub use peers::allow_peer_talk;
100
98
  pub use persist::{
101
99
  persist_resolved_send, DeliveryBlocker, InitialDisposition, InternalSendKind,
102
100
  LogicalRecipient, PersistResolution, PersistedSend, ResolvedSendIntent, SendOrigin,
@@ -45,64 +45,6 @@ impl Default for ResultRoute {
45
45
  }
46
46
  }
47
47
 
48
- pub fn results_for_case(
49
- workspace: &Path,
50
- case_id: &str,
51
- owner_team_id: Option<&str>,
52
- requested_owner_team_id: Option<&str>,
53
- ) -> Result<Vec<serde_json::Value>, MessagingError> {
54
- let current_db = workspace.join(".team/runtime/team.db");
55
- let legacy_db = workspace.join(".team/team.db");
56
- if current_db.exists() {
57
- return results_for_case_in_db(
58
- &current_db,
59
- case_id,
60
- owner_team_id,
61
- requested_owner_team_id,
62
- );
63
- }
64
- if legacy_db.exists() {
65
- return results_for_case_in_db(&legacy_db, case_id, owner_team_id, requested_owner_team_id);
66
- }
67
- Ok(Vec::new())
68
- }
69
-
70
- fn results_for_case_in_db(
71
- db_path: &Path,
72
- case_id: &str,
73
- owner_team_id: Option<&str>,
74
- requested_owner_team_id: Option<&str>,
75
- ) -> Result<Vec<serde_json::Value>, MessagingError> {
76
- let conn = crate::db::schema::open_db(db_path)?;
77
- // The second predicate works around
78
- // `.team/bugs/BUG-caseid-writable-when-route-undeclared-20260730.md`;
79
- // it is not design intent.
80
- let case_predicate = "task_id = ?1 or json_extract(envelope,'$.presentation.case_id') = ?1";
81
- let sql = match owner_team_id {
82
- Some(_) => format!(
83
- "select envelope from results where ({case_predicate}) \
84
- and (owner_team_id = ?2 or owner_team_id = ?3) \
85
- order by created_at, result_id"
86
- ),
87
- None => format!(
88
- "select envelope from results where {case_predicate} order by created_at, result_id"
89
- ),
90
- };
91
- let mut stmt = conn.prepare(&sql)?;
92
- let row_mapper = |row: &rusqlite::Row<'_>| row.get::<_, String>(0);
93
- let rows = match (owner_team_id, requested_owner_team_id) {
94
- (Some(team), Some(requested_team)) => {
95
- stmt.query_map(params![case_id, team, requested_team], row_mapper)
96
- }
97
- (Some(team), None) => stmt.query_map(params![case_id, team, team], row_mapper),
98
- (None, _) => stmt.query_map(params![case_id], row_mapper),
99
- }?
100
- .collect::<Result<Vec<_>, _>>()?;
101
- rows.into_iter()
102
- .map(|envelope| serde_json::from_str(&envelope).map_err(MessagingError::from))
103
- .collect()
104
- }
105
-
106
48
  /// `collect` (`results.py:45`):投递 pending、捞 uncollected results、校验 envelope、更新任务态、
107
49
  /// 写 team_state、ensure coordinator。CLI `collect` + coordinator tick 调。
108
50
  pub fn collect(