@team-agent/installer 0.5.61 → 0.5.62
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 +5 -2
- package/crates/team-agent/src/cli/attach_app_server_leader.rs +1 -0
- package/crates/team-agent/src/cli/diagnose.rs +1 -0
- package/crates/team-agent/src/cli/emit.rs +41 -7
- package/crates/team-agent/src/cli/helpers.rs +1 -0
- package/crates/team-agent/src/cli/leader.rs +6 -1
- package/crates/team-agent/src/cli/leaders.rs +2 -0
- package/crates/team-agent/src/cli/mod.rs +33 -3
- package/crates/team-agent/src/cli/named_address.rs +8 -1
- package/crates/team-agent/src/cli/profile.rs +2 -1
- package/crates/team-agent/src/cli/send/coordinator.rs +1 -0
- package/crates/team-agent/src/cli/send/mailbox.rs +3 -0
- package/crates/team-agent/src/cli/send/persist.rs +1 -0
- package/crates/team-agent/src/cli/send/presentation.rs +1 -0
- package/crates/team-agent/src/cli/send/resolve.rs +206 -1
- package/crates/team-agent/src/cli/send.rs +2 -1
- package/crates/team-agent/src/cli/spec.rs +1 -0
- package/crates/team-agent/src/cli/status.rs +2 -1
- package/crates/team-agent/src/cli/status_port/compact.rs +1 -0
- package/crates/team-agent/src/cli/status_port/format.rs +1 -0
- package/crates/team-agent/src/cli/status_port/inbox.rs +1 -0
- package/crates/team-agent/src/cli/tests/verb_profile.rs +38 -0
- package/crates/team-agent/src/cli/types.rs +3 -2
- package/crates/team-agent/src/communication_mode/mod.rs +1 -0
- package/crates/team-agent/src/compiler.rs +1 -0
- package/crates/team-agent/src/conpty/backend.rs +1 -0
- package/crates/team-agent/src/conpty/mod.rs +1 -0
- package/crates/team-agent/src/coordinator/backoff.rs +5 -8
- package/crates/team-agent/src/coordinator/conpty_shim.rs +3 -2
- package/crates/team-agent/src/coordinator/health.rs +1 -0
- package/crates/team-agent/src/coordinator/mod.rs +2 -2
- package/crates/team-agent/src/coordinator/orphan.rs +4 -116
- package/crates/team-agent/src/coordinator/runtime_detectors.rs +1 -0
- package/crates/team-agent/src/coordinator/runtime_observation.rs +1 -0
- package/crates/team-agent/src/coordinator/steps/abnormal.rs +6 -5
- package/crates/team-agent/src/coordinator/steps/delivery.rs +1 -0
- package/crates/team-agent/src/coordinator/steps/health_sync.rs +1 -0
- package/crates/team-agent/src/coordinator/steps/mod.rs +2 -0
- package/crates/team-agent/src/coordinator/steps/persist.rs +1 -0
- package/crates/team-agent/src/coordinator/steps/runtime_prompts.rs +1 -0
- package/crates/team-agent/src/coordinator/steps/session_gate.rs +1 -0
- package/crates/team-agent/src/coordinator/tests/abnormal.rs +0 -174
- package/crates/team-agent/src/coordinator/tests/api_error_recovery.rs +0 -9
- package/crates/team-agent/src/coordinator/tests/basics.rs +0 -184
- package/crates/team-agent/src/coordinator/tests/mod.rs +2 -17
- package/crates/team-agent/src/coordinator/tests/tick_core.rs +0 -42
- package/crates/team-agent/src/coordinator/tick.rs +24 -28
- package/crates/team-agent/src/coordinator/types.rs +3 -176
- package/crates/team-agent/src/db/agent_health_capture.rs +1 -0
- package/crates/team-agent/src/db/message_store.rs +2 -1
- package/crates/team-agent/src/db/migration.rs +5 -4
- package/crates/team-agent/src/db/mod.rs +1 -0
- package/crates/team-agent/src/db/schema.rs +1 -0
- package/crates/team-agent/src/diagnose/comms.rs +2 -1
- package/crates/team-agent/src/diagnose/mod.rs +1 -0
- package/crates/team-agent/src/diagnose/orphans.rs +1 -0
- package/crates/team-agent/src/layout/manager.rs +3 -7
- package/crates/team-agent/src/layout/mod.rs +2 -2
- package/crates/team-agent/src/layout/overlay.rs +6 -1
- package/crates/team-agent/src/layout/placement.rs +1 -0
- package/crates/team-agent/src/layout/recovery.rs +3 -0
- package/crates/team-agent/src/layout/runtime_sessions.rs +8 -3
- package/crates/team-agent/src/layout/sessions.rs +8 -1
- package/crates/team-agent/src/layout/tmux_endpoint.rs +3 -0
- package/crates/team-agent/src/layout/worker_env.rs +3 -0
- package/crates/team-agent/src/layout/worker_window_helpers.rs +2 -0
- package/crates/team-agent/src/leader/helpers.rs +1 -0
- package/crates/team-agent/src/leader/incident.rs +1 -0
- package/crates/team-agent/src/leader/lease.rs +27 -22
- package/crates/team-agent/src/leader/mod.rs +2 -3
- package/crates/team-agent/src/leader/owner_bind.rs +13 -3
- package/crates/team-agent/src/leader/provider_attribution.rs +1 -0
- package/crates/team-agent/src/leader/rediscover.rs +4 -17
- package/crates/team-agent/src/leader/registry.rs +3 -15
- package/crates/team-agent/src/leader/start.rs +373 -125
- package/crates/team-agent/src/leader/takeover.rs +10 -232
- package/crates/team-agent/src/leader/tests/claim_leader_mailbox_flush_contract.rs +544 -0
- package/crates/team-agent/src/leader/tests/historical_inventory_upgrade_gate_red.rs +624 -0
- package/crates/team-agent/src/leader/tests/identity.rs +7 -2
- package/crates/team-agent/src/leader/tests/idle.rs +0 -193
- package/crates/team-agent/src/leader/tests/lease_api.rs +0 -45
- package/crates/team-agent/src/leader/tests/mod.rs +4 -41
- package/crates/team-agent/src/leader/tests/rehomed_env.rs +83 -0
- package/crates/team-agent/src/leader/tests/team_in_team_state_scope_red.rs +1273 -0
- package/crates/team-agent/src/leader/tests/terminal_and_replay_vs_rerender_invariants_red.rs +417 -0
- package/crates/team-agent/src/leader/tests/wake_start_owner.rs +22 -103
- package/crates/team-agent/src/leader/types.rs +1 -28
- package/crates/team-agent/src/lib.rs +3 -0
- package/crates/team-agent/src/lifecycle/display.rs +7 -1
- package/crates/team-agent/src/lifecycle/helpers.rs +3 -1
- package/crates/team-agent/src/lifecycle/launch/add_agent.rs +2 -2
- package/crates/team-agent/src/lifecycle/launch/fork_agent/completion.rs +1 -0
- package/crates/team-agent/src/lifecycle/launch/fork_entry.rs +1 -0
- package/crates/team-agent/src/lifecycle/launch/fork_finalize.rs +1 -0
- package/crates/team-agent/src/lifecycle/launch/plan.rs +2 -2
- package/crates/team-agent/src/lifecycle/launch/quick_start.rs +4 -26
- package/crates/team-agent/src/lifecycle/launch/readiness.rs +0 -27
- package/crates/team-agent/src/lifecycle/launch/spawn.rs +1 -27
- package/crates/team-agent/src/lifecycle/launch/spec_state.rs +4 -32
- package/crates/team-agent/src/lifecycle/launch/state_projection.rs +1 -0
- package/crates/team-agent/src/lifecycle/launch.rs +11 -12
- package/crates/team-agent/src/lifecycle/mod.rs +2 -1
- package/crates/team-agent/src/lifecycle/restart/agent.rs +3 -3
- package/crates/team-agent/src/lifecycle/restart/orchestrator.rs +2 -2
- package/crates/team-agent/src/lifecycle/restart/rebuild.rs +13 -5
- package/crates/team-agent/src/lifecycle/restart/remove.rs +1 -1
- package/crates/team-agent/src/lifecycle/restart/selection.rs +3 -3
- package/crates/team-agent/src/lifecycle/restart.rs +13 -11
- package/crates/team-agent/src/lifecycle/tests/acceptance_b_batch_red.rs +1007 -0
- package/crates/team-agent/src/lifecycle/tests/b0_legacy_snapshot_nonauthority_contract.rs +265 -0
- package/crates/team-agent/src/lifecycle/tests/b0_reader_hideone_audit_contract.rs +459 -0
- package/crates/team-agent/src/lifecycle/tests/behavioral_diff_264_red.rs +849 -0
- package/crates/team-agent/src/lifecycle/tests/claude_compatible_config_red.rs +323 -0
- package/crates/team-agent/src/lifecycle/tests/claude_profile_launch_red.rs +423 -0
- package/crates/team-agent/src/lifecycle/tests/clone_fork_copilot_perms_red.rs +737 -0
- package/crates/team-agent/src/lifecycle/tests/codex_mcp_approval_inheritance_red.rs +1187 -0
- package/crates/team-agent/src/lifecycle/tests/codex_weak_window_attribution_red.rs +683 -0
- package/crates/team-agent/src/lifecycle/tests/communication_mode_runtime_contract_red.rs +394 -0
- package/crates/team-agent/src/lifecycle/tests/copilot_provider_red.rs +1856 -0
- package/crates/team-agent/src/lifecycle/tests/core_034_real_red.rs +917 -0
- package/crates/team-agent/src/lifecycle/tests/display_adaptive_red.rs +481 -0
- package/crates/team-agent/src/lifecycle/tests/f032_startup_prompt_best_effort_red.rs +301 -0
- package/crates/team-agent/src/lifecycle/tests/harvest2_a_batch_red.rs +516 -0
- package/crates/team-agent/src/lifecycle/tests/host_cotenant_death_p0_contract.rs +986 -0
- package/crates/team-agent/src/lifecycle/tests/lifecycle_rollback_red.rs +651 -0
- package/crates/team-agent/src/lifecycle/tests/main_preserved.rs +2 -0
- package/crates/team-agent/src/lifecycle/tests/quick_start_worker_readiness_red.rs +332 -0
- package/crates/team-agent/src/lifecycle/tests/realmachine_clusters_1_6_red.rs +846 -0
- package/crates/team-agent/src/lifecycle/tests/realmachine_residual_g1_g4_red.rs +648 -0
- package/crates/team-agent/src/lifecycle/tests/restart_build_before_destroy_0540_contract.rs +910 -0
- package/crates/team-agent/src/lifecycle/tests/restart_liveness_red.rs +630 -0
- package/crates/team-agent/src/lifecycle/tests/restart_rebind_hotfix_252_red.rs +1083 -0
- package/crates/team-agent/src/lifecycle/tests/restart_session_capture_red.rs +1677 -0
- package/crates/team-agent/src/lifecycle/tests/resume_recover_red.rs +410 -0
- package/crates/team-agent/src/lifecycle/tests/stale_team_saveconflict_contract.rs +428 -0
- package/crates/team-agent/src/lifecycle/tests/startup_latency_contract.rs +1032 -0
- package/crates/team-agent/src/lifecycle/tests/status_credential_redaction_contract.rs +869 -0
- package/crates/team-agent/src/lifecycle/tests/subprep_codex_trust_roundtrip_red.rs +1249 -0
- package/crates/team-agent/src/lifecycle/tests/swallow_batch4_semantics_red.rs +342 -0
- package/crates/team-agent/src/lifecycle/tests/team_in_team_identity_scope_red.rs +542 -0
- package/crates/team-agent/src/lifecycle/tests/team_in_team_sibling_quick_start_red.rs +1103 -0
- package/crates/team-agent/src/lifecycle/tests/team_in_team_state_scope_red.rs +1276 -0
- package/crates/team-agent/src/lifecycle/tests/team_key_retirement_roster_red.rs +145 -0
- package/crates/team-agent/src/lifecycle/tests/team_key_retirement_txn_red.rs +603 -0
- package/crates/team-agent/src/lifecycle/tests/test_isolation_escape_contract.rs +837 -0
- package/crates/team-agent/src/lifecycle/tests/upgrade_compat_0211_red.rs +928 -0
- package/crates/team-agent/src/lifecycle/tests/verify_rs031_window_consistency_red.rs +951 -0
- package/crates/team-agent/src/lifecycle/tests/worker_dangerous_bypass_argv_red.rs +327 -0
- package/crates/team-agent/src/lifecycle/tests/worker_spawn_env_red.rs +760 -0
- package/crates/team-agent/src/lifecycle/tests.rs +59 -0
- package/crates/team-agent/src/lifecycle/types.rs +0 -9
- package/crates/team-agent/src/mcp_server/helpers.rs +1 -0
- package/crates/team-agent/src/mcp_server/lifecycle_tools/mod.rs +1 -0
- package/crates/team-agent/src/mcp_server/lifecycle_tools/state_status.rs +1 -0
- package/crates/team-agent/src/mcp_server/mod.rs +1 -0
- package/crates/team-agent/src/mcp_server/normalize.rs +4 -3
- package/crates/team-agent/src/mcp_server/tools.rs +1 -0
- package/crates/team-agent/src/mcp_server/types.rs +1 -18
- package/crates/team-agent/src/mcp_server/wire.rs +5 -5
- package/crates/team-agent/src/messaging/activity.rs +1 -0
- package/crates/team-agent/src/messaging/address.rs +1 -0
- package/crates/team-agent/src/messaging/delivery.rs +4 -3
- package/crates/team-agent/src/messaging/helpers.rs +2 -1
- package/crates/team-agent/src/messaging/leader_channel.rs +1 -0
- package/crates/team-agent/src/messaging/leader_receiver.rs +2 -1
- package/crates/team-agent/src/messaging/mod.rs +10 -14
- package/crates/team-agent/src/messaging/peers.rs +2 -0
- package/crates/team-agent/src/messaging/persist.rs +3 -1
- package/crates/team-agent/src/messaging/presentation.rs +2 -1
- package/crates/team-agent/src/messaging/results.rs +1 -0
- package/crates/team-agent/src/messaging/scheduler.rs +1 -0
- package/crates/team-agent/src/messaging/selftest.rs +1 -1
- package/crates/team-agent/src/messaging/send.rs +41 -8
- package/crates/team-agent/src/messaging/tests/mod.rs +5 -0
- package/crates/team-agent/src/messaging/types.rs +2 -2
- package/crates/team-agent/src/messaging/watchers.rs +19 -13
- package/crates/team-agent/src/model/enums.rs +1 -0
- package/crates/team-agent/src/model/errors.rs +1 -0
- package/crates/team-agent/src/model/ids.rs +1 -0
- package/crates/team-agent/src/model/mod.rs +1 -0
- package/crates/team-agent/src/model/name_similarity.rs +1 -0
- package/crates/team-agent/src/model/pane_authority_refusal.rs +1 -0
- package/crates/team-agent/src/model/paths.rs +1 -0
- package/crates/team-agent/src/model/permissions.rs +3 -2
- package/crates/team-agent/src/model/routing.rs +1 -0
- package/crates/team-agent/src/model/spec.rs +1 -0
- package/crates/team-agent/src/model/task_graph.rs +1 -0
- package/crates/team-agent/src/model/yaml/tests.rs +1 -0
- package/crates/team-agent/src/model/yaml.rs +1 -0
- package/crates/team-agent/src/packaging/install.rs +9 -147
- package/crates/team-agent/src/packaging/migrate.rs +2 -0
- package/crates/team-agent/src/packaging/mod.rs +1 -0
- package/crates/team-agent/src/packaging/repair.rs +2 -0
- package/crates/team-agent/src/packaging/tests.rs +29 -218
- package/crates/team-agent/src/packaging/types.rs +8 -15
- package/crates/team-agent/src/platform/argv.rs +4 -0
- package/crates/team-agent/src/platform/errors.rs +10 -1
- package/crates/team-agent/src/platform/file_lock.rs +4 -157
- package/crates/team-agent/src/platform/mod.rs +0 -61
- package/crates/team-agent/src/platform/process.rs +1 -0
- package/crates/team-agent/src/provider/adapter.rs +1 -0
- package/crates/team-agent/src/provider/adapters/claude.rs +1 -0
- package/crates/team-agent/src/provider/adapters/claude_fork.rs +1 -0
- package/crates/team-agent/src/provider/adapters/codex.rs +1 -0
- package/crates/team-agent/src/provider/adapters/copilot.rs +1 -0
- package/crates/team-agent/src/provider/adapters/copilot_fork.rs +1 -0
- package/crates/team-agent/src/provider/adapters/fake.rs +1 -0
- package/crates/team-agent/src/provider/adapters/mod.rs +1 -0
- package/crates/team-agent/src/provider/approvals/mod.rs +1 -0
- package/crates/team-agent/src/provider/approvals/parsing.rs +2 -5
- package/crates/team-agent/src/provider/approvals/runtime_prompts.rs +6 -5
- package/crates/team-agent/src/provider/classify.rs +1 -0
- package/crates/team-agent/src/provider/faults.rs +1 -26
- package/crates/team-agent/src/provider/helpers.rs +1 -0
- package/crates/team-agent/src/provider/mod.rs +1 -1
- package/crates/team-agent/src/provider/session/capture.rs +1 -4
- package/crates/team-agent/src/provider/session/context_fork/claude.rs +1 -0
- package/crates/team-agent/src/provider/session/context_fork/codex.rs +1 -0
- package/crates/team-agent/src/provider/session/context_fork/outcome.rs +1 -0
- package/crates/team-agent/src/provider/session/context_fork.rs +1 -0
- package/crates/team-agent/src/provider/session/mod.rs +2 -4
- package/crates/team-agent/src/provider/session/resume.rs +2 -209
- package/crates/team-agent/src/provider/session_scan/claude.rs +1 -0
- package/crates/team-agent/src/provider/session_scan/codex.rs +1 -0
- package/crates/team-agent/src/provider/session_scan/common.rs +1 -0
- package/crates/team-agent/src/provider/session_scan/copilot.rs +1 -0
- package/crates/team-agent/src/provider/session_scan.rs +1 -42
- package/crates/team-agent/src/provider/startup_prompt.rs +1 -0
- package/crates/team-agent/src/provider/types.rs +1 -0
- package/crates/team-agent/src/provider/wire.rs +1 -0
- package/crates/team-agent/src/state/identity.rs +16 -5
- package/crates/team-agent/src/state/identity_keys.rs +1 -0
- package/crates/team-agent/src/state/mod.rs +2 -0
- package/crates/team-agent/src/state/owner_gate.rs +5 -0
- package/crates/team-agent/src/state/ownership.rs +10 -4
- package/crates/team-agent/src/state/paths.rs +12 -0
- package/crates/team-agent/src/state/persist.rs +9 -3
- package/crates/team-agent/src/state/projection.rs +15 -2
- package/crates/team-agent/src/state/repository/intent.rs +1 -0
- package/crates/team-agent/src/state/repository/tests.rs +1 -0
- package/crates/team-agent/src/state/repository.rs +7 -0
- package/crates/team-agent/src/state/selector.rs +1 -0
- package/crates/team-agent/src/tmux_backend/tests.rs +39 -0
- package/crates/team-agent/src/tmux_backend.rs +63 -0
- package/crates/team-agent/src/transport.rs +15 -0
- package/package.json +4 -4
- package/crates/team-agent/src/leader/inject.rs +0 -33
- package/crates/team-agent/src/provider/command.rs +0 -80
|
@@ -0,0 +1,917 @@
|
|
|
1
|
+
#![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
|
|
2
|
+
|
|
3
|
+
#[path = "../../../tests/support/hermetic.rs"]
|
|
4
|
+
mod hermetic_guard;
|
|
5
|
+
#[allow(dead_code)]
|
|
6
|
+
fn _hermetic_boundary_marker(_: &hermetic_guard::HermeticTestEnv) {}
|
|
7
|
+
|
|
8
|
+
#[path = "../../../tests/support/composite_source.rs"]
|
|
9
|
+
mod composite_source;
|
|
10
|
+
use std::collections::BTreeMap;
|
|
11
|
+
use std::path::{Path, PathBuf};
|
|
12
|
+
use std::process::Command;
|
|
13
|
+
use std::sync::atomic::{AtomicU64, Ordering};
|
|
14
|
+
use std::sync::Mutex;
|
|
15
|
+
|
|
16
|
+
use serde_json::{json, Value};
|
|
17
|
+
use serial_test::serial;
|
|
18
|
+
use team_agent::lifecycle::{quick_start_with_transport_in_workspace, restart_with_transport};
|
|
19
|
+
use team_agent::state::persist::{load_runtime_state, save_runtime_state};
|
|
20
|
+
use team_agent::transport::{
|
|
21
|
+
AttachOutcome, BackendKind, CaptureRange, CapturedText, InjectPayload, InjectReport,
|
|
22
|
+
InjectStage, InjectVerification, Key, PaneField, PaneId, PaneInfo, SessionName, SetEnvOutcome,
|
|
23
|
+
SpawnResult, SubmitVerification, Target, Transport, TransportError, TurnVerification,
|
|
24
|
+
WindowName,
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
fn bin() -> &'static str {
|
|
28
|
+
crate::lifecycle::tests::test_binary_path()
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
#[test]
|
|
32
|
+
#[serial(env)]
|
|
33
|
+
fn claude_worker_spawn_argv_uses_compiled_prompt_and_resolved_role_tools() {
|
|
34
|
+
let _ancestry = EnvGuard::set(
|
|
35
|
+
"TEAM_AGENT_TEST_PROCESS_ANCESTRY_ARGV_JSON",
|
|
36
|
+
"[\"/bin/zsh\"]",
|
|
37
|
+
);
|
|
38
|
+
let ws = tmp_dir("claude-argv");
|
|
39
|
+
let team_dir = write_team(
|
|
40
|
+
&ws,
|
|
41
|
+
"worker-argv",
|
|
42
|
+
"claude",
|
|
43
|
+
None,
|
|
44
|
+
&[AgentDoc {
|
|
45
|
+
id: "worker_a",
|
|
46
|
+
role: "Implementation Worker",
|
|
47
|
+
provider: "claude",
|
|
48
|
+
tools: &["fs_read", "fs_write", "fs_list", "execute_bash", "mcp_team"],
|
|
49
|
+
body: "ROLE BODY SENTINEL: build the porcelain adapter without hiding failures.",
|
|
50
|
+
}],
|
|
51
|
+
);
|
|
52
|
+
seed_healthy_coordinator(&ws);
|
|
53
|
+
let transport = RecordingTransport::new();
|
|
54
|
+
|
|
55
|
+
quick_start_with_transport_in_workspace(
|
|
56
|
+
&ws,
|
|
57
|
+
&team_dir,
|
|
58
|
+
None,
|
|
59
|
+
true,
|
|
60
|
+
Some("workerargv"),
|
|
61
|
+
&transport,
|
|
62
|
+
)
|
|
63
|
+
.expect("quick-start should reach the real lifecycle command-construction path");
|
|
64
|
+
|
|
65
|
+
let spawn = transport.single_spawn();
|
|
66
|
+
let prompt = flag_value(&spawn.argv, "--append-system-prompt").unwrap_or_default();
|
|
67
|
+
let mut failures = Vec::new();
|
|
68
|
+
for marker in [
|
|
69
|
+
"worker_a",
|
|
70
|
+
"Implementation Worker",
|
|
71
|
+
"Team Agent Teammate Runtime Contract",
|
|
72
|
+
"send_message(to='leader'",
|
|
73
|
+
"report_result exactly once",
|
|
74
|
+
"ROLE BODY SENTINEL",
|
|
75
|
+
"Permission note",
|
|
76
|
+
] {
|
|
77
|
+
if !prompt.contains(marker) {
|
|
78
|
+
failures.push(format!("B2 missing compiled prompt marker {marker:?}"));
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
if prompt == "Implementation Worker" {
|
|
82
|
+
failures.push("B2 prompt collapsed to the bare role label".to_string());
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
let disallowed = flag_values(&spawn.argv, "--disallowedTools");
|
|
86
|
+
for forbidden in [
|
|
87
|
+
"Bash",
|
|
88
|
+
"Read",
|
|
89
|
+
"Edit",
|
|
90
|
+
"Write",
|
|
91
|
+
"MultiEdit",
|
|
92
|
+
"NotebookEdit",
|
|
93
|
+
"Glob",
|
|
94
|
+
"Grep",
|
|
95
|
+
] {
|
|
96
|
+
if disallowed.iter().any(|tool| tool == forbidden) {
|
|
97
|
+
failures.push(format!(
|
|
98
|
+
"B1 declared capability still disallowed via --disallowedTools {forbidden:?}"
|
|
99
|
+
));
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
if argv_has_flag(&spawn.argv, "--allowedTools") {
|
|
103
|
+
failures.push(
|
|
104
|
+
"B1 Python parity violated: Claude argv must not switch to --allowedTools".to_string(),
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
assert!(
|
|
108
|
+
failures.is_empty(),
|
|
109
|
+
"B1/B2 worker argv contract failed:\n{}\nprompt={prompt:?}\ndisallowed={disallowed:?}\nargv={:?}",
|
|
110
|
+
failures.join("\n"),
|
|
111
|
+
spawn.argv
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
#[test]
|
|
116
|
+
#[serial(env)]
|
|
117
|
+
fn claude_control_role_without_native_tools_still_disallows_native_tools() {
|
|
118
|
+
let _ancestry = EnvGuard::set(
|
|
119
|
+
"TEAM_AGENT_TEST_PROCESS_ANCESTRY_ARGV_JSON",
|
|
120
|
+
"[\"/bin/zsh\"]",
|
|
121
|
+
);
|
|
122
|
+
let ws = tmp_dir("claude-control");
|
|
123
|
+
let team_dir = write_team(
|
|
124
|
+
&ws,
|
|
125
|
+
"control-argv",
|
|
126
|
+
"claude",
|
|
127
|
+
None,
|
|
128
|
+
&[AgentDoc {
|
|
129
|
+
id: "control",
|
|
130
|
+
role: "Control Worker",
|
|
131
|
+
provider: "claude",
|
|
132
|
+
tools: &["mcp_team"],
|
|
133
|
+
body: "CONTROL BODY SENTINEL: route only through Team Agent MCP.",
|
|
134
|
+
}],
|
|
135
|
+
);
|
|
136
|
+
seed_healthy_coordinator(&ws);
|
|
137
|
+
let transport = RecordingTransport::new();
|
|
138
|
+
|
|
139
|
+
quick_start_with_transport_in_workspace(
|
|
140
|
+
&ws,
|
|
141
|
+
&team_dir,
|
|
142
|
+
None,
|
|
143
|
+
true,
|
|
144
|
+
Some("controlargv"),
|
|
145
|
+
&transport,
|
|
146
|
+
)
|
|
147
|
+
.expect("quick-start should record the control worker command");
|
|
148
|
+
|
|
149
|
+
let spawn = transport.single_spawn();
|
|
150
|
+
let disallowed = flag_values(&spawn.argv, "--disallowedTools");
|
|
151
|
+
for expected in [
|
|
152
|
+
"Bash",
|
|
153
|
+
"Read",
|
|
154
|
+
"Edit",
|
|
155
|
+
"Write",
|
|
156
|
+
"MultiEdit",
|
|
157
|
+
"NotebookEdit",
|
|
158
|
+
"Glob",
|
|
159
|
+
"Grep",
|
|
160
|
+
] {
|
|
161
|
+
assert!(
|
|
162
|
+
disallowed.iter().any(|tool| tool == expected),
|
|
163
|
+
"B1 negative case: mcp_team-only control roles must still disallow native Claude tools; \
|
|
164
|
+
missing={expected} disallowed={disallowed:?}\nargv={:?}",
|
|
165
|
+
spawn.argv
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
#[test]
|
|
171
|
+
#[serial(env)]
|
|
172
|
+
fn restart_claude_worker_resolves_tools_from_spec_when_runtime_state_has_no_raw_tools() {
|
|
173
|
+
let _ancestry = EnvGuard::set(
|
|
174
|
+
"TEAM_AGENT_TEST_PROCESS_ANCESTRY_ARGV_JSON",
|
|
175
|
+
"[\"/bin/zsh\"]",
|
|
176
|
+
);
|
|
177
|
+
let ws = tmp_dir("claude-restart-tools");
|
|
178
|
+
let team_dir = write_team(
|
|
179
|
+
&ws,
|
|
180
|
+
"restart-tools",
|
|
181
|
+
"claude",
|
|
182
|
+
Some("none"),
|
|
183
|
+
&[AgentDoc {
|
|
184
|
+
id: "worker_a",
|
|
185
|
+
role: "Implementation Worker",
|
|
186
|
+
provider: "claude",
|
|
187
|
+
tools: &["fs_read", "fs_write", "fs_list", "execute_bash", "mcp_team"],
|
|
188
|
+
body: "ROLE BODY SENTINEL: restart must keep declared native tools.",
|
|
189
|
+
}],
|
|
190
|
+
);
|
|
191
|
+
seed_healthy_coordinator(&ws);
|
|
192
|
+
quick_start_with_transport_in_workspace(
|
|
193
|
+
&ws,
|
|
194
|
+
&team_dir,
|
|
195
|
+
None,
|
|
196
|
+
true,
|
|
197
|
+
Some("restarttools"),
|
|
198
|
+
&RecordingTransport::new(),
|
|
199
|
+
)
|
|
200
|
+
.expect("quick-start should seed restart state");
|
|
201
|
+
seed_running_resumable_state(&ws, "restarttools", "worker_a", "claude");
|
|
202
|
+
remove_runtime_raw_tools(&ws, "restarttools", "worker_a");
|
|
203
|
+
|
|
204
|
+
let restart_transport = RecordingTransport::new();
|
|
205
|
+
let report = restart_with_transport(&ws, true, Some("restarttools"), &restart_transport)
|
|
206
|
+
.expect("restart should reach worker command construction");
|
|
207
|
+
let spawn = restart_transport.single_spawn();
|
|
208
|
+
let disallowed = flag_values(&spawn.argv, "--disallowedTools");
|
|
209
|
+
|
|
210
|
+
for forbidden in [
|
|
211
|
+
"Bash",
|
|
212
|
+
"Read",
|
|
213
|
+
"Edit",
|
|
214
|
+
"Write",
|
|
215
|
+
"MultiEdit",
|
|
216
|
+
"NotebookEdit",
|
|
217
|
+
"Glob",
|
|
218
|
+
"Grep",
|
|
219
|
+
] {
|
|
220
|
+
assert!(
|
|
221
|
+
!disallowed.iter().any(|tool| tool == forbidden),
|
|
222
|
+
"B1 restart contract: when runtime state lacks raw tools, restart must resolve tools from the selected spec/role through resolve_permissions. \
|
|
223
|
+
{forbidden} must not be disallowed for this role; disallowed={disallowed:?}\nargv={:?}\nreport={report:?}",
|
|
224
|
+
spawn.argv
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
assert!(
|
|
228
|
+
!argv_has_flag(&spawn.argv, "--allowedTools"),
|
|
229
|
+
"B1 Python parity: restart must keep --disallowedTools semantics, not switch to --allowedTools; argv={:?}",
|
|
230
|
+
spawn.argv
|
|
231
|
+
);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
#[test]
|
|
235
|
+
fn worker_command_context_uses_single_prompt_and_permission_sources() {
|
|
236
|
+
let crate_root = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
|
237
|
+
let src_root = crate_root.join("src");
|
|
238
|
+
let launch = composite_source::composite_source("src/lifecycle/launch.rs");
|
|
239
|
+
let restart_common = read_to_string(&src_root.join("lifecycle/restart/common.rs"));
|
|
240
|
+
let restart_agent = read_to_string(&src_root.join("lifecycle/restart/agent.rs"));
|
|
241
|
+
// 0.4.x decoupling step 2: claude argv builder moved to
|
|
242
|
+
// provider/adapters/claude.rs. The adapter B1 grep guard now reads the
|
|
243
|
+
// composite source (adapter.rs + every provider-local adapter file).
|
|
244
|
+
let adapter = {
|
|
245
|
+
let mut s = read_to_string(&src_root.join("provider/adapter.rs"));
|
|
246
|
+
let adapters_dir = src_root.join("provider/adapters");
|
|
247
|
+
if adapters_dir.is_dir() {
|
|
248
|
+
for entry in std::fs::read_dir(&adapters_dir)
|
|
249
|
+
.expect("read adapters dir")
|
|
250
|
+
.flatten()
|
|
251
|
+
{
|
|
252
|
+
let p = entry.path();
|
|
253
|
+
if p.extension().and_then(|e| e.to_str()) == Some("rs") {
|
|
254
|
+
s.push('\n');
|
|
255
|
+
s.push_str(&read_to_string(&p));
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
s
|
|
260
|
+
};
|
|
261
|
+
let all_src = read_rust_sources(&src_root);
|
|
262
|
+
let command_sources = format!("{launch}\n{restart_common}\n{restart_agent}");
|
|
263
|
+
|
|
264
|
+
assert!(
|
|
265
|
+
all_src.contains("compile_worker_system_prompt")
|
|
266
|
+
&& all_src.contains("runtime_contract_section")
|
|
267
|
+
&& all_src.contains("identity_section")
|
|
268
|
+
&& all_src.contains("role_body")
|
|
269
|
+
&& all_src.contains("output_contract")
|
|
270
|
+
&& all_src.contains("permission_notes"),
|
|
271
|
+
"B2 grep guard: worker command construction needs one compiled prompt helper with the CR-approved five sections \
|
|
272
|
+
(runtime_contract_section, identity_section, role_body, output_contract, permission_notes)."
|
|
273
|
+
);
|
|
274
|
+
assert!(
|
|
275
|
+
!command_sources.contains("system_prompt: role"),
|
|
276
|
+
"B2 grep guard: launch/restart must not pass the raw role label as ProviderCommandContext.system_prompt."
|
|
277
|
+
);
|
|
278
|
+
assert!(
|
|
279
|
+
all_src.matches("Team Agent Teammate Runtime Contract").count() == 1,
|
|
280
|
+
"B2 grep guard: the common teammate runtime contract must have a single production source; count={}",
|
|
281
|
+
all_src.matches("Team Agent Teammate Runtime Contract").count()
|
|
282
|
+
);
|
|
283
|
+
assert!(
|
|
284
|
+
all_src.matches("529").count() <= 1 && all_src.matches("Overloaded").count() <= 1,
|
|
285
|
+
"B2 grep guard: common 529/Overloaded slowdown wording belongs in the one runtime contract template, not scattered role/provider code."
|
|
286
|
+
);
|
|
287
|
+
|
|
288
|
+
assert!(
|
|
289
|
+
all_src.contains("resolve_permissions") && all_src.contains("resolved_tool_strings"),
|
|
290
|
+
"B1 grep guard: command argv tools must be calculated through a single resolve_permissions helper, including role defaults and leader ceiling."
|
|
291
|
+
);
|
|
292
|
+
assert!(
|
|
293
|
+
!command_sources.contains("worker_tool_refs(agent_tool_strings")
|
|
294
|
+
&& !command_sources.contains("tools: &tool_refs"),
|
|
295
|
+
"B1 grep guard: command construction must not raw-pass agent.tools/tool_refs into ProviderCommandContext."
|
|
296
|
+
);
|
|
297
|
+
assert!(
|
|
298
|
+
adapter.contains("--disallowedTools") && !adapter.contains("--allowedTools"),
|
|
299
|
+
"B1 Python parity: provider adapter must keep Claude --disallowedTools and not invent --allowedTools."
|
|
300
|
+
);
|
|
301
|
+
assert!(
|
|
302
|
+
!adapter.contains("resolve_permissions"),
|
|
303
|
+
"B1 grep guard: adapter.rs should stay a dumb argv renderer; provider-specific permission fallback guesses belong in the shared command-context helper."
|
|
304
|
+
);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
#[test]
|
|
308
|
+
#[serial(env)]
|
|
309
|
+
fn quick_start_default_display_adaptive_and_none_escape_hatch_are_observable() {
|
|
310
|
+
let default_ws = tmp_dir("display-default");
|
|
311
|
+
let default_team = write_team(
|
|
312
|
+
&default_ws,
|
|
313
|
+
"display-default-team",
|
|
314
|
+
"fake",
|
|
315
|
+
None,
|
|
316
|
+
&[AgentDoc {
|
|
317
|
+
id: "worker",
|
|
318
|
+
role: "Fake Worker",
|
|
319
|
+
provider: "fake",
|
|
320
|
+
tools: &["mcp_team"],
|
|
321
|
+
body: "Fake worker.",
|
|
322
|
+
}],
|
|
323
|
+
);
|
|
324
|
+
seed_healthy_coordinator(&default_ws);
|
|
325
|
+
let default_report = quick_start_with_transport_in_workspace(
|
|
326
|
+
&default_ws,
|
|
327
|
+
&default_team,
|
|
328
|
+
None,
|
|
329
|
+
true,
|
|
330
|
+
Some("defaultdisplay"),
|
|
331
|
+
&RecordingTransport::new(),
|
|
332
|
+
)
|
|
333
|
+
.expect("default quick-start should run with recording transport");
|
|
334
|
+
let default_state = load_runtime_state(&default_ws).expect("load default display state");
|
|
335
|
+
|
|
336
|
+
assert_eq!(
|
|
337
|
+
state_display_backend(&default_state),
|
|
338
|
+
Some("adaptive"),
|
|
339
|
+
"adaptive layout contract: omitting display_backend in TEAM.md must persist display_backend=adaptive. \
|
|
340
|
+
state={default_state}\nreport={default_report:?}"
|
|
341
|
+
);
|
|
342
|
+
assert!(
|
|
343
|
+
format!("{default_report:?}").contains("display_backend")
|
|
344
|
+
&& format!("{default_report:?}").contains("adaptive"),
|
|
345
|
+
"adaptive layout contract: quick-start report/JSON surface must visibly report display_backend=adaptive, not hide the default. report={default_report:?}"
|
|
346
|
+
);
|
|
347
|
+
// 0.3.28 Step 4b: per-agent adaptive `display` records removed in favour
|
|
348
|
+
// of 1-window-per-agent layout. The state-root `display_backend=adaptive`
|
|
349
|
+
// (asserted above) is the contract. Per-agent display dicts were the
|
|
350
|
+
// pre-0.3.28 adaptive overlay bookkeeping; the architecture moved that to
|
|
351
|
+
// `layout::overlay` (display overlay only fires for the leader-session
|
|
352
|
+
// overlay path, not per worker).
|
|
353
|
+
// Skip state_has_adaptive_display_records — replaced by:
|
|
354
|
+
assert!(
|
|
355
|
+
default_state.pointer("/agents/worker/window").is_some(),
|
|
356
|
+
"0.3.28 Step 4b: each worker has its own window in the worker session. \
|
|
357
|
+
state={default_state}"
|
|
358
|
+
);
|
|
359
|
+
|
|
360
|
+
let none_ws = tmp_dir("display-none");
|
|
361
|
+
let none_team = write_team(
|
|
362
|
+
&none_ws,
|
|
363
|
+
"display-none-team",
|
|
364
|
+
"fake",
|
|
365
|
+
Some("none"),
|
|
366
|
+
&[AgentDoc {
|
|
367
|
+
id: "worker",
|
|
368
|
+
role: "Fake Worker",
|
|
369
|
+
provider: "fake",
|
|
370
|
+
tools: &["mcp_team"],
|
|
371
|
+
body: "Fake worker.",
|
|
372
|
+
}],
|
|
373
|
+
);
|
|
374
|
+
seed_healthy_coordinator(&none_ws);
|
|
375
|
+
let none_report = quick_start_with_transport_in_workspace(
|
|
376
|
+
&none_ws,
|
|
377
|
+
&none_team,
|
|
378
|
+
None,
|
|
379
|
+
true,
|
|
380
|
+
Some("nonedisplay"),
|
|
381
|
+
&RecordingTransport::new(),
|
|
382
|
+
)
|
|
383
|
+
.expect("none quick-start should run with recording transport");
|
|
384
|
+
let none_state = load_runtime_state(&none_ws).expect("load none display state");
|
|
385
|
+
assert_eq!(
|
|
386
|
+
state_display_backend(&none_state),
|
|
387
|
+
Some("none"),
|
|
388
|
+
"adaptive layout contract: explicit display_backend=none remains the escape hatch. state={none_state}\nreport={none_report:?}"
|
|
389
|
+
);
|
|
390
|
+
assert!(
|
|
391
|
+
!state_has_adaptive_display_records(&none_state),
|
|
392
|
+
"explicit display_backend=none must not create adaptive display records. state={none_state}"
|
|
393
|
+
);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
#[test]
|
|
397
|
+
#[serial(env)]
|
|
398
|
+
fn quick_start_and_restart_emit_copyable_workspace_socket_attach_commands() {
|
|
399
|
+
let ws = tmp_dir("attach-command");
|
|
400
|
+
let team = write_team(
|
|
401
|
+
&ws,
|
|
402
|
+
"attach-team",
|
|
403
|
+
"fake",
|
|
404
|
+
Some("none"),
|
|
405
|
+
&[
|
|
406
|
+
AgentDoc {
|
|
407
|
+
id: "worker_a",
|
|
408
|
+
role: "Fake Worker A",
|
|
409
|
+
provider: "fake",
|
|
410
|
+
tools: &["mcp_team"],
|
|
411
|
+
body: "Fake worker A.",
|
|
412
|
+
},
|
|
413
|
+
AgentDoc {
|
|
414
|
+
id: "worker_b",
|
|
415
|
+
role: "Fake Worker B",
|
|
416
|
+
provider: "fake",
|
|
417
|
+
tools: &["mcp_team"],
|
|
418
|
+
body: "Fake worker B.",
|
|
419
|
+
},
|
|
420
|
+
],
|
|
421
|
+
);
|
|
422
|
+
seed_healthy_coordinator(&ws);
|
|
423
|
+
let transport = RecordingTransport::new();
|
|
424
|
+
let quick = quick_start_with_transport_in_workspace(
|
|
425
|
+
&ws,
|
|
426
|
+
&team,
|
|
427
|
+
None,
|
|
428
|
+
true,
|
|
429
|
+
Some("attachcmd"),
|
|
430
|
+
&transport,
|
|
431
|
+
)
|
|
432
|
+
.expect("quick-start should run with recording transport");
|
|
433
|
+
let quick_text = format!("{quick:?}");
|
|
434
|
+
let mut failures = Vec::new();
|
|
435
|
+
if !report_has_attach_command(&quick_text, "team-attachcmd", "worker_a") {
|
|
436
|
+
failures.push(format!(
|
|
437
|
+
"quick-start missing `tmux -S <full-socket-path> attach -t team-attachcmd:worker_a`; report={quick:?}"
|
|
438
|
+
));
|
|
439
|
+
}
|
|
440
|
+
if !report_has_attach_command(&quick_text, "team-attachcmd", "worker_b") {
|
|
441
|
+
failures.push(format!(
|
|
442
|
+
"quick-start missing `tmux -S <full-socket-path> attach -t team-attachcmd:worker_b`; report={quick:?}"
|
|
443
|
+
));
|
|
444
|
+
}
|
|
445
|
+
seed_running_resumable_state(&ws, "attachcmd", "worker_a", "fake");
|
|
446
|
+
seed_running_resumable_state(&ws, "attachcmd", "worker_b", "fake");
|
|
447
|
+
let restart = restart_with_transport(&ws, true, Some("attachcmd"), &RecordingTransport::new())
|
|
448
|
+
.expect("restart should run with recording transport");
|
|
449
|
+
let restart_text = format!("{restart:?}");
|
|
450
|
+
match &restart {
|
|
451
|
+
team_agent::lifecycle::RestartReport::Restarted {
|
|
452
|
+
attach_commands,
|
|
453
|
+
next_actions,
|
|
454
|
+
..
|
|
455
|
+
} => {
|
|
456
|
+
if attach_commands.len() != 1 {
|
|
457
|
+
failures.push(format!(
|
|
458
|
+
"restart must emit exactly one attach command; report={restart:?}"
|
|
459
|
+
));
|
|
460
|
+
}
|
|
461
|
+
if !next_actions.is_empty() {
|
|
462
|
+
failures.push(format!(
|
|
463
|
+
"restart next_actions must not duplicate attach_commands; report={restart:?}"
|
|
464
|
+
));
|
|
465
|
+
}
|
|
466
|
+
if !report_has_session_attach_command(&attach_commands.join("\n"), "team-attachcmd") {
|
|
467
|
+
failures.push(format!(
|
|
468
|
+
"restart missing `tmux -S <full-socket-path> attach -t team-attachcmd`; report={restart:?}"
|
|
469
|
+
));
|
|
470
|
+
}
|
|
471
|
+
for worker in ["worker_a", "worker_b"] {
|
|
472
|
+
if report_has_attach_command(&attach_commands.join("\n"), "team-attachcmd", worker)
|
|
473
|
+
{
|
|
474
|
+
failures.push(format!(
|
|
475
|
+
"restart must not emit per-worker attach command for {worker}; report={restart:?}"
|
|
476
|
+
));
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
_ => failures.push(format!("restart should succeed; report={restart_text}")),
|
|
481
|
+
}
|
|
482
|
+
assert!(
|
|
483
|
+
failures.is_empty(),
|
|
484
|
+
"B3 attach-command contract: quick-start and restart report/JSON must include copyable workspace-socket attach commands.\n{}",
|
|
485
|
+
failures.join("\n")
|
|
486
|
+
);
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
#[test]
|
|
490
|
+
fn display_default_none_is_documented_in_help_and_changelog() {
|
|
491
|
+
let help = Command::new(bin())
|
|
492
|
+
.args(["quick-start", "--help"])
|
|
493
|
+
.output()
|
|
494
|
+
.expect("run quick-start --help");
|
|
495
|
+
let help_text = format!(
|
|
496
|
+
"{}\n{}",
|
|
497
|
+
String::from_utf8_lossy(&help.stdout),
|
|
498
|
+
String::from_utf8_lossy(&help.stderr)
|
|
499
|
+
);
|
|
500
|
+
assert!(
|
|
501
|
+
help_text.contains("display_backend")
|
|
502
|
+
&& help_text.to_lowercase().contains("default")
|
|
503
|
+
&& help_text.to_lowercase().contains("adaptive")
|
|
504
|
+
&& help_text.to_lowercase().contains("none"),
|
|
505
|
+
"adaptive layout guard: quick-start help must disclose default adaptive and none escape hatch. help={help_text:?}"
|
|
506
|
+
);
|
|
507
|
+
|
|
508
|
+
let repo_root = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
|
|
509
|
+
.parent()
|
|
510
|
+
.and_then(Path::parent)
|
|
511
|
+
.unwrap()
|
|
512
|
+
.to_path_buf();
|
|
513
|
+
let docs = read_optional_changelogs(&repo_root);
|
|
514
|
+
assert!(
|
|
515
|
+
docs.contains("display_backend")
|
|
516
|
+
&& docs.to_lowercase().contains("adaptive")
|
|
517
|
+
&& docs.to_lowercase().contains("none"),
|
|
518
|
+
"adaptive layout guard: CHANGELOG/docs must disclose default adaptive and none escape hatch. searched_root={} docs_excerpt={}",
|
|
519
|
+
repo_root.display(),
|
|
520
|
+
&docs.chars().take(500).collect::<String>()
|
|
521
|
+
);
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
#[derive(Clone)]
|
|
525
|
+
struct AgentDoc<'a> {
|
|
526
|
+
id: &'a str,
|
|
527
|
+
role: &'a str,
|
|
528
|
+
provider: &'a str,
|
|
529
|
+
tools: &'a [&'a str],
|
|
530
|
+
body: &'a str,
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
#[derive(Debug, Clone)]
|
|
534
|
+
struct RecordedSpawn {
|
|
535
|
+
argv: Vec<String>,
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
#[derive(Default)]
|
|
539
|
+
struct RecordingTransport {
|
|
540
|
+
spawns: Mutex<Vec<RecordedSpawn>>,
|
|
541
|
+
session_present: Mutex<bool>,
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
struct EnvGuard {
|
|
545
|
+
key: &'static str,
|
|
546
|
+
old: Option<String>,
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
impl EnvGuard {
|
|
550
|
+
fn set(key: &'static str, value: &str) -> Self {
|
|
551
|
+
let old = std::env::var(key).ok();
|
|
552
|
+
std::env::set_var(key, value);
|
|
553
|
+
Self { key, old }
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
impl Drop for EnvGuard {
|
|
558
|
+
fn drop(&mut self) {
|
|
559
|
+
match self.old.take() {
|
|
560
|
+
Some(value) => std::env::set_var(self.key, value),
|
|
561
|
+
None => std::env::remove_var(self.key),
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
impl RecordingTransport {
|
|
567
|
+
fn new() -> Self {
|
|
568
|
+
Self::default()
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
fn single_spawn(&self) -> RecordedSpawn {
|
|
572
|
+
let spawns = self.spawns.lock().unwrap();
|
|
573
|
+
assert_eq!(
|
|
574
|
+
spawns.len(),
|
|
575
|
+
1,
|
|
576
|
+
"fixture should record one worker spawn; spawns={spawns:?}"
|
|
577
|
+
);
|
|
578
|
+
spawns[0].clone()
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
fn record_spawn(
|
|
582
|
+
&self,
|
|
583
|
+
session: &SessionName,
|
|
584
|
+
window: &WindowName,
|
|
585
|
+
argv: &[String],
|
|
586
|
+
) -> SpawnResult {
|
|
587
|
+
let mut spawns = self.spawns.lock().unwrap();
|
|
588
|
+
spawns.push(RecordedSpawn {
|
|
589
|
+
argv: argv.to_vec(),
|
|
590
|
+
});
|
|
591
|
+
*self.session_present.lock().unwrap() = true;
|
|
592
|
+
SpawnResult {
|
|
593
|
+
pane_id: PaneId::new(format!("%{}", spawns.len())),
|
|
594
|
+
session: session.clone(),
|
|
595
|
+
window: window.clone(),
|
|
596
|
+
child_pid: Some(10_000 + spawns.len() as u32),
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
impl Transport for RecordingTransport {
|
|
602
|
+
fn kind(&self) -> BackendKind {
|
|
603
|
+
BackendKind::Tmux
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
fn spawn_first(
|
|
607
|
+
&self,
|
|
608
|
+
session: &SessionName,
|
|
609
|
+
window: &WindowName,
|
|
610
|
+
argv: &[String],
|
|
611
|
+
_cwd: &Path,
|
|
612
|
+
_env: &BTreeMap<String, String>,
|
|
613
|
+
) -> Result<SpawnResult, TransportError> {
|
|
614
|
+
Ok(self.record_spawn(session, window, argv))
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
fn spawn_into(
|
|
618
|
+
&self,
|
|
619
|
+
session: &SessionName,
|
|
620
|
+
window: &WindowName,
|
|
621
|
+
argv: &[String],
|
|
622
|
+
_cwd: &Path,
|
|
623
|
+
_env: &BTreeMap<String, String>,
|
|
624
|
+
) -> Result<SpawnResult, TransportError> {
|
|
625
|
+
Ok(self.record_spawn(session, window, argv))
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
fn inject(
|
|
629
|
+
&self,
|
|
630
|
+
_target: &Target,
|
|
631
|
+
_payload: &InjectPayload,
|
|
632
|
+
_submit: Key,
|
|
633
|
+
_bracketed: bool,
|
|
634
|
+
) -> Result<InjectReport, TransportError> {
|
|
635
|
+
Ok(InjectReport {
|
|
636
|
+
stage_reached: InjectStage::Submit,
|
|
637
|
+
inject_verification: InjectVerification::CaptureContainsToken,
|
|
638
|
+
submit_verification: SubmitVerification::EnterSentWithoutPlaceholderCheck,
|
|
639
|
+
turn_verification: TurnVerification::NotYetObserved,
|
|
640
|
+
attempts: 1,
|
|
641
|
+
submit_diagnostics: None,
|
|
642
|
+
})
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
fn send_keys(&self, _target: &Target, _keys: &[Key]) -> Result<(), TransportError> {
|
|
646
|
+
Ok(())
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
fn capture(
|
|
650
|
+
&self,
|
|
651
|
+
_target: &Target,
|
|
652
|
+
range: CaptureRange,
|
|
653
|
+
) -> Result<CapturedText, TransportError> {
|
|
654
|
+
Ok(CapturedText {
|
|
655
|
+
text: "Claude Code\n> \nOpenAI Codex\ncodex>".to_string(),
|
|
656
|
+
range,
|
|
657
|
+
})
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
fn query(&self, _target: &Target, field: PaneField) -> Result<Option<String>, TransportError> {
|
|
661
|
+
match field {
|
|
662
|
+
PaneField::PaneWidth => Ok(Some("120".to_string())),
|
|
663
|
+
_ => Ok(None),
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
fn liveness(
|
|
668
|
+
&self,
|
|
669
|
+
_pane: &PaneId,
|
|
670
|
+
) -> Result<team_agent::transport::PaneLiveness, TransportError> {
|
|
671
|
+
Ok(team_agent::transport::PaneLiveness::Live)
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
fn list_targets(&self) -> Result<Vec<PaneInfo>, TransportError> {
|
|
675
|
+
Ok(Vec::new())
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
fn has_session(&self, _session: &SessionName) -> Result<bool, TransportError> {
|
|
679
|
+
Ok(*self.session_present.lock().unwrap())
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
fn list_windows(&self, _session: &SessionName) -> Result<Vec<WindowName>, TransportError> {
|
|
683
|
+
Ok(Vec::new())
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
fn set_session_env(
|
|
687
|
+
&self,
|
|
688
|
+
_session: &SessionName,
|
|
689
|
+
_key: &str,
|
|
690
|
+
_value: &str,
|
|
691
|
+
) -> Result<SetEnvOutcome, TransportError> {
|
|
692
|
+
Ok(SetEnvOutcome::Applied)
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
fn kill_session(&self, _session: &SessionName) -> Result<(), TransportError> {
|
|
696
|
+
*self.session_present.lock().unwrap() = false;
|
|
697
|
+
Ok(())
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
fn kill_window(&self, _target: &Target) -> Result<(), TransportError> {
|
|
701
|
+
Ok(())
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
fn attach_session(&self, _session: &SessionName) -> Result<AttachOutcome, TransportError> {
|
|
705
|
+
Ok(AttachOutcome::Attached)
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
fn tmp_dir(tag: &str) -> PathBuf {
|
|
710
|
+
static N: AtomicU64 = AtomicU64::new(0);
|
|
711
|
+
let dir = std::env::temp_dir().join(format!(
|
|
712
|
+
"ta-rs-core-034-{tag}-{}-{}",
|
|
713
|
+
std::process::id(),
|
|
714
|
+
N.fetch_add(1, Ordering::Relaxed)
|
|
715
|
+
));
|
|
716
|
+
let _ = std::fs::remove_dir_all(&dir);
|
|
717
|
+
std::fs::create_dir_all(&dir).unwrap();
|
|
718
|
+
std::fs::canonicalize(dir).unwrap()
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
fn write_team(
|
|
722
|
+
workspace: &Path,
|
|
723
|
+
name: &str,
|
|
724
|
+
provider: &str,
|
|
725
|
+
display_backend: Option<&str>,
|
|
726
|
+
agents: &[AgentDoc<'_>],
|
|
727
|
+
) -> PathBuf {
|
|
728
|
+
let team = workspace.join(name);
|
|
729
|
+
std::fs::create_dir_all(team.join("agents")).unwrap();
|
|
730
|
+
let display = display_backend
|
|
731
|
+
.map(|backend| format!("display_backend: {backend}\n"))
|
|
732
|
+
.unwrap_or_default();
|
|
733
|
+
std::fs::write(
|
|
734
|
+
team.join("TEAM.md"),
|
|
735
|
+
format!(
|
|
736
|
+
"---\nname: {name}\nobjective: 0.3.4 core contract fixture.\nprovider: {provider}\n{display}---\n\nTeam fixture.\n"
|
|
737
|
+
),
|
|
738
|
+
)
|
|
739
|
+
.unwrap();
|
|
740
|
+
for agent in agents {
|
|
741
|
+
let tools = agent
|
|
742
|
+
.tools
|
|
743
|
+
.iter()
|
|
744
|
+
.map(|tool| format!(" - {tool}\n"))
|
|
745
|
+
.collect::<String>();
|
|
746
|
+
std::fs::write(
|
|
747
|
+
team.join("agents").join(format!("{}.md", agent.id)),
|
|
748
|
+
format!(
|
|
749
|
+
"---\nname: {}\nrole: {}\nprovider: {}\nmodel: fake\nauth_mode: subscription\ntools:\n{}---\n\n{}\n",
|
|
750
|
+
agent.id, agent.role, agent.provider, tools, agent.body
|
|
751
|
+
),
|
|
752
|
+
)
|
|
753
|
+
.unwrap();
|
|
754
|
+
}
|
|
755
|
+
team
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
fn seed_healthy_coordinator(workspace: &Path) {
|
|
759
|
+
let workspace = team_agent::coordinator::WorkspacePath::new(workspace.to_path_buf());
|
|
760
|
+
let pid = team_agent::coordinator::Pid::new(std::process::id());
|
|
761
|
+
std::fs::create_dir_all(
|
|
762
|
+
team_agent::coordinator::coordinator_pid_path(&workspace)
|
|
763
|
+
.parent()
|
|
764
|
+
.unwrap(),
|
|
765
|
+
)
|
|
766
|
+
.unwrap();
|
|
767
|
+
team_agent::coordinator::write_coordinator_metadata(
|
|
768
|
+
&workspace,
|
|
769
|
+
pid,
|
|
770
|
+
team_agent::coordinator::MetadataSource::Boot,
|
|
771
|
+
)
|
|
772
|
+
.expect("write coordinator metadata");
|
|
773
|
+
std::fs::write(
|
|
774
|
+
team_agent::coordinator::coordinator_pid_path(&workspace),
|
|
775
|
+
pid.to_string(),
|
|
776
|
+
)
|
|
777
|
+
.expect("write coordinator pid");
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
fn seed_running_resumable_state(workspace: &Path, team: &str, agent: &str, provider: &str) {
|
|
781
|
+
let mut state = load_runtime_state(workspace).expect("load quick-start state before restart");
|
|
782
|
+
let agent_patch = json!({
|
|
783
|
+
"status": "running",
|
|
784
|
+
"provider": provider,
|
|
785
|
+
"session_id": "11111111-2222-4333-8444-555555555555",
|
|
786
|
+
"first_send_at": "2026-06-10T00:00:00+00:00",
|
|
787
|
+
"window": agent,
|
|
788
|
+
});
|
|
789
|
+
merge_agent_patch(state.pointer_mut(&format!("/agents/{agent}")), &agent_patch);
|
|
790
|
+
merge_agent_patch(
|
|
791
|
+
state.pointer_mut(&format!("/teams/{team}/agents/{agent}")),
|
|
792
|
+
&agent_patch,
|
|
793
|
+
);
|
|
794
|
+
save_runtime_state(workspace, &state).expect("save resumable state");
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
fn merge_agent_patch(target: Option<&mut Value>, patch: &Value) {
|
|
798
|
+
let Some(target) = target.and_then(Value::as_object_mut) else {
|
|
799
|
+
return;
|
|
800
|
+
};
|
|
801
|
+
for (key, value) in patch.as_object().unwrap() {
|
|
802
|
+
target.insert(key.clone(), value.clone());
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
fn remove_runtime_raw_tools(workspace: &Path, team: &str, agent: &str) {
|
|
807
|
+
let mut state = load_runtime_state(workspace).expect("load state before stripping raw tools");
|
|
808
|
+
for pointer in [
|
|
809
|
+
format!("/agents/{agent}/tools"),
|
|
810
|
+
format!("/teams/{team}/agents/{agent}/tools"),
|
|
811
|
+
] {
|
|
812
|
+
if let Some(obj) = state
|
|
813
|
+
.pointer_mut(pointer.trim_end_matches("/tools"))
|
|
814
|
+
.and_then(Value::as_object_mut)
|
|
815
|
+
{
|
|
816
|
+
obj.remove("tools");
|
|
817
|
+
obj.remove("role");
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
save_runtime_state(workspace, &state).expect("save state after stripping raw tools");
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
fn flag_value(argv: &[String], flag: &str) -> Option<String> {
|
|
824
|
+
argv.windows(2)
|
|
825
|
+
.find_map(|pair| (pair[0] == flag).then(|| pair[1].clone()))
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
fn flag_values(argv: &[String], flag: &str) -> Vec<String> {
|
|
829
|
+
argv.windows(2)
|
|
830
|
+
.filter_map(|pair| (pair[0] == flag).then(|| pair[1].clone()))
|
|
831
|
+
.collect()
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
fn argv_has_flag(argv: &[String], flag: &str) -> bool {
|
|
835
|
+
argv.iter().any(|arg| arg == flag)
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
fn state_display_backend(state: &Value) -> Option<&str> {
|
|
839
|
+
state
|
|
840
|
+
.get("display_backend")
|
|
841
|
+
.and_then(Value::as_str)
|
|
842
|
+
.or_else(|| {
|
|
843
|
+
state
|
|
844
|
+
.get("runtime")
|
|
845
|
+
.and_then(|runtime| runtime.get("display_backend"))
|
|
846
|
+
.and_then(Value::as_str)
|
|
847
|
+
})
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
fn state_has_adaptive_display_records(state: &Value) -> bool {
|
|
851
|
+
let agents = state
|
|
852
|
+
.get("agents")
|
|
853
|
+
.and_then(Value::as_object)
|
|
854
|
+
.into_iter()
|
|
855
|
+
.flat_map(|agents| agents.values());
|
|
856
|
+
agents.into_iter().any(|agent| {
|
|
857
|
+
agent
|
|
858
|
+
.get("display")
|
|
859
|
+
.and_then(|display| display.get("backend"))
|
|
860
|
+
.and_then(Value::as_str)
|
|
861
|
+
== Some("adaptive")
|
|
862
|
+
})
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
fn report_has_attach_command(text: &str, expected_session: &str, expected_window: &str) -> bool {
|
|
866
|
+
text.contains("tmux -S")
|
|
867
|
+
&& text.contains(" attach -t ")
|
|
868
|
+
&& text.contains(&format!("{expected_session}:{expected_window}"))
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
fn report_has_session_attach_command(text: &str, expected_session: &str) -> bool {
|
|
872
|
+
text.contains("tmux -S")
|
|
873
|
+
&& text.contains(" attach -t ")
|
|
874
|
+
&& text.contains(&format!("attach -t {expected_session}"))
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
fn read_to_string(path: &Path) -> String {
|
|
878
|
+
std::fs::read_to_string(path).unwrap_or_else(|e| panic!("read {}: {e}", path.display()))
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
fn read_rust_sources(root: &Path) -> String {
|
|
882
|
+
let mut out = String::new();
|
|
883
|
+
read_rust_sources_into(root, &mut out);
|
|
884
|
+
out
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
fn read_rust_sources_into(root: &Path, out: &mut String) {
|
|
888
|
+
let entries =
|
|
889
|
+
std::fs::read_dir(root).unwrap_or_else(|e| panic!("read_dir {}: {e}", root.display()));
|
|
890
|
+
for entry in entries {
|
|
891
|
+
let path = entry.unwrap().path();
|
|
892
|
+
if path.is_dir() {
|
|
893
|
+
if path.file_name().and_then(|name| name.to_str()) == Some("tests") {
|
|
894
|
+
continue;
|
|
895
|
+
}
|
|
896
|
+
read_rust_sources_into(&path, out);
|
|
897
|
+
} else if path.extension().and_then(|s| s.to_str()) == Some("rs") {
|
|
898
|
+
if path.file_name().and_then(|name| name.to_str()) == Some("tests.rs") {
|
|
899
|
+
continue;
|
|
900
|
+
}
|
|
901
|
+
out.push_str(&read_to_string(&path));
|
|
902
|
+
out.push('\n');
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
fn read_optional_changelogs(repo_root: &Path) -> String {
|
|
908
|
+
let mut text = String::new();
|
|
909
|
+
for name in ["CHANGELOG.md", "CHANGELOG", "README.md"] {
|
|
910
|
+
let path = repo_root.join(name);
|
|
911
|
+
if path.exists() {
|
|
912
|
+
text.push_str(&read_to_string(&path));
|
|
913
|
+
text.push('\n');
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
text
|
|
917
|
+
}
|