@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
@@ -1,6 +1,7 @@
1
1
  use super::*;
2
2
  use crate::transport::test_support::OfflineTransport;
3
3
  use serial_test::serial;
4
+ use serde_json::json;
4
5
 
5
6
  const QS_TEAM_MD: &str =
6
7
  "---\nname: quickteam\nobjective: Quick start.\nprovider: codex\n---\n\nQuick-start team.\n";
@@ -18,6 +19,38 @@ pub(super) fn quick_start_team_dir(role_doc: &str) -> PathBuf {
18
19
  team
19
20
  }
20
21
 
22
+ fn quick_start_team_dir_with_roles(role_docs: &[(&str, &str)]) -> PathBuf {
23
+ let team = temp_ws().join("teamdir");
24
+ std::fs::create_dir_all(team.join("agents")).unwrap();
25
+ std::fs::write(team.join("TEAM.md"), QS_TEAM_MD).unwrap();
26
+ for (file, role_doc) in role_docs {
27
+ std::fs::write(team.join("agents").join(file), role_doc).unwrap();
28
+ }
29
+ team
30
+ }
31
+
32
+ fn role_doc(id: &str) -> String {
33
+ format!(
34
+ "---\nname: {id}\nrole: {id} Worker\nprovider: codex\nmodel: gpt-5.5\nauth_mode: subscription\ntools:\n - mcp_team\n---\n\n{id} worker.\n"
35
+ )
36
+ }
37
+
38
+ fn layout_pane(session: &str, window: &str, pane: &str, pane_index: u32) -> crate::transport::PaneInfo {
39
+ crate::transport::PaneInfo {
40
+ pane_id: crate::transport::PaneId::new(pane),
41
+ session: crate::transport::SessionName::new(session),
42
+ window_index: None,
43
+ window_name: Some(crate::transport::WindowName::new(window)),
44
+ pane_index: Some(pane_index),
45
+ tty: None,
46
+ current_command: Some("codex".to_string()),
47
+ current_path: None,
48
+ active: pane_index == 0,
49
+ pane_pid: None,
50
+ leader_env: std::collections::BTreeMap::new(),
51
+ }
52
+ }
53
+
21
54
  /// E5 spec 迁移:spec 不再落 team_dir,而在 <team_workspace>/.team/runtime/<team_key>/。
22
55
  /// team_key = team_dir.name(quick_start_team_dir → "teamdir");workspace = team_workspace(team)。
23
56
  pub(super) fn quick_start_runtime_spec_path(team: &std::path::Path) -> PathBuf {
@@ -121,12 +154,23 @@ fn quick_start_teamdir_under_dot_team_uses_project_workspace_for_status_and_coll
121
154
 
122
155
  let transport = OfflineTransport::new();
123
156
  let result = quick_start_with_transport(&team, None, true, true, None, &transport);
124
- assert!(matches!(result, Ok(QuickStartReport::Ready { .. })), "quick-start failed: {result:?}");
157
+ assert!(
158
+ matches!(result, Ok(QuickStartReport::Ready { .. })),
159
+ "quick-start failed: {result:?}"
160
+ );
125
161
 
126
162
  let state_path = crate::state::persist::runtime_state_path(&workspace);
127
- assert!(state_path.exists(), "quick-start .team/current must persist runtime state under project root");
128
163
  assert!(
129
- !workspace.join(".team").join(".team").join("runtime").join("state.json").exists(),
164
+ state_path.exists(),
165
+ "quick-start .team/current must persist runtime state under project root"
166
+ );
167
+ assert!(
168
+ !workspace
169
+ .join(".team")
170
+ .join(".team")
171
+ .join("runtime")
172
+ .join("state.json")
173
+ .exists(),
130
174
  "quick-start .team/current must not create nested .team/.team runtime state"
131
175
  );
132
176
 
@@ -137,11 +181,21 @@ fn quick_start_teamdir_under_dot_team_uses_project_workspace_for_status_and_coll
137
181
  crate::state::selector::SelectorMode::RuntimeOnly,
138
182
  )
139
183
  .expect("status/collect selector should resolve project root");
140
- assert_eq!(selected.run_workspace, workspace, "input={}", input.display());
184
+ assert_eq!(
185
+ selected.run_workspace,
186
+ workspace,
187
+ "input={}",
188
+ input.display()
189
+ );
141
190
  // E5: selector resolves spec_path to .team/runtime/<team_key>/ (team_key="current").
142
191
  let expected_spec = crate::model::paths::runtime_spec_path(&workspace, "current");
143
192
  assert_eq!(
144
- selected.spec_path.as_deref().map(std::fs::canonicalize).transpose().unwrap(),
193
+ selected
194
+ .spec_path
195
+ .as_deref()
196
+ .map(std::fs::canonicalize)
197
+ .transpose()
198
+ .unwrap(),
145
199
  Some(std::fs::canonicalize(&expected_spec).unwrap()),
146
200
  "input={}",
147
201
  input.display()
@@ -155,14 +209,20 @@ fn quick_start_teamdir_under_dot_team_uses_project_workspace_for_status_and_coll
155
209
  summary: false,
156
210
  json: true,
157
211
  });
158
- assert!(status.is_ok(), "status should normalize teamdir to project-root runtime state: {status:?}");
212
+ assert!(
213
+ status.is_ok(),
214
+ "status should normalize teamdir to project-root runtime state: {status:?}"
215
+ );
159
216
 
160
217
  let collect = crate::cli::cmd_collect(&crate::cli::CollectArgs {
161
218
  result_file: None,
162
219
  workspace: team.clone(),
163
220
  json: true,
164
221
  });
165
- assert!(collect.is_ok(), "collect should normalize teamdir to the same project-root state/spec: {collect:?}");
222
+ assert!(
223
+ collect.is_ok(),
224
+ "collect should normalize teamdir to the same project-root state/spec: {collect:?}"
225
+ );
166
226
  }
167
227
 
168
228
  // P0 — quick_start over an INVALID role doc (missing `provider`) must surface the REAL compile
@@ -222,7 +282,15 @@ fn launch_dry_run_resolves_real_plan_not_stub_error() {
222
282
  fn start_agent_proceeds_past_owner_gate_for_unowned_workspace() {
223
283
  let ws = unowned_running_ws();
224
284
  let transport = OfflineTransport::new();
225
- let result = start_agent_with_transport(&ws, &AgentId::new("w1"), false, false, true, None, &transport);
285
+ let result = start_agent_with_transport(
286
+ &ws,
287
+ &AgentId::new("w1"),
288
+ false,
289
+ false,
290
+ true,
291
+ None,
292
+ &transport,
293
+ );
226
294
  assert!(
227
295
  !matches!(result, Err(LifecycleError::OwnerRefused(_))),
228
296
  "start_agent must reach the real chain for a no-owner workspace, not hard-refuse owner; got {result:?}"
@@ -292,7 +360,10 @@ fn fork_agent_proceeds_past_owner_gate_for_unowned_workspace() {
292
360
  fn quick_start_full_ready_real_spawn() {
293
361
  let team = quick_start_team_dir(QS_VALID_ROLE);
294
362
  let report = quick_start(&team, None, true, true, None).expect("quick_start launches the team");
295
- assert!(matches!(report, QuickStartReport::Ready { .. }), "got {report:?}");
363
+ assert!(
364
+ matches!(report, QuickStartReport::Ready { .. }),
365
+ "got {report:?}"
366
+ );
296
367
  }
297
368
 
298
369
  // ═════════════════════════════════════════════════════════════════════════
@@ -375,7 +446,9 @@ fn compiled_spec_path_with_paused_agent(team: &std::path::Path) -> PathBuf {
375
446
  _ => None,
376
447
  })
377
448
  .expect("compiled spec must contain agents");
378
- let first = agents.first_mut().expect("compiled spec must contain an agent");
449
+ let first = agents
450
+ .first_mut()
451
+ .expect("compiled spec must contain an agent");
379
452
  let crate::model::yaml::Value::Map(agent) = first else {
380
453
  panic!("compiled agent must be a map");
381
454
  };
@@ -422,7 +495,10 @@ fn spine_launch_dry_run_safety_reflects_spec_dangerous() {
422
495
  let team = quick_start_team_dir_custom(QS_TEAM_MD_DANGEROUS, QS_VALID_ROLE);
423
496
  let spec_path = compiled_spec_path(&team);
424
497
  let report = launch(&spec_path, true, false, true).expect("dry_run launch");
425
- assert!(report.safety.enabled, "safety must reflect spec.runtime.dangerous_auto_approve=true");
498
+ assert!(
499
+ report.safety.enabled,
500
+ "safety must reflect spec.runtime.dangerous_auto_approve=true"
501
+ );
426
502
  assert_eq!(
427
503
  report.safety.source,
428
504
  DangerousApprovalSource::RuntimeConfig,
@@ -463,19 +539,35 @@ fn spine_restart_empty_team_is_not_atomic_refusal() {
463
539
  );
464
540
  }
465
541
 
466
- // #14 — add_agent rejects a duplicate agent id (golden operations.py:301 'agent id already exists'),
467
- // BEFORE any full recompile. Current does a full compile_team with no duplicate-id guard.
542
+ // E25 — add_agent duplicate membership is runtime-state based, not role-doc/spec-file based.
468
543
  #[test]
469
544
  fn spine_add_agent_rejects_duplicate_agent_id() {
470
545
  let team = quick_start_team_dir(QS_VALID_ROLE); // team already has agent 'implementer'
546
+ crate::state::persist::save_runtime_state(
547
+ &team,
548
+ &json!({
549
+ "session_name": "team-quickteam",
550
+ "agents": {
551
+ "implementer": {"status": "running", "provider": "codex", "window": "implementer"}
552
+ }
553
+ }),
554
+ )
555
+ .unwrap();
471
556
  let role = team.join("dup-role.md");
472
557
  std::fs::write(&role, QS_VALID_ROLE).unwrap(); // a role doc named 'implementer' = duplicate
473
558
  let transport = OfflineTransport::new();
474
- let result = add_agent_with_transport(&team, &AgentId::new("implementer"), &role, false, None, &transport);
559
+ let result = add_agent_with_transport(
560
+ &team,
561
+ &AgentId::new("implementer"),
562
+ &role,
563
+ false,
564
+ None,
565
+ &transport,
566
+ );
475
567
  let text = format!("{result:?}");
476
568
  assert!(
477
569
  text.contains("already exists"),
478
- "add_agent must reject a duplicate agent id (golden 'agent id already exists'); got {text}"
570
+ "add_agent must reject an id already present in runtime state; got {text}"
479
571
  );
480
572
  }
481
573
 
@@ -524,7 +616,7 @@ fn e5_add_agent_does_not_copy_role_into_platform_dir_and_injects_into_spec() {
524
616
  #[test]
525
617
  fn e5_restart_rebuilds_runtime_spec_from_role_docs() {
526
618
  let ws = restart_ws_two_resumable_workers(); // has TEAM.md + agents/{alpha,bravo}.md + state
527
- // Edit a role doc AFTER initial compile: change alpha's role text.
619
+ // Edit a role doc AFTER initial compile: change alpha's role text.
528
620
  std::fs::write(
529
621
  ws.join("agents").join("alpha.md"),
530
622
  "---\nname: alpha\nrole: RENAMED Alpha Role\nprovider: codex\nmodel: gpt-5.5\nauth_mode: subscription\ntools:\n - mcp_team\n---\n\nAlpha edited.\n",
@@ -551,10 +643,13 @@ fn e5_restart_rebuilds_runtime_spec_from_role_docs() {
551
643
  #[test]
552
644
  fn red2_restart_finds_runtime_spec_not_missing_when_user_dir_has_no_spec() {
553
645
  let ws = restart_ws_two_resumable_workers(); // has TEAM.md + agents + state
554
- // Simulate spec-demote: remove any user-dir spec the fixture wrote; restart must still find
555
- // the runtime spec (rebuilt from role docs / read-order B), NOT report "missing spec".
646
+ // Simulate spec-demote: remove any user-dir spec the fixture wrote; restart must still find
647
+ // the runtime spec (rebuilt from role docs / read-order B), NOT report "missing spec".
556
648
  let _ = std::fs::remove_file(ws.join("team.spec.yaml"));
557
- assert!(!ws.join("team.spec.yaml").exists(), "user-dir spec removed (spec-demote condition)");
649
+ assert!(
650
+ !ws.join("team.spec.yaml").exists(),
651
+ "user-dir spec removed (spec-demote condition)"
652
+ );
558
653
  let transport = OfflineTransport::new();
559
654
  let result = restart_with_transport(&ws, false, None, &transport);
560
655
  // The gate must NOT short-circuit with "missing spec for restart" — restart must proceed
@@ -575,7 +670,11 @@ fn red2_restart_empty_workspace_still_reports_missing() {
575
670
  let result = restart_with_transport(&ws, false, None, &transport);
576
671
  let text = format!("{result:?}");
577
672
  assert!(
578
- text.contains("missing spec") || text.to_lowercase().contains("not found") || text.contains("no_local_team_context") || text.to_lowercase().contains("invalid workspace"),
673
+ text.contains("missing spec")
674
+ || text.to_lowercase().contains("not found")
675
+ || text.contains("no_local_team_context")
676
+ || text.to_lowercase().contains("invalid workspace")
677
+ || text.contains("role definitions missing"),
579
678
  "RED-2 negative: empty workspace restart must still error (missing/not-found); got {text}"
580
679
  );
581
680
  let _ = std::fs::remove_dir_all(&ws);
@@ -595,7 +694,11 @@ fn red2still_entry_gate_resolves_parent_dot_team_form_a() {
595
694
  let base = temp_ws();
596
695
  let team_dir = base.join("teamdir");
597
696
  std::fs::create_dir_all(team_dir.join("agents")).unwrap();
598
- std::fs::write(team_dir.join("TEAM.md"), "---\nname: teamdir\nobjective: o\nprovider: codex\n---\nt.\n").unwrap();
697
+ std::fs::write(
698
+ team_dir.join("TEAM.md"),
699
+ "---\nname: teamdir\nobjective: o\nprovider: codex\n---\nt.\n",
700
+ )
701
+ .unwrap();
599
702
  std::fs::write(team_dir.join("agents").join("w1.md"), QS_VALID_ROLE).unwrap();
600
703
  // .team lives in the PARENT (base), with a runtime spec — mirrors quick-start <dir> from base cwd.
601
704
  let spec = crate::compiler::compile_team(&team_dir).unwrap();
@@ -622,7 +725,10 @@ fn red2still_entry_gate_resolves_parent_dot_team_form_a() {
622
725
  );
623
726
  // Full restart on team_dir must NOT early-exit with missing spec.
624
727
  let transport = OfflineTransport::new();
625
- let text = format!("{:?}", restart_with_transport(&team_dir, false, None, &transport));
728
+ let text = format!(
729
+ "{:?}",
730
+ restart_with_transport(&team_dir, false, None, &transport)
731
+ );
626
732
  assert!(
627
733
  !text.contains("missing spec for restart") && !text.contains("active team spec not found"),
628
734
  "RED-2-STILL Form A: restart <team_dir> (.team in parent) must not report missing; got {text}"
@@ -635,7 +741,11 @@ fn red2still_entry_gate_same_dir_dot_team_form_b() {
635
741
  // Form B: .team in team_dir itself (cwd == team_dir at quick-start time).
636
742
  let team_dir = temp_ws();
637
743
  std::fs::create_dir_all(team_dir.join("agents")).unwrap();
638
- std::fs::write(team_dir.join("TEAM.md"), "---\nname: tb\nobjective: o\nprovider: codex\n---\nt.\n").unwrap();
744
+ std::fs::write(
745
+ team_dir.join("TEAM.md"),
746
+ "---\nname: tb\nobjective: o\nprovider: codex\n---\nt.\n",
747
+ )
748
+ .unwrap();
639
749
  std::fs::write(team_dir.join("agents").join("w1.md"), QS_VALID_ROLE).unwrap();
640
750
  let spec = crate::compiler::compile_team(&team_dir).unwrap();
641
751
  let runtime_spec = crate::model::paths::runtime_spec_path(&team_dir, "tb");
@@ -649,7 +759,10 @@ fn red2still_entry_gate_same_dir_dot_team_form_b() {
649
759
  )
650
760
  .unwrap();
651
761
  let transport = OfflineTransport::new();
652
- let text = format!("{:?}", restart_with_transport(&team_dir, false, None, &transport));
762
+ let text = format!(
763
+ "{:?}",
764
+ restart_with_transport(&team_dir, false, None, &transport)
765
+ );
653
766
  assert!(
654
767
  !text.contains("missing spec for restart") && !text.contains("active team spec not found"),
655
768
  "RED-2-STILL Form B: restart <team_dir> (.team in team_dir) must not report missing; got {text}"
@@ -703,7 +816,8 @@ fn e5_add_agent_resolves_team_dir_to_role_dir_when_runtime_spec_exists() {
703
816
  )
704
817
  .unwrap();
705
818
  let transport = OfflineTransport::new();
706
- let result = add_agent_with_transport(&team, &AgentId::new("w2"), &role, false, None, &transport);
819
+ let result =
820
+ add_agent_with_transport(&team, &AgentId::new("w2"), &role, false, None, &transport);
707
821
  // Core decoupling signature: must NOT fail because compile_team looked for TEAM.md/agents under
708
822
  // the runtime spec dir. Before the fix, team_dir=spec_workspace(runtime) → "missing TEAM.md".
709
823
  let text = format!("{result:?}");
@@ -825,15 +939,29 @@ fn launch_with_transport_records_one_spawn_per_agent_carrying_build_command() {
825
939
  "exactly one spawn per started agent; recorded={recorded:?} started={:?}",
826
940
  report.started
827
941
  );
828
- assert!(!report.started.is_empty(), "a real (non-dry-run) launch must spawn >=1 worker");
829
- assert!(!report.dry_run, "launch_with_transport(dry_run=false) must report dry_run=false");
830
- assert_eq!(recorded[0].0, "spawn_first", "the first worker uses new-session (spawn_first)");
831
942
  assert!(
832
- recorded.iter().any(|(_, argv)| argv.iter().any(|a| a == "codex")),
943
+ !report.started.is_empty(),
944
+ "a real (non-dry-run) launch must spawn >=1 worker"
945
+ );
946
+ assert!(
947
+ !report.dry_run,
948
+ "launch_with_transport(dry_run=false) must report dry_run=false"
949
+ );
950
+ assert_eq!(
951
+ recorded[0].0, "spawn_first",
952
+ "the first worker uses new-session (spawn_first)"
953
+ );
954
+ assert!(
955
+ recorded
956
+ .iter()
957
+ .any(|(_, argv)| argv.iter().any(|a| a == "codex")),
833
958
  "each spawn argv must carry the agent's provider build_command (codex); got {recorded:?}"
834
959
  );
835
960
  assert!(
836
- report.started.iter().any(|s| s.agent_id.as_str() == "implementer"),
961
+ report
962
+ .started
963
+ .iter()
964
+ .any(|s| s.agent_id.as_str() == "implementer"),
837
965
  "LaunchReport.started must list the compiled agent; got {:?}",
838
966
  report.started
839
967
  );
@@ -853,8 +981,17 @@ pub(super) fn seed_healthy_coordinator(workspace: &std::path::Path) {
853
981
  std::fs::create_dir_all(crate::model::paths::runtime_dir(workspace)).unwrap();
854
982
  let _ = crate::message_store::MessageStore::open(workspace).unwrap(); // create db schema (schema.ok)
855
983
  let me = crate::coordinator::Pid::new(std::process::id());
856
- crate::coordinator::write_coordinator_metadata(&wp, me, crate::coordinator::MetadataSource::Boot).unwrap();
857
- std::fs::write(crate::coordinator::coordinator_pid_path(&wp), me.to_string()).unwrap();
984
+ crate::coordinator::write_coordinator_metadata(
985
+ &wp,
986
+ me,
987
+ crate::coordinator::MetadataSource::Boot,
988
+ )
989
+ .unwrap();
990
+ std::fs::write(
991
+ crate::coordinator::coordinator_pid_path(&wp),
992
+ me.to_string(),
993
+ )
994
+ .unwrap();
858
995
  }
859
996
 
860
997
  // RED — quick_start_with_transport must drive the REAL spawn path: launch.dry_run==false, started
@@ -889,7 +1026,10 @@ fn quick_start_with_transport_spawns_workers_not_dry_run() {
889
1026
  launch.started
890
1027
  );
891
1028
  assert!(
892
- launch.started.iter().any(|s| s.agent_id.as_str() == "implementer"),
1029
+ launch
1030
+ .started
1031
+ .iter()
1032
+ .any(|s| s.agent_id.as_str() == "implementer"),
893
1033
  "launch.started must list the compiled agent 'implementer'; got {:?}",
894
1034
  launch.started
895
1035
  );
@@ -899,13 +1039,212 @@ fn quick_start_with_transport_spawns_workers_not_dry_run() {
899
1039
  !recorded.is_empty(),
900
1040
  "quick_start must drive the transport spawn path (>=1 spawn recorded); the dry-run bug records ZERO spawns"
901
1041
  );
902
- assert_eq!(recorded[0].0, "spawn_first", "the first worker uses new-session (spawn_first); got {recorded:?}");
1042
+ assert_eq!(
1043
+ recorded[0].0, "spawn_first",
1044
+ "the first worker uses new-session (spawn_first); got {recorded:?}"
1045
+ );
903
1046
  assert!(
904
- recorded.iter().any(|(_, argv)| argv.iter().any(|a| a == "codex")),
1047
+ recorded
1048
+ .iter()
1049
+ .any(|(_, argv)| argv.iter().any(|a| a == "codex")),
905
1050
  "the spawn argv must carry the agent's provider build_command (codex); got {recorded:?}"
906
1051
  );
907
1052
  }
908
1053
 
1054
+ #[test]
1055
+ fn adaptive_layout_plan_8_workers_is_3_3_2() {
1056
+ let agents = (1..=8)
1057
+ .map(|i| AgentId::new(format!("w{i}")))
1058
+ .collect::<Vec<_>>();
1059
+ let plan = adaptive_layout_plan(&agents, 3);
1060
+ let windows = plan
1061
+ .iter()
1062
+ .map(|placement| placement.layout_window.as_str().to_string())
1063
+ .collect::<Vec<_>>();
1064
+ assert_eq!(
1065
+ windows,
1066
+ vec![
1067
+ "team-w1", "team-w1", "team-w1", "team-w2", "team-w2", "team-w2", "team-w3",
1068
+ "team-w3",
1069
+ ]
1070
+ );
1071
+ let pane_counts = ["team-w1", "team-w2", "team-w3"]
1072
+ .into_iter()
1073
+ .map(|window| windows.iter().filter(|actual| actual.as_str() == window).count())
1074
+ .collect::<Vec<_>>();
1075
+ assert_eq!(pane_counts, vec![3, 3, 2]);
1076
+ assert_eq!(
1077
+ plan.iter()
1078
+ .filter(|placement| placement.starts_window)
1079
+ .map(|placement| placement.agent_id.as_str().to_string())
1080
+ .collect::<Vec<_>>(),
1081
+ vec!["w1", "w4", "w7"]
1082
+ );
1083
+ }
1084
+
1085
+ #[test]
1086
+ fn quick_start_default_adaptive_groups_workers_into_layout_panes() {
1087
+ let roles = (1..=4)
1088
+ .map(|i| {
1089
+ let id = format!("w{i}");
1090
+ (format!("{id}.md"), role_doc(&id))
1091
+ })
1092
+ .collect::<Vec<_>>();
1093
+ let role_refs = roles
1094
+ .iter()
1095
+ .map(|(file, doc)| (file.as_str(), doc.as_str()))
1096
+ .collect::<Vec<_>>();
1097
+ let team = quick_start_team_dir_with_roles(&role_refs);
1098
+ let workspace = team.parent().expect("team_workspace(team_dir) = parent");
1099
+ seed_healthy_coordinator(workspace);
1100
+ let transport = OfflineTransport::new();
1101
+
1102
+ let report = quick_start_with_transport(&team, None, true, true, None, &transport)
1103
+ .expect("quick_start_with_transport must reach Ready");
1104
+
1105
+ let (launch, attach_commands, display_backend) = match report {
1106
+ QuickStartReport::Ready { launch, attach_commands, display_backend, .. } => {
1107
+ (*launch, attach_commands, display_backend)
1108
+ }
1109
+ other => panic!("quick_start must reach Ready; got {other:?}"),
1110
+ };
1111
+ assert_eq!(display_backend, "adaptive");
1112
+ assert_eq!(
1113
+ transport
1114
+ .spawn_records()
1115
+ .iter()
1116
+ .map(|(kind, _)| kind.as_str())
1117
+ .collect::<Vec<_>>(),
1118
+ vec!["spawn_first", "spawn_split", "spawn_split", "spawn_into"],
1119
+ "4 default-adaptive workers should create team-w1 with 3 panes, then team-w2"
1120
+ );
1121
+ assert_eq!(
1122
+ launch
1123
+ .started
1124
+ .iter()
1125
+ .map(|started| {
1126
+ (
1127
+ started.agent_id.as_str().to_string(),
1128
+ started.layout_window.as_ref().map(|w| w.as_str().to_string()),
1129
+ started.pane_index,
1130
+ )
1131
+ })
1132
+ .collect::<Vec<_>>(),
1133
+ vec![
1134
+ ("w1".to_string(), Some("team-w1".to_string()), Some(0)),
1135
+ ("w2".to_string(), Some("team-w1".to_string()), Some(1)),
1136
+ ("w3".to_string(), Some("team-w1".to_string()), Some(2)),
1137
+ ("w4".to_string(), Some("team-w2".to_string()), Some(0)),
1138
+ ]
1139
+ );
1140
+ assert!(
1141
+ attach_commands.iter().any(|cmd| cmd.contains(":team-w1")),
1142
+ "attach commands must include the first layout window: {attach_commands:?}"
1143
+ );
1144
+ assert!(
1145
+ attach_commands.iter().any(|cmd| cmd.contains(":team-w2")),
1146
+ "attach commands must include the second layout window: {attach_commands:?}"
1147
+ );
1148
+
1149
+ let (_raw, state) = raw_runtime_state(workspace);
1150
+ assert_eq!(state["display_backend"], json!("adaptive"));
1151
+ assert_eq!(state.pointer("/agents/w1/window"), Some(&json!("team-w1")));
1152
+ assert_eq!(state.pointer("/agents/w1/layout_window"), Some(&json!("team-w1")));
1153
+ assert_eq!(state.pointer("/agents/w1/pane_index"), Some(&json!(0)));
1154
+ assert_eq!(
1155
+ state.pointer("/agents/w1/display/backend"),
1156
+ Some(&json!("adaptive"))
1157
+ );
1158
+ assert_eq!(
1159
+ state.pointer("/agents/w4/layout_window"),
1160
+ Some(&json!("team-w2"))
1161
+ );
1162
+ }
1163
+
1164
+ #[test]
1165
+ #[serial(env)]
1166
+ fn quick_start_tmux_backend_prefers_absolute_tmux_env_endpoint() {
1167
+ let leader_socket = "/tmp/team-agent-layout-leader-socket";
1168
+ let _tmux = EnvVarGuard::set("TMUX", &format!("{leader_socket},123,0"));
1169
+ let backend = quick_start_tmux_backend(std::path::Path::new("/tmp/layout-workspace"));
1170
+
1171
+ assert_eq!(
1172
+ crate::transport::Transport::tmux_endpoint(&backend).as_deref(),
1173
+ Some(leader_socket),
1174
+ "quick-start from inside tmux must use the caller's full $TMUX socket endpoint"
1175
+ );
1176
+ assert_eq!(
1177
+ selected_tmux_socket_source(&backend, std::path::Path::new("/tmp/layout-workspace")),
1178
+ Some("leader_env")
1179
+ );
1180
+ }
1181
+
1182
+ #[test]
1183
+ fn quick_start_persists_selected_tmux_endpoint_and_attach_commands() {
1184
+ let team = quick_start_team_dir(QS_VALID_ROLE);
1185
+ let workspace = team.parent().expect("team_workspace(team_dir) = parent");
1186
+ seed_healthy_coordinator(workspace);
1187
+ let endpoint = "/tmp/team-agent-layout-selected-socket";
1188
+ let transport = OfflineTransport::new().with_tmux_endpoint(endpoint);
1189
+
1190
+ let report = quick_start_with_transport(&team, None, true, true, None, &transport)
1191
+ .expect("quick_start_with_transport must reach Ready");
1192
+ let attach_commands = match report {
1193
+ QuickStartReport::Ready { attach_commands, .. } => attach_commands,
1194
+ other => panic!("quick_start must reach Ready; got {other:?}"),
1195
+ };
1196
+
1197
+ assert!(
1198
+ attach_commands
1199
+ .iter()
1200
+ .any(|cmd| cmd.contains(&format!("tmux -S {endpoint} attach -t "))),
1201
+ "attach commands must use the selected socket endpoint: {attach_commands:?}"
1202
+ );
1203
+ let (_raw, state) = raw_runtime_state(workspace);
1204
+ assert_eq!(state["tmux_endpoint"], json!(endpoint));
1205
+ assert_eq!(state["tmux_socket"], json!(endpoint));
1206
+ }
1207
+
1208
+ #[test]
1209
+ fn quick_start_no_display_keeps_one_window_per_agent() {
1210
+ let roles = ["w1", "w2"]
1211
+ .into_iter()
1212
+ .map(|id| (format!("{id}.md"), role_doc(id)))
1213
+ .collect::<Vec<_>>();
1214
+ let role_refs = roles
1215
+ .iter()
1216
+ .map(|(file, doc)| (file.as_str(), doc.as_str()))
1217
+ .collect::<Vec<_>>();
1218
+ let team = quick_start_team_dir_with_roles(&role_refs);
1219
+ let workspace = team.parent().expect("team_workspace(team_dir) = parent");
1220
+ seed_healthy_coordinator(workspace);
1221
+ let transport = OfflineTransport::new();
1222
+
1223
+ let report = quick_start_with_transport_in_workspace_with_display(
1224
+ workspace, &team, None, true, true, None, &transport, false,
1225
+ )
1226
+ .expect("quick_start_with_transport must reach Ready");
1227
+
1228
+ let display_backend = match report {
1229
+ QuickStartReport::Ready { display_backend, .. } => display_backend,
1230
+ other => panic!("quick_start must reach Ready; got {other:?}"),
1231
+ };
1232
+ assert_eq!(display_backend, "none");
1233
+ assert_eq!(
1234
+ transport
1235
+ .spawn_records()
1236
+ .iter()
1237
+ .map(|(kind, _)| kind.as_str())
1238
+ .collect::<Vec<_>>(),
1239
+ vec!["spawn_first", "spawn_into"],
1240
+ "--no-display must use the legacy one-worker-window spawn path"
1241
+ );
1242
+ let (_raw, state) = raw_runtime_state(workspace);
1243
+ assert_eq!(state["display_backend"], json!("none"));
1244
+ assert_eq!(state.pointer("/agents/w1/window"), Some(&json!("w1")));
1245
+ assert_eq!(state.pointer("/agents/w1/layout_window"), None);
1246
+ }
1247
+
909
1248
  // REAL-MACHINE residency boundary (acceptance framework): the PUBLIC quick_start (real TmuxBackend +
910
1249
  // real start_coordinator) on a fresh ws must leave a LIVE tmux session AND a ps-verifiable resident
911
1250
  // coordinator daemon (start_coordinator -> live pid). The framework verifies residency via ps; here we
@@ -917,7 +1256,10 @@ fn quick_start_fresh_ws_spawns_resident_tmux_and_coordinator() {
917
1256
  let report = quick_start(&team, None, true, true, None).expect("quick_start");
918
1257
  match report {
919
1258
  QuickStartReport::Ready { launch, .. } => {
920
- assert!(!launch.dry_run, "a real quick_start must spawn (not dry-run)");
1259
+ assert!(
1260
+ !launch.dry_run,
1261
+ "a real quick_start must spawn (not dry-run)"
1262
+ );
921
1263
  assert!(
922
1264
  !launch.started.is_empty(),
923
1265
  "a real quick_start must spawn >=1 worker into a live tmux session; got {:?}",
@@ -949,7 +1291,11 @@ pub(super) fn restart_ws_two_resumable_workers() -> PathBuf {
949
1291
  let bravo_rollout = ws.join("bravo-rollout.jsonl");
950
1292
  std::fs::write(&alpha_rollout, "{}\n").unwrap();
951
1293
  std::fs::write(&bravo_rollout, "{}\n").unwrap();
952
- std::fs::write(ws.join("TEAM.md"), "---\nname: restartteam\nobjective: Restart probe.\nprovider: codex\n---\n\nteam.\n").unwrap();
1294
+ std::fs::write(
1295
+ ws.join("TEAM.md"),
1296
+ "---\nname: restartteam\nobjective: Restart probe.\nprovider: codex\n---\n\nteam.\n",
1297
+ )
1298
+ .unwrap();
953
1299
  std::fs::write(ws.join("agents").join("alpha.md"), DELEG_ROLE_ALPHA).unwrap();
954
1300
  std::fs::write(ws.join("agents").join("bravo.md"), DELEG_ROLE_BRAVO).unwrap();
955
1301
  let spec = crate::compiler::compile_team(&ws).expect("compile 2-agent team");
@@ -1039,18 +1385,31 @@ fn restart_with_transport_spawns_resumable_workers_not_stub() {
1039
1385
 
1040
1386
  let ws = restart_ws_two_resumable_workers();
1041
1387
  let dead_after_first = OfflineTransport::new().with_session_absent_after_spawn_first();
1042
- let result =
1043
- restart_with_transport_with_readiness_deadline(&ws, false, None, &dead_after_first, Some(0));
1388
+ let result = restart_with_transport_with_readiness_deadline(
1389
+ &ws,
1390
+ false,
1391
+ None,
1392
+ &dead_after_first,
1393
+ Some(0),
1394
+ );
1044
1395
  let recorded = dead_after_first.spawn_records();
1045
1396
  assert_eq!(
1046
1397
  recorded.len(),
1047
- 2,
1048
- "if the session disappears after alpha, restart must isolate alpha and still try bravo; got result={result:?} recorded={recorded:?}"
1398
+ 1,
1399
+ "if a resumed session disappears after alpha, restart must fail fast before bravo; got result={result:?} recorded={recorded:?}"
1049
1400
  );
1050
1401
  assert!(
1051
1402
  recorded.iter().all(|(kind, _)| kind == "spawn_first"),
1052
1403
  "restart must not call spawn_into/new-window against a dead server; result={result:?} recorded={recorded:?}"
1053
1404
  );
1405
+ let result_debug = format!("{result:?}");
1406
+ let report = result.expect("resume integrity failure should return a typed failed report");
1407
+ let RestartReport::Failed { failed_agents, .. } = report else {
1408
+ panic!("resume integrity failure must not be Partial/Restarted; got {report:?}");
1409
+ };
1410
+ assert_eq!(failed_agents.len(), 1);
1411
+ assert_eq!(failed_agents[0].agent_id.as_str(), "alpha");
1412
+ assert_eq!(failed_agents[0].phase, "resume");
1054
1413
  assert!(
1055
1414
  dead_after_first.calls().iter().all(|call| *call != "kill_session"),
1056
1415
  "a single worker/session disappearance must not trigger another shared-session kill; calls={:?}",
@@ -1061,13 +1420,16 @@ fn restart_with_transport_spawns_resumable_workers_not_stub() {
1061
1420
  .iter()
1062
1421
  .find(|event| event.get("event").and_then(|v| v.as_str()) == Some("restart.agent_failed"))
1063
1422
  .expect("restart.agent_failed event for isolated alpha");
1064
- assert_eq!(failed.get("agent_id").and_then(|v| v.as_str()), Some("alpha"));
1423
+ assert_eq!(
1424
+ failed.get("agent_id").and_then(|v| v.as_str()),
1425
+ Some("alpha")
1426
+ );
1065
1427
  assert!(
1066
1428
  failed
1067
1429
  .get("error")
1068
1430
  .and_then(|v| v.as_str())
1069
1431
  .is_some_and(|error| error.contains("session_disappeared_after_spawn")),
1070
- "restart must report the first resumed agent/session disappearance explicitly; event={failed} result={result:?}"
1432
+ "restart must report the first resumed agent/session disappearance explicitly; event={failed} result={result_debug}"
1071
1433
  );
1072
1434
  }
1073
1435
 
@@ -1088,9 +1450,15 @@ fn restart_spawn_failure_isolated_to_partial_report() {
1088
1450
  else {
1089
1451
  panic!("single worker failure must return RestartReport::Partial; got {report:?}");
1090
1452
  };
1091
- assert!(coordinator_started, "partial restart still starts coordinator for successful agents");
1453
+ assert!(
1454
+ coordinator_started,
1455
+ "partial restart still starts coordinator for successful agents"
1456
+ );
1092
1457
  assert_eq!(
1093
- agents.iter().map(|agent| agent.agent_id.as_str()).collect::<Vec<_>>(),
1458
+ agents
1459
+ .iter()
1460
+ .map(|agent| agent.agent_id.as_str())
1461
+ .collect::<Vec<_>>(),
1094
1462
  vec!["alpha"]
1095
1463
  );
1096
1464
  assert_eq!(failed_agents.len(), 1);
@@ -1098,8 +1466,18 @@ fn restart_spawn_failure_isolated_to_partial_report() {
1098
1466
  assert_eq!(failed_agents[0].phase, "spawn");
1099
1467
 
1100
1468
  let state = crate::state::persist::load_runtime_state(&ws).unwrap();
1101
- assert_eq!(state.pointer("/agents/alpha/status").and_then(|v| v.as_str()), Some("running"));
1102
- assert_eq!(state.pointer("/agents/bravo/status").and_then(|v| v.as_str()), Some("failed"));
1469
+ assert_eq!(
1470
+ state
1471
+ .pointer("/agents/alpha/status")
1472
+ .and_then(|v| v.as_str()),
1473
+ Some("running")
1474
+ );
1475
+ assert_eq!(
1476
+ state
1477
+ .pointer("/agents/bravo/status")
1478
+ .and_then(|v| v.as_str()),
1479
+ Some("failed")
1480
+ );
1103
1481
  assert!(
1104
1482
  state
1105
1483
  .pointer("/agents/bravo/restart_error")
@@ -1113,7 +1491,10 @@ fn restart_spawn_failure_isolated_to_partial_report() {
1113
1491
  .iter()
1114
1492
  .find(|event| event.get("event").and_then(|v| v.as_str()) == Some("restart.completed"))
1115
1493
  .expect("restart.completed event for partial restart");
1116
- assert_eq!(completed.get("rc").and_then(|v| v.as_str()), Some("partial"));
1494
+ assert_eq!(
1495
+ completed.get("rc").and_then(|v| v.as_str()),
1496
+ Some("partial")
1497
+ );
1117
1498
  assert!(
1118
1499
  completed
1119
1500
  .get("successful_agents")
@@ -1201,10 +1582,14 @@ fn restart_times_out_when_spawned_worker_pane_is_not_addressable() {
1201
1582
  let events = crate::event_log::EventLog::new(&ws).tail(20).unwrap();
1202
1583
  let timeout = events
1203
1584
  .iter()
1204
- .find(|event| event.get("event").and_then(|v| v.as_str()) == Some("restart.readiness_timeout"))
1585
+ .find(|event| {
1586
+ event.get("event").and_then(|v| v.as_str()) == Some("restart.readiness_timeout")
1587
+ })
1205
1588
  .expect("restart.readiness_timeout event");
1206
1589
  assert_eq!(
1207
- timeout.get("worker_pane_addressable").and_then(|v| v.as_bool()),
1590
+ timeout
1591
+ .get("worker_pane_addressable")
1592
+ .and_then(|v| v.as_bool()),
1208
1593
  Some(false),
1209
1594
  "timeout event must carry the failed readiness condition: {timeout}"
1210
1595
  );
@@ -1230,7 +1615,15 @@ fn start_agent_with_transport_spawns_resume_not_stub() {
1230
1615
  seed_healthy_coordinator(&ws);
1231
1616
  let transport = OfflineTransport::new();
1232
1617
 
1233
- let _result = start_agent_with_transport(&ws, &AgentId::new("alpha"), false, false, false, None, &transport);
1618
+ let _result = start_agent_with_transport(
1619
+ &ws,
1620
+ &AgentId::new("alpha"),
1621
+ false,
1622
+ false,
1623
+ false,
1624
+ None,
1625
+ &transport,
1626
+ );
1234
1627
 
1235
1628
  let recorded = transport.spawn_records();
1236
1629
  assert_eq!(
@@ -1253,14 +1646,25 @@ fn start_agent_with_transport_spawns_resume_not_stub() {
1253
1646
  fn add_agent_with_transport_spawns_new_worker_not_stub() {
1254
1647
  let team = temp_ws().join("addteam");
1255
1648
  std::fs::create_dir_all(team.join("agents")).unwrap();
1256
- std::fs::write(team.join("TEAM.md"), "---\nname: addteam\nobjective: Add probe.\nprovider: codex\n---\n\nteam.\n").unwrap();
1649
+ std::fs::write(
1650
+ team.join("TEAM.md"),
1651
+ "---\nname: addteam\nobjective: Add probe.\nprovider: codex\n---\n\nteam.\n",
1652
+ )
1653
+ .unwrap();
1257
1654
  std::fs::write(team.join("agents").join("implementer.md"), QS_VALID_ROLE).unwrap(); // existing agent
1258
1655
  let role_file = team.join("worker2-role.md"); // OUTSIDE agents/ -> not a duplicate of an existing agent
1259
1656
  std::fs::write(&role_file, DELEG_ROLE_WORKER2).unwrap();
1260
1657
  seed_healthy_coordinator(&team);
1261
1658
  let transport = OfflineTransport::new();
1262
1659
 
1263
- let _result = add_agent_with_transport(&team, &AgentId::new("worker2"), &role_file, false, None, &transport);
1660
+ let _result = add_agent_with_transport(
1661
+ &team,
1662
+ &AgentId::new("worker2"),
1663
+ &role_file,
1664
+ false,
1665
+ None,
1666
+ &transport,
1667
+ );
1264
1668
 
1265
1669
  // (a) the recompiled spec was written (real subsystem step). E5: under .team/runtime/<team_key>/,
1266
1670
  // NOT the user team dir.
@@ -1286,6 +1690,419 @@ fn add_agent_with_transport_spawns_new_worker_not_stub() {
1286
1690
  );
1287
1691
  }
1288
1692
 
1693
+ #[test]
1694
+ fn add_agent_role_doc_exists_runtime_missing_succeeds() {
1695
+ let team = temp_ws().join("add_role_doc_team");
1696
+ std::fs::create_dir_all(team.join("agents")).unwrap();
1697
+ std::fs::write(
1698
+ team.join("TEAM.md"),
1699
+ "---\nname: addroledoc\nobjective: Add role-doc probe.\nprovider: codex\n---\n\nteam.\n",
1700
+ )
1701
+ .unwrap();
1702
+ std::fs::write(team.join("agents").join("implementer.md"), QS_VALID_ROLE).unwrap();
1703
+ let role_file = team.join("agents").join("worker2.md");
1704
+ std::fs::write(&role_file, DELEG_ROLE_WORKER2).unwrap();
1705
+ crate::state::persist::save_runtime_state(
1706
+ &team,
1707
+ &json!({
1708
+ "session_name": "team-addroledoc",
1709
+ "agents": {
1710
+ "implementer": {"status": "running", "provider": "codex", "window": "implementer"}
1711
+ }
1712
+ }),
1713
+ )
1714
+ .unwrap();
1715
+ seed_healthy_coordinator(&team);
1716
+ let transport = OfflineTransport::new();
1717
+
1718
+ let result = add_agent_with_transport(
1719
+ &team,
1720
+ &AgentId::new("worker2"),
1721
+ &role_file,
1722
+ false,
1723
+ None,
1724
+ &transport,
1725
+ );
1726
+
1727
+ assert!(
1728
+ result.is_ok(),
1729
+ "a prepared agents/<id>.md role doc is not membership; runtime state is authoritative: {result:?}"
1730
+ );
1731
+ let state = crate::state::persist::load_runtime_state(&team).expect("load state");
1732
+ assert!(
1733
+ state.pointer("/agents/worker2").is_some(),
1734
+ "add-agent must add worker2 to runtime state; state={state}"
1735
+ );
1736
+ assert_eq!(
1737
+ transport.spawn_records().len(),
1738
+ 1,
1739
+ "add-agent must spawn exactly the new worker"
1740
+ );
1741
+ }
1742
+
1743
+ #[test]
1744
+ fn start_agent_runtime_missing_role_doc_exists_no_side_effect() {
1745
+ let team = temp_ws().join("start_missing_team");
1746
+ std::fs::create_dir_all(team.join("agents")).unwrap();
1747
+ std::fs::write(
1748
+ team.join("TEAM.md"),
1749
+ "---\nname: startmissing\nobjective: Start missing probe.\nprovider: codex\n---\n\nteam.\n",
1750
+ )
1751
+ .unwrap();
1752
+ std::fs::write(team.join("agents").join("reviewer.md"), DELEG_ROLE_WORKER2).unwrap();
1753
+ crate::state::persist::save_runtime_state(
1754
+ &team,
1755
+ &json!({
1756
+ "session_name": "team-startmissing",
1757
+ "agents": {
1758
+ "implementer": {"status": "running", "provider": "codex", "window": "implementer"}
1759
+ }
1760
+ }),
1761
+ )
1762
+ .unwrap();
1763
+ let transport = OfflineTransport::new();
1764
+
1765
+ let result = start_agent_with_transport(
1766
+ &team,
1767
+ &AgentId::new("reviewer"),
1768
+ false,
1769
+ false,
1770
+ true,
1771
+ None,
1772
+ &transport,
1773
+ );
1774
+
1775
+ let text = format!("{result:?}");
1776
+ assert!(
1777
+ text.contains("not found"),
1778
+ "start-agent must refuse runtime-missing agents before side effects; got {text}"
1779
+ );
1780
+ assert!(
1781
+ transport.spawn_records().is_empty(),
1782
+ "runtime-missing start-agent must not spawn a pane"
1783
+ );
1784
+ assert!(
1785
+ !team
1786
+ .join(".team")
1787
+ .join("runtime")
1788
+ .join("mcp")
1789
+ .join("reviewer.json")
1790
+ .exists(),
1791
+ "runtime-missing start-agent must not write worker MCP config"
1792
+ );
1793
+ let events = crate::event_log::EventLog::new(&team)
1794
+ .tail(20)
1795
+ .unwrap_or_default();
1796
+ assert!(
1797
+ !events.iter().any(|event| {
1798
+ event.get("event").and_then(|v| v.as_str()) == Some("start_agent.agent_start")
1799
+ }),
1800
+ "runtime-missing start-agent must not emit start_agent.agent_start; events={events:?}"
1801
+ );
1802
+ }
1803
+
1804
+ #[test]
1805
+ fn restart_allow_fresh_copilot_persists_expected_session_id() {
1806
+ let ws = temp_ws().join("restartcopilot");
1807
+ std::fs::create_dir_all(ws.join("agents")).unwrap();
1808
+ std::fs::write(
1809
+ ws.join("TEAM.md"),
1810
+ "---\nname: restartcopilot\nobjective: Restart copilot probe.\nprovider: copilot\n---\n\nteam.\n",
1811
+ )
1812
+ .unwrap();
1813
+ std::fs::write(
1814
+ ws.join("agents").join("alpha.md"),
1815
+ "---\nname: alpha\nrole: Alpha Worker\nprovider: copilot\nmodel: gpt-5.5\nauth_mode: subscription\ntools:\n - mcp_team\n---\n\nAlpha.\n",
1816
+ )
1817
+ .unwrap();
1818
+ let spec = crate::compiler::compile_team(&ws).expect("compile copilot team");
1819
+ std::fs::write(ws.join("team.spec.yaml"), crate::model::yaml::dumps(&spec)).unwrap();
1820
+ crate::state::persist::save_runtime_state(
1821
+ &ws,
1822
+ &json!({
1823
+ "session_name": "team-restartcopilot",
1824
+ "agents": {
1825
+ "alpha": {
1826
+ "status": "running",
1827
+ "provider": "copilot",
1828
+ "window": "alpha",
1829
+ "first_send_at": "2026-05-27T10:00:00+00:00",
1830
+ "session_id": null,
1831
+ "rollout_path": "/tmp/stale-copilot-rollout.jsonl",
1832
+ "captured_at": "2026-05-27T10:00:00+00:00",
1833
+ "captured_via": "stale",
1834
+ "attribution_confidence": "stale"
1835
+ }
1836
+ }
1837
+ }),
1838
+ )
1839
+ .unwrap();
1840
+ seed_healthy_coordinator(&ws);
1841
+ let transport = OfflineTransport::new();
1842
+
1843
+ let result = restart_with_transport(&ws, true, None, &transport);
1844
+
1845
+ assert!(
1846
+ matches!(result, Ok(RestartReport::Restarted { .. })),
1847
+ "allow-fresh restart should fresh-spawn the copilot worker; got {result:?}"
1848
+ );
1849
+ let recorded = transport.spawn_records();
1850
+ assert_eq!(
1851
+ recorded.len(),
1852
+ 1,
1853
+ "expected one copilot spawn; got {recorded:?}"
1854
+ );
1855
+ let session_arg = recorded[0]
1856
+ .1
1857
+ .windows(2)
1858
+ .find_map(|pair| (pair[0] == "--session-id").then(|| pair[1].clone()))
1859
+ .expect("copilot fresh spawn argv must include --session-id");
1860
+ let state = crate::state::persist::load_runtime_state(&ws).expect("load state");
1861
+ let agent = state.pointer("/agents/alpha").expect("alpha state");
1862
+ assert_eq!(
1863
+ agent.get("session_id").and_then(serde_json::Value::as_str),
1864
+ Some(session_arg.as_str()),
1865
+ "fresh restart must promote expected_session_id to persisted session_id"
1866
+ );
1867
+ assert_eq!(
1868
+ agent
1869
+ .get("_pending_session_id")
1870
+ .and_then(serde_json::Value::as_str),
1871
+ Some(session_arg.as_str()),
1872
+ "fresh restart must retain pending session audit state"
1873
+ );
1874
+ for stale in [
1875
+ "rollout_path",
1876
+ "captured_at",
1877
+ "captured_via",
1878
+ "attribution_confidence",
1879
+ ] {
1880
+ assert_eq!(
1881
+ agent.get(stale),
1882
+ Some(&serde_json::Value::Null),
1883
+ "fresh restart must clear stale capture field {stale}: {agent}"
1884
+ );
1885
+ }
1886
+ let plan = crate::lifecycle::restart::classify_restart_plan(&state, false)
1887
+ .expect("classify second restart");
1888
+ assert!(
1889
+ plan.unresumable
1890
+ .iter()
1891
+ .all(|worker| worker.reason != "no_persisted_session_id"),
1892
+ "second restart must not loop as no_persisted_session_id; unresumable={:?}",
1893
+ plan.unresumable
1894
+ );
1895
+ }
1896
+
1897
+ #[test]
1898
+ fn add_agent_adaptive_splits_last_non_full_layout_window() {
1899
+ let roles = [("w1.md", role_doc("w1")), ("w2.md", role_doc("w2"))];
1900
+ let role_refs = roles
1901
+ .iter()
1902
+ .map(|(file, doc)| (*file, doc.as_str()))
1903
+ .collect::<Vec<_>>();
1904
+ let team = quick_start_team_dir_with_roles(&role_refs);
1905
+ let session = "team-layout-add";
1906
+ crate::state::persist::save_runtime_state(
1907
+ &team,
1908
+ &json!({
1909
+ "session_name": session,
1910
+ "display_backend": "adaptive",
1911
+ "active_team_key": "teamdir",
1912
+ "agents": {
1913
+ "w1": {"status": "running", "provider": "codex", "window": "team-w1", "layout_window": "team-w1", "layout_index": 0, "pane_index": 0, "pane_id": "%1"},
1914
+ "w2": {"status": "running", "provider": "codex", "window": "team-w1", "layout_window": "team-w1", "layout_index": 0, "pane_index": 1, "pane_id": "%2"}
1915
+ }
1916
+ }),
1917
+ )
1918
+ .unwrap();
1919
+ seed_healthy_coordinator(&team);
1920
+ let role_file = team.join("w3-role.md");
1921
+ std::fs::write(&role_file, role_doc("w3")).unwrap();
1922
+ let transport = OfflineTransport::new()
1923
+ .with_session_present(true)
1924
+ .with_targets(vec![
1925
+ layout_pane(session, "team-w1", "%1", 0),
1926
+ layout_pane(session, "team-w1", "%2", 1),
1927
+ ])
1928
+ .with_pane_presence("%1", true)
1929
+ .with_pane_presence("%2", true);
1930
+
1931
+ add_agent_with_transport(
1932
+ &team,
1933
+ &AgentId::new("w3"),
1934
+ &role_file,
1935
+ true,
1936
+ None,
1937
+ &transport,
1938
+ )
1939
+ .expect("adaptive add-agent should start");
1940
+
1941
+ assert_eq!(
1942
+ transport.spawn_window_records().last(),
1943
+ Some(&(String::from("spawn_split"), String::from("team-w1"))),
1944
+ "last non-full layout window should be split"
1945
+ );
1946
+ let (_raw, state) = raw_runtime_state(&team);
1947
+ assert_eq!(state.pointer("/agents/w3/layout_window"), Some(&json!("team-w1")));
1948
+ assert_eq!(state.pointer("/agents/w3/pane_index"), Some(&json!(2)));
1949
+ }
1950
+
1951
+ #[test]
1952
+ fn add_agent_adaptive_creates_suffix_window_when_last_layout_full_and_name_collides() {
1953
+ let roles = [
1954
+ ("w1.md", role_doc("w1")),
1955
+ ("w2.md", role_doc("w2")),
1956
+ ("w3.md", role_doc("w3")),
1957
+ ];
1958
+ let role_refs = roles
1959
+ .iter()
1960
+ .map(|(file, doc)| (*file, doc.as_str()))
1961
+ .collect::<Vec<_>>();
1962
+ let team = quick_start_team_dir_with_roles(&role_refs);
1963
+ let session = "team-layout-add-full";
1964
+ crate::state::persist::save_runtime_state(
1965
+ &team,
1966
+ &json!({
1967
+ "session_name": session,
1968
+ "display_backend": "adaptive",
1969
+ "active_team_key": "teamdir",
1970
+ "agents": {
1971
+ "w1": {"status": "running", "provider": "codex", "window": "team-w1", "layout_window": "team-w1", "layout_index": 0, "pane_index": 0, "pane_id": "%1"},
1972
+ "w2": {"status": "running", "provider": "codex", "window": "team-w1", "layout_window": "team-w1", "layout_index": 0, "pane_index": 1, "pane_id": "%2"},
1973
+ "w3": {"status": "running", "provider": "codex", "window": "team-w1", "layout_window": "team-w1", "layout_index": 0, "pane_index": 2, "pane_id": "%3"}
1974
+ }
1975
+ }),
1976
+ )
1977
+ .unwrap();
1978
+ seed_healthy_coordinator(&team);
1979
+ let role_file = team.join("w4-role.md");
1980
+ std::fs::write(&role_file, role_doc("w4")).unwrap();
1981
+ let transport = OfflineTransport::new()
1982
+ .with_session_present(true)
1983
+ .with_targets(vec![
1984
+ layout_pane(session, "team-w1", "%1", 0),
1985
+ layout_pane(session, "team-w1", "%2", 1),
1986
+ layout_pane(session, "team-w1", "%3", 2),
1987
+ layout_pane(session, "team-w2", "%9", 0),
1988
+ ])
1989
+ .with_pane_presence("%1", true)
1990
+ .with_pane_presence("%2", true)
1991
+ .with_pane_presence("%3", true);
1992
+
1993
+ add_agent_with_transport(
1994
+ &team,
1995
+ &AgentId::new("w4"),
1996
+ &role_file,
1997
+ true,
1998
+ None,
1999
+ &transport,
2000
+ )
2001
+ .expect("adaptive add-agent should start");
2002
+
2003
+ assert_eq!(
2004
+ transport.spawn_window_records().last(),
2005
+ Some(&(String::from("spawn_into"), String::from("team-w2-2"))),
2006
+ "full last layout window should create next window with collision suffix"
2007
+ );
2008
+ let (_raw, state) = raw_runtime_state(&team);
2009
+ assert_eq!(state.pointer("/agents/w4/layout_window"), Some(&json!("team-w2-2")));
2010
+ assert_eq!(state.pointer("/agents/w4/pane_index"), Some(&json!(0)));
2011
+ }
2012
+
2013
+ #[test]
2014
+ fn stop_agent_adaptive_kills_target_pane_not_shared_layout_window() {
2015
+ let roles = [("w1.md", role_doc("w1")), ("w2.md", role_doc("w2"))];
2016
+ let role_refs = roles
2017
+ .iter()
2018
+ .map(|(file, doc)| (*file, doc.as_str()))
2019
+ .collect::<Vec<_>>();
2020
+ let team = quick_start_team_dir_with_roles(&role_refs);
2021
+ let spec = crate::compiler::compile_team(&team).unwrap();
2022
+ std::fs::write(team.join("team.spec.yaml"), crate::model::yaml::dumps(&spec)).unwrap();
2023
+ let session = "team-layout-stop";
2024
+ crate::state::persist::save_runtime_state(
2025
+ &team,
2026
+ &json!({
2027
+ "session_name": session,
2028
+ "display_backend": "adaptive",
2029
+ "agents": {
2030
+ "w1": {"status": "running", "provider": "codex", "window": "team-w1", "layout_window": "team-w1", "layout_index": 0, "pane_index": 0, "pane_id": "%1"},
2031
+ "w2": {"status": "running", "provider": "codex", "window": "team-w1", "layout_window": "team-w1", "layout_index": 0, "pane_index": 1, "pane_id": "%2"}
2032
+ }
2033
+ }),
2034
+ )
2035
+ .unwrap();
2036
+ let transport = OfflineTransport::new()
2037
+ .with_targets(vec![
2038
+ layout_pane(session, "team-w1", "%1", 0),
2039
+ layout_pane(session, "team-w1", "%2", 1),
2040
+ ])
2041
+ .with_pane_presence("%2", true);
2042
+
2043
+ let report =
2044
+ stop_agent_with_transport(&team, &AgentId::new("w2"), None, &transport).unwrap();
2045
+
2046
+ assert!(report.stopped);
2047
+ assert_eq!(report.target, "%2");
2048
+ assert!(
2049
+ transport.calls().iter().any(|call| *call == "kill_pane"),
2050
+ "adaptive stop must kill only the target pane"
2051
+ );
2052
+ assert!(
2053
+ !transport.calls().iter().any(|call| *call == "kill_window"),
2054
+ "adaptive stop must not kill the shared layout window"
2055
+ );
2056
+ let (_raw, state) = raw_runtime_state(&team);
2057
+ assert_eq!(state.pointer("/agents/w1/pane_id"), Some(&json!("%1")));
2058
+ assert_eq!(state.pointer("/agents/w1/layout_window"), Some(&json!("team-w1")));
2059
+ }
2060
+
2061
+ #[test]
2062
+ fn start_agent_adaptive_restarts_missing_pane_in_existing_layout_window() {
2063
+ let ws = temp_ws();
2064
+ crate::state::persist::save_runtime_state(
2065
+ &ws,
2066
+ &json!({
2067
+ "session_name": "team-layout-restart",
2068
+ "display_backend": "adaptive",
2069
+ "agents": {
2070
+ "w1": {"status": "running", "provider": "codex", "role": "w1", "model": "gpt-5.5", "auth_mode": "subscription", "window": "team-w1", "layout_window": "team-w1", "layout_index": 0, "pane_index": 0, "pane_id": "%1"},
2071
+ "w2": {"status": "running", "provider": "codex", "role": "w2", "model": "gpt-5.5", "auth_mode": "subscription", "window": "team-w1", "layout_window": "team-w1", "layout_index": 0, "pane_index": 1, "pane_id": "%2"}
2072
+ }
2073
+ }),
2074
+ )
2075
+ .unwrap();
2076
+ seed_healthy_coordinator(&ws);
2077
+ let transport = OfflineTransport::new()
2078
+ .with_session_present(true)
2079
+ .with_targets(vec![layout_pane("team-layout-restart", "team-w1", "%2", 1)])
2080
+ .with_pane_presence("%1", false)
2081
+ .with_pane_presence("%2", true);
2082
+
2083
+ let outcome = start_agent_with_transport(
2084
+ &ws,
2085
+ &AgentId::new("w1"),
2086
+ false,
2087
+ true,
2088
+ true,
2089
+ None,
2090
+ &transport,
2091
+ )
2092
+ .expect("adaptive restart should spawn");
2093
+
2094
+ assert!(matches!(outcome, StartAgentOutcome::Running { .. }));
2095
+ assert_eq!(
2096
+ transport.spawn_window_records().last(),
2097
+ Some(&(String::from("spawn_split"), String::from("team-w1"))),
2098
+ "missing worker pane should be rebuilt in its existing layout window"
2099
+ );
2100
+ let (_raw, state) = raw_runtime_state(&ws);
2101
+ assert_eq!(state.pointer("/agents/w1/layout_window"), Some(&json!("team-w1")));
2102
+ assert_eq!(state.pointer("/agents/w1/window"), Some(&json!("team-w1")));
2103
+ assert_eq!(state.pointer("/agents/w2/pane_id"), Some(&json!("%2")));
2104
+ }
2105
+
1289
2106
  // ═════════════════════════════════════════════════════════════════════════
1290
2107
  // WAVE 2 · LANE A — agent-lifecycle byte-parity contracts. stop_agent / reset_agent / remove_agent /
1291
2108
  // fork_agent are stubs (OwnerRefused / RequirementUnmet / "session_id missing"). These RED contracts
@@ -1306,7 +2123,9 @@ fn quick_start_seeds_tasks_key_from_compiled_spec() {
1306
2123
  let team = quick_start_team_dir(QS_VALID_ROLE);
1307
2124
  let transport = OfflineTransport::new();
1308
2125
  let _ = quick_start_with_transport(&team, None, true, true, None, &transport);
1309
- let workspace = team.parent().expect("team_workspace(<base>/teamdir) = <base>");
2126
+ let workspace = team
2127
+ .parent()
2128
+ .expect("team_workspace(<base>/teamdir) = <base>");
1310
2129
  let state = crate::state::persist::load_runtime_state(workspace)
1311
2130
  .expect("runtime state.json must exist after quick_start");
1312
2131
  // (b) the tasks KEY must always be present and a JSON array (golden launch/core.py:69 default []).
@@ -1318,10 +2137,14 @@ fn quick_start_seeds_tasks_key_from_compiled_spec() {
1318
2137
  state.as_object().map(|o| o.keys().cloned().collect::<Vec<_>>())
1319
2138
  ),
1320
2139
  };
1321
- let tasks = tasks.as_array().expect("`tasks` must be a JSON array (golden default [])");
2140
+ let tasks = tasks
2141
+ .as_array()
2142
+ .expect("`tasks` must be a JSON array (golden default [])");
1322
2143
  // (a) spec.tasks must be carried into runtime state — the doc compiler's default task id.
1323
2144
  assert!(
1324
- tasks.iter().any(|t| t.get("id").and_then(|v| v.as_str()) == Some("task_initial")),
2145
+ tasks
2146
+ .iter()
2147
+ .any(|t| t.get("id").and_then(|v| v.as_str()) == Some("task_initial")),
1325
2148
  "state.tasks must carry the compiled spec's task (id=task_initial); got {tasks:?}"
1326
2149
  );
1327
2150
  }
@@ -1355,11 +2178,18 @@ fn quick_start_state_seeds_spec_path_workspace_leader_display_backend() {
1355
2178
  let team = quick_start_team_dir(QS_VALID_ROLE);
1356
2179
  let transport = OfflineTransport::new();
1357
2180
  let _ = quick_start_with_transport(&team, None, true, true, None, &transport);
1358
- let workspace = team.parent().expect("team_workspace(<base>/teamdir) = <base>");
2181
+ let workspace = team
2182
+ .parent()
2183
+ .expect("team_workspace(<base>/teamdir) = <base>");
1359
2184
  // E5: spec_path in state now points to .team/runtime/<team_key>/, team_dir stays the user role dir.
1360
2185
  let spec_path = quick_start_runtime_spec_path(&team);
1361
2186
  let (raw, state) = raw_runtime_state(workspace);
1362
- let keys = state.as_object().expect("state root object").keys().cloned().collect::<Vec<_>>();
2187
+ let keys = state
2188
+ .as_object()
2189
+ .expect("state root object")
2190
+ .keys()
2191
+ .cloned()
2192
+ .collect::<Vec<_>>();
1363
2193
  assert_eq!(
1364
2194
  keys,
1365
2195
  vec![
@@ -1382,7 +2212,10 @@ fn quick_start_state_seeds_spec_path_workspace_leader_display_backend() {
1382
2212
  truth (no shadow copy on the root that callers could read across teams); \
1383
2213
  raw={raw}"
1384
2214
  );
1385
- assert_eq!(state["spec_path"], json!(spec_path.canonicalize().unwrap().to_string_lossy()));
2215
+ assert_eq!(
2216
+ state["spec_path"],
2217
+ json!(spec_path.canonicalize().unwrap().to_string_lossy())
2218
+ );
1386
2219
  assert_eq!(state["workspace"], json!(workspace.to_string_lossy()));
1387
2220
  assert_eq!(state["team_dir"], json!(team.to_string_lossy()));
1388
2221
  assert_eq!(state["session_name"], json!("team-quickteam"));
@@ -1392,21 +2225,25 @@ fn quick_start_state_seeds_spec_path_workspace_leader_display_backend() {
1392
2225
  "leader must be copied from compiled spec.leader"
1393
2226
  );
1394
2227
  assert!(
1395
- state["agents"].as_object().is_some_and(|agents| agents.contains_key("implementer")),
2228
+ state["agents"]
2229
+ .as_object()
2230
+ .is_some_and(|agents| agents.contains_key("implementer")),
1396
2231
  "existing agents value must remain seeded from compiled spec; got {:?}",
1397
2232
  state["agents"]
1398
2233
  );
1399
2234
  assert!(
1400
2235
  state["tasks"].as_array().is_some_and(|tasks| {
1401
- tasks.iter().any(|task| task.get("id").and_then(|id| id.as_str()) == Some("task_initial"))
2236
+ tasks
2237
+ .iter()
2238
+ .any(|task| task.get("id").and_then(|id| id.as_str()) == Some("task_initial"))
1402
2239
  }),
1403
2240
  "existing tasks value must remain seeded from compiled spec; got {:?}",
1404
2241
  state["tasks"]
1405
2242
  );
1406
2243
  assert_eq!(
1407
2244
  state["display_backend"],
1408
- json!("none"),
1409
- "golden resolve_display_backend(None, source='launch') defaults to none"
2245
+ json!("adaptive"),
2246
+ "adaptive layout directive: resolve_display_backend(None, source='launch') defaults to adaptive"
1410
2247
  );
1411
2248
  }
1412
2249
 
@@ -1418,17 +2255,23 @@ fn quick_start_state_seeds_spec_path_workspace_leader_display_backend() {
1418
2255
  #[serial(env)]
1419
2256
  fn quick_start_running_agent_state_shape_after_spawn_is_golden() {
1420
2257
  const FIXED_SPAWNED_AT: &str = "2026-06-04T00:00:00+00:00";
1421
- let _clock_guard =
1422
- EnvVarGuard::set("TEAM_AGENT_TEST_FIXED_SPAWNED_AT", FIXED_SPAWNED_AT);
2258
+ let _clock_guard = EnvVarGuard::set("TEAM_AGENT_TEST_FIXED_SPAWNED_AT", FIXED_SPAWNED_AT);
1423
2259
  let team = quick_start_team_dir(QS_VALID_ROLE);
1424
2260
  let transport = OfflineTransport::new();
1425
2261
  let _ = quick_start_with_transport(&team, None, true, true, None, &transport);
1426
- let workspace = team.parent().expect("team_workspace(<base>/teamdir) = <base>");
2262
+ let workspace = team
2263
+ .parent()
2264
+ .expect("team_workspace(<base>/teamdir) = <base>");
1427
2265
  let (raw, state) = raw_runtime_state(workspace);
1428
2266
  let agent = state
1429
2267
  .pointer("/agents/implementer")
1430
2268
  .unwrap_or_else(|| panic!("implementer agent state missing; raw={raw}"));
1431
- let keys = agent.as_object().expect("agent state object").keys().cloned().collect::<Vec<_>>();
2269
+ let keys = agent
2270
+ .as_object()
2271
+ .expect("agent state object")
2272
+ .keys()
2273
+ .cloned()
2274
+ .collect::<Vec<_>>();
1432
2275
  assert_eq!(
1433
2276
  keys,
1434
2277
  vec![
@@ -1447,11 +2290,15 @@ fn quick_start_running_agent_state_shape_after_spawn_is_golden() {
1447
2290
  "captured_at",
1448
2291
  "captured_via",
1449
2292
  "attribution_confidence",
2293
+ "layout_window",
2294
+ "layout_index",
2295
+ "pane_index",
2296
+ "display",
1450
2297
  "spawn_cwd",
1451
2298
  "spawned_at",
1452
2299
  "pane_id",
1453
2300
  ],
1454
- "running agent state key order must match golden launch/core.py:238-255; raw={raw}"
2301
+ "running agent state key order must include adaptive layout fields; raw={raw}"
1455
2302
  );
1456
2303
  assert_eq!(agent["status"], json!("running"));
1457
2304
  assert_eq!(agent["provider"], json!("codex"));
@@ -1459,10 +2306,12 @@ fn quick_start_running_agent_state_shape_after_spawn_is_golden() {
1459
2306
  assert_eq!(agent["model"], json!("gpt-5.5"));
1460
2307
  assert_eq!(agent["auth_mode"], json!("subscription"));
1461
2308
  assert!(agent["profile"].is_null());
1462
- assert_eq!(agent["window"], json!("implementer"));
2309
+ assert_eq!(agent["window"], json!("team-w1"));
1463
2310
  assert_eq!(
1464
2311
  agent["mcp_config"],
1465
- json!(workspace.join(".team/runtime/mcp/implementer.json").to_string_lossy())
2312
+ json!(workspace
2313
+ .join(".team/runtime/mcp/implementer.json")
2314
+ .to_string_lossy())
1466
2315
  );
1467
2316
  assert_eq!(
1468
2317
  agent["permissions"],
@@ -1479,6 +2328,12 @@ fn quick_start_running_agent_state_shape_after_spawn_is_golden() {
1479
2328
  assert!(agent["captured_at"].is_null());
1480
2329
  assert!(agent["captured_via"].is_null());
1481
2330
  assert!(agent["attribution_confidence"].is_null());
2331
+ assert_eq!(agent["layout_window"], json!("team-w1"));
2332
+ assert_eq!(agent["layout_index"], json!(0));
2333
+ assert_eq!(agent["pane_index"], json!(0));
2334
+ assert_eq!(agent.pointer("/display/backend"), Some(&json!("adaptive")));
2335
+ assert_eq!(agent.pointer("/display/window"), Some(&json!("team-w1")));
2336
+ assert_eq!(agent.pointer("/display/pane_id"), Some(&json!("%0")));
1482
2337
  // D5 (#264) / Python launch/core.py:253 — fresh launch persists spawn_cwd=workspace.
1483
2338
  assert_eq!(agent["spawn_cwd"], json!(workspace.to_string_lossy()));
1484
2339
  assert_eq!(agent["spawned_at"], json!(FIXED_SPAWNED_AT));
@@ -1495,7 +2350,9 @@ fn quick_start_running_agent_state_shape_after_spawn_is_golden() {
1495
2350
  fn quick_start_paused_agent_state_is_paused_provider_only_and_not_spawned() {
1496
2351
  let team = quick_start_team_dir(QS_VALID_ROLE);
1497
2352
  let spec_path = compiled_spec_path_with_paused_agent(&team);
1498
- let workspace = team.parent().expect("team_workspace(<base>/teamdir) = <base>");
2353
+ let workspace = team
2354
+ .parent()
2355
+ .expect("team_workspace(<base>/teamdir) = <base>");
1499
2356
  crate::state::persist::save_runtime_state(
1500
2357
  workspace,
1501
2358
  &json!({
@@ -1519,7 +2376,12 @@ fn quick_start_paused_agent_state_is_paused_provider_only_and_not_spawned() {
1519
2376
  let agent = state
1520
2377
  .pointer("/agents/implementer")
1521
2378
  .unwrap_or_else(|| panic!("implementer agent state missing; raw={raw}"));
1522
- let keys = agent.as_object().expect("agent state object").keys().cloned().collect::<Vec<_>>();
2379
+ let keys = agent
2380
+ .as_object()
2381
+ .expect("agent state object")
2382
+ .keys()
2383
+ .cloned()
2384
+ .collect::<Vec<_>>();
1523
2385
  assert_eq!(
1524
2386
  keys,
1525
2387
  vec!["status", "provider"],