@team-agent/installer 0.5.71 → 0.5.73
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/adapters.rs +107 -9
- package/crates/team-agent/src/cli/emit.rs +35 -5
- package/crates/team-agent/src/cli/mod.rs +78 -7
- package/crates/team-agent/src/cli/models.rs +443 -0
- package/crates/team-agent/src/cli/spec.rs +4 -1
- package/crates/team-agent/src/cli/types.rs +8 -0
- package/crates/team-agent/src/compiler.rs +157 -0
- package/crates/team-agent/src/leader/owner_bind.rs +21 -0
- package/crates/team-agent/src/leader/provider_attribution.rs +25 -0
- package/crates/team-agent/src/leader/registry.rs +25 -1
- package/crates/team-agent/src/leader/start.rs +41 -9
- package/crates/team-agent/src/lifecycle/launch/add_agent.rs +169 -10
- package/crates/team-agent/src/lifecycle/launch/pi_mcp.rs +119 -9
- package/crates/team-agent/src/lifecycle/launch/quick_start.rs +859 -12
- package/crates/team-agent/src/lifecycle/launch/spawn.rs +1 -0
- package/crates/team-agent/src/lifecycle/restart/common.rs +1 -0
- package/crates/team-agent/src/lifecycle/tests/lane_ops.rs +168 -0
- package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +209 -0
- package/crates/team-agent/src/lifecycle/tests/pi_catalog_red.rs +1 -1
- package/crates/team-agent/src/lifecycle/tests/pi_command_red.rs +71 -1
- package/crates/team-agent/src/lifecycle/tests/pi_compiler_red.rs +3 -1
- package/crates/team-agent/src/lifecycle/tests/pi_executable_mcp_red.rs +396 -3
- package/crates/team-agent/src/lifecycle/tests/pi_session_delivery_red.rs +3 -1
- package/crates/team-agent/src/lifecycle/types.rs +8 -0
- package/crates/team-agent/src/provider/adapters/pi.rs +12 -6
- package/npm/check-skill-links.mjs +88 -0
- package/package.json +5 -4
- package/skills/team-agent/SKILL.md +15 -49
- package/skills/team-agent/command-coverage.json +8 -1
- package/skills/team-agent/references/team-agent-operator.md +665 -0
|
@@ -220,6 +220,16 @@ fn bind_provider_from_env_or_pane(command: &str, pane: Option<PaneInfo>) -> Prov
|
|
|
220
220
|
.unwrap_or(Provider::Codex)
|
|
221
221
|
}
|
|
222
222
|
|
|
223
|
+
pub(crate) fn strict_owner_bind_provider(
|
|
224
|
+
explicit_provider: Option<&str>,
|
|
225
|
+
observed_command: &str,
|
|
226
|
+
) -> Option<Provider> {
|
|
227
|
+
match explicit_provider.filter(|value| !value.is_empty()) {
|
|
228
|
+
Some(raw) => super::helpers::parse_provider(raw),
|
|
229
|
+
None => super::attribute_command_provider(observed_command),
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
223
233
|
pub fn owner_bind_provider_wire(command: &str) -> &'static str {
|
|
224
234
|
if let Ok(raw) = std::env::var("TEAM_AGENT_LEADER_PROVIDER") {
|
|
225
235
|
// env 显式 provider:经 parse_provider(单一表,知 copilot)校验后透传其 wire 串;
|
|
@@ -341,6 +351,17 @@ mod e11_provider_bind_tests {
|
|
|
341
351
|
assert_eq!(owner_bind_provider_wire("copilot --banner"), "copilot");
|
|
342
352
|
}
|
|
343
353
|
|
|
354
|
+
#[test]
|
|
355
|
+
fn strict_provider_never_falls_back_from_unknown_explicit_or_command() {
|
|
356
|
+
assert_eq!(
|
|
357
|
+
strict_owner_bind_provider(Some("pi"), "codex"),
|
|
358
|
+
Some(Provider::Pi)
|
|
359
|
+
);
|
|
360
|
+
assert_eq!(strict_owner_bind_provider(Some("unknown"), "codex"), None);
|
|
361
|
+
assert_eq!(strict_owner_bind_provider(None, "pi"), Some(Provider::Pi));
|
|
362
|
+
assert_eq!(strict_owner_bind_provider(None, "node"), None);
|
|
363
|
+
}
|
|
364
|
+
|
|
344
365
|
#[test]
|
|
345
366
|
fn known_commands_map_via_single_source() {
|
|
346
367
|
assert_eq!(
|
|
@@ -96,6 +96,9 @@ fn provider_from_pid_env(pid: u32) -> Option<Provider> {
|
|
|
96
96
|
|
|
97
97
|
fn provider_from_command_text(text: &str) -> Option<Provider> {
|
|
98
98
|
let lower = text.to_ascii_lowercase();
|
|
99
|
+
if pi_command_text(&lower) {
|
|
100
|
+
return Some(Provider::Pi);
|
|
101
|
+
}
|
|
99
102
|
if cursor_agent_command_text(&lower) {
|
|
100
103
|
return Some(Provider::CursorAgent);
|
|
101
104
|
}
|
|
@@ -104,6 +107,18 @@ fn provider_from_command_text(text: &str) -> Option<Provider> {
|
|
|
104
107
|
.find_map(|(needle, provider)| lower.contains(needle).then_some(*provider))
|
|
105
108
|
}
|
|
106
109
|
|
|
110
|
+
fn pi_command_text(lower: &str) -> bool {
|
|
111
|
+
lower
|
|
112
|
+
.split_whitespace()
|
|
113
|
+
.next()
|
|
114
|
+
.is_some_and(|token| {
|
|
115
|
+
token == "pi"
|
|
116
|
+
|| token == "pi.exe"
|
|
117
|
+
|| token.ends_with("/pi")
|
|
118
|
+
|| token.ends_with("\\pi.exe")
|
|
119
|
+
})
|
|
120
|
+
}
|
|
121
|
+
|
|
107
122
|
/// Cursor 二进制名为 `agent`,不能做 substring contains(会误伤 `team-agent`)。
|
|
108
123
|
/// 认精确 token `cursor-agent` / `…/agent`,且必须带 `--sandbox` 或 `--trust`。
|
|
109
124
|
fn cursor_agent_command_text(lower: &str) -> bool {
|
|
@@ -194,6 +209,16 @@ mod tests {
|
|
|
194
209
|
}
|
|
195
210
|
}
|
|
196
211
|
|
|
212
|
+
#[test]
|
|
213
|
+
fn exact_pi_command_is_attributed_without_substring_guessing() {
|
|
214
|
+
assert_eq!(attribute_command_provider("pi"), Some(Provider::Pi));
|
|
215
|
+
assert_eq!(
|
|
216
|
+
attribute_command_provider("/opt/homebrew/bin/pi --model openai/gpt-5"),
|
|
217
|
+
Some(Provider::Pi)
|
|
218
|
+
);
|
|
219
|
+
assert_eq!(attribute_command_provider("pilot"), None);
|
|
220
|
+
}
|
|
221
|
+
|
|
197
222
|
#[test]
|
|
198
223
|
fn direct_command_form_recognizes_copilot() {
|
|
199
224
|
assert_eq!(
|
|
@@ -243,7 +243,10 @@ pub fn register_binding_from_state_best_effort(
|
|
|
243
243
|
);
|
|
244
244
|
let event_log = crate::event_log::EventLog::new(workspace);
|
|
245
245
|
let write_result = write_entry_best_effort(&entry);
|
|
246
|
-
|
|
246
|
+
// Fresh quick-start is acquisition-only: it must never displace another
|
|
247
|
+
// same-pane row. Its caller performs a validated-LIVE check before this
|
|
248
|
+
// write; manual/restart ownership paths keep existing behavior.
|
|
249
|
+
if write_result.is_some() && source != "quick-start" {
|
|
247
250
|
let _ = mark_displaced_same_pane_entries_unbound(&entry);
|
|
248
251
|
}
|
|
249
252
|
let status = if let Some(path) = &write_result {
|
|
@@ -543,6 +546,27 @@ pub fn list_validated_with_gc() -> Vec<(LeaderRegistryEntry, &'static str, Optio
|
|
|
543
546
|
out
|
|
544
547
|
}
|
|
545
548
|
|
|
549
|
+
/// Refuse a fresh quick-start binding when the caller pane already backs a
|
|
550
|
+
/// canonically LIVE receiver for another workspace/team. This is read-only:
|
|
551
|
+
/// quick-start never displaces or takes over an existing binding.
|
|
552
|
+
#[must_use]
|
|
553
|
+
pub(crate) fn live_same_pane_binding_elsewhere(
|
|
554
|
+
pane_id: &str,
|
|
555
|
+
workspace: &Path,
|
|
556
|
+
team_key: &str,
|
|
557
|
+
) -> bool {
|
|
558
|
+
let workspace = std::fs::canonicalize(workspace).unwrap_or_else(|_| workspace.to_path_buf());
|
|
559
|
+
list_validated_no_gc().into_iter().any(|(entry, status, _)| {
|
|
560
|
+
status == "LIVE"
|
|
561
|
+
&& entry
|
|
562
|
+
.channel
|
|
563
|
+
.get("pane_id")
|
|
564
|
+
.and_then(Value::as_str)
|
|
565
|
+
== Some(pane_id)
|
|
566
|
+
&& (entry.workspace != workspace || entry.team_key != team_key)
|
|
567
|
+
})
|
|
568
|
+
}
|
|
569
|
+
|
|
546
570
|
/// Same as `list_validated_with_gc` but preserves all entries. `send
|
|
547
571
|
/// --to-leader` needs to see stale entries so it can refuse with
|
|
548
572
|
/// `registry_stale` (and never silently fall through to
|
|
@@ -138,15 +138,18 @@ pub(crate) fn prepare_leader_start_with_nested_attach(
|
|
|
138
138
|
) -> Result<PreparedLeaderStart, PrepareLeaderStartError> {
|
|
139
139
|
let explicit_external_path = external_leader || attach_existing || attach_session.is_some();
|
|
140
140
|
let state_external_path = workspace_state_uses_external_leader(workspace);
|
|
141
|
-
let (ambient_authority, managed_client_attach_mode,
|
|
141
|
+
let (ambient_authority, managed_client_attach_mode, managed_exec_provider) =
|
|
142
142
|
if explicit_external_path || state_external_path {
|
|
143
143
|
(ambient_pane_authority_preflight(workspace)?, None, false)
|
|
144
144
|
} else if workspace_state_is_managed_provider_reentry(workspace) {
|
|
145
145
|
(ambient_pane_authority_preflight(workspace)?, None, true)
|
|
146
146
|
} else {
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
147
|
+
managed_launcher_ambient_route(
|
|
148
|
+
provider,
|
|
149
|
+
provider_args,
|
|
150
|
+
workspace,
|
|
151
|
+
allow_nested_attach,
|
|
152
|
+
)?
|
|
150
153
|
};
|
|
151
154
|
let plan = leader_start_plan_with_ambient_authority(
|
|
152
155
|
provider,
|
|
@@ -158,7 +161,8 @@ pub(crate) fn prepare_leader_start_with_nested_attach(
|
|
|
158
161
|
external_leader,
|
|
159
162
|
std::env::var_os("TMUX").is_some(),
|
|
160
163
|
managed_client_attach_mode,
|
|
161
|
-
|
|
164
|
+
managed_exec_provider,
|
|
165
|
+
allow_nested_attach,
|
|
162
166
|
)?;
|
|
163
167
|
Ok(PreparedLeaderStart {
|
|
164
168
|
plan,
|
|
@@ -193,6 +197,7 @@ pub(crate) fn leader_start_plan_after_ambient_authority_check(
|
|
|
193
197
|
in_tmux,
|
|
194
198
|
managed_client_attach_mode,
|
|
195
199
|
false,
|
|
200
|
+
false,
|
|
196
201
|
)
|
|
197
202
|
}
|
|
198
203
|
|
|
@@ -206,7 +211,8 @@ fn leader_start_plan_with_ambient_authority(
|
|
|
206
211
|
external_leader: bool,
|
|
207
212
|
in_tmux: bool,
|
|
208
213
|
managed_client_attach_mode: Option<ManagedClientAttachMode>,
|
|
209
|
-
|
|
214
|
+
managed_exec_provider: bool,
|
|
215
|
+
allow_nested_attach: bool,
|
|
210
216
|
) -> Result<LeaderStartPlan, LeaderError> {
|
|
211
217
|
if attach_session.is_some() && !confirm_attach {
|
|
212
218
|
return Err(LeaderError::Start(
|
|
@@ -264,7 +270,7 @@ fn leader_start_plan_with_ambient_authority(
|
|
|
264
270
|
} else {
|
|
265
271
|
false
|
|
266
272
|
};
|
|
267
|
-
let mode = if
|
|
273
|
+
let mode = if managed_exec_provider {
|
|
268
274
|
LeaderStartMode::ExecProvider
|
|
269
275
|
} else if !external_path {
|
|
270
276
|
LeaderStartMode::ManagedTmuxClient
|
|
@@ -324,6 +330,11 @@ fn leader_start_plan_with_ambient_authority(
|
|
|
324
330
|
tool_categories: &tools,
|
|
325
331
|
team_mcp_tools: &team_mcp_tools,
|
|
326
332
|
mcp_config: &mcp_config,
|
|
333
|
+
session_scope: crate::lifecycle::launch::pi_mcp::pi_leader_session_scope(
|
|
334
|
+
provider_args,
|
|
335
|
+
external_path,
|
|
336
|
+
allow_nested_attach,
|
|
337
|
+
),
|
|
327
338
|
},
|
|
328
339
|
)
|
|
329
340
|
.map_err(|error| LeaderError::Start(error.to_string()))?
|
|
@@ -555,17 +566,24 @@ fn ambient_pane_authority_preflight(
|
|
|
555
566
|
}
|
|
556
567
|
|
|
557
568
|
fn managed_launcher_ambient_route(
|
|
569
|
+
provider: Provider,
|
|
570
|
+
provider_args: &[String],
|
|
558
571
|
workspace: &Path,
|
|
559
572
|
allow_nested_attach: bool,
|
|
560
573
|
) -> Result<
|
|
561
574
|
(
|
|
562
575
|
Option<VerifiedAmbientPaneAuthority>,
|
|
563
576
|
Option<ManagedClientAttachMode>,
|
|
577
|
+
bool,
|
|
564
578
|
),
|
|
565
579
|
PrepareLeaderStartError,
|
|
566
580
|
> {
|
|
567
581
|
let Some(tmux) = std::env::var_os("TMUX") else {
|
|
568
|
-
return Ok((
|
|
582
|
+
return Ok((
|
|
583
|
+
None,
|
|
584
|
+
Some(ManagedClientAttachMode::AttachSession),
|
|
585
|
+
false,
|
|
586
|
+
));
|
|
569
587
|
};
|
|
570
588
|
let tmux = tmux.into_string().map_err(|_| {
|
|
571
589
|
PrepareLeaderStartError::PaneAuthorityRefused(ambient_tmux_endpoint_refusal(
|
|
@@ -575,6 +593,15 @@ fn managed_launcher_ambient_route(
|
|
|
575
593
|
})?;
|
|
576
594
|
let observed_endpoint = validated_ambient_tmux_endpoint(workspace, &tmux)
|
|
577
595
|
.map_err(PrepareLeaderStartError::PaneAuthorityRefused)?;
|
|
596
|
+
if provider == Provider::Pi && provider_args.is_empty() && !allow_nested_attach {
|
|
597
|
+
// Pi's zero-argument managed entry already owns this verified pane as its
|
|
598
|
+
// user-facing terminal. Replacing the current process is safe on either
|
|
599
|
+
// the workspace server or another server; switching/attaching would move
|
|
600
|
+
// the user away from the positively verified workspace pane.
|
|
601
|
+
let authority = verified_ambient_pane_authority(workspace, &tmux)
|
|
602
|
+
.map_err(PrepareLeaderStartError::PaneAuthorityRefused)?;
|
|
603
|
+
return Ok((Some(authority), None, true));
|
|
604
|
+
}
|
|
578
605
|
let target_socket_name = TmuxBackend::for_workspace(workspace)
|
|
579
606
|
.tmux_endpoint()
|
|
580
607
|
.ok_or_else(|| LeaderError::Start("workspace tmux endpoint missing".to_string()))?;
|
|
@@ -589,7 +616,11 @@ fn managed_launcher_ambient_route(
|
|
|
589
616
|
if same_server {
|
|
590
617
|
let authority = verified_ambient_pane_authority(workspace, &tmux)
|
|
591
618
|
.map_err(PrepareLeaderStartError::PaneAuthorityRefused)?;
|
|
592
|
-
return Ok((
|
|
619
|
+
return Ok((
|
|
620
|
+
Some(authority),
|
|
621
|
+
Some(ManagedClientAttachMode::SwitchClient),
|
|
622
|
+
false,
|
|
623
|
+
));
|
|
593
624
|
}
|
|
594
625
|
if !allow_nested_attach {
|
|
595
626
|
return Err(LeaderError::Validation(format!(
|
|
@@ -603,6 +634,7 @@ fn managed_launcher_ambient_route(
|
|
|
603
634
|
Ok((
|
|
604
635
|
Some(authority),
|
|
605
636
|
Some(ManagedClientAttachMode::AttachSession),
|
|
637
|
+
false,
|
|
606
638
|
))
|
|
607
639
|
}
|
|
608
640
|
|
|
@@ -93,6 +93,51 @@ impl Drop for AgentReservation {
|
|
|
93
93
|
}
|
|
94
94
|
}
|
|
95
95
|
|
|
96
|
+
fn discover_pi_model_candidates(requested: &str) -> Result<Vec<String>, ()> {
|
|
97
|
+
crate::lifecycle::launch::pi_mcp::pi_model_candidates(requested).map_err(|_| ())
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
fn preflight_role_with_pi_model(
|
|
101
|
+
run_workspace: &Path,
|
|
102
|
+
team_dir: &Path,
|
|
103
|
+
agent_id: &AgentId,
|
|
104
|
+
role_file_path: &Path,
|
|
105
|
+
discover: &mut dyn FnMut(&str) -> Result<Vec<String>, ()>,
|
|
106
|
+
) -> Result<(), LifecycleError> {
|
|
107
|
+
if !role_file_path.exists() {
|
|
108
|
+
return Err(LifecycleError::Compile(format!(
|
|
109
|
+
"role file not found: {}",
|
|
110
|
+
role_file_path.display()
|
|
111
|
+
)));
|
|
112
|
+
}
|
|
113
|
+
let (meta, _) = crate::compiler::read_front_matter(role_file_path)
|
|
114
|
+
.map_err(|error| LifecycleError::Compile(error.to_string()))?;
|
|
115
|
+
crate::compiler::preflight_pi_role_model_with(&meta, discover).map_err(|error| {
|
|
116
|
+
LifecycleError::PiModelPreflight {
|
|
117
|
+
requested: error.requested,
|
|
118
|
+
candidates: error.candidates,
|
|
119
|
+
action: error.action,
|
|
120
|
+
not_ready: error.not_ready,
|
|
121
|
+
}
|
|
122
|
+
})?;
|
|
123
|
+
let team_meta = crate::compiler::read_front_matter(&team_dir.join("TEAM.md"))
|
|
124
|
+
.map(|(meta, _)| meta)
|
|
125
|
+
.unwrap_or(Value::Null);
|
|
126
|
+
let compiled = crate::compiler::compile_role_agent(
|
|
127
|
+
role_file_path,
|
|
128
|
+
&team_meta,
|
|
129
|
+
&run_workspace.display().to_string(),
|
|
130
|
+
)
|
|
131
|
+
.map_err(|error| LifecycleError::Compile(error.to_string()))?;
|
|
132
|
+
if compiled.id != agent_id.as_str() {
|
|
133
|
+
return Err(LifecycleError::Compile(format!(
|
|
134
|
+
"role file declares name '{}' but add-agent id is '{}'",
|
|
135
|
+
compiled.id, agent_id
|
|
136
|
+
)));
|
|
137
|
+
}
|
|
138
|
+
Ok(())
|
|
139
|
+
}
|
|
140
|
+
|
|
96
141
|
fn reserve_agent_slot(
|
|
97
142
|
workspace: &Path,
|
|
98
143
|
team: Option<&str>,
|
|
@@ -209,6 +254,25 @@ pub fn add_agent(
|
|
|
209
254
|
role_file_path: &Path,
|
|
210
255
|
open_display: bool,
|
|
211
256
|
team: Option<&str>,
|
|
257
|
+
) -> Result<AddAgentReport, LifecycleError> {
|
|
258
|
+
let mut discover = discover_pi_model_candidates;
|
|
259
|
+
add_agent_with_pi_preflight(
|
|
260
|
+
workspace,
|
|
261
|
+
agent_id,
|
|
262
|
+
role_file_path,
|
|
263
|
+
open_display,
|
|
264
|
+
team,
|
|
265
|
+
&mut discover,
|
|
266
|
+
)
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
fn add_agent_with_pi_preflight(
|
|
270
|
+
workspace: &Path,
|
|
271
|
+
agent_id: &AgentId,
|
|
272
|
+
role_file_path: &Path,
|
|
273
|
+
open_display: bool,
|
|
274
|
+
team: Option<&str>,
|
|
275
|
+
discover: &mut dyn FnMut(&str) -> Result<Vec<String>, ()>,
|
|
212
276
|
) -> Result<AddAgentReport, LifecycleError> {
|
|
213
277
|
let selected = match crate::state::selector::resolve_active_team(
|
|
214
278
|
workspace,
|
|
@@ -225,13 +289,14 @@ pub fn add_agent(
|
|
|
225
289
|
workspace, team,
|
|
226
290
|
)
|
|
227
291
|
.unwrap_or_else(|_| crate::tmux_backend::TmuxBackend::for_workspace(workspace));
|
|
228
|
-
return
|
|
292
|
+
return add_agent_with_transport_pi_preflight(
|
|
229
293
|
workspace,
|
|
230
294
|
agent_id,
|
|
231
295
|
role_file_path,
|
|
232
296
|
open_display,
|
|
233
297
|
team,
|
|
234
298
|
&transport,
|
|
299
|
+
discover,
|
|
235
300
|
);
|
|
236
301
|
}
|
|
237
302
|
Err(error) => return Err(LifecycleError::TeamSelect(error.to_string())),
|
|
@@ -242,6 +307,13 @@ pub fn add_agent(
|
|
|
242
307
|
team: Some(selected.team_key.as_str()),
|
|
243
308
|
agent_id: Some(agent_id),
|
|
244
309
|
})?;
|
|
310
|
+
preflight_role_with_pi_model(
|
|
311
|
+
&selected.run_workspace,
|
|
312
|
+
&selected.team_dir,
|
|
313
|
+
agent_id,
|
|
314
|
+
role_file_path,
|
|
315
|
+
discover,
|
|
316
|
+
)?;
|
|
245
317
|
let reservation = reserve_agent_slot(
|
|
246
318
|
&selected.run_workspace,
|
|
247
319
|
Some(selected.team_key.as_str()),
|
|
@@ -292,9 +364,38 @@ pub fn add_agent_force(
|
|
|
292
364
|
open_display: bool,
|
|
293
365
|
team: Option<&str>,
|
|
294
366
|
force: bool,
|
|
367
|
+
) -> Result<AddAgentReport, LifecycleError> {
|
|
368
|
+
let mut discover = discover_pi_model_candidates;
|
|
369
|
+
add_agent_force_with_pi_preflight(
|
|
370
|
+
workspace,
|
|
371
|
+
agent_id,
|
|
372
|
+
role_file_path,
|
|
373
|
+
open_display,
|
|
374
|
+
team,
|
|
375
|
+
force,
|
|
376
|
+
&mut discover,
|
|
377
|
+
)
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
#[allow(clippy::too_many_arguments)]
|
|
381
|
+
fn add_agent_force_with_pi_preflight(
|
|
382
|
+
workspace: &Path,
|
|
383
|
+
agent_id: &AgentId,
|
|
384
|
+
role_file_path: &Path,
|
|
385
|
+
open_display: bool,
|
|
386
|
+
team: Option<&str>,
|
|
387
|
+
force: bool,
|
|
388
|
+
discover: &mut dyn FnMut(&str) -> Result<Vec<String>, ()>,
|
|
295
389
|
) -> Result<AddAgentReport, LifecycleError> {
|
|
296
390
|
if !force {
|
|
297
|
-
return
|
|
391
|
+
return add_agent_with_pi_preflight(
|
|
392
|
+
workspace,
|
|
393
|
+
agent_id,
|
|
394
|
+
role_file_path,
|
|
395
|
+
open_display,
|
|
396
|
+
team,
|
|
397
|
+
discover,
|
|
398
|
+
);
|
|
298
399
|
}
|
|
299
400
|
let selected = crate::state::selector::resolve_active_team(
|
|
300
401
|
workspace,
|
|
@@ -322,6 +423,7 @@ pub fn add_agent_force(
|
|
|
322
423
|
open_display,
|
|
323
424
|
Some(canonical_team_key.as_str()),
|
|
324
425
|
&transport,
|
|
426
|
+
discover,
|
|
325
427
|
)
|
|
326
428
|
}
|
|
327
429
|
|
|
@@ -340,6 +442,28 @@ pub(crate) fn add_agent_with_transport(
|
|
|
340
442
|
open_display: bool,
|
|
341
443
|
team: Option<&str>,
|
|
342
444
|
transport: &dyn Transport,
|
|
445
|
+
) -> Result<AddAgentReport, LifecycleError> {
|
|
446
|
+
let mut discover = discover_pi_model_candidates;
|
|
447
|
+
add_agent_with_transport_pi_preflight(
|
|
448
|
+
workspace,
|
|
449
|
+
agent_id,
|
|
450
|
+
role_file_path,
|
|
451
|
+
open_display,
|
|
452
|
+
team,
|
|
453
|
+
transport,
|
|
454
|
+
&mut discover,
|
|
455
|
+
)
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
#[allow(clippy::too_many_arguments)]
|
|
459
|
+
pub(crate) fn add_agent_with_transport_pi_preflight(
|
|
460
|
+
workspace: &Path,
|
|
461
|
+
agent_id: &AgentId,
|
|
462
|
+
role_file_path: &Path,
|
|
463
|
+
open_display: bool,
|
|
464
|
+
team: Option<&str>,
|
|
465
|
+
transport: &dyn Transport,
|
|
466
|
+
discover: &mut dyn FnMut(&str) -> Result<Vec<String>, ()>,
|
|
343
467
|
) -> Result<AddAgentReport, LifecycleError> {
|
|
344
468
|
let run_workspace = crate::model::paths::canonical_run_workspace(workspace)
|
|
345
469
|
.map_err(|e| LifecycleError::StatePersist(e.to_string()))?;
|
|
@@ -349,6 +473,13 @@ pub(crate) fn add_agent_with_transport(
|
|
|
349
473
|
team,
|
|
350
474
|
agent_id: Some(agent_id),
|
|
351
475
|
})?;
|
|
476
|
+
preflight_role_with_pi_model(
|
|
477
|
+
&run_workspace,
|
|
478
|
+
workspace,
|
|
479
|
+
agent_id,
|
|
480
|
+
role_file_path,
|
|
481
|
+
discover,
|
|
482
|
+
)?;
|
|
352
483
|
let reservation = reserve_agent_slot(&run_workspace, team, agent_id)?;
|
|
353
484
|
drop(lifecycle_lock);
|
|
354
485
|
add_agent_with_transport_at_paths_reserved(
|
|
@@ -380,15 +511,40 @@ pub(crate) fn add_agent_with_transport_force(
|
|
|
380
511
|
team: Option<&str>,
|
|
381
512
|
force: bool,
|
|
382
513
|
transport: &dyn Transport,
|
|
514
|
+
) -> Result<AddAgentReport, LifecycleError> {
|
|
515
|
+
let mut discover = discover_pi_model_candidates;
|
|
516
|
+
add_agent_with_transport_force_pi_preflight(
|
|
517
|
+
workspace,
|
|
518
|
+
agent_id,
|
|
519
|
+
role_file_path,
|
|
520
|
+
open_display,
|
|
521
|
+
team,
|
|
522
|
+
force,
|
|
523
|
+
transport,
|
|
524
|
+
&mut discover,
|
|
525
|
+
)
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
#[allow(clippy::too_many_arguments)]
|
|
529
|
+
pub(crate) fn add_agent_with_transport_force_pi_preflight(
|
|
530
|
+
workspace: &Path,
|
|
531
|
+
agent_id: &AgentId,
|
|
532
|
+
role_file_path: &Path,
|
|
533
|
+
open_display: bool,
|
|
534
|
+
team: Option<&str>,
|
|
535
|
+
force: bool,
|
|
536
|
+
transport: &dyn Transport,
|
|
537
|
+
discover: &mut dyn FnMut(&str) -> Result<Vec<String>, ()>,
|
|
383
538
|
) -> Result<AddAgentReport, LifecycleError> {
|
|
384
539
|
if !force {
|
|
385
|
-
return
|
|
540
|
+
return add_agent_with_transport_pi_preflight(
|
|
386
541
|
workspace,
|
|
387
542
|
agent_id,
|
|
388
543
|
role_file_path,
|
|
389
544
|
open_display,
|
|
390
545
|
team,
|
|
391
546
|
transport,
|
|
547
|
+
discover,
|
|
392
548
|
);
|
|
393
549
|
}
|
|
394
550
|
let run_workspace = crate::model::paths::canonical_run_workspace(workspace)
|
|
@@ -407,6 +563,7 @@ pub(crate) fn add_agent_with_transport_force(
|
|
|
407
563
|
open_display,
|
|
408
564
|
team,
|
|
409
565
|
transport,
|
|
566
|
+
discover,
|
|
410
567
|
)
|
|
411
568
|
}
|
|
412
569
|
|
|
@@ -415,6 +572,7 @@ pub(crate) fn add_agent_with_transport_force(
|
|
|
415
572
|
/// returns: 新席报告;成功后还要过快照的一致性校验
|
|
416
573
|
/// errors: 角色文件不存在先行返回 Compile;摘除、加回或一致性校验失败时按快照恢复并返回错误
|
|
417
574
|
/// ---
|
|
575
|
+
#[allow(clippy::too_many_arguments)]
|
|
418
576
|
pub(super) fn force_recreate_with_transport_locked(
|
|
419
577
|
run_workspace: &Path,
|
|
420
578
|
team_dir: &Path,
|
|
@@ -423,15 +581,16 @@ pub(super) fn force_recreate_with_transport_locked(
|
|
|
423
581
|
open_display: bool,
|
|
424
582
|
team: Option<&str>,
|
|
425
583
|
transport: &dyn Transport,
|
|
584
|
+
discover: &mut dyn FnMut(&str) -> Result<Vec<String>, ()>,
|
|
426
585
|
) -> Result<AddAgentReport, LifecycleError> {
|
|
427
586
|
// Reject an unusable replacement source before consuming the old seat.
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
587
|
+
preflight_role_with_pi_model(
|
|
588
|
+
run_workspace,
|
|
589
|
+
team_dir,
|
|
590
|
+
agent_id,
|
|
591
|
+
role_file_path,
|
|
592
|
+
discover,
|
|
593
|
+
)?;
|
|
435
594
|
let snapshot = crate::lifecycle::restart::remove::ForceRecreateSnapshot::capture(
|
|
436
595
|
run_workspace,
|
|
437
596
|
agent_id,
|