@team-agent/installer 0.5.69 → 0.5.71

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.
@@ -2522,10 +2522,10 @@ fn write_restart_agent_failed_event(
2522
2522
  "session_id": decision.session_id.as_ref().map(|session| session.as_str()),
2523
2523
  "phase": phase,
2524
2524
  "error": error,
2525
- "action": format!(
2526
- "inspect worker {} output, then restart that worker with `team-agent restart-agent {}` or rerun `team-agent restart --allow-fresh`",
2527
- decision.agent_id,
2528
- decision.agent_id
2525
+ "action": restart_failure_action(
2526
+ &decision.agent_id,
2527
+ phase,
2528
+ decision.session_id.as_ref(),
2529
2529
  ),
2530
2530
  "log": format!(
2531
2531
  ".team/logs/coordinator.log and .team/runtime/state.json agent={}",
@@ -3008,14 +3008,30 @@ fn restart_failure_next_actions(failed_agents: &[RestartFailedAgent]) -> Vec<Str
3008
3008
  failed_agents
3009
3009
  .iter()
3010
3010
  .map(|failure| {
3011
- format!(
3012
- "inspect worker {} output, then restart that worker with `team-agent restart-agent {}` or rerun `team-agent restart --allow-fresh`",
3013
- failure.agent_id, failure.agent_id
3011
+ restart_failure_action(
3012
+ &failure.agent_id,
3013
+ &failure.phase,
3014
+ failure.session_id.as_ref(),
3014
3015
  )
3015
3016
  })
3016
3017
  .collect()
3017
3018
  }
3018
3019
 
3020
+ pub(crate) fn restart_failure_action(
3021
+ agent_id: &AgentId,
3022
+ phase: &str,
3023
+ session_id: Option<&SessionId>,
3024
+ ) -> String {
3025
+ if phase == "spawn" && session_id.is_none() {
3026
+ return format!(
3027
+ "worker {agent_id} failed before a pane was created; inspect the reported provider error, then rerun `team-agent restart --allow-fresh`"
3028
+ );
3029
+ }
3030
+ format!(
3031
+ "inspect worker {agent_id} output, then reset that worker with `team-agent reset-agent {agent_id}` or rerun `team-agent restart --allow-fresh`"
3032
+ )
3033
+ }
3034
+
3019
3035
  fn start_mode_wire(mode: StartMode) -> &'static str {
3020
3036
  match mode {
3021
3037
  StartMode::Resumed => "resumed",
@@ -92,7 +92,8 @@ pub(crate) use rebuild::restart_with_transport_with_session_convergence_deadline
92
92
  // timer + worker timing writer so `lifecycle::launch::launch_with_transport_in_workspace`
93
93
  // can emit the same instrumentation event family with `source="launch"`.
94
94
  pub(crate) use rebuild::{
95
- provider_wire_from_state, write_worker_spawn_timing_event, RestartPhaseTimer,
95
+ provider_wire_from_state, restart_failure_action, write_worker_spawn_timing_event,
96
+ RestartPhaseTimer,
96
97
  };
97
98
  pub use rebuild::{restart, restart_with_session_convergence_deadline};
98
99
  pub(crate) use rebuild::{
@@ -18,8 +18,8 @@ fn command_for_model(model: &str) -> Vec<String> {
18
18
  build_pi_command_argv(PiCommandRequest {
19
19
  executable: Path::new("/verified/pi"),
20
20
  extension: Path::new("/workspace/.team/runtime/pi/t1/w1/team-mcp.ts"),
21
- model,
22
- effort: ProviderEffort::Medium,
21
+ model: Some(model),
22
+ effort: Some(ProviderEffort::Medium),
23
23
  system_prompt: "worker contract",
24
24
  tool_categories: &["mcp_team"],
25
25
  session_dir: Path::new("/workspace/.team/runtime/pi/t1/w1/sessions"),
@@ -7,8 +7,8 @@ fn request(session: PiSessionSelector<'static>) -> PiCommandRequest<'static> {
7
7
  PiCommandRequest {
8
8
  executable: Path::new("/verified/pi"),
9
9
  extension: Path::new("/workspace/.team/runtime/pi/team-a/worker-a/team-mcp.ts"),
10
- model: "team-agent/qwen3.8-27b",
11
- effort: ProviderEffort::High,
10
+ model: Some("team-agent/qwen3.8-27b"),
11
+ effort: Some(ProviderEffort::High),
12
12
  system_prompt: "frozen worker prompt",
13
13
  tool_categories: &["mcp_team", "fs_read", "fs_write", "execute_bash"],
14
14
  session_dir: Path::new("/workspace/.team/runtime/pi/team-a/worker-a/sessions"),
@@ -18,7 +18,20 @@ fn request(session: PiSessionSelector<'static>) -> PiCommandRequest<'static> {
18
18
  }
19
19
 
20
20
  #[test]
21
- fn pi_command_is_exact_regular_tui_and_has_no_ambient_flags() {
21
+ fn pi_command_omits_unset_model_and_thinking_flags() {
22
+ let mut request = request(PiSessionSelector::Fresh {
23
+ session_id: "a2320d4e-7b3a-44ae-b3b8-d5de57033b01",
24
+ });
25
+ request.model = None;
26
+ request.effort = None;
27
+ let argv = build_pi_command_argv(request).expect("Pi provider defaults");
28
+
29
+ assert!(!argv.iter().any(|arg| arg == "--model"), "argv={argv:?}");
30
+ assert!(!argv.iter().any(|arg| arg == "--thinking"), "argv={argv:?}");
31
+ }
32
+
33
+ #[test]
34
+ fn pi_command_preserves_direct_pi_defaults_with_only_team_runtime_additions() {
22
35
  let fresh = build_pi_command_argv(request(PiSessionSelector::Fresh {
23
36
  session_id: "da8c3622-2378-4d05-a26c-e826a6ef6d63",
24
37
  }))
@@ -27,23 +40,14 @@ fn pi_command_is_exact_regular_tui_and_has_no_ambient_flags() {
27
40
  fresh,
28
41
  [
29
42
  "/verified/pi",
30
- "--no-extensions",
31
43
  "-e",
32
44
  "/workspace/.team/runtime/pi/team-a/worker-a/team-mcp.ts",
33
- "--no-approve",
34
- "--no-context-files",
35
- "--no-skills",
36
- "--no-prompt-templates",
37
- "--tui-mode",
38
- "regular",
39
45
  "--model",
40
46
  "team-agent/qwen3.8-27b",
41
47
  "--thinking",
42
48
  "high",
43
- "--system-prompt",
49
+ "--append-system-prompt",
44
50
  "frozen worker prompt",
45
- "--tools",
46
- "bash,edit,mcp,read,write",
47
51
  "--session-dir",
48
52
  "/workspace/.team/runtime/pi/team-a/worker-a/sessions",
49
53
  "--session-id",
@@ -88,6 +92,14 @@ fn pi_command_is_exact_regular_tui_and_has_no_ambient_flags() {
88
92
  "--force",
89
93
  "--trust",
90
94
  "--sandbox",
95
+ "--no-extensions",
96
+ "--no-approve",
97
+ "--no-context-files",
98
+ "--no-skills",
99
+ "--no-prompt-templates",
100
+ "--tui-mode",
101
+ "--tools",
102
+ "--system-prompt",
91
103
  ] {
92
104
  assert!(
93
105
  !argv.iter().any(|arg| arg == forbidden),
@@ -98,23 +110,26 @@ fn pi_command_is_exact_regular_tui_and_has_no_ambient_flags() {
98
110
  }
99
111
 
100
112
  #[test]
101
- fn pi_command_uses_no_context_no_extensions_no_approve() {
113
+ fn pi_command_keeps_direct_plugins_skills_context_auth_and_tools() {
102
114
  let argv = build_pi_command_argv(request(PiSessionSelector::Fresh {
103
115
  session_id: "76f682b1-ecc3-4586-850f-ab2e2bb04cb3",
104
116
  }))
105
117
  .expect("fresh Pi command");
106
118
 
107
- for required in [
119
+ for forbidden in [
108
120
  "--no-context-files",
109
121
  "--no-extensions",
110
122
  "--no-approve",
111
123
  "--no-skills",
112
124
  "--no-prompt-templates",
125
+ "--tools",
126
+ "--system-prompt",
127
+ "--tui-mode",
113
128
  ] {
114
129
  assert_eq!(
115
- argv.iter().filter(|arg| arg.as_str() == required).count(),
116
- 1,
117
- "required isolation flag must occur once: {required}; argv={argv:?}"
130
+ argv.iter().filter(|arg| arg.as_str() == forbidden).count(),
131
+ 0,
132
+ "TeamMate Pi must preserve the direct Pi default instead of adding {forbidden}; argv={argv:?}"
118
133
  );
119
134
  }
120
135
  assert!(argv.windows(2).any(|pair| {
@@ -124,5 +139,7 @@ fn pi_command_uses_no_context_no_extensions_no_approve() {
124
139
  ]
125
140
  }));
126
141
  assert_eq!(argv.iter().filter(|arg| arg.as_str() == "-e").count(), 1);
127
- assert!(!argv.iter().any(|arg| arg == "--approve"));
142
+ assert!(argv
143
+ .windows(2)
144
+ .any(|pair| pair == ["--append-system-prompt", "frozen worker prompt"]));
128
145
  }
@@ -14,6 +14,13 @@ const CATALOG: &[u8] = include_bytes!("fixtures/pi_list_models_g0.stdout.txt");
14
14
  static NEXT_ROLE: AtomicU32 = AtomicU32::new(0);
15
15
 
16
16
  fn compile_pi_role(front_matter: &str) -> Result<CompiledRole, ModelError> {
17
+ compile_pi_role_with_team(front_matter, &Value::Map(Vec::new()))
18
+ }
19
+
20
+ fn compile_pi_role_with_team(
21
+ front_matter: &str,
22
+ team_meta: &Value,
23
+ ) -> Result<CompiledRole, ModelError> {
17
24
  let seq = NEXT_ROLE.fetch_add(1, Ordering::Relaxed);
18
25
  let root = std::env::temp_dir().join(format!(
19
26
  "team-agent-pi-compiler-{}-{seq}",
@@ -26,7 +33,7 @@ fn compile_pi_role(front_matter: &str) -> Result<CompiledRole, ModelError> {
26
33
  format!("---\n{front_matter}\n---\nPi worker contract.\n"),
27
34
  )
28
35
  .expect("write role fixture");
29
- let result = compile_role_agent(&role, &Value::Map(Vec::new()), "/workspace");
36
+ let result = compile_role_agent(&role, team_meta, "/workspace");
30
37
  std::fs::remove_dir_all(root).expect("remove role fixture root");
31
38
  result
32
39
  }
@@ -48,8 +55,8 @@ fn command(tools: &[&str], effort: ProviderEffort, model: &str) -> Result<Vec<St
48
55
  build_pi_command_argv(PiCommandRequest {
49
56
  executable: Path::new("/verified/pi"),
50
57
  extension: Path::new("/workspace/.team/runtime/pi/t1/pi-worker/team-mcp.ts"),
51
- model,
52
- effort,
58
+ model: Some(model),
59
+ effort: Some(effort),
53
60
  system_prompt: "Pi worker contract.",
54
61
  tool_categories: tools,
55
62
  session_dir: Path::new("/workspace/.team/runtime/pi/t1/pi-worker/sessions"),
@@ -62,19 +69,15 @@ fn command(tools: &[&str], effort: ProviderEffort, model: &str) -> Result<Vec<St
62
69
  }
63
70
 
64
71
  #[test]
65
- fn pi_role_requires_qualified_model_explicit_effort_and_mcp_team() {
72
+ fn pi_role_requires_mcp_team_and_preserves_provider_defaults() {
66
73
  let positive = compile_pi_role(&valid_role_with(""));
67
74
  assert!(positive.is_ok(), "fully explicit Pi role must compile");
68
75
 
69
- let missing_model = valid_role_with("").replace("model: team-agent/qwen3.8-27b\n", "");
70
- let missing_effort = valid_role_with("").replace("effort: medium\n", "");
71
76
  let missing_mcp = valid_role_with("").replace(" - mcp_team\n", "");
72
77
  let unqualified =
73
78
  valid_role_with("").replace("model: team-agent/qwen3.8-27b", "model: qwen3.8-27b");
74
79
 
75
80
  for (label, role) in [
76
- ("model", missing_model),
77
- ("effort", missing_effort),
78
81
  ("mcp_team", missing_mcp),
79
82
  ("qualified exact model", unqualified),
80
83
  ] {
@@ -85,13 +88,45 @@ fn pi_role_requires_qualified_model_explicit_effort_and_mcp_team() {
85
88
  );
86
89
  }
87
90
 
91
+ let defaults = valid_role_with("")
92
+ .replace("model: team-agent/qwen3.8-27b\n", "")
93
+ .replace("effort: medium\n", "");
94
+ let team_meta = Value::Map(vec![
95
+ (
96
+ "provider_models".to_string(),
97
+ Value::Map(vec![(
98
+ "pi".to_string(),
99
+ Value::Str("team-default/forced-model".to_string()),
100
+ )]),
101
+ ),
102
+ (
103
+ "default_model".to_string(),
104
+ Value::Str("global-default/forced-model".to_string()),
105
+ ),
106
+ (
107
+ "provider_effort".to_string(),
108
+ Value::Str("high".to_string()),
109
+ ),
110
+ ]);
111
+ let compiled = compile_pi_role_with_team(&defaults, &team_meta)
112
+ .expect("Pi model and effort may use provider defaults");
113
+ assert_eq!(
114
+ compiled.agent.get("model"),
115
+ Some(&Value::Null),
116
+ "omitted Pi model must not inherit a synthesized framework default"
117
+ );
118
+ assert!(
119
+ compiled.agent.get("effort").is_none(),
120
+ "omitted Pi effort must not inherit a synthesized framework default"
121
+ );
122
+
88
123
  let models = parse_pi_list_models_table(CATALOG).expect("catalog fixture");
89
124
  assert!(select_exact_pi_model(&models, "team-agent/qwen3.8-27b").is_ok());
90
125
  assert!(select_exact_pi_model(&models, "foo/bar").is_err());
91
126
  }
92
127
 
93
128
  #[test]
94
- fn pi_role_requires_intrinsic_unrestricted_ack_true() {
129
+ fn pi_role_uses_standard_bypass_field_without_fabricating_argv() {
95
130
  let compiled = compile_pi_role(&valid_role_with("")).expect("true acknowledgement");
96
131
  assert_eq!(
97
132
  compiled.agent.get("dangerously_skip_permissions"),
@@ -122,10 +157,16 @@ fn pi_role_requires_intrinsic_unrestricted_ack_true() {
122
157
  "dangerously_skip_permissions: true",
123
158
  "dangerously_skip_permissions: false",
124
159
  );
125
- let error = compile_error(compile_pi_role(&false_ack));
126
- let text = error.to_string();
160
+ let compiled = compile_pi_role(&false_ack).expect("standard false bypass value");
161
+ assert_eq!(
162
+ compiled.agent.get("dangerously_skip_permissions"),
163
+ Some(&Value::Bool(false))
164
+ );
165
+
166
+ let missing_ack = valid_role_with("").replace("dangerously_skip_permissions: true\n", "");
167
+ let text = compile_error(compile_pi_role(&missing_ack)).to_string();
127
168
  assert!(
128
- text.contains("dangerously_skip_permissions") && text.contains("true"),
169
+ text.contains("missing front matter field dangerously_skip_permissions"),
129
170
  "got {text}"
130
171
  );
131
172
  }
@@ -153,7 +194,7 @@ fn pi_max_effort_is_supported_without_model_suffix() {
153
194
  }
154
195
 
155
196
  #[test]
156
- fn pi_tools_allowlist_keeps_mcp_and_rejects_unknown_categories() {
197
+ fn pi_role_tools_validate_team_capabilities_without_replacing_direct_pi_tools() {
157
198
  assert_eq!(pi_tool_mapping("mcp_team"), PiToolMapping::Mcp);
158
199
  assert_eq!(
159
200
  pi_tool_mapping("fs_read"),
@@ -194,14 +235,9 @@ fn pi_tools_allowlist_keeps_mcp_and_rejects_unknown_categories() {
194
235
  ProviderEffort::Medium,
195
236
  "team-agent/qwen3.8-27b",
196
237
  )
197
- .expect("known allowlist");
198
- let tools = argv
199
- .windows(2)
200
- .find(|pair| pair[0] == "--tools")
201
- .map(|pair| pair[1].as_str())
202
- .expect("--tools value");
203
- assert_eq!(
204
- tools, "bash,edit,find,grep,ls,mcp,read,write",
205
- "tools must be sorted and deduplicated"
238
+ .expect("known standard role tools");
239
+ assert!(
240
+ !argv.iter().any(|arg| arg == "--tools"),
241
+ "role tools validate Team Agent capabilities but must not replace direct Pi tools: {argv:?}"
206
242
  );
207
243
  }
@@ -224,12 +224,27 @@ fn pi_leader_and_teammate_body(hermetic: &HermeticTestEnv) {
224
224
  );
225
225
  let leader = parse_pi_leader_args(&leader_argv[2..])
226
226
  .expect("leader exact model and effort must compile into the shared plan input");
227
- assert_eq!(leader.model, "team-agent/qwen3.8-27b");
228
- assert_eq!(leader.effort, ProviderEffort::Max);
227
+ assert_eq!(leader.model.as_deref(), Some("team-agent/qwen3.8-27b"));
228
+ assert_eq!(leader.effort, Some(ProviderEffort::Max));
229
+
230
+ let defaults = parse_pi_leader_args(&[]).expect("leader provider defaults");
231
+ assert_eq!(defaults.model, None);
232
+ assert_eq!(defaults.effort, None);
233
+ assert_eq!(
234
+ parse_pi_leader_args(&["--thinking".to_string(), "medium".to_string()])
235
+ .expect("explicit thinking only")
236
+ .effort,
237
+ Some(ProviderEffort::Medium)
238
+ );
239
+ assert_eq!(
240
+ parse_pi_leader_args(&["--model".to_string(), "team-agent/qwen3.8-27b".to_string(),])
241
+ .expect("explicit model only")
242
+ .model
243
+ .as_deref(),
244
+ Some("team-agent/qwen3.8-27b")
245
+ );
229
246
 
230
247
  for invalid in [
231
- vec!["--thinking".to_string(), "medium".to_string()],
232
- vec!["--model".to_string(), "team-agent/qwen3.8-27b".to_string()],
233
248
  vec![
234
249
  "--model".to_string(),
235
250
  "qwen3.8-27b".to_string(),
@@ -247,7 +262,7 @@ fn pi_leader_and_teammate_body(hermetic: &HermeticTestEnv) {
247
262
  ] {
248
263
  assert!(
249
264
  parse_pi_leader_args(&invalid).is_err(),
250
- "leader input must refuse missing, ambiguous, or materializer-owned fields: {invalid:?}"
265
+ "leader input must refuse ambiguous or materializer-owned fields: {invalid:?}"
251
266
  );
252
267
  }
253
268
 
@@ -267,12 +282,15 @@ fn pi_leader_and_teammate_body(hermetic: &HermeticTestEnv) {
267
282
 
268
283
  std::fs::write(
269
284
  &role,
270
- "---\nname: worker-a\nrole: developer\nprovider: pi\nauth_mode: subscription\neffort: max\ntools:\n - mcp_team\ndangerously_skip_permissions: true\n---\nworker contract\n",
285
+ "---\nname: worker-a\nrole: developer\nprovider: pi\nauth_mode: subscription\ntools:\n - mcp_team\ndangerously_skip_permissions: true\n---\nworker contract\n",
271
286
  )
272
- .expect("write missing-model teammate role");
287
+ .expect("write provider-default teammate role");
288
+ let defaults = compile_role_agent(&role, &Value::Map(Vec::new()), "/workspace")
289
+ .expect("provider: pi teammate may use Pi model and effort defaults");
290
+ assert_eq!(defaults.agent.get("model"), Some(&Value::Null));
273
291
  assert!(
274
- compile_role_agent(&role, &Value::Map(Vec::new()), "/workspace").is_err(),
275
- "provider: pi teammate without an exact model must refuse"
292
+ defaults.agent.get("effort").is_none(),
293
+ "omitted Pi model and effort must remain absent"
276
294
  );
277
295
  std::fs::remove_dir_all(root).expect("remove role fixture");
278
296