@team-agent/installer 0.5.75 → 0.5.78
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/README.md +9 -0
- package/README.zh.md +8 -0
- package/crates/team-agent/src/cli/adapters.rs +184 -125
- package/crates/team-agent/src/cli/diagnose.rs +333 -68
- package/crates/team-agent/src/cli/emit.rs +59 -15
- package/crates/team-agent/src/cli/mod.rs +600 -134
- package/crates/team-agent/src/cli/models.rs +485 -44
- package/crates/team-agent/src/cli/named_address.rs +19 -5
- package/crates/team-agent/src/cli/profile.rs +35 -4
- package/crates/team-agent/src/cli/spec.rs +7 -2
- package/crates/team-agent/src/cli/status_port/brief.rs +1137 -0
- package/crates/team-agent/src/cli/status_port/compact.rs +12 -8
- package/crates/team-agent/src/cli/status_port/snapshot.rs +15 -12
- package/crates/team-agent/src/cli/status_port/store.rs +1 -1
- package/crates/team-agent/src/cli/status_port.rs +8 -0
- package/crates/team-agent/src/cli/tests/missing_subcommands.rs +62 -8
- package/crates/team-agent/src/cli/tests/mod.rs +1 -0
- package/crates/team-agent/src/cli/tests/named_address.rs +70 -0
- package/crates/team-agent/src/cli/tests/run_delegation.rs +29 -24
- package/crates/team-agent/src/cli/tests/status_brief.rs +691 -0
- package/crates/team-agent/src/cli/tests/status_send.rs +8 -2
- package/crates/team-agent/src/cli/tests/verb_profile.rs +81 -6
- package/crates/team-agent/src/cli/types.rs +1 -0
- package/crates/team-agent/src/leader/lease.rs +538 -41
- package/crates/team-agent/src/leader/owner_bind.rs +40 -12
- package/crates/team-agent/src/leader/rediscover/tests.rs +65 -0
- package/crates/team-agent/src/leader/rediscover.rs +36 -6
- package/crates/team-agent/src/leader/start.rs +151 -14
- package/crates/team-agent/src/leader/tests/wake_start_owner.rs +15 -9
- package/crates/team-agent/src/leader/types.rs +4 -0
- package/crates/team-agent/src/lifecycle/launch/add_agent.rs +28 -227
- package/crates/team-agent/src/lifecycle/launch/add_agent_state.rs +0 -18
- package/crates/team-agent/src/lifecycle/launch/cursor_mcp.rs +94 -227
- package/crates/team-agent/src/lifecycle/launch/mcp_config.rs +59 -10
- package/crates/team-agent/src/lifecycle/launch/pi_mcp.rs +138 -14
- package/crates/team-agent/src/lifecycle/launch/quick_start.rs +1449 -282
- package/crates/team-agent/src/lifecycle/launch/readiness.rs +349 -4
- package/crates/team-agent/src/lifecycle/launch/spawn.rs +11 -11
- package/crates/team-agent/src/lifecycle/launch/spec_state.rs +35 -3
- package/crates/team-agent/src/lifecycle/launch/worker_env.rs +16 -0
- package/crates/team-agent/src/lifecycle/launch.rs +30 -7
- package/crates/team-agent/src/lifecycle/profile_launch.rs +37 -3
- package/crates/team-agent/src/lifecycle/restart/agent.rs +112 -100
- package/crates/team-agent/src/lifecycle/restart/common.rs +82 -52
- package/crates/team-agent/src/lifecycle/restart/rebuild.rs +1002 -65
- package/crates/team-agent/src/lifecycle/restart.rs +2 -2
- package/crates/team-agent/src/lifecycle/tests/acceptance_b_batch_red.rs +3 -0
- package/crates/team-agent/src/lifecycle/tests/b0_reader_hideone_audit_contract.rs +45 -14
- package/crates/team-agent/src/lifecycle/tests/behavioral_diff_264_red.rs +15 -0
- package/crates/team-agent/src/lifecycle/tests/claude_compatible_config_red.rs +5 -0
- package/crates/team-agent/src/lifecycle/tests/claude_profile_launch_red.rs +11 -13
- package/crates/team-agent/src/lifecycle/tests/clone_agent_preserves_source_tools.rs +119 -3
- package/crates/team-agent/src/lifecycle/tests/communication_mode_runtime_contract_red.rs +12 -0
- package/crates/team-agent/src/lifecycle/tests/copilot_provider_red.rs +22 -0
- package/crates/team-agent/src/lifecycle/tests/core_034_real_red.rs +12 -0
- package/crates/team-agent/src/lifecycle/tests/cursor_mcp_overlay.rs +265 -198
- package/crates/team-agent/src/lifecycle/tests/cursor_require_explicit_model_red.rs +14 -30
- package/crates/team-agent/src/lifecycle/tests/cursor_restart_resume_red.rs +3 -0
- package/crates/team-agent/src/lifecycle/tests/g1_silent_faces.rs +2 -5
- package/crates/team-agent/src/lifecycle/tests/grok_effort_argv_red.rs +10 -0
- package/crates/team-agent/src/lifecycle/tests/grok_mcp_overlay_red.rs +58 -35
- package/crates/team-agent/src/lifecycle/tests/grok_require_explicit_model_red.rs +10 -0
- package/crates/team-agent/src/lifecycle/tests/grok_restart_resume_red.rs +9 -0
- package/crates/team-agent/src/lifecycle/tests/harvest2_a_batch_red.rs +4 -3
- package/crates/team-agent/src/lifecycle/tests/lane_ops.rs +0 -168
- package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +145 -212
- package/crates/team-agent/src/lifecycle/tests/phase_b_contracts.rs +16 -2
- package/crates/team-agent/src/lifecycle/tests/phase_golden.rs +69 -28
- package/crates/team-agent/src/lifecycle/tests/pi_dual_entry_red.rs +78 -0
- package/crates/team-agent/src/lifecycle/tests/team_in_team_identity_scope_red.rs +3 -0
- package/crates/team-agent/src/lifecycle/tests/test_isolation_escape_contract.rs +51 -10
- package/crates/team-agent/src/lifecycle/tests/upgrade_compat_0211_red.rs +22 -10
- package/crates/team-agent/src/lifecycle/tests/verify_rs031_window_consistency_red.rs +50 -7
- package/crates/team-agent/src/lifecycle/tests/worker_spawn_env_red.rs +137 -0
- package/crates/team-agent/src/lifecycle/types.rs +12 -0
- package/crates/team-agent/src/mcp_server/wire.rs +140 -11
- package/crates/team-agent/src/messaging/delivery.rs +67 -8
- package/crates/team-agent/src/messaging/leader_channel.rs +56 -0
- package/crates/team-agent/src/messaging/tests/runtime.rs +10 -2
- package/crates/team-agent/src/messaging/watchers.rs +48 -4
- package/crates/team-agent/src/provider/adapters/cursor_agent.rs +2 -5
- package/crates/team-agent/src/state/paths.rs +17 -2
- package/crates/team-agent/src/state/persist.rs +97 -1
- package/crates/team-agent/src/state/projection.rs +18 -2
- package/crates/team-agent/src/state/repository.rs +29 -0
- package/crates/team-agent/src/state/selector.rs +23 -3
- package/crates/team-agent/src/test_support/models_timeout_fixture.rs +144 -0
- package/crates/team-agent/src/tmux_backend/tests.rs +121 -4
- package/crates/team-agent/src/tmux_backend.rs +208 -118
- package/crates/team-agent/src/transport.rs +72 -0
- package/crates/team-agent/src/transport_factory.rs +53 -0
- package/package.json +4 -5
- package/skills/team-agent/SKILL.md +45 -17
- package/skills/team-agent/command-coverage.json +4 -29
- package/npm/check-skill-links.mjs +0 -88
- package/skills/team-agent/references/team-agent-operator.md +0 -672
|
@@ -377,6 +377,7 @@ fn save_runtime_state_with_merge_options(
|
|
|
377
377
|
exact_owner_seed_to_clear,
|
|
378
378
|
None,
|
|
379
379
|
None,
|
|
380
|
+
None,
|
|
380
381
|
)
|
|
381
382
|
}
|
|
382
383
|
|
|
@@ -399,6 +400,7 @@ pub(crate) fn save_runtime_state_with_receiver_authority_and_expected(
|
|
|
399
400
|
None,
|
|
400
401
|
Some((expected_owner, expected_receiver)),
|
|
401
402
|
None,
|
|
403
|
+
None,
|
|
402
404
|
)
|
|
403
405
|
}
|
|
404
406
|
|
|
@@ -421,6 +423,38 @@ pub(crate) fn save_runtime_state_with_exact_owner_receiver_cleanup(
|
|
|
421
423
|
None,
|
|
422
424
|
None,
|
|
423
425
|
Some((team_key, expected_owner, expected_receiver)),
|
|
426
|
+
None,
|
|
427
|
+
)
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
pub(crate) fn save_runtime_state_with_exact_owner_receiver_restore(
|
|
431
|
+
workspace: &Path,
|
|
432
|
+
state: &Value,
|
|
433
|
+
team_key: &str,
|
|
434
|
+
expected_owner: &Value,
|
|
435
|
+
expected_receiver: &Value,
|
|
436
|
+
previous_owner: &Value,
|
|
437
|
+
previous_receiver: &Value,
|
|
438
|
+
) -> Result<(), StateError> {
|
|
439
|
+
persist_runtime_state_with_merge_options_and_expected(
|
|
440
|
+
workspace,
|
|
441
|
+
state,
|
|
442
|
+
&[],
|
|
443
|
+
None,
|
|
444
|
+
&[],
|
|
445
|
+
None,
|
|
446
|
+
&[],
|
|
447
|
+
Some(team_key),
|
|
448
|
+
None,
|
|
449
|
+
None,
|
|
450
|
+
None,
|
|
451
|
+
Some((
|
|
452
|
+
team_key,
|
|
453
|
+
expected_owner,
|
|
454
|
+
expected_receiver,
|
|
455
|
+
previous_owner,
|
|
456
|
+
previous_receiver,
|
|
457
|
+
)),
|
|
424
458
|
)
|
|
425
459
|
}
|
|
426
460
|
|
|
@@ -436,6 +470,7 @@ fn persist_runtime_state_with_merge_options_and_expected(
|
|
|
436
470
|
exact_owner_seed_to_clear: Option<&Value>,
|
|
437
471
|
expected_owner_receiver: Option<(&Value, &Value)>,
|
|
438
472
|
exact_owner_receiver_to_clear: Option<(&str, &Value, &Value)>,
|
|
473
|
+
exact_owner_receiver_to_restore: Option<(&str, &Value, &Value, &Value, &Value)>,
|
|
439
474
|
) -> Result<(), StateError> {
|
|
440
475
|
let path = runtime_state_path(workspace);
|
|
441
476
|
// Python `state.py:497`:先对入参 state 跑 `_migrate_state_identity`(就地填缺失 leader uuid)。
|
|
@@ -459,7 +494,8 @@ fn persist_runtime_state_with_merge_options_and_expected(
|
|
|
459
494
|
// and leave the seed, or skip copying a concurrent owner.
|
|
460
495
|
let skip_pre_lock_fast_path = exact_owner_seed_to_clear.is_some()
|
|
461
496
|
|| expected_owner_receiver.is_some()
|
|
462
|
-
|| exact_owner_receiver_to_clear.is_some()
|
|
497
|
+
|| exact_owner_receiver_to_clear.is_some()
|
|
498
|
+
|| exact_owner_receiver_to_restore.is_some();
|
|
463
499
|
if !skip_pre_lock_fast_path && cache_equals(&path, &migrated) {
|
|
464
500
|
return Ok(());
|
|
465
501
|
}
|
|
@@ -544,6 +580,7 @@ fn persist_runtime_state_with_merge_options_and_expected(
|
|
|
544
580
|
&receiver_updates,
|
|
545
581
|
receiver_update_team_key.zip(exact_owner_seed_to_clear),
|
|
546
582
|
exact_owner_receiver_to_clear,
|
|
583
|
+
exact_owner_receiver_to_restore,
|
|
547
584
|
)?;
|
|
548
585
|
}
|
|
549
586
|
// Stage 3 save-output strip second pass (defence-in-depth): after the
|
|
@@ -613,6 +650,7 @@ fn apply_persist_merge_contract(
|
|
|
613
650
|
receiver_updates: &BTreeSet<String>,
|
|
614
651
|
exact_owner_seed_to_clear: Option<(&str, &Value)>,
|
|
615
652
|
exact_owner_receiver_to_clear: Option<(&str, &Value, &Value)>,
|
|
653
|
+
exact_owner_receiver_to_restore: Option<(&str, &Value, &Value, &Value, &Value)>,
|
|
616
654
|
) -> Result<(), StateError> {
|
|
617
655
|
// A0/R1: the projection gate only guards the TOP-LEVEL passes (top-level agents and
|
|
618
656
|
// the top-level<->active-team cross projections depend on which team is active); the
|
|
@@ -745,6 +783,25 @@ fn apply_persist_merge_contract(
|
|
|
745
783
|
);
|
|
746
784
|
}
|
|
747
785
|
}
|
|
786
|
+
if let Some((
|
|
787
|
+
restore_team,
|
|
788
|
+
expected_owner,
|
|
789
|
+
expected_receiver,
|
|
790
|
+
previous_owner,
|
|
791
|
+
previous_receiver,
|
|
792
|
+
)) = exact_owner_receiver_to_restore
|
|
793
|
+
{
|
|
794
|
+
if team == restore_team {
|
|
795
|
+
apply_lock_held_exact_owner_receiver_restore(
|
|
796
|
+
incoming_entry,
|
|
797
|
+
latest_entry,
|
|
798
|
+
expected_owner,
|
|
799
|
+
expected_receiver,
|
|
800
|
+
previous_owner,
|
|
801
|
+
previous_receiver,
|
|
802
|
+
);
|
|
803
|
+
}
|
|
804
|
+
}
|
|
748
805
|
preserve_latest_endpoint_convergence_fields(incoming_entry, latest_entry);
|
|
749
806
|
}
|
|
750
807
|
}
|
|
@@ -919,6 +976,43 @@ fn apply_lock_held_exact_owner_receiver_cleanup(
|
|
|
919
976
|
}
|
|
920
977
|
}
|
|
921
978
|
|
|
979
|
+
fn apply_lock_held_exact_owner_receiver_restore(
|
|
980
|
+
incoming: &mut Value,
|
|
981
|
+
latest: &Value,
|
|
982
|
+
expected_owner: &Value,
|
|
983
|
+
expected_receiver: &Value,
|
|
984
|
+
previous_owner: &Value,
|
|
985
|
+
previous_receiver: &Value,
|
|
986
|
+
) {
|
|
987
|
+
let latest_owner = latest.get("team_owner");
|
|
988
|
+
let latest_receiver = latest.get("leader_receiver");
|
|
989
|
+
let Some(incoming_obj) = incoming.as_object_mut() else {
|
|
990
|
+
return;
|
|
991
|
+
};
|
|
992
|
+
if latest_owner == Some(expected_owner) && latest_receiver == Some(expected_receiver) {
|
|
993
|
+
incoming_obj.insert("team_owner".to_string(), previous_owner.clone());
|
|
994
|
+
incoming_obj.insert("leader_receiver".to_string(), previous_receiver.clone());
|
|
995
|
+
if let Some(epoch) = previous_owner
|
|
996
|
+
.get("owner_epoch")
|
|
997
|
+
.cloned()
|
|
998
|
+
.or_else(|| latest.get("owner_epoch").cloned())
|
|
999
|
+
{
|
|
1000
|
+
incoming_obj.insert("owner_epoch".to_string(), epoch);
|
|
1001
|
+
}
|
|
1002
|
+
return;
|
|
1003
|
+
}
|
|
1004
|
+
for key in ["team_owner", "leader_receiver", "owner_epoch"] {
|
|
1005
|
+
match latest.get(key) {
|
|
1006
|
+
Some(value) => {
|
|
1007
|
+
incoming_obj.insert(key.to_string(), value.clone());
|
|
1008
|
+
}
|
|
1009
|
+
None => {
|
|
1010
|
+
incoming_obj.remove(key);
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
}
|
|
1015
|
+
|
|
922
1016
|
fn preserve_latest_endpoint_convergence_fields(incoming: &mut Value, latest: &Value) {
|
|
923
1017
|
if !latest_has_preferable_endpoint_convergence(incoming, latest) {
|
|
924
1018
|
return;
|
|
@@ -3113,6 +3207,7 @@ mod tests {
|
|
|
3113
3207
|
&BTreeSet::new(),
|
|
3114
3208
|
None,
|
|
3115
3209
|
None,
|
|
3210
|
+
None,
|
|
3116
3211
|
)
|
|
3117
3212
|
.unwrap();
|
|
3118
3213
|
|
|
@@ -3149,6 +3244,7 @@ mod tests {
|
|
|
3149
3244
|
&receiver_updates,
|
|
3150
3245
|
None,
|
|
3151
3246
|
None,
|
|
3247
|
+
None,
|
|
3152
3248
|
)
|
|
3153
3249
|
.unwrap();
|
|
3154
3250
|
|
|
@@ -16,7 +16,8 @@ use serde_json::{json, Map, Value};
|
|
|
16
16
|
|
|
17
17
|
use super::StateError;
|
|
18
18
|
use crate::state::persist::{
|
|
19
|
-
load_runtime_state,
|
|
19
|
+
load_runtime_state, load_runtime_state_without_migrations,
|
|
20
|
+
save_runtime_state_with_deleted_agents,
|
|
20
21
|
save_runtime_state_with_lifecycle_topology_authority,
|
|
21
22
|
save_runtime_state_with_lifecycle_topology_authority_and_capture_backfill_skip,
|
|
22
23
|
save_runtime_state_with_team_tombstone_lifecycle_topology_authority,
|
|
@@ -504,7 +505,22 @@ pub fn resolve_runtime_team_scope(
|
|
|
504
505
|
workspace: &Path,
|
|
505
506
|
team: Option<&str>,
|
|
506
507
|
) -> Result<TeamScopeResolution, StateError> {
|
|
507
|
-
|
|
508
|
+
resolve_runtime_team_scope_loaded(load_runtime_state(workspace)?, team)
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
/// Same team selection as [`resolve_runtime_team_scope`], but never migrates or
|
|
512
|
+
/// writes runtime state. Status and other read-only CLI surfaces must use this.
|
|
513
|
+
pub fn resolve_runtime_team_scope_readonly(
|
|
514
|
+
workspace: &Path,
|
|
515
|
+
team: Option<&str>,
|
|
516
|
+
) -> Result<TeamScopeResolution, StateError> {
|
|
517
|
+
resolve_runtime_team_scope_loaded(load_runtime_state_without_migrations(workspace)?, team)
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
fn resolve_runtime_team_scope_loaded(
|
|
521
|
+
state: Value,
|
|
522
|
+
team: Option<&str>,
|
|
523
|
+
) -> Result<TeamScopeResolution, StateError> {
|
|
508
524
|
let alive = team_state_candidates(&state);
|
|
509
525
|
// Python `if team:` —— 空串 falsy,等同无 team(对抗 P1:此前 Some("") 误入 team 分支,空
|
|
510
526
|
// team_dir 匹配致歧义/未找到错误串漂移)。
|
|
@@ -52,6 +52,8 @@ use super::persist::{
|
|
|
52
52
|
as helper_write_root_with_receiver_authority_and_expected,
|
|
53
53
|
save_runtime_state_with_exact_owner_receiver_cleanup
|
|
54
54
|
as helper_write_root_with_exact_owner_receiver_cleanup,
|
|
55
|
+
save_runtime_state_with_exact_owner_receiver_restore
|
|
56
|
+
as helper_write_root_with_exact_owner_receiver_restore,
|
|
55
57
|
save_runtime_state_with_team_tombstone_lifecycle_topology_authority as helper_write_root_with_team_tombstone_lifecycle_topology_authority,
|
|
56
58
|
save_runtime_state_with_team_tombstoned_agents as helper_write_root_with_team_tombstoned_agents,
|
|
57
59
|
save_runtime_state_without_migrations as helper_write_root_without_migrations,
|
|
@@ -224,9 +226,20 @@ pub enum StateWriteIntent<'a> {
|
|
|
224
226
|
seed: &'a Value,
|
|
225
227
|
expected_receiver: &'a Value,
|
|
226
228
|
},
|
|
229
|
+
RestoreExactTeamOwnerAndReceiver {
|
|
230
|
+
team_key: &'a str,
|
|
231
|
+
expected_owner: &'a Value,
|
|
232
|
+
expected_receiver: &'a Value,
|
|
233
|
+
previous_owner: &'a Value,
|
|
234
|
+
previous_receiver: &'a Value,
|
|
235
|
+
},
|
|
227
236
|
LeaderBindingRestoreNonTargetTeams {
|
|
228
237
|
target_team_key: &'a str,
|
|
229
238
|
},
|
|
239
|
+
/// Strip extra keys written by a failed or degraded quick-start bind attempt.
|
|
240
|
+
QuickStartBindingCleanup {
|
|
241
|
+
team_key: &'a str,
|
|
242
|
+
},
|
|
230
243
|
LeaderStartBinding {
|
|
231
244
|
team_key: &'a str,
|
|
232
245
|
transport_kind: &'a str,
|
|
@@ -428,6 +441,22 @@ fn route_direct(
|
|
|
428
441
|
seed,
|
|
429
442
|
expected_receiver,
|
|
430
443
|
),
|
|
444
|
+
StateWriteIntent::RestoreExactTeamOwnerAndReceiver {
|
|
445
|
+
team_key,
|
|
446
|
+
expected_owner,
|
|
447
|
+
expected_receiver,
|
|
448
|
+
previous_owner,
|
|
449
|
+
previous_receiver,
|
|
450
|
+
} => helper_write_root_with_exact_owner_receiver_restore(
|
|
451
|
+
workspace,
|
|
452
|
+
state,
|
|
453
|
+
team_key,
|
|
454
|
+
expected_owner,
|
|
455
|
+
expected_receiver,
|
|
456
|
+
previous_owner,
|
|
457
|
+
previous_receiver,
|
|
458
|
+
),
|
|
459
|
+
StateWriteIntent::QuickStartBindingCleanup { .. } => helper_write_root(workspace, state),
|
|
431
460
|
StateWriteIntent::LeaderBindingRestoreNonTargetTeams { .. } => {
|
|
432
461
|
helper_write_root_without_migrations(workspace, state)
|
|
433
462
|
}
|
|
@@ -7,7 +7,9 @@ use serde_json::Value;
|
|
|
7
7
|
|
|
8
8
|
use crate::model::paths::{canonical_run_workspace, runtime_spec_path, team_workspace};
|
|
9
9
|
use crate::state::persist::runtime_state_path;
|
|
10
|
-
use crate::state::projection::
|
|
10
|
+
use crate::state::projection::{
|
|
11
|
+
resolve_runtime_team_scope, resolve_runtime_team_scope_readonly, TeamScopeResolution,
|
|
12
|
+
};
|
|
11
13
|
use crate::state::StateError;
|
|
12
14
|
|
|
13
15
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
@@ -35,6 +37,24 @@ pub fn resolve_active_team(
|
|
|
35
37
|
input: &Path,
|
|
36
38
|
team: Option<&str>,
|
|
37
39
|
mode: SelectorMode,
|
|
40
|
+
) -> Result<SelectedTeam, StateError> {
|
|
41
|
+
resolve_active_team_with(input, team, mode, resolve_runtime_team_scope)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/// Status-path selector: in-memory projection only, no runtime-state writeback.
|
|
45
|
+
pub fn resolve_active_team_readonly(
|
|
46
|
+
input: &Path,
|
|
47
|
+
team: Option<&str>,
|
|
48
|
+
mode: SelectorMode,
|
|
49
|
+
) -> Result<SelectedTeam, StateError> {
|
|
50
|
+
resolve_active_team_with(input, team, mode, resolve_runtime_team_scope_readonly)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
fn resolve_active_team_with(
|
|
54
|
+
input: &Path,
|
|
55
|
+
team: Option<&str>,
|
|
56
|
+
mode: SelectorMode,
|
|
57
|
+
resolve_scope: fn(&Path, Option<&str>) -> Result<TeamScopeResolution, StateError>,
|
|
38
58
|
) -> Result<SelectedTeam, StateError> {
|
|
39
59
|
let explicit_spec = input.join("team.spec.yaml");
|
|
40
60
|
let (run_workspace, resolved) = if explicit_spec.exists() {
|
|
@@ -44,7 +64,7 @@ pub fn resolve_active_team(
|
|
|
44
64
|
} else {
|
|
45
65
|
team_run
|
|
46
66
|
};
|
|
47
|
-
let resolved =
|
|
67
|
+
let resolved = resolve_scope(&run, team)?;
|
|
48
68
|
(run, resolved)
|
|
49
69
|
} else {
|
|
50
70
|
let run =
|
|
@@ -59,7 +79,7 @@ pub fn resolve_active_team(
|
|
|
59
79
|
input.display()
|
|
60
80
|
)));
|
|
61
81
|
}
|
|
62
|
-
let resolved =
|
|
82
|
+
let resolved = resolve_scope(&run, team)?;
|
|
63
83
|
(run, resolved)
|
|
64
84
|
};
|
|
65
85
|
let state = resolved.state;
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
use std::fs::OpenOptions;
|
|
2
|
+
use std::io::Write;
|
|
3
|
+
use std::path::{Path, PathBuf};
|
|
4
|
+
use std::process::{Command, Stdio};
|
|
5
|
+
use std::time::{Duration, Instant};
|
|
6
|
+
|
|
7
|
+
const CHILD_FLAG: &str = "--team-agent-models-timeout-child";
|
|
8
|
+
const LIST_MODELS_FLAG: &str = "--list-models";
|
|
9
|
+
const RECEIPT_ENV: &str = "TEAM_AGENT_MODELS_TIMEOUT_RECEIPT";
|
|
10
|
+
const STAGE_RECEIPT_ENV: &str = "TEAM_AGENT_MODELS_TIMEOUT_STAGE_RECEIPT";
|
|
11
|
+
const MODE_ENV: &str = "TEAM_AGENT_MODELS_TIMEOUT_MODE";
|
|
12
|
+
const STDOUT_MARKER: &[u8] = b"__team_agent_models_timeout_descendant_stdout_v1__\n";
|
|
13
|
+
const PARENT_HANDOFF_TIMEOUT: Duration = Duration::from_secs(1);
|
|
14
|
+
|
|
15
|
+
// Stage values are UNIX epoch milliseconds so the test process and helper use
|
|
16
|
+
// one directly comparable clock. Stage writes are best-effort diagnostics and
|
|
17
|
+
// never change the fixture's production-facing exit semantics.
|
|
18
|
+
fn append_stage(key: &str) {
|
|
19
|
+
let Some(path) = std::env::var_os(STAGE_RECEIPT_ENV) else {
|
|
20
|
+
return;
|
|
21
|
+
};
|
|
22
|
+
let Some(milliseconds) = std::time::SystemTime::now()
|
|
23
|
+
.duration_since(std::time::UNIX_EPOCH)
|
|
24
|
+
.ok()
|
|
25
|
+
.map(|duration| duration.as_millis())
|
|
26
|
+
else {
|
|
27
|
+
return;
|
|
28
|
+
};
|
|
29
|
+
let _ = append_receipt(Path::new(&path), key, milliseconds);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
fn receipt_path() -> PathBuf {
|
|
33
|
+
std::env::var_os(RECEIPT_ENV)
|
|
34
|
+
.map(PathBuf::from)
|
|
35
|
+
.unwrap_or_else(|| std::process::exit(4))
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
fn append_receipt(path: &Path, key: &str, value: impl std::fmt::Display) -> Result<(), ()> {
|
|
39
|
+
let mut receipt = OpenOptions::new()
|
|
40
|
+
.create(true)
|
|
41
|
+
.append(true)
|
|
42
|
+
.open(path)
|
|
43
|
+
.map_err(|_| ())?;
|
|
44
|
+
writeln!(receipt, "{key}={value}").map_err(|_| ())
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
fn wait_for_parent_handoff(receipt: &Path) {
|
|
48
|
+
let expected = format!("child_pid={}", std::process::id());
|
|
49
|
+
let deadline = Instant::now() + PARENT_HANDOFF_TIMEOUT;
|
|
50
|
+
loop {
|
|
51
|
+
if std::fs::read_to_string(receipt)
|
|
52
|
+
.ok()
|
|
53
|
+
.is_some_and(|text| text.lines().any(|line| line == expected))
|
|
54
|
+
{
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
if Instant::now() >= deadline {
|
|
58
|
+
std::process::exit(5);
|
|
59
|
+
}
|
|
60
|
+
std::thread::sleep(Duration::from_millis(1));
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
fn child_main(args: &[String]) {
|
|
65
|
+
if args.len() != 1 || args[0] != CHILD_FLAG {
|
|
66
|
+
std::process::exit(5);
|
|
67
|
+
}
|
|
68
|
+
let receipt = receipt_path();
|
|
69
|
+
wait_for_parent_handoff(&receipt);
|
|
70
|
+
|
|
71
|
+
let mut stdout = std::io::stdout();
|
|
72
|
+
if stdout.write_all(STDOUT_MARKER).is_err() || stdout.flush().is_err() {
|
|
73
|
+
std::process::exit(5);
|
|
74
|
+
}
|
|
75
|
+
if append_receipt(&receipt, "child_argv_exact", 1).is_err()
|
|
76
|
+
|| append_receipt(&receipt, "descendant_pid", std::process::id()).is_err()
|
|
77
|
+
|| append_receipt(&receipt, "descendant_started", 1).is_err()
|
|
78
|
+
|| append_receipt(&receipt, "descendant_stdout_inherited", 1).is_err()
|
|
79
|
+
{
|
|
80
|
+
std::process::exit(5);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
std::thread::sleep(Duration::from_secs(2));
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
fn main() {
|
|
87
|
+
let args: Vec<String> = std::env::args().skip(1).collect();
|
|
88
|
+
if args.first().is_some_and(|arg| arg == CHILD_FLAG) {
|
|
89
|
+
child_main(&args);
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
if args.len() != 1 || args[0] != LIST_MODELS_FLAG {
|
|
93
|
+
std::process::exit(2);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
match std::env::var(MODE_ENV) {
|
|
97
|
+
Ok(mode) if mode == "exit7" => {
|
|
98
|
+
eprintln!("sensitive-token");
|
|
99
|
+
std::process::exit(7);
|
|
100
|
+
}
|
|
101
|
+
Ok(mode) if mode == "oversize" => {
|
|
102
|
+
let mut stdout = std::io::stdout();
|
|
103
|
+
let bytes = [b'x'; 64];
|
|
104
|
+
let _ = stdout.write_all(&bytes);
|
|
105
|
+
let _ = stdout.flush();
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
Ok(mode) if mode == "sleep" => {
|
|
109
|
+
std::thread::sleep(Duration::from_secs(2));
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
Ok(mode) if mode == "descendant" => {}
|
|
113
|
+
Err(std::env::VarError::NotPresent) => {}
|
|
114
|
+
Ok(_) | Err(std::env::VarError::NotUnicode(_)) => std::process::exit(2),
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
append_stage("parent_entry_ms");
|
|
118
|
+
let receipt = receipt_path();
|
|
119
|
+
if append_receipt(&receipt, "parent_started_once", 1).is_err()
|
|
120
|
+
|| append_receipt(&receipt, "parent_argv_exact", 1).is_err()
|
|
121
|
+
|| append_receipt(&receipt, "parent_pid", std::process::id()).is_err()
|
|
122
|
+
|| append_receipt(&receipt, "child_spawn_attempts", 1).is_err()
|
|
123
|
+
{
|
|
124
|
+
std::process::exit(4);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
let executable = std::env::current_exe().unwrap_or_else(|_| std::process::exit(4));
|
|
128
|
+
append_stage("parent_before_child_spawn_ms");
|
|
129
|
+
let child = Command::new(executable)
|
|
130
|
+
.arg(CHILD_FLAG)
|
|
131
|
+
.env(RECEIPT_ENV, &receipt)
|
|
132
|
+
.stdin(Stdio::null())
|
|
133
|
+
.stdout(Stdio::inherit())
|
|
134
|
+
.stderr(Stdio::null())
|
|
135
|
+
.spawn()
|
|
136
|
+
.unwrap_or_else(|_| std::process::exit(4));
|
|
137
|
+
append_stage("parent_after_child_spawn_ms");
|
|
138
|
+
if append_receipt(&receipt, "child_pid", child.id()).is_err() {
|
|
139
|
+
std::process::exit(4);
|
|
140
|
+
}
|
|
141
|
+
append_stage("parent_before_exit_ms");
|
|
142
|
+
// The parent exits successfully without waiting; the child retains the
|
|
143
|
+
// inherited stdout pipe and self-exits after the real reader deadline.
|
|
144
|
+
}
|
|
@@ -28,10 +28,10 @@ use crate::layout::worker_env::{
|
|
|
28
28
|
use crate::model::enums::{PaneLiveness, Provider};
|
|
29
29
|
use crate::transport::{
|
|
30
30
|
command_basename, normalize_capture, tmux_capture_argv, tmux_query_argv, tmux_send_keys_argv,
|
|
31
|
-
tmux_spawn_argv, tmux_submit_key_name, AttachOutcome,
|
|
32
|
-
InjectPayload, InjectStage, InjectVerification, InputSurfaceProbe, Key,
|
|
33
|
-
|
|
34
|
-
TransportError, TurnVerification, WindowName,
|
|
31
|
+
tmux_spawn_argv, tmux_submit_key_name, AttachOutcome, BusySignalKind, CaptureRange,
|
|
32
|
+
CaptureSampleOutcome, InjectPayload, InjectStage, InjectVerification, InputSurfaceProbe, Key,
|
|
33
|
+
PaneField, PaneId, QueryOutcome, SessionName, SetEnvOutcome, SubmitConsumptionReason,
|
|
34
|
+
SubmitVerification, Target, Transport, TransportError, TurnVerification, WindowName,
|
|
35
35
|
};
|
|
36
36
|
|
|
37
37
|
type RecordedArgv = Arc<Mutex<Vec<Vec<String>>>>;
|
|
@@ -1051,6 +1051,15 @@ fn inject_text_runs_buffer_paste_submit_sequence_and_reports_submit() {
|
|
|
1051
1051
|
SubmitVerification::EnterSentWithoutPlaceholderCheck
|
|
1052
1052
|
);
|
|
1053
1053
|
assert_eq!(report.turn_verification, TurnVerification::NotYetObserved);
|
|
1054
|
+
let diagnostics = report.submit_diagnostics.expect("diagnostics");
|
|
1055
|
+
assert_eq!(
|
|
1056
|
+
diagnostics.consumption_reason,
|
|
1057
|
+
SubmitConsumptionReason::NoMarker
|
|
1058
|
+
);
|
|
1059
|
+
assert_eq!(diagnostics.busy_signal_kind, None);
|
|
1060
|
+
assert_eq!(diagnostics.current_marker_in_bottom_15, None);
|
|
1061
|
+
assert_eq!(diagnostics.paste_identity_in_composer, None);
|
|
1062
|
+
assert_eq!(diagnostics.consumption_from_capture_result, None);
|
|
1054
1063
|
}
|
|
1055
1064
|
|
|
1056
1065
|
#[test]
|
|
@@ -1409,10 +1418,18 @@ fn e46_post_submit_matched_token_without_scroll_is_unverified() {
|
|
|
1409
1418
|
"token still in composer and no busy must report 没开跑, not unknown/started"
|
|
1410
1419
|
);
|
|
1411
1420
|
let diagnostics = report.submit_diagnostics.expect("diagnostics");
|
|
1421
|
+
assert_eq!(diagnostics.busy_signal_kind, None);
|
|
1422
|
+
assert_eq!(diagnostics.current_marker_in_bottom_15, Some(true));
|
|
1423
|
+
assert_eq!(diagnostics.paste_identity_in_composer, None);
|
|
1424
|
+
assert_eq!(diagnostics.consumption_from_capture_result, Some(false));
|
|
1412
1425
|
assert!(
|
|
1413
1426
|
diagnostics.attempts_detail.iter().any(|obs| obs.matched),
|
|
1414
1427
|
"the unverified verdict must still record that paste landed"
|
|
1415
1428
|
);
|
|
1429
|
+
assert_eq!(
|
|
1430
|
+
diagnostics.consumption_reason,
|
|
1431
|
+
SubmitConsumptionReason::Unverified
|
|
1432
|
+
);
|
|
1416
1433
|
}
|
|
1417
1434
|
|
|
1418
1435
|
#[test]
|
|
@@ -1452,6 +1469,56 @@ fn e46_unconsumed_token_with_live_busy_state_is_treated_as_processing() {
|
|
|
1452
1469
|
"busy-state capture should be recorded in attempts_detail: {:?}",
|
|
1453
1470
|
diagnostics.attempts_detail
|
|
1454
1471
|
);
|
|
1472
|
+
assert_eq!(
|
|
1473
|
+
diagnostics.consumption_reason,
|
|
1474
|
+
SubmitConsumptionReason::FallbackBusy
|
|
1475
|
+
);
|
|
1476
|
+
assert_eq!(diagnostics.before_busy_signal_kind, None);
|
|
1477
|
+
assert_eq!(diagnostics.busy_signal_kind, Some(BusySignalKind::Working));
|
|
1478
|
+
assert_eq!(diagnostics.busy_line_from_bottom, Some(0));
|
|
1479
|
+
assert_eq!(diagnostics.current_marker_in_bottom_15, Some(true));
|
|
1480
|
+
assert_eq!(diagnostics.paste_identity_in_composer, None);
|
|
1481
|
+
assert_eq!(diagnostics.consumption_from_capture_result, Some(false));
|
|
1482
|
+
}
|
|
1483
|
+
|
|
1484
|
+
#[test]
|
|
1485
|
+
fn e46_capture_error_clears_previous_consumption_observation() {
|
|
1486
|
+
let token_text = "Team Agent message from leader:\n\nhi\n\n[team-agent-token:msg_capture_error]";
|
|
1487
|
+
let busy_tail = format!("{token_text}\n● Working (1s · esc to interrupt)\n");
|
|
1488
|
+
let visible = ok(token_text);
|
|
1489
|
+
let queued = vec![
|
|
1490
|
+
MockResp::Out(ok("")), // set-buffer
|
|
1491
|
+
MockResp::Out(ok("")), // paste-buffer
|
|
1492
|
+
MockResp::Out(ok("")), // delete-buffer
|
|
1493
|
+
MockResp::Out(visible.clone()), // pasted-content prompt: continue
|
|
1494
|
+
MockResp::Out(visible), // pre-submit token visibility
|
|
1495
|
+
MockResp::Out(ok("")), // Escape
|
|
1496
|
+
MockResp::Out(ok("")), // Enter
|
|
1497
|
+
MockResp::Out(ok(&busy_tail)), // first post-Enter capture: busy
|
|
1498
|
+
// Default capture error forces the next poll/fallback/resend reads to fail.
|
|
1499
|
+
];
|
|
1500
|
+
let (be, _rec) = backend_with(MockResp::Io(std::io::ErrorKind::Other), queued);
|
|
1501
|
+
let report = be
|
|
1502
|
+
.inject(
|
|
1503
|
+
&Target::Pane(PaneId::new("%7")),
|
|
1504
|
+
&InjectPayload::Text(token_text.to_string()),
|
|
1505
|
+
Key::Enter,
|
|
1506
|
+
true,
|
|
1507
|
+
)
|
|
1508
|
+
.expect("inject runs through capture failure");
|
|
1509
|
+
let diagnostics = report.submit_diagnostics.expect("diagnostics");
|
|
1510
|
+
assert_eq!(
|
|
1511
|
+
report.submit_verification,
|
|
1512
|
+
SubmitVerification::SubmitConsumptionUnverified
|
|
1513
|
+
);
|
|
1514
|
+
assert_eq!(diagnostics.consumption_reason, SubmitConsumptionReason::Unverified);
|
|
1515
|
+
assert!(diagnostics.capture_err_count > 0);
|
|
1516
|
+
assert_eq!(diagnostics.before_busy_signal_kind, None);
|
|
1517
|
+
assert_eq!(diagnostics.busy_signal_kind, None);
|
|
1518
|
+
assert_eq!(diagnostics.busy_line_from_bottom, None);
|
|
1519
|
+
assert_eq!(diagnostics.current_marker_in_bottom_15, None);
|
|
1520
|
+
assert_eq!(diagnostics.paste_identity_in_composer, None);
|
|
1521
|
+
assert_eq!(diagnostics.consumption_from_capture_result, None);
|
|
1455
1522
|
}
|
|
1456
1523
|
|
|
1457
1524
|
/// NeverSeen + 无占位符:空 pane 不得再把「token 不在底部」写成 consumed。
|
|
@@ -1521,6 +1588,14 @@ fn e46_inject_text_with_token_consumed_after_enter_keeps_enter_sent_without_plac
|
|
|
1521
1588
|
hold (provider_submit_verification_red.rs:113-159). Got {:?}",
|
|
1522
1589
|
report.submit_verification
|
|
1523
1590
|
);
|
|
1591
|
+
let diagnostics = report.submit_diagnostics.expect("diagnostics");
|
|
1592
|
+
assert_eq!(
|
|
1593
|
+
diagnostics.consumption_reason,
|
|
1594
|
+
SubmitConsumptionReason::ConsumptionFromCapture
|
|
1595
|
+
);
|
|
1596
|
+
assert_eq!(diagnostics.busy_signal_kind, None);
|
|
1597
|
+
assert_eq!(diagnostics.current_marker_in_bottom_15, Some(false));
|
|
1598
|
+
assert_eq!(diagnostics.consumption_from_capture_result, Some(true));
|
|
1524
1599
|
}
|
|
1525
1600
|
|
|
1526
1601
|
/// **E46 RED-3 (resend-to-cap, no double-submit)**: when the FIRST Enter
|
|
@@ -3289,6 +3364,15 @@ fn cursor_single_enter_busy_transcript_placeholder_does_not_retry() {
|
|
|
3289
3364
|
"busy after first Enter is consumption for cursor; got {:?}",
|
|
3290
3365
|
report.submit_verification
|
|
3291
3366
|
);
|
|
3367
|
+
let diagnostics = report.submit_diagnostics.expect("diagnostics");
|
|
3368
|
+
assert_eq!(
|
|
3369
|
+
diagnostics.consumption_reason,
|
|
3370
|
+
SubmitConsumptionReason::CursorPollingBusy
|
|
3371
|
+
);
|
|
3372
|
+
assert_eq!(diagnostics.busy_signal_kind, Some(BusySignalKind::Working));
|
|
3373
|
+
assert!(diagnostics.busy_line_from_bottom.is_some());
|
|
3374
|
+
assert!(diagnostics.current_marker_in_bottom_15.is_some());
|
|
3375
|
+
assert!(diagnostics.consumption_from_capture_result.is_some());
|
|
3292
3376
|
let calls = rec.lock().unwrap().clone();
|
|
3293
3377
|
assert_eq!(
|
|
3294
3378
|
count_submit_enters(&calls),
|
|
@@ -3570,6 +3654,39 @@ fn grok_fold_worker_text_payload_polls_consumption() {
|
|
|
3570
3654
|
);
|
|
3571
3655
|
}
|
|
3572
3656
|
|
|
3657
|
+
#[test]
|
|
3658
|
+
fn provider_busy_signal_match_preserves_category_priority_and_tail_boundary() {
|
|
3659
|
+
let cases = [
|
|
3660
|
+
("working", BusySignalKind::Working),
|
|
3661
|
+
("thinking", BusySignalKind::Thinking),
|
|
3662
|
+
("processing", BusySignalKind::Processing),
|
|
3663
|
+
("esc to interrupt", BusySignalKind::EscToInterrupt),
|
|
3664
|
+
("⠋", BusySignalKind::SpinnerGlyph),
|
|
3665
|
+
("working ⠋", BusySignalKind::Working),
|
|
3666
|
+
];
|
|
3667
|
+
for (text, expected) in cases {
|
|
3668
|
+
let matched = super::provider_busy_signal_match(text).expect("busy match");
|
|
3669
|
+
assert_eq!(matched.kind, expected, "text={text:?}");
|
|
3670
|
+
assert_eq!(matched.line_from_bottom, 0, "text={text:?}");
|
|
3671
|
+
assert!(super::provider_busy_signal_in_tail(text));
|
|
3672
|
+
}
|
|
3673
|
+
|
|
3674
|
+
let inside_bottom_15 = std::iter::once("working".to_string())
|
|
3675
|
+
.chain((0..14).map(|index| format!("tail-{index}")))
|
|
3676
|
+
.collect::<Vec<_>>()
|
|
3677
|
+
.join("\n");
|
|
3678
|
+
let matched = super::provider_busy_signal_match(&inside_bottom_15).expect("15th line");
|
|
3679
|
+
assert_eq!(matched.kind, BusySignalKind::Working);
|
|
3680
|
+
assert_eq!(matched.line_from_bottom, 14);
|
|
3681
|
+
|
|
3682
|
+
let outside_bottom_15 = std::iter::once("working".to_string())
|
|
3683
|
+
.chain((0..15).map(|index| format!("tail-{index}")))
|
|
3684
|
+
.collect::<Vec<_>>()
|
|
3685
|
+
.join("\n");
|
|
3686
|
+
assert!(super::provider_busy_signal_match(&outside_bottom_15).is_none());
|
|
3687
|
+
assert!(!super::provider_busy_signal_in_tail(&outside_bottom_15));
|
|
3688
|
+
}
|
|
3689
|
+
|
|
3573
3690
|
const G4_TOKEN: &str = "[team-agent-token:msg_g4]";
|
|
3574
3691
|
|
|
3575
3692
|
#[test]
|