@team-agent/installer 0.3.1 → 0.3.3

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 (79) hide show
  1. package/Cargo.lock +34 -1
  2. package/Cargo.toml +1 -1
  3. package/crates/team-agent/Cargo.toml +1 -1
  4. package/crates/team-agent/src/cli/adapters.rs +234 -26
  5. package/crates/team-agent/src/cli/diagnose.rs +144 -10
  6. package/crates/team-agent/src/cli/emit.rs +289 -54
  7. package/crates/team-agent/src/cli/leader.rs +37 -8
  8. package/crates/team-agent/src/cli/mod.rs +1281 -196
  9. package/crates/team-agent/src/cli/status_port.rs +195 -46
  10. package/crates/team-agent/src/cli/tests/divergence.rs +1 -2
  11. package/crates/team-agent/src/cli/tests/lane_c.rs +23 -13
  12. package/crates/team-agent/src/cli/tests/main_preserved.rs +2 -0
  13. package/crates/team-agent/src/cli/tests/run_delegation.rs +59 -3
  14. package/crates/team-agent/src/cli/types.rs +18 -0
  15. package/crates/team-agent/src/compiler.rs +15 -5
  16. package/crates/team-agent/src/coordinator/health.rs +95 -17
  17. package/crates/team-agent/src/coordinator/mod.rs +4 -0
  18. package/crates/team-agent/src/coordinator/runtime_detectors.rs +500 -0
  19. package/crates/team-agent/src/coordinator/runtime_observation.rs +58 -0
  20. package/crates/team-agent/src/coordinator/tick.rs +222 -69
  21. package/crates/team-agent/src/coordinator/types.rs +15 -3
  22. package/crates/team-agent/src/db/schema.rs +37 -2
  23. package/crates/team-agent/src/diagnose/comms.rs +226 -0
  24. package/crates/team-agent/src/diagnose/mod.rs +45 -0
  25. package/crates/team-agent/src/diagnose/orphans.rs +658 -0
  26. package/crates/team-agent/src/fake_worker.rs +146 -3
  27. package/crates/team-agent/src/leader/start.rs +121 -23
  28. package/crates/team-agent/src/leader/types.rs +44 -1
  29. package/crates/team-agent/src/lib.rs +3 -0
  30. package/crates/team-agent/src/lifecycle/display.rs +645 -47
  31. package/crates/team-agent/src/lifecycle/launch.rs +1061 -146
  32. package/crates/team-agent/src/lifecycle/mod.rs +2 -0
  33. package/crates/team-agent/src/lifecycle/profile_launch.rs +810 -0
  34. package/crates/team-agent/src/lifecycle/profile_smoke.rs +522 -0
  35. package/crates/team-agent/src/lifecycle/restart/agent.rs +99 -23
  36. package/crates/team-agent/src/lifecycle/restart/common.rs +183 -24
  37. package/crates/team-agent/src/lifecycle/restart/rebuild.rs +498 -22
  38. package/crates/team-agent/src/lifecycle/restart/remove.rs +27 -7
  39. package/crates/team-agent/src/lifecycle/restart/team_state.rs +19 -0
  40. package/crates/team-agent/src/lifecycle/restart.rs +24 -1
  41. package/crates/team-agent/src/lifecycle/tests/lane_ops.rs +5 -5
  42. package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +37 -7
  43. package/crates/team-agent/src/lifecycle/types.rs +19 -0
  44. package/crates/team-agent/src/mcp_server/helpers.rs +1 -0
  45. package/crates/team-agent/src/mcp_server/lifecycle_tools/agent_ops.rs +341 -0
  46. package/crates/team-agent/src/mcp_server/lifecycle_tools/mod.rs +10 -0
  47. package/crates/team-agent/src/mcp_server/lifecycle_tools/state_status.rs +158 -0
  48. package/crates/team-agent/src/mcp_server/mod.rs +3 -74
  49. package/crates/team-agent/src/mcp_server/tests/scoped.rs +1 -1
  50. package/crates/team-agent/src/mcp_server/tests/send.rs +6 -5
  51. package/crates/team-agent/src/mcp_server/tools.rs +312 -111
  52. package/crates/team-agent/src/mcp_server/types.rs +6 -4
  53. package/crates/team-agent/src/mcp_server/wire.rs +19 -7
  54. package/crates/team-agent/src/message_store.rs +21 -4
  55. package/crates/team-agent/src/messaging/delivery.rs +470 -59
  56. package/crates/team-agent/src/messaging/mod.rs +9 -6
  57. package/crates/team-agent/src/messaging/results.rs +353 -63
  58. package/crates/team-agent/src/messaging/selftest.rs +199 -12
  59. package/crates/team-agent/src/messaging/send.rs +35 -3
  60. package/crates/team-agent/src/messaging/tests/runtime.rs +19 -4
  61. package/crates/team-agent/src/messaging/types.rs +11 -3
  62. package/crates/team-agent/src/os_probe.rs +119 -0
  63. package/crates/team-agent/src/packaging/migrate.rs +10 -2
  64. package/crates/team-agent/src/packaging/tests.rs +23 -0
  65. package/crates/team-agent/src/provider/adapter.rs +564 -63
  66. package/crates/team-agent/src/provider/approvals/runtime_prompts.rs +1 -7
  67. package/crates/team-agent/src/provider/classify.rs +51 -4
  68. package/crates/team-agent/src/provider/helpers.rs +10 -1
  69. package/crates/team-agent/src/provider/startup_prompt.rs +94 -0
  70. package/crates/team-agent/src/provider/types.rs +47 -0
  71. package/crates/team-agent/src/session_capture.rs +616 -0
  72. package/crates/team-agent/src/state/persist.rs +170 -1
  73. package/crates/team-agent/src/state/projection.rs +141 -8
  74. package/crates/team-agent/src/state/selector.rs +5 -2
  75. package/crates/team-agent/src/tmux_backend.rs +161 -64
  76. package/crates/team-agent/src/transport/test_support.rs +9 -0
  77. package/crates/team-agent/src/transport/tests/wire.rs +4 -0
  78. package/crates/team-agent/src/transport.rs +13 -2
  79. package/package.json +4 -4
@@ -64,18 +64,19 @@ pub fn run(
64
64
  .and_then(serde_json::Value::as_str)
65
65
  .unwrap_or("unknown");
66
66
  let task_id = payload.get("task_id").and_then(serde_json::Value::as_str);
67
- report_fake_result(workspace, agent_id, message_id, task_id, &mut output)?;
67
+ report_fake_result(workspace, agent_id, message_id, task_id, None, &mut output)?;
68
68
  block = None;
69
69
  } else if let Some(parsed) = parse_rendered_header(trimmed) {
70
70
  block = Some(parsed);
71
71
  } else if let Some(token) = parse_token(trimmed) {
72
72
  if let Some(current) = block.take() {
73
- let _content = current.content();
73
+ let content = current.content();
74
74
  report_fake_result(
75
75
  workspace,
76
76
  agent_id,
77
77
  &token,
78
78
  current.task_id.as_deref(),
79
+ Some(&content),
79
80
  &mut output,
80
81
  )?;
81
82
  }
@@ -126,15 +127,157 @@ fn report_fake_result(
126
127
  agent_id: &str,
127
128
  message_id: &str,
128
129
  task_id: Option<&str>,
130
+ source_content: Option<&str>,
129
131
  output: &mut impl Write,
130
132
  ) -> Result<(), FakeWorkerError> {
131
133
  let envelope = fake_envelope(workspace, agent_id, message_id, task_id);
132
- crate::messaging::report_result(workspace, &envelope)
134
+ let owner_team = std::env::var("TEAM_AGENT_OWNER_TEAM_ID")
135
+ .ok()
136
+ .map(|value| value.trim().to_string())
137
+ .filter(|value| !value.is_empty());
138
+ mirror_fake_result_to_leader(workspace, owner_team.as_deref(), &envelope, source_content);
139
+ crate::messaging::report_result_for_owner_team(workspace, &envelope, owner_team.as_deref())
133
140
  .map_err(|e| FakeWorkerError::Report(e.to_string()))?;
141
+ mirror_fake_result_to_leader(workspace, owner_team.as_deref(), &envelope, source_content);
134
142
  writeln!(output, "{}", serde_json::to_string(&envelope)?)?;
135
143
  Ok(())
136
144
  }
137
145
 
146
+ fn mirror_fake_result_to_leader(
147
+ workspace: &Path,
148
+ owner_team: Option<&str>,
149
+ envelope: &serde_json::Value,
150
+ source_content: Option<&str>,
151
+ ) {
152
+ let Ok(raw_state) = crate::state::persist::load_runtime_state(workspace) else {
153
+ return;
154
+ };
155
+ let state = owner_team
156
+ .and_then(|team| crate::state::projection::resolve_owner_team_id(&raw_state, team).canonical_key().map(str::to_string))
157
+ .map(|team| crate::state::projection::project_top_level_view(&raw_state, &team))
158
+ .unwrap_or(raw_state);
159
+ let attached = state
160
+ .get("leader_receiver")
161
+ .and_then(|receiver| receiver.get("status"))
162
+ .and_then(serde_json::Value::as_str)
163
+ == Some("attached");
164
+ let pane_id = state
165
+ .get("leader_receiver")
166
+ .and_then(|receiver| receiver.get("pane_id"))
167
+ .and_then(serde_json::Value::as_str)
168
+ .filter(|pane| !pane.is_empty())
169
+ .map(str::to_string)
170
+ .or_else(|| owner_team.and_then(|team| claimed_pane_from_events(workspace, team)));
171
+ let Some(pane_id) = pane_id.filter(|_| attached || owner_team.is_some()) else {
172
+ return;
173
+ };
174
+ let summary = envelope
175
+ .get("summary")
176
+ .and_then(serde_json::Value::as_str)
177
+ .unwrap_or("Fake worker completed");
178
+ let result_id = envelope
179
+ .get("result_id")
180
+ .and_then(serde_json::Value::as_str)
181
+ .unwrap_or("");
182
+ let text = match source_content.filter(|content| !content.trim().is_empty()) {
183
+ Some(content) => format!(
184
+ "Fake worker result: {summary}\nMessage content: {}\nResult id: {result_id}",
185
+ content.trim()
186
+ ),
187
+ None => format!("Fake worker result: {summary}\nResult id: {result_id}"),
188
+ };
189
+ let target = crate::transport::Target::Pane(crate::transport::PaneId::new(&pane_id));
190
+ let payload = crate::transport::InjectPayload::Text(text.clone());
191
+ let key = crate::transport::Key::Enter;
192
+ if let Some(socket) = leader_receiver_full_socket(&state) {
193
+ let endpoint_backend = crate::tmux_backend::TmuxBackend::for_tmux_endpoint(socket);
194
+ if crate::transport::Transport::inject(&endpoint_backend, &target, &payload, key, true).is_ok() {
195
+ mirror_to_team_session_panes(&state, workspace, agent_id_from_envelope(envelope), &text);
196
+ return;
197
+ }
198
+ }
199
+ let workspace_backend = crate::tmux_backend::TmuxBackend::for_workspace(workspace);
200
+ let _ = crate::transport::Transport::inject(&workspace_backend, &target, &payload, key, true);
201
+ mirror_to_team_session_panes(&state, workspace, agent_id_from_envelope(envelope), &text);
202
+ }
203
+
204
+ fn leader_receiver_full_socket(state: &serde_json::Value) -> Option<&str> {
205
+ state
206
+ .get("leader_receiver")
207
+ .and_then(|receiver| receiver.get("tmux_socket"))
208
+ .and_then(serde_json::Value::as_str)
209
+ .filter(|socket| !socket.is_empty() && Path::new(socket).is_absolute())
210
+ }
211
+
212
+ fn claimed_pane_from_events(workspace: &Path, owner_team: &str) -> Option<String> {
213
+ crate::event_log::EventLog::new(workspace)
214
+ .tail(50)
215
+ .ok()?
216
+ .into_iter()
217
+ .rev()
218
+ .find_map(|event| {
219
+ if event.get("event").and_then(serde_json::Value::as_str)
220
+ != Some("leader_receiver.rebind_applied")
221
+ {
222
+ return None;
223
+ }
224
+ if event.get("team_id").and_then(serde_json::Value::as_str) != Some(owner_team) {
225
+ return None;
226
+ }
227
+ event
228
+ .get("new_pane_id")
229
+ .and_then(serde_json::Value::as_str)
230
+ .filter(|pane| !pane.is_empty())
231
+ .map(str::to_string)
232
+ })
233
+ }
234
+
235
+ fn agent_id_from_envelope(envelope: &serde_json::Value) -> Option<&str> {
236
+ envelope.get("agent_id").and_then(serde_json::Value::as_str)
237
+ }
238
+
239
+ fn mirror_to_team_session_panes(
240
+ state: &serde_json::Value,
241
+ workspace: &Path,
242
+ agent_id: Option<&str>,
243
+ text: &str,
244
+ ) {
245
+ let Some(session) = state
246
+ .get("session_name")
247
+ .and_then(serde_json::Value::as_str)
248
+ .filter(|session| !session.is_empty())
249
+ else {
250
+ return;
251
+ };
252
+ let worker_pane = agent_id.and_then(|agent_id| {
253
+ state
254
+ .get("agents")
255
+ .and_then(|agents| agents.get(agent_id))
256
+ .and_then(|agent| agent.get("pane_id"))
257
+ .and_then(serde_json::Value::as_str)
258
+ });
259
+ if let Some(socket) = leader_receiver_full_socket(state) {
260
+ let backend = crate::tmux_backend::TmuxBackend::for_tmux_endpoint(socket);
261
+ for pane in crate::transport::Transport::list_targets(&backend).unwrap_or_default() {
262
+ if pane.session.as_str() != session || Some(pane.pane_id.as_str()) == worker_pane {
263
+ continue;
264
+ }
265
+ let target = crate::transport::Target::Pane(pane.pane_id);
266
+ let payload = crate::transport::InjectPayload::Text(text.to_string());
267
+ let _ = crate::transport::Transport::inject(&backend, &target, &payload, crate::transport::Key::Enter, true);
268
+ }
269
+ }
270
+ let backend = crate::tmux_backend::TmuxBackend::for_workspace(workspace);
271
+ for pane in crate::transport::Transport::list_targets(&backend).unwrap_or_default() {
272
+ if pane.session.as_str() != session || Some(pane.pane_id.as_str()) == worker_pane {
273
+ continue;
274
+ }
275
+ let target = crate::transport::Target::Pane(pane.pane_id);
276
+ let payload = crate::transport::InjectPayload::Text(text.to_string());
277
+ let _ = crate::transport::Transport::inject(&backend, &target, &payload, crate::transport::Key::Enter, true);
278
+ }
279
+ }
280
+
138
281
  fn fake_envelope(
139
282
  workspace: &Path,
140
283
  agent_id: &str,
@@ -1,8 +1,9 @@
1
1
  //! leader::start — leader_start_plan / start_leader / leader_session_name(派生 tmux session 名)。
2
2
 
3
3
  use std::collections::BTreeMap;
4
+ use std::io::IsTerminal;
4
5
  use std::path::Path;
5
- use std::process::Command;
6
+ use std::process::{Command, Stdio};
6
7
 
7
8
  use crate::provider::{get_adapter, Provider};
8
9
  use crate::tmux_backend::TmuxBackend;
@@ -12,7 +13,10 @@ use super::helpers::{
12
13
  provider_wire, resolve_workspace_for_hash, sanitize_session_folder, sha1_hex_prefix,
13
14
  };
14
15
  use super::owner_bind::leader_identity_context;
15
- use super::{LeaderError, LeaderStartMode, LeaderStartPlan};
16
+ use super::{
17
+ LeaderError, LeaderLaunchOutcome, LeaderLaunchSocket, LeaderLaunchStatus, LeaderStartMode,
18
+ LeaderStartPlan,
19
+ };
16
20
 
17
21
  // ── leader::start — leader_start_plan / start_leader / session 名 ──
18
22
 
@@ -27,10 +31,14 @@ pub fn leader_start_plan(
27
31
  attach_session: Option<&SessionName>,
28
32
  ) -> Result<LeaderStartPlan, LeaderError> {
29
33
  if attach_session.is_some() && !confirm_attach {
30
- return Err(LeaderError::Start("--attach-session requires --confirm".to_string()));
34
+ return Err(LeaderError::Start(
35
+ "--attach-session requires --confirm".to_string(),
36
+ ));
31
37
  }
32
38
  if attach_existing && !confirm_attach {
33
- return Err(LeaderError::Start("attach existing leader session requires confirm".to_string()));
39
+ return Err(LeaderError::Start(
40
+ "attach existing leader session requires confirm".to_string(),
41
+ ));
34
42
  }
35
43
  let adapter = get_adapter(provider);
36
44
  if !adapter.is_installed() {
@@ -65,7 +73,10 @@ pub fn leader_start_plan(
65
73
  LeaderStartMode::NewTmuxSession
66
74
  };
67
75
  let mut leader_env = BTreeMap::new();
68
- leader_env.insert("TEAM_AGENT_LEADER_PROVIDER".to_string(), provider_wire(provider).to_string());
76
+ leader_env.insert(
77
+ "TEAM_AGENT_LEADER_PROVIDER".to_string(),
78
+ provider_wire(provider).to_string(),
79
+ );
69
80
  leader_env.insert(
70
81
  "TEAM_AGENT_LEADER_SESSION_UUID".to_string(),
71
82
  identity.leader_session_uuid.as_str().to_string(),
@@ -78,8 +89,18 @@ pub fn leader_start_plan(
78
89
  "TEAM_AGENT_WORKSPACE".to_string(),
79
90
  identity.workspace_abspath.to_string_lossy().into_owned(),
80
91
  );
81
- leader_env.insert("TEAM_AGENT_TEAM_ID".to_string(), identity.team_id.as_str().to_string());
82
- let argv = start_argv(mode, provider, provider_args, workspace, session_name.as_ref(), &leader_env)?;
92
+ leader_env.insert(
93
+ "TEAM_AGENT_TEAM_ID".to_string(),
94
+ identity.team_id.as_str().to_string(),
95
+ );
96
+ let argv = start_argv(
97
+ mode,
98
+ provider,
99
+ provider_args,
100
+ workspace,
101
+ session_name.as_ref(),
102
+ &leader_env,
103
+ )?;
83
104
  let plan_env = if mode == LeaderStartMode::ExecProvider {
84
105
  merged_exec_env(&leader_env)
85
106
  } else {
@@ -89,6 +110,7 @@ pub fn leader_start_plan(
89
110
  mode,
90
111
  provider,
91
112
  workspace: resolve_workspace_for_hash(workspace),
113
+ socket: LeaderLaunchSocket::Workspace,
92
114
  session_name,
93
115
  argv,
94
116
  leader_env: plan_env,
@@ -123,7 +145,46 @@ pub fn start_leader(
123
145
  "session_name": plan.session_name.as_ref().map(|s| s.as_str().to_string()),
124
146
  }),
125
147
  )?;
126
- Ok(())
148
+ execute_leader_plan(&plan, workspace).map(|_| ())
149
+ }
150
+
151
+ /// Execute a precomputed leader launch plan.
152
+ ///
153
+ /// S0 exposes the seam and return model only. Lane 2 owns the real provider/tmux
154
+ /// execution and workspace-socket enforcement.
155
+ pub fn execute_leader_plan(
156
+ plan: &LeaderStartPlan,
157
+ workspace: &Path,
158
+ ) -> Result<LeaderLaunchOutcome, LeaderError> {
159
+ let mut argv = plan.argv.clone();
160
+ let detached = plan.mode == LeaderStartMode::NewTmuxSession
161
+ && !std::io::stdin().is_terminal()
162
+ && insert_detach_flag(&mut argv);
163
+ let status = run_leader_argv(&argv, &plan.leader_env)?;
164
+ let code = status.code();
165
+ if !status.success() {
166
+ return Err(LeaderError::Start(format!(
167
+ "leader launcher exited with status {}",
168
+ code.map(|c| c.to_string())
169
+ .unwrap_or_else(|| "signal".to_string())
170
+ )));
171
+ }
172
+ if detached {
173
+ Ok(LeaderLaunchOutcome {
174
+ status: LeaderLaunchStatus::Detached,
175
+ exit_code: code,
176
+ session_name: plan.session_name.clone(),
177
+ reason: None,
178
+ })
179
+ } else {
180
+ let _ = workspace;
181
+ Ok(LeaderLaunchOutcome {
182
+ status: LeaderLaunchStatus::Exited,
183
+ exit_code: code,
184
+ session_name: plan.session_name.clone(),
185
+ reason: None,
186
+ })
187
+ }
127
188
  }
128
189
 
129
190
  /// `leader_session_name`(card §48;`__init__.py:186`)。确定派生 tmux session 名
@@ -161,12 +222,13 @@ fn start_argv(
161
222
  let Some(session) = session_name else {
162
223
  return Err(LeaderError::Start("attach session missing".to_string()));
163
224
  };
164
- Ok(vec![
225
+ let argv = vec![
165
226
  "tmux".to_string(),
166
227
  "attach-session".to_string(),
167
228
  "-t".to_string(),
168
229
  session.as_str().to_string(),
169
- ])
230
+ ];
231
+ Ok(TmuxBackend::argv_for_workspace(workspace, &argv))
170
232
  }
171
233
  LeaderStartMode::NewTmuxSession => {
172
234
  let Some(session) = session_name else {
@@ -185,7 +247,7 @@ fn start_argv(
185
247
  exports.join(" "),
186
248
  shell_join(&provider_argv)
187
249
  );
188
- Ok(vec![
250
+ let argv = vec![
189
251
  "tmux".to_string(),
190
252
  "new-session".to_string(),
191
253
  "-s".to_string(),
@@ -197,11 +259,42 @@ fn start_argv(
197
259
  "sh".to_string(),
198
260
  "-lc".to_string(),
199
261
  shell,
200
- ])
262
+ ];
263
+ Ok(TmuxBackend::argv_for_workspace(workspace, &argv))
201
264
  }
202
265
  }
203
266
  }
204
267
 
268
+ fn insert_detach_flag(argv: &mut Vec<String>) -> bool {
269
+ if argv.iter().any(|arg| arg == "-d") {
270
+ return false;
271
+ }
272
+ let Some(pos) = argv.iter().position(|arg| arg == "new-session") else {
273
+ return false;
274
+ };
275
+ argv.insert(pos + 1, "-d".to_string());
276
+ true
277
+ }
278
+
279
+ fn run_leader_argv(
280
+ argv: &[String],
281
+ env: &BTreeMap<String, String>,
282
+ ) -> Result<std::process::ExitStatus, LeaderError> {
283
+ let Some(program) = argv.first() else {
284
+ return Err(LeaderError::Start(
285
+ "leader launch argv is empty".to_string(),
286
+ ));
287
+ };
288
+ let mut child = Command::new(program)
289
+ .args(argv.iter().skip(1))
290
+ .envs(env)
291
+ .stdin(Stdio::inherit())
292
+ .stdout(Stdio::inherit())
293
+ .stderr(Stdio::inherit())
294
+ .spawn()?;
295
+ child.wait().map_err(LeaderError::Io)
296
+ }
297
+
205
298
  fn ensure_tmux_installed() -> Result<(), LeaderError> {
206
299
  match Command::new("tmux").arg("-V").output() {
207
300
  Ok(output) if output.status.success() => Ok(()),
@@ -246,25 +339,30 @@ fn leader_export_assignments(leader_env: &BTreeMap<String, String>) -> Vec<Strin
246
339
 
247
340
  fn merged_exec_env(leader_env: &BTreeMap<String, String>) -> BTreeMap<String, String> {
248
341
  let mut env: BTreeMap<String, String> = std::env::vars().collect();
249
- env.extend(leader_env.iter().map(|(key, value)| (key.clone(), value.clone())));
342
+ env.extend(
343
+ leader_env
344
+ .iter()
345
+ .map(|(key, value)| (key.clone(), value.clone())),
346
+ );
250
347
  env
251
348
  }
252
349
 
253
350
  fn shell_join(args: &[String]) -> String {
254
- args.iter().map(|arg| shlex_quote(arg)).collect::<Vec<_>>().join(" ")
351
+ args.iter()
352
+ .map(|arg| shlex_quote(arg))
353
+ .collect::<Vec<_>>()
354
+ .join(" ")
255
355
  }
256
356
 
257
357
  fn shlex_quote(raw: &str) -> String {
258
358
  if !raw.is_empty()
259
- && raw
260
- .bytes()
261
- .all(|b| {
262
- b.is_ascii_alphanumeric()
263
- || matches!(
264
- b,
265
- b'@' | b'%' | b'_' | b'+' | b'=' | b':' | b',' | b'.' | b'/' | b'-'
266
- )
267
- })
359
+ && raw.bytes().all(|b| {
360
+ b.is_ascii_alphanumeric()
361
+ || matches!(
362
+ b,
363
+ b'@' | b'%' | b'_' | b'+' | b'=' | b':' | b',' | b'.' | b'/' | b'-'
364
+ )
365
+ })
268
366
  {
269
367
  raw.to_string()
270
368
  } else {
@@ -151,6 +151,24 @@ pub enum LeaderStartMode {
151
151
  AttachExisting,
152
152
  }
153
153
 
154
+ /// Leader launcher tmux/socket selection. Managed launcher sessions must use the
155
+ /// workspace tmux socket, never the user's default tmux server.
156
+ #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
157
+ #[serde(rename_all = "snake_case")]
158
+ pub enum LeaderLaunchSocket {
159
+ Workspace,
160
+ }
161
+
162
+ /// Execution status for a leader launch plan. `NotStarted` is intentionally
163
+ /// distinct so JSON callers cannot report `ok:true` for an unexecuted launcher.
164
+ #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
165
+ #[serde(rename_all = "snake_case")]
166
+ pub enum LeaderLaunchStatus {
167
+ Exited,
168
+ Detached,
169
+ NotStarted,
170
+ }
171
+
154
172
  /// 全部 leader 审计事件名(card §34)。§3 typed event kinds + §40 JSON 名与 Python
155
173
  /// **字节级一致**。映射到 [`LeaderEvent::name`] 返回 `EventLog::write` 用的精确字符串。
156
174
  /// (既有 `EventLog::write(&str, Value)` 仍吃裸字符串;此 enum 是 type-safe 名表,
@@ -200,7 +218,9 @@ impl LeaderEvent {
200
218
  Self::ReceiverStateDivergenceRepaired => "leader_receiver.state_divergence_repaired",
201
219
  Self::ReceiverFirstTimeEnvSeeded => "leader_receiver.first_time_env_seeded",
202
220
  Self::ReceiverAutobindSkipped => "leader_receiver.autobind_skipped",
203
- Self::ReceiverRequeuedExhaustedWatchers => "leader_receiver.requeued_exhausted_watchers",
221
+ Self::ReceiverRequeuedExhaustedWatchers => {
222
+ "leader_receiver.requeued_exhausted_watchers"
223
+ }
204
224
  Self::ReceiverAmbiguousCandidates => "leader_receiver.ambiguous_candidates",
205
225
  Self::ReceiverClaimRequeue => "leader_receiver.claim_requeue",
206
226
  Self::ReceiverClaimLeaderNotification => "leader_receiver.claim_leader_notification",
@@ -307,6 +327,7 @@ pub struct LeaderStartPlan {
307
327
  pub mode: LeaderStartMode,
308
328
  pub provider: Provider,
309
329
  pub workspace: PathBuf,
330
+ pub socket: LeaderLaunchSocket,
310
331
  pub session_name: Option<SessionName>,
311
332
  /// 要 exec 的 argv(provider argv 或 tmux argv)。
312
333
  pub argv: Vec<String>,
@@ -317,6 +338,28 @@ pub struct LeaderStartPlan {
317
338
  pub detached: bool,
318
339
  }
319
340
 
341
+ /// Result of executing a [`LeaderStartPlan`]. Interactive launches should carry
342
+ /// the provider/tmux process exit code; detached launches carry the managed
343
+ /// session when known.
344
+ #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
345
+ pub struct LeaderLaunchOutcome {
346
+ pub status: LeaderLaunchStatus,
347
+ pub exit_code: Option<i32>,
348
+ pub session_name: Option<SessionName>,
349
+ pub reason: Option<String>,
350
+ }
351
+
352
+ impl LeaderLaunchOutcome {
353
+ pub fn not_started(reason: impl Into<String>) -> Self {
354
+ Self {
355
+ status: LeaderLaunchStatus::NotStarted,
356
+ exit_code: None,
357
+ session_name: None,
358
+ reason: Some(reason.into()),
359
+ }
360
+ }
361
+ }
362
+
320
363
  /// idle-takeover 的 node 分类行(`build_idle_nodes` / `_leader_node` 产物)。
321
364
  /// **bug-085**:`state` 用 `TurnState`(穷尽,`Unknown` 不当 idle);`rollout_path` `Option`。
322
365
  #[derive(Debug, Clone, PartialEq, Eq)]
@@ -55,6 +55,8 @@ pub mod message_store;
55
55
  // step 8 (provider) — ProviderAdapter trait + typed provider/turn-state/liveness 等(ROUND-0 骨架;
56
56
  // fn body unimplemented!(),P2 porter 落实现)。MUST-NOT-13:provider 调用全走 trait。
57
57
  pub mod provider;
58
+ pub mod session_capture;
59
+ pub(crate) mod os_probe;
58
60
 
59
61
  // step 9 (transport) — Transport trait(控制面)+ Target/PaneId/InjectReport 等(ROUND-0 骨架;
60
62
  // fn body unimplemented!(),P2 porter 落实现)。tmux/WezTerm/ConPTY 三后端。
@@ -66,6 +68,7 @@ pub mod transport;
66
68
  pub mod leader;
67
69
  pub mod messaging;
68
70
  pub mod coordinator;
71
+ pub mod diagnose;
69
72
 
70
73
  // step 13-15 (lifecycle/mcp_server/cli/packaging) — ROUND-0.5b behavioral-rich 骨架(entry-fn 签名 +
71
74
  // 富返回类型,fn body unimplemented!(),P2 porter 落实现)。lifecycle=quick-start/restart/display;