@team-agent/installer 0.5.68 → 0.5.70
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.
- package/Cargo.lock +1 -1
- package/Cargo.toml +1 -1
- package/crates/team-agent/src/cli/diagnose.rs +74 -0
- package/crates/team-agent/src/cli/emit.rs +5 -4
- package/crates/team-agent/src/cli/leader.rs +1 -0
- package/crates/team-agent/src/cli/spec.rs +1 -0
- package/crates/team-agent/src/compiler.rs +54 -7
- package/crates/team-agent/src/coordinator/runtime_detectors.rs +1 -0
- package/crates/team-agent/src/layout/worker_window_helpers.rs +1 -0
- package/crates/team-agent/src/leader/start.rs +53 -12
- package/crates/team-agent/src/lifecycle/launch/add_agent.rs +67 -15
- package/crates/team-agent/src/lifecycle/launch/add_agent_state.rs +18 -0
- package/crates/team-agent/src/lifecycle/launch/agent_state.rs +1 -0
- package/crates/team-agent/src/lifecycle/launch/cursor_mcp.rs +260 -44
- package/crates/team-agent/src/lifecycle/launch/cursor_mcp_iso.rs +120 -0
- package/crates/team-agent/src/lifecycle/launch/fork_agent.rs +5 -1
- package/crates/team-agent/src/lifecycle/launch/mcp_config.rs +10 -59
- package/crates/team-agent/src/lifecycle/launch/pi_mcp.rs +838 -0
- package/crates/team-agent/src/lifecycle/launch/quick_start_transport.rs +9 -3
- package/crates/team-agent/src/lifecycle/launch/spawn.rs +60 -28
- package/crates/team-agent/src/lifecycle/launch/spec_state.rs +1 -0
- package/crates/team-agent/src/lifecycle/launch.rs +10 -2
- package/crates/team-agent/src/lifecycle/profile_launch.rs +3 -2
- package/crates/team-agent/src/lifecycle/profile_smoke.rs +1 -0
- package/crates/team-agent/src/lifecycle/restart/agent.rs +90 -160
- package/crates/team-agent/src/lifecycle/restart/common.rs +136 -28
- package/crates/team-agent/src/lifecycle/restart/remove.rs +37 -1
- package/crates/team-agent/src/lifecycle/tests/claude_profile_launch_red.rs +24 -3
- package/crates/team-agent/src/lifecycle/tests/cursor_mcp_overlay.rs +195 -18
- package/crates/team-agent/src/lifecycle/tests/cursor_require_explicit_model_red.rs +30 -12
- package/crates/team-agent/src/lifecycle/tests/fixtures/pi_list_models_g0.stdout.txt +222 -0
- package/crates/team-agent/src/lifecycle/tests/g1_silent_faces.rs +5 -2
- package/crates/team-agent/src/lifecycle/tests/grok_mcp_overlay_red.rs +35 -48
- package/crates/team-agent/src/lifecycle/tests/harvest2_a_batch_red.rs +1 -1
- package/crates/team-agent/src/lifecycle/tests/pi_catalog_red.rs +97 -0
- package/crates/team-agent/src/lifecycle/tests/pi_command_red.rs +145 -0
- package/crates/team-agent/src/lifecycle/tests/pi_compiler_red.rs +243 -0
- package/crates/team-agent/src/lifecycle/tests/pi_dual_entry_red.rs +856 -0
- package/crates/team-agent/src/lifecycle/tests/pi_executable_mcp_red.rs +387 -0
- package/crates/team-agent/src/lifecycle/tests/pi_lifecycle_doctor_red.rs +98 -0
- package/crates/team-agent/src/lifecycle/tests/pi_session_delivery_red.rs +216 -0
- package/crates/team-agent/src/lifecycle/tests/restart_rebind_hotfix_252_red.rs +1 -0
- package/crates/team-agent/src/lifecycle/tests.rs +7 -0
- package/crates/team-agent/src/lifecycle/worker_command_context.rs +61 -4
- package/crates/team-agent/src/messaging/delivery.rs +36 -12
- package/crates/team-agent/src/model/enums.rs +3 -1
- package/crates/team-agent/src/model/permissions.rs +1 -1
- package/crates/team-agent/src/provider/adapter.rs +29 -3
- package/crates/team-agent/src/provider/adapters/cursor_agent.rs +5 -2
- package/crates/team-agent/src/provider/adapters/mod.rs +1 -0
- package/crates/team-agent/src/provider/adapters/pi.rs +146 -0
- package/crates/team-agent/src/provider/bypass_flags.rs +1 -0
- package/crates/team-agent/src/provider/classify.rs +2 -2
- package/crates/team-agent/src/provider/faults.rs +1 -1
- package/crates/team-agent/src/provider/session/context_fork.rs +2 -1
- package/crates/team-agent/src/provider/session_scan/common.rs +1 -0
- package/crates/team-agent/src/provider/session_scan/pi.rs +172 -0
- package/crates/team-agent/src/provider/session_scan.rs +4 -0
- package/crates/team-agent/src/provider/tests/adapter.rs +80 -0
- package/crates/team-agent/src/provider/wire.rs +7 -1
- package/crates/team-agent/src/tmux_backend.rs +52 -0
- package/package.json +4 -4
- package/skills/team-agent/SKILL.md +9 -5
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
use std::path::{Path, PathBuf};
|
|
2
|
+
use std::sync::atomic::{AtomicU32, Ordering};
|
|
3
|
+
use std::time::Duration;
|
|
4
|
+
|
|
5
|
+
use crate::lifecycle::launch::pi_mcp::pi_seat_paths;
|
|
6
|
+
use crate::messaging::delivery::{
|
|
7
|
+
paste_to_submit_floor_for_recipient, recipient_allows_explicit_queue_flush, recipient_is_busy,
|
|
8
|
+
recipient_requires_single_enter,
|
|
9
|
+
};
|
|
10
|
+
use crate::model::enums::{Provider, ProviderEffort};
|
|
11
|
+
use crate::provider::adapters::pi::{build_pi_command_argv, PiCommandRequest, PiSessionSelector};
|
|
12
|
+
use crate::provider::session_scan::{scan_session_candidates_once, CaptureSessionContext};
|
|
13
|
+
use crate::provider::{get_adapter, AuthMode, ProviderError, SessionId};
|
|
14
|
+
|
|
15
|
+
static NEXT_ROOT: AtomicU32 = AtomicU32::new(0);
|
|
16
|
+
|
|
17
|
+
fn temp_root(label: &str) -> PathBuf {
|
|
18
|
+
let seq = NEXT_ROOT.fetch_add(1, Ordering::Relaxed);
|
|
19
|
+
let root = std::env::temp_dir().join(format!(
|
|
20
|
+
"team-agent-pi-{label}-{}-{seq}",
|
|
21
|
+
std::process::id()
|
|
22
|
+
));
|
|
23
|
+
std::fs::create_dir_all(&root).expect("create fixture root");
|
|
24
|
+
root
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
fn write_header(path: &Path, id: &str, cwd: &Path, timestamp: &str) {
|
|
28
|
+
std::fs::create_dir_all(path.parent().expect("session parent")).expect("create session parent");
|
|
29
|
+
let header = serde_json::json!({
|
|
30
|
+
"type": "session",
|
|
31
|
+
"version": 3,
|
|
32
|
+
"id": id,
|
|
33
|
+
"cwd": cwd,
|
|
34
|
+
"timestamp": timestamp,
|
|
35
|
+
});
|
|
36
|
+
std::fs::write(
|
|
37
|
+
path,
|
|
38
|
+
format!("{header}\nsecret conversation body must not be read\n"),
|
|
39
|
+
)
|
|
40
|
+
.expect("write session fixture");
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
fn scan_context(cwd: &Path, root: &Path, id: &str, generation: &str) -> CaptureSessionContext {
|
|
44
|
+
CaptureSessionContext {
|
|
45
|
+
agent_id: "worker-a".to_string(),
|
|
46
|
+
spawn_cwd: cwd.to_path_buf(),
|
|
47
|
+
pane_id: None,
|
|
48
|
+
pane_pid: None,
|
|
49
|
+
spawned_at: Some(generation.to_string()),
|
|
50
|
+
expected_session_id: Some(SessionId::new(id)),
|
|
51
|
+
provider_projects_root: Some(root.to_path_buf()),
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
fn resume_argv(path: &Path) -> Result<Vec<String>, ProviderError> {
|
|
56
|
+
build_pi_command_argv(PiCommandRequest {
|
|
57
|
+
executable: Path::new("/verified/pi"),
|
|
58
|
+
extension: Path::new("/workspace/.team/runtime/pi/team-a/worker-a/team-mcp.ts"),
|
|
59
|
+
model: Some("team-agent/qwen3.8-27b"),
|
|
60
|
+
effort: Some(ProviderEffort::Medium),
|
|
61
|
+
system_prompt: "worker contract",
|
|
62
|
+
tool_categories: &["mcp_team"],
|
|
63
|
+
session_dir: Path::new("/workspace/.team/runtime/pi/team-a/worker-a/sessions"),
|
|
64
|
+
session: PiSessionSelector::Resume { path },
|
|
65
|
+
agent_id: "worker-a",
|
|
66
|
+
})
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
#[test]
|
|
70
|
+
fn pi_session_capture_requires_header_id_cwd_generation_and_unique_claim() {
|
|
71
|
+
let root = temp_root("session-capture");
|
|
72
|
+
let cwd = root.join("workspace");
|
|
73
|
+
let sessions = root.join("seat/sessions");
|
|
74
|
+
std::fs::create_dir_all(&cwd).expect("create cwd");
|
|
75
|
+
let id = "652756ea-aaee-4437-a30f-40c5606826d5";
|
|
76
|
+
let generation = "2026-08-29T10:00:00Z";
|
|
77
|
+
let session_timestamp = "2026-08-29T10:00:01Z";
|
|
78
|
+
let expected_path = sessions.join("2026/08/29/session.jsonl");
|
|
79
|
+
write_header(&expected_path, id, &cwd, session_timestamp);
|
|
80
|
+
|
|
81
|
+
let context = scan_context(&cwd, &sessions, id, generation);
|
|
82
|
+
let candidates = scan_session_candidates_once(Provider::Pi, &context).expect("Pi scan");
|
|
83
|
+
assert_eq!(candidates.len(), 1);
|
|
84
|
+
assert_eq!(
|
|
85
|
+
candidates[0]
|
|
86
|
+
.captured
|
|
87
|
+
.session_id
|
|
88
|
+
.as_ref()
|
|
89
|
+
.map(SessionId::as_str),
|
|
90
|
+
Some(id)
|
|
91
|
+
);
|
|
92
|
+
assert_eq!(
|
|
93
|
+
candidates[0]
|
|
94
|
+
.captured
|
|
95
|
+
.rollout_path
|
|
96
|
+
.as_ref()
|
|
97
|
+
.map(|path| path.as_path()),
|
|
98
|
+
Some(expected_path.as_path())
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
for (label, header_id, header_cwd, header_timestamp) in [
|
|
102
|
+
("wrong-id", "wrong-id", cwd.as_path(), session_timestamp),
|
|
103
|
+
("wrong-cwd", id, root.as_path(), session_timestamp),
|
|
104
|
+
(
|
|
105
|
+
"wrong-generation",
|
|
106
|
+
id,
|
|
107
|
+
cwd.as_path(),
|
|
108
|
+
"2026-08-29T09:00:00+00:00",
|
|
109
|
+
),
|
|
110
|
+
] {
|
|
111
|
+
let case_root = root.join(label);
|
|
112
|
+
let case_path = case_root.join(format!("{id}.jsonl"));
|
|
113
|
+
write_header(&case_path, header_id, header_cwd, header_timestamp);
|
|
114
|
+
let case_context = scan_context(&cwd, &case_root, id, generation);
|
|
115
|
+
assert!(
|
|
116
|
+
scan_session_candidates_once(Provider::Pi, &case_context)
|
|
117
|
+
.expect("negative scan")
|
|
118
|
+
.is_empty(),
|
|
119
|
+
"filename or mtime must not override a mismatched header: {label}"
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
let second = sessions.join("duplicate/session.jsonl");
|
|
124
|
+
write_header(&second, id, &cwd, session_timestamp);
|
|
125
|
+
assert_eq!(
|
|
126
|
+
scan_session_candidates_once(Provider::Pi, &context)
|
|
127
|
+
.expect("ambiguous scan")
|
|
128
|
+
.len(),
|
|
129
|
+
2,
|
|
130
|
+
"multiple exact candidates must remain ambiguous for the shared allocator"
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
std::fs::remove_dir_all(root).expect("remove session fixture");
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
#[test]
|
|
137
|
+
fn pi_restart_uses_exact_path_and_refuses_missing_backing() {
|
|
138
|
+
let root = temp_root("resume");
|
|
139
|
+
let cwd = root.join("workspace");
|
|
140
|
+
let sessions = root.join("seat/sessions");
|
|
141
|
+
std::fs::create_dir_all(&cwd).expect("create cwd");
|
|
142
|
+
let id = "cc09d133-878f-4241-920f-f7ebfefc2a9e";
|
|
143
|
+
let generation = "2026-08-29T10:00:00Z";
|
|
144
|
+
let exact = sessions.join("nested/exact.jsonl");
|
|
145
|
+
write_header(&exact, id, &cwd, "2026-08-29T10:00:01Z");
|
|
146
|
+
|
|
147
|
+
let candidates =
|
|
148
|
+
scan_session_candidates_once(Provider::Pi, &scan_context(&cwd, &sessions, id, generation))
|
|
149
|
+
.expect("resume preflight scan");
|
|
150
|
+
assert_eq!(
|
|
151
|
+
candidates.len(),
|
|
152
|
+
1,
|
|
153
|
+
"exact backing must revalidate before restart"
|
|
154
|
+
);
|
|
155
|
+
|
|
156
|
+
let argv = resume_argv(&exact).expect("resume exact path");
|
|
157
|
+
assert!(argv
|
|
158
|
+
.windows(2)
|
|
159
|
+
.any(|pair| { pair[0] == "--session" && pair[1] == exact.to_string_lossy().as_ref() }));
|
|
160
|
+
assert!(!argv.iter().any(|arg| {
|
|
161
|
+
matches!(
|
|
162
|
+
arg.as_str(),
|
|
163
|
+
"--session-id" | "-c" | "--continue" | "-r" | "--resume"
|
|
164
|
+
)
|
|
165
|
+
}));
|
|
166
|
+
|
|
167
|
+
std::fs::remove_file(&exact).expect("remove backing tooth");
|
|
168
|
+
let missing = resume_argv(&exact);
|
|
169
|
+
assert!(
|
|
170
|
+
matches!(missing, Err(ProviderError::ResumeUnavailable(_))),
|
|
171
|
+
"missing exact backing must refuse rather than create a new session: {missing:?}"
|
|
172
|
+
);
|
|
173
|
+
std::fs::remove_dir_all(root).expect("remove resume fixture");
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
#[test]
|
|
177
|
+
fn pi_delivery_submits_one_enter_and_never_flushes_or_retries() {
|
|
178
|
+
let idle = serde_json::json!({
|
|
179
|
+
"agents": {
|
|
180
|
+
"worker-a": {"provider": "pi", "status": "running"}
|
|
181
|
+
}
|
|
182
|
+
});
|
|
183
|
+
assert!(recipient_requires_single_enter(&idle, "worker-a"));
|
|
184
|
+
assert!(!recipient_allows_explicit_queue_flush(&idle, "worker-a"));
|
|
185
|
+
assert_eq!(
|
|
186
|
+
paste_to_submit_floor_for_recipient(&idle, "worker-a"),
|
|
187
|
+
Duration::ZERO
|
|
188
|
+
);
|
|
189
|
+
assert!(!recipient_is_busy(&idle, "worker-a"));
|
|
190
|
+
|
|
191
|
+
let busy = serde_json::json!({
|
|
192
|
+
"agents": {
|
|
193
|
+
"worker-a": {"provider": "pi", "status": "busy"}
|
|
194
|
+
}
|
|
195
|
+
});
|
|
196
|
+
assert!(
|
|
197
|
+
recipient_is_busy(&busy, "worker-a"),
|
|
198
|
+
"known busy must take the existing send.deferred_busy path before paste"
|
|
199
|
+
);
|
|
200
|
+
assert!(recipient_requires_single_enter(&busy, "worker-a"));
|
|
201
|
+
assert!(!recipient_allows_explicit_queue_flush(&busy, "worker-a"));
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
#[test]
|
|
205
|
+
fn pi_clone_is_fresh_and_fork_is_not_clone() {
|
|
206
|
+
let workspace = Path::new("/workspace");
|
|
207
|
+
let source_paths = pi_seat_paths(workspace, "team-a", "source");
|
|
208
|
+
let clone_paths = pi_seat_paths(workspace, "team-a", "clone");
|
|
209
|
+
assert_ne!(source_paths.runtime_root, clone_paths.runtime_root);
|
|
210
|
+
assert_ne!(source_paths.wrapper, clone_paths.wrapper);
|
|
211
|
+
assert_ne!(source_paths.sessions, clone_paths.sessions);
|
|
212
|
+
|
|
213
|
+
let source_session = SessionId::new("652756ea-aaee-4437-a30f-40c5606826d5");
|
|
214
|
+
let fork = get_adapter(Provider::Pi).fork(Some(&source_session), AuthMode::Subscription, None);
|
|
215
|
+
assert!(matches!(fork, Err(ProviderError::CapabilityUnsupported(_))));
|
|
216
|
+
}
|
|
@@ -79,6 +79,13 @@ mod main_preserved;
|
|
|
79
79
|
mod mcp_tool_name_format_red;
|
|
80
80
|
mod phase_b_contracts;
|
|
81
81
|
mod phase_golden;
|
|
82
|
+
mod pi_catalog_red;
|
|
83
|
+
mod pi_command_red;
|
|
84
|
+
mod pi_compiler_red;
|
|
85
|
+
mod pi_dual_entry_red;
|
|
86
|
+
mod pi_executable_mcp_red;
|
|
87
|
+
mod pi_lifecycle_doctor_red;
|
|
88
|
+
mod pi_session_delivery_red;
|
|
82
89
|
mod quick_start_worker_readiness_red;
|
|
83
90
|
mod realmachine_clusters_1_6_red;
|
|
84
91
|
mod realmachine_residual_g1_g4_red;
|
|
@@ -209,11 +209,25 @@ pub(crate) fn compile_worker_system_prompt(
|
|
|
209
209
|
// C-1 cr verdict / B2 灵魂件 — identity 必须 FIRST(MUST-4 行为层守:空白上下文问
|
|
210
210
|
// "你是谁"必须先答 Team Agent worker 身份)。runtime contract 跟后。
|
|
211
211
|
let send_message = mcp_tool_name(agent.provider, "team_orchestrator", "send_message");
|
|
212
|
-
let report_result =
|
|
212
|
+
let report_result = if agent.provider == Provider::Pi {
|
|
213
|
+
r#"mcp({tool:"team_orchestrator_report_result", args:{summary:"..."}})"#.to_string()
|
|
214
|
+
} else {
|
|
215
|
+
mcp_tool_name(agent.provider, "team_orchestrator", "report_result")
|
|
216
|
+
};
|
|
217
|
+
let runtime_contract = if agent.provider == Provider::Pi {
|
|
218
|
+
pi_runtime_contract_section()
|
|
219
|
+
} else {
|
|
220
|
+
runtime_contract_section(&send_message, &report_result)
|
|
221
|
+
};
|
|
222
|
+
let communication_contract = if agent.provider == Provider::Pi {
|
|
223
|
+
pi_communication_contract(agent.communication_mode)
|
|
224
|
+
} else {
|
|
225
|
+
agent.communication_mode.runtime_contract(&send_message)
|
|
226
|
+
};
|
|
213
227
|
let mut chunks = vec![
|
|
214
228
|
identity_section(agent),
|
|
215
|
-
|
|
216
|
-
|
|
229
|
+
runtime_contract,
|
|
230
|
+
communication_contract,
|
|
217
231
|
role_body(agent)?,
|
|
218
232
|
];
|
|
219
233
|
if let Some(contract) = output_contract(agent, &report_result) {
|
|
@@ -229,6 +243,17 @@ pub(crate) fn compile_worker_system_prompt(
|
|
|
229
243
|
.join("\n\n"))
|
|
230
244
|
}
|
|
231
245
|
|
|
246
|
+
pub(crate) fn compile_pi_leader_system_prompt() -> String {
|
|
247
|
+
r#"You are the Team Agent leader for this workspace. Coordinate work through the existing Team Agent MCP server.
|
|
248
|
+
|
|
249
|
+
Use the Pi MCP proxy call form with server-prefixed tool names, for example:
|
|
250
|
+
mcp({tool:"team_orchestrator_get_team_status", args:{}})
|
|
251
|
+
mcp({tool:"team_orchestrator_send_message", args:{to:"<agent_id>", content:"..."}})
|
|
252
|
+
|
|
253
|
+
Do not invent a second team protocol or assume that a configured lazy MCP server is connected before a call returns."#
|
|
254
|
+
.to_string()
|
|
255
|
+
}
|
|
256
|
+
|
|
232
257
|
/// ---
|
|
233
258
|
/// purpose: 解析该 agent 最终生效的工具串
|
|
234
259
|
/// params:
|
|
@@ -249,7 +274,7 @@ pub(crate) fn resolved_tool_strings_for_command(
|
|
|
249
274
|
// 不再收 team/runtime/leader argv 派生的 `DangerousApproval`。true → 加
|
|
250
275
|
// `dangerous_auto_approve` 哨兵(adapter 消费点不变);provider 无 bypass
|
|
251
276
|
// argv 定义 → fail-loud,不静默 fallback。
|
|
252
|
-
if agent.dangerously_skip_permissions {
|
|
277
|
+
if agent.dangerously_skip_permissions && provider != Provider::Pi {
|
|
253
278
|
let flag = crate::provider::bypass_flags::provider_bypass_flag(provider).ok_or_else(
|
|
254
279
|
|| {
|
|
255
280
|
LifecycleError::RequirementUnmet(format!(
|
|
@@ -272,6 +297,7 @@ fn mcp_tool_name(provider: Provider, server: &str, tool: &str) -> String {
|
|
|
272
297
|
match provider {
|
|
273
298
|
Provider::Claude | Provider::ClaudeCode => format!("mcp__{server}__{tool}"),
|
|
274
299
|
Provider::Grok => format!("{server}__{tool}"),
|
|
300
|
+
Provider::Pi => format!("{server}_{tool}"),
|
|
275
301
|
// CursorAgent / Codex / Copilot / GeminiCli / Fake: 未验证,沿用现状点号。
|
|
276
302
|
// CursorAgent 不可与 grok 同臂:仓库里没有活转录,`{server}__{tool}` 是推断。
|
|
277
303
|
Provider::CursorAgent
|
|
@@ -291,6 +317,7 @@ fn provider_display_name(provider: Provider) -> &'static str {
|
|
|
291
317
|
Provider::GeminiCli => "gemini_cli",
|
|
292
318
|
Provider::Grok => "grok",
|
|
293
319
|
Provider::CursorAgent => "cursor_agent",
|
|
320
|
+
Provider::Pi => "pi",
|
|
294
321
|
Provider::Fake => "fake",
|
|
295
322
|
}
|
|
296
323
|
}
|
|
@@ -314,6 +341,36 @@ fn runtime_contract_section(send_message: &str, report_result: &str) -> String {
|
|
|
314
341
|
.replace("{report_result}", report_result)
|
|
315
342
|
}
|
|
316
343
|
|
|
344
|
+
fn pi_runtime_contract_section() -> String {
|
|
345
|
+
RUNTIME_CONTRACT_SECTION
|
|
346
|
+
.replace(
|
|
347
|
+
"{send_message}(to='<agent_id>', content='...')",
|
|
348
|
+
r#"mcp({tool:"team_orchestrator_send_message", args:{to:"<agent_id>", content:"..."}})"#,
|
|
349
|
+
)
|
|
350
|
+
.replace(
|
|
351
|
+
"{send_message}(to='*', content='...')",
|
|
352
|
+
r#"mcp({tool:"team_orchestrator_send_message", args:{to:"*", content:"..."}})"#,
|
|
353
|
+
)
|
|
354
|
+
.replace(
|
|
355
|
+
"{report_result}(summary='...')",
|
|
356
|
+
r#"mcp({tool:"team_orchestrator_report_result", args:{summary:"..."}})"#,
|
|
357
|
+
)
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
fn pi_communication_contract(mode: CommunicationMode) -> String {
|
|
361
|
+
match mode {
|
|
362
|
+
CommunicationMode::LeaderCentric => r#"# Team Agent communication contract: leader_centric
|
|
363
|
+
|
|
364
|
+
- Progress, blockers, questions: mcp({tool:"team_orchestrator_send_message", args:{to:"leader", content:"..."}})
|
|
365
|
+
|
|
366
|
+
When you receive a message from the leader or a teammate, you MUST respond
|
|
367
|
+
through MCP tools. Writing a reply in your terminal does nothing — the sender
|
|
368
|
+
will never see it."#
|
|
369
|
+
.to_string(),
|
|
370
|
+
CommunicationMode::Orchestrated => mode.runtime_contract("mcp"),
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
317
374
|
fn communication_mode(value: Option<&str>) -> Result<CommunicationMode, LifecycleError> {
|
|
318
375
|
let Some(value) = value else {
|
|
319
376
|
return Ok(CommunicationMode::default());
|
|
@@ -572,11 +572,11 @@ pub fn deliver_pending_message(
|
|
|
572
572
|
canonical_owner_team_id.as_deref(),
|
|
573
573
|
resolved.metadata.as_ref(),
|
|
574
574
|
);
|
|
575
|
-
let
|
|
575
|
+
let recipient_single_enter = recipient_requires_single_enter(state, &message.recipient);
|
|
576
576
|
let inject_attempt = crate::tmux_backend::with_paste_to_submit_floor(
|
|
577
577
|
paste_to_submit_floor_for_recipient(state, &message.recipient),
|
|
578
578
|
|| {
|
|
579
|
-
crate::tmux_backend::with_cursor_single_enter(
|
|
579
|
+
crate::tmux_backend::with_cursor_single_enter(recipient_single_enter, || {
|
|
580
580
|
transport.inject_with_submit_observer(
|
|
581
581
|
&target,
|
|
582
582
|
&payload,
|
|
@@ -592,7 +592,9 @@ pub fn deliver_pending_message(
|
|
|
592
592
|
// grok 忙时第一次回车只入显式队列。默认 send-now:只重按回车顶出去。
|
|
593
593
|
// 无 `Enter:send now` 时零次额外按键,claude/codex 行为不变。
|
|
594
594
|
// cursor:第二下 Enter 打断进行中回合,不走 flush。
|
|
595
|
-
if !crate::provider::submit_now::keep_provider_queue_requested()
|
|
595
|
+
if !crate::provider::submit_now::keep_provider_queue_requested()
|
|
596
|
+
&& recipient_allows_explicit_queue_flush(state, &message.recipient)
|
|
597
|
+
{
|
|
596
598
|
match crate::provider::submit_now::flush_explicit_queue(transport, &target) {
|
|
597
599
|
Ok(flush) if flush.extra_enters > 0 => {
|
|
598
600
|
let _ = event_log.write(
|
|
@@ -2221,7 +2223,11 @@ fn recipient_pane_has_actionable_startup_prompt(
|
|
|
2221
2223
|
crate::provider::classify_copilot_startup_screen(&captured),
|
|
2222
2224
|
crate::provider::StartupScreenDecision::AnswerWorkspaceTrust
|
|
2223
2225
|
),
|
|
2224
|
-
Provider::
|
|
2226
|
+
Provider::Pi
|
|
2227
|
+
| Provider::Grok
|
|
2228
|
+
| Provider::CursorAgent
|
|
2229
|
+
| Provider::GeminiCli
|
|
2230
|
+
| Provider::Fake => false,
|
|
2225
2231
|
}
|
|
2226
2232
|
}
|
|
2227
2233
|
|
|
@@ -2235,13 +2241,6 @@ fn recipient_provider(state: &serde_json::Value, recipient: &str) -> Option<Prov
|
|
|
2235
2241
|
.and_then(parse_canonical_provider)
|
|
2236
2242
|
}
|
|
2237
2243
|
|
|
2238
|
-
fn recipient_is_cursor_agent(state: &serde_json::Value, recipient: &str) -> bool {
|
|
2239
|
-
matches!(
|
|
2240
|
-
recipient_provider(state, recipient),
|
|
2241
|
-
Some(Provider::CursorAgent)
|
|
2242
|
-
)
|
|
2243
|
-
}
|
|
2244
|
-
|
|
2245
2244
|
/// ---
|
|
2246
2245
|
/// purpose: paste→Enter 地板按收件人 provider 选择
|
|
2247
2246
|
/// contract: CursorAgent 与 Grok 为 1s;其余(含 claude)为 ZERO
|
|
@@ -2259,7 +2258,32 @@ pub(crate) fn paste_to_submit_floor_for_recipient(
|
|
|
2259
2258
|
}
|
|
2260
2259
|
}
|
|
2261
2260
|
|
|
2262
|
-
|
|
2261
|
+
/// ---
|
|
2262
|
+
/// purpose: 判断收件 provider 是否必须禁止 submit observer 重按 Enter
|
|
2263
|
+
/// returns: CursorAgent 与 Pi 为 true,其余为 false
|
|
2264
|
+
/// ---
|
|
2265
|
+
pub(crate) fn recipient_requires_single_enter(state: &serde_json::Value, recipient: &str) -> bool {
|
|
2266
|
+
matches!(
|
|
2267
|
+
recipient_provider(state, recipient),
|
|
2268
|
+
Some(Provider::CursorAgent | Provider::Pi)
|
|
2269
|
+
)
|
|
2270
|
+
}
|
|
2271
|
+
|
|
2272
|
+
/// ---
|
|
2273
|
+
/// purpose: 判断收件 provider 是否允许 Grok send-now 显式队列 flush
|
|
2274
|
+
/// returns: CursorAgent 与 Pi 为 false,其余为 true
|
|
2275
|
+
/// ---
|
|
2276
|
+
pub(crate) fn recipient_allows_explicit_queue_flush(
|
|
2277
|
+
state: &serde_json::Value,
|
|
2278
|
+
recipient: &str,
|
|
2279
|
+
) -> bool {
|
|
2280
|
+
!matches!(
|
|
2281
|
+
recipient_provider(state, recipient),
|
|
2282
|
+
Some(Provider::CursorAgent | Provider::Pi)
|
|
2283
|
+
)
|
|
2284
|
+
}
|
|
2285
|
+
|
|
2286
|
+
pub(crate) fn recipient_is_busy(state: &serde_json::Value, recipient: &str) -> bool {
|
|
2263
2287
|
state
|
|
2264
2288
|
.get("agents")
|
|
2265
2289
|
.and_then(serde_json::Value::as_object)
|
|
@@ -27,6 +27,8 @@ pub enum Provider {
|
|
|
27
27
|
/// Cursor `agent` CLI (0.5.67 provider-adapter). Append system prompt via
|
|
28
28
|
/// workspace `.cursor/rules/*.mdc` (方案 1 变体), bypass `--force`.
|
|
29
29
|
CursorAgent,
|
|
30
|
+
/// Pi coding agent CLI. Models are selected from its live catalog.
|
|
31
|
+
Pi,
|
|
30
32
|
Fake,
|
|
31
33
|
}
|
|
32
34
|
|
|
@@ -89,7 +91,7 @@ impl ProviderEffort {
|
|
|
89
91
|
/// Copilot/Gemini/CursorAgent/Fake ignore the field (warning at runtime).
|
|
90
92
|
pub fn is_supported_by(self, provider: Provider) -> bool {
|
|
91
93
|
match provider {
|
|
92
|
-
Provider::Claude | Provider::ClaudeCode => true,
|
|
94
|
+
Provider::Claude | Provider::ClaudeCode | Provider::Pi => true,
|
|
93
95
|
Provider::Codex | Provider::Grok => !self.is_claude_only(),
|
|
94
96
|
Provider::Copilot | Provider::GeminiCli | Provider::CursorAgent | Provider::Fake => {
|
|
95
97
|
false
|
|
@@ -191,7 +191,7 @@ fn provider_enforcement(provider: Provider, tool: Tool) -> Enforcement {
|
|
|
191
191
|
Provider::Fake => Hard,
|
|
192
192
|
// 0.5.67: grok/cursor_agent 无 deny-kind 概念 (同 codex 精神), 诚实
|
|
193
193
|
// prompt_only, 不替 provider 决。
|
|
194
|
-
Provider::Grok | Provider::CursorAgent => PromptOnly,
|
|
194
|
+
Provider::Grok | Provider::CursorAgent | Provider::Pi => PromptOnly,
|
|
195
195
|
// Copilot(C-2-1 cr verdict):execute_bash/fs_write/network/mcp_team = hard,
|
|
196
196
|
// fs_read/fs_list/git_diff/provider_builtin = prompt_only(诚实:copilot 无
|
|
197
197
|
// 对应 deny kind,framework 不替决,留给 provider prompt 控制;MUST-NOT-13)。
|
|
@@ -358,6 +358,12 @@ impl ProviderAdapter for BasicProviderAdapter {
|
|
|
358
358
|
native_mcp_config: false,
|
|
359
359
|
writes_global_settings: false,
|
|
360
360
|
},
|
|
361
|
+
Provider::Pi => ProviderCaps {
|
|
362
|
+
resume: true,
|
|
363
|
+
fork: false,
|
|
364
|
+
native_mcp_config: false,
|
|
365
|
+
writes_global_settings: false,
|
|
366
|
+
},
|
|
361
367
|
Provider::Fake => ProviderCaps {
|
|
362
368
|
resume: false,
|
|
363
369
|
fork: false,
|
|
@@ -408,6 +414,7 @@ impl ProviderAdapter for BasicProviderAdapter {
|
|
|
408
414
|
// Cursor: 登录探测未接。U-17 前不得把未观测写成 Present(shim 对
|
|
409
415
|
// `cursor-agent status` 会假绿 Logged in)。缺键 ≠ 已知未登录,故不用 Missing。
|
|
410
416
|
Provider::CursorAgent => AuthHintStatus::Unknown,
|
|
417
|
+
Provider::Pi => AuthHintStatus::Unknown,
|
|
411
418
|
_ => match auth_mode {
|
|
412
419
|
AuthMode::Subscription => AuthHintStatus::Present,
|
|
413
420
|
AuthMode::OfficialApi | AuthMode::CompatibleApi => AuthHintStatus::MissingOrUnknown,
|
|
@@ -491,6 +498,9 @@ impl ProviderAdapter for BasicProviderAdapter {
|
|
|
491
498
|
false,
|
|
492
499
|
None,
|
|
493
500
|
)?),
|
|
501
|
+
Provider::Pi => Err(ProviderError::Command(
|
|
502
|
+
"Pi commands require the shared lifecycle materializer".to_string(),
|
|
503
|
+
)),
|
|
494
504
|
Provider::Fake => Ok(fake_worker_command()),
|
|
495
505
|
}
|
|
496
506
|
}
|
|
@@ -645,6 +655,9 @@ impl ProviderAdapter for BasicProviderAdapter {
|
|
|
645
655
|
managed_mcp_config: false,
|
|
646
656
|
})
|
|
647
657
|
}
|
|
658
|
+
Provider::Pi => Err(ProviderError::Command(
|
|
659
|
+
"Pi commands require the shared lifecycle materializer".to_string(),
|
|
660
|
+
)),
|
|
648
661
|
_ => self
|
|
649
662
|
.build_command_with_tools(
|
|
650
663
|
ctx.auth_mode,
|
|
@@ -843,6 +856,9 @@ impl ProviderAdapter for BasicProviderAdapter {
|
|
|
843
856
|
argv.insert(2, session_id.as_str().to_string());
|
|
844
857
|
Ok(argv)
|
|
845
858
|
}
|
|
859
|
+
Provider::Pi => Err(ProviderError::ResumeUnavailable(
|
|
860
|
+
"pi resume requires the captured exact session path".to_string(),
|
|
861
|
+
)),
|
|
846
862
|
Provider::GeminiCli | Provider::Fake => Err(ProviderError::ResumeUnavailable(format!(
|
|
847
863
|
"{} resume requires session_id",
|
|
848
864
|
provider_wire(self.provider)
|
|
@@ -1068,7 +1084,7 @@ impl ProviderAdapter for BasicProviderAdapter {
|
|
|
1068
1084
|
argv.push("--fork-session".to_string());
|
|
1069
1085
|
Ok(argv)
|
|
1070
1086
|
}
|
|
1071
|
-
Provider::CursorAgent | Provider::GeminiCli | Provider::Fake => {
|
|
1087
|
+
Provider::Pi | Provider::CursorAgent | Provider::GeminiCli | Provider::Fake => {
|
|
1072
1088
|
Err(ProviderError::CapabilityUnsupported(format!(
|
|
1073
1089
|
"{} does not support native session fork",
|
|
1074
1090
|
provider_wire(self.provider)
|
|
@@ -1271,6 +1287,12 @@ impl ProviderAdapter for BasicProviderAdapter {
|
|
|
1271
1287
|
r"working|processing",
|
|
1272
1288
|
r"Error|panic",
|
|
1273
1289
|
),
|
|
1290
|
+
// Pi: 无已验证 turn-state 检测;占位串不会匹配真实 pane chrome。
|
|
1291
|
+
Provider::Pi => patterns(
|
|
1292
|
+
r"pi-idle-unset-until-turn-state",
|
|
1293
|
+
r"pi-busy-unset-until-turn-state",
|
|
1294
|
+
r"pi-error-unset-until-turn-state",
|
|
1295
|
+
),
|
|
1274
1296
|
// Grok: 暂无专有 status 正则, 沿用 generic。
|
|
1275
1297
|
Provider::Grok | Provider::GeminiCli | Provider::Fake => {
|
|
1276
1298
|
patterns(r">", r"working|processing", r"Error|Traceback")
|
|
@@ -1285,8 +1307,12 @@ impl ProviderAdapter for BasicProviderAdapter {
|
|
|
1285
1307
|
}
|
|
1286
1308
|
}
|
|
1287
1309
|
|
|
1288
|
-
fn validate_model(&self,
|
|
1289
|
-
|
|
1310
|
+
fn validate_model(&self, model: &str) -> Result<bool, ProviderError> {
|
|
1311
|
+
if self.provider != Provider::Pi {
|
|
1312
|
+
return Ok(true);
|
|
1313
|
+
}
|
|
1314
|
+
let (_, catalog) = crate::lifecycle::launch::pi_mcp::resolve_pi_executable_chain()?;
|
|
1315
|
+
Ok(crate::lifecycle::launch::pi_mcp::select_exact_pi_model(&catalog, model).is_ok())
|
|
1290
1316
|
}
|
|
1291
1317
|
}
|
|
1292
1318
|
|
|
@@ -15,8 +15,10 @@
|
|
|
15
15
|
//! `.team/scripts/cursor_seat.sh` 实测一致:
|
|
16
16
|
//! `--trust --sandbox disabled --workspace <物理路径> [--force] [--model]`
|
|
17
17
|
//! Role 不入 argv,写 `<workspace>/.cursor/rules/*.mdc` + `alwaysApply: true`。
|
|
18
|
-
//! MCP 无 `--mcp-config`;身份必须写进
|
|
18
|
+
//! MCP 无 `--mcp-config`;身份必须写进 per-seat
|
|
19
|
+
//! `provider-config/<id>/cursor/.cursor/mcp.json` 的 env 表
|
|
19
20
|
//! (cursor 不把父进程 TEAM_AGENT_* 传给 MCP 子进程)。
|
|
21
|
+
//! spawn 把 `--workspace` 指到该工程根,并用 `--add-dir` 挂上真 workspace。
|
|
20
22
|
//!
|
|
21
23
|
//! 隐藏 flag `--system-prompt` / `--allowed-tools` 实测存在但 help 未列,
|
|
22
24
|
//! 随时可能消失。可以在注释里记录,代码里不许调。
|
|
@@ -73,7 +75,8 @@ pub(crate) fn cursor_agent_base_command(
|
|
|
73
75
|
let _ = effort;
|
|
74
76
|
// system_prompt 不入 argv(help 无 --rules / --append-system-prompt)。
|
|
75
77
|
// launch 写 `<workspace>/.cursor/rules/team-agent-role-<agent_id>.mdc`。
|
|
76
|
-
// mcp_config 也不入 argv(无 --mcp-config)。launch 写
|
|
78
|
+
// mcp_config 也不入 argv(无 --mcp-config)。launch 写
|
|
79
|
+
// `.team/runtime/provider-config/<id>/cursor/.cursor/mcp.json`
|
|
77
80
|
// 并 `agent mcp enable team_orchestrator`。这里收下以免静默丢弃。
|
|
78
81
|
let _ = (adapter, auth_mode, mcp_config, managed_mcp_config, system_prompt);
|
|
79
82
|
argv.push("--workspace".to_string());
|