@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
@@ -37,6 +37,120 @@ pub fn cmd_send(args: &SendArgs) -> Result<CmdResult, CliError> {
37
37
  Ok(CmdResult::from_json(value, args.json))
38
38
  }
39
39
 
40
+ pub fn cmd_fallback_send_leader(args: &FallbackSendLeaderArgs) -> Result<CmdResult, CliError> {
41
+ if let Some(value) = fallback_business_refusal(&args.primary_error, args.json) {
42
+ return Ok(value);
43
+ }
44
+ let selected = crate::state::selector::resolve_active_team(
45
+ &args.workspace,
46
+ args.team.as_deref(),
47
+ crate::state::selector::SelectorMode::RuntimeOnly,
48
+ )?;
49
+ let target = MessageTarget::Single("leader".to_string());
50
+ let opts = SendOptions {
51
+ task_id: args.task.as_ref().map(|task| TaskId::new(task.clone())),
52
+ route_task_id: false,
53
+ sender: args.sender.clone(),
54
+ requires_ack: false,
55
+ wait_visible: false,
56
+ block_until_delivered: false,
57
+ team: Some(TeamKey::new(selected.team_key.clone())),
58
+ message_id: Some(args.message_id.clone()),
59
+ ..SendOptions::default()
60
+ };
61
+ let primary = messaging::send_message(&selected.run_workspace, &target, &args.content, &opts);
62
+ let message_id = match &primary {
63
+ Ok(outcome) => outcome
64
+ .message_id
65
+ .clone()
66
+ .unwrap_or_else(|| args.message_id.clone()),
67
+ Err(_) => args.message_id.clone(),
68
+ };
69
+ if let Ok(outcome) = &primary {
70
+ if is_business_refusal_outcome(outcome) {
71
+ let value = json!({
72
+ "ok": false,
73
+ "status": "refused",
74
+ "reason": "business_reject",
75
+ "primary_error": args.primary_error,
76
+ "message_id": outcome.message_id,
77
+ "action": "N38 fallback refused: business rule refusals must not use fallback pane delivery",
78
+ });
79
+ return Ok(CmdResult::from_json(value, args.json));
80
+ }
81
+ }
82
+
83
+ let state = selected_state_with_active_key(&selected);
84
+ let event_log = crate::event_log::EventLog::new(&selected.run_workspace);
85
+ let primary_error = match primary {
86
+ Ok(outcome) if primary_delivery_succeeded(outcome.status) => {
87
+ let mut value = delivery_outcome_json(&outcome, &target, &args.content, &opts);
88
+ if let Some(obj) = value.as_object_mut() {
89
+ obj.insert("fallback_used".to_string(), json!(false));
90
+ obj.insert("primary_error".to_string(), json!(args.primary_error));
91
+ }
92
+ return Ok(CmdResult::from_json(value, args.json));
93
+ }
94
+ Ok(outcome) => format!(
95
+ "{}; fallback_cli_primary_status={}",
96
+ args.primary_error,
97
+ delivery_status_wire(outcome.status)
98
+ ),
99
+ Err(error) => format!("{}; fallback_cli_primary_error={error}", args.primary_error),
100
+ };
101
+ let outcome = messaging::deliver_to_leader_fallback_pane(
102
+ &selected.run_workspace,
103
+ &state,
104
+ &message_id,
105
+ None,
106
+ &args.content,
107
+ false,
108
+ Some(&primary_error),
109
+ &event_log,
110
+ )?;
111
+ let mut value = delivery_outcome_json(&outcome, &target, &args.content, &opts);
112
+ if let Some(obj) = value.as_object_mut() {
113
+ obj.insert("primary_error".to_string(), json!(args.primary_error));
114
+ obj.insert("delivered_via".to_string(), json!("fallback_pane"));
115
+ obj.insert(
116
+ "next_action".to_string(),
117
+ json!("run team-agent restart-agent to refresh the worker MCP transport"),
118
+ );
119
+ }
120
+ Ok(CmdResult::from_json(value, args.json))
121
+ }
122
+
123
+ pub fn cmd_fallback_report_result(args: &FallbackReportResultArgs) -> Result<CmdResult, CliError> {
124
+ if let Some(value) = fallback_business_refusal(&args.primary_error, args.json) {
125
+ return Ok(value);
126
+ }
127
+ let selected = crate::state::selector::resolve_active_team(
128
+ &args.workspace,
129
+ args.team.as_deref(),
130
+ crate::state::selector::SelectorMode::RuntimeOnly,
131
+ )?;
132
+ let envelope = fallback_result_envelope(args)?;
133
+ let value = messaging::report_result_for_owner_team_with_primary_error(
134
+ &selected.run_workspace,
135
+ &envelope,
136
+ Some(&selected.team_key),
137
+ Some(&args.primary_error),
138
+ )?;
139
+ let mut value = value;
140
+ if let Some(obj) = value.as_object_mut() {
141
+ obj.insert("primary_error".to_string(), json!(args.primary_error));
142
+ obj.insert(
143
+ "fallback_protocol".to_string(),
144
+ json!("fallback-report-result"),
145
+ );
146
+ obj.insert(
147
+ "next_action".to_string(),
148
+ json!("run team-agent restart-agent to refresh the worker MCP transport"),
149
+ );
150
+ }
151
+ Ok(CmdResult::from_json(value, args.json))
152
+ }
153
+
40
154
  fn routing_ambiguous_value(
41
155
  workspace: &Path,
42
156
  args: &SendArgs,
@@ -77,6 +191,101 @@ fn routing_ambiguous_value(
77
191
  }))
78
192
  }
79
193
 
194
+ fn selected_state_with_active_key(selected: &crate::state::selector::SelectedTeam) -> Value {
195
+ let mut state = selected.state.clone();
196
+ if let Some(obj) = state.as_object_mut() {
197
+ obj.insert(
198
+ "active_team_key".to_string(),
199
+ Value::String(selected.team_key.clone()),
200
+ );
201
+ }
202
+ state
203
+ }
204
+
205
+ fn fallback_result_envelope(args: &FallbackReportResultArgs) -> Result<Value, CliError> {
206
+ let mut envelope: Value = serde_json::from_str(&args.result_json)?;
207
+ let Some(obj) = envelope.as_object_mut() else {
208
+ return Err(CliError::Usage(
209
+ "--result-json must be a JSON object".to_string(),
210
+ ));
211
+ };
212
+ obj.entry("schema_version".to_string())
213
+ .or_insert_with(|| json!("result_envelope_v1"));
214
+ obj.entry("task_id".to_string())
215
+ .or_insert_with(|| json!(args.task_id));
216
+ obj.entry("agent_id".to_string())
217
+ .or_insert_with(|| json!(args.agent_id));
218
+ obj.entry("status".to_string())
219
+ .or_insert_with(|| json!("success"));
220
+ obj.entry("summary".to_string())
221
+ .or_insert_with(|| json!("completed"));
222
+ for key in ["changes", "tests", "risks", "artifacts", "next_actions"] {
223
+ obj.entry(key.to_string()).or_insert_with(|| json!([]));
224
+ }
225
+ Ok(envelope)
226
+ }
227
+
228
+ fn fallback_business_refusal(primary_error: &str, as_json: bool) -> Option<CmdResult> {
229
+ is_business_reject_text(primary_error).then(|| {
230
+ CmdResult::from_json(
231
+ json!({
232
+ "ok": false,
233
+ "status": "refused",
234
+ "reason": "business_reject",
235
+ "primary_error": primary_error,
236
+ "action": "N38 fallback refused: business rule refusals must not use fallback pane delivery",
237
+ }),
238
+ as_json,
239
+ )
240
+ })
241
+ }
242
+
243
+ fn is_business_refusal_outcome(outcome: &DeliveryOutcome) -> bool {
244
+ matches!(
245
+ outcome.reason,
246
+ Some(
247
+ DeliveryRefusal::TargetNotInTeam
248
+ | DeliveryRefusal::HumanConfirmationRequired
249
+ | DeliveryRefusal::MissingPermissions
250
+ | DeliveryRefusal::UnknownRecipient
251
+ | DeliveryRefusal::TeamOwnerMismatch
252
+ | DeliveryRefusal::Ambiguous
253
+ | DeliveryRefusal::RecipientPaneInNonInputMode
254
+ | DeliveryRefusal::SessionDrift
255
+ | DeliveryRefusal::RoutingAmbiguous
256
+ | DeliveryRefusal::EmptyTargetList
257
+ )
258
+ )
259
+ }
260
+
261
+ fn primary_delivery_succeeded(status: DeliveryStatus) -> bool {
262
+ matches!(
263
+ status,
264
+ DeliveryStatus::Delivered | DeliveryStatus::AlreadyDelivered
265
+ )
266
+ }
267
+
268
+ fn is_business_reject_text(error: &str) -> bool {
269
+ let lower = error.to_ascii_lowercase();
270
+ [
271
+ "peer_not_in_scope",
272
+ "target_not_in_team",
273
+ "permission denied",
274
+ "missing_permissions",
275
+ "human_confirmation_required",
276
+ "unknown_recipient",
277
+ "routing_ambiguous",
278
+ "quota",
279
+ "rate limit",
280
+ "rate_limit",
281
+ "blacklist",
282
+ "blacklisted",
283
+ "forbidden",
284
+ ]
285
+ .iter()
286
+ .any(|needle| lower.contains(needle))
287
+ }
288
+
80
289
  /// `_send_target`(`commands.py:181-184`):`--to` comma-split fanout / `target` 单值 / None。
81
290
  pub fn send_target(targets: Option<&str>, target: Option<&str>) -> MessageTarget {
82
291
  if let Some(targets) = targets.filter(|s| !s.is_empty()) {
@@ -221,3 +430,46 @@ fn delivery_stage_wire(stage: DeliveryStage) -> &'static str {
221
430
  DeliveryStage::VisibleCheck => "visible_check",
222
431
  }
223
432
  }
433
+
434
+ #[cfg(test)]
435
+ mod e23_tests {
436
+ use super::*;
437
+
438
+ #[test]
439
+ fn fallback_error_classifier_allows_transport_and_primary_bugs() {
440
+ for error in [
441
+ "Transport closed",
442
+ "Connection refused",
443
+ "Broken pipe",
444
+ "EOF on transport",
445
+ "MCP timeout after 5s",
446
+ "internal assertion failed: unwrap on Err",
447
+ "primary_delivery_error: serialize failed",
448
+ ] {
449
+ assert!(
450
+ !is_business_reject_text(error),
451
+ "failure should be fallback-eligible, not classified as a business refusal: {error}"
452
+ );
453
+ }
454
+ }
455
+
456
+ #[test]
457
+ fn fallback_error_classifier_blocks_business_refusals() {
458
+ for error in [
459
+ "peer_not_in_scope",
460
+ "target_not_in_team",
461
+ "permission denied",
462
+ "missing_permissions",
463
+ "human_confirmation_required",
464
+ "unknown_recipient",
465
+ "quota exceeded",
466
+ "rate_limit",
467
+ "blacklisted target",
468
+ ] {
469
+ assert!(
470
+ is_business_reject_text(error),
471
+ "business refusal must not use fallback pane delivery: {error}"
472
+ );
473
+ }
474
+ }
475
+ }
@@ -206,6 +206,7 @@ fn current_uid() -> Option<String> {
206
206
  team_id: None,
207
207
  yes: true,
208
208
  fresh: false,
209
+ no_display: false,
209
210
  json: true,
210
211
  };
211
212
  let _ = cmd_quick_start(&args); // real quick_start compiles the spec before any coordinator/launch step
@@ -240,6 +241,7 @@ fn current_uid() -> Option<String> {
240
241
  team_id: None,
241
242
  yes: false,
242
243
  fresh: false,
244
+ no_display: false,
243
245
  json: true,
244
246
  };
245
247
  let text = outcome_text(cmd_quick_start(&args));
@@ -275,15 +277,25 @@ fn current_uid() -> Option<String> {
275
277
  );
276
278
  }
277
279
 
278
- // 4 [P1] — cmd_add_agent with a DUPLICATE agent id surfaces the REAL crate::lifecycle::add_agent
279
- // "agent id already exists" error (launch.rs:252). The placeholder returns {ok:true} -> RED.
280
- // OS-safe (the dup check fires before compile/spawn).
280
+ // 4 [P1] — cmd_add_agent with a DUPLICATE runtime agent id surfaces the REAL
281
+ // crate::lifecycle::add_agent "agent id already exists" error. E25: role docs/spec files are
282
+ // inputs, not membership; duplicate membership is runtime-state based.
281
283
  #[test]
282
284
  fn cli_add_agent_duplicate_id_surfaces_real_error() {
283
285
  let team = deleg_uniq_dir("addagent");
284
286
  std::fs::create_dir_all(team.join("agents")).unwrap();
285
287
  std::fs::write(team.join("TEAM.md"), DELEG_TEAM_MD).unwrap();
286
288
  std::fs::write(team.join("agents").join("implementer.md"), DELEG_VALID_ROLE).unwrap(); // 'implementer' already exists
289
+ crate::state::persist::save_runtime_state(
290
+ &team,
291
+ &json!({
292
+ "session_name": "team-deleg-addagent",
293
+ "agents": {
294
+ "implementer": {"status": "running", "provider": "codex", "window": "implementer"}
295
+ }
296
+ }),
297
+ )
298
+ .unwrap();
287
299
  let dup_role = team.join("dup-role.md");
288
300
  std::fs::write(&dup_role, DELEG_VALID_ROLE).unwrap(); // role file must exist
289
301
  let args = AddAgentArgs {
@@ -158,11 +158,33 @@ fn tmp_shutdown_workspace(tag: &str) -> PathBuf {
158
158
 
159
159
  struct CleanShutdownTransport {
160
160
  session_present: Mutex<bool>,
161
+ targets: Vec<PaneInfo>,
162
+ kill_server_called: Mutex<bool>,
163
+ probe_timeout_kind: Option<&'static str>,
161
164
  }
162
165
 
163
166
  impl CleanShutdownTransport {
164
167
  fn new() -> Self {
165
- Self { session_present: Mutex::new(true) }
168
+ Self {
169
+ session_present: Mutex::new(true),
170
+ targets: Vec::new(),
171
+ kill_server_called: Mutex::new(false),
172
+ probe_timeout_kind: None,
173
+ }
174
+ }
175
+
176
+ fn with_targets(mut self, targets: Vec<PaneInfo>) -> Self {
177
+ self.targets = targets;
178
+ self
179
+ }
180
+
181
+ fn kill_server_called(&self) -> bool {
182
+ *self.kill_server_called.lock().unwrap()
183
+ }
184
+
185
+ fn with_probe_timeout(mut self, probe: &'static str) -> Self {
186
+ self.probe_timeout_kind = Some(probe);
187
+ self
166
188
  }
167
189
  }
168
190
 
@@ -227,7 +249,11 @@ impl Transport for CleanShutdownTransport {
227
249
  }
228
250
 
229
251
  fn list_targets(&self) -> Result<Vec<PaneInfo>, TransportError> {
230
- Ok(Vec::new())
252
+ if *self.session_present.lock().unwrap() {
253
+ Ok(self.targets.clone())
254
+ } else {
255
+ Ok(Vec::new())
256
+ }
231
257
  }
232
258
 
233
259
  fn has_session(&self, _session: &SessionName) -> Result<bool, TransportError> {
@@ -248,10 +274,18 @@ impl Transport for CleanShutdownTransport {
248
274
  }
249
275
 
250
276
  fn kill_session(&self, _session: &SessionName) -> Result<(), TransportError> {
277
+ if let Some(probe) = self.probe_timeout_kind {
278
+ crate::os_probe::set_probe_timeout_for_test(probe, None, 900);
279
+ }
251
280
  *self.session_present.lock().unwrap() = false;
252
281
  Ok(())
253
282
  }
254
283
 
284
+ fn kill_server(&self) -> Result<(), TransportError> {
285
+ *self.kill_server_called.lock().unwrap() = true;
286
+ Ok(())
287
+ }
288
+
255
289
  fn kill_window(&self, _target: &Target) -> Result<(), TransportError> {
256
290
  Ok(())
257
291
  }
@@ -260,3 +294,126 @@ impl Transport for CleanShutdownTransport {
260
294
  Ok(AttachOutcome::Attached)
261
295
  }
262
296
  }
297
+
298
+ #[test]
299
+ fn lsof_cwd_timeout_is_diagnostic_not_shutdown_partial() {
300
+ let ws = tmp_shutdown_workspace("lsof-cwd-timeout-diagnostic");
301
+ crate::state::persist::save_runtime_state(
302
+ &ws,
303
+ &json!({
304
+ "session_name": "team-lsof-cwd-timeout",
305
+ "agents": {
306
+ "fake_impl": {
307
+ "status": "running",
308
+ "provider": "fake",
309
+ "window": "fake_impl"
310
+ }
311
+ }
312
+ }),
313
+ )
314
+ .unwrap();
315
+
316
+ let out = crate::cli::lifecycle_port::shutdown_with_transport(
317
+ &ws,
318
+ true,
319
+ None,
320
+ &CleanShutdownTransport::new().with_probe_timeout("lsof_cwd"),
321
+ )
322
+ .expect("shutdown should complete");
323
+
324
+ assert_eq!(out["ok"], json!(true));
325
+ assert_eq!(out["status"], json!("ok"));
326
+ assert_eq!(out["phase"], json!(null));
327
+ assert_eq!(out["verification_degraded"], json!(true));
328
+ assert_eq!(out["probe_timeout_kind"], json!("lsof_cwd"));
329
+ assert_eq!(out["residuals"]["sessions"], json!([]));
330
+ assert_eq!(out["residuals"]["processes"], json!([]));
331
+
332
+ let events = crate::event_log::EventLog::new(&ws).tail(0).expect("events");
333
+ let shutdown = events
334
+ .iter()
335
+ .find(|event| {
336
+ event.get("event").and_then(serde_json::Value::as_str) == Some("lifecycle.shutdown")
337
+ })
338
+ .unwrap_or_else(|| panic!("missing lifecycle.shutdown event: {events:?}"));
339
+ assert_eq!(shutdown["status"], json!("ok"));
340
+ assert_eq!(shutdown["phase"], json!(null));
341
+ assert_eq!(shutdown["verification_degraded"], json!(true));
342
+ assert_eq!(shutdown["probe_timeout_kind"], json!("lsof_cwd"));
343
+ }
344
+
345
+ #[test]
346
+ fn ps_table_timeout_still_degrades_shutdown_truth() {
347
+ let ws = tmp_shutdown_workspace("ps-table-timeout-partial");
348
+ crate::state::persist::save_runtime_state(
349
+ &ws,
350
+ &json!({
351
+ "session_name": "team-ps-table-timeout",
352
+ "agents": {
353
+ "fake_impl": {
354
+ "status": "running",
355
+ "provider": "fake",
356
+ "window": "fake_impl"
357
+ }
358
+ }
359
+ }),
360
+ )
361
+ .unwrap();
362
+
363
+ let out = crate::cli::lifecycle_port::shutdown_with_transport(
364
+ &ws,
365
+ true,
366
+ None,
367
+ &CleanShutdownTransport::new().with_probe_timeout("ps_table"),
368
+ )
369
+ .expect("shutdown should complete");
370
+
371
+ assert_eq!(out["ok"], json!(false));
372
+ assert_eq!(out["status"], json!("partial"));
373
+ assert_eq!(out["phase"], json!("os_probe"));
374
+ assert_eq!(out["verification_degraded"], json!(true));
375
+ assert_eq!(out["probe_timeout_kind"], json!("ps_table"));
376
+ }
377
+
378
+ #[test]
379
+ fn leader_env_tmux_socket_never_kills_server_even_when_sessions_look_exclusive() {
380
+ let ws = tmp_shutdown_workspace("leader-env-socket-no-kill-server");
381
+ crate::state::persist::save_runtime_state(
382
+ &ws,
383
+ &json!({
384
+ "tmux_socket_source": "leader_env",
385
+ "agents": {
386
+ "fake_impl": {
387
+ "status": "running",
388
+ "provider": "fake",
389
+ "window": "fake_impl"
390
+ }
391
+ }
392
+ }),
393
+ )
394
+ .unwrap();
395
+ let transport = CleanShutdownTransport::new().with_targets(vec![PaneInfo {
396
+ pane_id: PaneId::new("%1"),
397
+ session: SessionName::new("team-layout"),
398
+ window_index: Some(0),
399
+ window_name: Some(WindowName::new("team-w1")),
400
+ pane_index: Some(0),
401
+ tty: None,
402
+ current_command: None,
403
+ current_path: None,
404
+ active: true,
405
+ pane_pid: None,
406
+ leader_env: BTreeMap::new(),
407
+ }]);
408
+
409
+ let out = crate::cli::lifecycle_port::shutdown_with_transport(&ws, true, None, &transport)
410
+ .expect("shutdown should complete");
411
+
412
+ assert_eq!(out["ok"], json!(true));
413
+ assert_eq!(out["killed_sessions"], json!(["team-layout"]));
414
+ assert_eq!(out["spared_sessions"], json!([]));
415
+ assert!(
416
+ !transport.kill_server_called(),
417
+ "leader-env/shared socket shutdown must kill sessions individually, never kill-server"
418
+ );
419
+ }
@@ -97,7 +97,8 @@ impl CliError {
97
97
  "tmux session `{session}` already exists. It may be an active team. Do not terminate existing tmux sessions from startup; use a different team name or runtime.session_name and start again."
98
98
  );
99
99
  payload.next_actions = Some(vec![
100
- "Use a different team name or runtime.session_name before starting again.".to_string(),
100
+ "Use a different team name or runtime.session_name before starting again."
101
+ .to_string(),
101
102
  ]);
102
103
  }
103
104
  }
@@ -262,6 +263,7 @@ pub struct QuickStartArgs {
262
263
  pub team_id: Option<String>,
263
264
  pub yes: bool,
264
265
  pub fresh: bool,
266
+ pub no_display: bool,
265
267
  pub json: bool,
266
268
  }
267
269
 
@@ -304,6 +306,33 @@ pub struct SendArgs {
304
306
  pub message_id: Option<String>,
305
307
  }
306
308
 
309
+ /// E23 worker-side emergency fallback for `team_orchestrator.send_message`
310
+ /// transport failures. This is not a general control-plane send path.
311
+ #[derive(Debug, Clone, PartialEq, Eq)]
312
+ pub struct FallbackSendLeaderArgs {
313
+ pub workspace: PathBuf,
314
+ pub team: Option<String>,
315
+ pub sender: String,
316
+ pub task: Option<String>,
317
+ pub message_id: String,
318
+ pub content: String,
319
+ pub primary_error: String,
320
+ pub json: bool,
321
+ }
322
+
323
+ /// E23 worker-side emergency fallback for `team_orchestrator.report_result`
324
+ /// transport failures. It must still persist through the results DB.
325
+ #[derive(Debug, Clone, PartialEq, Eq)]
326
+ pub struct FallbackReportResultArgs {
327
+ pub workspace: PathBuf,
328
+ pub team: Option<String>,
329
+ pub agent_id: String,
330
+ pub task_id: String,
331
+ pub result_json: String,
332
+ pub primary_error: String,
333
+ pub json: bool,
334
+ }
335
+
307
336
  /// `allow-peer-talk`(`parser.py`): allow direct peer communication between two agents.
308
337
  #[derive(Debug, Clone, PartialEq, Eq)]
309
338
  pub struct AllowPeerTalkArgs {
@@ -179,7 +179,7 @@ fn front_matter_non_object_errors() {
179
179
  // separators=(",",":"))` with the workspace path templated to __WS__.
180
180
  // (team-agent-public v0.2.11, /tmp/probe_compiler.py.)
181
181
 
182
- const BASE_NOPROFILE_JSON: &str = r#"{"version":1,"team":{"name":"doc-team","mode":"supervisor_worker","objective":"Compile role docs.","workspace":"__WS__"},"leader":{"id":"leader","role":"leader","provider":"codex","model":"gpt-5.5","tools":["fs_read","fs_list","mcp_team"],"context_policy":{"keep_user_thread":true,"receive_worker_outputs":"business_messages_and_short_summaries","max_worker_result_tokens":2000}},"agents":[{"id":"implementer","role":"Implementation Engineer","provider":"codex","model":"gpt-5.5","auth_mode":"subscription","working_directory":"__WS__","system_prompt":{"inline":"Implement bounded tasks and report result_envelope_v1.","file":null},"tools":["fs_read","fs_write","execute_bash","mcp_team"],"permission_mode":"restricted","preferred_for":["implementer","Implementation Engineer"],"avoid_for":[],"output_contract":{"format":"result_envelope_v1","required_fields":["task_id","status","summary","artifacts"]}}],"routing":{"default_assignee":"implementer","rules":[{"id":"route-implementer","match":{"assignee":["implementer"]},"assign_to":"implementer","priority":10}]},"communication":{"protocol":"mcp_inbox","topology":"leader_centered","worker_to_worker":true,"ack_timeout_sec":60,"result_format":"result_envelope_v1","message_store":{"sqlite":".team/runtime/team.db","mirror_files":".team/messages"}},"runtime":{"backend":"tmux","display_backend":"none","session_name":"team-doc-team","auto_launch":true,"require_user_approval_before_launch":true,"max_active_agents":1,"startup_order":["implementer"],"dangerous_auto_approve":false,"fast":false,"tick_interval_sec":2,"push_min_interval_sec":60,"stuck_timeout_sec":300},"context":{"state_file":"team_state.md","artifact_dir":".team/artifacts","log_dir":".team/logs","summarization":{"worker_full_logs":"retain_outside_leader_context","state_update":"after_each_result"}},"tasks":[{"id":"task_initial","title":"Initial document-driven team task","type":"implementation","assignee":"implementer","deps":[],"acceptance":["Worker reports valid result_envelope_v1"],"status":"pending","requires_tools":["mcp_team"],"files":[],"risk":"low"}]}"#;
182
+ const BASE_NOPROFILE_JSON: &str = r#"{"version":1,"team":{"name":"doc-team","mode":"supervisor_worker","objective":"Compile role docs.","workspace":"__WS__"},"leader":{"id":"leader","role":"leader","provider":"codex","model":"gpt-5.5","tools":["fs_read","fs_list","mcp_team"],"context_policy":{"keep_user_thread":true,"receive_worker_outputs":"business_messages_and_short_summaries","max_worker_result_tokens":2000}},"agents":[{"id":"implementer","role":"Implementation Engineer","provider":"codex","model":"gpt-5.5","auth_mode":"subscription","working_directory":"__WS__","system_prompt":{"inline":"Implement bounded tasks and report result_envelope_v1.","file":null},"tools":["fs_read","fs_write","execute_bash","mcp_team"],"permission_mode":"restricted","preferred_for":["implementer","Implementation Engineer"],"avoid_for":[],"output_contract":{"format":"result_envelope_v1","required_fields":["task_id","status","summary","artifacts"]}}],"routing":{"default_assignee":"implementer","rules":[{"id":"route-implementer","match":{"assignee":["implementer"]},"assign_to":"implementer","priority":10}]},"communication":{"protocol":"mcp_inbox","topology":"leader_centered","worker_to_worker":true,"ack_timeout_sec":60,"result_format":"result_envelope_v1","message_store":{"sqlite":".team/runtime/team.db","mirror_files":".team/messages"}},"runtime":{"backend":"tmux","display_backend":"adaptive","session_name":"team-doc-team","auto_launch":true,"require_user_approval_before_launch":true,"max_active_agents":1,"startup_order":["implementer"],"dangerous_auto_approve":false,"fast":false,"tick_interval_sec":2,"push_min_interval_sec":60,"stuck_timeout_sec":300},"context":{"state_file":"team_state.md","artifact_dir":".team/artifacts","log_dir":".team/logs","summarization":{"worker_full_logs":"retain_outside_leader_context","state_update":"after_each_result"}},"tasks":[{"id":"task_initial","title":"Initial document-driven team task","type":"implementation","assignee":"implementer","deps":[],"acceptance":["Worker reports valid result_envelope_v1"],"status":"pending","requires_tools":["mcp_team"],"files":[],"risk":"low"}]}"#;
183
183
 
184
184
  #[test]
185
185
  fn compile_base_noprofile_matches_python_dict_order_and_values() {
@@ -352,7 +352,7 @@ tools:
352
352
  Bravo body.
353
353
  ";
354
354
 
355
- const TWO_AGENTS_JSON: &str = r#"{"version":1,"team":{"name":"doc-team","mode":"supervisor_worker","objective":"Compile role docs.","workspace":"__WS__"},"leader":{"id":"leader","role":"leader","provider":"codex","model":"gpt-5.5","tools":["fs_read","fs_list","mcp_team"],"context_policy":{"keep_user_thread":true,"receive_worker_outputs":"business_messages_and_short_summaries","max_worker_result_tokens":2000}},"agents":[{"id":"alpha","role":"Alpha Worker","provider":"codex","model":"gpt-5.5","auth_mode":"subscription","working_directory":"__WS__","system_prompt":{"inline":"Alpha body.","file":null},"tools":["mcp_team"],"permission_mode":"restricted","preferred_for":["alpha","Alpha Worker"],"avoid_for":[],"output_contract":{"format":"result_envelope_v1","required_fields":["task_id","status","summary","artifacts"]}},{"id":"bravo","role":"Bravo Worker","provider":"codex","model":"gpt-5.5","auth_mode":"subscription","working_directory":"__WS__","system_prompt":{"inline":"Bravo body.","file":null},"tools":["mcp_team"],"permission_mode":"restricted","preferred_for":["bravo","Bravo Worker"],"avoid_for":[],"output_contract":{"format":"result_envelope_v1","required_fields":["task_id","status","summary","artifacts"]}}],"routing":{"default_assignee":"alpha","rules":[{"id":"route-alpha","match":{"assignee":["alpha"]},"assign_to":"alpha","priority":10},{"id":"route-bravo","match":{"assignee":["bravo"]},"assign_to":"bravo","priority":10}]},"communication":{"protocol":"mcp_inbox","topology":"leader_centered","worker_to_worker":true,"ack_timeout_sec":60,"result_format":"result_envelope_v1","message_store":{"sqlite":".team/runtime/team.db","mirror_files":".team/messages"}},"runtime":{"backend":"tmux","display_backend":"none","session_name":"team-doc-team","auto_launch":true,"require_user_approval_before_launch":true,"max_active_agents":2,"startup_order":["alpha","bravo"],"dangerous_auto_approve":false,"fast":false,"tick_interval_sec":2,"push_min_interval_sec":60,"stuck_timeout_sec":300},"context":{"state_file":"team_state.md","artifact_dir":".team/artifacts","log_dir":".team/logs","summarization":{"worker_full_logs":"retain_outside_leader_context","state_update":"after_each_result"}},"tasks":[{"id":"task_initial","title":"Initial document-driven team task","type":"implementation","assignee":"alpha","deps":[],"acceptance":["Worker reports valid result_envelope_v1"],"status":"pending","requires_tools":["mcp_team"],"files":[],"risk":"low"}]}"#;
355
+ const TWO_AGENTS_JSON: &str = r#"{"version":1,"team":{"name":"doc-team","mode":"supervisor_worker","objective":"Compile role docs.","workspace":"__WS__"},"leader":{"id":"leader","role":"leader","provider":"codex","model":"gpt-5.5","tools":["fs_read","fs_list","mcp_team"],"context_policy":{"keep_user_thread":true,"receive_worker_outputs":"business_messages_and_short_summaries","max_worker_result_tokens":2000}},"agents":[{"id":"alpha","role":"Alpha Worker","provider":"codex","model":"gpt-5.5","auth_mode":"subscription","working_directory":"__WS__","system_prompt":{"inline":"Alpha body.","file":null},"tools":["mcp_team"],"permission_mode":"restricted","preferred_for":["alpha","Alpha Worker"],"avoid_for":[],"output_contract":{"format":"result_envelope_v1","required_fields":["task_id","status","summary","artifacts"]}},{"id":"bravo","role":"Bravo Worker","provider":"codex","model":"gpt-5.5","auth_mode":"subscription","working_directory":"__WS__","system_prompt":{"inline":"Bravo body.","file":null},"tools":["mcp_team"],"permission_mode":"restricted","preferred_for":["bravo","Bravo Worker"],"avoid_for":[],"output_contract":{"format":"result_envelope_v1","required_fields":["task_id","status","summary","artifacts"]}}],"routing":{"default_assignee":"alpha","rules":[{"id":"route-alpha","match":{"assignee":["alpha"]},"assign_to":"alpha","priority":10},{"id":"route-bravo","match":{"assignee":["bravo"]},"assign_to":"bravo","priority":10}]},"communication":{"protocol":"mcp_inbox","topology":"leader_centered","worker_to_worker":true,"ack_timeout_sec":60,"result_format":"result_envelope_v1","message_store":{"sqlite":".team/runtime/team.db","mirror_files":".team/messages"}},"runtime":{"backend":"tmux","display_backend":"adaptive","session_name":"team-doc-team","auto_launch":true,"require_user_approval_before_launch":true,"max_active_agents":2,"startup_order":["alpha","bravo"],"dangerous_auto_approve":false,"fast":false,"tick_interval_sec":2,"push_min_interval_sec":60,"stuck_timeout_sec":300},"context":{"state_file":"team_state.md","artifact_dir":".team/artifacts","log_dir":".team/logs","summarization":{"worker_full_logs":"retain_outside_leader_context","state_update":"after_each_result"}},"tasks":[{"id":"task_initial","title":"Initial document-driven team task","type":"implementation","assignee":"alpha","deps":[],"acceptance":["Worker reports valid result_envelope_v1"],"status":"pending","requires_tools":["mcp_team"],"files":[],"risk":"low"}]}"#;
356
356
 
357
357
  #[test]
358
358
  fn compile_two_agents_sorted_by_filename_with_routing_and_startup_order() {
@@ -247,7 +247,7 @@ pub fn compile_team(team_dir: &Path) -> Result<Value, ModelError> {
247
247
  "display_backend",
248
248
  Value::Str(
249
249
  string_field(&team_meta, "display_backend")
250
- .unwrap_or_else(|| "none".to_string()),
250
+ .unwrap_or_else(|| "adaptive".to_string()),
251
251
  ),
252
252
  ),
253
253
  ("session_name", Value::Str(session_name(&team_meta, &team_name))),