@team-agent/installer 0.5.119 → 0.5.121

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 (73) hide show
  1. package/Cargo.lock +1 -1
  2. package/Cargo.toml +1 -1
  3. package/README.md +46 -1
  4. package/README.zh.md +46 -1
  5. package/crates/team-agent/src/cli/adapters.rs +11 -0
  6. package/crates/team-agent/src/cli/arguments.rs +401 -0
  7. package/crates/team-agent/src/cli/emit.rs +241 -142
  8. package/crates/team-agent/src/cli/intent/runtime.rs +1175 -0
  9. package/crates/team-agent/src/cli/intent/runtime_tests.rs +698 -0
  10. package/crates/team-agent/src/cli/intent/tests.rs +654 -0
  11. package/crates/team-agent/src/cli/intent.rs +642 -0
  12. package/crates/team-agent/src/cli/mod.rs +53 -6
  13. package/crates/team-agent/src/cli/named_address.rs +10 -1
  14. package/crates/team-agent/src/cli/spec.rs +22 -10
  15. package/crates/team-agent/src/cli/status_port/brief.rs +117 -16
  16. package/crates/team-agent/src/cli/tests/base.rs +6 -1
  17. package/crates/team-agent/src/cli/tests/parser_safety.rs +383 -0
  18. package/crates/team-agent/src/cli/tests/run_delegation.rs +63 -19
  19. package/crates/team-agent/src/cli/tests/shutdown_kill_plan.rs +56 -0
  20. package/crates/team-agent/src/cli/tests/verb_install_skill.rs +3 -2
  21. package/crates/team-agent/src/cli/tests/verb_profile.rs +3 -1
  22. package/crates/team-agent/src/contract_runtime/backend.rs +1 -1
  23. package/crates/team-agent/src/db/message_store.rs +65 -0
  24. package/crates/team-agent/src/lifecycle/launch/agent_state.rs +8 -0
  25. package/crates/team-agent/src/lifecycle/launch/cursor_mcp.rs +111 -4
  26. package/crates/team-agent/src/lifecycle/launch/cursor_mcp_iso.rs +244 -82
  27. package/crates/team-agent/src/lifecycle/launch/quick_start.rs +166 -77
  28. package/crates/team-agent/src/lifecycle/launch.rs +29 -0
  29. package/crates/team-agent/src/lifecycle/restart/common.rs +7 -1
  30. package/crates/team-agent/src/lifecycle/restart.rs +17 -0
  31. package/crates/team-agent/src/lifecycle/tests/acceptance_b_batch_red.rs +1 -3
  32. package/crates/team-agent/src/lifecycle/tests/behavioral_diff_264_red.rs +5 -15
  33. package/crates/team-agent/src/lifecycle/tests/claude_compatible_config_red.rs +2 -3
  34. package/crates/team-agent/src/lifecycle/tests/claude_profile_launch_red.rs +2 -3
  35. package/crates/team-agent/src/lifecycle/tests/cli_argv_routing_red.rs +10 -2
  36. package/crates/team-agent/src/lifecycle/tests/clone_agent_preserves_source_tools.rs +8 -1
  37. package/crates/team-agent/src/lifecycle/tests/clone_fork_copilot_perms_red.rs +3 -2
  38. package/crates/team-agent/src/lifecycle/tests/communication_mode_runtime_contract_red.rs +6 -9
  39. package/crates/team-agent/src/lifecycle/tests/copilot_provider_red.rs +7 -5
  40. package/crates/team-agent/src/lifecycle/tests/cursor_mcp_overlay.rs +64 -19
  41. package/crates/team-agent/src/lifecycle/tests/cursor_require_explicit_model_red.rs +5 -2
  42. package/crates/team-agent/src/lifecycle/tests/cursor_restart_resume_red.rs +34 -10
  43. package/crates/team-agent/src/lifecycle/tests/grok_effort_argv_red.rs +2 -4
  44. package/crates/team-agent/src/lifecycle/tests/grok_mcp_overlay_red.rs +3 -4
  45. package/crates/team-agent/src/lifecycle/tests/grok_require_explicit_model_red.rs +5 -4
  46. package/crates/team-agent/src/lifecycle/tests/grok_restart_resume_red.rs +2 -4
  47. package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +78 -36
  48. package/crates/team-agent/src/lifecycle/tests/phase_b_contracts.rs +8 -4
  49. package/crates/team-agent/src/lifecycle/tests/phase_golden.rs +6 -12
  50. package/crates/team-agent/src/lifecycle/tests/pi_fork_session_inheritance_red.rs +4 -0
  51. package/crates/team-agent/src/lifecycle/tests/startup_latency_contract.rs +1 -3
  52. package/crates/team-agent/src/lifecycle/tests/status_credential_redaction_contract.rs +7 -0
  53. package/crates/team-agent/src/lifecycle/tests/subprep_codex_trust_roundtrip_red.rs +47 -4
  54. package/crates/team-agent/src/lifecycle/tests/team_in_team_identity_scope_red.rs +1 -3
  55. package/crates/team-agent/src/lifecycle/tests/verify_rs031_window_consistency_red.rs +2 -1
  56. package/crates/team-agent/src/lifecycle/types.rs +10 -0
  57. package/crates/team-agent/src/mcp_server/mod.rs +2 -0
  58. package/crates/team-agent/src/mcp_server/readiness.rs +432 -0
  59. package/crates/team-agent/src/mcp_server/readiness_tests.rs +242 -0
  60. package/crates/team-agent/src/mcp_server/wire.rs +96 -11
  61. package/crates/team-agent/src/messaging/delivery.rs +62 -0
  62. package/crates/team-agent/src/messaging/tests/dup_inject.rs +41 -1
  63. package/crates/team-agent/src/provider/adapter.rs +7 -0
  64. package/crates/team-agent/src/tmux_backend/tests.rs +37 -0
  65. package/crates/team-agent/src/tmux_backend.rs +8 -1
  66. package/package.json +4 -4
  67. package/skills/team-agent/SKILL.md +71 -54
  68. package/skills/team-agent/command-coverage.json +283 -13
  69. package/skills/team-agent/docs/reference/team-agent-operator.md +220 -0
  70. package/skills/team-agent/references/bug-as-artifact-flow.md +1 -1
  71. package/skills/team-agent/references/recovery-runbook.md +61 -212
  72. package/skills/team-agent/references/team-agent-operator.md +181 -48
  73. package/skills/team-agent/references/team-in-team.md +47 -80
@@ -190,17 +190,20 @@ fn run_stdio_loop_inner<R: BufRead, W: Write>(
190
190
  report: &mut ServerRunReport,
191
191
  marker: &McpServerLifecycleMarker,
192
192
  ) -> Result<ServerRunReport, McpError> {
193
+ let mut initialized = false;
193
194
  for line in reader.lines() {
194
195
  let line = line?;
195
196
  report.requests_read = report.requests_read.saturating_add(1);
196
- let (frame, tools_list_success) = handle_stdin_line(tools, &line, report)?;
197
+ let (frame, response_kind) = handle_stdin_line(tools, &line, report)?;
197
198
  if let Some(value) = frame {
198
199
  let value = crate::redaction::redact_external_value(&value);
199
200
  serde_json::to_writer(&mut *writer, &value)?;
200
201
  writer.write_all(b"\n")?;
201
202
  writer.flush()?;
202
- if tools_list_success {
203
- marker.write_tools_list_response_written();
203
+ match response_kind {
204
+ ResponseKind::Initialize => initialized = true,
205
+ ResponseKind::ToolsList => marker.write_tools_list_response_written(initialized),
206
+ ResponseKind::Other => {}
204
207
  }
205
208
  report.responses_written = report.responses_written.saturating_add(1);
206
209
  }
@@ -209,11 +212,17 @@ fn run_stdio_loop_inner<R: BufRead, W: Write>(
209
212
  Ok(report.clone())
210
213
  }
211
214
 
215
+ enum ResponseKind {
216
+ Initialize,
217
+ ToolsList,
218
+ Other,
219
+ }
220
+
212
221
  fn handle_stdin_line(
213
222
  tools: &TeamOrchestratorTools,
214
223
  line: &str,
215
224
  report: &mut ServerRunReport,
216
- ) -> Result<(Option<Value>, bool), McpError> {
225
+ ) -> Result<(Option<Value>, ResponseKind), McpError> {
217
226
  let request: Value = match serde_json::from_str(line) {
218
227
  Ok(value) => value,
219
228
  Err(err) => {
@@ -224,23 +233,28 @@ fn handle_stdin_line(
224
233
  -32700,
225
234
  format!("parse error: {err}"),
226
235
  )),
227
- false,
236
+ ResponseKind::Other,
228
237
  ));
229
238
  }
230
239
  };
231
240
  if request.get("jsonrpc").and_then(Value::as_str) == Some("2.0") {
232
- let is_tools_list = request.get("method").and_then(Value::as_str) == Some("tools/list");
241
+ let method = request.get("method").and_then(Value::as_str);
242
+ let valid_id = request.get("id").is_some_and(|id| id.is_string() || id.is_number());
233
243
  match handle_mcp(tools, &request)? {
234
244
  Some(response) => {
235
- let tools_list_success = is_tools_list && response.error.is_none();
245
+ let kind = match (method, valid_id && response.error.is_none()) {
246
+ (Some("initialize"), true) => ResponseKind::Initialize,
247
+ (Some("tools/list"), true) => ResponseKind::ToolsList,
248
+ _ => ResponseKind::Other,
249
+ };
236
250
  if response.error.is_some() {
237
251
  report.error_frames = report.error_frames.saturating_add(1);
238
252
  }
239
- Ok((Some(serde_json::to_value(response)?), tools_list_success))
253
+ Ok((Some(serde_json::to_value(response)?), kind))
240
254
  }
241
255
  None => {
242
256
  report.notifications_skipped = report.notifications_skipped.saturating_add(1);
243
- Ok((None, false))
257
+ Ok((None, ResponseKind::Other))
244
258
  }
245
259
  }
246
260
  } else {
@@ -251,7 +265,7 @@ fn handle_stdin_line(
251
265
  err.to_envelope()
252
266
  }
253
267
  };
254
- Ok((Some(value), false))
268
+ Ok((Some(value), ResponseKind::Other))
255
269
  }
256
270
  }
257
271
 
@@ -281,6 +295,7 @@ struct McpServerLifecycleMarker {
281
295
  workspace: std::path::PathBuf,
282
296
  agent_id: Option<String>,
283
297
  owner_team_id: Option<String>,
298
+ started_at: chrono::DateTime<chrono::Utc>,
284
299
  pid: u32,
285
300
  ppid: u32,
286
301
  }
@@ -291,6 +306,7 @@ impl McpServerLifecycleMarker {
291
306
  workspace: workspace.to_path_buf(),
292
307
  agent_id: non_empty_env("TEAM_AGENT_ID"),
293
308
  owner_team_id: non_empty_env("TEAM_AGENT_OWNER_TEAM_ID"),
309
+ started_at: chrono::Utc::now(),
294
310
  pid: std::process::id(),
295
311
  ppid: parent_pid(),
296
312
  }
@@ -300,7 +316,7 @@ impl McpServerLifecycleMarker {
300
316
  self.write("mcp.server_started", None, None);
301
317
  }
302
318
 
303
- fn write_tools_list_response_written(&self) {
319
+ fn write_tools_list_response_written(&self, initialized: bool) {
304
320
  let _ = EventLog::new(&self.workspace).write(
305
321
  "mcp.tools_list_response_written",
306
322
  serde_json::json!({
@@ -313,8 +329,17 @@ impl McpServerLifecycleMarker {
313
329
  "ppid": self.ppid,
314
330
  "spawn_epoch": null,
315
331
  "spawn_epoch_source": "unavailable",
332
+ "initialize_response_flushed": initialized,
316
333
  }),
317
334
  );
335
+ #[cfg(unix)]
336
+ if initialized {
337
+ if let (Some(owner), Some(agent)) = (&self.owner_team_id, &self.agent_id) {
338
+ if let Err(reason) = super::readiness::publish(&self.workspace, owner, agent, self.started_at) {
339
+ self.write("mcp.handshake_receipt_failed", Some(reason), None);
340
+ }
341
+ }
342
+ }
318
343
  }
319
344
 
320
345
  fn write_exit(&self, reason: &str, error: Option<&str>) {
@@ -725,6 +750,66 @@ mod e23_lifecycle_marker_tests {
725
750
  let _ = std::fs::remove_dir_all(&ws);
726
751
  }
727
752
 
753
+ #[cfg(unix)]
754
+ fn run_bound_handshake<W: Write>(ws: &Path, input: &str, writer: &mut W) -> Result<ServerRunReport, McpError> {
755
+ let marker = McpServerLifecycleMarker {
756
+ workspace: ws.to_path_buf(), agent_id: Some("worker".into()),
757
+ owner_team_id: Some("owner".into()), started_at: chrono::Utc::now(),
758
+ pid: std::process::id(), ppid: parent_pid(),
759
+ };
760
+ run_stdio_loop_inner(&TeamOrchestratorTools::new(ws), std::io::Cursor::new(input.as_bytes()),
761
+ writer, &mut ServerRunReport::default(), &marker)
762
+ }
763
+
764
+ #[cfg(unix)]
765
+ fn handshake_receipts(ws: &Path) -> usize {
766
+ std::fs::read_dir(ws.join(".team/runtime/mcp-handshakes"))
767
+ .map(|files| files.filter_map(Result::ok)
768
+ .filter(|file| file.path().extension().is_some_and(|ext| ext == "json")).count())
769
+ .unwrap_or(0)
770
+ }
771
+
772
+ #[test]
773
+ #[cfg(unix)]
774
+ fn stdio_handshake_receipt_requires_initialize_and_tools_list_flush() {
775
+ let initialize = r#"{"jsonrpc":"2.0","id":1,"method":"initialize"}"#;
776
+ let tools = r#"{"jsonrpc":"2.0","id":2,"method":"tools/list"}"#;
777
+ let invalid_initialize = r#"{"jsonrpc":"2.0","method":"initialize"}"#;
778
+ let invalid_tools = r#"{"jsonrpc":"2.0","method":"tools/list"}"#;
779
+ for (tag, requests, expected) in [
780
+ ("init-only", vec![initialize], 0),
781
+ ("list-only", vec![tools], 0),
782
+ ("invalid-init", vec![invalid_initialize, tools], 0),
783
+ ("invalid-list", vec![initialize, invalid_tools], 0),
784
+ ("ready", vec![initialize, tools], 1),
785
+ ] {
786
+ let ws = marker_ws(tag);
787
+ let mut output = Vec::new();
788
+ run_bound_handshake(&ws, &(requests.join("\n") + "\n"), &mut output).unwrap();
789
+ assert_eq!(handshake_receipts(&ws), expected, "{tag}");
790
+ let _ = std::fs::remove_dir_all(ws);
791
+ }
792
+ }
793
+
794
+ #[test]
795
+ #[cfg(unix)]
796
+ fn stdio_failed_tools_list_flush_cannot_publish_a_handshake() {
797
+ struct FailSecondFlush(usize);
798
+ impl Write for FailSecondFlush {
799
+ fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> { Ok(buf.len()) }
800
+ fn flush(&mut self) -> std::io::Result<()> {
801
+ self.0 += 1;
802
+ if self.0 == 2 { Err(std::io::Error::other("controlled tools/list flush failure")) }
803
+ else { Ok(()) }
804
+ }
805
+ }
806
+ let ws = marker_ws("handshake-flush-failure");
807
+ let input = "{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"initialize\"}\n{\"jsonrpc\":\"2.0\",\"id\":2,\"method\":\"tools/list\"}\n";
808
+ assert!(run_bound_handshake(&ws, input, &mut FailSecondFlush(0)).is_err());
809
+ assert_eq!(handshake_receipts(&ws), 0);
810
+ let _ = std::fs::remove_dir_all(ws);
811
+ }
812
+
728
813
  #[test]
729
814
  fn stdio_loop_writes_started_and_stdin_eof_exit_markers() {
730
815
  let ws = marker_ws("eof");
@@ -302,6 +302,49 @@ fn prepare_message_state(
302
302
  Ok(Ok(prepared))
303
303
  }
304
304
 
305
+ #[cfg(unix)]
306
+ fn mcp_handshake_pending_outcome(
307
+ store: &MessageStore,
308
+ event_log: &EventLog,
309
+ message_id: &str,
310
+ attempt: u32,
311
+ reason: &str,
312
+ ) -> Result<DeliveryOutcome, MessagingError> {
313
+ // No paste/key has occurred. Only this exact claim may be released; a
314
+ // concurrent cancellation or newer delivery must never be overwritten.
315
+ if !store.release_delivery_before_input(message_id, attempt, reason)? {
316
+ return Ok(DeliveryOutcome {
317
+ ok: false,
318
+ status: DeliveryStatus::Refused,
319
+ message_status: MessageStatusShadow("target_resolved".to_string()),
320
+ message_id: Some(message_id.to_string()),
321
+ verification: None,
322
+ stage: None,
323
+ reason: Some(DeliveryRefusal::MessageAlreadyClaimed),
324
+ channel: None,
325
+ ack_forced_off: false,
326
+ turn_verification: None,
327
+ });
328
+ }
329
+ event_log.write("delivery.deferred_mcp_handshake", serde_json::json!({
330
+ "message_id": message_id,
331
+ "reason": reason,
332
+ "effect": "none",
333
+ }))?;
334
+ Ok(DeliveryOutcome {
335
+ ok: false,
336
+ status: DeliveryStatus::Degraded,
337
+ message_status: MessageStatusShadow("target_resolved".to_string()),
338
+ message_id: Some(message_id.to_string()),
339
+ verification: Some(reason.to_string()),
340
+ stage: None,
341
+ reason: None,
342
+ channel: None,
343
+ ack_forced_off: false,
344
+ turn_verification: None,
345
+ })
346
+ }
347
+
305
348
  fn deliver_prepared_message(
306
349
  workspace: &Path,
307
350
  store: &MessageStore,
@@ -642,6 +685,25 @@ fn deliver_prepared_message(
642
685
  turn_verification: None,
643
686
  });
644
687
  }
688
+ // Startup control must remain reachable before protocol admission: some
689
+ // clients cannot initialize MCP until their trust prompt is answered.
690
+ // No business input is allowed before the current tools/list flush. A
691
+ // readiness-only refusal releases this exact claim without spending an
692
+ // input attempt; it never holds a pane-input lock or sleeps for startup.
693
+ #[cfg(unix)]
694
+ {
695
+ let admission = crate::mcp_server::readiness::preflight(
696
+ workspace, canonical_owner_team_id.as_deref(), &message.recipient, state,
697
+ ).and_then(|admission| {
698
+ if let Some(admission) = admission.as_ref() {
699
+ crate::mcp_server::readiness::revalidate(admission, &target, &live_targets)?;
700
+ }
701
+ Ok(())
702
+ });
703
+ if let Err(reason) = admission {
704
+ return mcp_handshake_pending_outcome(store, event_log, message_id, attempt, reason);
705
+ }
706
+ }
645
707
  let rendered = render_message(
646
708
  &message.sender,
647
709
  message.task_id.as_deref(),
@@ -28,6 +28,44 @@ use crate::transport::{
28
28
  TurnVerification, WindowName,
29
29
  };
30
30
 
31
+ #[test]
32
+ #[cfg(unix)]
33
+ fn eager_mcp_waits_without_spending_input_attempts_then_delivers_the_same_message() {
34
+ let mut case = WorkerCase::new("mcp-handshake", "cursor_agent");
35
+ let carrier = crate::mcp_server::readiness::test_carrier_pid().unwrap();
36
+ let config = case.workspace.join(".team/runtime/mcp/w1.json");
37
+ std::fs::create_dir_all(config.parent().unwrap()).unwrap();
38
+ std::fs::write(&config, serde_json::to_vec(&serde_json::json!({
39
+ "mcpServers": {"team_orchestrator": {
40
+ "command": std::env::current_exe().unwrap().canonicalize().unwrap(),
41
+ "env": {"TEAM_AGENT_WORKSPACE": case.workspace, "TEAM_AGENT_ID": "w1", "TEAM_AGENT_OWNER_TEAM_ID": "dup-team"}
42
+ }}
43
+ })).unwrap()).unwrap();
44
+ case.state["team_key"] = serde_json::json!("dup-team");
45
+ case.state["agents"]["w1"]["mcp_config"] = serde_json::json!(config);
46
+ case.state["agents"]["w1"]["pane_pid"] = serde_json::json!(carrier);
47
+ case.state["agents"]["w1"]["spawned_at"] = serde_json::json!((chrono::Utc::now() - chrono::Duration::seconds(10)).to_rfc3339());
48
+ case.state["teams"] = serde_json::json!({"dup-team": case.state.clone()});
49
+ crate::state::persist::save_runtime_state(&case.workspace, &case.state).unwrap();
50
+ let transport = RecordingTransport { carrier: Some(carrier), ..RecordingTransport::new("") };
51
+ let message_id = case.store.create_message(None, "leader", "w1", "handshake canary", None, false, Some("dup-team")).unwrap();
52
+ for _ in 0..3 {
53
+ let outcome = deliver_pending_message(&case.workspace, &case.store, &transport,
54
+ &message_id, &case.event_log, &case.state).unwrap();
55
+ assert!(!outcome.ok);
56
+ assert!(outcome.verification.as_deref().unwrap().starts_with("mcp_handshake_"));
57
+ assert_eq!(transport.inject_count(), 0);
58
+ }
59
+ let connection = rusqlite::Connection::open(case.store.db_path()).unwrap();
60
+ let attempts: i64 = connection.query_row("select delivery_attempts from messages where message_id=?1", [&message_id], |row| row.get(0)).unwrap();
61
+ assert_eq!(attempts, 0, "waiting is not a physical delivery attempt");
62
+ crate::mcp_server::readiness::publish(&case.workspace, "dup-team", "w1", chrono::Utc::now()).unwrap();
63
+ let outcome = deliver_pending_message(&case.workspace, &case.store, &transport,
64
+ &message_id, &case.event_log, &case.state).unwrap();
65
+ assert_eq!(outcome.message_id.as_deref(), Some(message_id.as_str()));
66
+ assert_eq!(transport.inject_count(), 1, "same queued row proceeds after the real receipt");
67
+ }
68
+
31
69
  #[test]
32
70
  fn error_retry_claim_is_single_winner_across_two_delivers() {
33
71
  let case = WorkerCase::new("claim-gate", "fake");
@@ -273,6 +311,7 @@ fn poison_state_file(workspace: &Path) {
273
311
  struct RecordingTransport {
274
312
  injects: Arc<AtomicUsize>,
275
313
  capture: Arc<Mutex<String>>,
314
+ carrier: Option<u32>,
276
315
  }
277
316
 
278
317
  impl RecordingTransport {
@@ -280,6 +319,7 @@ impl RecordingTransport {
280
319
  Self {
281
320
  injects: Arc::new(AtomicUsize::new(0)),
282
321
  capture: Arc::new(Mutex::new(capture.to_string())),
322
+ carrier: None,
283
323
  }
284
324
  }
285
325
 
@@ -370,7 +410,7 @@ impl Transport for RecordingTransport {
370
410
  current_command: Some("sleep".to_string()),
371
411
  current_path: None,
372
412
  active: true,
373
- pane_pid: None,
413
+ pane_pid: self.carrier,
374
414
  leader_env: BTreeMap::new(),
375
415
  }])
376
416
  }
@@ -214,6 +214,13 @@ pub trait ProviderAdapter {
214
214
  /// compatible_api 走 `ensure_compatible_claude_mcp_config`)。
215
215
  fn mcp_config(&self, auth_mode: AuthMode) -> Result<McpConfig, ProviderError>;
216
216
 
217
+ /// Eager stdio clients must publish their initial MCP handshake before input.
218
+ /// Pi owns admission in its runtime registry hooks; Fake has no MCP client.
219
+ /// The process-stamp receipt port is currently POSIX-only, not a Windows claim.
220
+ fn requires_worker_mcp_handshake(&self) -> bool {
221
+ cfg!(unix) && !matches!(self.provider(), Provider::Fake | Provider::Pi)
222
+ }
223
+
217
224
  /// 安装 MCP server。gemini 写全局 `~/.gemini/settings.json` 并备份/还原
218
225
  /// (`gemini.py:40-78`)——有副作用 I/O,失败可还原 → Result。
219
226
  fn install_mcp(&self, config: &McpConfig) -> Result<(), ProviderError>;
@@ -2217,6 +2217,43 @@ fn kill_session_and_kill_window_argv() {
2217
2217
  );
2218
2218
  }
2219
2219
 
2220
+ #[test]
2221
+ fn issue336_session_listing_accepts_tab_and_literal_escape_without_rewriting_names() {
2222
+ for separator in ["\t", "\\t"] {
2223
+ let (backend, calls) = backend_with(
2224
+ MockResp::Out(ok("")),
2225
+ vec![
2226
+ MockResp::Out(ok(&format!("%7{separator}worker\\tname\n"))),
2227
+ MockResp::Out(ok("%8\n")),
2228
+ ],
2229
+ );
2230
+ backend.preserve_session_before_stop(
2231
+ &SessionName::new("sess"),
2232
+ &[Target::SessionWindow {
2233
+ session: SessionName::new("sess"),
2234
+ window: WindowName::new("worker\\tname"),
2235
+ }],
2236
+ ).unwrap();
2237
+ assert_eq!(calls.lock().unwrap().len(), 4, "separator={separator:?}");
2238
+ }
2239
+ }
2240
+
2241
+ #[test]
2242
+ fn issue336_escaped_listing_keeps_survivors_and_malformed_rows_fail_closed() {
2243
+ let (backend, calls) = backend_with(MockResp::Out(ok("%7\\tworker\n%8\\tleader\n")), vec![]);
2244
+ backend.preserve_session_before_stop(
2245
+ &SessionName::new("sess"), &[Target::Pane(PaneId::new("%7"))],
2246
+ ).unwrap();
2247
+ assert_eq!(calls.lock().unwrap().len(), 1);
2248
+ for row in ["%7 worker", "%7", ""] {
2249
+ let (backend, calls) = backend_with(MockResp::Out(ok(row)), vec![]);
2250
+ assert!(backend.preserve_session_before_stop(
2251
+ &SessionName::new("sess"), &[Target::Pane(PaneId::new("%7"))],
2252
+ ).is_err());
2253
+ assert_eq!(calls.lock().unwrap().len(), 1);
2254
+ }
2255
+ }
2256
+
2220
2257
  #[test]
2221
2258
  fn stop_session_preservation_creates_processless_anchor_by_exact_pane_id() {
2222
2259
  let (be, rec) = backend_with(
@@ -4201,13 +4201,20 @@ impl Transport for TmuxBackend {
4201
4201
  return Err(subprocess_error(list_argv, output));
4202
4202
  }
4203
4203
  for line in output.stdout.lines() {
4204
- let Some((pane, window)) = line.split_once('\t') else {
4204
+ // Some tmux/runner combinations return the literal escape. Split
4205
+ // only the first column boundary; window names are not unescaped.
4206
+ let separator = line.find('\t').map(|at| (at, 1)).into_iter()
4207
+ .chain(line.find("\\t").map(|at| (at, 2)))
4208
+ .min_by_key(|(at, _)| *at);
4209
+ let Some((at, width)) = separator else {
4205
4210
  return Err(TransportError::Subprocess {
4206
4211
  argv: list_argv,
4207
4212
  code: output.code,
4208
4213
  stderr: "tmux returned an invalid session pane listing".to_string(),
4209
4214
  });
4210
4215
  };
4216
+ let (pane, rest) = line.split_at(at);
4217
+ let window = &rest[width..];
4211
4218
  if !targets.iter().any(|target| match target {
4212
4219
  Target::Pane(id) => id.as_str() == pane,
4213
4220
  Target::SessionWindow {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@team-agent/installer",
3
- "version": "0.5.119",
3
+ "version": "0.5.121",
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.119",
24
- "@team-agent/cli-darwin-x64": "0.5.119",
25
- "@team-agent/cli-linux-x64": "0.5.119"
23
+ "@team-agent/cli-darwin-arm64": "0.5.121",
24
+ "@team-agent/cli-darwin-x64": "0.5.121",
25
+ "@team-agent/cli-linux-x64": "0.5.121"
26
26
  },
27
27
  "scripts": {
28
28
  "postinstall": "node npm/bincheck.mjs",
@@ -5,27 +5,30 @@ requires_team_agent: ">=0.5.0"
5
5
  ---
6
6
  # Team Agent
7
7
 
8
- The current assistant is the **leader**. Workers use separate role files and report through Team Agent. Start from a tmux-addressable terminal or supported agent pane, in the workspace root (the parent of the team directory).
8
+ Use the authorized **Leader's** native tool context for lifecycle operations. Workers report through their team-scoped MCP tools; loading this skill does not promote a worker to Leader. Operate in the workspace root, not inside another team's role directory.
9
9
 
10
- ## Decide before creating a team
10
+ ## Check the actual CLI
11
+
12
+ Check `team-agent --version`, `team-agent start --help` and `team-agent team --help` before using this guide. It requires the intuitive `start`, `stop`, `team` and `agent` surface. A version string or this skill's compatibility floor is not proof those commands exist. If missing, ask for an authorized installation of a supporting build; do not silently switch binaries, rewrite bindings or fall back to an old creation recipe.
13
+
14
+ ## User decisions come first
11
15
 
12
16
  **The user decides each worker's provider and permission bypass. Do not silently choose Pi, true, or false.** Reuse an explicit user policy when it covers the worker; otherwise explain the choices and ask before starting.
13
17
 
14
- Use existing resources rather than establishing new accounts: `team-agent doctor --workspace . --json` reports discovered tools, but installed does not mean authenticated and an absent entry or unknown auth is not proof of unavailability. For providers supported by `models`, use `team-agent models --provider NAME` to check exact model IDs. Do not inspect credentials, automatically log in, or silently substitute a provider after a failed probe.
18
+ Use existing resources rather than establishing accounts. `team-agent doctor --workspace . --json` reports discovered tools; installed is not authenticated, and an unknown probe is not proof of unavailability. Use `team-agent models --provider NAME` where supported for exact model IDs. Do not inspect credentials, automatically log in, or silently substitute providers.
15
19
 
16
- | Choice | What to explain |
20
+ | Choice | Explain |
17
21
  |---|---|
18
- | Pi | One Agent tool can use several model catalogs; explicit models require a qualified ID such as `openai-codex/gpt-6-luna` |
19
- | Codex | The user's existing Codex workflow, native tools, permissions and resumable sessions |
20
- | Claude / Claude Code | The user's existing Claude workflow, models, native tools and session behavior |
21
- | Bypass `true` | Fewer permission interruptions, but broader automatic execution authority |
22
- | Bypass `false` | Keep the selected tool's native permissions; human confirmation may interrupt unattended work |
22
+ | Pi | Several model catalogs; an explicit model needs a qualified ID such as `openai-codex/gpt-6-luna` |
23
+ | Codex / Claude / other native CLIs | The user's existing workflow, native permissions, model catalog and supported session capabilities |
24
+ | Bypass `true` | Fewer interruptions, broader automatic authority |
25
+ | Bypass `false` | Native permissions remain; human confirmation may interrupt unattended work |
23
26
 
24
- False is **not** a read-only or sandbox guarantee. Native permission capabilities differ between tools. Suggest combinations based on the user's installed tools, subscriptions and task: for example, an explicitly chosen Codex implementer and Claude reviewer, or two Pi workers when those are the available resources. Different workers may use different providers; an existing worker cannot change provider.
27
+ False is **not** a read-only or sandbox guarantee. Different workers may use different providers; an existing worker cannot change provider. Do not invent a model or effort when the user left it unset.
25
28
 
26
- ## Create and start
29
+ ## Prepare once, start every day
27
30
 
28
- Keep the goal in `TEAM.md` and each worker's instructions in `agents/*.md`. The filename stem is the worker's in-team short name.
31
+ Keep the goal in `TEAM.md` and each worker's instructions in `agents/*.md`. The filename stem is the worker's in-team short name:
29
32
 
30
33
  ```text
31
34
  .team/current/
@@ -33,7 +36,7 @@ Keep the goal in `TEAM.md` and each worker's instructions in `agents/*.md`. The
33
36
  agents/reviewer.md
34
37
  ```
35
38
 
36
- For a **new plan**, record the user's two decisions in every role. This example assumes the user selected Pi and bypass false; it is not a default:
39
+ Record provider and boolean bypass in every new role. This example assumes the user explicitly chose Pi and bypass false; it is not a default:
37
40
 
38
41
  ```markdown
39
42
  ---
@@ -43,43 +46,58 @@ dangerously_skip_permissions: false
43
46
  Review the change and report concrete regression risks.
44
47
  ```
45
48
 
49
+ From the authorized Leader context, select the intended workspace/team:
50
+
46
51
  ```sh
47
- team-agent quick-start .team/current
48
- team-agent send reviewer "Review this change"
49
- team-agent inbox reviewer -n 3
52
+ team-agent start --from .team/current --workspace . --team project-team
53
+ team-agent start --workspace . --team project-team
54
+ team-agent send reviewer "Review this change" --workspace . --team project-team
55
+ team-agent inbox leader --workspace . --team project-team -n 3
50
56
  ```
51
57
 
52
- Traditional file-based quick-start/compile behavior remains supported, including legacy minimal role documents; no conversion to CLI-created roles is required. Legacy interpretation is not permission to silently choose settings for a new user plan. Model and effort may remain unset; preserve the existing file compiler's native/default interpretation rather than inventing a model or effort.
58
+ The first `start` creates a complete configured team through the original creation path. Later `start` keeps physically verified, drift-free live seats unchanged and starts stopped non-paused seats, including explicitly stopped seats. It is not a reset. Missing configuration, ambiguous scope or unknown physical identity is a refusal, not permission to overwrite state. `--dry-run` plans without lifecycle effects; final locked admission is still deferred.
59
+
60
+ Accepted/queued send is **not** the worker's answer. Wait for a natural reply/result. `ok: true` with pending readiness is not business readiness. A live no-op does not synchronize edited role bodies or apply changed launch settings.
53
61
 
54
- To add a **new** seat to an existing team, provider and bypass must be explicitly defined in the CLI or role file. A complete traditional role file needs no repeated flags. If both sources define a field, equal values are accepted and **conflicting values are rejected before any write or startup**:
62
+ ## Stop, wake and restart
55
63
 
56
64
  ```sh
57
- # These values must be the user's choices, not inferred defaults.
58
- team-agent add-agent analyst --provider pi --bypass true --prompt "Analyze the proposed change"
59
- # auditor.md already contains the user's provider and bypass decisions.
60
- team-agent add-agent auditor --role-file /absolute/path/to/auditor.md
65
+ team-agent stop reviewer --workspace . --team project-team
66
+ team-agent start reviewer --workspace . --team project-team
67
+ team-agent restart reviewer --workspace . --team project-team
68
+ team-agent stop --workspace . --team project-team --json
69
+ team-agent start --workspace . --team project-team
70
+ team-agent team restart --workspace . --team project-team --json
61
71
  ```
62
72
 
63
- Add creates and immediately starts the seat. Ordinary add rejects an occupied ID, including stopped seats. Disaster recovery has one explicit exception: `add-agent ID --role-file FILE --force` may reuse the existing force-recreate lifecycle only when the registered old pane is positively confirmed dead and no live target cohort exists. A live seat or unknown/missing death proof is refused; force is not a routine configuration-update shortcut. External role files are imported into the team's `agents/ID.md` and left untouched. A file already at that path is used in place, without copying itself.
73
+ | Intent | Rule |
74
+ |---|---|
75
+ | `start AGENT` | Wake a stopped registered seat; verified live/no-drift is a no-op. A RoleOnly seat can be added from its complete role file |
76
+ | `start` | Explicitly activate the selected non-paused roster; do not use it unless that activation is authorized |
77
+ | `stop AGENT` / `stop` | Retain saved session associations; whole-team stop also checks scoped residual resources |
78
+ | `restart AGENT` / `team restart` | Default `--session auto`: resume when proven, otherwise only the original legal fresh fallback. **Auto does not guarantee preserved memory** |
79
+ | `--session preserve` | Require available continuation evidence; refuse before stopping when continuation is known unavailable |
80
+
81
+ Team restart preserves explicitly stopped and paused seats; use `start` when intentionally waking the roster. Paused seats are not implicitly unpaused. A same-named directory makes `restart ID` ambiguous: use `agent restart ID`. `reviewer.v2` remains an agent ID; choose a team source with `--from DIR`, `team start DIR` or an explicit path such as `./roles`.
82
+
83
+ For strict recovery, prefer `team-agent team restart --session preserve --workspace . --team project-team`. Do not convert a refusal or partial/unknown replacement into reset, force or blind retry. Inspect `status`/`doctor` in the same scope first. Native unsupported capabilities remain refusals, never a legacy fallback.
64
84
 
65
- ## Change and start an existing worker
85
+ ## Add or change one worker
66
86
 
67
- **Stop first if it is running.** Start never implicitly kills or restarts a running worker:
87
+ With the user's provider/bypass choices, `start` unifies adding an absent seat and waking an existing one:
68
88
 
69
89
  ```sh
70
- team-agent stop-agent reviewer
71
- team-agent start-agent reviewer --effort high --bypass true
72
- team-agent stop-agent reviewer
73
- team-agent start-agent reviewer --prompt "Review only regression risks; do not change product code"
90
+ team-agent start auditor --role-file /absolute/path/to/auditor.md
91
+ team-agent start analyst --provider pi --bypass false --prompt "Analyze the proposed change"
92
+ team-agent stop reviewer
93
+ team-agent start reviewer --effort high --bypass false
74
94
  ```
75
95
 
76
- Start updates the same role file and recompiles it before normal startup. Omitted options preserve the file's values and body; `--prompt` replaces the permanent role body, not a one-time task message. `--bypass true` and `--bypass false` are explicit values, not bare switches. Model, effort and an existing profile can also be set. Direct file edits followed by start use the same reload path.
96
+ The Pi/false example above still requires the user's explicit choices. File/CLI conflicts are rejected before writing or startup. Omitted settings/body are preserved; `--prompt` replaces the permanent role body, not a one-time task. Stop a running worker before changing settings or instructions. Provider changes are refused. Startup failure restores role/spec bytes and reports failure, not an applied update.
77
97
 
78
- A different `--provider`, including a manually edited role provider, is rejected before launch. A matching provider is allowed. Create a separate ID to use a different engine. If startup fails, the role file and runtime spec are restored to their pre-command contents; the command reports failure, not an applied update.
98
+ Use `agent add/remove/reset/clone/fork` only for those explicit operations. Reset needs intentional approval and `--discard-session`; fork requires `--as NEW_ID`, and `--in-place` is not supported. See [the operator reference](references/team-agent-operator.md) for capability, profile, force and import boundaries. Never put secrets in role text or copy authentication from another directory.
79
99
 
80
- Profiles keep provider authentication, endpoints and custom-model settings local. Imported roles use this team's/workspace's `profiles`, never the original external directory. Prepare a named profile locally through the public profile commands; do not search for or copy external credentials. Never put secrets in `TEAM.md` or a role file. See [the operator reference](references/team-agent-operator.md) for existing profile and field details. Supported metadata also includes `agent_id` (legacy `name`), `role`, `auth_mode`, and `communication_mode`; old `tools`, `permission_mode`, and `label` keys do not configure a worker.
81
-
82
- ## Global native CLI argv (optional)
100
+ ## Optional native CLI argv routing
83
101
 
84
102
  Default OFF; no workspace or provider login is needed to manage routes:
85
103
 
@@ -93,29 +111,28 @@ team-agent route clear pi
93
111
  team-agent route disable
94
112
  ```
95
113
 
96
- `set` replaces one mapping, `add` appends without deduplication, `clear` removes it;
97
- none changes the switch. `enable`/`disable` preserve mappings in `~/.team-agent/argv-routing.json`.
98
- `TEAM_AGENT_CLI_ARGV_ROUTING` overrides the switch: `1/true/on`, `0/false/off` (case-insensitive,
99
- trimmed); invalid values disable routing. Check `persisted_enabled`, `effective_enabled` and
100
- `override_status` rather than assuming `enable` defeats an environment OFF.
114
+ `set` replaces one mapping, `add` appends without deduplication, `clear` removes it; none changes the switch. `enable`/`disable` preserve mappings in `~/.team-agent/argv-routing.json`. `TEAM_AGENT_CLI_ARGV_ROUTING` overrides the switch: `1/true/on`, `0/false/off` (case-insensitive, trimmed); invalid values disable routing. Check `persisted_enabled`, `effective_enabled` and `override_status`.
101
115
 
102
- Keys: `claude`, `codex`, `copilot`, `gemini_cli`, `grok`, `cursor_agent`, `pi`.
103
- Claude/Claude Code share `claude`; aliases `claude_code/claude-code`, `agent/cursor` are accepted.
104
- The first `--` ends control parsing: subsequent tokens, including `--help`, `--json`, empty strings
105
- and `{workspace}`, are literal data. Tokens go after the executable and before its original tail;
106
- there is no shell expansion. Do not route secrets or override framework-managed session/MCP/permission flags.
116
+ Keys: `claude`, `codex`, `copilot`, `gemini_cli`, `grok`, `cursor_agent`, `pi`. Claude/Claude Code share `claude`; aliases `claude_code/claude-code`, `agent/cursor` are accepted. Everything after the first `--`, including `--help`, `--json`, empty strings and `{workspace}`, is literal argv. No shell expansion. Do not route secrets or override framework-managed session/MCP/permission flags.
107
117
 
108
- The current mapping affects the next actual Leader/worker spawn (also restart, single-seat start/reset,
109
- add/clone, Pi new-seat fork and remove rollback), not running/attached processes, dry-run or helper probes.
110
- Enabled bad configuration prevents native spawn; an emergency environment OFF avoids reading it,
111
- while `route disable` will not overwrite a broken file. Pi `--mode rpc` does not provide an RPC host
112
- or change the existing Team Agent transport.
118
+ Mappings affect the next actual native spawn, not running/attached processes, dry-run or helper probes. Invalid enabled configuration refuses spawn; an emergency environment OFF avoids reading it, while `route disable` will not overwrite a broken file. Pi `--mode rpc` does not provide an RPC host or replace Team Agent's transport.
113
119
 
114
- ## Inspect and stop
120
+ ## Observe, communicate and clean up
115
121
 
116
- Use `team-agent status --json` for readiness and the installed command's `--help` for syntax. `ok: true` with `ready: false` means the team is not ready; follow the reported action. Accepted/queued send is not the worker's reply: wait for a natural response or result. Use `<workspace>::<team>/<agent>` when a fully qualified recipient is needed.
122
+ Use `team-agent status --json` (or `team status`) for the concise node projection and `doctor` for diagnosis; `--detail` does not restore old internal status dumps. Use `<workspace>::<team>/<agent>` for an explicitly qualified logical recipient, or the in-team short name. Workers have exactly `send_message`, `report_result` and optional `get_team_status`; lifecycle stays on the authorized CLI. Read [nested teams](references/team-in-team.md) only when explicitly requested and [recovery](references/recovery-runbook.md) for incidents.
117
123
 
118
- Worker MCP provides `send_message`, `report_result`, and `get_team_status`. Lifecycle actions remain on the authorized CLI. See [nested teams](references/team-in-team.md) only when needed. Stop the selected team with `team-agent shutdown --workspace .` when authorized.
124
+ Finish with `team-agent stop --workspace . --team project-team --json` when authorized. Check the actual scoped residual report; exit0 alone is not proof of zero resources. Do not kill shared servers or touch sibling workspaces.
119
125
 
120
- ---
121
- This skill is a concise public guide. Do not infer a tested version from package metadata; verify the installed CLI with `team-agent --version` when version-specific behavior matters.
126
+ ## Compatibility bridges, not the daily workflow
127
+
128
+ Old commands remain available for existing scripts; their output/defaults are not silently rewritten:
129
+
130
+ | Compatibility entry | Preferred daily surface |
131
+ |---|---|
132
+ | `quick-start` | `start --from TEAMDIR` for creation and later `start` for activation |
133
+ | `shutdown` | `stop` / `team stop` |
134
+ | `add-agent` / `start-agent` / `stop-agent` | `agent add` for strict add, or `start AGENT` / `stop AGENT` |
135
+ | `remove-agent` / `reset-agent` / `clone-agent` / `fork-agent` | The corresponding `agent` operation |
136
+ | Bare or explicit-path `restart` | Frozen preserve bridge; deliberate `team restart` defaults to auto |
137
+
138
+ These are not all byte-equivalent aliases: legacy single-seat start refuses a running seat, while new start can prove a no-op. Do not mechanically rename automation, assume unsupported provider capabilities, or infer deployment from package metadata.