@team-agent/installer 0.5.42 → 0.5.44

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 (156) hide show
  1. package/Cargo.lock +1 -1
  2. package/Cargo.toml +1 -1
  3. package/crates/team-agent/src/cli/adapters.rs +129 -54
  4. package/crates/team-agent/src/cli/diagnose.rs +1 -2
  5. package/crates/team-agent/src/cli/emit.rs +1 -7
  6. package/crates/team-agent/src/cli/helpers.rs +3 -1
  7. package/crates/team-agent/src/cli/leader.rs +2 -1
  8. package/crates/team-agent/src/cli/mod.rs +126 -16
  9. package/crates/team-agent/src/cli/named_address.rs +14 -5
  10. package/crates/team-agent/src/cli/profile.rs +19 -7
  11. package/crates/team-agent/src/cli/send.rs +9 -3
  12. package/crates/team-agent/src/cli/status.rs +8 -30
  13. package/crates/team-agent/src/cli/status_port.rs +1341 -1272
  14. package/crates/team-agent/src/cli/tests/base.rs +738 -660
  15. package/crates/team-agent/src/cli/tests/compile.rs +45 -18
  16. package/crates/team-agent/src/cli/tests/divergence.rs +462 -444
  17. package/crates/team-agent/src/cli/tests/lane_c.rs +365 -282
  18. package/crates/team-agent/src/cli/tests/leader_watch.rs +356 -329
  19. package/crates/team-agent/src/cli/tests/main_preserved.rs +672 -564
  20. package/crates/team-agent/src/cli/tests/missing_subcommands.rs +284 -224
  21. package/crates/team-agent/src/cli/tests/mod.rs +17 -7
  22. package/crates/team-agent/src/cli/tests/named_address.rs +8 -2
  23. package/crates/team-agent/src/cli/tests/peer_allow.rs +10 -2
  24. package/crates/team-agent/src/cli/tests/run_delegation.rs +314 -273
  25. package/crates/team-agent/src/cli/tests/shutdown_kill_plan.rs +26 -15
  26. package/crates/team-agent/src/cli/tests/status_send.rs +707 -629
  27. package/crates/team-agent/src/cli/tests/verb_install_skill.rs +20 -4
  28. package/crates/team-agent/src/cli/tests/verb_profile.rs +46 -17
  29. package/crates/team-agent/src/cli/tests/verb_validate.rs +15 -3
  30. package/crates/team-agent/src/codex_app_server.rs +2 -5
  31. package/crates/team-agent/src/compiler/tests.rs +139 -33
  32. package/crates/team-agent/src/compiler.rs +55 -22
  33. package/crates/team-agent/src/conpty/backend.rs +23 -33
  34. package/crates/team-agent/src/coordinator/backoff.rs +2 -7
  35. package/crates/team-agent/src/coordinator/conpty_shim.rs +55 -67
  36. package/crates/team-agent/src/coordinator/health.rs +46 -31
  37. package/crates/team-agent/src/coordinator/mod.rs +3 -3
  38. package/crates/team-agent/src/coordinator/orphan.rs +22 -10
  39. package/crates/team-agent/src/coordinator/steps/abnormal.rs +51 -56
  40. package/crates/team-agent/src/coordinator/tests/abnormal.rs +55 -19
  41. package/crates/team-agent/src/coordinator/tests/basics.rs +179 -41
  42. package/crates/team-agent/src/coordinator/tests/daemon.rs +53 -13
  43. package/crates/team-agent/src/coordinator/tests/health_sync.rs +78 -19
  44. package/crates/team-agent/src/coordinator/tests/main_preserved.rs +61 -11
  45. package/crates/team-agent/src/coordinator/tests/mod.rs +33 -39
  46. package/crates/team-agent/src/coordinator/tests/spine.rs +52 -12
  47. package/crates/team-agent/src/coordinator/tests/takeover.rs +73 -15
  48. package/crates/team-agent/src/coordinator/tests/tick_core.rs +50 -15
  49. package/crates/team-agent/src/coordinator/tests/watch.rs +74 -20
  50. package/crates/team-agent/src/db/message_store.rs +138 -30
  51. package/crates/team-agent/src/db/migration.rs +249 -61
  52. package/crates/team-agent/src/db/schema.rs +303 -82
  53. package/crates/team-agent/src/diagnose/comms.rs +9 -2
  54. package/crates/team-agent/src/diagnose/mod.rs +1 -3
  55. package/crates/team-agent/src/diagnose/orphans.rs +79 -61
  56. package/crates/team-agent/src/event_log.rs +70 -16
  57. package/crates/team-agent/src/layout/manager.rs +15 -4
  58. package/crates/team-agent/src/layout/mod.rs +4 -4
  59. package/crates/team-agent/src/layout/overlay.rs +10 -3
  60. package/crates/team-agent/src/layout/placement.rs +5 -1
  61. package/crates/team-agent/src/layout/recovery.rs +4 -2
  62. package/crates/team-agent/src/layout/runtime_sessions.rs +7 -7
  63. package/crates/team-agent/src/layout/sessions.rs +17 -9
  64. package/crates/team-agent/src/layout/tmux_endpoint.rs +1 -1
  65. package/crates/team-agent/src/layout/worker_env.rs +87 -19
  66. package/crates/team-agent/src/leader/helpers.rs +7 -1
  67. package/crates/team-agent/src/leader/lease.rs +199 -89
  68. package/crates/team-agent/src/leader/owner_bind.rs +55 -22
  69. package/crates/team-agent/src/leader/provider_attribution.rs +25 -6
  70. package/crates/team-agent/src/leader/rediscover/tests.rs +88 -24
  71. package/crates/team-agent/src/leader/rediscover.rs +74 -25
  72. package/crates/team-agent/src/leader/registry.rs +1 -1
  73. package/crates/team-agent/src/leader/start.rs +75 -54
  74. package/crates/team-agent/src/leader/takeover.rs +46 -11
  75. package/crates/team-agent/src/leader/tests/basics.rs +320 -167
  76. package/crates/team-agent/src/leader/tests/byte_findings.rs +361 -219
  77. package/crates/team-agent/src/leader/tests/identity.rs +428 -356
  78. package/crates/team-agent/src/leader/tests/idle.rs +285 -254
  79. package/crates/team-agent/src/leader/tests/lease_api.rs +338 -274
  80. package/crates/team-agent/src/leader/tests/lease_claim.rs +643 -593
  81. package/crates/team-agent/src/leader/tests/mod.rs +115 -99
  82. package/crates/team-agent/src/leader/tests/rediscover.rs +74 -22
  83. package/crates/team-agent/src/leader/tests/wake_start_owner.rs +237 -211
  84. package/crates/team-agent/src/lib.rs +4 -4
  85. package/crates/team-agent/src/lifecycle/display.rs +7 -3
  86. package/crates/team-agent/src/lifecycle/launch.rs +55 -15
  87. package/crates/team-agent/src/lifecycle/mod.rs +9 -1
  88. package/crates/team-agent/src/lifecycle/profile_launch.rs +77 -34
  89. package/crates/team-agent/src/lifecycle/profile_smoke.rs +3 -1
  90. package/crates/team-agent/src/lifecycle/restart/agent.rs +1 -6
  91. package/crates/team-agent/src/lifecycle/restart/common.rs +6 -2
  92. package/crates/team-agent/src/lifecycle/restart/orchestrator.rs +1 -4
  93. package/crates/team-agent/src/lifecycle/restart/preflight.rs +6 -5
  94. package/crates/team-agent/src/lifecycle/restart/rebuild.rs +26 -22
  95. package/crates/team-agent/src/lifecycle/restart/remove.rs +45 -35
  96. package/crates/team-agent/src/lifecycle/restart/team_state.rs +63 -17
  97. package/crates/team-agent/src/lifecycle/tests/agent_ops.rs +251 -84
  98. package/crates/team-agent/src/lifecycle/tests/lane_ops.rs +198 -48
  99. package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +2 -1
  100. package/crates/team-agent/src/lifecycle/tests/main_preserved.rs +152 -32
  101. package/crates/team-agent/src/lifecycle/tests/phase_b_contracts.rs +1 -5
  102. package/crates/team-agent/src/lifecycle/tests/phase_golden.rs +3 -0
  103. package/crates/team-agent/src/lifecycle/tests.rs +2 -2
  104. package/crates/team-agent/src/main.rs +4 -4
  105. package/crates/team-agent/src/mcp_server/lifecycle_tools/agent_ops.rs +47 -20
  106. package/crates/team-agent/src/mcp_server/mod.rs +11 -2
  107. package/crates/team-agent/src/mcp_server/types.rs +14 -3
  108. package/crates/team-agent/src/mcp_server/wire.rs +230 -68
  109. package/crates/team-agent/src/messaging/delivery.rs +20 -18
  110. package/crates/team-agent/src/messaging/helpers.rs +25 -4
  111. package/crates/team-agent/src/messaging/leader_receiver.rs +4 -5
  112. package/crates/team-agent/src/messaging/mod.rs +2 -3
  113. package/crates/team-agent/src/messaging/selftest.rs +46 -26
  114. package/crates/team-agent/src/messaging/tests/main_preserved.rs +47 -12
  115. package/crates/team-agent/src/messaging/tests/runtime.rs +57 -22
  116. package/crates/team-agent/src/messaging/tests/spine.rs +154 -40
  117. package/crates/team-agent/src/messaging/tests/wave2.rs +31 -32
  118. package/crates/team-agent/src/messaging/trust.rs +25 -2
  119. package/crates/team-agent/src/messaging/watchers.rs +37 -9
  120. package/crates/team-agent/src/model/enums.rs +65 -16
  121. package/crates/team-agent/src/model/ids.rs +12 -3
  122. package/crates/team-agent/src/model/paths.rs +28 -7
  123. package/crates/team-agent/src/model/permissions.rs +176 -33
  124. package/crates/team-agent/src/model/routing.rs +66 -20
  125. package/crates/team-agent/src/model/spec.rs +365 -69
  126. package/crates/team-agent/src/model/task_graph.rs +36 -9
  127. package/crates/team-agent/src/model/yaml/tests.rs +24 -6
  128. package/crates/team-agent/src/model/yaml.rs +7 -6
  129. package/crates/team-agent/src/packaging/install.rs +23 -9
  130. package/crates/team-agent/src/packaging/migrate.rs +5 -7
  131. package/crates/team-agent/src/packaging/mod.rs +9 -1
  132. package/crates/team-agent/src/packaging/repair.rs +13 -6
  133. package/crates/team-agent/src/packaging/tests.rs +63 -16
  134. package/crates/team-agent/src/packaging/types.rs +22 -7
  135. package/crates/team-agent/src/platform/argv.rs +4 -1
  136. package/crates/team-agent/src/platform/file_lock.rs +22 -8
  137. package/crates/team-agent/src/platform/process.rs +54 -24
  138. package/crates/team-agent/src/provider/adapters/claude.rs +1 -3
  139. package/crates/team-agent/src/provider/approvals/parsing.rs +134 -26
  140. package/crates/team-agent/src/provider/approvals/runtime_prompts.rs +13 -3
  141. package/crates/team-agent/src/provider/classify.rs +127 -42
  142. package/crates/team-agent/src/provider/faults.rs +17 -5
  143. package/crates/team-agent/src/provider/helpers.rs +6 -5
  144. package/crates/team-agent/src/provider/startup_prompt.rs +75 -27
  145. package/crates/team-agent/src/state/persist.rs +28 -0
  146. package/crates/team-agent/src/state/repository.rs +8 -3
  147. package/crates/team-agent/src/tmux_backend/tests.rs +1637 -1398
  148. package/crates/team-agent/src/tmux_backend.rs +80 -44
  149. package/crates/team-agent/src/topology.rs +40 -20
  150. package/crates/team-agent/src/transport/test_support.rs +20 -23
  151. package/crates/team-agent/src/transport/tests/behavior.rs +292 -293
  152. package/crates/team-agent/src/transport/tests/mod.rs +178 -187
  153. package/crates/team-agent/src/transport/tests/wire.rs +561 -525
  154. package/crates/team-agent/src/transport.rs +12 -17
  155. package/crates/team-agent/src/transport_factory.rs +29 -14
  156. package/package.json +4 -4
@@ -1,8 +1,8 @@
1
- use super::*;
2
1
  use super::launch_spawn::{
3
2
  quick_start_team_dir, seed_healthy_coordinator, DELEG_ROLE_ALPHA, DELEG_ROLE_BRAVO,
4
3
  QS_VALID_ROLE,
5
4
  };
5
+ use super::*;
6
6
  use crate::transport::test_support::OfflineTransport;
7
7
 
8
8
  /// A no-owner workspace (= self-contained team dir) with a compiled 2-agent spec (alpha, bravo) + state
@@ -10,7 +10,11 @@ use crate::transport::test_support::OfflineTransport;
10
10
  pub(super) fn lanea_team_ws(status: &str) -> PathBuf {
11
11
  let ws = temp_ws().join("laneateam");
12
12
  std::fs::create_dir_all(ws.join("agents")).unwrap();
13
- std::fs::write(ws.join("TEAM.md"), "---\nname: laneateam\nobjective: Lane A probe.\nprovider: codex\n---\n\nteam.\n").unwrap();
13
+ std::fs::write(
14
+ ws.join("TEAM.md"),
15
+ "---\nname: laneateam\nobjective: Lane A probe.\nprovider: codex\n---\n\nteam.\n",
16
+ )
17
+ .unwrap();
14
18
  std::fs::write(ws.join("agents").join("alpha.md"), DELEG_ROLE_ALPHA).unwrap();
15
19
  std::fs::write(ws.join("agents").join("bravo.md"), DELEG_ROLE_BRAVO).unwrap();
16
20
  let spec = crate::compiler::compile_team(&ws).expect("compile lane-A team");
@@ -21,9 +25,18 @@ pub(super) fn lanea_team_ws(status: &str) -> PathBuf {
21
25
  .replace("default_assignee: \"alpha\"", "default_assignee: \"bravo\"")
22
26
  .replace("assign_to: \"alpha\"", "assign_to: \"bravo\"")
23
27
  .replace("assignee: \"alpha\"", "assignee: \"bravo\"");
24
- assert!(!yaml.contains("default_assignee: \"alpha\""), "fixture unroute: default_assignee still alpha");
25
- assert!(!yaml.contains("assign_to: \"alpha\""), "fixture unroute: a routing rule still assign_to alpha");
26
- assert!(!yaml.contains("assignee: \"alpha\""), "fixture unroute: task still assignee alpha");
28
+ assert!(
29
+ !yaml.contains("default_assignee: \"alpha\""),
30
+ "fixture unroute: default_assignee still alpha"
31
+ );
32
+ assert!(
33
+ !yaml.contains("assign_to: \"alpha\""),
34
+ "fixture unroute: a routing rule still assign_to alpha"
35
+ );
36
+ assert!(
37
+ !yaml.contains("assignee: \"alpha\""),
38
+ "fixture unroute: task still assignee alpha"
39
+ );
27
40
  std::fs::write(ws.join("team.spec.yaml"), yaml).unwrap();
28
41
  crate::state::persist::save_runtime_state(
29
42
  &ws,
@@ -90,7 +103,10 @@ fn lanea_stop_agent_unknown_agent_is_unknown_worker_not_owner_refused() {
90
103
  #[test]
91
104
  fn lanea_fork_agent_unknown_source_is_unknown_worker_before_session_check() {
92
105
  let ws = lanea_team_ws("stopped");
93
- let text = format!("{:?}", fork_agent(&ws, &aid("ghost"), &aid("newfork"), None, false, None));
106
+ let text = format!(
107
+ "{:?}",
108
+ fork_agent(&ws, &aid("ghost"), &aid("newfork"), None, false, None)
109
+ );
94
110
  assert!(
95
111
  text.contains("unknown worker"),
96
112
  "fork_agent must reject an UNKNOWN source as 'unknown worker agent id: ghost' BEFORE the session-id \
@@ -105,7 +121,10 @@ fn lanea_fork_agent_unknown_source_is_unknown_worker_before_session_check() {
105
121
  fn lanea_fork_agent_duplicate_target_is_already_exists_before_session_check() {
106
122
  let ws = lanea_team_ws("stopped");
107
123
  // target 'bravo' already exists in the spec -> duplicate; source 'alpha' exists (its session_id is irrelevant).
108
- let text = format!("{:?}", fork_agent(&ws, &aid("alpha"), &aid("bravo"), None, false, None));
124
+ let text = format!(
125
+ "{:?}",
126
+ fork_agent(&ws, &aid("alpha"), &aid("bravo"), None, false, None)
127
+ );
109
128
  assert!(
110
129
  text.contains("already exists"),
111
130
  "fork_agent must reject a DUPLICATE target 'bravo' as 'agent id already exists' BEFORE the session-id \
@@ -125,8 +144,14 @@ fn lanea_stop_agent_kills_window_and_marks_stopped() {
125
144
  let ws = lanea_team_ws("running");
126
145
  let _ = stop_agent(&ws, &aid("alpha"), None); // real machine: tmux kill-window team-laneateam:alpha
127
146
  let state = crate::state::persist::load_runtime_state(&ws).expect("load state");
128
- let status = state.pointer("/agents/alpha/status").and_then(serde_json::Value::as_str);
129
- assert_eq!(status, Some("stopped"), "stop_agent must mark agents[alpha].status='stopped' after killing the window");
147
+ let status = state
148
+ .pointer("/agents/alpha/status")
149
+ .and_then(serde_json::Value::as_str);
150
+ assert_eq!(
151
+ status,
152
+ Some("stopped"),
153
+ "stop_agent must mark agents[alpha].status='stopped' after killing the window"
154
+ );
130
155
  }
131
156
 
132
157
  // ═════════════════════════════════════════════════════════════════════════
@@ -145,8 +170,10 @@ fn lanea_stop_agent_kills_window_and_marks_stopped() {
145
170
  #[test]
146
171
  fn add_agent_joins_w2_into_running_roster_and_existing_session() {
147
172
  let team_dir = quick_start_team_dir(QS_VALID_ROLE); // <base>/teamdir (agents/implementer.md)
148
- let workspace = team_dir.parent().expect("team_workspace(team_dir) = parent"); // the RUNNING team's workspace
149
- // a RUNNING team already in the session (roster = ['w1'], session_name set).
173
+ let workspace = team_dir
174
+ .parent()
175
+ .expect("team_workspace(team_dir) = parent"); // the RUNNING team's workspace
176
+ // a RUNNING team already in the session (roster = ['w1'], session_name set).
150
177
  crate::state::persist::save_runtime_state(
151
178
  workspace,
152
179
  &json!({
@@ -156,7 +183,7 @@ fn add_agent_joins_w2_into_running_roster_and_existing_session() {
156
183
  )
157
184
  .unwrap();
158
185
  seed_healthy_coordinator(workspace); // start_coordinator -> AlreadyRunning (no real daemon)
159
- // the new agent's role file, OUTSIDE agents/ so it's not a duplicate of an existing agent.
186
+ // the new agent's role file, OUTSIDE agents/ so it's not a duplicate of an existing agent.
160
187
  let role_file = team_dir.join("w2-role.md");
161
188
  std::fs::write(
162
189
  &role_file,
@@ -165,7 +192,14 @@ fn add_agent_joins_w2_into_running_roster_and_existing_session() {
165
192
  .unwrap();
166
193
  let transport = OfflineTransport::new().with_session_present(true);
167
194
 
168
- let _ = add_agent_with_transport(team_dir.as_path(), &aid("w2"), &role_file, false, None, &transport);
195
+ let _ = add_agent_with_transport(
196
+ team_dir.as_path(),
197
+ &aid("w2"),
198
+ &role_file,
199
+ false,
200
+ None,
201
+ &transport,
202
+ );
169
203
 
170
204
  // (1) [load-bearing] the RUNNING roster (team_workspace) must now contain w2.
171
205
  let state = crate::state::persist::load_runtime_state(workspace).expect("load running state");
@@ -218,11 +252,11 @@ impl crate::tmux_backend::CommandRunner for SocketRecordingRunner {
218
252
  fn socket_for_workspace(workspace: &std::path::Path) -> String {
219
253
  use crate::transport::Transport as _;
220
254
  let recorded = std::sync::Arc::new(std::sync::Mutex::new(Vec::new()));
221
- let runner = SocketRecordingRunner { recorded: std::sync::Arc::clone(&recorded) };
222
- let backend = crate::tmux_backend::TmuxBackend::with_runner_for_workspace(
223
- Box::new(runner),
224
- workspace,
225
- );
255
+ let runner = SocketRecordingRunner {
256
+ recorded: std::sync::Arc::clone(&recorded),
257
+ };
258
+ let backend =
259
+ crate::tmux_backend::TmuxBackend::with_runner_for_workspace(Box::new(runner), workspace);
226
260
  backend.has_session(&sess("team-implteam")).unwrap();
227
261
  let socket = recorded.lock().unwrap()[0][2].clone();
228
262
  socket
@@ -257,7 +291,10 @@ fn bug_a_team_dir_lifecycle_socket_matches_daemon_run_workspace_socket() {
257
291
  fn bug_a_stop_agent_team_dir_input_kills_existing_window_real_machine() {
258
292
  let team_dir = quick_start_team_dir(QS_VALID_ROLE);
259
293
  let report = stop_agent(&team_dir, &aid("w1"), None).expect("real stop-agent");
260
- assert!(report.stopped, "existing worker window must be killed, not silently reported absent");
294
+ assert!(
295
+ report.stopped,
296
+ "existing worker window must be killed, not silently reported absent"
297
+ );
261
298
  }
262
299
 
263
300
  struct EnvVarGuard {
@@ -467,11 +504,16 @@ fn e27_stop_agent_uses_attached_explicit_state_socket() {
467
504
 
468
505
  let report = stop_agent(&ws, &aid("alpha"), None).expect("stop-agent");
469
506
 
470
- assert!(report.stopped, "attached explicit-socket worker should be stopped");
507
+ assert!(
508
+ report.stopped,
509
+ "attached explicit-socket worker should be stopped"
510
+ );
471
511
  assert_only_expected_socket_used(&shim.log, endpoint);
472
512
  let state = crate::state::persist::load_runtime_state(&ws).unwrap();
473
513
  assert_eq!(
474
- state.pointer("/agents/alpha/status").and_then(serde_json::Value::as_str),
514
+ state
515
+ .pointer("/agents/alpha/status")
516
+ .and_then(serde_json::Value::as_str),
475
517
  Some("stopped")
476
518
  );
477
519
  }
@@ -494,14 +536,19 @@ fn e27_reset_agent_uses_attached_explicit_state_socket_for_stop_and_spawn() {
494
536
  );
495
537
  assert_only_expected_socket_used(&shim.log, endpoint);
496
538
  let raw = std::fs::read_to_string(&shim.log).unwrap();
497
- assert!(raw.contains("kill-pane"), "reset must stop the old pane first; argv log:\n{raw}");
539
+ assert!(
540
+ raw.contains("kill-pane"),
541
+ "reset must stop the old pane first; argv log:\n{raw}"
542
+ );
498
543
  assert!(
499
544
  raw.contains("new-window") || raw.contains("new-session"),
500
545
  "reset must respawn the worker through the same endpoint; argv log:\n{raw}"
501
546
  );
502
547
  let state = crate::state::persist::load_runtime_state(&ws).unwrap();
503
548
  assert_ne!(
504
- state.pointer("/agents/alpha/session_id").and_then(serde_json::Value::as_str),
549
+ state
550
+ .pointer("/agents/alpha/session_id")
551
+ .and_then(serde_json::Value::as_str),
505
552
  Some("old-session"),
506
553
  "discard-session must not preserve the old session id"
507
554
  );
@@ -553,10 +600,16 @@ fn e27_stop_agent_expands_short_state_socket_name() {
553
600
  // Read every events.jsonl the verb could write to (run-workspace resolves to either the team dir or its
554
601
  // parent; read both so the lock is robust to that resolution).
555
602
  fn lifecycle_events(ws: &std::path::Path) -> Vec<serde_json::Value> {
556
- let mut out = crate::event_log::EventLog::new(ws).tail(0).unwrap_or_default();
603
+ let mut out = crate::event_log::EventLog::new(ws)
604
+ .tail(0)
605
+ .unwrap_or_default();
557
606
  if let Ok(parent) = crate::model::paths::team_workspace(ws) {
558
607
  if parent != ws {
559
- out.extend(crate::event_log::EventLog::new(&parent).tail(0).unwrap_or_default());
608
+ out.extend(
609
+ crate::event_log::EventLog::new(&parent)
610
+ .tail(0)
611
+ .unwrap_or_default(),
612
+ );
560
613
  }
561
614
  }
562
615
  out
@@ -565,16 +618,30 @@ fn lifecycle_events(ws: &std::path::Path) -> Vec<serde_json::Value> {
565
618
  fn payload_keys(event: &serde_json::Value) -> std::collections::BTreeSet<String> {
566
619
  event
567
620
  .as_object()
568
- .map(|o| o.keys().filter(|k| *k != "ts" && *k != "event").cloned().collect())
621
+ .map(|o| {
622
+ o.keys()
623
+ .filter(|k| *k != "ts" && *k != "event")
624
+ .cloned()
625
+ .collect()
626
+ })
569
627
  .unwrap_or_default()
570
628
  }
571
629
 
572
630
  fn find_event<'a>(events: &'a [serde_json::Value], name: &str) -> Option<&'a serde_json::Value> {
573
- events.iter().find(|e| e.get("event").and_then(|v| v.as_str()) == Some(name))
631
+ events
632
+ .iter()
633
+ .find(|e| e.get("event").and_then(|v| v.as_str()) == Some(name))
574
634
  }
575
635
 
576
636
  fn names(events: &[serde_json::Value]) -> Vec<String> {
577
- events.iter().filter_map(|e| e.get("event").and_then(|v| v.as_str()).map(ToString::to_string)).collect()
637
+ events
638
+ .iter()
639
+ .filter_map(|e| {
640
+ e.get("event")
641
+ .and_then(|v| v.as_str())
642
+ .map(ToString::to_string)
643
+ })
644
+ .collect()
578
645
  }
579
646
 
580
647
  // remove-agent — golden agents.py:66-147 writes lifecycle.remove_step_completed (per step) +
@@ -590,30 +657,62 @@ fn remove_agent_emits_golden_lifecycle_event_payloads() {
590
657
  "remove_agent must write `remove_agent.complete` (golden agents.py:140); Rust restart/remove.rs emits NO \
591
658
  events. events seen: {:?}", names(&events)
592
659
  ));
593
- let expected: std::collections::BTreeSet<String> =
594
- ["agent_id", "from_spec", "force", "stopped", "role_file_removed", "cleared_locations"]
595
- .iter().map(ToString::to_string).collect();
596
- assert_eq!(payload_keys(complete), expected,
597
- "remove_agent.complete payload key set must match golden agents.py:140-147; got {:?}", payload_keys(complete));
598
- assert_eq!(complete.get("agent_id").and_then(|v| v.as_str()), Some("alpha"));
599
- assert_eq!(complete.get("from_spec").and_then(|v| v.as_bool()), Some(true));
660
+ let expected: std::collections::BTreeSet<String> = [
661
+ "agent_id",
662
+ "from_spec",
663
+ "force",
664
+ "stopped",
665
+ "role_file_removed",
666
+ "cleared_locations",
667
+ ]
668
+ .iter()
669
+ .map(ToString::to_string)
670
+ .collect();
671
+ assert_eq!(
672
+ payload_keys(complete),
673
+ expected,
674
+ "remove_agent.complete payload key set must match golden agents.py:140-147; got {:?}",
675
+ payload_keys(complete)
676
+ );
677
+ assert_eq!(
678
+ complete.get("agent_id").and_then(|v| v.as_str()),
679
+ Some("alpha")
680
+ );
681
+ assert_eq!(
682
+ complete.get("from_spec").and_then(|v| v.as_bool()),
683
+ Some(true)
684
+ );
600
685
  assert_eq!(complete.get("force").and_then(|v| v.as_bool()), Some(true));
601
686
 
602
687
  // lifecycle.remove_step_completed (golden agents.py:66-72) — fired per step; key set {agent_id, step,
603
688
  // resource}; the workspace_state + agent_health steps fire for a stopped from_spec remove.
604
- let step_events: Vec<&serde_json::Value> = events.iter()
605
- .filter(|e| e.get("event").and_then(|v| v.as_str()) == Some("lifecycle.remove_step_completed")).collect();
606
- let steps: Vec<&str> = step_events.iter().filter_map(|e| e.get("step").and_then(|v| v.as_str())).collect();
689
+ let step_events: Vec<&serde_json::Value> = events
690
+ .iter()
691
+ .filter(|e| {
692
+ e.get("event").and_then(|v| v.as_str()) == Some("lifecycle.remove_step_completed")
693
+ })
694
+ .collect();
695
+ let steps: Vec<&str> = step_events
696
+ .iter()
697
+ .filter_map(|e| e.get("step").and_then(|v| v.as_str()))
698
+ .collect();
607
699
  assert!(steps.contains(&"workspace_state") && steps.contains(&"agent_health"),
608
700
  "remove_agent must write lifecycle.remove_step_completed for each step (golden agents.py:86,109); got steps {steps:?}");
609
- let ws_step = step_events.iter().find(|e| e.get("step").and_then(|v| v.as_str()) == Some("workspace_state"))
701
+ let ws_step = step_events
702
+ .iter()
703
+ .find(|e| e.get("step").and_then(|v| v.as_str()) == Some("workspace_state"))
610
704
  .expect("the workspace_state step event");
611
- let expected_step: std::collections::BTreeSet<String> =
612
- ["agent_id", "step", "resource"].iter().map(ToString::to_string).collect();
705
+ let expected_step: std::collections::BTreeSet<String> = ["agent_id", "step", "resource"]
706
+ .iter()
707
+ .map(ToString::to_string)
708
+ .collect();
613
709
  assert_eq!(payload_keys(ws_step), expected_step,
614
710
  "lifecycle.remove_step_completed (non-stop step) payload must be EXACTLY {{agent_id, step, resource}} (golden agents.py:68-70)");
615
- assert_eq!(ws_step.get("resource").and_then(|v| v.as_str()), Some("state.json:agents"),
616
- "workspace_state step resource == golden 'state.json:agents' (agents.py:82)");
711
+ assert_eq!(
712
+ ws_step.get("resource").and_then(|v| v.as_str()),
713
+ Some("state.json:agents"),
714
+ "workspace_state step resource == golden 'state.json:agents' (agents.py:82)"
715
+ );
617
716
  }
618
717
 
619
718
  // reset-agent — golden operations.py:123/132 writes discard.session_tombstone {agent_id,
@@ -627,7 +726,14 @@ fn reset_agent_emits_golden_lifecycle_event_payloads() {
627
726
  crate::state::persist::save_runtime_state(&ws, &state).unwrap();
628
727
 
629
728
  let transport = OfflineTransport::new().with_session_present(true);
630
- let _ = crate::lifecycle::reset_agent_with_transport(&ws, &aid("alpha"), true, false, None, &transport);
729
+ let _ = crate::lifecycle::reset_agent_with_transport(
730
+ &ws,
731
+ &aid("alpha"),
732
+ true,
733
+ false,
734
+ None,
735
+ &transport,
736
+ );
631
737
  let events = lifecycle_events(&ws);
632
738
 
633
739
  // discard.session_tombstone (golden operations.py:123) — EXACT key set + the discarded session id.
@@ -635,23 +741,38 @@ fn reset_agent_emits_golden_lifecycle_event_payloads() {
635
741
  "reset_agent must write `discard.session_tombstone` (golden operations.py:123); Rust restart/agent.rs emits \
636
742
  NO events. events seen: {:?}", names(&events)
637
743
  ));
638
- let expected_tomb: std::collections::BTreeSet<String> =
639
- ["agent_id", "discarded_session_id"].iter().map(ToString::to_string).collect();
744
+ let expected_tomb: std::collections::BTreeSet<String> = ["agent_id", "discarded_session_id"]
745
+ .iter()
746
+ .map(ToString::to_string)
747
+ .collect();
640
748
  assert_eq!(payload_keys(tombstone), expected_tomb,
641
749
  "discard.session_tombstone payload key set must be EXACTLY {{agent_id, discarded_session_id}} (golden operations.py:123)");
642
- assert_eq!(tombstone.get("agent_id").and_then(|v| v.as_str()), Some("alpha"));
643
- assert_eq!(tombstone.get("discarded_session_id").and_then(|v| v.as_str()), Some("S-alpha"),
644
- "discarded_session_id == the agent's stored session_id (golden operations.py:118)");
750
+ assert_eq!(
751
+ tombstone.get("agent_id").and_then(|v| v.as_str()),
752
+ Some("alpha")
753
+ );
754
+ assert_eq!(
755
+ tombstone
756
+ .get("discarded_session_id")
757
+ .and_then(|v| v.as_str()),
758
+ Some("S-alpha"),
759
+ "discarded_session_id == the agent's stored session_id (golden operations.py:118)"
760
+ );
645
761
 
646
762
  // reset_agent.complete (golden operations.py:132) — EXACT key set {agent_id, stopped, started}.
647
763
  let complete = find_event(&events, "reset_agent.complete").unwrap_or_else(|| panic!(
648
764
  "reset_agent must write `reset_agent.complete` (golden operations.py:132); events seen: {:?}", names(&events)
649
765
  ));
650
- let expected_complete: std::collections::BTreeSet<String> =
651
- ["agent_id", "stopped", "started"].iter().map(ToString::to_string).collect();
766
+ let expected_complete: std::collections::BTreeSet<String> = ["agent_id", "stopped", "started"]
767
+ .iter()
768
+ .map(ToString::to_string)
769
+ .collect();
652
770
  assert_eq!(payload_keys(complete), expected_complete,
653
771
  "reset_agent.complete payload key set must be EXACTLY {{agent_id, stopped, started}} (golden operations.py:132)");
654
- assert_eq!(complete.get("agent_id").and_then(|v| v.as_str()), Some("alpha"));
772
+ assert_eq!(
773
+ complete.get("agent_id").and_then(|v| v.as_str()),
774
+ Some("alpha")
775
+ );
655
776
  }
656
777
 
657
778
  // 0.4.10+ reset duplicate-window fix RED tests
@@ -666,7 +787,7 @@ fn reset_agent_emits_golden_lifecycle_event_payloads() {
666
787
  /// kills by pane_id, returns stopped=true. Reset proceeds normally.
667
788
  #[test]
668
789
  fn reset_agent_stale_pane_with_same_role_residue_kills_by_pane_id() {
669
- use crate::transport::{PaneInfo, PaneId as PaneIdT, SessionName, WindowName};
790
+ use crate::transport::{PaneId as PaneIdT, PaneInfo, SessionName, WindowName};
670
791
  let ws = lanea_team_ws("running");
671
792
  let mut state = crate::state::persist::load_runtime_state(&ws).unwrap();
672
793
  state["agents"]["alpha"]["pane_id"] = json!("%STALE");
@@ -713,8 +834,8 @@ fn reset_agent_stale_pane_with_same_role_residue_kills_by_pane_id() {
713
834
  );
714
835
  // The stop_complete event must report stopped=true (residual was killed).
715
836
  let events = lifecycle_events(&ws);
716
- let stop_complete = find_event(&events, "stop_agent.complete")
717
- .expect("stop_agent.complete must be emitted");
837
+ let stop_complete =
838
+ find_event(&events, "stop_agent.complete").expect("stop_agent.complete must be emitted");
718
839
  assert_eq!(
719
840
  stop_complete.get("stopped").and_then(|v| v.as_bool()),
720
841
  Some(true),
@@ -756,8 +877,7 @@ fn reset_agent_stop_not_proven_grep_guard_hard_gate_wired() {
756
877
  "gate must run only when stop.stopped=false (the dangerous case)"
757
878
  );
758
879
  assert!(
759
- contents.contains("list_same_role_panes")
760
- && contents.contains("is_per_agent_window"),
880
+ contents.contains("list_same_role_panes") && contents.contains("is_per_agent_window"),
761
881
  "stop_agent_at_paths must enumerate same-role panes via the new helpers"
762
882
  );
763
883
  }
@@ -805,12 +925,7 @@ fn reset_agent_already_absent_can_start() {
805
925
  fn stop_agent_already_absent_still_returns_stopped_false() {
806
926
  let ws = lanea_team_ws("stopped");
807
927
  let transport = OfflineTransport::new().with_session_present(true);
808
- let result = crate::lifecycle::stop_agent_with_transport(
809
- &ws,
810
- &aid("alpha"),
811
- None,
812
- &transport,
813
- );
928
+ let result = crate::lifecycle::stop_agent_with_transport(&ws, &aid("alpha"), None, &transport);
814
929
  let report = result.expect("stop_agent must return Ok for absent agent");
815
930
  assert!(
816
931
  !report.stopped,
@@ -832,11 +947,16 @@ fn stop_agent_emits_golden_complete_event_payload() {
832
947
  "stop_agent must write `stop_agent.complete` (golden operations.py:98); Rust restart/agent.rs emits NO \
833
948
  events for stop. events seen: {:?}", names(&events)
834
949
  ));
835
- let expected: std::collections::BTreeSet<String> =
836
- ["agent_id", "target", "stopped"].iter().map(ToString::to_string).collect();
950
+ let expected: std::collections::BTreeSet<String> = ["agent_id", "target", "stopped"]
951
+ .iter()
952
+ .map(ToString::to_string)
953
+ .collect();
837
954
  assert_eq!(payload_keys(complete), expected,
838
955
  "stop_agent.complete payload key set must be EXACTLY {{agent_id, target, stopped}} (golden operations.py:98)");
839
- assert_eq!(complete.get("agent_id").and_then(|v| v.as_str()), Some("alpha"));
956
+ assert_eq!(
957
+ complete.get("agent_id").and_then(|v| v.as_str()),
958
+ Some("alpha")
959
+ );
840
960
  }
841
961
 
842
962
  // start-agent — golden start.py:225 writes start_agent.agent_start on the spawn path (9 keys). Rust emits
@@ -845,7 +965,15 @@ fn stop_agent_emits_golden_complete_event_payload() {
845
965
  fn start_agent_emits_golden_agent_start_event_payload() {
846
966
  let ws = lanea_team_ws("stopped"); // not running -> fresh spawn path (not noop)
847
967
  let transport = OfflineTransport::new();
848
- let _ = crate::lifecycle::start_agent_with_transport(&ws, &aid("alpha"), true, false, true, None, &transport);
968
+ let _ = crate::lifecycle::start_agent_with_transport(
969
+ &ws,
970
+ &aid("alpha"),
971
+ true,
972
+ false,
973
+ true,
974
+ None,
975
+ &transport,
976
+ );
849
977
  let events = lifecycle_events(&ws);
850
978
 
851
979
  let agent_start = find_event(&events, "start_agent.agent_start").unwrap_or_else(|| panic!(
@@ -853,14 +981,33 @@ fn start_agent_emits_golden_agent_start_event_payload() {
853
981
  start_agent.noop. events seen: {:?}", names(&events)
854
982
  ));
855
983
  let expected: std::collections::BTreeSet<String> = [
856
- "agent_id", "provider", "start_mode", "session_id", "session", "window",
857
- "tmux_start_mode", "command", "mcp_config",
858
- ].iter().map(ToString::to_string).collect();
859
- assert_eq!(payload_keys(agent_start), expected,
860
- "start_agent.agent_start payload key set must match golden start.py:225-235 (9 keys)");
861
- assert_eq!(agent_start.get("agent_id").and_then(|v| v.as_str()), Some("alpha"));
862
- assert_eq!(agent_start.get("window").and_then(|v| v.as_str()), Some("alpha"),
863
- "agent_start.window == agent_id (golden start.py:232)");
984
+ "agent_id",
985
+ "provider",
986
+ "start_mode",
987
+ "session_id",
988
+ "session",
989
+ "window",
990
+ "tmux_start_mode",
991
+ "command",
992
+ "mcp_config",
993
+ ]
994
+ .iter()
995
+ .map(ToString::to_string)
996
+ .collect();
997
+ assert_eq!(
998
+ payload_keys(agent_start),
999
+ expected,
1000
+ "start_agent.agent_start payload key set must match golden start.py:225-235 (9 keys)"
1001
+ );
1002
+ assert_eq!(
1003
+ agent_start.get("agent_id").and_then(|v| v.as_str()),
1004
+ Some("alpha")
1005
+ );
1006
+ assert_eq!(
1007
+ agent_start.get("window").and_then(|v| v.as_str()),
1008
+ Some("alpha"),
1009
+ "agent_start.window == agent_id (golden start.py:232)"
1010
+ );
864
1011
  }
865
1012
 
866
1013
  // restart — golden orchestration.py:507 writes ONE restart.resume_decision per non-paused worker
@@ -872,16 +1019,36 @@ fn restart_emits_golden_resume_decision_event_payload() {
872
1019
  let _ = crate::lifecycle::restart_with_transport(&ws, true, None, &transport);
873
1020
  let events = lifecycle_events(&ws);
874
1021
 
875
- let decisions: Vec<&serde_json::Value> = events.iter()
876
- .filter(|e| e.get("event").and_then(|v| v.as_str()) == Some("restart.resume_decision")).collect();
877
- assert!(!decisions.is_empty(), "restart must write one `restart.resume_decision` per non-paused worker \
878
- (golden orchestration.py:128/507); Rust emits NO restart events. events seen: {:?}", names(&events));
1022
+ let decisions: Vec<&serde_json::Value> = events
1023
+ .iter()
1024
+ .filter(|e| e.get("event").and_then(|v| v.as_str()) == Some("restart.resume_decision"))
1025
+ .collect();
1026
+ assert!(
1027
+ !decisions.is_empty(),
1028
+ "restart must write one `restart.resume_decision` per non-paused worker \
1029
+ (golden orchestration.py:128/507); Rust emits NO restart events. events seen: {:?}",
1030
+ names(&events)
1031
+ );
879
1032
  let expected: std::collections::BTreeSet<String> = [
880
- "worker_id", "has_first_send_at", "has_session_id", "allow_fresh", "decision", "first_send_at", "session_id",
881
- ].iter().map(ToString::to_string).collect();
1033
+ "worker_id",
1034
+ "has_first_send_at",
1035
+ "has_session_id",
1036
+ "allow_fresh",
1037
+ "decision",
1038
+ "first_send_at",
1039
+ "session_id",
1040
+ ]
1041
+ .iter()
1042
+ .map(ToString::to_string)
1043
+ .collect();
882
1044
  assert_eq!(payload_keys(decisions[0]), expected,
883
1045
  "restart.resume_decision payload key set must match golden orchestration.py:507-514 (7 keys, `worker_id`)");
884
- let worker_ids: Vec<&str> = decisions.iter().filter_map(|e| e.get("worker_id").and_then(|v| v.as_str())).collect();
885
- assert!(worker_ids.contains(&"alpha") || worker_ids.contains(&"bravo"),
886
- "restart.resume_decision.worker_id must name a real worker (golden); got {worker_ids:?}");
1046
+ let worker_ids: Vec<&str> = decisions
1047
+ .iter()
1048
+ .filter_map(|e| e.get("worker_id").and_then(|v| v.as_str()))
1049
+ .collect();
1050
+ assert!(
1051
+ worker_ids.contains(&"alpha") || worker_ids.contains(&"bravo"),
1052
+ "restart.resume_decision.worker_id must name a real worker (golden); got {worker_ids:?}"
1053
+ );
887
1054
  }