@team-agent/installer 0.5.48 → 0.5.49

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.
package/Cargo.lock CHANGED
@@ -575,7 +575,7 @@ dependencies = [
575
575
 
576
576
  [[package]]
577
577
  name = "team-agent"
578
- version = "0.5.48"
578
+ version = "0.5.49"
579
579
  dependencies = [
580
580
  "anyhow",
581
581
  "chrono",
package/Cargo.toml CHANGED
@@ -9,7 +9,7 @@ members = ["crates/team-agent", "crates/win-conpty-phase0", "crates/conpty-trans
9
9
 
10
10
  [workspace.package]
11
11
  edition = "2021"
12
- version = "0.5.48"
12
+ version = "0.5.49"
13
13
  license = "AGPL-3.0"
14
14
  rust-version = "1.95"
15
15
 
@@ -472,7 +472,10 @@ pub(crate) fn parse_leader_target_workspace_and_team(
472
472
  let target_workspace = resolve_workspace(sender_workspace, parsed.workspace.as_deref())?;
473
473
  match parsed.target {
474
474
  ParsedTarget::TeamEntity { team, entity } if entity == "leader" => {
475
- Ok(Some((target_workspace, team)))
475
+ let state = load_state_checked(&target_workspace)?;
476
+ let canonical = canonical_named_team_key(&target_workspace, &state, &team)
477
+ .unwrap_or(team);
478
+ Ok(Some((target_workspace, canonical)))
476
479
  }
477
480
  _ => Ok(None),
478
481
  }
@@ -607,23 +610,30 @@ fn resolve_in_workspace(
607
610
  ParsedTarget::BareAgent(agent) => {
608
611
  resolve_bare_agent(sender_workspace, target_workspace, state, agent, transport)
609
612
  }
610
- ParsedTarget::TeamEntity { team, entity } if entity == "leader" => resolve_leader(
611
- sender_workspace,
612
- target_workspace,
613
- state,
614
- team,
615
- parsed,
616
- transport,
617
- ),
618
- ParsedTarget::TeamEntity { team, entity } => resolve_worker(
619
- sender_workspace,
620
- target_workspace,
621
- state,
622
- team,
623
- entity,
624
- parsed,
625
- transport,
626
- ),
613
+ ParsedTarget::TeamEntity { team, entity } => {
614
+ let canonical = canonical_named_team_key(target_workspace, state, team)
615
+ .unwrap_or_else(|| team.clone());
616
+ if entity == "leader" {
617
+ resolve_leader(
618
+ sender_workspace,
619
+ target_workspace,
620
+ state,
621
+ &canonical,
622
+ parsed,
623
+ transport,
624
+ )
625
+ } else {
626
+ resolve_worker(
627
+ sender_workspace,
628
+ target_workspace,
629
+ state,
630
+ &canonical,
631
+ entity,
632
+ parsed,
633
+ transport,
634
+ )
635
+ }
636
+ }
627
637
  ParsedTarget::SessionWindow { session, window } => resolve_session_window(
628
638
  sender_workspace,
629
639
  target_workspace,
@@ -1149,8 +1159,10 @@ fn transport_for_cli_target(
1149
1159
  parsed: &ParsedNamedAddress,
1150
1160
  ) -> Box<dyn Transport> {
1151
1161
  if let ParsedTarget::TeamEntity { team, entity } = &parsed.target {
1162
+ let canonical = canonical_named_team_key(target_workspace, state, team)
1163
+ .unwrap_or_else(|| team.clone());
1152
1164
  if entity == "leader" {
1153
- let team_scoped_socket = team_entry(state, team)
1165
+ let team_scoped_socket = team_entry(state, &canonical)
1154
1166
  .and_then(|entry| entry.get("leader_receiver"))
1155
1167
  .and_then(|receiver| string_field(receiver, "tmux_socket"));
1156
1168
  let socket = team_scoped_socket.map(str::to_string).or_else(|| {
@@ -1164,7 +1176,7 @@ fn transport_for_cli_target(
1164
1176
  return Box::new(crate::tmux_backend::TmuxBackend::for_tmux_endpoint(&socket));
1165
1177
  }
1166
1178
  }
1167
- if let Some(entry) = team_entry(state, team) {
1179
+ if let Some(entry) = team_entry(state, &canonical) {
1168
1180
  let selected = crate::tmux_backend::tmux_backend_for_runtime_state_or_workspace(
1169
1181
  target_workspace,
1170
1182
  Some(entry),
@@ -1247,6 +1259,30 @@ fn team_entry<'a>(state: &'a Value, team: &str) -> Option<&'a Value> {
1247
1259
  .and_then(|teams| teams.get(team))
1248
1260
  }
1249
1261
 
1262
+ fn canonical_named_team_key(
1263
+ target_workspace: &Path,
1264
+ state: &Value,
1265
+ requested: &str,
1266
+ ) -> Option<String> {
1267
+ if team_entry(state, requested).is_some() || is_legacy_single_team_active(state, requested) {
1268
+ return Some(requested.to_string());
1269
+ }
1270
+ let selected = crate::state::projection::select_runtime_state(
1271
+ target_workspace,
1272
+ Some(requested),
1273
+ )
1274
+ .ok()?;
1275
+ let canonical = selected
1276
+ .get("active_team_key")
1277
+ .and_then(Value::as_str)
1278
+ .filter(|team| !team.is_empty())?;
1279
+ state
1280
+ .get("teams")
1281
+ .and_then(Value::as_object)
1282
+ .is_some_and(|teams| teams.contains_key(canonical))
1283
+ .then(|| canonical.to_string())
1284
+ }
1285
+
1250
1286
  /// E6 (0.5.9 offline-mailbox-toname-design §3.1): decide which
1251
1287
  /// `leader_receiver` object, if any, applies to `<team>/leader` before any
1252
1288
  /// downstream code reads it. Wrong runtime keys are rejected here with
@@ -144,9 +144,10 @@ pub fn cmd_send(args: &SendArgs) -> Result<CmdResult, CliError> {
144
144
  )?;
145
145
  let target = send_target(args.targets.as_deref(), args.target.as_deref());
146
146
  let mut opts = send_options_from_args(args);
147
- if opts.team.is_none() {
148
- opts.team = Some(TeamKey::new(selected.team_key.clone()));
149
- }
147
+ // `args.team` is a selector and may be a legacy session/team-dir alias.
148
+ // All downstream membership and DB scope must use the canonical key that
149
+ // resolve_active_team returned, never the original selector spelling.
150
+ opts.team = Some(TeamKey::new(selected.team_key.clone()));
150
151
  let content = args.message.join(" ");
151
152
  // CR-061/N27 routing-ambiguous: a single positional with no `--to`/`--targets` and an
152
153
  // empty message body is a prompt-only invocation (`team-agent send "fix the build"`).
@@ -222,6 +222,29 @@ fn resolve_name_team_disambiguated() {
222
222
  let _ = std::fs::remove_dir_all(&ws);
223
223
  }
224
224
 
225
+ #[test]
226
+ fn resolve_qualified_name_canonicalizes_legacy_session_alias() {
227
+ let ws = named_ws("legacy-session-alias");
228
+ seed_state(
229
+ &ws,
230
+ state_with_teams(json!({
231
+ "teamdir": worker_team("team-displayname", "qa", "%1", "qa")
232
+ })),
233
+ );
234
+ let transport = OfflineTransport::new().with_targets(vec![pane(
235
+ "team-displayname",
236
+ "qa",
237
+ "%1",
238
+ )]);
239
+
240
+ let resolved = resolve_name_with_transport(&ws, "displayname/qa", &transport)
241
+ .expect("qualified legacy alias must resolve through the canonical selector");
242
+ assert_eq!(resolved.team_key.as_deref(), Some("teamdir"));
243
+ assert_eq!(resolved.agent_id.as_deref(), Some("qa"));
244
+ assert_eq!(resolved.pane_id, "%1");
245
+ let _ = std::fs::remove_dir_all(&ws);
246
+ }
247
+
225
248
  #[test]
226
249
  fn resolve_name_stale_pane() {
227
250
  let ws = named_ws("stale-pane");
@@ -230,7 +230,7 @@ fn cli_quick_start_invokes_real_lifecycle_compiles_spec() {
230
230
  };
231
231
  let _ = cmd_quick_start(&args); // real quick_start compiles the spec before any coordinator/launch step
232
232
  // E5: spec compiled to .team/runtime/<team_key>/, NOT the user team dir.
233
- let team_key = team.file_name().unwrap().to_string_lossy().to_string();
233
+ let team_key = "clidelegteam";
234
234
  let workspace = crate::model::paths::team_workspace(&team).unwrap();
235
235
  let runtime_spec = crate::model::paths::runtime_spec_path(&workspace, &team_key);
236
236
  assert!(
@@ -706,6 +706,48 @@ fn cmd_send_failed_outcome_yields_error_exit() {
706
706
  }
707
707
  }
708
708
 
709
+ #[test]
710
+ fn cmd_send_canonicalizes_legacy_session_alias_before_membership_projection() {
711
+ let ws = deleg_uniq_dir("send-team-alias");
712
+ let _ = crate::message_store::MessageStore::open(&ws).unwrap();
713
+ crate::state::persist::save_runtime_state(
714
+ &ws,
715
+ &json!({
716
+ "active_team_key": "teamdir",
717
+ "team_key": "teamdir",
718
+ "teams": {
719
+ "teamdir": {
720
+ "status": "alive",
721
+ "session_name": "team-displayname",
722
+ "agents": {"w1": {"provider": "codex", "status": "running"}}
723
+ }
724
+ }
725
+ }),
726
+ )
727
+ .unwrap();
728
+ let args = SendArgs {
729
+ workspace: ws,
730
+ target: Some("w1".into()),
731
+ team: Some("displayname".into()),
732
+ task: None,
733
+ no_wait: true,
734
+ watch_result: false,
735
+ json: true,
736
+ ..send_args_fixture()
737
+ };
738
+
739
+ let result = cmd_send(&args).expect("alias selection should reach canonical team send");
740
+ let value = match result.output {
741
+ CmdOutput::Json(value) => value,
742
+ other => panic!("expected JSON send outcome, got {other:?}"),
743
+ };
744
+ assert_ne!(
745
+ value.get("reason").and_then(serde_json::Value::as_str),
746
+ Some("target_not_in_team"),
747
+ "selector accepted the alias, so membership must use its canonical team key: {value}"
748
+ );
749
+ }
750
+
709
751
  // ═══════════════════════════════════════════════════════════════════════════
710
752
  // coordinator.ok — non-compact status carries the FULL coordinator_health (incl. `ok`); compact
711
753
  // strips to {status,pid,metadata_ok,schema_ok} (golden queries.py:77 + compact.py:35; ok =
@@ -3167,10 +3167,18 @@ pub fn quick_start_with_transport_in_workspace_with_display(
3167
3167
  if !open_display {
3168
3168
  override_spec_display_backend(&mut spec, "none");
3169
3169
  }
3170
- let requested_team = quick_start_requested_team_key(team_id, name)
3171
- .map(str::to_string)
3172
- .or_else(|| spec_team_id(&spec));
3173
3170
  let explicit_team_key = quick_start_requested_team_key(team_id, name).map(str::to_string);
3171
+ let canonical_team_key = explicit_team_key
3172
+ .clone()
3173
+ .or_else(|| spec_team_id(&spec).filter(|team| !team.is_empty()))
3174
+ .unwrap_or_else(|| {
3175
+ runtime_team_key_for_spec(
3176
+ &agents_dir.join("team.spec.yaml"),
3177
+ &spec,
3178
+ &spec_session_name(&spec),
3179
+ )
3180
+ });
3181
+ let requested_team = Some(canonical_team_key.clone());
3174
3182
  let team_depth = quick_start_depth_guard(
3175
3183
  &workspace,
3176
3184
  agents_dir,
@@ -3248,14 +3256,12 @@ pub fn quick_start_with_transport_in_workspace_with_display(
3248
3256
  // spec's runtime.session_name with one derived from the REQUESTED team identity
3249
3257
  // so launch_with_transport (which reads runtime.session_name) spawns into an
3250
3258
  // isolated session per requested team.
3251
- if let Some(requested) = requested_team.as_deref() {
3259
+ if let Some(requested) = explicit_team_key.as_deref() {
3252
3260
  override_spec_session_name(&mut spec, &format!("team-{requested}"));
3253
3261
  }
3254
3262
  let session_name = spec_session_name(&spec);
3255
- // team_key 身份源 = team_dir(agents_dir).name(角色定义目录),不依赖 spec 落点。
3256
- let state_team_key = explicit_team_key.clone().unwrap_or_else(|| {
3257
- runtime_team_key_for_spec(&agents_dir.join("team.spec.yaml"), &spec, &session_name)
3258
- });
3263
+ // team_key 已在入口由显式 id/name 或 compiled spec identity 单次确定。
3264
+ let state_team_key = canonical_team_key;
3259
3265
  warn_ignored_owner_team_id(workspace.as_path(), agents_dir, &state_team_key);
3260
3266
  // E5 spec 迁移:spec 写到 .team/runtime/<team_key>/(中间产物,绝不落用户目录 agents_dir)。
3261
3267
  // Bug2:原子写(tmp+rename),避免半截 spec。
@@ -3265,7 +3271,13 @@ pub fn quick_start_with_transport_in_workspace_with_display(
3265
3271
  .map_err(|e| LifecycleError::StatePersist(e.to_string()))?;
3266
3272
  let resolved_spec_path =
3267
3273
  std::fs::canonicalize(&spec_path).unwrap_or_else(|_| spec_path.clone());
3268
- let mut state = initial_runtime_state(&spec, &resolved_spec_path, &workspace, agents_dir);
3274
+ let mut state = initial_runtime_state(
3275
+ &spec,
3276
+ &resolved_spec_path,
3277
+ &workspace,
3278
+ agents_dir,
3279
+ &state_team_key,
3280
+ );
3269
3281
  // 0.5.x Phase 1d hot-path 接线(裁决1 msg_76e1d98202b8): use the
3270
3282
  // generic annotator that writes `state.transport = { kind, source }`
3271
3283
  // for every backend AND (for tmux) preserves the existing
@@ -5104,6 +5116,7 @@ fn initial_runtime_state(
5104
5116
  spec_path: &Path,
5105
5117
  workspace: &Path,
5106
5118
  team_dir: &Path,
5119
+ team_key: &str,
5107
5120
  ) -> serde_json::Value {
5108
5121
  let mut agents = serde_json::Map::new();
5109
5122
  for agent in spec_agent_values(spec) {
@@ -5145,6 +5158,7 @@ fn initial_runtime_state(
5145
5158
  "team_dir".to_string(),
5146
5159
  serde_json::json!(team_dir.to_string_lossy().to_string()),
5147
5160
  );
5161
+ state.insert("team_key".to_string(), serde_json::json!(team_key));
5148
5162
  state.insert(
5149
5163
  "session_name".to_string(),
5150
5164
  serde_json::json!(spec_session_name(spec).as_str()),
@@ -80,11 +80,10 @@ fn layout_pane(
80
80
  }
81
81
 
82
82
  /// E5 spec 迁移:spec 不再落 team_dir,而在 <team_workspace>/.team/runtime/<team_key>/。
83
- /// team_key = team_dir.name(quick_start_team_dir "teamdir");workspace = team_workspace(team)。
83
+ /// quick-start 未显式指定 id 时,team_key 来自 compiled TEAM.md name(`quickteam`)。
84
84
  pub(super) fn quick_start_runtime_spec_path(team: &std::path::Path) -> PathBuf {
85
85
  let workspace = crate::model::paths::team_workspace(team).unwrap();
86
- let team_key = team.file_name().unwrap().to_string_lossy().to_string();
87
- crate::model::paths::runtime_spec_path(&workspace, &team_key)
86
+ crate::model::paths::runtime_spec_path(&workspace, "quickteam")
88
87
  }
89
88
 
90
89
  /// 在 team 自身或其 team_workspace 父下找 .team/runtime/*/team.spec.yaml。run_workspace 解析
@@ -172,6 +171,26 @@ fn quick_start_compiles_real_spec_to_team_spec_yaml() {
172
171
  }
173
172
  }
174
173
 
174
+ #[test]
175
+ fn quick_start_without_team_id_uses_compiled_team_name_as_canonical_key() {
176
+ let team = quick_start_team_dir(QS_VALID_ROLE);
177
+ let workspace = crate::model::paths::team_workspace(&team).unwrap();
178
+ let transport = OfflineTransport::new();
179
+
180
+ let result = quick_start_with_transport(&team, None, true, None, &transport);
181
+ assert!(result.is_ok(), "quick-start fixture must run: {result:?}");
182
+
183
+ let state = crate::state::persist::load_runtime_state(&workspace).unwrap();
184
+ assert_eq!(state["active_team_key"], json!("quickteam"));
185
+ assert_eq!(state["team_key"], json!("quickteam"));
186
+ assert!(state["teams"].get("quickteam").is_some(), "{state}");
187
+ assert!(state["teams"].get("teamdir").is_none(), "{state}");
188
+ assert!(
189
+ crate::model::paths::runtime_spec_path(&workspace, "quickteam").exists(),
190
+ "runtime spec must share the canonical key"
191
+ );
192
+ }
193
+
175
194
  #[test]
176
195
  fn quick_start_teamdir_under_dot_team_uses_project_workspace_for_status_and_collect() {
177
196
  let workspace = temp_ws();
@@ -215,8 +234,8 @@ fn quick_start_teamdir_under_dot_team_uses_project_workspace_for_status_and_coll
215
234
  "input={}",
216
235
  input.display()
217
236
  );
218
- // E5: selector resolves spec_path to .team/runtime/<team_key>/ (team_key="current").
219
- let expected_spec = crate::model::paths::runtime_spec_path(&workspace, "current");
237
+ // E5: selector resolves spec_path to .team/runtime/<canonical team_key>/.
238
+ let expected_spec = crate::model::paths::runtime_spec_path(&workspace, "quickteam");
220
239
  assert_eq!(
221
240
  selected
222
241
  .spec_path
@@ -1319,7 +1338,7 @@ fn quick_start_persists_selected_tmux_endpoint_and_attach_commands() {
1319
1338
  assert_eq!(state["tmux_socket"], json!(endpoint));
1320
1339
  assert_eq!(state["is_external_leader"], json!(false));
1321
1340
  assert_eq!(
1322
- state["teams"]["teamdir"]["is_external_leader"],
1341
+ state["teams"]["quickteam"]["is_external_leader"],
1323
1342
  json!(false)
1324
1343
  );
1325
1344
  }
@@ -2876,7 +2895,7 @@ fn quick_start_seeds_tasks_key_from_compiled_spec() {
2876
2895
  }
2877
2896
 
2878
2897
  // Stage A — golden launch/core.py:62-71 seeded top-level runtime state in insertion order:
2879
- // spec_path, workspace, team_dir, session_name, leader, agents, tasks, display_backend.
2898
+ // spec_path, workspace, team_dir, team_key, session_name, leader, agents, tasks, display_backend.
2880
2899
  //
2881
2900
  // OLD (Python parity): the top-level shape ended at `display_backend`.
2882
2901
  // NEW (Bug 1/2 — team-in-team state scope, see tests/team_in_team_state_scope_red.rs):
@@ -2889,9 +2908,9 @@ fn quick_start_seeds_tasks_key_from_compiled_spec() {
2889
2908
  // The post-launch hook drops the top-level owner triple when the launched pane is
2890
2909
  // unbound (empty pane_id), and only the per-team entry retains the binding.
2891
2910
  // R1: topology is explicit; fresh managed teams carry `is_external_leader=false`
2892
- // before the team-in-team suffix. Order remains the golden prefix
2893
- // (spec_path display_backend) + topology marker + new suffix (active_team_key,
2894
- // teams). display_backend stays the resolved backend from display/backend.py:12-29
2911
+ // before the team-in-team suffix. Canonical `team_key` is seeded before owner
2912
+ // binding; the remaining order stays the golden prefix + topology marker + new
2913
+ // suffix (active_team_key, teams). display_backend stays the resolved backend from display/backend.py:12-29
2895
2914
  // (default adaptive), not the raw optional spec field.
2896
2915
  #[test]
2897
2916
  #[serial(env)]
@@ -2924,6 +2943,9 @@ fn quick_start_state_seeds_spec_path_workspace_leader_display_backend() {
2924
2943
  "spec_path",
2925
2944
  "workspace",
2926
2945
  "team_dir",
2946
+ // Seed canonical identity before owner binding so the initial
2947
+ // receiver is written under the same team key as the saved state.
2948
+ "team_key",
2927
2949
  "session_name",
2928
2950
  "leader",
2929
2951
  "agents",
@@ -2937,17 +2959,12 @@ fn quick_start_state_seeds_spec_path_workspace_leader_display_backend() {
2937
2959
  // field); source is `"unknown"` until Phase 2 threads
2938
2960
  // `ResolvedTransport.source` through the launch call site.
2939
2961
  "transport",
2940
- // 0.4.0 refactor: `team_key` added as top-level topology marker
2941
- // alongside active_team_key (refactor-modular-architecture). The
2942
- // owner / leader_receiver / owner_epoch invariants below still
2943
- // hold — those live only under teams[<active_team_key>].
2944
- "team_key",
2945
2962
  "active_team_key",
2946
2963
  "teams",
2947
2964
  ],
2948
2965
  "state.json top-level key order must match golden launch/core.py:62-71 \
2949
2966
  plus R1 topology marker and Bug 1/2 team-in-team suffix \
2950
- (is_external_leader, team_key, active_team_key, teams). \
2967
+ (is_external_leader, active_team_key, teams). \
2951
2968
  Bug 2 owner team-scope (N1/N12/N18/N29) deliberately keeps owner / \
2952
2969
  leader_receiver / owner_epoch OFF the top level — they live ONLY under \
2953
2970
  teams[<active_team_key>] so per-team isolation has a single source of \
@@ -451,7 +451,7 @@ fn team_dir_with_roles(role_docs: &[(&str, &str)]) -> PathBuf {
451
451
  std::fs::create_dir_all(team.join("agents")).unwrap();
452
452
  std::fs::write(
453
453
  team.join("TEAM.md"),
454
- "---\nname: phaseb\nobjective: Phase B.\nprovider: codex\n---\n\nPhase B team.\n",
454
+ "---\nname: teamdir\nobjective: Phase B.\nprovider: codex\nsession_name: team-phaseb\n---\n\nPhase B team.\n",
455
455
  )
456
456
  .unwrap();
457
457
  for (file, role_doc) in role_docs {
@@ -536,7 +536,7 @@ fn add_fixture() -> (PathBuf, PathBuf, PathBuf, OfflineTransport) {
536
536
  quick_start_with_transport_in_workspace_with_display(
537
537
  &workspace,
538
538
  &team,
539
- None,
539
+ Some("teamdir"),
540
540
  true,
541
541
  None,
542
542
  &launch_transport,
@@ -367,7 +367,7 @@ fn two_worker_team_dir(hermetic: &HermeticTestEnv) -> PathBuf {
367
367
  std::fs::create_dir_all(team.join("agents")).unwrap();
368
368
  std::fs::write(
369
369
  team.join("TEAM.md"),
370
- "---\nname: phasegolden\nobjective: Phase golden.\nprovider: codex\n---\n\nPhase golden team.\n",
370
+ "---\nname: teamdir\nobjective: Phase golden.\nprovider: codex\nsession_name: team-phasegolden\n---\n\nPhase golden team.\n",
371
371
  )
372
372
  .unwrap();
373
373
  for id in ["w1", "w2"] {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@team-agent/installer",
3
- "version": "0.5.48",
3
+ "version": "0.5.49",
4
4
  "description": "npx installer for Team Agent",
5
5
  "keywords": [
6
6
  "codex",
@@ -20,9 +20,9 @@
20
20
  "team-agent-installer": "npm/install.mjs"
21
21
  },
22
22
  "optionalDependencies": {
23
- "@team-agent/cli-darwin-arm64": "0.5.48",
24
- "@team-agent/cli-darwin-x64": "0.5.48",
25
- "@team-agent/cli-linux-x64": "0.5.48"
23
+ "@team-agent/cli-darwin-arm64": "0.5.49",
24
+ "@team-agent/cli-darwin-x64": "0.5.49",
25
+ "@team-agent/cli-linux-x64": "0.5.49"
26
26
  },
27
27
  "scripts": {
28
28
  "postinstall": "node npm/bincheck.mjs",