@team-agent/installer 0.3.21 → 0.3.23

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 (31) hide show
  1. package/Cargo.lock +1 -1
  2. package/Cargo.toml +1 -1
  3. package/crates/team-agent/src/cli/emit.rs +9 -1
  4. package/crates/team-agent/src/cli/mod.rs +158 -2
  5. package/crates/team-agent/src/cli/status.rs +61 -0
  6. package/crates/team-agent/src/cli/status_port.rs +2 -2
  7. package/crates/team-agent/src/cli/tests/base.rs +26 -0
  8. package/crates/team-agent/src/cli/tests/shutdown_kill_plan.rs +233 -9
  9. package/crates/team-agent/src/coordinator/tests/energy.rs +548 -0
  10. package/crates/team-agent/src/coordinator/tests/mod.rs +1 -0
  11. package/crates/team-agent/src/coordinator/tick.rs +452 -17
  12. package/crates/team-agent/src/lifecycle/display.rs +23 -302
  13. package/crates/team-agent/src/lifecycle/launch.rs +38 -0
  14. package/crates/team-agent/src/lifecycle/restart/agent.rs +27 -6
  15. package/crates/team-agent/src/lifecycle/restart/common.rs +179 -0
  16. package/crates/team-agent/src/lifecycle/restart/rebuild.rs +295 -13
  17. package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +30 -0
  18. package/crates/team-agent/src/lifecycle/tests/main_preserved.rs +89 -5
  19. package/crates/team-agent/src/messaging/delivery.rs +324 -95
  20. package/crates/team-agent/src/messaging/scheduler.rs +95 -73
  21. package/crates/team-agent/src/messaging/send.rs +10 -0
  22. package/crates/team-agent/src/messaging/tests/runtime.rs +460 -0
  23. package/crates/team-agent/src/messaging/tests/spine.rs +51 -2
  24. package/crates/team-agent/src/session_capture.rs +261 -1
  25. package/crates/team-agent/src/tmux_backend/tests.rs +97 -1
  26. package/crates/team-agent/src/tmux_backend.rs +138 -2
  27. package/crates/team-agent/src/transport/test_support.rs +25 -0
  28. package/crates/team-agent/src/transport.rs +11 -0
  29. package/package.json +4 -4
  30. package/skills/team-agent/SKILL.md +2 -1
  31. package/skills/team-agent/references/team-in-team.md +127 -0
@@ -6,8 +6,8 @@
6
6
 
7
7
  use crate::cli::lifecycle_port::{sessions_to_kill, KillDecision};
8
8
  use crate::transport::{
9
- AttachOutcome, BackendKind, CaptureRange, CapturedText, InjectPayload, InjectReport,
10
- Key, PaneField, PaneId, PaneInfo, SessionName, SetEnvOutcome, SpawnResult, Target, Transport,
9
+ AttachOutcome, BackendKind, CaptureRange, CapturedText, InjectPayload, InjectReport, Key,
10
+ PaneField, PaneId, PaneInfo, SessionName, SetEnvOutcome, SpawnResult, Target, Transport,
11
11
  TransportError, WindowName,
12
12
  };
13
13
  use serde_json::json;
@@ -33,7 +33,10 @@ fn rc4_exclusive_socket_kills_server() {
33
33
  // 空 session 集 → 逐 kill(no-op),不整 server 拆(没东西可拆)。
34
34
  assert_eq!(
35
35
  sessions_to_kill(&[], &BTreeSet::new()),
36
- KillDecision::KillIndividually { to_kill: vec![], spared: vec![] }
36
+ KillDecision::KillIndividually {
37
+ to_kill: vec![],
38
+ spared: vec![]
39
+ }
37
40
  );
38
41
  }
39
42
 
@@ -45,8 +48,16 @@ fn rc1_in_tmux_no_prefix_anchor_spares_user_session() {
45
48
  let decision = sessions_to_kill(&sessions, &anchor);
46
49
  match decision {
47
50
  KillDecision::KillIndividually { to_kill, spared } => {
48
- assert_eq!(to_kill, names(&["team-x"]), "only non-anchor session killed");
49
- assert_eq!(spared, names(&["team-coder-team"]), "user/leader session spared by anchor");
51
+ assert_eq!(
52
+ to_kill,
53
+ names(&["team-x"]),
54
+ "only non-anchor session killed"
55
+ );
56
+ assert_eq!(
57
+ spared,
58
+ names(&["team-coder-team"]),
59
+ "user/leader session spared by anchor"
60
+ );
50
61
  }
51
62
  other => panic!("anchor session must force per-session kill, not {other:?}"),
52
63
  }
@@ -106,7 +117,10 @@ fn union_prefix_and_anchor_no_double_count() {
106
117
  let decision = sessions_to_kill(&sessions, &anchor);
107
118
  assert_eq!(
108
119
  decision,
109
- KillDecision::KillIndividually { to_kill: vec![], spared: names(&["team-agent-leader-claude-ws-beef"]) }
120
+ KillDecision::KillIndividually {
121
+ to_kill: vec![],
122
+ spared: names(&["team-agent-leader-claude-ws-beef"])
123
+ }
110
124
  );
111
125
  }
112
126
 
@@ -136,12 +150,214 @@ fn missing_coordinator_is_ok_when_shutdown_cleaned_session() {
136
150
  .expect("shutdown should complete");
137
151
  assert_eq!(out["coordinator"]["status"], json!("missing"));
138
152
  assert_eq!(
139
- out["ok"], json!(true),
153
+ out["ok"],
154
+ json!(true),
140
155
  "coordinator.status=missing alone must not make a fully cleaned shutdown partial: {out}"
141
156
  );
142
157
  assert_eq!(out["status"], json!("ok"));
143
158
  assert_eq!(out["residuals"]["sessions"], json!([]));
144
159
  assert_eq!(out["residuals"]["processes"], json!([]));
160
+ assert_eq!(out["residuals"]["owned_files"], json!([]));
161
+ }
162
+
163
+ #[test]
164
+ fn owned_empty_endpoint_cleanup_removes_socket_file_before_reporting_ok() {
165
+ let ws = tmp_shutdown_workspace("owned-empty-endpoint-clean");
166
+ let socket = ws.join("owned.sock");
167
+ std::fs::write(&socket, b"socket").unwrap();
168
+ crate::state::persist::save_runtime_state(
169
+ &ws,
170
+ &json!({
171
+ "session_name": "team-owned-clean",
172
+ "tmux_endpoint": socket.to_string_lossy(),
173
+ "tmux_socket": socket.to_string_lossy(),
174
+ "tmux_socket_source": "workspace",
175
+ "is_external_leader": false,
176
+ "agents": {}
177
+ }),
178
+ )
179
+ .unwrap();
180
+ let transport = CleanShutdownTransport::new();
181
+
182
+ let out = crate::cli::lifecycle_port::shutdown_with_transport(&ws, true, None, &transport)
183
+ .expect("shutdown should complete");
184
+
185
+ assert_eq!(out["ok"], json!(true));
186
+ assert_eq!(out["status"], json!("ok"));
187
+ assert_eq!(out["residuals"]["owned_files"], json!([]));
188
+ assert!(
189
+ !socket.exists(),
190
+ "owned empty endpoint socket file must be removed"
191
+ );
192
+ assert!(
193
+ transport.kill_server_called(),
194
+ "owned empty endpoint should be torn down after session cleanup"
195
+ );
196
+ }
197
+
198
+ #[test]
199
+ fn leader_env_endpoint_is_not_owned_or_removed() {
200
+ let ws = tmp_shutdown_workspace("leader-env-endpoint-spared");
201
+ let socket = ws.join("leader-env.sock");
202
+ std::fs::write(&socket, b"socket").unwrap();
203
+ crate::state::persist::save_runtime_state(
204
+ &ws,
205
+ &json!({
206
+ "session_name": "team-external",
207
+ "tmux_endpoint": socket.to_string_lossy(),
208
+ "tmux_socket": socket.to_string_lossy(),
209
+ "tmux_socket_source": "leader_env",
210
+ "is_external_leader": true,
211
+ "agents": {}
212
+ }),
213
+ )
214
+ .unwrap();
215
+ let transport = CleanShutdownTransport::new();
216
+
217
+ let out = crate::cli::lifecycle_port::shutdown_with_transport(&ws, true, None, &transport)
218
+ .expect("shutdown should complete");
219
+
220
+ assert_eq!(out["ok"], json!(true));
221
+ assert_eq!(out["residuals"]["owned_files"], json!([]));
222
+ assert!(socket.exists(), "leader_env socket is not team-owned");
223
+ assert!(
224
+ !transport.kill_server_called(),
225
+ "leader_env/shared socket must never be torn down by owned cleanup"
226
+ );
227
+ }
228
+
229
+ #[test]
230
+ fn owned_file_residual_makes_shutdown_failed_not_partial() {
231
+ let ws = tmp_shutdown_workspace("owned-file-residual-failed");
232
+ let socket_dir = ws.join("owned-dir.sock");
233
+ std::fs::create_dir_all(&socket_dir).unwrap();
234
+ crate::state::persist::save_runtime_state(
235
+ &ws,
236
+ &json!({
237
+ "session_name": "team-owned-residual",
238
+ "tmux_endpoint": socket_dir.to_string_lossy(),
239
+ "tmux_socket": socket_dir.to_string_lossy(),
240
+ "tmux_socket_source": "workspace",
241
+ "is_external_leader": false,
242
+ "agents": {}
243
+ }),
244
+ )
245
+ .unwrap();
246
+ let transport = CleanShutdownTransport::new();
247
+
248
+ let out = crate::cli::lifecycle_port::shutdown_with_transport(&ws, true, None, &transport)
249
+ .expect("shutdown should complete");
250
+
251
+ assert_eq!(out["ok"], json!(false));
252
+ assert_eq!(out["status"], json!("failed"));
253
+ assert_eq!(out["phase"], json!(null));
254
+ assert_eq!(
255
+ out["residuals"]["owned_files"],
256
+ json!([{ "path": socket_dir.display().to_string() }])
257
+ );
258
+ }
259
+
260
+ #[test]
261
+ fn scoped_shutdown_keeps_owned_endpoint_when_sibling_session_remains() {
262
+ let ws = tmp_shutdown_workspace("scoped-owned-endpoint-sibling");
263
+ let socket = ws.join("owned-shared.sock");
264
+ std::fs::write(&socket, b"socket").unwrap();
265
+ crate::state::persist::save_runtime_state(
266
+ &ws,
267
+ &json!({
268
+ "active_team_key": "team-a",
269
+ "teams": {
270
+ "team-a": {
271
+ "team_key": "team-a",
272
+ "status": "alive",
273
+ "session_name": "team-a",
274
+ "tmux_endpoint": socket.to_string_lossy(),
275
+ "tmux_socket": socket.to_string_lossy(),
276
+ "tmux_socket_source": "workspace",
277
+ "is_external_leader": false,
278
+ "agents": {}
279
+ },
280
+ "team-b": {
281
+ "team_key": "team-b",
282
+ "status": "alive",
283
+ "session_name": "team-b",
284
+ "tmux_endpoint": socket.to_string_lossy(),
285
+ "tmux_socket": socket.to_string_lossy(),
286
+ "tmux_socket_source": "workspace",
287
+ "is_external_leader": false,
288
+ "agents": {}
289
+ }
290
+ }
291
+ }),
292
+ )
293
+ .unwrap();
294
+ let transport = CleanShutdownTransport::new()
295
+ .with_targets(vec![PaneInfo {
296
+ pane_id: PaneId::new("%2"),
297
+ session: SessionName::new("team-b"),
298
+ window_index: Some(0),
299
+ window_name: Some(WindowName::new("worker")),
300
+ pane_index: Some(0),
301
+ tty: None,
302
+ current_command: Some("fake".to_string()),
303
+ current_path: None,
304
+ active: true,
305
+ pane_pid: None,
306
+ leader_env: BTreeMap::new(),
307
+ }])
308
+ .with_targets_persist_after_kill();
309
+
310
+ let out =
311
+ crate::cli::lifecycle_port::shutdown_with_transport(&ws, true, Some("team-a"), &transport)
312
+ .expect("shutdown should complete");
313
+
314
+ assert_eq!(out["ok"], json!(true));
315
+ assert_eq!(out["residuals"]["owned_files"], json!([]));
316
+ assert!(
317
+ socket.exists(),
318
+ "owned endpoint stays while sibling team session remains"
319
+ );
320
+ assert!(
321
+ !transport.kill_server_called(),
322
+ "scoped shutdown must not tear down a non-empty shared owned endpoint"
323
+ );
324
+ }
325
+
326
+ #[test]
327
+ fn repeated_owned_endpoint_shutdowns_leave_no_socket_file_growth() {
328
+ let ws = tmp_shutdown_workspace("owned-loop-no-growth");
329
+ let sockets = (0..20)
330
+ .map(|idx| ws.join(format!("owned-loop-{idx}.sock")))
331
+ .collect::<Vec<_>>();
332
+ let starting = sockets.iter().filter(|path| path.exists()).count();
333
+ for socket in &sockets {
334
+ std::fs::write(socket, b"socket").unwrap();
335
+ crate::state::persist::save_runtime_state(
336
+ &ws,
337
+ &json!({
338
+ "session_name": "team-owned-loop",
339
+ "tmux_endpoint": socket.to_string_lossy(),
340
+ "tmux_socket": socket.to_string_lossy(),
341
+ "tmux_socket_source": "workspace",
342
+ "is_external_leader": false,
343
+ "agents": {}
344
+ }),
345
+ )
346
+ .unwrap();
347
+ let out = crate::cli::lifecycle_port::shutdown_with_transport(
348
+ &ws,
349
+ true,
350
+ None,
351
+ &CleanShutdownTransport::new(),
352
+ )
353
+ .expect("shutdown should complete");
354
+ assert_eq!(out["ok"], json!(true), "shutdown report: {out}");
355
+ }
356
+ let ending = sockets.iter().filter(|path| path.exists()).count();
357
+ assert_eq!(
358
+ ending, starting,
359
+ "owned socket files must not grow across loops"
360
+ );
145
361
  }
146
362
 
147
363
  fn tmp_shutdown_workspace(tag: &str) -> PathBuf {
@@ -241,7 +457,10 @@ impl Transport for CleanShutdownTransport {
241
457
  _target: &Target,
242
458
  range: CaptureRange,
243
459
  ) -> Result<CapturedText, TransportError> {
244
- Ok(CapturedText { text: String::new(), range })
460
+ Ok(CapturedText {
461
+ text: String::new(),
462
+ range,
463
+ })
245
464
  }
246
465
 
247
466
  fn query(&self, _target: &Target, _field: PaneField) -> Result<Option<String>, TransportError> {
@@ -337,7 +556,9 @@ fn lsof_cwd_timeout_is_diagnostic_not_shutdown_partial() {
337
556
  assert_eq!(out["residuals"]["sessions"], json!([]));
338
557
  assert_eq!(out["residuals"]["processes"], json!([]));
339
558
 
340
- let events = crate::event_log::EventLog::new(&ws).tail(0).expect("events");
559
+ let events = crate::event_log::EventLog::new(&ws)
560
+ .tail(0)
561
+ .expect("events");
341
562
  let shutdown = events
342
563
  .iter()
343
564
  .find(|event| {
@@ -412,6 +633,7 @@ fn shutdown_outcome_late_or_postcheck_gone_is_ok_with_lsof_diagnostic() {
412
633
  kill_error: false,
413
634
  session_residuals: false,
414
635
  process_residuals: false,
636
+ owned_file_residuals: false,
415
637
  cleanup_truth_degraded: false,
416
638
  coordinator_timeout: true,
417
639
  coordinator_stop_ok: None,
@@ -431,6 +653,7 @@ fn shutdown_outcome_coordinator_timeout_still_running_is_timeout() {
431
653
  kill_error: false,
432
654
  session_residuals: false,
433
655
  process_residuals: false,
656
+ owned_file_residuals: false,
434
657
  cleanup_truth_degraded: false,
435
658
  coordinator_timeout: true,
436
659
  coordinator_stop_ok: None,
@@ -450,6 +673,7 @@ fn shutdown_outcome_ps_table_degraded_still_partial_after_coordinator_gone() {
450
673
  kill_error: false,
451
674
  session_residuals: false,
452
675
  process_residuals: false,
676
+ owned_file_residuals: false,
453
677
  cleanup_truth_degraded: true,
454
678
  coordinator_timeout: true,
455
679
  coordinator_stop_ok: None,