@team-agent/installer 0.5.73 → 0.5.75

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 (38) hide show
  1. package/Cargo.lock +1 -1
  2. package/Cargo.toml +1 -1
  3. package/crates/team-agent/src/cli/adapters.rs +19 -13
  4. package/crates/team-agent/src/cli/emit.rs +6 -3
  5. package/crates/team-agent/src/cli/mod.rs +151 -0
  6. package/crates/team-agent/src/cli/tests/leader_watch.rs +28 -0
  7. package/crates/team-agent/src/cli/tests/run_delegation.rs +3 -0
  8. package/crates/team-agent/src/cli/types.rs +6 -0
  9. package/crates/team-agent/src/compiler.rs +14 -1
  10. package/crates/team-agent/src/layout/worker_env.rs +366 -0
  11. package/crates/team-agent/src/leader/lease.rs +334 -53
  12. package/crates/team-agent/src/leader/registry.rs +177 -108
  13. package/crates/team-agent/src/lifecycle/launch/pi_mcp.rs +12 -1
  14. package/crates/team-agent/src/lifecycle/launch/quick_start.rs +2272 -128
  15. package/crates/team-agent/src/lifecycle/launch/quick_start_transport.rs +16 -6
  16. package/crates/team-agent/src/lifecycle/launch/spawn.rs +4 -0
  17. package/crates/team-agent/src/lifecycle/launch/spec_state.rs +138 -13
  18. package/crates/team-agent/src/lifecycle/launch/state_projection.rs +5 -3
  19. package/crates/team-agent/src/lifecycle/restart/common.rs +3 -11
  20. package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +60 -0
  21. package/crates/team-agent/src/lifecycle/tests/pi_compiler_red.rs +31 -0
  22. package/crates/team-agent/src/lifecycle/tests/pi_dual_entry_red.rs +25 -23
  23. package/crates/team-agent/src/lifecycle/tests/worker_spawn_env_red.rs +110 -31
  24. package/crates/team-agent/src/messaging/delivery.rs +226 -37
  25. package/crates/team-agent/src/messaging/leader_channel.rs +12 -3
  26. package/crates/team-agent/src/messaging/tests/runtime.rs +234 -0
  27. package/crates/team-agent/src/model/spec.rs +81 -7
  28. package/crates/team-agent/src/provider/adapters/pi.rs +14 -0
  29. package/crates/team-agent/src/state/persist.rs +433 -4
  30. package/crates/team-agent/src/state/repository.rs +45 -2
  31. package/crates/team-agent/src/tmux_backend/tests.rs +414 -7
  32. package/crates/team-agent/src/tmux_backend.rs +274 -26
  33. package/crates/team-agent/src/transport/test_support.rs +18 -3
  34. package/crates/team-agent/src/transport.rs +99 -0
  35. package/package.json +4 -4
  36. package/skills/team-agent/SKILL.md +3 -0
  37. package/skills/team-agent/command-coverage.json +21 -0
  38. package/skills/team-agent/references/team-agent-operator.md +9 -2
@@ -61,13 +61,15 @@ use std::process::Stdio;
61
61
  use std::time::{Duration, Instant};
62
62
 
63
63
  use crate::model::enums::PaneLiveness;
64
+ use crate::provider::wire::{parse_provider, provider_wire};
64
65
  use crate::transport::{
65
- normalize_capture, tmux_capture_argv, tmux_empty_inject_argv, tmux_inject_text_argv,
66
- tmux_query_argv, tmux_send_keys_argv, tmux_send_submit_argv, tmux_spawn_argv, AttachOutcome,
67
- BackendKind, CaptureRange, CapturedText, InjectPayload, InjectReport, InjectStage,
68
- InjectVerification, Key, PaneField, PaneId, PaneInfo, PaneMode, SessionName, SetEnvOutcome,
69
- SpawnResult, SubmitAttemptObservation, SubmitObserver, SubmitVerification, Target, Transport,
70
- TransportError, TurnVerification, WindowName,
66
+ command_basename, normalize_capture, tmux_capture_argv, tmux_empty_inject_argv,
67
+ tmux_inject_text_argv, tmux_query_argv, tmux_send_keys_argv, tmux_send_submit_argv,
68
+ tmux_spawn_argv, AttachOutcome, BackendKind, CaptureRange, CaptureSampleOutcome, CapturedText,
69
+ InjectPayload, InjectReport, InjectStage, InjectVerification, InputSurfaceProbe, Key,
70
+ PaneField, PaneId, PaneInfo, PaneMode, QueryOutcome, SessionName, SetEnvOutcome, SpawnResult,
71
+ SubmitAttemptObservation, SubmitDiagnostics, SubmitObserver, SubmitVerification, Target,
72
+ Transport, TransportError, TurnVerification, WindowName,
71
73
  };
72
74
 
73
75
  pub const PANE_BINDING_NONCE_METADATA_KEY: &str = "TEAM_AGENT_PANE_BINDING_NONCE";
@@ -378,6 +380,63 @@ impl TmuxBackend {
378
380
  self.run_ok(&argv)
379
381
  }
380
382
 
383
+ /// Set the pane-instance nonce only when the pane option is unset. tmux
384
+ /// serializes commands at the server, so concurrent first claims have one
385
+ /// winner; losers read back that winner instead of persisting a stale nonce.
386
+ pub(crate) fn set_pane_binding_nonce_if_unset(
387
+ &self,
388
+ pane: &PaneId,
389
+ nonce: &str,
390
+ ) -> Result<String, TransportError> {
391
+ let argv = self.tmux_argv(&[
392
+ "tmux".to_string(),
393
+ "set-option".to_string(),
394
+ "-p".to_string(),
395
+ "-o".to_string(),
396
+ "-t".to_string(),
397
+ pane.as_str().to_string(),
398
+ TMUX_PANE_BINDING_NONCE_OPTION.to_string(),
399
+ nonce.to_string(),
400
+ ]);
401
+ let output = self.runner.run(&argv)?;
402
+ if output.success {
403
+ return Ok(nonce.to_string());
404
+ }
405
+ if output
406
+ .stderr
407
+ .to_ascii_lowercase()
408
+ .contains("already set")
409
+ {
410
+ return self.read_pane_binding_nonce(pane);
411
+ }
412
+ Err(subprocess_error(argv, output))
413
+ }
414
+
415
+ fn read_pane_binding_nonce(&self, pane: &PaneId) -> Result<String, TransportError> {
416
+ let argv = self.tmux_argv(&[
417
+ "tmux".to_string(),
418
+ "show-options".to_string(),
419
+ "-p".to_string(),
420
+ "-v".to_string(),
421
+ "-t".to_string(),
422
+ pane.as_str().to_string(),
423
+ TMUX_PANE_BINDING_NONCE_OPTION.to_string(),
424
+ ]);
425
+ let output = self.runner.run(&argv)?;
426
+ if !output.success {
427
+ return Err(subprocess_error(argv, output));
428
+ }
429
+ let nonce = output.stdout.trim();
430
+ if nonce.is_empty() {
431
+ return Err(TransportError::Subprocess {
432
+ argv,
433
+ code: output.code,
434
+ stderr: "pane binding nonce remained unset after conditional write".to_string(),
435
+ });
436
+ }
437
+ Ok(nonce.to_string())
438
+ }
439
+
381
440
  /// Backend with an injected runner (tests: canned/recording tmux output). Shared default socket.
382
441
  /// ---
383
442
  /// purpose: 用注入的 runner 构造后端(测试用录制/罐装 tmux 输出),绑共享默认 socket
@@ -1389,6 +1448,107 @@ fn pane_from_target(target: &Target) -> PaneId {
1389
1448
  }
1390
1449
  }
1391
1450
 
1451
+ #[derive(Default)]
1452
+ struct CaptureTally {
1453
+ ok: u32,
1454
+ err: u32,
1455
+ last: CaptureSampleOutcome,
1456
+ }
1457
+
1458
+ impl CaptureTally {
1459
+ fn record_ok(&mut self, text: &str, marker: Option<&str>) {
1460
+ self.ok = self.ok.saturating_add(1);
1461
+ self.last = if marker.is_some_and(|token| text.contains(token)) {
1462
+ CaptureSampleOutcome::OkTokenSeen
1463
+ } else {
1464
+ CaptureSampleOutcome::OkTokenMissing
1465
+ };
1466
+ }
1467
+
1468
+ fn record_err(&mut self) {
1469
+ self.err = self.err.saturating_add(1);
1470
+ self.last = CaptureSampleOutcome::Failed;
1471
+ }
1472
+
1473
+ fn seen_after_ok_samples(&self, visible: bool) -> Option<bool> {
1474
+ if self.ok == 0 {
1475
+ None
1476
+ } else {
1477
+ Some(visible)
1478
+ }
1479
+ }
1480
+ }
1481
+
1482
+ struct InjectTargetSurface {
1483
+ pane_command_basename: Option<String>,
1484
+ pane_command_query: QueryOutcome,
1485
+ input_surface: InputSurfaceProbe,
1486
+ target_pane_id: String,
1487
+ target_pane_query_matched: Option<bool>,
1488
+ }
1489
+
1490
+ fn probe_inject_target_surface(backend: &TmuxBackend, target: &Target) -> InjectTargetSurface {
1491
+ let pane = pane_from_target(target);
1492
+ let (pane_command_basename, pane_command_query) =
1493
+ match backend.query(target, PaneField::PaneCurrentCommand) {
1494
+ Ok(Some(raw)) => match command_basename(&raw) {
1495
+ Some(name) => (Some(name), QueryOutcome::Observed),
1496
+ None => (None, QueryOutcome::Unavailable),
1497
+ },
1498
+ _ => (None, QueryOutcome::Unavailable),
1499
+ };
1500
+ let input_surface = match backend.query(target, PaneField::PaneMode) {
1501
+ Ok(Some(raw)) => match pane_mode_from_raw(Some(raw)) {
1502
+ None => InputSurfaceProbe::Input,
1503
+ Some(PaneMode::Copy) => InputSurfaceProbe::Copy,
1504
+ Some(PaneMode::Tree) => InputSurfaceProbe::Tree,
1505
+ Some(PaneMode::View) => InputSurfaceProbe::View,
1506
+ Some(PaneMode::Client) => InputSurfaceProbe::Client,
1507
+ Some(PaneMode::Unknown) => InputSurfaceProbe::UnknownMode,
1508
+ },
1509
+ _ => InputSurfaceProbe::Unavailable,
1510
+ };
1511
+ let target_pane_query_matched = match backend.query(target, PaneField::PaneId) {
1512
+ Ok(Some(id)) if !id.is_empty() => Some(id == pane.as_str()),
1513
+ _ => None,
1514
+ };
1515
+ InjectTargetSurface {
1516
+ pane_command_basename,
1517
+ pane_command_query,
1518
+ input_surface,
1519
+ target_pane_id: pane.as_str().to_string(),
1520
+ target_pane_query_matched,
1521
+ }
1522
+ }
1523
+
1524
+ fn submit_diagnostics_for_inject(
1525
+ total_elapsed_ms: u64,
1526
+ attempts_detail: Vec<SubmitAttemptObservation>,
1527
+ tally: &CaptureTally,
1528
+ token_seen_after_paste: Option<bool>,
1529
+ token_seen_after_enter: Option<bool>,
1530
+ surface: InjectTargetSurface,
1531
+ ) -> SubmitDiagnostics {
1532
+ SubmitDiagnostics {
1533
+ appear_gate_elapsed_ms: 0,
1534
+ appear_gate_matched: false,
1535
+ total_elapsed_ms,
1536
+ attempts_detail,
1537
+ token_seen_before_paste: None,
1538
+ token_seen_after_paste,
1539
+ token_seen_after_enter,
1540
+ capture_ok_count: tally.ok,
1541
+ capture_err_count: tally.err,
1542
+ capture_sample_count: tally.ok.saturating_add(tally.err),
1543
+ last_capture_outcome: tally.last,
1544
+ pane_command_basename: surface.pane_command_basename,
1545
+ pane_command_query: surface.pane_command_query,
1546
+ input_surface: surface.input_surface,
1547
+ target_pane_id: Some(surface.target_pane_id),
1548
+ target_pane_query_matched: surface.target_pane_query_matched,
1549
+ }
1550
+ }
1551
+
1392
1552
  fn target_name(target: &Target) -> String {
1393
1553
  match target {
1394
1554
  Target::Pane(pane) => pane.as_str().to_string(),
@@ -1590,13 +1750,23 @@ fn post_submit_token_visible(
1590
1750
  backend: &TmuxBackend,
1591
1751
  target: &Target,
1592
1752
  payload: &InjectPayload,
1753
+ tally: &mut CaptureTally,
1593
1754
  ) -> Result<Option<bool>, TransportError> {
1594
- if payload_token_marker(payload).is_none() {
1755
+ let Some(marker) = payload_token_marker(payload) else {
1595
1756
  return Ok(None);
1596
- }
1757
+ };
1597
1758
  for attempt in 0..TOKEN_POST_SUBMIT_READBACK_POLLS {
1598
- if let Some(true) = token_visible_in_capture(backend, target, payload)? {
1599
- return Ok(Some(true));
1759
+ match backend.capture(target, CaptureRange::Tail(80)) {
1760
+ Ok(cap) => {
1761
+ tally.record_ok(&cap.text, Some(marker));
1762
+ if cap.text.contains(marker) {
1763
+ return Ok(Some(true));
1764
+ }
1765
+ }
1766
+ Err(_) => {
1767
+ tally.record_err();
1768
+ return Ok(None);
1769
+ }
1600
1770
  }
1601
1771
  if attempt + 1 < TOKEN_POST_SUBMIT_READBACK_POLLS {
1602
1772
  std::thread::sleep(Duration::from_millis(25));
@@ -2388,6 +2558,7 @@ fn shell_command(
2388
2558
  ) -> String {
2389
2559
  let unset_set: std::collections::BTreeSet<&str> =
2390
2560
  env_unset.iter().map(String::as_str).collect();
2561
+ let caller_provider = caller_provider_for_invocation(env);
2391
2562
  let mut parts = Vec::new();
2392
2563
  parts.push("cd".to_string());
2393
2564
  parts.push(shell_quote(&cwd.to_string_lossy()));
@@ -2407,16 +2578,47 @@ fn shell_command(
2407
2578
  // contains the very keys we want to scrub (e.g. CLAUDE_EFFORT carried
2408
2579
  // forward from the launching shell into the env map).
2409
2580
  for (key, value) in env {
2410
- if unset_set.contains(key.as_str()) {
2581
+ if unset_set.contains(key.as_str())
2582
+ || key.starts_with(crate::layout::worker_env::CALLER_CONTEXT_PREFIX)
2583
+ {
2411
2584
  continue;
2412
2585
  }
2413
2586
  parts.push(format!("{key}={}", shell_quote(value)));
2414
2587
  }
2588
+ append_caller_context_assignments(&mut parts, caller_provider);
2415
2589
  parts.push("exec".to_string());
2416
2590
  parts.extend(argv.iter().map(|arg| shell_quote(arg)));
2417
2591
  parts.join(" ")
2418
2592
  }
2419
2593
 
2594
+ fn caller_provider_for_invocation(env: &BTreeMap<String, String>) -> Option<&'static str> {
2595
+ env.get(crate::layout::worker_env::CALLER_PROVIDER_ENV)
2596
+ .and_then(|value| parse_provider(value))
2597
+ .map(provider_wire)
2598
+ }
2599
+
2600
+ fn append_caller_context_assignments(parts: &mut Vec<String>, provider: Option<&str>) {
2601
+ let Some(provider) = provider else {
2602
+ return;
2603
+ };
2604
+ parts.push(format!(
2605
+ "{}={}",
2606
+ crate::layout::worker_env::CALLER_PROVIDER_ENV,
2607
+ shell_quote(provider)
2608
+ ));
2609
+ // These are expanded by the shell created inside the target pane. They are
2610
+ // assignments on the provider command only, so the exit marker/inert tail
2611
+ // cannot retain a usable caller context.
2612
+ parts.push(format!(
2613
+ "{}=\"${{TMUX_PANE-}}\"",
2614
+ crate::layout::worker_env::CALLER_PANE_ENV
2615
+ ));
2616
+ parts.push(format!(
2617
+ "{}=\"${{TMUX%%,*}}\"",
2618
+ crate::layout::worker_env::CALLER_ENDPOINT_ENV
2619
+ ));
2620
+ }
2621
+
2420
2622
  /// 0.4.x (CR R6): single-source marker prefix. The exit marker emitted by
2421
2623
  /// `leader_shell_wrapper_command` and the substring detected by
2422
2624
  /// `leader_provider_health` MUST share this prefix exactly. Format:
@@ -2586,6 +2788,7 @@ pub fn worker_shell_wrapper_command(
2586
2788
  ) -> String {
2587
2789
  let unset_set: std::collections::BTreeSet<&str> =
2588
2790
  env_unset.iter().map(String::as_str).collect();
2791
+ let caller_provider = caller_provider_for_invocation(env);
2589
2792
  let mut parts = Vec::new();
2590
2793
  parts.push("cd".to_string());
2591
2794
  parts.push(shell_quote(&cwd.to_string_lossy()));
@@ -2596,11 +2799,14 @@ pub fn worker_shell_wrapper_command(
2596
2799
  parts.push("&&".to_string());
2597
2800
  }
2598
2801
  for (key, value) in env {
2599
- if unset_set.contains(key.as_str()) {
2802
+ if unset_set.contains(key.as_str())
2803
+ || key.starts_with(crate::layout::worker_env::CALLER_CONTEXT_PREFIX)
2804
+ {
2600
2805
  continue;
2601
2806
  }
2602
2807
  parts.push(format!("{key}={}", shell_quote(value)));
2603
2808
  }
2809
+ append_caller_context_assignments(&mut parts, caller_provider);
2604
2810
  parts.extend(argv.iter().map(|arg| shell_quote(arg)));
2605
2811
  parts.push(";".to_string());
2606
2812
  parts.push("rc=$?;".to_string());
@@ -2945,11 +3151,15 @@ impl Transport for TmuxBackend {
2945
3151
  let poll_start = std::time::Instant::now();
2946
3152
  let mut token_ever_visible = false;
2947
3153
  let mut tracked_paste: Option<PasteLatch> = None;
3154
+ let mut capture_tally = CaptureTally::default();
3155
+ let mut token_seen_after_paste = None;
3156
+ let mut token_seen_after_enter = None;
2948
3157
  token_visible_for_report = if let Some(m) = payload_token_marker(payload) {
2949
3158
  let mut visible = false;
2950
3159
  while poll_start.elapsed().as_millis() < token_poll_timeout_ms as u128 {
2951
3160
  match self.capture(target, CaptureRange::Tail(80)) {
2952
3161
  Ok(cap) => {
3162
+ capture_tally.record_ok(&cap.text, Some(m));
2953
3163
  tracked_paste = latch_paste(&cap.text, tracked_paste);
2954
3164
  if cap.text.contains(m) {
2955
3165
  visible = true;
@@ -2960,10 +3170,14 @@ impl Transport for TmuxBackend {
2960
3170
  break;
2961
3171
  }
2962
3172
  }
2963
- Err(_) => break, // tmux unavailable, skip poll
3173
+ Err(_) => {
3174
+ capture_tally.record_err();
3175
+ break; // tmux unavailable, skip poll
3176
+ }
2964
3177
  }
2965
3178
  std::thread::sleep(Duration::from_millis(50));
2966
3179
  }
3180
+ token_seen_after_paste = capture_tally.seen_after_ok_samples(visible);
2967
3181
  Some(visible)
2968
3182
  } else {
2969
3183
  None
@@ -2985,6 +3199,7 @@ impl Transport for TmuxBackend {
2985
3199
  self.run_inject_stage(&submit_argv, InjectStage::Submit)?;
2986
3200
  notify_submit_observer(observer);
2987
3201
  let total_elapsed_ms = inject_start.elapsed().as_millis() as u64;
3202
+ let surface = probe_inject_target_surface(self, target);
2988
3203
  return Ok(InjectReport {
2989
3204
  stage_reached: InjectStage::Submit,
2990
3205
  inject_verification: inject_verification_after_readback(
@@ -2994,12 +3209,14 @@ impl Transport for TmuxBackend {
2994
3209
  submit_verification: submit_verification_for_key(submit),
2995
3210
  turn_verification: TurnVerification::NotYetObserved,
2996
3211
  attempts: 1,
2997
- submit_diagnostics: Some(crate::transport::SubmitDiagnostics {
2998
- appear_gate_elapsed_ms: 0,
2999
- appear_gate_matched: false,
3212
+ submit_diagnostics: Some(submit_diagnostics_for_inject(
3000
3213
  total_elapsed_ms,
3001
- attempts_detail: Vec::new(),
3002
- }),
3214
+ Vec::new(),
3215
+ &capture_tally,
3216
+ token_seen_after_paste,
3217
+ None,
3218
+ surface,
3219
+ )),
3003
3220
  });
3004
3221
  }
3005
3222
 
@@ -3042,8 +3259,12 @@ impl Transport for TmuxBackend {
3042
3259
  if attempt > 0 {
3043
3260
  if let Some(m) = marker {
3044
3261
  if let Ok(cap) = self.capture(target, CaptureRange::Tail(40)) {
3262
+ capture_tally.record_ok(&cap.text, Some(m));
3045
3263
  if cap.text.contains(m) {
3046
3264
  token_ever_visible = true;
3265
+ token_seen_after_enter = Some(true);
3266
+ } else if token_seen_after_enter.is_none() {
3267
+ token_seen_after_enter = Some(false);
3047
3268
  }
3048
3269
  tracked_paste = latch_paste(&cap.text, tracked_paste);
3049
3270
  let obs = submit_attempt_observation(
@@ -3115,10 +3336,22 @@ impl Transport for TmuxBackend {
3115
3336
  // Post-submit token readback (U1 #7 parity: check token
3116
3337
  // visible after Enter for no-echo panes).
3117
3338
  if attempt == 0 && matches!(token_visible_for_report, Some(false)) {
3118
- token_visible_for_report =
3119
- post_submit_token_visible(self, target, payload).unwrap_or(Some(false));
3120
- if token_visible_for_report == Some(true) {
3339
+ let post = post_submit_token_visible(
3340
+ self,
3341
+ target,
3342
+ payload,
3343
+ &mut capture_tally,
3344
+ )
3345
+ .unwrap_or(None);
3346
+ if post == Some(true) {
3347
+ token_visible_for_report = Some(true);
3121
3348
  token_ever_visible = true;
3349
+ token_seen_after_enter = Some(true);
3350
+ } else {
3351
+ token_visible_for_report = Some(false);
3352
+ if post == Some(false) {
3353
+ token_seen_after_enter = Some(false);
3354
+ }
3122
3355
  }
3123
3356
  }
3124
3357
 
@@ -3132,10 +3365,14 @@ impl Transport for TmuxBackend {
3132
3365
  std::thread::sleep(Duration::from_millis(100));
3133
3366
  match self.capture(target, CaptureRange::Tail(40)) {
3134
3367
  Ok(cap) => {
3368
+ capture_tally.record_ok(&cap.text, Some(m));
3135
3369
  consecutive_capture_failures = 0;
3136
3370
  saw_capture = true;
3137
3371
  if cap.text.contains(m) {
3138
3372
  token_ever_visible = true;
3373
+ token_seen_after_enter = Some(true);
3374
+ } else if token_seen_after_enter.is_none() {
3375
+ token_seen_after_enter = Some(false);
3139
3376
  }
3140
3377
  tracked_paste = latch_paste(&cap.text, tracked_paste);
3141
3378
  let obs = submit_attempt_observation(
@@ -3166,6 +3403,7 @@ impl Transport for TmuxBackend {
3166
3403
  }
3167
3404
  }
3168
3405
  Err(_) => {
3406
+ capture_tally.record_err();
3169
3407
  capture_retries = capture_retries.saturating_add(1);
3170
3408
  consecutive_capture_failures =
3171
3409
  consecutive_capture_failures.saturating_add(1);
@@ -3265,8 +3503,12 @@ impl Transport for TmuxBackend {
3265
3503
  std::thread::sleep(Duration::from_millis(100));
3266
3504
  match self.capture(target, CaptureRange::Tail(40)) {
3267
3505
  Ok(cap) => {
3506
+ capture_tally.record_ok(&cap.text, Some(m));
3268
3507
  if cap.text.contains(m) {
3269
3508
  token_ever_visible = true;
3509
+ token_seen_after_enter = Some(true);
3510
+ } else if token_seen_after_enter.is_none() {
3511
+ token_seen_after_enter = Some(false);
3270
3512
  }
3271
3513
  tracked_paste = latch_paste(&cap.text, tracked_paste);
3272
3514
  let obs = submit_attempt_observation(
@@ -3304,7 +3546,10 @@ impl Transport for TmuxBackend {
3304
3546
  break;
3305
3547
  }
3306
3548
  }
3307
- Err(_) => break,
3549
+ Err(_) => {
3550
+ capture_tally.record_err();
3551
+ break;
3552
+ }
3308
3553
  }
3309
3554
  }
3310
3555
  if found_consumed {
@@ -3316,6 +3561,7 @@ impl Transport for TmuxBackend {
3316
3561
  }
3317
3562
  }
3318
3563
  let total_elapsed_ms = inject_start.elapsed().as_millis() as u64;
3564
+ let surface = probe_inject_target_surface(self, target);
3319
3565
  return Ok(InjectReport {
3320
3566
  stage_reached: InjectStage::Submit,
3321
3567
  inject_verification: inject_verification_after_readback(
@@ -3328,12 +3574,14 @@ impl Transport for TmuxBackend {
3328
3574
  last_turn_text.as_deref(),
3329
3575
  ),
3330
3576
  attempts: consumption_attempts.saturating_add(capture_retries),
3331
- submit_diagnostics: Some(crate::transport::SubmitDiagnostics {
3332
- appear_gate_elapsed_ms: 0,
3333
- appear_gate_matched: false,
3577
+ submit_diagnostics: Some(submit_diagnostics_for_inject(
3334
3578
  total_elapsed_ms,
3335
3579
  attempts_detail,
3336
- }),
3580
+ &capture_tally,
3581
+ token_seen_after_paste,
3582
+ token_seen_after_enter,
3583
+ surface,
3584
+ )),
3337
3585
  });
3338
3586
  }
3339
3587
  }
@@ -1,7 +1,7 @@
1
1
  //! 测试默认零真-spawn、零真-tmux:lifecycle/CLI spawn 路径必经 *_with_transport 注入离线
2
2
  //! mock;确需真 tmux 者必须 provider:fake + RAII kill_server 守卫 + #[ignore=real-machine].
3
3
 
4
- use std::collections::BTreeMap;
4
+ use std::collections::{BTreeMap, VecDeque};
5
5
  use std::path::Path;
6
6
  use std::sync::{Arc, Mutex};
7
7
 
@@ -26,6 +26,7 @@ struct OfflineState {
26
26
  session_present: bool,
27
27
  session_absent_after_spawn_first: bool,
28
28
  targets: Vec<PaneInfo>,
29
+ target_snapshots: VecDeque<Vec<PaneInfo>>,
29
30
  windows: Vec<WindowName>,
30
31
  pane_presence: BTreeMap<String, bool>,
31
32
  spawn_failures: BTreeMap<String, String>,
@@ -62,6 +63,7 @@ impl Default for OfflineState {
62
63
  session_present: false,
63
64
  session_absent_after_spawn_first: false,
64
65
  targets: Vec::new(),
66
+ target_snapshots: VecDeque::new(),
65
67
  windows: Vec::new(),
66
68
  pane_presence: BTreeMap::new(),
67
69
  spawn_failures: BTreeMap::new(),
@@ -104,7 +106,15 @@ impl OfflineTransport {
104
106
  }
105
107
 
106
108
  pub fn with_targets(self, targets: Vec<PaneInfo>) -> Self {
107
- self.with_state(|state| state.targets = targets);
109
+ self.with_state(|state| {
110
+ state.targets = targets;
111
+ state.target_snapshots.clear();
112
+ });
113
+ self
114
+ }
115
+
116
+ pub fn with_target_snapshots(self, snapshots: Vec<Vec<PaneInfo>>) -> Self {
117
+ self.with_state(|state| state.target_snapshots = snapshots.into());
108
118
  self
109
119
  }
110
120
 
@@ -507,7 +517,12 @@ impl Transport for OfflineTransport {
507
517
  detail,
508
518
  });
509
519
  }
510
- Ok(self.with_state(|state| state.targets.clone()))
520
+ Ok(self.with_state(|state| {
521
+ state
522
+ .target_snapshots
523
+ .pop_front()
524
+ .unwrap_or_else(|| state.targets.clone())
525
+ }))
511
526
  }
512
527
 
513
528
  fn has_session(&self, _session: &SessionName) -> Result<bool, TransportError> {
@@ -483,6 +483,105 @@ pub struct SubmitDiagnostics {
483
483
  /// prompt loop. Empty when `saw_pasted_prompt == false` and the inject
484
484
  /// went through the E46 token path.
485
485
  pub attempts_detail: Vec<SubmitAttemptObservation>,
486
+ /// Token visibility before paste. `None` means this inject did not sample
487
+ /// (honest unknown). Never inferred from later captures.
488
+ pub token_seen_before_paste: Option<bool>,
489
+ /// Token visibility from successful post-paste captures. `None` if every
490
+ /// post-paste capture failed or none ran.
491
+ pub token_seen_after_paste: Option<bool>,
492
+ /// Token visibility from successful post-Enter captures. `None` if none ran
493
+ /// or all failed.
494
+ pub token_seen_after_enter: Option<bool>,
495
+ pub capture_ok_count: u32,
496
+ pub capture_err_count: u32,
497
+ pub capture_sample_count: u32,
498
+ pub last_capture_outcome: CaptureSampleOutcome,
499
+ /// Basename only of `#{pane_current_command}` at inject; never argv.
500
+ pub pane_command_basename: Option<String>,
501
+ pub pane_command_query: QueryOutcome,
502
+ pub input_surface: InputSurfaceProbe,
503
+ pub target_pane_id: Option<String>,
504
+ /// Inject target pane id vs `#{pane_id}` queried on that same target.
505
+ pub target_pane_query_matched: Option<bool>,
506
+ }
507
+
508
+ /// Last capture sample of the inject token path. Distinguishes "read failed"
509
+ /// from "read succeeded and token was absent". Does not change delivery gates.
510
+ #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
511
+ pub enum CaptureSampleOutcome {
512
+ #[default]
513
+ NotAttempted,
514
+ OkTokenSeen,
515
+ OkTokenMissing,
516
+ Failed,
517
+ }
518
+
519
+ impl CaptureSampleOutcome {
520
+ pub fn as_str(self) -> &'static str {
521
+ match self {
522
+ Self::NotAttempted => "not_attempted",
523
+ Self::OkTokenSeen => "ok_token_seen",
524
+ Self::OkTokenMissing => "ok_token_missing",
525
+ Self::Failed => "failed",
526
+ }
527
+ }
528
+ }
529
+
530
+ #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
531
+ pub enum QueryOutcome {
532
+ #[default]
533
+ Unavailable,
534
+ Observed,
535
+ }
536
+
537
+ impl QueryOutcome {
538
+ pub fn as_str(self) -> &'static str {
539
+ match self {
540
+ Self::Unavailable => "unavailable",
541
+ Self::Observed => "observed",
542
+ }
543
+ }
544
+ }
545
+
546
+ /// Typed pane input surface from existing `pane_mode` probe. `Unavailable`
547
+ /// means the query failed; it is not a pass.
548
+ #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
549
+ pub enum InputSurfaceProbe {
550
+ #[default]
551
+ Unavailable,
552
+ Input,
553
+ Copy,
554
+ Tree,
555
+ View,
556
+ Client,
557
+ UnknownMode,
558
+ }
559
+
560
+ impl InputSurfaceProbe {
561
+ pub fn as_str(self) -> &'static str {
562
+ match self {
563
+ Self::Unavailable => "unavailable",
564
+ Self::Input => "input",
565
+ Self::Copy => "copy",
566
+ Self::Tree => "tree",
567
+ Self::View => "view",
568
+ Self::Client => "client",
569
+ Self::UnknownMode => "unknown_mode",
570
+ }
571
+ }
572
+ }
573
+
574
+ /// First token of a pane command, basename only. Empty/missing → `None`.
575
+ pub(crate) fn command_basename(raw: &str) -> Option<String> {
576
+ let first = raw.split_whitespace().next()?.trim();
577
+ if first.is_empty() {
578
+ return None;
579
+ }
580
+ std::path::Path::new(first)
581
+ .file_name()
582
+ .and_then(|name| name.to_str())
583
+ .filter(|name| !name.is_empty())
584
+ .map(str::to_string)
486
585
  }
487
586
 
488
587
  /// E50 PR-1 single attempt observation (one Enter + one capture). All fields
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@team-agent/installer",
3
- "version": "0.5.73",
3
+ "version": "0.5.75",
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.5.73",
24
- "@team-agent/cli-darwin-x64": "0.5.73",
25
- "@team-agent/cli-linux-x64": "0.5.73"
23
+ "@team-agent/cli-darwin-arm64": "0.5.75",
24
+ "@team-agent/cli-darwin-x64": "0.5.75",
25
+ "@team-agent/cli-linux-x64": "0.5.75"
26
26
  },
27
27
  "scripts": {
28
28
  "postinstall": "node npm/bincheck.mjs",
@@ -27,10 +27,13 @@ For a Pi worker, use this existing three-step flow:
27
27
 
28
28
  Pi roles preserve the direct Pi login, model defaults, extensions/plugins, skills, context, trust, and tools. Team Agent adds only per-seat identity/session isolation. `model` and `effort` are optional explicit pass-throughs. Use the shared role schema, a block-style `tools` list containing `mcp_team`, and the required boolean `dangerously_skip_permissions`.
29
29
 
30
+ Pi tool contract: use only `mcp_team`, `fs_read`, `fs_list`, `fs_write`, and `execute_bash`. Do not copy `provider_builtin`, `git_diff`, or `network` from a generic Codex role; Pi cannot map those categories. `compile`/`validate` rejects unsupported Pi categories before quick-start persists runtime state.
31
+
30
32
  ## Operate
31
33
 
32
34
  - Send: `team-agent send TO MESSAGE` using an in-team short name or fully qualified `<workspace>::<team>/<agent>`; these are co-equal positional TO forms. For example, `team-agent send reviewer "..."` uses an in-team short name. Do not poll after success.
33
35
  - Inspect: `team-agent status` or `team-agent status --json`; `ok: true` with `ready: false` is not a crash.
36
+ - `quick-start` / `restart` / `claim-leader` JSON is compact by default (`ok`, `status`/`reason`, next action, attach/send, and `readiness.all_workers_spawned`); pass `--detail` only for internal diagnostics.
34
37
  - On a structured failure with an `action`, run that action first, then stop. Never guess flags.
35
38
  - Never rewrite a user-supplied model id or read `.env` files.
36
39