@team-agent/installer 0.5.60 → 0.5.61

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 (54) hide show
  1. package/Cargo.lock +1 -1
  2. package/Cargo.toml +1 -1
  3. package/crates/team-agent/src/cli/adapters.rs +44 -1
  4. package/crates/team-agent/src/cli/diagnose.rs +183 -0
  5. package/crates/team-agent/src/cli/emit.rs +63 -14
  6. package/crates/team-agent/src/cli/leader.rs +8 -1
  7. package/crates/team-agent/src/cli/mod.rs +263 -5
  8. package/crates/team-agent/src/cli/named_address.rs +18 -1
  9. package/crates/team-agent/src/cli/send/presentation.rs +2 -0
  10. package/crates/team-agent/src/cli/spec.rs +4 -1
  11. package/crates/team-agent/src/cli/status_port/store.rs +7 -3
  12. package/crates/team-agent/src/cli/tests/named_address.rs +65 -0
  13. package/crates/team-agent/src/cli/types.rs +33 -0
  14. package/crates/team-agent/src/communication_mode/mod.rs +53 -0
  15. package/crates/team-agent/src/compiler/tests.rs +5 -5
  16. package/crates/team-agent/src/compiler.rs +53 -1
  17. package/crates/team-agent/src/db/message_store.rs +45 -1
  18. package/crates/team-agent/src/fake_worker.rs +21 -1
  19. package/crates/team-agent/src/leader/start.rs +732 -94
  20. package/crates/team-agent/src/leader/tests/identity.rs +64 -57
  21. package/crates/team-agent/src/leader/tests/identity_session_names.rs +42 -0
  22. package/crates/team-agent/src/lib.rs +4 -0
  23. package/crates/team-agent/src/lifecycle/launch/fork_agent.rs +1 -1
  24. package/crates/team-agent/src/lifecycle/launch/spawn.rs +1 -1
  25. package/crates/team-agent/src/lifecycle/restart/agent.rs +1 -1
  26. package/crates/team-agent/src/lifecycle/restart/common.rs +1 -1
  27. package/crates/team-agent/src/lifecycle/worker_command_context.rs +29 -11
  28. package/crates/team-agent/src/mcp_server/normalize.rs +1 -0
  29. package/crates/team-agent/src/mcp_server/tests/send.rs +26 -0
  30. package/crates/team-agent/src/mcp_server/tests/wire.rs +1 -1
  31. package/crates/team-agent/src/mcp_server/tools.rs +100 -66
  32. package/crates/team-agent/src/mcp_server/types.rs +5 -0
  33. package/crates/team-agent/src/mcp_server/wire.rs +27 -21
  34. package/crates/team-agent/src/messaging/delivery.rs +105 -38
  35. package/crates/team-agent/src/messaging/leader_channel.rs +33 -10
  36. package/crates/team-agent/src/messaging/leader_receiver.rs +40 -23
  37. package/crates/team-agent/src/messaging/presentation.rs +109 -0
  38. package/crates/team-agent/src/messaging/results.rs +165 -17
  39. package/crates/team-agent/src/messaging/send.rs +94 -81
  40. package/crates/team-agent/src/messaging/tests/leader_channel.rs +6 -6
  41. package/crates/team-agent/src/messaging/tests/leader_inject_acceptance.rs +187 -0
  42. package/crates/team-agent/src/messaging/tests/runtime.rs +13 -6
  43. package/crates/team-agent/src/messaging/types.rs +5 -0
  44. package/crates/team-agent/src/messaging/watchers.rs +5 -0
  45. package/crates/team-agent/src/model/mod.rs +1 -0
  46. package/crates/team-agent/src/model/pane_authority_refusal.rs +358 -0
  47. package/crates/team-agent/src/model/spec.rs +16 -0
  48. package/crates/team-agent/src/provider/session/capture.rs +40 -18
  49. package/crates/team-agent/src/provider/session_scan/claude.rs +92 -3
  50. package/crates/team-agent/src/provider/session_scan/common.rs +23 -7
  51. package/crates/team-agent/src/provider/session_scan.rs +5 -0
  52. package/crates/team-agent/src/topology.rs +3 -0
  53. package/package.json +4 -4
  54. package/skills/team-agent/command-coverage.json +379 -0
package/Cargo.lock CHANGED
@@ -575,7 +575,7 @@ dependencies = [
575
575
 
576
576
  [[package]]
577
577
  name = "team-agent"
578
- version = "0.5.60"
578
+ version = "0.5.61"
579
579
  dependencies = [
580
580
  "anyhow",
581
581
  "chrono",
package/Cargo.toml CHANGED
@@ -9,7 +9,7 @@ members = ["crates/team-agent", "crates/win-conpty-phase0", "crates/conpty-trans
9
9
 
10
10
  [workspace.package]
11
11
  edition = "2021"
12
- version = "0.5.60"
12
+ version = "0.5.61"
13
13
  license = "AGPL-3.0"
14
14
  rust-version = "1.95"
15
15
 
@@ -483,6 +483,43 @@ pub fn cmd_collect_for_team(args: &CollectArgs, team: Option<&str>) -> Result<Cm
483
483
  ))
484
484
  }
485
485
 
486
+ pub fn cmd_results(args: &ResultsArgs) -> Result<CmdResult, CliError> {
487
+ let selected = match crate::state::selector::resolve_active_team(
488
+ &args.workspace,
489
+ args.team.as_deref(),
490
+ crate::state::selector::SelectorMode::RuntimeOnly,
491
+ ) {
492
+ Ok(selected) => selected,
493
+ Err(error) => {
494
+ return Ok(CmdResult::from_json(
495
+ json!({
496
+ "ok": false,
497
+ "error": error.to_string(),
498
+ "workspace": args.workspace.to_string_lossy().to_string(),
499
+ }),
500
+ args.json,
501
+ ));
502
+ }
503
+ };
504
+ let results = messaging::results::results_for_case(
505
+ &selected.run_workspace,
506
+ &args.case_id,
507
+ Some(selected.team_key.as_str()),
508
+ args.team.as_deref(),
509
+ )?;
510
+ let value = json!({
511
+ "ok": true,
512
+ "case_id": args.case_id,
513
+ "results": results,
514
+ "workspace": selected.run_workspace.to_string_lossy().to_string(),
515
+ });
516
+ Ok(if args.json {
517
+ CmdResult::from_ordered_json(value)
518
+ } else {
519
+ CmdResult::from_json(value, false)
520
+ })
521
+ }
522
+
486
523
  /// `cmd_allow_peer_talk`(`parser.py allow-peer-talk`).
487
524
  pub fn cmd_allow_peer_talk(args: &AllowPeerTalkArgs) -> Result<CmdResult, CliError> {
488
525
  if args.team.is_some() {
@@ -1485,7 +1522,13 @@ pub fn cmd_doctor(args: &DoctorArgs) -> Result<CmdResult, CliError> {
1485
1522
  } else if args.fix_schema {
1486
1523
  diagnose_port::fix_schema(&args.workspace)?
1487
1524
  } else {
1488
- diagnose_port::doctor(&args.workspace, args.spec.as_deref())?
1525
+ let mut value = diagnose_port::doctor(&args.workspace, args.spec.as_deref())?;
1526
+ crate::cli::diagnose::append_selected_live_leader_workspace_mismatch(
1527
+ &args.workspace,
1528
+ args.team.as_deref(),
1529
+ &mut value,
1530
+ );
1531
+ value
1489
1532
  };
1490
1533
  Ok(CmdResult::from_json(value, args.json))
1491
1534
  }
@@ -1,5 +1,10 @@
1
1
  //! diagnose/preflight/wait-ready CLI helpers.
2
2
  use super::*;
3
+ use crate::model::pane_authority_refusal::{
4
+ PaneAuthorityRecoveryAction, PaneAuthorityRecoveryHint, PaneAuthorityRefusal,
5
+ PaneAuthorityRefusalFacts, PaneAuthorityRefusalField, ACTION_FIELD, ACTION_REQUIRED_FIELD,
6
+ HINT_ACTION_FIELD, REASON_FIELD,
7
+ };
3
8
  use crate::provider::wire::{command_name, parse_provider, provider_wire};
4
9
  use crate::transport::Transport;
5
10
 
@@ -195,12 +200,190 @@ pub(crate) fn diagnose_runtime_for_workspace(
195
200
  backend: &dyn Transport,
196
201
  ) -> (Value, Value) {
197
202
  let (mut issues, mut repairs) = diagnose_runtime(state, backend);
203
+ append_live_leader_workspace_mismatch_issue(
204
+ workspace,
205
+ state,
206
+ backend,
207
+ &mut issues,
208
+ &mut repairs,
209
+ );
198
210
  append_legacy_snapshot_issue(workspace, state, &mut issues);
199
211
  append_coordinator_health_issue(workspace, state, &mut issues, &mut repairs);
200
212
  append_runtime_bindings_stale_after_boot_issue(workspace, state, &mut issues, &mut repairs);
201
213
  (issues, repairs)
202
214
  }
203
215
 
216
+ fn append_live_leader_workspace_mismatch_issue(
217
+ workspace: &std::path::Path,
218
+ state: &Value,
219
+ backend: &dyn Transport,
220
+ issues: &mut Value,
221
+ repairs: &mut Value,
222
+ ) {
223
+ let Some((issue, repair)) = live_leader_workspace_mismatch(workspace, state, backend) else {
224
+ return;
225
+ };
226
+ if let Some(items) = issues.as_array_mut() {
227
+ items.push(issue);
228
+ }
229
+ if let Some(items) = repairs.as_array_mut() {
230
+ items.push(repair);
231
+ }
232
+ }
233
+
234
+ fn live_leader_workspace_mismatch(
235
+ workspace: &std::path::Path,
236
+ state: &Value,
237
+ backend: &dyn Transport,
238
+ ) -> Option<(Value, Value)> {
239
+ let receiver = state.get("leader_receiver")?;
240
+ let facts = match crate::messaging::resolve_live_leader_channel(workspace, receiver, backend) {
241
+ crate::messaging::LeaderChannelResolution::Unbound(
242
+ crate::messaging::LeaderChannelUnbound::PaneWorkspaceMismatch(facts),
243
+ ) => facts,
244
+ _ => return None,
245
+ };
246
+ let team = state
247
+ .get("team_key")
248
+ .and_then(Value::as_str)
249
+ .filter(|team| !team.is_empty())
250
+ .unwrap_or("current");
251
+ let provider = receiver
252
+ .get("provider")
253
+ .and_then(Value::as_str)
254
+ .filter(|provider| !provider.is_empty())
255
+ .unwrap_or("claude");
256
+ let refusal =
257
+ PaneAuthorityRefusal::new(PaneAuthorityRefusalFacts::PaneWorkspaceMismatch(facts));
258
+ let PaneAuthorityRefusalFacts::PaneWorkspaceMismatch(facts) = &refusal.facts else {
259
+ return None;
260
+ };
261
+ let mut fields = serde_json::Map::new();
262
+ fields.insert(
263
+ REASON_FIELD.to_string(),
264
+ Value::String(refusal.reason().as_str().to_string()),
265
+ );
266
+ fields.insert(
267
+ PaneAuthorityRefusalField::RequestedWorkspace
268
+ .as_str()
269
+ .to_string(),
270
+ Value::String(facts.requested_workspace.to_string_lossy().into_owned()),
271
+ );
272
+ fields.insert(
273
+ PaneAuthorityRefusalField::ObservedPaneId
274
+ .as_str()
275
+ .to_string(),
276
+ Value::String(facts.observed_pane_id.clone()),
277
+ );
278
+ fields.insert(
279
+ PaneAuthorityRefusalField::ObservedPaneWorkspace
280
+ .as_str()
281
+ .to_string(),
282
+ Value::String(facts.observed_pane_workspace.to_string_lossy().into_owned()),
283
+ );
284
+ fields.insert(
285
+ PaneAuthorityRefusalField::Endpoint.as_str().to_string(),
286
+ Value::String(facts.endpoint.clone()),
287
+ );
288
+ let mut issue = Value::Object(fields.clone());
289
+ if let Some(object) = issue.as_object_mut() {
290
+ object.insert(
291
+ "id".to_string(),
292
+ Value::String("leader_channel_workspace_mismatch".to_string()),
293
+ );
294
+ }
295
+ let mut repair = Value::Object(fields);
296
+ if let Some(object) = repair.as_object_mut() {
297
+ object.insert(
298
+ "issue".to_string(),
299
+ Value::String("leader_channel_workspace_mismatch".to_string()),
300
+ );
301
+ object.insert(
302
+ ACTION_REQUIRED_FIELD.to_string(),
303
+ Value::Bool(refusal.recovery.action_required),
304
+ );
305
+ object.insert("advisory".to_string(), Value::Bool(true));
306
+ object.insert(
307
+ "broken_class".to_string(),
308
+ Value::String("leader_channel_workspace_mismatch".to_string()),
309
+ );
310
+ object.insert(
311
+ HINT_ACTION_FIELD.to_string(),
312
+ Value::String(
313
+ match refusal.recovery.hint_action {
314
+ PaneAuthorityRecoveryHint::AttachLeader => "team-agent attach-leader",
315
+ }
316
+ .to_string(),
317
+ ),
318
+ );
319
+ object.insert(
320
+ "dedupe_key".to_string(),
321
+ Value::String(format!("{team}:leader_channel_workspace_mismatch")),
322
+ );
323
+ object.insert(
324
+ ACTION_FIELD.to_string(),
325
+ Value::String(match refusal.recovery.action {
326
+ PaneAuthorityRecoveryAction::OpenTerminalOutsideCurrentTmuxPaneOrAttachFromMatchingPane => {
327
+ format!(
328
+ "open a new terminal window outside the current tmux/pane, change \
329
+ directory to the requested workspace, then run \
330
+ `team-agent attach-leader --team {team} --provider {provider} \
331
+ --confirm --json`; then rerun \
332
+ `team-agent diagnose --team {team} --json`"
333
+ )
334
+ }
335
+ }),
336
+ );
337
+ }
338
+ Some((issue, repair))
339
+ }
340
+
341
+ pub(crate) fn append_selected_live_leader_workspace_mismatch(
342
+ workspace: &std::path::Path,
343
+ team: Option<&str>,
344
+ report: &mut Value,
345
+ ) {
346
+ let Some((issue, repair)) = selected_live_leader_workspace_mismatch(workspace, team) else {
347
+ return;
348
+ };
349
+ let Some(object) = report.as_object_mut() else {
350
+ return;
351
+ };
352
+ object.insert("issues".to_string(), Value::Array(vec![issue]));
353
+ object.insert("suggested_repairs".to_string(), Value::Array(vec![repair]));
354
+ object.insert("ok".to_string(), Value::Bool(false));
355
+ if object.get("error").is_none_or(Value::is_null) {
356
+ object.insert(
357
+ "error".to_string(),
358
+ Value::String("PaneWorkspaceMismatch".to_string()),
359
+ );
360
+ }
361
+ }
362
+
363
+ fn selected_live_leader_workspace_mismatch(
364
+ workspace: &std::path::Path,
365
+ team: Option<&str>,
366
+ ) -> Option<(Value, Value)> {
367
+ let selected = crate::state::selector::resolve_active_team(
368
+ workspace,
369
+ team,
370
+ crate::state::selector::SelectorMode::RuntimeOnly,
371
+ )
372
+ .ok()?;
373
+ let backend: Box<dyn crate::transport::Transport> =
374
+ match crate::transport_factory::resolve_read_only_transport(
375
+ &selected.run_workspace,
376
+ Some(&selected.state),
377
+ crate::transport_factory::TransportPurpose::Diagnose,
378
+ ) {
379
+ Ok(resolved) => resolved.backend,
380
+ Err(_) => Box::new(crate::tmux_backend::TmuxBackend::for_workspace(
381
+ &selected.run_workspace,
382
+ )),
383
+ };
384
+ live_leader_workspace_mismatch(&selected.run_workspace, &selected.state, backend.as_ref())
385
+ }
386
+
204
387
  /// 0.5.41 Slice 1 (fault-invisibility-locate.md §5/§6.2): read the
205
388
  /// coordinator heartbeat sidecar for the last-observed host boot
206
389
  /// identity and compare it against the current host boot. If they
@@ -9,13 +9,25 @@ use std::io::{ErrorKind, Write as _};
9
9
  /// 否则 dict 逐键 `key: value`(嵌套 dict/list 内联 compact json,`ensure_ascii=False`)、非 dict 直接 print。
10
10
  /// 返回应打印到 stdout 的字符串(bin main 负责实际 println)。
11
11
  pub fn emit(output: &CmdOutput, as_json: bool) -> Option<String> {
12
+ emit_with_json_order(output, as_json, false)
13
+ }
14
+
15
+ fn emit_with_json_order(
16
+ output: &CmdOutput,
17
+ as_json: bool,
18
+ preserve_json_order: bool,
19
+ ) -> Option<String> {
12
20
  match output {
13
21
  CmdOutput::None => None,
14
22
  CmdOutput::Human(text) => Some(crate::redaction::redact_external_text(text)),
15
23
  CmdOutput::Json(value) => {
16
24
  let value = crate::redaction::redact_external_value(value);
17
25
  if as_json {
18
- return serde_json::to_string_pretty(&sort_json(&value)).ok();
26
+ return if preserve_json_order {
27
+ serde_json::to_string_pretty(&value).ok()
28
+ } else {
29
+ serde_json::to_string_pretty(&sort_json(&value)).ok()
30
+ };
19
31
  }
20
32
  if let Value::Object(obj) = value {
21
33
  let lines: Vec<String> = obj
@@ -84,7 +96,7 @@ fn emit_result(r: CmdResult) -> ExitCode {
84
96
  .map(ToString::to_string),
85
97
  _ => None,
86
98
  };
87
- if let Some(text) = emit(&r.output, r.as_json) {
99
+ if let Some(text) = emit_with_json_order(&r.output, r.as_json, r.preserve_json_order) {
88
100
  if let Err(error) = write_stdout_line(&text) {
89
101
  if let Some(message_id) = persisted_message_id {
90
102
  let stderr = std::io::stderr();
@@ -180,6 +192,7 @@ fn dispatch(command: &str, args: &[String], cwd: &Path) -> Result<ExitCode, CliE
180
192
  cmd_collect_for_team(&collect_args(args, cwd)?, parse_args(args).team.as_deref())
181
193
  .map(emit_result)
182
194
  }
195
+ "results" => cmd_results(&results_args(args, cwd)?).map(emit_result),
183
196
  "diagnose" => cmd_diagnose(&diagnose_args(args, cwd)).map(emit_result),
184
197
  "preflight" => cmd_preflight(&preflight_args(args, cwd)).map(emit_result),
185
198
  "wait-ready" => cmd_wait_ready(&wait_ready_args(args, cwd)).map(emit_result),
@@ -226,6 +239,7 @@ const DISPATCH_COMMANDS: &[&str] = &[
226
239
  "install-skill",
227
240
  "profile",
228
241
  "collect",
242
+ "results",
229
243
  "diagnose",
230
244
  "preflight",
231
245
  "wait-ready",
@@ -271,7 +285,7 @@ fn default_help() -> String {
271
285
  append_help_section(
272
286
  &mut out,
273
287
  "Core",
274
- &["quick-start", "send", "status", "collect"],
288
+ &["quick-start", "send", "status", "collect", "results"],
275
289
  );
276
290
  append_help_section(
277
291
  &mut out,
@@ -346,10 +360,10 @@ fn command_help(command: Option<&str>) -> String {
346
360
  Some("send") => concat!(
347
361
  "usage: team-agent send TO MESSAGE... ",
348
362
  "[--workspace WORKSPACE] [--team TEAM] ",
349
- "[--presentation-sink leader|casefile|silent --message-class CLASS [--case-id CASE]] ",
363
+ "[--mailbox] ",
350
364
  "[--json]\n\n",
351
365
  "TO is a logical recipient; send returns after the message is persisted. ",
352
- "Presentation changes live display only; every sink remains durable."
366
+ "--mailbox stores durably without live injection; omitted sends to the live conversation."
353
367
  )
354
368
  .to_string(),
355
369
  Some("allow-peer-talk") => "usage: team-agent allow-peer-talk A B [--workspace WORKSPACE] [--json]".to_string(),
@@ -383,6 +397,7 @@ fn command_help(command: Option<&str>) -> String {
383
397
  Some("install-skill") => "usage: team-agent install-skill (--source DIR | --uninstall) [--target codex|claude|copilot|all] [--dest DIR] [--dry-run] [--json]".to_string(),
384
398
  Some("profile") => "usage: team-agent profile COMMAND NAME [--workspace WORKSPACE] [--team TEAM] [--auth-mode MODE] [--json]".to_string(),
385
399
  Some("collect") => "usage: team-agent collect [--workspace WORKSPACE] [--team TEAM] [--result-file FILE] [--json]".to_string(),
400
+ Some("results") => "usage: team-agent results --case CASE_ID [--workspace WORKSPACE] [--team TEAM] [--json]".to_string(),
386
401
  Some("diagnose") => "usage: team-agent diagnose [--workspace WORKSPACE] [--team TEAM] [--json]".to_string(),
387
402
  Some("preflight") => "usage: team-agent preflight [TEAMDIR] [--json]".to_string(),
388
403
  Some("wait-ready") => "usage: team-agent wait-ready [--workspace WORKSPACE] [--team TEAM] [--timeout SECONDS] [--json]".to_string(),
@@ -806,6 +821,7 @@ struct ParsedArgs {
806
821
  socket: Option<String>,
807
822
  thread_id: Option<String>,
808
823
  message_id: Option<String>,
824
+ mailbox: bool,
809
825
  presentation_sink: Option<String>,
810
826
  message_class: Option<String>,
811
827
  case_id: Option<String>,
@@ -899,6 +915,7 @@ fn parse_args(args: &[String]) -> ParsedArgs {
899
915
  "--socket" => parsed.socket = next_arg(args, &mut i),
900
916
  "--thread-id" => parsed.thread_id = next_arg(args, &mut i),
901
917
  "--message-id" => parsed.message_id = next_arg(args, &mut i),
918
+ "--mailbox" => parsed.mailbox = true,
902
919
  "--presentation-sink" => parsed.presentation_sink = next_arg(args, &mut i),
903
920
  "--message-class" => parsed.message_class = next_arg(args, &mut i),
904
921
  "--case-id" => parsed.case_id = next_arg(args, &mut i),
@@ -1089,10 +1106,14 @@ fn send_args(args: &[String], cwd: &Path) -> Result<SendArgs, CliError> {
1089
1106
  } else {
1090
1107
  None
1091
1108
  };
1092
- let (presentation, presentation_error) =
1093
- crate::messaging::presentation::normalize_presentation(presentation_value.as_ref());
1094
- if let Some(error) = presentation_error {
1095
- return Err(CliError::Usage(format!("invalid presentation: {error}")));
1109
+ let mailbox_value = parsed.mailbox.then(|| serde_json::json!(true));
1110
+ let normalized = crate::messaging::presentation::normalize_send_presentation(
1111
+ mailbox_value.as_ref(),
1112
+ presentation_value.as_ref(),
1113
+ )
1114
+ .map_err(|error| CliError::Usage(format!("invalid send routing: {error}")))?;
1115
+ if let Some(deprecation) = normalized.deprecation {
1116
+ eprintln!("warning: {deprecation}");
1096
1117
  }
1097
1118
  Ok(SendArgs {
1098
1119
  target,
@@ -1114,7 +1135,7 @@ fn send_args(args: &[String], cwd: &Path) -> Result<SendArgs, CliError> {
1114
1135
  confirm_human: false,
1115
1136
  json: parsed.json,
1116
1137
  message_id: None,
1117
- presentation,
1138
+ presentation: normalized.request,
1118
1139
  pane: parsed.pane.clone(),
1119
1140
  to_name: parsed.to_name.clone(),
1120
1141
  to_leader: parsed.to_leader.clone(),
@@ -1177,6 +1198,7 @@ fn validate_send_flags(args: &[String]) -> Result<(), CliError> {
1177
1198
  "--timeout",
1178
1199
  "--confirm-human",
1179
1200
  "--message-id",
1201
+ "--mailbox",
1180
1202
  "--presentation-sink",
1181
1203
  "--message-class",
1182
1204
  "--case-id",
@@ -1593,6 +1615,27 @@ fn collect_args(args: &[String], cwd: &Path) -> Result<CollectArgs, CliError> {
1593
1615
  })
1594
1616
  }
1595
1617
 
1618
+ fn results_args(args: &[String], cwd: &Path) -> Result<ResultsArgs, CliError> {
1619
+ if args
1620
+ .iter()
1621
+ .any(|arg| arg == "--to" || arg.starts_with("--to="))
1622
+ {
1623
+ return Err(CliError::Usage(
1624
+ "results does not accept --to; it is a read-only command".to_string(),
1625
+ ));
1626
+ }
1627
+ let parsed = parse_args(args);
1628
+ let case_id = option_value(args, "--case")
1629
+ .filter(|value| !value.is_empty())
1630
+ .ok_or_else(|| CliError::Usage("missing --case".to_string()))?;
1631
+ Ok(ResultsArgs {
1632
+ case_id,
1633
+ workspace: workspace(&parsed, cwd),
1634
+ team: parsed.team,
1635
+ json: parsed.json,
1636
+ })
1637
+ }
1638
+
1596
1639
  fn option_value(args: &[String], flag: &str) -> Option<String> {
1597
1640
  let prefix = format!("{flag}=");
1598
1641
  let mut i = 0usize;
@@ -2093,8 +2136,14 @@ mod tests {
2093
2136
  }
2094
2137
 
2095
2138
  #[test]
2096
- fn send_presentation_flags_map_to_the_typed_request() {
2139
+ fn send_mailbox_and_legacy_progress_map_to_the_one_bit_request() {
2097
2140
  let cwd = tmp_workspace();
2141
+ let mailbox = send_args(&cli_argv(&["leader", "progress", "--mailbox"]), &cwd).unwrap();
2142
+ assert_eq!(
2143
+ mailbox.presentation.sink,
2144
+ crate::messaging::presentation::PresentationSink::Casefile
2145
+ );
2146
+
2098
2147
  let args = send_args(
2099
2148
  &cli_argv(&[
2100
2149
  "leader",
@@ -2115,9 +2164,9 @@ mod tests {
2115
2164
  );
2116
2165
  assert_eq!(
2117
2166
  args.presentation.class,
2118
- crate::messaging::presentation::PresentationClass::Progress
2167
+ crate::messaging::presentation::PresentationClass::Message
2119
2168
  );
2120
- assert_eq!(args.presentation.case_id.as_deref(), Some("case-9"));
2169
+ assert_eq!(args.presentation.case_id, None);
2121
2170
  }
2122
2171
 
2123
2172
  #[test]
@@ -2130,7 +2179,7 @@ mod tests {
2130
2179
  .unwrap_err();
2131
2180
  assert!(matches!(
2132
2181
  error,
2133
- CliError::Usage(message) if message == "invalid presentation: missing_class"
2182
+ CliError::Usage(message) if message == "invalid send routing: missing_class"
2134
2183
  ));
2135
2184
  }
2136
2185
 
@@ -39,6 +39,8 @@ pub fn leader_launcher_args(values: &[String]) -> Result<LeaderLauncherArgs, Cli
39
39
  out.confirm_attach = true;
40
40
  } else if token == "--external-leader" {
41
41
  out.external_leader = true;
42
+ } else if token == "--allow-nested-attach" {
43
+ out.allow_nested_attach = true;
42
44
  } else if token == "--attach-session" {
43
45
  let Some(value) = values.get(idx + 1) else {
44
46
  return Err(CliError::Runtime(
@@ -60,7 +62,12 @@ pub fn leader_launcher_args(values: &[String]) -> Result<LeaderLauncherArgs, Cli
60
62
  fn is_leader_launcher_flag(value: &str) -> bool {
61
63
  matches!(
62
64
  value,
63
- "--external-leader" | "--attach" | "--attach-existing" | "--confirm" | "--attach-session"
65
+ "--external-leader"
66
+ | "--allow-nested-attach"
67
+ | "--attach"
68
+ | "--attach-existing"
69
+ | "--confirm"
70
+ | "--attach-session"
64
71
  ) || value.starts_with("--attach-session=")
65
72
  }
66
73