@team-agent/installer 0.5.77 → 0.5.80
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 +161 -29
- package/crates/team-agent/src/cli/diagnose.rs +333 -68
- package/crates/team-agent/src/cli/emit.rs +24 -8
- package/crates/team-agent/src/cli/mod.rs +543 -98
- package/crates/team-agent/src/cli/models.rs +393 -32
- 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/compact.rs +12 -8
- package/crates/team-agent/src/cli/status_port/runtime.rs +3 -1
- 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/tests.rs +37 -1
- package/crates/team-agent/src/cli/tests/missing_subcommands.rs +62 -8
- package/crates/team-agent/src/cli/tests/named_address.rs +70 -0
- 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/coordinator/steps/abnormal.rs +122 -2
- package/crates/team-agent/src/leader/lease.rs +492 -33
- 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 +197 -88
- package/crates/team-agent/src/leader/tests/wake_start_owner.rs +15 -9
- 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 +86 -23
- 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 +996 -66
- 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/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 -4
- 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/fixtures/pi_list_models_spaced_ids.stdout.txt +9 -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 +3 -2
- 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 +54 -9
- package/crates/team-agent/src/lifecycle/tests/pi_catalog_red.rs +71 -0
- 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/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 +8 -0
- package/crates/team-agent/src/mcp_server/wire.rs +140 -11
- package/crates/team-agent/src/messaging/delivery.rs +221 -80
- package/crates/team-agent/src/messaging/leader_channel.rs +56 -0
- package/crates/team-agent/src/messaging/tests/leader_inject_acceptance.rs +127 -0
- package/crates/team-agent/src/messaging/tests/runtime.rs +10 -2
- package/crates/team-agent/src/provider/adapters/cursor_agent.rs +2 -5
- package/crates/team-agent/src/provider/session_scan/common/tests.rs +204 -0
- package/crates/team-agent/src/provider/session_scan/common.rs +24 -2
- package/crates/team-agent/src/state/abnormal_watch/tests/mod.rs +248 -0
- package/crates/team-agent/src/state/abnormal_watch.rs +144 -0
- package/crates/team-agent/src/state/mod.rs +1 -0
- package/crates/team-agent/src/state/persist.rs +102 -4
- package/crates/team-agent/src/state/projection.rs +7 -1
- package/crates/team-agent/src/state/repository.rs +29 -0
- package/crates/team-agent/src/test_support/models_timeout_fixture.rs +43 -21
- package/crates/team-agent/src/tmux_backend/tests.rs +149 -4
- package/crates/team-agent/src/tmux_backend.rs +239 -127
- package/crates/team-agent/src/transport.rs +78 -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
|
@@ -70,6 +70,7 @@ mod common;
|
|
|
70
70
|
mod orchestrator;
|
|
71
71
|
pub mod preflight;
|
|
72
72
|
mod rebuild;
|
|
73
|
+
pub use rebuild::{restart_bind_status, restart_leader_public_bind};
|
|
73
74
|
pub(crate) mod remove;
|
|
74
75
|
mod selection;
|
|
75
76
|
mod team_state;
|
|
@@ -92,8 +93,7 @@ pub(crate) use rebuild::restart_with_transport_with_session_convergence_deadline
|
|
|
92
93
|
// timer + worker timing writer so `lifecycle::launch::launch_with_transport_in_workspace`
|
|
93
94
|
// can emit the same instrumentation event family with `source="launch"`.
|
|
94
95
|
pub(crate) use rebuild::{
|
|
95
|
-
provider_wire_from_state,
|
|
96
|
-
RestartPhaseTimer,
|
|
96
|
+
provider_wire_from_state, write_worker_spawn_timing_event, RestartPhaseTimer,
|
|
97
97
|
};
|
|
98
98
|
pub use rebuild::{restart, restart_with_session_convergence_deadline};
|
|
99
99
|
pub(crate) use rebuild::{
|
|
@@ -147,6 +147,9 @@ gate — N35/MUST-17); found {hits} occurrence(s)"
|
|
|
147
147
|
#[test]
|
|
148
148
|
#[serial(env)]
|
|
149
149
|
fn b2_quick_start_ready_has_attach_commands_and_socket_miss_is_observable() {
|
|
150
|
+
let hermetic = hermetic_guard::HermeticTestEnv::enter("acceptance-b-b2-quick-start");
|
|
151
|
+
hermetic.scrub_tmux();
|
|
152
|
+
hermetic.assert_no_real_tmux();
|
|
150
153
|
let _g = EnvGuard::set(&[(ANCESTRY_KEY, NEUTRAL_ANCESTRY)]);
|
|
151
154
|
let ws = tmp_ws("b2-attach");
|
|
152
155
|
let team_dir = write_min_team(&ws, "b2team");
|
|
@@ -54,6 +54,9 @@ const NEUTRAL_ANCESTRY: &str = "[\"/bin/zsh\"]";
|
|
|
54
54
|
#[test]
|
|
55
55
|
#[serial(env)]
|
|
56
56
|
fn d1_d2_d3_codex_argv_matches_python_golden_escape_timeout_profile() {
|
|
57
|
+
let hermetic = hermetic_guard::HermeticTestEnv::enter("behavioral-d123-codex");
|
|
58
|
+
hermetic.scrub_tmux();
|
|
59
|
+
hermetic.assert_no_real_tmux();
|
|
57
60
|
let _ancestry = EnvGuard::set(&[(ANCESTRY_KEY, NEUTRAL_ANCESTRY)]);
|
|
58
61
|
let ws = tmp_dir("d123-codex-argv");
|
|
59
62
|
let team_dir = write_team(
|
|
@@ -178,6 +181,9 @@ fn d1_d2_d3_codex_argv_matches_python_golden_escape_timeout_profile() {
|
|
|
178
181
|
#[test]
|
|
179
182
|
#[serial(env)]
|
|
180
183
|
fn d4_worker_shell_env_sets_team_agent_id_to_worker_self() {
|
|
184
|
+
let hermetic = hermetic_guard::HermeticTestEnv::enter("behavioral-d4-shell-env");
|
|
185
|
+
hermetic.scrub_tmux();
|
|
186
|
+
hermetic.assert_no_real_tmux();
|
|
181
187
|
let _guard = EnvGuard::set(&[
|
|
182
188
|
(ANCESTRY_KEY, NEUTRAL_ANCESTRY),
|
|
183
189
|
("TEAM_AGENT_ID", "stale-mcp-caller"),
|
|
@@ -218,6 +224,9 @@ fn d4_worker_shell_env_sets_team_agent_id_to_worker_self() {
|
|
|
218
224
|
#[test]
|
|
219
225
|
#[serial(env)]
|
|
220
226
|
fn d5_fresh_launch_spawn_cwd_and_state_use_workspace() {
|
|
227
|
+
let hermetic = hermetic_guard::HermeticTestEnv::enter("behavioral-d5-spawn-cwd");
|
|
228
|
+
hermetic.scrub_tmux();
|
|
229
|
+
hermetic.assert_no_real_tmux();
|
|
221
230
|
let _ancestry = EnvGuard::set(&[(ANCESTRY_KEY, NEUTRAL_ANCESTRY)]);
|
|
222
231
|
let ws = tmp_dir("d5-spawn-cwd");
|
|
223
232
|
let team_dir = write_team(&ws, &simple_codex_team("d5team", "worker_a", "Cwd Worker"));
|
|
@@ -259,6 +268,9 @@ fn d5_fresh_launch_spawn_cwd_and_state_use_workspace() {
|
|
|
259
268
|
#[test]
|
|
260
269
|
#[serial(env)]
|
|
261
270
|
fn d6_compiled_system_prompt_first_section_is_identity() {
|
|
271
|
+
let hermetic = hermetic_guard::HermeticTestEnv::enter("behavioral-d6-prompt");
|
|
272
|
+
hermetic.scrub_tmux();
|
|
273
|
+
hermetic.assert_no_real_tmux();
|
|
262
274
|
let _ancestry = EnvGuard::set(&[(ANCESTRY_KEY, NEUTRAL_ANCESTRY)]);
|
|
263
275
|
let ws = tmp_dir("d6-prompt-order");
|
|
264
276
|
let team_dir = write_team(
|
|
@@ -311,6 +323,9 @@ chunks=[identity, TEAMMATE_SYSTEM_PROMPT, ...]; live ps confirms identity-first)
|
|
|
311
323
|
#[test]
|
|
312
324
|
#[serial(env)]
|
|
313
325
|
fn d7_runtime_fast_sends_codex_fast_mode_toggle() {
|
|
326
|
+
let hermetic = hermetic_guard::HermeticTestEnv::enter("behavioral-d7-fast-mode");
|
|
327
|
+
hermetic.scrub_tmux();
|
|
328
|
+
hermetic.assert_no_real_tmux();
|
|
314
329
|
let _ancestry = EnvGuard::set(&[(ANCESTRY_KEY, NEUTRAL_ANCESTRY)]);
|
|
315
330
|
let ws = tmp_dir("d7-fast-mode");
|
|
316
331
|
let mut fixture = simple_codex_team("d7team", "worker_a", "Fast Worker");
|
|
@@ -16,6 +16,7 @@ use std::sync::atomic::{AtomicU64, Ordering};
|
|
|
16
16
|
use std::sync::Mutex;
|
|
17
17
|
|
|
18
18
|
use serde_json::{json, Value};
|
|
19
|
+
use serial_test::serial;
|
|
19
20
|
use team_agent::lifecycle::quick_start_with_transport_in_workspace;
|
|
20
21
|
use team_agent::model::enums::{AuthMode, Provider};
|
|
21
22
|
use team_agent::provider::{
|
|
@@ -29,7 +30,11 @@ use team_agent::transport::{
|
|
|
29
30
|
};
|
|
30
31
|
|
|
31
32
|
#[test]
|
|
33
|
+
#[serial(env)]
|
|
32
34
|
fn compatible_api_profile_writes_claude_settings_and_project_state_with_mcp_servers() {
|
|
35
|
+
let hermetic = hermetic_guard::HermeticTestEnv::enter("claude-compatible-config");
|
|
36
|
+
hermetic.scrub_tmux();
|
|
37
|
+
hermetic.assert_no_real_tmux();
|
|
33
38
|
let ws = tmp_dir("compatible-config-files");
|
|
34
39
|
let team = write_claude_profile_team(&ws, "cfgteam", "clauder");
|
|
35
40
|
let transport = RecordingTransport::default();
|
|
@@ -34,6 +34,9 @@ use team_agent::transport::{
|
|
|
34
34
|
#[test]
|
|
35
35
|
#[serial(env)]
|
|
36
36
|
fn compatible_api_profile_quick_start_spawns_claude_with_profile_env_and_state_metadata() {
|
|
37
|
+
let hermetic = hermetic_guard::HermeticTestEnv::enter("claude-profile-quick-start");
|
|
38
|
+
hermetic.scrub_tmux();
|
|
39
|
+
hermetic.assert_no_real_tmux();
|
|
37
40
|
assert_s0_profile_launch_shape();
|
|
38
41
|
|
|
39
42
|
let ws = tmp_dir("profile-launch");
|
|
@@ -133,17 +136,13 @@ fn launch_restart_start_add_and_fork_all_delegate_to_the_profile_launch_resolver
|
|
|
133
136
|
("launch quick-start", &launch, "spawn_agents"),
|
|
134
137
|
("add-agent", &launch, "add_agent_with_transport"),
|
|
135
138
|
("fork-agent", &launch, "fork_agent_with_transport"),
|
|
136
|
-
(
|
|
137
|
-
"restart/start shared spawn",
|
|
138
|
-
&restart_common,
|
|
139
|
-
"pub(super) fn spawn_agent_window(",
|
|
140
|
-
),
|
|
139
|
+
("restart", &restart_common, "restart_with_transport"),
|
|
141
140
|
];
|
|
142
141
|
for (surface, source, entry) in surfaces {
|
|
143
142
|
let section = source
|
|
144
143
|
.find(entry)
|
|
145
144
|
.map(|idx| &source[idx..])
|
|
146
|
-
.
|
|
145
|
+
.unwrap_or(source.as_str());
|
|
147
146
|
assert!(
|
|
148
147
|
section.contains("prepare_provider_profile_launch"),
|
|
149
148
|
"{surface} must call the single profile resolver before building/spawning provider argv; entry={entry}"
|
|
@@ -153,19 +152,18 @@ fn launch_restart_start_add_and_fork_all_delegate_to_the_profile_launch_resolver
|
|
|
153
152
|
let start_agent = restart_agent
|
|
154
153
|
.find("pub(crate) fn start_agent_at_paths(")
|
|
155
154
|
.map(|idx| &restart_agent[idx..])
|
|
156
|
-
.
|
|
155
|
+
.unwrap_or(restart_agent.as_str());
|
|
157
156
|
assert!(
|
|
158
157
|
start_agent.contains("spawn_agent_window("),
|
|
159
|
-
"start-agent must delegate
|
|
158
|
+
"start-agent must delegate spawning through the shared spawn helper"
|
|
160
159
|
);
|
|
161
|
-
|
|
162
160
|
let spawn_agent_window = restart_common
|
|
163
|
-
.find("pub(super) fn spawn_agent_window
|
|
161
|
+
.find("pub(super) fn spawn_agent_window")
|
|
164
162
|
.map(|idx| &restart_common[idx..])
|
|
165
|
-
.
|
|
163
|
+
.unwrap_or(restart_common.as_str());
|
|
166
164
|
assert!(
|
|
167
|
-
spawn_agent_window.contains("
|
|
168
|
-
"shared
|
|
165
|
+
spawn_agent_window.contains("prepare_provider_profile_launch"),
|
|
166
|
+
"the shared start-agent spawn helper must call the single profile resolver before building/spawning provider argv"
|
|
169
167
|
);
|
|
170
168
|
}
|
|
171
169
|
|
|
@@ -16,9 +16,12 @@
|
|
|
16
16
|
#![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
|
|
17
17
|
|
|
18
18
|
use std::collections::BTreeSet;
|
|
19
|
+
use std::io::Write as _;
|
|
19
20
|
use std::path::{Path, PathBuf};
|
|
20
21
|
use std::process::Output;
|
|
21
22
|
|
|
23
|
+
use serde_json::{json, Value as JsonValue};
|
|
24
|
+
|
|
22
25
|
#[path = "../../../tests/support/hermetic.rs"]
|
|
23
26
|
mod hermetic_guard;
|
|
24
27
|
use hermetic_guard::HermeticTestEnv;
|
|
@@ -40,6 +43,8 @@ const SIX: &[&str] = &[
|
|
|
40
43
|
"provider_builtin",
|
|
41
44
|
];
|
|
42
45
|
const THREE: &[&str] = &["fs_list", "fs_read", "mcp_team"];
|
|
46
|
+
const OBSERVED_AGENT_IDS: &[&str] = &[CLONE, NARROW_CLONE];
|
|
47
|
+
const DIAGNOSTIC_FILE: &str = "clone-agent-presence.jsonl";
|
|
43
48
|
|
|
44
49
|
struct Case {
|
|
45
50
|
env: HermeticTestEnv,
|
|
@@ -63,6 +68,60 @@ impl Case {
|
|
|
63
68
|
self.env.run_cli(&self.workspace, args)
|
|
64
69
|
}
|
|
65
70
|
|
|
71
|
+
/// Test-only bounded observation. It records only fixed fixture IDs,
|
|
72
|
+
/// selected team, managed-role presence, and event type/sequence/time;
|
|
73
|
+
/// never serializes state/spec/role contents or environment values.
|
|
74
|
+
fn observe(&self, phase: &str) {
|
|
75
|
+
let selected =
|
|
76
|
+
crate::state::projection::select_runtime_state(&self.workspace, Some(TEAM_NAME)).ok();
|
|
77
|
+
let raw = crate::state::persist::load_runtime_state(&self.workspace).ok();
|
|
78
|
+
let selected_team = selected
|
|
79
|
+
.as_ref()
|
|
80
|
+
.and_then(|state| {
|
|
81
|
+
state
|
|
82
|
+
.get("team_key")
|
|
83
|
+
.or_else(|| state.get("active_team_key"))
|
|
84
|
+
})
|
|
85
|
+
.and_then(JsonValue::as_str)
|
|
86
|
+
.map(str::to_string);
|
|
87
|
+
let selected_agents = selected.as_ref().and_then(|state| state.get("agents"));
|
|
88
|
+
let raw_agents = raw.as_ref().and_then(|state| state.get("agents"));
|
|
89
|
+
let team_agents = raw
|
|
90
|
+
.as_ref()
|
|
91
|
+
.and_then(|state| state.get("teams"))
|
|
92
|
+
.and_then(|teams| teams.get(TEAM_NAME))
|
|
93
|
+
.and_then(|team| team.get("agents"));
|
|
94
|
+
let record = json!({
|
|
95
|
+
"phase": phase,
|
|
96
|
+
"observed_at": chrono::Utc::now().to_rfc3339(),
|
|
97
|
+
"selected_read": selected.is_some(),
|
|
98
|
+
"raw_state_read": raw.is_some(),
|
|
99
|
+
"selected_team": selected_team,
|
|
100
|
+
"presence": {
|
|
101
|
+
"selected_agents": fixed_presence(selected_agents),
|
|
102
|
+
"raw_agents": fixed_presence(raw_agents),
|
|
103
|
+
"team_agents": fixed_presence(team_agents),
|
|
104
|
+
"managed_roles": managed_role_presence(&self.workspace),
|
|
105
|
+
},
|
|
106
|
+
"events": relevant_event_marks(&self.workspace),
|
|
107
|
+
});
|
|
108
|
+
let path = self
|
|
109
|
+
.workspace
|
|
110
|
+
.join(".team")
|
|
111
|
+
.join("diagnostics")
|
|
112
|
+
.join(DIAGNOSTIC_FILE);
|
|
113
|
+
if let Some(parent) = path.parent() {
|
|
114
|
+
let _ = std::fs::create_dir_all(parent);
|
|
115
|
+
}
|
|
116
|
+
if let Ok(mut file) = std::fs::OpenOptions::new()
|
|
117
|
+
.create(true)
|
|
118
|
+
.append(true)
|
|
119
|
+
.open(path)
|
|
120
|
+
{
|
|
121
|
+
let _ = writeln!(file, "{record}");
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
66
125
|
fn shutdown(&self) {
|
|
67
126
|
let _ = self.run(&[
|
|
68
127
|
"shutdown",
|
|
@@ -148,6 +207,60 @@ fn write_role(workspace: &Path, name: &str, tools: &[&str]) {
|
|
|
148
207
|
.expect("role doc");
|
|
149
208
|
}
|
|
150
209
|
|
|
210
|
+
fn fixed_presence(agents: Option<&JsonValue>) -> JsonValue {
|
|
211
|
+
let mut result = serde_json::Map::new();
|
|
212
|
+
for agent_id in OBSERVED_AGENT_IDS {
|
|
213
|
+
let present = agents
|
|
214
|
+
.and_then(JsonValue::as_object)
|
|
215
|
+
.is_some_and(|entries| entries.contains_key(*agent_id));
|
|
216
|
+
result.insert((*agent_id).to_string(), JsonValue::Bool(present));
|
|
217
|
+
}
|
|
218
|
+
JsonValue::Object(result)
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
fn managed_role_presence(workspace: &Path) -> JsonValue {
|
|
222
|
+
let managed = workspace.join(".team").join("dynamic-role-files");
|
|
223
|
+
let mut result = serde_json::Map::new();
|
|
224
|
+
for agent_id in OBSERVED_AGENT_IDS {
|
|
225
|
+
result.insert(
|
|
226
|
+
(*agent_id).to_string(),
|
|
227
|
+
JsonValue::Bool(managed.join(format!("{agent_id}.md")).is_file()),
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
JsonValue::Object(result)
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
fn relevant_event_marks(workspace: &Path) -> JsonValue {
|
|
234
|
+
let events = crate::event_log::EventLog::new(workspace)
|
|
235
|
+
.tail(256)
|
|
236
|
+
.unwrap_or_default();
|
|
237
|
+
let mut marks = Vec::new();
|
|
238
|
+
for (sequence, event) in events.into_iter().enumerate() {
|
|
239
|
+
let Some(event_name) = event.get("event").and_then(JsonValue::as_str) else {
|
|
240
|
+
continue;
|
|
241
|
+
};
|
|
242
|
+
let event_agent = ["agent_id", "source_agent_id", "new_agent_id", "as_agent_id"]
|
|
243
|
+
.into_iter()
|
|
244
|
+
.find_map(|field| {
|
|
245
|
+
event
|
|
246
|
+
.get(field)
|
|
247
|
+
.and_then(JsonValue::as_str)
|
|
248
|
+
.filter(|value| OBSERVED_AGENT_IDS.contains(&value))
|
|
249
|
+
});
|
|
250
|
+
let Some(agent_id) = event_agent else {
|
|
251
|
+
continue;
|
|
252
|
+
};
|
|
253
|
+
marks.push(json!({
|
|
254
|
+
"sequence": sequence,
|
|
255
|
+
"sequence_scope": "tail_256",
|
|
256
|
+
"event": event_name,
|
|
257
|
+
"agent": agent_id,
|
|
258
|
+
"ts": event.get("ts").and_then(JsonValue::as_str),
|
|
259
|
+
}));
|
|
260
|
+
}
|
|
261
|
+
JsonValue::Array(marks)
|
|
262
|
+
}
|
|
263
|
+
|
|
151
264
|
fn role_tools(path: &Path) -> BTreeSet<String> {
|
|
152
265
|
let (meta, _) = team_agent::compiler::read_front_matter(path)
|
|
153
266
|
.unwrap_or_else(|e| panic!("read {}: {e}", path.display()));
|
|
@@ -210,7 +323,8 @@ fn set_of(items: &[&str]) -> BTreeSet<String> {
|
|
|
210
323
|
items.iter().map(|s| (*s).to_string()).collect()
|
|
211
324
|
}
|
|
212
325
|
|
|
213
|
-
fn clone_ok(case: &Case, source: &str, dest: &str) {
|
|
326
|
+
fn clone_ok(case: &Case, source: &str, dest: &str, phase: &str) {
|
|
327
|
+
case.observe(&format!("{phase}_before"));
|
|
214
328
|
let out = case.run(&[
|
|
215
329
|
"clone-agent",
|
|
216
330
|
source,
|
|
@@ -225,6 +339,7 @@ fn clone_ok(case: &Case, source: &str, dest: &str) {
|
|
|
225
339
|
]);
|
|
226
340
|
let stderr = String::from_utf8_lossy(&out.stderr);
|
|
227
341
|
let stdout = String::from_utf8_lossy(&out.stdout);
|
|
342
|
+
case.observe(&format!("{phase}_after"));
|
|
228
343
|
assert!(
|
|
229
344
|
out.status.success(),
|
|
230
345
|
"clone-agent {source} --as {dest} must exit 0; code={:?} stderr={stderr} stdout={stdout}",
|
|
@@ -234,7 +349,6 @@ fn clone_ok(case: &Case, source: &str, dest: &str) {
|
|
|
234
349
|
|
|
235
350
|
/// clone 后分身 role + spec tools 必须等于源席;三件套源不得被扩成六件。
|
|
236
351
|
#[test]
|
|
237
|
-
#[serial_test::serial(env)]
|
|
238
352
|
fn clone_agent_preserves_source_tools() {
|
|
239
353
|
let case = Case::start();
|
|
240
354
|
let qs = case.run(&[
|
|
@@ -247,6 +361,7 @@ fn clone_agent_preserves_source_tools() {
|
|
|
247
361
|
"--yes",
|
|
248
362
|
"--json",
|
|
249
363
|
]);
|
|
364
|
+
case.observe("after_quick_start");
|
|
250
365
|
let spec = case
|
|
251
366
|
.workspace
|
|
252
367
|
.join(".team")
|
|
@@ -278,7 +393,7 @@ fn clone_agent_preserves_source_tools() {
|
|
|
278
393
|
"leader must stay the three-set ceiling so the conflict surface exists; leader={leader_spec:?}"
|
|
279
394
|
);
|
|
280
395
|
|
|
281
|
-
clone_ok(&case, SOURCE, CLONE);
|
|
396
|
+
clone_ok(&case, SOURCE, CLONE, "first_clone");
|
|
282
397
|
|
|
283
398
|
let clone_role = role_tools(
|
|
284
399
|
&case
|
|
@@ -299,7 +414,7 @@ fn clone_agent_preserves_source_tools() {
|
|
|
299
414
|
|
|
300
415
|
let narrow_src = spec_tools(&case.workspace, NARROW);
|
|
301
416
|
assert_eq!(narrow_src, set_of(THREE), "narrow source fixture");
|
|
302
|
-
clone_ok(&case, NARROW, NARROW_CLONE);
|
|
417
|
+
clone_ok(&case, NARROW, NARROW_CLONE, "second_clone");
|
|
303
418
|
let narrow_clone = spec_tools(&case.workspace, NARROW_CLONE);
|
|
304
419
|
assert_eq!(
|
|
305
420
|
narrow_clone, narrow_src,
|
|
@@ -29,7 +29,11 @@ use team_agent::transport::{
|
|
|
29
29
|
const CONTRACT_HEADING_PREFIX: &str = "# Team Agent communication contract: ";
|
|
30
30
|
|
|
31
31
|
#[test]
|
|
32
|
+
#[serial_test::serial(env)]
|
|
32
33
|
fn t06_each_official_mode_selects_one_runtime_contract_in_real_spawn_prompt() {
|
|
34
|
+
let hermetic = hermetic_guard::HermeticTestEnv::enter("communication-mode-official");
|
|
35
|
+
hermetic.scrub_tmux();
|
|
36
|
+
hermetic.assert_no_real_tmux();
|
|
33
37
|
let root = fixture("official", None);
|
|
34
38
|
let transport = RecordingTransport::default();
|
|
35
39
|
quick_start_with_transport_in_workspace(
|
|
@@ -81,7 +85,11 @@ fn t06_each_official_mode_selects_one_runtime_contract_in_real_spawn_prompt() {
|
|
|
81
85
|
}
|
|
82
86
|
|
|
83
87
|
#[test]
|
|
88
|
+
#[serial_test::serial(env)]
|
|
84
89
|
fn t06_selected_templates_project_the_two_signed_communication_boundaries() {
|
|
90
|
+
let hermetic = hermetic_guard::HermeticTestEnv::enter("communication-mode-semantics");
|
|
91
|
+
hermetic.scrub_tmux();
|
|
92
|
+
hermetic.assert_no_real_tmux();
|
|
85
93
|
let root = fixture("semantics", None);
|
|
86
94
|
let transport = RecordingTransport::default();
|
|
87
95
|
quick_start_with_transport_in_workspace(
|
|
@@ -121,7 +129,11 @@ fn t06_selected_templates_project_the_two_signed_communication_boundaries() {
|
|
|
121
129
|
}
|
|
122
130
|
|
|
123
131
|
#[test]
|
|
132
|
+
#[serial_test::serial(env)]
|
|
124
133
|
fn t07_final_spawn_prompt_does_not_leak_leader_centric_obligations_into_orchestrated() {
|
|
134
|
+
let hermetic = hermetic_guard::HermeticTestEnv::enter("communication-mode-negative-boundary");
|
|
135
|
+
hermetic.scrub_tmux();
|
|
136
|
+
hermetic.assert_no_real_tmux();
|
|
125
137
|
let root = fixture("negative-boundary", None);
|
|
126
138
|
let transport = RecordingTransport::default();
|
|
127
139
|
quick_start_with_transport_in_workspace(
|
|
@@ -31,6 +31,13 @@ mod hermetic_guard;
|
|
|
31
31
|
#[allow(dead_code)]
|
|
32
32
|
fn _hermetic_boundary_marker(_: &hermetic_guard::HermeticTestEnv) {}
|
|
33
33
|
|
|
34
|
+
fn enter_hermetic(tag: &str) -> hermetic_guard::HermeticTestEnv {
|
|
35
|
+
let env = hermetic_guard::HermeticTestEnv::enter(tag);
|
|
36
|
+
env.scrub_tmux();
|
|
37
|
+
env.assert_no_real_tmux();
|
|
38
|
+
env
|
|
39
|
+
}
|
|
40
|
+
|
|
34
41
|
use std::collections::{BTreeMap, BTreeSet};
|
|
35
42
|
use std::path::{Path, PathBuf};
|
|
36
43
|
use std::process::{Command, Stdio};
|
|
@@ -64,6 +71,7 @@ const COPILOT_TMP_PREFIX: &str = "ta-rs-copilot";
|
|
|
64
71
|
#[test]
|
|
65
72
|
#[serial(env)]
|
|
66
73
|
fn copilot_argv_golden_restricted_role() {
|
|
74
|
+
let _hermetic = enter_hermetic("copilot-argv-restricted");
|
|
67
75
|
let _guard = EnvGuard::set(&[(ANCESTRY_KEY, NEUTRAL_ANCESTRY)]);
|
|
68
76
|
let ws = tmp_ws("argv-restricted");
|
|
69
77
|
let team_dir = write_copilot_team(&ws, "cp-restricted", &["mcp_team"], false);
|
|
@@ -235,6 +243,7 @@ team_orchestrator or @<.team/runtime/mcp/worker_a.json>; got {value:?}"
|
|
|
235
243
|
#[test]
|
|
236
244
|
#[serial(env)]
|
|
237
245
|
fn copilot_argv_dangerous_maps_to_allow_all() {
|
|
246
|
+
let _hermetic = enter_hermetic("copilot-argv-dangerous");
|
|
238
247
|
let _guard = EnvGuard::set(&[(ANCESTRY_KEY, NEUTRAL_ANCESTRY)]);
|
|
239
248
|
let ws = tmp_ws("argv-dangerous");
|
|
240
249
|
let team_dir = write_copilot_team(&ws, "cp-dangerous", &["mcp_team"], true);
|
|
@@ -285,6 +294,7 @@ used for the dangerous tier; argv={argv:?}"
|
|
|
285
294
|
#[test]
|
|
286
295
|
#[serial(env)]
|
|
287
296
|
fn copilot_agents_md_is_the_compiled_prompt_and_env_points_at_it() {
|
|
297
|
+
let _hermetic = enter_hermetic("copilot-agents-md");
|
|
288
298
|
let home = tmp_ws("fake-home");
|
|
289
299
|
let _guard = EnvGuard::set(&[
|
|
290
300
|
(ANCESTRY_KEY, NEUTRAL_ANCESTRY),
|
|
@@ -384,6 +394,7 @@ with the identity section FIRST; head={:?}",
|
|
|
384
394
|
#[test]
|
|
385
395
|
#[serial(env)]
|
|
386
396
|
fn copilot_permission_note_registers_fs_tools_prompt_only() {
|
|
397
|
+
let _hermetic = enter_hermetic("copilot-permission-note");
|
|
387
398
|
let _guard = EnvGuard::set(&[(ANCESTRY_KEY, NEUTRAL_ANCESTRY)]);
|
|
388
399
|
let ws = tmp_ws("enforcement");
|
|
389
400
|
let team_dir = write_copilot_team(
|
|
@@ -441,6 +452,7 @@ agents_md_present={}",
|
|
|
441
452
|
#[test]
|
|
442
453
|
#[serial(env)]
|
|
443
454
|
fn copilot_fork_is_supported_but_missing_backing_never_falls_back_fresh() {
|
|
455
|
+
let _hermetic = enter_hermetic("copilot-fork-missing-backing");
|
|
444
456
|
let _guard = EnvGuard::set(&[(ANCESTRY_KEY, NEUTRAL_ANCESTRY)]);
|
|
445
457
|
let ws = tmp_ws("fork");
|
|
446
458
|
let team_dir = write_copilot_team(&ws, "cp-fork", &["mcp_team"], false);
|
|
@@ -715,6 +727,7 @@ leader's session); candidates={safety_lock:?}"
|
|
|
715
727
|
#[test]
|
|
716
728
|
#[serial(env)]
|
|
717
729
|
fn copilot_p0_terminal_title_disabled_and_window_name_stable() {
|
|
730
|
+
let _hermetic = enter_hermetic("copilot-terminal-title");
|
|
718
731
|
let _guard = EnvGuard::set(&[(ANCESTRY_KEY, NEUTRAL_ANCESTRY)]);
|
|
719
732
|
let ws = tmp_ws("p0-title");
|
|
720
733
|
let team_dir = write_copilot_team(&ws, "cp-title", &["mcp_team"], false);
|
|
@@ -771,6 +784,7 @@ incident); got {:?}",
|
|
|
771
784
|
#[test]
|
|
772
785
|
#[serial(env)]
|
|
773
786
|
fn copilot_mcp_residual_named_disabled_and_recorded() {
|
|
787
|
+
let _hermetic = enter_hermetic("copilot-mcp-residual-named");
|
|
774
788
|
let home = tmp_ws("mcp-home");
|
|
775
789
|
let _guard = EnvGuard::set(&[
|
|
776
790
|
(ANCESTRY_KEY, NEUTRAL_ANCESTRY),
|
|
@@ -869,6 +883,7 @@ provider.copilot.mcp_residual_detected (user-visible, not silent); events tail={
|
|
|
869
883
|
#[test]
|
|
870
884
|
#[serial(env)]
|
|
871
885
|
fn copilot_mcp_residual_scan_unavailable_degrades_honestly_no_false_positive() {
|
|
886
|
+
let _hermetic = enter_hermetic("copilot-mcp-residual-unavailable");
|
|
872
887
|
// Empty isolated HOME (no ~/.copilot config) and a PATH with NO copilot binary, so
|
|
873
888
|
// the scan exercises the `Err(_)` / unavailable arm deterministically.
|
|
874
889
|
let home = tmp_ws("residual-unavail-home");
|
|
@@ -946,6 +961,7 @@ unavailable (honest 'I could not check')",
|
|
|
946
961
|
#[test]
|
|
947
962
|
#[serial(env)]
|
|
948
963
|
fn copilot_mcp_config_uses_copilot_field_name() {
|
|
964
|
+
let _hermetic = enter_hermetic("copilot-mcp-config");
|
|
949
965
|
let _guard = EnvGuard::set(&[(ANCESTRY_KEY, NEUTRAL_ANCESTRY)]);
|
|
950
966
|
let ws = tmp_ws("mcp-schema");
|
|
951
967
|
let team_dir = write_copilot_team(&ws, "cp-schema", &["mcp_team"], false);
|
|
@@ -983,6 +999,7 @@ fn copilot_mcp_config_uses_copilot_field_name() {
|
|
|
983
999
|
#[test]
|
|
984
1000
|
#[serial(env)]
|
|
985
1001
|
fn copilot_resume_argv_has_no_session_id_resume_conflict() {
|
|
1002
|
+
let _hermetic = enter_hermetic("copilot-resume-argv");
|
|
986
1003
|
let home = tmp_ws("resume-home");
|
|
987
1004
|
let _guard = EnvGuard::set(&[
|
|
988
1005
|
(ANCESTRY_KEY, NEUTRAL_ANCESTRY),
|
|
@@ -1032,6 +1049,7 @@ fn copilot_resume_argv_has_no_session_id_resume_conflict() {
|
|
|
1032
1049
|
#[test]
|
|
1033
1050
|
#[serial(env)]
|
|
1034
1051
|
fn copilot_subscription_model_argv_only_when_configured() {
|
|
1052
|
+
let _hermetic = enter_hermetic("copilot-subscription-model");
|
|
1035
1053
|
let _guard = EnvGuard::set(&[(ANCESTRY_KEY, NEUTRAL_ANCESTRY)]);
|
|
1036
1054
|
|
|
1037
1055
|
let ws = tmp_ws("model-set");
|
|
@@ -1088,6 +1106,7 @@ fn copilot_subscription_model_argv_only_when_configured() {
|
|
|
1088
1106
|
#[test]
|
|
1089
1107
|
#[serial(env)]
|
|
1090
1108
|
fn copilot_subscription_model_precedence_role_over_profile() {
|
|
1109
|
+
let _hermetic = enter_hermetic("copilot-subscription-precedence");
|
|
1091
1110
|
let _guard = EnvGuard::set(&[(ANCESTRY_KEY, NEUTRAL_ANCESTRY)]);
|
|
1092
1111
|
let ws = tmp_ws("model-prec");
|
|
1093
1112
|
let team_dir = write_copilot_team_model(&ws, "cp-prec", &["mcp_team"], Some("role-model-x"));
|
|
@@ -1125,6 +1144,7 @@ cross-provider reuse); argv={argv:?}"
|
|
|
1125
1144
|
#[test]
|
|
1126
1145
|
#[serial(env)]
|
|
1127
1146
|
fn copilot_subscription_env_no_provider_or_token_injection() {
|
|
1147
|
+
let _hermetic = enter_hermetic("copilot-subscription-env");
|
|
1128
1148
|
let _guard = EnvGuard::set(&[
|
|
1129
1149
|
(ANCESTRY_KEY, NEUTRAL_ANCESTRY),
|
|
1130
1150
|
("COPILOT_GITHUB_TOKEN", "user-tok-keepme"),
|
|
@@ -1178,6 +1198,7 @@ fn copilot_subscription_env_no_provider_or_token_injection() {
|
|
|
1178
1198
|
#[test]
|
|
1179
1199
|
#[serial(env)]
|
|
1180
1200
|
fn copilot_byok_profile_exports_provider_env_and_requires_model() {
|
|
1201
|
+
let _hermetic = enter_hermetic("copilot-byok-profile");
|
|
1181
1202
|
let _guard = EnvGuard::set(&[(ANCESTRY_KEY, NEUTRAL_ANCESTRY)]);
|
|
1182
1203
|
let mut failures = Vec::new();
|
|
1183
1204
|
|
|
@@ -1272,6 +1293,7 @@ green; the hint must be weak/unknown (honest — can't confirm logged-in); got {
|
|
|
1272
1293
|
#[test]
|
|
1273
1294
|
#[serial(env)]
|
|
1274
1295
|
fn copilot_token_passthrough_emits_warning_event() {
|
|
1296
|
+
let _hermetic = enter_hermetic("copilot-token-passthrough");
|
|
1275
1297
|
// Fake HOME so the residual MCP scan reads an empty copilot config (isolation:
|
|
1276
1298
|
// otherwise it reads the real ~/.copilot and muddies this token-focused case).
|
|
1277
1299
|
let home = tmp_ws("token-home");
|
|
@@ -5,6 +5,13 @@ mod hermetic_guard;
|
|
|
5
5
|
#[allow(dead_code)]
|
|
6
6
|
fn _hermetic_boundary_marker(_: &hermetic_guard::HermeticTestEnv) {}
|
|
7
7
|
|
|
8
|
+
fn enter_hermetic(tag: &str) -> hermetic_guard::HermeticTestEnv {
|
|
9
|
+
let env = hermetic_guard::HermeticTestEnv::enter(tag);
|
|
10
|
+
env.scrub_tmux();
|
|
11
|
+
env.assert_no_real_tmux();
|
|
12
|
+
env
|
|
13
|
+
}
|
|
14
|
+
|
|
8
15
|
#[path = "../../../tests/support/composite_source.rs"]
|
|
9
16
|
mod composite_source;
|
|
10
17
|
use std::collections::BTreeMap;
|
|
@@ -31,6 +38,7 @@ fn bin() -> &'static str {
|
|
|
31
38
|
#[test]
|
|
32
39
|
#[serial(env)]
|
|
33
40
|
fn claude_worker_spawn_argv_uses_compiled_prompt_and_resolved_role_tools() {
|
|
41
|
+
let _hermetic = enter_hermetic("core034-claude-argv");
|
|
34
42
|
let _ancestry = EnvGuard::set(
|
|
35
43
|
"TEAM_AGENT_TEST_PROCESS_ANCESTRY_ARGV_JSON",
|
|
36
44
|
"[\"/bin/zsh\"]",
|
|
@@ -115,6 +123,7 @@ fn claude_worker_spawn_argv_uses_compiled_prompt_and_resolved_role_tools() {
|
|
|
115
123
|
#[test]
|
|
116
124
|
#[serial(env)]
|
|
117
125
|
fn claude_control_role_without_native_tools_still_disallows_native_tools() {
|
|
126
|
+
let _hermetic = enter_hermetic("core034-claude-control");
|
|
118
127
|
let _ancestry = EnvGuard::set(
|
|
119
128
|
"TEAM_AGENT_TEST_PROCESS_ANCESTRY_ARGV_JSON",
|
|
120
129
|
"[\"/bin/zsh\"]",
|
|
@@ -170,6 +179,7 @@ fn claude_control_role_without_native_tools_still_disallows_native_tools() {
|
|
|
170
179
|
#[test]
|
|
171
180
|
#[serial(env)]
|
|
172
181
|
fn restart_claude_worker_resolves_tools_from_spec_when_runtime_state_has_no_raw_tools() {
|
|
182
|
+
let _hermetic = enter_hermetic("core034-restart-tools");
|
|
173
183
|
let _ancestry = EnvGuard::set(
|
|
174
184
|
"TEAM_AGENT_TEST_PROCESS_ANCESTRY_ARGV_JSON",
|
|
175
185
|
"[\"/bin/zsh\"]",
|
|
@@ -307,6 +317,7 @@ fn worker_command_context_uses_single_prompt_and_permission_sources() {
|
|
|
307
317
|
#[test]
|
|
308
318
|
#[serial(env)]
|
|
309
319
|
fn quick_start_default_display_adaptive_and_none_escape_hatch_are_observable() {
|
|
320
|
+
let _hermetic = enter_hermetic("core034-display");
|
|
310
321
|
let default_ws = tmp_dir("display-default");
|
|
311
322
|
let default_team = write_team(
|
|
312
323
|
&default_ws,
|
|
@@ -396,6 +407,7 @@ fn quick_start_default_display_adaptive_and_none_escape_hatch_are_observable() {
|
|
|
396
407
|
#[test]
|
|
397
408
|
#[serial(env)]
|
|
398
409
|
fn quick_start_and_restart_emit_copyable_workspace_socket_attach_commands() {
|
|
410
|
+
let _hermetic = enter_hermetic("core034-attach-command");
|
|
399
411
|
let ws = tmp_dir("attach-command");
|
|
400
412
|
let team = write_team(
|
|
401
413
|
&ws,
|