@team-agent/installer 0.5.113 → 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 +28 -15
- package/crates/team-agent/src/cli/diagnose.rs +36 -13
- package/crates/team-agent/src/cli/emit.rs +55 -20
- 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 +152 -8
- package/crates/team-agent/src/cli/models.rs +68 -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 +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 +260 -16
- 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 +472 -388
- 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/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(),
|
|
@@ -2421,6 +2518,11 @@ fn run_leader_argv(
|
|
|
2421
2518
|
diagnostics_path.display()
|
|
2422
2519
|
))
|
|
2423
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;
|
|
2424
2526
|
let stdout_reader = child.stdout.take().map(|mut child_stdout| {
|
|
2425
2527
|
std::thread::spawn(move || {
|
|
2426
2528
|
let mut captured = Vec::new();
|
|
@@ -2432,7 +2534,9 @@ fn run_leader_argv(
|
|
|
2432
2534
|
if read == 0 {
|
|
2433
2535
|
break;
|
|
2434
2536
|
}
|
|
2435
|
-
|
|
2537
|
+
if !withhold_transport_output {
|
|
2538
|
+
let _ = std::io::stdout().write_all(&buf[..read]);
|
|
2539
|
+
}
|
|
2436
2540
|
captured.extend_from_slice(&buf[..read]);
|
|
2437
2541
|
}
|
|
2438
2542
|
captured
|
|
@@ -2449,7 +2553,9 @@ fn run_leader_argv(
|
|
|
2449
2553
|
if read == 0 {
|
|
2450
2554
|
break;
|
|
2451
2555
|
}
|
|
2452
|
-
|
|
2556
|
+
if !withhold_transport_output {
|
|
2557
|
+
let _ = std::io::stderr().write_all(&buf[..read]);
|
|
2558
|
+
}
|
|
2453
2559
|
captured.extend_from_slice(&buf[..read]);
|
|
2454
2560
|
}
|
|
2455
2561
|
captured
|
|
@@ -2487,8 +2593,8 @@ fn run_leader_argv(
|
|
|
2487
2593
|
.code()
|
|
2488
2594
|
.map(|code| code.to_string())
|
|
2489
2595
|
.unwrap_or_else(|| "signal".to_string()),
|
|
2490
|
-
|
|
2491
|
-
|
|
2596
|
+
prompt_aware_diagnostic_text(plan, &stdout),
|
|
2597
|
+
prompt_aware_diagnostic_text(plan, &stderr),
|
|
2492
2598
|
),
|
|
2493
2599
|
)
|
|
2494
2600
|
.map_err(|error| {
|
|
@@ -2497,6 +2603,11 @@ fn run_leader_argv(
|
|
|
2497
2603
|
diagnostics_path.display()
|
|
2498
2604
|
))
|
|
2499
2605
|
})?;
|
|
2606
|
+
let stderr = if plan.leader_prompt.is_some() {
|
|
2607
|
+
prompt_aware_diagnostic_text(plan, &stderr)
|
|
2608
|
+
} else {
|
|
2609
|
+
stderr
|
|
2610
|
+
};
|
|
2500
2611
|
Ok(LeaderProcessExit {
|
|
2501
2612
|
status,
|
|
2502
2613
|
stderr,
|
|
@@ -2591,6 +2702,14 @@ fn launcher_diagnostics_path(workspace: &Path) -> PathBuf {
|
|
|
2591
2702
|
))
|
|
2592
2703
|
}
|
|
2593
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
|
+
|
|
2594
2713
|
fn write_launcher_diagnostics_header(
|
|
2595
2714
|
path: &Path,
|
|
2596
2715
|
plan: &LeaderStartPlan,
|
|
@@ -2599,13 +2718,22 @@ fn write_launcher_diagnostics_header(
|
|
|
2599
2718
|
if let Some(parent) = path.parent() {
|
|
2600
2719
|
std::fs::create_dir_all(parent)?;
|
|
2601
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();
|
|
2602
2729
|
std::fs::write(
|
|
2603
2730
|
path,
|
|
2604
2731
|
format!(
|
|
2605
|
-
"startup_stage=prepare\nmode={:?}\nprovider={}\nargv={}\n",
|
|
2732
|
+
"startup_stage=prepare\nmode={:?}\nprovider={}\nargv={}\n{}",
|
|
2606
2733
|
plan.mode,
|
|
2607
2734
|
provider_wire(plan.provider),
|
|
2608
|
-
|
|
2735
|
+
prompt_aware_diagnostic_text(plan, &argv.join(" ")),
|
|
2736
|
+
metadata,
|
|
2609
2737
|
),
|
|
2610
2738
|
)
|
|
2611
2739
|
}
|
|
@@ -3345,6 +3473,8 @@ mod tests {
|
|
|
3345
3473
|
session_name: None,
|
|
3346
3474
|
argv: vec!["sh".to_string(), "-c".to_string(), command],
|
|
3347
3475
|
provider_argv: vec!["codex".to_string()],
|
|
3476
|
+
leader_prompt: None,
|
|
3477
|
+
session_capture: None,
|
|
3348
3478
|
leader_window: None,
|
|
3349
3479
|
is_external_leader: true,
|
|
3350
3480
|
leader_env: BTreeMap::new(),
|
|
@@ -3402,6 +3532,8 @@ mod tests {
|
|
|
3402
3532
|
session_name: None,
|
|
3403
3533
|
argv: vec!["sh".to_string(), "-c".to_string(), command],
|
|
3404
3534
|
provider_argv: vec!["fake".to_string()],
|
|
3535
|
+
leader_prompt: None,
|
|
3536
|
+
session_capture: None,
|
|
3405
3537
|
leader_window: None,
|
|
3406
3538
|
is_external_leader: false,
|
|
3407
3539
|
leader_env: BTreeMap::new(),
|
|
@@ -3931,6 +4063,8 @@ mod tests {
|
|
|
3931
4063
|
session_name: Some(SessionName::new("team-agent-leader-claude_code-demo")),
|
|
3932
4064
|
argv: vec!["tmux".to_string(), "attach-session".to_string()],
|
|
3933
4065
|
provider_argv: vec!["claude".to_string()],
|
|
4066
|
+
leader_prompt: None,
|
|
4067
|
+
session_capture: None,
|
|
3934
4068
|
leader_window: Some(WindowName::new("claude_code")),
|
|
3935
4069
|
is_external_leader: false,
|
|
3936
4070
|
leader_env: BTreeMap::new(),
|
|
@@ -3939,6 +4073,74 @@ mod tests {
|
|
|
3939
4073
|
}
|
|
3940
4074
|
}
|
|
3941
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
|
+
|
|
3942
4144
|
#[test]
|
|
3943
4145
|
fn multi_leader_window_keeps_base_when_free() {
|
|
3944
4146
|
let window =
|
|
@@ -4020,6 +4222,48 @@ mod tests {
|
|
|
4020
4222
|
let _ = std::fs::remove_dir_all(&workspace);
|
|
4021
4223
|
}
|
|
4022
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
|
+
|
|
4023
4267
|
#[test]
|
|
4024
4268
|
fn multi_leader_persist_keeps_first_owner_uuid() {
|
|
4025
4269
|
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 {
|