@team-agent/installer 0.4.8 → 0.4.10

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.
@@ -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)]
@@ -636,7 +636,21 @@ impl TmuxBackend {
636
636
  first: bool,
637
637
  ) -> Result<SpawnResult, TransportError> {
638
638
  let command = shell_command(argv, cwd, env, env_unset);
639
- let spawn_argv = tmux_spawn_argv(session, window, &command, first);
639
+ self.spawn_with_command(session, window, &command, first)
640
+ }
641
+
642
+ /// 0.4.x (CR C-2): spawn variant that takes a pre-built shell command
643
+ /// (used by `spawn_first_with_leader_shell_wrapper` /
644
+ /// `spawn_into_with_leader_shell_wrapper` to inject the leader wrapper
645
+ /// shape without going through `shell_command`'s `exec`-only template).
646
+ fn spawn_with_command(
647
+ &self,
648
+ session: &SessionName,
649
+ window: &WindowName,
650
+ command: &str,
651
+ first: bool,
652
+ ) -> Result<SpawnResult, TransportError> {
653
+ let spawn_argv = tmux_spawn_argv(session, window, command, first);
640
654
  self.run_spawn(&spawn_argv)?;
641
655
  let pane_argv = vec![
642
656
  "tmux".to_string(),
@@ -1307,6 +1321,8 @@ fn shell_command(
1307
1321
  env: &BTreeMap<String, String>,
1308
1322
  env_unset: &[String],
1309
1323
  ) -> String {
1324
+ let unset_set: std::collections::BTreeSet<&str> =
1325
+ env_unset.iter().map(String::as_str).collect();
1310
1326
  let mut parts = Vec::new();
1311
1327
  parts.push("cd".to_string());
1312
1328
  parts.push(shell_quote(&cwd.to_string_lossy()));
@@ -1319,7 +1335,16 @@ fn shell_command(
1319
1335
  parts.push(key.clone());
1320
1336
  parts.push("&&".to_string());
1321
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).
1322
1344
  for (key, value) in env {
1345
+ if unset_set.contains(key.as_str()) {
1346
+ continue;
1347
+ }
1323
1348
  parts.push(format!("{key}={}", shell_quote(value)));
1324
1349
  }
1325
1350
  parts.push("exec".to_string());
@@ -1327,6 +1352,82 @@ fn shell_command(
1327
1352
  parts.join(" ")
1328
1353
  }
1329
1354
 
1355
+ /// 0.4.x (CR R6): single-source marker prefix. The exit marker emitted by
1356
+ /// `leader_shell_wrapper_command` and the substring detected by
1357
+ /// `leader_provider_health` MUST share this prefix exactly. Format:
1358
+ /// `"[team-agent] {provider_label} exited with {rc}"`.
1359
+ pub const LEADER_PROVIDER_EXIT_MARKER_PREFIX: &str = "[team-agent]";
1360
+ pub const LEADER_PROVIDER_EXIT_MARKER_SUFFIX: &str = "exited with";
1361
+
1362
+ /// 0.4.x (CR R6): build the leader exit marker text for `provider_label`.
1363
+ /// Used by both the shell wrapper (printf source) and the health check
1364
+ /// (capture substring) so they cannot drift.
1365
+ pub fn leader_provider_exit_marker(provider_label: &str) -> String {
1366
+ format!(
1367
+ "{LEADER_PROVIDER_EXIT_MARKER_PREFIX} {provider_label} {LEADER_PROVIDER_EXIT_MARKER_SUFFIX}"
1368
+ )
1369
+ }
1370
+
1371
+ /// 0.4.x (CR C-2): leader shell wrapper — provider runs as a CHILD of a
1372
+ /// long-lived shell, not as the pane's primary process. When the provider
1373
+ /// exits, the pane returns to an interactive shell with an explicit exit
1374
+ /// marker, matching manual `tmux new-session` then `claude` behaviour.
1375
+ ///
1376
+ /// Four required envelope sections (CR C-2):
1377
+ /// 1. cd <cwd> — same as `shell_command`
1378
+ /// 2. unset <KEY> ... — provider env_unset block
1379
+ /// 3. KEY=val ... <provider> — env exports + provider invocation
1380
+ /// (NO `exec` — runs as child)
1381
+ /// 4. printf exit marker; exec shell -l
1382
+ ///
1383
+ /// `provider_label` is a human-readable provider name (e.g. "claude",
1384
+ /// "codex") embedded in the exit marker for diagnostics.
1385
+ pub fn leader_shell_wrapper_command(
1386
+ argv: &[String],
1387
+ cwd: &Path,
1388
+ env: &BTreeMap<String, String>,
1389
+ env_unset: &[String],
1390
+ provider_label: &str,
1391
+ ) -> String {
1392
+ let unset_set: std::collections::BTreeSet<&str> =
1393
+ env_unset.iter().map(String::as_str).collect();
1394
+ let mut parts = Vec::new();
1395
+ // 1. cd
1396
+ parts.push("cd".to_string());
1397
+ parts.push(shell_quote(&cwd.to_string_lossy()));
1398
+ parts.push("&&".to_string());
1399
+ // 2. unset
1400
+ for key in env_unset {
1401
+ parts.push("unset".to_string());
1402
+ parts.push(key.clone());
1403
+ parts.push("&&".to_string());
1404
+ }
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.
1408
+ for (key, value) in env {
1409
+ if unset_set.contains(key.as_str()) {
1410
+ continue;
1411
+ }
1412
+ parts.push(format!("{key}={}", shell_quote(value)));
1413
+ }
1414
+ parts.extend(argv.iter().map(|arg| shell_quote(arg)));
1415
+ parts.push(";".to_string());
1416
+ // 4. exit marker + fall back to interactive shell
1417
+ parts.push("rc=$?;".to_string());
1418
+ parts.push("printf".to_string());
1419
+ // CR R6: marker text comes from single-source `leader_provider_exit_marker`.
1420
+ parts.push(shell_quote(&format!(
1421
+ "\n{} %s\n",
1422
+ leader_provider_exit_marker(provider_label)
1423
+ )));
1424
+ parts.push("\"$rc\";".to_string());
1425
+ parts.push("exec".to_string());
1426
+ parts.push("\"${SHELL:-/bin/zsh}\"".to_string());
1427
+ parts.push("-l".to_string());
1428
+ parts.join(" ")
1429
+ }
1430
+
1330
1431
  fn shell_quote(raw: &str) -> String {
1331
1432
  if raw.is_empty() {
1332
1433
  return "''".to_string();
@@ -1422,6 +1523,38 @@ impl Transport for TmuxBackend {
1422
1523
  self.spawn_split(session, window, argv, cwd, env, env_unset)
1423
1524
  }
1424
1525
 
1526
+ /// 0.4.x (CR C-2): TmuxBackend override of the leader-shell-wrapper
1527
+ /// variant. Builds the wrapper shell line via
1528
+ /// `leader_shell_wrapper_command` and runs it through
1529
+ /// `spawn_with_command` (bypassing the default `exec <cmd>` shape).
1530
+ fn spawn_first_with_leader_shell_wrapper(
1531
+ &self,
1532
+ session: &SessionName,
1533
+ window: &WindowName,
1534
+ argv: &[String],
1535
+ cwd: &Path,
1536
+ env: &BTreeMap<String, String>,
1537
+ env_unset: &[String],
1538
+ provider_label: &str,
1539
+ ) -> Result<SpawnResult, TransportError> {
1540
+ let command = leader_shell_wrapper_command(argv, cwd, env, env_unset, provider_label);
1541
+ self.spawn_with_command(session, window, &command, true)
1542
+ }
1543
+
1544
+ fn spawn_into_with_leader_shell_wrapper(
1545
+ &self,
1546
+ session: &SessionName,
1547
+ window: &WindowName,
1548
+ argv: &[String],
1549
+ cwd: &Path,
1550
+ env: &BTreeMap<String, String>,
1551
+ env_unset: &[String],
1552
+ provider_label: &str,
1553
+ ) -> Result<SpawnResult, TransportError> {
1554
+ let command = leader_shell_wrapper_command(argv, cwd, env, env_unset, provider_label);
1555
+ self.spawn_with_command(session, window, &command, false)
1556
+ }
1557
+
1425
1558
  fn inject(
1426
1559
  &self,
1427
1560
  target: &Target,
@@ -50,6 +50,9 @@ struct OfflineState {
50
50
  /// U1-C Tail-peek contract: every `capture()` call records its `CaptureRange`,
51
51
  /// in order, so a test can prove the delivery peek site narrowed Full → Tail(80).
52
52
  capture_ranges: Vec<CaptureRange>,
53
+ /// 0.4.x (CR C-3 / CR C-5): pre-staged `query(PaneField::PaneCurrentCommand)`
54
+ /// answer keyed by pane id. Used by leader provider health tests.
55
+ pane_current_commands: BTreeMap<String, String>,
53
56
  }
54
57
 
55
58
  impl Default for OfflineState {
@@ -73,6 +76,7 @@ impl Default for OfflineState {
73
76
  list_targets_error: None,
74
77
  capture_text: BTreeMap::new(),
75
78
  capture_ranges: Vec::new(),
79
+ pane_current_commands: BTreeMap::new(),
76
80
  }
77
81
  }
78
82
  }
@@ -163,6 +167,52 @@ impl OfflineTransport {
163
167
  self
164
168
  }
165
169
 
170
+ /// 0.4.x (CR C-3 / CR C-5): pre-stage a `pane_current_command` answer
171
+ /// for `query(PaneField::PaneCurrentCommand)`. Used by leader provider
172
+ /// health tests to simulate "pane is now in a shell" vs
173
+ /// "pane is running the provider".
174
+ pub fn with_pane_current_command(
175
+ self,
176
+ pane_id: impl Into<String>,
177
+ command: impl Into<String>,
178
+ ) -> Self {
179
+ self.with_state(|state| {
180
+ state.pane_current_commands.insert(pane_id.into(), command.into());
181
+ });
182
+ self
183
+ }
184
+
185
+ /// 0.4.x (CR C-3 / CR C-5): mutable setters for fluent test seeding
186
+ /// (alternative to builder chain when the transport already exists).
187
+ pub fn set_pane_addressable(&self, pane: &PaneId, addressable: bool) {
188
+ self.with_state(|state| {
189
+ state.liveness.insert(
190
+ pane.as_str().to_string(),
191
+ if addressable {
192
+ PaneLiveness::Live
193
+ } else {
194
+ PaneLiveness::Dead
195
+ },
196
+ );
197
+ });
198
+ }
199
+
200
+ pub fn set_pane_current_command(&self, pane: &PaneId, command: &str) {
201
+ self.with_state(|state| {
202
+ state
203
+ .pane_current_commands
204
+ .insert(pane.as_str().to_string(), command.to_string());
205
+ });
206
+ }
207
+
208
+ pub fn set_pane_capture(&self, pane: &PaneId, text: &str) {
209
+ self.with_state(|state| {
210
+ state
211
+ .capture_text
212
+ .insert(pane.as_str().to_string(), text.to_string());
213
+ });
214
+ }
215
+
166
216
  /// Pre-stage `capture()` output for a `Target::SessionWindow{session,window}` key.
167
217
  pub fn with_capture_for_session_window(
168
218
  self,
@@ -378,11 +428,20 @@ impl Transport for OfflineTransport {
378
428
 
379
429
  fn query(
380
430
  &self,
381
- _target: &Target,
382
- _field: PaneField,
431
+ target: &Target,
432
+ field: PaneField,
383
433
  ) -> Result<Option<String>, TransportError> {
384
434
  self.record("query");
385
- Ok(None)
435
+ if !matches!(field, PaneField::PaneCurrentCommand) {
436
+ return Ok(None);
437
+ }
438
+ let pane_id = match target {
439
+ Target::Pane(p) => p.as_str().to_string(),
440
+ Target::SessionWindow { .. } => return Ok(None),
441
+ };
442
+ Ok(self.with_state(|state| {
443
+ state.pane_current_commands.get(&pane_id).cloned()
444
+ }))
386
445
  }
387
446
 
388
447
  fn liveness(&self, pane: &PaneId) -> Result<PaneLiveness, TransportError> {
@@ -584,6 +584,44 @@ pub trait Transport: Send + Sync {
584
584
  self.spawn_into(session, window, argv, cwd, env)
585
585
  }
586
586
 
587
+ /// 0.4.x (CR C-2): leader-specific spawn variant. Instead of `exec <cmd>`
588
+ /// (which makes the provider the pane's primary process and turns the
589
+ /// pane into `[exited]` when the provider exits), build a shell line
590
+ /// that runs the provider as a CHILD of a long-lived shell:
591
+ /// `cd ... && unset ... && KEY=val ... <cmd>; rc=$?; printf '\n[team-agent] <provider> exited with %s\n' "$rc"; exec "${SHELL:-/bin/zsh}" -l`.
592
+ /// When the provider exits, the pane returns to an interactive shell with
593
+ /// an explicit exit marker — matching manual `tmux new-session` then
594
+ /// `claude` behaviour. Default falls back to plain `spawn_first_with_env_unset`
595
+ /// for backends that have no shell layer (test-only `OfflineTransport`).
596
+ fn spawn_first_with_leader_shell_wrapper(
597
+ &self,
598
+ session: &SessionName,
599
+ window: &WindowName,
600
+ argv: &[String],
601
+ cwd: &Path,
602
+ env: &BTreeMap<String, String>,
603
+ env_unset: &[String],
604
+ provider_label: &str,
605
+ ) -> Result<SpawnResult, TransportError> {
606
+ let _ = provider_label;
607
+ self.spawn_first_with_env_unset(session, window, argv, cwd, env, env_unset)
608
+ }
609
+
610
+ /// 同 [`Transport::spawn_first_with_leader_shell_wrapper`],对应 `spawn_into`。
611
+ fn spawn_into_with_leader_shell_wrapper(
612
+ &self,
613
+ session: &SessionName,
614
+ window: &WindowName,
615
+ argv: &[String],
616
+ cwd: &Path,
617
+ env: &BTreeMap<String, String>,
618
+ env_unset: &[String],
619
+ provider_label: &str,
620
+ ) -> Result<SpawnResult, TransportError> {
621
+ let _ = provider_label;
622
+ self.spawn_into_with_env_unset(session, window, argv, cwd, env, env_unset)
623
+ }
624
+
587
625
  // —— INJECT / CAPTURE / QUERY(RIE):按稳定 Target 寻址 ——
588
626
 
589
627
  /// 归并 set/load-buffer + paste-buffer + send submit;空文本走纯 send-keys
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@team-agent/installer",
3
- "version": "0.4.8",
3
+ "version": "0.4.10",
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.8",
24
- "@team-agent/cli-darwin-x64": "0.4.8",
25
- "@team-agent/cli-linux-x64": "0.4.8"
23
+ "@team-agent/cli-darwin-arm64": "0.4.10",
24
+ "@team-agent/cli-darwin-x64": "0.4.10",
25
+ "@team-agent/cli-linux-x64": "0.4.10"
26
26
  },
27
27
  "scripts": {
28
28
  "postinstall": "node npm/bincheck.mjs",