@team-agent/installer 0.3.13 → 0.3.15

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 (38) hide show
  1. package/Cargo.lock +1 -1
  2. package/Cargo.toml +1 -1
  3. package/crates/team-agent/src/cli/adapters.rs +1 -0
  4. package/crates/team-agent/src/cli/emit.rs +92 -11
  5. package/crates/team-agent/src/cli/mod.rs +228 -85
  6. package/crates/team-agent/src/cli/send.rs +252 -0
  7. package/crates/team-agent/src/cli/tests/run_delegation.rs +15 -3
  8. package/crates/team-agent/src/cli/tests/shutdown_kill_plan.rs +159 -2
  9. package/crates/team-agent/src/cli/types.rs +30 -1
  10. package/crates/team-agent/src/compiler/tests.rs +2 -2
  11. package/crates/team-agent/src/compiler.rs +1 -1
  12. package/crates/team-agent/src/fake_worker.rs +32 -145
  13. package/crates/team-agent/src/leader/start.rs +279 -4
  14. package/crates/team-agent/src/lifecycle/display.rs +3 -3
  15. package/crates/team-agent/src/lifecycle/launch.rs +692 -92
  16. package/crates/team-agent/src/lifecycle/restart/agent.rs +137 -17
  17. package/crates/team-agent/src/lifecycle/restart/common.rs +88 -42
  18. package/crates/team-agent/src/lifecycle/restart/rebuild.rs +197 -33
  19. package/crates/team-agent/src/lifecycle/restart/selection.rs +9 -6
  20. package/crates/team-agent/src/lifecycle/tests/core.rs +195 -50
  21. package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +934 -72
  22. package/crates/team-agent/src/lifecycle/types.rs +5 -0
  23. package/crates/team-agent/src/lifecycle/worker_command_context.rs +8 -0
  24. package/crates/team-agent/src/mcp_server/wire.rs +153 -3
  25. package/crates/team-agent/src/messaging/leader_receiver.rs +276 -43
  26. package/crates/team-agent/src/messaging/mod.rs +6 -3
  27. package/crates/team-agent/src/messaging/results.rs +240 -158
  28. package/crates/team-agent/src/messaging/send.rs +3 -2
  29. package/crates/team-agent/src/messaging/tests/e23.rs +35 -0
  30. package/crates/team-agent/src/messaging/tests/mod.rs +6 -2
  31. package/crates/team-agent/src/messaging/tests/runtime.rs +9 -9
  32. package/crates/team-agent/src/os_probe.rs +11 -0
  33. package/crates/team-agent/src/state/persist.rs +6 -0
  34. package/crates/team-agent/src/tmux_backend.rs +90 -0
  35. package/crates/team-agent/src/transport/test_support.rs +46 -1
  36. package/crates/team-agent/src/transport.rs +31 -0
  37. package/package.json +4 -4
  38. package/skills/team-agent/references/recovery-runbook.md +277 -0
@@ -107,7 +107,10 @@ fn parse_rendered_header(line: &str) -> Option<RenderedBlock> {
107
107
  .split_once(" for ")
108
108
  .map(|(_, task)| task.trim().to_string())
109
109
  .filter(|task| !task.is_empty());
110
- Some(RenderedBlock { task_id, lines: Vec::new() })
110
+ Some(RenderedBlock {
111
+ task_id,
112
+ lines: Vec::new(),
113
+ })
111
114
  }
112
115
 
113
116
  fn parse_token(line: &str) -> Option<String> {
@@ -135,149 +138,13 @@ fn report_fake_result(
135
138
  .ok()
136
139
  .map(|value| value.trim().to_string())
137
140
  .filter(|value| !value.is_empty());
138
- mirror_fake_result_to_leader(workspace, owner_team.as_deref(), &envelope, source_content);
141
+ let _ = source_content;
139
142
  crate::messaging::report_result_for_owner_team(workspace, &envelope, owner_team.as_deref())
140
143
  .map_err(|e| FakeWorkerError::Report(e.to_string()))?;
141
- mirror_fake_result_to_leader(workspace, owner_team.as_deref(), &envelope, source_content);
142
144
  writeln!(output, "{}", serde_json::to_string(&envelope)?)?;
143
145
  Ok(())
144
146
  }
145
147
 
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
-
281
148
  fn fake_envelope(
282
149
  workspace: &Path,
283
150
  agent_id: &str,
@@ -331,7 +198,9 @@ mod tests {
331
198
  fn stored_result(ws: &std::path::Path) -> (String, String, String, serde_json::Value) {
332
199
  let store = MessageStore::open(ws).unwrap();
333
200
  let conn = open_db(store.db_path()).unwrap();
334
- let mut stmt = conn.prepare("select task_id, agent_id, status, envelope from results").unwrap();
201
+ let mut stmt = conn
202
+ .prepare("select task_id, agent_id, status, envelope from results")
203
+ .unwrap();
335
204
  let row = stmt
336
205
  .query_row([], |r| {
337
206
  Ok((
@@ -354,9 +223,18 @@ mod tests {
354
223
  ("t1", "w1", "success"),
355
224
  "stored result row must be task_id=t1 agent_id=w1 status=success"
356
225
  );
357
- assert_eq!(env["schema_version"], serde_json::json!("result_envelope_v1"));
358
- assert_eq!(env["summary"], serde_json::json!("Fake worker handled message m1"));
359
- assert_eq!(env["tests"], serde_json::json!([{"command": "fake-provider", "status": "passed"}]));
226
+ assert_eq!(
227
+ env["schema_version"],
228
+ serde_json::json!("result_envelope_v1")
229
+ );
230
+ assert_eq!(
231
+ env["summary"],
232
+ serde_json::json!("Fake worker handled message m1")
233
+ );
234
+ assert_eq!(
235
+ env["tests"],
236
+ serde_json::json!([{"command": "fake-provider", "status": "passed"}])
237
+ );
360
238
  let artifact_path = env["artifacts"][0]["path"].as_str().unwrap_or_default();
361
239
  assert!(
362
240
  artifact_path.ends_with(".team/logs/w1.scrollback"),
@@ -375,8 +253,14 @@ mod tests {
375
253
 
376
254
  assert_golden_envelope(&ws);
377
255
  let printed = String::from_utf8(out).unwrap();
378
- assert!(printed.contains("TEAM_AGENT_FAKE_READY agent=w1"), "must print the READY idle marker; got {printed}");
379
- assert!(printed.contains("TEAM_AGENT_FAKE_WORKING agent=w1"), "must print the WORKING marker on a non-empty line; got {printed}");
256
+ assert!(
257
+ printed.contains("TEAM_AGENT_FAKE_READY agent=w1"),
258
+ "must print the READY idle marker; got {printed}"
259
+ );
260
+ assert!(
261
+ printed.contains("TEAM_AGENT_FAKE_WORKING agent=w1"),
262
+ "must print the WORKING marker on a non-empty line; got {printed}"
263
+ );
380
264
  }
381
265
 
382
266
  // Rendered-message form: a `Team Agent message from leader for t1:` block ending `[team-agent-token:m1]`
@@ -391,6 +275,9 @@ mod tests {
391
275
 
392
276
  assert_golden_envelope(&ws);
393
277
  let printed = String::from_utf8(out).unwrap();
394
- assert!(printed.contains("TEAM_AGENT_FAKE_WORKING agent=w1"), "rendered form prints WORKING per line; got {printed}");
278
+ assert!(
279
+ printed.contains("TEAM_AGENT_FAKE_WORKING agent=w1"),
280
+ "rendered form prints WORKING per line; got {printed}"
281
+ );
395
282
  }
396
283
  }
@@ -2,12 +2,12 @@
2
2
 
3
3
  use std::collections::BTreeMap;
4
4
  use std::io::IsTerminal;
5
- use std::path::Path;
5
+ use std::path::{Path, PathBuf};
6
6
  use std::process::{Command, Stdio};
7
7
 
8
8
  use crate::provider::{get_adapter, Provider};
9
9
  use crate::tmux_backend::TmuxBackend;
10
- use crate::transport::{SessionName, Transport};
10
+ use crate::transport::{PaneId, SessionName, Target, Transport};
11
11
 
12
12
  use super::helpers::{
13
13
  provider_wire, resolve_workspace_for_hash, sanitize_session_folder, sha1_hex_prefix,
@@ -125,7 +125,10 @@ pub(crate) fn leader_env_for_identity(
125
125
  identity.team_id.as_str().to_string(),
126
126
  );
127
127
  if provider == Provider::Copilot {
128
- leader_env.insert("COPILOT_DISABLE_TERMINAL_TITLE".to_string(), "1".to_string());
128
+ leader_env.insert(
129
+ "COPILOT_DISABLE_TERMINAL_TITLE".to_string(),
130
+ "1".to_string(),
131
+ );
129
132
  }
130
133
  leader_env
131
134
  }
@@ -171,7 +174,7 @@ pub fn execute_leader_plan(
171
174
  let detached = plan.mode == LeaderStartMode::NewTmuxSession
172
175
  && !std::io::stdin().is_terminal()
173
176
  && insert_detach_flag(&mut argv);
174
- let status = run_leader_argv(&argv, &plan.leader_env)?;
177
+ let status = run_leader_argv(&argv, &plan.leader_env, plan, workspace)?;
175
178
  let code = status.code();
176
179
  if !status.success() {
177
180
  return Err(LeaderError::Start(format!(
@@ -294,6 +297,8 @@ fn insert_detach_flag(argv: &mut Vec<String>) -> bool {
294
297
  fn run_leader_argv(
295
298
  argv: &[String],
296
299
  env: &BTreeMap<String, String>,
300
+ plan: &LeaderStartPlan,
301
+ workspace: &Path,
297
302
  ) -> Result<std::process::ExitStatus, LeaderError> {
298
303
  let Some(program) = argv.first() else {
299
304
  return Err(LeaderError::Start(
@@ -307,9 +312,84 @@ fn run_leader_argv(
307
312
  .stdout(Stdio::inherit())
308
313
  .stderr(Stdio::inherit())
309
314
  .spawn()?;
315
+ if plan.mode == LeaderStartMode::ExecProvider {
316
+ spawn_exec_provider_startup_prompt_handler(plan.provider, workspace.to_path_buf());
317
+ }
310
318
  child.wait().map_err(LeaderError::Io)
311
319
  }
312
320
 
321
+ fn spawn_exec_provider_startup_prompt_handler(provider: Provider, workspace: PathBuf) {
322
+ let Some(pane_id) = std::env::var("TMUX_PANE")
323
+ .ok()
324
+ .filter(|value| !value.trim().is_empty())
325
+ else {
326
+ write_leader_startup_prompt_event(
327
+ &workspace,
328
+ "leader.startup_prompt_skipped",
329
+ serde_json::json!({
330
+ "provider": provider_wire(provider),
331
+ "reason": "tmux_pane_missing",
332
+ "action": "skip",
333
+ }),
334
+ );
335
+ return;
336
+ };
337
+ std::thread::spawn(move || {
338
+ let transport = tmux_transport_for_current_pane();
339
+ let _ = handle_exec_provider_startup_prompts(
340
+ provider, &workspace, &pane_id, &transport, 30, 0.5,
341
+ );
342
+ });
343
+ }
344
+
345
+ fn tmux_transport_for_current_pane() -> TmuxBackend {
346
+ crate::tmux_backend::socket_name_from_tmux_env()
347
+ .map(|endpoint| TmuxBackend::for_tmux_endpoint(&endpoint))
348
+ .unwrap_or_else(TmuxBackend::new)
349
+ }
350
+
351
+ pub fn handle_exec_provider_startup_prompts(
352
+ provider: Provider,
353
+ workspace: &Path,
354
+ pane_id: &str,
355
+ transport: &dyn Transport,
356
+ checks: usize,
357
+ sleep_s: f64,
358
+ ) -> crate::provider::StartupPromptOutcome {
359
+ let target = Target::Pane(PaneId::new(pane_id.to_string()));
360
+ let outcome =
361
+ get_adapter(provider).handle_startup_prompts_outcome(transport, &target, checks, sleep_s);
362
+ for handled in &outcome.handled {
363
+ write_leader_startup_prompt_event(
364
+ workspace,
365
+ "leader.startup_prompt_handled",
366
+ serde_json::json!({
367
+ "provider": provider_wire(provider),
368
+ "pane_id": pane_id,
369
+ "prompt": handled.prompt,
370
+ "action": handled.action,
371
+ }),
372
+ );
373
+ }
374
+ if let Some(error) = &outcome.capture_error {
375
+ write_leader_startup_prompt_event(
376
+ workspace,
377
+ "leader.startup_prompt_capture_failed",
378
+ serde_json::json!({
379
+ "provider": provider_wire(provider),
380
+ "pane_id": pane_id,
381
+ "action": "capture",
382
+ "error": error,
383
+ }),
384
+ );
385
+ }
386
+ outcome
387
+ }
388
+
389
+ fn write_leader_startup_prompt_event(workspace: &Path, event: &str, fields: serde_json::Value) {
390
+ let _ = crate::event_log::EventLog::new(workspace).write(event, fields);
391
+ }
392
+
313
393
  fn ensure_tmux_installed() -> Result<(), LeaderError> {
314
394
  match Command::new("tmux").arg("-V").output() {
315
395
  Ok(output) if output.status.success() => Ok(()),
@@ -388,3 +468,198 @@ fn shlex_quote(raw: &str) -> String {
388
468
  format!("'{}'", raw.replace('\'', "'\"'\"'"))
389
469
  }
390
470
  }
471
+
472
+ #[cfg(test)]
473
+ mod tests {
474
+ use std::collections::BTreeMap;
475
+ use std::path::Path;
476
+ use std::sync::Mutex;
477
+
478
+ use crate::model::enums::PaneLiveness;
479
+ use crate::provider::{Provider, COPILOT_READY_MARKER, COPILOT_TRUST_PROMPT_MARKER};
480
+ use crate::transport::{
481
+ AttachOutcome, BackendKind, CaptureRange, CapturedText, InjectPayload, InjectReport,
482
+ InjectStage, InjectVerification, Key, PaneField, PaneId, PaneInfo, SessionName,
483
+ SetEnvOutcome, SpawnResult, SubmitVerification, Target, Transport, TransportError,
484
+ TurnVerification, WindowName,
485
+ };
486
+
487
+ use super::handle_exec_provider_startup_prompts;
488
+
489
+ struct ScriptedTransport {
490
+ screens: Mutex<Vec<String>>,
491
+ sent: Mutex<Vec<(Target, Vec<Key>)>>,
492
+ }
493
+
494
+ impl ScriptedTransport {
495
+ fn new(screens: Vec<String>) -> Self {
496
+ Self {
497
+ screens: Mutex::new(screens),
498
+ sent: Mutex::new(Vec::new()),
499
+ }
500
+ }
501
+
502
+ fn sent(&self) -> Vec<(Target, Vec<Key>)> {
503
+ match self.sent.lock() {
504
+ Ok(guard) => guard.clone(),
505
+ Err(poisoned) => poisoned.into_inner().clone(),
506
+ }
507
+ }
508
+ }
509
+
510
+ impl Transport for ScriptedTransport {
511
+ fn kind(&self) -> BackendKind {
512
+ BackendKind::Tmux
513
+ }
514
+
515
+ fn spawn_first(
516
+ &self,
517
+ _session: &SessionName,
518
+ _window: &WindowName,
519
+ _argv: &[String],
520
+ _cwd: &Path,
521
+ _env: &BTreeMap<String, String>,
522
+ ) -> Result<SpawnResult, TransportError> {
523
+ Err(TransportError::Io(std::io::Error::other(
524
+ "spawn_first not used by startup-prompt test",
525
+ )))
526
+ }
527
+
528
+ fn spawn_into(
529
+ &self,
530
+ _session: &SessionName,
531
+ _window: &WindowName,
532
+ _argv: &[String],
533
+ _cwd: &Path,
534
+ _env: &BTreeMap<String, String>,
535
+ ) -> Result<SpawnResult, TransportError> {
536
+ Err(TransportError::Io(std::io::Error::other(
537
+ "spawn_into not used by startup-prompt test",
538
+ )))
539
+ }
540
+
541
+ fn inject(
542
+ &self,
543
+ _target: &Target,
544
+ _payload: &InjectPayload,
545
+ _submit: Key,
546
+ _bracketed: bool,
547
+ ) -> Result<InjectReport, TransportError> {
548
+ Ok(InjectReport {
549
+ stage_reached: InjectStage::Submit,
550
+ inject_verification: InjectVerification::CaptureContainsToken,
551
+ submit_verification: SubmitVerification::EnterSentWithoutPlaceholderCheck,
552
+ turn_verification: TurnVerification::NotRequired,
553
+ attempts: 1,
554
+ })
555
+ }
556
+
557
+ fn send_keys(&self, target: &Target, keys: &[Key]) -> Result<(), TransportError> {
558
+ match self.sent.lock() {
559
+ Ok(mut guard) => guard.push((target.clone(), keys.to_vec())),
560
+ Err(poisoned) => poisoned.into_inner().push((target.clone(), keys.to_vec())),
561
+ }
562
+ Ok(())
563
+ }
564
+
565
+ fn capture(
566
+ &self,
567
+ _target: &Target,
568
+ range: CaptureRange,
569
+ ) -> Result<CapturedText, TransportError> {
570
+ let text = match self.screens.lock() {
571
+ Ok(mut guard) => {
572
+ if guard.is_empty() {
573
+ String::new()
574
+ } else {
575
+ guard.remove(0)
576
+ }
577
+ }
578
+ Err(poisoned) => {
579
+ let mut guard = poisoned.into_inner();
580
+ if guard.is_empty() {
581
+ String::new()
582
+ } else {
583
+ guard.remove(0)
584
+ }
585
+ }
586
+ };
587
+ Ok(CapturedText { text, range })
588
+ }
589
+
590
+ fn query(
591
+ &self,
592
+ _target: &Target,
593
+ _field: PaneField,
594
+ ) -> Result<Option<String>, TransportError> {
595
+ Ok(None)
596
+ }
597
+
598
+ fn liveness(&self, _pane: &PaneId) -> Result<PaneLiveness, TransportError> {
599
+ Ok(PaneLiveness::Unknown)
600
+ }
601
+
602
+ fn list_targets(&self) -> Result<Vec<PaneInfo>, TransportError> {
603
+ Ok(Vec::new())
604
+ }
605
+
606
+ fn has_session(&self, _session: &SessionName) -> Result<bool, TransportError> {
607
+ Ok(false)
608
+ }
609
+
610
+ fn list_windows(&self, _session: &SessionName) -> Result<Vec<WindowName>, TransportError> {
611
+ Ok(Vec::new())
612
+ }
613
+
614
+ fn set_session_env(
615
+ &self,
616
+ _session: &SessionName,
617
+ _key: &str,
618
+ _value: &str,
619
+ ) -> Result<SetEnvOutcome, TransportError> {
620
+ Ok(SetEnvOutcome::Applied)
621
+ }
622
+
623
+ fn kill_session(&self, _session: &SessionName) -> Result<(), TransportError> {
624
+ Ok(())
625
+ }
626
+
627
+ fn kill_window(&self, _target: &Target) -> Result<(), TransportError> {
628
+ Ok(())
629
+ }
630
+
631
+ fn attach_session(&self, _session: &SessionName) -> Result<AttachOutcome, TransportError> {
632
+ Ok(AttachOutcome::Unsupported {
633
+ reason: "not used by startup-prompt test".to_string(),
634
+ })
635
+ }
636
+ }
637
+
638
+ #[test]
639
+ fn exec_provider_leader_startup_prompt_handler_reuses_copilot_adapter() {
640
+ let transport = ScriptedTransport::new(vec![
641
+ COPILOT_TRUST_PROMPT_MARKER.to_string(),
642
+ COPILOT_READY_MARKER.to_string(),
643
+ ]);
644
+ let workspace =
645
+ std::env::temp_dir().join(format!("ta_rs_red2_leader_startup_{}", std::process::id()));
646
+ let _ = std::fs::create_dir_all(&workspace);
647
+
648
+ let outcome = handle_exec_provider_startup_prompts(
649
+ Provider::Copilot,
650
+ &workspace,
651
+ "%0",
652
+ &transport,
653
+ 5,
654
+ 0.0,
655
+ );
656
+
657
+ assert_eq!(outcome.handled.len(), 1);
658
+ assert_eq!(outcome.handled[0].prompt, "copilot_workspace_trust");
659
+ assert_eq!(outcome.handled[0].action, "sent_enter_yes_session");
660
+ let sent = transport.sent();
661
+ assert_eq!(sent.len(), 1);
662
+ assert_eq!(sent[0].0, Target::Pane(PaneId::new("%0")));
663
+ assert_eq!(sent[0].1, vec![Key::Enter]);
664
+ }
665
+ }
@@ -11,15 +11,15 @@ use super::*;
11
11
  // ── lifecycle::display —— 能力门 / 后端解析 / 开关 / rebind 后重建 ────────────
12
12
 
13
13
  /// `resolve_display_backend(requested, recorded, source)`(`display/backend.py`)。默认
14
- /// none;非默认非静默发 `display.backend_resolved`。
14
+ /// adaptive;显式 none 是逃生口。
15
15
  pub fn resolve_display_backend(
16
16
  requested: Option<DisplayBackend>,
17
17
  recorded: Option<DisplayBackend>,
18
18
  ) -> ResolvedBackend {
19
- let backend = requested.or(recorded).unwrap_or(DisplayBackend::None);
19
+ let backend = requested.or(recorded).unwrap_or(DisplayBackend::Adaptive);
20
20
  ResolvedBackend {
21
21
  backend,
22
- non_default: backend != DisplayBackend::None,
22
+ non_default: backend != DisplayBackend::Adaptive,
23
23
  }
24
24
  }
25
25