@team-agent/installer 0.5.73 → 0.5.74

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 (30) hide show
  1. package/Cargo.lock +1 -1
  2. package/Cargo.toml +1 -1
  3. package/crates/team-agent/src/cli/adapters.rs +19 -13
  4. package/crates/team-agent/src/cli/emit.rs +6 -3
  5. package/crates/team-agent/src/cli/mod.rs +151 -0
  6. package/crates/team-agent/src/cli/tests/leader_watch.rs +28 -0
  7. package/crates/team-agent/src/cli/tests/run_delegation.rs +3 -0
  8. package/crates/team-agent/src/cli/types.rs +6 -0
  9. package/crates/team-agent/src/compiler.rs +14 -1
  10. package/crates/team-agent/src/leader/lease.rs +106 -21
  11. package/crates/team-agent/src/leader/registry.rs +0 -89
  12. package/crates/team-agent/src/lifecycle/launch/quick_start.rs +457 -73
  13. package/crates/team-agent/src/lifecycle/launch/quick_start_transport.rs +16 -6
  14. package/crates/team-agent/src/lifecycle/launch/state_projection.rs +5 -3
  15. package/crates/team-agent/src/lifecycle/restart/common.rs +0 -11
  16. package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +60 -0
  17. package/crates/team-agent/src/lifecycle/tests/pi_compiler_red.rs +31 -0
  18. package/crates/team-agent/src/lifecycle/tests/pi_dual_entry_red.rs +25 -23
  19. package/crates/team-agent/src/messaging/delivery.rs +26 -8
  20. package/crates/team-agent/src/messaging/tests/runtime.rs +234 -0
  21. package/crates/team-agent/src/model/spec.rs +81 -7
  22. package/crates/team-agent/src/provider/adapters/pi.rs +14 -0
  23. package/crates/team-agent/src/state/persist.rs +272 -3
  24. package/crates/team-agent/src/state/repository.rs +9 -2
  25. package/crates/team-agent/src/tmux_backend/tests.rs +64 -0
  26. package/crates/team-agent/src/tmux_backend.rs +57 -0
  27. package/package.json +4 -4
  28. package/skills/team-agent/SKILL.md +3 -0
  29. package/skills/team-agent/command-coverage.json +21 -0
  30. package/skills/team-agent/references/team-agent-operator.md +9 -2
package/Cargo.lock CHANGED
@@ -581,7 +581,7 @@ dependencies = [
581
581
 
582
582
  [[package]]
583
583
  name = "team-agent"
584
- version = "0.5.73"
584
+ version = "0.5.74"
585
585
  dependencies = [
586
586
  "anyhow",
587
587
  "chrono",
package/Cargo.toml CHANGED
@@ -9,7 +9,7 @@ members = ["crates/team-agent", "crates/win-conpty-phase0", "crates/conpty-trans
9
9
 
10
10
  [workspace.package]
11
11
  edition = "2021"
12
- version = "0.5.73"
12
+ version = "0.5.74"
13
13
  license = "AGPL-3.0"
14
14
  rust-version = "1.95"
15
15
 
@@ -129,6 +129,9 @@ pub fn cmd_quick_start(args: &QuickStartArgs) -> Result<CmdResult, CliError> {
129
129
  .get("status")
130
130
  .and_then(Value::as_str)
131
131
  .map(str::to_string);
132
+ if !args.detail {
133
+ lifecycle_port::compact_quick_start_value(&mut value);
134
+ }
132
135
  if args.json
133
136
  || value.get("ok").and_then(Value::as_bool) == Some(false)
134
137
  || session_capture_incomplete
@@ -1377,10 +1380,12 @@ pub fn cmd_takeover(args: &TakeoverArgs) -> Result<CmdResult, CliError> {
1377
1380
 
1378
1381
  /// `cmd_claim_leader`(`commands.py:156`)。
1379
1382
  pub fn cmd_claim_leader(args: &ClaimLeaderArgs) -> Result<CmdResult, CliError> {
1380
- Ok(CmdResult::from_json(
1381
- leader_port::claim_leader(&args.workspace, args.team.as_deref(), args.confirm)?,
1382
- args.json,
1383
- ))
1383
+ let mut value =
1384
+ leader_port::claim_leader(&args.workspace, args.team.as_deref(), args.confirm)?;
1385
+ if !args.detail {
1386
+ leader_port::compact_lease_value(&mut value);
1387
+ }
1388
+ Ok(CmdResult::from_json(value, args.json))
1384
1389
  }
1385
1390
 
1386
1391
  /// `cmd_identity`(`commands.py:160`)。
@@ -1401,15 +1406,16 @@ pub fn cmd_shutdown(args: &ShutdownArgs) -> Result<CmdResult, CliError> {
1401
1406
 
1402
1407
  /// `cmd_restart`(`commands.py:344`)。
1403
1408
  pub fn cmd_restart(args: &RestartArgs) -> Result<CmdResult, CliError> {
1404
- Ok(CmdResult::from_json(
1405
- lifecycle_port::restart(
1406
- &args.workspace,
1407
- args.allow_fresh,
1408
- args.team.as_deref(),
1409
- args.session_converge_deadline_ms,
1410
- )?,
1411
- args.json,
1412
- ))
1409
+ let mut value = lifecycle_port::restart(
1410
+ &args.workspace,
1411
+ args.allow_fresh,
1412
+ args.team.as_deref(),
1413
+ args.session_converge_deadline_ms,
1414
+ )?;
1415
+ if !args.detail {
1416
+ lifecycle_port::compact_restart_value(&mut value);
1417
+ }
1418
+ Ok(CmdResult::from_json(value, args.json))
1413
1419
  }
1414
1420
 
1415
1421
  /// `cmd_start_agent`(`commands.py:348`)。
@@ -374,7 +374,7 @@ fn command_help(command: Option<&str>) -> String {
374
374
  match command {
375
375
  None => default_help(),
376
376
  Some("init") => compat_hidden_help("init", "usage: team-agent init [--workspace WORKSPACE] [--force] [--json]"),
377
- Some("quick-start") => "usage: team-agent quick-start [TEAMDIR] [--workspace WORKSPACE] [--name NAME] [--team-id TEAM|--team TEAM] [--yes] [--no-display] [--backend tmux|conpty] [--json]\n\ndefaults: display_backend=adaptive; set display_backend: none in TEAM.md or pass --no-display to use one worker window per agent.\n\n--backend selects the worker transport (Phase 1d Batch 2): tmux (default on POSIX; unchanged behavior), conpty (Windows-native ConPTY worker transport; requires the shim binary and Windows host).\n\nAfter a successful start, use the returned `send_commands` (or choose an agent explicitly) with `team-agent send AGENT MESSAGE`.".to_string(),
377
+ Some("quick-start") => "usage: team-agent quick-start [TEAMDIR] [--workspace WORKSPACE] [--name NAME] [--team-id TEAM|--team TEAM] [--yes] [--no-display] [--backend tmux|conpty] [--json] [--detail]\n\ndefaults: display_backend=adaptive; set display_backend: none in TEAM.md or pass --no-display to use one worker window per agent.\n\n--backend selects the worker transport (Phase 1d Batch 2): tmux (default on POSIX; unchanged behavior), conpty (Windows-native ConPTY worker transport; requires the shim binary and Windows host).\n\n--detail includes internal receiver/topology diagnostics in JSON output.\n\nAfter a successful start, use the returned `send_commands` (or choose an agent explicitly) with `team-agent send AGENT MESSAGE`.".to_string(),
378
378
  Some("start") => compat_hidden_help("start", "usage: team-agent start [TEAMDIR] [--yes] [--fresh] [--json]"),
379
379
  Some("compile") => "usage: team-agent compile --team TEAM [--out FILE] [--json]".to_string(),
380
380
  Some("send") => concat!(
@@ -391,7 +391,7 @@ fn command_help(command: Option<&str>) -> String {
391
391
  Some("models") => "usage: team-agent models --provider pi [--search TEXT] [--json]\n\nPrints models.v1 exact role_model entries; each entry includes current=true|false. Catalog readiness is reported as auth=ok|not_ready with auth_basis=catalog_visibility.".to_string(),
392
392
  Some("stop") => compat_hidden_help("stop", "usage: team-agent stop [--workspace WORKSPACE] [--team TEAM] [--keep-logs] [--json]"),
393
393
  Some("shutdown") => "usage: team-agent shutdown [--workspace WORKSPACE] [--team TEAM] [--keep-logs] [--json]".to_string(),
394
- Some("restart") => "usage: team-agent restart [WORKSPACE] [--team TEAM] [--allow-fresh] [--session-converge-deadline SECONDS] [--json]".to_string(),
394
+ Some("restart") => "usage: team-agent restart [WORKSPACE] [--team TEAM] [--allow-fresh] [--session-converge-deadline SECONDS] [--json] [--detail]".to_string(),
395
395
  Some("restart-agent") => compat_hidden_help("restart-agent", "usage: team-agent restart-agent AGENT [--workspace WORKSPACE] [--team TEAM] [--discard-session] [--no-display] [--json]"),
396
396
  Some("reset-agent") => "usage: team-agent reset-agent AGENT [--workspace WORKSPACE] [--team TEAM] [--discard-session] [--no-display] [--json]".to_string(),
397
397
  Some("start-agent") => "usage: team-agent start-agent AGENT [--workspace WORKSPACE] [--team TEAM] [--force] [--allow-fresh] [--no-display] [--json]\n\nAfter a successful start, use the returned `send_commands` with `team-agent send AGENT MESSAGE`.".to_string(),
@@ -405,7 +405,7 @@ fn command_help(command: Option<&str>) -> String {
405
405
  Some("stuck-cancel") => "usage: team-agent stuck-cancel AGENT [--workspace WORKSPACE] [--alert-type stuck|idle_fallback|cross_worker_deadlock|all] [--json]".to_string(),
406
406
  Some("acknowledge-idle") => "usage: team-agent acknowledge-idle [--workspace WORKSPACE] [--team TEAM] [--json]".to_string(),
407
407
  Some("takeover") => "usage: team-agent takeover [--workspace WORKSPACE] [--team TEAM] [--confirm] [--json]".to_string(),
408
- Some("claim-leader") => "usage: team-agent claim-leader [--workspace WORKSPACE] [--team TEAM] [--confirm] [--json]".to_string(),
408
+ Some("claim-leader") => "usage: team-agent claim-leader [--workspace WORKSPACE] [--team TEAM] [--confirm] [--json] [--detail]".to_string(),
409
409
  Some("attach-leader") => "usage: team-agent attach-leader [--workspace WORKSPACE] [--team TEAM] [--pane PANE] [--provider PROVIDER] [--confirm] [--json]".to_string(),
410
410
  Some("attach-app-server-leader") => "usage: team-agent attach-app-server-leader [--workspace WORKSPACE] [--team TEAM] --socket unix:///path.sock --thread-id THREAD_ID [--json]".to_string(),
411
411
  Some("identity") => "usage: team-agent identity [--workspace WORKSPACE] [--team TEAM] [--json]".to_string(),
@@ -1063,6 +1063,7 @@ fn quick_start_args(args: &[String], cwd: &Path) -> Result<QuickStartArgs, CliEr
1063
1063
  yes: parsed.yes,
1064
1064
  no_display: parsed.no_display,
1065
1065
  json: parsed.json,
1066
+ detail: parsed.detail,
1066
1067
  backend: parsed.backend,
1067
1068
  })
1068
1069
  }
@@ -1404,6 +1405,7 @@ fn claim_leader_args(args: &[String], cwd: &Path) -> ClaimLeaderArgs {
1404
1405
  team: parsed.team,
1405
1406
  confirm: parsed.confirm,
1406
1407
  json: parsed.json,
1408
+ detail: parsed.detail,
1407
1409
  }
1408
1410
  }
1409
1411
 
@@ -1477,6 +1479,7 @@ fn restart_args(args: &[String], cwd: &Path) -> Result<RestartArgs, CliError> {
1477
1479
  allow_fresh: parsed.allow_fresh,
1478
1480
  session_converge_deadline_ms: parsed.session_converge_deadline_ms,
1479
1481
  json: parsed.json,
1482
+ detail: parsed.detail,
1480
1483
  })
1481
1484
  }
1482
1485
 
@@ -3233,6 +3233,33 @@ pub mod lifecycle_port {
3233
3233
  );
3234
3234
  }
3235
3235
 
3236
+ const COMPACT_READINESS_KEYS: [&str; 6] = [
3237
+ "all_workers_spawned",
3238
+ "state",
3239
+ "ready",
3240
+ "reason",
3241
+ "next_action",
3242
+ "unhealthy_agents",
3243
+ ];
3244
+
3245
+ fn compact_readiness_object(value: Option<&mut Value>) {
3246
+ let Some(object) = value.and_then(Value::as_object_mut) else {
3247
+ return;
3248
+ };
3249
+ object.retain(|key, _| COMPACT_READINESS_KEYS.contains(&key.as_str()));
3250
+ }
3251
+
3252
+ pub(crate) fn compact_quick_start_value(value: &mut Value) {
3253
+ if let Some(object) = value.as_object_mut() {
3254
+ // Default contract: outcome, error/next action, attach/send, and a
3255
+ // short readiness verdict. Diagnostic aliases stay behind --detail.
3256
+ object.remove("agent_ids");
3257
+ object.remove("dry_run");
3258
+ compact_readiness_object(object.get_mut("readiness"));
3259
+ compact_readiness_object(object.get_mut("worker_readiness"));
3260
+ }
3261
+ }
3262
+
3236
3263
  fn quick_start_value(report: crate::lifecycle::QuickStartReport) -> Value {
3237
3264
  match report {
3238
3265
  crate::lifecycle::QuickStartReport::Ready {
@@ -3265,6 +3292,7 @@ pub mod lifecycle_port {
3265
3292
  ),
3266
3293
  false,
3267
3294
  json!({
3295
+ "reason": "worker_unhealthy",
3268
3296
  "all_spawned": all_spawned,
3269
3297
  "all_workers_spawned": all_workers_spawned,
3270
3298
  "all_attached_receiver": all_attached_receiver,
@@ -3383,6 +3411,8 @@ pub mod lifecycle_port {
3383
3411
  attach_commands,
3384
3412
  } => json!({
3385
3413
  "ok": false,
3414
+ "status": "existing_runtime",
3415
+ "reason": "team already has runtime state; use restart",
3386
3416
  "summary": "existing runtime",
3387
3417
  "team": team,
3388
3418
  "session_name": session_name.map(|s| s.as_str().to_string()),
@@ -3398,6 +3428,8 @@ pub mod lifecycle_port {
3398
3428
  attach_commands,
3399
3429
  } => json!({
3400
3430
  "ok": false,
3431
+ "status": "preflight_blocked",
3432
+ "reason": "quick-start preflight blocked",
3401
3433
  "summary": summary,
3402
3434
  "blockers": blockers,
3403
3435
  "next_actions": next_actions,
@@ -3411,6 +3443,89 @@ pub mod lifecycle_port {
3411
3443
  mod quick_start_value_tests {
3412
3444
  use super::*;
3413
3445
 
3446
+ #[test]
3447
+ fn quick_start_default_receipt_is_compact_but_detail_preserves_readiness() {
3448
+ let readiness = json!({
3449
+ "all_spawned": true,
3450
+ "all_workers_spawned": true,
3451
+ "all_attached_receiver": false,
3452
+ "attached_receiver": false,
3453
+ "leader_receiver_attached": false,
3454
+ "all_resumable_have_session": true,
3455
+ "all_resumable_agents_have_sessions": true,
3456
+ "ready": false,
3457
+ "state": "leader_receiver_unbound",
3458
+ "session_capture_complete": true,
3459
+ "session_capture_incomplete": false,
3460
+ "incomplete_session_capture_agents": [],
3461
+ "pending_session_agent_ids": [],
3462
+ "reason": "launched team has no attached leader receiver",
3463
+ "next_action": "claim-leader"
3464
+ });
3465
+ let detail = json!({
3466
+ "ok": false,
3467
+ "status": "leader_receiver_unbound",
3468
+ "reason": "launched team has no attached leader receiver",
3469
+ "ready": false,
3470
+ "session_name": "team-fresh",
3471
+ "next_actions": ["claim-leader"],
3472
+ "attach_commands": ["tmux attach"],
3473
+ "send_commands": ["team-agent send worker hi"],
3474
+ "agent_ids": ["worker"],
3475
+ "readiness": readiness.clone(),
3476
+ "worker_readiness": readiness,
3477
+ "dry_run": false
3478
+ });
3479
+ let mut default = detail.clone();
3480
+ compact_quick_start_value(&mut default);
3481
+ assert_eq!(default["status"], json!("leader_receiver_unbound"));
3482
+ assert_eq!(
3483
+ default["reason"],
3484
+ json!("launched team has no attached leader receiver")
3485
+ );
3486
+ assert_eq!(default["next_actions"], json!(["claim-leader"]));
3487
+ assert_eq!(default["attach_commands"], json!(["tmux attach"]));
3488
+ assert_eq!(
3489
+ default["send_commands"],
3490
+ json!(["team-agent send worker hi"])
3491
+ );
3492
+ assert!(default.get("agent_ids").is_none());
3493
+ assert!(default.get("dry_run").is_none());
3494
+ 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"));
3497
+ assert!(default["readiness"].get("all_spawned").is_none());
3498
+ assert!(default["readiness"].get("all_attached_receiver").is_none());
3499
+ assert!(default["readiness"].get("leader_receiver_attached").is_none());
3500
+ assert_eq!(
3501
+ default["worker_readiness"]["all_workers_spawned"],
3502
+ json!(true)
3503
+ );
3504
+ assert!(default["worker_readiness"].get("all_spawned").is_none());
3505
+ assert!(detail["readiness"].get("all_spawned").is_some());
3506
+ assert!(detail["worker_readiness"].get("leader_receiver_attached").is_some());
3507
+ }
3508
+
3509
+ #[test]
3510
+ fn restart_default_receipt_is_compact_but_detail_preserves_coordinator() {
3511
+ let detail = json!({
3512
+ "ok": false,
3513
+ "status": "partial",
3514
+ "reason": "restart_agent_failed",
3515
+ "failed_agents": [{"agent_id": "worker", "error": "spawn failed"}],
3516
+ "next_actions": ["restart-agent worker"],
3517
+ "attach_commands": [],
3518
+ "coordinator": {"status": "running", "transport": "default"}
3519
+ });
3520
+ let mut default = detail.clone();
3521
+ compact_restart_value(&mut default);
3522
+ assert_eq!(default["status"], json!("partial"));
3523
+ assert_eq!(default["reason"], json!("restart_agent_failed"));
3524
+ assert_eq!(default["next_actions"], json!(["restart-agent worker"]));
3525
+ assert!(default.get("coordinator").is_none());
3526
+ assert!(detail.get("coordinator").is_some());
3527
+ }
3528
+
3414
3529
  #[test]
3415
3530
  fn existing_runtime_json_includes_attach_commands() {
3416
3531
  let value = quick_start_value(crate::lifecycle::QuickStartReport::ExistingRuntime {
@@ -3500,6 +3615,15 @@ pub mod lifecycle_port {
3500
3615
  }
3501
3616
  }
3502
3617
 
3618
+ pub(crate) fn compact_restart_value(value: &mut Value) {
3619
+ if let Some(object) = value.as_object_mut() {
3620
+ // Coordinator internals remain available only through --detail;
3621
+ // outcome, failed-agent errors, attach commands and next actions
3622
+ // stay in the default receipt.
3623
+ object.remove("coordinator");
3624
+ }
3625
+ }
3626
+
3503
3627
  fn restart_value(report: crate::lifecycle::RestartReport, team: Option<&str>) -> Value {
3504
3628
  match report {
3505
3629
  crate::lifecycle::RestartReport::Restarted {
@@ -3855,6 +3979,7 @@ pub mod lifecycle_port {
3855
3979
  "status_class": "refused_resume_atomicity",
3856
3980
  "allow_fresh": allow_fresh,
3857
3981
  "error": error_str,
3982
+ "next_action": "pass --allow-fresh to start fresh, or restore the provider session backing files",
3858
3983
  "unresumable": unresumable_detail,
3859
3984
  "unresumable_ids": unresumable_ids,
3860
3985
  "reminder": crate::cli::QUICK_START_REMINDER,
@@ -3894,6 +4019,7 @@ pub mod lifecycle_port {
3894
4019
  "status": "refused_invalid_first_send_at",
3895
4020
  "allow_fresh": allow_fresh,
3896
4021
  "error": error,
4022
+ "next_action": "pass --allow-fresh to discard the invalid session marker and start fresh",
3897
4023
  "invalid": invalid.iter().map(|w| w.worker_id.as_str()).collect::<Vec<_>>(),
3898
4024
  "reminder": crate::cli::QUICK_START_REMINDER,
3899
4025
  }),
@@ -5075,6 +5201,31 @@ pub mod leader_port {
5075
5201
  })
5076
5202
  }
5077
5203
 
5204
+ pub(crate) fn compact_lease_value(value: &mut Value) {
5205
+ let topology_status = value
5206
+ .get("topology_convergence")
5207
+ .and_then(|topology| topology.get("status"))
5208
+ .cloned();
5209
+ if value.get("reason").is_none() {
5210
+ if let Some(status) = topology_status {
5211
+ if status.as_str() != Some("converged") {
5212
+ value["reason"] = status;
5213
+ }
5214
+ }
5215
+ }
5216
+ if let Some(obj) = value.as_object_mut() {
5217
+ for key in [
5218
+ "owner_epoch",
5219
+ "leader_receiver",
5220
+ "team_owner",
5221
+ "topology_convergence",
5222
+ "leader_registry",
5223
+ ] {
5224
+ obj.remove(key);
5225
+ }
5226
+ }
5227
+ }
5228
+
5078
5229
  fn lease_value(result: crate::leader::LeaseResult) -> Value {
5079
5230
  let mut out = serde_json::Map::new();
5080
5231
  out.insert("ok".to_string(), json!(result.ok));
@@ -238,6 +238,34 @@ fn leader_port_takeover_confirm_without_pane_refuses_caller_pane_missing() {
238
238
  );
239
239
  }
240
240
 
241
+ #[test]
242
+ fn claim_leader_default_response_hides_internal_binding_details() {
243
+ let mut value = serde_json::json!({
244
+ "ok": true,
245
+ "status": "already_bound",
246
+ "owner_epoch": 3,
247
+ "bound_pane_id": "%77",
248
+ "leader_receiver": {"pane_id": "%77"},
249
+ "team_owner": {"pane_id": "%77"},
250
+ "topology_convergence": {"status": "not_converged_old_endpoint_live"},
251
+ "leader_registry": {"status": "registered"}
252
+ });
253
+ super::leader_port::compact_lease_value(&mut value);
254
+ assert_eq!(value["ok"], json!(true));
255
+ assert_eq!(value["status"], json!("already_bound"));
256
+ assert_eq!(value["bound_pane_id"], json!("%77"));
257
+ assert_eq!(value["reason"], json!("not_converged_old_endpoint_live"));
258
+ for field in [
259
+ "owner_epoch",
260
+ "leader_receiver",
261
+ "team_owner",
262
+ "topology_convergence",
263
+ "leader_registry",
264
+ ] {
265
+ assert!(value.get(field).is_none(), "default claim response leaked {field}");
266
+ }
267
+ }
268
+
241
269
  // RED — claim_leader(confirm=false) with no $TMUX_PANE: runtime.claim_leader is
242
270
  // ALSO Family A (runtime.py:791) -> the bind gate fires FIRST, so the no-pane
243
271
  // refusal is caller_pane_missing (NOT the leader-lane "not_in_tmux_pane").
@@ -225,6 +225,7 @@ fn cli_quick_start_invokes_real_lifecycle_compiles_spec() {
225
225
  no_display: false,
226
226
  backend: None,
227
227
  json: true,
228
+ detail: false,
228
229
  };
229
230
  let _ = cmd_quick_start(&args); // real quick_start compiles the spec before any coordinator/launch step
230
231
  // E5: spec compiled to .team/runtime/<team_key>/, NOT the user team dir.
@@ -260,6 +261,7 @@ fn cli_quick_start_invalid_spec_surfaces_real_compile_error() {
260
261
  no_display: false,
261
262
  backend: None,
262
263
  json: true,
264
+ detail: false,
263
265
  };
264
266
  let text = outcome_text(cmd_quick_start(&args));
265
267
  assert!(
@@ -281,6 +283,7 @@ fn cli_restart_missing_spec_surfaces_real_teamselect() {
281
283
  allow_fresh: false,
282
284
  session_converge_deadline_ms: None,
283
285
  json: true,
286
+ detail: false,
284
287
  };
285
288
  let text = outcome_text(cmd_restart(&args));
286
289
  // RED-2-STILL: entry gate now resolves via canonical_run_workspace; for a no-spec workspace
@@ -297,6 +297,8 @@ pub struct QuickStartArgs {
297
297
  pub yes: bool,
298
298
  pub no_display: bool,
299
299
  pub json: bool,
300
+ /// Include internal receiver/topology diagnostics in the response.
301
+ pub detail: bool,
300
302
  /// 0.5.x Phase 1d Batch 2: explicit backend override
301
303
  /// (`--backend <tmux|conpty>`). `None` = default (factory 5-layer
302
304
  /// resolution; almost always `tmux` on a cold quick-start).
@@ -433,6 +435,8 @@ pub struct ClaimLeaderArgs {
433
435
  pub team: Option<String>,
434
436
  pub confirm: bool,
435
437
  pub json: bool,
438
+ /// Include internal receiver/topology diagnostics in the response.
439
+ pub detail: bool,
436
440
  }
437
441
 
438
442
  /// `attach-leader` public CLI args. `cmd_attach_leader` consumes the typed pane/provider
@@ -482,6 +486,8 @@ pub struct RestartArgs {
482
486
  pub allow_fresh: bool,
483
487
  pub session_converge_deadline_ms: Option<u64>,
484
488
  pub json: bool,
489
+ /// Include internal receiver/topology diagnostics in the response.
490
+ pub detail: bool,
485
491
  }
486
492
 
487
493
  /// `start-agent`(`parser.py:369`)。
@@ -30,7 +30,8 @@ use std::path::Path;
30
30
  use crate::communication_mode::CommunicationMode;
31
31
  use crate::model::enums::{Provider, ProviderEffort};
32
32
  use crate::model::yaml::Value;
33
- use crate::model::{paths, spec, yaml, ModelError};
33
+ use crate::model::{paths, permissions, spec, yaml, ModelError};
34
+ use crate::provider::adapters::pi::first_unsupported_pi_tool_category;
34
35
  use crate::provider::wire::{
35
36
  builtin_provider_model as wire_builtin_provider_model, is_claude_family,
36
37
  parse_canonical_provider, provider_model_keys,
@@ -764,6 +765,18 @@ fn validate_pi_role_fields(meta: &Value, path: &Path, provider: &str) -> Result<
764
765
  path.display()
765
766
  )));
766
767
  }
768
+ let expanded = permissions::expand_tool_strings(tools.iter().map(String::as_str));
769
+ if let Some(category) = first_unsupported_pi_tool_category(
770
+ expanded
771
+ .iter()
772
+ .filter(|category| permissions::is_canonical_tool(category))
773
+ .map(String::as_str),
774
+ ) {
775
+ return Err(ModelError::Validation(format!(
776
+ "{}: Pi does not support Team Agent tool category {category:?}; remove it from the role's tools",
777
+ path.display()
778
+ )));
779
+ }
767
780
  Ok(())
768
781
  }
769
782
 
@@ -288,6 +288,23 @@ struct AttachLeaderTarget {
288
288
 
289
289
  fn attach_leader_targets(workspace: &Path, state: &Value) -> Vec<AttachLeaderTarget> {
290
290
  let mut targets = Vec::new();
291
+ // The caller's verified tmux server is authoritative for discovering its
292
+ // leader pane. Keep state-recorded worker endpoints as additional targets,
293
+ // but do not let a cross-socket pane-id collision hide the caller pane.
294
+ if let Some(endpoint) = crate::tmux_backend::socket_name_from_tmux_env() {
295
+ let backend = tmux_backend_for_endpoint(&endpoint);
296
+ let resolved_endpoint = backend.tmux_endpoint();
297
+ targets.extend(
298
+ backend
299
+ .list_targets()
300
+ .unwrap_or_default()
301
+ .into_iter()
302
+ .map(|info| AttachLeaderTarget {
303
+ info,
304
+ endpoint: resolved_endpoint.clone(),
305
+ }),
306
+ );
307
+ }
291
308
  for endpoint in state_recorded_tmux_endpoints(state) {
292
309
  let backend = tmux_backend_for_endpoint(&endpoint);
293
310
  let resolved_endpoint = backend.tmux_endpoint();
@@ -554,7 +571,18 @@ pub fn claim_leader(
554
571
  "explicit cross-workspace claim requires an absolute tmux endpoint".to_string(),
555
572
  ));
556
573
  }
557
- let nonce = next_pane_binding_nonce(workspace, endpoint, &candidate.info.pane_id);
574
+ // A live pane nonce identifies the pane instance, not this team's
575
+ // claim. Preserve it so another child team remains authorized.
576
+ let nonce = pane_binding_nonce_for_claim(
577
+ workspace,
578
+ endpoint,
579
+ &candidate.info.pane_id,
580
+ candidate
581
+ .info
582
+ .leader_env
583
+ .get(crate::tmux_backend::PANE_BINDING_NONCE_METADATA_KEY)
584
+ .map(String::as_str),
585
+ );
558
586
  target.scope_authority = Some("explicit_claim".to_string());
559
587
  target.authorized_team_workspace = Some(canonical_workspace(workspace));
560
588
  target.binding_nonce = Some(nonce);
@@ -600,7 +628,7 @@ pub fn claim_leader(
600
628
  confirm,
601
629
  &event_log,
602
630
  &liveness,
603
- caller_target.as_ref(),
631
+ caller_target.as_mut(),
604
632
  caller_pane_info,
605
633
  scoped_team.map(|_| team_id.as_str()),
606
634
  )?;
@@ -692,7 +720,7 @@ fn claim_lease_no_incident_with_target(
692
720
  confirm: bool,
693
721
  event_log: &crate::event_log::EventLog,
694
722
  liveness: &dyn crate::state::owner_gate::PaneLivenessProbe,
695
- caller_target: Option<&LeaderClaimTarget>,
723
+ mut caller_target: Option<&mut LeaderClaimTarget>,
696
724
  caller_pane_info: Option<&PaneInfo>,
697
725
  scoped_team: Option<&str>,
698
726
  ) -> Result<LeaseResult, LeaderError> {
@@ -734,9 +762,13 @@ fn claim_lease_no_incident_with_target(
734
762
  let non_empty_caller_pane = NonEmptyPaneId::try_from_pane(caller_pane)?;
735
763
  let bound_endpoint_matches_caller = bound_endpoint_matches_current_process(state);
736
764
  if bound_pane_id.as_deref() == Some(caller_pane.as_str()) && bound_endpoint_matches_caller {
737
- write_claim_target_pane_nonce(caller_target)?;
765
+ if let Some(target) = caller_target.as_deref_mut() {
766
+ write_claim_target_pane_nonce(target)?;
767
+ }
738
768
  let current_endpoint = crate::tmux_backend::socket_name_from_tmux_env();
739
- let observed_endpoint = caller_target.and_then(|target| target.endpoint.as_deref());
769
+ let observed_endpoint = caller_target
770
+ .as_deref()
771
+ .and_then(|target| target.endpoint.as_deref());
740
772
  let convergence_candidate = observed_endpoint.or(current_endpoint.as_deref());
741
773
  let candidate_source = if observed_endpoint.is_some() {
742
774
  Some("observed_target_endpoint")
@@ -758,7 +790,7 @@ fn claim_lease_no_incident_with_target(
758
790
  pane_info,
759
791
  observed_endpoint.or(current_endpoint.as_deref()),
760
792
  scoped_team.or(team),
761
- caller_target,
793
+ caller_target.as_deref(),
762
794
  )
763
795
  });
764
796
  if converged || observation_refreshed {
@@ -924,23 +956,33 @@ fn claim_lease_no_incident_with_target(
924
956
  } else {
925
957
  LeaseReason::VacantAcquired
926
958
  };
927
- write_claim_target_pane_nonce(caller_target)?;
959
+ if let Some(target) = caller_target.as_deref_mut() {
960
+ write_claim_target_pane_nonce(target)?;
961
+ }
928
962
  let mut identity = leader_identity_context(workspace, Some(team_id.as_str()), Some(state))?;
929
- if let Some(uuid) = caller_target.and_then(|target| target.leader_session_uuid.as_ref()) {
963
+ if let Some(uuid) = caller_target
964
+ .as_deref()
965
+ .and_then(|target| target.leader_session_uuid.as_ref()) {
930
966
  identity.leader_session_uuid = uuid.clone();
931
967
  }
932
968
  let next_epoch = OwnerEpoch(pre_epoch.0.saturating_add(1));
933
- let provider = caller_target.map_or_else(|| prior_provider(state), |target| target.provider);
934
- let observed_endpoint = caller_target.and_then(|target| target.endpoint.clone());
969
+ let provider = caller_target
970
+ .as_deref()
971
+ .map_or_else(|| prior_provider(state), |target| target.provider);
972
+ let observed_endpoint = caller_target
973
+ .as_deref()
974
+ .and_then(|target| target.endpoint.clone());
935
975
  let mut receiver = make_receiver(
936
976
  provider,
937
977
  &non_empty_caller_pane,
938
978
  &identity.leader_session_uuid,
939
979
  next_epoch,
940
980
  Discovery::ClaimLeader,
941
- caller_target.and_then(|target| target.pane_info.clone()),
981
+ caller_target
982
+ .as_deref()
983
+ .and_then(|target| target.pane_info.clone()),
942
984
  );
943
- if let Some(target) = caller_target {
985
+ if let Some(target) = caller_target.as_deref() {
944
986
  receiver.scope_authority.clone_from(&target.scope_authority);
945
987
  receiver
946
988
  .authorized_team_workspace
@@ -1540,10 +1582,10 @@ fn workspace_claim_target_from_pane_info(
1540
1582
  claim_target_from_pane_info(target)
1541
1583
  }
1542
1584
 
1543
- fn write_claim_target_pane_nonce(target: Option<&LeaderClaimTarget>) -> Result<(), LeaderError> {
1544
- let Some(target) = target.filter(|target| target.scope_authority.is_some()) else {
1585
+ fn write_claim_target_pane_nonce(target: &mut LeaderClaimTarget) -> Result<(), LeaderError> {
1586
+ if target.scope_authority.is_none() {
1545
1587
  return Ok(());
1546
- };
1588
+ }
1547
1589
  let endpoint = target.endpoint.as_deref().ok_or_else(|| {
1548
1590
  LeaderError::Validation("explicit claim is missing its tmux endpoint".to_string())
1549
1591
  })?;
@@ -1557,13 +1599,30 @@ fn write_claim_target_pane_nonce(target: Option<&LeaderClaimTarget>) -> Result<(
1557
1599
  let nonce = target.binding_nonce.as_deref().ok_or_else(|| {
1558
1600
  LeaderError::Validation("explicit claim is missing its pane binding nonce".to_string())
1559
1601
  })?;
1560
- tmux_backend_for_endpoint(endpoint)
1561
- .set_pane_binding_nonce(pane, nonce)
1562
- .map_err(|error| {
1563
- LeaderError::Validation(format!(
1564
- "failed to bind the explicit leader pane instance: {error}"
1565
- ))
1602
+ let live_nonce = target
1603
+ .pane_info
1604
+ .as_ref()
1605
+ .and_then(|info| {
1606
+ info.leader_env
1607
+ .get(crate::tmux_backend::PANE_BINDING_NONCE_METADATA_KEY)
1608
+ .map(String::as_str)
1566
1609
  })
1610
+ .filter(|nonce| !nonce.is_empty());
1611
+ // The pane option is the pane-instance identity shared by all authorized
1612
+ // teams. Rewriting an observed value would revoke another team's grant.
1613
+ let effective_nonce = if let Some(live_nonce) = live_nonce {
1614
+ live_nonce.to_string()
1615
+ } else {
1616
+ tmux_backend_for_endpoint(endpoint)
1617
+ .set_pane_binding_nonce_if_unset(pane, nonce)
1618
+ .map_err(|error| {
1619
+ LeaderError::Validation(format!(
1620
+ "failed to bind the explicit leader pane instance: {error}"
1621
+ ))
1622
+ })?
1623
+ };
1624
+ target.binding_nonce = Some(effective_nonce);
1625
+ Ok(())
1567
1626
  }
1568
1627
 
1569
1628
  fn canonical_workspace(workspace: &Path) -> PathBuf {
@@ -1591,6 +1650,18 @@ fn next_pane_binding_nonce(workspace: &Path, endpoint: &str, pane: &PaneId) -> S
1591
1650
  .collect()
1592
1651
  }
1593
1652
 
1653
+ fn pane_binding_nonce_for_claim(
1654
+ workspace: &Path,
1655
+ endpoint: &str,
1656
+ pane: &PaneId,
1657
+ live_nonce: Option<&str>,
1658
+ ) -> String {
1659
+ live_nonce
1660
+ .filter(|nonce| !nonce.is_empty())
1661
+ .map(str::to_string)
1662
+ .unwrap_or_else(|| next_pane_binding_nonce(workspace, endpoint, pane))
1663
+ }
1664
+
1594
1665
  fn target_leader_session_uuid(target: &PaneInfo) -> Option<crate::model::ids::LeaderSessionUuid> {
1595
1666
  target
1596
1667
  .leader_env
@@ -2568,6 +2639,20 @@ mod tests {
2568
2639
  assert_ne!(first, second);
2569
2640
  }
2570
2641
 
2642
+ #[test]
2643
+ fn explicit_claim_generates_nonce_only_when_live_nonce_is_missing() {
2644
+ let workspace = Path::new("/tmp/workspace-a");
2645
+ let endpoint = "/tmp/team-agent-live.sock";
2646
+ let pane = PaneId::new("%7");
2647
+ let generated = pane_binding_nonce_for_claim(workspace, endpoint, &pane, None);
2648
+ assert_eq!(generated.len(), 32);
2649
+ assert!(generated.chars().all(|ch| ch.is_ascii_hexdigit()));
2650
+ assert_eq!(
2651
+ pane_binding_nonce_for_claim(workspace, endpoint, &pane, Some("live-nonce")),
2652
+ "live-nonce"
2653
+ );
2654
+ }
2655
+
2571
2656
  #[test]
2572
2657
  fn claim_authority_preserves_recorded_absolute_observation() {
2573
2658
  let workspace = Path::new("/tmp/workspace-a");