@team-agent/installer 0.5.101 → 0.5.103

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 (49) 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 +1 -1
  6. package/crates/team-agent/src/cli/tests/main_preserved.rs +0 -74
  7. package/crates/team-agent/src/compiler/tests.rs +20 -23
  8. package/crates/team-agent/src/compiler.rs +49 -65
  9. package/crates/team-agent/src/coordinator/tests/s3_bounded.rs +16 -48
  10. package/crates/team-agent/src/leader/start.rs +1 -14
  11. package/crates/team-agent/src/lifecycle/launch/pi_mcp.rs +37 -161
  12. package/crates/team-agent/src/lifecycle/launch/spawn.rs +1 -1
  13. package/crates/team-agent/src/lifecycle/restart/common.rs +1 -1
  14. package/crates/team-agent/src/lifecycle/tests/host_cotenant_death_p0_contract.rs +0 -28
  15. package/crates/team-agent/src/lifecycle/tests/pi_compiler_red.rs +11 -9
  16. package/crates/team-agent/src/lifecycle/tests/pi_dual_entry_red.rs +8 -401
  17. package/crates/team-agent/src/lifecycle/tests/pi_executable_mcp_red.rs +2 -2
  18. package/crates/team-agent/src/lifecycle/worker_command_context.rs +22 -8
  19. package/crates/team-agent/src/mcp_server/helpers.rs +0 -16
  20. package/crates/team-agent/src/mcp_server/lifecycle_tools/mod.rs +2 -7
  21. package/crates/team-agent/src/mcp_server/lifecycle_tools/state_status.rs +1 -127
  22. package/crates/team-agent/src/mcp_server/mod.rs +7 -12
  23. package/crates/team-agent/src/mcp_server/tests/scoped.rs +1 -132
  24. package/crates/team-agent/src/mcp_server/tests/send.rs +1 -93
  25. package/crates/team-agent/src/mcp_server/tests/tools.rs +0 -61
  26. package/crates/team-agent/src/mcp_server/tests/wire.rs +6 -51
  27. package/crates/team-agent/src/mcp_server/tests.rs +4 -200
  28. package/crates/team-agent/src/mcp_server/tools.rs +5 -396
  29. package/crates/team-agent/src/mcp_server/types.rs +3 -36
  30. package/crates/team-agent/src/mcp_server/wire.rs +2 -223
  31. package/crates/team-agent/src/messaging/leader_receiver.rs +1 -1
  32. package/crates/team-agent/src/messaging/mod.rs +2 -2
  33. package/crates/team-agent/src/messaging/results.rs +11 -42
  34. package/crates/team-agent/src/messaging/scheduler.rs +2 -205
  35. package/crates/team-agent/src/messaging/tests/basic.rs +0 -31
  36. package/crates/team-agent/src/messaging/tests/main_preserved.rs +0 -73
  37. package/crates/team-agent/src/messaging/tests/runtime.rs +0 -26
  38. package/crates/team-agent/src/messaging/types.rs +0 -20
  39. package/crates/team-agent/src/model/spec.rs +14 -18
  40. package/crates/team-agent/src/provider/mod.rs +1 -0
  41. package/crates/team-agent/src/provider/model_catalog.rs +650 -0
  42. package/crates/team-agent/src/state/repository/tests.rs +1 -16
  43. package/crates/team-agent/src/state/repository.rs +1 -33
  44. package/package.json +4 -4
  45. package/skills/team-agent/SKILL.md +5 -5
  46. package/skills/team-agent/command-coverage.json +7 -0
  47. package/skills/team-agent/references/team-agent-operator.md +24 -116
  48. package/skills/team-agent/references/team-in-team.md +4 -19
  49. package/crates/team-agent/src/mcp_server/lifecycle_tools/agent_ops.rs +0 -484
package/Cargo.lock CHANGED
@@ -581,7 +581,7 @@ dependencies = [
581
581
 
582
582
  [[package]]
583
583
  name = "team-agent"
584
- version = "0.5.101"
584
+ version = "0.5.103"
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.101"
12
+ version = "0.5.103"
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 },
@@ -414,31 +414,6 @@ fn valid_result_envelope() -> serde_json::Value {
414
414
  "next_actions": []
415
415
  })
416
416
  }
417
- fn seed_collect_state(ws: &std::path::Path) {
418
- seed_team_spec(ws);
419
- crate::state::persist::save_runtime_state(
420
- ws,
421
- &json!({
422
- "agents": {"fake_impl": {"status": "idle"}},
423
- "tasks": [{
424
- "id": "task_impl",
425
- "title": "Fake implementation",
426
- "type": "implementation",
427
- "assignee": "fake_impl",
428
- "deps": [],
429
- "acceptance": ["fake result collected"],
430
- "status": "pending",
431
- "requires_tools": [],
432
- "files": [],
433
- "risk": "low"
434
- }],
435
- "session_name": Value::Null,
436
- "active_team_key": Value::Null,
437
- "spec_path": ws.join("team.spec.yaml").to_string_lossy()
438
- }),
439
- )
440
- .unwrap();
441
- }
442
417
  fn seed_uncollected_result(ws: &std::path::Path, result_id: &str) {
443
418
  let store = crate::message_store::MessageStore::open(ws).unwrap();
444
419
  let conn = crate::db::schema::open_db(store.db_path()).unwrap();
@@ -450,18 +425,6 @@ fn seed_uncollected_result(ws: &std::path::Path, result_id: &str) {
450
425
  )
451
426
  .unwrap();
452
427
  }
453
- fn read_state(ws: &std::path::Path) -> serde_json::Value {
454
- serde_json::from_str(
455
- &std::fs::read_to_string(crate::state::persist::runtime_state_path(ws)).unwrap(),
456
- )
457
- .unwrap()
458
- }
459
- fn read_events(ws: &std::path::Path) -> Vec<serde_json::Value> {
460
- crate::event_log::EventLog::new(ws).tail(50).unwrap()
461
- }
462
- fn seeded_team_key(ws: &std::path::Path) -> String {
463
- ws.file_name().unwrap().to_string_lossy().to_string()
464
- }
465
428
  fn json_output(result: CmdResult) -> serde_json::Value {
466
429
  match result.output {
467
430
  CmdOutput::Json(v) => v,
@@ -590,40 +553,3 @@ fn remove_agent_from_spec_refusal_is_not_success_envelope() {
590
553
  "refused remove-agent must not delete the spec-defined agent"
591
554
  );
592
555
  }
593
- #[test]
594
- fn stuck_cancel_persists_suppression_and_stuck_list_reads_state() {
595
- let ws = tmp_workspace();
596
- seed_collect_state(&ws);
597
- let out = messaging::stuck_cancel(&ws, "fake_impl", None, "leader").unwrap();
598
- let team_key = seeded_team_key(&ws);
599
- assert_eq!(out["ok"], json!(true));
600
- assert_eq!(
601
- out["alert_types"],
602
- json!(["cross_worker_deadlock", "idle_fallback", "stuck"])
603
- );
604
- assert!(
605
- out["suppressed"]["idle_fallback"]["snapshot"]["assigned_task_ids"]
606
- .as_array()
607
- .unwrap()
608
- .contains(&json!("task_impl"))
609
- );
610
- let state = read_state(&ws);
611
- assert_eq!(
612
- state["coordinator"]["suppressed_idle_alerts"][&team_key]["fake_impl"]["idle_fallback"]
613
- ["suppressed_by"],
614
- json!("leader")
615
- );
616
- assert!(
617
- read_events(&ws)
618
- .iter()
619
- .any(|e| e["event"] == json!("coordinator.idle_alert_suppressed")
620
- && e["agent_id"] == json!("fake_impl")),
621
- "stuck_cancel must write coordinator.idle_alert_suppressed"
622
- );
623
- let listed = messaging::stuck_list(&ws).unwrap();
624
- assert_eq!(
625
- listed["suppressed_idle_alerts"]["fake_impl"]["stuck"]["suppressed_by"],
626
- json!("leader"),
627
- "stuck-list must read the persisted state mirror, not return a hard-coded empty list"
628
- );
629
- }
@@ -163,25 +163,21 @@ fn front_matter_body_lstrip_strips_only_newlines() {
163
163
  }
164
164
 
165
165
  #[test]
166
- fn front_matter_unterminated_errors() {
167
- let p = write_tmp("unterminated.md", "---\nname: x\n");
168
- let err = read_front_matter(&p).unwrap_err();
169
- assert!(
170
- err.to_string().contains("unterminated front matter"),
171
- "got: {err}"
172
- );
166
+ fn front_matter_unterminated_falls_back_to_plain_text() {
167
+ let text = "---\nname: x\n";
168
+ let p = write_tmp("unterminated.md", text);
169
+ let (meta, body) = read_front_matter(&p).expect("unterminated front matter is plain text");
170
+ assert_eq!(meta, Value::Map(Vec::new()));
171
+ assert_eq!(body, text);
173
172
  }
174
173
 
175
174
  #[test]
176
- fn front_matter_non_object_errors() {
177
- // A YAML list in the block → "front matter must be a YAML object" (compiler.py:183-184).
178
- let p = write_tmp("list.md", "---\n- a\n- b\n---\nbody\n");
179
- let err = read_front_matter(&p).unwrap_err();
180
- assert!(
181
- err.to_string()
182
- .contains("front matter must be a YAML object"),
183
- "got: {err}"
184
- );
175
+ fn front_matter_non_object_falls_back_to_plain_text() {
176
+ let text = "---\n- a\n- b\n---\nbody\n";
177
+ let p = write_tmp("list.md", text);
178
+ let (meta, body) = read_front_matter(&p).expect("non-object front matter is plain text");
179
+ assert_eq!(meta, Value::Map(Vec::new()));
180
+ assert_eq!(body, text);
185
181
  }
186
182
 
187
183
  // ───────────────────────────── compile_team: full dict parity ─────────────────────────────
@@ -395,7 +391,7 @@ const ROLE_MISSING_PROVIDER: &str = "\
395
391
  ---
396
392
  name: implementer
397
393
  role: Implementation Engineer
398
- model: gpt-5.5
394
+ model: team-agent/qwen3.8-27b
399
395
  auth_mode: subscription
400
396
  dangerously_skip_permissions: false
401
397
  tools:
@@ -406,13 +402,14 @@ Implement bounded tasks.
406
402
  ";
407
403
 
408
404
  #[test]
409
- fn compile_missing_required_provider_field_errors() {
405
+ fn compile_missing_provider_defaults_to_pi() {
410
406
  let team = build_team(TEAM_BASE, &[("implementer.md", ROLE_MISSING_PROVIDER)], &[]);
411
- let err = compile_team(&team).unwrap_err();
412
- assert!(
413
- err.to_string()
414
- .contains("missing front matter field provider"),
415
- "got: {err}"
407
+ let spec = compile_team(&team).expect("an omitted provider defaults to Pi");
408
+ let agent = agent0(&spec);
409
+ assert_eq!(agent.get("provider"), Some(&Value::Str("pi".to_string())));
410
+ assert_eq!(
411
+ agent.get("model"),
412
+ Some(&Value::Str("team-agent/qwen3.8-27b".to_string()))
416
413
  );
417
414
  }
418
415