@team-agent/installer 0.5.119 → 0.5.120

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 (59) hide show
  1. package/Cargo.lock +1 -1
  2. package/Cargo.toml +1 -1
  3. package/README.md +46 -1
  4. package/README.zh.md +46 -1
  5. package/crates/team-agent/src/cli/adapters.rs +11 -0
  6. package/crates/team-agent/src/cli/arguments.rs +401 -0
  7. package/crates/team-agent/src/cli/emit.rs +241 -142
  8. package/crates/team-agent/src/cli/intent/runtime.rs +1139 -0
  9. package/crates/team-agent/src/cli/intent/runtime_tests.rs +584 -0
  10. package/crates/team-agent/src/cli/intent/tests.rs +654 -0
  11. package/crates/team-agent/src/cli/intent.rs +642 -0
  12. package/crates/team-agent/src/cli/mod.rs +49 -5
  13. package/crates/team-agent/src/cli/named_address.rs +10 -1
  14. package/crates/team-agent/src/cli/spec.rs +22 -10
  15. package/crates/team-agent/src/cli/status_port/brief.rs +117 -16
  16. package/crates/team-agent/src/cli/tests/base.rs +6 -1
  17. package/crates/team-agent/src/cli/tests/parser_safety.rs +383 -0
  18. package/crates/team-agent/src/cli/tests/run_delegation.rs +63 -19
  19. package/crates/team-agent/src/cli/tests/verb_install_skill.rs +3 -2
  20. package/crates/team-agent/src/cli/tests/verb_profile.rs +3 -1
  21. package/crates/team-agent/src/contract_runtime/backend.rs +1 -1
  22. package/crates/team-agent/src/lifecycle/launch/quick_start.rs +166 -77
  23. package/crates/team-agent/src/lifecycle/launch.rs +28 -0
  24. package/crates/team-agent/src/lifecycle/restart.rs +17 -0
  25. package/crates/team-agent/src/lifecycle/tests/acceptance_b_batch_red.rs +1 -3
  26. package/crates/team-agent/src/lifecycle/tests/behavioral_diff_264_red.rs +5 -15
  27. package/crates/team-agent/src/lifecycle/tests/claude_compatible_config_red.rs +2 -3
  28. package/crates/team-agent/src/lifecycle/tests/claude_profile_launch_red.rs +2 -3
  29. package/crates/team-agent/src/lifecycle/tests/cli_argv_routing_red.rs +10 -2
  30. package/crates/team-agent/src/lifecycle/tests/clone_agent_preserves_source_tools.rs +8 -1
  31. package/crates/team-agent/src/lifecycle/tests/clone_fork_copilot_perms_red.rs +3 -2
  32. package/crates/team-agent/src/lifecycle/tests/communication_mode_runtime_contract_red.rs +6 -9
  33. package/crates/team-agent/src/lifecycle/tests/copilot_provider_red.rs +7 -5
  34. package/crates/team-agent/src/lifecycle/tests/cursor_mcp_overlay.rs +4 -2
  35. package/crates/team-agent/src/lifecycle/tests/cursor_require_explicit_model_red.rs +5 -2
  36. package/crates/team-agent/src/lifecycle/tests/cursor_restart_resume_red.rs +4 -3
  37. package/crates/team-agent/src/lifecycle/tests/grok_effort_argv_red.rs +2 -4
  38. package/crates/team-agent/src/lifecycle/tests/grok_mcp_overlay_red.rs +3 -4
  39. package/crates/team-agent/src/lifecycle/tests/grok_require_explicit_model_red.rs +5 -4
  40. package/crates/team-agent/src/lifecycle/tests/grok_restart_resume_red.rs +2 -4
  41. package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +78 -36
  42. package/crates/team-agent/src/lifecycle/tests/phase_b_contracts.rs +8 -4
  43. package/crates/team-agent/src/lifecycle/tests/phase_golden.rs +6 -12
  44. package/crates/team-agent/src/lifecycle/tests/pi_fork_session_inheritance_red.rs +4 -0
  45. package/crates/team-agent/src/lifecycle/tests/startup_latency_contract.rs +1 -3
  46. package/crates/team-agent/src/lifecycle/tests/status_credential_redaction_contract.rs +7 -0
  47. package/crates/team-agent/src/lifecycle/tests/team_in_team_identity_scope_red.rs +1 -3
  48. package/crates/team-agent/src/lifecycle/tests/verify_rs031_window_consistency_red.rs +2 -1
  49. package/crates/team-agent/src/lifecycle/types.rs +10 -0
  50. package/crates/team-agent/src/tmux_backend/tests.rs +37 -0
  51. package/crates/team-agent/src/tmux_backend.rs +8 -1
  52. package/package.json +4 -4
  53. package/skills/team-agent/SKILL.md +71 -54
  54. package/skills/team-agent/command-coverage.json +283 -13
  55. package/skills/team-agent/docs/reference/team-agent-operator.md +220 -0
  56. package/skills/team-agent/references/bug-as-artifact-flow.md +1 -1
  57. package/skills/team-agent/references/recovery-runbook.md +61 -212
  58. package/skills/team-agent/references/team-agent-operator.md +181 -48
  59. package/skills/team-agent/references/team-in-team.md +47 -80
@@ -57,6 +57,8 @@ pub(crate) const STATUS_REMINDER: &str =
57
57
  "Next: use team-agent inbox leader -n 3 for replies. Status is not proof of task completion; do not inspect worker terminal content.";
58
58
 
59
59
  pub mod adapters;
60
+ mod arguments;
61
+ mod intent;
60
62
  #[cfg(unix)]
61
63
  mod shutdown_socket;
62
64
  pub mod attach_app_server_leader;
@@ -194,7 +196,7 @@ pub mod lifecycle_port {
194
196
  crate::leader::LeaderLaunchStatus::NotStarted => false,
195
197
  };
196
198
  let leader_attach_command = if outcome.reason.as_deref() == Some("PaneWorkspaceMismatch") {
197
- Some("team-agent attach-leader".to_string())
199
+ Some(format!("team-agent attach-leader --provider {}", crate::provider::wire::provider_wire(provider)))
198
200
  } else {
199
201
  leader_attach_command_for_plan(cwd, &plan)
200
202
  };
@@ -387,14 +389,16 @@ pub mod lifecycle_port {
387
389
  }
388
390
 
389
391
  let hint_action = match refusal.recovery.hint_action {
390
- PaneAuthorityRecoveryHint::AttachLeader => "team-agent attach-leader",
392
+ PaneAuthorityRecoveryHint::AttachLeader => format!(
393
+ "team-agent attach-leader --provider {}", crate::provider::wire::provider_wire(provider)
394
+ ),
391
395
  };
392
396
  let action = match refusal.recovery.action {
393
397
  PaneAuthorityRecoveryAction::OpenTerminalOutsideCurrentTmuxPaneOrAttachFromMatchingPane => {
394
- "open a new terminal window outside the current tmux/pane, confirm TMUX and \
398
+ format!("open a new terminal window outside the current tmux/pane, confirm TMUX and \
395
399
  TMUX_PANE are absent, change directory to the requested workspace, and restart \
396
400
  the leader; alternatively enter the matching workspace pane and run \
397
- `team-agent attach-leader`"
401
+ `{hint_action}`")
398
402
  }
399
403
  };
400
404
  facts.insert(
@@ -4091,6 +4095,25 @@ pub mod lifecycle_port {
4091
4095
  "reminder": crate::cli::QUICK_START_REMINDER,
4092
4096
  "readiness": readiness_json.clone(),
4093
4097
  "worker_readiness": readiness_json,
4098
+ "effects": {"worker_panes_started": agent_ids, "coordinator_start": "running_confirmed"},
4099
+ })
4100
+ }
4101
+ crate::lifecycle::QuickStartReport::Partial {
4102
+ workspace, session_name, launch, team, error, coordinator_state,
4103
+ } => {
4104
+ let agents: Vec<_> = launch.started.iter().map(|agent| agent.agent_id.as_str())
4105
+ .chain(launch.contract_started.iter().map(|id| id.as_str())).collect();
4106
+ let recovery = format!(
4107
+ "team-agent doctor --workspace {} --team {} --json; inspect confirmed effects before any retry; no automatic rollback is claimed",
4108
+ shell_quote(&workspace.to_string_lossy()), shell_quote(&team),
4109
+ );
4110
+ json!({
4111
+ "ok": false, "status": "partial", "phase": "post_spawn",
4112
+ "workspace": workspace, "team": team, "session_name": session_name.as_str(),
4113
+ "error": crate::redaction::redact_external_text(&error),
4114
+ "effects": {"worker_panes_started": agents, "coordinator_start": coordinator_state},
4115
+ "last_confirmed_effect": if agents.is_empty() {Value::Null} else {json!("worker_panes_started")},
4116
+ "current_state": "unknown", "recovery": recovery,
4094
4117
  })
4095
4118
  }
4096
4119
  crate::lifecycle::QuickStartReport::ExistingRuntime {
@@ -4122,6 +4145,7 @@ pub mod lifecycle_port {
4122
4145
  "ok": false,
4123
4146
  "status": "preflight_blocked",
4124
4147
  "reason": "quick-start preflight blocked",
4148
+ "effects": {"worker_panes_started": [], "coordinator_start": "not_attempted"},
4125
4149
  "summary": summary,
4126
4150
  "blockers": blockers,
4127
4151
  "next_actions": next_actions,
@@ -4205,6 +4229,25 @@ pub mod lifecycle_port {
4205
4229
  .is_some());
4206
4230
  }
4207
4231
 
4232
+ #[test]
4233
+ fn issue337_partial_start_receipt_retains_workers_and_scoped_recovery() {
4234
+ let crate::lifecycle::QuickStartReport::Ready { launch, .. } = pending_tool_load_report() else { panic!() };
4235
+ let value = quick_start_value(crate::lifecycle::QuickStartReport::Partial {
4236
+ workspace: PathBuf::from("/owned/work space"),
4237
+ session_name: crate::transport::SessionName::new("team-demo"),
4238
+ launch, team: "demo".into(), error: "caller disappeared".into(),
4239
+ coordinator_state: "not_attempted".into(),
4240
+ });
4241
+ assert_eq!(value["status"], "partial");
4242
+ assert_eq!(value["effects"]["worker_panes_started"], json!(["worker"]));
4243
+ assert_eq!(value["effects"]["coordinator_start"], "not_attempted");
4244
+ assert_eq!(value["last_confirmed_effect"], "worker_panes_started");
4245
+ let recovery = value["recovery"].as_str().unwrap();
4246
+ let argv = split_shell_argv(recovery);
4247
+ assert!(argv.windows(2).any(|pair| pair == ["--workspace", "/owned/work space"]));
4248
+ assert!(argv.windows(2).any(|pair| pair == ["--team", "demo"]));
4249
+ }
4250
+
4208
4251
  fn pending_tool_load_report() -> crate::lifecycle::QuickStartReport {
4209
4252
  crate::lifecycle::QuickStartReport::Ready {
4210
4253
  session_name: crate::transport::SessionName::new("team-demo"),
@@ -4425,7 +4468,7 @@ pub mod lifecycle_port {
4425
4468
  #[test]
4426
4469
  #[serial_test::serial(env)]
4427
4470
  fn existing_runtime_producer_scopes_send_team() {
4428
- let _hermetic = super::super::hermetic_test_support::HermeticTestEnv::enter(
4471
+ let _hermetic = super::super::hermetic_test_support::HermeticTestEnv::enter_with_leader(
4429
4472
  "quick-start-value-existing-runtime",
4430
4473
  );
4431
4474
  let root = std::env::temp_dir().join(format!(
@@ -4448,6 +4491,7 @@ pub mod lifecycle_port {
4448
4491
  "---\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",
4449
4492
  )
4450
4493
  .unwrap();
4494
+ super::super::hermetic_test_support::seed_in_process_coordinator(&root);
4451
4495
  let transport = crate::transport::test_support::OfflineTransport::new();
4452
4496
  let first = crate::lifecycle::quick_start_with_transport(
4453
4497
  &team_dir,
@@ -1050,6 +1050,7 @@ fn resolve_leader(
1050
1050
  None,
1051
1051
  None,
1052
1052
  transport.tmux_endpoint(),
1053
+ None,
1053
1054
  );
1054
1055
  err.candidates =
1055
1056
  leader_advisory_candidates(state, team, transport, "state_recorded_socket");
@@ -1111,6 +1112,7 @@ fn resolve_leader(
1111
1112
  session,
1112
1113
  window,
1113
1114
  socket,
1115
+ string_field(receiver, "provider"),
1114
1116
  );
1115
1117
  if matches!(
1116
1118
  &reason,
@@ -1131,6 +1133,7 @@ fn resolve_leader(
1131
1133
  session,
1132
1134
  window,
1133
1135
  socket,
1136
+ string_field(receiver, "provider"),
1134
1137
  );
1135
1138
  err.log = format!("{} channel_probe_failed={error}", err.log);
1136
1139
  Err(err)
@@ -1574,14 +1577,20 @@ fn leader_not_attached_third_party(
1574
1577
  session: Option<&str>,
1575
1578
  window: Option<&str>,
1576
1579
  socket: Option<String>,
1580
+ provider: Option<&str>,
1577
1581
  ) -> NamedAddressError {
1578
1582
  let workspace = target_workspace.display();
1579
1583
  let mut err = NamedAddressError::new(
1580
1584
  NamedAddressErrorKind::LeaderNotAttached,
1581
1585
  "target team leader is not attached on its recorded tmux endpoint",
1582
1586
  );
1587
+ let provider = provider.and_then(crate::provider::wire::parse_provider)
1588
+ .map(crate::provider::wire::provider_wire);
1589
+ let workspace_arg = super::adapters::shell_quote(&target_workspace.to_string_lossy());
1590
+ let team_arg = super::adapters::shell_quote(team);
1583
1591
  err.action = format!(
1584
- "Ask the target team owner to run `team-agent attach-leader --workspace {workspace} --team {team}`; the sender-side offline mailbox will requeue the message once attach succeeds"
1592
+ "Ask the target team owner to select the actual Leader provider and run `team-agent attach-leader --workspace {workspace_arg} --team {team_arg} --provider {}`; the sender-side offline mailbox will requeue the message once attach succeeds",
1593
+ provider.unwrap_or("<provider>")
1585
1594
  );
1586
1595
  err.log = format!(
1587
1596
  "name={team}/leader expected_pane={} expected_session={} expected_window={} tmux_socket={}",
@@ -32,6 +32,10 @@ pub(crate) enum TokenUsage {
32
32
  }
33
33
  #[derive(Clone, Copy, Debug, Eq, PartialEq)]
34
34
  pub(crate) enum DispatchKind {
35
+ Start,
36
+ Stop,
37
+ Team,
38
+ Agent,
35
39
  QuickStart,
36
40
  Send,
37
41
  Status,
@@ -59,6 +63,10 @@ pub(crate) enum DispatchKind {
59
63
  AttachLeader,
60
64
  }
61
65
  pub(crate) const ALL_DISPATCH_KINDS: &[DispatchKind] = &[
66
+ DispatchKind::Start,
67
+ DispatchKind::Stop,
68
+ DispatchKind::Team,
69
+ DispatchKind::Agent,
62
70
  DispatchKind::QuickStart,
63
71
  DispatchKind::Send,
64
72
  DispatchKind::Status,
@@ -110,20 +118,24 @@ pub(crate) struct CommandSpec {
110
118
  // Only human operations belong here. Compatibility commands are private in emit.rs.
111
119
  #[rustfmt::skip]
112
120
  pub(crate) const COMMAND_SPECS: &[CommandSpec] = &[
113
- CommandSpec { name: "quick-start", tier: CommandTier::Core, category: CommandCategory::Start, kind: CommandKind::Dispatch(DispatchKind::QuickStart), summary: "Create a team from role files; use restart for an initialized team", usage: "team-agent quick-start [TEAMDIR] [--workspace WORKSPACE] [--team TEAM] [--name NAME] [--team-id TEAM] [--yes] [--backend tmux|conpty] [--json] [--detail]", default_help: true, command_help: true, suggestion_index: true, token_usage: TokenUsage::Conditional, sunset: None, action: None, governance: None },
121
+ CommandSpec { name: "start", tier: CommandTier::Core, category: CommandCategory::Start, kind: CommandKind::Dispatch(DispatchKind::Start), summary: "Start a team or agent; verified live instances stay unchanged", usage: "team-agent start [AGENT|./TEAMDIR] [--from TEAMDIR] [--workspace PATH] [--team KEY] [--session preserve|auto] [--dry-run] [--json]", default_help: true, command_help: true, suggestion_index: true, token_usage: TokenUsage::Conditional, sunset: None, action: None, governance: None },
122
+ CommandSpec { name: "stop", tier: CommandTier::Core, category: CommandCategory::TeamLifecycle, kind: CommandKind::Dispatch(DispatchKind::Stop), summary: "Stop the selected team or one agent, retaining session associations", usage: "team-agent stop [AGENT] [--workspace PATH] [--team KEY] [--json]", default_help: true, command_help: true, suggestion_index: true, token_usage: TokenUsage::No, sunset: None, action: None, governance: None },
123
+ CommandSpec { name: "team", tier: CommandTier::Core, category: CommandCategory::TeamLifecycle, kind: CommandKind::Dispatch(DispatchKind::Team), summary: "Explicit team start, stop, restart or status", usage: "team-agent team <start|stop|restart|status> [OPTIONS]", default_help: true, command_help: true, suggestion_index: true, token_usage: TokenUsage::Conditional, sunset: None, action: None, governance: None },
124
+ CommandSpec { name: "agent", tier: CommandTier::Core, category: CommandCategory::WorkerLifecycle, kind: CommandKind::Dispatch(DispatchKind::Agent), summary: "Explicit agent lifecycle operations", usage: "team-agent agent <start|stop|restart|add|remove|reset|clone|fork> AGENT [OPTIONS]", default_help: true, command_help: true, suggestion_index: true, token_usage: TokenUsage::Conditional, sunset: None, action: None, governance: None },
125
+ CommandSpec { name: "quick-start", tier: CommandTier::Core, category: CommandCategory::Start, kind: CommandKind::Dispatch(DispatchKind::QuickStart), summary: "Compatibility creation bridge; prefer start for daily activation", usage: "team-agent quick-start [TEAMDIR] [--workspace WORKSPACE] [--team TEAM] [--name NAME] [--team-id TEAM] [--yes] [--backend tmux|conpty] [--json] [--detail]", default_help: true, command_help: true, suggestion_index: true, token_usage: TokenUsage::Conditional, sunset: None, action: None, governance: None },
114
126
  CommandSpec { name: "send", tier: CommandTier::Core, category: CommandCategory::Start, kind: CommandKind::Dispatch(DispatchKind::Send), summary: "Send a task to a specified agent", usage: "team-agent send <agent> MESSAGE... [--workspace WORKSPACE] [--team TEAM] [--mailbox] [--json]", default_help: true, command_help: true, suggestion_index: true, token_usage: TokenUsage::Conditional, sunset: Some("a future compatibility release"), action: Some("Use team-agent send <agent> 'task message'; select scope with --workspace and --team"), governance: None },
115
127
  CommandSpec { name: "status", tier: CommandTier::Core, category: CommandCategory::Start, kind: CommandKind::Dispatch(DispatchKind::Status), summary: "Check agent status without reading conversation content", usage: "team-agent status [<agent>] [--workspace WORKSPACE] [--team TEAM] [--json] [--summary|--detail]", default_help: true, command_help: true, suggestion_index: true, token_usage: TokenUsage::No, sunset: None, action: None, governance: None },
116
128
  CommandSpec { name: "models", tier: CommandTier::Core, category: CommandCategory::Start, kind: CommandKind::Dispatch(DispatchKind::Models), summary: "Discover model names supported by the native provider", usage: "team-agent models [--provider pi|cursor|cursor_agent|codex|claude|claude_code|grok|kiro] [QUERY|--search TEXT] [--json]", default_help: true, command_help: true, suggestion_index: true, token_usage: TokenUsage::No, sunset: None, action: None, governance: None },
117
129
  CommandSpec { name: "inbox", tier: CommandTier::Core, category: CommandCategory::Start, kind: CommandKind::Dispatch(DispatchKind::Inbox), summary: "Read agent replies", usage: "team-agent inbox <agent> [-n N|--limit N] [--workspace WORKSPACE] [--team TEAM] [--json]", default_help: true, command_help: true, suggestion_index: true, token_usage: TokenUsage::No, sunset: None, action: None, governance: None },
118
- CommandSpec { name: "restart", tier: CommandTier::Core, category: CommandCategory::TeamLifecycle, kind: CommandKind::Dispatch(DispatchKind::Restart), summary: "Restart an existing team using saved sessions when available", usage: "team-agent restart [WORKSPACE] [--team TEAM] [--allow-fresh] [--session-converge-deadline SECONDS] [--json] [--detail]", default_help: true, command_help: true, suggestion_index: true, token_usage: TokenUsage::Conditional, sunset: None, action: None, governance: None },
119
- CommandSpec { name: "shutdown", tier: CommandTier::Core, category: CommandCategory::TeamLifecycle, kind: CommandKind::Dispatch(DispatchKind::Shutdown), summary: "Shut down the selected team and report residual resources", usage: "team-agent shutdown [--workspace WORKSPACE] [--team TEAM] [--keep-logs] [--json]", default_help: true, command_help: true, suggestion_index: true, token_usage: TokenUsage::No, sunset: None, action: None, governance: None },
120
- CommandSpec { name: "add-agent", tier: CommandTier::Core, category: CommandCategory::WorkerLifecycle, kind: CommandKind::Dispatch(DispatchKind::AddAgent), summary: "Add and start an agent", usage: "team-agent add-agent <agent> [--role-file FILE] [--provider TOOL] [--model MODEL] [--effort LEVEL] [--bypass true|false] [--prompt TEXT] [--profile NAME] [--workspace WORKSPACE] [--team TEAM] [--json]", default_help: true, command_help: true, suggestion_index: true, token_usage: TokenUsage::Conditional, sunset: None, action: None, governance: None },
121
- CommandSpec { name: "start-agent", tier: CommandTier::Core, category: CommandCategory::WorkerLifecycle, kind: CommandKind::Dispatch(DispatchKind::StartAgent), summary: "Start an existing team agent", usage: "team-agent start-agent <agent> [--provider TOOL] [--model MODEL] [--effort LEVEL] [--bypass true|false] [--prompt TEXT] [--profile NAME] [--workspace WORKSPACE] [--team TEAM] [--allow-fresh] [--json]", default_help: true, command_help: true, suggestion_index: true, token_usage: TokenUsage::Conditional, sunset: None, action: None, governance: None },
122
- CommandSpec { name: "stop-agent", tier: CommandTier::Core, category: CommandCategory::WorkerLifecycle, kind: CommandKind::Dispatch(DispatchKind::StopAgent), summary: "Stop an agent while retaining configuration and session history", usage: "team-agent stop-agent <agent> [--workspace WORKSPACE] [--team TEAM] [--json]", default_help: true, command_help: true, suggestion_index: true, token_usage: TokenUsage::No, sunset: None, action: None, governance: None },
123
- CommandSpec { name: "reset-agent", tier: CommandTier::Core, category: CommandCategory::WorkerLifecycle, kind: CommandKind::Dispatch(DispatchKind::ResetAgent), summary: "Discard an agent's saved session association", usage: "team-agent reset-agent <agent> --discard-session [--workspace WORKSPACE] [--team TEAM] [--json]", default_help: true, command_help: true, suggestion_index: true, token_usage: TokenUsage::No, sunset: None, action: None, governance: None },
124
- CommandSpec { name: "clone-agent", tier: CommandTier::Core, category: CommandCategory::WorkerLifecycle, kind: CommandKind::Dispatch(DispatchKind::CloneAgent), summary: "Clone agent configuration into a new agent", usage: "team-agent clone-agent <agent> --as NEW_AGENT [--label LABEL] [--workspace WORKSPACE] [--team TEAM] [--json]", default_help: true, command_help: true, suggestion_index: true, token_usage: TokenUsage::Conditional, sunset: None, action: None, governance: None },
125
- CommandSpec { name: "fork-agent", tier: CommandTier::Core, category: CommandCategory::WorkerLifecycle, kind: CommandKind::Dispatch(DispatchKind::ForkAgent), summary: "Fork an agent session where supported by the provider", usage: "team-agent fork-agent <agent> --as NEW_AGENT [--label LABEL] [--workspace WORKSPACE] [--team TEAM] [--json]", default_help: true, command_help: true, suggestion_index: true, token_usage: TokenUsage::Conditional, sunset: None, action: None, governance: None },
126
- CommandSpec { name: "remove-agent", tier: CommandTier::Core, category: CommandCategory::WorkerLifecycle, kind: CommandKind::Dispatch(DispatchKind::RemoveAgent), summary: "Remove an agent after confirmation", usage: "team-agent remove-agent <agent> --confirm [--workspace WORKSPACE] [--team TEAM] [--from-spec] [--force] [--json]", default_help: true, command_help: true, suggestion_index: true, token_usage: TokenUsage::No, sunset: None, action: None, governance: None },
130
+ CommandSpec { name: "restart", tier: CommandTier::Core, category: CommandCategory::TeamLifecycle, kind: CommandKind::Dispatch(DispatchKind::Restart), summary: "Restart one agent, or use the frozen bare/path team bridge", usage: "team-agent restart [AGENT|./WORKSPACE] [--workspace WORKSPACE] [--team TEAM] [--session auto|preserve] [--dry-run] [--allow-fresh] [--session-converge-deadline SECONDS] [--json] [--detail]", default_help: true, command_help: true, suggestion_index: true, token_usage: TokenUsage::Conditional, sunset: None, action: None, governance: None },
131
+ CommandSpec { name: "shutdown", tier: CommandTier::Core, category: CommandCategory::TeamLifecycle, kind: CommandKind::Dispatch(DispatchKind::Shutdown), summary: "Compatibility: shut down the selected team; prefer stop", usage: "team-agent shutdown [--workspace WORKSPACE] [--team TEAM] [--keep-logs] [--json]", default_help: true, command_help: true, suggestion_index: true, token_usage: TokenUsage::No, sunset: None, action: None, governance: None },
132
+ CommandSpec { name: "add-agent", tier: CommandTier::Core, category: CommandCategory::WorkerLifecycle, kind: CommandKind::Dispatch(DispatchKind::AddAgent), summary: "Compatibility: strict add-and-start; prefer agent add or start AGENT", usage: "team-agent add-agent <agent> [--role-file FILE] [--provider TOOL] [--model MODEL] [--effort LEVEL] [--bypass true|false] [--prompt TEXT] [--profile NAME] [--workspace WORKSPACE] [--team TEAM] [--json]", default_help: true, command_help: true, suggestion_index: true, token_usage: TokenUsage::Conditional, sunset: None, action: None, governance: None },
133
+ CommandSpec { name: "start-agent", tier: CommandTier::Core, category: CommandCategory::WorkerLifecycle, kind: CommandKind::Dispatch(DispatchKind::StartAgent), summary: "Compatibility: start an existing stopped agent; prefer start AGENT", usage: "team-agent start-agent <agent> [--provider TOOL] [--model MODEL] [--effort LEVEL] [--bypass true|false] [--prompt TEXT] [--profile NAME] [--workspace WORKSPACE] [--team TEAM] [--allow-fresh] [--json]", default_help: true, command_help: true, suggestion_index: true, token_usage: TokenUsage::Conditional, sunset: None, action: None, governance: None },
134
+ CommandSpec { name: "stop-agent", tier: CommandTier::Core, category: CommandCategory::WorkerLifecycle, kind: CommandKind::Dispatch(DispatchKind::StopAgent), summary: "Compatibility: stop an agent; prefer stop AGENT", usage: "team-agent stop-agent <agent> [--workspace WORKSPACE] [--team TEAM] [--json]", default_help: true, command_help: true, suggestion_index: true, token_usage: TokenUsage::No, sunset: None, action: None, governance: None },
135
+ CommandSpec { name: "reset-agent", tier: CommandTier::Core, category: CommandCategory::WorkerLifecycle, kind: CommandKind::Dispatch(DispatchKind::ResetAgent), summary: "Compatibility: discard session only with approval; prefer agent reset", usage: "team-agent reset-agent <agent> --discard-session [--workspace WORKSPACE] [--team TEAM] [--json]", default_help: true, command_help: true, suggestion_index: true, token_usage: TokenUsage::No, sunset: None, action: None, governance: None },
136
+ CommandSpec { name: "clone-agent", tier: CommandTier::Core, category: CommandCategory::WorkerLifecycle, kind: CommandKind::Dispatch(DispatchKind::CloneAgent), summary: "Compatibility: copy agent configuration; prefer agent clone", usage: "team-agent clone-agent <agent> --as NEW_AGENT [--label LABEL] [--workspace WORKSPACE] [--team TEAM] [--json]", default_help: true, command_help: true, suggestion_index: true, token_usage: TokenUsage::Conditional, sunset: None, action: None, governance: None },
137
+ CommandSpec { name: "fork-agent", tier: CommandTier::Core, category: CommandCategory::WorkerLifecycle, kind: CommandKind::Dispatch(DispatchKind::ForkAgent), summary: "Compatibility: admitted new-seat continuation; prefer agent fork", usage: "team-agent fork-agent <agent> --as NEW_AGENT [--label LABEL] [--workspace WORKSPACE] [--team TEAM] [--json]", default_help: true, command_help: true, suggestion_index: true, token_usage: TokenUsage::Conditional, sunset: None, action: None, governance: None },
138
+ CommandSpec { name: "remove-agent", tier: CommandTier::Core, category: CommandCategory::WorkerLifecycle, kind: CommandKind::Dispatch(DispatchKind::RemoveAgent), summary: "Compatibility: confirmed removal; prefer agent remove", usage: "team-agent remove-agent <agent> --confirm [--workspace WORKSPACE] [--team TEAM] [--from-spec] [--force] [--json]", default_help: true, command_help: true, suggestion_index: true, token_usage: TokenUsage::No, sunset: None, action: None, governance: None },
127
139
  CommandSpec { name: "leaders", tier: CommandTier::Core, category: CommandCategory::Observe, kind: CommandKind::Dispatch(DispatchKind::Leaders), summary: "List local leaders and their team scopes", usage: "team-agent leaders [QUERY|--search TEXT] [--all|--stale] [--json] | --prune [--dry-run] [--json]", default_help: true, command_help: true, suggestion_index: true, token_usage: TokenUsage::No, sunset: None, action: None, governance: None },
128
140
  CommandSpec { name: "doctor", tier: CommandTier::Core, category: CommandCategory::Observe, kind: CommandKind::Dispatch(DispatchKind::Doctor), summary: "Diagnose a team and suggest recovery; read-only by default", usage: "team-agent doctor [SPEC] [--workspace WORKSPACE] [--team TEAM] [--comms] [--gate orphans|comms] [--fix] [--fix-schema] [--cleanup-orphans] [--confirm] [--json]", default_help: true, command_help: true, suggestion_index: true, token_usage: TokenUsage::No, sunset: None, action: None, governance: None },
129
141
  CommandSpec { name: "approvals", tier: CommandTier::Core, category: CommandCategory::Observe, kind: CommandKind::Dispatch(DispatchKind::Approvals), summary: "List pending agent approvals", usage: "team-agent approvals [<agent>] [--workspace WORKSPACE] [--team TEAM] [--json]", default_help: true, command_help: true, suggestion_index: true, token_usage: TokenUsage::No, sunset: None, action: None, governance: None },
@@ -115,6 +115,7 @@ struct BriefNode {
115
115
  health: String,
116
116
  session_name: Option<String>,
117
117
  tmux_command: Option<String>,
118
+ observation_reason: Option<&'static str>,
118
119
  }
119
120
 
120
121
  #[derive(Clone, Debug, PartialEq, Eq)]
@@ -125,9 +126,10 @@ struct AttachContext {
125
126
  pane: String,
126
127
  }
127
128
 
128
- #[derive(Debug)]
129
+ #[derive(Debug, Default)]
129
130
  struct ProbeResult {
130
131
  nodes: Vec<ProbeNode>,
132
+ exited: BTreeSet<(String, String, String)>,
131
133
  }
132
134
 
133
135
  #[derive(Debug, Deserialize)]
@@ -158,7 +160,7 @@ pub(crate) fn status_brief_scoped(workspace: &Path, state: &Value, agent: Option
158
160
  json!({
159
161
  "nodes": nodes
160
162
  .into_iter()
161
- .map(|(node, _)| node.into_json())
163
+ .map(|(node, _)| scoped_node_json(node, workspace, state))
162
164
  .collect::<Vec<_>>()
163
165
  })
164
166
  }
@@ -167,14 +169,22 @@ pub(crate) fn status_brief_scoped(workspace: &Path, state: &Value, agent: Option
167
169
  /// context only when multiple nodes make that repetition useful.
168
170
  pub(crate) fn format_status_brief(workspace: &Path, state: &Value, agent: Option<&str>) -> String {
169
171
  let nodes = project_status_nodes(workspace, state, agent);
170
- if nodes.len() <= 1 {
171
- return nodes
172
- .into_iter()
173
- .filter_map(|(node, _)| format_brief_node(&node.into_json()))
174
- .collect::<Vec<_>>()
175
- .join("\n");
172
+ let diagnostics = nodes.iter().filter(|(node, _)| node.observation_reason.is_some())
173
+ .map(|(node, _)| {
174
+ let value = scoped_node_json(node.clone(), workspace, state);
175
+ format!("observation {}: {}; next_action: {}", node.name,
176
+ value["observation_reason"].as_str().unwrap_or("observation_inconclusive"),
177
+ value["next_action"].as_str().unwrap_or(""))
178
+ }).collect::<Vec<_>>();
179
+ let mut output = if nodes.len() <= 1 {
180
+ nodes.into_iter().filter_map(|(node, _)| format_brief_node(&node.into_json()))
181
+ .collect::<Vec<_>>().join("\n")
182
+ } else { format_multi_node_brief(nodes) };
183
+ if !diagnostics.is_empty() {
184
+ output.push('\n');
185
+ output.push_str(&diagnostics.join("\n"));
176
186
  }
177
- format_multi_node_brief(nodes)
187
+ output
178
188
  }
179
189
 
180
190
  fn project_status_nodes(
@@ -200,13 +210,18 @@ fn project_status_nodes(
200
210
  .map(|node| {
201
211
  #[cfg(unix)]
202
212
  if crate::contract_runtime::registry::descriptor(&node.provider).is_some() {
203
- let mut brief = BriefNode::unknown(node);
213
+ let mut brief = BriefNode::unknown(node).with_reason("native_runtime_projection_unavailable");
204
214
  if let Ok(native) = crate::contract_runtime::framework::project_seat(
205
215
  _workspace,
206
216
  &crate::state::projection::team_state_key(state),
207
217
  &node.name,
208
218
  ) {
209
219
  brief.runtime_status = native["status"].as_str().unwrap_or("unknown").into();
220
+ brief.observation_reason = match brief.runtime_status.as_str() {
221
+ "running" => Some("activity_or_health_not_observed"),
222
+ "stopped" => None,
223
+ _ => Some("native_runtime_not_ready"),
224
+ };
210
225
  brief.model = native["model"].as_str().map(str::to_owned);
211
226
  brief.effort = native["effort"].as_str().map(str::to_owned);
212
227
  if brief.runtime_status == "running" {
@@ -596,11 +611,14 @@ fn sample_native_panes(
596
611
  } else {
597
612
  snapshots(&pids)
598
613
  };
614
+ let exited = panes.iter().filter(|fields| {
615
+ native_pane_pid(fields[3]).and_then(|pid| processes.get(&pid)).is_some_and(|p| p.zombie)
616
+ }).map(|fields| (fields[0].to_string(), fields[1].to_string(), fields[2].to_string())).collect();
599
617
  let nodes = panes
600
618
  .into_iter()
601
619
  .filter_map(|fields| parse_native_pane(fields, endpoint, registered, &processes))
602
620
  .collect();
603
- ProbeResult { nodes }
621
+ ProbeResult { nodes, exited }
604
622
  }
605
623
 
606
624
  fn native_pane_fields(line: &str) -> Option<[&str; 5]> {
@@ -1199,7 +1217,7 @@ fn parse_probe(bytes: &[u8], requested_endpoint: &str) -> Result<ProbeResult, ()
1199
1217
  .iter()
1200
1218
  .filter_map(|node| parse_probe_node(node, report_socket))
1201
1219
  .collect();
1202
- Ok(ProbeResult { nodes })
1220
+ Ok(ProbeResult { nodes, ..Default::default() })
1203
1221
  }
1204
1222
 
1205
1223
  #[cfg(test)]
@@ -1273,18 +1291,34 @@ fn project_node_with_attach(
1273
1291
  health: "unknown".to_string(),
1274
1292
  session_name: None,
1275
1293
  tmux_command: None,
1294
+ observation_reason: None,
1276
1295
  },
1277
- MatchResult::Unique(_) | MatchResult::Ambiguous => BriefNode::unknown(node),
1296
+ MatchResult::Unique(_) | MatchResult::Ambiguous => BriefNode::unknown(node).with_reason("stopped_registration_has_live_pane"),
1278
1297
  };
1279
1298
  return (projected, None);
1280
1299
  }
1281
1300
  let MatchResult::Unique(observed) = matched else {
1282
- return (BriefNode::unknown(node), None);
1301
+ let reason = if matches!(matched, MatchResult::Ambiguous) {
1302
+ "pane_observation_ambiguous"
1303
+ } else if node.endpoint.is_none() {
1304
+ "endpoint_unregistered"
1305
+ } else if node.session.is_none() || node.pane.is_none() {
1306
+ "pane_binding_incomplete"
1307
+ } else if let Some(sample) = probe.and_then(Option::as_ref) {
1308
+ if sample.exited.iter().any(|(session, window, pane)| {
1309
+ node.session.as_deref() == Some(session.as_str())
1310
+ && node.window.as_deref().is_none_or(|name| name == window)
1311
+ && node.pane.as_deref() == Some(pane.as_str())
1312
+ }) { "pane_process_exited" }
1313
+ else if matches!(node.lifecycle.as_deref(), Some("starting" | "launching" | "pending")) { "startup_not_yet_observed" }
1314
+ else { "registered_pane_not_observed" }
1315
+ } else { "endpoint_probe_unavailable" };
1316
+ return (BriefNode::unknown(node).with_reason(reason), None);
1283
1317
  };
1284
1318
  if !node.provider.eq_ignore_ascii_case("unknown")
1285
1319
  && !node.provider.eq_ignore_ascii_case(&observed.provider)
1286
1320
  {
1287
- return (BriefNode::unknown(node), None);
1321
+ return (BriefNode::unknown(node).with_reason("provider_observation_mismatch"), None);
1288
1322
  }
1289
1323
  let pi_channel = observed.provider.eq_ignore_ascii_case("pi")
1290
1324
  && observed.evidence_method.as_deref() == Some("pi_activity_channel");
@@ -1321,6 +1355,7 @@ fn project_node_with_attach(
1321
1355
  health: health.to_string(),
1322
1356
  session_name: observed.session_name.clone(),
1323
1357
  tmux_command,
1358
+ observation_reason: (activity == "unknown" || health == "unknown").then_some("activity_or_health_not_observed"),
1324
1359
  },
1325
1360
  attach,
1326
1361
  )
@@ -1405,9 +1440,15 @@ impl BriefNode {
1405
1440
  health: "unknown".to_string(),
1406
1441
  session_name: None,
1407
1442
  tmux_command: None,
1443
+ observation_reason: Some("observation_inconclusive"),
1408
1444
  }
1409
1445
  }
1410
1446
 
1447
+ fn with_reason(mut self, reason: &'static str) -> Self {
1448
+ self.observation_reason = Some(reason);
1449
+ self
1450
+ }
1451
+
1411
1452
  fn into_json(self) -> Value {
1412
1453
  let mut object = Map::new();
1413
1454
  object.insert("name".to_string(), json!(self.name));
@@ -1425,10 +1466,30 @@ impl BriefNode {
1425
1466
  "tmux_command".to_string(),
1426
1467
  self.tmux_command.map(Value::String).unwrap_or(Value::Null),
1427
1468
  );
1469
+ if let Some(reason) = self.observation_reason {
1470
+ object.insert("observation_reason".into(), json!(reason));
1471
+ }
1428
1472
  Value::Object(object)
1429
1473
  }
1430
1474
  }
1431
1475
 
1476
+ fn scoped_node_json(node: BriefNode, workspace: &Path, state: &Value) -> Value {
1477
+ let mut value = node.into_json();
1478
+ if let Some(reason) = value["observation_reason"].as_str() {
1479
+ let guidance = match reason {
1480
+ "pane_process_exited" => "pane process exit confirmed; inspect saved session and Leader binding before explicit start",
1481
+ "startup_not_yet_observed" | "native_runtime_not_ready" => "wait for startup convergence and recheck this scope; do not reset on unknown",
1482
+ _ => "inspect this scope's observation and identity; do not infer process exit or retry lifecycle from unknown alone",
1483
+ };
1484
+ value["next_action"] = json!(format!(
1485
+ "team-agent doctor --workspace {} --team {} --json; {guidance}",
1486
+ shell_quote(&workspace.to_string_lossy()),
1487
+ shell_quote(&crate::state::projection::team_state_key(state)),
1488
+ ));
1489
+ }
1490
+ value
1491
+ }
1492
+
1432
1493
  #[cfg(test)]
1433
1494
  mod native_ps_batch_regressions {
1434
1495
  #![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
@@ -1448,6 +1509,35 @@ mod native_ps_batch_regressions {
1448
1509
  }
1449
1510
  }
1450
1511
 
1512
+ #[test]
1513
+ fn issue338_unknown_status_has_reason_and_same_scope_next_action() {
1514
+ let state = json!({
1515
+ "team_key": "scoped-team", "session_name": "sess",
1516
+ "agents": {"worker": {"provider": "pi", "status": "running"}}
1517
+ });
1518
+ let status = status_brief_scoped(Path::new("/owned/workspace with spaces"), &state, None);
1519
+ let node = &status["nodes"][0];
1520
+ assert_eq!(node["runtime_status"], "unknown");
1521
+ assert_eq!(node["observation_reason"], "endpoint_unregistered");
1522
+ let action = node["next_action"].as_str().unwrap();
1523
+ assert!(action.contains("--workspace '/owned/workspace with spaces'"));
1524
+ assert!(action.contains("--team 'scoped-team'"));
1525
+ assert!(action.contains("doctor"));
1526
+ assert!(!action.contains("restart"));
1527
+ }
1528
+
1529
+ #[test]
1530
+ fn issue338_confirmed_zombie_is_not_reported_as_unexplained_unknown() {
1531
+ let node = registered("e1", "%7");
1532
+ let probe = sample_native_panes("hp\tw\t%7\t123\tpi\n", "e1", &[node.clone()], |_| {
1533
+ BTreeMap::from([(123, ProcessSnapshot { zombie: true })])
1534
+ });
1535
+ let value = project_node(&node, Some(&Some(probe))).into_json();
1536
+ assert_eq!(value["runtime_status"], "unknown");
1537
+ assert_eq!(value["observation_reason"], "pane_process_exited");
1538
+ assert!(value["tmux_command"].is_null());
1539
+ }
1540
+
1451
1541
  #[test]
1452
1542
  fn eight_panes_use_one_endpoint_query() {
1453
1543
  let registered = (0..8)
@@ -1713,6 +1803,7 @@ mod tests {
1713
1803
  None,
1714
1804
  Some(ProbeResult {
1715
1805
  nodes: vec![probe("pi_activity_channel")],
1806
+ ..Default::default()
1716
1807
  }),
1717
1808
  ] {
1718
1809
  let value = project_node(&node, Some(&sample)).into_json();
@@ -1724,7 +1815,11 @@ mod tests {
1724
1815
  settings.0.unwrap_or("null"),
1725
1816
  settings.1.unwrap_or("null")
1726
1817
  )));
1727
- assert_eq!(value.as_object().unwrap().len(), 9);
1818
+ let diagnostics = usize::from(value.get("observation_reason").is_some());
1819
+ assert_eq!(value.as_object().unwrap().len(), 9 + diagnostics);
1820
+ if value["runtime_status"] == "unknown" {
1821
+ assert!(!value["observation_reason"].as_str().unwrap().is_empty());
1822
+ }
1728
1823
  }
1729
1824
  }
1730
1825
  }
@@ -1757,6 +1852,7 @@ mod tests {
1757
1852
  &node,
1758
1853
  Some(&Some(ProbeResult {
1759
1854
  nodes: vec![observed],
1855
+ ..Default::default()
1760
1856
  })),
1761
1857
  );
1762
1858
  assert_eq!(projected.runtime_status, "running");
@@ -1772,6 +1868,7 @@ mod tests {
1772
1868
  &node,
1773
1869
  Some(&Some(ProbeResult {
1774
1870
  nodes: vec![observed],
1871
+ ..Default::default()
1775
1872
  })),
1776
1873
  );
1777
1874
  assert_eq!(projected.activity, "idle");
@@ -1800,6 +1897,7 @@ mod tests {
1800
1897
  &node,
1801
1898
  Some(&Some(ProbeResult {
1802
1899
  nodes: vec![observed],
1900
+ ..Default::default()
1803
1901
  })),
1804
1902
  );
1805
1903
  assert_eq!(projected.runtime_status, "unknown");
@@ -1814,6 +1912,7 @@ mod tests {
1814
1912
  &node,
1815
1913
  Some(&Some(ProbeResult {
1816
1914
  nodes: vec![probe("pi_activity_channel"), probe("pi_activity_channel")],
1915
+ ..Default::default()
1817
1916
  })),
1818
1917
  );
1819
1918
  assert_eq!(projected.runtime_status, "unknown");
@@ -1829,6 +1928,7 @@ mod tests {
1829
1928
  &node,
1830
1929
  Some(&Some(ProbeResult {
1831
1930
  nodes: vec![observed],
1931
+ ..Default::default()
1832
1932
  })),
1833
1933
  );
1834
1934
  assert_eq!(projected.runtime_status, "unknown");
@@ -1841,6 +1941,7 @@ mod tests {
1841
1941
  &node,
1842
1942
  Some(&Some(ProbeResult {
1843
1943
  nodes: vec![probe("pi_activity_channel"), probe("pi_activity_channel")],
1944
+ ..Default::default()
1844
1945
  })),
1845
1946
  );
1846
1947
  assert_eq!(projected.runtime_status, "unknown");
@@ -875,7 +875,12 @@ fn kiro_leader_entry_has_help_and_typed_refusal_without_native_or_fallback_effec
875
875
  assert_eq!(value["reason"], "kiro_leader_not_admitted");
876
876
  let mut argv = vec!["kiro".into()];
877
877
  argv.extend(args);
878
- assert_eq!(crate::cli::run(&argv, &cwd), ExitCode::Error);
878
+ let expected = if values.contains(&"--attach-existing") {
879
+ ExitCode::Usage // Unknown controls fail before capability dispatch.
880
+ } else {
881
+ ExitCode::Error // Supported syntax retains the typed refusal.
882
+ };
883
+ assert_eq!(crate::cli::run(&argv, &cwd), expected);
879
884
  }
880
885
  assert_eq!(std::fs::read_dir(&cwd).unwrap().count(), 0);
881
886
  assert_eq!(