@team-agent/installer 0.5.77 → 0.5.80

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 (95) 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 +393 -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/runtime.rs +3 -1
  13. package/crates/team-agent/src/cli/status_port/snapshot.rs +15 -12
  14. package/crates/team-agent/src/cli/status_port/store.rs +1 -1
  15. package/crates/team-agent/src/cli/status_port/tests.rs +37 -1
  16. package/crates/team-agent/src/cli/tests/missing_subcommands.rs +62 -8
  17. package/crates/team-agent/src/cli/tests/named_address.rs +70 -0
  18. package/crates/team-agent/src/cli/tests/verb_profile.rs +81 -6
  19. package/crates/team-agent/src/cli/types.rs +1 -0
  20. package/crates/team-agent/src/coordinator/steps/abnormal.rs +122 -2
  21. package/crates/team-agent/src/leader/lease.rs +492 -33
  22. package/crates/team-agent/src/leader/owner_bind.rs +40 -12
  23. package/crates/team-agent/src/leader/rediscover/tests.rs +65 -0
  24. package/crates/team-agent/src/leader/rediscover.rs +36 -6
  25. package/crates/team-agent/src/leader/start.rs +197 -88
  26. package/crates/team-agent/src/leader/tests/wake_start_owner.rs +15 -9
  27. package/crates/team-agent/src/lifecycle/launch/add_agent.rs +28 -227
  28. package/crates/team-agent/src/lifecycle/launch/add_agent_state.rs +0 -18
  29. package/crates/team-agent/src/lifecycle/launch/cursor_mcp.rs +94 -227
  30. package/crates/team-agent/src/lifecycle/launch/mcp_config.rs +59 -10
  31. package/crates/team-agent/src/lifecycle/launch/pi_mcp.rs +86 -23
  32. package/crates/team-agent/src/lifecycle/launch/quick_start.rs +1449 -282
  33. package/crates/team-agent/src/lifecycle/launch/readiness.rs +349 -4
  34. package/crates/team-agent/src/lifecycle/launch/spawn.rs +11 -11
  35. package/crates/team-agent/src/lifecycle/launch/spec_state.rs +35 -3
  36. package/crates/team-agent/src/lifecycle/launch/worker_env.rs +16 -0
  37. package/crates/team-agent/src/lifecycle/launch.rs +30 -7
  38. package/crates/team-agent/src/lifecycle/profile_launch.rs +37 -3
  39. package/crates/team-agent/src/lifecycle/restart/agent.rs +112 -100
  40. package/crates/team-agent/src/lifecycle/restart/common.rs +82 -52
  41. package/crates/team-agent/src/lifecycle/restart/rebuild.rs +996 -66
  42. package/crates/team-agent/src/lifecycle/restart.rs +2 -2
  43. package/crates/team-agent/src/lifecycle/tests/acceptance_b_batch_red.rs +3 -0
  44. package/crates/team-agent/src/lifecycle/tests/behavioral_diff_264_red.rs +15 -0
  45. package/crates/team-agent/src/lifecycle/tests/claude_compatible_config_red.rs +5 -0
  46. package/crates/team-agent/src/lifecycle/tests/claude_profile_launch_red.rs +11 -13
  47. package/crates/team-agent/src/lifecycle/tests/clone_agent_preserves_source_tools.rs +119 -4
  48. package/crates/team-agent/src/lifecycle/tests/communication_mode_runtime_contract_red.rs +12 -0
  49. package/crates/team-agent/src/lifecycle/tests/copilot_provider_red.rs +22 -0
  50. package/crates/team-agent/src/lifecycle/tests/core_034_real_red.rs +12 -0
  51. package/crates/team-agent/src/lifecycle/tests/cursor_mcp_overlay.rs +265 -198
  52. package/crates/team-agent/src/lifecycle/tests/cursor_require_explicit_model_red.rs +14 -30
  53. package/crates/team-agent/src/lifecycle/tests/cursor_restart_resume_red.rs +3 -0
  54. package/crates/team-agent/src/lifecycle/tests/fixtures/pi_list_models_spaced_ids.stdout.txt +9 -0
  55. package/crates/team-agent/src/lifecycle/tests/g1_silent_faces.rs +2 -5
  56. package/crates/team-agent/src/lifecycle/tests/grok_effort_argv_red.rs +10 -0
  57. package/crates/team-agent/src/lifecycle/tests/grok_mcp_overlay_red.rs +58 -35
  58. package/crates/team-agent/src/lifecycle/tests/grok_require_explicit_model_red.rs +10 -0
  59. package/crates/team-agent/src/lifecycle/tests/grok_restart_resume_red.rs +9 -0
  60. package/crates/team-agent/src/lifecycle/tests/harvest2_a_batch_red.rs +3 -2
  61. package/crates/team-agent/src/lifecycle/tests/lane_ops.rs +0 -168
  62. package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +145 -212
  63. package/crates/team-agent/src/lifecycle/tests/phase_b_contracts.rs +16 -2
  64. package/crates/team-agent/src/lifecycle/tests/phase_golden.rs +54 -9
  65. package/crates/team-agent/src/lifecycle/tests/pi_catalog_red.rs +71 -0
  66. package/crates/team-agent/src/lifecycle/tests/pi_dual_entry_red.rs +78 -0
  67. package/crates/team-agent/src/lifecycle/tests/team_in_team_identity_scope_red.rs +3 -0
  68. package/crates/team-agent/src/lifecycle/tests/test_isolation_escape_contract.rs +51 -10
  69. package/crates/team-agent/src/lifecycle/tests/verify_rs031_window_consistency_red.rs +50 -7
  70. package/crates/team-agent/src/lifecycle/tests/worker_spawn_env_red.rs +137 -0
  71. package/crates/team-agent/src/lifecycle/types.rs +8 -0
  72. package/crates/team-agent/src/mcp_server/wire.rs +140 -11
  73. package/crates/team-agent/src/messaging/delivery.rs +221 -80
  74. package/crates/team-agent/src/messaging/leader_channel.rs +56 -0
  75. package/crates/team-agent/src/messaging/tests/leader_inject_acceptance.rs +127 -0
  76. package/crates/team-agent/src/messaging/tests/runtime.rs +10 -2
  77. package/crates/team-agent/src/provider/adapters/cursor_agent.rs +2 -5
  78. package/crates/team-agent/src/provider/session_scan/common/tests.rs +204 -0
  79. package/crates/team-agent/src/provider/session_scan/common.rs +24 -2
  80. package/crates/team-agent/src/state/abnormal_watch/tests/mod.rs +248 -0
  81. package/crates/team-agent/src/state/abnormal_watch.rs +144 -0
  82. package/crates/team-agent/src/state/mod.rs +1 -0
  83. package/crates/team-agent/src/state/persist.rs +102 -4
  84. package/crates/team-agent/src/state/projection.rs +7 -1
  85. package/crates/team-agent/src/state/repository.rs +29 -0
  86. package/crates/team-agent/src/test_support/models_timeout_fixture.rs +43 -21
  87. package/crates/team-agent/src/tmux_backend/tests.rs +149 -4
  88. package/crates/team-agent/src/tmux_backend.rs +239 -127
  89. package/crates/team-agent/src/transport.rs +78 -0
  90. package/crates/team-agent/src/transport_factory.rs +53 -0
  91. package/package.json +4 -5
  92. package/skills/team-agent/SKILL.md +45 -17
  93. package/skills/team-agent/command-coverage.json +4 -29
  94. package/npm/check-skill-links.mjs +0 -88
  95. 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:
@@ -2619,24 +2656,22 @@ fn append_caller_context_assignments(parts: &mut Vec<String>, provider: Option<&
2619
2656
  ));
2620
2657
  }
2621
2658
 
2622
- /// 0.4.x (CR R6): single-source marker prefix. The exit marker emitted by
2623
- /// `leader_shell_wrapper_command` and the substring detected by
2624
- /// `leader_provider_health` MUST share this prefix exactly. Format:
2659
+ /// Public exit-marker text emitted by `leader_shell_wrapper_command`. Format:
2625
2660
  /// `"[team-agent] {provider_label} exited with {rc}"`.
2626
2661
  pub const LEADER_PROVIDER_EXIT_MARKER_PREFIX: &str = "[team-agent]";
2627
2662
  pub const LEADER_PROVIDER_EXIT_MARKER_SUFFIX: &str = "exited with";
2663
+ const PROVIDER_EXIT_STATUS_OPTION: &str = "@team-agent-provider-exit-status";
2628
2664
 
2629
2665
  /// 0.4.x (CR R6): build the leader exit marker text for `provider_label`.
2630
- /// Used by both the shell wrapper (printf source) and the health check
2631
- /// (capture substring) so they cannot drift.
2666
+ /// Used by the shell wrapper for human-readable output.
2632
2667
  /// ---
2633
2668
  /// purpose: 单一来源地拼出 leader pane 的 provider 退出标记文本
2634
2669
  /// params:
2635
2670
  /// provider_label: 人类可读的 provider 名,原样嵌入标记
2636
2671
  /// returns: 前缀 + provider 名 + 后缀 拼成的标记(不含退出码,退出码由 shell 的 printf 补)
2637
2672
  /// boundary:
2638
- /// - 写标记的 shell wrapper 与读标记的健康检查必须都用这一个函数,禁止各写一份字面量
2639
- /// - 这是内容信号:pane 里出现同样文本(如 cat 日志)就会误触发,判定强度到此为止
2673
+ /// - shell wrapper 统一使用此显示标记,进程状态另由监督 shell 收据证明
2674
+ /// - 仅供显示;历史正文可包含相同文本,不得据此判定进程退出
2640
2675
  /// ---
2641
2676
  pub fn leader_provider_exit_marker(provider_label: &str) -> String {
2642
2677
  format!(
@@ -2868,6 +2903,11 @@ pub fn leader_shell_wrapper_command(
2868
2903
  parts.push(key.clone());
2869
2904
  parts.push("&&".to_string());
2870
2905
  }
2906
+ // Clear any prior receipt before invoking the provider. Terminal content,
2907
+ // including replayed sessions, must never be treated as an exit receipt.
2908
+ parts.push(format!(
2909
+ "{{ tmux -S \"${{TMUX%%,*}}\" set-option -p -t \"$TMUX_PANE\" {PROVIDER_EXIT_STATUS_OPTION} running >/dev/null 2>&1 || :; }} &&"
2910
+ ));
2871
2911
  // 3. env exports + provider (NO `exec` so the provider is a child).
2872
2912
  // 0.4.x ordering fix: skip keys present in env_unset so KEY=val does not
2873
2913
  // re-introduce a just-unset variable from the inherited env map.
@@ -2881,6 +2921,9 @@ pub fn leader_shell_wrapper_command(
2881
2921
  parts.push(";".to_string());
2882
2922
  // 4. exit marker + inert shell tail
2883
2923
  parts.push("rc=$?;".to_string());
2924
+ parts.push(format!(
2925
+ "tmux -S \"${{TMUX%%,*}}\" set-option -p -t \"$TMUX_PANE\" {PROVIDER_EXIT_STATUS_OPTION} \"$rc\" >/dev/null 2>&1;"
2926
+ ));
2884
2927
  parts.push("printf".to_string());
2885
2928
  // CR R6: marker text comes from single-source `leader_provider_exit_marker`.
2886
2929
  parts.push(shell_quote(&format!(
@@ -2893,8 +2936,8 @@ pub fn leader_shell_wrapper_command(
2893
2936
  }
2894
2937
 
2895
2938
  fn inert_pane_tail_command() -> String {
2896
- // `sh` is deliberate: provider-exit health checks use the shell basename
2897
- // to reach the exit-marker branch. `sh -c` does not read commands from
2939
+ // `sh` is deliberate: exited-wrapper input classifiers recognize shell
2940
+ // basenames. `sh -c` does not read commands from
2898
2941
  // stdin; disabling echo also prevents input from appearing accepted.
2899
2942
  let script = r#"trap '' INT QUIT; stty -echo 2>/dev/null; printf "%s\n" "[team-agent] Provider exited; this pane no longer accepts input. Restart from another pane with the appropriate team-agent start command."; while :; do sleep 3600 & wait "$!"; done"#;
2900
2943
  format!("exec /bin/sh -c {}", shell_quote(script))
@@ -3227,6 +3270,8 @@ impl Transport for TmuxBackend {
3227
3270
  let mut consumption_attempts: u32 = 0;
3228
3271
  let mut capture_retries: u32 = 0;
3229
3272
  let mut consumed: Option<bool> = None;
3273
+ let mut consumption_reason = SubmitConsumptionReason::Unknown;
3274
+ let mut consumption_diagnostics = ConsumptionDiagnostics::default();
3230
3275
  let mut attempts_detail: Vec<SubmitAttemptObservation> = Vec::new();
3231
3276
  let mut any_attempt_matched = false;
3232
3277
 
@@ -3267,6 +3312,13 @@ impl Transport for TmuxBackend {
3267
3312
  token_seen_after_enter = Some(false);
3268
3313
  }
3269
3314
  tracked_paste = latch_paste(&cap.text, tracked_paste);
3315
+ let observation = consumption_capture_observation(
3316
+ &cap.text,
3317
+ marker,
3318
+ token_ever_visible,
3319
+ tracked_paste,
3320
+ );
3321
+ consumption_diagnostics.record(observation);
3270
3322
  let obs = submit_attempt_observation(
3271
3323
  attempt_index,
3272
3324
  &cap,
@@ -3279,18 +3331,16 @@ impl Transport for TmuxBackend {
3279
3331
  attempts_detail.push(obs);
3280
3332
  // NeverSeen 不得把 token 缺席写成 consumed;
3281
3333
  // 见过再消失,或本次占位符身份离开 composer,才停手。
3282
- if consumption_from_capture(
3283
- &cap.text,
3284
- m,
3285
- token_ever_visible,
3286
- tracked_paste,
3287
- ) == Some(true)
3288
- {
3334
+ if observation.consumption_from_capture_result == Some(true) {
3335
+ consumption_reason =
3336
+ SubmitConsumptionReason::ConsumptionFromCapture;
3289
3337
  consumed = Some(true);
3290
3338
  break;
3291
3339
  }
3292
3340
  if cursor_single_enter_enabled() {
3293
- if provider_busy_signal_in_tail(&cap.text) {
3341
+ if observation.busy.is_some() {
3342
+ consumption_reason =
3343
+ SubmitConsumptionReason::CursorPollingBusy;
3294
3344
  consumed = Some(true);
3295
3345
  break;
3296
3346
  }
@@ -3375,6 +3425,13 @@ impl Transport for TmuxBackend {
3375
3425
  token_seen_after_enter = Some(false);
3376
3426
  }
3377
3427
  tracked_paste = latch_paste(&cap.text, tracked_paste);
3428
+ let observation = consumption_capture_observation(
3429
+ &cap.text,
3430
+ marker,
3431
+ token_ever_visible,
3432
+ tracked_paste,
3433
+ );
3434
+ consumption_diagnostics.record(observation);
3378
3435
  let obs = submit_attempt_observation(
3379
3436
  attempt_index,
3380
3437
  &cap,
@@ -3385,24 +3442,22 @@ impl Transport for TmuxBackend {
3385
3442
  any_attempt_matched = true;
3386
3443
  }
3387
3444
  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
- {
3445
+ if observation.consumption_from_capture_result == Some(true) {
3446
+ consumption_reason =
3447
+ SubmitConsumptionReason::ConsumptionFromCapture;
3395
3448
  found_consumed = true;
3396
3449
  break;
3397
3450
  }
3398
- if cursor_single_enter_enabled()
3399
- && provider_busy_signal_in_tail(&cap.text)
3400
- {
3451
+ if cursor_single_enter_enabled() && observation.busy.is_some() {
3452
+ consumption_reason =
3453
+ SubmitConsumptionReason::CursorPollingBusy;
3401
3454
  found_consumed = true;
3402
3455
  break;
3403
3456
  }
3404
3457
  }
3405
3458
  Err(_) => {
3459
+ consumption_diagnostics
3460
+ .record(ConsumptionCaptureObservation::default());
3406
3461
  capture_tally.record_err();
3407
3462
  capture_retries = capture_retries.saturating_add(1);
3408
3463
  consecutive_capture_failures =
@@ -3426,7 +3481,8 @@ impl Transport for TmuxBackend {
3426
3481
  }
3427
3482
  } else {
3428
3483
  // Non-token payload: single Enter, no consumption check
3429
- // (0.3.27). Not 「没能判断」— 本门不覆盖无 token 路径。
3484
+ // (0.3.27). Keep the explicit no-marker reason.
3485
+ consumption_reason = SubmitConsumptionReason::NoMarker;
3430
3486
  consumed = Some(true);
3431
3487
  break;
3432
3488
  }
@@ -3451,21 +3507,38 @@ impl Transport for TmuxBackend {
3451
3507
  Some(false) => match self.capture(target, CaptureRange::Tail(15)) {
3452
3508
  Ok(cap) => {
3453
3509
  last_turn_text = Some(cap.text.clone());
3510
+ let observation = consumption_capture_observation(
3511
+ &cap.text,
3512
+ marker,
3513
+ token_ever_visible,
3514
+ tracked_paste,
3515
+ );
3516
+ consumption_diagnostics.record(observation);
3454
3517
  attempts_detail.push(submit_attempt_observation(
3455
3518
  consumption_attempts.max(1),
3456
3519
  &cap,
3457
3520
  marker,
3458
3521
  inject_start.elapsed().as_millis() as u64,
3459
3522
  ));
3460
- if provider_busy_signal_in_tail(&cap.text) {
3523
+ if observation.busy.is_some() {
3524
+ consumption_reason = SubmitConsumptionReason::FallbackBusy;
3461
3525
  SubmitVerification::EnterSentWithoutPlaceholderCheck
3462
3526
  } else {
3527
+ consumption_reason = SubmitConsumptionReason::Unverified;
3463
3528
  SubmitVerification::SubmitConsumptionUnverified
3464
3529
  }
3465
3530
  }
3466
- Err(_) => SubmitVerification::SubmitConsumptionUnverified,
3531
+ Err(_) => {
3532
+ consumption_diagnostics
3533
+ .record(ConsumptionCaptureObservation::default());
3534
+ consumption_reason = SubmitConsumptionReason::Unverified;
3535
+ SubmitVerification::SubmitConsumptionUnverified
3536
+ }
3467
3537
  },
3468
- None => SubmitVerification::SubmitConsumptionUnverified,
3538
+ None => {
3539
+ consumption_reason = SubmitConsumptionReason::Unverified;
3540
+ SubmitVerification::SubmitConsumptionUnverified
3541
+ }
3469
3542
  };
3470
3543
  // Unverified + 折行缺口:只补一颗 C-m。A 的 should_resubmit_enter
3471
3544
  // 已为真(latch/底15 token,含打满 cap)时 B 不介入。身份拿不到
@@ -3511,6 +3584,13 @@ impl Transport for TmuxBackend {
3511
3584
  token_seen_after_enter = Some(false);
3512
3585
  }
3513
3586
  tracked_paste = latch_paste(&cap.text, tracked_paste);
3587
+ let observation = consumption_capture_observation(
3588
+ &cap.text,
3589
+ marker,
3590
+ token_ever_visible,
3591
+ tracked_paste,
3592
+ );
3593
+ consumption_diagnostics.record(observation);
3514
3594
  let obs = submit_attempt_observation(
3515
3595
  consumption_attempts.max(1),
3516
3596
  &cap,
@@ -3519,20 +3599,22 @@ impl Transport for TmuxBackend {
3519
3599
  );
3520
3600
  attempts_detail.push(obs);
3521
3601
  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
- {
3602
+ let capture_consumed = observation
3603
+ .consumption_from_capture_result
3604
+ == Some(true);
3605
+ let busy = observation.busy.is_some();
3606
+ if capture_consumed || busy {
3530
3607
  // Gone+无 latch 会把折行 token 写成已消费;身份仍在则不算。
3531
3608
  if !this_paste_identity_in_composer(
3532
3609
  &cap.text,
3533
3610
  m,
3534
3611
  tracked_paste,
3535
3612
  ) {
3613
+ consumption_reason = if capture_consumed {
3614
+ SubmitConsumptionReason::ConsumptionFromCapture
3615
+ } else {
3616
+ SubmitConsumptionReason::FallbackBusy
3617
+ };
3536
3618
  found_consumed = true;
3537
3619
  break;
3538
3620
  }
@@ -3542,11 +3624,14 @@ impl Transport for TmuxBackend {
3542
3624
  m,
3543
3625
  tracked_paste,
3544
3626
  ) {
3627
+ consumption_reason = SubmitConsumptionReason::Unknown;
3545
3628
  found_consumed = true;
3546
3629
  break;
3547
3630
  }
3548
3631
  }
3549
3632
  Err(_) => {
3633
+ consumption_diagnostics
3634
+ .record(ConsumptionCaptureObservation::default());
3550
3635
  capture_tally.record_err();
3551
3636
  break;
3552
3637
  }
@@ -3562,6 +3647,24 @@ impl Transport for TmuxBackend {
3562
3647
  }
3563
3648
  let total_elapsed_ms = inject_start.elapsed().as_millis() as u64;
3564
3649
  let surface = probe_inject_target_surface(self, target);
3650
+ let mut submit_diagnostics = submit_diagnostics_for_inject(
3651
+ total_elapsed_ms,
3652
+ attempts_detail,
3653
+ &capture_tally,
3654
+ token_seen_after_paste,
3655
+ token_seen_after_enter,
3656
+ surface,
3657
+ );
3658
+ submit_diagnostics.consumption_reason = consumption_reason;
3659
+ submit_diagnostics.busy_signal_kind = consumption_diagnostics.busy_signal_kind;
3660
+ submit_diagnostics.busy_line_from_bottom =
3661
+ consumption_diagnostics.busy_line_from_bottom;
3662
+ submit_diagnostics.current_marker_in_bottom_15 =
3663
+ consumption_diagnostics.current_marker_in_bottom_15;
3664
+ submit_diagnostics.paste_identity_in_composer =
3665
+ consumption_diagnostics.paste_identity_in_composer;
3666
+ submit_diagnostics.consumption_from_capture_result =
3667
+ consumption_diagnostics.consumption_from_capture_result;
3565
3668
  return Ok(InjectReport {
3566
3669
  stage_reached: InjectStage::Submit,
3567
3670
  inject_verification: inject_verification_after_readback(
@@ -3574,14 +3677,7 @@ impl Transport for TmuxBackend {
3574
3677
  last_turn_text.as_deref(),
3575
3678
  ),
3576
3679
  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
- )),
3680
+ submit_diagnostics: Some(submit_diagnostics),
3585
3681
  });
3586
3682
  }
3587
3683
  }
@@ -3653,6 +3749,22 @@ impl Transport for TmuxBackend {
3653
3749
  Ok(Some(output.stdout.trim().to_string()))
3654
3750
  }
3655
3751
 
3752
+ fn provider_exit_status(&self, pane: &PaneId) -> Result<Option<u8>, TransportError> {
3753
+ let argv = self.tmux_argv(&[
3754
+ "tmux".to_string(),
3755
+ "display-message".to_string(),
3756
+ "-p".to_string(),
3757
+ "-t".to_string(),
3758
+ pane.as_str().to_string(),
3759
+ format!("#{{{PROVIDER_EXIT_STATUS_OPTION}}}"),
3760
+ ]);
3761
+ let output = self.runner.run(&argv)?;
3762
+ if !output.success {
3763
+ return Ok(None);
3764
+ }
3765
+ Ok(output.stdout.trim().parse::<u8>().ok())
3766
+ }
3767
+
3656
3768
  fn liveness(&self, pane: &PaneId) -> Result<PaneLiveness, TransportError> {
3657
3769
  let argv = self.tmux_argv(&[
3658
3770
  "tmux".to_string(),