@team-agent/installer 0.5.112 → 0.5.114
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 +119 -50
- package/crates/team-agent/src/cli/diagnose.rs +36 -13
- package/crates/team-agent/src/cli/emit.rs +173 -109
- 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/leaders.rs +32 -32
- package/crates/team-agent/src/cli/mod.rs +158 -14
- package/crates/team-agent/src/cli/models.rs +68 -18
- package/crates/team-agent/src/cli/named_address.rs +17 -16
- package/crates/team-agent/src/cli/route.rs +4 -4
- package/crates/team-agent/src/cli/send/presentation.rs +19 -18
- package/crates/team-agent/src/cli/send/resolve.rs +11 -11
- package/crates/team-agent/src/cli/send.rs +4 -4
- package/crates/team-agent/src/cli/spec.rs +35 -29
- package/crates/team-agent/src/cli/status.rs +7 -7
- package/crates/team-agent/src/cli/status_port/brief.rs +268 -101
- package/crates/team-agent/src/cli/tests/base.rs +94 -6
- package/crates/team-agent/src/cli/tests/divergence.rs +3 -2
- package/crates/team-agent/src/cli/tests/lane_c.rs +1 -1
- package/crates/team-agent/src/cli/tests/named_address.rs +3 -3
- package/crates/team-agent/src/cli/tests/status_send.rs +9 -7
- package/crates/team-agent/src/cli/triage.rs +53 -2
- package/crates/team-agent/src/cli/types.rs +8 -8
- package/crates/team-agent/src/compiler/tests.rs +12 -0
- package/crates/team-agent/src/compiler.rs +65 -26
- package/crates/team-agent/src/contract_runtime/backend.rs +646 -0
- package/crates/team-agent/src/contract_runtime/config.rs +309 -0
- package/crates/team-agent/src/contract_runtime/discovery.rs +237 -0
- package/crates/team-agent/src/contract_runtime/forward.rs +229 -0
- package/crates/team-agent/src/contract_runtime/framework.rs +261 -0
- package/crates/team-agent/src/contract_runtime/mcp.rs +396 -0
- package/crates/team-agent/src/contract_runtime/mod.rs +14 -0
- package/crates/team-agent/src/contract_runtime/prompt.rs +216 -0
- package/crates/team-agent/src/contract_runtime/pump.rs +207 -0
- package/crates/team-agent/src/contract_runtime/registry.rs +30 -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 +265 -16
- package/crates/team-agent/src/leader/tests/identity.rs +21 -0
- 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/identity.rs +172 -13
- package/crates/team-agent/src/lifecycle/launch/quick_start.rs +760 -365
- package/crates/team-agent/src/lifecycle/launch/spawn.rs +38 -9
- package/crates/team-agent/src/lifecycle/launch/state_projection.rs +17 -0
- package/crates/team-agent/src/lifecycle/launch.rs +39 -28
- package/crates/team-agent/src/lifecycle/profile_launch.rs +58 -46
- package/crates/team-agent/src/lifecycle/restart/rebuild.rs +97 -52
- package/crates/team-agent/src/lifecycle/restart.rs +1 -1
- package/crates/team-agent/src/lifecycle/tests/stale_team_saveconflict_contract.rs +3 -2
- 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/provider/adapter.rs +8 -59
- package/crates/team-agent/src/provider/adapters/claude.rs +6 -14
- package/crates/team-agent/src/provider/adapters/codex.rs +8 -18
- package/crates/team-agent/src/provider/adapters/copilot.rs +5 -22
- package/crates/team-agent/src/provider/adapters/cursor_agent.rs +8 -45
- package/crates/team-agent/src/provider/adapters/grok.rs +7 -31
- package/crates/team-agent/src/provider/adapters/pi.rs +7 -18
- package/crates/team-agent/src/provider/command_helpers.rs +45 -0
- package/crates/team-agent/src/provider/mod.rs +1 -0
- package/crates/team-agent/src/provider/model_catalog.rs +21 -28
- package/crates/team-agent/src/provider/tests/adapter.rs +70 -0
- package/crates/team-agent/src/provider/tests/command_helpers.rs +55 -0
- 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.
|
|
@@ -403,6 +444,11 @@ pub(crate) fn leader_env_for_identity(
|
|
|
403
444
|
"1".to_string(),
|
|
404
445
|
);
|
|
405
446
|
}
|
|
447
|
+
// Managed providers run after `sh -lc`; restore the launcher's native
|
|
448
|
+
// executable lookup path at invocation, not just in the tmux client env.
|
|
449
|
+
if let Some(path) = std::env::var_os("PATH").and_then(|path| path.into_string().ok()) {
|
|
450
|
+
leader_env.insert("PATH".to_string(), path);
|
|
451
|
+
}
|
|
406
452
|
leader_env
|
|
407
453
|
}
|
|
408
454
|
|
|
@@ -1340,7 +1386,7 @@ fn ensure_managed_leader_pane(
|
|
|
1340
1386
|
&env_unset,
|
|
1341
1387
|
provider_label,
|
|
1342
1388
|
)
|
|
1343
|
-
.map_err(|error|
|
|
1389
|
+
.map_err(|error| prompt_aware_spawn_error(plan, error))
|
|
1344
1390
|
} else {
|
|
1345
1391
|
let env_unset = leader_env_unset_for_provider(plan.provider);
|
|
1346
1392
|
let provider_label = provider_command_name(plan.provider);
|
|
@@ -1354,8 +1400,35 @@ fn ensure_managed_leader_pane(
|
|
|
1354
1400
|
&env_unset,
|
|
1355
1401
|
provider_label,
|
|
1356
1402
|
)
|
|
1357
|
-
.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
|
+
_ => {}
|
|
1358
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)
|
|
1359
1432
|
}
|
|
1360
1433
|
|
|
1361
1434
|
struct ManagedAttachWatch<'a> {
|
|
@@ -1735,6 +1808,25 @@ fn managed_spawned_pane_in_targets(transport: &dyn Transport, spawned: &SpawnRes
|
|
|
1735
1808
|
})
|
|
1736
1809
|
}
|
|
1737
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
|
+
|
|
1738
1830
|
fn persist_managed_leader_binding(
|
|
1739
1831
|
plan: &LeaderStartPlan,
|
|
1740
1832
|
workspace: &Path,
|
|
@@ -1850,7 +1942,7 @@ fn persist_managed_leader_binding(
|
|
|
1850
1942
|
} else {
|
|
1851
1943
|
serde_json::json!("managed_launcher")
|
|
1852
1944
|
};
|
|
1853
|
-
let receiver = serde_json::json!({
|
|
1945
|
+
let mut receiver = serde_json::json!({
|
|
1854
1946
|
"mode": "direct_tmux",
|
|
1855
1947
|
"status": "attached",
|
|
1856
1948
|
"provider": provider.clone(),
|
|
@@ -1865,6 +1957,15 @@ fn persist_managed_leader_binding(
|
|
|
1865
1957
|
"discovery": discovery,
|
|
1866
1958
|
"bound_panes": bound_panes,
|
|
1867
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
|
+
}
|
|
1868
1969
|
let owner_uuid = if keep_existing_scalar {
|
|
1869
1970
|
keep_field(
|
|
1870
1971
|
existing_owner.as_ref(),
|
|
@@ -2023,6 +2124,7 @@ fn persist_exec_provider_leader_binding(
|
|
|
2023
2124
|
}
|
|
2024
2125
|
obj.insert("tmux_socket".to_string(), serde_json::json!(socket));
|
|
2025
2126
|
}
|
|
2127
|
+
bind_plan_session(&mut receiver, plan);
|
|
2026
2128
|
let owner = serde_json::json!({
|
|
2027
2129
|
"pane_id": pane,
|
|
2028
2130
|
"provider": provider.clone(),
|
|
@@ -2416,6 +2518,11 @@ fn run_leader_argv(
|
|
|
2416
2518
|
diagnostics_path.display()
|
|
2417
2519
|
))
|
|
2418
2520
|
})?;
|
|
2521
|
+
// Captured transport diagnostics can echo the shell argv carrying the
|
|
2522
|
+
// prompt. Do not tee those bytes before redaction. Native ExecProvider
|
|
2523
|
+
// business streams and inherited TTY stdout keep their existing behavior.
|
|
2524
|
+
let withhold_transport_output = plan.leader_prompt.is_some()
|
|
2525
|
+
&& plan.mode != LeaderStartMode::ExecProvider;
|
|
2419
2526
|
let stdout_reader = child.stdout.take().map(|mut child_stdout| {
|
|
2420
2527
|
std::thread::spawn(move || {
|
|
2421
2528
|
let mut captured = Vec::new();
|
|
@@ -2427,7 +2534,9 @@ fn run_leader_argv(
|
|
|
2427
2534
|
if read == 0 {
|
|
2428
2535
|
break;
|
|
2429
2536
|
}
|
|
2430
|
-
|
|
2537
|
+
if !withhold_transport_output {
|
|
2538
|
+
let _ = std::io::stdout().write_all(&buf[..read]);
|
|
2539
|
+
}
|
|
2431
2540
|
captured.extend_from_slice(&buf[..read]);
|
|
2432
2541
|
}
|
|
2433
2542
|
captured
|
|
@@ -2444,7 +2553,9 @@ fn run_leader_argv(
|
|
|
2444
2553
|
if read == 0 {
|
|
2445
2554
|
break;
|
|
2446
2555
|
}
|
|
2447
|
-
|
|
2556
|
+
if !withhold_transport_output {
|
|
2557
|
+
let _ = std::io::stderr().write_all(&buf[..read]);
|
|
2558
|
+
}
|
|
2448
2559
|
captured.extend_from_slice(&buf[..read]);
|
|
2449
2560
|
}
|
|
2450
2561
|
captured
|
|
@@ -2482,8 +2593,8 @@ fn run_leader_argv(
|
|
|
2482
2593
|
.code()
|
|
2483
2594
|
.map(|code| code.to_string())
|
|
2484
2595
|
.unwrap_or_else(|| "signal".to_string()),
|
|
2485
|
-
|
|
2486
|
-
|
|
2596
|
+
prompt_aware_diagnostic_text(plan, &stdout),
|
|
2597
|
+
prompt_aware_diagnostic_text(plan, &stderr),
|
|
2487
2598
|
),
|
|
2488
2599
|
)
|
|
2489
2600
|
.map_err(|error| {
|
|
@@ -2492,6 +2603,11 @@ fn run_leader_argv(
|
|
|
2492
2603
|
diagnostics_path.display()
|
|
2493
2604
|
))
|
|
2494
2605
|
})?;
|
|
2606
|
+
let stderr = if plan.leader_prompt.is_some() {
|
|
2607
|
+
prompt_aware_diagnostic_text(plan, &stderr)
|
|
2608
|
+
} else {
|
|
2609
|
+
stderr
|
|
2610
|
+
};
|
|
2495
2611
|
Ok(LeaderProcessExit {
|
|
2496
2612
|
status,
|
|
2497
2613
|
stderr,
|
|
@@ -2586,6 +2702,14 @@ fn launcher_diagnostics_path(workspace: &Path) -> PathBuf {
|
|
|
2586
2702
|
))
|
|
2587
2703
|
}
|
|
2588
2704
|
|
|
2705
|
+
fn prompt_aware_diagnostic_text(plan: &LeaderStartPlan, text: &str) -> String {
|
|
2706
|
+
if plan.leader_prompt.is_some() {
|
|
2707
|
+
"[withheld: Leader prompt-bearing launch]".to_string()
|
|
2708
|
+
} else {
|
|
2709
|
+
crate::redaction::redact_external_text(text)
|
|
2710
|
+
}
|
|
2711
|
+
}
|
|
2712
|
+
|
|
2589
2713
|
fn write_launcher_diagnostics_header(
|
|
2590
2714
|
path: &Path,
|
|
2591
2715
|
plan: &LeaderStartPlan,
|
|
@@ -2594,13 +2718,22 @@ fn write_launcher_diagnostics_header(
|
|
|
2594
2718
|
if let Some(parent) = path.parent() {
|
|
2595
2719
|
std::fs::create_dir_all(parent)?;
|
|
2596
2720
|
}
|
|
2721
|
+
let metadata = plan
|
|
2722
|
+
.leader_prompt
|
|
2723
|
+
.as_ref()
|
|
2724
|
+
.map(serde_json::to_string)
|
|
2725
|
+
.transpose()
|
|
2726
|
+
.map_err(|error| std::io::Error::new(std::io::ErrorKind::InvalidData, error))?
|
|
2727
|
+
.map(|metadata| format!("leader_prompt={metadata}\n"))
|
|
2728
|
+
.unwrap_or_default();
|
|
2597
2729
|
std::fs::write(
|
|
2598
2730
|
path,
|
|
2599
2731
|
format!(
|
|
2600
|
-
"startup_stage=prepare\nmode={:?}\nprovider={}\nargv={}\n",
|
|
2732
|
+
"startup_stage=prepare\nmode={:?}\nprovider={}\nargv={}\n{}",
|
|
2601
2733
|
plan.mode,
|
|
2602
2734
|
provider_wire(plan.provider),
|
|
2603
|
-
|
|
2735
|
+
prompt_aware_diagnostic_text(plan, &argv.join(" ")),
|
|
2736
|
+
metadata,
|
|
2604
2737
|
),
|
|
2605
2738
|
)
|
|
2606
2739
|
}
|
|
@@ -3340,6 +3473,8 @@ mod tests {
|
|
|
3340
3473
|
session_name: None,
|
|
3341
3474
|
argv: vec!["sh".to_string(), "-c".to_string(), command],
|
|
3342
3475
|
provider_argv: vec!["codex".to_string()],
|
|
3476
|
+
leader_prompt: None,
|
|
3477
|
+
session_capture: None,
|
|
3343
3478
|
leader_window: None,
|
|
3344
3479
|
is_external_leader: true,
|
|
3345
3480
|
leader_env: BTreeMap::new(),
|
|
@@ -3397,6 +3532,8 @@ mod tests {
|
|
|
3397
3532
|
session_name: None,
|
|
3398
3533
|
argv: vec!["sh".to_string(), "-c".to_string(), command],
|
|
3399
3534
|
provider_argv: vec!["fake".to_string()],
|
|
3535
|
+
leader_prompt: None,
|
|
3536
|
+
session_capture: None,
|
|
3400
3537
|
leader_window: None,
|
|
3401
3538
|
is_external_leader: false,
|
|
3402
3539
|
leader_env: BTreeMap::new(),
|
|
@@ -3926,6 +4063,8 @@ mod tests {
|
|
|
3926
4063
|
session_name: Some(SessionName::new("team-agent-leader-claude_code-demo")),
|
|
3927
4064
|
argv: vec!["tmux".to_string(), "attach-session".to_string()],
|
|
3928
4065
|
provider_argv: vec!["claude".to_string()],
|
|
4066
|
+
leader_prompt: None,
|
|
4067
|
+
session_capture: None,
|
|
3929
4068
|
leader_window: Some(WindowName::new("claude_code")),
|
|
3930
4069
|
is_external_leader: false,
|
|
3931
4070
|
leader_env: BTreeMap::new(),
|
|
@@ -3934,6 +4073,74 @@ mod tests {
|
|
|
3934
4073
|
}
|
|
3935
4074
|
}
|
|
3936
4075
|
|
|
4076
|
+
#[cfg(unix)]
|
|
4077
|
+
#[test]
|
|
4078
|
+
fn leader_prompt_transport_capture_hides_argv_echo_without_muting_native_exec() {
|
|
4079
|
+
const TEST: &str = "leader::start::tests::leader_prompt_transport_capture_hides_argv_echo_without_muting_native_exec";
|
|
4080
|
+
const KEY: &str = "TEAM_AGENT_TEST_PROMPT_CAPTURE_CASE";
|
|
4081
|
+
const BODY: &str = "SENSITIVE_PROMPT_309_CAPTURE";
|
|
4082
|
+
if let Ok(case) = std::env::var(KEY) {
|
|
4083
|
+
let root = std::env::temp_dir().join(format!("ta-prompt-capture-{}-{case}", std::process::id()));
|
|
4084
|
+
std::fs::create_dir_all(&root).unwrap();
|
|
4085
|
+
let mut plan = persist_test_plan(&root);
|
|
4086
|
+
plan.mode = if case == "exec_configured" { LeaderStartMode::ExecProvider } else { LeaderStartMode::NewTmuxSession };
|
|
4087
|
+
if case != "transport_absent" {
|
|
4088
|
+
plan.leader_prompt = Some(crate::leader::prompt::PromptMetadata {
|
|
4089
|
+
config_path: root.join("leader-prompt.txt"), carrier: "--append-system-prompt", bytes: BODY.len(),
|
|
4090
|
+
});
|
|
4091
|
+
}
|
|
4092
|
+
// Controlled launcher echoes its received argv and fails; no native Agent/Team.
|
|
4093
|
+
let argv = vec!["/bin/sh".to_string(), "-c".to_string(),
|
|
4094
|
+
"printf '%s\\n' \"$1\"; printf '%s\\n' \"$1\" >&2; exit 23".to_string(), "launcher".to_string(), BODY.to_string()];
|
|
4095
|
+
let outcome = super::run_leader_argv(&argv, &BTreeMap::new(), &plan, &root, None).unwrap();
|
|
4096
|
+
assert_eq!(outcome.status.code(), Some(23));
|
|
4097
|
+
if plan.leader_prompt.is_some() { assert!(!outcome.stderr.contains(BODY)); }
|
|
4098
|
+
std::fs::remove_dir_all(root).unwrap();
|
|
4099
|
+
return;
|
|
4100
|
+
}
|
|
4101
|
+
for case in ["transport_configured", "transport_absent", "exec_configured"] {
|
|
4102
|
+
let output = std::process::Command::new(std::env::current_exe().unwrap())
|
|
4103
|
+
.args(["--exact", TEST, "--nocapture", "--test-threads=1"])
|
|
4104
|
+
.env(KEY, case).env_remove("TMUX").env_remove("TMUX_PANE")
|
|
4105
|
+
.output().unwrap();
|
|
4106
|
+
assert!(output.status.success(), "controlled child failed: {:?} {}", output.status, String::from_utf8_lossy(&output.stderr));
|
|
4107
|
+
for bytes in [&output.stdout, &output.stderr] {
|
|
4108
|
+
assert_eq!(String::from_utf8_lossy(bytes).contains(BODY), case != "transport_configured", "capture policy differs for {case}");
|
|
4109
|
+
}
|
|
4110
|
+
}
|
|
4111
|
+
}
|
|
4112
|
+
|
|
4113
|
+
#[test]
|
|
4114
|
+
fn leader_prompt_diagnostics_hide_payload_but_keep_unconfigured_baseline_and_native_cause() {
|
|
4115
|
+
let root = std::env::temp_dir().join(format!("ta-prompt-diagnostics-{}", std::process::id()));
|
|
4116
|
+
std::fs::create_dir_all(&root).unwrap();
|
|
4117
|
+
let path = root.join("launcher.log");
|
|
4118
|
+
let mut plan = persist_test_plan(&root);
|
|
4119
|
+
let plain = vec!["tmux".to_string(), "attach-session".to_string()];
|
|
4120
|
+
super::write_launcher_diagnostics_header(&path, &plan, &plain).unwrap();
|
|
4121
|
+
assert_eq!(std::fs::read_to_string(&path).unwrap(),
|
|
4122
|
+
"startup_stage=prepare\nmode=ManagedTmuxClient\nprovider=claude_code\nargv=tmux attach-session\n");
|
|
4123
|
+
const BODY: &str = "LEADER_ONLY_SENSITIVE_309";
|
|
4124
|
+
plan.leader_prompt = Some(crate::leader::prompt::PromptMetadata {
|
|
4125
|
+
config_path: root.join("leader-prompt.txt"), carrier: "--append-system-prompt", bytes: BODY.len(),
|
|
4126
|
+
});
|
|
4127
|
+
let shell = vec!["tmux".to_string(), "new-session".to_string(),
|
|
4128
|
+
format!("claude --append-system-prompt '{BODY}'")];
|
|
4129
|
+
super::write_launcher_diagnostics_header(&path, &plan, &shell).unwrap();
|
|
4130
|
+
let header = std::fs::read_to_string(&path).unwrap();
|
|
4131
|
+
assert!(!header.contains(BODY));
|
|
4132
|
+
assert!(header.contains("leader_prompt={") && header.contains("--append-system-prompt"));
|
|
4133
|
+
assert!(!super::prompt_aware_diagnostic_text(&plan, BODY).contains(BODY));
|
|
4134
|
+
let error = super::prompt_aware_spawn_error(&plan, TransportError::Subprocess {
|
|
4135
|
+
argv: shell, code: Some(7), stderr: BODY.to_string(),
|
|
4136
|
+
});
|
|
4137
|
+
let crate::leader::LeaderError::Prompt(error) = error else { panic!("typed carrier failure required") };
|
|
4138
|
+
assert_eq!(error.reason, "leader_prompt_carrier_failed");
|
|
4139
|
+
assert_eq!(error.exit_code, Some(7));
|
|
4140
|
+
assert!(!error.report().to_string().contains(BODY));
|
|
4141
|
+
std::fs::remove_dir_all(root).unwrap();
|
|
4142
|
+
}
|
|
4143
|
+
|
|
3937
4144
|
#[test]
|
|
3938
4145
|
fn multi_leader_window_keeps_base_when_free() {
|
|
3939
4146
|
let window =
|
|
@@ -4015,6 +4222,48 @@ mod tests {
|
|
|
4015
4222
|
let _ = std::fs::remove_dir_all(&workspace);
|
|
4016
4223
|
}
|
|
4017
4224
|
|
|
4225
|
+
#[test]
|
|
4226
|
+
fn pi_managed_leader_persists_native_identity_with_the_canonical_pane() {
|
|
4227
|
+
let workspace = std::env::temp_dir().join(format!(
|
|
4228
|
+
"ta-pi-leader-native-binding-{}", std::process::id()
|
|
4229
|
+
));
|
|
4230
|
+
std::fs::create_dir_all(workspace.join(".team/runtime")).unwrap();
|
|
4231
|
+
let mut plan = persist_test_plan(&workspace);
|
|
4232
|
+
plan.provider = Provider::Pi;
|
|
4233
|
+
plan.session_capture = Some(crate::provider::CaptureSessionContext {
|
|
4234
|
+
agent_id: "leader".into(),
|
|
4235
|
+
spawn_cwd: workspace.clone(),
|
|
4236
|
+
pane_id: None,
|
|
4237
|
+
pane_pid: None,
|
|
4238
|
+
spawned_at: Some("2026-10-10T03:59:07Z".into()),
|
|
4239
|
+
expected_session_id: Some(crate::provider::SessionId::new("native-first")),
|
|
4240
|
+
provider_projects_root: Some(workspace.join("pi-sessions")),
|
|
4241
|
+
});
|
|
4242
|
+
let mut pane = SpawnResult {
|
|
4243
|
+
pane_id: PaneId::new("%0"),
|
|
4244
|
+
session: SessionName::new("team-agent-leader-pi-demo"),
|
|
4245
|
+
window: WindowName::new("pi"),
|
|
4246
|
+
child_pid: Some(1),
|
|
4247
|
+
};
|
|
4248
|
+
persist_managed_leader_binding(&plan, &workspace, &pane).unwrap();
|
|
4249
|
+
let first = crate::state::persist::load_runtime_state(&workspace).unwrap();
|
|
4250
|
+
let receiver = &first["teams"]["current"]["leader_receiver"];
|
|
4251
|
+
let native = &receiver["native_session"];
|
|
4252
|
+
assert_eq!(native["expected_session_id"], "native-first");
|
|
4253
|
+
assert_ne!(native["expected_session_id"], receiver["leader_session_uuid"]);
|
|
4254
|
+
assert_eq!(native["provider_projects_root"], serde_json::json!(workspace.join("pi-sessions")));
|
|
4255
|
+
for key in ["pane_id", "tmux_socket", "leader_session_uuid", "owner_epoch"] {
|
|
4256
|
+
assert_eq!(native[key], receiver[key]);
|
|
4257
|
+
}
|
|
4258
|
+
pane.pane_id = PaneId::new("%1");
|
|
4259
|
+
plan.session_capture.as_mut().unwrap().expected_session_id =
|
|
4260
|
+
Some(crate::provider::SessionId::new("native-second"));
|
|
4261
|
+
persist_managed_leader_binding(&plan, &workspace, &pane).unwrap();
|
|
4262
|
+
let second = crate::state::persist::load_runtime_state(&workspace).unwrap();
|
|
4263
|
+
assert_eq!(second["teams"]["current"]["leader_receiver"]["native_session"], *native);
|
|
4264
|
+
let _ = std::fs::remove_dir_all(workspace);
|
|
4265
|
+
}
|
|
4266
|
+
|
|
4018
4267
|
#[test]
|
|
4019
4268
|
fn multi_leader_persist_keeps_first_owner_uuid() {
|
|
4020
4269
|
let workspace = std::env::temp_dir().join(format!(
|
|
@@ -291,6 +291,27 @@ fn copilot_leader_env_disables_terminal_title_only_for_copilot() {
|
|
|
291
291
|
}
|
|
292
292
|
}
|
|
293
293
|
|
|
294
|
+
#[test]
|
|
295
|
+
#[serial_test::serial(env)]
|
|
296
|
+
fn leader_env_preserves_native_path_for_managed_shell_wrapper() {
|
|
297
|
+
let _lock = ENV_LOCK.lock().unwrap_or_else(|p| p.into_inner());
|
|
298
|
+
let native_path = "/tmp/native leader/bin:/usr/bin:/bin";
|
|
299
|
+
let _env = EnvGuard::apply(&[("PATH", Some(native_path))]);
|
|
300
|
+
let ws = p2_temp_ws("native_path");
|
|
301
|
+
let identity = leader_identity_context(&ws, None, Some(&serde_json::json!({}))).unwrap();
|
|
302
|
+
let env = leader_env_for_identity(Provider::Pi, &identity);
|
|
303
|
+
assert_eq!(env.get("PATH").map(String::as_str), Some(native_path));
|
|
304
|
+
let command = crate::tmux_backend::leader_shell_wrapper_command(
|
|
305
|
+
&["pi".to_string(), "--name".to_string(), "leader".to_string()],
|
|
306
|
+
&ws, &env, &[], "pi",
|
|
307
|
+
);
|
|
308
|
+
assert!(command.contains("PATH='/tmp/native leader/bin:/usr/bin:/bin'"));
|
|
309
|
+
assert!(command.find("PATH=").unwrap() < command.find(" pi --name leader").unwrap());
|
|
310
|
+
assert_eq!(env.get("TEAM_AGENT_TEAM_ID").map(String::as_str), Some(identity.team_id.as_str()));
|
|
311
|
+
let _unset = EnvGuard::apply(&[("PATH", None)]);
|
|
312
|
+
assert!(!leader_env_for_identity(Provider::Pi, &identity).contains_key("PATH"));
|
|
313
|
+
}
|
|
314
|
+
|
|
294
315
|
// ═══════════════ P2 FIX-LOOP RED (复绿即对抗 cross-model findings) ═══════════════
|
|
295
316
|
// Lock CORRECT Python v0.2.11 leader-identity behavior the contracts missed.
|
|
296
317
|
// Golden re-probed via /tmp/probe_p2_leader.py vs team-agent-public @ 439bef8
|
|
@@ -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 {
|