@team-agent/installer 0.5.77 → 0.5.78

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. package/Cargo.lock +1 -1
  2. package/Cargo.toml +1 -1
  3. package/crates/team-agent/src/cli/adapters.rs +161 -29
  4. package/crates/team-agent/src/cli/diagnose.rs +333 -68
  5. package/crates/team-agent/src/cli/emit.rs +24 -8
  6. package/crates/team-agent/src/cli/mod.rs +543 -98
  7. package/crates/team-agent/src/cli/models.rs +362 -32
  8. package/crates/team-agent/src/cli/named_address.rs +19 -5
  9. package/crates/team-agent/src/cli/profile.rs +35 -4
  10. package/crates/team-agent/src/cli/spec.rs +7 -2
  11. package/crates/team-agent/src/cli/status_port/compact.rs +12 -8
  12. package/crates/team-agent/src/cli/status_port/snapshot.rs +15 -12
  13. package/crates/team-agent/src/cli/status_port/store.rs +1 -1
  14. package/crates/team-agent/src/cli/tests/missing_subcommands.rs +62 -8
  15. package/crates/team-agent/src/cli/tests/named_address.rs +70 -0
  16. package/crates/team-agent/src/cli/tests/verb_profile.rs +81 -6
  17. package/crates/team-agent/src/cli/types.rs +1 -0
  18. package/crates/team-agent/src/leader/lease.rs +492 -33
  19. package/crates/team-agent/src/leader/owner_bind.rs +40 -12
  20. package/crates/team-agent/src/leader/rediscover/tests.rs +65 -0
  21. package/crates/team-agent/src/leader/rediscover.rs +36 -6
  22. package/crates/team-agent/src/leader/start.rs +151 -14
  23. package/crates/team-agent/src/leader/tests/wake_start_owner.rs +15 -9
  24. package/crates/team-agent/src/lifecycle/launch/add_agent.rs +28 -227
  25. package/crates/team-agent/src/lifecycle/launch/add_agent_state.rs +0 -18
  26. package/crates/team-agent/src/lifecycle/launch/cursor_mcp.rs +94 -227
  27. package/crates/team-agent/src/lifecycle/launch/mcp_config.rs +59 -10
  28. package/crates/team-agent/src/lifecycle/launch/pi_mcp.rs +65 -18
  29. package/crates/team-agent/src/lifecycle/launch/quick_start.rs +1449 -282
  30. package/crates/team-agent/src/lifecycle/launch/readiness.rs +349 -4
  31. package/crates/team-agent/src/lifecycle/launch/spawn.rs +11 -11
  32. package/crates/team-agent/src/lifecycle/launch/spec_state.rs +35 -3
  33. package/crates/team-agent/src/lifecycle/launch/worker_env.rs +16 -0
  34. package/crates/team-agent/src/lifecycle/launch.rs +30 -7
  35. package/crates/team-agent/src/lifecycle/profile_launch.rs +37 -3
  36. package/crates/team-agent/src/lifecycle/restart/agent.rs +112 -100
  37. package/crates/team-agent/src/lifecycle/restart/common.rs +82 -52
  38. package/crates/team-agent/src/lifecycle/restart/rebuild.rs +996 -66
  39. package/crates/team-agent/src/lifecycle/restart.rs +2 -2
  40. package/crates/team-agent/src/lifecycle/tests/acceptance_b_batch_red.rs +3 -0
  41. package/crates/team-agent/src/lifecycle/tests/behavioral_diff_264_red.rs +15 -0
  42. package/crates/team-agent/src/lifecycle/tests/claude_compatible_config_red.rs +5 -0
  43. package/crates/team-agent/src/lifecycle/tests/claude_profile_launch_red.rs +11 -13
  44. package/crates/team-agent/src/lifecycle/tests/clone_agent_preserves_source_tools.rs +119 -4
  45. package/crates/team-agent/src/lifecycle/tests/communication_mode_runtime_contract_red.rs +12 -0
  46. package/crates/team-agent/src/lifecycle/tests/copilot_provider_red.rs +22 -0
  47. package/crates/team-agent/src/lifecycle/tests/core_034_real_red.rs +12 -0
  48. package/crates/team-agent/src/lifecycle/tests/cursor_mcp_overlay.rs +265 -198
  49. package/crates/team-agent/src/lifecycle/tests/cursor_require_explicit_model_red.rs +14 -30
  50. package/crates/team-agent/src/lifecycle/tests/cursor_restart_resume_red.rs +3 -0
  51. package/crates/team-agent/src/lifecycle/tests/g1_silent_faces.rs +2 -5
  52. package/crates/team-agent/src/lifecycle/tests/grok_effort_argv_red.rs +10 -0
  53. package/crates/team-agent/src/lifecycle/tests/grok_mcp_overlay_red.rs +58 -35
  54. package/crates/team-agent/src/lifecycle/tests/grok_require_explicit_model_red.rs +10 -0
  55. package/crates/team-agent/src/lifecycle/tests/grok_restart_resume_red.rs +9 -0
  56. package/crates/team-agent/src/lifecycle/tests/harvest2_a_batch_red.rs +3 -2
  57. package/crates/team-agent/src/lifecycle/tests/lane_ops.rs +0 -168
  58. package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +145 -212
  59. package/crates/team-agent/src/lifecycle/tests/phase_b_contracts.rs +16 -2
  60. package/crates/team-agent/src/lifecycle/tests/phase_golden.rs +54 -9
  61. package/crates/team-agent/src/lifecycle/tests/pi_dual_entry_red.rs +78 -0
  62. package/crates/team-agent/src/lifecycle/tests/team_in_team_identity_scope_red.rs +3 -0
  63. package/crates/team-agent/src/lifecycle/tests/test_isolation_escape_contract.rs +51 -10
  64. package/crates/team-agent/src/lifecycle/tests/verify_rs031_window_consistency_red.rs +50 -7
  65. package/crates/team-agent/src/lifecycle/tests/worker_spawn_env_red.rs +137 -0
  66. package/crates/team-agent/src/lifecycle/types.rs +8 -0
  67. package/crates/team-agent/src/mcp_server/wire.rs +140 -11
  68. package/crates/team-agent/src/messaging/delivery.rs +67 -8
  69. package/crates/team-agent/src/messaging/leader_channel.rs +56 -0
  70. package/crates/team-agent/src/messaging/tests/runtime.rs +10 -2
  71. package/crates/team-agent/src/provider/adapters/cursor_agent.rs +2 -5
  72. package/crates/team-agent/src/state/persist.rs +97 -1
  73. package/crates/team-agent/src/state/repository.rs +29 -0
  74. package/crates/team-agent/src/test_support/models_timeout_fixture.rs +43 -21
  75. package/crates/team-agent/src/tmux_backend/tests.rs +121 -4
  76. package/crates/team-agent/src/tmux_backend.rs +208 -118
  77. package/crates/team-agent/src/transport.rs +72 -0
  78. package/crates/team-agent/src/transport_factory.rs +53 -0
  79. package/package.json +4 -5
  80. package/skills/team-agent/SKILL.md +45 -17
  81. package/skills/team-agent/command-coverage.json +4 -29
  82. package/npm/check-skill-links.mjs +0 -88
  83. package/skills/team-agent/references/team-agent-operator.md +0 -672
@@ -159,6 +159,11 @@ pub(crate) struct CommandSpec {
159
159
  pub(crate) governance: Option<GovernanceNote>,
160
160
  }
161
161
 
162
+ /// Visible default-help contract:
163
+ /// - d40 / v0.5.77 published 15 names including `models` and excluding `results`.
164
+ /// - `results` remains a public handler and must stay discoverable.
165
+ /// - Exact composed set is d40 ∪ {results}. This is a frozen name set, not a
166
+ /// numeric slack threshold.
162
167
  #[rustfmt::skip]
163
168
  pub(crate) const COMMAND_SPECS: &[CommandSpec] = &[
164
169
  CommandSpec { name: "quick-start", tier: CommandTier::Core, category: CommandCategory::Start, kind: CommandKind::Dispatch(DispatchKind::QuickStart), summary: "start or attach a team from TEAM.md", usage: "usage: team-agent quick-start [TEAMDIR] [--workspace WORKSPACE] [--name NAME] [--team-id TEAM|--team TEAM] [--yes] [--no-display] [--backend tmux|conpty] [--json]", default_help: true, command_help: true, suggestion_index: true, token_usage: TokenUsage::Conditional, alias_of: None, sunset: None, action: None, governance: None },
@@ -166,7 +171,7 @@ pub(crate) const COMMAND_SPECS: &[CommandSpec] = &[
166
171
  CommandSpec { name: "status", tier: CommandTier::Core, category: CommandCategory::Daily, kind: CommandKind::Dispatch(DispatchKind::Status), summary: "show current team status", usage: "usage: team-agent status [AGENT] [--workspace WORKSPACE] [--team TEAM] [--summary|--json] [--detail]", default_help: true, command_help: true, suggestion_index: true, token_usage: TokenUsage::No, alias_of: None, sunset: None, action: None, governance: None },
167
172
  CommandSpec { name: "models", tier: CommandTier::Core, category: CommandCategory::Observe, kind: CommandKind::Dispatch(DispatchKind::Models), summary: "list exact provider model ids", usage: "usage: team-agent models --provider pi [--search TEXT] [--json]", default_help: true, command_help: true, suggestion_index: true, token_usage: TokenUsage::No, alias_of: None, sunset: None, action: None, governance: None },
168
173
  CommandSpec { name: "collect", tier: CommandTier::Core, category: CommandCategory::Daily, kind: CommandKind::Dispatch(DispatchKind::Collect), summary: "collect reported results", usage: "usage: team-agent collect [--workspace WORKSPACE] [--team TEAM] [--result-file FILE] [--json]", default_help: true, command_help: true, suggestion_index: true, token_usage: TokenUsage::No, alias_of: None, sunset: None, action: None, governance: None },
169
- CommandSpec { name: "results", tier: CommandTier::Core, category: CommandCategory::Daily, kind: CommandKind::Dispatch(DispatchKind::Results), summary: "read reported results for a case", usage: "usage: team-agent results --case CASE_ID [--workspace WORKSPACE] [--team TEAM] [--json]", default_help: false, command_help: true, suggestion_index: true, token_usage: TokenUsage::No, alias_of: None, sunset: None, action: None, governance: None },
174
+ CommandSpec { name: "results", tier: CommandTier::Core, category: CommandCategory::Daily, kind: CommandKind::Dispatch(DispatchKind::Results), summary: "read reported results for a case", usage: "usage: team-agent results --case CASE_ID [--workspace WORKSPACE] [--team TEAM] [--json]", default_help: true, command_help: true, suggestion_index: true, token_usage: TokenUsage::No, alias_of: None, sunset: None, action: None, governance: None },
170
175
  CommandSpec { name: "wait", tier: CommandTier::Core, category: CommandCategory::Daily, kind: CommandKind::Dispatch(DispatchKind::Wait), summary: "block until a task result is stored", usage: "usage: team-agent wait --task TASK [--workspace WORKSPACE] [--json]", default_help: false, command_help: true, suggestion_index: true, token_usage: TokenUsage::No, alias_of: None, sunset: None, action: None, governance: None },
171
176
  CommandSpec { name: "restart", tier: CommandTier::Core, category: CommandCategory::TeamLifecycle, kind: CommandKind::Dispatch(DispatchKind::Restart), summary: "restart the selected team", usage: "usage: team-agent restart [WORKSPACE] [--team TEAM] [--allow-fresh] [--session-converge-deadline SECONDS] [--json]", default_help: true, command_help: true, suggestion_index: true, token_usage: TokenUsage::Conditional, alias_of: None, sunset: None, action: None, governance: None },
172
177
  CommandSpec { name: "shutdown", tier: CommandTier::Core, category: CommandCategory::TeamLifecycle, kind: CommandKind::Dispatch(DispatchKind::Shutdown), summary: "stop the selected team", usage: "usage: team-agent shutdown [--workspace WORKSPACE] [--team TEAM] [--keep-logs] [--json]", default_help: true, command_help: true, suggestion_index: true, token_usage: TokenUsage::No, alias_of: None, sunset: None, action: None, governance: None },
@@ -201,7 +206,7 @@ pub(crate) const COMMAND_SPECS: &[CommandSpec] = &[
201
206
  CommandSpec { name: "clone-agent", tier: CommandTier::Secondary, category: CommandCategory::WorkerLifecycle, kind: CommandKind::Dispatch(DispatchKind::CloneAgent), summary: "clone a worker role into a fresh session", usage: "usage: team-agent clone-agent SOURCE_AGENT --as AGENT [--label LABEL] [--workspace WORKSPACE] [--team TEAM] [--no-display] [--json]", default_help: false, command_help: true, suggestion_index: false, token_usage: TokenUsage::Conditional, alias_of: None, sunset: None, action: None, governance: None },
202
207
  CommandSpec { name: "allow-peer-talk", tier: CommandTier::Secondary, category: CommandCategory::Setup, kind: CommandKind::Dispatch(DispatchKind::AllowPeerTalk), summary: "allow direct worker peer talk", usage: "usage: team-agent allow-peer-talk A B [--workspace WORKSPACE] [--json]", default_help: false, command_help: true, suggestion_index: false, token_usage: TokenUsage::No, alias_of: None, sunset: None, action: None, governance: Some(GovernanceNote { decision: "secondary", reason: "capability/config surface" }) },
203
208
  CommandSpec { name: "approvals", tier: CommandTier::Secondary, category: CommandCategory::Observe, kind: CommandKind::Dispatch(DispatchKind::Approvals), summary: "inspect provider approval state", usage: "usage: team-agent approvals [AGENT] [--workspace WORKSPACE] [--team TEAM] [--json]", default_help: false, command_help: true, suggestion_index: false, token_usage: TokenUsage::No, alias_of: None, sunset: None, action: None, governance: Some(GovernanceNote { decision: "secondary", reason: "approval diagnostics" }) },
204
- CommandSpec { name: "profile", tier: CommandTier::Secondary, category: CommandCategory::Setup, kind: CommandKind::Dispatch(DispatchKind::Profile), summary: "manage provider profiles", usage: "usage: team-agent profile COMMAND NAME [--workspace WORKSPACE] [--team TEAM] [--auth-mode MODE] [--json]", default_help: false, command_help: true, suggestion_index: false, token_usage: TokenUsage::No, alias_of: None, sunset: None, action: None, governance: Some(GovernanceNote { decision: "secondary", reason: "provider/profile setup" }) },
209
+ CommandSpec { name: "profile", tier: CommandTier::Secondary, category: CommandCategory::Setup, kind: CommandKind::Dispatch(DispatchKind::Profile), summary: "manage provider profiles", usage: "usage: team-agent profile COMMAND NAME [--workspace WORKSPACE] [--team TEAM] [--auth-mode MODE] [--proxy-mode direct|inherit] [--json]", default_help: false, command_help: true, suggestion_index: false, token_usage: TokenUsage::No, alias_of: None, sunset: None, action: None, governance: Some(GovernanceNote { decision: "secondary", reason: "provider/profile setup" }) },
205
210
  CommandSpec { name: "install-skill", tier: CommandTier::Secondary, category: CommandCategory::Setup, kind: CommandKind::Dispatch(DispatchKind::InstallSkill), summary: "install or uninstall Team Agent skills", usage: "usage: team-agent install-skill (--source DIR | --uninstall) [--target codex|claude|copilot|all] [--dest DIR] [--dry-run] [--json]", default_help: false, command_help: true, suggestion_index: false, token_usage: TokenUsage::No, alias_of: None, sunset: None, action: None, governance: Some(GovernanceNote { decision: "secondary", reason: "capability setup" }) },
206
211
  CommandSpec { name: "inbox", tier: CommandTier::Secondary, category: CommandCategory::Observe, kind: CommandKind::Dispatch(DispatchKind::Inbox), summary: "inspect queued messages", usage: "usage: team-agent inbox AGENT [--workspace WORKSPACE] [--team TEAM] [--limit N] [--since CURSOR] [--json]", default_help: false, command_help: true, suggestion_index: false, token_usage: TokenUsage::No, alias_of: None, sunset: None, action: None, governance: None },
207
212
 
@@ -60,19 +60,23 @@ pub(super) fn compact_not_ready(full: &Value) -> Value {
60
60
  })
61
61
  .unwrap_or_default();
62
62
  let mut obj = Map::new();
63
- let channel_unbound = reasons
64
- .iter()
65
- .any(|reason| reason == "leader_receiver_unbound");
66
63
  obj.insert(
67
64
  "reasons".to_string(),
68
65
  Value::Array(reasons.into_iter().map(Value::String).collect()),
69
66
  );
70
67
  obj.insert("agents".to_string(), Value::Array(agents));
71
- if channel_unbound {
72
- obj.insert(
73
- "next_action".to_string(),
74
- Value::String("claim-leader".to_string()),
75
- );
68
+ if let Some(next_action) = full
69
+ .pointer("/readiness/next_action")
70
+ .and_then(Value::as_str)
71
+ {
72
+ let truly_unbound = full.pointer("/readiness/state").and_then(Value::as_str)
73
+ == Some("leader_receiver_unbound");
74
+ if !next_action.contains("claim-leader") || truly_unbound {
75
+ obj.insert(
76
+ "next_action".to_string(),
77
+ Value::String(next_action.to_string()),
78
+ );
79
+ }
76
80
  }
77
81
  Value::Object(obj)
78
82
  }
@@ -95,18 +95,21 @@ impl RuntimeSnapshot {
95
95
  .filter(|team| !team.is_empty())
96
96
  .map(str::to_string)
97
97
  .unwrap_or_else(|| crate::state::projection::team_state_key(state));
98
- if !team_key.is_empty()
99
- && !crate::lifecycle::launch::launched_team_receiver_is_attached(workspace, &team_key)
100
- {
101
- if let Some(object) = readiness.as_object_mut() {
102
- object.insert("all_attached_receiver".to_string(), json!(false));
103
- object.insert("ready".to_string(), json!(false));
104
- object.insert("state".to_string(), json!("leader_receiver_unbound"));
105
- object.insert("next_action".to_string(), json!("claim-leader"));
106
- object.insert(
107
- "reason".to_string(),
108
- json!("leader registry does not authorize this workspace"),
109
- );
98
+ if !team_key.is_empty() {
99
+ let class = crate::lifecycle::launch::classify_leader_binding(workspace, &team_key);
100
+ if class != crate::lifecycle::launch::LeaderBindingClass::Attached {
101
+ if let Some(object) = readiness.as_object_mut() {
102
+ object.insert("all_attached_receiver".to_string(), json!(false));
103
+ object.insert("ready".to_string(), json!(false));
104
+ object.insert("state".to_string(), json!(class.issue_id()));
105
+ object.insert("reason".to_string(), json!(class.issue_id()));
106
+ if class == crate::lifecycle::launch::LeaderBindingClass::Unbound {
107
+ object.insert(
108
+ "next_action".to_string(),
109
+ json!(class.repair()),
110
+ );
111
+ }
112
+ }
110
113
  }
111
114
  }
112
115
  let grok_slot = crate::cli::grok_slot_report(workspace, state).to_json();
@@ -230,7 +230,7 @@ pub(super) fn pending_leader_notifications(
230
230
  "created_at": row.get::<_, Option<String>>(4)?,
231
231
  "delivery_attempts": row.get::<_, i64>(5)?,
232
232
  "channel": channel,
233
- "action": "run team-agent attach-leader or team-agent takeover",
233
+ "action": "historical leader mailbox debt; not proof of current unbound owner",
234
234
  }))
235
235
  };
236
236
  let rows = match owner_team_id {
@@ -90,6 +90,44 @@ fn seed_self_signed_attached_leader(ws: &std::path::Path) {
90
90
  .unwrap();
91
91
  }
92
92
 
93
+ fn seed_canonical_healthy_leader(ws: &std::path::Path, endpoint: &str) {
94
+ crate::state::persist::save_runtime_state(
95
+ ws,
96
+ &json!({
97
+ "team_key": "current",
98
+ "workspace": ws,
99
+ "teams": {
100
+ "current": {
101
+ "team_owner": {
102
+ "pane_id": "%1",
103
+ "owner_epoch": 1,
104
+ "provider": "codex"
105
+ },
106
+ "leader_receiver": {
107
+ "mode": "direct_tmux",
108
+ "status": "attached",
109
+ "pane_id": "%1",
110
+ "owner_epoch": 1,
111
+ "provider": "codex",
112
+ "tmux_socket": endpoint
113
+ }
114
+ }
115
+ }
116
+ }),
117
+ )
118
+ .unwrap();
119
+ assert_eq!(
120
+ crate::leader::registry::register_binding_from_state_best_effort(
121
+ ws,
122
+ Some("current"),
123
+ "diagnose-healthy-fixture",
124
+ )
125
+ .as_ref()
126
+ .map(|outcome| outcome.status),
127
+ Some("registered")
128
+ );
129
+ }
130
+
93
131
  fn seed_leader_registry_entry(ws: &std::path::Path) {
94
132
  let entry = crate::leader::registry::build_entry(
95
133
  ws,
@@ -345,17 +383,33 @@ fn dispatch_routes_diagnose_healthy_leader() {
345
383
  ]);
346
384
  let _home = IsolatedHome::enter("diagnose-healthy");
347
385
  let ws = tmp_workspace();
348
- seed_self_signed_attached_leader(&ws);
349
- seed_leader_registry_entry(&ws);
350
- let code = run(
351
- &cli_argv(&["diagnose", "--workspace", &ws.to_string_lossy(), "--json"]),
352
- &ws,
353
- );
386
+ let endpoint = "/tmp/ta-diagnose-healthy.sock";
387
+ seed_canonical_healthy_leader(&ws, endpoint);
388
+ let transport = crate::transport::test_support::OfflineTransport::default()
389
+ .with_tmux_endpoint(endpoint)
390
+ .with_targets(vec![crate::transport::PaneInfo {
391
+ pane_id: crate::transport::PaneId::new("%1"),
392
+ session: crate::transport::SessionName::new("s"),
393
+ window_index: None,
394
+ window_name: None,
395
+ pane_index: None,
396
+ tty: None,
397
+ current_command: Some("codex".to_string()),
398
+ current_path: Some(ws.clone()),
399
+ active: true,
400
+ pane_pid: None,
401
+ leader_env: Default::default(),
402
+ }]);
403
+ let code = crate::transport_factory::with_leader_endpoint_transport(endpoint, transport, || {
404
+ run(
405
+ &cli_argv(&["diagnose", "--workspace", &ws.to_string_lossy(), "--json"]),
406
+ &ws,
407
+ )
408
+ });
354
409
  assert_eq!(
355
410
  code,
356
411
  ExitCode::Ok,
357
- "`diagnose` must be Ok when state.json looks attached AND ~/.team-agent/leaders/ \
358
- has a matching attached row; got {code:?}"
412
+ "`diagnose` must be Ok when canonical owner/receiver, production registry, and live align; got {code:?}"
359
413
  );
360
414
  let _ = std::fs::remove_dir_all(&ws);
361
415
  }
@@ -234,6 +234,76 @@ fn resolve_name_team_disambiguated() {
234
234
  let _ = std::fs::remove_dir_all(&ws);
235
235
  }
236
236
 
237
+ #[test]
238
+ fn resolve_qualified_name_prefers_unique_state_pane_over_same_window_idle_pane() {
239
+ let ws = named_ws("qualified-state-pane-disambiguation");
240
+ seed_state(
241
+ &ws,
242
+ state_with_teams(json!({
243
+ "team-a": worker_team("team-a", "qa", "%worker", "qa")
244
+ })),
245
+ );
246
+ let transport = OfflineTransport::new().with_targets(vec![
247
+ pane("team-a", "qa", "%worker"),
248
+ pane("team-a", "qa", "%idle"),
249
+ ]);
250
+
251
+ let resolved = resolve_name_with_transport(
252
+ &ws,
253
+ &format!("{}::team-a/qa", ws.display()),
254
+ &transport,
255
+ )
256
+ .expect("a qualified worker address should use its unique state pane");
257
+ assert_eq!(resolved.pane_id, "%worker");
258
+ assert_eq!(resolved.state_pane_id.as_deref(), Some("%worker"));
259
+ assert!(!resolved.state_pane_stale);
260
+ let _ = std::fs::remove_dir_all(&ws);
261
+ }
262
+
263
+ #[test]
264
+ fn resolve_qualified_name_state_pane_must_uniquely_match_scoped_candidates() {
265
+ for scenario in ["missing", "stale", "cross-session", "duplicate"] {
266
+ let ws = named_ws(&format!("qualified-state-pane-{scenario}"));
267
+ let mut team = worker_team("team-a", "qa", "%worker", "qa");
268
+ let targets = match scenario {
269
+ "missing" => {
270
+ team["agents"]["qa"]
271
+ .as_object_mut()
272
+ .unwrap()
273
+ .remove("pane_id");
274
+ vec![pane("team-a", "qa", "%worker"), pane("team-a", "qa", "%idle")]
275
+ }
276
+ "stale" => {
277
+ team["agents"]["qa"]["pane_id"] = json!("%stale");
278
+ vec![pane("team-a", "qa", "%worker"), pane("team-a", "qa", "%idle")]
279
+ }
280
+ "cross-session" => vec![
281
+ pane("old-team-a", "qa", "%worker"),
282
+ pane("team-a", "qa", "%current-one"),
283
+ pane("team-a", "qa", "%current-two"),
284
+ ],
285
+ "duplicate" => {
286
+ team["agents"]["qa"]["pane_id"] = json!("%duplicate");
287
+ vec![
288
+ pane("team-a", "qa", "%duplicate"),
289
+ pane("team-a", "qa", "%duplicate"),
290
+ ]
291
+ }
292
+ _ => unreachable!(),
293
+ };
294
+ seed_state(&ws, state_with_teams(json!({"team-a": team})));
295
+ let transport = OfflineTransport::new().with_targets(targets);
296
+ let err = resolve_name_with_transport(
297
+ &ws,
298
+ &format!("{}::team-a/qa", ws.display()),
299
+ &transport,
300
+ )
301
+ .expect_err("non-unique state pane evidence must remain fail-closed");
302
+ assert_eq!(err.kind, NamedAddressErrorKind::NameAmbiguous, "{scenario}");
303
+ let _ = std::fs::remove_dir_all(&ws);
304
+ }
305
+ }
306
+
237
307
  #[test]
238
308
  fn resolve_qualified_name_canonicalizes_legacy_session_alias() {
239
309
  let ws = named_ws("legacy-session-alias");
@@ -35,7 +35,7 @@ fn seed_official_api_profile(ws: &std::path::Path) {
35
35
 
36
36
  // Golden source:
37
37
  // - cli/parser.py:131-150 registers `profile {init,doctor,show}`:
38
- // * init NAME --workspace . --team TEAM --auth-mode choices(sorted AUTH_MODES) --json
38
+ // * init NAME --workspace . --team TEAM --auth-mode choices(sorted AUTH_MODES) --proxy-mode direct|inherit --json
39
39
  // * doctor NAME --workspace . --team TEAM --json
40
40
  // * show NAME --workspace . --team TEAM --json
41
41
  // - cli/commands.py:47-67 resolves scope then delegates to profiles.init_profile/
@@ -52,7 +52,7 @@ fn seed_official_api_profile(ws: &std::path::Path) {
52
52
  // PYTHONPATH=/Users/alauda/Documents/code/team-agent-public/src \
53
53
  // python3 /tmp/probe_profile_cli.py
54
54
  // profile init codex_sub --auth-mode subscription --json rc=0 and creates:
55
- // codex_sub.env = "AUTH_MODE=subscription\nPROFILE_NAME=codex_sub\n" (mode 0600),
55
+ // codex_sub.env = "AUTH_MODE=subscription\nPROFILE_NAME=codex_sub\nPROXY_MODE=inherit\n" (mode 0600),
56
56
  // codex_sub.example.env with same body, plus AGENTS.md/CLAUDE.md secret boundary files.
57
57
  #[test]
58
58
  fn profile_init_routes_and_creates_secret_boundary_files() {
@@ -87,12 +87,12 @@ fn profile_init_routes_and_creates_secret_boundary_files() {
87
87
  );
88
88
  assert_eq!(
89
89
  std::fs::read_to_string(dir.join("codex_sub.env")).unwrap(),
90
- "AUTH_MODE=subscription\nPROFILE_NAME=codex_sub\n",
90
+ "AUTH_MODE=subscription\nPROFILE_NAME=codex_sub\nPROXY_MODE=inherit\n",
91
91
  "subscription profile template body must match golden"
92
92
  );
93
93
  assert_eq!(
94
94
  std::fs::read_to_string(dir.join("codex_sub.example.env")).unwrap(),
95
- "AUTH_MODE=subscription\nPROFILE_NAME=codex_sub\n",
95
+ "AUTH_MODE=subscription\nPROFILE_NAME=codex_sub\nPROXY_MODE=inherit\n",
96
96
  "subscription example template body must match golden"
97
97
  );
98
98
 
@@ -116,7 +116,7 @@ fn profile_init_routes_and_creates_secret_boundary_files() {
116
116
  );
117
117
  assert_eq!(
118
118
  std::fs::read_to_string(dir.join("codex_sub.env")).unwrap(),
119
- "AUTH_MODE=subscription\nPROFILE_NAME=codex_sub\n",
119
+ "AUTH_MODE=subscription\nPROFILE_NAME=codex_sub\nPROXY_MODE=inherit\n",
120
120
  "idempotent init must not rewrite existing profile bytes"
121
121
  );
122
122
  let _ = std::fs::remove_dir_all(&ws);
@@ -145,7 +145,7 @@ fn compatible_api_profile_init_template_contains_common_five_fields() {
145
145
  );
146
146
 
147
147
  let expected =
148
- "AUTH_MODE=compatible_api\nPROFILE_NAME=compat\nBASE_URL=\nAPI_KEY=\nMODEL=\n";
148
+ "AUTH_MODE=compatible_api\nPROFILE_NAME=compat\nPROXY_MODE=inherit\nBASE_URL=\nAPI_KEY=\nMODEL=\n";
149
149
  let dir = profiles_dir(&ws);
150
150
  assert_eq!(
151
151
  std::fs::read_to_string(dir.join("compat.env")).unwrap(),
@@ -160,6 +160,81 @@ fn compatible_api_profile_init_template_contains_common_five_fields() {
160
160
  let _ = std::fs::remove_dir_all(&ws);
161
161
  }
162
162
 
163
+ #[test]
164
+ fn profile_init_accepts_local_proxy_mode_and_rejects_unknown_values() {
165
+ let ws = tmp_workspace();
166
+ let direct = run(
167
+ &profile_argv(&[
168
+ "profile",
169
+ "init",
170
+ "cursor_sub",
171
+ "--workspace",
172
+ ".",
173
+ "--auth-mode",
174
+ "subscription",
175
+ "--proxy-mode=direct",
176
+ "--json",
177
+ ]),
178
+ &ws,
179
+ );
180
+ assert_eq!(direct, ExitCode::Ok, "direct proxy mode must be accepted");
181
+ let body = std::fs::read_to_string(profiles_dir(&ws).join("cursor_sub.env")).unwrap();
182
+ assert!(body.contains("PROXY_MODE=direct\n"));
183
+
184
+ let invalid = run(
185
+ &profile_argv(&[
186
+ "profile",
187
+ "init",
188
+ "bad_proxy",
189
+ "--workspace",
190
+ ".",
191
+ "--proxy-mode",
192
+ "sideways",
193
+ "--json",
194
+ ]),
195
+ &ws,
196
+ );
197
+ assert_eq!(invalid, ExitCode::Error, "unknown proxy mode must fail");
198
+ let _ = std::fs::remove_dir_all(&ws);
199
+ }
200
+
201
+ #[test]
202
+ fn profile_init_existing_profile_reports_actual_mode_and_show_matches() {
203
+ let ws = tmp_workspace();
204
+ let make_args = |command: &str, mode: Option<&str>| ProfileArgs {
205
+ command: command.to_string(),
206
+ name: "cursor_sub".to_string(),
207
+ workspace: ws.clone(),
208
+ team: None,
209
+ auth_mode: Some("subscription".to_string()),
210
+ proxy_mode: mode.map(str::to_string),
211
+ json: true,
212
+ };
213
+ let first = cmd_profile(&make_args("init", Some("direct"))).unwrap();
214
+ let first = match first.output {
215
+ CmdOutput::Json(value) => value,
216
+ other => panic!("expected json output, got {other:?}"),
217
+ };
218
+ assert_eq!(first["proxy_mode"], "direct");
219
+ assert_eq!(first["proxy_mode_changed"], true);
220
+
221
+ let repeat = cmd_profile(&make_args("init", Some("inherit"))).unwrap();
222
+ let repeat = match repeat.output {
223
+ CmdOutput::Json(value) => value,
224
+ other => panic!("expected json output, got {other:?}"),
225
+ };
226
+ assert_eq!(repeat["proxy_mode"], "direct");
227
+ assert_eq!(repeat["proxy_mode_changed"], false);
228
+
229
+ let shown = cmd_profile(&make_args("show", None)).unwrap();
230
+ let shown = match shown.output {
231
+ CmdOutput::Json(value) => value,
232
+ other => panic!("expected json output, got {other:?}"),
233
+ };
234
+ assert_eq!(shown["proxy_mode"], "direct");
235
+ let _ = std::fs::remove_dir_all(&ws);
236
+ }
237
+
163
238
  // Golden source:
164
239
  // - profiles/core.py:95-119 doctor_profile returns ok=true for existing profiles and
165
240
  // ok=false for missing profiles; parser.py:506-508 maps result.ok false to exit 1.
@@ -667,6 +667,7 @@ pub struct ProfileArgs {
667
667
  pub workspace: PathBuf,
668
668
  pub team: Option<String>,
669
669
  pub auth_mode: Option<String>,
670
+ pub proxy_mode: Option<String>,
670
671
  pub json: bool,
671
672
  }
672
673