@team-agent/installer 0.5.73 → 0.5.75
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 +19 -13
- package/crates/team-agent/src/cli/emit.rs +6 -3
- package/crates/team-agent/src/cli/mod.rs +151 -0
- package/crates/team-agent/src/cli/tests/leader_watch.rs +28 -0
- package/crates/team-agent/src/cli/tests/run_delegation.rs +3 -0
- package/crates/team-agent/src/cli/types.rs +6 -0
- package/crates/team-agent/src/compiler.rs +14 -1
- package/crates/team-agent/src/layout/worker_env.rs +366 -0
- package/crates/team-agent/src/leader/lease.rs +334 -53
- package/crates/team-agent/src/leader/registry.rs +177 -108
- package/crates/team-agent/src/lifecycle/launch/pi_mcp.rs +12 -1
- package/crates/team-agent/src/lifecycle/launch/quick_start.rs +2272 -128
- package/crates/team-agent/src/lifecycle/launch/quick_start_transport.rs +16 -6
- package/crates/team-agent/src/lifecycle/launch/spawn.rs +4 -0
- package/crates/team-agent/src/lifecycle/launch/spec_state.rs +138 -13
- package/crates/team-agent/src/lifecycle/launch/state_projection.rs +5 -3
- package/crates/team-agent/src/lifecycle/restart/common.rs +3 -11
- package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +60 -0
- package/crates/team-agent/src/lifecycle/tests/pi_compiler_red.rs +31 -0
- package/crates/team-agent/src/lifecycle/tests/pi_dual_entry_red.rs +25 -23
- package/crates/team-agent/src/lifecycle/tests/worker_spawn_env_red.rs +110 -31
- package/crates/team-agent/src/messaging/delivery.rs +226 -37
- package/crates/team-agent/src/messaging/leader_channel.rs +12 -3
- package/crates/team-agent/src/messaging/tests/runtime.rs +234 -0
- package/crates/team-agent/src/model/spec.rs +81 -7
- package/crates/team-agent/src/provider/adapters/pi.rs +14 -0
- package/crates/team-agent/src/state/persist.rs +433 -4
- package/crates/team-agent/src/state/repository.rs +45 -2
- package/crates/team-agent/src/tmux_backend/tests.rs +414 -7
- package/crates/team-agent/src/tmux_backend.rs +274 -26
- package/crates/team-agent/src/transport/test_support.rs +18 -3
- package/crates/team-agent/src/transport.rs +99 -0
- package/package.json +4 -4
- package/skills/team-agent/SKILL.md +3 -0
- package/skills/team-agent/command-coverage.json +21 -0
- package/skills/team-agent/references/team-agent-operator.md +9 -2
|
@@ -61,13 +61,15 @@ use std::process::Stdio;
|
|
|
61
61
|
use std::time::{Duration, Instant};
|
|
62
62
|
|
|
63
63
|
use crate::model::enums::PaneLiveness;
|
|
64
|
+
use crate::provider::wire::{parse_provider, provider_wire};
|
|
64
65
|
use crate::transport::{
|
|
65
|
-
normalize_capture, tmux_capture_argv, tmux_empty_inject_argv,
|
|
66
|
-
tmux_query_argv, tmux_send_keys_argv, tmux_send_submit_argv,
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
66
|
+
command_basename, normalize_capture, tmux_capture_argv, tmux_empty_inject_argv,
|
|
67
|
+
tmux_inject_text_argv, tmux_query_argv, tmux_send_keys_argv, tmux_send_submit_argv,
|
|
68
|
+
tmux_spawn_argv, AttachOutcome, BackendKind, CaptureRange, CaptureSampleOutcome, CapturedText,
|
|
69
|
+
InjectPayload, InjectReport, InjectStage, InjectVerification, InputSurfaceProbe, Key,
|
|
70
|
+
PaneField, PaneId, PaneInfo, PaneMode, QueryOutcome, SessionName, SetEnvOutcome, SpawnResult,
|
|
71
|
+
SubmitAttemptObservation, SubmitDiagnostics, SubmitObserver, SubmitVerification, Target,
|
|
72
|
+
Transport, TransportError, TurnVerification, WindowName,
|
|
71
73
|
};
|
|
72
74
|
|
|
73
75
|
pub const PANE_BINDING_NONCE_METADATA_KEY: &str = "TEAM_AGENT_PANE_BINDING_NONCE";
|
|
@@ -378,6 +380,63 @@ impl TmuxBackend {
|
|
|
378
380
|
self.run_ok(&argv)
|
|
379
381
|
}
|
|
380
382
|
|
|
383
|
+
/// Set the pane-instance nonce only when the pane option is unset. tmux
|
|
384
|
+
/// serializes commands at the server, so concurrent first claims have one
|
|
385
|
+
/// winner; losers read back that winner instead of persisting a stale nonce.
|
|
386
|
+
pub(crate) fn set_pane_binding_nonce_if_unset(
|
|
387
|
+
&self,
|
|
388
|
+
pane: &PaneId,
|
|
389
|
+
nonce: &str,
|
|
390
|
+
) -> Result<String, TransportError> {
|
|
391
|
+
let argv = self.tmux_argv(&[
|
|
392
|
+
"tmux".to_string(),
|
|
393
|
+
"set-option".to_string(),
|
|
394
|
+
"-p".to_string(),
|
|
395
|
+
"-o".to_string(),
|
|
396
|
+
"-t".to_string(),
|
|
397
|
+
pane.as_str().to_string(),
|
|
398
|
+
TMUX_PANE_BINDING_NONCE_OPTION.to_string(),
|
|
399
|
+
nonce.to_string(),
|
|
400
|
+
]);
|
|
401
|
+
let output = self.runner.run(&argv)?;
|
|
402
|
+
if output.success {
|
|
403
|
+
return Ok(nonce.to_string());
|
|
404
|
+
}
|
|
405
|
+
if output
|
|
406
|
+
.stderr
|
|
407
|
+
.to_ascii_lowercase()
|
|
408
|
+
.contains("already set")
|
|
409
|
+
{
|
|
410
|
+
return self.read_pane_binding_nonce(pane);
|
|
411
|
+
}
|
|
412
|
+
Err(subprocess_error(argv, output))
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
fn read_pane_binding_nonce(&self, pane: &PaneId) -> Result<String, TransportError> {
|
|
416
|
+
let argv = self.tmux_argv(&[
|
|
417
|
+
"tmux".to_string(),
|
|
418
|
+
"show-options".to_string(),
|
|
419
|
+
"-p".to_string(),
|
|
420
|
+
"-v".to_string(),
|
|
421
|
+
"-t".to_string(),
|
|
422
|
+
pane.as_str().to_string(),
|
|
423
|
+
TMUX_PANE_BINDING_NONCE_OPTION.to_string(),
|
|
424
|
+
]);
|
|
425
|
+
let output = self.runner.run(&argv)?;
|
|
426
|
+
if !output.success {
|
|
427
|
+
return Err(subprocess_error(argv, output));
|
|
428
|
+
}
|
|
429
|
+
let nonce = output.stdout.trim();
|
|
430
|
+
if nonce.is_empty() {
|
|
431
|
+
return Err(TransportError::Subprocess {
|
|
432
|
+
argv,
|
|
433
|
+
code: output.code,
|
|
434
|
+
stderr: "pane binding nonce remained unset after conditional write".to_string(),
|
|
435
|
+
});
|
|
436
|
+
}
|
|
437
|
+
Ok(nonce.to_string())
|
|
438
|
+
}
|
|
439
|
+
|
|
381
440
|
/// Backend with an injected runner (tests: canned/recording tmux output). Shared default socket.
|
|
382
441
|
/// ---
|
|
383
442
|
/// purpose: 用注入的 runner 构造后端(测试用录制/罐装 tmux 输出),绑共享默认 socket
|
|
@@ -1389,6 +1448,107 @@ fn pane_from_target(target: &Target) -> PaneId {
|
|
|
1389
1448
|
}
|
|
1390
1449
|
}
|
|
1391
1450
|
|
|
1451
|
+
#[derive(Default)]
|
|
1452
|
+
struct CaptureTally {
|
|
1453
|
+
ok: u32,
|
|
1454
|
+
err: u32,
|
|
1455
|
+
last: CaptureSampleOutcome,
|
|
1456
|
+
}
|
|
1457
|
+
|
|
1458
|
+
impl CaptureTally {
|
|
1459
|
+
fn record_ok(&mut self, text: &str, marker: Option<&str>) {
|
|
1460
|
+
self.ok = self.ok.saturating_add(1);
|
|
1461
|
+
self.last = if marker.is_some_and(|token| text.contains(token)) {
|
|
1462
|
+
CaptureSampleOutcome::OkTokenSeen
|
|
1463
|
+
} else {
|
|
1464
|
+
CaptureSampleOutcome::OkTokenMissing
|
|
1465
|
+
};
|
|
1466
|
+
}
|
|
1467
|
+
|
|
1468
|
+
fn record_err(&mut self) {
|
|
1469
|
+
self.err = self.err.saturating_add(1);
|
|
1470
|
+
self.last = CaptureSampleOutcome::Failed;
|
|
1471
|
+
}
|
|
1472
|
+
|
|
1473
|
+
fn seen_after_ok_samples(&self, visible: bool) -> Option<bool> {
|
|
1474
|
+
if self.ok == 0 {
|
|
1475
|
+
None
|
|
1476
|
+
} else {
|
|
1477
|
+
Some(visible)
|
|
1478
|
+
}
|
|
1479
|
+
}
|
|
1480
|
+
}
|
|
1481
|
+
|
|
1482
|
+
struct InjectTargetSurface {
|
|
1483
|
+
pane_command_basename: Option<String>,
|
|
1484
|
+
pane_command_query: QueryOutcome,
|
|
1485
|
+
input_surface: InputSurfaceProbe,
|
|
1486
|
+
target_pane_id: String,
|
|
1487
|
+
target_pane_query_matched: Option<bool>,
|
|
1488
|
+
}
|
|
1489
|
+
|
|
1490
|
+
fn probe_inject_target_surface(backend: &TmuxBackend, target: &Target) -> InjectTargetSurface {
|
|
1491
|
+
let pane = pane_from_target(target);
|
|
1492
|
+
let (pane_command_basename, pane_command_query) =
|
|
1493
|
+
match backend.query(target, PaneField::PaneCurrentCommand) {
|
|
1494
|
+
Ok(Some(raw)) => match command_basename(&raw) {
|
|
1495
|
+
Some(name) => (Some(name), QueryOutcome::Observed),
|
|
1496
|
+
None => (None, QueryOutcome::Unavailable),
|
|
1497
|
+
},
|
|
1498
|
+
_ => (None, QueryOutcome::Unavailable),
|
|
1499
|
+
};
|
|
1500
|
+
let input_surface = match backend.query(target, PaneField::PaneMode) {
|
|
1501
|
+
Ok(Some(raw)) => match pane_mode_from_raw(Some(raw)) {
|
|
1502
|
+
None => InputSurfaceProbe::Input,
|
|
1503
|
+
Some(PaneMode::Copy) => InputSurfaceProbe::Copy,
|
|
1504
|
+
Some(PaneMode::Tree) => InputSurfaceProbe::Tree,
|
|
1505
|
+
Some(PaneMode::View) => InputSurfaceProbe::View,
|
|
1506
|
+
Some(PaneMode::Client) => InputSurfaceProbe::Client,
|
|
1507
|
+
Some(PaneMode::Unknown) => InputSurfaceProbe::UnknownMode,
|
|
1508
|
+
},
|
|
1509
|
+
_ => InputSurfaceProbe::Unavailable,
|
|
1510
|
+
};
|
|
1511
|
+
let target_pane_query_matched = match backend.query(target, PaneField::PaneId) {
|
|
1512
|
+
Ok(Some(id)) if !id.is_empty() => Some(id == pane.as_str()),
|
|
1513
|
+
_ => None,
|
|
1514
|
+
};
|
|
1515
|
+
InjectTargetSurface {
|
|
1516
|
+
pane_command_basename,
|
|
1517
|
+
pane_command_query,
|
|
1518
|
+
input_surface,
|
|
1519
|
+
target_pane_id: pane.as_str().to_string(),
|
|
1520
|
+
target_pane_query_matched,
|
|
1521
|
+
}
|
|
1522
|
+
}
|
|
1523
|
+
|
|
1524
|
+
fn submit_diagnostics_for_inject(
|
|
1525
|
+
total_elapsed_ms: u64,
|
|
1526
|
+
attempts_detail: Vec<SubmitAttemptObservation>,
|
|
1527
|
+
tally: &CaptureTally,
|
|
1528
|
+
token_seen_after_paste: Option<bool>,
|
|
1529
|
+
token_seen_after_enter: Option<bool>,
|
|
1530
|
+
surface: InjectTargetSurface,
|
|
1531
|
+
) -> SubmitDiagnostics {
|
|
1532
|
+
SubmitDiagnostics {
|
|
1533
|
+
appear_gate_elapsed_ms: 0,
|
|
1534
|
+
appear_gate_matched: false,
|
|
1535
|
+
total_elapsed_ms,
|
|
1536
|
+
attempts_detail,
|
|
1537
|
+
token_seen_before_paste: None,
|
|
1538
|
+
token_seen_after_paste,
|
|
1539
|
+
token_seen_after_enter,
|
|
1540
|
+
capture_ok_count: tally.ok,
|
|
1541
|
+
capture_err_count: tally.err,
|
|
1542
|
+
capture_sample_count: tally.ok.saturating_add(tally.err),
|
|
1543
|
+
last_capture_outcome: tally.last,
|
|
1544
|
+
pane_command_basename: surface.pane_command_basename,
|
|
1545
|
+
pane_command_query: surface.pane_command_query,
|
|
1546
|
+
input_surface: surface.input_surface,
|
|
1547
|
+
target_pane_id: Some(surface.target_pane_id),
|
|
1548
|
+
target_pane_query_matched: surface.target_pane_query_matched,
|
|
1549
|
+
}
|
|
1550
|
+
}
|
|
1551
|
+
|
|
1392
1552
|
fn target_name(target: &Target) -> String {
|
|
1393
1553
|
match target {
|
|
1394
1554
|
Target::Pane(pane) => pane.as_str().to_string(),
|
|
@@ -1590,13 +1750,23 @@ fn post_submit_token_visible(
|
|
|
1590
1750
|
backend: &TmuxBackend,
|
|
1591
1751
|
target: &Target,
|
|
1592
1752
|
payload: &InjectPayload,
|
|
1753
|
+
tally: &mut CaptureTally,
|
|
1593
1754
|
) -> Result<Option<bool>, TransportError> {
|
|
1594
|
-
|
|
1755
|
+
let Some(marker) = payload_token_marker(payload) else {
|
|
1595
1756
|
return Ok(None);
|
|
1596
|
-
}
|
|
1757
|
+
};
|
|
1597
1758
|
for attempt in 0..TOKEN_POST_SUBMIT_READBACK_POLLS {
|
|
1598
|
-
|
|
1599
|
-
|
|
1759
|
+
match backend.capture(target, CaptureRange::Tail(80)) {
|
|
1760
|
+
Ok(cap) => {
|
|
1761
|
+
tally.record_ok(&cap.text, Some(marker));
|
|
1762
|
+
if cap.text.contains(marker) {
|
|
1763
|
+
return Ok(Some(true));
|
|
1764
|
+
}
|
|
1765
|
+
}
|
|
1766
|
+
Err(_) => {
|
|
1767
|
+
tally.record_err();
|
|
1768
|
+
return Ok(None);
|
|
1769
|
+
}
|
|
1600
1770
|
}
|
|
1601
1771
|
if attempt + 1 < TOKEN_POST_SUBMIT_READBACK_POLLS {
|
|
1602
1772
|
std::thread::sleep(Duration::from_millis(25));
|
|
@@ -2388,6 +2558,7 @@ fn shell_command(
|
|
|
2388
2558
|
) -> String {
|
|
2389
2559
|
let unset_set: std::collections::BTreeSet<&str> =
|
|
2390
2560
|
env_unset.iter().map(String::as_str).collect();
|
|
2561
|
+
let caller_provider = caller_provider_for_invocation(env);
|
|
2391
2562
|
let mut parts = Vec::new();
|
|
2392
2563
|
parts.push("cd".to_string());
|
|
2393
2564
|
parts.push(shell_quote(&cwd.to_string_lossy()));
|
|
@@ -2407,16 +2578,47 @@ fn shell_command(
|
|
|
2407
2578
|
// contains the very keys we want to scrub (e.g. CLAUDE_EFFORT carried
|
|
2408
2579
|
// forward from the launching shell into the env map).
|
|
2409
2580
|
for (key, value) in env {
|
|
2410
|
-
if unset_set.contains(key.as_str())
|
|
2581
|
+
if unset_set.contains(key.as_str())
|
|
2582
|
+
|| key.starts_with(crate::layout::worker_env::CALLER_CONTEXT_PREFIX)
|
|
2583
|
+
{
|
|
2411
2584
|
continue;
|
|
2412
2585
|
}
|
|
2413
2586
|
parts.push(format!("{key}={}", shell_quote(value)));
|
|
2414
2587
|
}
|
|
2588
|
+
append_caller_context_assignments(&mut parts, caller_provider);
|
|
2415
2589
|
parts.push("exec".to_string());
|
|
2416
2590
|
parts.extend(argv.iter().map(|arg| shell_quote(arg)));
|
|
2417
2591
|
parts.join(" ")
|
|
2418
2592
|
}
|
|
2419
2593
|
|
|
2594
|
+
fn caller_provider_for_invocation(env: &BTreeMap<String, String>) -> Option<&'static str> {
|
|
2595
|
+
env.get(crate::layout::worker_env::CALLER_PROVIDER_ENV)
|
|
2596
|
+
.and_then(|value| parse_provider(value))
|
|
2597
|
+
.map(provider_wire)
|
|
2598
|
+
}
|
|
2599
|
+
|
|
2600
|
+
fn append_caller_context_assignments(parts: &mut Vec<String>, provider: Option<&str>) {
|
|
2601
|
+
let Some(provider) = provider else {
|
|
2602
|
+
return;
|
|
2603
|
+
};
|
|
2604
|
+
parts.push(format!(
|
|
2605
|
+
"{}={}",
|
|
2606
|
+
crate::layout::worker_env::CALLER_PROVIDER_ENV,
|
|
2607
|
+
shell_quote(provider)
|
|
2608
|
+
));
|
|
2609
|
+
// These are expanded by the shell created inside the target pane. They are
|
|
2610
|
+
// assignments on the provider command only, so the exit marker/inert tail
|
|
2611
|
+
// cannot retain a usable caller context.
|
|
2612
|
+
parts.push(format!(
|
|
2613
|
+
"{}=\"${{TMUX_PANE-}}\"",
|
|
2614
|
+
crate::layout::worker_env::CALLER_PANE_ENV
|
|
2615
|
+
));
|
|
2616
|
+
parts.push(format!(
|
|
2617
|
+
"{}=\"${{TMUX%%,*}}\"",
|
|
2618
|
+
crate::layout::worker_env::CALLER_ENDPOINT_ENV
|
|
2619
|
+
));
|
|
2620
|
+
}
|
|
2621
|
+
|
|
2420
2622
|
/// 0.4.x (CR R6): single-source marker prefix. The exit marker emitted by
|
|
2421
2623
|
/// `leader_shell_wrapper_command` and the substring detected by
|
|
2422
2624
|
/// `leader_provider_health` MUST share this prefix exactly. Format:
|
|
@@ -2586,6 +2788,7 @@ pub fn worker_shell_wrapper_command(
|
|
|
2586
2788
|
) -> String {
|
|
2587
2789
|
let unset_set: std::collections::BTreeSet<&str> =
|
|
2588
2790
|
env_unset.iter().map(String::as_str).collect();
|
|
2791
|
+
let caller_provider = caller_provider_for_invocation(env);
|
|
2589
2792
|
let mut parts = Vec::new();
|
|
2590
2793
|
parts.push("cd".to_string());
|
|
2591
2794
|
parts.push(shell_quote(&cwd.to_string_lossy()));
|
|
@@ -2596,11 +2799,14 @@ pub fn worker_shell_wrapper_command(
|
|
|
2596
2799
|
parts.push("&&".to_string());
|
|
2597
2800
|
}
|
|
2598
2801
|
for (key, value) in env {
|
|
2599
|
-
if unset_set.contains(key.as_str())
|
|
2802
|
+
if unset_set.contains(key.as_str())
|
|
2803
|
+
|| key.starts_with(crate::layout::worker_env::CALLER_CONTEXT_PREFIX)
|
|
2804
|
+
{
|
|
2600
2805
|
continue;
|
|
2601
2806
|
}
|
|
2602
2807
|
parts.push(format!("{key}={}", shell_quote(value)));
|
|
2603
2808
|
}
|
|
2809
|
+
append_caller_context_assignments(&mut parts, caller_provider);
|
|
2604
2810
|
parts.extend(argv.iter().map(|arg| shell_quote(arg)));
|
|
2605
2811
|
parts.push(";".to_string());
|
|
2606
2812
|
parts.push("rc=$?;".to_string());
|
|
@@ -2945,11 +3151,15 @@ impl Transport for TmuxBackend {
|
|
|
2945
3151
|
let poll_start = std::time::Instant::now();
|
|
2946
3152
|
let mut token_ever_visible = false;
|
|
2947
3153
|
let mut tracked_paste: Option<PasteLatch> = None;
|
|
3154
|
+
let mut capture_tally = CaptureTally::default();
|
|
3155
|
+
let mut token_seen_after_paste = None;
|
|
3156
|
+
let mut token_seen_after_enter = None;
|
|
2948
3157
|
token_visible_for_report = if let Some(m) = payload_token_marker(payload) {
|
|
2949
3158
|
let mut visible = false;
|
|
2950
3159
|
while poll_start.elapsed().as_millis() < token_poll_timeout_ms as u128 {
|
|
2951
3160
|
match self.capture(target, CaptureRange::Tail(80)) {
|
|
2952
3161
|
Ok(cap) => {
|
|
3162
|
+
capture_tally.record_ok(&cap.text, Some(m));
|
|
2953
3163
|
tracked_paste = latch_paste(&cap.text, tracked_paste);
|
|
2954
3164
|
if cap.text.contains(m) {
|
|
2955
3165
|
visible = true;
|
|
@@ -2960,10 +3170,14 @@ impl Transport for TmuxBackend {
|
|
|
2960
3170
|
break;
|
|
2961
3171
|
}
|
|
2962
3172
|
}
|
|
2963
|
-
Err(_) =>
|
|
3173
|
+
Err(_) => {
|
|
3174
|
+
capture_tally.record_err();
|
|
3175
|
+
break; // tmux unavailable, skip poll
|
|
3176
|
+
}
|
|
2964
3177
|
}
|
|
2965
3178
|
std::thread::sleep(Duration::from_millis(50));
|
|
2966
3179
|
}
|
|
3180
|
+
token_seen_after_paste = capture_tally.seen_after_ok_samples(visible);
|
|
2967
3181
|
Some(visible)
|
|
2968
3182
|
} else {
|
|
2969
3183
|
None
|
|
@@ -2985,6 +3199,7 @@ impl Transport for TmuxBackend {
|
|
|
2985
3199
|
self.run_inject_stage(&submit_argv, InjectStage::Submit)?;
|
|
2986
3200
|
notify_submit_observer(observer);
|
|
2987
3201
|
let total_elapsed_ms = inject_start.elapsed().as_millis() as u64;
|
|
3202
|
+
let surface = probe_inject_target_surface(self, target);
|
|
2988
3203
|
return Ok(InjectReport {
|
|
2989
3204
|
stage_reached: InjectStage::Submit,
|
|
2990
3205
|
inject_verification: inject_verification_after_readback(
|
|
@@ -2994,12 +3209,14 @@ impl Transport for TmuxBackend {
|
|
|
2994
3209
|
submit_verification: submit_verification_for_key(submit),
|
|
2995
3210
|
turn_verification: TurnVerification::NotYetObserved,
|
|
2996
3211
|
attempts: 1,
|
|
2997
|
-
submit_diagnostics: Some(
|
|
2998
|
-
appear_gate_elapsed_ms: 0,
|
|
2999
|
-
appear_gate_matched: false,
|
|
3212
|
+
submit_diagnostics: Some(submit_diagnostics_for_inject(
|
|
3000
3213
|
total_elapsed_ms,
|
|
3001
|
-
|
|
3002
|
-
|
|
3214
|
+
Vec::new(),
|
|
3215
|
+
&capture_tally,
|
|
3216
|
+
token_seen_after_paste,
|
|
3217
|
+
None,
|
|
3218
|
+
surface,
|
|
3219
|
+
)),
|
|
3003
3220
|
});
|
|
3004
3221
|
}
|
|
3005
3222
|
|
|
@@ -3042,8 +3259,12 @@ impl Transport for TmuxBackend {
|
|
|
3042
3259
|
if attempt > 0 {
|
|
3043
3260
|
if let Some(m) = marker {
|
|
3044
3261
|
if let Ok(cap) = self.capture(target, CaptureRange::Tail(40)) {
|
|
3262
|
+
capture_tally.record_ok(&cap.text, Some(m));
|
|
3045
3263
|
if cap.text.contains(m) {
|
|
3046
3264
|
token_ever_visible = true;
|
|
3265
|
+
token_seen_after_enter = Some(true);
|
|
3266
|
+
} else if token_seen_after_enter.is_none() {
|
|
3267
|
+
token_seen_after_enter = Some(false);
|
|
3047
3268
|
}
|
|
3048
3269
|
tracked_paste = latch_paste(&cap.text, tracked_paste);
|
|
3049
3270
|
let obs = submit_attempt_observation(
|
|
@@ -3115,10 +3336,22 @@ impl Transport for TmuxBackend {
|
|
|
3115
3336
|
// Post-submit token readback (U1 #7 parity: check token
|
|
3116
3337
|
// visible after Enter for no-echo panes).
|
|
3117
3338
|
if attempt == 0 && matches!(token_visible_for_report, Some(false)) {
|
|
3118
|
-
|
|
3119
|
-
|
|
3120
|
-
|
|
3339
|
+
let post = post_submit_token_visible(
|
|
3340
|
+
self,
|
|
3341
|
+
target,
|
|
3342
|
+
payload,
|
|
3343
|
+
&mut capture_tally,
|
|
3344
|
+
)
|
|
3345
|
+
.unwrap_or(None);
|
|
3346
|
+
if post == Some(true) {
|
|
3347
|
+
token_visible_for_report = Some(true);
|
|
3121
3348
|
token_ever_visible = true;
|
|
3349
|
+
token_seen_after_enter = Some(true);
|
|
3350
|
+
} else {
|
|
3351
|
+
token_visible_for_report = Some(false);
|
|
3352
|
+
if post == Some(false) {
|
|
3353
|
+
token_seen_after_enter = Some(false);
|
|
3354
|
+
}
|
|
3122
3355
|
}
|
|
3123
3356
|
}
|
|
3124
3357
|
|
|
@@ -3132,10 +3365,14 @@ impl Transport for TmuxBackend {
|
|
|
3132
3365
|
std::thread::sleep(Duration::from_millis(100));
|
|
3133
3366
|
match self.capture(target, CaptureRange::Tail(40)) {
|
|
3134
3367
|
Ok(cap) => {
|
|
3368
|
+
capture_tally.record_ok(&cap.text, Some(m));
|
|
3135
3369
|
consecutive_capture_failures = 0;
|
|
3136
3370
|
saw_capture = true;
|
|
3137
3371
|
if cap.text.contains(m) {
|
|
3138
3372
|
token_ever_visible = true;
|
|
3373
|
+
token_seen_after_enter = Some(true);
|
|
3374
|
+
} else if token_seen_after_enter.is_none() {
|
|
3375
|
+
token_seen_after_enter = Some(false);
|
|
3139
3376
|
}
|
|
3140
3377
|
tracked_paste = latch_paste(&cap.text, tracked_paste);
|
|
3141
3378
|
let obs = submit_attempt_observation(
|
|
@@ -3166,6 +3403,7 @@ impl Transport for TmuxBackend {
|
|
|
3166
3403
|
}
|
|
3167
3404
|
}
|
|
3168
3405
|
Err(_) => {
|
|
3406
|
+
capture_tally.record_err();
|
|
3169
3407
|
capture_retries = capture_retries.saturating_add(1);
|
|
3170
3408
|
consecutive_capture_failures =
|
|
3171
3409
|
consecutive_capture_failures.saturating_add(1);
|
|
@@ -3265,8 +3503,12 @@ impl Transport for TmuxBackend {
|
|
|
3265
3503
|
std::thread::sleep(Duration::from_millis(100));
|
|
3266
3504
|
match self.capture(target, CaptureRange::Tail(40)) {
|
|
3267
3505
|
Ok(cap) => {
|
|
3506
|
+
capture_tally.record_ok(&cap.text, Some(m));
|
|
3268
3507
|
if cap.text.contains(m) {
|
|
3269
3508
|
token_ever_visible = true;
|
|
3509
|
+
token_seen_after_enter = Some(true);
|
|
3510
|
+
} else if token_seen_after_enter.is_none() {
|
|
3511
|
+
token_seen_after_enter = Some(false);
|
|
3270
3512
|
}
|
|
3271
3513
|
tracked_paste = latch_paste(&cap.text, tracked_paste);
|
|
3272
3514
|
let obs = submit_attempt_observation(
|
|
@@ -3304,7 +3546,10 @@ impl Transport for TmuxBackend {
|
|
|
3304
3546
|
break;
|
|
3305
3547
|
}
|
|
3306
3548
|
}
|
|
3307
|
-
Err(_) =>
|
|
3549
|
+
Err(_) => {
|
|
3550
|
+
capture_tally.record_err();
|
|
3551
|
+
break;
|
|
3552
|
+
}
|
|
3308
3553
|
}
|
|
3309
3554
|
}
|
|
3310
3555
|
if found_consumed {
|
|
@@ -3316,6 +3561,7 @@ impl Transport for TmuxBackend {
|
|
|
3316
3561
|
}
|
|
3317
3562
|
}
|
|
3318
3563
|
let total_elapsed_ms = inject_start.elapsed().as_millis() as u64;
|
|
3564
|
+
let surface = probe_inject_target_surface(self, target);
|
|
3319
3565
|
return Ok(InjectReport {
|
|
3320
3566
|
stage_reached: InjectStage::Submit,
|
|
3321
3567
|
inject_verification: inject_verification_after_readback(
|
|
@@ -3328,12 +3574,14 @@ impl Transport for TmuxBackend {
|
|
|
3328
3574
|
last_turn_text.as_deref(),
|
|
3329
3575
|
),
|
|
3330
3576
|
attempts: consumption_attempts.saturating_add(capture_retries),
|
|
3331
|
-
submit_diagnostics: Some(
|
|
3332
|
-
appear_gate_elapsed_ms: 0,
|
|
3333
|
-
appear_gate_matched: false,
|
|
3577
|
+
submit_diagnostics: Some(submit_diagnostics_for_inject(
|
|
3334
3578
|
total_elapsed_ms,
|
|
3335
3579
|
attempts_detail,
|
|
3336
|
-
|
|
3580
|
+
&capture_tally,
|
|
3581
|
+
token_seen_after_paste,
|
|
3582
|
+
token_seen_after_enter,
|
|
3583
|
+
surface,
|
|
3584
|
+
)),
|
|
3337
3585
|
});
|
|
3338
3586
|
}
|
|
3339
3587
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
//! 测试默认零真-spawn、零真-tmux:lifecycle/CLI spawn 路径必经 *_with_transport 注入离线
|
|
2
2
|
//! mock;确需真 tmux 者必须 provider:fake + RAII kill_server 守卫 + #[ignore=real-machine].
|
|
3
3
|
|
|
4
|
-
use std::collections::BTreeMap;
|
|
4
|
+
use std::collections::{BTreeMap, VecDeque};
|
|
5
5
|
use std::path::Path;
|
|
6
6
|
use std::sync::{Arc, Mutex};
|
|
7
7
|
|
|
@@ -26,6 +26,7 @@ struct OfflineState {
|
|
|
26
26
|
session_present: bool,
|
|
27
27
|
session_absent_after_spawn_first: bool,
|
|
28
28
|
targets: Vec<PaneInfo>,
|
|
29
|
+
target_snapshots: VecDeque<Vec<PaneInfo>>,
|
|
29
30
|
windows: Vec<WindowName>,
|
|
30
31
|
pane_presence: BTreeMap<String, bool>,
|
|
31
32
|
spawn_failures: BTreeMap<String, String>,
|
|
@@ -62,6 +63,7 @@ impl Default for OfflineState {
|
|
|
62
63
|
session_present: false,
|
|
63
64
|
session_absent_after_spawn_first: false,
|
|
64
65
|
targets: Vec::new(),
|
|
66
|
+
target_snapshots: VecDeque::new(),
|
|
65
67
|
windows: Vec::new(),
|
|
66
68
|
pane_presence: BTreeMap::new(),
|
|
67
69
|
spawn_failures: BTreeMap::new(),
|
|
@@ -104,7 +106,15 @@ impl OfflineTransport {
|
|
|
104
106
|
}
|
|
105
107
|
|
|
106
108
|
pub fn with_targets(self, targets: Vec<PaneInfo>) -> Self {
|
|
107
|
-
self.with_state(|state|
|
|
109
|
+
self.with_state(|state| {
|
|
110
|
+
state.targets = targets;
|
|
111
|
+
state.target_snapshots.clear();
|
|
112
|
+
});
|
|
113
|
+
self
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
pub fn with_target_snapshots(self, snapshots: Vec<Vec<PaneInfo>>) -> Self {
|
|
117
|
+
self.with_state(|state| state.target_snapshots = snapshots.into());
|
|
108
118
|
self
|
|
109
119
|
}
|
|
110
120
|
|
|
@@ -507,7 +517,12 @@ impl Transport for OfflineTransport {
|
|
|
507
517
|
detail,
|
|
508
518
|
});
|
|
509
519
|
}
|
|
510
|
-
Ok(self.with_state(|state|
|
|
520
|
+
Ok(self.with_state(|state| {
|
|
521
|
+
state
|
|
522
|
+
.target_snapshots
|
|
523
|
+
.pop_front()
|
|
524
|
+
.unwrap_or_else(|| state.targets.clone())
|
|
525
|
+
}))
|
|
511
526
|
}
|
|
512
527
|
|
|
513
528
|
fn has_session(&self, _session: &SessionName) -> Result<bool, TransportError> {
|
|
@@ -483,6 +483,105 @@ pub struct SubmitDiagnostics {
|
|
|
483
483
|
/// prompt loop. Empty when `saw_pasted_prompt == false` and the inject
|
|
484
484
|
/// went through the E46 token path.
|
|
485
485
|
pub attempts_detail: Vec<SubmitAttemptObservation>,
|
|
486
|
+
/// Token visibility before paste. `None` means this inject did not sample
|
|
487
|
+
/// (honest unknown). Never inferred from later captures.
|
|
488
|
+
pub token_seen_before_paste: Option<bool>,
|
|
489
|
+
/// Token visibility from successful post-paste captures. `None` if every
|
|
490
|
+
/// post-paste capture failed or none ran.
|
|
491
|
+
pub token_seen_after_paste: Option<bool>,
|
|
492
|
+
/// Token visibility from successful post-Enter captures. `None` if none ran
|
|
493
|
+
/// or all failed.
|
|
494
|
+
pub token_seen_after_enter: Option<bool>,
|
|
495
|
+
pub capture_ok_count: u32,
|
|
496
|
+
pub capture_err_count: u32,
|
|
497
|
+
pub capture_sample_count: u32,
|
|
498
|
+
pub last_capture_outcome: CaptureSampleOutcome,
|
|
499
|
+
/// Basename only of `#{pane_current_command}` at inject; never argv.
|
|
500
|
+
pub pane_command_basename: Option<String>,
|
|
501
|
+
pub pane_command_query: QueryOutcome,
|
|
502
|
+
pub input_surface: InputSurfaceProbe,
|
|
503
|
+
pub target_pane_id: Option<String>,
|
|
504
|
+
/// Inject target pane id vs `#{pane_id}` queried on that same target.
|
|
505
|
+
pub target_pane_query_matched: Option<bool>,
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
/// Last capture sample of the inject token path. Distinguishes "read failed"
|
|
509
|
+
/// from "read succeeded and token was absent". Does not change delivery gates.
|
|
510
|
+
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
|
511
|
+
pub enum CaptureSampleOutcome {
|
|
512
|
+
#[default]
|
|
513
|
+
NotAttempted,
|
|
514
|
+
OkTokenSeen,
|
|
515
|
+
OkTokenMissing,
|
|
516
|
+
Failed,
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
impl CaptureSampleOutcome {
|
|
520
|
+
pub fn as_str(self) -> &'static str {
|
|
521
|
+
match self {
|
|
522
|
+
Self::NotAttempted => "not_attempted",
|
|
523
|
+
Self::OkTokenSeen => "ok_token_seen",
|
|
524
|
+
Self::OkTokenMissing => "ok_token_missing",
|
|
525
|
+
Self::Failed => "failed",
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
|
531
|
+
pub enum QueryOutcome {
|
|
532
|
+
#[default]
|
|
533
|
+
Unavailable,
|
|
534
|
+
Observed,
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
impl QueryOutcome {
|
|
538
|
+
pub fn as_str(self) -> &'static str {
|
|
539
|
+
match self {
|
|
540
|
+
Self::Unavailable => "unavailable",
|
|
541
|
+
Self::Observed => "observed",
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
/// Typed pane input surface from existing `pane_mode` probe. `Unavailable`
|
|
547
|
+
/// means the query failed; it is not a pass.
|
|
548
|
+
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
|
549
|
+
pub enum InputSurfaceProbe {
|
|
550
|
+
#[default]
|
|
551
|
+
Unavailable,
|
|
552
|
+
Input,
|
|
553
|
+
Copy,
|
|
554
|
+
Tree,
|
|
555
|
+
View,
|
|
556
|
+
Client,
|
|
557
|
+
UnknownMode,
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
impl InputSurfaceProbe {
|
|
561
|
+
pub fn as_str(self) -> &'static str {
|
|
562
|
+
match self {
|
|
563
|
+
Self::Unavailable => "unavailable",
|
|
564
|
+
Self::Input => "input",
|
|
565
|
+
Self::Copy => "copy",
|
|
566
|
+
Self::Tree => "tree",
|
|
567
|
+
Self::View => "view",
|
|
568
|
+
Self::Client => "client",
|
|
569
|
+
Self::UnknownMode => "unknown_mode",
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
/// First token of a pane command, basename only. Empty/missing → `None`.
|
|
575
|
+
pub(crate) fn command_basename(raw: &str) -> Option<String> {
|
|
576
|
+
let first = raw.split_whitespace().next()?.trim();
|
|
577
|
+
if first.is_empty() {
|
|
578
|
+
return None;
|
|
579
|
+
}
|
|
580
|
+
std::path::Path::new(first)
|
|
581
|
+
.file_name()
|
|
582
|
+
.and_then(|name| name.to_str())
|
|
583
|
+
.filter(|name| !name.is_empty())
|
|
584
|
+
.map(str::to_string)
|
|
486
585
|
}
|
|
487
586
|
|
|
488
587
|
/// E50 PR-1 single attempt observation (one Enter + one capture). All fields
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@team-agent/installer",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.75",
|
|
4
4
|
"description": "npx installer for Team Agent",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"codex",
|
|
@@ -20,9 +20,9 @@
|
|
|
20
20
|
"team-agent-installer": "npm/install.mjs"
|
|
21
21
|
},
|
|
22
22
|
"optionalDependencies": {
|
|
23
|
-
"@team-agent/cli-darwin-arm64": "0.5.
|
|
24
|
-
"@team-agent/cli-darwin-x64": "0.5.
|
|
25
|
-
"@team-agent/cli-linux-x64": "0.5.
|
|
23
|
+
"@team-agent/cli-darwin-arm64": "0.5.75",
|
|
24
|
+
"@team-agent/cli-darwin-x64": "0.5.75",
|
|
25
|
+
"@team-agent/cli-linux-x64": "0.5.75"
|
|
26
26
|
},
|
|
27
27
|
"scripts": {
|
|
28
28
|
"postinstall": "node npm/bincheck.mjs",
|
|
@@ -27,10 +27,13 @@ For a Pi worker, use this existing three-step flow:
|
|
|
27
27
|
|
|
28
28
|
Pi roles preserve the direct Pi login, model defaults, extensions/plugins, skills, context, trust, and tools. Team Agent adds only per-seat identity/session isolation. `model` and `effort` are optional explicit pass-throughs. Use the shared role schema, a block-style `tools` list containing `mcp_team`, and the required boolean `dangerously_skip_permissions`.
|
|
29
29
|
|
|
30
|
+
Pi tool contract: use only `mcp_team`, `fs_read`, `fs_list`, `fs_write`, and `execute_bash`. Do not copy `provider_builtin`, `git_diff`, or `network` from a generic Codex role; Pi cannot map those categories. `compile`/`validate` rejects unsupported Pi categories before quick-start persists runtime state.
|
|
31
|
+
|
|
30
32
|
## Operate
|
|
31
33
|
|
|
32
34
|
- Send: `team-agent send TO MESSAGE` using an in-team short name or fully qualified `<workspace>::<team>/<agent>`; these are co-equal positional TO forms. For example, `team-agent send reviewer "..."` uses an in-team short name. Do not poll after success.
|
|
33
35
|
- Inspect: `team-agent status` or `team-agent status --json`; `ok: true` with `ready: false` is not a crash.
|
|
36
|
+
- `quick-start` / `restart` / `claim-leader` JSON is compact by default (`ok`, `status`/`reason`, next action, attach/send, and `readiness.all_workers_spawned`); pass `--detail` only for internal diagnostics.
|
|
34
37
|
- On a structured failure with an `action`, run that action first, then stop. Never guess flags.
|
|
35
38
|
- Never rewrite a user-supplied model id or read `.env` files.
|
|
36
39
|
|