@team-agent/installer 0.4.9 → 0.4.11

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.
@@ -128,3 +128,82 @@ fn read_and_remove(path: &std::path::Path) -> Vec<u8> {
128
128
  let _ = std::fs::remove_file(path);
129
129
  stdout
130
130
  }
131
+
132
+ /// 0.4.x Phase 1 (fg-pgrp): terminal foreground process-group probe.
133
+ ///
134
+ /// macOS probe confirmed (2026-06-29, .team/artifacts/fg-pgrp-probe.md):
135
+ /// `TIOCGPGRP` ioctl returns Errno 25 on tmux slave PTYs opened from a
136
+ /// non-controlling process. We use `ps -o tpgid,pgid -p <pid>` instead —
137
+ /// reliable on macOS AND Linux, no platform-specific cfg needed.
138
+ ///
139
+ /// Returns `Ok(Some((tpgid, pgid)))` on success, `Ok(None)` when the PID
140
+ /// is gone / ps returned no row / values unparseable. The caller maps
141
+ /// `None` to `WorkerRuntimeState::Unknown` (never to idle — Iron Law).
142
+ ///
143
+ /// `tpgid` = terminal foreground process group ID.
144
+ /// `pgid` = the agent root's own process group ID.
145
+ /// `tpgid != pgid` means a child process owns the foreground (BUSY signal).
146
+ ///
147
+ /// Bounded via [`run_bounded_command`] so a wedged ps cannot stall the
148
+ /// coordinator tick.
149
+ pub(crate) fn pane_foreground_and_root_pgrp(pane_pid: u32) -> io::Result<Option<(u32, u32)>> {
150
+ let mut command = Command::new("ps");
151
+ command.args(["-o", "tpgid=,pgid=", "-p", &pane_pid.to_string()]);
152
+ let output = bounded_command_output_with_probe(
153
+ &mut command,
154
+ "pane_foreground_and_root_pgrp",
155
+ Some(pane_pid),
156
+ )?;
157
+ if probe_timed_out() {
158
+ return Ok(None);
159
+ }
160
+ if !output.status.success() {
161
+ return Ok(None);
162
+ }
163
+ let text = String::from_utf8_lossy(&output.stdout);
164
+ let first_line = text.lines().next().unwrap_or("").trim();
165
+ if first_line.is_empty() {
166
+ return Ok(None);
167
+ }
168
+ let parts: Vec<&str> = first_line.split_whitespace().collect();
169
+ if parts.len() < 2 {
170
+ return Ok(None);
171
+ }
172
+ let tpgid = parts[0].parse::<i64>().ok();
173
+ let pgid = parts[1].parse::<i64>().ok();
174
+ match (tpgid, pgid) {
175
+ // ps prints `-1` for tpgid when there is no controlling terminal —
176
+ // treat as unknown rather than fabricating a u32.
177
+ (Some(t), Some(p)) if t > 0 && p > 0 => Ok(Some((t as u32, p as u32))),
178
+ _ => Ok(None),
179
+ }
180
+ }
181
+
182
+ #[cfg(test)]
183
+ mod fg_pgrp_tests {
184
+ use super::*;
185
+
186
+ #[test]
187
+ fn pane_foreground_and_root_pgrp_self_returns_some_or_none() {
188
+ // The current test process is not necessarily attached to a
189
+ // controlling terminal (CI runs detached), so we accept Some OR
190
+ // None. The contract is: never panic, return io::Result.
191
+ let pid = std::process::id();
192
+ match pane_foreground_and_root_pgrp(pid) {
193
+ Ok(Some((tpgid, pgid))) => {
194
+ assert!(tpgid > 0 && pgid > 0, "positive pgid/tpgid");
195
+ }
196
+ Ok(None) => {} // headless / no tty — acceptable
197
+ Err(e) => panic!("probe must not error in normal env: {e}"),
198
+ }
199
+ }
200
+
201
+ #[test]
202
+ fn pane_foreground_and_root_pgrp_missing_pid_returns_none() {
203
+ // PID 1 is init/launchd — exists but we read fields; non-existent
204
+ // PIDs return Ok(None) (ps exits non-zero).
205
+ let result =
206
+ pane_foreground_and_root_pgrp(0xFFFF_FFFE).expect("must not error on missing pid");
207
+ assert!(result.is_none(), "missing pid → None");
208
+ }
209
+ }
@@ -410,7 +410,7 @@ impl ProviderAdapter for BasicProviderAdapter {
410
410
  Provider::Claude | Provider::ClaudeCode => {
411
411
  Ok(claude_launch_command(self, auth_mode, mcp_config, system_prompt, model, tools)?)
412
412
  }
413
- Provider::Codex => Ok(codex_base_command(None, auth_mode, mcp_config, system_prompt, model, tools, None)),
413
+ Provider::Codex => Ok(codex_base_command(None, auth_mode, mcp_config, system_prompt, model, tools, None, None)),
414
414
  // §C1 worker argv 形态 + C-1/C-5/C-6 cr verdict:
415
415
  // copilot --no-color --no-auto-update [<dangerous|granular>] [--model]
416
416
  // --additional-mcp-config <inline json> --session-id <expected_uuid> -C <cwd>
@@ -469,6 +469,7 @@ impl ProviderAdapter for BasicProviderAdapter {
469
469
  model,
470
470
  ctx.tools,
471
471
  managed,
472
+ ctx.effort,
472
473
  )?;
473
474
  argv.push("--session-id".to_string());
474
475
  argv.push(expected.clone());
@@ -507,6 +508,7 @@ impl ProviderAdapter for BasicProviderAdapter {
507
508
  ctx.model,
508
509
  ctx.tools,
509
510
  ctx.profile_launch.map(|profile| &profile.command_overrides),
511
+ ctx.effort,
510
512
  ))),
511
513
  // §C1 + §C4 cr verdict — copilot plan 端预定 UUID + workspace `-C` 双保险:
512
514
  // * `--session-id <uuid>`(claude 同法,捕获免目录扫描,sqlite 仅校验)
@@ -638,13 +640,14 @@ impl ProviderAdapter for BasicProviderAdapter {
638
640
  model,
639
641
  tools,
640
642
  None,
643
+ None,
641
644
  );
642
645
  argv.push(session_id.as_str().to_string());
643
646
  Ok(argv)
644
647
  }
645
648
  Provider::Claude | Provider::ClaudeCode => {
646
649
  let mut argv =
647
- claude_base_command(self, auth_mode, mcp_config, system_prompt, model, tools, false)?;
650
+ claude_base_command(self, auth_mode, mcp_config, system_prompt, model, tools, false, None)?;
648
651
  argv.push("--resume".to_string());
649
652
  argv.push(session_id.as_str().to_string());
650
653
  Ok(argv)
@@ -686,6 +689,7 @@ impl ProviderAdapter for BasicProviderAdapter {
686
689
  model,
687
690
  ctx.tools,
688
691
  managed,
692
+ ctx.effort,
689
693
  )?;
690
694
  argv.push("--resume".to_string());
691
695
  argv.push(session_id.as_str().to_string());
@@ -725,6 +729,7 @@ impl ProviderAdapter for BasicProviderAdapter {
725
729
  ctx.model,
726
730
  ctx.tools,
727
731
  ctx.profile_launch.map(|profile| &profile.command_overrides),
732
+ ctx.effort,
728
733
  );
729
734
  argv.push(session_id.as_str().to_string());
730
735
  Ok(CommandPlan::argv_only(argv))
@@ -779,13 +784,14 @@ impl ProviderAdapter for BasicProviderAdapter {
779
784
  model,
780
785
  tools,
781
786
  None,
787
+ None,
782
788
  );
783
789
  argv.push(session_id.as_str().to_string());
784
790
  Ok(argv)
785
791
  }
786
792
  Provider::Claude | Provider::ClaudeCode => {
787
793
  let mut argv =
788
- claude_base_command(self, auth_mode, mcp_config, system_prompt, model, tools, false)?;
794
+ claude_base_command(self, auth_mode, mcp_config, system_prompt, model, tools, false, None)?;
789
795
  argv.push("--session-id".to_string());
790
796
  argv.push(next_session_token());
791
797
  argv.push("--resume".to_string());
@@ -837,6 +843,7 @@ impl ProviderAdapter for BasicProviderAdapter {
837
843
  model,
838
844
  ctx.tools,
839
845
  managed,
846
+ ctx.effort,
840
847
  )?;
841
848
  argv.push("--session-id".to_string());
842
849
  argv.push(expected.clone());
@@ -870,6 +877,7 @@ impl ProviderAdapter for BasicProviderAdapter {
870
877
  ctx.model,
871
878
  ctx.tools,
872
879
  ctx.profile_launch.map(|profile| &profile.command_overrides),
880
+ ctx.effort,
873
881
  );
874
882
  argv.push(session_id.as_str().to_string());
875
883
  Ok(CommandPlan::argv_only(argv))
@@ -34,6 +34,7 @@ pub(crate) fn claude_launch_command(
34
34
  model,
35
35
  tools,
36
36
  false,
37
+ None,
37
38
  )?;
38
39
  argv.push("--session-id".to_string());
39
40
  argv.push(next_session_token());
@@ -48,6 +49,9 @@ pub(crate) fn claude_base_command(
48
49
  model: Option<&str>,
49
50
  tools: &[&str],
50
51
  managed_mcp_config: bool,
52
+ // 0.4.x provider effort MVP step 5: when Some, inject `--effort <level>`
53
+ // immediately after the model (before prompt/MCP).
54
+ effort: Option<crate::model::enums::ProviderEffort>,
51
55
  ) -> Result<Vec<String>, ProviderError> {
52
56
  let mut argv = vec!["claude".to_string()];
53
57
  if claude_dangerous_auto_approve(tools) {
@@ -60,6 +64,10 @@ pub(crate) fn claude_base_command(
60
64
  argv.push("--model".to_string());
61
65
  argv.push(model.to_string());
62
66
  }
67
+ if let Some(effort) = effort {
68
+ argv.push("--effort".to_string());
69
+ argv.push(effort.as_str().to_string());
70
+ }
63
71
  if let Some(prompt) = system_prompt {
64
72
  argv.push("--append-system-prompt".to_string());
65
73
  argv.push(prompt.to_string());
@@ -17,6 +17,12 @@ pub(crate) fn codex_base_command(
17
17
  model: Option<&str>,
18
18
  tools: &[&str],
19
19
  overrides: Option<&ProviderCommandOverrides>,
20
+ // 0.4.x provider effort MVP step 6: when Some, inject
21
+ // `-c model_reasoning_effort=<level>` AFTER existing profile
22
+ // codex_config overrides — explicit launch effort wins over profile.
23
+ // Codex does not support `max`; the caller filters that case via
24
+ // `ProviderEffort::is_supported_by` before reaching this point.
25
+ effort: Option<crate::model::enums::ProviderEffort>,
20
26
  ) -> Vec<String> {
21
27
  let mut argv = vec!["codex".to_string()];
22
28
  if let Some(subcommand) = subcommand {
@@ -51,6 +57,10 @@ pub(crate) fn codex_base_command(
51
57
  argv.push(config.clone());
52
58
  }
53
59
  }
60
+ if let Some(effort) = effort {
61
+ argv.push("-c".to_string());
62
+ argv.push(format!("model_reasoning_effort={}", effort.as_str()));
63
+ }
54
64
  if let Some(prompt) = system_prompt {
55
65
  // codex.py:120 — escape order matters: backslash first, then quote, then newline.
56
66
  let escaped = prompt
@@ -347,6 +347,14 @@ pub struct ProviderCommandContext<'a> {
347
347
  /// no `--name` CLI flag (probe verdict 2026-06-22) and ignores this
348
348
  /// field. None = legacy callers (the field is purely additive).
349
349
  pub agent_id_hint: Option<&'a str>,
350
+ /// 0.4.x provider effort MVP step 4: reasoning effort level resolved
351
+ /// from role doc / TEAM.md / provider default. `None` means the
352
+ /// framework passes no effort flag (provider default).
353
+ /// - Claude / ClaudeCode: low|medium|high|xhigh|max → `--effort <level>`
354
+ /// - Codex: low|medium|high|xhigh → `-c model_reasoning_effort=<level>`
355
+ /// - Copilot / Gemini / Fake: ignored, warning event emitted at the
356
+ /// caller (lifecycle/launch.rs / lifecycle/restart) before construct.
357
+ pub effort: Option<crate::model::enums::ProviderEffort>,
350
358
  }
351
359
 
352
360
  #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
@@ -1321,6 +1321,8 @@ fn shell_command(
1321
1321
  env: &BTreeMap<String, String>,
1322
1322
  env_unset: &[String],
1323
1323
  ) -> String {
1324
+ let unset_set: std::collections::BTreeSet<&str> =
1325
+ env_unset.iter().map(String::as_str).collect();
1324
1326
  let mut parts = Vec::new();
1325
1327
  parts.push("cd".to_string());
1326
1328
  parts.push(shell_quote(&cwd.to_string_lossy()));
@@ -1333,7 +1335,16 @@ fn shell_command(
1333
1335
  parts.push(key.clone());
1334
1336
  parts.push("&&".to_string());
1335
1337
  }
1338
+ // 0.4.x ordering fix (env-leak symptom #3): KEY=val exports must NOT
1339
+ // re-introduce any key that was just unset. Filter env entries whose key
1340
+ // appears in env_unset so the unset wins on the final shell line. This
1341
+ // matters when inherited env (worker_spawn_env / apply_profile_launch_env)
1342
+ // contains the very keys we want to scrub (e.g. CLAUDE_EFFORT carried
1343
+ // forward from the launching shell into the env map).
1336
1344
  for (key, value) in env {
1345
+ if unset_set.contains(key.as_str()) {
1346
+ continue;
1347
+ }
1337
1348
  parts.push(format!("{key}={}", shell_quote(value)));
1338
1349
  }
1339
1350
  parts.push("exec".to_string());
@@ -1378,6 +1389,8 @@ pub fn leader_shell_wrapper_command(
1378
1389
  env_unset: &[String],
1379
1390
  provider_label: &str,
1380
1391
  ) -> String {
1392
+ let unset_set: std::collections::BTreeSet<&str> =
1393
+ env_unset.iter().map(String::as_str).collect();
1381
1394
  let mut parts = Vec::new();
1382
1395
  // 1. cd
1383
1396
  parts.push("cd".to_string());
@@ -1389,8 +1402,13 @@ pub fn leader_shell_wrapper_command(
1389
1402
  parts.push(key.clone());
1390
1403
  parts.push("&&".to_string());
1391
1404
  }
1392
- // 3. env exports + provider (NO `exec` so the provider is a child)
1405
+ // 3. env exports + provider (NO `exec` so the provider is a child).
1406
+ // 0.4.x ordering fix: skip keys present in env_unset so KEY=val does not
1407
+ // re-introduce a just-unset variable from the inherited env map.
1393
1408
  for (key, value) in env {
1409
+ if unset_set.contains(key.as_str()) {
1410
+ continue;
1411
+ }
1394
1412
  parts.push(format!("{key}={}", shell_quote(value)));
1395
1413
  }
1396
1414
  parts.extend(argv.iter().map(|arg| shell_quote(arg)));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@team-agent/installer",
3
- "version": "0.4.9",
3
+ "version": "0.4.11",
4
4
  "description": "npx installer for Team Agent",
5
5
  "keywords": [
6
6
  "codex",
@@ -20,9 +20,9 @@
20
20
  "team-agent-installer": "npm/install.mjs"
21
21
  },
22
22
  "optionalDependencies": {
23
- "@team-agent/cli-darwin-arm64": "0.4.9",
24
- "@team-agent/cli-darwin-x64": "0.4.9",
25
- "@team-agent/cli-linux-x64": "0.4.9"
23
+ "@team-agent/cli-darwin-arm64": "0.4.11",
24
+ "@team-agent/cli-darwin-x64": "0.4.11",
25
+ "@team-agent/cli-linux-x64": "0.4.11"
26
26
  },
27
27
  "scripts": {
28
28
  "postinstall": "node npm/bincheck.mjs",