@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.
Files changed (63) hide show
  1. package/Cargo.lock +1 -1
  2. package/Cargo.toml +1 -1
  3. package/crates/team-agent/src/cli/diagnose.rs +74 -0
  4. package/crates/team-agent/src/cli/emit.rs +5 -4
  5. package/crates/team-agent/src/cli/leader.rs +1 -0
  6. package/crates/team-agent/src/cli/spec.rs +1 -0
  7. package/crates/team-agent/src/compiler.rs +62 -5
  8. package/crates/team-agent/src/coordinator/runtime_detectors.rs +1 -0
  9. package/crates/team-agent/src/layout/worker_window_helpers.rs +1 -0
  10. package/crates/team-agent/src/leader/start.rs +53 -12
  11. package/crates/team-agent/src/lifecycle/launch/add_agent.rs +67 -15
  12. package/crates/team-agent/src/lifecycle/launch/add_agent_state.rs +18 -0
  13. package/crates/team-agent/src/lifecycle/launch/agent_state.rs +1 -0
  14. package/crates/team-agent/src/lifecycle/launch/cursor_mcp.rs +260 -44
  15. package/crates/team-agent/src/lifecycle/launch/cursor_mcp_iso.rs +120 -0
  16. package/crates/team-agent/src/lifecycle/launch/fork_agent.rs +5 -1
  17. package/crates/team-agent/src/lifecycle/launch/mcp_config.rs +10 -59
  18. package/crates/team-agent/src/lifecycle/launch/pi_mcp.rs +830 -0
  19. package/crates/team-agent/src/lifecycle/launch/quick_start_transport.rs +9 -3
  20. package/crates/team-agent/src/lifecycle/launch/spawn.rs +70 -28
  21. package/crates/team-agent/src/lifecycle/launch/spec_state.rs +1 -0
  22. package/crates/team-agent/src/lifecycle/launch.rs +10 -2
  23. package/crates/team-agent/src/lifecycle/profile_launch.rs +3 -2
  24. package/crates/team-agent/src/lifecycle/profile_smoke.rs +1 -0
  25. package/crates/team-agent/src/lifecycle/restart/agent.rs +90 -160
  26. package/crates/team-agent/src/lifecycle/restart/common.rs +146 -28
  27. package/crates/team-agent/src/lifecycle/restart/remove.rs +37 -1
  28. package/crates/team-agent/src/lifecycle/tests/claude_profile_launch_red.rs +24 -3
  29. package/crates/team-agent/src/lifecycle/tests/cursor_mcp_overlay.rs +195 -18
  30. package/crates/team-agent/src/lifecycle/tests/cursor_require_explicit_model_red.rs +30 -12
  31. package/crates/team-agent/src/lifecycle/tests/fixtures/pi_list_models_g0.stdout.txt +222 -0
  32. package/crates/team-agent/src/lifecycle/tests/g1_silent_faces.rs +5 -2
  33. package/crates/team-agent/src/lifecycle/tests/grok_mcp_overlay_red.rs +35 -48
  34. package/crates/team-agent/src/lifecycle/tests/harvest2_a_batch_red.rs +1 -1
  35. package/crates/team-agent/src/lifecycle/tests/pi_catalog_red.rs +97 -0
  36. package/crates/team-agent/src/lifecycle/tests/pi_command_red.rs +128 -0
  37. package/crates/team-agent/src/lifecycle/tests/pi_compiler_red.rs +207 -0
  38. package/crates/team-agent/src/lifecycle/tests/pi_dual_entry_red.rs +838 -0
  39. package/crates/team-agent/src/lifecycle/tests/pi_executable_mcp_red.rs +320 -0
  40. package/crates/team-agent/src/lifecycle/tests/pi_lifecycle_doctor_red.rs +98 -0
  41. package/crates/team-agent/src/lifecycle/tests/pi_session_delivery_red.rs +216 -0
  42. package/crates/team-agent/src/lifecycle/tests/restart_rebind_hotfix_252_red.rs +1 -0
  43. package/crates/team-agent/src/lifecycle/tests.rs +7 -0
  44. package/crates/team-agent/src/lifecycle/worker_command_context.rs +61 -4
  45. package/crates/team-agent/src/messaging/delivery.rs +36 -12
  46. package/crates/team-agent/src/model/enums.rs +3 -1
  47. package/crates/team-agent/src/model/permissions.rs +1 -1
  48. package/crates/team-agent/src/provider/adapter.rs +29 -3
  49. package/crates/team-agent/src/provider/adapters/cursor_agent.rs +5 -2
  50. package/crates/team-agent/src/provider/adapters/mod.rs +1 -0
  51. package/crates/team-agent/src/provider/adapters/pi.rs +152 -0
  52. package/crates/team-agent/src/provider/bypass_flags.rs +1 -0
  53. package/crates/team-agent/src/provider/classify.rs +2 -2
  54. package/crates/team-agent/src/provider/faults.rs +1 -1
  55. package/crates/team-agent/src/provider/session/context_fork.rs +2 -1
  56. package/crates/team-agent/src/provider/session_scan/common.rs +1 -0
  57. package/crates/team-agent/src/provider/session_scan/pi.rs +172 -0
  58. package/crates/team-agent/src/provider/session_scan.rs +4 -0
  59. package/crates/team-agent/src/provider/tests/adapter.rs +80 -0
  60. package/crates/team-agent/src/provider/wire.rs +7 -1
  61. package/crates/team-agent/src/tmux_backend.rs +52 -0
  62. package/package.json +4 -4
  63. 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 = mcp_tool_name(agent.provider, "team_orchestrator", "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
- runtime_contract_section(&send_message, &report_result),
216
- agent.communication_mode.runtime_contract(&send_message),
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 recipient_cursor = recipient_is_cursor_agent(state, &message.recipient);
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(recipient_cursor, || {
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() && !recipient_cursor {
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::Grok | Provider::CursorAgent | Provider::GeminiCli | Provider::Fake => false,
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
- fn recipient_is_busy(state: &serde_json::Value, recipient: &str) -> bool {
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, _model: &str) -> Result<bool, ProviderError> {
1289
- Ok(true)
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`;身份必须写进 `.cursor/mcp.json` 的 env 表
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 写 `.cursor/mcp.json`
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());
@@ -30,3 +30,4 @@ pub(crate) mod copilot_fork;
30
30
  pub(crate) mod fake;
31
31
  pub(crate) mod grok;
32
32
  pub(crate) mod cursor_agent;
33
+ pub(crate) mod pi;
@@ -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
  }
@@ -122,6 +122,7 @@ pub(super) fn candidate_session_files(
122
122
  Provider::Copilot
123
123
  | Provider::Grok
124
124
  | Provider::CursorAgent
125
+ | Provider::Pi
125
126
  | Provider::GeminiCli
126
127
  | Provider::Fake => {}
127
128
  }