@team-agent/installer 0.5.75 → 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 (98) hide show
  1. package/Cargo.lock +1 -1
  2. package/Cargo.toml +1 -1
  3. package/README.md +9 -0
  4. package/README.zh.md +8 -0
  5. package/crates/team-agent/src/cli/adapters.rs +184 -125
  6. package/crates/team-agent/src/cli/diagnose.rs +333 -68
  7. package/crates/team-agent/src/cli/emit.rs +59 -15
  8. package/crates/team-agent/src/cli/mod.rs +600 -134
  9. package/crates/team-agent/src/cli/models.rs +485 -44
  10. package/crates/team-agent/src/cli/named_address.rs +19 -5
  11. package/crates/team-agent/src/cli/profile.rs +35 -4
  12. package/crates/team-agent/src/cli/spec.rs +7 -2
  13. package/crates/team-agent/src/cli/status_port/brief.rs +1137 -0
  14. package/crates/team-agent/src/cli/status_port/compact.rs +12 -8
  15. package/crates/team-agent/src/cli/status_port/snapshot.rs +15 -12
  16. package/crates/team-agent/src/cli/status_port/store.rs +1 -1
  17. package/crates/team-agent/src/cli/status_port.rs +8 -0
  18. package/crates/team-agent/src/cli/tests/missing_subcommands.rs +62 -8
  19. package/crates/team-agent/src/cli/tests/mod.rs +1 -0
  20. package/crates/team-agent/src/cli/tests/named_address.rs +70 -0
  21. package/crates/team-agent/src/cli/tests/run_delegation.rs +29 -24
  22. package/crates/team-agent/src/cli/tests/status_brief.rs +691 -0
  23. package/crates/team-agent/src/cli/tests/status_send.rs +8 -2
  24. package/crates/team-agent/src/cli/tests/verb_profile.rs +81 -6
  25. package/crates/team-agent/src/cli/types.rs +1 -0
  26. package/crates/team-agent/src/leader/lease.rs +538 -41
  27. package/crates/team-agent/src/leader/owner_bind.rs +40 -12
  28. package/crates/team-agent/src/leader/rediscover/tests.rs +65 -0
  29. package/crates/team-agent/src/leader/rediscover.rs +36 -6
  30. package/crates/team-agent/src/leader/start.rs +151 -14
  31. package/crates/team-agent/src/leader/tests/wake_start_owner.rs +15 -9
  32. package/crates/team-agent/src/leader/types.rs +4 -0
  33. package/crates/team-agent/src/lifecycle/launch/add_agent.rs +28 -227
  34. package/crates/team-agent/src/lifecycle/launch/add_agent_state.rs +0 -18
  35. package/crates/team-agent/src/lifecycle/launch/cursor_mcp.rs +94 -227
  36. package/crates/team-agent/src/lifecycle/launch/mcp_config.rs +59 -10
  37. package/crates/team-agent/src/lifecycle/launch/pi_mcp.rs +138 -14
  38. package/crates/team-agent/src/lifecycle/launch/quick_start.rs +1449 -282
  39. package/crates/team-agent/src/lifecycle/launch/readiness.rs +349 -4
  40. package/crates/team-agent/src/lifecycle/launch/spawn.rs +11 -11
  41. package/crates/team-agent/src/lifecycle/launch/spec_state.rs +35 -3
  42. package/crates/team-agent/src/lifecycle/launch/worker_env.rs +16 -0
  43. package/crates/team-agent/src/lifecycle/launch.rs +30 -7
  44. package/crates/team-agent/src/lifecycle/profile_launch.rs +37 -3
  45. package/crates/team-agent/src/lifecycle/restart/agent.rs +112 -100
  46. package/crates/team-agent/src/lifecycle/restart/common.rs +82 -52
  47. package/crates/team-agent/src/lifecycle/restart/rebuild.rs +1002 -65
  48. package/crates/team-agent/src/lifecycle/restart.rs +2 -2
  49. package/crates/team-agent/src/lifecycle/tests/acceptance_b_batch_red.rs +3 -0
  50. package/crates/team-agent/src/lifecycle/tests/b0_reader_hideone_audit_contract.rs +45 -14
  51. package/crates/team-agent/src/lifecycle/tests/behavioral_diff_264_red.rs +15 -0
  52. package/crates/team-agent/src/lifecycle/tests/claude_compatible_config_red.rs +5 -0
  53. package/crates/team-agent/src/lifecycle/tests/claude_profile_launch_red.rs +11 -13
  54. package/crates/team-agent/src/lifecycle/tests/clone_agent_preserves_source_tools.rs +119 -3
  55. package/crates/team-agent/src/lifecycle/tests/communication_mode_runtime_contract_red.rs +12 -0
  56. package/crates/team-agent/src/lifecycle/tests/copilot_provider_red.rs +22 -0
  57. package/crates/team-agent/src/lifecycle/tests/core_034_real_red.rs +12 -0
  58. package/crates/team-agent/src/lifecycle/tests/cursor_mcp_overlay.rs +265 -198
  59. package/crates/team-agent/src/lifecycle/tests/cursor_require_explicit_model_red.rs +14 -30
  60. package/crates/team-agent/src/lifecycle/tests/cursor_restart_resume_red.rs +3 -0
  61. package/crates/team-agent/src/lifecycle/tests/g1_silent_faces.rs +2 -5
  62. package/crates/team-agent/src/lifecycle/tests/grok_effort_argv_red.rs +10 -0
  63. package/crates/team-agent/src/lifecycle/tests/grok_mcp_overlay_red.rs +58 -35
  64. package/crates/team-agent/src/lifecycle/tests/grok_require_explicit_model_red.rs +10 -0
  65. package/crates/team-agent/src/lifecycle/tests/grok_restart_resume_red.rs +9 -0
  66. package/crates/team-agent/src/lifecycle/tests/harvest2_a_batch_red.rs +4 -3
  67. package/crates/team-agent/src/lifecycle/tests/lane_ops.rs +0 -168
  68. package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +145 -212
  69. package/crates/team-agent/src/lifecycle/tests/phase_b_contracts.rs +16 -2
  70. package/crates/team-agent/src/lifecycle/tests/phase_golden.rs +69 -28
  71. package/crates/team-agent/src/lifecycle/tests/pi_dual_entry_red.rs +78 -0
  72. package/crates/team-agent/src/lifecycle/tests/team_in_team_identity_scope_red.rs +3 -0
  73. package/crates/team-agent/src/lifecycle/tests/test_isolation_escape_contract.rs +51 -10
  74. package/crates/team-agent/src/lifecycle/tests/upgrade_compat_0211_red.rs +22 -10
  75. package/crates/team-agent/src/lifecycle/tests/verify_rs031_window_consistency_red.rs +50 -7
  76. package/crates/team-agent/src/lifecycle/tests/worker_spawn_env_red.rs +137 -0
  77. package/crates/team-agent/src/lifecycle/types.rs +12 -0
  78. package/crates/team-agent/src/mcp_server/wire.rs +140 -11
  79. package/crates/team-agent/src/messaging/delivery.rs +67 -8
  80. package/crates/team-agent/src/messaging/leader_channel.rs +56 -0
  81. package/crates/team-agent/src/messaging/tests/runtime.rs +10 -2
  82. package/crates/team-agent/src/messaging/watchers.rs +48 -4
  83. package/crates/team-agent/src/provider/adapters/cursor_agent.rs +2 -5
  84. package/crates/team-agent/src/state/paths.rs +17 -2
  85. package/crates/team-agent/src/state/persist.rs +97 -1
  86. package/crates/team-agent/src/state/projection.rs +18 -2
  87. package/crates/team-agent/src/state/repository.rs +29 -0
  88. package/crates/team-agent/src/state/selector.rs +23 -3
  89. package/crates/team-agent/src/test_support/models_timeout_fixture.rs +144 -0
  90. package/crates/team-agent/src/tmux_backend/tests.rs +121 -4
  91. package/crates/team-agent/src/tmux_backend.rs +208 -118
  92. package/crates/team-agent/src/transport.rs +72 -0
  93. package/crates/team-agent/src/transport_factory.rs +53 -0
  94. package/package.json +4 -5
  95. package/skills/team-agent/SKILL.md +45 -17
  96. package/skills/team-agent/command-coverage.json +4 -29
  97. package/npm/check-skill-links.mjs +0 -88
  98. package/skills/team-agent/references/team-agent-operator.md +0 -672
@@ -185,7 +185,7 @@ fn run_stdio_loop<R: BufRead, W: Write>(
185
185
  let mut report = ServerRunReport::default();
186
186
  let marker = McpServerLifecycleMarker::from_env(workspace);
187
187
  marker.write_started();
188
- let result = run_stdio_loop_inner(&tools, reader, &mut writer, &mut report);
188
+ let result = run_stdio_loop_inner(&tools, reader, &mut writer, &mut report, &marker);
189
189
  match &result {
190
190
  Ok(_) => marker.write_exit("stdin_eof", None),
191
191
  Err(error) => marker.write_exit("fatal_error", Some(&error.to_string())),
@@ -198,16 +198,20 @@ fn run_stdio_loop_inner<R: BufRead, W: Write>(
198
198
  reader: R,
199
199
  writer: &mut W,
200
200
  report: &mut ServerRunReport,
201
+ marker: &McpServerLifecycleMarker,
201
202
  ) -> Result<ServerRunReport, McpError> {
202
203
  for line in reader.lines() {
203
204
  let line = line?;
204
205
  report.requests_read = report.requests_read.saturating_add(1);
205
- let frame = handle_stdin_line(tools, &line, report)?;
206
+ let (frame, tools_list_success) = handle_stdin_line(tools, &line, report)?;
206
207
  if let Some(value) = frame {
207
208
  let value = crate::redaction::redact_external_value(&value);
208
209
  serde_json::to_writer(&mut *writer, &value)?;
209
210
  writer.write_all(b"\n")?;
210
211
  writer.flush()?;
212
+ if tools_list_success {
213
+ marker.write_tools_list_response_written();
214
+ }
211
215
  report.responses_written = report.responses_written.saturating_add(1);
212
216
  }
213
217
  }
@@ -219,29 +223,34 @@ fn handle_stdin_line(
219
223
  tools: &TeamOrchestratorTools,
220
224
  line: &str,
221
225
  report: &mut ServerRunReport,
222
- ) -> Result<Option<Value>, McpError> {
226
+ ) -> Result<(Option<Value>, bool), McpError> {
223
227
  let request: Value = match serde_json::from_str(line) {
224
228
  Ok(value) => value,
225
229
  Err(err) => {
226
230
  report.error_frames = report.error_frames.saturating_add(1);
227
- return Ok(Some(error_response_value(
228
- RpcId::Null,
229
- -32700,
230
- format!("parse error: {err}"),
231
- )));
231
+ return Ok((
232
+ Some(error_response_value(
233
+ RpcId::Null,
234
+ -32700,
235
+ format!("parse error: {err}"),
236
+ )),
237
+ false,
238
+ ));
232
239
  }
233
240
  };
234
241
  if request.get("jsonrpc").and_then(Value::as_str) == Some("2.0") {
242
+ let is_tools_list = request.get("method").and_then(Value::as_str) == Some("tools/list");
235
243
  match handle_mcp(tools, &request)? {
236
244
  Some(response) => {
245
+ let tools_list_success = is_tools_list && response.error.is_none();
237
246
  if response.error.is_some() {
238
247
  report.error_frames = report.error_frames.saturating_add(1);
239
248
  }
240
- Ok(Some(serde_json::to_value(response)?))
249
+ Ok((Some(serde_json::to_value(response)?), tools_list_success))
241
250
  }
242
251
  None => {
243
252
  report.notifications_skipped = report.notifications_skipped.saturating_add(1);
244
- Ok(None)
253
+ Ok((None, false))
245
254
  }
246
255
  }
247
256
  } else {
@@ -252,7 +261,7 @@ fn handle_stdin_line(
252
261
  err.to_envelope()
253
262
  }
254
263
  };
255
- Ok(Some(value))
264
+ Ok((Some(value), false))
256
265
  }
257
266
  }
258
267
 
@@ -301,6 +310,23 @@ impl McpServerLifecycleMarker {
301
310
  self.write("mcp.server_started", None, None);
302
311
  }
303
312
 
313
+ fn write_tools_list_response_written(&self) {
314
+ let _ = EventLog::new(&self.workspace).write(
315
+ "mcp.tools_list_response_written",
316
+ serde_json::json!({
317
+ "method": "tools/list",
318
+ "success": true,
319
+ "agent_id": self.agent_id.as_deref(),
320
+ "owner_team_id": self.owner_team_id.as_deref(),
321
+ "workspace": self.workspace.display().to_string(),
322
+ "pid": self.pid,
323
+ "ppid": self.ppid,
324
+ "spawn_epoch": null,
325
+ "spawn_epoch_source": "unavailable",
326
+ }),
327
+ );
328
+ }
329
+
304
330
  fn write_exit(&self, reason: &str, error: Option<&str>) {
305
331
  self.write("mcp.server_exit", Some(reason), error);
306
332
  }
@@ -798,6 +824,109 @@ mod e23_lifecycle_marker_tests {
798
824
  path
799
825
  }
800
826
 
827
+ #[test]
828
+ fn stdio_loop_logs_tools_list_after_flush_once_per_response() {
829
+ let ws = marker_ws("tools-list-success");
830
+ let input = std::io::Cursor::new(
831
+ br#"{"jsonrpc":"2.0","id":1,"method":"tools/list"}
832
+ {"jsonrpc":"2.0","id":2,"method":"tools/list"}
833
+ "#.to_vec(),
834
+ );
835
+ let mut output = Vec::new();
836
+
837
+ let report = run_stdio_loop(&ws, input, &mut output).unwrap();
838
+
839
+ assert_eq!(report.responses_written, 2);
840
+ assert_eq!(output.iter().filter(|byte| **byte == b'\n').count(), 2);
841
+ let events = EventLog::new(&ws).tail(0).unwrap();
842
+ let tool_events: Vec<_> = events
843
+ .iter()
844
+ .filter(|event| event["event"] == serde_json::json!("mcp.tools_list_response_written"))
845
+ .collect();
846
+ assert_eq!(tool_events.len(), 2, "one event per successful tools/list response");
847
+ for event in tool_events {
848
+ assert_eq!(event["method"], serde_json::json!("tools/list"));
849
+ assert_eq!(event["success"], serde_json::json!(true));
850
+ assert_eq!(event["workspace"], serde_json::json!(ws.display().to_string()));
851
+ assert_eq!(event["pid"], serde_json::json!(std::process::id()));
852
+ assert_eq!(event["spawn_epoch"], serde_json::Value::Null);
853
+ }
854
+ let _ = std::fs::remove_dir_all(&ws);
855
+ }
856
+
857
+ #[test]
858
+ fn stdio_loop_does_not_log_tools_list_when_write_fails() {
859
+ struct FailingWriter;
860
+ impl Write for FailingWriter {
861
+ fn write(&mut self, _buf: &[u8]) -> std::io::Result<usize> {
862
+ Err(std::io::Error::other("simulated stdout write failure"))
863
+ }
864
+
865
+ fn flush(&mut self) -> std::io::Result<()> {
866
+ Ok(())
867
+ }
868
+ }
869
+
870
+ let ws = marker_ws("tools-list-write-fail");
871
+ let input = std::io::Cursor::new(
872
+ br#"{"jsonrpc":"2.0","id":1,"method":"tools/list"}"#.to_vec(),
873
+ );
874
+ let error = run_stdio_loop(&ws, input, FailingWriter).expect_err("write must fail");
875
+ assert!(error.to_string().contains("simulated stdout write failure"));
876
+ let events = EventLog::new(&ws).tail(0).unwrap();
877
+ assert!(!events
878
+ .iter()
879
+ .any(|event| event["event"] == serde_json::json!("mcp.tools_list_response_written")));
880
+ let _ = std::fs::remove_dir_all(&ws);
881
+ }
882
+
883
+ #[test]
884
+ fn stdio_loop_does_not_log_tools_list_when_flush_fails() {
885
+ struct FlushFailWriter(Vec<u8>);
886
+ impl Write for FlushFailWriter {
887
+ fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
888
+ self.0.extend_from_slice(buf);
889
+ Ok(buf.len())
890
+ }
891
+
892
+ fn flush(&mut self) -> std::io::Result<()> {
893
+ Err(std::io::Error::other("simulated stdout flush failure"))
894
+ }
895
+ }
896
+
897
+ let ws = marker_ws("tools-list-flush-fail");
898
+ let input = std::io::Cursor::new(
899
+ br#"{"jsonrpc":"2.0","id":1,"method":"tools/list"}"#.to_vec(),
900
+ );
901
+ let error = run_stdio_loop(&ws, input, FlushFailWriter(Vec::new()))
902
+ .expect_err("flush must fail");
903
+ assert!(error.to_string().contains("simulated stdout flush failure"));
904
+ let events = EventLog::new(&ws).tail(0).unwrap();
905
+ assert!(!events
906
+ .iter()
907
+ .any(|event| event["event"] == serde_json::json!("mcp.tools_list_response_written")));
908
+ let _ = std::fs::remove_dir_all(&ws);
909
+ }
910
+
911
+ #[test]
912
+ fn stdio_loop_does_not_log_tools_list_for_initialize_or_notification() {
913
+ let ws = marker_ws("tools-list-not-applicable");
914
+ let input = std::io::Cursor::new(
915
+ br#"{"jsonrpc":"2.0","id":1,"method":"initialize"}
916
+ {"jsonrpc":"2.0","method":"notifications/initialized"}
917
+ "#.to_vec(),
918
+ );
919
+ let mut output = Vec::new();
920
+
921
+ run_stdio_loop(&ws, input, &mut output).unwrap();
922
+
923
+ let events = EventLog::new(&ws).tail(0).unwrap();
924
+ assert!(!events
925
+ .iter()
926
+ .any(|event| event["event"] == serde_json::json!("mcp.tools_list_response_written")));
927
+ let _ = std::fs::remove_dir_all(&ws);
928
+ }
929
+
801
930
  #[test]
802
931
  fn stdio_loop_writes_started_and_stdin_eof_exit_markers() {
803
932
  let ws = marker_ws("eof");
@@ -31,9 +31,10 @@ use crate::provider::wire::{
31
31
  is_claude_family, parse_canonical_provider, parse_provider, provider_wire,
32
32
  };
33
33
  use crate::transport::{
34
- submit_verification_wire, turn_verification_wire, CaptureRange, CaptureSampleOutcome,
35
- InjectPayload, InjectReport, InjectVerification, InputSurfaceProbe, Key, PaneId, PaneInfo,
36
- QueryOutcome, SessionName, SubmitObserver, SubmitVerification, Target, Transport, WindowName,
34
+ submit_verification_wire, turn_verification_wire, BusySignalKind, CaptureRange,
35
+ CaptureSampleOutcome, InjectPayload, InjectReport, InjectVerification, InputSurfaceProbe, Key,
36
+ PaneId, PaneInfo, QueryOutcome, SessionName, SubmitConsumptionReason, SubmitObserver,
37
+ SubmitVerification, Target, Transport, WindowName,
37
38
  };
38
39
 
39
40
  use super::helpers::{message_exists, MessageStatusShadow};
@@ -572,11 +573,11 @@ pub fn deliver_pending_message(
572
573
  canonical_owner_team_id.as_deref(),
573
574
  resolved.metadata.as_ref(),
574
575
  );
575
- let recipient_single_enter = recipient_requires_single_enter(state, &message.recipient);
576
+ let recipient_cursor = recipient_is_cursor_agent(state, &message.recipient);
576
577
  let inject_attempt = crate::tmux_backend::with_paste_to_submit_floor(
577
578
  paste_to_submit_floor_for_recipient(state, &message.recipient),
578
579
  || {
579
- crate::tmux_backend::with_cursor_single_enter(recipient_single_enter, || {
580
+ crate::tmux_backend::with_cursor_single_enter(recipient_cursor, || {
580
581
  transport.inject_with_submit_observer(
581
582
  &target,
582
583
  &payload,
@@ -592,9 +593,7 @@ pub fn deliver_pending_message(
592
593
  // grok 忙时第一次回车只入显式队列。默认 send-now:只重按回车顶出去。
593
594
  // 无 `Enter:send now` 时零次额外按键,claude/codex 行为不变。
594
595
  // cursor:第二下 Enter 打断进行中回合,不走 flush。
595
- if !crate::provider::submit_now::keep_provider_queue_requested()
596
- && recipient_allows_explicit_queue_flush(state, &message.recipient)
597
- {
596
+ if !crate::provider::submit_now::keep_provider_queue_requested() && !recipient_cursor {
598
597
  match crate::provider::submit_now::flush_explicit_queue(transport, &target) {
599
598
  Ok(flush) if flush.extra_enters > 0 => {
600
599
  let _ = event_log.write(
@@ -1999,6 +1998,36 @@ fn merge_inject_readback_fields(
1999
1998
  .map(|d| d.last_capture_outcome.as_str())
2000
1999
  .unwrap_or(CaptureSampleOutcome::NotAttempted.as_str())),
2001
2000
  );
2001
+ obj.insert(
2002
+ "consumption_reason".to_string(),
2003
+ serde_json::json!(diag
2004
+ .map(|d| d.consumption_reason.as_str())
2005
+ .unwrap_or("unknown")),
2006
+ );
2007
+ obj.insert(
2008
+ "before_busy_signal_kind".to_string(),
2009
+ serde_json::json!(diag.and_then(|d| d.before_busy_signal_kind.map(|kind| kind.as_str()))),
2010
+ );
2011
+ obj.insert(
2012
+ "busy_signal_kind".to_string(),
2013
+ serde_json::json!(diag.and_then(|d| d.busy_signal_kind.map(|kind| kind.as_str()))),
2014
+ );
2015
+ obj.insert(
2016
+ "busy_line_from_bottom".to_string(),
2017
+ serde_json::json!(diag.and_then(|d| d.busy_line_from_bottom)),
2018
+ );
2019
+ obj.insert(
2020
+ "current_marker_in_bottom_15".to_string(),
2021
+ serde_json::json!(diag.and_then(|d| d.current_marker_in_bottom_15)),
2022
+ );
2023
+ obj.insert(
2024
+ "paste_identity_in_composer".to_string(),
2025
+ serde_json::json!(diag.and_then(|d| d.paste_identity_in_composer)),
2026
+ );
2027
+ obj.insert(
2028
+ "consumption_from_capture_result".to_string(),
2029
+ serde_json::json!(diag.and_then(|d| d.consumption_from_capture_result)),
2030
+ );
2002
2031
  obj.insert(
2003
2032
  "pane_command_basename".to_string(),
2004
2033
  diag.and_then(|d| d.pane_command_basename.as_deref())
@@ -2357,6 +2386,13 @@ fn recipient_provider(state: &serde_json::Value, recipient: &str) -> Option<Prov
2357
2386
  .and_then(parse_canonical_provider)
2358
2387
  }
2359
2388
 
2389
+ fn recipient_is_cursor_agent(state: &serde_json::Value, recipient: &str) -> bool {
2390
+ matches!(
2391
+ recipient_provider(state, recipient),
2392
+ Some(Provider::CursorAgent)
2393
+ )
2394
+ }
2395
+
2360
2396
  /// ---
2361
2397
  /// purpose: paste→Enter 地板按收件人 provider 选择
2362
2398
  /// contract: CursorAgent 与 Grok 为 1s;其余(含 claude)为 ZERO
@@ -3391,6 +3427,13 @@ mod paste_floor_tests {
3391
3427
  turn_verification: crate::transport::TurnVerification::NotYetObserved,
3392
3428
  attempts: 1,
3393
3429
  submit_diagnostics: Some(crate::transport::SubmitDiagnostics {
3430
+ consumption_reason: SubmitConsumptionReason::Unverified,
3431
+ before_busy_signal_kind: Some(BusySignalKind::Working),
3432
+ busy_signal_kind: Some(BusySignalKind::SpinnerGlyph),
3433
+ busy_line_from_bottom: Some(2),
3434
+ current_marker_in_bottom_15: Some(false),
3435
+ paste_identity_in_composer: None,
3436
+ consumption_from_capture_result: Some(false),
3394
3437
  last_capture_outcome: CaptureSampleOutcome::Failed,
3395
3438
  capture_err_count: 1,
3396
3439
  capture_sample_count: 1,
@@ -3419,6 +3462,22 @@ mod paste_floor_tests {
3419
3462
  None,
3420
3463
  );
3421
3464
  assert_eq!(event["last_capture_outcome"], serde_json::json!("failed"));
3465
+ assert_eq!(event["consumption_reason"], serde_json::json!("unverified"));
3466
+ assert_eq!(
3467
+ event["before_busy_signal_kind"],
3468
+ serde_json::json!("working")
3469
+ );
3470
+ assert_eq!(event["busy_signal_kind"], serde_json::json!("spinner_glyph"));
3471
+ assert_eq!(event["busy_line_from_bottom"], serde_json::json!(2));
3472
+ assert_eq!(
3473
+ event["current_marker_in_bottom_15"],
3474
+ serde_json::json!(false)
3475
+ );
3476
+ assert_eq!(event["paste_identity_in_composer"], serde_json::Value::Null);
3477
+ assert_eq!(
3478
+ event["consumption_from_capture_result"],
3479
+ serde_json::json!(false)
3480
+ );
3422
3481
  assert_eq!(event["token_seen_after_paste"], serde_json::Value::Null);
3423
3482
  assert_eq!(event["pane_command_basename"], serde_json::json!("grok"));
3424
3483
  assert_eq!(event["input_surface"], serde_json::json!("input"));
@@ -229,3 +229,59 @@ fn string_field<'a>(value: &'a Value, key: &str) -> Option<&'a str> {
229
229
  .and_then(Value::as_str)
230
230
  .filter(|value| !value.is_empty())
231
231
  }
232
+
233
+ #[cfg(test)]
234
+ mod claim_rework_live_tests {
235
+ use super::*;
236
+ use crate::transport::test_support::OfflineTransport;
237
+ use crate::transport::{PaneId, PaneInfo, SessionName};
238
+ use serde_json::json;
239
+ use std::path::PathBuf;
240
+
241
+ #[test]
242
+ fn pending_receiver_is_not_a_live_channel() {
243
+ let workspace = PathBuf::from("/tmp/claim-rework-a3-pending");
244
+ let receiver = json!({
245
+ "mode": "direct_tmux",
246
+ "status": "pending",
247
+ "pane_id": "%1",
248
+ "tmux_socket": "/tmp/ta-offline.sock"
249
+ });
250
+ let transport = OfflineTransport::default().with_tmux_endpoint("/tmp/ta-offline.sock");
251
+ match resolve_live_leader_channel(&workspace, &receiver, &transport) {
252
+ LeaderChannelResolution::Unbound(LeaderChannelUnbound::ReceiverNotAttached) => {}
253
+ other => panic!("pending seed must not be live; got {other:?}"),
254
+ }
255
+ }
256
+
257
+ #[test]
258
+ fn dead_pane_with_matching_endpoint_is_pane_not_live() {
259
+ let workspace = PathBuf::from("/tmp/claim-rework-a3-dead");
260
+ let endpoint = "/tmp/ta-claim-rework-a3.sock";
261
+ let receiver = json!({
262
+ "mode": "direct_tmux",
263
+ "status": "attached",
264
+ "pane_id": "%missing",
265
+ "tmux_socket": endpoint
266
+ });
267
+ let transport = OfflineTransport::default()
268
+ .with_tmux_endpoint(endpoint)
269
+ .with_targets(vec![PaneInfo {
270
+ pane_id: PaneId::new("%other"),
271
+ session: SessionName::new("team"),
272
+ window_index: None,
273
+ window_name: None,
274
+ pane_index: None,
275
+ tty: None,
276
+ current_command: None,
277
+ current_path: None,
278
+ active: true,
279
+ pane_pid: None,
280
+ leader_env: Default::default(),
281
+ }]);
282
+ match resolve_live_leader_channel(&workspace, &receiver, &transport) {
283
+ LeaderChannelResolution::Unbound(LeaderChannelUnbound::PaneNotLive) => {}
284
+ other => panic!("matching endpoint + missing pane must be PaneNotLive; got {other:?}"),
285
+ }
286
+ }
287
+ }
@@ -3376,10 +3376,18 @@ fn gate054_status_surfaces_pending_leader_notifications() {
3376
3376
  entry.get("channel").and_then(serde_json::Value::as_str),
3377
3377
  Some("rebind_required")
3378
3378
  );
3379
- assert!(entry
3379
+ let action = entry
3380
3380
  .get("action")
3381
3381
  .and_then(serde_json::Value::as_str)
3382
- .is_some_and(|action| action.contains("attach-leader") || action.contains("takeover")));
3382
+ .unwrap_or("");
3383
+ assert!(
3384
+ action.contains("historical leader mailbox debt"),
3385
+ "historical debt action must stay debt-scoped; action={action}"
3386
+ );
3387
+ assert!(
3388
+ !action.contains("attach-leader") && !action.contains("takeover") && !action.contains("claim-leader"),
3389
+ "historical debt must not authorize bind commands; action={action}"
3390
+ );
3383
3391
  }
3384
3392
 
3385
3393
  #[test]
@@ -653,12 +653,30 @@ pub fn recover_watchers_for_incident(
653
653
  /// row for watcher-backed messages. A `submitted_pending_acceptance` row has
654
654
  /// already crossed the transport boundary and is claim-immutable. Any future
655
655
  /// retry belongs to a separate typed recovery arm, not attach/claim convergence.
656
+ /// Compatibility helper retained for callers that only need the total.
657
+ /// The typed recovery path below is the single writer; this wrapper performs
658
+ /// no second requeue and only maps the first result to `total()`.
656
659
  pub(crate) fn requeue_blocked_leader_messages(
657
660
  store: &MessageStore,
658
661
  event_log: &EventLog,
659
662
  owner_team_id: &TeamKey,
660
663
  claimed_pane_id: &PaneId,
661
664
  ) -> Result<usize, MessagingError> {
665
+ requeue_blocked_leader_messages_with_counts(
666
+ store,
667
+ event_log,
668
+ owner_team_id,
669
+ claimed_pane_id,
670
+ )
671
+ .map(crate::message_store::BlockedLeaderRequeueCounts::total)
672
+ }
673
+
674
+ pub(crate) fn requeue_blocked_leader_messages_with_counts(
675
+ store: &MessageStore,
676
+ event_log: &EventLog,
677
+ owner_team_id: &TeamKey,
678
+ claimed_pane_id: &PaneId,
679
+ ) -> Result<crate::message_store::BlockedLeaderRequeueCounts, MessagingError> {
662
680
  // E6 (0.5.9 offline-mailbox §6.5): also requeue rows that a third-party
663
681
  // sender left in `queued_until_leader_attach` via the leader mailbox.
664
682
  // Same idempotent requeue funnel (row/message_id/leader_notification_log
@@ -690,7 +708,7 @@ pub(crate) fn requeue_blocked_leader_messages(
690
708
  }),
691
709
  )?;
692
710
  }
693
- Ok(requeued)
711
+ Ok(counts)
694
712
  }
695
713
 
696
714
  /// `requeue_delivery_exhausted_watchers`: attach-leader 成功后把已经耗尽投递
@@ -702,12 +720,33 @@ pub(crate) fn requeue_blocked_leader_messages(
702
720
  /// `deliver_pending_messages` never re-picks — the new leader would never see the
703
721
  /// pending notification.
704
722
  pub fn requeue_delivery_exhausted_watchers(
705
- _workspace: &Path,
723
+ workspace: &Path,
706
724
  store: &MessageStore,
707
725
  event_log: &EventLog,
708
726
  owner_team_id: &TeamKey,
709
727
  claimed_pane_id: &PaneId,
710
728
  ) -> Result<Vec<WatcherNotice>, MessagingError> {
729
+ requeue_delivery_exhausted_watchers_with_counts(
730
+ workspace,
731
+ store,
732
+ event_log,
733
+ owner_team_id,
734
+ claimed_pane_id,
735
+ )
736
+ .map(|(notices, _)| notices)
737
+ }
738
+
739
+ /// Attach-leader's typed recovery result. The counts come from the same
740
+ /// owner-team scoped attach-recovery call (the watcher and message updates
741
+ /// remain their existing transactions), so an attach caller can expose only
742
+ /// this operation's debt rather than scanning arbitrary historical events.
743
+ pub(crate) fn requeue_delivery_exhausted_watchers_with_counts(
744
+ _workspace: &Path,
745
+ store: &MessageStore,
746
+ event_log: &EventLog,
747
+ owner_team_id: &TeamKey,
748
+ claimed_pane_id: &PaneId,
749
+ ) -> Result<(Vec<WatcherNotice>, crate::message_store::BlockedLeaderRequeueCounts), MessagingError> {
711
750
  let conn = crate::db::schema::open_db(store.db_path())?;
712
751
  let mut stmt = conn.prepare(
713
752
  "select watcher_id, result_id, status from result_watchers
@@ -748,8 +787,13 @@ pub fn requeue_delivery_exhausted_watchers(
748
787
  )?;
749
788
  }
750
789
  drop(stmt);
751
- let _ = requeue_blocked_leader_messages(store, event_log, owner_team_id, claimed_pane_id)?;
752
- Ok(out)
790
+ let counts = requeue_blocked_leader_messages_with_counts(
791
+ store,
792
+ event_log,
793
+ owner_team_id,
794
+ claimed_pane_id,
795
+ )?;
796
+ Ok((out, counts))
753
797
  }
754
798
 
755
799
  /// `delivered_result_message` (`result_delivery.py:394`):内容级去重 —— 查某 result_id 是否已有
@@ -15,10 +15,8 @@
15
15
  //! `.team/scripts/cursor_seat.sh` 实测一致:
16
16
  //! `--trust --sandbox disabled --workspace <物理路径> [--force] [--model]`
17
17
  //! Role 不入 argv,写 `<workspace>/.cursor/rules/*.mdc` + `alwaysApply: true`。
18
- //! MCP 无 `--mcp-config`;身份必须写进 per-seat
19
- //! `provider-config/<id>/cursor/.cursor/mcp.json` 的 env 表
18
+ //! MCP 无 `--mcp-config`;身份必须写进 `.cursor/mcp.json` 的 env 表
20
19
  //! (cursor 不把父进程 TEAM_AGENT_* 传给 MCP 子进程)。
21
- //! spawn 把 `--workspace` 指到该工程根,并用 `--add-dir` 挂上真 workspace。
22
20
  //!
23
21
  //! 隐藏 flag `--system-prompt` / `--allowed-tools` 实测存在但 help 未列,
24
22
  //! 随时可能消失。可以在注释里记录,代码里不许调。
@@ -75,8 +73,7 @@ pub(crate) fn cursor_agent_base_command(
75
73
  let _ = effort;
76
74
  // system_prompt 不入 argv(help 无 --rules / --append-system-prompt)。
77
75
  // launch 写 `<workspace>/.cursor/rules/team-agent-role-<agent_id>.mdc`。
78
- // mcp_config 也不入 argv(无 --mcp-config)。launch 写
79
- // `.team/runtime/provider-config/<id>/cursor/.cursor/mcp.json`
76
+ // mcp_config 也不入 argv(无 --mcp-config)。launch 写 `.cursor/mcp.json`
80
77
  // 并 `agent mcp enable team_orchestrator`。这里收下以免静默丢弃。
81
78
  let _ = (adapter, auth_mode, mcp_config, managed_mcp_config, system_prompt);
82
79
  argv.push("--workspace".to_string());
@@ -27,7 +27,7 @@
27
27
  use std::path::{Path, PathBuf};
28
28
 
29
29
  use crate::model::paths::{runtime_dir, runtime_spec_path};
30
- use crate::state::persist::load_runtime_state;
30
+ use crate::state::persist::{load_runtime_state, load_runtime_state_without_migrations};
31
31
  use crate::state::projection::team_state_candidates;
32
32
 
33
33
  /// The identity of a team within a workspace. Carries the *workspace root*
@@ -193,10 +193,25 @@ impl CommandScope {
193
193
  /// On any I/O error the empty case is returned — destructive commands
194
194
  /// will run their own selector and surface the real error.
195
195
  pub fn resolve(workspace: &Path, requested_team: Option<&str>) -> Self {
196
+ Self::resolve_from_state(requested_team, load_runtime_state(workspace))
197
+ }
198
+
199
+ /// Ambiguity gate for read-only commands. Must not migrate or persist state.
200
+ pub fn resolve_readonly(workspace: &Path, requested_team: Option<&str>) -> Self {
201
+ Self::resolve_from_state(
202
+ requested_team,
203
+ load_runtime_state_without_migrations(workspace),
204
+ )
205
+ }
206
+
207
+ fn resolve_from_state(
208
+ requested_team: Option<&str>,
209
+ loaded: Result<serde_json::Value, crate::state::StateError>,
210
+ ) -> Self {
196
211
  if let Some(team) = requested_team.filter(|t| !t.is_empty()) {
197
212
  return Self::Resolved(team.to_string());
198
213
  }
199
- let Ok(state) = load_runtime_state(workspace) else {
214
+ let Ok(state) = loaded else {
200
215
  return Self::EmptyWorkspace;
201
216
  };
202
217
  let alive = team_state_candidates(&state);