@team-agent/installer 0.5.102 → 0.5.104

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 (33) hide show
  1. package/Cargo.lock +1 -1
  2. package/Cargo.toml +1 -1
  3. package/crates/team-agent/src/cli/emit.rs +2 -1
  4. package/crates/team-agent/src/cli/models.rs +92 -226
  5. package/crates/team-agent/src/cli/spec.rs +2 -2
  6. package/crates/team-agent/src/compiler/tests.rs +19 -27
  7. package/crates/team-agent/src/compiler.rs +11 -105
  8. package/crates/team-agent/src/lifecycle/launch/add_agent_state.rs +4 -2
  9. package/crates/team-agent/src/lifecycle/launch/clone_agent.rs +2 -5
  10. package/crates/team-agent/src/lifecycle/launch/identity.rs +9 -13
  11. package/crates/team-agent/src/lifecycle/launch/pi_mcp.rs +37 -161
  12. package/crates/team-agent/src/lifecycle/launch/role_source.rs +1 -25
  13. package/crates/team-agent/src/lifecycle/launch/spawn.rs +1 -0
  14. package/crates/team-agent/src/lifecycle/profile_launch.rs +0 -12
  15. package/crates/team-agent/src/lifecycle/restart/avatar.rs +0 -1
  16. package/crates/team-agent/src/lifecycle/restart/common.rs +1 -1
  17. package/crates/team-agent/src/lifecycle/tests/cursor_require_explicit_model_red.rs +7 -19
  18. package/crates/team-agent/src/lifecycle/tests/grok_require_explicit_model_red.rs +8 -41
  19. package/crates/team-agent/src/lifecycle/tests/pi_compiler_red.rs +14 -8
  20. package/crates/team-agent/src/lifecycle/tests/pi_dual_entry_red.rs +8 -4
  21. package/crates/team-agent/src/lifecycle/worker_command_context.rs +4 -4
  22. package/crates/team-agent/src/messaging/results.rs +11 -42
  23. package/crates/team-agent/src/model/enums.rs +0 -9
  24. package/crates/team-agent/src/model/spec.rs +2 -2
  25. package/crates/team-agent/src/provider/adapters/cursor_agent.rs +7 -8
  26. package/crates/team-agent/src/provider/adapters/grok.rs +5 -18
  27. package/crates/team-agent/src/provider/mod.rs +1 -0
  28. package/crates/team-agent/src/provider/model_catalog.rs +650 -0
  29. package/crates/team-agent/src/provider/tests/adapter.rs +15 -24
  30. package/crates/team-agent/src/provider/wire.rs +2 -29
  31. package/package.json +4 -4
  32. package/skills/team-agent/SKILL.md +48 -33
  33. package/skills/team-agent/references/team-agent-operator.md +49 -526
package/Cargo.lock CHANGED
@@ -581,7 +581,7 @@ dependencies = [
581
581
 
582
582
  [[package]]
583
583
  name = "team-agent"
584
- version = "0.5.102"
584
+ version = "0.5.104"
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.102"
12
+ version = "0.5.104"
13
13
  license = "AGPL-3.0"
14
14
  rust-version = "1.95"
15
15
 
@@ -290,6 +290,7 @@ pub(crate) fn default_help() -> String {
290
290
  &["claim-leader", "takeover", "attach-leader"],
291
291
  );
292
292
  out.push_str("\nWorkflow: `team-agent models --provider pi`, then quick-start/add-agent/start-agent, then `team-agent send AGENT MESSAGE`.\n");
293
+ out.push_str("Model catalogs: pi, cursor_agent, codex, claude, claude_code.\n");
293
294
  out.push_str("Provider launchers:\n team-agent codex|claude|copilot|grok|cursor|pi ...\n");
294
295
  out.push_str("\nRun `team-agent <command> --help` for command flags.");
295
296
  out
@@ -340,7 +341,7 @@ fn command_help(command: Option<&str>) -> String {
340
341
  .to_string(),
341
342
  Some("allow-peer-talk") => "usage: team-agent allow-peer-talk A B [--workspace WORKSPACE] [--json]".to_string(),
342
343
  Some("status") => "usage: team-agent status [AGENT] [--workspace WORKSPACE] [--team TEAM] [--summary|--json] [--detail]\n\n输出七字段:name/provider/runtime_status/activity/health/session_name/tmux_command;人读与 --json 使用同一投影。缺少可靠定位或原生 tmux/process 采样时显示 unknown;tmux_command 可复制到对应目标。--summary/--detail 仅保留兼容性,不增加诊断字段。".to_string(),
343
- Some("models") => "usage: team-agent models [--provider pi|cursor_agent] [QUERY|--search TEXT] [--json]\n\nLists exact provider model ids with case-insensitive multi-word search across provider, vendor, id, and display name. Cursor uses the local `agent --list-models` catalog.".to_string(),
344
+ Some("models") => "usage: team-agent models [--provider pi|cursor_agent|codex|claude|claude_code] [QUERY|--search TEXT] [--json]\n\nLists exact provider model ids with case-insensitive multi-word search across provider, vendor, id, display name, and source aliases. Uses each provider's native catalog; Codex uses `codex debug models` and Claude uses SDK stream-json initialize.".to_string(),
344
345
  Some("leaders") => "usage: team-agent leaders [QUERY|--search TEXT] [--all|--stale] [--json] | --prune [--dry-run] [--json]\n\nLists LIVE leaders by default. Use --all to include retained STALE entries, --stale to inspect only STALE entries, QUERY or --search TEXT to match workspace/team/name fields, and --prune to remove only entries proven terminal by canonical state. --dry-run is valid only with --prune.".to_string(),
345
346
  Some("shutdown") => "usage: team-agent shutdown [--workspace WORKSPACE] [--team TEAM] [--keep-logs] [--json]".to_string(),
346
347
  Some("restart") => "usage: team-agent restart [WORKSPACE] [--team TEAM] [--allow-fresh] [--session-converge-deadline SECONDS] [--json] [--detail]".to_string(),
@@ -3,292 +3,157 @@ use super::{CliError, CmdOutput, CmdResult, ExitCode, ModelsArgs};
3
3
  use serde_json::{json, Value};
4
4
  #[cfg(test)]
5
5
  use std::collections::BTreeMap;
6
+ #[cfg(test)]
6
7
  use std::path::Path;
8
+ #[cfg(test)]
7
9
  use std::time::Duration;
10
+
11
+ #[cfg(test)]
8
12
  const CATALOG_TIMEOUT: Duration = Duration::from_secs(10);
13
+ #[cfg(test)]
9
14
  const MAX_CATALOG_BYTES: u64 = 1024 * 1024;
15
+ type ModelRecord = crate::provider::model_catalog::ModelRecord;
10
16
 
17
+ #[cfg(test)]
11
18
  #[derive(Clone, Copy, Debug, Eq, PartialEq)]
12
19
  enum CatalogFormat {
13
20
  Pi,
14
21
  CursorAgent,
15
22
  }
16
23
 
17
- #[derive(Clone, Debug)]
18
- struct ModelRecord {
19
- provider: String,
20
- vendor: String,
21
- id: String,
22
- display_name: String,
23
- default: Option<bool>,
24
- }
25
-
26
24
  pub fn cmd_models(args: &ModelsArgs) -> Result<CmdResult, CliError> {
27
- let (program, format) = match args.provider.as_str() {
28
- "pi" => (Path::new("pi"), CatalogFormat::Pi),
29
- "cursor_agent" => (Path::new("agent"), CatalogFormat::CursorAgent),
30
- provider => {
31
- return Ok(failure(
32
- args,
33
- &format!("unsupported model provider {provider:?}"),
34
- ))
25
+ let records = match crate::provider::model_catalog::discover_model_catalog(&args.provider) {
26
+ Ok(records) => records,
27
+ Err(error) => {
28
+ let message = error.message(&args.provider);
29
+ let action = error.action(&args.provider);
30
+ return Ok(failure_with_action(args, &message, &action));
35
31
  }
36
32
  };
37
- cmd_models_with_format(args, program, format, CATALOG_TIMEOUT, MAX_CATALOG_BYTES)
38
- }
39
-
40
- /// Existing injectable Pi boundary retained for deterministic command/timeout tests.
41
- fn cmd_models_with(
42
- args: &ModelsArgs,
43
- program: &Path,
44
- timeout: Duration,
45
- max_bytes: u64,
46
- ) -> Result<CmdResult, CliError> {
47
- cmd_models_with_format(args, program, CatalogFormat::Pi, timeout, max_bytes)
33
+ render_catalog(args, records)
48
34
  }
49
35
 
50
- fn cmd_models_with_format(
51
- args: &ModelsArgs,
52
- program: &Path,
53
- format: CatalogFormat,
54
- timeout: Duration,
55
- max_bytes: u64,
56
- ) -> Result<CmdResult, CliError> {
57
- let bytes = match run_catalog(program, timeout, max_bytes) {
58
- Ok(bytes) => bytes,
59
- Err(message) => return Ok(failure(args, &catalog_error_prefix(format, message))),
60
- };
61
- let all = match parse_catalog(&bytes, format) {
62
- Ok(models) => models,
63
- Err(message) => return Ok(failure(args, &message)),
64
- };
36
+ fn render_catalog(args: &ModelsArgs, all: Vec<ModelRecord>) -> Result<CmdResult, CliError> {
65
37
  let visible = args.search.as_deref().map_or_else(
66
38
  || all.clone(),
67
- |search| {
68
- all.iter()
69
- .filter(|model| model_matches(model, search))
70
- .cloned()
71
- .collect::<Vec<_>>()
72
- },
39
+ |search| all.iter().filter(|model| crate::provider::model_catalog::model_matches(model, search)).cloned().collect(),
73
40
  );
74
- let current = current_model_for(format, &all);
75
- let entries: Vec<Value> = visible
76
- .iter()
77
- .map(|model| {
78
- json!({
79
- "role_model": model.id,
80
- "current": if format == CatalogFormat::Pi {
81
- json!(current.as_deref() == Some(model.id.as_str()))
82
- } else {
83
- Value::Null
84
- },
85
- "provider": model.provider,
86
- "vendor": model.vendor,
87
- "model_id": model.id,
88
- "display_name": model.display_name,
89
- "default": model.default,
90
- })
91
- })
92
- .collect();
41
+ let current = if args.provider == "pi" {
42
+ current_role_model(&all.iter().map(|model| model.id.clone()).collect::<Vec<_>>())
43
+ } else {
44
+ None
45
+ };
46
+ let entries: Vec<Value> = visible.iter().map(|model| json!({
47
+ "role_model": model.id,
48
+ "current": if args.provider == "pi" { json!(current.as_deref() == Some(model.id.as_str())) } else { Value::Null },
49
+ "provider": model.provider,
50
+ "vendor": model.vendor,
51
+ "model_id": model.id,
52
+ "display_name": model.display_name,
53
+ "default": model.default,
54
+ "aliases": model.aliases,
55
+ })).collect();
93
56
  let provider = args.provider.as_str();
94
57
  let value = json!({
95
58
  "schema_version": "models.v1", "ok": true, "provider": provider, "models": entries,
96
59
  "auth": "ok", "auth_basis": "catalog_visibility", "current_role_model": current, "search": args.search,
97
60
  });
98
61
  if args.json {
99
- let text = serde_json::to_string_pretty(&value)?;
100
62
  Ok(CmdResult {
101
- output: CmdOutput::Human(text),
63
+ output: CmdOutput::Human(serde_json::to_string_pretty(&value)?),
102
64
  exit: ExitCode::Ok,
103
65
  as_json: false,
104
66
  preserve_json_order: true,
105
67
  })
106
68
  } else {
107
- let mut lines = vec![format!(
108
- "models.v1 | {provider} models (copyable model_id):"
109
- )];
69
+ let mut lines = vec![format!("models.v1 | {provider} models (copyable model_id):")];
110
70
  lines.push("PROVIDER VENDOR MODEL_ID DISPLAY_NAME CURRENT DEFAULT".to_string());
111
- lines.extend(visible.iter().map(|model| {
112
- format!(
113
- "{} {} {} {} {} {}",
114
- model.provider,
115
- model.vendor,
116
- model.id,
117
- model.display_name,
118
- if format == CatalogFormat::Pi {
119
- (current.as_deref() == Some(model.id.as_str())).to_string()
120
- } else {
121
- "—".to_string()
122
- },
123
- model
124
- .default
125
- .map_or_else(|| "—".to_string(), |value| value.to_string()),
126
- )
127
- }));
128
- lines.push(format!(
129
- "auth: ok (catalog_visibility); {} model(s)",
130
- visible.len()
131
- ));
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()));
132
77
  if visible.is_empty() {
133
- lines.push("No models matched --search/query.".to_string());
78
+ lines.push("No models matched --search/query; rerun without a search to list the full catalog.".to_string());
134
79
  }
135
80
  Ok(CmdResult::human(lines.join("\n")))
136
81
  }
137
82
  }
138
83
 
84
+ /// Existing injectable Pi boundary retained for deterministic command/timeout tests.
85
+ #[cfg(test)]
86
+ fn cmd_models_with(args: &ModelsArgs, program: &Path, timeout: Duration, max_bytes: u64) -> Result<CmdResult, CliError> {
87
+ cmd_models_with_format(args, program, CatalogFormat::Pi, timeout, max_bytes)
88
+ }
89
+
90
+ #[cfg(test)]
91
+ fn cmd_models_with_format(args: &ModelsArgs, program: &Path, format: CatalogFormat, timeout: Duration, max_bytes: u64) -> Result<CmdResult, CliError> {
92
+ let bytes = match run_catalog(program, timeout, max_bytes) {
93
+ Ok(bytes) => bytes,
94
+ Err(message) => return Ok(failure(args, &message)),
95
+ };
96
+ let all = match parse_catalog(&bytes, format) {
97
+ Ok(models) => models,
98
+ Err(message) => return Ok(failure(args, &message)),
99
+ };
100
+ render_catalog(args, all)
101
+ }
102
+
103
+ #[cfg(test)]
139
104
  fn parse_catalog(bytes: &[u8], format: CatalogFormat) -> Result<Vec<ModelRecord>, String> {
140
- match format {
141
- CatalogFormat::Pi => crate::lifecycle::launch::pi_mcp::parse_pi_list_models_table(bytes)
142
- .map(|models| {
143
- models
144
- .into_iter()
145
- .map(|id| {
146
- let vendor = id
147
- .split_once('/')
148
- .map_or_else(|| "pi".to_string(), |(vendor, _)| vendor.to_string());
149
- ModelRecord {
150
- provider: "pi".to_string(),
151
- vendor,
152
- display_name: id.clone(),
153
- id,
154
- default: None,
155
- }
156
- })
157
- .collect()
158
- })
159
- .map_err(safe_catalog_error),
160
- CatalogFormat::CursorAgent => parse_cursor_catalog(bytes),
161
- }
105
+ let provider = match format { CatalogFormat::Pi => "pi", CatalogFormat::CursorAgent => "cursor_agent" };
106
+ let result = match format {
107
+ CatalogFormat::Pi => crate::provider::model_catalog::parse_pi_catalog(bytes),
108
+ CatalogFormat::CursorAgent => crate::provider::model_catalog::parse_cursor_catalog(bytes),
109
+ };
110
+ result.map_err(|error| error.message(provider))
162
111
  }
163
112
 
113
+ #[cfg(test)]
164
114
  fn parse_cursor_catalog(bytes: &[u8]) -> Result<Vec<ModelRecord>, String> {
165
- let text = std::str::from_utf8(bytes)
166
- .map_err(|_| "Cursor model catalog is not valid UTF-8".to_string())?;
167
- let mut models = Vec::new();
168
- for line in text.lines() {
169
- let line = line.trim();
170
- if line.is_empty()
171
- || line
172
- .trim_end_matches(':')
173
- .eq_ignore_ascii_case("available models")
174
- {
175
- continue;
176
- }
177
- let Some((id, display)) = line.split_once(" - ") else {
178
- return Err("Cursor model catalog contains a malformed row".to_string());
179
- };
180
- let id = id.trim();
181
- if id.is_empty() || id.chars().any(char::is_whitespace) || display.trim().is_empty() {
182
- return Err("Cursor model catalog contains a malformed row".to_string());
183
- }
184
- if models.iter().any(|model: &ModelRecord| model.id == id) {
185
- return Err("Cursor model catalog contains duplicate model ids".to_string());
186
- }
187
- let default = display.trim_end().ends_with("(default)");
188
- let display_name = display.trim().strip_suffix("(default)").map_or_else(
189
- || display.trim().to_string(),
190
- |name| name.trim().to_string(),
191
- );
192
- let vendor = id
193
- .split_once('-')
194
- .map_or_else(|| "cursor".to_string(), |(vendor, _)| vendor.to_string());
195
- models.push(ModelRecord {
196
- provider: "cursor_agent".to_string(),
197
- vendor,
198
- id: id.to_string(),
199
- display_name,
200
- default: Some(default),
201
- });
202
- }
203
- if models.is_empty() {
204
- return Err("Cursor model catalog is empty".to_string());
205
- }
206
- Ok(models)
115
+ crate::provider::model_catalog::parse_cursor_catalog(bytes)
116
+ .map_err(|error| error.message("cursor_agent"))
207
117
  }
208
118
 
119
+ #[cfg(test)]
209
120
  fn model_matches(model: &ModelRecord, search: &str) -> bool {
210
- let fields = [
211
- model.provider.as_str(),
212
- model.vendor.as_str(),
213
- model.id.as_str(),
214
- model.display_name.as_str(),
215
- ];
216
- search
217
- .split_whitespace()
218
- .map(str::to_lowercase)
219
- .all(|needle| {
220
- fields
221
- .iter()
222
- .any(|field| field.to_lowercase().contains(needle.as_str()))
223
- })
121
+ crate::provider::model_catalog::model_matches(model, search)
224
122
  }
225
123
 
124
+ #[cfg(test)]
226
125
  fn current_model_for(format: CatalogFormat, models: &[ModelRecord]) -> Option<String> {
227
126
  match format {
228
- CatalogFormat::Pi => current_role_model(
229
- &models
230
- .iter()
231
- .map(|model| model.id.clone())
232
- .collect::<Vec<_>>(),
233
- ),
234
- // Cursor's catalog does not expose the active role model; never mark a
235
- // catalog default as current without an explicit runtime identity.
127
+ CatalogFormat::Pi => current_role_model(&models.iter().map(|model| model.id.clone()).collect::<Vec<_>>()),
236
128
  CatalogFormat::CursorAgent => None,
237
129
  }
238
130
  }
239
131
 
240
- fn catalog_error_prefix(format: CatalogFormat, message: String) -> String {
241
- match format {
242
- CatalogFormat::Pi => message,
243
- CatalogFormat::CursorAgent => message.replace("Pi", "Cursor"),
244
- }
245
- }
246
-
132
+ #[cfg(test)]
247
133
  fn failure(args: &ModelsArgs, message: &str) -> CmdResult {
248
- let executable = if args.provider == "cursor_agent" {
249
- "agent"
250
- } else {
251
- "pi"
252
- };
253
134
  let action = if message.starts_with("unsupported model provider") {
254
- "choose a supported provider: pi or cursor_agent".to_string()
135
+ "choose a supported provider: pi, cursor_agent, codex, claude, or claude_code".to_string()
255
136
  } else {
256
- format!(
257
- "install or repair the PATH-first `{executable}` executable, then retry `team-agent models --provider {}`",
258
- args.provider
259
- )
137
+ let executable = match args.provider.as_str() {
138
+ "cursor_agent" => "agent",
139
+ "codex" => "codex",
140
+ "claude" | "claude_code" => "claude",
141
+ _ => "pi",
142
+ };
143
+ format!("install or repair the PATH-first `{executable}` executable, then retry `team-agent models --provider {}`", args.provider)
260
144
  };
145
+ failure_with_action(args, message, &action)
146
+ }
147
+
148
+ fn failure_with_action(args: &ModelsArgs, message: &str, action: &str) -> CmdResult {
261
149
  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 });
262
150
  if args.json {
263
- let text = serde_json::to_string_pretty(&value)
264
- .unwrap_or_else(|_| "{\"schema_version\":\"models.v1\",\"ok\":false}".to_string());
265
- CmdResult {
266
- output: CmdOutput::Human(text),
267
- exit: ExitCode::Error,
268
- as_json: false,
269
- preserve_json_order: true,
270
- }
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 }
271
153
  } else {
272
154
  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 }
273
155
  }
274
156
  }
275
-
276
- fn safe_catalog_error(error: crate::provider::ProviderError) -> String {
277
- let text = error.to_string();
278
- if text.contains("not UTF-8") {
279
- "Pi model catalog is not valid UTF-8".into()
280
- } else if text.contains("duplicate") {
281
- "Pi model catalog contains duplicate model ids".into()
282
- } else if text.contains("header") {
283
- "Pi model catalog has an unexpected header".into()
284
- } else if text.contains("row") {
285
- "Pi model catalog contains a malformed row".into()
286
- } else if text.contains("empty") || text.contains("no models") {
287
- "Pi model catalog is empty".into()
288
- } else {
289
- "Pi model catalog is invalid".into()
290
- }
291
- }
292
157
  fn current_role_model(models: &[String]) -> Option<String> {
293
158
  current_role_model_from(
294
159
  models,
@@ -309,6 +174,7 @@ fn current_role_model_from(
309
174
  }
310
175
 
311
176
  /// Injectable executable, deadline, and byte-limit boundary for deterministic tests.
177
+ #[cfg(test)]
312
178
  fn run_catalog(program: &Path, timeout: Duration, max_bytes: u64) -> Result<Vec<u8>, String> {
313
179
  crate::lifecycle::launch::pi_mcp::run_pi_catalog(program, timeout, max_bytes)
314
180
  }
@@ -995,7 +861,7 @@ mod tests {
995
861
  assert!(text.contains("No models matched --search") && text.contains("auth: ok"));
996
862
  let refusal = crate::cli::emit::__test_dispatch(
997
863
  "models",
998
- &["--provider".into(), "codex".into()],
864
+ &["--provider".into(), "cloud".into()],
999
865
  Path::new("/tmp"),
1000
866
  );
1001
867
  assert!(matches!(refusal, Ok(ExitCode::Error)));
@@ -1095,14 +961,14 @@ mod tests {
1095
961
  );
1096
962
  assert_eq!(
1097
963
  crate::cli::run(
1098
- &["models".into(), "--provider".into(), "codex".into()],
964
+ &["models".into(), "--provider".into(), "cloud".into()],
1099
965
  Path::new("/tmp")
1100
966
  ),
1101
967
  ExitCode::Error
1102
968
  );
1103
969
  assert_eq!(
1104
970
  crate::cli::spec::command_spec("models").unwrap().usage,
1105
- "usage: team-agent models [--provider pi|cursor_agent] [QUERY|--search TEXT] [--json]"
971
+ "usage: team-agent models [--provider pi|cursor_agent|codex|claude|claude_code] [QUERY|--search TEXT] [--json]"
1106
972
  );
1107
973
  }
1108
974
 
@@ -147,7 +147,7 @@ pub(crate) const COMMAND_SPECS: &[CommandSpec] = &[
147
147
  CommandSpec { name: "quick-start", tier: CommandTier::Core, category: CommandCategory::Start, kind: CommandKind::Dispatch(DispatchKind::QuickStart), summary: "start or attach a team from TEAM.md", usage: "usage: team-agent quick-start [TEAMDIR] [--workspace WORKSPACE] [--name NAME] [--team-id TEAM|--team TEAM] [--yes] [--backend tmux|conpty] [--json]", default_help: true, command_help: true, suggestion_index: true, token_usage: TokenUsage::Conditional, sunset: None, action: None, governance: None },
148
148
  CommandSpec { name: "send", tier: CommandTier::Core, category: CommandCategory::Daily, kind: CommandKind::Dispatch(DispatchKind::Send), summary: "persist a message for an in-team short name or fully-qualified logical recipient", usage: "usage: team-agent send TO MESSAGE... [--workspace WORKSPACE] [--team TEAM] [--mailbox] [--json]\nTO forms are co-equal: an in-team short name (for example `team-agent send reviewer \"Review\"`) or `<workspace>::<team>/<agent>`.", default_help: true, command_help: true, suggestion_index: true, token_usage: TokenUsage::Conditional, sunset: Some("next compatibility release"), action: Some("use positional logical TO and the returned message id"), governance: None },
149
149
  CommandSpec { name: "status", tier: CommandTier::Core, category: CommandCategory::Daily, kind: CommandKind::Dispatch(DispatchKind::Status), summary: "show current team status", usage: "usage: team-agent status [AGENT] [--workspace WORKSPACE] [--team TEAM] [--summary|--json] [--detail]", default_help: true, command_help: true, suggestion_index: true, token_usage: TokenUsage::No, sunset: None, action: None, governance: None },
150
- CommandSpec { name: "models", tier: CommandTier::Core, category: CommandCategory::Observe, kind: CommandKind::Dispatch(DispatchKind::Models), summary: "list exact provider model ids", usage: "usage: team-agent models [--provider pi|cursor_agent] [QUERY|--search TEXT] [--json]", default_help: true, command_help: true, suggestion_index: true, token_usage: TokenUsage::No, sunset: None, action: None, governance: None },
150
+ CommandSpec { name: "models", tier: CommandTier::Core, category: CommandCategory::Observe, kind: CommandKind::Dispatch(DispatchKind::Models), summary: "list exact provider model ids", usage: "usage: team-agent models [--provider pi|cursor_agent|codex|claude|claude_code] [QUERY|--search TEXT] [--json]", default_help: true, command_help: true, suggestion_index: true, token_usage: TokenUsage::No, sunset: None, action: None, governance: None },
151
151
  CommandSpec { name: "results", tier: CommandTier::Core, category: CommandCategory::Daily, kind: CommandKind::Dispatch(DispatchKind::Results), summary: "read reported results for a case", usage: "usage: team-agent results --case CASE_ID [--workspace WORKSPACE] [--team TEAM] [--json]", default_help: true, command_help: true, suggestion_index: true, token_usage: TokenUsage::No, sunset: None, action: None, governance: None },
152
152
  CommandSpec { name: "wait", tier: CommandTier::Core, category: CommandCategory::Daily, kind: CommandKind::Dispatch(DispatchKind::Wait), summary: "block until a task result is stored", usage: "usage: team-agent wait --task TASK [--workspace WORKSPACE] [--json]", default_help: false, command_help: true, suggestion_index: true, token_usage: TokenUsage::No, sunset: None, action: None, governance: None },
153
153
  CommandSpec { name: "restart", tier: CommandTier::Core, category: CommandCategory::TeamLifecycle, kind: CommandKind::Dispatch(DispatchKind::Restart), summary: "restart the selected team", usage: "usage: team-agent restart [WORKSPACE] [--team TEAM] [--allow-fresh] [--session-converge-deadline SECONDS] [--json]", default_help: true, command_help: true, suggestion_index: true, token_usage: TokenUsage::Conditional, sunset: None, action: None, governance: None },
@@ -165,7 +165,7 @@ pub(crate) const COMMAND_SPECS: &[CommandSpec] = &[
165
165
  CommandSpec { name: "copilot", tier: CommandTier::Core, category: CommandCategory::Start, kind: CommandKind::LeaderPassthrough { provider: "copilot" }, summary: "launch a Copilot leader", usage: "usage: team-agent copilot [provider args...]", default_help: false, command_help: true, suggestion_index: true, token_usage: TokenUsage::Yes, sunset: None, action: None, governance: None },
166
166
  CommandSpec { name: "grok", tier: CommandTier::Core, category: CommandCategory::Start, kind: CommandKind::LeaderPassthrough { provider: "grok" }, summary: "launch a Grok leader", usage: "usage: team-agent grok [provider args...]", default_help: false, command_help: true, suggestion_index: true, token_usage: TokenUsage::Yes, sunset: None, action: None, governance: None },
167
167
  CommandSpec { name: "cursor", tier: CommandTier::Core, category: CommandCategory::Start, kind: CommandKind::LeaderPassthrough { provider: "cursor" }, summary: "launch a Cursor leader", usage: "usage: team-agent cursor [provider args...]", default_help: false, command_help: true, suggestion_index: true, token_usage: TokenUsage::Yes, sunset: None, action: None, governance: None },
168
- CommandSpec { name: "pi", tier: CommandTier::Core, category: CommandCategory::Start, kind: CommandKind::LeaderPassthrough { provider: "pi" }, summary: "launch a Pi leader", usage: "usage: team-agent pi -- --model <exact> --thinking <effort>", default_help: false, command_help: true, suggestion_index: true, token_usage: TokenUsage::Yes, sunset: None, action: None, governance: None },
168
+ CommandSpec { name: "pi", tier: CommandTier::Core, category: CommandCategory::Start, kind: CommandKind::LeaderPassthrough { provider: "pi" }, summary: "launch a Pi leader", usage: "usage: team-agent pi [provider args...]", default_help: false, command_help: true, suggestion_index: true, token_usage: TokenUsage::Yes, sunset: None, action: None, governance: None },
169
169
 
170
170
  CommandSpec { name: "leaders", tier: CommandTier::Secondary, category: CommandCategory::Observe, kind: CommandKind::Dispatch(DispatchKind::Leaders), summary: "list live, stale, or ambiguous host leaders", usage: "usage: team-agent leaders [QUERY|--search TEXT] [--all|--stale] [--json] | --prune [--dry-run] [--json]", default_help: false, command_help: true, suggestion_index: false, token_usage: TokenUsage::No, sunset: None, action: None, governance: None },
171
171
  CommandSpec { name: "doctor", tier: CommandTier::Core, category: CommandCategory::Observe, kind: CommandKind::Dispatch(DispatchKind::Doctor), summary: "inspect environment and Team runtime health", usage: "usage: team-agent doctor [SPEC] [--workspace WORKSPACE] [--team TEAM] [--gate orphans|comms] [--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 },
@@ -221,9 +221,8 @@ fn compile_subscription_without_profile_is_thin_manifest() {
221
221
  );
222
222
  }
223
223
 
224
- // Runtime front-matter defaults: every knob from TEAM.md flows through without
225
- // per-role repetition; thin role inherits default_model; leader.model is null
226
- // (TEAM.md has no `model:` key, only `default_model:`).
224
+ // Runtime front-matter defaults: TEAM.md runtime settings remain available,
225
+ // but its default_model does not fill an omitted role model; leader.model is null.
227
226
  const RUNTIME_DEFAULTS_TEAM: &str = "\
228
227
  ---
229
228
  name: doc-team
@@ -256,7 +255,7 @@ dangerously_skip_permissions: false
256
255
  Implement bounded tasks.
257
256
  ";
258
257
 
259
- const RUNTIME_DEFAULTS_JSON: &str = r#"{"version":1,"team":{"name":"doc-team","mode":"supervisor_worker","objective":"Compile role docs.","workspace":"__WS__"},"leader":{"id":"leader","role":"leader","provider":"codex","model":null,"context_policy":{"keep_user_thread":true,"receive_worker_outputs":"business_messages_and_short_summaries","max_worker_result_tokens":2000}},"agents":[{"id":"implementer","role":"Implementation Engineer","provider":"codex","model":"gpt-5.4","auth_mode":"subscription","working_directory":"__WS__","system_prompt":{"inline":"Implement bounded tasks.","file":null},"dangerously_skip_permissions":false,"communication_mode":"leader_centric","preferred_for":["implementer","Implementation Engineer"],"avoid_for":[],"output_contract":{"format":"result_envelope_v1","required_fields":["task_id","status","summary","artifacts"]}}],"routing":{"default_assignee":"implementer","rules":[{"id":"route-implementer","match":{"assignee":["implementer"]},"assign_to":"implementer","priority":10}]},"communication":{"protocol":"mcp_inbox","topology":"leader_centered","worker_to_worker":true,"ack_timeout_sec":60,"result_format":"result_envelope_v1","message_store":{"sqlite":".team/runtime/team.db","mirror_files":".team/messages"}},"runtime":{"backend":"tmux","session_name":"team-doc-team","auto_launch":true,"require_user_approval_before_launch":true,"max_active_agents":1,"startup_order":["implementer"],"fast":true,"tick_interval_sec":1,"push_min_interval_sec":3,"stuck_timeout_sec":5},"context":{"state_file":"team_state.md","artifact_dir":".team/artifacts","log_dir":".team/logs","summarization":{"worker_full_logs":"retain_outside_leader_context","state_update":"after_each_result"}},"tasks":[{"id":"task_initial","title":"Initial document-driven team task","type":"implementation","assignee":"implementer","deps":[],"acceptance":["Worker reports valid result_envelope_v1"],"status":"pending","requires_tools":["mcp_team"],"files":[],"risk":"low"}]}"#;
258
+ const RUNTIME_DEFAULTS_JSON: &str = r#"{"version":1,"team":{"name":"doc-team","mode":"supervisor_worker","objective":"Compile role docs.","workspace":"__WS__"},"leader":{"id":"leader","role":"leader","provider":"codex","model":null,"context_policy":{"keep_user_thread":true,"receive_worker_outputs":"business_messages_and_short_summaries","max_worker_result_tokens":2000}},"agents":[{"id":"implementer","role":"Implementation Engineer","provider":"codex","model":null,"auth_mode":"subscription","working_directory":"__WS__","system_prompt":{"inline":"Implement bounded tasks.","file":null},"dangerously_skip_permissions":false,"communication_mode":"leader_centric","preferred_for":["implementer","Implementation Engineer"],"avoid_for":[],"output_contract":{"format":"result_envelope_v1","required_fields":["task_id","status","summary","artifacts"]}}],"routing":{"default_assignee":"implementer","rules":[{"id":"route-implementer","match":{"assignee":["implementer"]},"assign_to":"implementer","priority":10}]},"communication":{"protocol":"mcp_inbox","topology":"leader_centered","worker_to_worker":true,"ack_timeout_sec":60,"result_format":"result_envelope_v1","message_store":{"sqlite":".team/runtime/team.db","mirror_files":".team/messages"}},"runtime":{"backend":"tmux","session_name":"team-doc-team","auto_launch":true,"require_user_approval_before_launch":true,"max_active_agents":1,"startup_order":["implementer"],"fast":true,"tick_interval_sec":1,"push_min_interval_sec":3,"stuck_timeout_sec":5},"context":{"state_file":"team_state.md","artifact_dir":".team/artifacts","log_dir":".team/logs","summarization":{"worker_full_logs":"retain_outside_leader_context","state_update":"after_each_result"}},"tasks":[{"id":"task_initial","title":"Initial document-driven team task","type":"implementation","assignee":"implementer","deps":[],"acceptance":["Worker reports valid result_envelope_v1"],"status":"pending","requires_tools":["mcp_team"],"files":[],"risk":"low"}]}"#;
260
259
 
261
260
  #[test]
262
261
  fn compile_runtime_front_matter_defaults_match_python() {
@@ -269,8 +268,7 @@ fn compile_runtime_front_matter_defaults_match_python() {
269
268
  assert_eq!(templated_compact_json(&spec), RUNTIME_DEFAULTS_JSON);
270
269
  }
271
270
 
272
- // provider_models alias ladder: role provider `claude_code` with NO `claude_code`
273
- // key in provider_models falls back to the `claude` key (compiler.py:317-318).
271
+ // TEAM provider_models do not fill a role whose model is omitted.
274
272
  const ALIAS_TEAM: &str = "\
275
273
  ---
276
274
  name: debate-team
@@ -296,16 +294,16 @@ dangerously_skip_permissions: false
296
294
  Edit and defend the argument.
297
295
  ";
298
296
 
299
- const ALIAS_JSON: &str = r#"{"version":1,"team":{"name":"debate-team","mode":"supervisor_worker","objective":"Compile thin role docs.","workspace":"__WS__"},"leader":{"id":"leader","role":"leader","provider":"codex","model":null,"context_policy":{"keep_user_thread":true,"receive_worker_outputs":"business_messages_and_short_summaries","max_worker_result_tokens":2000}},"agents":[{"id":"editor","role":"Editor and Defender","provider":"claude_code","model":"claude-sonnet-4-6","auth_mode":"subscription","working_directory":"__WS__","system_prompt":{"inline":"Edit and defend the argument.","file":null},"dangerously_skip_permissions":false,"communication_mode":"leader_centric","preferred_for":["editor","Editor and Defender"],"avoid_for":[],"output_contract":{"format":"result_envelope_v1","required_fields":["task_id","status","summary","artifacts"]}}],"routing":{"default_assignee":"editor","rules":[{"id":"route-editor","match":{"assignee":["editor"]},"assign_to":"editor","priority":10}]},"communication":{"protocol":"mcp_inbox","topology":"leader_centered","worker_to_worker":true,"ack_timeout_sec":60,"result_format":"result_envelope_v1","message_store":{"sqlite":".team/runtime/team.db","mirror_files":".team/messages"}},"runtime":{"backend":"tmux","session_name":"team-debate-team","auto_launch":true,"require_user_approval_before_launch":true,"max_active_agents":1,"startup_order":["editor"],"fast":false,"tick_interval_sec":2,"push_min_interval_sec":60,"stuck_timeout_sec":300},"context":{"state_file":"team_state.md","artifact_dir":".team/artifacts","log_dir":".team/logs","summarization":{"worker_full_logs":"retain_outside_leader_context","state_update":"after_each_result"}},"tasks":[{"id":"task_initial","title":"Initial document-driven team task","type":"implementation","assignee":"editor","deps":[],"acceptance":["Worker reports valid result_envelope_v1"],"status":"pending","requires_tools":["mcp_team"],"files":[],"risk":"low"}]}"#;
297
+ const ALIAS_JSON: &str = r#"{"version":1,"team":{"name":"debate-team","mode":"supervisor_worker","objective":"Compile thin role docs.","workspace":"__WS__"},"leader":{"id":"leader","role":"leader","provider":"codex","model":null,"context_policy":{"keep_user_thread":true,"receive_worker_outputs":"business_messages_and_short_summaries","max_worker_result_tokens":2000}},"agents":[{"id":"editor","role":"Editor and Defender","provider":"claude_code","model":null,"auth_mode":"subscription","working_directory":"__WS__","system_prompt":{"inline":"Edit and defend the argument.","file":null},"dangerously_skip_permissions":false,"communication_mode":"leader_centric","preferred_for":["editor","Editor and Defender"],"avoid_for":[],"output_contract":{"format":"result_envelope_v1","required_fields":["task_id","status","summary","artifacts"]}}],"routing":{"default_assignee":"editor","rules":[{"id":"route-editor","match":{"assignee":["editor"]},"assign_to":"editor","priority":10}]},"communication":{"protocol":"mcp_inbox","topology":"leader_centered","worker_to_worker":true,"ack_timeout_sec":60,"result_format":"result_envelope_v1","message_store":{"sqlite":".team/runtime/team.db","mirror_files":".team/messages"}},"runtime":{"backend":"tmux","session_name":"team-debate-team","auto_launch":true,"require_user_approval_before_launch":true,"max_active_agents":1,"startup_order":["editor"],"fast":false,"tick_interval_sec":2,"push_min_interval_sec":60,"stuck_timeout_sec":300},"context":{"state_file":"team_state.md","artifact_dir":".team/artifacts","log_dir":".team/logs","summarization":{"worker_full_logs":"retain_outside_leader_context","state_update":"after_each_result"}},"tasks":[{"id":"task_initial","title":"Initial document-driven team task","type":"implementation","assignee":"editor","deps":[],"acceptance":["Worker reports valid result_envelope_v1"],"status":"pending","requires_tools":["mcp_team"],"files":[],"risk":"low"}]}"#;
300
298
 
301
299
  #[test]
302
- fn compile_provider_models_claude_code_falls_back_to_claude_alias() {
300
+ fn compile_provider_models_do_not_fill_claude_code_role_model() {
303
301
  let team = build_team(ALIAS_TEAM, &[("implementer.md", ALIAS_ROLE)], &[]);
304
302
  let spec = compile_team(&team).unwrap();
305
303
  assert_eq!(templated_compact_json(&spec), ALIAS_JSON);
306
304
  }
307
305
 
308
- // Builtin provider default: no model anywhere → DEFAULT_PROVIDER_MODELS[codex] = gpt-5.5.
306
+ // Omitted role model remains null; the provider supplies its native default.
309
307
  const BUILTIN_TEAM: &str = "\
310
308
  ---
311
309
  name: default-model-team
@@ -329,10 +327,10 @@ tools:
329
327
  Implement bounded tasks.
330
328
  ";
331
329
 
332
- const BUILTIN_JSON: &str = r#"{"version":1,"team":{"name":"default-model-team","mode":"supervisor_worker","objective":"Compile role docs without model fields.","workspace":"__WS__"},"leader":{"id":"leader","role":"leader","provider":"codex","model":null,"context_policy":{"keep_user_thread":true,"receive_worker_outputs":"business_messages_and_short_summaries","max_worker_result_tokens":2000}},"agents":[{"id":"implementer","role":"Implementation Engineer","provider":"codex","model":"gpt-5.5","auth_mode":"subscription","working_directory":"__WS__","system_prompt":{"inline":"Implement bounded tasks.","file":null},"dangerously_skip_permissions":false,"communication_mode":"leader_centric","preferred_for":["implementer","Implementation Engineer"],"avoid_for":[],"output_contract":{"format":"result_envelope_v1","required_fields":["task_id","status","summary","artifacts"]}}],"routing":{"default_assignee":"implementer","rules":[{"id":"route-implementer","match":{"assignee":["implementer"]},"assign_to":"implementer","priority":10}]},"communication":{"protocol":"mcp_inbox","topology":"leader_centered","worker_to_worker":true,"ack_timeout_sec":60,"result_format":"result_envelope_v1","message_store":{"sqlite":".team/runtime/team.db","mirror_files":".team/messages"}},"runtime":{"backend":"tmux","session_name":"team-default-model-team","auto_launch":true,"require_user_approval_before_launch":true,"max_active_agents":1,"startup_order":["implementer"],"fast":false,"tick_interval_sec":2,"push_min_interval_sec":60,"stuck_timeout_sec":300},"context":{"state_file":"team_state.md","artifact_dir":".team/artifacts","log_dir":".team/logs","summarization":{"worker_full_logs":"retain_outside_leader_context","state_update":"after_each_result"}},"tasks":[{"id":"task_initial","title":"Initial document-driven team task","type":"implementation","assignee":"implementer","deps":[],"acceptance":["Worker reports valid result_envelope_v1"],"status":"pending","requires_tools":["mcp_team"],"files":[],"risk":"low"}]}"#;
330
+ const BUILTIN_JSON: &str = r#"{"version":1,"team":{"name":"default-model-team","mode":"supervisor_worker","objective":"Compile role docs without model fields.","workspace":"__WS__"},"leader":{"id":"leader","role":"leader","provider":"codex","model":null,"context_policy":{"keep_user_thread":true,"receive_worker_outputs":"business_messages_and_short_summaries","max_worker_result_tokens":2000}},"agents":[{"id":"implementer","role":"Implementation Engineer","provider":"codex","model":null,"auth_mode":"subscription","working_directory":"__WS__","system_prompt":{"inline":"Implement bounded tasks.","file":null},"dangerously_skip_permissions":false,"communication_mode":"leader_centric","preferred_for":["implementer","Implementation Engineer"],"avoid_for":[],"output_contract":{"format":"result_envelope_v1","required_fields":["task_id","status","summary","artifacts"]}}],"routing":{"default_assignee":"implementer","rules":[{"id":"route-implementer","match":{"assignee":["implementer"]},"assign_to":"implementer","priority":10}]},"communication":{"protocol":"mcp_inbox","topology":"leader_centered","worker_to_worker":true,"ack_timeout_sec":60,"result_format":"result_envelope_v1","message_store":{"sqlite":".team/runtime/team.db","mirror_files":".team/messages"}},"runtime":{"backend":"tmux","session_name":"team-default-model-team","auto_launch":true,"require_user_approval_before_launch":true,"max_active_agents":1,"startup_order":["implementer"],"fast":false,"tick_interval_sec":2,"push_min_interval_sec":60,"stuck_timeout_sec":300},"context":{"state_file":"team_state.md","artifact_dir":".team/artifacts","log_dir":".team/logs","summarization":{"worker_full_logs":"retain_outside_leader_context","state_update":"after_each_result"}},"tasks":[{"id":"task_initial","title":"Initial document-driven team task","type":"implementation","assignee":"implementer","deps":[],"acceptance":["Worker reports valid result_envelope_v1"],"status":"pending","requires_tools":["mcp_team"],"files":[],"risk":"low"}]}"#;
333
331
 
334
332
  #[test]
335
- fn compile_subscription_without_model_uses_builtin_provider_default() {
333
+ fn compile_subscription_without_model_keeps_provider_native_default() {
336
334
  let team = build_team(BUILTIN_TEAM, &[("implementer.md", BUILTIN_ROLE)], &[]);
337
335
  let spec = compile_team(&team).unwrap();
338
336
  assert_eq!(templated_compact_json(&spec), BUILTIN_JSON);
@@ -504,38 +502,32 @@ fn build_layout(parent: &str, leaf: &str, team_md: Option<&str>, agents: AgentsD
504
502
  // ── A1 model resolution ──
505
503
 
506
504
  #[test]
507
- fn fix_a1_provider_models_precede_default_model() {
508
- // precedence: provider_models[provider] BEFORE default_model (current swaps them).
505
+ fn team_model_defaults_do_not_fill_role_model() {
506
+ // TEAM default_model and provider_models are not implicit role settings.
509
507
  let tm = "---\nname: T\nprovider: codex\ndefault_model: team-y\nprovider_models:\n codex: pm-z\n---\nx\n";
510
508
  let team = build_team(tm, &[("w.md", &role_nomodel("codex"))], &[]);
511
509
  let spec = compile_team(&team).unwrap();
512
- assert_eq!(
513
- agent0(&spec).get("model").and_then(Value::as_str),
514
- Some("pm-z")
515
- );
510
+ assert_eq!(agent0(&spec).get("model"), Some(&Value::Null));
516
511
  }
517
512
 
518
513
  #[test]
519
- fn fix_a1_claude_aliases_to_claude_code_provider_models() {
520
- // TWO-WAY alias: provider `claude` consumes `provider_models[claude_code]`.
514
+ fn team_provider_model_aliases_do_not_fill_role_model() {
515
+ // TEAM provider-model aliases do not affect an omitted role model.
521
516
  let tm = "---\nname: T\nprovider: codex\nprovider_models:\n claude_code: cc-v\n---\nx\n";
522
517
  let team = build_team(tm, &[("w.md", &role_nomodel("claude"))], &[]);
523
518
  let spec = compile_team(&team).unwrap();
524
- assert_eq!(
525
- agent0(&spec).get("model").and_then(Value::as_str),
526
- Some("cc-v")
527
- );
519
+ assert_eq!(agent0(&spec).get("model"), Some(&Value::Null));
528
520
  }
529
521
 
530
522
  #[test]
531
- fn fix_a1_builtin_claude_default_is_sonnet_4_6() {
532
- // DEFAULT_PROVIDER_MODELS: claude / claude_code → "claude-sonnet-4-6" (not -4-5).
523
+ fn claude_role_without_model_keeps_provider_native_default() {
524
+ // A missing explicit model stays null for both Claude provider names.
533
525
  for prov in ["claude", "claude_code"] {
534
526
  let team = build_team(TM_CODEX, &[("w.md", &role_nomodel(prov))], &[]);
535
527
  let spec = compile_team(&team).unwrap();
536
528
  assert_eq!(
537
- agent0(&spec).get("model").and_then(Value::as_str),
538
- Some("claude-sonnet-4-6"),
529
+ agent0(&spec).get("model"),
530
+ Some(&Value::Null),
539
531
  "provider {prov}"
540
532
  );
541
533
  }