@team-agent/installer 0.5.75 → 0.5.78

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 (98) hide show
  1. package/Cargo.lock +1 -1
  2. package/Cargo.toml +1 -1
  3. package/README.md +9 -0
  4. package/README.zh.md +8 -0
  5. package/crates/team-agent/src/cli/adapters.rs +184 -125
  6. package/crates/team-agent/src/cli/diagnose.rs +333 -68
  7. package/crates/team-agent/src/cli/emit.rs +59 -15
  8. package/crates/team-agent/src/cli/mod.rs +600 -134
  9. package/crates/team-agent/src/cli/models.rs +485 -44
  10. package/crates/team-agent/src/cli/named_address.rs +19 -5
  11. package/crates/team-agent/src/cli/profile.rs +35 -4
  12. package/crates/team-agent/src/cli/spec.rs +7 -2
  13. package/crates/team-agent/src/cli/status_port/brief.rs +1137 -0
  14. package/crates/team-agent/src/cli/status_port/compact.rs +12 -8
  15. package/crates/team-agent/src/cli/status_port/snapshot.rs +15 -12
  16. package/crates/team-agent/src/cli/status_port/store.rs +1 -1
  17. package/crates/team-agent/src/cli/status_port.rs +8 -0
  18. package/crates/team-agent/src/cli/tests/missing_subcommands.rs +62 -8
  19. package/crates/team-agent/src/cli/tests/mod.rs +1 -0
  20. package/crates/team-agent/src/cli/tests/named_address.rs +70 -0
  21. package/crates/team-agent/src/cli/tests/run_delegation.rs +29 -24
  22. package/crates/team-agent/src/cli/tests/status_brief.rs +691 -0
  23. package/crates/team-agent/src/cli/tests/status_send.rs +8 -2
  24. package/crates/team-agent/src/cli/tests/verb_profile.rs +81 -6
  25. package/crates/team-agent/src/cli/types.rs +1 -0
  26. package/crates/team-agent/src/leader/lease.rs +538 -41
  27. package/crates/team-agent/src/leader/owner_bind.rs +40 -12
  28. package/crates/team-agent/src/leader/rediscover/tests.rs +65 -0
  29. package/crates/team-agent/src/leader/rediscover.rs +36 -6
  30. package/crates/team-agent/src/leader/start.rs +151 -14
  31. package/crates/team-agent/src/leader/tests/wake_start_owner.rs +15 -9
  32. package/crates/team-agent/src/leader/types.rs +4 -0
  33. package/crates/team-agent/src/lifecycle/launch/add_agent.rs +28 -227
  34. package/crates/team-agent/src/lifecycle/launch/add_agent_state.rs +0 -18
  35. package/crates/team-agent/src/lifecycle/launch/cursor_mcp.rs +94 -227
  36. package/crates/team-agent/src/lifecycle/launch/mcp_config.rs +59 -10
  37. package/crates/team-agent/src/lifecycle/launch/pi_mcp.rs +138 -14
  38. package/crates/team-agent/src/lifecycle/launch/quick_start.rs +1449 -282
  39. package/crates/team-agent/src/lifecycle/launch/readiness.rs +349 -4
  40. package/crates/team-agent/src/lifecycle/launch/spawn.rs +11 -11
  41. package/crates/team-agent/src/lifecycle/launch/spec_state.rs +35 -3
  42. package/crates/team-agent/src/lifecycle/launch/worker_env.rs +16 -0
  43. package/crates/team-agent/src/lifecycle/launch.rs +30 -7
  44. package/crates/team-agent/src/lifecycle/profile_launch.rs +37 -3
  45. package/crates/team-agent/src/lifecycle/restart/agent.rs +112 -100
  46. package/crates/team-agent/src/lifecycle/restart/common.rs +82 -52
  47. package/crates/team-agent/src/lifecycle/restart/rebuild.rs +1002 -65
  48. package/crates/team-agent/src/lifecycle/restart.rs +2 -2
  49. package/crates/team-agent/src/lifecycle/tests/acceptance_b_batch_red.rs +3 -0
  50. package/crates/team-agent/src/lifecycle/tests/b0_reader_hideone_audit_contract.rs +45 -14
  51. package/crates/team-agent/src/lifecycle/tests/behavioral_diff_264_red.rs +15 -0
  52. package/crates/team-agent/src/lifecycle/tests/claude_compatible_config_red.rs +5 -0
  53. package/crates/team-agent/src/lifecycle/tests/claude_profile_launch_red.rs +11 -13
  54. package/crates/team-agent/src/lifecycle/tests/clone_agent_preserves_source_tools.rs +119 -3
  55. package/crates/team-agent/src/lifecycle/tests/communication_mode_runtime_contract_red.rs +12 -0
  56. package/crates/team-agent/src/lifecycle/tests/copilot_provider_red.rs +22 -0
  57. package/crates/team-agent/src/lifecycle/tests/core_034_real_red.rs +12 -0
  58. package/crates/team-agent/src/lifecycle/tests/cursor_mcp_overlay.rs +265 -198
  59. package/crates/team-agent/src/lifecycle/tests/cursor_require_explicit_model_red.rs +14 -30
  60. package/crates/team-agent/src/lifecycle/tests/cursor_restart_resume_red.rs +3 -0
  61. package/crates/team-agent/src/lifecycle/tests/g1_silent_faces.rs +2 -5
  62. package/crates/team-agent/src/lifecycle/tests/grok_effort_argv_red.rs +10 -0
  63. package/crates/team-agent/src/lifecycle/tests/grok_mcp_overlay_red.rs +58 -35
  64. package/crates/team-agent/src/lifecycle/tests/grok_require_explicit_model_red.rs +10 -0
  65. package/crates/team-agent/src/lifecycle/tests/grok_restart_resume_red.rs +9 -0
  66. package/crates/team-agent/src/lifecycle/tests/harvest2_a_batch_red.rs +4 -3
  67. package/crates/team-agent/src/lifecycle/tests/lane_ops.rs +0 -168
  68. package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +145 -212
  69. package/crates/team-agent/src/lifecycle/tests/phase_b_contracts.rs +16 -2
  70. package/crates/team-agent/src/lifecycle/tests/phase_golden.rs +69 -28
  71. package/crates/team-agent/src/lifecycle/tests/pi_dual_entry_red.rs +78 -0
  72. package/crates/team-agent/src/lifecycle/tests/team_in_team_identity_scope_red.rs +3 -0
  73. package/crates/team-agent/src/lifecycle/tests/test_isolation_escape_contract.rs +51 -10
  74. package/crates/team-agent/src/lifecycle/tests/upgrade_compat_0211_red.rs +22 -10
  75. package/crates/team-agent/src/lifecycle/tests/verify_rs031_window_consistency_red.rs +50 -7
  76. package/crates/team-agent/src/lifecycle/tests/worker_spawn_env_red.rs +137 -0
  77. package/crates/team-agent/src/lifecycle/types.rs +12 -0
  78. package/crates/team-agent/src/mcp_server/wire.rs +140 -11
  79. package/crates/team-agent/src/messaging/delivery.rs +67 -8
  80. package/crates/team-agent/src/messaging/leader_channel.rs +56 -0
  81. package/crates/team-agent/src/messaging/tests/runtime.rs +10 -2
  82. package/crates/team-agent/src/messaging/watchers.rs +48 -4
  83. package/crates/team-agent/src/provider/adapters/cursor_agent.rs +2 -5
  84. package/crates/team-agent/src/state/paths.rs +17 -2
  85. package/crates/team-agent/src/state/persist.rs +97 -1
  86. package/crates/team-agent/src/state/projection.rs +18 -2
  87. package/crates/team-agent/src/state/repository.rs +29 -0
  88. package/crates/team-agent/src/state/selector.rs +23 -3
  89. package/crates/team-agent/src/test_support/models_timeout_fixture.rs +144 -0
  90. package/crates/team-agent/src/tmux_backend/tests.rs +121 -4
  91. package/crates/team-agent/src/tmux_backend.rs +208 -118
  92. package/crates/team-agent/src/transport.rs +72 -0
  93. package/crates/team-agent/src/transport_factory.rs +53 -0
  94. package/package.json +4 -5
  95. package/skills/team-agent/SKILL.md +45 -17
  96. package/skills/team-agent/command-coverage.json +4 -29
  97. package/npm/check-skill-links.mjs +0 -88
  98. package/skills/team-agent/references/team-agent-operator.md +0 -672
@@ -60,19 +60,23 @@ pub(super) fn compact_not_ready(full: &Value) -> Value {
60
60
  })
61
61
  .unwrap_or_default();
62
62
  let mut obj = Map::new();
63
- let channel_unbound = reasons
64
- .iter()
65
- .any(|reason| reason == "leader_receiver_unbound");
66
63
  obj.insert(
67
64
  "reasons".to_string(),
68
65
  Value::Array(reasons.into_iter().map(Value::String).collect()),
69
66
  );
70
67
  obj.insert("agents".to_string(), Value::Array(agents));
71
- if channel_unbound {
72
- obj.insert(
73
- "next_action".to_string(),
74
- Value::String("claim-leader".to_string()),
75
- );
68
+ if let Some(next_action) = full
69
+ .pointer("/readiness/next_action")
70
+ .and_then(Value::as_str)
71
+ {
72
+ let truly_unbound = full.pointer("/readiness/state").and_then(Value::as_str)
73
+ == Some("leader_receiver_unbound");
74
+ if !next_action.contains("claim-leader") || truly_unbound {
75
+ obj.insert(
76
+ "next_action".to_string(),
77
+ Value::String(next_action.to_string()),
78
+ );
79
+ }
76
80
  }
77
81
  Value::Object(obj)
78
82
  }
@@ -95,18 +95,21 @@ impl RuntimeSnapshot {
95
95
  .filter(|team| !team.is_empty())
96
96
  .map(str::to_string)
97
97
  .unwrap_or_else(|| crate::state::projection::team_state_key(state));
98
- if !team_key.is_empty()
99
- && !crate::lifecycle::launch::launched_team_receiver_is_attached(workspace, &team_key)
100
- {
101
- if let Some(object) = readiness.as_object_mut() {
102
- object.insert("all_attached_receiver".to_string(), json!(false));
103
- object.insert("ready".to_string(), json!(false));
104
- object.insert("state".to_string(), json!("leader_receiver_unbound"));
105
- object.insert("next_action".to_string(), json!("claim-leader"));
106
- object.insert(
107
- "reason".to_string(),
108
- json!("leader registry does not authorize this workspace"),
109
- );
98
+ if !team_key.is_empty() {
99
+ let class = crate::lifecycle::launch::classify_leader_binding(workspace, &team_key);
100
+ if class != crate::lifecycle::launch::LeaderBindingClass::Attached {
101
+ if let Some(object) = readiness.as_object_mut() {
102
+ object.insert("all_attached_receiver".to_string(), json!(false));
103
+ object.insert("ready".to_string(), json!(false));
104
+ object.insert("state".to_string(), json!(class.issue_id()));
105
+ object.insert("reason".to_string(), json!(class.issue_id()));
106
+ if class == crate::lifecycle::launch::LeaderBindingClass::Unbound {
107
+ object.insert(
108
+ "next_action".to_string(),
109
+ json!(class.repair()),
110
+ );
111
+ }
112
+ }
110
113
  }
111
114
  }
112
115
  let grok_slot = crate::cli::grok_slot_report(workspace, state).to_json();
@@ -230,7 +230,7 @@ pub(super) fn pending_leader_notifications(
230
230
  "created_at": row.get::<_, Option<String>>(4)?,
231
231
  "delivery_attempts": row.get::<_, i64>(5)?,
232
232
  "channel": channel,
233
- "action": "run team-agent attach-leader or team-agent takeover",
233
+ "action": "historical leader mailbox debt; not proof of current unbound owner",
234
234
  }))
235
235
  };
236
236
  let rows = match owner_team_id {
@@ -34,6 +34,14 @@ use store::{
34
34
  mod compact;
35
35
  pub(super) use compact::*;
36
36
 
37
+ mod brief;
38
+ pub(crate) use brief::{format_status_brief, registered_agent_exists, status_brief_scoped};
39
+ #[cfg(test)]
40
+ pub(crate) use brief::{
41
+ with_test_nodeprobe, with_test_nodeprobe_candidates, with_test_nodeprobe_resolver_delay,
42
+ with_test_nodeprobe_unbound, ResolverDelaySignal,
43
+ };
44
+
37
45
  mod runtime;
38
46
  pub(super) use runtime::*;
39
47
 
@@ -90,6 +90,44 @@ fn seed_self_signed_attached_leader(ws: &std::path::Path) {
90
90
  .unwrap();
91
91
  }
92
92
 
93
+ fn seed_canonical_healthy_leader(ws: &std::path::Path, endpoint: &str) {
94
+ crate::state::persist::save_runtime_state(
95
+ ws,
96
+ &json!({
97
+ "team_key": "current",
98
+ "workspace": ws,
99
+ "teams": {
100
+ "current": {
101
+ "team_owner": {
102
+ "pane_id": "%1",
103
+ "owner_epoch": 1,
104
+ "provider": "codex"
105
+ },
106
+ "leader_receiver": {
107
+ "mode": "direct_tmux",
108
+ "status": "attached",
109
+ "pane_id": "%1",
110
+ "owner_epoch": 1,
111
+ "provider": "codex",
112
+ "tmux_socket": endpoint
113
+ }
114
+ }
115
+ }
116
+ }),
117
+ )
118
+ .unwrap();
119
+ assert_eq!(
120
+ crate::leader::registry::register_binding_from_state_best_effort(
121
+ ws,
122
+ Some("current"),
123
+ "diagnose-healthy-fixture",
124
+ )
125
+ .as_ref()
126
+ .map(|outcome| outcome.status),
127
+ Some("registered")
128
+ );
129
+ }
130
+
93
131
  fn seed_leader_registry_entry(ws: &std::path::Path) {
94
132
  let entry = crate::leader::registry::build_entry(
95
133
  ws,
@@ -345,17 +383,33 @@ fn dispatch_routes_diagnose_healthy_leader() {
345
383
  ]);
346
384
  let _home = IsolatedHome::enter("diagnose-healthy");
347
385
  let ws = tmp_workspace();
348
- seed_self_signed_attached_leader(&ws);
349
- seed_leader_registry_entry(&ws);
350
- let code = run(
351
- &cli_argv(&["diagnose", "--workspace", &ws.to_string_lossy(), "--json"]),
352
- &ws,
353
- );
386
+ let endpoint = "/tmp/ta-diagnose-healthy.sock";
387
+ seed_canonical_healthy_leader(&ws, endpoint);
388
+ let transport = crate::transport::test_support::OfflineTransport::default()
389
+ .with_tmux_endpoint(endpoint)
390
+ .with_targets(vec![crate::transport::PaneInfo {
391
+ pane_id: crate::transport::PaneId::new("%1"),
392
+ session: crate::transport::SessionName::new("s"),
393
+ window_index: None,
394
+ window_name: None,
395
+ pane_index: None,
396
+ tty: None,
397
+ current_command: Some("codex".to_string()),
398
+ current_path: Some(ws.clone()),
399
+ active: true,
400
+ pane_pid: None,
401
+ leader_env: Default::default(),
402
+ }]);
403
+ let code = crate::transport_factory::with_leader_endpoint_transport(endpoint, transport, || {
404
+ run(
405
+ &cli_argv(&["diagnose", "--workspace", &ws.to_string_lossy(), "--json"]),
406
+ &ws,
407
+ )
408
+ });
354
409
  assert_eq!(
355
410
  code,
356
411
  ExitCode::Ok,
357
- "`diagnose` must be Ok when state.json looks attached AND ~/.team-agent/leaders/ \
358
- has a matching attached row; got {code:?}"
412
+ "`diagnose` must be Ok when canonical owner/receiver, production registry, and live align; got {code:?}"
359
413
  );
360
414
  let _ = std::fs::remove_dir_all(&ws);
361
415
  }
@@ -133,6 +133,7 @@ mod named_address;
133
133
  mod peer_allow;
134
134
  mod run_delegation;
135
135
  mod shutdown_kill_plan;
136
+ mod status_brief;
136
137
  mod status_send;
137
138
  mod verb_install_skill;
138
139
  mod verb_profile;
@@ -234,6 +234,76 @@ fn resolve_name_team_disambiguated() {
234
234
  let _ = std::fs::remove_dir_all(&ws);
235
235
  }
236
236
 
237
+ #[test]
238
+ fn resolve_qualified_name_prefers_unique_state_pane_over_same_window_idle_pane() {
239
+ let ws = named_ws("qualified-state-pane-disambiguation");
240
+ seed_state(
241
+ &ws,
242
+ state_with_teams(json!({
243
+ "team-a": worker_team("team-a", "qa", "%worker", "qa")
244
+ })),
245
+ );
246
+ let transport = OfflineTransport::new().with_targets(vec![
247
+ pane("team-a", "qa", "%worker"),
248
+ pane("team-a", "qa", "%idle"),
249
+ ]);
250
+
251
+ let resolved = resolve_name_with_transport(
252
+ &ws,
253
+ &format!("{}::team-a/qa", ws.display()),
254
+ &transport,
255
+ )
256
+ .expect("a qualified worker address should use its unique state pane");
257
+ assert_eq!(resolved.pane_id, "%worker");
258
+ assert_eq!(resolved.state_pane_id.as_deref(), Some("%worker"));
259
+ assert!(!resolved.state_pane_stale);
260
+ let _ = std::fs::remove_dir_all(&ws);
261
+ }
262
+
263
+ #[test]
264
+ fn resolve_qualified_name_state_pane_must_uniquely_match_scoped_candidates() {
265
+ for scenario in ["missing", "stale", "cross-session", "duplicate"] {
266
+ let ws = named_ws(&format!("qualified-state-pane-{scenario}"));
267
+ let mut team = worker_team("team-a", "qa", "%worker", "qa");
268
+ let targets = match scenario {
269
+ "missing" => {
270
+ team["agents"]["qa"]
271
+ .as_object_mut()
272
+ .unwrap()
273
+ .remove("pane_id");
274
+ vec![pane("team-a", "qa", "%worker"), pane("team-a", "qa", "%idle")]
275
+ }
276
+ "stale" => {
277
+ team["agents"]["qa"]["pane_id"] = json!("%stale");
278
+ vec![pane("team-a", "qa", "%worker"), pane("team-a", "qa", "%idle")]
279
+ }
280
+ "cross-session" => vec![
281
+ pane("old-team-a", "qa", "%worker"),
282
+ pane("team-a", "qa", "%current-one"),
283
+ pane("team-a", "qa", "%current-two"),
284
+ ],
285
+ "duplicate" => {
286
+ team["agents"]["qa"]["pane_id"] = json!("%duplicate");
287
+ vec![
288
+ pane("team-a", "qa", "%duplicate"),
289
+ pane("team-a", "qa", "%duplicate"),
290
+ ]
291
+ }
292
+ _ => unreachable!(),
293
+ };
294
+ seed_state(&ws, state_with_teams(json!({"team-a": team})));
295
+ let transport = OfflineTransport::new().with_targets(targets);
296
+ let err = resolve_name_with_transport(
297
+ &ws,
298
+ &format!("{}::team-a/qa", ws.display()),
299
+ &transport,
300
+ )
301
+ .expect_err("non-unique state pane evidence must remain fail-closed");
302
+ assert_eq!(err.kind, NamedAddressErrorKind::NameAmbiguous, "{scenario}");
303
+ let _ = std::fs::remove_dir_all(&ws);
304
+ }
305
+ }
306
+
237
307
  #[test]
238
308
  fn resolve_qualified_name_canonicalizes_legacy_session_alias() {
239
309
  let ws = named_ws("legacy-session-alias");
@@ -335,13 +335,12 @@ fn cli_add_agent_duplicate_id_surfaces_real_error() {
335
335
  );
336
336
  }
337
337
 
338
- // 5 [P1] — cmd_status --json reflects the REAL coordinator_health + real DB message/result counts.
339
- // The placeholder status_port::status hardcodes coordinator={status:stopped, schema_ok:false} and
340
- // messages/results={count:0}. Seed: state.json + a real team.db (2 messages + 1 result) + a healthy
341
- // coordinator (this pid + metadata) -> the real path must surface running-coordinator + non-zero
342
- // counts. OS-safe (all reads/seeds, no spawn).
338
+ // 5 [P1] — cmd_status --json emits the concise seven-field node projection.
339
+ // Status must not leak coordinator/db/history diagnostics; missing exact nodeprobe evidence remains
340
+ // an explicit unknown projection. The fixture still seeds a real store to ensure the status path
341
+ // does not accidentally depend on coordinator or mailbox reads.
343
342
  #[test]
344
- fn cli_status_reflects_real_coordinator_health_and_db_counts() {
343
+ fn cli_status_emits_seven_field_brief_without_diagnostics() {
345
344
  let ws = seed_status_workspace(); // writes .team/runtime/state.json
346
345
  let store = crate::message_store::MessageStore::open(&ws).unwrap();
347
346
  let _ = store
@@ -386,25 +385,31 @@ fn cli_status_reflects_real_coordinator_health_and_db_counts() {
386
385
  CmdOutput::Json(v) => v,
387
386
  other => panic!("status --json must yield a Json CmdOutput; got {other:?}"),
388
387
  };
389
- assert_ne!(
390
- value["coordinator"],
391
- json!({"status": "stopped", "schema_ok": false}),
392
- "status `coordinator` must reflect the REAL coordinator_health (seeded running), not the \
393
- placeholder's hardcoded {{status:stopped, schema_ok:false}}; got {}",
394
- value["coordinator"]
388
+ let nodes = value
389
+ .get("nodes")
390
+ .and_then(serde_json::Value::as_array)
391
+ .expect("status brief must expose a nodes array");
392
+ assert_eq!(nodes.len(), 1);
393
+ let fields: std::collections::BTreeSet<&str> = nodes[0]
394
+ .as_object()
395
+ .expect("status brief node must be an object")
396
+ .keys()
397
+ .map(String::as_str)
398
+ .collect();
399
+ assert_eq!(
400
+ fields,
401
+ [
402
+ "name",
403
+ "provider",
404
+ "runtime_status",
405
+ "activity",
406
+ "health",
407
+ "session_name",
408
+ "tmux_command",
409
+ ]
410
+ .into_iter()
411
+ .collect()
395
412
  );
396
- assert_ne!(
397
- value["messages"],
398
- json!({"count": 0}),
399
- "status `messages` must reflect the REAL DB rows (2 seeded), not the placeholder's {{count:0}}; got {}",
400
- value["messages"]
401
- );
402
- assert_ne!(
403
- value["results"],
404
- json!({"count": 0}),
405
- "status `results` must reflect the REAL DB result row, not the placeholder's {{count:0}}; got {}",
406
- value["results"]
407
- );
408
413
  }
409
414
 
410
415
  // 6 [P1] — cmd_shutdown delegates to the REAL crate::coordinator::stop_coordinator (+ tmux