@team-agent/installer 0.5.68 → 0.5.69
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 +62 -5
- 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 +830 -0
- package/crates/team-agent/src/lifecycle/launch/quick_start_transport.rs +9 -3
- package/crates/team-agent/src/lifecycle/launch/spawn.rs +70 -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 +146 -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 +128 -0
- package/crates/team-agent/src/lifecycle/tests/pi_compiler_red.rs +207 -0
- package/crates/team-agent/src/lifecycle/tests/pi_dual_entry_red.rs +838 -0
- package/crates/team-agent/src/lifecycle/tests/pi_executable_mcp_red.rs +320 -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 +152 -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 +2 -4
|
@@ -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());
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
//! ---
|
|
2
|
+
//! purpose: Pi CLI 的纯工具映射与 fresh/resume argv 构造
|
|
3
|
+
//! contract:
|
|
4
|
+
//! provides:
|
|
5
|
+
//! - name: build_pi_command_argv
|
|
6
|
+
//! what: 仅从已验证输入构造隔离的 regular-TUI Pi argv
|
|
7
|
+
//! boundary:
|
|
8
|
+
//! - 不发现 executable、catalog、adapter 或 MCP 配置
|
|
9
|
+
//! - 除 resume exact backing 存在性外不读文件;不写文件、不启动进程
|
|
10
|
+
//! maturity: wired
|
|
11
|
+
//! ---
|
|
12
|
+
|
|
13
|
+
use std::collections::BTreeSet;
|
|
14
|
+
use std::path::Path;
|
|
15
|
+
|
|
16
|
+
use crate::model::enums::ProviderEffort;
|
|
17
|
+
use crate::provider::ProviderError;
|
|
18
|
+
|
|
19
|
+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
20
|
+
pub(crate) enum PiToolMapping {
|
|
21
|
+
Mcp,
|
|
22
|
+
Builtin(&'static [&'static str]),
|
|
23
|
+
Unsupported,
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/// ---
|
|
27
|
+
/// purpose: 把 Team tool category 映射为 exact Pi builtin/proxy names
|
|
28
|
+
/// returns: 单个或多个 builtin、MCP proxy 或 Unsupported
|
|
29
|
+
/// ---
|
|
30
|
+
pub(crate) fn pi_tool_mapping(category: &str) -> PiToolMapping {
|
|
31
|
+
match category {
|
|
32
|
+
"mcp_team" => PiToolMapping::Mcp,
|
|
33
|
+
"fs_read" => PiToolMapping::Builtin(&["read"]),
|
|
34
|
+
"fs_list" => PiToolMapping::Builtin(&["grep", "find", "ls"]),
|
|
35
|
+
"fs_write" => PiToolMapping::Builtin(&["edit", "write"]),
|
|
36
|
+
"execute_bash" => PiToolMapping::Builtin(&["bash"]),
|
|
37
|
+
_ => PiToolMapping::Unsupported,
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
#[derive(Debug, Clone, Copy)]
|
|
42
|
+
pub(crate) enum PiSessionSelector<'a> {
|
|
43
|
+
Fresh { session_id: &'a str },
|
|
44
|
+
Resume { path: &'a Path },
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
pub(crate) struct PiCommandRequest<'a> {
|
|
48
|
+
pub executable: &'a Path,
|
|
49
|
+
pub extension: &'a Path,
|
|
50
|
+
pub model: &'a str,
|
|
51
|
+
pub effort: ProviderEffort,
|
|
52
|
+
pub system_prompt: &'a str,
|
|
53
|
+
pub tool_categories: &'a [&'a str],
|
|
54
|
+
pub session_dir: &'a Path,
|
|
55
|
+
pub session: PiSessionSelector<'a>,
|
|
56
|
+
pub agent_id: &'a str,
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/// ---
|
|
60
|
+
/// purpose: 从已验证 semantic request 构造无 ambient flag 的 Pi argv
|
|
61
|
+
/// returns: exact ordered fresh/resume argv
|
|
62
|
+
/// errors: 必需字段、mcp_team 或工具 category 非法时返回 ProviderError
|
|
63
|
+
/// ---
|
|
64
|
+
pub(crate) fn build_pi_command_argv(
|
|
65
|
+
request: PiCommandRequest<'_>,
|
|
66
|
+
) -> Result<Vec<String>, ProviderError> {
|
|
67
|
+
if request.executable.as_os_str().is_empty()
|
|
68
|
+
|| request.extension.as_os_str().is_empty()
|
|
69
|
+
|| request.session_dir.as_os_str().is_empty()
|
|
70
|
+
|| request.model.trim().is_empty()
|
|
71
|
+
|| request.system_prompt.trim().is_empty()
|
|
72
|
+
|| request.agent_id.trim().is_empty()
|
|
73
|
+
{
|
|
74
|
+
return Err(ProviderError::Command(
|
|
75
|
+
"Pi command requires executable, extension, model, prompt, session directory, and agent id"
|
|
76
|
+
.to_string(),
|
|
77
|
+
));
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
let mut tools = BTreeSet::new();
|
|
81
|
+
for category in request.tool_categories {
|
|
82
|
+
match pi_tool_mapping(category) {
|
|
83
|
+
PiToolMapping::Mcp => {
|
|
84
|
+
tools.insert("mcp");
|
|
85
|
+
}
|
|
86
|
+
PiToolMapping::Builtin(names) => tools.extend(names.iter().copied()),
|
|
87
|
+
PiToolMapping::Unsupported => {
|
|
88
|
+
return Err(ProviderError::Command(format!(
|
|
89
|
+
"Pi does not support Team Agent tool category {category:?}"
|
|
90
|
+
)));
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
if !tools.contains("mcp") {
|
|
95
|
+
return Err(ProviderError::Command(
|
|
96
|
+
"Pi command requires mcp_team".to_string(),
|
|
97
|
+
));
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
let mut argv = vec![
|
|
101
|
+
request.executable.to_string_lossy().into_owned(),
|
|
102
|
+
"--no-extensions".to_string(),
|
|
103
|
+
"-e".to_string(),
|
|
104
|
+
request.extension.to_string_lossy().into_owned(),
|
|
105
|
+
"--no-approve".to_string(),
|
|
106
|
+
"--no-context-files".to_string(),
|
|
107
|
+
"--no-skills".to_string(),
|
|
108
|
+
"--no-prompt-templates".to_string(),
|
|
109
|
+
"--tui-mode".to_string(),
|
|
110
|
+
"regular".to_string(),
|
|
111
|
+
"--model".to_string(),
|
|
112
|
+
request.model.to_string(),
|
|
113
|
+
"--thinking".to_string(),
|
|
114
|
+
request.effort.as_str().to_string(),
|
|
115
|
+
"--system-prompt".to_string(),
|
|
116
|
+
request.system_prompt.to_string(),
|
|
117
|
+
"--tools".to_string(),
|
|
118
|
+
tools.into_iter().collect::<Vec<_>>().join(","),
|
|
119
|
+
"--session-dir".to_string(),
|
|
120
|
+
request.session_dir.to_string_lossy().into_owned(),
|
|
121
|
+
];
|
|
122
|
+
// The shared lifecycle materializer always revalidates resume root/path/header.
|
|
123
|
+
// Keep the pure argv builder usable before a fixture root is materialized, while
|
|
124
|
+
// refusing a missing exact file once the recorded path's parent exists.
|
|
125
|
+
match request.session {
|
|
126
|
+
PiSessionSelector::Fresh { session_id } if !session_id.trim().is_empty() => {
|
|
127
|
+
argv.push("--session-id".to_string());
|
|
128
|
+
argv.push(session_id.to_string());
|
|
129
|
+
}
|
|
130
|
+
PiSessionSelector::Resume { path }
|
|
131
|
+
if !path.as_os_str().is_empty()
|
|
132
|
+
&& (!path.parent().is_some_and(Path::is_dir) || path.is_file()) =>
|
|
133
|
+
{
|
|
134
|
+
argv.push("--session".to_string());
|
|
135
|
+
argv.push(path.to_string_lossy().into_owned());
|
|
136
|
+
}
|
|
137
|
+
PiSessionSelector::Resume { path } if !path.as_os_str().is_empty() => {
|
|
138
|
+
return Err(ProviderError::ResumeUnavailable(format!(
|
|
139
|
+
"Pi exact session backing is missing: {}",
|
|
140
|
+
path.display()
|
|
141
|
+
)));
|
|
142
|
+
}
|
|
143
|
+
_ => {
|
|
144
|
+
return Err(ProviderError::Command(
|
|
145
|
+
"Pi command requires a non-empty session selector".to_string(),
|
|
146
|
+
));
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
argv.push("--name".to_string());
|
|
150
|
+
argv.push(request.agent_id.to_string());
|
|
151
|
+
Ok(argv)
|
|
152
|
+
}
|
|
@@ -24,6 +24,7 @@ pub(crate) fn provider_bypass_flag(provider: Provider) -> Option<&'static str> {
|
|
|
24
24
|
Provider::Copilot => Some("--allow-all"),
|
|
25
25
|
Provider::Grok => Some("--always-approve"),
|
|
26
26
|
Provider::CursorAgent => Some("--force"),
|
|
27
|
+
Provider::Pi => None,
|
|
27
28
|
// TODO: 查 gemini_cli 的等价 bypass 参数;未定义前 fail-loud。
|
|
28
29
|
Provider::GeminiCli => None,
|
|
29
30
|
Provider::Fake => None,
|
|
@@ -215,7 +215,7 @@ fn latest_explicit_error_fact_for_record(
|
|
|
215
215
|
Provider::Claude | Provider::ClaudeCode => claude_latest_explicit_error_fact(record),
|
|
216
216
|
// C-3-5 cr verdict: copilot N35 通知不依赖 turn-state 分类;一期 Unknown,
|
|
217
217
|
// 与 GeminiCli/Fake 同精神。二期接 sqlite turns 表后再回填。
|
|
218
|
-
Provider::Copilot | Provider::Grok | Provider::CursorAgent | Provider::GeminiCli
|
|
218
|
+
Provider::Copilot | Provider::Grok | Provider::CursorAgent | Provider::Pi | Provider::GeminiCli
|
|
219
219
|
| Provider::Fake => None,
|
|
220
220
|
}
|
|
221
221
|
}
|
|
@@ -379,7 +379,7 @@ fn extract_lifecycle_facts(
|
|
|
379
379
|
Provider::Codex => codex_lifecycle_fact(record),
|
|
380
380
|
// C-3-1 cr verdict: copilot lifecycle facts 一期不导出(classify→None,
|
|
381
381
|
// Unknown);二期读 sqlite turns 表(turn_index/assistant_response)。
|
|
382
|
-
Provider::Copilot | Provider::Grok | Provider::CursorAgent | Provider::GeminiCli
|
|
382
|
+
Provider::Copilot | Provider::Grok | Provider::CursorAgent | Provider::Pi | Provider::GeminiCli
|
|
383
383
|
| Provider::Fake => None,
|
|
384
384
|
})
|
|
385
385
|
.collect()
|
|
@@ -46,7 +46,7 @@ fn fault_fact(provider: Provider, record: &serde_json::Value) -> Option<FaultFac
|
|
|
46
46
|
match provider {
|
|
47
47
|
Provider::Claude | Provider::ClaudeCode => claude_fault_fact(record),
|
|
48
48
|
Provider::Codex => codex_fault_fact(record),
|
|
49
|
-
Provider::Copilot | Provider::Grok | Provider::CursorAgent | Provider::GeminiCli
|
|
49
|
+
Provider::Copilot | Provider::Grok | Provider::CursorAgent | Provider::Pi | Provider::GeminiCli
|
|
50
50
|
| Provider::Fake => None,
|
|
51
51
|
}
|
|
52
52
|
}
|
|
@@ -69,7 +69,7 @@ pub(crate) fn context_fork_convergence_deadline(provider: Provider) -> Duration
|
|
|
69
69
|
match provider {
|
|
70
70
|
Provider::Claude | Provider::ClaudeCode => Duration::from_secs(45),
|
|
71
71
|
Provider::Codex => Duration::from_secs(10),
|
|
72
|
-
Provider::Copilot | Provider::Grok | Provider::CursorAgent | Provider::GeminiCli
|
|
72
|
+
Provider::Copilot | Provider::Grok | Provider::CursorAgent | Provider::Pi | Provider::GeminiCli
|
|
73
73
|
| Provider::Fake => Duration::from_secs(5),
|
|
74
74
|
}
|
|
75
75
|
}
|
|
@@ -252,6 +252,7 @@ fn provider_backing_root(provider: Provider, plan: &CommandPlan) -> PathBuf {
|
|
|
252
252
|
Provider::Claude | Provider::ClaudeCode => home.join(".claude").join("projects"),
|
|
253
253
|
Provider::Codex => home.join(".codex").join("sessions"),
|
|
254
254
|
Provider::Copilot => home.join(".copilot").join("session-state"),
|
|
255
|
+
Provider::Pi => PathBuf::new(),
|
|
255
256
|
Provider::Grok | Provider::CursorAgent | Provider::GeminiCli | Provider::Fake => home,
|
|
256
257
|
}
|
|
257
258
|
}
|