@team-agent/installer 0.5.49 → 0.5.51
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 +6 -4
- package/crates/team-agent/src/cli/emit.rs +91 -39
- package/crates/team-agent/src/cli/mod.rs +33 -15
- package/crates/team-agent/src/cli/named_address.rs +82 -53
- package/crates/team-agent/src/cli/send/coordinator.rs +163 -0
- package/crates/team-agent/src/cli/send/mailbox.rs +99 -0
- package/crates/team-agent/src/cli/send/persist.rs +154 -0
- package/crates/team-agent/src/cli/send/presentation.rs +333 -0
- package/crates/team-agent/src/cli/send/resolve.rs +361 -0
- package/crates/team-agent/src/cli/send.rs +103 -1308
- package/crates/team-agent/src/cli/spec.rs +2 -2
- package/crates/team-agent/src/cli/status_port/agents.rs +358 -0
- package/crates/team-agent/src/cli/status_port/approvals.rs +79 -0
- package/crates/team-agent/src/cli/status_port/compact.rs +207 -0
- package/crates/team-agent/src/cli/status_port/format.rs +145 -0
- package/crates/team-agent/src/cli/status_port/inbox.rs +36 -0
- package/crates/team-agent/src/cli/status_port/runtime.rs +195 -0
- package/crates/team-agent/src/cli/status_port/snapshot.rs +181 -0
- package/crates/team-agent/src/cli/status_port/store.rs +412 -0
- package/crates/team-agent/src/cli/status_port/tests.rs +54 -0
- package/crates/team-agent/src/cli/status_port.rs +47 -1548
- package/crates/team-agent/src/cli/tests/leader_watch.rs +1 -1
- package/crates/team-agent/src/cli/tests/named_address.rs +9 -7
- package/crates/team-agent/src/cli/tests/run_delegation.rs +2 -3
- package/crates/team-agent/src/cli/tests/status_send.rs +17 -33
- package/crates/team-agent/src/cli/types.rs +5 -8
- package/crates/team-agent/src/coordinator/conpty_shim.rs +34 -30
- package/crates/team-agent/src/coordinator/steps/abnormal.rs +135 -13
- package/crates/team-agent/src/coordinator/tick.rs +37 -0
- package/crates/team-agent/src/db/agent_health_capture.rs +18 -13
- package/crates/team-agent/src/db/message_store.rs +154 -44
- package/crates/team-agent/src/event_log.rs +73 -0
- package/crates/team-agent/src/leader/start.rs +28 -4
- package/crates/team-agent/src/lifecycle/launch/add_agent.rs +424 -0
- package/crates/team-agent/src/lifecycle/launch/add_agent_state.rs +297 -0
- package/crates/team-agent/src/lifecycle/launch/agent_state.rs +160 -0
- package/crates/team-agent/src/lifecycle/launch/approval.rs +134 -0
- package/crates/team-agent/src/lifecycle/launch/fork_agent.rs +492 -0
- package/crates/team-agent/src/lifecycle/launch/fork_state.rs +297 -0
- package/crates/team-agent/src/lifecycle/launch/identity.rs +372 -0
- package/crates/team-agent/src/lifecycle/launch/layout.rs +313 -0
- package/crates/team-agent/src/lifecycle/launch/leader_context.rs +478 -0
- package/crates/team-agent/src/lifecycle/launch/mcp_config.rs +201 -0
- package/crates/team-agent/src/lifecycle/launch/ownership.rs +66 -0
- package/crates/team-agent/src/lifecycle/launch/quick_start.rs +477 -0
- package/crates/team-agent/src/lifecycle/launch/quick_start_transport.rs +278 -0
- package/crates/team-agent/src/lifecycle/launch/readiness.rs +123 -0
- package/crates/team-agent/src/lifecycle/launch/spawn.rs +377 -0
- package/crates/team-agent/src/lifecycle/launch/spec_state.rs +434 -0
- package/crates/team-agent/src/lifecycle/launch/state_projection.rs +499 -0
- package/crates/team-agent/src/lifecycle/launch/worker_env.rs +438 -0
- package/crates/team-agent/src/lifecycle/launch.rs +119 -5351
- package/crates/team-agent/src/lifecycle/restart/agent.rs +44 -26
- package/crates/team-agent/src/lifecycle/restart/common.rs +53 -27
- package/crates/team-agent/src/lifecycle/restart/rebuild.rs +67 -26
- package/crates/team-agent/src/lifecycle/restart/remove.rs +435 -72
- package/crates/team-agent/src/lifecycle/restart.rs +1 -1
- package/crates/team-agent/src/lifecycle/tests/lane_ops.rs +575 -17
- package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +55 -2
- package/crates/team-agent/src/lifecycle/tests/lifecycle_lock.rs +24 -1
- package/crates/team-agent/src/lifecycle/tests/phase_golden.rs +1 -1
- package/crates/team-agent/src/mcp_server/lifecycle_tools/agent_ops.rs +30 -7
- package/crates/team-agent/src/mcp_server/lifecycle_tools/state_status.rs +8 -4
- package/crates/team-agent/src/mcp_server/mod.rs +2 -2
- package/crates/team-agent/src/mcp_server/tests/send.rs +22 -15
- package/crates/team-agent/src/mcp_server/tests/wire.rs +6 -0
- package/crates/team-agent/src/mcp_server/tools.rs +26 -15
- package/crates/team-agent/src/mcp_server/wire.rs +2 -18
- package/crates/team-agent/src/messaging/activity.rs +4 -2
- package/crates/team-agent/src/messaging/address.rs +86 -0
- package/crates/team-agent/src/messaging/delivery.rs +165 -39
- package/crates/team-agent/src/messaging/helpers.rs +17 -13
- package/crates/team-agent/src/messaging/leader_receiver.rs +60 -35
- package/crates/team-agent/src/messaging/mod.rs +11 -2
- package/crates/team-agent/src/messaging/persist.rs +309 -0
- package/crates/team-agent/src/messaging/results.rs +16 -24
- package/crates/team-agent/src/messaging/scheduler.rs +4 -2
- package/crates/team-agent/src/messaging/selftest.rs +19 -12
- package/crates/team-agent/src/messaging/send.rs +133 -58
- package/crates/team-agent/src/messaging/tests/leader_inject_acceptance.rs +305 -0
- package/crates/team-agent/src/messaging/tests/mod.rs +1 -0
- package/crates/team-agent/src/messaging/tests/runtime.rs +38 -17
- package/crates/team-agent/src/messaging/watchers.rs +13 -3
- package/crates/team-agent/src/redaction.rs +72 -2
- package/crates/team-agent/src/state/persist.rs +2 -1
- package/crates/team-agent/src/state/repository/tests.rs +47 -0
- package/crates/team-agent/src/state/repository.rs +59 -16
- package/package.json +4 -4
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
use std::collections::{BTreeMap, BTreeSet};
|
|
2
|
+
use std::path::{Path, PathBuf};
|
|
3
|
+
use std::process::Command;
|
|
4
|
+
|
|
5
|
+
use crate::lifecycle::*;
|
|
6
|
+
use crate::model::enums::{AuthMode, DisplayBackend, PaneLiveness, Provider, ProviderEffort};
|
|
7
|
+
use crate::model::ids::AgentId;
|
|
8
|
+
use crate::model::permissions::{self, AgentPermissionInput};
|
|
9
|
+
use crate::model::yaml::{self, Value};
|
|
10
|
+
use crate::state::persist::load_runtime_state;
|
|
11
|
+
use crate::transport::{PaneId, SessionName, Target, Transport, WindowName};
|
|
12
|
+
|
|
13
|
+
use crate::lifecycle::lock::{acquire_agent_lifecycle_lock, LifecycleLockRequest};
|
|
14
|
+
|
|
15
|
+
use super::*;
|
|
16
|
+
|
|
17
|
+
pub(crate) fn ensure_owner_allowed(workspace: &Path) -> Result<(), LifecycleError> {
|
|
18
|
+
let state = crate::state::persist::load_runtime_state(workspace)
|
|
19
|
+
.map_err(|e| LifecycleError::StatePersist(e.to_string()))?;
|
|
20
|
+
ensure_owner_allowed_for_state(&state, None)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
pub(crate) fn ensure_owner_allowed_for_state(
|
|
24
|
+
state: &serde_json::Value,
|
|
25
|
+
target_role: Option<&AgentId>,
|
|
26
|
+
) -> Result<(), LifecycleError> {
|
|
27
|
+
struct NoopLiveness;
|
|
28
|
+
impl crate::state::owner_gate::PaneLivenessProbe for NoopLiveness {
|
|
29
|
+
fn liveness(&self, _pane_id: &str) -> crate::model::enums::PaneLiveness {
|
|
30
|
+
crate::model::enums::PaneLiveness::Live
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
let target_team = crate::state::projection::team_state_key(state);
|
|
35
|
+
if caller_is_target_role_in_team(&target_team, target_role) {
|
|
36
|
+
return Ok(());
|
|
37
|
+
}
|
|
38
|
+
let caller = crate::state::identity::caller_identity_from_env(
|
|
39
|
+
Some(state),
|
|
40
|
+
&crate::state::identity::SystemEnv,
|
|
41
|
+
Some(&target_team),
|
|
42
|
+
None,
|
|
43
|
+
)
|
|
44
|
+
.map_err(|e| LifecycleError::StatePersist(e.to_string()))?;
|
|
45
|
+
if let Some(refusal) =
|
|
46
|
+
crate::state::owner_gate::check_team_owner(state, &caller, false, &NoopLiveness)
|
|
47
|
+
{
|
|
48
|
+
return Err(LifecycleError::OwnerRefused(refusal.to_string()));
|
|
49
|
+
}
|
|
50
|
+
Ok(())
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
pub(super) fn caller_is_target_role_in_team(
|
|
54
|
+
target_team: &str,
|
|
55
|
+
target_role: Option<&AgentId>,
|
|
56
|
+
) -> bool {
|
|
57
|
+
let Some(target_role) = target_role else {
|
|
58
|
+
return false;
|
|
59
|
+
};
|
|
60
|
+
std::env::var("TEAM_AGENT_ID").ok().as_deref() == Some(target_role.as_str())
|
|
61
|
+
&& std::env::var("TEAM_AGENT_TEAM_ID").ok().as_deref() == Some(target_team)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
pub(crate) fn state_path(workspace: &Path) -> std::path::PathBuf {
|
|
65
|
+
crate::state::persist::runtime_state_path(workspace)
|
|
66
|
+
}
|
|
@@ -0,0 +1,477 @@
|
|
|
1
|
+
use std::collections::{BTreeMap, BTreeSet};
|
|
2
|
+
use std::path::{Path, PathBuf};
|
|
3
|
+
use std::process::Command;
|
|
4
|
+
|
|
5
|
+
use crate::lifecycle::*;
|
|
6
|
+
use crate::model::enums::{AuthMode, DisplayBackend, PaneLiveness, Provider, ProviderEffort};
|
|
7
|
+
use crate::model::ids::AgentId;
|
|
8
|
+
use crate::model::permissions::{self, AgentPermissionInput};
|
|
9
|
+
use crate::model::yaml::{self, Value};
|
|
10
|
+
use crate::state::persist::load_runtime_state;
|
|
11
|
+
use crate::transport::{PaneId, SessionName, Target, Transport, WindowName};
|
|
12
|
+
|
|
13
|
+
use crate::lifecycle::lock::{acquire_agent_lifecycle_lock, LifecycleLockRequest};
|
|
14
|
+
|
|
15
|
+
use super::*;
|
|
16
|
+
|
|
17
|
+
/// `quick_start(agents_dir, name, yes, team_id)`(`diagnose/quick_start.py:18`)。
|
|
18
|
+
/// 面向用户的零配置入口:编译 team_dir → `launch` → autobind leader receiver → 起
|
|
19
|
+
/// coordinator → `wait_ready` 轮询就绪。归入 lifecycle module(不与 diagnose 混)。
|
|
20
|
+
pub fn quick_start(
|
|
21
|
+
agents_dir: &Path,
|
|
22
|
+
name: Option<&str>,
|
|
23
|
+
yes: bool,
|
|
24
|
+
team_id: Option<&str>,
|
|
25
|
+
) -> Result<QuickStartReport, LifecycleError> {
|
|
26
|
+
let workspace = team_workspace(agents_dir);
|
|
27
|
+
quick_start_in_workspace(&workspace, agents_dir, name, yes, team_id)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
pub fn quick_start_in_workspace(
|
|
31
|
+
workspace: &Path,
|
|
32
|
+
agents_dir: &Path,
|
|
33
|
+
name: Option<&str>,
|
|
34
|
+
yes: bool,
|
|
35
|
+
team_id: Option<&str>,
|
|
36
|
+
) -> Result<QuickStartReport, LifecycleError> {
|
|
37
|
+
let workspace = explicit_quick_start_workspace(workspace);
|
|
38
|
+
let transport = quick_start_tmux_backend(&workspace);
|
|
39
|
+
quick_start_with_transport_in_workspace(&workspace, agents_dir, name, yes, team_id, &transport)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
pub fn quick_start_in_workspace_with_display(
|
|
43
|
+
workspace: &Path,
|
|
44
|
+
agents_dir: &Path,
|
|
45
|
+
name: Option<&str>,
|
|
46
|
+
yes: bool,
|
|
47
|
+
team_id: Option<&str>,
|
|
48
|
+
open_display: bool,
|
|
49
|
+
) -> Result<QuickStartReport, LifecycleError> {
|
|
50
|
+
// Legacy entrypoint: no `--backend` override. Preserves the exact
|
|
51
|
+
// tmux behavior we shipped in Phase 1c (byte-equivalent tmux path
|
|
52
|
+
// when no explicit backend is asked for).
|
|
53
|
+
quick_start_in_workspace_with_display_and_backend(
|
|
54
|
+
workspace,
|
|
55
|
+
agents_dir,
|
|
56
|
+
name,
|
|
57
|
+
yes,
|
|
58
|
+
team_id,
|
|
59
|
+
open_display,
|
|
60
|
+
None,
|
|
61
|
+
)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/// 0.5.x Phase 1d Batch 2: quick-start with an optional
|
|
65
|
+
/// `--backend <tmux|conpty>` override. When `backend` is `None` or
|
|
66
|
+
/// `Some("tmux")` the transport is the same one the legacy entrypoint
|
|
67
|
+
/// built (byte-equivalent to Phase 1c), so tmux users see no
|
|
68
|
+
/// behavioral change.
|
|
69
|
+
///
|
|
70
|
+
/// When `backend` is `Some("conpty")`, this call routes through the
|
|
71
|
+
/// factory. On a host without a live shim client (i.e. every
|
|
72
|
+
/// non-Windows host today), the resulting `ConPtyBackend` degrades
|
|
73
|
+
/// its spawn/inject/capture calls to `TransportError::MuxUnavailable`
|
|
74
|
+
/// honestly — MUST-NOT-13 + CR C-1 ①. Users see a real "conpty
|
|
75
|
+
/// unavailable" error rather than a silent tmux fallback.
|
|
76
|
+
pub fn quick_start_in_workspace_with_display_and_backend(
|
|
77
|
+
workspace: &Path,
|
|
78
|
+
agents_dir: &Path,
|
|
79
|
+
name: Option<&str>,
|
|
80
|
+
yes: bool,
|
|
81
|
+
team_id: Option<&str>,
|
|
82
|
+
open_display: bool,
|
|
83
|
+
backend: Option<&str>,
|
|
84
|
+
) -> Result<QuickStartReport, LifecycleError> {
|
|
85
|
+
let workspace = explicit_quick_start_workspace(workspace);
|
|
86
|
+
// Default / `tmux` literal: preserve the existing tmux path
|
|
87
|
+
// BYTE-FOR-BYTE. This is the CR §Batch 2 Verification anchor:
|
|
88
|
+
// `quick-start` without `--backend` produces byte-equivalent tmux
|
|
89
|
+
// behavior.
|
|
90
|
+
let literal = backend.map(str::trim);
|
|
91
|
+
let is_tmux_or_default = matches!(literal, None | Some("") | Some("tmux") | Some("TMUX"));
|
|
92
|
+
if is_tmux_or_default {
|
|
93
|
+
let transport = quick_start_tmux_backend(&workspace);
|
|
94
|
+
return quick_start_with_transport_in_workspace_with_display(
|
|
95
|
+
&workspace,
|
|
96
|
+
agents_dir,
|
|
97
|
+
name,
|
|
98
|
+
yes,
|
|
99
|
+
team_id,
|
|
100
|
+
&transport,
|
|
101
|
+
open_display,
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
// Explicit non-tmux backend: route through the factory. Parse the
|
|
105
|
+
// literal, then delegate. On unsupported literals or refused
|
|
106
|
+
// preconditions we return `LifecycleError` — never silently pick
|
|
107
|
+
// tmux (CR C-1 ①/②).
|
|
108
|
+
let requested = crate::transport_factory::RequestedTransportBackend::parse_literal(
|
|
109
|
+
literal.unwrap_or_default(),
|
|
110
|
+
)
|
|
111
|
+
.ok_or_else(|| {
|
|
112
|
+
LifecycleError::TeamSelect(format!(
|
|
113
|
+
"unsupported --backend literal {literal:?}; expected `tmux` or `conpty` \
|
|
114
|
+
(Phase 1d does not auto-map `pty` to conpty — CR C-1 ②)"
|
|
115
|
+
))
|
|
116
|
+
})?;
|
|
117
|
+
let team_key_for_factory = team_id;
|
|
118
|
+
// 0.5.x Windows portability Batch 8 F7 (leader msg_590b4dce0f68):
|
|
119
|
+
// shim ownership moves to the coordinator. quick-start no longer
|
|
120
|
+
// calls `spawn_shim_and_handshake` directly. Instead we ensure
|
|
121
|
+
// the coordinator daemon is running; the coordinator's boot
|
|
122
|
+
// path calls `conpty_shim::ensure_shim_running` (idempotent —
|
|
123
|
+
// spawns if no live shim, reconnects if one is recorded).
|
|
124
|
+
//
|
|
125
|
+
// Rationale (from Batch 7 gate report F7): quick-start is a
|
|
126
|
+
// one-shot process; if it owns the shim, the shim dies when
|
|
127
|
+
// quick-start exits. Moving ownership to the coord daemon
|
|
128
|
+
// gives us the "coord can die, shim survives" invariant the
|
|
129
|
+
// design's §Shim Lifecycle chapter requires.
|
|
130
|
+
//
|
|
131
|
+
// The `ensure_coordinator_running` call is idempotent per
|
|
132
|
+
// `coordinator/health.rs::start_coordinator`. On non-Windows
|
|
133
|
+
// this whole block is cfg'd out.
|
|
134
|
+
#[cfg(windows)]
|
|
135
|
+
if matches!(
|
|
136
|
+
requested,
|
|
137
|
+
crate::transport_factory::RequestedTransportBackend::ConPty
|
|
138
|
+
) {
|
|
139
|
+
let team_key_str = team_key_for_factory.ok_or_else(|| {
|
|
140
|
+
LifecycleError::TeamSelect(
|
|
141
|
+
"team_key required for --backend conpty on Windows (Batch 9 F8)".to_string(),
|
|
142
|
+
)
|
|
143
|
+
})?;
|
|
144
|
+
// 0.5.x Windows portability Batch 9 F8 (leader msg_2a4cc1fa54c0):
|
|
145
|
+
// Batch 8's seed-state pattern (writing active_team_key +
|
|
146
|
+
// transport.kind to state.json before start_coordinator)
|
|
147
|
+
// caused downstream launch code to see "existing runtime, use
|
|
148
|
+
// restart" and skip spec compile. F8 fix: pass `--team`
|
|
149
|
+
// directly to the coord daemon via `start_coordinator_with_team`
|
|
150
|
+
// so state doesn't need pre-seeding.
|
|
151
|
+
//
|
|
152
|
+
// The coord daemon's `run_daemon_with_coordinator_and_boot_tmux`
|
|
153
|
+
// then calls `ensure_shim_running` with the CLI-supplied
|
|
154
|
+
// team_key. When quick-start's downstream code runs, state.json
|
|
155
|
+
// still has whatever it had before (empty on fresh launch), so
|
|
156
|
+
// the spec-compile + worker-spawn path runs normally.
|
|
157
|
+
let run_ws = crate::coordinator::WorkspacePath::new(workspace.clone());
|
|
158
|
+
let start_report =
|
|
159
|
+
crate::coordinator::health::start_coordinator_with_team(&run_ws, Some(team_key_str))
|
|
160
|
+
.map_err(|e| LifecycleError::TeamSelect(format!("coordinator start: {e}")))?;
|
|
161
|
+
if !start_report.ok {
|
|
162
|
+
return Err(LifecycleError::TeamSelect(format!(
|
|
163
|
+
"coordinator start failed: schema_error={:?}, action={:?}",
|
|
164
|
+
start_report.schema_error, start_report.action
|
|
165
|
+
)));
|
|
166
|
+
}
|
|
167
|
+
// Give the coordinator a beat to write its transport.shim
|
|
168
|
+
// block so the factory's `pipe_ready` gate opens on the
|
|
169
|
+
// next resolve. The coordinator's `run_daemon` calls
|
|
170
|
+
// `ensure_shim_running` inside its boot code path (see
|
|
171
|
+
// `coordinator::backoff::run_daemon`).
|
|
172
|
+
std::thread::sleep(std::time::Duration::from_millis(2500));
|
|
173
|
+
}
|
|
174
|
+
let input = crate::transport_factory::TransportFactoryInput::new(
|
|
175
|
+
&workspace,
|
|
176
|
+
crate::transport_factory::TransportPurpose::Launch,
|
|
177
|
+
)
|
|
178
|
+
.with_team_key(team_key_for_factory)
|
|
179
|
+
.with_explicit_backend(Some(requested));
|
|
180
|
+
// On Windows the factory needs to SEE the freshly-persisted
|
|
181
|
+
// `state.transport.shim.pipe_ready = true` marker so its
|
|
182
|
+
// `conpty_pipe_ready` gate opens.
|
|
183
|
+
#[cfg(windows)]
|
|
184
|
+
let state_value = crate::state::repository::StateRepository::new(&workspace)
|
|
185
|
+
.load_workspace_if_exists_without_migrations()
|
|
186
|
+
.ok()
|
|
187
|
+
.flatten();
|
|
188
|
+
#[cfg(windows)]
|
|
189
|
+
let input = match state_value.as_ref() {
|
|
190
|
+
Some(v) => input.with_state(Some(v)),
|
|
191
|
+
None => input,
|
|
192
|
+
};
|
|
193
|
+
let resolved = crate::transport_factory::resolve_transport(input)
|
|
194
|
+
.map_err(|e| LifecycleError::TeamSelect(e.to_string()))?;
|
|
195
|
+
// Hand the boxed backend down as `&dyn Transport`.
|
|
196
|
+
quick_start_with_transport_in_workspace_with_display(
|
|
197
|
+
&workspace,
|
|
198
|
+
agents_dir,
|
|
199
|
+
name,
|
|
200
|
+
yes,
|
|
201
|
+
team_id,
|
|
202
|
+
&*resolved.backend,
|
|
203
|
+
open_display,
|
|
204
|
+
)
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
pub fn quick_start_with_transport(
|
|
208
|
+
agents_dir: &Path,
|
|
209
|
+
name: Option<&str>,
|
|
210
|
+
yes: bool,
|
|
211
|
+
team_id: Option<&str>,
|
|
212
|
+
transport: &dyn Transport,
|
|
213
|
+
) -> Result<QuickStartReport, LifecycleError> {
|
|
214
|
+
let workspace = team_workspace(agents_dir);
|
|
215
|
+
quick_start_with_transport_in_workspace(&workspace, agents_dir, name, yes, team_id, transport)
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
pub fn quick_start_with_transport_in_workspace(
|
|
219
|
+
workspace: &Path,
|
|
220
|
+
agents_dir: &Path,
|
|
221
|
+
name: Option<&str>,
|
|
222
|
+
yes: bool,
|
|
223
|
+
team_id: Option<&str>,
|
|
224
|
+
transport: &dyn Transport,
|
|
225
|
+
) -> Result<QuickStartReport, LifecycleError> {
|
|
226
|
+
quick_start_with_transport_in_workspace_with_display(
|
|
227
|
+
workspace, agents_dir, name, yes, team_id, transport, true,
|
|
228
|
+
)
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
pub fn quick_start_with_transport_in_workspace_with_display(
|
|
232
|
+
workspace: &Path,
|
|
233
|
+
agents_dir: &Path,
|
|
234
|
+
name: Option<&str>,
|
|
235
|
+
yes: bool,
|
|
236
|
+
team_id: Option<&str>,
|
|
237
|
+
transport: &dyn Transport,
|
|
238
|
+
open_display: bool,
|
|
239
|
+
) -> Result<QuickStartReport, LifecycleError> {
|
|
240
|
+
// B-7 / 036b N38 三行 fail-fast — TEAM_AGENT_LEADER_PANE_ID 主动路径在 quick-start
|
|
241
|
+
// 入口验活;死/缺(Dead)的 pane 必须明确报错,不可 silent bind 到 spawner /
|
|
242
|
+
// owner_bind / lease / display 任一消费点。被动路径(display/seed 等)各自走
|
|
243
|
+
// 降级+event,不在这里挡。错误三行式:error(含 pane id 字面)/action(unset
|
|
244
|
+
// 或修 env)/log(env var 名)。
|
|
245
|
+
let team_workspace = team_workspace(agents_dir);
|
|
246
|
+
let warning_workspaces = [workspace, team_workspace.as_path()];
|
|
247
|
+
validate_active_leader_pane_env_with_workspaces(transport, &warning_workspaces)?;
|
|
248
|
+
if !agents_dir.exists() {
|
|
249
|
+
return Err(LifecycleError::Compile(format!(
|
|
250
|
+
"agents dir not found: {}",
|
|
251
|
+
agents_dir.display()
|
|
252
|
+
)));
|
|
253
|
+
}
|
|
254
|
+
let workspace = workspace.to_path_buf();
|
|
255
|
+
let mut spec = crate::compiler::compile_team(agents_dir)
|
|
256
|
+
.map_err(|e| LifecycleError::Compile(e.to_string()))?;
|
|
257
|
+
override_spec_workspace(&mut spec, &workspace);
|
|
258
|
+
if !open_display {
|
|
259
|
+
override_spec_display_backend(&mut spec, "none");
|
|
260
|
+
}
|
|
261
|
+
let explicit_team_key = quick_start_requested_team_key(team_id, name).map(str::to_string);
|
|
262
|
+
let canonical_team_key = explicit_team_key
|
|
263
|
+
.clone()
|
|
264
|
+
.or_else(|| spec_team_id(&spec).filter(|team| !team.is_empty()))
|
|
265
|
+
.unwrap_or_else(|| {
|
|
266
|
+
runtime_team_key_for_spec(
|
|
267
|
+
&agents_dir.join("team.spec.yaml"),
|
|
268
|
+
&spec,
|
|
269
|
+
&spec_session_name(&spec),
|
|
270
|
+
)
|
|
271
|
+
});
|
|
272
|
+
let requested_team = Some(canonical_team_key.clone());
|
|
273
|
+
let team_depth = quick_start_depth_guard(
|
|
274
|
+
&workspace,
|
|
275
|
+
agents_dir,
|
|
276
|
+
requested_team.as_deref(),
|
|
277
|
+
matches!(transport.kind(), crate::transport::BackendKind::Tmux),
|
|
278
|
+
)?;
|
|
279
|
+
if team_depth.team_depth > 2 {
|
|
280
|
+
let parent = team_depth.parent_team_key.as_deref().unwrap_or("");
|
|
281
|
+
return Err(LifecycleError::RequirementUnmet(format!(
|
|
282
|
+
"team nesting depth limit exceeded: parent_team_key={parent} parent_depth={} max_depth=2",
|
|
283
|
+
team_depth.team_depth.saturating_sub(1)
|
|
284
|
+
)));
|
|
285
|
+
}
|
|
286
|
+
let state_path = crate::state::persist::runtime_state_path(&workspace);
|
|
287
|
+
if state_path.exists() {
|
|
288
|
+
let state = crate::state::persist::load_runtime_state(&workspace)
|
|
289
|
+
.map_err(|e| LifecycleError::StatePersist(e.to_string()))?;
|
|
290
|
+
if requested_team
|
|
291
|
+
.as_deref()
|
|
292
|
+
.is_none_or(|team| runtime_state_has_quick_start_team(&state, team))
|
|
293
|
+
{
|
|
294
|
+
let session_name = state
|
|
295
|
+
.get("session_name")
|
|
296
|
+
.and_then(serde_json::Value::as_str)
|
|
297
|
+
.filter(|s| !s.is_empty())
|
|
298
|
+
.map(SessionName::new);
|
|
299
|
+
let attach_commands = session_name
|
|
300
|
+
.as_ref()
|
|
301
|
+
.map(|session| {
|
|
302
|
+
let windows = quick_start_attach_window_names(&state);
|
|
303
|
+
attach_commands_for_runtime_windows(
|
|
304
|
+
state
|
|
305
|
+
.get("tmux_endpoint")
|
|
306
|
+
.and_then(serde_json::Value::as_str)
|
|
307
|
+
.or_else(|| {
|
|
308
|
+
state.get("tmux_socket").and_then(serde_json::Value::as_str)
|
|
309
|
+
}),
|
|
310
|
+
&workspace,
|
|
311
|
+
session,
|
|
312
|
+
windows.iter().map(String::as_str),
|
|
313
|
+
)
|
|
314
|
+
})
|
|
315
|
+
.unwrap_or_default();
|
|
316
|
+
// Stage QR (design doc .team/artifacts/quickstart-restart-separation-design.md):
|
|
317
|
+
// quick-start is initial-creation-only. When the team
|
|
318
|
+
// already has runtime state, do NOT mention `--fresh`
|
|
319
|
+
// (it's been removed); steer the operator to the
|
|
320
|
+
// restart flow which owns resume + reset semantics.
|
|
321
|
+
let mut next_actions = vec![
|
|
322
|
+
"this team already has runtime state — use `team-agent restart` \
|
|
323
|
+
to resume it (quick-start is for first-time creation only). \
|
|
324
|
+
If recovery is impossible and the operator EXPLICITLY \
|
|
325
|
+
accepts losing context, restart accepts `--allow-fresh`."
|
|
326
|
+
.to_string(),
|
|
327
|
+
];
|
|
328
|
+
if session_name.is_some() {
|
|
329
|
+
if crate::tmux_backend::socket_probe_missing_for_workspace(&workspace) {
|
|
330
|
+
next_actions.push(crate::tmux_backend::socket_missing_hint_for_workspace(
|
|
331
|
+
&workspace,
|
|
332
|
+
));
|
|
333
|
+
}
|
|
334
|
+
next_actions.extend(attach_commands.iter().cloned());
|
|
335
|
+
}
|
|
336
|
+
return Ok(QuickStartReport::ExistingRuntime {
|
|
337
|
+
team: requested_team.clone(),
|
|
338
|
+
session_name,
|
|
339
|
+
state_path: Some(state_path),
|
|
340
|
+
next_actions,
|
|
341
|
+
attach_commands,
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
// CR-040/042: repeated quick-start from one template with distinct --team-id/--name
|
|
346
|
+
// must NOT collide on the template-derived tmux session. Override the compiled
|
|
347
|
+
// spec's runtime.session_name with one derived from the REQUESTED team identity
|
|
348
|
+
// so launch_with_transport (which reads runtime.session_name) spawns into an
|
|
349
|
+
// isolated session per requested team.
|
|
350
|
+
if let Some(requested) = explicit_team_key.as_deref() {
|
|
351
|
+
override_spec_session_name(&mut spec, &format!("team-{requested}"));
|
|
352
|
+
}
|
|
353
|
+
let session_name = spec_session_name(&spec);
|
|
354
|
+
// team_key 已在入口由显式 id/name 或 compiled spec identity 单次确定。
|
|
355
|
+
let state_team_key = canonical_team_key;
|
|
356
|
+
warn_ignored_owner_team_id(workspace.as_path(), agents_dir, &state_team_key);
|
|
357
|
+
// E5 spec 迁移:spec 写到 .team/runtime/<team_key>/(中间产物,绝不落用户目录 agents_dir)。
|
|
358
|
+
// Bug2:原子写(tmp+rename),避免半截 spec。
|
|
359
|
+
let spec_path = crate::model::paths::runtime_spec_path(&workspace, &state_team_key);
|
|
360
|
+
write_spec_atomic(&spec_path, &spec)?;
|
|
361
|
+
let _store = crate::message_store::MessageStore::open(&workspace)
|
|
362
|
+
.map_err(|e| LifecycleError::StatePersist(e.to_string()))?;
|
|
363
|
+
let resolved_spec_path =
|
|
364
|
+
std::fs::canonicalize(&spec_path).unwrap_or_else(|_| spec_path.clone());
|
|
365
|
+
let mut state = initial_runtime_state(
|
|
366
|
+
&spec,
|
|
367
|
+
&resolved_spec_path,
|
|
368
|
+
&workspace,
|
|
369
|
+
agents_dir,
|
|
370
|
+
&state_team_key,
|
|
371
|
+
);
|
|
372
|
+
// 0.5.x Phase 1d hot-path 接线(裁决1 msg_76e1d98202b8): use the
|
|
373
|
+
// generic annotator that writes `state.transport = { kind, source }`
|
|
374
|
+
// for every backend AND (for tmux) preserves the existing
|
|
375
|
+
// `tmux_endpoint`/`tmux_socket`/`tmux_socket_source` fields via the
|
|
376
|
+
// inner `annotate_runtime_tmux_endpoint` call. Source is threaded
|
|
377
|
+
// from the caller-side factory selection when available; the
|
|
378
|
+
// legacy quick-start entrypoint currently passes `None` (defaults
|
|
379
|
+
// to "unknown" in the payload) — Phase 2 refactor will thread
|
|
380
|
+
// ResolvedTransport.source through the launch signature.
|
|
381
|
+
annotate_runtime_transport(&mut state, transport, &workspace, None);
|
|
382
|
+
save_launched_team_state_for_key(&workspace, &state, Some(&state_team_key))?;
|
|
383
|
+
annotate_persisted_team_depth(
|
|
384
|
+
&workspace,
|
|
385
|
+
&state_team_key,
|
|
386
|
+
team_depth.parent_team_key.as_deref(),
|
|
387
|
+
team_depth.team_depth,
|
|
388
|
+
)?;
|
|
389
|
+
// FIX (rt-host-a real-machine finding): dry_run=false so launch_with_transport calls spawn_agents
|
|
390
|
+
// and really creates the tmux session + worker windows (was hardcoded true → never spawned, which
|
|
391
|
+
// also starved the coordinator: no session → first tick TmuxSessionMissing → run_daemon loop exits).
|
|
392
|
+
// 0.5.38 (`.team/artifacts/startup-latency-locate.md` §5): quick-start
|
|
393
|
+
// owns the outer `launch.phase` timer so `coordinator_start` /
|
|
394
|
+
// `readiness_wait` / `completed` fire monotonically after the inner
|
|
395
|
+
// launch_with_transport's own `compile_spec` / `spawn_all` events.
|
|
396
|
+
let quick_start_phase_timer = crate::lifecycle::restart::RestartPhaseTimer::start();
|
|
397
|
+
let mut launch =
|
|
398
|
+
launch_with_transport_in_workspace(&workspace, &spec_path, false, yes, true, transport)?;
|
|
399
|
+
annotate_persisted_team_depth(
|
|
400
|
+
&workspace,
|
|
401
|
+
&state_team_key,
|
|
402
|
+
team_depth.parent_team_key.as_deref(),
|
|
403
|
+
team_depth.team_depth,
|
|
404
|
+
)?;
|
|
405
|
+
launch.leader_receiver_attached =
|
|
406
|
+
launched_team_receiver_is_attached(&workspace, &state_team_key);
|
|
407
|
+
launch.session_capture_incomplete_agents =
|
|
408
|
+
quick_start_session_capture_incomplete_agents(&workspace, &state_team_key);
|
|
409
|
+
let coordinator_workspace = crate::coordinator::WorkspacePath::new(workspace.clone());
|
|
410
|
+
let coordinator_started = crate::coordinator::start_coordinator(&coordinator_workspace)
|
|
411
|
+
.map(|report| report.ok)
|
|
412
|
+
.map_err(|e| LifecycleError::StatePersist(e.to_string()))?;
|
|
413
|
+
quick_start_phase_timer.emit(&workspace, "launch.phase", "coordinator_start");
|
|
414
|
+
let coordinator_action = if coordinator_started {
|
|
415
|
+
"coordinator started"
|
|
416
|
+
} else {
|
|
417
|
+
"coordinator not started"
|
|
418
|
+
};
|
|
419
|
+
// BUG-7: build an honest readiness verdict from the post-spawn runtime state.
|
|
420
|
+
// - If persist_spawn_agent_state (BUG-2 fix) marked any agent non-running, the
|
|
421
|
+
// team is observably Degraded.
|
|
422
|
+
// - Otherwise the framework cannot itself verify that the worker's MCP tool set
|
|
423
|
+
// loaded successfully (provider-side codex/claude schema rejections happen
|
|
424
|
+
// asynchronously after spawn), so the verdict is PendingToolLoad — never
|
|
425
|
+
// bare Ready.
|
|
426
|
+
quick_start_phase_timer.emit(&workspace, "launch.phase", "readiness_wait");
|
|
427
|
+
let worker_readiness = quick_start_worker_readiness(&workspace, &state_team_key);
|
|
428
|
+
let attach_windows = load_runtime_state(&workspace)
|
|
429
|
+
.ok()
|
|
430
|
+
.map(|state| {
|
|
431
|
+
attach_window_names_with_managed_leader(
|
|
432
|
+
&state,
|
|
433
|
+
started_attach_window_names(&launch.started),
|
|
434
|
+
)
|
|
435
|
+
})
|
|
436
|
+
.unwrap_or_else(|| started_attach_window_names(&launch.started));
|
|
437
|
+
let attach_commands = attach_commands_for_runtime_windows(
|
|
438
|
+
launch.tmux_endpoint.as_deref(),
|
|
439
|
+
&workspace,
|
|
440
|
+
&session_name,
|
|
441
|
+
attach_windows.iter().map(String::as_str),
|
|
442
|
+
);
|
|
443
|
+
let mut next_actions = vec![format!(
|
|
444
|
+
"team compiled; real spawn is behind the transport/provider boundary; {coordinator_action}"
|
|
445
|
+
)];
|
|
446
|
+
// Stage QR (design doc
|
|
447
|
+
// .team/artifacts/quickstart-restart-separation-design.md):
|
|
448
|
+
// quick-start is initial-creation-only. After success, remind the
|
|
449
|
+
// operator that subsequent starts must go through restart so the
|
|
450
|
+
// user's context (sessions, ownership, tasks) survives.
|
|
451
|
+
next_actions.push(
|
|
452
|
+
"quick-start initialized this team; for all subsequent starts use \
|
|
453
|
+
`team-agent restart` to resume context (quick-start refuses on \
|
|
454
|
+
already-initialised teams)"
|
|
455
|
+
.to_string(),
|
|
456
|
+
);
|
|
457
|
+
if crate::tmux_backend::socket_probe_missing_for_workspace(&workspace) {
|
|
458
|
+
next_actions.push(crate::tmux_backend::socket_missing_hint_for_workspace(
|
|
459
|
+
&workspace,
|
|
460
|
+
));
|
|
461
|
+
}
|
|
462
|
+
next_actions.extend(attach_commands.iter().cloned());
|
|
463
|
+
let display_backend = state
|
|
464
|
+
.get("display_backend")
|
|
465
|
+
.and_then(serde_json::Value::as_str)
|
|
466
|
+
.unwrap_or("none")
|
|
467
|
+
.to_string();
|
|
468
|
+
quick_start_phase_timer.emit(&workspace, "launch.phase", "completed");
|
|
469
|
+
Ok(QuickStartReport::Ready {
|
|
470
|
+
session_name,
|
|
471
|
+
launch: Box::new(launch),
|
|
472
|
+
next_actions,
|
|
473
|
+
attach_commands,
|
|
474
|
+
display_backend,
|
|
475
|
+
worker_readiness,
|
|
476
|
+
})
|
|
477
|
+
}
|