@team-agent/installer 0.5.111 → 0.5.113
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 +93 -779
- package/crates/team-agent/src/cli/diagnose.rs +2 -424
- package/crates/team-agent/src/cli/emit.rs +158 -373
- package/crates/team-agent/src/cli/leaders.rs +32 -32
- package/crates/team-agent/src/cli/mod.rs +8 -15
- package/crates/team-agent/src/cli/named_address.rs +17 -16
- package/crates/team-agent/src/cli/route.rs +4 -4
- package/crates/team-agent/src/cli/send/presentation.rs +19 -18
- package/crates/team-agent/src/cli/send/resolve.rs +11 -11
- package/crates/team-agent/src/cli/send.rs +4 -4
- package/crates/team-agent/src/cli/spec.rs +29 -32
- package/crates/team-agent/src/cli/status.rs +7 -7
- package/crates/team-agent/src/cli/tests/base.rs +6 -6
- package/crates/team-agent/src/cli/tests/divergence.rs +3 -2
- package/crates/team-agent/src/cli/tests/lane_c.rs +1 -1
- package/crates/team-agent/src/cli/tests/leader_watch.rs +2 -86
- package/crates/team-agent/src/cli/tests/main_preserved.rs +15 -140
- package/crates/team-agent/src/cli/tests/missing_subcommands.rs +1 -258
- package/crates/team-agent/src/cli/tests/mod.rs +0 -2
- package/crates/team-agent/src/cli/tests/named_address.rs +3 -3
- package/crates/team-agent/src/cli/tests/status_send.rs +7 -7
- package/crates/team-agent/src/cli/triage.rs +53 -2
- package/crates/team-agent/src/cli/types.rs +12 -116
- package/crates/team-agent/src/compiler/tests.rs +12 -0
- package/crates/team-agent/src/compiler.rs +0 -9
- package/crates/team-agent/src/coordinator/health.rs +3 -283
- package/crates/team-agent/src/coordinator/mod.rs +0 -5
- package/crates/team-agent/src/coordinator/tests/basics.rs +0 -9
- package/crates/team-agent/src/coordinator/tests/mod.rs +0 -1
- package/crates/team-agent/src/coordinator/tests/tick_core.rs +0 -17
- package/crates/team-agent/src/coordinator/types.rs +1 -21
- package/crates/team-agent/src/db/message_store.rs +0 -42
- package/crates/team-agent/src/db/migration.rs +49 -9
- package/crates/team-agent/src/db/schema.rs +27 -20
- package/crates/team-agent/src/leader/owner_bind.rs +0 -24
- package/crates/team-agent/src/leader/start.rs +5 -0
- package/crates/team-agent/src/leader/tests/byte_findings.rs +0 -20
- package/crates/team-agent/src/leader/tests/identity.rs +21 -32
- package/crates/team-agent/src/leader/tests/lease_claim.rs +0 -55
- package/crates/team-agent/src/lifecycle/launch/identity.rs +172 -13
- package/crates/team-agent/src/lifecycle/launch/quick_start.rs +325 -14
- package/crates/team-agent/src/lifecycle/launch.rs +2 -2
- package/crates/team-agent/src/lifecycle/profile_launch.rs +58 -46
- package/crates/team-agent/src/lifecycle/restart/rebuild.rs +97 -52
- package/crates/team-agent/src/lifecycle/restart.rs +1 -1
- package/crates/team-agent/src/lifecycle/tests/restart_session_capture_red.rs +2 -51
- package/crates/team-agent/src/lifecycle/tests/stale_team_saveconflict_contract.rs +3 -2
- package/crates/team-agent/src/lifecycle/tests/status_credential_redaction_contract.rs +23 -23
- package/crates/team-agent/src/lifecycle/tests/subprep_codex_trust_roundtrip_red.rs +29 -55
- package/crates/team-agent/src/messaging/mod.rs +0 -2
- package/crates/team-agent/src/messaging/results.rs +0 -58
- package/crates/team-agent/src/messaging/tests/runtime.rs +0 -46
- package/crates/team-agent/src/provider/adapter.rs +8 -59
- package/crates/team-agent/src/provider/adapters/claude.rs +6 -14
- package/crates/team-agent/src/provider/adapters/codex.rs +8 -18
- package/crates/team-agent/src/provider/adapters/copilot.rs +5 -22
- package/crates/team-agent/src/provider/adapters/cursor_agent.rs +8 -45
- package/crates/team-agent/src/provider/adapters/grok.rs +7 -31
- package/crates/team-agent/src/provider/adapters/pi.rs +7 -18
- package/crates/team-agent/src/provider/command_helpers.rs +45 -0
- package/crates/team-agent/src/provider/mod.rs +1 -0
- package/crates/team-agent/src/provider/model_catalog.rs +21 -28
- package/crates/team-agent/src/provider/tests/adapter.rs +70 -0
- package/crates/team-agent/src/provider/tests/command_helpers.rs +55 -0
- package/crates/team-agent/src/state/repository.rs +1 -5
- package/package.json +4 -4
- package/templates/team_state.md +1 -1
- package/crates/team-agent/src/cli/tests/peer_allow.rs +0 -164
- package/crates/team-agent/src/cli/tests/verb_validate.rs +0 -195
- package/crates/team-agent/src/coordinator/tests/watch.rs +0 -223
- package/crates/team-agent/src/messaging/peers.rs +0 -34
|
@@ -41,6 +41,71 @@ use crate::lifecycle::lock::{acquire_agent_lifecycle_lock, LifecycleLockRequest}
|
|
|
41
41
|
// `pane_verify_ms`, `startup_prompt_handler_ms`, `tmux_start_mode`) so
|
|
42
42
|
// bounded-concurrency spawn can be justified with real numbers.
|
|
43
43
|
|
|
44
|
+
/// Stable wire labels for launch/restart timing events, not readiness states.
|
|
45
|
+
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
|
46
|
+
pub(crate) enum LifecyclePhase {
|
|
47
|
+
ResolveContext,
|
|
48
|
+
CompileSpec,
|
|
49
|
+
PlanClassification,
|
|
50
|
+
Teardown,
|
|
51
|
+
SpawnAll,
|
|
52
|
+
SaveState,
|
|
53
|
+
CoordinatorStart,
|
|
54
|
+
ReadinessWait,
|
|
55
|
+
Completed,
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
impl LifecyclePhase {
|
|
59
|
+
pub(crate) fn as_str(self) -> &'static str {
|
|
60
|
+
match self {
|
|
61
|
+
Self::ResolveContext => "resolve_context",
|
|
62
|
+
Self::CompileSpec => "compile_spec",
|
|
63
|
+
Self::PlanClassification => "plan_classification",
|
|
64
|
+
Self::Teardown => "teardown",
|
|
65
|
+
Self::SpawnAll => "spawn_all",
|
|
66
|
+
Self::SaveState => "save_state",
|
|
67
|
+
Self::CoordinatorStart => "coordinator_start",
|
|
68
|
+
Self::ReadinessWait => "readiness_wait",
|
|
69
|
+
Self::Completed => "completed",
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/// Pure event formatting shared by launch/restart; no clock, I/O or readiness policy.
|
|
74
|
+
pub(crate) fn event_fields(self, elapsed_ms: u64) -> serde_json::Value {
|
|
75
|
+
serde_json::json!({
|
|
76
|
+
"phase": self.as_str(),
|
|
77
|
+
"elapsed_ms": elapsed_ms,
|
|
78
|
+
})
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
#[test]
|
|
83
|
+
fn lifecycle_phase_wire_labels_remain_stable() {
|
|
84
|
+
for (phase, wire) in [
|
|
85
|
+
(LifecyclePhase::ResolveContext, "resolve_context"),
|
|
86
|
+
(LifecyclePhase::CompileSpec, "compile_spec"),
|
|
87
|
+
(LifecyclePhase::PlanClassification, "plan_classification"),
|
|
88
|
+
(LifecyclePhase::Teardown, "teardown"),
|
|
89
|
+
(LifecyclePhase::SpawnAll, "spawn_all"),
|
|
90
|
+
(LifecyclePhase::SaveState, "save_state"),
|
|
91
|
+
(LifecyclePhase::CoordinatorStart, "coordinator_start"),
|
|
92
|
+
(LifecyclePhase::ReadinessWait, "readiness_wait"),
|
|
93
|
+
(LifecyclePhase::Completed, "completed"),
|
|
94
|
+
] {
|
|
95
|
+
assert_eq!(phase.as_str(), wire);
|
|
96
|
+
for elapsed_ms in [0, 123, u64::MAX] {
|
|
97
|
+
assert_eq!(phase.event_fields(elapsed_ms), serde_json::json!({
|
|
98
|
+
"phase": wire,
|
|
99
|
+
"elapsed_ms": elapsed_ms,
|
|
100
|
+
}));
|
|
101
|
+
assert_eq!(
|
|
102
|
+
serde_json::to_string(&phase.event_fields(elapsed_ms)).expect("phase JSON"),
|
|
103
|
+
format!("{{\"phase\":\"{wire}\",\"elapsed_ms\":{elapsed_ms}}}"),
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
44
109
|
pub(crate) struct RestartPhaseTimer {
|
|
45
110
|
started_at: std::time::Instant,
|
|
46
111
|
}
|
|
@@ -71,15 +136,9 @@ impl RestartPhaseTimer {
|
|
|
71
136
|
/// phase: 阶段名
|
|
72
137
|
/// returns: 写事件失败被吞掉,不影响主流程
|
|
73
138
|
/// ---
|
|
74
|
-
pub(crate) fn emit(&self, workspace: &Path, kind: &'static str, phase:
|
|
139
|
+
pub(crate) fn emit(&self, workspace: &Path, kind: &'static str, phase: LifecyclePhase) {
|
|
75
140
|
let event_log = crate::event_log::EventLog::new(workspace);
|
|
76
|
-
let _ = event_log.write(
|
|
77
|
-
kind,
|
|
78
|
-
serde_json::json!({
|
|
79
|
-
"phase": phase,
|
|
80
|
-
"elapsed_ms": self.elapsed_ms(),
|
|
81
|
-
}),
|
|
82
|
-
);
|
|
141
|
+
let _ = event_log.write(kind, phase.event_fields(self.elapsed_ms()));
|
|
83
142
|
}
|
|
84
143
|
}
|
|
85
144
|
|
|
@@ -305,7 +364,7 @@ fn restart_with_selected_team_and_transport(
|
|
|
305
364
|
// resolved a context; downstream phases are emitted with monotonic
|
|
306
365
|
// `elapsed_ms` for at-a-glance latency triage.
|
|
307
366
|
let phase_timer = RestartPhaseTimer::start();
|
|
308
|
-
phase_timer.emit(&selected.run_workspace, "restart.phase",
|
|
367
|
+
phase_timer.emit(&selected.run_workspace, "restart.phase", LifecyclePhase::ResolveContext);
|
|
309
368
|
let lifecycle_lock = acquire_agent_lifecycle_lock(LifecycleLockRequest {
|
|
310
369
|
workspace: &selected.run_workspace,
|
|
311
370
|
operation: "restart",
|
|
@@ -345,7 +404,7 @@ fn restart_with_selected_team_and_transport(
|
|
|
345
404
|
// 写 runtime spec。角色缺(TEAM.md/agents 不在)→ 显式拒(列缺哪些),旧 spec 原地保留不删不用。
|
|
346
405
|
let spec =
|
|
347
406
|
rebuild_runtime_spec_from_roles(&selected.run_workspace, &selected.team_key, &state)?;
|
|
348
|
-
phase_timer.emit(&selected.run_workspace, "restart.phase",
|
|
407
|
+
phase_timer.emit(&selected.run_workspace, "restart.phase", LifecyclePhase::CompileSpec);
|
|
349
408
|
// 重建后 spec_workspace 恒为 runtime spec 的父目录(.team/runtime/<team_key>/)。
|
|
350
409
|
let runtime_spec =
|
|
351
410
|
crate::model::paths::runtime_spec_path(&selected.run_workspace, &selected.team_key);
|
|
@@ -441,7 +500,7 @@ fn restart_with_selected_team_and_transport(
|
|
|
441
500
|
phase_timer.emit(
|
|
442
501
|
&selected.run_workspace,
|
|
443
502
|
"restart.phase",
|
|
444
|
-
|
|
503
|
+
LifecyclePhase::PlanClassification,
|
|
445
504
|
);
|
|
446
505
|
write_restart_resume_decision_events(
|
|
447
506
|
&selected.run_workspace,
|
|
@@ -558,8 +617,8 @@ fn restart_with_selected_team_and_transport(
|
|
|
558
617
|
&topology_authority_agent_ids,
|
|
559
618
|
)?;
|
|
560
619
|
}
|
|
561
|
-
phase_timer.emit(&selected.run_workspace, "restart.phase",
|
|
562
|
-
phase_timer.emit(&selected.run_workspace, "restart.phase",
|
|
620
|
+
phase_timer.emit(&selected.run_workspace, "restart.phase", LifecyclePhase::Teardown);
|
|
621
|
+
phase_timer.emit(&selected.run_workspace, "restart.phase", LifecyclePhase::SpawnAll);
|
|
563
622
|
// 0.5.40 Slice 3 (tmux-server-death-locate §7 Slice 3): when the
|
|
564
623
|
// pre-spawn teardown was DEFERRED (live worker session with
|
|
565
624
|
// authoritative running agents), snapshot the pre-spawn agent rows
|
|
@@ -582,8 +641,8 @@ fn restart_with_selected_team_and_transport(
|
|
|
582
641
|
// tmux session is created deterministically; the remaining independent
|
|
583
642
|
// decisions run their `transport.spawn_into` in parallel via a thread
|
|
584
643
|
// scope with concurrency capped at `min(4, workers-1)`. Every spawn
|
|
585
|
-
// result is collected in-memory; `
|
|
586
|
-
//
|
|
644
|
+
// result is collected in-memory; `mark_agent_respawned` is then
|
|
645
|
+
// applied in ORIGINAL PLAN ORDER so
|
|
587
646
|
// persisted `spawn_epoch` / `spawned_at` / `pane_id` / `window` stay
|
|
588
647
|
// deterministic regardless of thread completion order. Failure
|
|
589
648
|
// aggregation stays equivalent to serial: per-agent errors go into
|
|
@@ -837,17 +896,14 @@ fn restart_with_selected_team_and_transport(
|
|
|
837
896
|
agent.get("dangerously_skip_permissions"),
|
|
838
897
|
Some(serde_json::Value::Bool(true))
|
|
839
898
|
);
|
|
840
|
-
if let Err(error) =
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
launched_bypass,
|
|
849
|
-
)
|
|
850
|
-
})
|
|
899
|
+
if let Err(error) = mark_agent_respawned(
|
|
900
|
+
&mut state,
|
|
901
|
+
&decision.agent_id,
|
|
902
|
+
decision.restart_mode,
|
|
903
|
+
&spawn,
|
|
904
|
+
transport,
|
|
905
|
+
launched_bypass,
|
|
906
|
+
)
|
|
851
907
|
{
|
|
852
908
|
let error = error.to_string();
|
|
853
909
|
mark_agent_restart_failed(&mut state, decision, &error);
|
|
@@ -1026,7 +1082,7 @@ fn restart_with_selected_team_and_transport(
|
|
|
1026
1082
|
&capture_backfill_skip_agent_ids,
|
|
1027
1083
|
&topology_authority_agent_ids,
|
|
1028
1084
|
)?;
|
|
1029
|
-
phase_timer.emit(&selected.run_workspace, "restart.phase",
|
|
1085
|
+
phase_timer.emit(&selected.run_workspace, "restart.phase", LifecyclePhase::SaveState);
|
|
1030
1086
|
if fatal_resume_failure {
|
|
1031
1087
|
let attach_commands = Vec::new();
|
|
1032
1088
|
let next_actions = restart_failure_next_actions(&failed_agents);
|
|
@@ -1304,10 +1360,10 @@ fn restart_with_selected_team_and_transport(
|
|
|
1304
1360
|
phase_timer.emit(
|
|
1305
1361
|
&selected.run_workspace,
|
|
1306
1362
|
"restart.phase",
|
|
1307
|
-
|
|
1363
|
+
LifecyclePhase::CoordinatorStart,
|
|
1308
1364
|
);
|
|
1309
1365
|
let coordinator_started = coordinator.ok;
|
|
1310
|
-
phase_timer.emit(&selected.run_workspace, "restart.phase",
|
|
1366
|
+
phase_timer.emit(&selected.run_workspace, "restart.phase", LifecyclePhase::ReadinessWait);
|
|
1311
1367
|
wait_restart_readiness_or_timeout(
|
|
1312
1368
|
&selected.run_workspace,
|
|
1313
1369
|
&state,
|
|
@@ -1331,7 +1387,7 @@ fn restart_with_selected_team_and_transport(
|
|
|
1331
1387
|
let mut next_actions = Vec::new();
|
|
1332
1388
|
if !failed_agents.is_empty() {
|
|
1333
1389
|
next_actions.extend(restart_failure_next_actions(&failed_agents));
|
|
1334
|
-
phase_timer.emit(&selected.run_workspace, "restart.phase",
|
|
1390
|
+
phase_timer.emit(&selected.run_workspace, "restart.phase", LifecyclePhase::Completed);
|
|
1335
1391
|
write_restart_completed_event(
|
|
1336
1392
|
&selected.run_workspace,
|
|
1337
1393
|
&successful_agents,
|
|
@@ -1360,7 +1416,7 @@ fn restart_with_selected_team_and_transport(
|
|
|
1360
1416
|
leader_bind_reason,
|
|
1361
1417
|
});
|
|
1362
1418
|
}
|
|
1363
|
-
phase_timer.emit(&selected.run_workspace, "restart.phase",
|
|
1419
|
+
phase_timer.emit(&selected.run_workspace, "restart.phase", LifecyclePhase::Completed);
|
|
1364
1420
|
write_restart_completed_event(
|
|
1365
1421
|
&selected.run_workspace,
|
|
1366
1422
|
&successful_agents,
|
|
@@ -1852,14 +1908,6 @@ fn yes_no(value: bool) -> &'static str {
|
|
|
1852
1908
|
}
|
|
1853
1909
|
}
|
|
1854
1910
|
|
|
1855
|
-
fn verify_spawned_agent_live(
|
|
1856
|
-
_agent_id: &AgentId,
|
|
1857
|
-
_spawn: &SpawnedAgentWindow,
|
|
1858
|
-
_transport: &dyn crate::transport::Transport,
|
|
1859
|
-
) -> Result<(), LifecycleError> {
|
|
1860
|
-
Ok(())
|
|
1861
|
-
}
|
|
1862
|
-
|
|
1863
1911
|
/// 0.3.30 Bug 1: restart success path auto-attach.
|
|
1864
1912
|
/// When `TMUX_PANE` is present in the caller's env, treat the restart as if
|
|
1865
1913
|
/// the user had also run `attach-leader` from that pane. Mirrors quick-start's
|
|
@@ -3088,8 +3136,8 @@ fn run_bounded_parallel_worker_spawns(
|
|
|
3088
3136
|
outcomes
|
|
3089
3137
|
}
|
|
3090
3138
|
|
|
3091
|
-
/// 0.5.38 Step 2: serial post-spawn stage. Runs
|
|
3092
|
-
///
|
|
3139
|
+
/// 0.5.38 Step 2: serial post-spawn stage. Runs mark_agent_respawned
|
|
3140
|
+
/// in plan order, emits worker.spawn_timing, and
|
|
3093
3141
|
/// updates the successful/failed agent lists exactly like the pre-0.5.38
|
|
3094
3142
|
/// serial loop. Keeping this serial guarantees deterministic persisted
|
|
3095
3143
|
/// state (`spawn_epoch`, `spawned_at`, etc.) regardless of parallel
|
|
@@ -3111,17 +3159,14 @@ fn apply_marked_respawn(
|
|
|
3111
3159
|
launched_bypass: bool,
|
|
3112
3160
|
) {
|
|
3113
3161
|
let verify_start = std::time::Instant::now();
|
|
3114
|
-
if let Err(error) =
|
|
3115
|
-
|
|
3116
|
-
|
|
3117
|
-
|
|
3118
|
-
|
|
3119
|
-
|
|
3120
|
-
|
|
3121
|
-
|
|
3122
|
-
launched_bypass,
|
|
3123
|
-
)
|
|
3124
|
-
})
|
|
3162
|
+
if let Err(error) = mark_agent_respawned(
|
|
3163
|
+
state,
|
|
3164
|
+
&decision.agent_id,
|
|
3165
|
+
decision.restart_mode,
|
|
3166
|
+
spawn,
|
|
3167
|
+
transport,
|
|
3168
|
+
launched_bypass,
|
|
3169
|
+
)
|
|
3125
3170
|
{
|
|
3126
3171
|
let error = error.to_string();
|
|
3127
3172
|
mark_agent_restart_failed(state, decision, &error);
|
|
@@ -100,7 +100,7 @@ pub(crate) use rebuild::restart_with_transport_with_session_convergence_deadline
|
|
|
100
100
|
// timer + worker timing writer so `lifecycle::launch::launch_with_transport_in_workspace`
|
|
101
101
|
// can emit the same instrumentation event family with `source="launch"`.
|
|
102
102
|
pub(crate) use rebuild::{
|
|
103
|
-
provider_wire_from_state, write_worker_spawn_timing_event, RestartPhaseTimer,
|
|
103
|
+
provider_wire_from_state, write_worker_spawn_timing_event, LifecyclePhase, RestartPhaseTimer,
|
|
104
104
|
};
|
|
105
105
|
pub use rebuild::{restart, restart_with_session_convergence_deadline};
|
|
106
106
|
pub(crate) use rebuild::{
|
|
@@ -21,7 +21,6 @@ use std::sync::Mutex;
|
|
|
21
21
|
|
|
22
22
|
use serde_json::{json, Value};
|
|
23
23
|
use serial_test::serial;
|
|
24
|
-
use team_agent::cli::{cmd_wait_ready, CmdOutput, WaitReadyArgs};
|
|
25
24
|
use team_agent::lifecycle::{
|
|
26
25
|
classify_restart_plan, restart_with_transport, RestartReport, ResumeDecision, StartMode,
|
|
27
26
|
};
|
|
@@ -448,48 +447,7 @@ fn restart_refuses_first_send_at_null_running_resumable_session_incomplete_when_
|
|
|
448
447
|
}
|
|
449
448
|
|
|
450
449
|
#[test]
|
|
451
|
-
|
|
452
|
-
#[serial(env)]
|
|
453
|
-
fn wait_ready_reports_session_capture_incomplete_for_running_resumable_worker_without_session() {
|
|
454
|
-
let fixture = RestartFixture::new("wait-ready-session-incomplete", "codex");
|
|
455
|
-
seed_running_state_without_session(&fixture.team, "codex", &fixture.spawn_cwd);
|
|
456
|
-
let mut state = load_runtime_state(&fixture.team).unwrap();
|
|
457
|
-
let mcp_config = fixture.team.join(".team/runtime/worker_a.mcp.json");
|
|
458
|
-
std::fs::create_dir_all(mcp_config.parent().unwrap()).unwrap();
|
|
459
|
-
std::fs::write(&mcp_config, "{}").unwrap();
|
|
460
|
-
state["agents"]["worker_a"]["pane_id"] = json!("%1");
|
|
461
|
-
state["agents"]["worker_a"]["mcp_config"] = json!(mcp_config.to_string_lossy().to_string());
|
|
462
|
-
save_runtime_state(&fixture.team, &state).unwrap();
|
|
463
|
-
|
|
464
|
-
let out = json_result(
|
|
465
|
-
cmd_wait_ready(&WaitReadyArgs {
|
|
466
|
-
workspace: fixture.team.clone(),
|
|
467
|
-
timeout: 0.0,
|
|
468
|
-
json: true,
|
|
469
|
-
team: None,
|
|
470
|
-
})
|
|
471
|
-
.expect("wait-ready should return structured JSON"),
|
|
472
|
-
);
|
|
473
|
-
|
|
474
|
-
assert_eq!(
|
|
475
|
-
out["ok"],
|
|
476
|
-
json!(false),
|
|
477
|
-
"wait-ready cannot report ready while a running Codex/Claude worker is missing captured session context; out={out}"
|
|
478
|
-
);
|
|
479
|
-
assert_eq!(
|
|
480
|
-
out["reason"],
|
|
481
|
-
json!("session_capture_incomplete"),
|
|
482
|
-
"readiness reason must distinguish missing resumable session from generic workers_not_ready; out={out}"
|
|
483
|
-
);
|
|
484
|
-
assert!(
|
|
485
|
-
out["readiness"]["session_capture_incomplete"].as_bool() == Some(true)
|
|
486
|
-
&& out["readiness"]["all_resumable_have_session"].as_bool() == Some(false),
|
|
487
|
-
"readiness truth table must include all_resumable_have_session and session_capture_incomplete; out={out}"
|
|
488
|
-
);
|
|
489
|
-
}
|
|
490
|
-
|
|
491
|
-
#[test]
|
|
492
|
-
fn status_quick_start_and_wait_ready_surfaces_include_session_capture_completeness() {
|
|
450
|
+
fn status_and_quick_start_surfaces_include_session_capture_completeness() {
|
|
493
451
|
let diagnose =
|
|
494
452
|
std::fs::read_to_string(concat!(env!("CARGO_MANIFEST_DIR"), "/src/cli/diagnose.rs"))
|
|
495
453
|
.unwrap();
|
|
@@ -500,7 +458,7 @@ fn status_quick_start_and_wait_ready_surfaces_include_session_capture_completene
|
|
|
500
458
|
.unwrap();
|
|
501
459
|
|
|
502
460
|
for (name, source) in [
|
|
503
|
-
("
|
|
461
|
+
("status readiness formula", diagnose.as_str()),
|
|
504
462
|
("status --json", status.as_str()),
|
|
505
463
|
("quick-start readiness", launch.as_str()),
|
|
506
464
|
("quick-start --json", adapters.as_str()),
|
|
@@ -945,13 +903,6 @@ fn read_events(team: &Path) -> String {
|
|
|
945
903
|
std::fs::read_to_string(team.join(".team/logs/events.jsonl")).unwrap_or_default()
|
|
946
904
|
}
|
|
947
905
|
|
|
948
|
-
fn json_result(result: team_agent::cli::CmdResult) -> Value {
|
|
949
|
-
match result.output {
|
|
950
|
-
CmdOutput::Json(value) => value,
|
|
951
|
-
other => panic!("expected JSON command output, got {other:?}"),
|
|
952
|
-
}
|
|
953
|
-
}
|
|
954
|
-
|
|
955
906
|
fn assert_no_agent_attribution_ambiguous<const N: usize>(
|
|
956
907
|
state: &Value,
|
|
957
908
|
expected_agents: [&str; N],
|
|
@@ -165,14 +165,15 @@ fn purge_agent_help_and_dispatch_are_consistent() {
|
|
|
165
165
|
output_text(&case.run(["purge-agent", "ghost", "--workspace", case.ws(), "--json"]));
|
|
166
166
|
if help.contains("purge-agent") {
|
|
167
167
|
assert!(
|
|
168
|
-
!command.contains("invalid choice") && !command.contains("unknown subcommand")
|
|
168
|
+
!command.contains("invalid choice") && !command.contains("unknown subcommand")
|
|
169
|
+
&& !command.contains("Unknown command:") && !command.contains("Commands:"),
|
|
169
170
|
"RED6: help exposes purge-agent, so dispatch must be real or the command must be removed from help; help={help} command={command}"
|
|
170
171
|
);
|
|
171
172
|
} else {
|
|
172
173
|
assert!(
|
|
173
174
|
command.contains("unknown subcommand")
|
|
174
175
|
|| command.contains("invalid choice")
|
|
175
|
-
|| command.contains("
|
|
176
|
+
|| command.contains("Unknown command:"),
|
|
176
177
|
"RED6: if purge-agent is not implemented, help and dispatch must agree it is absent; help={help} command={command}"
|
|
177
178
|
);
|
|
178
179
|
}
|
|
@@ -23,7 +23,7 @@ use std::time::{Duration, Instant};
|
|
|
23
23
|
|
|
24
24
|
use serde_json::{json, Value};
|
|
25
25
|
use serial_test::serial;
|
|
26
|
-
use team_agent::cli::{
|
|
26
|
+
use team_agent::cli::{cmd_doctor, emit, CmdOutput, DoctorArgs};
|
|
27
27
|
use team_agent::event_log::EventLog;
|
|
28
28
|
use team_agent::message_store::MessageStore;
|
|
29
29
|
use team_agent::state::persist::save_runtime_state;
|
|
@@ -334,11 +334,10 @@ fn red6_cli_restart_reports_and_top_level_error_logs_are_redacted() {
|
|
|
334
334
|
env.scrub_tmux();
|
|
335
335
|
env.assert_no_real_tmux();
|
|
336
336
|
let workspace = env.workspace("red6");
|
|
337
|
-
let spec = write_invalid_validate_spec(&workspace, &marker);
|
|
338
337
|
let mut combined = String::new();
|
|
339
338
|
for json_mode in [true, false] {
|
|
340
339
|
let mut command = Command::new(crate::lifecycle::tests::test_binary_path());
|
|
341
|
-
command.arg("
|
|
340
|
+
command.arg("profile").arg(credential_url(&marker)).arg("synthetic-profile");
|
|
342
341
|
if json_mode {
|
|
343
342
|
command.arg("--json");
|
|
344
343
|
}
|
|
@@ -351,7 +350,7 @@ fn red6_cli_restart_reports_and_top_level_error_logs_are_redacted() {
|
|
|
351
350
|
.expect("run top-level CLI error boundary");
|
|
352
351
|
assert!(
|
|
353
352
|
!output.status.success(),
|
|
354
|
-
"invalid
|
|
353
|
+
"invalid profile subcommand must reach CLI error"
|
|
355
354
|
);
|
|
356
355
|
combined.push_str(&String::from_utf8_lossy(&output.stdout));
|
|
357
356
|
combined.push_str(&String::from_utf8_lossy(&output.stderr));
|
|
@@ -391,15 +390,30 @@ fn red7_pane_and_profile_scrubbers_converge_without_losing_existing_policy() {
|
|
|
391
390
|
let env = hermetic_guard::HermeticTestEnv::enter("redact-red7");
|
|
392
391
|
let pane_policy_preserved = pane_excerpt_uses_shared_redaction(&env, &marker);
|
|
393
392
|
let team = write_profile_fixture(env.root(), server.base_url(), &profile_token);
|
|
394
|
-
let result =
|
|
393
|
+
let result = cmd_doctor(&DoctorArgs {
|
|
394
|
+
spec: Some(team.join("TEAM.md")),
|
|
395
|
+
workspace: team,
|
|
396
|
+
gate: None,
|
|
397
|
+
comms: false,
|
|
398
|
+
team: None,
|
|
399
|
+
fix: false,
|
|
400
|
+
fix_schema: false,
|
|
401
|
+
cleanup_orphans: false,
|
|
402
|
+
confirm: false,
|
|
403
|
+
json: true,
|
|
404
|
+
})
|
|
395
405
|
.expect("profile smoke returns a diagnostic report");
|
|
396
406
|
let report = match result.output {
|
|
397
407
|
CmdOutput::Json(value) => value,
|
|
398
|
-
other => panic!("expected JSON
|
|
408
|
+
other => panic!("expected JSON doctor output, got {other:?}"),
|
|
399
409
|
};
|
|
400
|
-
let
|
|
401
|
-
let
|
|
402
|
-
|
|
410
|
+
let report_text = report.to_string();
|
|
411
|
+
let text = report
|
|
412
|
+
.get("profile_smoke")
|
|
413
|
+
.unwrap_or_else(|| panic!("doctor must expose profile_smoke; report={report}"))
|
|
414
|
+
.to_string();
|
|
415
|
+
let profile_policy_preserved = !report_text.contains(&marker)
|
|
416
|
+
&& !report_text.contains(&profile_token)
|
|
403
417
|
&& !text.contains(tail_sentinel)
|
|
404
418
|
&& text.contains(REDACTED)
|
|
405
419
|
&& text.contains("proxy.invalid:8443/path")
|
|
@@ -555,20 +569,6 @@ fn run_mcp_status_frames(
|
|
|
555
569
|
.collect()
|
|
556
570
|
}
|
|
557
571
|
|
|
558
|
-
fn write_invalid_validate_spec(workspace: &Path, marker: &str) -> PathBuf {
|
|
559
|
-
let path = workspace.join("invalid-provider.spec.yaml");
|
|
560
|
-
fs::write(
|
|
561
|
-
&path,
|
|
562
|
-
format!(
|
|
563
|
-
"version: 1\nteam:\n id: current\n name: current\n session_name: team-redaction-contract\n workspace: \"{}\"\nleader:\n provider: \"{}\"\nagents:\n - id: worker\n provider: fake\n model: fake\n role: Worker\n window: worker\ntasks: []\n",
|
|
564
|
-
workspace.display(),
|
|
565
|
-
credential_url(marker)
|
|
566
|
-
),
|
|
567
|
-
)
|
|
568
|
-
.unwrap();
|
|
569
|
-
path
|
|
570
|
-
}
|
|
571
|
-
|
|
572
572
|
fn read_cli_error_logs(workspace: &Path) -> String {
|
|
573
573
|
let dir = workspace.join(".team/logs");
|
|
574
574
|
fs::read_dir(dir)
|
|
@@ -92,15 +92,10 @@ const SUBROOT_FULL_TRUST_PANE_AFTER_QUICKSTART: &str = r#"> You are in /private/
|
|
|
92
92
|
│ directory: /private/tmp/…/teamdir │
|
|
93
93
|
╰───────────────────────────────────────╯
|
|
94
94
|
|
|
95
|
-
|
|
96
95
|
› Find and fix a bug in @filename
|
|
97
96
|
|
|
98
97
|
gpt-5.5 default · /private/tmp/ta-subroot-SUB-PROBE-1-trust-startup.uL9Boc/te…
|
|
99
98
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
99
|
"#;
|
|
105
100
|
|
|
106
101
|
fn tmp_dir(tag: &str) -> TempWorkspace {
|
|
@@ -419,8 +414,7 @@ fn contract_a_own_workspace_trust_is_actionable_not_ready_and_foreign_is_pending
|
|
|
419
414
|
}
|
|
420
415
|
|
|
421
416
|
#[test]
|
|
422
|
-
fn
|
|
423
|
-
use team_agent::cli::{cmd_wait_ready, CmdOutput, WaitReadyArgs};
|
|
417
|
+
fn contract_a_runtime_readiness_is_false_while_actionable_trust_remains() {
|
|
424
418
|
use team_agent::provider::{classify_codex_startup_screen, StartupScreenDecision};
|
|
425
419
|
|
|
426
420
|
assert_eq!(
|
|
@@ -429,67 +423,47 @@ fn contract_a_probe5_quick_start_ready_is_invalid_while_actionable_trust_remains
|
|
|
429
423
|
"fixture sanity: probe5 pane text is an actionable own-workspace trust prompt"
|
|
430
424
|
);
|
|
431
425
|
|
|
432
|
-
let ws = tmp_dir("a-
|
|
426
|
+
let ws = tmp_dir("a-runtime-readiness-trust");
|
|
433
427
|
let mcp_config = ws.join(".team").join("runtime").join("mcp").join("w1.json");
|
|
434
428
|
std::fs::create_dir_all(mcp_config.parent().unwrap()).unwrap();
|
|
435
429
|
std::fs::write(&mcp_config, "{}").unwrap();
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
430
|
+
// Keep every other readiness gate satisfied; an unattached leader would
|
|
431
|
+
// make ready=false independently of the trust prompt under test.
|
|
432
|
+
let mut state = json!({
|
|
433
|
+
"session_name": "team-subprep",
|
|
434
|
+
"tmux_session_present": true,
|
|
435
|
+
"leader_receiver": {"status": "attached", "pane_id": "%9"},
|
|
436
|
+
"agents": {
|
|
437
|
+
"w1": {
|
|
438
|
+
"provider": "codex",
|
|
439
|
+
"status": "awaiting_trust_prompt",
|
|
440
|
+
"startup_prompt_status": "awaiting_trust_prompt",
|
|
441
|
+
"startup_prompts": "awaiting_trust_prompt",
|
|
442
|
+
"pane_id": "%7",
|
|
443
|
+
"window": "w1",
|
|
444
|
+
"mcp_config": mcp_config.to_string_lossy().to_string(),
|
|
445
|
+
"task_prompt_delivered": true
|
|
451
446
|
}
|
|
452
|
-
}
|
|
453
|
-
)
|
|
454
|
-
.unwrap();
|
|
455
|
-
|
|
456
|
-
let result = cmd_wait_ready(&WaitReadyArgs {
|
|
457
|
-
workspace: ws.to_path_buf(),
|
|
458
|
-
timeout: 0.0,
|
|
459
|
-
json: true,
|
|
460
|
-
team: None,
|
|
461
|
-
})
|
|
462
|
-
.expect("wait-ready SUT should return shaped JSON");
|
|
463
|
-
let value = match result.output {
|
|
464
|
-
CmdOutput::Json(value) => value,
|
|
465
|
-
other => panic!("wait-ready --json must return JSON output, got {other:?}"),
|
|
466
|
-
};
|
|
447
|
+
}
|
|
448
|
+
});
|
|
467
449
|
|
|
450
|
+
let value = crate::cli::diagnose::wait_readiness(&state);
|
|
468
451
|
assert_eq!(
|
|
469
|
-
value["
|
|
470
|
-
json!(false),
|
|
471
|
-
"Contract A: quick-start/wait-ready readiness must not report ok=true while actionable trust remains; got {value}"
|
|
472
|
-
);
|
|
473
|
-
assert_eq!(
|
|
474
|
-
value["status"],
|
|
475
|
-
json!("pending"),
|
|
476
|
-
"Contract A: actionable trust prompt should keep readiness pending, not ready; got {value}"
|
|
477
|
-
);
|
|
478
|
-
assert_eq!(
|
|
479
|
-
value["reason"],
|
|
480
|
-
json!("awaiting_trust_prompt"),
|
|
481
|
-
"Contract A: not-ready reason must identify the startup trust boundary; got {value}"
|
|
482
|
-
);
|
|
483
|
-
assert_eq!(
|
|
484
|
-
value["readiness"]["awaiting_trust_prompt"],
|
|
452
|
+
value["awaiting_trust_prompt"],
|
|
485
453
|
json!(true),
|
|
486
454
|
"Contract A: ready logic must surface awaiting_trust_prompt=true; got {value}"
|
|
487
455
|
);
|
|
488
456
|
assert_eq!(
|
|
489
|
-
value["
|
|
457
|
+
value["ready"],
|
|
490
458
|
json!(false),
|
|
491
459
|
"Contract A: ready must stay false until the actionable trust prompt is cleared; got {value}"
|
|
492
460
|
);
|
|
461
|
+
state["agents"]["w1"]["status"] = json!("running");
|
|
462
|
+
state["agents"]["w1"]["startup_prompt_status"] = json!("complete");
|
|
463
|
+
state["agents"]["w1"]["startup_prompts"] = json!("complete");
|
|
464
|
+
let cleared = crate::cli::diagnose::wait_readiness(&state);
|
|
465
|
+
assert_eq!(cleared["awaiting_trust_prompt"], json!(false));
|
|
466
|
+
assert_eq!(cleared["ready"], json!(true), "positive control: {cleared}");
|
|
493
467
|
}
|
|
494
468
|
|
|
495
469
|
#[test]
|
|
@@ -67,7 +67,6 @@ pub mod delivery;
|
|
|
67
67
|
pub mod helpers;
|
|
68
68
|
pub mod leader_channel;
|
|
69
69
|
pub mod leader_receiver;
|
|
70
|
-
pub mod peers;
|
|
71
70
|
pub mod persist;
|
|
72
71
|
pub mod presentation;
|
|
73
72
|
pub mod results;
|
|
@@ -96,7 +95,6 @@ pub use leader_receiver::{
|
|
|
96
95
|
mirror_peer_message_to_leader, send_to_leader_receiver,
|
|
97
96
|
send_to_leader_receiver_with_presentation,
|
|
98
97
|
};
|
|
99
|
-
pub use peers::allow_peer_talk;
|
|
100
98
|
pub use persist::{
|
|
101
99
|
persist_resolved_send, DeliveryBlocker, InitialDisposition, InternalSendKind,
|
|
102
100
|
LogicalRecipient, PersistResolution, PersistedSend, ResolvedSendIntent, SendOrigin,
|
|
@@ -45,64 +45,6 @@ impl Default for ResultRoute {
|
|
|
45
45
|
}
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
-
pub fn results_for_case(
|
|
49
|
-
workspace: &Path,
|
|
50
|
-
case_id: &str,
|
|
51
|
-
owner_team_id: Option<&str>,
|
|
52
|
-
requested_owner_team_id: Option<&str>,
|
|
53
|
-
) -> Result<Vec<serde_json::Value>, MessagingError> {
|
|
54
|
-
let current_db = workspace.join(".team/runtime/team.db");
|
|
55
|
-
let legacy_db = workspace.join(".team/team.db");
|
|
56
|
-
if current_db.exists() {
|
|
57
|
-
return results_for_case_in_db(
|
|
58
|
-
¤t_db,
|
|
59
|
-
case_id,
|
|
60
|
-
owner_team_id,
|
|
61
|
-
requested_owner_team_id,
|
|
62
|
-
);
|
|
63
|
-
}
|
|
64
|
-
if legacy_db.exists() {
|
|
65
|
-
return results_for_case_in_db(&legacy_db, case_id, owner_team_id, requested_owner_team_id);
|
|
66
|
-
}
|
|
67
|
-
Ok(Vec::new())
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
fn results_for_case_in_db(
|
|
71
|
-
db_path: &Path,
|
|
72
|
-
case_id: &str,
|
|
73
|
-
owner_team_id: Option<&str>,
|
|
74
|
-
requested_owner_team_id: Option<&str>,
|
|
75
|
-
) -> Result<Vec<serde_json::Value>, MessagingError> {
|
|
76
|
-
let conn = crate::db::schema::open_db(db_path)?;
|
|
77
|
-
// The second predicate works around
|
|
78
|
-
// `.team/bugs/BUG-caseid-writable-when-route-undeclared-20260730.md`;
|
|
79
|
-
// it is not design intent.
|
|
80
|
-
let case_predicate = "task_id = ?1 or json_extract(envelope,'$.presentation.case_id') = ?1";
|
|
81
|
-
let sql = match owner_team_id {
|
|
82
|
-
Some(_) => format!(
|
|
83
|
-
"select envelope from results where ({case_predicate}) \
|
|
84
|
-
and (owner_team_id = ?2 or owner_team_id = ?3) \
|
|
85
|
-
order by created_at, result_id"
|
|
86
|
-
),
|
|
87
|
-
None => format!(
|
|
88
|
-
"select envelope from results where {case_predicate} order by created_at, result_id"
|
|
89
|
-
),
|
|
90
|
-
};
|
|
91
|
-
let mut stmt = conn.prepare(&sql)?;
|
|
92
|
-
let row_mapper = |row: &rusqlite::Row<'_>| row.get::<_, String>(0);
|
|
93
|
-
let rows = match (owner_team_id, requested_owner_team_id) {
|
|
94
|
-
(Some(team), Some(requested_team)) => {
|
|
95
|
-
stmt.query_map(params![case_id, team, requested_team], row_mapper)
|
|
96
|
-
}
|
|
97
|
-
(Some(team), None) => stmt.query_map(params![case_id, team, team], row_mapper),
|
|
98
|
-
(None, _) => stmt.query_map(params![case_id], row_mapper),
|
|
99
|
-
}?
|
|
100
|
-
.collect::<Result<Vec<_>, _>>()?;
|
|
101
|
-
rows.into_iter()
|
|
102
|
-
.map(|envelope| serde_json::from_str(&envelope).map_err(MessagingError::from))
|
|
103
|
-
.collect()
|
|
104
|
-
}
|
|
105
|
-
|
|
106
48
|
/// `collect` (`results.py:45`):投递 pending、捞 uncollected results、校验 envelope、更新任务态、
|
|
107
49
|
/// 写 team_state、ensure coordinator。CLI `collect` + coordinator tick 调。
|
|
108
50
|
pub fn collect(
|