@team-agent/installer 0.5.106 → 0.5.108

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 (41) hide show
  1. package/Cargo.lock +1 -1
  2. package/Cargo.toml +1 -1
  3. package/crates/team-agent/src/cli/adapters.rs +1 -1
  4. package/crates/team-agent/src/cli/emit.rs +3 -3
  5. package/crates/team-agent/src/cli/mod.rs +87 -4
  6. package/crates/team-agent/src/cli/models.rs +252 -3
  7. package/crates/team-agent/src/cli/spec.rs +1 -1
  8. package/crates/team-agent/src/cli/status_port/brief.rs +155 -37
  9. package/crates/team-agent/src/cli/tests/base.rs +112 -225
  10. package/crates/team-agent/src/cli/tests/shutdown_kill_plan.rs +184 -0
  11. package/crates/team-agent/src/cli/tests/status_brief.rs +624 -0
  12. package/crates/team-agent/src/compiler/tests.rs +46 -59
  13. package/crates/team-agent/src/leader/tests/byte_findings.rs +1 -14
  14. package/crates/team-agent/src/lifecycle/launch/pi_mcp.rs +57 -173
  15. package/crates/team-agent/src/lifecycle/launch/pi_mcp_extension.js +226 -0
  16. package/crates/team-agent/src/lifecycle/launch/quick_start.rs +0 -37
  17. package/crates/team-agent/src/lifecycle/tests/agent_ops.rs +0 -9
  18. package/crates/team-agent/src/lifecycle/tests/fixtures/pi_native_mcp_host.mjs +71 -0
  19. package/crates/team-agent/src/lifecycle/tests/lane_ops.rs +0 -66
  20. package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +0 -392
  21. package/crates/team-agent/src/lifecycle/tests/lifecycle_lock.rs +0 -5
  22. package/crates/team-agent/src/lifecycle/tests/phase_golden.rs +2 -361
  23. package/crates/team-agent/src/lifecycle/tests/pi_dual_entry_red.rs +1 -16
  24. package/crates/team-agent/src/lifecycle/tests/pi_executable_mcp_red.rs +20 -112
  25. package/crates/team-agent/src/lifecycle/tests/pi_fork_session_inheritance_red.rs +1 -12
  26. package/crates/team-agent/src/lifecycle/tests/pi_native_mcp_capability_red.rs +546 -0
  27. package/crates/team-agent/src/lifecycle/tests.rs +1 -0
  28. package/crates/team-agent/src/lifecycle/worker_command_context.rs +11 -54
  29. package/crates/team-agent/src/messaging/tests/wave2.rs +2 -302
  30. package/crates/team-agent/src/model/name_similarity.rs +52 -67
  31. package/crates/team-agent/src/model/task_graph.rs +37 -124
  32. package/crates/team-agent/src/model/yaml/tests.rs +46 -165
  33. package/crates/team-agent/src/provider/model_catalog.rs +166 -3
  34. package/crates/team-agent/src/provider/testdata/cursor-list-models-native.stdout +250 -0
  35. package/crates/team-agent/src/provider/testdata/grok-models-authenticated.stdout +9 -0
  36. package/crates/team-agent/src/provider/testdata/grok-models-unauthenticated.stdout +7 -0
  37. package/crates/team-agent/src/provider/tests/adapter.rs +0 -20
  38. package/crates/team-agent/src/provider/tests/classify.rs +106 -198
  39. package/crates/team-agent/src/tmux_backend/tests.rs +178 -501
  40. package/crates/team-agent/src/transport/tests/wire.rs +0 -55
  41. package/package.json +4 -4
@@ -0,0 +1,546 @@
1
+ #![allow(clippy::expect_used, clippy::panic, clippy::unwrap_used)]
2
+
3
+ #[path = "../../../tests/support/hermetic.rs"]
4
+ mod hermetic;
5
+ #[path = "../../../tests/support/mcp_sim_harness.rs"]
6
+ mod sim;
7
+
8
+ use hermetic::HermeticTestEnv;
9
+ use serde_json::{json, Value};
10
+ use std::collections::BTreeSet;
11
+ use std::io::{BufRead, BufReader, Write};
12
+ use std::os::unix::fs::PermissionsExt;
13
+ use std::path::{Path, PathBuf};
14
+ use std::process::{Child, ChildStdin, Command, ExitStatus, Stdio};
15
+ use std::sync::mpsc::{self, Receiver};
16
+ use std::thread;
17
+ use std::time::{Duration, Instant};
18
+
19
+ use crate::event_log::EventLog;
20
+ use crate::lifecycle::launch::pi_mcp::{
21
+ materialize_pi_plan, pi_model_candidates, pi_seat_paths, PiMaterializeRequest, PiSessionScope,
22
+ };
23
+ use crate::lifecycle::launch::run_pi_catalog_preflight;
24
+ use crate::lifecycle::tests::test_binary_path;
25
+ use crate::message_store::MessageStore;
26
+ use crate::provider::McpConfig;
27
+
28
+ const CHILD_MARKER: &str = "TEAM_AGENT_TEST_PI_NATIVE_MCP_CHILD";
29
+ const TEST_PATH: &str = concat!(
30
+ "lifecycle::tests::pi_native_mcp_capability_red::",
31
+ "native_pi_without_adapter_registers_and_serves_all_team_tools"
32
+ );
33
+ const REQUIRED_TOOLS: [&str; 3] = ["send_message", "report_result", "get_team_status"];
34
+
35
+ #[test]
36
+ fn native_pi_without_adapter_registers_and_serves_all_team_tools() {
37
+ if std::env::var_os(CHILD_MARKER).is_some() {
38
+ native_pi_without_adapter_registers_and_serves_all_team_tools_body();
39
+ return;
40
+ }
41
+
42
+ let output = Command::new(std::env::current_exe().expect("lib-test executable"))
43
+ .args(["--exact", TEST_PATH, "--nocapture", "--test-threads=1"])
44
+ .env(CHILD_MARKER, "1")
45
+ .output()
46
+ .expect("run N1 native Pi fixture in an isolated test process");
47
+ assert!(
48
+ output.status.success(),
49
+ "isolated N1 child failed: status={:?}\nstdout:\n{}\nstderr:\n{}",
50
+ output.status.code(),
51
+ String::from_utf8_lossy(&output.stdout),
52
+ String::from_utf8_lossy(&output.stderr),
53
+ );
54
+ print!("{}", String::from_utf8_lossy(&output.stdout));
55
+ eprint!("{}", String::from_utf8_lossy(&output.stderr));
56
+ }
57
+
58
+ fn native_pi_without_adapter_registers_and_serves_all_team_tools_body() {
59
+ let hermetic = HermeticTestEnv::enter("pi-native-mcp-n1");
60
+ let system_path = std::env::var_os("PATH").expect("test PATH");
61
+ let runner = executable_on_path("bun", &system_path)
62
+ .or_else(|| executable_on_path("node", &system_path))
63
+ .expect("N1 fixture requires Bun or Node.js (>=22) runtime");
64
+ let root = hermetic.workspace("native-pi");
65
+ let bin = root.join("bin");
66
+ std::fs::create_dir_all(&bin).expect("create fake Pi bin");
67
+ let call_log = root.join("pi-calls.txt");
68
+ let pi_real = root.join("pi-real");
69
+ let script = r##"#!/bin/sh
70
+ printf '%s\n' "${1-}" >> "$TEAM_AGENT_TEST_PI_CALL_LOG"
71
+ case "${1-}" in
72
+ --version) printf '1.0.0\n' ;;
73
+ --list-models) printf 'provider model\nteam-agent qwen3.8-27b\n' ;;
74
+ list) exit 0 ;;
75
+ *) exit 64 ;;
76
+ esac
77
+ "##;
78
+ std::fs::write(&pi_real, script).expect("write standard Pi fixture");
79
+ std::fs::set_permissions(&pi_real, std::fs::Permissions::from_mode(0o755))
80
+ .expect("make fake Pi executable");
81
+ std::os::unix::fs::symlink(&pi_real, bin.join("pi")).expect("create npm-style Pi symlink");
82
+ let mut fixture_path_entries = vec![bin.clone()];
83
+ fixture_path_entries.extend(std::env::split_paths(&system_path));
84
+ let fixture_path = std::env::join_paths(fixture_path_entries).expect("join fake Pi PATH");
85
+ let _path = hermetic.with_env(
86
+ "PATH",
87
+ fixture_path.to_str().expect("fixture PATH must be UTF-8"),
88
+ );
89
+ let _call_log = hermetic.with_env(
90
+ "TEAM_AGENT_TEST_PI_CALL_LOG",
91
+ call_log.to_str().expect("call log path must be UTF-8"),
92
+ );
93
+
94
+ let roles = hermetic.workspace("roles");
95
+ let agents = roles.join("agents");
96
+ std::fs::create_dir_all(&agents).expect("create Pi role directory");
97
+ std::fs::write(
98
+ agents.join("n1.md"),
99
+ "---\nprovider: pi\nmodel: team-agent/qwen3.8-27b\n---\nN1 native Pi fixture\n",
100
+ )
101
+ .expect("write exact-model Pi role");
102
+ assert_eq!(
103
+ pi_model_candidates("qwen3.8-27b").expect("fake Pi model catalog"),
104
+ ["team-agent/qwen3.8-27b"],
105
+ "catalog discovery must succeed without consulting the package list"
106
+ );
107
+ let mut discover = |requested: &str| pi_model_candidates(requested).map_err(|_| ());
108
+ run_pi_catalog_preflight(&roles, &mut discover)
109
+ .expect("exact-model catalog preflight must pass on native-only Pi");
110
+
111
+ let harness = sim::McpSimHarness::new();
112
+ harness.bind_leader_receiver_to_team("teamA");
113
+ let workspace = harness.workspace_path();
114
+ let candidate = PathBuf::from(test_binary_path());
115
+ assert!(
116
+ candidate.is_absolute() && candidate.is_file(),
117
+ "MCP candidate must be an absolute built binary: {}",
118
+ candidate.display()
119
+ );
120
+ let args = vec![
121
+ "mcp-server".to_string(),
122
+ "--workspace".to_string(),
123
+ workspace.to_string_lossy().into_owned(),
124
+ ];
125
+ let env = json!({
126
+ "TEAM_AGENT_WORKSPACE": workspace,
127
+ "TEAM_AGENT_ID": "worker_a",
128
+ "TEAM_AGENT_AGENT_ID": "worker_a",
129
+ "TEAM_AGENT_OWNER_TEAM_ID": "teamA",
130
+ "TEAM_AGENT_LEADER_SESSION_UUID_OVERRIDE": "leader-session-team-a"
131
+ });
132
+ let config = McpConfig {
133
+ raw: json!({
134
+ "team_orchestrator": {
135
+ "command": candidate,
136
+ "args": args,
137
+ "cwd": workspace,
138
+ "env": env
139
+ }
140
+ }),
141
+ };
142
+ let plan = materialize_pi_plan(PiMaterializeRequest {
143
+ workspace,
144
+ team_id: "teamA",
145
+ agent_id: "worker_a",
146
+ model: Some("team-agent/qwen3.8-27b"),
147
+ effort: None,
148
+ system_prompt: "N1 native Pi MCP capability fixture",
149
+ team_mcp_tools: &REQUIRED_TOOLS,
150
+ mcp_config: &config,
151
+ session_scope: PiSessionScope::Isolated,
152
+ });
153
+ let pi_calls = std::fs::read_to_string(&call_log).unwrap_or_default();
154
+ assert!(
155
+ plan.is_ok(),
156
+ "N1 must materialize on native-capable Pi when `pi list` is empty; got={plan:?}; observed_pi_commands={pi_calls:?}"
157
+ );
158
+ let plan = plan.expect("N1 materialization after the assertion above");
159
+
160
+ let commands = pi_calls.lines().collect::<Vec<_>>();
161
+ assert!(
162
+ !commands.iter().any(|command| *command == "list"),
163
+ "capability resolution must not call `pi list` or inspect package presence: {commands:?}"
164
+ );
165
+ let seat = pi_seat_paths(workspace, "teamA", "worker_a");
166
+ assert!(
167
+ seat.wrapper.is_file(),
168
+ "successful materialization must publish the seat wrapper"
169
+ );
170
+ assert!(
171
+ plan.argv
172
+ .iter()
173
+ .any(|arg| arg == seat.wrapper.to_string_lossy().as_ref()),
174
+ "Pi command plan must load the generated native extension: {:?}",
175
+ plan.argv
176
+ );
177
+
178
+ let receipt_path = workspace.join(".team/test-evidence/native-registration.json");
179
+ std::fs::create_dir_all(receipt_path.parent().expect("registration receipt parent"))
180
+ .expect("create registration receipt directory");
181
+ let host_fixture = Path::new(env!("CARGO_MANIFEST_DIR"))
182
+ .join("src/lifecycle/tests/fixtures/pi_native_mcp_host.mjs");
183
+ let host = Command::new(&runner)
184
+ .arg(host_fixture)
185
+ .arg(&seat.wrapper)
186
+ .arg(&receipt_path)
187
+ .arg(workspace)
188
+ .current_dir(workspace)
189
+ .output()
190
+ .expect("execute generated extension in the Pi native API fixture");
191
+ assert!(
192
+ host.status.success(),
193
+ "Pi native host fixture failed: status={:?}\nstdout:\n{}\nstderr:\n{}",
194
+ host.status.code(),
195
+ String::from_utf8_lossy(&host.stdout),
196
+ String::from_utf8_lossy(&host.stderr),
197
+ );
198
+ let native_receipt: Value = serde_json::from_slice(
199
+ &std::fs::read(&receipt_path).expect("native registerMcpServer receipt"),
200
+ )
201
+ .expect("parse native registration receipt");
202
+ let registrations = native_receipt["registrations"]
203
+ .as_array()
204
+ .expect("host recorded registerMcpServer calls");
205
+ assert_eq!(
206
+ registrations.len(),
207
+ 1,
208
+ "one owned native server registration is expected"
209
+ );
210
+ let expected_runtime_tools = BTreeSet::from(REQUIRED_TOOLS.map(|tool| {
211
+ format!(
212
+ "mcp__{}__{tool}",
213
+ registrations[0]["name"].as_str().unwrap()
214
+ )
215
+ }));
216
+ let runtime_tools = native_receipt["activeTools"]
217
+ .as_array()
218
+ .expect("native Pi consumer exposes active tools")
219
+ .iter()
220
+ .filter_map(Value::as_str)
221
+ .map(str::to_string)
222
+ .collect::<BTreeSet<_>>();
223
+ assert_eq!(runtime_tools, expected_runtime_tools);
224
+ assert_eq!(
225
+ native_receipt["expectedTools"]
226
+ .as_array()
227
+ .expect("host confirms native binding tools")
228
+ .iter()
229
+ .filter_map(Value::as_str)
230
+ .map(str::to_string)
231
+ .collect::<BTreeSet<_>>(),
232
+ expected_runtime_tools,
233
+ );
234
+ let wire_prompt = native_receipt["wirePrompt"]
235
+ .as_str()
236
+ .expect("Pi wrapper installs an actual tool wire binding");
237
+ assert!(REQUIRED_TOOLS.iter().all(|tool| wire_prompt.contains(tool)));
238
+ let registration = &registrations[0];
239
+ let alias = registration["name"]
240
+ .as_str()
241
+ .expect("native registration name");
242
+ let alias_id = alias
243
+ .strip_prefix("team_")
244
+ .expect("native server alias must be Team-owned");
245
+ assert_eq!(
246
+ alias_id.len(),
247
+ 32,
248
+ "Team alias retains UUID entropy: {alias:?}"
249
+ );
250
+ assert!(alias_id.bytes().all(|byte| byte.is_ascii_hexdigit()));
251
+ assert!(!alias.contains('-') && !alias.contains('/'));
252
+ let native_config = &registration["config"];
253
+ assert_eq!(native_config["command"], json!(candidate.to_string_lossy()));
254
+ assert_eq!(native_config["args"], json!(args));
255
+ assert_eq!(native_config["cwd"], json!(workspace.to_string_lossy()));
256
+ assert_eq!(native_config["exposure"], json!("hidden"));
257
+ let exposure = native_config["toolExposure"]
258
+ .as_object()
259
+ .expect("native per-tool exposure map");
260
+ assert_eq!(
261
+ exposure.keys().cloned().collect::<BTreeSet<_>>(),
262
+ BTreeSet::from(REQUIRED_TOOLS.map(str::to_string)),
263
+ "only the three Team operations are directly exposed"
264
+ );
265
+ assert!(exposure.values().all(|value| value == "direct"));
266
+ for (key, expected) in [
267
+ ("TEAM_AGENT_WORKSPACE", workspace.to_string_lossy().as_ref()),
268
+ ("TEAM_AGENT_ID", "worker_a"),
269
+ ("TEAM_AGENT_AGENT_ID", "worker_a"),
270
+ ("TEAM_AGENT_OWNER_TEAM_ID", "teamA"),
271
+ ] {
272
+ assert_eq!(
273
+ native_config["env"][key].as_str(),
274
+ Some(expected),
275
+ "native server must retain captured seat identity for {key}"
276
+ );
277
+ }
278
+
279
+ let store = MessageStore::open(workspace).expect("open isolated MCP store");
280
+ store
281
+ .create_message(
282
+ Some("task_mcp"),
283
+ "leader",
284
+ "worker_a",
285
+ "N1_NATIVE_MCP_INPUT",
286
+ None,
287
+ false,
288
+ Some("teamA"),
289
+ )
290
+ .expect("seed attributable task input");
291
+ let mut client = RawMcpClient::spawn(native_config, &system_path, hermetic.home());
292
+ let initialize = client.rpc("initialize", json!({"protocolVersion":"2024-11-05"}));
293
+ assert_eq!(
294
+ initialize["result"]["serverInfo"]["name"],
295
+ json!("team_orchestrator")
296
+ );
297
+ client.notification("notifications/initialized", json!({}));
298
+ let listed = client.rpc("tools/list", json!({}));
299
+ let tool_names = listed["result"]["tools"]
300
+ .as_array()
301
+ .expect("raw tools/list response")
302
+ .iter()
303
+ .filter_map(|tool| tool["name"].as_str())
304
+ .map(str::to_string)
305
+ .collect::<BTreeSet<_>>();
306
+ assert_eq!(
307
+ tool_names,
308
+ BTreeSet::from(REQUIRED_TOOLS.map(str::to_string)),
309
+ "native config must reach the actual stdio MCP server with exactly the three Team tools"
310
+ );
311
+
312
+ let status = client.call_tool("get_team_status", json!({}));
313
+ assert!(
314
+ !status.is_error && status.body["ok"] == json!(true),
315
+ "get_team_status stdio call: {}",
316
+ status.body
317
+ );
318
+ assert!(status.body["teams"].get("teamA").is_some());
319
+ let marker = format!("N1_NATIVE_MCP_SEND_{}", std::process::id());
320
+ let send = client.call_tool("send_message", json!({"to":"leader","content":marker}));
321
+ assert!(
322
+ !send.is_error && send.body["ok"] == json!(true),
323
+ "send_message stdio call: {}",
324
+ send.body
325
+ );
326
+ let result_marker = format!("N1_NATIVE_MCP_RESULT_{}", std::process::id());
327
+ let report = client.call_tool(
328
+ "report_result",
329
+ json!({"task_id":"task_mcp","summary":result_marker}),
330
+ );
331
+ assert!(
332
+ !report.is_error,
333
+ "report_result stdio call: {}",
334
+ report.body
335
+ );
336
+ let result_id = report.body["result_id"]
337
+ .as_str()
338
+ .expect("durable result id");
339
+ let result_row = harness
340
+ .result_row(result_id)
341
+ .expect("report_result persists result");
342
+ assert_eq!(result_row.owner_team_id.as_deref(), Some("teamA"));
343
+ assert_eq!(result_row.agent_id, "worker_a");
344
+ assert_eq!(result_row.task_id, "task_mcp");
345
+ harness.drive_delivery_twice();
346
+ assert!(harness.pane_text("leader").contains(&marker));
347
+ assert!(harness.pane_text("leader").contains(&result_marker));
348
+
349
+ let trace_path = workspace.join(".team/test-evidence/native-mcp-stdio.jsonl");
350
+ client.write_trace(&trace_path);
351
+ let raw_trace = std::fs::read_to_string(&trace_path).expect("raw MCP stdio trace");
352
+ let server_pid = client.pid();
353
+ let exit = client.close_stdin_and_wait();
354
+ assert!(
355
+ exit.success(),
356
+ "native candidate stdio server exit must be successful: {exit:?}"
357
+ );
358
+ let events = EventLog::new(workspace)
359
+ .tail(0)
360
+ .expect("MCP server lifecycle receipt");
361
+ assert!(
362
+ events.iter().any(|event| {
363
+ event["event"] == "mcp.server_started" && event["pid"] == json!(server_pid)
364
+ }),
365
+ "actual stdio server start receipt must match child pid {server_pid}"
366
+ );
367
+ assert!(
368
+ events.iter().any(|event| {
369
+ event["event"] == "mcp.server_exit"
370
+ && event["pid"] == json!(server_pid)
371
+ && event["reason"] == json!("stdin_eof")
372
+ }),
373
+ "actual stdio server exit receipt must match child pid and clean EOF"
374
+ );
375
+ println!(
376
+ "N1_NATIVE_MCP_RECEIPT={}",
377
+ json!({"alias":alias,"candidate":candidate,"native_config":native_config,"native_host":native_receipt,"server_pid":server_pid,"exit_code":exit.code(),"tool_names":tool_names,"pi_commands":commands,"server_events":events,"stdio_trace":trace_path})
378
+ );
379
+ println!("N1_NATIVE_MCP_TRACE_BEGIN");
380
+ print!("{raw_trace}");
381
+ println!("N1_NATIVE_MCP_TRACE_END");
382
+ }
383
+
384
+ fn executable_on_path(name: &str, path: &std::ffi::OsStr) -> Option<PathBuf> {
385
+ std::env::split_paths(path)
386
+ .map(|entry| entry.join(name))
387
+ .find(|candidate| candidate.is_file())
388
+ }
389
+
390
+ struct RawMcpClient {
391
+ child: Child,
392
+ stdin: Option<ChildStdin>,
393
+ responses: Receiver<String>,
394
+ next_id: u64,
395
+ trace: Vec<Value>,
396
+ waited: bool,
397
+ }
398
+
399
+ struct ToolCall {
400
+ body: Value,
401
+ is_error: bool,
402
+ }
403
+
404
+ impl RawMcpClient {
405
+ fn spawn(config: &Value, path: &std::ffi::OsStr, home: &Path) -> Self {
406
+ let program = config["command"].as_str().expect("registered MCP command");
407
+ let args = config["args"]
408
+ .as_array()
409
+ .expect("registered MCP args")
410
+ .iter()
411
+ .map(|arg| arg.as_str().expect("string MCP arg").to_string())
412
+ .collect::<Vec<_>>();
413
+ let cwd = config["cwd"].as_str().expect("registered MCP cwd");
414
+ let env = config["env"].as_object().expect("registered MCP env");
415
+ let mut command = Command::new(program);
416
+ command
417
+ .args(&args)
418
+ .env_clear()
419
+ .env("PATH", path)
420
+ .env("HOME", home)
421
+ .env("LANG", "C.UTF-8")
422
+ .current_dir(cwd)
423
+ .stdin(Stdio::piped())
424
+ .stdout(Stdio::piped())
425
+ .stderr(Stdio::null());
426
+ for (key, value) in env {
427
+ command.env(key, value.as_str().expect("string MCP env value"));
428
+ }
429
+ let mut child = command
430
+ .spawn()
431
+ .expect("spawn registered native stdio MCP command");
432
+ let stdin = child.stdin.take().expect("MCP child stdin");
433
+ let stdout = child.stdout.take().expect("MCP child stdout");
434
+ let (tx, responses) = mpsc::channel();
435
+ thread::spawn(move || {
436
+ for line in BufReader::new(stdout).lines() {
437
+ let Ok(line) = line else { break };
438
+ if tx.send(line).is_err() {
439
+ break;
440
+ }
441
+ }
442
+ });
443
+ Self {
444
+ child,
445
+ stdin: Some(stdin),
446
+ responses,
447
+ next_id: 1,
448
+ trace: Vec::new(),
449
+ waited: false,
450
+ }
451
+ }
452
+
453
+ fn pid(&self) -> u32 {
454
+ self.child.id()
455
+ }
456
+
457
+ fn rpc(&mut self, method: &str, params: Value) -> Value {
458
+ let id = self.next_id;
459
+ self.next_id += 1;
460
+ let request = json!({"jsonrpc":"2.0","id":id,"method":method,"params":params});
461
+ let line = serde_json::to_string(&request).expect("serialize MCP request");
462
+ let stdin = self.stdin.as_mut().expect("MCP stdin is open");
463
+ writeln!(stdin, "{line}").expect("write MCP request");
464
+ stdin.flush().expect("flush MCP request");
465
+ let response_line = self
466
+ .responses
467
+ .recv_timeout(Duration::from_secs(75))
468
+ .unwrap_or_else(|_| panic!("timed out waiting for stdio MCP {method} response"));
469
+ let response: Value = serde_json::from_str(&response_line).unwrap_or_else(|error| {
470
+ panic!("invalid MCP response for {method}: {error}; {response_line}")
471
+ });
472
+ assert_eq!(response["id"], json!(id), "MCP response id for {method}");
473
+ assert!(
474
+ response.get("error").is_none(),
475
+ "MCP protocol error for {method}: {response}"
476
+ );
477
+ self.trace
478
+ .push(json!({"request":request,"response":response}));
479
+ response
480
+ }
481
+
482
+ fn notification(&mut self, method: &str, params: Value) {
483
+ let request = json!({"jsonrpc":"2.0","method":method,"params":params});
484
+ let line = serde_json::to_string(&request).expect("serialize MCP notification");
485
+ let stdin = self.stdin.as_mut().expect("MCP stdin is open");
486
+ writeln!(stdin, "{line}").expect("write MCP notification");
487
+ stdin.flush().expect("flush MCP notification");
488
+ self.trace.push(json!({"request":request,"response":null}));
489
+ }
490
+
491
+ fn call_tool(&mut self, name: &str, arguments: Value) -> ToolCall {
492
+ let response = self.rpc("tools/call", json!({"name":name,"arguments":arguments}));
493
+ let result = response
494
+ .get("result")
495
+ .unwrap_or_else(|| panic!("tools/call missing result: {response}"));
496
+ let text = result["content"][0]["text"]
497
+ .as_str()
498
+ .expect("tool response text");
499
+ ToolCall {
500
+ body: serde_json::from_str(text).unwrap_or_else(|_| json!({"raw_text":text})),
501
+ is_error: result["isError"].as_bool().unwrap_or(false),
502
+ }
503
+ }
504
+
505
+ fn write_trace(&self, path: &Path) {
506
+ let parent = path.parent().expect("MCP trace parent");
507
+ std::fs::create_dir_all(parent).expect("create MCP trace directory");
508
+ let mut file = std::fs::File::create(path).expect("create raw MCP trace");
509
+ for row in &self.trace {
510
+ writeln!(
511
+ file,
512
+ "{}",
513
+ serde_json::to_string(row).expect("serialize trace row")
514
+ )
515
+ .expect("write raw MCP trace");
516
+ }
517
+ }
518
+
519
+ fn close_stdin_and_wait(&mut self) -> ExitStatus {
520
+ self.stdin.take();
521
+ let deadline = Instant::now() + Duration::from_secs(15);
522
+ loop {
523
+ if let Some(status) = self.child.try_wait().expect("poll MCP server exit") {
524
+ self.waited = true;
525
+ return status;
526
+ }
527
+ if Instant::now() >= deadline {
528
+ let _ = self.child.kill();
529
+ let status = self.child.wait().expect("reap timed-out MCP server");
530
+ self.waited = true;
531
+ panic!("native stdio MCP server did not exit after EOF: {status:?}");
532
+ }
533
+ thread::sleep(Duration::from_millis(10));
534
+ }
535
+ }
536
+ }
537
+
538
+ impl Drop for RawMcpClient {
539
+ fn drop(&mut self) {
540
+ if !self.waited {
541
+ let _ = self.child.kill();
542
+ let _ = self.child.wait();
543
+ self.waited = true;
544
+ }
545
+ }
546
+ }
@@ -84,6 +84,7 @@ mod pi_dual_entry_red;
84
84
  mod pi_executable_mcp_red;
85
85
  mod pi_fork_session_inheritance_red;
86
86
  mod pi_lifecycle_doctor_red;
87
+ mod pi_native_mcp_capability_red;
87
88
  mod pi_session_delivery_red;
88
89
  mod quick_start_worker_readiness_red;
89
90
  mod realmachine_clusters_1_6_red;
@@ -185,26 +185,13 @@ pub(crate) fn compile_worker_system_prompt(
185
185
  // C-1 cr verdict / B2 灵魂件 — identity 必须 FIRST(MUST-4 行为层守:空白上下文问
186
186
  // "你是谁"必须先答 Team Agent worker 身份)。runtime contract 跟后。
187
187
  let send_message = mcp_tool_name(agent.provider, "team_orchestrator", "send_message");
188
- let get_team_status = if agent.provider == Provider::Pi {
189
- r#"mcp({tool:"team_orchestrator_get_team_status", args:{}})"#.to_string()
190
- } else {
191
- format!("{}()", mcp_tool_name(agent.provider, "team_orchestrator", "get_team_status"))
192
- };
193
- let report_result = if agent.provider == Provider::Pi {
194
- r#"mcp({tool:"team_orchestrator_report_result", args:{summary:"..."}})"#.to_string()
195
- } else {
196
- mcp_tool_name(agent.provider, "team_orchestrator", "report_result")
197
- };
198
- let runtime_contract = if agent.provider == Provider::Pi {
199
- pi_runtime_contract_section(&get_team_status)
200
- } else {
201
- runtime_contract_section(&send_message, &report_result, &get_team_status)
202
- };
203
- let communication_contract = if agent.provider == Provider::Pi {
204
- pi_communication_contract(agent.communication_mode)
205
- } else {
206
- agent.communication_mode.runtime_contract(&send_message)
207
- };
188
+ let get_team_status = format!(
189
+ "{}()",
190
+ mcp_tool_name(agent.provider, "team_orchestrator", "get_team_status")
191
+ );
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);
194
+ let communication_contract = agent.communication_mode.runtime_contract(&send_message);
208
195
  let mut chunks = vec![
209
196
  identity_section(agent),
210
197
  runtime_contract,
@@ -224,11 +211,9 @@ pub(crate) fn compile_worker_system_prompt(
224
211
  pub(crate) fn compile_pi_leader_system_prompt() -> String {
225
212
  r#"You are the Team Agent leader for this workspace. Coordinate work through the existing Team Agent MCP server.
226
213
 
227
- Use the Pi MCP proxy call form with server-prefixed tool names, for example:
228
- mcp({tool:"team_orchestrator_get_team_status", args:{}})
229
- mcp({tool:"team_orchestrator_send_message", args:{to:"<agent_id>", content:"..."}})
214
+ The logical operations are send_message, report_result and get_team_status. The Team-owned Pi extension supplies the actual native/proxy tool names in the MCP wire binding before a run. Use that binding, not guessed tool names.
230
215
 
231
- Do not invent a second team protocol or assume that a configured lazy MCP server is connected before a call returns."#
216
+ Do not pass sender, task_id or schema_version. Do not invent a second team protocol or assume that MCP registration proves a call has returned."#
232
217
  .to_string()
233
218
  }
234
219
 
@@ -238,7 +223,8 @@ fn mcp_tool_name(provider: Provider, server: &str, tool: &str) -> String {
238
223
  match provider {
239
224
  Provider::Claude | Provider::ClaudeCode => format!("mcp__{server}__{tool}"),
240
225
  Provider::Grok => format!("{server}__{tool}"),
241
- Provider::Pi => format!("{server}_{tool}"),
226
+ // Pi's owned extension binds these logical operations to the observed MCP call surface.
227
+ Provider::Pi => tool.to_string(),
242
228
  // CursorAgent / Codex / Copilot / GeminiCli / Fake: 未验证,沿用现状点号。
243
229
  // CursorAgent 不可与 grok 同臂:仓库里没有活转录,`{server}__{tool}` 是推断。
244
230
  Provider::CursorAgent
@@ -260,35 +246,6 @@ fn runtime_contract_section(
260
246
  .replace("{get_team_status}", get_team_status)
261
247
  }
262
248
 
263
- fn pi_runtime_contract_section(get_team_status: &str) -> String {
264
- RUNTIME_CONTRACT_SECTION
265
- .replace(
266
- "{send_message}(to='<agent_id>', content='...')",
267
- r#"mcp({tool:"team_orchestrator_send_message", args:{to:"<agent_id>", content:"..."}})"#,
268
- )
269
- .replace(
270
- "{send_message}(to='*', content='...')",
271
- r#"mcp({tool:"team_orchestrator_send_message", args:{to:"*", content:"..."}})"#,
272
- )
273
- .replace(
274
- "{report_result}(summary='...')",
275
- r#"mcp({tool:"team_orchestrator_report_result", args:{summary:"..."}})"#,
276
- )
277
- .replace("{get_team_status}", get_team_status)
278
- }
279
-
280
- fn pi_communication_contract(mode: CommunicationMode) -> String {
281
- match mode {
282
- CommunicationMode::LeaderCentric => r#"# Team Agent communication contract: leader_centric
283
-
284
- - Progress, blockers, questions: mcp({tool:"team_orchestrator_send_message", args:{to:"leader", content:"..."}})
285
-
286
- Respond through Team Agent MCP tools only to actionable requests or questions; writing in your terminal does not deliver it."#
287
- .to_string(),
288
- CommunicationMode::Orchestrated => mode.runtime_contract("mcp"),
289
- }
290
- }
291
-
292
249
  fn communication_mode(value: Option<&str>) -> Result<CommunicationMode, LifecycleError> {
293
250
  let Some(value) = value else {
294
251
  return Ok(CommunicationMode::default());