@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
package/Cargo.lock
CHANGED
package/Cargo.toml
CHANGED
|
@@ -9,6 +9,74 @@ use crate::model::pane_authority_refusal::{
|
|
|
9
9
|
use crate::provider::wire::{command_name, parse_provider, provider_wire};
|
|
10
10
|
use crate::transport::Transport;
|
|
11
11
|
|
|
12
|
+
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize)]
|
|
13
|
+
#[serde(rename_all = "snake_case")]
|
|
14
|
+
pub(crate) enum PiBackingStatus {
|
|
15
|
+
Pending,
|
|
16
|
+
Captured,
|
|
17
|
+
Missing,
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize)]
|
|
21
|
+
#[serde(rename_all = "snake_case")]
|
|
22
|
+
pub(crate) enum PiMcpStatus {
|
|
23
|
+
Unavailable,
|
|
24
|
+
ConfiguredLazy,
|
|
25
|
+
RoundtripVerified,
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
pub(crate) struct PiDoctorInput {
|
|
29
|
+
pub executable_chain_verified: bool,
|
|
30
|
+
pub pi_version: Option<String>,
|
|
31
|
+
pub adapter_version: Option<String>,
|
|
32
|
+
pub catalog_sha256: Option<String>,
|
|
33
|
+
pub selected_model: Option<String>,
|
|
34
|
+
pub candidate_executable: Option<std::path::PathBuf>,
|
|
35
|
+
pub wrapper: Option<std::path::PathBuf>,
|
|
36
|
+
pub mcp_roundtrip_at: Option<String>,
|
|
37
|
+
pub backing: PiBackingStatus,
|
|
38
|
+
pub activity: crate::provider::TurnState,
|
|
39
|
+
pub activity_source: crate::provider::ClassifySource,
|
|
40
|
+
pub env_key_names: Vec<String>,
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)]
|
|
44
|
+
pub(crate) struct PiDoctorFacts {
|
|
45
|
+
pub mcp: PiMcpStatus,
|
|
46
|
+
pub mcp_connected: bool,
|
|
47
|
+
pub backing: PiBackingStatus,
|
|
48
|
+
pub activity: crate::provider::TurnState,
|
|
49
|
+
pub activity_source: crate::provider::ClassifySource,
|
|
50
|
+
pub auth_hint: crate::provider::AuthHintStatus,
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
pub(crate) fn pi_doctor_facts(input: PiDoctorInput) -> PiDoctorFacts {
|
|
54
|
+
let mcp = if input.mcp_roundtrip_at.is_some() {
|
|
55
|
+
PiMcpStatus::RoundtripVerified
|
|
56
|
+
} else if input.wrapper.is_some() {
|
|
57
|
+
PiMcpStatus::ConfiguredLazy
|
|
58
|
+
} else {
|
|
59
|
+
PiMcpStatus::Unavailable
|
|
60
|
+
};
|
|
61
|
+
let _ = (
|
|
62
|
+
input.executable_chain_verified,
|
|
63
|
+
input.pi_version,
|
|
64
|
+
input.adapter_version,
|
|
65
|
+
input.catalog_sha256,
|
|
66
|
+
input.selected_model,
|
|
67
|
+
input.candidate_executable,
|
|
68
|
+
input.env_key_names,
|
|
69
|
+
);
|
|
70
|
+
PiDoctorFacts {
|
|
71
|
+
mcp,
|
|
72
|
+
mcp_connected: mcp == PiMcpStatus::RoundtripVerified,
|
|
73
|
+
backing: input.backing,
|
|
74
|
+
activity: input.activity,
|
|
75
|
+
activity_source: input.activity_source,
|
|
76
|
+
auth_hint: crate::provider::AuthHintStatus::Unknown,
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
12
80
|
/// 0.5.39 Slice 1 (tmux-server-death-locate §11.1 B): classify a tmux
|
|
13
81
|
/// transport error string into a diagnose issue id. When the underlying
|
|
14
82
|
/// tmux subprocess stderr contains `server exited unexpectedly`, the
|
|
@@ -1424,6 +1492,11 @@ fn fake_agent_started(agent: &Value) -> bool {
|
|
|
1424
1492
|
}
|
|
1425
1493
|
|
|
1426
1494
|
fn agent_mcp_ready(agent: &Value) -> bool {
|
|
1495
|
+
if agent.get("provider").and_then(Value::as_str) == Some("pi") {
|
|
1496
|
+
// The generated wrapper proves configured-lazy only. No persisted
|
|
1497
|
+
// objective Pi MCP roundtrip receipt exists on the readiness path yet.
|
|
1498
|
+
return false;
|
|
1499
|
+
}
|
|
1427
1500
|
agent
|
|
1428
1501
|
.get("mcp_config")
|
|
1429
1502
|
.and_then(Value::as_str)
|
|
@@ -1567,6 +1640,7 @@ pub(crate) fn provider_doctor_checks() -> Value {
|
|
|
1567
1640
|
crate::provider::Provider::GeminiCli,
|
|
1568
1641
|
crate::provider::Provider::Grok,
|
|
1569
1642
|
crate::provider::Provider::CursorAgent,
|
|
1643
|
+
crate::provider::Provider::Pi,
|
|
1570
1644
|
crate::provider::Provider::Fake,
|
|
1571
1645
|
] {
|
|
1572
1646
|
let adapter = crate::provider::get_adapter(provider);
|
|
@@ -262,9 +262,10 @@ const DISPATCH_COMMANDS: &[&str] = &[
|
|
|
262
262
|
const SPEC_ONLY_HELP_COMMANDS: &[&str] = &["start"];
|
|
263
263
|
// Command grammar, not provider identity parsing: these are top-level CLI
|
|
264
264
|
// passthrough verbs for starting a leader under a provider executable.
|
|
265
|
-
const LEADER_PASSTHROUGH_COMMANDS: &[&str] =
|
|
265
|
+
const LEADER_PASSTHROUGH_COMMANDS: &[&str] =
|
|
266
|
+
&["codex", "claude", "copilot", "grok", "cursor", "pi"];
|
|
266
267
|
|
|
267
|
-
fn is_leader_passthrough_command(command: &str) -> bool {
|
|
268
|
+
pub(crate) fn is_leader_passthrough_command(command: &str) -> bool {
|
|
268
269
|
LEADER_PASSTHROUGH_COMMANDS.contains(&command)
|
|
269
270
|
&& matches!(
|
|
270
271
|
command_spec(command).map(|spec| spec.kind),
|
|
@@ -285,7 +286,7 @@ fn is_known_subcommand(command: &str) -> bool {
|
|
|
285
286
|
command_spec(command).is_some_and(|spec| spec.command_help)
|
|
286
287
|
}
|
|
287
288
|
|
|
288
|
-
fn default_help() -> String {
|
|
289
|
+
pub(crate) fn default_help() -> String {
|
|
289
290
|
let mut out = String::from("usage: team-agent <command> [options]\n");
|
|
290
291
|
append_help_section(
|
|
291
292
|
&mut out,
|
|
@@ -310,7 +311,7 @@ fn default_help() -> String {
|
|
|
310
311
|
"Guided recovery",
|
|
311
312
|
&["claim-leader", "takeover", "attach-leader"],
|
|
312
313
|
);
|
|
313
|
-
out.push_str("\nProvider launchers:\n team-agent codex|claude|copilot|grok|cursor ...\n");
|
|
314
|
+
out.push_str("\nProvider launchers:\n team-agent codex|claude|copilot|grok|cursor|pi ...\n");
|
|
314
315
|
out.push_str("\nRun `team-agent <command> --help` for command flags.");
|
|
315
316
|
out
|
|
316
317
|
}
|
|
@@ -181,6 +181,7 @@ pub(crate) const COMMAND_SPECS: &[CommandSpec] = &[
|
|
|
181
181
|
CommandSpec { name: "copilot", tier: CommandTier::Core, category: CommandCategory::Start, kind: CommandKind::LeaderPassthrough { provider: "copilot" }, summary: "launch a Copilot leader", usage: "usage: team-agent copilot [provider args...]", default_help: false, command_help: true, suggestion_index: true, token_usage: TokenUsage::Yes, alias_of: None, sunset: None, action: None, governance: None },
|
|
182
182
|
CommandSpec { name: "grok", tier: CommandTier::Core, category: CommandCategory::Start, kind: CommandKind::LeaderPassthrough { provider: "grok" }, summary: "launch a Grok leader", usage: "usage: team-agent grok [provider args...]", default_help: false, command_help: true, suggestion_index: true, token_usage: TokenUsage::Yes, alias_of: None, sunset: None, action: None, governance: None },
|
|
183
183
|
CommandSpec { name: "cursor", tier: CommandTier::Core, category: CommandCategory::Start, kind: CommandKind::LeaderPassthrough { provider: "cursor" }, summary: "launch a Cursor leader", usage: "usage: team-agent cursor [provider args...]", default_help: false, command_help: true, suggestion_index: true, token_usage: TokenUsage::Yes, alias_of: None, sunset: None, action: None, governance: None },
|
|
184
|
+
CommandSpec { name: "pi", tier: CommandTier::Core, category: CommandCategory::Start, kind: CommandKind::LeaderPassthrough { provider: "pi" }, summary: "launch a Pi leader", usage: "usage: team-agent pi -- --model <exact> --thinking <effort>", default_help: false, command_help: true, suggestion_index: true, token_usage: TokenUsage::Yes, alias_of: None, sunset: None, action: None, governance: None },
|
|
184
185
|
|
|
185
186
|
CommandSpec { name: "init", tier: CommandTier::CompatHidden, category: CommandCategory::Compat, kind: CommandKind::Dispatch(DispatchKind::Init), summary: "legacy bootstrap command", usage: "usage: team-agent init [--workspace WORKSPACE] [--force] [--json]", default_help: false, command_help: true, suggestion_index: false, token_usage: TokenUsage::No, alias_of: Some("quick-start"), sunset: Some("C2"), action: Some("use `team-agent quick-start`"), governance: None },
|
|
186
187
|
CommandSpec { name: "start", tier: CommandTier::CompatHidden, category: CommandCategory::Compat, kind: CommandKind::SpecOnlyAlias { alias_of: "quick-start" }, summary: "legacy start alias", usage: "usage: team-agent start [TEAMDIR] [--yes] [--fresh] [--json]", default_help: false, command_help: true, suggestion_index: false, token_usage: TokenUsage::Conditional, alias_of: Some("quick-start"), sunset: Some("C2"), action: Some("use `team-agent quick-start` or `team-agent restart`"), governance: None },
|
|
@@ -392,10 +392,25 @@ fn compile_role_agent_with_mode(
|
|
|
392
392
|
let provider = required_string(&meta, role_path, "provider")?;
|
|
393
393
|
require_explicit_grok_role_model(&meta, role_path, &provider)?;
|
|
394
394
|
require_explicit_cursor_role_model(&meta, role_path, &provider)?;
|
|
395
|
-
|
|
395
|
+
validate_pi_role_fields(&meta, role_path, &provider)?;
|
|
396
|
+
let is_pi = parse_canonical_provider(&provider) == Some(Provider::Pi);
|
|
397
|
+
let model = if is_pi {
|
|
398
|
+
string_field(&meta, "model")
|
|
399
|
+
.filter(|value| !value.trim().is_empty())
|
|
400
|
+
.map(Value::Str)
|
|
401
|
+
.unwrap_or(Value::Null)
|
|
402
|
+
} else {
|
|
403
|
+
resolve_model(&meta, team_meta, &provider)
|
|
404
|
+
};
|
|
396
405
|
let auth_mode = string_field(&meta, "auth_mode")
|
|
397
406
|
.or_else(|| string_field(team_meta, "default_auth_mode"))
|
|
398
407
|
.unwrap_or_else(|| "subscription".to_string());
|
|
408
|
+
if is_pi && auth_mode != "subscription" {
|
|
409
|
+
return Err(ModelError::Validation(format!(
|
|
410
|
+
"{}: Pi roles support subscription auth_mode only",
|
|
411
|
+
role_path.display()
|
|
412
|
+
)));
|
|
413
|
+
}
|
|
399
414
|
if auth_mode != "subscription" && meta.get("profile").is_none() {
|
|
400
415
|
return Err(ModelError::Validation(format!(
|
|
401
416
|
"{}: profile is required when auth_mode is '{auth_mode}'",
|
|
@@ -466,7 +481,11 @@ fn compile_role_agent_with_mode(
|
|
|
466
481
|
Some(raw) if !raw.trim().is_empty() => ProviderEffort::parse(raw.trim()),
|
|
467
482
|
_ => None,
|
|
468
483
|
};
|
|
469
|
-
let resolved_effort =
|
|
484
|
+
let resolved_effort = if is_pi {
|
|
485
|
+
role_effort
|
|
486
|
+
} else {
|
|
487
|
+
role_effort.or(team_effort)
|
|
488
|
+
};
|
|
470
489
|
if let Some(effort) = resolved_effort {
|
|
471
490
|
// Reject max + non-Claude at compile time.
|
|
472
491
|
let provider_str = agent_items
|
|
@@ -478,7 +497,10 @@ fn compile_role_agent_with_mode(
|
|
|
478
497
|
})
|
|
479
498
|
.unwrap_or("");
|
|
480
499
|
let provider_enum = parse_canonical_provider(provider_str).unwrap_or(Provider::Codex);
|
|
481
|
-
if effort.is_claude_only()
|
|
500
|
+
if effort.is_claude_only()
|
|
501
|
+
&& !is_claude_family(provider_enum)
|
|
502
|
+
&& provider_enum != Provider::Pi
|
|
503
|
+
{
|
|
482
504
|
return Err(ModelError::Validation(format!(
|
|
483
505
|
"{}: effort '{}' is only supported by claude/claude_code (provider: {provider_str})",
|
|
484
506
|
role_path.display(),
|
|
@@ -609,12 +631,37 @@ model: sonnet-4-thinking",
|
|
|
609
631
|
)))
|
|
610
632
|
}
|
|
611
633
|
|
|
634
|
+
fn validate_pi_role_fields(meta: &Value, path: &Path, provider: &str) -> Result<(), ModelError> {
|
|
635
|
+
if parse_canonical_provider(provider) != Some(Provider::Pi) {
|
|
636
|
+
return Ok(());
|
|
637
|
+
}
|
|
638
|
+
if let Some(model) = string_field(meta, "model").filter(|value| !value.trim().is_empty()) {
|
|
639
|
+
let (catalog, name) = model.trim().split_once('/').ok_or_else(|| {
|
|
640
|
+
ModelError::Validation(format!(
|
|
641
|
+
"{}: Pi roles require a qualified exact model such as provider/model",
|
|
642
|
+
path.display()
|
|
643
|
+
))
|
|
644
|
+
})?;
|
|
645
|
+
if catalog.is_empty() || name.is_empty() || model.contains('*') {
|
|
646
|
+
return Err(ModelError::Validation(format!(
|
|
647
|
+
"{}: Pi roles require a qualified exact model",
|
|
648
|
+
path.display()
|
|
649
|
+
)));
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
let tools = required_tools(meta, path)?;
|
|
653
|
+
if !tools.iter().any(|tool| tool == "mcp_team") {
|
|
654
|
+
return Err(ModelError::Validation(format!(
|
|
655
|
+
"{}: Pi roles require mcp_team",
|
|
656
|
+
path.display()
|
|
657
|
+
)));
|
|
658
|
+
}
|
|
659
|
+
Ok(())
|
|
660
|
+
}
|
|
661
|
+
|
|
612
662
|
/// 0.5.66 bypass 单源:角色 md 的 `dangerously_skip_permissions` 必填 bool。
|
|
613
663
|
/// 缺 = 编译失败(消息含硬串);非 bool = 同 fail-loud。
|
|
614
|
-
fn required_dangerously_skip_permissions(
|
|
615
|
-
meta: &Value,
|
|
616
|
-
path: &Path,
|
|
617
|
-
) -> Result<bool, ModelError> {
|
|
664
|
+
fn required_dangerously_skip_permissions(meta: &Value, path: &Path) -> Result<bool, ModelError> {
|
|
618
665
|
match meta.get("dangerously_skip_permissions") {
|
|
619
666
|
Some(Value::Bool(value)) => Ok(*value),
|
|
620
667
|
Some(_) => Err(ModelError::Validation(format!(
|
|
@@ -285,10 +285,58 @@ fn leader_start_plan_with_ambient_authority(
|
|
|
285
285
|
),
|
|
286
286
|
_ => None,
|
|
287
287
|
};
|
|
288
|
+
let provider_argv = if provider == Provider::Pi && mode != LeaderStartMode::AttachExisting {
|
|
289
|
+
let parsed = crate::lifecycle::launch::pi_mcp::parse_pi_leader_args(provider_args)
|
|
290
|
+
.map_err(|error| LeaderError::Start(error.to_string()))?;
|
|
291
|
+
let mcp_config = adapter
|
|
292
|
+
.mcp_config(crate::model::enums::AuthMode::Subscription)
|
|
293
|
+
.map_err(|error| LeaderError::Start(error.to_string()))?;
|
|
294
|
+
let mcp_config = crate::lifecycle::launch::resolve_mcp_config(
|
|
295
|
+
mcp_config,
|
|
296
|
+
workspace,
|
|
297
|
+
"leader",
|
|
298
|
+
identity.team_id.as_str(),
|
|
299
|
+
);
|
|
300
|
+
let prompt = crate::lifecycle::worker_command_context::compile_pi_leader_system_prompt();
|
|
301
|
+
let tools = ["mcp_team", "fs_read", "fs_list", "fs_write", "execute_bash"];
|
|
302
|
+
let team_mcp_tools = [
|
|
303
|
+
"assign_task",
|
|
304
|
+
"send_message",
|
|
305
|
+
"update_state",
|
|
306
|
+
"get_team_status",
|
|
307
|
+
"stop_agent",
|
|
308
|
+
"reset_agent",
|
|
309
|
+
"add_agent",
|
|
310
|
+
"clone_agent",
|
|
311
|
+
"fork_agent",
|
|
312
|
+
"request_human",
|
|
313
|
+
"stuck_list",
|
|
314
|
+
"stuck_cancel",
|
|
315
|
+
];
|
|
316
|
+
crate::lifecycle::launch::pi_mcp::materialize_pi_plan(
|
|
317
|
+
crate::lifecycle::launch::pi_mcp::PiMaterializeRequest {
|
|
318
|
+
workspace,
|
|
319
|
+
team_id: identity.team_id.as_str(),
|
|
320
|
+
agent_id: "leader",
|
|
321
|
+
model: parsed.model.as_deref(),
|
|
322
|
+
effort: parsed.effort,
|
|
323
|
+
system_prompt: &prompt,
|
|
324
|
+
tool_categories: &tools,
|
|
325
|
+
team_mcp_tools: &team_mcp_tools,
|
|
326
|
+
mcp_config: &mcp_config,
|
|
327
|
+
},
|
|
328
|
+
)
|
|
329
|
+
.map_err(|error| LeaderError::Start(error.to_string()))?
|
|
330
|
+
.argv
|
|
331
|
+
} else if provider == Provider::Pi {
|
|
332
|
+
Vec::new()
|
|
333
|
+
} else {
|
|
334
|
+
provider_command_argv(provider, provider_args)
|
|
335
|
+
};
|
|
288
336
|
let argv = start_argv(
|
|
289
337
|
mode,
|
|
290
338
|
provider,
|
|
291
|
-
|
|
339
|
+
&provider_argv,
|
|
292
340
|
workspace,
|
|
293
341
|
session_name.as_ref(),
|
|
294
342
|
managed_window.as_ref(),
|
|
@@ -305,7 +353,6 @@ fn leader_start_plan_with_ambient_authority(
|
|
|
305
353
|
} else {
|
|
306
354
|
leader_env.clone()
|
|
307
355
|
};
|
|
308
|
-
let provider_argv = provider_command_argv(provider, provider_args);
|
|
309
356
|
Ok(LeaderStartPlan {
|
|
310
357
|
mode,
|
|
311
358
|
provider,
|
|
@@ -970,20 +1017,15 @@ fn managed_leader_window_for_launch(
|
|
|
970
1017
|
fn start_argv(
|
|
971
1018
|
mode: LeaderStartMode,
|
|
972
1019
|
provider: Provider,
|
|
973
|
-
|
|
1020
|
+
provider_argv: &[String],
|
|
974
1021
|
workspace: &Path,
|
|
975
1022
|
session_name: Option<&SessionName>,
|
|
976
1023
|
leader_window: Option<&WindowName>,
|
|
977
1024
|
leader_env: &BTreeMap<String, String>,
|
|
978
1025
|
managed_client_attach_mode: Option<ManagedClientAttachMode>,
|
|
979
1026
|
) -> Result<Vec<String>, LeaderError> {
|
|
980
|
-
let provider_cmd = provider_command_name(provider).to_string();
|
|
981
1027
|
match mode {
|
|
982
|
-
LeaderStartMode::ExecProvider =>
|
|
983
|
-
let mut argv = vec![provider_cmd];
|
|
984
|
-
argv.extend(normalized_provider_args(provider_args));
|
|
985
|
-
Ok(argv)
|
|
986
|
-
}
|
|
1028
|
+
LeaderStartMode::ExecProvider => Ok(provider_argv.to_vec()),
|
|
987
1029
|
LeaderStartMode::ManagedTmuxClient => {
|
|
988
1030
|
let Some(session) = session_name else {
|
|
989
1031
|
return Err(LeaderError::Start(
|
|
@@ -1021,13 +1063,11 @@ fn start_argv(
|
|
|
1021
1063
|
if let Some(path) = std::env::var_os("PATH").and_then(|p| p.into_string().ok()) {
|
|
1022
1064
|
exports.push(shlex_quote(&format!("PATH={path}")));
|
|
1023
1065
|
}
|
|
1024
|
-
let mut provider_argv = vec![provider_cmd];
|
|
1025
|
-
provider_argv.extend(normalized_provider_args(provider_args));
|
|
1026
1066
|
let shell = format!(
|
|
1027
1067
|
"cd {} && export {} && exec {}",
|
|
1028
1068
|
shlex_quote(&resolved_workspace.to_string_lossy()),
|
|
1029
1069
|
exports.join(" "),
|
|
1030
|
-
shell_join(
|
|
1070
|
+
shell_join(provider_argv)
|
|
1031
1071
|
);
|
|
1032
1072
|
let argv = vec![
|
|
1033
1073
|
"tmux".to_string(),
|
|
@@ -2777,6 +2817,7 @@ fn provider_command_name(provider: Provider) -> &'static str {
|
|
|
2777
2817
|
Provider::GeminiCli => "gemini",
|
|
2778
2818
|
Provider::Grok => "grok",
|
|
2779
2819
|
Provider::CursorAgent => "agent",
|
|
2820
|
+
Provider::Pi => "pi",
|
|
2780
2821
|
Provider::Fake => "fake",
|
|
2781
2822
|
}
|
|
2782
2823
|
}
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
//! boundary:
|
|
21
21
|
//! - 不拷贝外部角色文件进 team 目录,就地读取编译
|
|
22
22
|
//! - 起席一律走 restart 的 start_agent_at_paths,本文件不直接 spawn
|
|
23
|
-
//! -
|
|
23
|
+
//! - start path 按 exact pane receipt 回收已 spawn pane;本文件恢复 spec/state,并删除新 Pi 席位的 exact wrapper
|
|
24
24
|
//! maturity: wired
|
|
25
25
|
//! ---
|
|
26
26
|
use std::collections::{BTreeMap, BTreeSet};
|
|
@@ -217,18 +217,14 @@ pub fn add_agent(
|
|
|
217
217
|
) {
|
|
218
218
|
Ok(selected) => selected,
|
|
219
219
|
Err(_) if workspace.join("TEAM.md").exists() => {
|
|
220
|
-
//
|
|
221
|
-
//
|
|
222
|
-
//
|
|
223
|
-
// and routes the new agent's spawn to the SAME tmux socket the live
|
|
224
|
-
// team uses. Cold workspaces / first-agent paths safely fall back to
|
|
225
|
-
// `TmuxBackend::for_workspace(team_workspace)` inside the resolver.
|
|
226
|
-
let team_ws = team_workspace(workspace);
|
|
220
|
+
// The MCP server already passes the canonical run workspace here.
|
|
221
|
+
// Reapplying team_workspace() would strip the scratch root and bind
|
|
222
|
+
// the first dynamic worker to the parent workspace's tmux server.
|
|
227
223
|
let transport =
|
|
228
224
|
crate::lifecycle::restart::lifecycle_worker_tmux_backend_for_selected_state(
|
|
229
|
-
|
|
225
|
+
workspace, team,
|
|
230
226
|
)
|
|
231
|
-
.unwrap_or_else(|_| crate::tmux_backend::TmuxBackend::for_workspace(
|
|
227
|
+
.unwrap_or_else(|_| crate::tmux_backend::TmuxBackend::for_workspace(workspace));
|
|
232
228
|
return add_agent_with_transport(
|
|
233
229
|
workspace,
|
|
234
230
|
agent_id,
|
|
@@ -516,6 +512,28 @@ pub(super) fn maybe_fail_force_recreate_after_spawn() -> Result<(), LifecycleErr
|
|
|
516
512
|
)))
|
|
517
513
|
}
|
|
518
514
|
|
|
515
|
+
fn rollback_added_pi_wrapper(
|
|
516
|
+
run_workspace: &Path,
|
|
517
|
+
team_key: &str,
|
|
518
|
+
agent_id: &AgentId,
|
|
519
|
+
role_meta: &Value,
|
|
520
|
+
) -> Result<(), LifecycleError> {
|
|
521
|
+
if role_meta.get("provider").and_then(Value::as_str) != Some("pi") {
|
|
522
|
+
return Ok(());
|
|
523
|
+
}
|
|
524
|
+
let wrapper =
|
|
525
|
+
crate::lifecycle::launch::pi_mcp::pi_seat_paths(run_workspace, team_key, agent_id.as_str())
|
|
526
|
+
.wrapper;
|
|
527
|
+
match std::fs::remove_file(&wrapper) {
|
|
528
|
+
Ok(()) => Ok(()),
|
|
529
|
+
Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(()),
|
|
530
|
+
Err(error) => Err(LifecycleError::StatePersist(format!(
|
|
531
|
+
"failed to roll back added Pi wrapper {}: {error}",
|
|
532
|
+
wrapper.display()
|
|
533
|
+
))),
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
|
|
519
537
|
/// ---
|
|
520
538
|
/// purpose: 加席的实体实现,含 owner 门、重名拒绝、重编译 spec 原子写、席位 state upsert 与起席
|
|
521
539
|
/// params:
|
|
@@ -701,6 +719,8 @@ fn add_agent_with_transport_at_paths_reserved(
|
|
|
701
719
|
) {
|
|
702
720
|
Ok(started) => started,
|
|
703
721
|
Err(error) => {
|
|
722
|
+
let wrapper_rollback =
|
|
723
|
+
rollback_added_pi_wrapper(run_workspace, &canonical_team_key, agent_id, &meta);
|
|
704
724
|
rollback_add_agent_atomic(
|
|
705
725
|
run_workspace,
|
|
706
726
|
&spec_path,
|
|
@@ -709,15 +729,42 @@ fn add_agent_with_transport_at_paths_reserved(
|
|
|
709
729
|
agent_id,
|
|
710
730
|
"start_agent_failed",
|
|
711
731
|
);
|
|
712
|
-
return
|
|
732
|
+
return match wrapper_rollback {
|
|
733
|
+
Ok(()) => Err(error),
|
|
734
|
+
Err(rollback_error) => Err(LifecycleError::StatePersist(format!(
|
|
735
|
+
"{error}; {rollback_error}"
|
|
736
|
+
))),
|
|
737
|
+
};
|
|
713
738
|
}
|
|
714
739
|
};
|
|
715
740
|
let (env, start_mode) = match started {
|
|
716
741
|
StartAgentOutcome::Running {
|
|
717
742
|
env, start_mode, ..
|
|
718
743
|
} => (env, start_mode),
|
|
719
|
-
StartAgentOutcome::Noop {
|
|
744
|
+
StartAgentOutcome::Noop { .. } => {
|
|
745
|
+
let wrapper_rollback =
|
|
746
|
+
rollback_added_pi_wrapper(run_workspace, &canonical_team_key, agent_id, &meta);
|
|
747
|
+
rollback_add_agent_atomic(
|
|
748
|
+
run_workspace,
|
|
749
|
+
&spec_path,
|
|
750
|
+
pre_spec_text.as_deref(),
|
|
751
|
+
None,
|
|
752
|
+
agent_id,
|
|
753
|
+
"added_agent_noop",
|
|
754
|
+
);
|
|
755
|
+
let error = LifecycleError::RequirementUnmet(format!(
|
|
756
|
+
"newly added agent {agent_id} returned start_agent.noop"
|
|
757
|
+
));
|
|
758
|
+
return match wrapper_rollback {
|
|
759
|
+
Ok(()) => Err(error),
|
|
760
|
+
Err(rollback_error) => Err(LifecycleError::StatePersist(format!(
|
|
761
|
+
"{error}; {rollback_error}"
|
|
762
|
+
))),
|
|
763
|
+
};
|
|
764
|
+
}
|
|
720
765
|
StartAgentOutcome::Paused { .. } => {
|
|
766
|
+
let wrapper_rollback =
|
|
767
|
+
rollback_added_pi_wrapper(run_workspace, &canonical_team_key, agent_id, &meta);
|
|
721
768
|
rollback_add_agent_atomic(
|
|
722
769
|
run_workspace,
|
|
723
770
|
&spec_path,
|
|
@@ -726,9 +773,14 @@ fn add_agent_with_transport_at_paths_reserved(
|
|
|
726
773
|
agent_id,
|
|
727
774
|
"added_agent_paused",
|
|
728
775
|
);
|
|
729
|
-
|
|
730
|
-
"added agent {agent_id} is paused"
|
|
731
|
-
|
|
776
|
+
let error =
|
|
777
|
+
LifecycleError::RequirementUnmet(format!("added agent {agent_id} is paused"));
|
|
778
|
+
return match wrapper_rollback {
|
|
779
|
+
Ok(()) => Err(error),
|
|
780
|
+
Err(rollback_error) => Err(LifecycleError::StatePersist(format!(
|
|
781
|
+
"{error}; {rollback_error}"
|
|
782
|
+
))),
|
|
783
|
+
};
|
|
732
784
|
}
|
|
733
785
|
};
|
|
734
786
|
if let Some(reservation) = reservation {
|
|
@@ -167,6 +167,24 @@ pub(super) fn upsert_agent_state_from_role(
|
|
|
167
167
|
"runtime state root is not an object".to_string(),
|
|
168
168
|
));
|
|
169
169
|
};
|
|
170
|
+
if !root
|
|
171
|
+
.get("session_name")
|
|
172
|
+
.and_then(serde_json::Value::as_str)
|
|
173
|
+
.is_some_and(|session| !session.is_empty())
|
|
174
|
+
{
|
|
175
|
+
if let Some(owner_session) = root
|
|
176
|
+
.get("leader_receiver")
|
|
177
|
+
.and_then(|receiver| receiver.get("session_name"))
|
|
178
|
+
.and_then(serde_json::Value::as_str)
|
|
179
|
+
.filter(|session| !session.is_empty())
|
|
180
|
+
.map(str::to_string)
|
|
181
|
+
{
|
|
182
|
+
root.insert(
|
|
183
|
+
"session_name".to_string(),
|
|
184
|
+
serde_json::Value::String(owner_session),
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
170
188
|
let agents = root
|
|
171
189
|
.entry("agents".to_string())
|
|
172
190
|
.or_insert_with(|| serde_json::json!({}));
|