@team-agent/installer 0.5.77 → 0.5.78

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 (83) hide show
  1. package/Cargo.lock +1 -1
  2. package/Cargo.toml +1 -1
  3. package/crates/team-agent/src/cli/adapters.rs +161 -29
  4. package/crates/team-agent/src/cli/diagnose.rs +333 -68
  5. package/crates/team-agent/src/cli/emit.rs +24 -8
  6. package/crates/team-agent/src/cli/mod.rs +543 -98
  7. package/crates/team-agent/src/cli/models.rs +362 -32
  8. package/crates/team-agent/src/cli/named_address.rs +19 -5
  9. package/crates/team-agent/src/cli/profile.rs +35 -4
  10. package/crates/team-agent/src/cli/spec.rs +7 -2
  11. package/crates/team-agent/src/cli/status_port/compact.rs +12 -8
  12. package/crates/team-agent/src/cli/status_port/snapshot.rs +15 -12
  13. package/crates/team-agent/src/cli/status_port/store.rs +1 -1
  14. package/crates/team-agent/src/cli/tests/missing_subcommands.rs +62 -8
  15. package/crates/team-agent/src/cli/tests/named_address.rs +70 -0
  16. package/crates/team-agent/src/cli/tests/verb_profile.rs +81 -6
  17. package/crates/team-agent/src/cli/types.rs +1 -0
  18. package/crates/team-agent/src/leader/lease.rs +492 -33
  19. package/crates/team-agent/src/leader/owner_bind.rs +40 -12
  20. package/crates/team-agent/src/leader/rediscover/tests.rs +65 -0
  21. package/crates/team-agent/src/leader/rediscover.rs +36 -6
  22. package/crates/team-agent/src/leader/start.rs +151 -14
  23. package/crates/team-agent/src/leader/tests/wake_start_owner.rs +15 -9
  24. package/crates/team-agent/src/lifecycle/launch/add_agent.rs +28 -227
  25. package/crates/team-agent/src/lifecycle/launch/add_agent_state.rs +0 -18
  26. package/crates/team-agent/src/lifecycle/launch/cursor_mcp.rs +94 -227
  27. package/crates/team-agent/src/lifecycle/launch/mcp_config.rs +59 -10
  28. package/crates/team-agent/src/lifecycle/launch/pi_mcp.rs +65 -18
  29. package/crates/team-agent/src/lifecycle/launch/quick_start.rs +1449 -282
  30. package/crates/team-agent/src/lifecycle/launch/readiness.rs +349 -4
  31. package/crates/team-agent/src/lifecycle/launch/spawn.rs +11 -11
  32. package/crates/team-agent/src/lifecycle/launch/spec_state.rs +35 -3
  33. package/crates/team-agent/src/lifecycle/launch/worker_env.rs +16 -0
  34. package/crates/team-agent/src/lifecycle/launch.rs +30 -7
  35. package/crates/team-agent/src/lifecycle/profile_launch.rs +37 -3
  36. package/crates/team-agent/src/lifecycle/restart/agent.rs +112 -100
  37. package/crates/team-agent/src/lifecycle/restart/common.rs +82 -52
  38. package/crates/team-agent/src/lifecycle/restart/rebuild.rs +996 -66
  39. package/crates/team-agent/src/lifecycle/restart.rs +2 -2
  40. package/crates/team-agent/src/lifecycle/tests/acceptance_b_batch_red.rs +3 -0
  41. package/crates/team-agent/src/lifecycle/tests/behavioral_diff_264_red.rs +15 -0
  42. package/crates/team-agent/src/lifecycle/tests/claude_compatible_config_red.rs +5 -0
  43. package/crates/team-agent/src/lifecycle/tests/claude_profile_launch_red.rs +11 -13
  44. package/crates/team-agent/src/lifecycle/tests/clone_agent_preserves_source_tools.rs +119 -4
  45. package/crates/team-agent/src/lifecycle/tests/communication_mode_runtime_contract_red.rs +12 -0
  46. package/crates/team-agent/src/lifecycle/tests/copilot_provider_red.rs +22 -0
  47. package/crates/team-agent/src/lifecycle/tests/core_034_real_red.rs +12 -0
  48. package/crates/team-agent/src/lifecycle/tests/cursor_mcp_overlay.rs +265 -198
  49. package/crates/team-agent/src/lifecycle/tests/cursor_require_explicit_model_red.rs +14 -30
  50. package/crates/team-agent/src/lifecycle/tests/cursor_restart_resume_red.rs +3 -0
  51. package/crates/team-agent/src/lifecycle/tests/g1_silent_faces.rs +2 -5
  52. package/crates/team-agent/src/lifecycle/tests/grok_effort_argv_red.rs +10 -0
  53. package/crates/team-agent/src/lifecycle/tests/grok_mcp_overlay_red.rs +58 -35
  54. package/crates/team-agent/src/lifecycle/tests/grok_require_explicit_model_red.rs +10 -0
  55. package/crates/team-agent/src/lifecycle/tests/grok_restart_resume_red.rs +9 -0
  56. package/crates/team-agent/src/lifecycle/tests/harvest2_a_batch_red.rs +3 -2
  57. package/crates/team-agent/src/lifecycle/tests/lane_ops.rs +0 -168
  58. package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +145 -212
  59. package/crates/team-agent/src/lifecycle/tests/phase_b_contracts.rs +16 -2
  60. package/crates/team-agent/src/lifecycle/tests/phase_golden.rs +54 -9
  61. package/crates/team-agent/src/lifecycle/tests/pi_dual_entry_red.rs +78 -0
  62. package/crates/team-agent/src/lifecycle/tests/team_in_team_identity_scope_red.rs +3 -0
  63. package/crates/team-agent/src/lifecycle/tests/test_isolation_escape_contract.rs +51 -10
  64. package/crates/team-agent/src/lifecycle/tests/verify_rs031_window_consistency_red.rs +50 -7
  65. package/crates/team-agent/src/lifecycle/tests/worker_spawn_env_red.rs +137 -0
  66. package/crates/team-agent/src/lifecycle/types.rs +8 -0
  67. package/crates/team-agent/src/mcp_server/wire.rs +140 -11
  68. package/crates/team-agent/src/messaging/delivery.rs +67 -8
  69. package/crates/team-agent/src/messaging/leader_channel.rs +56 -0
  70. package/crates/team-agent/src/messaging/tests/runtime.rs +10 -2
  71. package/crates/team-agent/src/provider/adapters/cursor_agent.rs +2 -5
  72. package/crates/team-agent/src/state/persist.rs +97 -1
  73. package/crates/team-agent/src/state/repository.rs +29 -0
  74. package/crates/team-agent/src/test_support/models_timeout_fixture.rs +43 -21
  75. package/crates/team-agent/src/tmux_backend/tests.rs +121 -4
  76. package/crates/team-agent/src/tmux_backend.rs +208 -118
  77. package/crates/team-agent/src/transport.rs +72 -0
  78. package/crates/team-agent/src/transport_factory.rs +53 -0
  79. package/package.json +4 -5
  80. package/skills/team-agent/SKILL.md +45 -17
  81. package/skills/team-agent/command-coverage.json +4 -29
  82. package/npm/check-skill-links.mjs +0 -88
  83. package/skills/team-agent/references/team-agent-operator.md +0 -672
@@ -65,10 +65,11 @@ use crate::provider::wire::{parse_provider, provider_wire};
65
65
  use crate::transport::{
66
66
  command_basename, normalize_capture, tmux_capture_argv, tmux_empty_inject_argv,
67
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,
68
+ tmux_spawn_argv, AttachOutcome, BackendKind, BusySignalKind, CaptureRange,
69
+ CaptureSampleOutcome, CapturedText, InjectPayload, InjectReport, InjectStage,
70
+ InjectVerification, InputSurfaceProbe, Key, PaneField, PaneId, PaneInfo, PaneMode,
71
+ QueryOutcome, SessionName, SetEnvOutcome, SpawnResult, SubmitAttemptObservation,
72
+ SubmitConsumptionReason, SubmitDiagnostics, SubmitObserver, SubmitVerification, Target,
72
73
  Transport, TransportError, TurnVerification, WindowName,
73
74
  };
74
75
 
@@ -242,8 +243,6 @@ impl TmuxSocketEndpoint {
242
243
  pub(crate) enum RuntimeTmuxEndpointSource {
243
244
  StateTmuxEndpoint,
244
245
  StateTmuxSocket,
245
- LeaderReceiverTmuxEndpoint,
246
- LeaderReceiverTmuxSocket,
247
246
  WorkspaceFallback,
248
247
  }
249
248
 
@@ -257,8 +256,6 @@ impl RuntimeTmuxEndpointSource {
257
256
  match self {
258
257
  Self::StateTmuxEndpoint => "state.tmux_endpoint",
259
258
  Self::StateTmuxSocket => "state.tmux_socket",
260
- Self::LeaderReceiverTmuxEndpoint => "leader_receiver.tmux_endpoint",
261
- Self::LeaderReceiverTmuxSocket => "leader_receiver.tmux_socket",
262
259
  Self::WorkspaceFallback => "workspace_fallback",
263
260
  }
264
261
  }
@@ -671,50 +668,6 @@ fn runtime_tmux_endpoint_from_state(
671
668
  })
672
669
  }
673
670
 
674
- /// ---
675
- /// purpose: 读取 selected team's persisted leader receiver endpoint
676
- /// params:
677
- /// state: 运行期全量或 selected-team 投影 JSON
678
- /// returns: nested selected receiver 优先,其次投影顶层 receiver;二者都缺则 None
679
- /// boundary: 只读 owning receiver,不读取普通 tmux_endpoint,不探活
680
- /// ---
681
- pub(crate) fn owning_tmux_endpoint_from_state(
682
- state: &serde_json::Value,
683
- ) -> Option<(&str, RuntimeTmuxEndpointSource)> {
684
- fn receiver_endpoint(state: &serde_json::Value) -> Option<(&str, RuntimeTmuxEndpointSource)> {
685
- state
686
- .pointer("/leader_receiver/tmux_endpoint")
687
- .and_then(serde_json::Value::as_str)
688
- .filter(|endpoint| !endpoint.is_empty())
689
- .map(|endpoint| {
690
- (
691
- endpoint,
692
- RuntimeTmuxEndpointSource::LeaderReceiverTmuxEndpoint,
693
- )
694
- })
695
- .or_else(|| {
696
- state
697
- .pointer("/leader_receiver/tmux_socket")
698
- .and_then(serde_json::Value::as_str)
699
- .filter(|endpoint| !endpoint.is_empty())
700
- .map(|endpoint| {
701
- (
702
- endpoint,
703
- RuntimeTmuxEndpointSource::LeaderReceiverTmuxSocket,
704
- )
705
- })
706
- })
707
- }
708
-
709
- state
710
- .get("active_team_key")
711
- .and_then(serde_json::Value::as_str)
712
- .filter(|team_key| !team_key.is_empty())
713
- .and_then(|team_key| state.get("teams").and_then(|teams| teams.get(team_key)))
714
- .and_then(receiver_endpoint)
715
- .or_else(|| receiver_endpoint(state))
716
- }
717
-
718
671
  /// CP-1 socket name: SHORT + DETERMINISTIC per canonical workspace path. `ta-` + 12 hex chars of a
719
672
  /// stable FNV-1a hash over the canonicalized path. AF_UNIX `sun_path` is ~104 chars and the socket
720
673
  /// lives at `/tmp/tmux-<uid>/<name>`, so we must NOT use the (~88-char) session name. §10: a
@@ -758,10 +711,6 @@ pub fn runtime_tmux_endpoint_from_state_pub(
758
711
  let src = match source {
759
712
  RuntimeTmuxEndpointSource::StateTmuxEndpoint => "state.tmux_endpoint",
760
713
  RuntimeTmuxEndpointSource::StateTmuxSocket => "state.tmux_socket",
761
- RuntimeTmuxEndpointSource::LeaderReceiverTmuxEndpoint => {
762
- "leader_receiver.tmux_endpoint"
763
- }
764
- RuntimeTmuxEndpointSource::LeaderReceiverTmuxSocket => "leader_receiver.tmux_socket",
765
714
  RuntimeTmuxEndpointSource::WorkspaceFallback => "workspace_fallback",
766
715
  };
767
716
  (endpoint.to_string(), src)
@@ -1530,6 +1479,14 @@ fn submit_diagnostics_for_inject(
1530
1479
  surface: InjectTargetSurface,
1531
1480
  ) -> SubmitDiagnostics {
1532
1481
  SubmitDiagnostics {
1482
+ consumption_reason: SubmitConsumptionReason::Unknown,
1483
+ // No pre-paste capture exists on this path; keep the diagnostic null.
1484
+ before_busy_signal_kind: None,
1485
+ busy_signal_kind: None,
1486
+ busy_line_from_bottom: None,
1487
+ current_marker_in_bottom_15: None,
1488
+ paste_identity_in_composer: None,
1489
+ consumption_from_capture_result: None,
1533
1490
  appear_gate_elapsed_ms: 0,
1534
1491
  appear_gate_matched: false,
1535
1492
  total_elapsed_ms,
@@ -2118,6 +2075,56 @@ pub(crate) fn consumption_from_capture(
2118
2075
  }
2119
2076
  }
2120
2077
 
2078
+ #[derive(Debug, Clone, Copy, Default)]
2079
+ struct ConsumptionCaptureObservation {
2080
+ busy: Option<BusySignalMatch>,
2081
+ marker_in_bottom_15: Option<bool>,
2082
+ paste_identity_in_composer: Option<bool>,
2083
+ consumption_from_capture_result: Option<bool>,
2084
+ }
2085
+
2086
+ fn consumption_capture_observation(
2087
+ text: &str,
2088
+ marker: Option<&str>,
2089
+ token_ever_visible: bool,
2090
+ tracked: Option<PasteLatch>,
2091
+ ) -> ConsumptionCaptureObservation {
2092
+ let Some(marker) = marker else {
2093
+ return ConsumptionCaptureObservation::default();
2094
+ };
2095
+ ConsumptionCaptureObservation {
2096
+ busy: provider_busy_signal_match(text),
2097
+ marker_in_bottom_15: Some(token_in_bottom_n(text, marker, 15)),
2098
+ paste_identity_in_composer: tracked
2099
+ .map(|tracked| this_paste_identity_in_composer(text, marker, Some(tracked))),
2100
+ consumption_from_capture_result: consumption_from_capture(
2101
+ text,
2102
+ marker,
2103
+ token_ever_visible,
2104
+ tracked,
2105
+ ),
2106
+ }
2107
+ }
2108
+
2109
+ #[derive(Debug, Clone, Copy, Default)]
2110
+ struct ConsumptionDiagnostics {
2111
+ busy_signal_kind: Option<BusySignalKind>,
2112
+ busy_line_from_bottom: Option<u8>,
2113
+ current_marker_in_bottom_15: Option<bool>,
2114
+ paste_identity_in_composer: Option<bool>,
2115
+ consumption_from_capture_result: Option<bool>,
2116
+ }
2117
+
2118
+ impl ConsumptionDiagnostics {
2119
+ fn record(&mut self, observation: ConsumptionCaptureObservation) {
2120
+ self.busy_signal_kind = observation.busy.map(|match_| match_.kind);
2121
+ self.busy_line_from_bottom = observation.busy.map(|match_| match_.line_from_bottom);
2122
+ self.current_marker_in_bottom_15 = observation.marker_in_bottom_15;
2123
+ self.paste_identity_in_composer = observation.paste_identity_in_composer;
2124
+ self.consumption_from_capture_result = observation.consumption_from_capture_result;
2125
+ }
2126
+ }
2127
+
2121
2128
  /// ---
2122
2129
  /// purpose: Gone 分支在判已消费前复核 composer 占位符(PasteLatch 身份)
2123
2130
  /// contract: 锁定身份仍在,或未锁定但 grok `[Pasted: …]` 仍在 ⇒ Some(false)
@@ -2147,37 +2154,67 @@ fn marker_position_from_bottom(text: &str, marker: &str) -> Option<u32> {
2147
2154
  None
2148
2155
  }
2149
2156
 
2150
- fn provider_busy_signal_in_tail(text: &str) -> bool {
2157
+ #[derive(Debug, Clone, Copy, PartialEq, Eq)]
2158
+ struct BusySignalMatch {
2159
+ kind: BusySignalKind,
2160
+ line_from_bottom: u8,
2161
+ }
2162
+
2163
+ fn busy_signal_kind_in_line(line: &str) -> Option<BusySignalKind> {
2164
+ let lower = line.to_ascii_lowercase();
2165
+ if lower.contains("working") {
2166
+ return Some(BusySignalKind::Working);
2167
+ }
2168
+ if lower.contains("thinking") {
2169
+ return Some(BusySignalKind::Thinking);
2170
+ }
2171
+ if lower.contains("processing") {
2172
+ return Some(BusySignalKind::Processing);
2173
+ }
2174
+ if lower.contains("esc to interrupt") {
2175
+ return Some(BusySignalKind::EscToInterrupt);
2176
+ }
2177
+ if line.contains('●')
2178
+ || line.contains('⏳')
2179
+ || line.contains('⠋')
2180
+ || line.contains('⠙')
2181
+ || line.contains('⠹')
2182
+ || line.contains('⠸')
2183
+ || line.contains('⠼')
2184
+ || line.contains('⠴')
2185
+ || line.contains('⠦')
2186
+ || line.contains('⠧')
2187
+ || line.contains('⠇')
2188
+ || line.contains('⠏')
2189
+ || line.contains('✶')
2190
+ || line.contains('✢')
2191
+ || line.contains('✻')
2192
+ || line.contains('✽')
2193
+ || line.contains('✳')
2194
+ {
2195
+ return Some(BusySignalKind::SpinnerGlyph);
2196
+ }
2197
+ None
2198
+ }
2199
+
2200
+ fn provider_busy_signal_match(text: &str) -> Option<BusySignalMatch> {
2151
2201
  text.lines()
2152
2202
  .rev()
2153
2203
  .filter(|line| !line.trim().is_empty())
2154
2204
  .take(15)
2155
- .any(|line| {
2156
- let lower = line.to_ascii_lowercase();
2157
- lower.contains("working")
2158
- || lower.contains("thinking")
2159
- || lower.contains("processing")
2160
- || lower.contains("esc to interrupt")
2161
- || line.contains('●')
2162
- || line.contains('⏳')
2163
- || line.contains('⠋')
2164
- || line.contains('⠙')
2165
- || line.contains('⠹')
2166
- || line.contains('⠸')
2167
- || line.contains('⠼')
2168
- || line.contains('⠴')
2169
- || line.contains('⠦')
2170
- || line.contains('⠧')
2171
- || line.contains('⠇')
2172
- || line.contains('⠏')
2173
- || line.contains('✶')
2174
- || line.contains('✢')
2175
- || line.contains('✻')
2176
- || line.contains('✽')
2177
- || line.contains('✳')
2205
+ .enumerate()
2206
+ .find_map(|(line_from_bottom, line)| {
2207
+ busy_signal_kind_in_line(line).map(|kind| BusySignalMatch {
2208
+ kind,
2209
+ line_from_bottom: line_from_bottom as u8,
2210
+ })
2178
2211
  })
2179
2212
  }
2180
2213
 
2214
+ fn provider_busy_signal_in_tail(text: &str) -> bool {
2215
+ provider_busy_signal_match(text).is_some()
2216
+ }
2217
+
2181
2218
  /// ---
2182
2219
  /// purpose: 入箱 vs 开跑(G4)。给定最近一次 pane 尾,回答席位是否因此开跑。
2183
2220
  /// params:
@@ -3227,6 +3264,8 @@ impl Transport for TmuxBackend {
3227
3264
  let mut consumption_attempts: u32 = 0;
3228
3265
  let mut capture_retries: u32 = 0;
3229
3266
  let mut consumed: Option<bool> = None;
3267
+ let mut consumption_reason = SubmitConsumptionReason::Unknown;
3268
+ let mut consumption_diagnostics = ConsumptionDiagnostics::default();
3230
3269
  let mut attempts_detail: Vec<SubmitAttemptObservation> = Vec::new();
3231
3270
  let mut any_attempt_matched = false;
3232
3271
 
@@ -3267,6 +3306,13 @@ impl Transport for TmuxBackend {
3267
3306
  token_seen_after_enter = Some(false);
3268
3307
  }
3269
3308
  tracked_paste = latch_paste(&cap.text, tracked_paste);
3309
+ let observation = consumption_capture_observation(
3310
+ &cap.text,
3311
+ marker,
3312
+ token_ever_visible,
3313
+ tracked_paste,
3314
+ );
3315
+ consumption_diagnostics.record(observation);
3270
3316
  let obs = submit_attempt_observation(
3271
3317
  attempt_index,
3272
3318
  &cap,
@@ -3279,18 +3325,16 @@ impl Transport for TmuxBackend {
3279
3325
  attempts_detail.push(obs);
3280
3326
  // NeverSeen 不得把 token 缺席写成 consumed;
3281
3327
  // 见过再消失,或本次占位符身份离开 composer,才停手。
3282
- if consumption_from_capture(
3283
- &cap.text,
3284
- m,
3285
- token_ever_visible,
3286
- tracked_paste,
3287
- ) == Some(true)
3288
- {
3328
+ if observation.consumption_from_capture_result == Some(true) {
3329
+ consumption_reason =
3330
+ SubmitConsumptionReason::ConsumptionFromCapture;
3289
3331
  consumed = Some(true);
3290
3332
  break;
3291
3333
  }
3292
3334
  if cursor_single_enter_enabled() {
3293
- if provider_busy_signal_in_tail(&cap.text) {
3335
+ if observation.busy.is_some() {
3336
+ consumption_reason =
3337
+ SubmitConsumptionReason::CursorPollingBusy;
3294
3338
  consumed = Some(true);
3295
3339
  break;
3296
3340
  }
@@ -3375,6 +3419,13 @@ impl Transport for TmuxBackend {
3375
3419
  token_seen_after_enter = Some(false);
3376
3420
  }
3377
3421
  tracked_paste = latch_paste(&cap.text, tracked_paste);
3422
+ let observation = consumption_capture_observation(
3423
+ &cap.text,
3424
+ marker,
3425
+ token_ever_visible,
3426
+ tracked_paste,
3427
+ );
3428
+ consumption_diagnostics.record(observation);
3378
3429
  let obs = submit_attempt_observation(
3379
3430
  attempt_index,
3380
3431
  &cap,
@@ -3385,24 +3436,22 @@ impl Transport for TmuxBackend {
3385
3436
  any_attempt_matched = true;
3386
3437
  }
3387
3438
  attempts_detail.push(obs);
3388
- if consumption_from_capture(
3389
- &cap.text,
3390
- m,
3391
- token_ever_visible,
3392
- tracked_paste,
3393
- ) == Some(true)
3394
- {
3439
+ if observation.consumption_from_capture_result == Some(true) {
3440
+ consumption_reason =
3441
+ SubmitConsumptionReason::ConsumptionFromCapture;
3395
3442
  found_consumed = true;
3396
3443
  break;
3397
3444
  }
3398
- if cursor_single_enter_enabled()
3399
- && provider_busy_signal_in_tail(&cap.text)
3400
- {
3445
+ if cursor_single_enter_enabled() && observation.busy.is_some() {
3446
+ consumption_reason =
3447
+ SubmitConsumptionReason::CursorPollingBusy;
3401
3448
  found_consumed = true;
3402
3449
  break;
3403
3450
  }
3404
3451
  }
3405
3452
  Err(_) => {
3453
+ consumption_diagnostics
3454
+ .record(ConsumptionCaptureObservation::default());
3406
3455
  capture_tally.record_err();
3407
3456
  capture_retries = capture_retries.saturating_add(1);
3408
3457
  consecutive_capture_failures =
@@ -3426,7 +3475,8 @@ impl Transport for TmuxBackend {
3426
3475
  }
3427
3476
  } else {
3428
3477
  // Non-token payload: single Enter, no consumption check
3429
- // (0.3.27). Not 「没能判断」— 本门不覆盖无 token 路径。
3478
+ // (0.3.27). Keep the explicit no-marker reason.
3479
+ consumption_reason = SubmitConsumptionReason::NoMarker;
3430
3480
  consumed = Some(true);
3431
3481
  break;
3432
3482
  }
@@ -3451,21 +3501,38 @@ impl Transport for TmuxBackend {
3451
3501
  Some(false) => match self.capture(target, CaptureRange::Tail(15)) {
3452
3502
  Ok(cap) => {
3453
3503
  last_turn_text = Some(cap.text.clone());
3504
+ let observation = consumption_capture_observation(
3505
+ &cap.text,
3506
+ marker,
3507
+ token_ever_visible,
3508
+ tracked_paste,
3509
+ );
3510
+ consumption_diagnostics.record(observation);
3454
3511
  attempts_detail.push(submit_attempt_observation(
3455
3512
  consumption_attempts.max(1),
3456
3513
  &cap,
3457
3514
  marker,
3458
3515
  inject_start.elapsed().as_millis() as u64,
3459
3516
  ));
3460
- if provider_busy_signal_in_tail(&cap.text) {
3517
+ if observation.busy.is_some() {
3518
+ consumption_reason = SubmitConsumptionReason::FallbackBusy;
3461
3519
  SubmitVerification::EnterSentWithoutPlaceholderCheck
3462
3520
  } else {
3521
+ consumption_reason = SubmitConsumptionReason::Unverified;
3463
3522
  SubmitVerification::SubmitConsumptionUnverified
3464
3523
  }
3465
3524
  }
3466
- Err(_) => SubmitVerification::SubmitConsumptionUnverified,
3525
+ Err(_) => {
3526
+ consumption_diagnostics
3527
+ .record(ConsumptionCaptureObservation::default());
3528
+ consumption_reason = SubmitConsumptionReason::Unverified;
3529
+ SubmitVerification::SubmitConsumptionUnverified
3530
+ }
3467
3531
  },
3468
- None => SubmitVerification::SubmitConsumptionUnverified,
3532
+ None => {
3533
+ consumption_reason = SubmitConsumptionReason::Unverified;
3534
+ SubmitVerification::SubmitConsumptionUnverified
3535
+ }
3469
3536
  };
3470
3537
  // Unverified + 折行缺口:只补一颗 C-m。A 的 should_resubmit_enter
3471
3538
  // 已为真(latch/底15 token,含打满 cap)时 B 不介入。身份拿不到
@@ -3511,6 +3578,13 @@ impl Transport for TmuxBackend {
3511
3578
  token_seen_after_enter = Some(false);
3512
3579
  }
3513
3580
  tracked_paste = latch_paste(&cap.text, tracked_paste);
3581
+ let observation = consumption_capture_observation(
3582
+ &cap.text,
3583
+ marker,
3584
+ token_ever_visible,
3585
+ tracked_paste,
3586
+ );
3587
+ consumption_diagnostics.record(observation);
3514
3588
  let obs = submit_attempt_observation(
3515
3589
  consumption_attempts.max(1),
3516
3590
  &cap,
@@ -3519,20 +3593,22 @@ impl Transport for TmuxBackend {
3519
3593
  );
3520
3594
  attempts_detail.push(obs);
3521
3595
  last_turn_text = Some(cap.text.clone());
3522
- if consumption_from_capture(
3523
- &cap.text,
3524
- m,
3525
- token_ever_visible,
3526
- tracked_paste,
3527
- ) == Some(true)
3528
- || provider_busy_signal_in_tail(&cap.text)
3529
- {
3596
+ let capture_consumed = observation
3597
+ .consumption_from_capture_result
3598
+ == Some(true);
3599
+ let busy = observation.busy.is_some();
3600
+ if capture_consumed || busy {
3530
3601
  // Gone+无 latch 会把折行 token 写成已消费;身份仍在则不算。
3531
3602
  if !this_paste_identity_in_composer(
3532
3603
  &cap.text,
3533
3604
  m,
3534
3605
  tracked_paste,
3535
3606
  ) {
3607
+ consumption_reason = if capture_consumed {
3608
+ SubmitConsumptionReason::ConsumptionFromCapture
3609
+ } else {
3610
+ SubmitConsumptionReason::FallbackBusy
3611
+ };
3536
3612
  found_consumed = true;
3537
3613
  break;
3538
3614
  }
@@ -3542,11 +3618,14 @@ impl Transport for TmuxBackend {
3542
3618
  m,
3543
3619
  tracked_paste,
3544
3620
  ) {
3621
+ consumption_reason = SubmitConsumptionReason::Unknown;
3545
3622
  found_consumed = true;
3546
3623
  break;
3547
3624
  }
3548
3625
  }
3549
3626
  Err(_) => {
3627
+ consumption_diagnostics
3628
+ .record(ConsumptionCaptureObservation::default());
3550
3629
  capture_tally.record_err();
3551
3630
  break;
3552
3631
  }
@@ -3562,6 +3641,24 @@ impl Transport for TmuxBackend {
3562
3641
  }
3563
3642
  let total_elapsed_ms = inject_start.elapsed().as_millis() as u64;
3564
3643
  let surface = probe_inject_target_surface(self, target);
3644
+ let mut submit_diagnostics = submit_diagnostics_for_inject(
3645
+ total_elapsed_ms,
3646
+ attempts_detail,
3647
+ &capture_tally,
3648
+ token_seen_after_paste,
3649
+ token_seen_after_enter,
3650
+ surface,
3651
+ );
3652
+ submit_diagnostics.consumption_reason = consumption_reason;
3653
+ submit_diagnostics.busy_signal_kind = consumption_diagnostics.busy_signal_kind;
3654
+ submit_diagnostics.busy_line_from_bottom =
3655
+ consumption_diagnostics.busy_line_from_bottom;
3656
+ submit_diagnostics.current_marker_in_bottom_15 =
3657
+ consumption_diagnostics.current_marker_in_bottom_15;
3658
+ submit_diagnostics.paste_identity_in_composer =
3659
+ consumption_diagnostics.paste_identity_in_composer;
3660
+ submit_diagnostics.consumption_from_capture_result =
3661
+ consumption_diagnostics.consumption_from_capture_result;
3565
3662
  return Ok(InjectReport {
3566
3663
  stage_reached: InjectStage::Submit,
3567
3664
  inject_verification: inject_verification_after_readback(
@@ -3574,14 +3671,7 @@ impl Transport for TmuxBackend {
3574
3671
  last_turn_text.as_deref(),
3575
3672
  ),
3576
3673
  attempts: consumption_attempts.saturating_add(capture_retries),
3577
- submit_diagnostics: Some(submit_diagnostics_for_inject(
3578
- total_elapsed_ms,
3579
- attempts_detail,
3580
- &capture_tally,
3581
- token_seen_after_paste,
3582
- token_seen_after_enter,
3583
- surface,
3584
- )),
3674
+ submit_diagnostics: Some(submit_diagnostics),
3585
3675
  });
3586
3676
  }
3587
3677
  }
@@ -467,6 +467,21 @@ pub struct InjectReport {
467
467
  /// pasted-prompt branch + (informational) for the appear-gate poll.
468
468
  #[derive(Debug, Clone, PartialEq, Eq, Default)]
469
469
  pub struct SubmitDiagnostics {
470
+ /// The concrete branch that produced the post-Enter consumption outcome.
471
+ /// Unknown is retained for paths that do not expose a branch reason.
472
+ pub consumption_reason: SubmitConsumptionReason,
473
+ /// Reserved for a pre-paste baseline; this path has no such capture and emits null.
474
+ pub before_busy_signal_kind: Option<BusySignalKind>,
475
+ /// Structured busy predicate category for the accepted/current capture.
476
+ pub busy_signal_kind: Option<BusySignalKind>,
477
+ /// Bottom-relative non-empty line index of the busy match.
478
+ pub busy_line_from_bottom: Option<u8>,
479
+ /// Whether this message marker is in the bottom 15 non-empty lines.
480
+ pub current_marker_in_bottom_15: Option<bool>,
481
+ /// Whether the tracked paste identity is still in the composer.
482
+ pub paste_identity_in_composer: Option<bool>,
483
+ /// Result of the structural consumption predicate on the current capture.
484
+ pub consumption_from_capture_result: Option<bool>,
470
485
  /// Time spent in the appear-gate (poll for the pasted-content placeholder
471
486
  /// before Enter). When `saw_pasted_prompt == false` this is the time we
472
487
  /// spent polling before falling through to the E46 token path.
@@ -505,6 +520,63 @@ pub struct SubmitDiagnostics {
505
520
  pub target_pane_query_matched: Option<bool>,
506
521
  }
507
522
 
523
+ /// Structured category returned by the provider-busy predicate.
524
+ ///
525
+ /// This is diagnostic metadata only; it does not change delivery semantics.
526
+ #[derive(Debug, Clone, Copy, PartialEq, Eq)]
527
+ pub enum BusySignalKind {
528
+ Working,
529
+ Thinking,
530
+ Processing,
531
+ EscToInterrupt,
532
+ SpinnerGlyph,
533
+ }
534
+
535
+ impl BusySignalKind {
536
+ pub fn as_str(self) -> &'static str {
537
+ match self {
538
+ Self::Working => "working",
539
+ Self::Thinking => "thinking",
540
+ Self::Processing => "processing",
541
+ Self::EscToInterrupt => "esc_to_interrupt",
542
+ Self::SpinnerGlyph => "spinner_glyph",
543
+ }
544
+ }
545
+ }
546
+
547
+ /// Structured reason for the post-Enter consumption observation.
548
+ ///
549
+ /// This is diagnostic metadata only; it does not change delivery semantics.
550
+ #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
551
+ pub enum SubmitConsumptionReason {
552
+ /// The token/input identity disappeared in the consumption capture.
553
+ ConsumptionFromCapture,
554
+ /// Cursor's bounded polling observed a provider-busy signal.
555
+ CursorPollingBusy,
556
+ /// The final fallback capture observed a provider-busy signal.
557
+ FallbackBusy,
558
+ /// The payload had no token marker, so no structural consumption probe ran.
559
+ NoMarker,
560
+ /// The bounded probe did not establish consumption.
561
+ Unverified,
562
+ /// An older or otherwise uncovered path did not provide a reason.
563
+ #[default]
564
+ Unknown,
565
+ }
566
+
567
+ impl SubmitConsumptionReason {
568
+ pub fn as_str(self) -> &'static str {
569
+ match self {
570
+ Self::ConsumptionFromCapture => "consumption_from_capture",
571
+ Self::CursorPollingBusy => "cursor_poll_busy",
572
+ Self::FallbackBusy => "fallback_busy",
573
+ Self::NoMarker => "no_marker",
574
+ Self::Unverified => "unverified",
575
+ Self::Unknown => "unknown",
576
+ }
577
+ }
578
+ }
579
+
508
580
  /// Last capture sample of the inject token path. Distinguishes "read failed"
509
581
  /// from "read succeeded and token was absent". Does not change delivery gates.
510
582
  #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
@@ -51,6 +51,59 @@ use crate::conpty::ConPtyBackend;
51
51
  use crate::tmux_backend::TmuxBackend;
52
52
  use crate::transport::{BackendKind, Transport};
53
53
 
54
+ #[cfg(test)]
55
+ thread_local! {
56
+ static LEADER_ENDPOINT_TRANSPORT_OVERRIDE: std::cell::RefCell<
57
+ Option<(String, crate::transport::test_support::OfflineTransport)>,
58
+ > = std::cell::RefCell::new(None);
59
+ }
60
+
61
+ /// Production live-probe transport for a recorded leader endpoint.
62
+ /// `classify_leader_binding`, restart `transport_for_receiver`, and
63
+ /// `attach_leader_targets` all go through this function.
64
+ pub fn leader_endpoint_transport(endpoint: &str) -> Box<dyn Transport> {
65
+ #[cfg(test)]
66
+ if let Some(injected) = LEADER_ENDPOINT_TRANSPORT_OVERRIDE.with(|slot| {
67
+ slot.borrow().as_ref().and_then(|(expected, transport)| {
68
+ (expected == endpoint).then(|| Box::new(transport.clone()) as Box<dyn Transport>)
69
+ })
70
+ }) {
71
+ return injected;
72
+ }
73
+ if endpoint.is_empty() || endpoint == "default" {
74
+ Box::new(tmux_default_transport())
75
+ } else {
76
+ Box::new(tmux_endpoint_transport(endpoint))
77
+ }
78
+ }
79
+
80
+ #[cfg(test)]
81
+ struct LeaderEndpointTransportGuard {
82
+ previous: Option<(String, crate::transport::test_support::OfflineTransport)>,
83
+ }
84
+
85
+ #[cfg(test)]
86
+ impl Drop for LeaderEndpointTransportGuard {
87
+ fn drop(&mut self) {
88
+ LEADER_ENDPOINT_TRANSPORT_OVERRIDE.with(|slot| {
89
+ *slot.borrow_mut() = self.previous.take();
90
+ });
91
+ }
92
+ }
93
+
94
+ #[cfg(test)]
95
+ pub fn with_leader_endpoint_transport<T>(
96
+ endpoint: &str,
97
+ transport: crate::transport::test_support::OfflineTransport,
98
+ f: impl FnOnce() -> T,
99
+ ) -> T {
100
+ let previous = LEADER_ENDPOINT_TRANSPORT_OVERRIDE.with(|slot| {
101
+ slot.replace(Some((endpoint.to_string(), transport)))
102
+ });
103
+ let _guard = LeaderEndpointTransportGuard { previous };
104
+ f()
105
+ }
106
+
54
107
  /// User-visible backend selector on the CLI (`--backend tmux|conpty`).
55
108
  #[derive(Debug, Clone, Copy, PartialEq, Eq)]
56
109
  pub enum RequestedTransportBackend {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@team-agent/installer",
3
- "version": "0.5.77",
3
+ "version": "0.5.78",
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.77",
24
- "@team-agent/cli-darwin-x64": "0.5.77",
25
- "@team-agent/cli-linux-x64": "0.5.77"
23
+ "@team-agent/cli-darwin-arm64": "0.5.78",
24
+ "@team-agent/cli-darwin-x64": "0.5.78",
25
+ "@team-agent/cli-linux-x64": "0.5.78"
26
26
  },
27
27
  "scripts": {
28
28
  "postinstall": "node npm/bincheck.mjs",
@@ -33,7 +33,6 @@
33
33
  "files": [
34
34
  "npm/bincheck.mjs",
35
35
  "npm/install.mjs",
36
- "npm/check-skill-links.mjs",
37
36
  "skills",
38
37
  "templates",
39
38
  "examples",