@team-agent/installer 0.5.112 → 0.5.114

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 (77) hide show
  1. package/Cargo.lock +252 -84
  2. package/Cargo.toml +3 -1
  3. package/crates/team-agent/Cargo.toml +3 -0
  4. package/crates/team-agent/src/cli/adapters.rs +119 -50
  5. package/crates/team-agent/src/cli/diagnose.rs +36 -13
  6. package/crates/team-agent/src/cli/emit.rs +173 -109
  7. package/crates/team-agent/src/cli/leader.rs +16 -0
  8. package/crates/team-agent/src/cli/leader_prompt.rs +261 -0
  9. package/crates/team-agent/src/cli/leaders.rs +32 -32
  10. package/crates/team-agent/src/cli/mod.rs +158 -14
  11. package/crates/team-agent/src/cli/models.rs +68 -18
  12. package/crates/team-agent/src/cli/named_address.rs +17 -16
  13. package/crates/team-agent/src/cli/route.rs +4 -4
  14. package/crates/team-agent/src/cli/send/presentation.rs +19 -18
  15. package/crates/team-agent/src/cli/send/resolve.rs +11 -11
  16. package/crates/team-agent/src/cli/send.rs +4 -4
  17. package/crates/team-agent/src/cli/spec.rs +35 -29
  18. package/crates/team-agent/src/cli/status.rs +7 -7
  19. package/crates/team-agent/src/cli/status_port/brief.rs +268 -101
  20. package/crates/team-agent/src/cli/tests/base.rs +94 -6
  21. package/crates/team-agent/src/cli/tests/divergence.rs +3 -2
  22. package/crates/team-agent/src/cli/tests/lane_c.rs +1 -1
  23. package/crates/team-agent/src/cli/tests/named_address.rs +3 -3
  24. package/crates/team-agent/src/cli/tests/status_send.rs +9 -7
  25. package/crates/team-agent/src/cli/triage.rs +53 -2
  26. package/crates/team-agent/src/cli/types.rs +8 -8
  27. package/crates/team-agent/src/compiler/tests.rs +12 -0
  28. package/crates/team-agent/src/compiler.rs +65 -26
  29. package/crates/team-agent/src/contract_runtime/backend.rs +646 -0
  30. package/crates/team-agent/src/contract_runtime/config.rs +309 -0
  31. package/crates/team-agent/src/contract_runtime/discovery.rs +237 -0
  32. package/crates/team-agent/src/contract_runtime/forward.rs +229 -0
  33. package/crates/team-agent/src/contract_runtime/framework.rs +261 -0
  34. package/crates/team-agent/src/contract_runtime/mcp.rs +396 -0
  35. package/crates/team-agent/src/contract_runtime/mod.rs +14 -0
  36. package/crates/team-agent/src/contract_runtime/prompt.rs +216 -0
  37. package/crates/team-agent/src/contract_runtime/pump.rs +207 -0
  38. package/crates/team-agent/src/contract_runtime/registry.rs +30 -0
  39. package/crates/team-agent/src/coordinator/tick.rs +26 -8
  40. package/crates/team-agent/src/leader/mod.rs +1 -0
  41. package/crates/team-agent/src/leader/prompt.rs +906 -0
  42. package/crates/team-agent/src/leader/start.rs +265 -16
  43. package/crates/team-agent/src/leader/tests/identity.rs +21 -0
  44. package/crates/team-agent/src/leader/tests/mod.rs +2 -0
  45. package/crates/team-agent/src/leader/tests/prompt_contract.rs +268 -0
  46. package/crates/team-agent/src/leader/types.rs +6 -0
  47. package/crates/team-agent/src/lib.rs +4 -0
  48. package/crates/team-agent/src/lifecycle/launch/identity.rs +172 -13
  49. package/crates/team-agent/src/lifecycle/launch/quick_start.rs +760 -365
  50. package/crates/team-agent/src/lifecycle/launch/spawn.rs +38 -9
  51. package/crates/team-agent/src/lifecycle/launch/state_projection.rs +17 -0
  52. package/crates/team-agent/src/lifecycle/launch.rs +39 -28
  53. package/crates/team-agent/src/lifecycle/profile_launch.rs +58 -46
  54. package/crates/team-agent/src/lifecycle/restart/rebuild.rs +97 -52
  55. package/crates/team-agent/src/lifecycle/restart.rs +1 -1
  56. package/crates/team-agent/src/lifecycle/tests/stale_team_saveconflict_contract.rs +3 -2
  57. package/crates/team-agent/src/lifecycle/types.rs +4 -1
  58. package/crates/team-agent/src/lifecycle/worker_command_context.rs +37 -27
  59. package/crates/team-agent/src/main.rs +6 -0
  60. package/crates/team-agent/src/messaging/delivery.rs +50 -1
  61. package/crates/team-agent/src/messaging/send.rs +10 -0
  62. package/crates/team-agent/src/messaging/tests/leader_inject_acceptance.rs +131 -0
  63. package/crates/team-agent/src/model/spec.rs +24 -12
  64. package/crates/team-agent/src/provider/adapter.rs +8 -59
  65. package/crates/team-agent/src/provider/adapters/claude.rs +6 -14
  66. package/crates/team-agent/src/provider/adapters/codex.rs +8 -18
  67. package/crates/team-agent/src/provider/adapters/copilot.rs +5 -22
  68. package/crates/team-agent/src/provider/adapters/cursor_agent.rs +8 -45
  69. package/crates/team-agent/src/provider/adapters/grok.rs +7 -31
  70. package/crates/team-agent/src/provider/adapters/pi.rs +7 -18
  71. package/crates/team-agent/src/provider/command_helpers.rs +45 -0
  72. package/crates/team-agent/src/provider/mod.rs +1 -0
  73. package/crates/team-agent/src/provider/model_catalog.rs +21 -28
  74. package/crates/team-agent/src/provider/tests/adapter.rs +70 -0
  75. package/crates/team-agent/src/provider/tests/command_helpers.rs +55 -0
  76. package/crates/team-agent/src/state/persist.rs +73 -0
  77. package/package.json +4 -4
@@ -100,7 +100,7 @@ pub(crate) use rebuild::restart_with_transport_with_session_convergence_deadline
100
100
  // timer + worker timing writer so `lifecycle::launch::launch_with_transport_in_workspace`
101
101
  // can emit the same instrumentation event family with `source="launch"`.
102
102
  pub(crate) use rebuild::{
103
- provider_wire_from_state, write_worker_spawn_timing_event, RestartPhaseTimer,
103
+ provider_wire_from_state, write_worker_spawn_timing_event, LifecyclePhase, RestartPhaseTimer,
104
104
  };
105
105
  pub use rebuild::{restart, restart_with_session_convergence_deadline};
106
106
  pub(crate) use rebuild::{
@@ -165,14 +165,15 @@ fn purge_agent_help_and_dispatch_are_consistent() {
165
165
  output_text(&case.run(["purge-agent", "ghost", "--workspace", case.ws(), "--json"]));
166
166
  if help.contains("purge-agent") {
167
167
  assert!(
168
- !command.contains("invalid choice") && !command.contains("unknown subcommand") && !command.contains("Commands:"),
168
+ !command.contains("invalid choice") && !command.contains("unknown subcommand")
169
+ && !command.contains("Unknown command:") && !command.contains("Commands:"),
169
170
  "RED6: help exposes purge-agent, so dispatch must be real or the command must be removed from help; help={help} command={command}"
170
171
  );
171
172
  } else {
172
173
  assert!(
173
174
  command.contains("unknown subcommand")
174
175
  || command.contains("invalid choice")
175
- || command.contains("没有这个操作"),
176
+ || command.contains("Unknown command:"),
176
177
  "RED6: if purge-agent is not implemented, help and dispatch must agree it is absent; help={help} command={command}"
177
178
  );
178
179
  }
@@ -433,8 +433,11 @@ pub enum LifecycleError {
433
433
  #[derive(Debug, Clone, PartialEq, Eq)]
434
434
  pub struct LaunchReport {
435
435
  pub session_name: SessionName,
436
- /// 实际起的 worker(冷启;dry-run 时空)。
436
+ /// Legacy transport starts; their targets belong to the team transport.
437
437
  pub started: Vec<StartedAgent>,
438
+ /// Committed contract starts, without fabricating legacy pane/window targets.
439
+ /// Empty on dry-run; populated only after the contract startup returns success.
440
+ pub contract_started: Vec<AgentId>,
438
441
  /// 是否 dry-run(只解析路由/权限,不起进程)。
439
442
  pub dry_run: bool,
440
443
  /// Selected tmux endpoint for the team transport when known.
@@ -63,16 +63,16 @@ pub(crate) struct WorkerCommandAgent {
63
63
  }
64
64
 
65
65
  impl WorkerCommandAgent {
66
- /// ---
67
- /// purpose: 从 spec 的 YAML agent 节点读出命令上下文
68
- /// params:
69
- /// agent: 单个 agent 的 YAML 节点
70
- /// fallback_id: agent 节点没写 id 时的兜底 id
71
- /// provider: 已解析的 provider
72
- /// returns: 填好的 WorkerCommandAgent
73
- /// errors: communication_mode 取值非法时返回 LifecycleError
74
- /// contract_id: lifecycle.worker_command_agent.from_source
75
- /// ---
66
+ /// ---
67
+ /// purpose: 从 spec 的 YAML agent 节点读出命令上下文
68
+ /// params:
69
+ /// agent: 单个 agent 的 YAML 节点
70
+ /// fallback_id: agent 节点没写 id 时的兜底 id
71
+ /// provider: 已解析的 provider
72
+ /// returns: 填好的 WorkerCommandAgent
73
+ /// errors: communication_mode 取值非法时返回 LifecycleError
74
+ /// contract_id: lifecycle.worker_command_agent.from_source
75
+ /// ---
76
76
  pub(crate) fn from_yaml(
77
77
  agent: &crate::model::yaml::Value,
78
78
  fallback_id: Option<&str>,
@@ -117,16 +117,16 @@ impl WorkerCommandAgent {
117
117
  })
118
118
  }
119
119
 
120
- /// ---
121
- /// purpose: 从 runtime state 的 JSON agent 节点读出命令上下文
122
- /// params:
123
- /// agent: 单个 agent 的 JSON 节点
124
- /// fallback_id: agent 节点没写 id 时的兜底 id
125
- /// provider: 已解析的 provider
126
- /// returns: 填好的 WorkerCommandAgent
127
- /// errors: communication_mode 取值非法时返回 LifecycleError
128
- /// contract_id: lifecycle.worker_command_agent.from_source
129
- /// ---
120
+ /// ---
121
+ /// purpose: 从 runtime state 的 JSON agent 节点读出命令上下文
122
+ /// params:
123
+ /// agent: 单个 agent 的 JSON 节点
124
+ /// fallback_id: agent 节点没写 id 时的兜底 id
125
+ /// provider: 已解析的 provider
126
+ /// returns: 填好的 WorkerCommandAgent
127
+ /// errors: communication_mode 取值非法时返回 LifecycleError
128
+ /// contract_id: lifecycle.worker_command_agent.from_source
129
+ /// ---
130
130
  pub(crate) fn from_json(
131
131
  agent: &serde_json::Value,
132
132
  fallback_id: Option<&str>,
@@ -190,7 +190,8 @@ pub(crate) fn compile_worker_system_prompt(
190
190
  mcp_tool_name(agent.provider, "team_orchestrator", "get_team_status")
191
191
  );
192
192
  let report_result = mcp_tool_name(agent.provider, "team_orchestrator", "report_result");
193
- let runtime_contract = runtime_contract_section(&send_message, &report_result, &get_team_status);
193
+ let runtime_contract =
194
+ runtime_contract_section(&send_message, &report_result, &get_team_status);
194
195
  let communication_contract = agent.communication_mode.runtime_contract(&send_message);
195
196
  let mut chunks = vec![
196
197
  identity_section(agent),
@@ -235,7 +236,7 @@ fn mcp_tool_name(provider: Provider, server: &str, tool: &str) -> String {
235
236
  }
236
237
  }
237
238
 
238
- fn runtime_contract_section(
239
+ pub(crate) fn runtime_contract_section(
239
240
  send_message: &str,
240
241
  report_result: &str,
241
242
  get_team_status: &str,
@@ -255,10 +256,15 @@ fn communication_mode(value: Option<&str>) -> Result<CommunicationMode, Lifecycl
255
256
  }
256
257
 
257
258
  fn identity_section(agent: &WorkerCommandAgent) -> String {
258
- format!(
259
- "You are Team Agent worker `{}` with role `{}`. When asked about your role or identity, answer with this Team Agent worker identity first, not only the generic provider product identity.",
259
+ worker_identity_section(
260
260
  agent.id.as_deref().unwrap_or("unknown"),
261
- agent.role.as_deref().unwrap_or("developer")
261
+ agent.role.as_deref().unwrap_or("developer"),
262
+ )
263
+ }
264
+
265
+ pub(crate) fn worker_identity_section(id: &str, role: &str) -> String {
266
+ format!(
267
+ "You are Team Agent worker `{id}` with role `{role}`. When asked about your role or identity, answer with this Team Agent worker identity first, not only the generic provider product identity."
262
268
  )
263
269
  }
264
270
 
@@ -344,7 +350,9 @@ mod tests {
344
350
  "Do not reply to pure ACKs, greetings, or unchanged status notices (such as \"paused\" or \"waiting\"); after reporting a blocker once or completing a task, remain silent until a new actionable instruction arrives."
345
351
  ));
346
352
  assert!(!prompt.contains("Silence and resumption (mandatory)"));
347
- assert!(!prompt.contains("When you receive a message from the leader or a teammate, you MUST respond"));
353
+ assert!(!prompt.contains(
354
+ "When you receive a message from the leader or a teammate, you MUST respond"
355
+ ));
348
356
  match mode {
349
357
  CommunicationMode::LeaderCentric => {
350
358
  assert!(prompt.contains(
@@ -352,7 +360,9 @@ mod tests {
352
360
  ));
353
361
  }
354
362
  CommunicationMode::Orchestrated => {
355
- assert!(prompt.contains("Respond to task-related messages through Team Agent MCP tools."));
363
+ assert!(prompt.contains(
364
+ "Respond to task-related messages through Team Agent MCP tools."
365
+ ));
356
366
  assert!(prompt.contains("A pure ACK, unrelated status, or non-task message does not require a response."));
357
367
  }
358
368
  }
@@ -5,6 +5,12 @@ fn main() -> anyhow::Result<()> {
5
5
  .skip(1)
6
6
  .map(|arg| arg.to_string_lossy().into_owned());
7
7
  let command = args.next();
8
+ #[cfg(unix)]
9
+ if matches!(command.as_deref(), Some("contract-mcp")) {
10
+ team_agent::contract_runtime::mcp::run(&args.collect::<Vec<_>>())
11
+ .map_err(|error| anyhow::anyhow!(error.to_string()))?;
12
+ return Ok(());
13
+ }
8
14
  if matches!(command.as_deref(), Some("fake-worker")) {
9
15
  let mut workspace = None;
10
16
  let mut agent_id = None;
@@ -1926,7 +1926,11 @@ fn delivery_transport_for_recipient<'a>(
1926
1926
  }
1927
1927
  return DeliveryTransport::Borrowed(product_transport);
1928
1928
  };
1929
- if socket == crate::tmux_backend::socket_name_for_workspace(workspace) {
1929
+ // An exact endpoint match can reuse the supplied channel; do not select
1930
+ // another server merely because it has a pane with the same numeric id.
1931
+ if product_transport.tmux_endpoint().as_deref() == Some(socket)
1932
+ || socket == crate::tmux_backend::socket_name_for_workspace(workspace)
1933
+ {
1930
1934
  DeliveryTransport::Borrowed(product_transport)
1931
1935
  } else {
1932
1936
  // 0.5.4 gate054 cross-team leak fix: when `leader_receiver.tmux_socket`
@@ -2523,6 +2527,51 @@ pub(crate) fn recipient_is_busy(state: &serde_json::Value, recipient: &str) -> b
2523
2527
  }
2524
2528
 
2525
2529
  fn leader_rollout_path(state: &serde_json::Value) -> Option<std::path::PathBuf> {
2530
+ if let Some(receiver) = leader_receiver_value(state).filter(|receiver| {
2531
+ receiver.get("provider").and_then(serde_json::Value::as_str) == Some("pi")
2532
+ && receiver.get("native_session").is_some()
2533
+ }) {
2534
+ let native = receiver.get("native_session")?;
2535
+ for key in ["pane_id", "tmux_socket", "leader_session_uuid", "owner_epoch"] {
2536
+ let bound = native.get(key)?;
2537
+ if bound.is_null() || receiver.get(key) != Some(bound) {
2538
+ return None;
2539
+ }
2540
+ }
2541
+ // Managed Pi's native ID is distinct from leader_session_uuid.
2542
+ // The JSONL appears asynchronously. Resolve only inside the recorded
2543
+ // root, by exact id/cwd/spawn time; ambiguity or an unknown
2544
+ // NativeDefault root stays unavailable (no HOME scan).
2545
+ let context = crate::provider::CaptureSessionContext {
2546
+ agent_id: "leader".into(),
2547
+ spawn_cwd: native.get("spawn_cwd")?.as_str()?.into(),
2548
+ pane_id: None,
2549
+ pane_pid: None,
2550
+ spawned_at: Some(native.get("spawned_at")?.as_str()?.into()),
2551
+ expected_session_id: Some(crate::provider::SessionId::new(
2552
+ native.get("expected_session_id")?.as_str()?,
2553
+ )),
2554
+ provider_projects_root: Some(native.get("provider_projects_root")?.as_str()?.into()),
2555
+ };
2556
+ if !context.spawn_cwd.is_absolute()
2557
+ || !context.provider_projects_root.as_ref()?.is_absolute()
2558
+ {
2559
+ return None;
2560
+ }
2561
+ let candidates = crate::provider::session_scan::scan_session_candidates_once(
2562
+ Provider::Pi,
2563
+ &context,
2564
+ )
2565
+ .ok()?;
2566
+ return match candidates.as_slice() {
2567
+ [candidate] => candidate
2568
+ .captured
2569
+ .rollout_path
2570
+ .as_ref()
2571
+ .map(|path| path.as_path().to_path_buf()),
2572
+ _ => None,
2573
+ };
2574
+ }
2526
2575
  state
2527
2576
  .get("leader")
2528
2577
  .and_then(|leader| leader.get("rollout_path"))
@@ -265,6 +265,16 @@ pub fn send_message(
265
265
  }
266
266
  }
267
267
  }
268
+ #[cfg(unix)]
269
+ if state
270
+ .get("agents")
271
+ .and_then(|agents| agents.get(recipient))
272
+ .is_some_and(crate::contract_runtime::framework::is_contract)
273
+ {
274
+ return crate::contract_runtime::framework::enqueue(
275
+ workspace, &state, recipient, content, opts,
276
+ );
277
+ }
268
278
  let stale_worker_target_missing =
269
279
  stale_worker_target_missing_preflight(workspace, &state, recipient)?;
270
280
  let coordinator_unavailable = if stale_worker_target_missing {
@@ -77,6 +77,131 @@ fn tmux_leader_submit_without_provider_receipt_stays_pending_then_accepts_same_r
77
77
  assert!(case.delivery_consumed_at(&message_id).is_some());
78
78
  }
79
79
 
80
+ fn pi_receipt_case(tag: &str) -> (Case, serde_json::Value) {
81
+ let mut case = Case::new_without_rollout(tag);
82
+ let root = case.workspace.join("pi-sessions");
83
+ std::fs::create_dir_all(&root).unwrap();
84
+ case.rollout = root.join("not-derived-from-the-session-id.jsonl");
85
+ let receiver = &mut case.state["leader_receiver"];
86
+ receiver["provider"] = serde_json::json!("pi");
87
+ receiver["tmux_socket"] = serde_json::json!(case.workspace.join("fixture-tmux.sock"));
88
+ receiver["leader_session_uuid"] = serde_json::json!("team-owner-not-native-id");
89
+ receiver["owner_epoch"] = serde_json::json!(1);
90
+ receiver["native_session"] = serde_json::json!({
91
+ "expected_session_id": "native-pi-session",
92
+ "provider_projects_root": root,
93
+ "spawn_cwd": case.workspace,
94
+ "spawned_at": "2026-10-10T03:59:07Z",
95
+ "pane_id": "%leader",
96
+ "tmux_socket": receiver["tmux_socket"],
97
+ "leader_session_uuid": "team-owner-not-native-id",
98
+ "owner_epoch": 1,
99
+ });
100
+ crate::state::persist::save_runtime_state(&case.workspace, &case.state).unwrap();
101
+ let header = serde_json::json!({
102
+ "type": "session", "version": 3, "id": "native-pi-session",
103
+ "cwd": case.workspace, "timestamp": "2026-10-10T03:59:08Z",
104
+ });
105
+ (case, header)
106
+ }
107
+
108
+ #[test]
109
+ fn pi_leader_receipt_binds_exact_header_and_consumes_without_reinjection() {
110
+ let (case, header) = pi_receipt_case("pi-native-receipt");
111
+ let endpoint = case.state["leader_receiver"]["tmux_socket"].as_str().unwrap();
112
+ assert!(Path::new(endpoint).is_absolute());
113
+ let unbound = crate::messaging::resolve_live_leader_channel(
114
+ &case.workspace,
115
+ &case.state["leader_receiver"],
116
+ &RecordingTransport::new(""),
117
+ );
118
+ assert!(
119
+ matches!(
120
+ unbound,
121
+ crate::messaging::LeaderChannelResolution::Unbound(
122
+ crate::messaging::LeaderChannelUnbound::EndpointMismatch
123
+ )
124
+ ),
125
+ "{unbound:?}"
126
+ );
127
+ let transport = RecordingTransport {
128
+ endpoint: Some(endpoint.to_string()),
129
+ ..RecordingTransport::new("")
130
+ };
131
+ let message_id = case.seed_message("Pi receipt canary");
132
+ std::fs::write(&case.rollout, format!("{header}\n")).unwrap();
133
+ let first = deliver_pending_message(
134
+ &case.workspace,
135
+ &case.store,
136
+ &transport,
137
+ &message_id,
138
+ &case.event_log,
139
+ &case.state,
140
+ )
141
+ .unwrap();
142
+ assert!(first.ok, "{first:?}");
143
+ assert_eq!(
144
+ case.message_status(&message_id),
145
+ "submitted_pending_acceptance"
146
+ );
147
+ assert_eq!(transport.inject_count(), 1);
148
+
149
+ let received = serde_json::json!({
150
+ "type": "message", "message": {"role": "user", "content": [
151
+ {"type": "text", "text": format!("5535 [team-agent-token:{message_id}]")}
152
+ ]}
153
+ });
154
+ std::fs::write(&case.rollout, format!("{header}\n{received}\n")).unwrap();
155
+ let second = deliver_pending_message(
156
+ &case.workspace,
157
+ &case.store,
158
+ &transport,
159
+ &message_id,
160
+ &case.event_log,
161
+ &case.state,
162
+ )
163
+ .unwrap();
164
+ assert!(second.ok, "{second:?}");
165
+ assert_eq!(case.message_status(&message_id), "delivered");
166
+ assert!(case.delivery_consumed_at(&message_id).is_some());
167
+ assert_eq!(transport.inject_count(), 1);
168
+ }
169
+
170
+ #[test]
171
+ fn pi_leader_receipt_refuses_missing_ambiguous_stale_and_other_owner_backing() {
172
+ use crate::messaging::delivery::{observe_leader_receipt, LeaderReceiptObservation::*};
173
+ let (mut case, header) = pi_receipt_case("pi-native-receipt-fences");
174
+ let old = case.workspace.join("old-leader.jsonl");
175
+ std::fs::write(&old, "[team-agent-token:canary]").unwrap();
176
+ case.state["leader"] = serde_json::json!({"rollout_path": old});
177
+ // An old root leader receipt cannot substitute for this native session.
178
+ assert_eq!(observe_leader_receipt(&case.state, "canary"), SourceUnavailable);
179
+ for (field, value) in [
180
+ ("id", serde_json::json!("another-session")),
181
+ ("cwd", serde_json::json!(case.workspace.join("other"))),
182
+ ("timestamp", serde_json::json!("2026-10-10T03:59:06Z")),
183
+ ] {
184
+ let mut wrong = header.clone();
185
+ wrong[field] = value;
186
+ std::fs::write(&case.rollout, format!("{wrong}\n[team-agent-token:canary]\n")).unwrap();
187
+ assert_eq!(observe_leader_receipt(&case.state, "canary"), SourceUnavailable);
188
+ }
189
+ std::fs::write(&case.rollout, format!("{header}\n")).unwrap();
190
+ assert_eq!(observe_leader_receipt(&case.state, "canary"), TokenAbsent);
191
+ let duplicate = case.rollout.with_file_name("duplicate.jsonl");
192
+ std::fs::write(&duplicate, format!("{header}\n[team-agent-token:canary]\n")).unwrap();
193
+ assert_eq!(observe_leader_receipt(&case.state, "canary"), SourceUnavailable);
194
+ std::fs::remove_file(duplicate).unwrap();
195
+ for key in ["pane_id", "tmux_socket", "leader_session_uuid", "owner_epoch"] {
196
+ let mut moved = case.state.clone();
197
+ moved["leader_receiver"][key] = serde_json::json!("changed");
198
+ assert_eq!(observe_leader_receipt(&moved, "canary"), SourceUnavailable);
199
+ }
200
+ case.state["leader_receiver"]["native_session"]["provider_projects_root"] =
201
+ serde_json::Value::Null;
202
+ assert_eq!(observe_leader_receipt(&case.state, "canary"), SourceUnavailable);
203
+ }
204
+
80
205
  #[test]
81
206
  fn attached_leader_mailbox_is_rechecked_by_the_normal_delivery_tick() {
82
207
  let case = Case::new("mailbox");
@@ -428,6 +553,7 @@ impl Case {
428
553
  struct RecordingTransport {
429
554
  injects: Arc<AtomicUsize>,
430
555
  capture: Arc<Mutex<String>>,
556
+ endpoint: Option<String>,
431
557
  }
432
558
 
433
559
  impl RecordingTransport {
@@ -435,6 +561,7 @@ impl RecordingTransport {
435
561
  Self {
436
562
  injects: Arc::new(AtomicUsize::new(0)),
437
563
  capture: Arc::new(Mutex::new(capture.to_string())),
564
+ endpoint: None,
438
565
  }
439
566
  }
440
567
 
@@ -444,6 +571,10 @@ impl RecordingTransport {
444
571
  }
445
572
 
446
573
  impl Transport for RecordingTransport {
574
+ fn tmux_endpoint(&self) -> Option<String> {
575
+ self.endpoint.clone()
576
+ }
577
+
447
578
  fn kind(&self) -> BackendKind {
448
579
  BackendKind::Tmux
449
580
  }
@@ -16,8 +16,8 @@ use crate::model::enums::TaskStatus;
16
16
  use crate::model::errors::ModelError;
17
17
  use crate::model::ids::TaskId;
18
18
  use crate::model::task_graph::{find_dependency_cycle, TaskNode};
19
- use crate::model::yaml::Value as Yaml;
20
19
  use crate::model::yaml;
20
+ use crate::model::yaml::Value as Yaml;
21
21
  use crate::provider::wire::parse_canonical_provider;
22
22
 
23
23
  const LEGACY_DISPLAY_KEY: &str = concat!("display", "_backend");
@@ -733,10 +733,13 @@ fn semantic_errors(spec: &Yaml, base_dir: &Path) -> Vec<String> {
733
733
 
734
734
  for (idx, agent) in agents.iter().enumerate() {
735
735
  let provider = agent.get("provider");
736
- if !provider
737
- .and_then(Yaml::as_str)
738
- .is_some_and(|p| parse_canonical_provider(p).is_some())
739
- {
736
+ if !provider.and_then(Yaml::as_str).is_some_and(|p| {
737
+ #[cfg(unix)]
738
+ if crate::contract_runtime::registry::descriptor(p).is_some() {
739
+ return true;
740
+ }
741
+ parse_canonical_provider(p).is_some()
742
+ }) {
740
743
  e.push(format!(
741
744
  "/agents/{idx}/provider: unknown provider {}",
742
745
  py_repr(provider)
@@ -1066,7 +1069,11 @@ mod tests {
1066
1069
  .iter()
1067
1070
  .filter(|m| m.starts_with("/agents/dangerously_skip_permissions: missing"))
1068
1071
  .collect();
1069
- assert_eq!(missing.len(), 1, "exactly one missing-field error, got: {errors:#?}");
1072
+ assert_eq!(
1073
+ missing.len(),
1074
+ 1,
1075
+ "exactly one missing-field error, got: {errors:#?}"
1076
+ );
1070
1077
  assert!(
1071
1078
  missing[0].contains("`codex_implementer`"),
1072
1079
  "must name the role id, got: {}",
@@ -1083,7 +1090,11 @@ mod tests {
1083
1090
  #[test]
1084
1091
  fn test_invalid_dangerously_type_rejected() {
1085
1092
  let text = include_str!("testdata/team.spec.yaml");
1086
- let bad = text.replacen(" dangerously_skip_permissions: false\n", " dangerously_skip_permissions: \"yes\"\n", 1);
1093
+ let bad = text.replacen(
1094
+ " dangerously_skip_permissions: false\n",
1095
+ " dangerously_skip_permissions: \"yes\"\n",
1096
+ 1,
1097
+ );
1087
1098
  assert_ne!(text, bad, "fixture must contain the field");
1088
1099
  let spec = yaml::loads(&bad).unwrap();
1089
1100
  let errors = all_spec_errors(&spec);
@@ -1091,7 +1102,11 @@ mod tests {
1091
1102
  .iter()
1092
1103
  .filter(|m| m.starts_with("/agents/dangerously_skip_permissions: must be a boolean"))
1093
1104
  .collect();
1094
- assert_eq!(type_errs.len(), 1, "exactly one type error, got: {errors:#?}");
1105
+ assert_eq!(
1106
+ type_errs.len(),
1107
+ 1,
1108
+ "exactly one type error, got: {errors:#?}"
1109
+ );
1095
1110
  assert!(
1096
1111
  type_errs[0].contains("`codex_implementer`"),
1097
1112
  "must name the role id, got: {}",
@@ -1139,10 +1154,7 @@ mod tests {
1139
1154
  // 缺失 role frontmatter 值时编译为安全默认 false;此处锁定公共编译入口的结果。
1140
1155
  #[test]
1141
1156
  fn test_all_7_triggers_use_compiled_default_bypass() {
1142
- let team = std::env::temp_dir().join(format!(
1143
- "ta-spec-7trigger-{}",
1144
- std::process::id()
1145
- ));
1157
+ let team = std::env::temp_dir().join(format!("ta-spec-7trigger-{}", std::process::id()));
1146
1158
  let _ = std::fs::remove_dir_all(&team);
1147
1159
  std::fs::create_dir_all(team.join("agents")).unwrap();
1148
1160
  std::fs::write(
@@ -300,6 +300,10 @@ pub fn get_adapter(p: Provider) -> Box<dyn ProviderAdapter> {
300
300
  Box::new(BasicProviderAdapter { provider: p })
301
301
  }
302
302
 
303
+ #[cfg(test)]
304
+ #[path = "tests/command_helpers.rs"]
305
+ mod command_helpers_tests;
306
+
303
307
  #[derive(Debug, Clone, Copy)]
304
308
  pub(crate) struct BasicProviderAdapter {
305
309
  provider: Provider,
@@ -480,22 +484,13 @@ impl ProviderAdapter for BasicProviderAdapter {
480
484
  Ok(argv)
481
485
  }
482
486
  Provider::Grok => Ok(grok_launch_command(
483
- self,
484
- auth_mode,
485
- mcp_config,
486
487
  system_prompt,
487
488
  model,
488
489
  dangerously_skip_permissions,
489
490
  )?),
490
491
  Provider::CursorAgent => Ok(cursor_agent_base_command(
491
- self,
492
- auth_mode,
493
- mcp_config,
494
- system_prompt,
495
492
  model,
496
493
  dangerously_skip_permissions,
497
- false,
498
- None,
499
494
  )?),
500
495
  Provider::Pi => Err(ProviderError::Command(
501
496
  "Pi commands require the shared lifecycle materializer".to_string(),
@@ -596,14 +591,9 @@ impl ProviderAdapter for BasicProviderAdapter {
596
591
  // never written and restart could not resume.
597
592
  let expected = next_session_token();
598
593
  let mut argv = grok_base_command(
599
- self,
600
- ctx.auth_mode,
601
- ctx.mcp_config,
602
594
  ctx.system_prompt,
603
595
  ctx.model,
604
596
  ctx.dangerously_skip_permissions,
605
- ctx.profile_launch
606
- .is_some_and(|profile| profile.managed_mcp_config),
607
597
  ctx.effort,
608
598
  )?;
609
599
  argv.push("--session-id".to_string());
@@ -637,15 +627,8 @@ impl ProviderAdapter for BasicProviderAdapter {
637
627
  // U-01 create-chat 无登录超时;禁止发明 --session-id。
638
628
  // pending 只能是 CLI chatId,本拍不造假 uuid。
639
629
  let argv = cursor_agent_base_command(
640
- self,
641
- ctx.auth_mode,
642
- ctx.mcp_config,
643
- ctx.system_prompt,
644
630
  ctx.model,
645
631
  ctx.dangerously_skip_permissions,
646
- ctx.profile_launch
647
- .is_some_and(|profile| profile.managed_mcp_config),
648
- ctx.effort,
649
632
  )?;
650
633
  Ok(CommandPlan {
651
634
  argv,
@@ -816,7 +799,7 @@ impl ProviderAdapter for BasicProviderAdapter {
816
799
  // copilot --resume 接受 id|name)。
817
800
  Provider::Copilot => {
818
801
  let mut argv =
819
- copilot_base_command_resume(auth_mode, mcp_config, system_prompt, model, dangerously_skip_permissions);
802
+ copilot_base_command(auth_mode, mcp_config, system_prompt, model, dangerously_skip_permissions);
820
803
  argv.push("--resume".to_string());
821
804
  argv.push(session_id.as_str().to_string());
822
805
  Ok(argv)
@@ -824,13 +807,9 @@ impl ProviderAdapter for BasicProviderAdapter {
824
807
  // Grok resume: base + `--resume <id>` (grok -r/--resume 接受 id|title)。
825
808
  Provider::Grok => {
826
809
  let mut argv = grok_base_command(
827
- self,
828
- auth_mode,
829
- mcp_config,
830
810
  system_prompt,
831
811
  model,
832
812
  dangerously_skip_permissions,
833
- false,
834
813
  None,
835
814
  )?;
836
815
  argv.push("--resume".to_string());
@@ -839,16 +818,7 @@ impl ProviderAdapter for BasicProviderAdapter {
839
818
  }
840
819
  // Cursor resume: base + `--resume <chatId>`。
841
820
  Provider::CursorAgent => {
842
- let mut argv = cursor_agent_base_command(
843
- self,
844
- auth_mode,
845
- mcp_config,
846
- system_prompt,
847
- model,
848
- dangerously_skip_permissions,
849
- false,
850
- None,
851
- )?;
821
+ let mut argv = cursor_agent_base_command(model, dangerously_skip_permissions)?;
852
822
  // Put --resume immediately after `agent` so the CLI treats it as
853
823
  // the session mode rather than a trailing flag after --workspace.
854
824
  argv.insert(1, "--resume".to_string());
@@ -941,14 +911,9 @@ impl ProviderAdapter for BasicProviderAdapter {
941
911
  ));
942
912
  };
943
913
  let mut argv = grok_base_command(
944
- self,
945
- ctx.auth_mode,
946
- ctx.mcp_config,
947
914
  ctx.system_prompt,
948
915
  ctx.model,
949
916
  ctx.dangerously_skip_permissions,
950
- ctx.profile_launch
951
- .is_some_and(|profile| profile.managed_mcp_config),
952
917
  ctx.effort,
953
918
  )?;
954
919
  argv.push("--resume".to_string());
@@ -968,15 +933,8 @@ impl ProviderAdapter for BasicProviderAdapter {
968
933
  ));
969
934
  }
970
935
  let mut argv = cursor_agent_base_command(
971
- self,
972
- ctx.auth_mode,
973
- ctx.mcp_config,
974
- ctx.system_prompt,
975
936
  ctx.model,
976
937
  ctx.dangerously_skip_permissions,
977
- ctx.profile_launch
978
- .is_some_and(|profile| profile.managed_mcp_config),
979
- ctx.effort,
980
938
  )?;
981
939
  argv.push("--resume".to_string());
982
940
  argv.push(raw.to_string());
@@ -1067,13 +1025,9 @@ impl ProviderAdapter for BasicProviderAdapter {
1067
1025
  // (grok help: --fork-session 需配合 --resume/--continue)。
1068
1026
  Provider::Grok => {
1069
1027
  let mut argv = grok_base_command(
1070
- self,
1071
- auth_mode,
1072
- mcp_config,
1073
1028
  system_prompt,
1074
1029
  model,
1075
1030
  dangerously_skip_permissions,
1076
- false,
1077
1031
  None,
1078
1032
  )?;
1079
1033
  argv.push("--session-id".to_string());
@@ -1191,7 +1145,7 @@ impl ProviderAdapter for BasicProviderAdapter {
1191
1145
  .ok_or_else(|| {
1192
1146
  ProviderError::Command("copilot fork root was not materialized".to_string())
1193
1147
  })?;
1194
- let mut argv = copilot_base_command_resume(
1148
+ let mut argv = copilot_base_command(
1195
1149
  ctx.auth_mode,
1196
1150
  ctx.mcp_config,
1197
1151
  ctx.system_prompt,
@@ -1214,14 +1168,9 @@ impl ProviderAdapter for BasicProviderAdapter {
1214
1168
  ));
1215
1169
  };
1216
1170
  let mut argv = grok_base_command(
1217
- self,
1218
- ctx.auth_mode,
1219
- ctx.mcp_config,
1220
1171
  ctx.system_prompt,
1221
1172
  ctx.model,
1222
1173
  ctx.dangerously_skip_permissions,
1223
- ctx.profile_launch
1224
- .is_some_and(|profile| profile.managed_mcp_config),
1225
1174
  ctx.effort,
1226
1175
  )?;
1227
1176
  argv.push("--session-id".to_string());
@@ -1405,7 +1354,7 @@ pub(crate) fn json_inline(value: &serde_json::Value) -> String {
1405
1354
  // the extracted base-command functions, not directly by this file.
1406
1355
  use super::adapters::claude::{claude_base_command, claude_launch_command};
1407
1356
  use super::adapters::codex::codex_base_command;
1408
- use super::adapters::copilot::{copilot_base_command, copilot_base_command_resume};
1357
+ use super::adapters::copilot::copilot_base_command;
1409
1358
  use super::adapters::cursor_agent::cursor_agent_base_command;
1410
1359
  use super::adapters::fake::fake_worker_command;
1411
1360
  use super::adapters::grok::{grok_base_command, grok_launch_command};