@team-agent/installer 0.5.50 → 0.5.52

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.
Files changed (93) hide show
  1. package/Cargo.lock +1 -1
  2. package/Cargo.toml +1 -1
  3. package/crates/team-agent/src/cli/adapters.rs +5 -3
  4. package/crates/team-agent/src/cli/emit.rs +2 -1
  5. package/crates/team-agent/src/cli/mod.rs +32 -14
  6. package/crates/team-agent/src/cli/named_address.rs +46 -115
  7. package/crates/team-agent/src/cli/send/coordinator.rs +163 -0
  8. package/crates/team-agent/src/cli/send/mailbox.rs +99 -0
  9. package/crates/team-agent/src/cli/send/persist.rs +154 -0
  10. package/crates/team-agent/src/cli/send/presentation.rs +333 -0
  11. package/crates/team-agent/src/cli/send/resolve.rs +361 -0
  12. package/crates/team-agent/src/cli/send.rs +44 -1169
  13. package/crates/team-agent/src/cli/spec.rs +1 -1
  14. package/crates/team-agent/src/cli/status_port/agents.rs +358 -0
  15. package/crates/team-agent/src/cli/status_port/approvals.rs +79 -0
  16. package/crates/team-agent/src/cli/status_port/compact.rs +207 -0
  17. package/crates/team-agent/src/cli/status_port/format.rs +145 -0
  18. package/crates/team-agent/src/cli/status_port/inbox.rs +36 -0
  19. package/crates/team-agent/src/cli/status_port/runtime.rs +195 -0
  20. package/crates/team-agent/src/cli/status_port/snapshot.rs +181 -0
  21. package/crates/team-agent/src/cli/status_port/store.rs +412 -0
  22. package/crates/team-agent/src/cli/status_port/tests.rs +54 -0
  23. package/crates/team-agent/src/cli/status_port.rs +47 -1548
  24. package/crates/team-agent/src/cli/tests/main_preserved.rs +1 -0
  25. package/crates/team-agent/src/cli/tests/run_delegation.rs +1 -0
  26. package/crates/team-agent/src/cli/types.rs +1 -0
  27. package/crates/team-agent/src/coordinator/conpty_shim.rs +34 -30
  28. package/crates/team-agent/src/coordinator/steps/abnormal.rs +135 -13
  29. package/crates/team-agent/src/coordinator/tick.rs +37 -0
  30. package/crates/team-agent/src/db/agent_health_capture.rs +18 -13
  31. package/crates/team-agent/src/db/message_store.rs +154 -44
  32. package/crates/team-agent/src/event_log.rs +73 -0
  33. package/crates/team-agent/src/leader/lease.rs +202 -16
  34. package/crates/team-agent/src/leader/start.rs +28 -4
  35. package/crates/team-agent/src/lifecycle/launch/add_agent.rs +424 -0
  36. package/crates/team-agent/src/lifecycle/launch/add_agent_state.rs +303 -0
  37. package/crates/team-agent/src/lifecycle/launch/agent_state.rs +160 -0
  38. package/crates/team-agent/src/lifecycle/launch/approval.rs +134 -0
  39. package/crates/team-agent/src/lifecycle/launch/fork_agent.rs +492 -0
  40. package/crates/team-agent/src/lifecycle/launch/fork_state.rs +297 -0
  41. package/crates/team-agent/src/lifecycle/launch/identity.rs +372 -0
  42. package/crates/team-agent/src/lifecycle/launch/layout.rs +313 -0
  43. package/crates/team-agent/src/lifecycle/launch/leader_context.rs +478 -0
  44. package/crates/team-agent/src/lifecycle/launch/mcp_config.rs +201 -0
  45. package/crates/team-agent/src/lifecycle/launch/ownership.rs +66 -0
  46. package/crates/team-agent/src/lifecycle/launch/quick_start.rs +477 -0
  47. package/crates/team-agent/src/lifecycle/launch/quick_start_transport.rs +278 -0
  48. package/crates/team-agent/src/lifecycle/launch/readiness.rs +123 -0
  49. package/crates/team-agent/src/lifecycle/launch/spawn.rs +377 -0
  50. package/crates/team-agent/src/lifecycle/launch/spec_state.rs +434 -0
  51. package/crates/team-agent/src/lifecycle/launch/state_projection.rs +499 -0
  52. package/crates/team-agent/src/lifecycle/launch/worker_env.rs +438 -0
  53. package/crates/team-agent/src/lifecycle/launch.rs +119 -5351
  54. package/crates/team-agent/src/lifecycle/restart/agent.rs +63 -29
  55. package/crates/team-agent/src/lifecycle/restart/common.rs +53 -27
  56. package/crates/team-agent/src/lifecycle/restart/rebuild.rs +167 -27
  57. package/crates/team-agent/src/lifecycle/restart/remove.rs +503 -77
  58. package/crates/team-agent/src/lifecycle/restart.rs +26 -2
  59. package/crates/team-agent/src/lifecycle/tests/lane_ops.rs +906 -17
  60. package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +67 -2
  61. package/crates/team-agent/src/lifecycle/tests/lifecycle_lock.rs +24 -1
  62. package/crates/team-agent/src/mcp_server/lifecycle_tools/agent_ops.rs +30 -7
  63. package/crates/team-agent/src/mcp_server/lifecycle_tools/state_status.rs +8 -4
  64. package/crates/team-agent/src/mcp_server/mod.rs +1 -1
  65. package/crates/team-agent/src/mcp_server/tests/send.rs +6 -10
  66. package/crates/team-agent/src/mcp_server/tests/tools.rs +18 -1
  67. package/crates/team-agent/src/mcp_server/tests.rs +11 -4
  68. package/crates/team-agent/src/mcp_server/tools.rs +14 -5
  69. package/crates/team-agent/src/messaging/activity.rs +4 -2
  70. package/crates/team-agent/src/messaging/address.rs +86 -0
  71. package/crates/team-agent/src/messaging/delivery.rs +288 -183
  72. package/crates/team-agent/src/messaging/helpers.rs +17 -13
  73. package/crates/team-agent/src/messaging/leader_channel.rs +171 -0
  74. package/crates/team-agent/src/messaging/leader_receiver.rs +60 -35
  75. package/crates/team-agent/src/messaging/mod.rs +15 -2
  76. package/crates/team-agent/src/messaging/persist.rs +309 -0
  77. package/crates/team-agent/src/messaging/results.rs +16 -24
  78. package/crates/team-agent/src/messaging/scheduler.rs +4 -2
  79. package/crates/team-agent/src/messaging/selftest.rs +19 -12
  80. package/crates/team-agent/src/messaging/send.rs +101 -49
  81. package/crates/team-agent/src/messaging/tests/leader_channel.rs +169 -0
  82. package/crates/team-agent/src/messaging/tests/leader_inject_acceptance.rs +305 -0
  83. package/crates/team-agent/src/messaging/tests/mod.rs +2 -0
  84. package/crates/team-agent/src/messaging/tests/runtime.rs +38 -17
  85. package/crates/team-agent/src/messaging/watchers.rs +13 -3
  86. package/crates/team-agent/src/redaction.rs +72 -2
  87. package/crates/team-agent/src/state/persist.rs +284 -21
  88. package/crates/team-agent/src/state/projection.rs +245 -30
  89. package/crates/team-agent/src/state/repository/intent.rs +13 -0
  90. package/crates/team-agent/src/state/repository/tests.rs +47 -0
  91. package/crates/team-agent/src/state/repository.rs +94 -27
  92. package/crates/team-agent/src/state/selector.rs +9 -18
  93. 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), None)?;
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
+ }