@team-agent/installer 0.5.113 → 0.5.114

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 (46) hide show
  1. package/Cargo.lock +252 -84
  2. package/Cargo.toml +3 -1
  3. package/crates/team-agent/Cargo.toml +3 -0
  4. package/crates/team-agent/src/cli/adapters.rs +28 -15
  5. package/crates/team-agent/src/cli/diagnose.rs +36 -13
  6. package/crates/team-agent/src/cli/emit.rs +55 -20
  7. package/crates/team-agent/src/cli/leader.rs +16 -0
  8. package/crates/team-agent/src/cli/leader_prompt.rs +261 -0
  9. package/crates/team-agent/src/cli/mod.rs +152 -8
  10. package/crates/team-agent/src/cli/models.rs +68 -18
  11. package/crates/team-agent/src/cli/spec.rs +7 -1
  12. package/crates/team-agent/src/cli/status_port/brief.rs +268 -101
  13. package/crates/team-agent/src/cli/tests/base.rs +88 -0
  14. package/crates/team-agent/src/cli/tests/status_send.rs +2 -0
  15. package/crates/team-agent/src/compiler.rs +66 -18
  16. package/crates/team-agent/src/contract_runtime/backend.rs +646 -0
  17. package/crates/team-agent/src/contract_runtime/config.rs +309 -0
  18. package/crates/team-agent/src/contract_runtime/discovery.rs +237 -0
  19. package/crates/team-agent/src/contract_runtime/forward.rs +229 -0
  20. package/crates/team-agent/src/contract_runtime/framework.rs +261 -0
  21. package/crates/team-agent/src/contract_runtime/mcp.rs +396 -0
  22. package/crates/team-agent/src/contract_runtime/mod.rs +14 -0
  23. package/crates/team-agent/src/contract_runtime/prompt.rs +216 -0
  24. package/crates/team-agent/src/contract_runtime/pump.rs +207 -0
  25. package/crates/team-agent/src/contract_runtime/registry.rs +30 -0
  26. package/crates/team-agent/src/coordinator/tick.rs +26 -8
  27. package/crates/team-agent/src/leader/mod.rs +1 -0
  28. package/crates/team-agent/src/leader/prompt.rs +906 -0
  29. package/crates/team-agent/src/leader/start.rs +260 -16
  30. package/crates/team-agent/src/leader/tests/mod.rs +2 -0
  31. package/crates/team-agent/src/leader/tests/prompt_contract.rs +268 -0
  32. package/crates/team-agent/src/leader/types.rs +6 -0
  33. package/crates/team-agent/src/lib.rs +4 -0
  34. package/crates/team-agent/src/lifecycle/launch/quick_start.rs +472 -388
  35. package/crates/team-agent/src/lifecycle/launch/spawn.rs +38 -9
  36. package/crates/team-agent/src/lifecycle/launch/state_projection.rs +17 -0
  37. package/crates/team-agent/src/lifecycle/launch.rs +39 -28
  38. package/crates/team-agent/src/lifecycle/types.rs +4 -1
  39. package/crates/team-agent/src/lifecycle/worker_command_context.rs +37 -27
  40. package/crates/team-agent/src/main.rs +6 -0
  41. package/crates/team-agent/src/messaging/delivery.rs +50 -1
  42. package/crates/team-agent/src/messaging/send.rs +10 -0
  43. package/crates/team-agent/src/messaging/tests/leader_inject_acceptance.rs +131 -0
  44. package/crates/team-agent/src/model/spec.rs +24 -12
  45. package/crates/team-agent/src/state/persist.rs +73 -0
  46. package/package.json +4 -4
@@ -66,6 +66,7 @@ pub mod leaders;
66
66
  pub mod models;
67
67
  pub mod named_address;
68
68
  pub mod profile;
69
+ pub(crate) mod leader_prompt;
69
70
  pub(crate) mod route;
70
71
  pub mod send;
71
72
  pub(crate) mod spec;
@@ -172,12 +173,18 @@ pub mod lifecycle_port {
172
173
  Err(crate::leader::start::PrepareLeaderStartError::PaneAuthorityRefused(refusal)) => {
173
174
  return Ok(pane_authority_refusal_value(provider, attach, &refusal));
174
175
  }
176
+ Err(crate::leader::start::PrepareLeaderStartError::Leader(
177
+ crate::leader::LeaderError::Prompt(error),
178
+ )) => return Ok(error.report()),
175
179
  Err(crate::leader::start::PrepareLeaderStartError::Leader(error)) => {
176
180
  return Err(CliError::Runtime(error.to_string()));
177
181
  }
178
182
  };
179
- let outcome = crate::leader::start::execute_prepared_leader_start(&prepared, cwd)
180
- .map_err(|e| CliError::Runtime(e.to_string()))?;
183
+ let outcome = match crate::leader::start::execute_prepared_leader_start(&prepared, cwd) {
184
+ Ok(outcome) => outcome,
185
+ Err(crate::leader::LeaderError::Prompt(error)) => return Ok(error.report()),
186
+ Err(error) => return Err(CliError::Runtime(error.to_string())),
187
+ };
181
188
  let plan = prepared.plan();
182
189
  let ok = match outcome.status {
183
190
  crate::leader::LeaderLaunchStatus::Exited => outcome.exit_code == Some(0),
@@ -1297,6 +1304,16 @@ pub mod lifecycle_port {
1297
1304
  Some(state) => state,
1298
1305
  None => shutdown_state_for_team(&run_workspace, team)?,
1299
1306
  };
1307
+ #[cfg(unix)]
1308
+ if crate::contract_runtime::framework::has_contract(&state) {
1309
+ crate::contract_runtime::framework::stop_selected(&run_workspace, &state).map_err(
1310
+ |error| {
1311
+ CliError::Runtime(format!(
1312
+ "native shutdown failed; owned resources preserved: {error}"
1313
+ ))
1314
+ },
1315
+ )?;
1316
+ }
1300
1317
  deadline.check("refresh_provider_sessions")?;
1301
1318
  let captured_missing_sessions =
1302
1319
  crate::lifecycle::restart::refresh_missing_provider_sessions(&mut state)
@@ -3108,6 +3125,10 @@ pub mod lifecycle_port {
3108
3125
  team: Option<&str>,
3109
3126
  session_converge_deadline_ms: Option<u64>,
3110
3127
  ) -> Result<Value, CliError> {
3128
+ #[cfg(unix)]
3129
+ crate::contract_runtime::framework::require_legacy_lifecycle(
3130
+ workspace, team, None, "restart",
3131
+ )?;
3111
3132
  match crate::lifecycle::restart_with_session_convergence_deadline(
3112
3133
  workspace,
3113
3134
  allow_fresh,
@@ -3128,6 +3149,19 @@ pub mod lifecycle_port {
3128
3149
  team: Option<&str>,
3129
3150
  role_config: &crate::lifecycle::role_config::RoleConfigPatch,
3130
3151
  ) -> Result<Value, CliError> {
3152
+ #[cfg(unix)]
3153
+ {
3154
+ crate::contract_runtime::framework::require_legacy_lifecycle(
3155
+ workspace,
3156
+ team,
3157
+ Some(agent),
3158
+ "start-agent",
3159
+ )?;
3160
+ crate::contract_runtime::framework::require_legacy_role(
3161
+ None,
3162
+ role_config.provider.as_deref(),
3163
+ )?;
3164
+ }
3131
3165
  let _ = force; // Never grants authority to stop an already-running seat.
3132
3166
  let agent_id = crate::model::ids::AgentId::new(agent);
3133
3167
  match crate::lifecycle::role_config::start_agent_from_role(
@@ -3195,6 +3229,13 @@ pub mod lifecycle_port {
3195
3229
  team: Option<&str>,
3196
3230
  ) -> Result<Value, CliError> {
3197
3231
  let agent_id = crate::model::ids::AgentId::new(agent);
3232
+ #[cfg(unix)]
3233
+ crate::contract_runtime::framework::require_legacy_lifecycle(
3234
+ workspace,
3235
+ team,
3236
+ Some(agent),
3237
+ "stop-agent",
3238
+ )?;
3198
3239
  match crate::lifecycle::stop_agent(workspace, &agent_id, team) {
3199
3240
  Ok(report) => Ok(json!({"ok": true, "agent_id": agent, "stopped": report.stopped})),
3200
3241
  Err(e) => Ok(error_value(e)),
@@ -3209,6 +3250,13 @@ pub mod lifecycle_port {
3209
3250
  team: Option<&str>,
3210
3251
  ) -> Result<Value, CliError> {
3211
3252
  let agent_id = crate::model::ids::AgentId::new(agent);
3253
+ #[cfg(unix)]
3254
+ crate::contract_runtime::framework::require_legacy_lifecycle(
3255
+ workspace,
3256
+ team,
3257
+ Some(agent),
3258
+ "reset-agent",
3259
+ )?;
3212
3260
  match crate::lifecycle::reset_agent(workspace, &agent_id, discard_session, true, team) {
3213
3261
  Ok(crate::lifecycle::ResetAgentOutcome::Reset {
3214
3262
  env,
@@ -3256,6 +3304,19 @@ pub mod lifecycle_port {
3256
3304
  ) -> Result<Value, CliError> {
3257
3305
  let agent_id = crate::model::ids::AgentId::new(agent);
3258
3306
  let source = (!role_file.is_empty()).then(|| Path::new(role_file));
3307
+ #[cfg(unix)]
3308
+ {
3309
+ crate::contract_runtime::framework::require_legacy_lifecycle(
3310
+ workspace,
3311
+ team,
3312
+ Some(agent),
3313
+ "add-agent",
3314
+ )?;
3315
+ crate::contract_runtime::framework::require_legacy_role(
3316
+ source,
3317
+ role_config.provider.as_deref(),
3318
+ )?;
3319
+ }
3259
3320
  match crate::lifecycle::role_config::add_agent_from_role(
3260
3321
  workspace,
3261
3322
  &agent_id,
@@ -3308,6 +3369,13 @@ pub mod lifecycle_port {
3308
3369
  ) -> Result<Value, CliError> {
3309
3370
  let source = crate::model::ids::AgentId::new(source_agent);
3310
3371
  let dest = crate::model::ids::AgentId::new(as_agent_id);
3372
+ #[cfg(unix)]
3373
+ crate::contract_runtime::framework::require_legacy_lifecycle(
3374
+ workspace,
3375
+ team,
3376
+ Some(source_agent),
3377
+ "fork-agent",
3378
+ )?;
3311
3379
  match crate::lifecycle::fork_agent(workspace, &source, &dest, label, true, team) {
3312
3380
  Ok(report) => {
3313
3381
  let mut value = json!({
@@ -3345,6 +3413,13 @@ pub mod lifecycle_port {
3345
3413
  ) -> Result<Value, CliError> {
3346
3414
  let source = crate::model::ids::AgentId::new(source_agent);
3347
3415
  let dest = crate::model::ids::AgentId::new(as_agent_id);
3416
+ #[cfg(unix)]
3417
+ crate::contract_runtime::framework::require_legacy_lifecycle(
3418
+ workspace,
3419
+ team,
3420
+ Some(source_agent),
3421
+ "clone-agent",
3422
+ )?;
3348
3423
  match crate::lifecycle::clone_agent(workspace, &source, &dest, label, true, team) {
3349
3424
  Ok(report) => Ok(json!({
3350
3425
  "ok": true,
@@ -3370,6 +3445,13 @@ pub mod lifecycle_port {
3370
3445
  team: Option<&str>,
3371
3446
  ) -> Result<Value, CliError> {
3372
3447
  let agent_id = crate::model::ids::AgentId::new(agent);
3448
+ #[cfg(unix)]
3449
+ crate::contract_runtime::framework::require_legacy_lifecycle(
3450
+ workspace,
3451
+ team,
3452
+ Some(agent),
3453
+ "remove-agent",
3454
+ )?;
3373
3455
  match crate::lifecycle::remove_agent_flag_requirements(workspace, &agent_id, team) {
3374
3456
  Ok(requirements) => {
3375
3457
  if !remove_agent_missing_flags(from_spec, confirm, force, &requirements).is_empty()
@@ -3712,7 +3794,13 @@ pub mod lifecycle_port {
3712
3794
  // tool-set load has not been confirmed yet (PendingToolLoad).
3713
3795
  let incomplete_session_capture_agents =
3714
3796
  launch.session_capture_incomplete_agents.clone();
3715
- let all_spawned = !launch.started.is_empty();
3797
+ let agent_ids: Vec<_> = launch
3798
+ .started
3799
+ .iter()
3800
+ .map(|agent| agent.agent_id.as_str())
3801
+ .chain(launch.contract_started.iter().map(|id| id.as_str()))
3802
+ .collect();
3803
+ let all_spawned = !agent_ids.is_empty();
3716
3804
  let leader_receiver_attached = launch.leader_receiver_attached;
3717
3805
  let all_resumable_have_session = incomplete_session_capture_agents.is_empty();
3718
3806
  let all_workers_spawned = all_spawned;
@@ -3840,7 +3928,7 @@ pub mod lifecycle_port {
3840
3928
  "ready": readiness_json.get("ready").cloned().unwrap_or(Value::Bool(false)),
3841
3929
  "session_name": session_name.as_str(),
3842
3930
  "team": team,
3843
- "agent_ids": launch.started.iter().map(|agent| agent.agent_id.as_str()).collect::<Vec<_>>(),
3931
+ "agent_ids": agent_ids,
3844
3932
  "dry_run": launch.dry_run,
3845
3933
  "next_actions": next_actions,
3846
3934
  "attach_commands": attach_commands,
@@ -3961,9 +4049,8 @@ pub mod lifecycle_port {
3961
4049
  .is_some());
3962
4050
  }
3963
4051
 
3964
- #[test]
3965
- fn pending_tool_load_success_output_explains_bound_send_without_doctor() {
3966
- let value = quick_start_value(crate::lifecycle::QuickStartReport::Ready {
4052
+ fn pending_tool_load_report() -> crate::lifecycle::QuickStartReport {
4053
+ crate::lifecycle::QuickStartReport::Ready {
3967
4054
  session_name: crate::transport::SessionName::new("team-demo"),
3968
4055
  launch: Box::new(crate::lifecycle::LaunchReport {
3969
4056
  session_name: crate::transport::SessionName::new("team-demo"),
@@ -3979,6 +4066,7 @@ pub mod lifecycle_port {
3979
4066
  provider_projects_root: None,
3980
4067
  managed_mcp_config: false,
3981
4068
  }],
4069
+ contract_started: Vec::new(),
3982
4070
  dry_run: false,
3983
4071
  tmux_endpoint: None,
3984
4072
  routes: Vec::new(),
@@ -3991,7 +4079,63 @@ pub mod lifecycle_port {
3991
4079
  attach_commands: Vec::new(),
3992
4080
  worker_readiness: crate::lifecycle::QuickStartReadiness::PendingToolLoad,
3993
4081
  team: "team-demo".to_string(),
3994
- });
4082
+ }
4083
+ }
4084
+
4085
+ #[test]
4086
+ fn contract_start_receipts_join_summary_without_bypassing_leader_binding() {
4087
+ for (legacy, contract) in [(false, true), (true, true), (true, false), (false, false)] {
4088
+ for bound in [false, true] {
4089
+ let mut report = pending_tool_load_report();
4090
+ let crate::lifecycle::QuickStartReport::Ready { launch, .. } = &mut report else {
4091
+ panic!("ready fixture required");
4092
+ };
4093
+ if !legacy {
4094
+ launch.started.clear();
4095
+ }
4096
+ if contract {
4097
+ launch
4098
+ .contract_started
4099
+ .push(crate::model::ids::AgentId::new("kiro-worker"));
4100
+ }
4101
+ launch.leader_receiver_attached = bound;
4102
+ launch.leader_bind_stage = (!bound).then(|| "caller_pane".into());
4103
+ launch.leader_bind_reason = (!bound).then(|| "caller_pane_missing".into());
4104
+ assert_eq!(launch.started.len(), usize::from(legacy));
4105
+ let expected: Vec<_> = [(legacy, "worker"), (contract, "kiro-worker")]
4106
+ .into_iter()
4107
+ .filter_map(|(started, id)| started.then_some(id))
4108
+ .collect();
4109
+ let value = quick_start_value(report);
4110
+ assert_eq!(value["agent_ids"], json!(expected));
4111
+ assert_eq!(
4112
+ value["readiness"]["all_workers_spawned"],
4113
+ json!(legacy || contract)
4114
+ );
4115
+ assert_eq!(value["readiness"]["all_spawned"], json!(legacy || contract));
4116
+ assert_eq!(value["readiness"]["leader_receiver_attached"], json!(bound));
4117
+ assert_eq!(value["ok"], json!((legacy || contract) && bound));
4118
+ assert_eq!(value["ready"], json!((legacy || contract) && bound));
4119
+ if !bound {
4120
+ assert_eq!(value["status"], "leader_binding_incomplete");
4121
+ assert_eq!(value["reason"], "caller_pane_missing");
4122
+ } else {
4123
+ assert_eq!(value["status"], "pending_tool_load");
4124
+ }
4125
+ let mut compact = value.clone();
4126
+ compact_quick_start_value(&mut compact);
4127
+ assert_eq!(
4128
+ compact["worker_readiness"]["all_workers_spawned"],
4129
+ json!(legacy || contract)
4130
+ );
4131
+ assert_eq!(compact["ready"], value["ready"]);
4132
+ }
4133
+ }
4134
+ }
4135
+
4136
+ #[test]
4137
+ fn pending_tool_load_success_output_explains_bound_send_without_doctor() {
4138
+ let value = quick_start_value(pending_tool_load_report());
3995
4139
  assert_eq!(
3996
4140
  value.get("summary").and_then(Value::as_str),
3997
4141
  Some("team started; leader bound; send a task next (worker tool load unverified): team-demo")
@@ -22,6 +22,10 @@ enum CatalogFormat {
22
22
  }
23
23
 
24
24
  pub fn cmd_models(args: &ModelsArgs) -> Result<CmdResult, CliError> {
25
+ #[cfg(unix)]
26
+ if crate::contract_runtime::registry::descriptor(&args.provider).is_some() {
27
+ return crate::contract_runtime::discovery::models(args);
28
+ }
25
29
  let records = match crate::provider::model_catalog::discover_model_catalog(&args.provider) {
26
30
  Ok(records) => records,
27
31
  Err(error) => {
@@ -36,7 +40,12 @@ pub fn cmd_models(args: &ModelsArgs) -> Result<CmdResult, CliError> {
36
40
  fn render_catalog(args: &ModelsArgs, all: Vec<ModelRecord>) -> Result<CmdResult, CliError> {
37
41
  let visible = args.search.as_deref().map_or_else(
38
42
  || all.clone(),
39
- |search| all.iter().filter(|model| crate::provider::model_catalog::model_matches(model, search)).cloned().collect(),
43
+ |search| {
44
+ all.iter()
45
+ .filter(|model| crate::provider::model_catalog::model_matches(model, search))
46
+ .cloned()
47
+ .collect()
48
+ },
40
49
  );
41
50
  let current = if args.provider == "pi" {
42
51
  current_role_model(&all.iter().map(|model| model.id.clone()).collect::<Vec<_>>())
@@ -66,14 +75,31 @@ fn render_catalog(args: &ModelsArgs, all: Vec<ModelRecord>) -> Result<CmdResult,
66
75
  preserve_json_order: true,
67
76
  })
68
77
  } else {
69
- let mut lines = vec![format!("models.v1 | {provider} models (copyable model_id):")];
78
+ let mut lines = vec![format!(
79
+ "models.v1 | {provider} models (copyable model_id):"
80
+ )];
70
81
  lines.push("PROVIDER VENDOR MODEL_ID DISPLAY_NAME CURRENT DEFAULT".to_string());
71
- lines.extend(visible.iter().map(|model| format!(
72
- "{} {} {} {} {} {}", model.provider, model.vendor, model.id, model.display_name,
73
- if args.provider == "pi" { (current.as_deref() == Some(model.id.as_str())).to_string() } else { "—".to_string() },
74
- model.default.map_or_else(|| "—".to_string(), |value| value.to_string()),
75
- )));
76
- lines.push(format!("auth: ok (catalog_visibility); {} model(s)", visible.len()));
82
+ lines.extend(visible.iter().map(|model| {
83
+ format!(
84
+ "{} {} {} {} {} {}",
85
+ model.provider,
86
+ model.vendor,
87
+ model.id,
88
+ model.display_name,
89
+ if args.provider == "pi" {
90
+ (current.as_deref() == Some(model.id.as_str())).to_string()
91
+ } else {
92
+ "—".to_string()
93
+ },
94
+ model
95
+ .default
96
+ .map_or_else(|| "—".to_string(), |value| value.to_string()),
97
+ )
98
+ }));
99
+ lines.push(format!(
100
+ "auth: ok (catalog_visibility); {} model(s)",
101
+ visible.len()
102
+ ));
77
103
  if visible.is_empty() {
78
104
  lines.push("No models matched --search/query; rerun without a search to list the full catalog.".to_string());
79
105
  }
@@ -83,12 +109,23 @@ fn render_catalog(args: &ModelsArgs, all: Vec<ModelRecord>) -> Result<CmdResult,
83
109
 
84
110
  /// Existing injectable Pi boundary retained for deterministic command/timeout tests.
85
111
  #[cfg(test)]
86
- fn cmd_models_with(args: &ModelsArgs, program: &Path, timeout: Duration, max_bytes: u64) -> Result<CmdResult, CliError> {
112
+ fn cmd_models_with(
113
+ args: &ModelsArgs,
114
+ program: &Path,
115
+ timeout: Duration,
116
+ max_bytes: u64,
117
+ ) -> Result<CmdResult, CliError> {
87
118
  cmd_models_with_format(args, program, CatalogFormat::Pi, timeout, max_bytes)
88
119
  }
89
120
 
90
121
  #[cfg(test)]
91
- fn cmd_models_with_format(args: &ModelsArgs, program: &Path, format: CatalogFormat, timeout: Duration, max_bytes: u64) -> Result<CmdResult, CliError> {
122
+ fn cmd_models_with_format(
123
+ args: &ModelsArgs,
124
+ program: &Path,
125
+ format: CatalogFormat,
126
+ timeout: Duration,
127
+ max_bytes: u64,
128
+ ) -> Result<CmdResult, CliError> {
92
129
  let bytes = match run_catalog(program, timeout, max_bytes) {
93
130
  Ok(bytes) => bytes,
94
131
  Err(message) => return Ok(failure(args, &message)),
@@ -102,7 +139,10 @@ fn cmd_models_with_format(args: &ModelsArgs, program: &Path, format: CatalogForm
102
139
 
103
140
  #[cfg(test)]
104
141
  fn parse_catalog(bytes: &[u8], format: CatalogFormat) -> Result<Vec<ModelRecord>, String> {
105
- let provider = match format { CatalogFormat::Pi => "pi", CatalogFormat::CursorAgent => "cursor_agent" };
142
+ let provider = match format {
143
+ CatalogFormat::Pi => "pi",
144
+ CatalogFormat::CursorAgent => "cursor_agent",
145
+ };
106
146
  let result = match format {
107
147
  CatalogFormat::Pi => crate::provider::model_catalog::parse_pi_catalog(bytes),
108
148
  CatalogFormat::CursorAgent => crate::provider::model_catalog::parse_cursor_catalog(bytes),
@@ -124,7 +164,12 @@ fn model_matches(model: &ModelRecord, search: &str) -> bool {
124
164
  #[cfg(test)]
125
165
  fn current_model_for(format: CatalogFormat, models: &[ModelRecord]) -> Option<String> {
126
166
  match format {
127
- CatalogFormat::Pi => current_role_model(&models.iter().map(|model| model.id.clone()).collect::<Vec<_>>()),
167
+ CatalogFormat::Pi => current_role_model(
168
+ &models
169
+ .iter()
170
+ .map(|model| model.id.clone())
171
+ .collect::<Vec<_>>(),
172
+ ),
128
173
  CatalogFormat::CursorAgent => None,
129
174
  }
130
175
  }
@@ -148,8 +193,14 @@ fn failure(args: &ModelsArgs, message: &str) -> CmdResult {
148
193
  fn failure_with_action(args: &ModelsArgs, message: &str, action: &str) -> CmdResult {
149
194
  let value = json!({ "schema_version": "models.v1", "ok": false, "provider": args.provider, "auth": "not_ready", "auth_basis": "catalog_visibility", "models": Value::Array(Vec::new()), "current_role_model": Value::Null, "error": message, "action": action });
150
195
  if args.json {
151
- let text = serde_json::to_string_pretty(&value).unwrap_or_else(|_| "{\"schema_version\":\"models.v1\",\"ok\":false}".to_string());
152
- CmdResult { output: CmdOutput::Human(text), exit: ExitCode::Error, as_json: false, preserve_json_order: true }
196
+ let text = serde_json::to_string_pretty(&value)
197
+ .unwrap_or_else(|_| "{\"schema_version\":\"models.v1\",\"ok\":false}".to_string());
198
+ CmdResult {
199
+ output: CmdOutput::Human(text),
200
+ exit: ExitCode::Error,
201
+ as_json: false,
202
+ preserve_json_order: true,
203
+ }
153
204
  } else {
154
205
  CmdResult { output: CmdOutput::Human(format!("models.v1 | error: {message}\naction: {action}\nauth: not_ready (catalog_visibility)")), exit: ExitCode::Error, as_json: false, preserve_json_order: false }
155
206
  }
@@ -1271,10 +1322,9 @@ mod tests {
1271
1322
  ),
1272
1323
  ExitCode::Error
1273
1324
  );
1274
- assert_eq!(
1275
- crate::cli::spec::command_spec("models").unwrap().usage,
1276
- "team-agent models [--provider pi|cursor_agent|codex|claude|claude_code|grok] [QUERY|--search TEXT] [--json]"
1277
- );
1325
+ let usage = "team-agent models [--provider pi|cursor_agent|codex|claude|claude_code|grok|kiro] [QUERY|--search TEXT] [--json]";
1326
+ assert_eq!(crate::cli::spec::command_spec("models").unwrap().usage, usage);
1327
+ assert!(crate::cli::emit::command_help(Some("models")).contains(usage));
1278
1328
  }
1279
1329
 
1280
1330
  #[test]
@@ -36,6 +36,7 @@ pub(crate) enum DispatchKind {
36
36
  Send,
37
37
  Status,
38
38
  Models,
39
+ KiroLeader,
39
40
  Inbox,
40
41
  Restart,
41
42
  Shutdown,
@@ -50,6 +51,7 @@ pub(crate) enum DispatchKind {
50
51
  Doctor,
51
52
  Approvals,
52
53
  Route,
54
+ LeaderPrompt,
53
55
  Profile,
54
56
  InstallSkill,
55
57
  ClaimLeader,
@@ -61,6 +63,7 @@ pub(crate) const ALL_DISPATCH_KINDS: &[DispatchKind] = &[
61
63
  DispatchKind::Send,
62
64
  DispatchKind::Status,
63
65
  DispatchKind::Models,
66
+ DispatchKind::KiroLeader,
64
67
  DispatchKind::Inbox,
65
68
  DispatchKind::Restart,
66
69
  DispatchKind::Shutdown,
@@ -75,6 +78,7 @@ pub(crate) const ALL_DISPATCH_KINDS: &[DispatchKind] = &[
75
78
  DispatchKind::Doctor,
76
79
  DispatchKind::Approvals,
77
80
  DispatchKind::Route,
81
+ DispatchKind::LeaderPrompt,
78
82
  DispatchKind::Profile,
79
83
  DispatchKind::InstallSkill,
80
84
  DispatchKind::ClaimLeader,
@@ -109,7 +113,7 @@ pub(crate) const COMMAND_SPECS: &[CommandSpec] = &[
109
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 },
110
114
  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 },
111
115
  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 },
112
- 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_agent|codex|claude|claude_code|grok] [QUERY|--search TEXT] [--json]", default_help: true, command_help: true, suggestion_index: true, token_usage: TokenUsage::No, sunset: None, action: None, governance: None },
116
+ 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_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 },
113
117
  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 },
114
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 },
115
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 },
@@ -124,11 +128,13 @@ pub(crate) const COMMAND_SPECS: &[CommandSpec] = &[
124
128
  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 },
125
129
  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 },
126
130
  CommandSpec { name: "route", tier: CommandTier::Core, category: CommandCategory::Setup, kind: CommandKind::Dispatch(DispatchKind::Route), summary: "Configure additional provider launch arguments", usage: "team-agent route [status|enable|disable|show [TOOL]|set TOOL -- ARG...|add TOOL -- ARG...|clear TOOL] [--json]", default_help: true, command_help: true, suggestion_index: true, token_usage: TokenUsage::No, sunset: None, action: None, governance: None },
131
+ CommandSpec { name: "leader-prompt", tier: CommandTier::Core, category: CommandCategory::Setup, kind: CommandKind::Dispatch(DispatchKind::LeaderPrompt), summary: "Persist supplemental instructions for new Leaders only", usage: "team-agent leader-prompt [show|set|append|clear] [--json] [--file PATH | -- TEXT]", default_help: true, command_help: true, suggestion_index: true, token_usage: TokenUsage::No, sunset: None, action: None, governance: None },
127
132
  CommandSpec { name: "profile", tier: CommandTier::Core, category: CommandCategory::Setup, kind: CommandKind::Dispatch(DispatchKind::Profile), summary: "Manage authentication and proxy profiles", usage: "team-agent profile init|doctor|show NAME [--workspace WORKSPACE] [--team TEAM] [--auth-mode MODE] [--proxy-mode direct|inherit] [--json]", default_help: true, command_help: true, suggestion_index: true, token_usage: TokenUsage::No, sunset: None, action: None, governance: None },
128
133
  CommandSpec { name: "install-skill", tier: CommandTier::Core, category: CommandCategory::Setup, kind: CommandKind::Dispatch(DispatchKind::InstallSkill), summary: "Optionally install an existing public operation skill", usage: "team-agent install-skill (--source DIR|--uninstall) [--target codex|claude|copilot|all] [--dest DIR] [--dry-run] [--json]", default_help: true, command_help: true, suggestion_index: true, token_usage: TokenUsage::No, sunset: None, action: None, governance: None },
129
134
  CommandSpec { name: "claim-leader", tier: CommandTier::Guided, category: CommandCategory::GuidedRecovery, kind: CommandKind::Dispatch(DispatchKind::ClaimLeader), summary: "Register the current terminal as leader only when diagnosis recommends it", usage: "team-agent claim-leader [--workspace WORKSPACE] [--team TEAM] [--confirm] [--json] [--detail]", default_help: true, command_help: true, suggestion_index: true, token_usage: TokenUsage::No, sunset: None, action: None, governance: None },
130
135
  CommandSpec { name: "takeover", tier: CommandTier::Guided, category: CommandCategory::GuidedRecovery, kind: CommandKind::Dispatch(DispatchKind::Takeover), summary: "Take over an existing team only after diagnosis and confirmation", usage: "team-agent takeover [--workspace WORKSPACE] [--team TEAM] [--confirm] [--json]", default_help: true, command_help: true, suggestion_index: true, token_usage: TokenUsage::No, sunset: None, action: None, governance: None },
131
136
  CommandSpec { name: "attach-leader", tier: CommandTier::Guided, category: CommandCategory::GuidedRecovery, kind: CommandKind::Dispatch(DispatchKind::AttachLeader), summary: "Attach a verified leader terminal only when diagnosis recommends it", usage: "team-agent attach-leader [--workspace WORKSPACE] [--team TEAM] --pane PANE --provider TOOL [--confirm] [--json]", default_help: true, command_help: true, suggestion_index: true, token_usage: TokenUsage::No, sunset: None, action: None, governance: None },
137
+ CommandSpec { name: "kiro", tier: CommandTier::Core, category: CommandCategory::Start, kind: CommandKind::Dispatch(DispatchKind::KiroLeader), summary: "Check Kiro leader launch admission (currently refused)", usage: "team-agent kiro [--json]", default_help: true, command_help: true, suggestion_index: true, token_usage: TokenUsage::No, sunset: None, action: None, governance: None },
132
138
  CommandSpec { name: "pi", tier: CommandTier::Core, category: CommandCategory::Start, kind: CommandKind::LeaderPassthrough { provider: "pi" }, summary: "Open a leader session with Pi", usage: "team-agent pi [--attach-existing] [--confirm] [--attach-session SESSION] [--external-leader] [--allow-nested-attach] [--json] [-- TOOL_ARGS...]", default_help: true, command_help: true, suggestion_index: true, token_usage: TokenUsage::Conditional, sunset: None, action: None, governance: None },
133
139
  CommandSpec { name: "codex", tier: CommandTier::Core, category: CommandCategory::Start, kind: CommandKind::LeaderPassthrough { provider: "codex" }, summary: "Open a leader session with Codex", usage: "team-agent codex [--attach-existing] [--confirm] [--attach-session SESSION] [--external-leader] [--allow-nested-attach] [--json] [-- TOOL_ARGS...]", default_help: true, command_help: true, suggestion_index: true, token_usage: TokenUsage::Conditional, sunset: None, action: None, governance: None },
134
140
  CommandSpec { name: "claude", tier: CommandTier::Core, category: CommandCategory::Start, kind: CommandKind::LeaderPassthrough { provider: "claude" }, summary: "Open a leader session with Claude Code", usage: "team-agent claude [--attach-existing] [--confirm] [--attach-session SESSION] [--external-leader] [--allow-nested-attach] [--json] [-- TOOL_ARGS...]", default_help: true, command_help: true, suggestion_index: true, token_usage: TokenUsage::Conditional, sunset: None, action: None, governance: None },