@team-agent/installer 0.5.113 → 0.5.115
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 +252 -84
- package/Cargo.toml +3 -1
- package/crates/team-agent/Cargo.toml +3 -0
- package/crates/team-agent/src/cli/adapters.rs +28 -15
- package/crates/team-agent/src/cli/diagnose.rs +36 -13
- package/crates/team-agent/src/cli/emit.rs +57 -22
- package/crates/team-agent/src/cli/leader.rs +16 -0
- package/crates/team-agent/src/cli/leader_prompt.rs +261 -0
- package/crates/team-agent/src/cli/mod.rs +157 -8
- package/crates/team-agent/src/cli/models.rs +119 -18
- package/crates/team-agent/src/cli/spec.rs +7 -1
- package/crates/team-agent/src/cli/status_port/brief.rs +268 -101
- package/crates/team-agent/src/cli/tests/base.rs +88 -0
- package/crates/team-agent/src/cli/tests/status_send.rs +2 -0
- package/crates/team-agent/src/compiler.rs +66 -18
- package/crates/team-agent/src/contract_runtime/backend.rs +959 -0
- package/crates/team-agent/src/contract_runtime/commands.rs +245 -0
- package/crates/team-agent/src/contract_runtime/config.rs +340 -0
- package/crates/team-agent/src/contract_runtime/config_tests.rs +56 -0
- package/crates/team-agent/src/contract_runtime/discovery.rs +383 -0
- package/crates/team-agent/src/contract_runtime/forward.rs +229 -0
- package/crates/team-agent/src/contract_runtime/framework.rs +294 -0
- package/crates/team-agent/src/contract_runtime/mcp.rs +396 -0
- package/crates/team-agent/src/contract_runtime/mod.rs +16 -0
- package/crates/team-agent/src/contract_runtime/prompt.rs +216 -0
- package/crates/team-agent/src/contract_runtime/pump.rs +269 -0
- package/crates/team-agent/src/contract_runtime/registry.rs +197 -0
- package/crates/team-agent/src/contract_runtime/workspace.rs +160 -0
- package/crates/team-agent/src/coordinator/tick.rs +26 -8
- package/crates/team-agent/src/leader/mod.rs +1 -0
- package/crates/team-agent/src/leader/prompt.rs +906 -0
- package/crates/team-agent/src/leader/start.rs +353 -21
- package/crates/team-agent/src/leader/tests/mod.rs +2 -0
- package/crates/team-agent/src/leader/tests/prompt_contract.rs +268 -0
- package/crates/team-agent/src/leader/types.rs +6 -0
- package/crates/team-agent/src/lib.rs +4 -0
- package/crates/team-agent/src/lifecycle/launch/quick_start.rs +571 -389
- package/crates/team-agent/src/lifecycle/launch/spawn.rs +38 -9
- package/crates/team-agent/src/lifecycle/launch/state_projection.rs +15 -0
- package/crates/team-agent/src/lifecycle/launch.rs +45 -28
- package/crates/team-agent/src/lifecycle/restart/rebuild.rs +23 -1
- package/crates/team-agent/src/lifecycle/restart/selection.rs +34 -0
- package/crates/team-agent/src/lifecycle/role_config.rs +17 -1
- package/crates/team-agent/src/lifecycle/types.rs +4 -1
- package/crates/team-agent/src/lifecycle/worker_command_context.rs +37 -27
- package/crates/team-agent/src/main.rs +6 -0
- package/crates/team-agent/src/messaging/delivery.rs +50 -1
- package/crates/team-agent/src/messaging/send.rs +10 -0
- package/crates/team-agent/src/messaging/tests/leader_inject_acceptance.rs +131 -0
- package/crates/team-agent/src/model/spec.rs +24 -12
- package/crates/team-agent/src/state/persist.rs +73 -0
- package/package.json +4 -4
|
@@ -23,7 +23,7 @@ use crate::provider::{get_adapter, Provider};
|
|
|
23
23
|
use crate::tmux_backend::TmuxBackend;
|
|
24
24
|
use crate::transport::{
|
|
25
25
|
CaptureRange, PaneId, PaneInfo, PaneLiveness, SessionName, SpawnResult, Target, Transport,
|
|
26
|
-
WindowName,
|
|
26
|
+
TransportError, WindowName,
|
|
27
27
|
};
|
|
28
28
|
|
|
29
29
|
use super::helpers::{
|
|
@@ -291,6 +291,13 @@ fn leader_start_plan_with_ambient_authority(
|
|
|
291
291
|
),
|
|
292
292
|
_ => None,
|
|
293
293
|
};
|
|
294
|
+
let configured_prompt = if mode == LeaderStartMode::AttachExisting {
|
|
295
|
+
None
|
|
296
|
+
} else {
|
|
297
|
+
super::prompt::load().map_err(|error| error.for_provider(provider))?
|
|
298
|
+
};
|
|
299
|
+
let mut pi_routed_argv = None;
|
|
300
|
+
let mut session_capture = None;
|
|
294
301
|
let mut provider_argv = if provider == Provider::Pi && mode != LeaderStartMode::AttachExisting {
|
|
295
302
|
let parsed = crate::lifecycle::launch::pi_mcp::parse_pi_leader_args(provider_args)
|
|
296
303
|
.map_err(|error| LeaderError::Start(error.to_string()))?;
|
|
@@ -303,9 +310,23 @@ fn leader_start_plan_with_ambient_authority(
|
|
|
303
310
|
"leader",
|
|
304
311
|
identity.team_id.as_str(),
|
|
305
312
|
);
|
|
306
|
-
let
|
|
313
|
+
let contract = crate::lifecycle::worker_command_context::compile_pi_leader_system_prompt();
|
|
314
|
+
let prompt = if let Some(stored) = configured_prompt.as_ref() {
|
|
315
|
+
// Read route once and validate conflicts before creating any Pi MCP artifacts.
|
|
316
|
+
let mut routed = vec!["pi".to_string()];
|
|
317
|
+
crate::provider::argv_route::apply(provider, &mut routed)
|
|
318
|
+
.map_err(|error| LeaderError::Start(error.to_string()))?;
|
|
319
|
+
let combined = super::prompt::compose_pi_contract(
|
|
320
|
+
&contract, &routed, &stored.text, &stored.path,
|
|
321
|
+
)?;
|
|
322
|
+
pi_routed_argv = Some(routed);
|
|
323
|
+
combined
|
|
324
|
+
} else {
|
|
325
|
+
contract
|
|
326
|
+
};
|
|
307
327
|
let team_mcp_tools = ["send_message", "report_result", "get_team_status"];
|
|
308
|
-
|
|
328
|
+
let spawned_at = chrono::Utc::now().to_rfc3339();
|
|
329
|
+
let command = crate::lifecycle::launch::pi_mcp::materialize_pi_plan(
|
|
309
330
|
crate::lifecycle::launch::pi_mcp::PiMaterializeRequest {
|
|
310
331
|
workspace,
|
|
311
332
|
team_id: identity.team_id.as_str(),
|
|
@@ -322,16 +343,32 @@ fn leader_start_plan_with_ambient_authority(
|
|
|
322
343
|
),
|
|
323
344
|
},
|
|
324
345
|
)
|
|
325
|
-
.map_err(|error| LeaderError::Start(error.to_string()))
|
|
326
|
-
|
|
346
|
+
.map_err(|error| LeaderError::Start(error.to_string()))?;
|
|
347
|
+
session_capture = Some(crate::provider::CaptureSessionContext {
|
|
348
|
+
agent_id: "leader".into(),
|
|
349
|
+
spawn_cwd: resolve_workspace_for_hash(workspace),
|
|
350
|
+
pane_id: None,
|
|
351
|
+
pane_pid: None,
|
|
352
|
+
spawned_at: Some(spawned_at),
|
|
353
|
+
expected_session_id: command.expected_session_id,
|
|
354
|
+
provider_projects_root: command.provider_projects_root,
|
|
355
|
+
});
|
|
356
|
+
command.argv
|
|
327
357
|
} else if provider == Provider::Pi {
|
|
328
358
|
Vec::new()
|
|
329
359
|
} else {
|
|
330
360
|
provider_command_argv(provider, provider_args)
|
|
331
361
|
};
|
|
332
362
|
if mode != LeaderStartMode::AttachExisting {
|
|
333
|
-
|
|
334
|
-
.
|
|
363
|
+
if let Some(routed) = pi_routed_argv {
|
|
364
|
+
provider_argv.splice(1..1, routed.into_iter().skip(1));
|
|
365
|
+
} else {
|
|
366
|
+
crate::provider::argv_route::apply(provider, &mut provider_argv)
|
|
367
|
+
.map_err(|error| LeaderError::Start(error.to_string()))?;
|
|
368
|
+
if let Some(stored) = configured_prompt.as_ref() {
|
|
369
|
+
super::prompt::inject(provider, &mut provider_argv, &stored.text, &stored.path)?;
|
|
370
|
+
}
|
|
371
|
+
}
|
|
335
372
|
}
|
|
336
373
|
let argv = start_argv(
|
|
337
374
|
mode,
|
|
@@ -361,6 +398,10 @@ fn leader_start_plan_with_ambient_authority(
|
|
|
361
398
|
session_name: plan_session_name,
|
|
362
399
|
argv,
|
|
363
400
|
provider_argv,
|
|
401
|
+
leader_prompt: configured_prompt
|
|
402
|
+
.as_ref()
|
|
403
|
+
.map(|stored| stored.metadata(provider)),
|
|
404
|
+
session_capture,
|
|
364
405
|
// 0.3.28 Step 2: leader window inside the dedicated leader session is
|
|
365
406
|
// named after the provider wire (e.g. `claude_code`), never the
|
|
366
407
|
// literal string `leader`. Occupied names get a per-launch suffix.
|
|
@@ -1345,7 +1386,7 @@ fn ensure_managed_leader_pane(
|
|
|
1345
1386
|
&env_unset,
|
|
1346
1387
|
provider_label,
|
|
1347
1388
|
)
|
|
1348
|
-
.map_err(|error|
|
|
1389
|
+
.map_err(|error| prompt_aware_spawn_error(plan, error))
|
|
1349
1390
|
} else {
|
|
1350
1391
|
let env_unset = leader_env_unset_for_provider(plan.provider);
|
|
1351
1392
|
let provider_label = provider_command_name(plan.provider);
|
|
@@ -1359,8 +1400,35 @@ fn ensure_managed_leader_pane(
|
|
|
1359
1400
|
&env_unset,
|
|
1360
1401
|
provider_label,
|
|
1361
1402
|
)
|
|
1362
|
-
.map_err(|error|
|
|
1403
|
+
.map_err(|error| prompt_aware_spawn_error(plan, error))
|
|
1404
|
+
}
|
|
1405
|
+
}
|
|
1406
|
+
|
|
1407
|
+
fn prompt_aware_spawn_error(plan: &LeaderStartPlan, error: TransportError) -> LeaderError {
|
|
1408
|
+
let Some(metadata) = plan.leader_prompt.as_ref() else {
|
|
1409
|
+
return LeaderError::Start(error.to_string());
|
|
1410
|
+
};
|
|
1411
|
+
// Transport subprocess errors contain the full tmux shell argv. Preserve
|
|
1412
|
+
// real exit/I/O evidence without exposing the process-level prompt payload.
|
|
1413
|
+
let mut safe = super::prompt::PromptError::new(
|
|
1414
|
+
"leader_prompt_carrier_failed",
|
|
1415
|
+
"Leader prompt-bearing transport spawn failed; argv and stderr withheld",
|
|
1416
|
+
Some(&metadata.config_path),
|
|
1417
|
+
)
|
|
1418
|
+
.for_provider(plan.provider);
|
|
1419
|
+
match error {
|
|
1420
|
+
TransportError::Subprocess { code, .. } => safe.exit_code = code,
|
|
1421
|
+
TransportError::Spawn { source, .. } | TransportError::Inject { source, .. }
|
|
1422
|
+
| TransportError::Capture { source } | TransportError::Io(source) => {
|
|
1423
|
+
safe.io_kind = Some(format!("{:?}", source.kind()));
|
|
1424
|
+
}
|
|
1425
|
+
oversized @ TransportError::CommandTooLong { .. } => {
|
|
1426
|
+
safe.error = oversized.to_string();
|
|
1427
|
+
}
|
|
1428
|
+
_ => {}
|
|
1363
1429
|
}
|
|
1430
|
+
safe.action = "Inspect the owned launcher and transport metadata; do not repeat or change carriers to bypass the failure";
|
|
1431
|
+
LeaderError::Prompt(safe)
|
|
1364
1432
|
}
|
|
1365
1433
|
|
|
1366
1434
|
struct ManagedAttachWatch<'a> {
|
|
@@ -1740,6 +1808,25 @@ fn managed_spawned_pane_in_targets(transport: &dyn Transport, spawned: &SpawnRes
|
|
|
1740
1808
|
})
|
|
1741
1809
|
}
|
|
1742
1810
|
|
|
1811
|
+
/// Keep the launcher's native identity with the canonical pane. The backing
|
|
1812
|
+
/// JSONL may not exist until Pi's first turn; receipt observation resolves it by
|
|
1813
|
+
/// exact header, never by filename/mtime or the Team Agent owner UUID.
|
|
1814
|
+
fn bind_plan_session(receiver: &mut serde_json::Value, plan: &LeaderStartPlan) {
|
|
1815
|
+
if let Some(capture) = &plan.session_capture {
|
|
1816
|
+
let native = serde_json::json!({
|
|
1817
|
+
"expected_session_id": capture.expected_session_id,
|
|
1818
|
+
"provider_projects_root": capture.provider_projects_root,
|
|
1819
|
+
"spawn_cwd": capture.spawn_cwd,
|
|
1820
|
+
"spawned_at": capture.spawned_at,
|
|
1821
|
+
"pane_id": receiver.get("pane_id"),
|
|
1822
|
+
"tmux_socket": receiver.get("tmux_socket"),
|
|
1823
|
+
"leader_session_uuid": receiver.get("leader_session_uuid"),
|
|
1824
|
+
"owner_epoch": receiver.get("owner_epoch"),
|
|
1825
|
+
});
|
|
1826
|
+
receiver["native_session"] = native;
|
|
1827
|
+
}
|
|
1828
|
+
}
|
|
1829
|
+
|
|
1743
1830
|
fn persist_managed_leader_binding(
|
|
1744
1831
|
plan: &LeaderStartPlan,
|
|
1745
1832
|
workspace: &Path,
|
|
@@ -1855,7 +1942,7 @@ fn persist_managed_leader_binding(
|
|
|
1855
1942
|
} else {
|
|
1856
1943
|
serde_json::json!("managed_launcher")
|
|
1857
1944
|
};
|
|
1858
|
-
let receiver = serde_json::json!({
|
|
1945
|
+
let mut receiver = serde_json::json!({
|
|
1859
1946
|
"mode": "direct_tmux",
|
|
1860
1947
|
"status": "attached",
|
|
1861
1948
|
"provider": provider.clone(),
|
|
@@ -1870,6 +1957,15 @@ fn persist_managed_leader_binding(
|
|
|
1870
1957
|
"discovery": discovery,
|
|
1871
1958
|
"bound_panes": bound_panes,
|
|
1872
1959
|
});
|
|
1960
|
+
if keep_existing_scalar {
|
|
1961
|
+
for key in ["native_session", "rollout_path"] {
|
|
1962
|
+
if let Some(value) = existing_recv.as_ref().and_then(|old| old.get(key)) {
|
|
1963
|
+
receiver[key] = value.clone();
|
|
1964
|
+
}
|
|
1965
|
+
}
|
|
1966
|
+
} else {
|
|
1967
|
+
bind_plan_session(&mut receiver, plan);
|
|
1968
|
+
}
|
|
1873
1969
|
let owner_uuid = if keep_existing_scalar {
|
|
1874
1970
|
keep_field(
|
|
1875
1971
|
existing_owner.as_ref(),
|
|
@@ -2028,6 +2124,7 @@ fn persist_exec_provider_leader_binding(
|
|
|
2028
2124
|
}
|
|
2029
2125
|
obj.insert("tmux_socket".to_string(), serde_json::json!(socket));
|
|
2030
2126
|
}
|
|
2127
|
+
bind_plan_session(&mut receiver, plan);
|
|
2031
2128
|
let owner = serde_json::json!({
|
|
2032
2129
|
"pane_id": pane,
|
|
2033
2130
|
"provider": provider.clone(),
|
|
@@ -2043,10 +2140,8 @@ fn persist_exec_provider_leader_binding(
|
|
|
2043
2140
|
"active_team_key".to_string(),
|
|
2044
2141
|
serde_json::json!(identity.team_id.as_str()),
|
|
2045
2142
|
);
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
serde_json::json!(target.session.as_str()),
|
|
2049
|
-
);
|
|
2143
|
+
// The ambient session identifies the leader receiver, not workers.
|
|
2144
|
+
// Leave any existing worker session untouched, including unknown state.
|
|
2050
2145
|
obj.insert("tmux_endpoint".to_string(), serde_json::json!(socket));
|
|
2051
2146
|
obj.insert("tmux_socket".to_string(), serde_json::json!(socket));
|
|
2052
2147
|
obj.insert(
|
|
@@ -2421,6 +2516,11 @@ fn run_leader_argv(
|
|
|
2421
2516
|
diagnostics_path.display()
|
|
2422
2517
|
))
|
|
2423
2518
|
})?;
|
|
2519
|
+
// Captured transport diagnostics can echo the shell argv carrying the
|
|
2520
|
+
// prompt. Do not tee those bytes before redaction. Native ExecProvider
|
|
2521
|
+
// business streams and inherited TTY stdout keep their existing behavior.
|
|
2522
|
+
let withhold_transport_output = plan.leader_prompt.is_some()
|
|
2523
|
+
&& plan.mode != LeaderStartMode::ExecProvider;
|
|
2424
2524
|
let stdout_reader = child.stdout.take().map(|mut child_stdout| {
|
|
2425
2525
|
std::thread::spawn(move || {
|
|
2426
2526
|
let mut captured = Vec::new();
|
|
@@ -2432,7 +2532,9 @@ fn run_leader_argv(
|
|
|
2432
2532
|
if read == 0 {
|
|
2433
2533
|
break;
|
|
2434
2534
|
}
|
|
2435
|
-
|
|
2535
|
+
if !withhold_transport_output {
|
|
2536
|
+
let _ = std::io::stdout().write_all(&buf[..read]);
|
|
2537
|
+
}
|
|
2436
2538
|
captured.extend_from_slice(&buf[..read]);
|
|
2437
2539
|
}
|
|
2438
2540
|
captured
|
|
@@ -2449,7 +2551,9 @@ fn run_leader_argv(
|
|
|
2449
2551
|
if read == 0 {
|
|
2450
2552
|
break;
|
|
2451
2553
|
}
|
|
2452
|
-
|
|
2554
|
+
if !withhold_transport_output {
|
|
2555
|
+
let _ = std::io::stderr().write_all(&buf[..read]);
|
|
2556
|
+
}
|
|
2453
2557
|
captured.extend_from_slice(&buf[..read]);
|
|
2454
2558
|
}
|
|
2455
2559
|
captured
|
|
@@ -2487,8 +2591,8 @@ fn run_leader_argv(
|
|
|
2487
2591
|
.code()
|
|
2488
2592
|
.map(|code| code.to_string())
|
|
2489
2593
|
.unwrap_or_else(|| "signal".to_string()),
|
|
2490
|
-
|
|
2491
|
-
|
|
2594
|
+
prompt_aware_diagnostic_text(plan, &stdout),
|
|
2595
|
+
prompt_aware_diagnostic_text(plan, &stderr),
|
|
2492
2596
|
),
|
|
2493
2597
|
)
|
|
2494
2598
|
.map_err(|error| {
|
|
@@ -2497,6 +2601,11 @@ fn run_leader_argv(
|
|
|
2497
2601
|
diagnostics_path.display()
|
|
2498
2602
|
))
|
|
2499
2603
|
})?;
|
|
2604
|
+
let stderr = if plan.leader_prompt.is_some() {
|
|
2605
|
+
prompt_aware_diagnostic_text(plan, &stderr)
|
|
2606
|
+
} else {
|
|
2607
|
+
stderr
|
|
2608
|
+
};
|
|
2500
2609
|
Ok(LeaderProcessExit {
|
|
2501
2610
|
status,
|
|
2502
2611
|
stderr,
|
|
@@ -2591,6 +2700,14 @@ fn launcher_diagnostics_path(workspace: &Path) -> PathBuf {
|
|
|
2591
2700
|
))
|
|
2592
2701
|
}
|
|
2593
2702
|
|
|
2703
|
+
fn prompt_aware_diagnostic_text(plan: &LeaderStartPlan, text: &str) -> String {
|
|
2704
|
+
if plan.leader_prompt.is_some() {
|
|
2705
|
+
"[withheld: Leader prompt-bearing launch]".to_string()
|
|
2706
|
+
} else {
|
|
2707
|
+
crate::redaction::redact_external_text(text)
|
|
2708
|
+
}
|
|
2709
|
+
}
|
|
2710
|
+
|
|
2594
2711
|
fn write_launcher_diagnostics_header(
|
|
2595
2712
|
path: &Path,
|
|
2596
2713
|
plan: &LeaderStartPlan,
|
|
@@ -2599,13 +2716,22 @@ fn write_launcher_diagnostics_header(
|
|
|
2599
2716
|
if let Some(parent) = path.parent() {
|
|
2600
2717
|
std::fs::create_dir_all(parent)?;
|
|
2601
2718
|
}
|
|
2719
|
+
let metadata = plan
|
|
2720
|
+
.leader_prompt
|
|
2721
|
+
.as_ref()
|
|
2722
|
+
.map(serde_json::to_string)
|
|
2723
|
+
.transpose()
|
|
2724
|
+
.map_err(|error| std::io::Error::new(std::io::ErrorKind::InvalidData, error))?
|
|
2725
|
+
.map(|metadata| format!("leader_prompt={metadata}\n"))
|
|
2726
|
+
.unwrap_or_default();
|
|
2602
2727
|
std::fs::write(
|
|
2603
2728
|
path,
|
|
2604
2729
|
format!(
|
|
2605
|
-
"startup_stage=prepare\nmode={:?}\nprovider={}\nargv={}\n",
|
|
2730
|
+
"startup_stage=prepare\nmode={:?}\nprovider={}\nargv={}\n{}",
|
|
2606
2731
|
plan.mode,
|
|
2607
2732
|
provider_wire(plan.provider),
|
|
2608
|
-
|
|
2733
|
+
prompt_aware_diagnostic_text(plan, &argv.join(" ")),
|
|
2734
|
+
metadata,
|
|
2609
2735
|
),
|
|
2610
2736
|
)
|
|
2611
2737
|
}
|
|
@@ -2934,7 +3060,8 @@ mod tests {
|
|
|
2934
3060
|
execute_leader_plan_after_ambient_authority, format_launcher_failure,
|
|
2935
3061
|
handle_exec_provider_startup_prompts, is_interactive_shell_basename,
|
|
2936
3062
|
lock_down_window_rename, managed_client_argv, managed_leader_window_from_occupied,
|
|
2937
|
-
|
|
3063
|
+
persist_exec_provider_leader_binding, persist_managed_leader_binding,
|
|
3064
|
+
push_bounded_stderr, shlex_quote,
|
|
2938
3065
|
write_window_rename_lockdown_event, ManagedClientAttachMode, VerifiedAmbientPaneAuthority,
|
|
2939
3066
|
LEADER_STDERR_LIMIT,
|
|
2940
3067
|
};
|
|
@@ -3345,6 +3472,8 @@ mod tests {
|
|
|
3345
3472
|
session_name: None,
|
|
3346
3473
|
argv: vec!["sh".to_string(), "-c".to_string(), command],
|
|
3347
3474
|
provider_argv: vec!["codex".to_string()],
|
|
3475
|
+
leader_prompt: None,
|
|
3476
|
+
session_capture: None,
|
|
3348
3477
|
leader_window: None,
|
|
3349
3478
|
is_external_leader: true,
|
|
3350
3479
|
leader_env: BTreeMap::new(),
|
|
@@ -3365,6 +3494,95 @@ mod tests {
|
|
|
3365
3494
|
let _ = std::fs::remove_dir_all(&workspace);
|
|
3366
3495
|
}
|
|
3367
3496
|
|
|
3497
|
+
fn exec_binding_fixture(workspace: &Path) -> (LeaderStartPlan, VerifiedAmbientPaneAuthority) {
|
|
3498
|
+
let identity = LeaderIdentity {
|
|
3499
|
+
leader_session_uuid: LeaderSessionUuid::derive(
|
|
3500
|
+
"fp", &workspace.to_string_lossy(), "tester", "current",
|
|
3501
|
+
).unwrap(),
|
|
3502
|
+
leader_session_uuid_source: LeaderSessionUuidSource::Derived,
|
|
3503
|
+
machine_fingerprint: "fp".into(), workspace_abspath: workspace.into(),
|
|
3504
|
+
os_user: "tester".into(), team_id: TeamKey::new("current"),
|
|
3505
|
+
};
|
|
3506
|
+
let plan = LeaderStartPlan {
|
|
3507
|
+
mode: LeaderStartMode::ExecProvider, provider: Provider::Pi,
|
|
3508
|
+
workspace: workspace.into(), socket: LeaderLaunchSocket::Workspace,
|
|
3509
|
+
session_name: None, argv: vec![], provider_argv: vec!["pi".into()],
|
|
3510
|
+
leader_prompt: None, session_capture: None, leader_window: None,
|
|
3511
|
+
is_external_leader: false, leader_env: BTreeMap::new(),
|
|
3512
|
+
identity: Some(identity), detached: false,
|
|
3513
|
+
};
|
|
3514
|
+
let authority = VerifiedAmbientPaneAuthority {
|
|
3515
|
+
pane_id: PaneId::new("%77"),
|
|
3516
|
+
observed: PaneInfo {
|
|
3517
|
+
pane_id: PaneId::new("%77"), session: SessionName::new("cursor-gate-leader"),
|
|
3518
|
+
window_index: Some(0), window_name: Some(WindowName::new("pi")),
|
|
3519
|
+
pane_index: Some(0), tty: Some("/dev/ttys077".into()),
|
|
3520
|
+
current_command: Some("pi".into()), current_path: Some(workspace.into()),
|
|
3521
|
+
active: true, pane_pid: None, leader_env: BTreeMap::new(),
|
|
3522
|
+
},
|
|
3523
|
+
endpoint: "/private/tmp/fixture-exec-leader.sock".into(),
|
|
3524
|
+
tmux: "/private/tmp/fixture-exec-leader.sock,123,0".into(),
|
|
3525
|
+
};
|
|
3526
|
+
(plan, authority)
|
|
3527
|
+
}
|
|
3528
|
+
|
|
3529
|
+
fn exec_binding_workspace() -> std::path::PathBuf {
|
|
3530
|
+
let workspace = std::env::temp_dir().join(format!("ta-exec-bootstrap-{}-{}",
|
|
3531
|
+
std::process::id(), std::time::SystemTime::now()
|
|
3532
|
+
.duration_since(std::time::UNIX_EPOCH).unwrap().as_nanos()));
|
|
3533
|
+
std::fs::create_dir(&workspace).unwrap();
|
|
3534
|
+
workspace
|
|
3535
|
+
}
|
|
3536
|
+
|
|
3537
|
+
#[test]
|
|
3538
|
+
fn exec_provider_non_prefixed_receiver_produces_fresh_quick_start_bootstrap() {
|
|
3539
|
+
let workspace = exec_binding_workspace();
|
|
3540
|
+
let (plan, authority) = exec_binding_fixture(&workspace);
|
|
3541
|
+
persist_exec_provider_leader_binding(&plan, &workspace, &authority, false).unwrap();
|
|
3542
|
+
let state = crate::state::persist::load_runtime_state(&workspace).unwrap();
|
|
3543
|
+
let before = state.clone();
|
|
3544
|
+
assert!(state["session_name"].is_null());
|
|
3545
|
+
assert!(state["teams"]["current"]["session_name"].is_null());
|
|
3546
|
+
assert_eq!(state["teams"]["current"]["leader_receiver"]["session_name"], "cursor-gate-leader");
|
|
3547
|
+
assert!(!crate::lifecycle::launch::quick_start_has_runtime_for_test(&state, "current"));
|
|
3548
|
+
assert_eq!(state, before);
|
|
3549
|
+
std::fs::remove_dir_all(workspace).unwrap();
|
|
3550
|
+
}
|
|
3551
|
+
|
|
3552
|
+
#[test]
|
|
3553
|
+
fn exec_provider_preserves_existing_workers_and_never_clears_runtime_markers() {
|
|
3554
|
+
for status in ["running", "stopped", "unknown"] {
|
|
3555
|
+
let workspace = exec_binding_workspace();
|
|
3556
|
+
let (plan, authority) = exec_binding_fixture(&workspace);
|
|
3557
|
+
let path = crate::state::persist::runtime_state_path(&workspace);
|
|
3558
|
+
std::fs::create_dir_all(path.parent().unwrap()).unwrap();
|
|
3559
|
+
std::fs::write(&path, serde_json::json!({
|
|
3560
|
+
"active_team_key":"current", "session_name":"team-existing-workers",
|
|
3561
|
+
"spec_path":"/fixture/spec.yml", "team_dir":"/fixture/roles",
|
|
3562
|
+
"agents":{"worker":{"agent_id":"worker","provider":"pi","status":status}},
|
|
3563
|
+
"tasks":[{"task_id":"existing-task"}],
|
|
3564
|
+
}).to_string()).unwrap();
|
|
3565
|
+
persist_exec_provider_leader_binding(&plan, &workspace, &authority, false).unwrap();
|
|
3566
|
+
let state = crate::state::persist::load_runtime_state(&workspace).unwrap();
|
|
3567
|
+
for row in [&state, &state["teams"]["current"]] {
|
|
3568
|
+
assert_eq!(row["session_name"], "team-existing-workers");
|
|
3569
|
+
assert_eq!(row["agents"]["worker"]["status"], status);
|
|
3570
|
+
assert_eq!(row["spec_path"], "/fixture/spec.yml");
|
|
3571
|
+
assert_eq!(row["team_dir"], "/fixture/roles");
|
|
3572
|
+
assert_eq!(row["tasks"][0]["task_id"], "existing-task");
|
|
3573
|
+
}
|
|
3574
|
+
// Ownership writers update only the canonical team record, not the
|
|
3575
|
+
// legacy root view that retains the existing worker runtime.
|
|
3576
|
+
assert_eq!(
|
|
3577
|
+
state["teams"]["current"]["leader_receiver"]["session_name"],
|
|
3578
|
+
"cursor-gate-leader"
|
|
3579
|
+
);
|
|
3580
|
+
assert!(state["leader_receiver"].is_null());
|
|
3581
|
+
assert!(crate::lifecycle::launch::quick_start_has_runtime_for_test(&state, "current"));
|
|
3582
|
+
std::fs::remove_dir_all(workspace).unwrap();
|
|
3583
|
+
}
|
|
3584
|
+
}
|
|
3585
|
+
|
|
3368
3586
|
#[test]
|
|
3369
3587
|
#[serial_test::serial(env)]
|
|
3370
3588
|
fn default_exec_provider_persists_current_pane_binding_before_provider_exec() {
|
|
@@ -3402,6 +3620,8 @@ mod tests {
|
|
|
3402
3620
|
session_name: None,
|
|
3403
3621
|
argv: vec!["sh".to_string(), "-c".to_string(), command],
|
|
3404
3622
|
provider_argv: vec!["fake".to_string()],
|
|
3623
|
+
leader_prompt: None,
|
|
3624
|
+
session_capture: None,
|
|
3405
3625
|
leader_window: None,
|
|
3406
3626
|
is_external_leader: false,
|
|
3407
3627
|
leader_env: BTreeMap::new(),
|
|
@@ -3931,6 +4151,8 @@ mod tests {
|
|
|
3931
4151
|
session_name: Some(SessionName::new("team-agent-leader-claude_code-demo")),
|
|
3932
4152
|
argv: vec!["tmux".to_string(), "attach-session".to_string()],
|
|
3933
4153
|
provider_argv: vec!["claude".to_string()],
|
|
4154
|
+
leader_prompt: None,
|
|
4155
|
+
session_capture: None,
|
|
3934
4156
|
leader_window: Some(WindowName::new("claude_code")),
|
|
3935
4157
|
is_external_leader: false,
|
|
3936
4158
|
leader_env: BTreeMap::new(),
|
|
@@ -3939,6 +4161,74 @@ mod tests {
|
|
|
3939
4161
|
}
|
|
3940
4162
|
}
|
|
3941
4163
|
|
|
4164
|
+
#[cfg(unix)]
|
|
4165
|
+
#[test]
|
|
4166
|
+
fn leader_prompt_transport_capture_hides_argv_echo_without_muting_native_exec() {
|
|
4167
|
+
const TEST: &str = "leader::start::tests::leader_prompt_transport_capture_hides_argv_echo_without_muting_native_exec";
|
|
4168
|
+
const KEY: &str = "TEAM_AGENT_TEST_PROMPT_CAPTURE_CASE";
|
|
4169
|
+
const BODY: &str = "SENSITIVE_PROMPT_309_CAPTURE";
|
|
4170
|
+
if let Ok(case) = std::env::var(KEY) {
|
|
4171
|
+
let root = std::env::temp_dir().join(format!("ta-prompt-capture-{}-{case}", std::process::id()));
|
|
4172
|
+
std::fs::create_dir_all(&root).unwrap();
|
|
4173
|
+
let mut plan = persist_test_plan(&root);
|
|
4174
|
+
plan.mode = if case == "exec_configured" { LeaderStartMode::ExecProvider } else { LeaderStartMode::NewTmuxSession };
|
|
4175
|
+
if case != "transport_absent" {
|
|
4176
|
+
plan.leader_prompt = Some(crate::leader::prompt::PromptMetadata {
|
|
4177
|
+
config_path: root.join("leader-prompt.txt"), carrier: "--append-system-prompt", bytes: BODY.len(),
|
|
4178
|
+
});
|
|
4179
|
+
}
|
|
4180
|
+
// Controlled launcher echoes its received argv and fails; no native Agent/Team.
|
|
4181
|
+
let argv = vec!["/bin/sh".to_string(), "-c".to_string(),
|
|
4182
|
+
"printf '%s\\n' \"$1\"; printf '%s\\n' \"$1\" >&2; exit 23".to_string(), "launcher".to_string(), BODY.to_string()];
|
|
4183
|
+
let outcome = super::run_leader_argv(&argv, &BTreeMap::new(), &plan, &root, None).unwrap();
|
|
4184
|
+
assert_eq!(outcome.status.code(), Some(23));
|
|
4185
|
+
if plan.leader_prompt.is_some() { assert!(!outcome.stderr.contains(BODY)); }
|
|
4186
|
+
std::fs::remove_dir_all(root).unwrap();
|
|
4187
|
+
return;
|
|
4188
|
+
}
|
|
4189
|
+
for case in ["transport_configured", "transport_absent", "exec_configured"] {
|
|
4190
|
+
let output = std::process::Command::new(std::env::current_exe().unwrap())
|
|
4191
|
+
.args(["--exact", TEST, "--nocapture", "--test-threads=1"])
|
|
4192
|
+
.env(KEY, case).env_remove("TMUX").env_remove("TMUX_PANE")
|
|
4193
|
+
.output().unwrap();
|
|
4194
|
+
assert!(output.status.success(), "controlled child failed: {:?} {}", output.status, String::from_utf8_lossy(&output.stderr));
|
|
4195
|
+
for bytes in [&output.stdout, &output.stderr] {
|
|
4196
|
+
assert_eq!(String::from_utf8_lossy(bytes).contains(BODY), case != "transport_configured", "capture policy differs for {case}");
|
|
4197
|
+
}
|
|
4198
|
+
}
|
|
4199
|
+
}
|
|
4200
|
+
|
|
4201
|
+
#[test]
|
|
4202
|
+
fn leader_prompt_diagnostics_hide_payload_but_keep_unconfigured_baseline_and_native_cause() {
|
|
4203
|
+
let root = std::env::temp_dir().join(format!("ta-prompt-diagnostics-{}", std::process::id()));
|
|
4204
|
+
std::fs::create_dir_all(&root).unwrap();
|
|
4205
|
+
let path = root.join("launcher.log");
|
|
4206
|
+
let mut plan = persist_test_plan(&root);
|
|
4207
|
+
let plain = vec!["tmux".to_string(), "attach-session".to_string()];
|
|
4208
|
+
super::write_launcher_diagnostics_header(&path, &plan, &plain).unwrap();
|
|
4209
|
+
assert_eq!(std::fs::read_to_string(&path).unwrap(),
|
|
4210
|
+
"startup_stage=prepare\nmode=ManagedTmuxClient\nprovider=claude_code\nargv=tmux attach-session\n");
|
|
4211
|
+
const BODY: &str = "LEADER_ONLY_SENSITIVE_309";
|
|
4212
|
+
plan.leader_prompt = Some(crate::leader::prompt::PromptMetadata {
|
|
4213
|
+
config_path: root.join("leader-prompt.txt"), carrier: "--append-system-prompt", bytes: BODY.len(),
|
|
4214
|
+
});
|
|
4215
|
+
let shell = vec!["tmux".to_string(), "new-session".to_string(),
|
|
4216
|
+
format!("claude --append-system-prompt '{BODY}'")];
|
|
4217
|
+
super::write_launcher_diagnostics_header(&path, &plan, &shell).unwrap();
|
|
4218
|
+
let header = std::fs::read_to_string(&path).unwrap();
|
|
4219
|
+
assert!(!header.contains(BODY));
|
|
4220
|
+
assert!(header.contains("leader_prompt={") && header.contains("--append-system-prompt"));
|
|
4221
|
+
assert!(!super::prompt_aware_diagnostic_text(&plan, BODY).contains(BODY));
|
|
4222
|
+
let error = super::prompt_aware_spawn_error(&plan, TransportError::Subprocess {
|
|
4223
|
+
argv: shell, code: Some(7), stderr: BODY.to_string(),
|
|
4224
|
+
});
|
|
4225
|
+
let crate::leader::LeaderError::Prompt(error) = error else { panic!("typed carrier failure required") };
|
|
4226
|
+
assert_eq!(error.reason, "leader_prompt_carrier_failed");
|
|
4227
|
+
assert_eq!(error.exit_code, Some(7));
|
|
4228
|
+
assert!(!error.report().to_string().contains(BODY));
|
|
4229
|
+
std::fs::remove_dir_all(root).unwrap();
|
|
4230
|
+
}
|
|
4231
|
+
|
|
3942
4232
|
#[test]
|
|
3943
4233
|
fn multi_leader_window_keeps_base_when_free() {
|
|
3944
4234
|
let window =
|
|
@@ -4020,6 +4310,48 @@ mod tests {
|
|
|
4020
4310
|
let _ = std::fs::remove_dir_all(&workspace);
|
|
4021
4311
|
}
|
|
4022
4312
|
|
|
4313
|
+
#[test]
|
|
4314
|
+
fn pi_managed_leader_persists_native_identity_with_the_canonical_pane() {
|
|
4315
|
+
let workspace = std::env::temp_dir().join(format!(
|
|
4316
|
+
"ta-pi-leader-native-binding-{}", std::process::id()
|
|
4317
|
+
));
|
|
4318
|
+
std::fs::create_dir_all(workspace.join(".team/runtime")).unwrap();
|
|
4319
|
+
let mut plan = persist_test_plan(&workspace);
|
|
4320
|
+
plan.provider = Provider::Pi;
|
|
4321
|
+
plan.session_capture = Some(crate::provider::CaptureSessionContext {
|
|
4322
|
+
agent_id: "leader".into(),
|
|
4323
|
+
spawn_cwd: workspace.clone(),
|
|
4324
|
+
pane_id: None,
|
|
4325
|
+
pane_pid: None,
|
|
4326
|
+
spawned_at: Some("2026-10-10T03:59:07Z".into()),
|
|
4327
|
+
expected_session_id: Some(crate::provider::SessionId::new("native-first")),
|
|
4328
|
+
provider_projects_root: Some(workspace.join("pi-sessions")),
|
|
4329
|
+
});
|
|
4330
|
+
let mut pane = SpawnResult {
|
|
4331
|
+
pane_id: PaneId::new("%0"),
|
|
4332
|
+
session: SessionName::new("team-agent-leader-pi-demo"),
|
|
4333
|
+
window: WindowName::new("pi"),
|
|
4334
|
+
child_pid: Some(1),
|
|
4335
|
+
};
|
|
4336
|
+
persist_managed_leader_binding(&plan, &workspace, &pane).unwrap();
|
|
4337
|
+
let first = crate::state::persist::load_runtime_state(&workspace).unwrap();
|
|
4338
|
+
let receiver = &first["teams"]["current"]["leader_receiver"];
|
|
4339
|
+
let native = &receiver["native_session"];
|
|
4340
|
+
assert_eq!(native["expected_session_id"], "native-first");
|
|
4341
|
+
assert_ne!(native["expected_session_id"], receiver["leader_session_uuid"]);
|
|
4342
|
+
assert_eq!(native["provider_projects_root"], serde_json::json!(workspace.join("pi-sessions")));
|
|
4343
|
+
for key in ["pane_id", "tmux_socket", "leader_session_uuid", "owner_epoch"] {
|
|
4344
|
+
assert_eq!(native[key], receiver[key]);
|
|
4345
|
+
}
|
|
4346
|
+
pane.pane_id = PaneId::new("%1");
|
|
4347
|
+
plan.session_capture.as_mut().unwrap().expected_session_id =
|
|
4348
|
+
Some(crate::provider::SessionId::new("native-second"));
|
|
4349
|
+
persist_managed_leader_binding(&plan, &workspace, &pane).unwrap();
|
|
4350
|
+
let second = crate::state::persist::load_runtime_state(&workspace).unwrap();
|
|
4351
|
+
assert_eq!(second["teams"]["current"]["leader_receiver"]["native_session"], *native);
|
|
4352
|
+
let _ = std::fs::remove_dir_all(workspace);
|
|
4353
|
+
}
|
|
4354
|
+
|
|
4023
4355
|
#[test]
|
|
4024
4356
|
fn multi_leader_persist_keeps_first_owner_uuid() {
|
|
4025
4357
|
let workspace = std::env::temp_dir().join(format!(
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
#![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
|
|
2
2
|
use super::*;
|
|
3
3
|
|
|
4
|
+
mod prompt_contract;
|
|
5
|
+
|
|
4
6
|
// ── helpers ────────────────────────────────────────────────────────────
|
|
5
7
|
/// derive 一个真 LeaderSessionUuid(骨架无公开裸构造器,仅 `derive`)。
|
|
6
8
|
fn uuid(fp: &str, ws: &str, user: &str, team: &str) -> LeaderSessionUuid {
|