@team-agent/installer 0.5.103 → 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 (27) hide show
  1. package/Cargo.lock +1 -1
  2. package/Cargo.toml +1 -1
  3. package/crates/team-agent/src/cli/spec.rs +1 -1
  4. package/crates/team-agent/src/compiler/tests.rs +19 -27
  5. package/crates/team-agent/src/compiler.rs +11 -105
  6. package/crates/team-agent/src/lifecycle/launch/add_agent_state.rs +4 -2
  7. package/crates/team-agent/src/lifecycle/launch/clone_agent.rs +2 -5
  8. package/crates/team-agent/src/lifecycle/launch/identity.rs +9 -13
  9. package/crates/team-agent/src/lifecycle/launch/role_source.rs +1 -25
  10. package/crates/team-agent/src/lifecycle/launch/spawn.rs +1 -0
  11. package/crates/team-agent/src/lifecycle/profile_launch.rs +0 -12
  12. package/crates/team-agent/src/lifecycle/restart/avatar.rs +0 -1
  13. package/crates/team-agent/src/lifecycle/restart/common.rs +1 -1
  14. package/crates/team-agent/src/lifecycle/tests/cursor_require_explicit_model_red.rs +7 -19
  15. package/crates/team-agent/src/lifecycle/tests/grok_require_explicit_model_red.rs +8 -41
  16. package/crates/team-agent/src/lifecycle/tests/pi_compiler_red.rs +14 -8
  17. package/crates/team-agent/src/lifecycle/tests/pi_dual_entry_red.rs +8 -4
  18. package/crates/team-agent/src/lifecycle/worker_command_context.rs +4 -4
  19. package/crates/team-agent/src/model/enums.rs +0 -9
  20. package/crates/team-agent/src/model/spec.rs +2 -2
  21. package/crates/team-agent/src/provider/adapters/cursor_agent.rs +7 -8
  22. package/crates/team-agent/src/provider/adapters/grok.rs +5 -18
  23. package/crates/team-agent/src/provider/tests/adapter.rs +15 -24
  24. package/crates/team-agent/src/provider/wire.rs +2 -29
  25. package/package.json +4 -4
  26. package/skills/team-agent/SKILL.md +48 -33
  27. 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.103"
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.103"
12
+ version = "0.5.104"
13
13
  license = "AGPL-3.0"
14
14
  rust-version = "1.95"
15
15
 
@@ -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
  }
@@ -15,11 +15,8 @@
15
15
  //! comparing byte-for-byte to Python golden. The absolute `workspace` path (env-
16
16
  //! dependent) is templated to `__WS__` on both sides so every other byte is pinned.
17
17
  //!
18
- //! SCOPE (this wave): no-profile `subscription` role docs only. The `.env`
19
- //! profile machinery (`profiles/`, `_profile_model`/`load_profile`) and the
20
- //! `rust_core` inline-secret *detection* (`contains_inline_secret` / the secret-
21
- //! lint rejection test) are DEFERRED to a follow-on — the compile path here only
22
- //! needs `contains_inline_secret` to return `false` for clean (non-secret) input.
18
+ //! Profile references are carried as role metadata; profile files and secrets are
19
+ //! handled by lifecycle/profile_launch, not loaded or inspected by this compiler.
23
20
  //!
24
21
  //! §10: pure lib layer — no panic on malformed input; every parse/validate path
25
22
  //! returns `Result<_, ModelError>` (mirrors Python `ValidationError`).
@@ -31,10 +28,7 @@ use crate::communication_mode::CommunicationMode;
31
28
  use crate::model::enums::{Provider, ProviderEffort};
32
29
  use crate::model::yaml::Value;
33
30
  use crate::model::{paths, spec, yaml, ModelError};
34
- use crate::provider::wire::{
35
- builtin_provider_model as wire_builtin_provider_model, parse_canonical_provider,
36
- provider_model_keys,
37
- };
31
+ use crate::provider::wire::parse_canonical_provider;
38
32
 
39
33
  pub const IGNORED_OWNER_TEAM_ID_FIELD: &str = "owner_team_id";
40
34
 
@@ -494,18 +488,12 @@ fn compile_role_agent_with_mode(
494
488
  let provider = string_field(&meta, "provider")
495
489
  .filter(|provider| !provider.trim().is_empty())
496
490
  .unwrap_or_else(|| "pi".to_string());
497
- require_explicit_grok_role_model(&meta, role_path, &provider)?;
498
- require_explicit_cursor_role_model(&meta, role_path, &provider)?;
499
- validate_pi_role_fields(&meta, role_path, &provider)?;
500
491
  let is_pi = parse_canonical_provider(&provider) == Some(Provider::Pi);
501
- let model = if is_pi {
502
- string_field(&meta, "model")
503
- .filter(|value| !value.trim().is_empty())
504
- .map(Value::Str)
505
- .unwrap_or_else(|| Value::Str("openai-codex/gpt-6-luna".to_string()))
506
- } else {
507
- resolve_model(&meta, team_meta, &provider)
508
- };
492
+ validate_pi_role_fields(&meta, role_path, &provider)?;
493
+ let model = string_field(&meta, "model")
494
+ .filter(|value| !value.trim().is_empty())
495
+ .map(Value::Str)
496
+ .unwrap_or(Value::Null);
509
497
  let auth_mode = string_field(&meta, "auth_mode")
510
498
  .or_else(|| string_field(team_meta, "default_auth_mode"))
511
499
  .unwrap_or_else(|| "subscription".to_string());
@@ -557,15 +545,11 @@ fn compile_role_agent_with_mode(
557
545
  ]),
558
546
  ),
559
547
  ];
560
- if let Some(label) = string_field(&meta, "label") {
561
- agent_items.insert(1, ("label", Value::Str(label)));
562
- }
563
548
  if let Some(profile) = string_field(&meta, "profile") {
564
549
  agent_items.push(("profile", Value::Str(profile)));
565
550
  }
566
- // 0.4.x provider effort MVP step 3: resolve effort with role > team > none
567
- // (Pi uses only explicit role effort). Validate syntax and provider support
568
- // here so unsupported combinations fail at compile, not at runtime.
551
+ // Role effort wins; Pi uses only an explicit role value. Other providers may
552
+ // inherit TEAM provider_effort for Issue #238 compatibility.
569
553
  let role_effort = match string_field(&meta, "effort") {
570
554
  Some(raw) if !raw.trim().is_empty() => {
571
555
  let value = raw.trim();
@@ -577,7 +561,6 @@ fn compile_role_agent_with_mode(
577
561
  })?;
578
562
  Some(parsed)
579
563
  }
580
- _ if is_pi => Some(ProviderEffort::Max),
581
564
  _ => None,
582
565
  };
583
566
  let team_effort = match string_field(team_meta, "provider_effort") {
@@ -674,54 +657,6 @@ fn string_field(meta: &Value, key: &str) -> Option<String> {
674
657
  .map(ToString::to_string)
675
658
  }
676
659
 
677
- /// 缺 model 时框架会填内建默认(grok 上是 grok-4),席位因此静默拿到
678
- /// 与角色文件无关的模型和上下文窗口,而 argv 看起来完全正常。
679
- /// 角色文件必须自己写死;内建默认、team 级默认、CLI 全局默认都是隐式来源。
680
- fn require_explicit_grok_role_model(
681
- meta: &Value,
682
- path: &Path,
683
- provider: &str,
684
- ) -> Result<(), ModelError> {
685
- if parse_canonical_provider(provider) != Some(Provider::Grok) {
686
- return Ok(());
687
- }
688
- if string_field(meta, "model").is_some_and(|value| !value.trim().is_empty()) {
689
- return Ok(());
690
- }
691
- Err(ModelError::Validation(format!(
692
- "{}: missing front matter field model. \
693
- Without an explicit model the framework fills a built-in default (grok-4 for grok), \
694
- so the seat silently gets a model and context window that are not in the role file, \
695
- while argv still looks normal. The role file must name the model itself; \
696
- built-in defaults, team-level defaults, and the Grok CLI global default are all implicit sources. Example:\n\
697
- model: grok-4.6",
698
- path.display()
699
- )))
700
- }
701
-
702
- /// 缺 model 时框架曾填 builtin `sonnet-4-thinking`,本机 shim 还会剥掉该默认。
703
- /// 角色必须自己写死;内建默认、team 级默认、CLI 全局默认都是隐式来源。
704
- fn require_explicit_cursor_role_model(
705
- meta: &Value,
706
- path: &Path,
707
- provider: &str,
708
- ) -> Result<(), ModelError> {
709
- if parse_canonical_provider(provider) != Some(Provider::CursorAgent) {
710
- return Ok(());
711
- }
712
- if string_field(meta, "model").is_some_and(|value| !value.trim().is_empty()) {
713
- return Ok(());
714
- }
715
- Err(ModelError::Validation(format!(
716
- "{}: missing front matter field model. \
717
- Without an explicit model the framework fills a built-in default (sonnet-4-thinking for cursor_agent), \
718
- so the seat silently gets a model that is not in the role file, while argv still looks normal. \
719
- The role file must name the model itself; built-in defaults, team-level defaults, and the Cursor CLI default are all implicit sources. Example:\n\
720
- model: sonnet-4-thinking",
721
- path.display()
722
- )))
723
- }
724
-
725
660
  fn validate_pi_role_fields(meta: &Value, path: &Path, provider: &str) -> Result<(), ModelError> {
726
661
  if parse_canonical_provider(provider) != Some(Provider::Pi) {
727
662
  return Ok(());
@@ -746,7 +681,7 @@ fn validate_pi_role_fields(meta: &Value, path: &Path, provider: &str) -> Result<
746
681
  fn optional_dangerously_skip_permissions(meta: &Value) -> bool {
747
682
  match meta.get("dangerously_skip_permissions") {
748
683
  Some(Value::Bool(value)) => *value,
749
- _ => true,
684
+ _ => false,
750
685
  }
751
686
  }
752
687
 
@@ -830,35 +765,6 @@ fn py_int_value(value: &Value) -> Option<i64> {
830
765
  }
831
766
  }
832
767
 
833
- fn resolve_model(role_meta: &Value, team_meta: &Value, provider: &str) -> Value {
834
- if let Some(model) = string_field(role_meta, "model") {
835
- return Value::Str(model);
836
- }
837
- if let Some(model) =
838
- provider_model(team_meta, provider).or_else(|| string_field(team_meta, "default_model"))
839
- {
840
- return Value::Str(model);
841
- }
842
- if role_meta.get("profile").is_some() {
843
- return Value::Null;
844
- }
845
- builtin_provider_model(provider)
846
- .map(|m| Value::Str(m.to_string()))
847
- .unwrap_or(Value::Null)
848
- }
849
-
850
- fn provider_model(team_meta: &Value, provider: &str) -> Option<String> {
851
- let models = team_meta.get("provider_models")?;
852
- let provider = parse_canonical_provider(provider)?;
853
- provider_model_keys(provider)
854
- .iter()
855
- .find_map(|key| string_field(models, key))
856
- }
857
-
858
- fn builtin_provider_model(provider: &str) -> Option<&'static str> {
859
- parse_canonical_provider(provider).and_then(wire_builtin_provider_model)
860
- }
861
-
862
768
  fn non_empty_trimmed(text: &str) -> Option<String> {
863
769
  let trimmed = text.trim();
864
770
  if trimmed.is_empty() {
@@ -390,7 +390,8 @@ fn starting_agent_entry(
390
390
  let provider = meta
391
391
  .get("provider")
392
392
  .and_then(Value::as_str)
393
- .unwrap_or("codex");
393
+ .filter(|provider| !provider.trim().is_empty())
394
+ .unwrap_or("pi");
394
395
  let auth_mode = meta
395
396
  .get("auth_mode")
396
397
  .and_then(Value::as_str)
@@ -438,8 +439,9 @@ fn starting_agent_entry(
438
439
  let meta_provider = meta
439
440
  .get("provider")
440
441
  .and_then(Value::as_str)
442
+ .filter(|provider| !provider.trim().is_empty())
441
443
  .and_then(crate::provider::wire::parse_provider)
442
- .unwrap_or(Provider::Codex);
444
+ .unwrap_or(Provider::Pi);
443
445
  persist_effective_approval_policy_from_yaml_agent(obj, meta, meta_provider);
444
446
  }
445
447
  entry
@@ -59,12 +59,9 @@ pub fn clone_agent(
59
59
  source_agent_id,
60
60
  as_agent_id,
61
61
  label,
62
- None,
63
62
  )?;
64
- // The materialized role carries the source role's DECLARED tools verbatim —
65
- // a clone must preserve the source seat's full tools set (add-agent does
66
- // the same; no leader-ceiling clamp). See role_source.rs for the removed
67
- // clamp_materialized_role_to_leader.
63
+ // Materialization preserves the role body and legacy metadata; the compiler
64
+ // only maps supported fields, so ignored header keys remain behaviorally inert.
68
65
  let added = add_agent(
69
66
  &selected.run_workspace,
70
67
  as_agent_id,
@@ -168,25 +168,21 @@ pub(crate) fn provider_effort_event_payload(
168
168
  }
169
169
 
170
170
  /// ---
171
- /// purpose: 对 claude 系 provider 兜底确保 CLAUDE_EFFORT 进入待清除环境变量表
171
+ /// purpose: 仅在存在显式 effort override 时清除继承的 CLAUDE_EFFORT
172
172
  /// params:
173
173
  /// base: 已有的待清除变量名列表
174
- /// returns: 非 claude 系原样返回;claude 系在缺失时补上 CLAUDE_EFFORT
175
- /// ---
176
- /// 0.4.x provider effort MVP step 9: defensive guarantee that `CLAUDE_EFFORT`
177
- /// is unset in the Claude/ClaudeCode worker spawn env. As of the
178
- /// `profile_launch::provider_env_unsets` update, the base list already
179
- /// includes `CLAUDE_EFFORT` for Claude — so this function is idempotent
180
- /// (returns input unchanged). Kept as a belt-and-braces guard so a future
181
- /// refactor that bypasses provider_env_unsets cannot silently drop the
182
- /// scrub. The structural win is in `tmux_backend::shell_command` which now
183
- /// filters env exports by env_unset (preventing inherited values from
184
- /// re-introducing keys we just unset).
174
+ /// has_effort_override: 本次启动是否实际施加 effort
175
+ /// returns: 无显式 override 或非 claude 系时原样返回
176
+ /// ---
177
+ /// Preserve provider-native `CLAUDE_EFFORT` unless a supported explicit role
178
+ /// effort is applied; in that case prevent the inherited environment from
179
+ /// overriding the requested value.
185
180
  pub(crate) fn extend_worker_env_unset_for_effort(
186
181
  base: Vec<String>,
187
182
  provider: Provider,
183
+ has_effort_override: bool,
188
184
  ) -> Vec<String> {
189
- if !matches!(provider, Provider::Claude | Provider::ClaudeCode) {
185
+ if !has_effort_override || !matches!(provider, Provider::Claude | Provider::ClaudeCode) {
190
186
  return base;
191
187
  }
192
188
  let mut out = base;
@@ -132,9 +132,8 @@ impl Drop for MaterializedRole {
132
132
  /// state: 用于找源席的 dynamic_role_file,找不到时退到 team 目录下的同名 md
133
133
  /// as_agent_id: 新席位名,写进 front matter 的 name
134
134
  /// label: 非空时覆盖 front matter 的 role
135
- /// agent_label: 非空时写入 front matter 的 label
136
135
  /// returns: 物化结果,未调用 keep 时 Drop 会删掉该文件
137
- /// errors: 源文件缺失、未声明 name 或声明与源席不符时返回 Compile;目标已存在返回 RequirementUnmet;建目录或写盘失败返回 StatePersist
136
+ /// errors: 源文件缺失时返回 Compile;目标已存在返回 RequirementUnmet;建目录或写盘失败返回 StatePersist
138
137
  /// ---
139
138
  pub(crate) fn materialize_latest_role(
140
139
  run_workspace: &Path,
@@ -143,27 +142,10 @@ pub(crate) fn materialize_latest_role(
143
142
  source_agent_id: &AgentId,
144
143
  as_agent_id: &AgentId,
145
144
  label: Option<&str>,
146
- agent_label: Option<&str>,
147
145
  ) -> Result<MaterializedRole, LifecycleError> {
148
146
  let source_path = resolve_role_source(run_workspace, team_dir, state, source_agent_id)?;
149
147
  let (mut meta, body) = crate::compiler::read_front_matter(&source_path)
150
148
  .map_err(|error| LifecycleError::Compile(error.to_string()))?;
151
- let declared = meta
152
- .get("name")
153
- .and_then(Value::as_str)
154
- .filter(|name| !name.is_empty())
155
- .ok_or_else(|| {
156
- LifecycleError::Compile(format!(
157
- "source role file does not declare name: {}",
158
- source_path.display()
159
- ))
160
- })?;
161
- if declared != source_agent_id.as_str() {
162
- return Err(LifecycleError::Compile(format!(
163
- "source role file declares name '{}' but source agent is '{}'",
164
- declared, source_agent_id
165
- )));
166
- }
167
149
  set_yaml_map_value(
168
150
  &mut meta,
169
151
  "name",
@@ -175,12 +157,6 @@ pub(crate) fn materialize_latest_role(
175
157
  {
176
158
  set_yaml_map_value(&mut meta, "role", Value::Str(label.to_string()))?;
177
159
  }
178
- if let Some(agent_label) = agent_label
179
- .map(strip_label_quotes)
180
- .filter(|value| !value.is_empty())
181
- {
182
- set_yaml_map_value(&mut meta, "label", Value::Str(agent_label.to_string()))?;
183
- }
184
160
  if let Some(role) = meta.get("role").and_then(Value::as_str).map(str::to_string) {
185
161
  let role_without_quotes = strip_label_quotes(&role);
186
162
  if !role_without_quotes.is_empty() && role_without_quotes != role {
@@ -255,6 +255,7 @@ pub(super) fn spawn_agents(
255
255
  extend_worker_env_unset_for_effort(
256
256
  profile_launch.env_unset.iter().cloned().collect(),
257
257
  provider,
258
+ agent_effort.is_some(),
258
259
  ),
259
260
  );
260
261
  // The provider is the final typed value after profile overlays. Keep it
@@ -578,18 +578,6 @@ pub(crate) fn provider_env_unsets(provider: Provider, auth_mode: AuthMode) -> BT
578
578
  unsets.insert("CLAUDE_CODE_ENTRYPOINT".to_string());
579
579
  unsets.insert("CLAUDE_CODE_EXECPATH".to_string());
580
580
  unsets.insert("CLAUDECODE".to_string());
581
- // 0.4.x provider effort MVP step 9: CLAUDE_EFFORT carried from the
582
- // launching shell would silently override the framework's effort
583
- // decision (or, when no spec effort is configured, override the
584
- // documented "provider default"). Scrubbing here is the single
585
- // source — `extend_worker_env_unset_for_effort` is a thin alias
586
- // that calls into this list. Applies to BOTH worker spawn AND
587
- // leader spawn (leader_env_unset_for_provider also reads this).
588
- // MVP scope: leader-effort is not configured yet, but if a
589
- // launching shell already carries CLAUDE_EFFORT, the leader
590
- // inheriting it would conflict with future leader-effort
591
- // semantics. Scrub uniformly for predictability.
592
- unsets.insert("CLAUDE_EFFORT".to_string());
593
581
  if auth_mode == AuthMode::CompatibleApi {
594
582
  unsets.insert("ANTHROPIC_API_KEY".to_string());
595
583
  }
@@ -315,7 +315,6 @@ pub(crate) fn fork_pi_new_seat_locked(
315
315
  source_agent_id,
316
316
  target_agent_id,
317
317
  label,
318
- Some(target_agent_id.as_str()),
319
318
  )?;
320
319
  role = Some(target_role);
321
320
  let target_role_path = role
@@ -510,6 +510,7 @@ pub(super) fn spawn_agent_window(
510
510
  crate::lifecycle::launch::extend_worker_env_unset_for_effort(
511
511
  profile_launch.env_unset.iter().cloned().collect(),
512
512
  provider,
513
+ restart_effort.is_some(),
513
514
  ),
514
515
  );
515
516
  // Keep the launcher-known typed provider independent from inherited
@@ -748,7 +749,6 @@ fn merge_command_context_fields(
748
749
  "auth_mode",
749
750
  "effort",
750
751
  "profile",
751
- "permission_mode",
752
752
  // 0.5.66 bypass 单源:rehydrate 同步合入,restart 的 safety 构造读到新值。
753
753
  "dangerously_skip_permissions",
754
754
  ] {
@@ -31,35 +31,23 @@ use team_agent::transport::test_support::OfflineTransport;
31
31
 
32
32
  #[test]
33
33
  #[serial(env)]
34
- fn cursor_role_missing_model_refuses_to_start() {
34
+ fn cursor_role_missing_model_uses_provider_default() {
35
35
  let ws = tmp_dir("cursor-no-model");
36
36
  let team = write_role_team(&ws, "cursortm", "cursor_writer", None, None);
37
37
  let transport = OfflineTransport::new();
38
- let result = quick_start_with_transport_in_workspace(
38
+ quick_start_with_transport_in_workspace(
39
39
  &ws,
40
40
  &team,
41
41
  None,
42
42
  true,
43
43
  Some("cursortm"),
44
44
  &transport,
45
- );
46
- let err = match result {
47
- Ok(report) => panic!("cursor role without model must refuse; report={report:?}"),
48
- Err(error) => error.to_string(),
49
- };
50
- let lower = err.to_ascii_lowercase();
51
- assert!(
52
- lower.contains("model"),
53
- "error must name the missing field; err={err}"
54
- );
55
- assert!(
56
- lower.contains("built-in") || lower.contains("builtin") || err.contains("内建"),
57
- "error must name the built-in default; err={err}"
58
- );
45
+ )
46
+ .expect("Cursor may use its provider-native model default");
47
+ let argv = first_spawn_argv(&transport);
59
48
  assert!(
60
- transport.spawn_records().is_empty(),
61
- "refusal must happen before spawn; records={:?}",
62
- transport.spawn_records()
49
+ !argv.iter().any(|arg| arg == "--model"),
50
+ "an omitted model must not fabricate a --model argument: {argv:?}"
63
51
  );
64
52
  }
65
53