@team-agent/installer 0.5.0 → 0.5.1

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.
@@ -7,6 +7,38 @@ use crate::messaging::{DeliveryOutcome, DeliveryRefusal, DeliveryStage, Delivery
7
7
  /// `cmd_send`(`commands.py:164`)。解析 target(`--to` fanout / 单 target / `*`)→ [`MessageTarget`],
8
8
  /// 拼 [`SendOptions`](no_ack→requires_ack 取反、no_wait→wait_visible 取反等)→ `messaging::send_message`。
9
9
  pub fn cmd_send(args: &SendArgs) -> Result<CmdResult, CliError> {
10
+ if let Some(ref to_name) = args.to_name {
11
+ if args.pane.is_some() || args.target.is_some() || args.targets.is_some() {
12
+ return Err(CliError::Usage(
13
+ "--to-name and --pane/TARGET/--to are mutually exclusive: \
14
+ --to-name resolves a stable workspace/team/name to a live pane"
15
+ .to_string(),
16
+ ));
17
+ }
18
+ let content = args.message.join(" ");
19
+ if content.is_empty() {
20
+ return Err(CliError::Usage("--to-name requires a non-empty message".to_string()));
21
+ }
22
+ let (resolved, transport) =
23
+ match crate::cli::named_address::resolve_name_for_cli(&args.workspace, to_name) {
24
+ Ok(resolved) => resolved,
25
+ Err(error) if args.json => {
26
+ return Ok(CmdResult::from_json(error.to_json(), args.json));
27
+ }
28
+ Err(error) => return Err(CliError::Usage(error.n38_message())),
29
+ };
30
+ let mut value = send_to_named_pane_direct(
31
+ &args.workspace,
32
+ transport.as_ref(),
33
+ &resolved,
34
+ &content,
35
+ &args.sender,
36
+ args.task.as_deref(),
37
+ args.json,
38
+ )?;
39
+ add_send_reminder_if_ok(&mut value);
40
+ return Ok(CmdResult::from_json(value, args.json));
41
+ }
10
42
  // F1 (0.3.26, cross-team send): --pane <pane_id> direct targeting.
11
43
  // Mutually exclusive with target / --to (agent-name routing).
12
44
  if let Some(ref pane_id) = args.pane {
@@ -145,6 +177,78 @@ fn send_to_pane_direct(
145
177
  }))
146
178
  }
147
179
 
180
+ fn send_to_named_pane_direct(
181
+ sender_workspace: &Path,
182
+ transport: &dyn crate::transport::Transport,
183
+ resolved: &crate::cli::named_address::ResolvedNamedAddress,
184
+ content: &str,
185
+ sender: &str,
186
+ task_id: Option<&str>,
187
+ _json: bool,
188
+ ) -> Result<serde_json::Value, CliError> {
189
+ use crate::messaging::delivery::render_message;
190
+ use crate::transport::{InjectPayload, Key, PaneId, Target};
191
+
192
+ let message_id = format!("named_send_{}", chrono::Utc::now().timestamp_millis());
193
+ let rendered = render_message(sender, task_id, content, &message_id);
194
+ let target = Target::Pane(PaneId::new(&resolved.pane_id));
195
+ let sender_run_workspace = crate::model::paths::canonical_run_workspace(sender_workspace)
196
+ .unwrap_or_else(|_| sender_workspace.to_path_buf());
197
+ let event_log = crate::event_log::EventLog::new(&sender_run_workspace);
198
+ if let Some(warning) = &resolved.warning {
199
+ eprintln!("warning: {warning}");
200
+ }
201
+ let report = transport
202
+ .inject(&target, &InjectPayload::Text(rendered), Key::Enter, true)
203
+ .map_err(|e| {
204
+ CliError::Runtime(format!(
205
+ "inject to named target {} pane {} failed: {e}",
206
+ resolved.raw_name, resolved.pane_id
207
+ ))
208
+ })?;
209
+ let target_kind = named_target_kind_wire(resolved.target_kind);
210
+ let event = serde_json::json!({
211
+ "to_name": resolved.raw_name,
212
+ "target_kind": target_kind,
213
+ "sender": sender,
214
+ "sender_workspace": sender_run_workspace.display().to_string(),
215
+ "target_workspace": resolved.target_workspace.display().to_string(),
216
+ "team_key": resolved.team_key,
217
+ "agent_id": resolved.agent_id,
218
+ "pane_id": resolved.pane_id,
219
+ "session_name": resolved.session_name,
220
+ "window_name": resolved.window_name,
221
+ "tmux_endpoint": resolved.tmux_endpoint,
222
+ "state_pane_id": resolved.state_pane_id,
223
+ "state_pane_stale": resolved.state_pane_stale,
224
+ "agent_status": resolved.agent_status,
225
+ "warning": resolved.warning,
226
+ "message_id": message_id,
227
+ "submit_verification": crate::transport::submit_verification_wire(report.submit_verification),
228
+ "inject_verification": format!("{:?}", report.inject_verification),
229
+ });
230
+ let _ = event_log.write("send.name_direct", event.clone());
231
+ let ok = matches!(
232
+ report.submit_verification,
233
+ crate::transport::SubmitVerification::EnterSentWithoutPlaceholderCheck
234
+ | crate::transport::SubmitVerification::PastedContentPromptAbsentAfterSubmit
235
+ | crate::transport::SubmitVerification::KeySentAfterVisibleToken { .. }
236
+ );
237
+ let mut value = event;
238
+ if let Some(obj) = value.as_object_mut() {
239
+ obj.insert("ok".to_string(), serde_json::json!(ok));
240
+ }
241
+ Ok(value)
242
+ }
243
+
244
+ fn named_target_kind_wire(kind: crate::cli::named_address::NamedTargetKind) -> &'static str {
245
+ match kind {
246
+ crate::cli::named_address::NamedTargetKind::Worker => "worker",
247
+ crate::cli::named_address::NamedTargetKind::Leader => "leader",
248
+ crate::cli::named_address::NamedTargetKind::SessionWindow => "session_window",
249
+ }
250
+ }
251
+
148
252
  pub fn cmd_fallback_send_leader(args: &FallbackSendLeaderArgs) -> Result<CmdResult, CliError> {
149
253
  if let Some(value) = fallback_business_refusal(&args.primary_error, args.json) {
150
254
  return Ok(value);
@@ -50,6 +50,7 @@ use super::*;
50
50
  json: true,
51
51
  message_id: None,
52
52
  pane: None,
53
+ to_name: None,
53
54
  };
54
55
  let _ = cmd_send(&args);
55
56
 
@@ -87,6 +87,7 @@ mod divergence;
87
87
  mod lane_c;
88
88
  mod leader_watch;
89
89
  mod missing_subcommands;
90
+ mod named_address;
90
91
  mod repair_state_byte_lock;
91
92
  mod peer_allow;
92
93
  mod run_delegation;
@@ -0,0 +1,455 @@
1
+ use super::*;
2
+ use crate::transport::test_support::OfflineTransport;
3
+ use crate::transport::{PaneId, PaneInfo, SessionName, WindowName};
4
+ use serde_json::json;
5
+ use std::collections::BTreeMap;
6
+ use std::path::{Path, PathBuf};
7
+
8
+ fn named_ws(tag: &str) -> PathBuf {
9
+ use std::sync::atomic::{AtomicU64, Ordering};
10
+ static N: AtomicU64 = AtomicU64::new(0);
11
+ let dir = std::env::temp_dir().join(format!(
12
+ "ta-named-{tag}-{}-{}",
13
+ std::process::id(),
14
+ N.fetch_add(1, Ordering::Relaxed)
15
+ ));
16
+ std::fs::create_dir_all(dir.join(".team").join("runtime")).unwrap();
17
+ dir
18
+ }
19
+
20
+ fn seed_state(ws: &Path, state: serde_json::Value) {
21
+ std::fs::create_dir_all(ws.join(".team").join("runtime")).unwrap();
22
+ std::fs::write(
23
+ ws.join(".team").join("runtime").join("state.json"),
24
+ serde_json::to_vec_pretty(&state).unwrap(),
25
+ )
26
+ .unwrap();
27
+ }
28
+
29
+ fn state_with_teams(teams: serde_json::Value) -> serde_json::Value {
30
+ json!({
31
+ "session_name": "projected",
32
+ "tmux_endpoint": "workspace-socket",
33
+ "teams": teams,
34
+ })
35
+ }
36
+
37
+ fn worker_team(session: &str, agent: &str, pane: &str, window: &str) -> serde_json::Value {
38
+ json!({
39
+ "session_name": session,
40
+ "tmux_endpoint": format!("{session}-socket"),
41
+ "agents": {
42
+ agent: {
43
+ "status": "running",
44
+ "pane_id": pane,
45
+ "window": window,
46
+ }
47
+ }
48
+ })
49
+ }
50
+
51
+ fn worker_team_with_status(
52
+ session: &str,
53
+ agent: &str,
54
+ pane: &str,
55
+ window: &str,
56
+ status: &str,
57
+ ) -> serde_json::Value {
58
+ let mut team = worker_team(session, agent, pane, window);
59
+ team["agents"][agent]["status"] = json!(status);
60
+ team
61
+ }
62
+
63
+ fn leader_team(session: &str, pane: &str, socket: &str) -> serde_json::Value {
64
+ json!({
65
+ "session_name": session,
66
+ "tmux_endpoint": format!("{session}-worker-socket"),
67
+ "agents": {},
68
+ "leader_receiver": {
69
+ "pane_id": pane,
70
+ "session_name": format!("{session}-leader"),
71
+ "window_name": "codex",
72
+ "tmux_socket": socket,
73
+ "status": "attached"
74
+ }
75
+ })
76
+ }
77
+
78
+ fn pane(session: &str, window: &str, pane_id: &str) -> PaneInfo {
79
+ PaneInfo {
80
+ pane_id: PaneId::new(pane_id),
81
+ session: SessionName::new(session),
82
+ window_index: None,
83
+ window_name: Some(WindowName::new(window)),
84
+ pane_index: None,
85
+ tty: None,
86
+ current_command: None,
87
+ current_path: None,
88
+ active: false,
89
+ pane_pid: None,
90
+ leader_env: BTreeMap::new(),
91
+ }
92
+ }
93
+
94
+ fn assert_kind(
95
+ result: Result<ResolvedNamedAddress, NamedAddressError>,
96
+ kind: NamedAddressErrorKind,
97
+ ) {
98
+ let err = result.unwrap_err();
99
+ assert_eq!(err.kind, kind, "{err}");
100
+ let n38 = err.n38_message();
101
+ assert!(n38.contains("Error: "), "{n38}");
102
+ assert!(n38.contains("Action: "), "{n38}");
103
+ assert!(n38.contains("Log: "), "{n38}");
104
+ }
105
+
106
+ fn named_send_args(
107
+ ws: &Path,
108
+ to_name: Option<&str>,
109
+ pane: Option<&str>,
110
+ targets: Option<&str>,
111
+ message: &[&str],
112
+ ) -> SendArgs {
113
+ SendArgs {
114
+ target: None,
115
+ message: message.iter().map(|part| (*part).to_string()).collect(),
116
+ targets: targets.map(str::to_string),
117
+ workspace: ws.to_path_buf(),
118
+ team: None,
119
+ task: None,
120
+ sender: "leader".to_string(),
121
+ no_ack: false,
122
+ no_wait: false,
123
+ watch_result: false,
124
+ timeout: 0.0,
125
+ confirm_human: false,
126
+ json: true,
127
+ message_id: None,
128
+ pane: pane.map(str::to_string),
129
+ to_name: to_name.map(str::to_string),
130
+ }
131
+ }
132
+
133
+ #[test]
134
+ fn resolve_name_current_team_unique() {
135
+ let ws = named_ws("unique");
136
+ seed_state(
137
+ &ws,
138
+ state_with_teams(json!({
139
+ "team-a": worker_team("team-a", "qa", "%old", "qa")
140
+ })),
141
+ );
142
+ let transport = OfflineTransport::new().with_targets(vec![pane("team-a", "qa", "%1")]);
143
+
144
+ let resolved = resolve_name_with_transport(&ws, "qa", &transport).unwrap();
145
+ assert_eq!(resolved.pane_id, "%1");
146
+ assert_eq!(resolved.team_key.as_deref(), Some("team-a"));
147
+ assert_eq!(resolved.agent_id.as_deref(), Some("qa"));
148
+ assert_eq!(resolved.target_kind, NamedTargetKind::Worker);
149
+ let _ = std::fs::remove_dir_all(&ws);
150
+ }
151
+
152
+ #[test]
153
+ fn resolve_name_ambiguous_same_role() {
154
+ let ws = named_ws("ambiguous-role");
155
+ seed_state(
156
+ &ws,
157
+ state_with_teams(json!({
158
+ "team-a": worker_team("team-a", "qa", "%1", "qa"),
159
+ "team-b": worker_team("team-b", "qa", "%2", "qa")
160
+ })),
161
+ );
162
+ let transport = OfflineTransport::new()
163
+ .with_targets(vec![pane("team-a", "qa", "%1"), pane("team-b", "qa", "%2")]);
164
+
165
+ let err = resolve_name_with_transport(&ws, "qa", &transport).unwrap_err();
166
+ assert_eq!(err.kind, NamedAddressErrorKind::NameAmbiguous);
167
+ let candidates = serde_json::to_string(&err.candidates).unwrap();
168
+ assert!(candidates.contains("team-a/qa"), "{candidates}");
169
+ assert!(candidates.contains("team-b/qa"), "{candidates}");
170
+ let _ = std::fs::remove_dir_all(&ws);
171
+ }
172
+
173
+ #[test]
174
+ fn resolve_name_team_disambiguated() {
175
+ let ws = named_ws("team-disambiguated");
176
+ seed_state(
177
+ &ws,
178
+ state_with_teams(json!({
179
+ "team-a": worker_team("team-a", "qa", "%1", "qa"),
180
+ "team-b": worker_team("team-b", "qa", "%2", "qa")
181
+ })),
182
+ );
183
+ let transport = OfflineTransport::new()
184
+ .with_targets(vec![pane("team-a", "qa", "%1"), pane("team-b", "qa", "%2")]);
185
+
186
+ let resolved = resolve_name_with_transport(&ws, "team-b/qa", &transport).unwrap();
187
+ assert_eq!(resolved.pane_id, "%2");
188
+ assert_eq!(resolved.team_key.as_deref(), Some("team-b"));
189
+ let _ = std::fs::remove_dir_all(&ws);
190
+ }
191
+
192
+ #[test]
193
+ fn resolve_name_stale_pane() {
194
+ let ws = named_ws("stale-pane");
195
+ seed_state(
196
+ &ws,
197
+ state_with_teams(json!({
198
+ "team-a": worker_team("team-a", "qa", "%old", "qa")
199
+ })),
200
+ );
201
+ let transport = OfflineTransport::new().with_targets(vec![pane("team-a", "qa", "%new")]);
202
+
203
+ let resolved = resolve_name_with_transport(&ws, "team-a/qa", &transport).unwrap();
204
+ assert_eq!(resolved.pane_id, "%new");
205
+ assert_eq!(resolved.state_pane_id.as_deref(), Some("%old"));
206
+ assert!(resolved.state_pane_stale);
207
+ let _ = std::fs::remove_dir_all(&ws);
208
+ }
209
+
210
+ #[test]
211
+ fn resolve_name_cross_workspace_socket() {
212
+ let sender = named_ws("sender");
213
+ let target = named_ws("target");
214
+ seed_state(&sender, state_with_teams(json!({})));
215
+ seed_state(
216
+ &target,
217
+ state_with_teams(json!({
218
+ "alpha": worker_team("alpha-session", "qa", "%target-old", "qa")
219
+ })),
220
+ );
221
+ let transport = OfflineTransport::new()
222
+ .with_tmux_endpoint("target-socket")
223
+ .with_targets(vec![pane("alpha-session", "qa", "%target-live")]);
224
+
225
+ let resolved = resolve_name_with_transport(
226
+ &sender,
227
+ &format!("{}::alpha/qa", target.display()),
228
+ &transport,
229
+ )
230
+ .unwrap();
231
+ assert_eq!(resolved.sender_workspace, sender);
232
+ assert_eq!(resolved.target_workspace, target);
233
+ assert_eq!(resolved.tmux_endpoint.as_deref(), Some("target-socket"));
234
+ assert_eq!(resolved.pane_id, "%target-live");
235
+ let _ = std::fs::remove_dir_all(&sender);
236
+ let _ = std::fs::remove_dir_all(&target);
237
+ }
238
+
239
+ #[test]
240
+ fn send_to_name_mutual_exclusion() {
241
+ let cwd = named_ws("mutual");
242
+ let args = named_send_args(&cwd, Some("team-a/qa"), Some("%1"), None, &["hello"]);
243
+ let err = cmd_send(&args).unwrap_err();
244
+ assert!(
245
+ matches!(err, CliError::Usage(ref message) if message.contains("--to-name and --pane/TARGET/--to are mutually exclusive")),
246
+ "expected --to-name mutual exclusion, got {err:?}"
247
+ );
248
+
249
+ let args = named_send_args(&cwd, Some("team-a/qa"), None, Some("worker"), &["hello"]);
250
+ let err = cmd_send(&args).unwrap_err();
251
+ assert!(
252
+ matches!(err, CliError::Usage(ref message) if message.contains("--to-name and --pane/TARGET/--to are mutually exclusive")),
253
+ "expected --to-name/--to mutual exclusion, got {err:?}"
254
+ );
255
+
256
+ let args = named_send_args(&cwd, Some("team-a/qa"), None, None, &[]);
257
+ let err = cmd_send(&args).unwrap_err();
258
+ assert!(
259
+ matches!(err, CliError::Usage(ref message) if message == "--to-name requires a non-empty message"),
260
+ "expected empty-message usage error, got {err:?}"
261
+ );
262
+ let _ = std::fs::remove_dir_all(&cwd);
263
+ }
264
+
265
+ #[test]
266
+ fn resolve_leader_name_uses_receiver_socket() {
267
+ let ws = named_ws("leader-socket");
268
+ seed_state(
269
+ &ws,
270
+ state_with_teams(json!({
271
+ "alpha": leader_team("alpha-session", "%leader", "/tmp/leader.sock")
272
+ })),
273
+ );
274
+ let transport = OfflineTransport::new()
275
+ .with_tmux_endpoint("/tmp/leader.sock")
276
+ .with_targets(vec![pane("alpha-session-leader", "codex", "%leader")]);
277
+
278
+ let resolved = resolve_name_with_transport(&ws, "alpha/leader", &transport).unwrap();
279
+ assert_eq!(resolved.target_kind, NamedTargetKind::Leader);
280
+ assert_eq!(resolved.pane_id, "%leader");
281
+ assert_eq!(resolved.tmux_endpoint.as_deref(), Some("/tmp/leader.sock"));
282
+ let _ = std::fs::remove_dir_all(&ws);
283
+ }
284
+
285
+ #[test]
286
+ fn resolve_leader_name_not_live() {
287
+ let ws = named_ws("leader-not-live");
288
+ seed_state(
289
+ &ws,
290
+ state_with_teams(json!({
291
+ "alpha": leader_team("alpha-session", "%missing", "/tmp/leader.sock")
292
+ })),
293
+ );
294
+ let transport = OfflineTransport::new()
295
+ .with_tmux_endpoint("/tmp/leader.sock")
296
+ .with_targets(vec![pane("other", "codex", "%other")]);
297
+
298
+ let err = resolve_name_with_transport(&ws, "alpha/leader", &transport).unwrap_err();
299
+ assert_eq!(err.kind, NamedAddressErrorKind::NameNotLive);
300
+ let n38 = err.n38_message();
301
+ assert!(n38.contains("claim-leader"), "{n38}");
302
+ assert!(n38.contains("attach-leader"), "{n38}");
303
+ assert!(n38.contains("takeover"), "{n38}");
304
+ let _ = std::fs::remove_dir_all(&ws);
305
+ }
306
+
307
+ #[test]
308
+ fn resolve_leader_name_after_rebind() {
309
+ let ws = named_ws("leader-rebind");
310
+ seed_state(
311
+ &ws,
312
+ state_with_teams(json!({
313
+ "alpha": leader_team("alpha-session", "%new", "/tmp/leader.sock")
314
+ })),
315
+ );
316
+ let transport = OfflineTransport::new()
317
+ .with_tmux_endpoint("/tmp/leader.sock")
318
+ .with_targets(vec![pane("alpha-session-leader", "codex", "%new")]);
319
+
320
+ let resolved = resolve_name_with_transport(&ws, "alpha/leader", &transport).unwrap();
321
+ assert_eq!(resolved.pane_id, "%new");
322
+ assert_eq!(resolved.target_kind, NamedTargetKind::Leader);
323
+ let _ = std::fs::remove_dir_all(&ws);
324
+ }
325
+
326
+ #[test]
327
+ fn resolve_name_invalid_syntax() {
328
+ let ws = named_ws("invalid");
329
+ let transport = OfflineTransport::new();
330
+ assert_kind(
331
+ resolve_name_with_transport(&ws, "", &transport),
332
+ NamedAddressErrorKind::NameInvalid,
333
+ );
334
+ assert_kind(
335
+ resolve_name_with_transport(&ws, "team//qa", &transport),
336
+ NamedAddressErrorKind::NameInvalid,
337
+ );
338
+ let _ = std::fs::remove_dir_all(&ws);
339
+ }
340
+
341
+ #[test]
342
+ fn resolve_name_workspace_not_found() {
343
+ let ws = named_ws("sender-missing-target");
344
+ let missing = std::env::temp_dir().join(format!(
345
+ "ta-named-missing-{}-{}",
346
+ std::process::id(),
347
+ chrono::Utc::now().timestamp_nanos_opt().unwrap_or_default()
348
+ ));
349
+ let transport = OfflineTransport::new();
350
+ assert_kind(
351
+ resolve_name_with_transport(&ws, &format!("{}::alpha/qa", missing.display()), &transport),
352
+ NamedAddressErrorKind::WorkspaceNotFound,
353
+ );
354
+ let _ = std::fs::remove_dir_all(&ws);
355
+ }
356
+
357
+ #[test]
358
+ fn resolve_name_state_missing() {
359
+ let ws = named_ws("state-missing");
360
+ let state_path = ws.join(".team").join("runtime").join("state.json");
361
+ let _ = std::fs::remove_file(state_path);
362
+ let transport = OfflineTransport::new();
363
+ assert_kind(
364
+ resolve_name_with_transport(&ws, "alpha/qa", &transport),
365
+ NamedAddressErrorKind::StateNotFound,
366
+ );
367
+ let _ = std::fs::remove_dir_all(&ws);
368
+ }
369
+
370
+ #[test]
371
+ fn resolve_name_same_team_key_across_workspaces() {
372
+ let sender = named_ws("same-team-sender");
373
+ let other = named_ws("same-team-other");
374
+ seed_state(
375
+ &sender,
376
+ state_with_teams(json!({
377
+ "alpha": worker_team("alpha-a", "qa", "%a", "qa")
378
+ })),
379
+ );
380
+ seed_state(
381
+ &other,
382
+ state_with_teams(json!({
383
+ "alpha": worker_team("alpha-b", "qa", "%b", "qa")
384
+ })),
385
+ );
386
+ let sender_transport = OfflineTransport::new().with_targets(vec![pane("alpha-a", "qa", "%a")]);
387
+ let other_transport = OfflineTransport::new().with_targets(vec![pane("alpha-b", "qa", "%b")]);
388
+
389
+ let err = resolve_name_with_workspace_transports(
390
+ &sender,
391
+ "alpha/qa",
392
+ &[
393
+ NamedWorkspaceTransport {
394
+ workspace: &sender,
395
+ transport: &sender_transport,
396
+ },
397
+ NamedWorkspaceTransport {
398
+ workspace: &other,
399
+ transport: &other_transport,
400
+ },
401
+ ],
402
+ )
403
+ .unwrap_err();
404
+ assert_eq!(err.kind, NamedAddressErrorKind::NameAmbiguous);
405
+ assert!(err.n38_message().contains("<workspace>::alpha/qa"));
406
+ let _ = std::fs::remove_dir_all(&sender);
407
+ let _ = std::fs::remove_dir_all(&other);
408
+ }
409
+
410
+ #[test]
411
+ fn resolve_paused_agent_with_live_pane_succeeds_with_warning() {
412
+ let ws = named_ws("paused-live");
413
+ seed_state(
414
+ &ws,
415
+ state_with_teams(json!({
416
+ "alpha": worker_team_with_status("alpha-session", "qa", "%paused", "qa", "paused")
417
+ })),
418
+ );
419
+ let transport =
420
+ OfflineTransport::new().with_targets(vec![pane("alpha-session", "qa", "%paused")]);
421
+
422
+ let resolved = resolve_name_with_transport(&ws, "alpha/qa", &transport).unwrap();
423
+ assert_eq!(resolved.pane_id, "%paused");
424
+ assert_eq!(resolved.agent_status.as_deref(), Some("paused"));
425
+ assert!(resolved.warning.as_deref().unwrap_or("").contains("paused"));
426
+ let _ = std::fs::remove_dir_all(&ws);
427
+ }
428
+
429
+ #[test]
430
+ fn named_address_resolver_does_not_write_leader_receiver_or_owner_fields() {
431
+ let src = std::fs::read_to_string(concat!(
432
+ env!("CARGO_MANIFEST_DIR"),
433
+ "/src/cli/named_address.rs"
434
+ ))
435
+ .unwrap();
436
+ for forbidden in ["leader_receiver.*=", "team_owner.*=", "owner_epoch.*="] {
437
+ assert!(
438
+ !src.contains(forbidden),
439
+ "resolver must not contain binding write pattern {forbidden}"
440
+ );
441
+ }
442
+ for line in src.lines() {
443
+ let trimmed = line.trim();
444
+ for field in ["leader_receiver", "team_owner", "owner_epoch"] {
445
+ assert!(
446
+ !(trimmed.starts_with(field) && trimmed.contains('=')),
447
+ "resolver must not assign binding field `{field}`: {trimmed}"
448
+ );
449
+ assert!(
450
+ !(trimmed.starts_with(&format!(".{field}")) && trimmed.contains('=')),
451
+ "resolver must not assign binding field `{field}`: {trimmed}"
452
+ );
453
+ }
454
+ }
455
+ }
@@ -447,6 +447,7 @@ use super::*;
447
447
  json: false,
448
448
  message_id: None,
449
449
  pane: None,
450
+ to_name: None,
450
451
  }
451
452
  }
452
453
 
@@ -323,6 +323,10 @@ pub struct SendArgs {
323
323
  /// the **cross-team communication** primitive: the target pane does not
324
324
  /// need to be in the sender's team.
325
325
  pub pane: Option<String>,
326
+ /// `--to-name <name>` — stable named addressing. Mutually exclusive with
327
+ /// `target` / `targets` / `--pane`; resolves workspace/team/agent or leader
328
+ /// name to the current live pane before using direct pane injection.
329
+ pub to_name: Option<String>,
326
330
  }
327
331
 
328
332
  /// E23 worker-side emergency fallback for `team_orchestrator.send_message`
@@ -11,6 +11,20 @@ use std::collections::{BTreeMap, BTreeSet};
11
11
  use std::path::{Path, PathBuf};
12
12
 
13
13
  const ANCESTRY_ENV: &str = "TEAM_AGENT_TEST_PROCESS_ANCESTRY_ARGV_JSON";
14
+ const CALLER_IDENTITY_ENVS: &[&str] = &[
15
+ "TMUX",
16
+ "TMUX_PANE",
17
+ "TEAM_AGENT_LEADER_PANE_ID",
18
+ "TEAM_AGENT_LEADER_SESSION_UUID",
19
+ "TEAM_AGENT_LEADER_SESSION_UUID_OVERRIDE",
20
+ "TEAM_AGENT_LEADER_PROVIDER",
21
+ "TEAM_AGENT_MACHINE_FINGERPRINT",
22
+ "TEAM_AGENT_WORKSPACE",
23
+ "TEAM_AGENT_TEAM_ID",
24
+ "TEAM_AGENT_OWNER_TEAM_ID",
25
+ "TEAM_AGENT_ACTIVE_TEAM",
26
+ "TEAM_AGENT_ID",
27
+ ];
14
28
 
15
29
  #[test]
16
30
  #[serial_test::serial(env)]
@@ -141,6 +155,10 @@ struct PhaseGolden {
141
155
 
142
156
  fn run_phase_golden(spec: PhaseGolden) -> Value {
143
157
  let _permission_mode = EnvVarGuard::set(ANCESTRY_ENV, "[]");
158
+ let _caller_identity = CALLER_IDENTITY_ENVS
159
+ .iter()
160
+ .map(|key| EnvVarGuard::unset(key))
161
+ .collect::<Vec<_>>();
144
162
  let team = two_worker_team_dir();
145
163
  let workspace = team.parent().expect("workspace").to_path_buf();
146
164
  seed_healthy_coordinator(&workspace);
@@ -186,6 +204,7 @@ fn run_phase_golden(spec: PhaseGolden) -> Value {
186
204
  json: true,
187
205
  message_id: Some("phase-golden-message".to_string()),
188
206
  pane: None,
207
+ to_name: None,
189
208
  });
190
209
  let collect = cmd_collect(&CollectArgs {
191
210
  workspace: workspace.clone(),
@@ -394,6 +413,14 @@ impl EnvVarGuard {
394
413
  }
395
414
  Self { key, previous }
396
415
  }
416
+
417
+ fn unset(key: &'static str) -> Self {
418
+ let previous = std::env::var(key).ok();
419
+ unsafe {
420
+ std::env::remove_var(key);
421
+ }
422
+ Self { key, previous }
423
+ }
397
424
  }
398
425
 
399
426
  impl Drop for EnvVarGuard {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@team-agent/installer",
3
- "version": "0.5.0",
3
+ "version": "0.5.1",
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.0",
24
- "@team-agent/cli-darwin-x64": "0.5.0",
25
- "@team-agent/cli-linux-x64": "0.5.0"
23
+ "@team-agent/cli-darwin-arm64": "0.5.1",
24
+ "@team-agent/cli-darwin-x64": "0.5.1",
25
+ "@team-agent/cli-linux-x64": "0.5.1"
26
26
  },
27
27
  "scripts": {
28
28
  "postinstall": "node npm/bincheck.mjs",