@team-agent/installer 0.5.75 → 0.5.78

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 (98) hide show
  1. package/Cargo.lock +1 -1
  2. package/Cargo.toml +1 -1
  3. package/README.md +9 -0
  4. package/README.zh.md +8 -0
  5. package/crates/team-agent/src/cli/adapters.rs +184 -125
  6. package/crates/team-agent/src/cli/diagnose.rs +333 -68
  7. package/crates/team-agent/src/cli/emit.rs +59 -15
  8. package/crates/team-agent/src/cli/mod.rs +600 -134
  9. package/crates/team-agent/src/cli/models.rs +485 -44
  10. package/crates/team-agent/src/cli/named_address.rs +19 -5
  11. package/crates/team-agent/src/cli/profile.rs +35 -4
  12. package/crates/team-agent/src/cli/spec.rs +7 -2
  13. package/crates/team-agent/src/cli/status_port/brief.rs +1137 -0
  14. package/crates/team-agent/src/cli/status_port/compact.rs +12 -8
  15. package/crates/team-agent/src/cli/status_port/snapshot.rs +15 -12
  16. package/crates/team-agent/src/cli/status_port/store.rs +1 -1
  17. package/crates/team-agent/src/cli/status_port.rs +8 -0
  18. package/crates/team-agent/src/cli/tests/missing_subcommands.rs +62 -8
  19. package/crates/team-agent/src/cli/tests/mod.rs +1 -0
  20. package/crates/team-agent/src/cli/tests/named_address.rs +70 -0
  21. package/crates/team-agent/src/cli/tests/run_delegation.rs +29 -24
  22. package/crates/team-agent/src/cli/tests/status_brief.rs +691 -0
  23. package/crates/team-agent/src/cli/tests/status_send.rs +8 -2
  24. package/crates/team-agent/src/cli/tests/verb_profile.rs +81 -6
  25. package/crates/team-agent/src/cli/types.rs +1 -0
  26. package/crates/team-agent/src/leader/lease.rs +538 -41
  27. package/crates/team-agent/src/leader/owner_bind.rs +40 -12
  28. package/crates/team-agent/src/leader/rediscover/tests.rs +65 -0
  29. package/crates/team-agent/src/leader/rediscover.rs +36 -6
  30. package/crates/team-agent/src/leader/start.rs +151 -14
  31. package/crates/team-agent/src/leader/tests/wake_start_owner.rs +15 -9
  32. package/crates/team-agent/src/leader/types.rs +4 -0
  33. package/crates/team-agent/src/lifecycle/launch/add_agent.rs +28 -227
  34. package/crates/team-agent/src/lifecycle/launch/add_agent_state.rs +0 -18
  35. package/crates/team-agent/src/lifecycle/launch/cursor_mcp.rs +94 -227
  36. package/crates/team-agent/src/lifecycle/launch/mcp_config.rs +59 -10
  37. package/crates/team-agent/src/lifecycle/launch/pi_mcp.rs +138 -14
  38. package/crates/team-agent/src/lifecycle/launch/quick_start.rs +1449 -282
  39. package/crates/team-agent/src/lifecycle/launch/readiness.rs +349 -4
  40. package/crates/team-agent/src/lifecycle/launch/spawn.rs +11 -11
  41. package/crates/team-agent/src/lifecycle/launch/spec_state.rs +35 -3
  42. package/crates/team-agent/src/lifecycle/launch/worker_env.rs +16 -0
  43. package/crates/team-agent/src/lifecycle/launch.rs +30 -7
  44. package/crates/team-agent/src/lifecycle/profile_launch.rs +37 -3
  45. package/crates/team-agent/src/lifecycle/restart/agent.rs +112 -100
  46. package/crates/team-agent/src/lifecycle/restart/common.rs +82 -52
  47. package/crates/team-agent/src/lifecycle/restart/rebuild.rs +1002 -65
  48. package/crates/team-agent/src/lifecycle/restart.rs +2 -2
  49. package/crates/team-agent/src/lifecycle/tests/acceptance_b_batch_red.rs +3 -0
  50. package/crates/team-agent/src/lifecycle/tests/b0_reader_hideone_audit_contract.rs +45 -14
  51. package/crates/team-agent/src/lifecycle/tests/behavioral_diff_264_red.rs +15 -0
  52. package/crates/team-agent/src/lifecycle/tests/claude_compatible_config_red.rs +5 -0
  53. package/crates/team-agent/src/lifecycle/tests/claude_profile_launch_red.rs +11 -13
  54. package/crates/team-agent/src/lifecycle/tests/clone_agent_preserves_source_tools.rs +119 -3
  55. package/crates/team-agent/src/lifecycle/tests/communication_mode_runtime_contract_red.rs +12 -0
  56. package/crates/team-agent/src/lifecycle/tests/copilot_provider_red.rs +22 -0
  57. package/crates/team-agent/src/lifecycle/tests/core_034_real_red.rs +12 -0
  58. package/crates/team-agent/src/lifecycle/tests/cursor_mcp_overlay.rs +265 -198
  59. package/crates/team-agent/src/lifecycle/tests/cursor_require_explicit_model_red.rs +14 -30
  60. package/crates/team-agent/src/lifecycle/tests/cursor_restart_resume_red.rs +3 -0
  61. package/crates/team-agent/src/lifecycle/tests/g1_silent_faces.rs +2 -5
  62. package/crates/team-agent/src/lifecycle/tests/grok_effort_argv_red.rs +10 -0
  63. package/crates/team-agent/src/lifecycle/tests/grok_mcp_overlay_red.rs +58 -35
  64. package/crates/team-agent/src/lifecycle/tests/grok_require_explicit_model_red.rs +10 -0
  65. package/crates/team-agent/src/lifecycle/tests/grok_restart_resume_red.rs +9 -0
  66. package/crates/team-agent/src/lifecycle/tests/harvest2_a_batch_red.rs +4 -3
  67. package/crates/team-agent/src/lifecycle/tests/lane_ops.rs +0 -168
  68. package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +145 -212
  69. package/crates/team-agent/src/lifecycle/tests/phase_b_contracts.rs +16 -2
  70. package/crates/team-agent/src/lifecycle/tests/phase_golden.rs +69 -28
  71. package/crates/team-agent/src/lifecycle/tests/pi_dual_entry_red.rs +78 -0
  72. package/crates/team-agent/src/lifecycle/tests/team_in_team_identity_scope_red.rs +3 -0
  73. package/crates/team-agent/src/lifecycle/tests/test_isolation_escape_contract.rs +51 -10
  74. package/crates/team-agent/src/lifecycle/tests/upgrade_compat_0211_red.rs +22 -10
  75. package/crates/team-agent/src/lifecycle/tests/verify_rs031_window_consistency_red.rs +50 -7
  76. package/crates/team-agent/src/lifecycle/tests/worker_spawn_env_red.rs +137 -0
  77. package/crates/team-agent/src/lifecycle/types.rs +12 -0
  78. package/crates/team-agent/src/mcp_server/wire.rs +140 -11
  79. package/crates/team-agent/src/messaging/delivery.rs +67 -8
  80. package/crates/team-agent/src/messaging/leader_channel.rs +56 -0
  81. package/crates/team-agent/src/messaging/tests/runtime.rs +10 -2
  82. package/crates/team-agent/src/messaging/watchers.rs +48 -4
  83. package/crates/team-agent/src/provider/adapters/cursor_agent.rs +2 -5
  84. package/crates/team-agent/src/state/paths.rs +17 -2
  85. package/crates/team-agent/src/state/persist.rs +97 -1
  86. package/crates/team-agent/src/state/projection.rs +18 -2
  87. package/crates/team-agent/src/state/repository.rs +29 -0
  88. package/crates/team-agent/src/state/selector.rs +23 -3
  89. package/crates/team-agent/src/test_support/models_timeout_fixture.rs +144 -0
  90. package/crates/team-agent/src/tmux_backend/tests.rs +121 -4
  91. package/crates/team-agent/src/tmux_backend.rs +208 -118
  92. package/crates/team-agent/src/transport.rs +72 -0
  93. package/crates/team-agent/src/transport_factory.rs +53 -0
  94. package/package.json +4 -5
  95. package/skills/team-agent/SKILL.md +45 -17
  96. package/skills/team-agent/command-coverage.json +4 -29
  97. package/npm/check-skill-links.mjs +0 -88
  98. package/skills/team-agent/references/team-agent-operator.md +0 -672
@@ -58,9 +58,9 @@ pub mod attach_app_server_leader;
58
58
  pub mod diagnose;
59
59
  pub mod emit;
60
60
  pub mod helpers;
61
+ pub mod models;
61
62
  pub mod leader;
62
63
  pub mod leaders;
63
- pub mod models;
64
64
  pub mod named_address;
65
65
  pub mod profile;
66
66
  pub mod send;
@@ -74,7 +74,6 @@ pub use diagnose::*;
74
74
  pub use emit::*;
75
75
  pub use leader::*;
76
76
  pub use leaders::*;
77
- pub use models::*;
78
77
  pub use named_address::*;
79
78
  pub use profile::*;
80
79
  pub use send::*;
@@ -104,6 +103,10 @@ pub(crate) use helpers::*;
104
103
  #[cfg(test)]
105
104
  mod tests;
106
105
 
106
+ #[cfg(test)]
107
+ #[path = "../../tests/support/hermetic.rs"]
108
+ mod hermetic_test_support;
109
+
107
110
  // =============================================================================
108
111
  // CROSS-LANE PLACEHOLDERS(sibling 14a-mcp / status / diagnose / step13-lifecycle
109
112
  // 尚未落地;leader 集成时收口到真模块。本层只声明 CLI 调用面所需的最小占位,
@@ -3075,15 +3078,6 @@ pub mod lifecycle_port {
3075
3078
  }
3076
3079
 
3077
3080
  fn error_value(error: crate::lifecycle::LifecycleError) -> Value {
3078
- if let crate::lifecycle::LifecycleError::PiModelPreflight {
3079
- requested,
3080
- candidates,
3081
- action,
3082
- not_ready,
3083
- } = error
3084
- {
3085
- return json!({"ok": false, "error": "pi model preflight failed", "pi": {"requested_model": requested, "candidates": candidates, "not_ready": not_ready, "action": action.clone(), "next_action": action}});
3086
- }
3087
3081
  let message = error.to_string();
3088
3082
  let mut payload = json!({"ok": false, "error": message});
3089
3083
  if let Some(next_action) = error_next_action(&message) {
@@ -3096,9 +3090,24 @@ pub mod lifecycle_port {
3096
3090
  /// 匹配 [`LifecycleError`] 的人读消息子串(`agent {id} not found` /
3097
3091
  /// `agent id already exists` / `unknown worker agent id`),给出下一步命令。
3098
3092
  fn error_next_action(message: &str) -> Option<&'static str> {
3099
- // start-agent 撞"agent ... not found":start-agent 语义=启动 state 已有 agent;
3100
- // 想新增角色应走 add-agent。
3101
- if message.contains("not found") && message.contains("agent") {
3093
+ // Missing spec/runtime is not an agent-id miss. Workspace paths often
3094
+ // contain "team-agent", so "not found"+"agent" must not win here.
3095
+ if message.contains("active team spec not found")
3096
+ || message.contains("missing spec for restart")
3097
+ {
3098
+ return Some(
3099
+ "no team spec or runtime was found in this workspace. \
3100
+ Run `team-agent quick-start <workspace>` for first launch, \
3101
+ or restart from a workspace that already contains `.team`.",
3102
+ );
3103
+ }
3104
+ // start-agent 撞"agent {id} not found":start-agent 语义=启动 state 已有 agent;
3105
+ // 想新增角色应走 add-agent。只在 RequirementUnmet detail 以 `agent ` 开头时匹配,
3106
+ // 避免 profile 文案中的 `team-agent profile ... not found` 误命中。
3107
+ let agent_not_found = message
3108
+ .strip_prefix("agent start requirement unmet: ")
3109
+ .is_some_and(|detail| detail.starts_with("agent ") && detail.contains(" not found"));
3110
+ if agent_not_found {
3102
3111
  return Some(
3103
3112
  "start-agent only starts an agent that already exists in state. \
3104
3113
  To add a NEW role at runtime use: team-agent add-agent <id> --role-file <path>",
@@ -3260,6 +3269,90 @@ pub mod lifecycle_port {
3260
3269
  }
3261
3270
  }
3262
3271
 
3272
+ fn public_leader_bind_failure(
3273
+ stage: Option<&str>,
3274
+ reason: Option<&str>,
3275
+ _workers_spawned: bool,
3276
+ ) -> (&'static str, String, Option<String>) {
3277
+ match reason {
3278
+ Some("invalid_caller_tuple") => (
3279
+ "leader_bind_refused",
3280
+ "invalid_caller_tuple".to_string(),
3281
+ Some(
3282
+ "fix conflicting CALLER_* identity; do not run claim-leader".to_string(),
3283
+ ),
3284
+ ),
3285
+ Some("provider_unresolved") => (
3286
+ "leader_bind_refused",
3287
+ "provider_unresolved".to_string(),
3288
+ Some(
3289
+ "set TEAM_AGENT_LEADER_PROVIDER or run from a provider pane; do not run claim-leader"
3290
+ .to_string(),
3291
+ ),
3292
+ ),
3293
+ Some("caller_pane_missing") => (
3294
+ "leader_binding_incomplete",
3295
+ "caller_pane_missing".to_string(),
3296
+ Some("run from the intended leader tmux pane; do not run claim-leader".to_string()),
3297
+ ),
3298
+ Some("registry_readback_unavailable")
3299
+ | Some("receiver_scope_unavailable")
3300
+ | Some("receiver_live_channel_unavailable")
3301
+ | Some("registry_identity_mismatch") => (
3302
+ "leader_binding_unknown",
3303
+ format!("{}:{}", stage.unwrap_or("bind"), reason.unwrap_or("unknown")),
3304
+ Some(
3305
+ "inspect the selected-team registry file and live leader channel; do not claim-leader"
3306
+ .to_string(),
3307
+ ),
3308
+ ),
3309
+ Some("no_attached_owner") => (
3310
+ "leader_receiver_unbound",
3311
+ "no_attached_owner".to_string(),
3312
+ Some("team-agent claim-leader --confirm --json".to_string()),
3313
+ ),
3314
+ Some(reason) => (
3315
+ "leader_binding_unknown",
3316
+ format!("{}:{reason}", stage.unwrap_or("bind")),
3317
+ Some(
3318
+ "inspect the selected-team registry file and live leader channel; do not claim-leader"
3319
+ .to_string(),
3320
+ ),
3321
+ ),
3322
+ None => (
3323
+ "leader_binding_unknown",
3324
+ "unclassified_bind_failure".to_string(),
3325
+ Some(
3326
+ "inspect the selected-team registry file and live leader channel; do not claim-leader"
3327
+ .to_string(),
3328
+ ),
3329
+ ),
3330
+ }
3331
+ }
3332
+
3333
+ fn dirty_topology_next_actions(reason: &str, issue_ids: &[String], team: &str) -> Vec<String> {
3334
+ if reason == "worker_session_is_leader_session"
3335
+ || issue_ids
3336
+ .iter()
3337
+ .any(|id| id == "worker_session_is_leader_session")
3338
+ {
3339
+ return vec![
3340
+ "repair state.session_name to the worker session; it currently names the leader launcher session".to_string(),
3341
+ format!("team-agent diagnose --team {team} --json"),
3342
+ ];
3343
+ }
3344
+ let mut actions = vec![format!("team-agent diagnose --team {team} --json")];
3345
+ if issue_ids
3346
+ .iter()
3347
+ .any(|id| id.contains("socket") || id.contains("endpoint"))
3348
+ {
3349
+ actions.push(
3350
+ "repair tmux endpoint/socket split from the intended leader socket".to_string(),
3351
+ );
3352
+ }
3353
+ actions
3354
+ }
3355
+
3263
3356
  fn quick_start_value(report: crate::lifecycle::QuickStartReport) -> Value {
3264
3357
  match report {
3265
3358
  crate::lifecycle::QuickStartReport::Ready {
@@ -3269,6 +3362,7 @@ pub mod lifecycle_port {
3269
3362
  attach_commands,
3270
3363
  display_backend,
3271
3364
  worker_readiness,
3365
+ team,
3272
3366
  } => {
3273
3367
  // BUG-7: never emit bare "ready" while worker tool-load is unverified.
3274
3368
  // The summary string + a structured `worker_readiness` block tell the
@@ -3337,7 +3431,7 @@ pub mod lifecycle_port {
3337
3431
  } else if launch.leader_receiver_attached {
3338
3432
  (
3339
3433
  format!(
3340
- "quick-start launched (worker tool load unverified): {}",
3434
+ "team started; leader bound; send a task next (worker tool load unverified): {}",
3341
3435
  session_name.as_str()
3342
3436
  ),
3343
3437
  all_spawned && all_attached_receiver && all_resumable_have_session,
@@ -3355,33 +3449,43 @@ pub mod lifecycle_port {
3355
3449
  "session_capture_incomplete": !all_resumable_have_session,
3356
3450
  "incomplete_session_capture_agents": incomplete_session_capture_agents.clone(),
3357
3451
  "pending_session_agent_ids": incomplete_session_capture_agents,
3358
- "reason": "worker MCP tool set load not yet confirmed; run `team-agent doctor` or wait for first worker turn",
3452
+ "reason": "team started; leader bound; send a task next (worker tool load unverified)",
3359
3453
  }),
3360
3454
  )
3361
3455
  } else {
3456
+ let (state, reason, next_action) = public_leader_bind_failure(
3457
+ launch.leader_bind_stage.as_deref(),
3458
+ launch.leader_bind_reason.as_deref(),
3459
+ all_spawned,
3460
+ );
3461
+ let mut readiness = json!({
3462
+ "all_spawned": all_spawned,
3463
+ "all_workers_spawned": all_workers_spawned,
3464
+ "all_attached_receiver": all_attached_receiver,
3465
+ "attached_receiver": attached_receiver,
3466
+ "leader_receiver_attached": leader_receiver_attached,
3467
+ "all_resumable_have_session": all_resumable_have_session,
3468
+ "all_resumable_agents_have_sessions": all_resumable_agents_have_sessions,
3469
+ "ready": false,
3470
+ "state": state,
3471
+ "session_capture_complete": all_resumable_have_session,
3472
+ "session_capture_incomplete": !all_resumable_have_session,
3473
+ "incomplete_session_capture_agents": incomplete_session_capture_agents.clone(),
3474
+ "pending_session_agent_ids": incomplete_session_capture_agents,
3475
+ "reason": reason,
3476
+ });
3477
+ if let Some(next_action) = next_action {
3478
+ if let Some(object) = readiness.as_object_mut() {
3479
+ object.insert("next_action".to_string(), json!(next_action));
3480
+ }
3481
+ }
3362
3482
  (
3363
3483
  format!(
3364
- "quick-start degraded: {}; leader receiver unbound",
3484
+ "quick-start degraded: {}; {reason}",
3365
3485
  session_name.as_str()
3366
3486
  ),
3367
3487
  false,
3368
- json!({
3369
- "all_spawned": all_spawned,
3370
- "all_workers_spawned": all_workers_spawned,
3371
- "all_attached_receiver": all_attached_receiver,
3372
- "attached_receiver": attached_receiver,
3373
- "leader_receiver_attached": leader_receiver_attached,
3374
- "all_resumable_have_session": all_resumable_have_session,
3375
- "all_resumable_agents_have_sessions": all_resumable_agents_have_sessions,
3376
- "ready": all_spawned && all_attached_receiver && all_resumable_have_session,
3377
- "state": "leader_receiver_unbound",
3378
- "session_capture_complete": all_resumable_have_session,
3379
- "session_capture_incomplete": !all_resumable_have_session,
3380
- "incomplete_session_capture_agents": incomplete_session_capture_agents.clone(),
3381
- "pending_session_agent_ids": incomplete_session_capture_agents,
3382
- "reason": "launched team has no attached leader receiver",
3383
- "next_action": "claim-leader",
3384
- }),
3488
+ readiness,
3385
3489
  )
3386
3490
  }
3387
3491
  }
@@ -3393,6 +3497,7 @@ pub mod lifecycle_port {
3393
3497
  "reason": readiness_json.get("reason").cloned().unwrap_or(Value::Null),
3394
3498
  "ready": readiness_json.get("ready").cloned().unwrap_or(Value::Bool(false)),
3395
3499
  "session_name": session_name.as_str(),
3500
+ "team": team,
3396
3501
  "agent_ids": launch.started.iter().map(|agent| agent.agent_id.as_str()).collect::<Vec<_>>(),
3397
3502
  "dry_run": launch.dry_run,
3398
3503
  "display_backend": display_backend,
@@ -3409,12 +3514,14 @@ pub mod lifecycle_port {
3409
3514
  state_path,
3410
3515
  next_actions,
3411
3516
  attach_commands,
3517
+ agent_ids,
3412
3518
  } => json!({
3413
3519
  "ok": false,
3414
3520
  "status": "existing_runtime",
3415
3521
  "reason": "team already has runtime state; use restart",
3416
3522
  "summary": "existing runtime",
3417
3523
  "team": team,
3524
+ "agent_ids": agent_ids,
3418
3525
  "session_name": session_name.map(|s| s.as_str().to_string()),
3419
3526
  "state_path": state_path.map(|p| p.to_string_lossy().to_string()),
3420
3527
  "next_actions": next_actions,
@@ -3454,21 +3561,21 @@ pub mod lifecycle_port {
3454
3561
  "all_resumable_have_session": true,
3455
3562
  "all_resumable_agents_have_sessions": true,
3456
3563
  "ready": false,
3457
- "state": "leader_receiver_unbound",
3564
+ "state": "leader_bind_refused",
3458
3565
  "session_capture_complete": true,
3459
3566
  "session_capture_incomplete": false,
3460
3567
  "incomplete_session_capture_agents": [],
3461
3568
  "pending_session_agent_ids": [],
3462
- "reason": "launched team has no attached leader receiver",
3463
- "next_action": "claim-leader"
3569
+ "reason": "provider_unresolved",
3570
+ "next_action": "set TEAM_AGENT_LEADER_PROVIDER or run from a provider pane; do not run claim-leader"
3464
3571
  });
3465
3572
  let detail = json!({
3466
3573
  "ok": false,
3467
- "status": "leader_receiver_unbound",
3468
- "reason": "launched team has no attached leader receiver",
3574
+ "status": "leader_bind_refused",
3575
+ "reason": "provider_unresolved",
3469
3576
  "ready": false,
3470
3577
  "session_name": "team-fresh",
3471
- "next_actions": ["claim-leader"],
3578
+ "next_actions": ["set TEAM_AGENT_LEADER_PROVIDER or run from a provider pane; do not run claim-leader"],
3472
3579
  "attach_commands": ["tmux attach"],
3473
3580
  "send_commands": ["team-agent send worker hi"],
3474
3581
  "agent_ids": ["worker"],
@@ -3478,12 +3585,15 @@ pub mod lifecycle_port {
3478
3585
  });
3479
3586
  let mut default = detail.clone();
3480
3587
  compact_quick_start_value(&mut default);
3481
- assert_eq!(default["status"], json!("leader_receiver_unbound"));
3588
+ assert_eq!(default["status"], json!("leader_bind_refused"));
3482
3589
  assert_eq!(
3483
3590
  default["reason"],
3484
- json!("launched team has no attached leader receiver")
3591
+ json!("provider_unresolved")
3592
+ );
3593
+ assert_eq!(
3594
+ default["next_actions"],
3595
+ json!(["set TEAM_AGENT_LEADER_PROVIDER or run from a provider pane; do not run claim-leader"])
3485
3596
  );
3486
- assert_eq!(default["next_actions"], json!(["claim-leader"]));
3487
3597
  assert_eq!(default["attach_commands"], json!(["tmux attach"]));
3488
3598
  assert_eq!(
3489
3599
  default["send_commands"],
@@ -3492,8 +3602,11 @@ pub mod lifecycle_port {
3492
3602
  assert!(default.get("agent_ids").is_none());
3493
3603
  assert!(default.get("dry_run").is_none());
3494
3604
  assert_eq!(default["readiness"]["all_workers_spawned"], json!(true));
3495
- assert_eq!(default["readiness"]["state"], json!("leader_receiver_unbound"));
3496
- assert_eq!(default["readiness"]["next_action"], json!("claim-leader"));
3605
+ assert_eq!(default["readiness"]["state"], json!("leader_bind_refused"));
3606
+ assert_eq!(
3607
+ default["readiness"]["next_action"],
3608
+ json!("set TEAM_AGENT_LEADER_PROVIDER or run from a provider pane; do not run claim-leader")
3609
+ );
3497
3610
  assert!(default["readiness"].get("all_spawned").is_none());
3498
3611
  assert!(default["readiness"].get("all_attached_receiver").is_none());
3499
3612
  assert!(default["readiness"].get("leader_receiver_attached").is_none());
@@ -3507,6 +3620,152 @@ pub mod lifecycle_port {
3507
3620
  }
3508
3621
 
3509
3622
  #[test]
3623
+ fn pending_tool_load_success_output_explains_bound_send_without_doctor() {
3624
+ let value = quick_start_value(crate::lifecycle::QuickStartReport::Ready {
3625
+ session_name: crate::transport::SessionName::new("team-demo"),
3626
+ launch: Box::new(crate::lifecycle::LaunchReport {
3627
+ session_name: crate::transport::SessionName::new("team-demo"),
3628
+ started: vec![crate::lifecycle::StartedAgent {
3629
+ agent_id: crate::model::ids::AgentId::new("worker"),
3630
+ start_mode: crate::lifecycle::StartMode::Fresh,
3631
+ target: "worker".to_string(),
3632
+ spawned_at: "2026-09-10T00:00:00Z".to_string(),
3633
+ session_id: None,
3634
+ rollout_path: None,
3635
+ pending_session_id: None,
3636
+ claude_config_dir: None,
3637
+ provider_projects_root: None,
3638
+ managed_mcp_config: false,
3639
+ layout_window: None,
3640
+ layout_index: None,
3641
+ pane_index: None,
3642
+ display: crate::lifecycle::WorkerDisplay::Blocked {
3643
+ reason: crate::lifecycle::AdaptiveBlockReason::AggregatorRebuildFailed,
3644
+ },
3645
+ }],
3646
+ dry_run: false,
3647
+ tmux_endpoint: None,
3648
+ routes: Vec::new(),
3649
+ permissions: Vec::new(),
3650
+ leader_receiver_attached: true,
3651
+ leader_bind_stage: None,
3652
+ leader_bind_reason: None,
3653
+ session_capture_incomplete_agents: Vec::new(),
3654
+ }),
3655
+ next_actions: Vec::new(),
3656
+ attach_commands: Vec::new(),
3657
+ display_backend: "none".to_string(),
3658
+ worker_readiness: crate::lifecycle::QuickStartReadiness::PendingToolLoad,
3659
+ team: "team-demo".to_string(),
3660
+ });
3661
+ assert_eq!(
3662
+ value.get("summary").and_then(Value::as_str),
3663
+ Some("team started; leader bound; send a task next (worker tool load unverified): team-demo")
3664
+ );
3665
+ assert_eq!(
3666
+ value.get("reason").and_then(Value::as_str),
3667
+ Some("team started; leader bound; send a task next (worker tool load unverified)")
3668
+ );
3669
+ assert_eq!(
3670
+ value.get("status").and_then(Value::as_str),
3671
+ Some("pending_tool_load")
3672
+ );
3673
+ assert_eq!(value.get("ready").and_then(Value::as_bool), Some(true));
3674
+ assert!(value
3675
+ .get("reason")
3676
+ .and_then(Value::as_str)
3677
+ .is_some_and(|reason| !reason.contains("doctor")));
3678
+ }
3679
+
3680
+ #[test]
3681
+ fn restarted_json_ok_follows_bind_status() {
3682
+ let coordinator = crate::lifecycle::CoordinatorStartSummary {
3683
+ ok: true,
3684
+ status: "started".to_string(),
3685
+ pid: None,
3686
+ binary_path: None,
3687
+ binary_version: None,
3688
+ rotation_reason: None,
3689
+ binary_identity_relation: "same".to_string(),
3690
+ };
3691
+ let value = restart_value(
3692
+ crate::lifecycle::RestartReport::Restarted {
3693
+ session_name: crate::transport::SessionName::new("s"),
3694
+ agents: Vec::new(),
3695
+ coordinator_started: true,
3696
+ coordinator: coordinator.clone(),
3697
+ next_actions: Vec::new(),
3698
+ attach_commands: Vec::new(),
3699
+ attach_window_failures: None,
3700
+ leader_bind_ok: true,
3701
+ leader_bind_reason: None,
3702
+ },
3703
+ None,
3704
+ );
3705
+ assert_eq!(value.get("ok").and_then(Value::as_bool), Some(true));
3706
+ let incomplete = restart_value(
3707
+ crate::lifecycle::RestartReport::Restarted {
3708
+ session_name: crate::transport::SessionName::new("s"),
3709
+ agents: Vec::new(),
3710
+ coordinator_started: true,
3711
+ coordinator,
3712
+ next_actions: Vec::new(),
3713
+ attach_commands: Vec::new(),
3714
+ attach_window_failures: None,
3715
+ leader_bind_ok: false,
3716
+ leader_bind_reason: Some("leader_registry_index_missing".to_string()),
3717
+ },
3718
+ None,
3719
+ );
3720
+ assert_eq!(incomplete.get("ok").and_then(Value::as_bool), Some(false));
3721
+ assert_eq!(
3722
+ incomplete.get("reason").and_then(Value::as_str),
3723
+ Some("leader_registry_index_missing")
3724
+ );
3725
+ }
3726
+
3727
+ #[test]
3728
+ fn partial_json_exposes_leader_bind_ok() {
3729
+ let value = restart_value(
3730
+ crate::lifecycle::RestartReport::Partial {
3731
+ session_name: crate::transport::SessionName::new("s"),
3732
+ agents: Vec::new(),
3733
+ failed_agents: Vec::new(),
3734
+ coordinator_started: true,
3735
+ coordinator: crate::lifecycle::CoordinatorStartSummary {
3736
+ ok: true,
3737
+ status: "started".to_string(),
3738
+ pid: None,
3739
+ binary_path: None,
3740
+ binary_version: None,
3741
+ rotation_reason: None,
3742
+ binary_identity_relation: "same".to_string(),
3743
+ },
3744
+ next_actions: Vec::new(),
3745
+ attach_commands: Vec::new(),
3746
+ attach_window_failures: Some(json!({"registry_maintain_failed": true})),
3747
+ leader_bind_ok: false,
3748
+ leader_bind_reason: Some("leader_registry_maintain_failed".to_string()),
3749
+ },
3750
+ None,
3751
+ );
3752
+ assert_eq!(value.get("ok").and_then(Value::as_bool), Some(false));
3753
+ assert_eq!(
3754
+ value.get("leader_bind_ok").and_then(Value::as_bool),
3755
+ Some(false)
3756
+ );
3757
+ assert_eq!(
3758
+ value.get("leader_bind_reason").and_then(Value::as_str),
3759
+ Some("leader_registry_maintain_failed")
3760
+ );
3761
+ assert_eq!(
3762
+ value
3763
+ .pointer("/attach_window_failures/registry_maintain_failed")
3764
+ .and_then(Value::as_bool),
3765
+ Some(true)
3766
+ );
3767
+ }
3768
+
3510
3769
  fn restart_default_receipt_is_compact_but_detail_preserves_coordinator() {
3511
3770
  let detail = json!({
3512
3771
  "ok": false,
@@ -3526,6 +3785,128 @@ pub mod lifecycle_port {
3526
3785
  assert!(detail.get("coordinator").is_some());
3527
3786
  }
3528
3787
 
3788
+ #[test]
3789
+ #[serial_test::serial(env)]
3790
+ fn existing_runtime_producer_scopes_send_team() {
3791
+ let _hermetic = super::super::hermetic_test_support::HermeticTestEnv::enter(
3792
+ "quick-start-value-existing-runtime",
3793
+ );
3794
+ let root = std::env::temp_dir().join(format!(
3795
+ "ta-n5-prod-{}-{}",
3796
+ std::process::id(),
3797
+ std::time::SystemTime::now()
3798
+ .duration_since(std::time::UNIX_EPOCH)
3799
+ .unwrap()
3800
+ .as_nanos()
3801
+ ));
3802
+ let team_dir = root.join("teamdir");
3803
+ std::fs::create_dir_all(team_dir.join("agents")).unwrap();
3804
+ std::fs::write(
3805
+ team_dir.join("TEAM.md"),
3806
+ "---\nname: quickteam\nobjective: Quick start.\nprovider: codex\n---\n\nQuick-start team.\n",
3807
+ )
3808
+ .unwrap();
3809
+ std::fs::write(
3810
+ team_dir.join("agents").join("implementer.md"),
3811
+ "---\nname: implementer\nrole: Implementation Engineer\nprovider: codex\nmodel: gpt-5.5\nauth_mode: subscription\ndangerously_skip_permissions: false\ntools:\n - mcp_team\n---\n\nImplement bounded tasks.\n",
3812
+ )
3813
+ .unwrap();
3814
+ let transport = crate::transport::test_support::OfflineTransport::new();
3815
+ let first = crate::lifecycle::quick_start_with_transport(
3816
+ &team_dir,
3817
+ Some("from-name"),
3818
+ true,
3819
+ None,
3820
+ &transport,
3821
+ )
3822
+ .expect("first quick-start");
3823
+ let crate::lifecycle::QuickStartReport::Ready { team: ready_team, .. } = &first
3824
+ else {
3825
+ panic!("first producer must return Ready; got {first:?}");
3826
+ };
3827
+ assert_eq!(ready_team, "from-name");
3828
+ let ready_command = crate::cli::adapters::send_command(
3829
+ "implementer",
3830
+ &team_dir,
3831
+ Some(ready_team.as_str()),
3832
+ )
3833
+ .expect("Ready send_command");
3834
+ assert_eq!(
3835
+ crate::cli::adapters::split_shell_argv(&ready_command),
3836
+ vec![
3837
+ "team-agent".to_string(),
3838
+ "send".to_string(),
3839
+ "implementer".to_string(),
3840
+ "MESSAGE".to_string(),
3841
+ "--workspace".to_string(),
3842
+ team_dir.to_string_lossy().into_owned(),
3843
+ "--team".to_string(),
3844
+ "from-name".to_string(),
3845
+ ]
3846
+ );
3847
+ let report = crate::lifecycle::quick_start_with_transport(
3848
+ &team_dir,
3849
+ Some("from-name"),
3850
+ true,
3851
+ None,
3852
+ &transport,
3853
+ )
3854
+ .expect("second quick-start");
3855
+ let crate::lifecycle::QuickStartReport::ExistingRuntime {
3856
+ team: canonical,
3857
+ agent_ids,
3858
+ ..
3859
+ } = &report
3860
+ else {
3861
+ panic!("producer must return ExistingRuntime; got {report:?}");
3862
+ };
3863
+ assert_eq!(canonical.as_deref(), Some("from-name"));
3864
+ assert!(agent_ids.iter().any(|id| id == "implementer"));
3865
+ let mut value = quick_start_value(report);
3866
+ crate::cli::adapters::append_send_guidance(&mut value, &team_dir, None);
3867
+ let command = value
3868
+ .pointer("/send_commands/0")
3869
+ .and_then(Value::as_str)
3870
+ .expect("producer send_commands");
3871
+ assert_eq!(
3872
+ crate::cli::adapters::split_shell_argv(command),
3873
+ vec![
3874
+ "team-agent".to_string(),
3875
+ "send".to_string(),
3876
+ "implementer".to_string(),
3877
+ "MESSAGE".to_string(),
3878
+ "--workspace".to_string(),
3879
+ team_dir.to_string_lossy().into_owned(),
3880
+ "--team".to_string(),
3881
+ "from-name".to_string(),
3882
+ ]
3883
+ );
3884
+ let _ = std::fs::remove_dir_all(&root);
3885
+ }
3886
+
3887
+ #[test]
3888
+ fn existing_runtime_json_includes_canonical_team_and_agent_ids() {
3889
+ let value = quick_start_value(crate::lifecycle::QuickStartReport::ExistingRuntime {
3890
+ team: Some("from-spec".to_string()),
3891
+ session_name: Some(crate::transport::SessionName::new("team-from-spec")),
3892
+ state_path: Some(PathBuf::from("/tmp/state.json")),
3893
+ next_actions: vec!["restart".to_string()],
3894
+ attach_commands: Vec::new(),
3895
+ agent_ids: vec!["sol".to_string(), "luna".to_string()],
3896
+ });
3897
+ assert_eq!(value.get("team").and_then(Value::as_str), Some("from-spec"));
3898
+ assert_eq!(
3899
+ value
3900
+ .get("agent_ids")
3901
+ .and_then(Value::as_array)
3902
+ .map(|items| items
3903
+ .iter()
3904
+ .filter_map(Value::as_str)
3905
+ .collect::<Vec<_>>()),
3906
+ Some(vec!["sol", "luna"])
3907
+ );
3908
+ }
3909
+
3529
3910
  #[test]
3530
3911
  fn existing_runtime_json_includes_attach_commands() {
3531
3912
  let value = quick_start_value(crate::lifecycle::QuickStartReport::ExistingRuntime {
@@ -3536,6 +3917,7 @@ pub mod lifecycle_port {
3536
3917
  attach_commands: vec![
3537
3918
  "tmux -S /tmp/tmux-501/ta-test attach -t team-teamA:worker".to_string()
3538
3919
  ],
3920
+ agent_ids: vec!["worker".to_string()],
3539
3921
  });
3540
3922
  assert_eq!(
3541
3923
  value.pointer("/attach_commands/0").and_then(Value::as_str),
@@ -3548,6 +3930,37 @@ pub mod lifecycle_port {
3548
3930
  );
3549
3931
  }
3550
3932
 
3933
+ #[test]
3934
+ fn existing_runtime_send_guidance_does_not_pair_alice_with_bob() {
3935
+ let mut value = quick_start_value(crate::lifecycle::QuickStartReport::ExistingRuntime {
3936
+ team: Some("bob".to_string()),
3937
+ session_name: Some(crate::transport::SessionName::new("team-bob")),
3938
+ state_path: Some(PathBuf::from("/tmp/state.json")),
3939
+ next_actions: vec!["restart".to_string()],
3940
+ attach_commands: Vec::new(),
3941
+ agent_ids: vec!["bob".to_string()],
3942
+ });
3943
+ crate::cli::adapters::append_send_guidance(
3944
+ &mut value,
3945
+ Path::new("/tmp/ws"),
3946
+ Some("bob"),
3947
+ );
3948
+ let command = value
3949
+ .pointer("/send_commands/0")
3950
+ .and_then(Value::as_str)
3951
+ .expect("bob send command");
3952
+ let argv = crate::cli::adapters::split_shell_argv(command);
3953
+ assert_eq!(argv.get(2).map(String::as_str), Some("bob"));
3954
+ assert_eq!(
3955
+ argv.iter()
3956
+ .position(|item| item == "--team")
3957
+ .and_then(|i| argv.get(i + 1))
3958
+ .map(String::as_str),
3959
+ Some("bob")
3960
+ );
3961
+ assert!(!argv.iter().any(|item| item == "alice"));
3962
+ }
3963
+
3551
3964
  #[test]
3552
3965
  fn preflight_blocked_json_includes_empty_attach_commands() {
3553
3966
  let value = quick_start_value(crate::lifecycle::QuickStartReport::PreflightBlocked {
@@ -3579,40 +3992,6 @@ pub mod lifecycle_port {
3579
3992
  Some(crate::cli::QUICK_START_REMINDER)
3580
3993
  );
3581
3994
  }
3582
-
3583
- #[test]
3584
- fn spawn_failure_without_session_does_not_claim_a_worker_pane_exists() {
3585
- let value = restart_value(
3586
- crate::lifecycle::RestartReport::Failed {
3587
- session_name: crate::transport::SessionName::new("team-current"),
3588
- failed_agents: vec![crate::lifecycle::RestartFailedAgent {
3589
- agent_id: crate::model::ids::AgentId::new("coder"),
3590
- restart_mode: crate::lifecycle::StartMode::Fresh,
3591
- decision: crate::lifecycle::ResumeDecision::FreshStart,
3592
- session_id: None,
3593
- phase: "spawn".to_string(),
3594
- error: "provider io error".to_string(),
3595
- }],
3596
- next_actions: Vec::new(),
3597
- attach_commands: Vec::new(),
3598
- },
3599
- None,
3600
- );
3601
- let action = value
3602
- .pointer("/failed_agents/0/action")
3603
- .and_then(Value::as_str)
3604
- .expect("spawn failure action");
3605
-
3606
- assert!(
3607
- action.contains("before a pane was created"),
3608
- "action={action}"
3609
- );
3610
- assert!(
3611
- !action.contains("inspect worker coder output"),
3612
- "action={action}"
3613
- );
3614
- assert!(!action.contains("restart-agent"), "action={action}");
3615
- }
3616
3995
  }
3617
3996
 
3618
3997
  pub(crate) fn compact_restart_value(value: &mut Value) {
@@ -3624,7 +4003,7 @@ pub mod lifecycle_port {
3624
4003
  }
3625
4004
  }
3626
4005
 
3627
- fn restart_value(report: crate::lifecycle::RestartReport, team: Option<&str>) -> Value {
4006
+ pub fn restart_value(report: crate::lifecycle::RestartReport, team: Option<&str>) -> Value {
3628
4007
  match report {
3629
4008
  crate::lifecycle::RestartReport::Restarted {
3630
4009
  session_name,
@@ -3633,17 +4012,29 @@ pub mod lifecycle_port {
3633
4012
  coordinator,
3634
4013
  next_actions,
3635
4014
  attach_commands,
3636
- } => json!({
3637
- "ok": true,
3638
- "status": "restarted",
3639
- "session_name": session_name.as_str(),
3640
- "agents": agents.iter().map(|a| a.agent_id.as_str()).collect::<Vec<_>>(),
3641
- "coordinator_started": coordinator_started,
3642
- "coordinator": crate::lifecycle::coordinator_start_summary_value(&coordinator),
3643
- "next_actions": next_actions,
3644
- "attach_commands": attach_commands,
3645
- "reminder": crate::cli::QUICK_START_REMINDER,
3646
- }),
4015
+ attach_window_failures,
4016
+ leader_bind_ok,
4017
+ leader_bind_reason,
4018
+ } => {
4019
+ let mut value = json!({
4020
+ "ok": leader_bind_ok,
4021
+ "status": if leader_bind_ok { "restarted" } else { "restarted_binding_incomplete" },
4022
+ "session_name": session_name.as_str(),
4023
+ "agents": agents.iter().map(|a| a.agent_id.as_str()).collect::<Vec<_>>(),
4024
+ "coordinator_started": coordinator_started,
4025
+ "coordinator": crate::lifecycle::coordinator_start_summary_value(&coordinator),
4026
+ "next_actions": next_actions,
4027
+ "attach_commands": attach_commands,
4028
+ "reminder": crate::cli::QUICK_START_REMINDER,
4029
+ });
4030
+ if let Some(debt) = attach_window_failures {
4031
+ value["attach_window_failures"] = debt;
4032
+ }
4033
+ if let Some(reason) = leader_bind_reason {
4034
+ value["reason"] = json!(reason);
4035
+ }
4036
+ value
4037
+ },
3647
4038
  crate::lifecycle::RestartReport::Partial {
3648
4039
  session_name,
3649
4040
  agents,
@@ -3652,35 +4043,52 @@ pub mod lifecycle_port {
3652
4043
  coordinator,
3653
4044
  next_actions,
3654
4045
  attach_commands,
3655
- } => json!({
3656
- "ok": false,
3657
- "status": "partial",
3658
- "reason": "restart_agent_failed",
3659
- "session_name": session_name.as_str(),
3660
- "agents": agents.iter().map(|a| a.agent_id.as_str()).collect::<Vec<_>>(),
3661
- "failed_agents": failed_agents.iter().map(|failure| json!({
3662
- "agent_id": failure.agent_id.as_str(),
3663
- "restart_mode": failure.restart_mode,
3664
- "decision": failure.decision,
3665
- "session_id": failure.session_id.as_ref().map(|session| session.as_str()),
3666
- "phase": failure.phase,
3667
- "error": failure.error,
3668
- "action": crate::lifecycle::restart_failure_action(
3669
- &failure.agent_id,
3670
- &failure.phase,
3671
- failure.session_id.as_ref(),
3672
- ),
3673
- "log": format!(
3674
- ".team/logs/coordinator.log and .team/runtime/state.json agent={}",
3675
- failure.agent_id
3676
- ),
3677
- })).collect::<Vec<_>>(),
3678
- "coordinator_started": coordinator_started,
3679
- "coordinator": crate::lifecycle::coordinator_start_summary_value(&coordinator),
3680
- "next_actions": next_actions,
3681
- "attach_commands": attach_commands,
3682
- "reminder": crate::cli::QUICK_START_REMINDER,
3683
- }),
4046
+ attach_window_failures,
4047
+ leader_bind_ok,
4048
+ leader_bind_reason,
4049
+ } => {
4050
+ let mut value = json!({
4051
+ "ok": false,
4052
+ "status": "partial",
4053
+ "reason": "restart_agent_failed",
4054
+ "leader_bind_ok": leader_bind_ok,
4055
+ "session_name": session_name.as_str(),
4056
+ "agents": agents.iter().map(|a| a.agent_id.as_str()).collect::<Vec<_>>(),
4057
+ "failed_agents": failed_agents.iter().map(|failure| json!({
4058
+ "agent_id": failure.agent_id.as_str(),
4059
+ "restart_mode": failure.restart_mode,
4060
+ "decision": failure.decision,
4061
+ "session_id": failure.session_id.as_ref().map(|session| session.as_str()),
4062
+ "phase": failure.phase,
4063
+ "error": failure.error,
4064
+ "action": format!(
4065
+ "inspect worker {} output, then restart that worker with `team-agent restart-agent {}` or rerun `team-agent restart --allow-fresh`",
4066
+ failure.agent_id,
4067
+ failure.agent_id
4068
+ ),
4069
+ "log": format!(
4070
+ ".team/logs/coordinator.log and .team/runtime/state.json agent={}",
4071
+ failure.agent_id
4072
+ ),
4073
+ })).collect::<Vec<_>>(),
4074
+ "coordinator_started": coordinator_started,
4075
+ "coordinator": crate::lifecycle::coordinator_start_summary_value(&coordinator),
4076
+ "next_actions": next_actions,
4077
+ "attach_commands": attach_commands,
4078
+ "reminder": crate::cli::QUICK_START_REMINDER,
4079
+ });
4080
+ if let Some(debt) = attach_window_failures {
4081
+ value["attach_window_failures"] = debt;
4082
+ }
4083
+ if !leader_bind_ok {
4084
+ value["ok"] = json!(false);
4085
+ value["status"] = json!("partial_binding_incomplete");
4086
+ }
4087
+ if let Some(reason) = leader_bind_reason {
4088
+ value["leader_bind_reason"] = json!(reason);
4089
+ }
4090
+ value
4091
+ },
3684
4092
  crate::lifecycle::RestartReport::Failed {
3685
4093
  session_name,
3686
4094
  failed_agents,
@@ -3699,10 +4107,10 @@ pub mod lifecycle_port {
3699
4107
  "session_id": failure.session_id.as_ref().map(|session| session.as_str()),
3700
4108
  "phase": failure.phase,
3701
4109
  "error": failure.error,
3702
- "action": crate::lifecycle::restart_failure_action(
3703
- &failure.agent_id,
3704
- &failure.phase,
3705
- failure.session_id.as_ref(),
4110
+ "action": format!(
4111
+ "inspect worker {} output, then restart that worker with `team-agent restart-agent {}` or rerun `team-agent restart --allow-fresh`",
4112
+ failure.agent_id,
4113
+ failure.agent_id
3706
4114
  ),
3707
4115
  "log": format!(
3708
4116
  ".team/logs/coordinator.log and .team/runtime/state.json agent={}",
@@ -4055,9 +4463,6 @@ pub mod lifecycle_port {
4055
4463
  let repair_team = team
4056
4464
  .filter(|team| !team.is_empty())
4057
4465
  .unwrap_or(session_name.as_str());
4058
- let claim =
4059
- format!("team-agent claim-leader --team {repair_team} --confirm --json");
4060
- let takeover = format!("team-agent takeover --team {repair_team} --confirm --json");
4061
4466
  json!({
4062
4467
  "ok": false,
4063
4468
  "status": "refused_dirty_topology",
@@ -4068,11 +4473,11 @@ pub mod lifecycle_port {
4068
4473
  .iter()
4069
4474
  .map(|id| json!({"id": id}))
4070
4475
  .collect::<Vec<_>>(),
4071
- "next_actions": [
4072
- "team-agent diagnose --json",
4073
- claim,
4074
- takeover
4075
- ],
4476
+ "next_actions": dirty_topology_next_actions(
4477
+ &reason,
4478
+ &issue_ids,
4479
+ repair_team,
4480
+ ),
4076
4481
  "reminder": crate::cli::QUICK_START_REMINDER,
4077
4482
  })
4078
4483
  }
@@ -4290,6 +4695,45 @@ pub mod lifecycle_port {
4290
4695
  );
4291
4696
  }
4292
4697
 
4698
+ #[test]
4699
+ fn restart_missing_team_spec_does_not_point_to_add_agent() {
4700
+ // Live empty-workspace restart: error is missing spec/runtime, but the
4701
+ // workspace path commonly contains "team-agent", which the substring
4702
+ // "not found"+"agent" matcher treats as start-agent miss.
4703
+ let msg = "team select: active team spec not found: \
4704
+ input_workspace=/tmp/team-agent-live-missing \
4705
+ expected_runtime_dir=/tmp/team-agent-live-missing/.team/runtime";
4706
+ let na = error_next_action(msg).expect("missing spec must keep a recovery action");
4707
+ assert!(
4708
+ !na.contains("add-agent"),
4709
+ "must not steer restart-missing-spec to add-agent: {na}"
4710
+ );
4711
+ assert!(
4712
+ !na.contains("start-agent only starts an agent"),
4713
+ "must not reuse the start-agent miss copy: {na}"
4714
+ );
4715
+ assert!(
4716
+ na.contains("quick-start"),
4717
+ "must steer first launch to quick-start: {na}"
4718
+ );
4719
+ }
4720
+
4721
+ #[test]
4722
+ fn restart_missing_spec_error_value_keeps_recovery_action() {
4723
+ let err = crate::lifecycle::LifecycleError::RequirementUnmet(
4724
+ "team select: active team spec not found: input_workspace=/tmp/team-agent-empty expected_runtime_dir=/tmp/team-agent-empty/.team/runtime"
4725
+ .to_string(),
4726
+ );
4727
+ let v = error_value(err);
4728
+ assert_eq!(v["ok"], serde_json::json!(false));
4729
+ let na = v["next_action"].as_str().unwrap_or("");
4730
+ assert!(
4731
+ na.contains("quick-start"),
4732
+ "error_value must attach spec-missing recovery, got {v}"
4733
+ );
4734
+ assert!(!na.contains("add-agent"), "must not attach add-agent: {v}");
4735
+ }
4736
+
4293
4737
  #[test]
4294
4738
  fn error_value_attaches_next_action_field() {
4295
4739
  let err = crate::lifecycle::LifecycleError::RequirementUnmet(
@@ -4305,6 +4749,21 @@ pub mod lifecycle_port {
4305
4749
  "error_value must attach the add-agent guidance: {v}"
4306
4750
  );
4307
4751
  }
4752
+
4753
+ #[test]
4754
+ fn profile_not_found_preserves_error_without_agent_guidance() {
4755
+ let detail =
4756
+ "profile pi-default not found; run team-agent profile init pi-default --auth-mode subscription";
4757
+ let v = error_value(crate::lifecycle::LifecycleError::RequirementUnmet(
4758
+ detail.to_string(),
4759
+ ));
4760
+ let expected = format!("agent start requirement unmet: {detail}");
4761
+ assert_eq!(v["error"].as_str(), Some(expected.as_str()));
4762
+ assert!(
4763
+ v.get("next_action").is_none(),
4764
+ "profile missing must not suggest start-agent/add-agent: {v}"
4765
+ );
4766
+ }
4308
4767
  }
4309
4768
  }
4310
4769
 
@@ -5260,11 +5719,14 @@ pub mod leader_port {
5260
5719
  if let Some(topology_convergence) = result.topology_convergence {
5261
5720
  out.insert("topology_convergence".to_string(), topology_convergence);
5262
5721
  }
5722
+ if let Some(attach_window_failures) = result.attach_window_failures {
5723
+ out.insert("attach_window_failures".to_string(), attach_window_failures);
5724
+ }
5263
5725
  Value::Object(out)
5264
5726
  }
5265
5727
 
5266
5728
  fn attach_lease_value(result: crate::leader::LeaseResult, requeued: Value) -> Value {
5267
- json!({
5729
+ let mut value = json!({
5268
5730
  "ok": result.ok,
5269
5731
  "leader_receiver": result
5270
5732
  .receiver
@@ -5277,7 +5739,11 @@ pub mod leader_port {
5277
5739
  "action": result.action,
5278
5740
  },
5279
5741
  "requeued_exhausted_watchers": requeued,
5280
- })
5742
+ });
5743
+ if let Some(attach_window_failures) = result.attach_window_failures {
5744
+ value["attach_window_failures"] = attach_window_failures;
5745
+ }
5746
+ value
5281
5747
  }
5282
5748
 
5283
5749
  fn attach_requeued_exhausted_watchers(