@team-agent/installer 0.5.77 → 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.
- package/Cargo.lock +1 -1
- package/Cargo.toml +1 -1
- package/crates/team-agent/src/cli/adapters.rs +161 -29
- package/crates/team-agent/src/cli/diagnose.rs +333 -68
- package/crates/team-agent/src/cli/emit.rs +24 -8
- package/crates/team-agent/src/cli/mod.rs +543 -98
- package/crates/team-agent/src/cli/models.rs +362 -32
- package/crates/team-agent/src/cli/named_address.rs +19 -5
- package/crates/team-agent/src/cli/profile.rs +35 -4
- package/crates/team-agent/src/cli/spec.rs +7 -2
- package/crates/team-agent/src/cli/status_port/compact.rs +12 -8
- package/crates/team-agent/src/cli/status_port/snapshot.rs +15 -12
- package/crates/team-agent/src/cli/status_port/store.rs +1 -1
- package/crates/team-agent/src/cli/tests/missing_subcommands.rs +62 -8
- package/crates/team-agent/src/cli/tests/named_address.rs +70 -0
- package/crates/team-agent/src/cli/tests/verb_profile.rs +81 -6
- package/crates/team-agent/src/cli/types.rs +1 -0
- package/crates/team-agent/src/leader/lease.rs +492 -33
- package/crates/team-agent/src/leader/owner_bind.rs +40 -12
- package/crates/team-agent/src/leader/rediscover/tests.rs +65 -0
- package/crates/team-agent/src/leader/rediscover.rs +36 -6
- package/crates/team-agent/src/leader/start.rs +151 -14
- package/crates/team-agent/src/leader/tests/wake_start_owner.rs +15 -9
- package/crates/team-agent/src/lifecycle/launch/add_agent.rs +28 -227
- package/crates/team-agent/src/lifecycle/launch/add_agent_state.rs +0 -18
- package/crates/team-agent/src/lifecycle/launch/cursor_mcp.rs +94 -227
- package/crates/team-agent/src/lifecycle/launch/mcp_config.rs +59 -10
- package/crates/team-agent/src/lifecycle/launch/pi_mcp.rs +65 -18
- package/crates/team-agent/src/lifecycle/launch/quick_start.rs +1449 -282
- package/crates/team-agent/src/lifecycle/launch/readiness.rs +349 -4
- package/crates/team-agent/src/lifecycle/launch/spawn.rs +11 -11
- package/crates/team-agent/src/lifecycle/launch/spec_state.rs +35 -3
- package/crates/team-agent/src/lifecycle/launch/worker_env.rs +16 -0
- package/crates/team-agent/src/lifecycle/launch.rs +30 -7
- package/crates/team-agent/src/lifecycle/profile_launch.rs +37 -3
- package/crates/team-agent/src/lifecycle/restart/agent.rs +112 -100
- package/crates/team-agent/src/lifecycle/restart/common.rs +82 -52
- package/crates/team-agent/src/lifecycle/restart/rebuild.rs +996 -66
- package/crates/team-agent/src/lifecycle/restart.rs +2 -2
- package/crates/team-agent/src/lifecycle/tests/acceptance_b_batch_red.rs +3 -0
- package/crates/team-agent/src/lifecycle/tests/behavioral_diff_264_red.rs +15 -0
- package/crates/team-agent/src/lifecycle/tests/claude_compatible_config_red.rs +5 -0
- package/crates/team-agent/src/lifecycle/tests/claude_profile_launch_red.rs +11 -13
- package/crates/team-agent/src/lifecycle/tests/clone_agent_preserves_source_tools.rs +119 -4
- package/crates/team-agent/src/lifecycle/tests/communication_mode_runtime_contract_red.rs +12 -0
- package/crates/team-agent/src/lifecycle/tests/copilot_provider_red.rs +22 -0
- package/crates/team-agent/src/lifecycle/tests/core_034_real_red.rs +12 -0
- package/crates/team-agent/src/lifecycle/tests/cursor_mcp_overlay.rs +265 -198
- package/crates/team-agent/src/lifecycle/tests/cursor_require_explicit_model_red.rs +14 -30
- package/crates/team-agent/src/lifecycle/tests/cursor_restart_resume_red.rs +3 -0
- package/crates/team-agent/src/lifecycle/tests/g1_silent_faces.rs +2 -5
- package/crates/team-agent/src/lifecycle/tests/grok_effort_argv_red.rs +10 -0
- package/crates/team-agent/src/lifecycle/tests/grok_mcp_overlay_red.rs +58 -35
- package/crates/team-agent/src/lifecycle/tests/grok_require_explicit_model_red.rs +10 -0
- package/crates/team-agent/src/lifecycle/tests/grok_restart_resume_red.rs +9 -0
- package/crates/team-agent/src/lifecycle/tests/harvest2_a_batch_red.rs +3 -2
- package/crates/team-agent/src/lifecycle/tests/lane_ops.rs +0 -168
- package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +145 -212
- package/crates/team-agent/src/lifecycle/tests/phase_b_contracts.rs +16 -2
- package/crates/team-agent/src/lifecycle/tests/phase_golden.rs +54 -9
- package/crates/team-agent/src/lifecycle/tests/pi_dual_entry_red.rs +78 -0
- package/crates/team-agent/src/lifecycle/tests/team_in_team_identity_scope_red.rs +3 -0
- package/crates/team-agent/src/lifecycle/tests/test_isolation_escape_contract.rs +51 -10
- package/crates/team-agent/src/lifecycle/tests/verify_rs031_window_consistency_red.rs +50 -7
- package/crates/team-agent/src/lifecycle/tests/worker_spawn_env_red.rs +137 -0
- package/crates/team-agent/src/lifecycle/types.rs +8 -0
- package/crates/team-agent/src/mcp_server/wire.rs +140 -11
- package/crates/team-agent/src/messaging/delivery.rs +67 -8
- package/crates/team-agent/src/messaging/leader_channel.rs +56 -0
- package/crates/team-agent/src/messaging/tests/runtime.rs +10 -2
- package/crates/team-agent/src/provider/adapters/cursor_agent.rs +2 -5
- package/crates/team-agent/src/state/persist.rs +97 -1
- package/crates/team-agent/src/state/repository.rs +29 -0
- package/crates/team-agent/src/test_support/models_timeout_fixture.rs +43 -21
- package/crates/team-agent/src/tmux_backend/tests.rs +121 -4
- package/crates/team-agent/src/tmux_backend.rs +208 -118
- package/crates/team-agent/src/transport.rs +72 -0
- package/crates/team-agent/src/transport_factory.rs +53 -0
- package/package.json +4 -5
- package/skills/team-agent/SKILL.md +45 -17
- package/skills/team-agent/command-coverage.json +4 -29
- package/npm/check-skill-links.mjs +0 -88
- 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
|
-
//
|
|
3100
|
-
//
|
|
3101
|
-
if message.contains("not found")
|
|
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
|
-
"
|
|
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": "
|
|
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: {};
|
|
3484
|
+
"quick-start degraded: {}; {reason}",
|
|
3365
3485
|
session_name.as_str()
|
|
3366
3486
|
),
|
|
3367
3487
|
false,
|
|
3368
|
-
|
|
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": "
|
|
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": "
|
|
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": "
|
|
3468
|
-
"reason": "
|
|
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!("
|
|
3588
|
+
assert_eq!(default["status"], json!("leader_bind_refused"));
|
|
3482
3589
|
assert_eq!(
|
|
3483
3590
|
default["reason"],
|
|
3484
|
-
json!("
|
|
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!("
|
|
3496
|
-
assert_eq!(
|
|
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,
|
|
@@ -3634,10 +4013,12 @@ pub mod lifecycle_port {
|
|
|
3634
4013
|
next_actions,
|
|
3635
4014
|
attach_commands,
|
|
3636
4015
|
attach_window_failures,
|
|
4016
|
+
leader_bind_ok,
|
|
4017
|
+
leader_bind_reason,
|
|
3637
4018
|
} => {
|
|
3638
4019
|
let mut value = json!({
|
|
3639
|
-
"ok":
|
|
3640
|
-
"status": "restarted",
|
|
4020
|
+
"ok": leader_bind_ok,
|
|
4021
|
+
"status": if leader_bind_ok { "restarted" } else { "restarted_binding_incomplete" },
|
|
3641
4022
|
"session_name": session_name.as_str(),
|
|
3642
4023
|
"agents": agents.iter().map(|a| a.agent_id.as_str()).collect::<Vec<_>>(),
|
|
3643
4024
|
"coordinator_started": coordinator_started,
|
|
@@ -3649,6 +4030,9 @@ pub mod lifecycle_port {
|
|
|
3649
4030
|
if let Some(debt) = attach_window_failures {
|
|
3650
4031
|
value["attach_window_failures"] = debt;
|
|
3651
4032
|
}
|
|
4033
|
+
if let Some(reason) = leader_bind_reason {
|
|
4034
|
+
value["reason"] = json!(reason);
|
|
4035
|
+
}
|
|
3652
4036
|
value
|
|
3653
4037
|
},
|
|
3654
4038
|
crate::lifecycle::RestartReport::Partial {
|
|
@@ -3660,11 +4044,14 @@ pub mod lifecycle_port {
|
|
|
3660
4044
|
next_actions,
|
|
3661
4045
|
attach_commands,
|
|
3662
4046
|
attach_window_failures,
|
|
4047
|
+
leader_bind_ok,
|
|
4048
|
+
leader_bind_reason,
|
|
3663
4049
|
} => {
|
|
3664
4050
|
let mut value = json!({
|
|
3665
4051
|
"ok": false,
|
|
3666
4052
|
"status": "partial",
|
|
3667
4053
|
"reason": "restart_agent_failed",
|
|
4054
|
+
"leader_bind_ok": leader_bind_ok,
|
|
3668
4055
|
"session_name": session_name.as_str(),
|
|
3669
4056
|
"agents": agents.iter().map(|a| a.agent_id.as_str()).collect::<Vec<_>>(),
|
|
3670
4057
|
"failed_agents": failed_agents.iter().map(|failure| json!({
|
|
@@ -3674,10 +4061,10 @@ pub mod lifecycle_port {
|
|
|
3674
4061
|
"session_id": failure.session_id.as_ref().map(|session| session.as_str()),
|
|
3675
4062
|
"phase": failure.phase,
|
|
3676
4063
|
"error": failure.error,
|
|
3677
|
-
"action":
|
|
3678
|
-
|
|
3679
|
-
|
|
3680
|
-
failure.
|
|
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
|
|
3681
4068
|
),
|
|
3682
4069
|
"log": format!(
|
|
3683
4070
|
".team/logs/coordinator.log and .team/runtime/state.json agent={}",
|
|
@@ -3693,6 +4080,13 @@ pub mod lifecycle_port {
|
|
|
3693
4080
|
if let Some(debt) = attach_window_failures {
|
|
3694
4081
|
value["attach_window_failures"] = debt;
|
|
3695
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
|
+
}
|
|
3696
4090
|
value
|
|
3697
4091
|
},
|
|
3698
4092
|
crate::lifecycle::RestartReport::Failed {
|
|
@@ -3713,10 +4107,10 @@ pub mod lifecycle_port {
|
|
|
3713
4107
|
"session_id": failure.session_id.as_ref().map(|session| session.as_str()),
|
|
3714
4108
|
"phase": failure.phase,
|
|
3715
4109
|
"error": failure.error,
|
|
3716
|
-
"action":
|
|
3717
|
-
|
|
3718
|
-
|
|
3719
|
-
failure.
|
|
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
|
|
3720
4114
|
),
|
|
3721
4115
|
"log": format!(
|
|
3722
4116
|
".team/logs/coordinator.log and .team/runtime/state.json agent={}",
|
|
@@ -4069,9 +4463,6 @@ pub mod lifecycle_port {
|
|
|
4069
4463
|
let repair_team = team
|
|
4070
4464
|
.filter(|team| !team.is_empty())
|
|
4071
4465
|
.unwrap_or(session_name.as_str());
|
|
4072
|
-
let claim =
|
|
4073
|
-
format!("team-agent claim-leader --team {repair_team} --confirm --json");
|
|
4074
|
-
let takeover = format!("team-agent takeover --team {repair_team} --confirm --json");
|
|
4075
4466
|
json!({
|
|
4076
4467
|
"ok": false,
|
|
4077
4468
|
"status": "refused_dirty_topology",
|
|
@@ -4082,11 +4473,11 @@ pub mod lifecycle_port {
|
|
|
4082
4473
|
.iter()
|
|
4083
4474
|
.map(|id| json!({"id": id}))
|
|
4084
4475
|
.collect::<Vec<_>>(),
|
|
4085
|
-
"next_actions":
|
|
4086
|
-
|
|
4087
|
-
|
|
4088
|
-
|
|
4089
|
-
|
|
4476
|
+
"next_actions": dirty_topology_next_actions(
|
|
4477
|
+
&reason,
|
|
4478
|
+
&issue_ids,
|
|
4479
|
+
repair_team,
|
|
4480
|
+
),
|
|
4090
4481
|
"reminder": crate::cli::QUICK_START_REMINDER,
|
|
4091
4482
|
})
|
|
4092
4483
|
}
|
|
@@ -4304,6 +4695,45 @@ pub mod lifecycle_port {
|
|
|
4304
4695
|
);
|
|
4305
4696
|
}
|
|
4306
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
|
+
|
|
4307
4737
|
#[test]
|
|
4308
4738
|
fn error_value_attaches_next_action_field() {
|
|
4309
4739
|
let err = crate::lifecycle::LifecycleError::RequirementUnmet(
|
|
@@ -4319,6 +4749,21 @@ pub mod lifecycle_port {
|
|
|
4319
4749
|
"error_value must attach the add-agent guidance: {v}"
|
|
4320
4750
|
);
|
|
4321
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
|
+
}
|
|
4322
4767
|
}
|
|
4323
4768
|
}
|
|
4324
4769
|
|