@team-agent/installer 0.3.6 → 0.3.8

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 (45) hide show
  1. package/Cargo.lock +1 -1
  2. package/Cargo.toml +1 -1
  3. package/crates/team-agent/src/cli/adapters.rs +52 -7
  4. package/crates/team-agent/src/cli/diagnose.rs +9 -0
  5. package/crates/team-agent/src/cli/emit.rs +175 -0
  6. package/crates/team-agent/src/cli/mod.rs +455 -63
  7. package/crates/team-agent/src/cli/status_port.rs +62 -0
  8. package/crates/team-agent/src/cli/tests/base.rs +9 -4
  9. package/crates/team-agent/src/cli/tests/missing_subcommands.rs +83 -1
  10. package/crates/team-agent/src/cli/tests/mod.rs +1 -0
  11. package/crates/team-agent/src/cli/tests/run_delegation.rs +10 -2
  12. package/crates/team-agent/src/cli/tests/shutdown_kill_plan.rs +86 -21
  13. package/crates/team-agent/src/cli/tests/verb_install_skill.rs +76 -0
  14. package/crates/team-agent/src/cli/types.rs +3 -2
  15. package/crates/team-agent/src/compiler.rs +73 -50
  16. package/crates/team-agent/src/coordinator/tick.rs +108 -20
  17. package/crates/team-agent/src/db/migration.rs +17 -1
  18. package/crates/team-agent/src/leader/owner_bind.rs +59 -20
  19. package/crates/team-agent/src/lifecycle/launch.rs +378 -56
  20. package/crates/team-agent/src/lifecycle/restart/common.rs +4 -9
  21. package/crates/team-agent/src/lifecycle/restart/rebuild.rs +91 -12
  22. package/crates/team-agent/src/lifecycle/restart/selection.rs +6 -4
  23. package/crates/team-agent/src/lifecycle/tests/core.rs +238 -3
  24. package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +257 -7
  25. package/crates/team-agent/src/lifecycle/types.rs +2 -0
  26. package/crates/team-agent/src/mcp_server/normalize.rs +29 -7
  27. package/crates/team-agent/src/mcp_server/tests/golden.rs +7 -5
  28. package/crates/team-agent/src/mcp_server/tests/normalize.rs +5 -2
  29. package/crates/team-agent/src/mcp_server/tools.rs +25 -1
  30. package/crates/team-agent/src/mcp_server/wire.rs +11 -1
  31. package/crates/team-agent/src/model/paths.rs +7 -0
  32. package/crates/team-agent/src/model/spec.rs +23 -1
  33. package/crates/team-agent/src/packaging/install.rs +42 -4
  34. package/crates/team-agent/src/packaging/tests.rs +91 -14
  35. package/crates/team-agent/src/packaging/types.rs +13 -1
  36. package/crates/team-agent/src/provider/adapter.rs +381 -15
  37. package/crates/team-agent/src/state/identity.rs +29 -0
  38. package/crates/team-agent/src/state/selector.rs +48 -14
  39. package/crates/team-agent/src/tmux_backend/tests.rs +44 -0
  40. package/crates/team-agent/src/tmux_backend.rs +104 -9
  41. package/crates/team-agent/src/transport/test_support.rs +57 -4
  42. package/crates/team-agent/src/transport.rs +13 -0
  43. package/npm/install.mjs +31 -35
  44. package/package.json +4 -4
  45. package/skills/team-agent/SKILL.md +82 -5
@@ -4,7 +4,7 @@ use std::path::{Path, PathBuf};
4
4
 
5
5
  use serde_json::Value;
6
6
 
7
- use crate::model::paths::{canonical_run_workspace, team_workspace};
7
+ use crate::model::paths::{canonical_run_workspace, runtime_spec_path, team_workspace};
8
8
  use crate::state::persist::{load_runtime_state, runtime_state_path};
9
9
  use crate::state::projection::{select_runtime_state, team_state_key};
10
10
  use crate::state::StateError;
@@ -20,7 +20,13 @@ pub struct SelectedTeam {
20
20
  pub run_workspace: PathBuf,
21
21
  pub team_key: String,
22
22
  pub state: Value,
23
+ /// E5 §3 解耦:**角色定义目录**(用户目录,含 TEAM.md+agents/*.md+profiles)。
24
+ /// 给 compile_team / 找角色定义 / profiles。**永远是用户目录**,不是 spec 落点。
25
+ /// 来源 = state.team_dir;缺则回落 run_workspace(自含 team-dir 布局)。
26
+ pub team_dir: PathBuf,
27
+ /// spec yaml 所在目录(demote 后 = .team/runtime/<team_key>/)。读写 spec yaml 用。
23
28
  pub spec_workspace: Option<PathBuf>,
29
+ /// spec yaml 路径(= runtime_spec_path(run_ws, team_key))。读写 spec yaml 用。
24
30
  pub spec_path: Option<PathBuf>,
25
31
  }
26
32
 
@@ -30,7 +36,7 @@ pub fn resolve_active_team(
30
36
  mode: SelectorMode,
31
37
  ) -> Result<SelectedTeam, StateError> {
32
38
  let explicit_spec = input.join("team.spec.yaml");
33
- let (run_workspace, state, spec_workspace) = if explicit_spec.exists() {
39
+ let (run_workspace, state) = if explicit_spec.exists() {
34
40
  let team_run = team_workspace(input).map_err(|e| StateError::TeamSelect(e.to_string()))?;
35
41
  let run = if runtime_state_path(input).exists() || !runtime_state_path(&team_run).exists() {
36
42
  input.to_path_buf()
@@ -38,7 +44,7 @@ pub fn resolve_active_team(
38
44
  team_run
39
45
  };
40
46
  let state = select_runtime_state(&run, team).or_else(|_| load_runtime_state(&run))?;
41
- (run, state, Some(input.to_path_buf()))
47
+ (run, state)
42
48
  } else {
43
49
  let run = canonical_run_workspace(input)
44
50
  .map_err(|e| StateError::TeamSelect(e.to_string()))?;
@@ -53,30 +59,58 @@ pub fn resolve_active_team(
53
59
  )));
54
60
  }
55
61
  let state = select_runtime_state(&run, team).or_else(|_| load_runtime_state(&run))?;
56
- let spec_workspace = spec_workspace_from_state(&state)
57
- .or_else(|| run.join("team.spec.yaml").exists().then(|| run.clone()));
58
- (run, state, spec_workspace)
62
+ (run, state)
59
63
  };
60
64
 
61
- let spec_path = spec_workspace.as_ref().map(|workspace| workspace.join("team.spec.yaml"));
65
+ // E5 spec 迁移·读序 B(architect+leader 裁定):
66
+ // 1) runtime spec 优先严格:<run_ws>/.team/runtime/<team_key>/team.spec.yaml 存在即必用。
67
+ // 2) 缺失才**只读回落**用户目录旧 spec(过渡腿;绝不在此写/迁移——迁移+清理只属启动重建)。
68
+ // TODO(E5 后续版本):新 team 永不写用户目录(G1),回落腿可在 legacy 清零后移除。
69
+ let team_key = selected_team_key(&state, team);
70
+ let runtime_spec = runtime_spec_path(&run_workspace, &team_key);
71
+ let (spec_workspace, spec_path) = if runtime_spec.exists() {
72
+ (
73
+ runtime_spec.parent().map(Path::to_path_buf),
74
+ Some(runtime_spec.clone()),
75
+ )
76
+ } else {
77
+ // 回落(只读):优先 explicit input/team.spec.yaml,其次 state 推断的 spec_workspace。
78
+ let legacy_ws = if explicit_spec.exists() {
79
+ Some(input.to_path_buf())
80
+ } else {
81
+ spec_workspace_from_state(&state)
82
+ .or_else(|| run_workspace.join("team.spec.yaml").exists().then(|| run_workspace.clone()))
83
+ };
84
+ let legacy_spec = legacy_ws.as_ref().map(|ws| ws.join("team.spec.yaml"));
85
+ (legacy_ws, legacy_spec)
86
+ };
62
87
  if matches!(mode, SelectorMode::RequireSpec) && !spec_path.as_ref().is_some_and(|path| path.exists()) {
63
- let expected = spec_path
64
- .as_ref()
65
- .cloned()
66
- .unwrap_or_else(|| run_workspace.join("team.spec.yaml"));
88
+ // 期望路径报 canonical runtime spec(重建落点),非用户目录。
89
+ let expected = spec_path.as_ref().cloned().unwrap_or(runtime_spec);
90
+ // E5 Bug2 N38:spec=中间产物,运行期由 restart 以角色定义重建;首装走 quick-start;
91
+ // 加新角色用 add-agent。不再提 reconcile(已废)
67
92
  return Err(StateError::TeamSelect(format!(
68
- "active team spec not found: input_workspace={} run_workspace={} team_key={} expected_spec_path={} hint=run quick-start or pass --team/--workspace <teamdir>",
93
+ "active team spec not found: input_workspace={} run_workspace={} team_key={} expected_spec_path={} hint=run `team-agent restart` to rebuild it from the role docs, or `team-agent quick-start <teamdir>` for first launch (to add a role at runtime use `team-agent add-agent <id> --role-file <path>`)",
69
94
  input.display(),
70
95
  run_workspace.display(),
71
- selected_team_key(&state, team),
96
+ team_key,
72
97
  expected.display()
73
98
  )));
74
99
  }
75
100
 
101
+ // E5 §3 解耦:team_dir = 角色定义目录(用户目录),恒取 state.team_dir;缺则回落 run_workspace。
102
+ let team_dir = state
103
+ .get("team_dir")
104
+ .and_then(Value::as_str)
105
+ .filter(|s| !s.is_empty())
106
+ .map(PathBuf::from)
107
+ .unwrap_or_else(|| run_workspace.clone());
108
+
76
109
  Ok(SelectedTeam {
77
110
  run_workspace,
78
- team_key: selected_team_key(&state, team),
111
+ team_key,
79
112
  state,
113
+ team_dir,
80
114
  spec_workspace,
81
115
  spec_path,
82
116
  })
@@ -503,6 +503,50 @@
503
503
  );
504
504
  }
505
505
 
506
+ #[test]
507
+ fn has_pane_is_direct_existence_probe_not_liveness_guess() {
508
+ let (be, rec) = backend_with(MockResp::Out(ok("%7")), vec![]);
509
+ assert_eq!(be.has_pane(&PaneId::new("%7")).expect("has_pane"), Some(true));
510
+ let argv0 = rec.lock().unwrap()[0].clone();
511
+ assert!(
512
+ argv0.contains(&"display-message".to_string())
513
+ && argv0.iter().any(|x| x.contains("#{pane_id}"))
514
+ && argv0.contains(&"%7".to_string()),
515
+ "has_pane must use the cheap display-message #{{pane_id}} probe; got {argv0:?}"
516
+ );
517
+
518
+ let (be, _r) = backend_with(MockResp::Out(ok("")), vec![]);
519
+ assert_eq!(
520
+ be.has_pane(&PaneId::new("%9999")).expect("has_pane"),
521
+ Some(false),
522
+ "real tmux can report a missing pane as exit 0 with empty stdout"
523
+ );
524
+
525
+ let (be, _r) = backend_with(MockResp::Out(fail(1, "can't find pane: %9999")), vec![]);
526
+ assert_eq!(be.has_pane(&PaneId::new("%9999")).expect("has_pane"), Some(false));
527
+
528
+ let (be, _r) = backend_with(MockResp::Out(ok("%8")), vec![]);
529
+ assert_eq!(
530
+ be.has_pane(&PaneId::new("%7")).expect("has_pane"),
531
+ None,
532
+ "a successful but mismatched pane id is not proof that the requested pane exists"
533
+ );
534
+
535
+ let (be, _r) = backend_with(MockResp::Out(ok("not-a-pane")), vec![]);
536
+ assert_eq!(
537
+ be.has_pane(&PaneId::new("%7")).expect("has_pane"),
538
+ None,
539
+ "a successful but invalid pane id stays Unknown"
540
+ );
541
+
542
+ let (be, _r) = backend_with(MockResp::Out(fail(1, "error connecting to server: No such file or directory")), vec![]);
543
+ assert_eq!(
544
+ be.has_pane(&PaneId::new("%7")).expect("has_pane"),
545
+ None,
546
+ "server/probe errors remain Unknown, not absent"
547
+ );
548
+ }
549
+
506
550
  // ── CP-1: per-team socket — for_workspace injects `-L ta-<hash>` at the run chokepoint; new() does NOT ─
507
551
  #[test]
508
552
  fn for_workspace_backend_injects_per_team_socket_but_default_backend_does_not() {
@@ -18,6 +18,7 @@
18
18
  use std::collections::BTreeMap;
19
19
  use std::hash::{Hash, Hasher};
20
20
  use std::io::{Read, Write};
21
+ use std::os::unix::fs::FileTypeExt;
21
22
  use std::path::{Path, PathBuf};
22
23
  use std::process::Stdio;
23
24
  use std::time::{Duration, Instant};
@@ -331,6 +332,20 @@ pub(crate) fn socket_name_for_workspace(workspace: &Path) -> String {
331
332
  }
332
333
 
333
334
  pub(crate) fn socket_path_for_workspace(workspace: &Path) -> Option<PathBuf> {
335
+ if let Some(existing) = existing_socket_path_for_workspace(workspace) {
336
+ return Some(existing);
337
+ }
338
+ let uid = unsafe { libc::geteuid() };
339
+ let default_root = PathBuf::from(format!("/tmp/tmux-{uid}"));
340
+ let default_root = default_root.canonicalize().unwrap_or(default_root);
341
+ Some(default_root.join(socket_name_for_workspace(workspace)))
342
+ }
343
+
344
+ pub(crate) fn socket_probe_missing_for_workspace(workspace: &Path) -> bool {
345
+ existing_socket_path_for_workspace(workspace).is_none()
346
+ }
347
+
348
+ fn existing_socket_path_for_workspace(workspace: &Path) -> Option<PathBuf> {
334
349
  let socket_name = socket_name_for_workspace(workspace);
335
350
  let roots = tmux_socket_roots();
336
351
  for root in &roots {
@@ -340,12 +355,19 @@ pub(crate) fn socket_path_for_workspace(workspace: &Path) -> Option<PathBuf> {
340
355
  return Some(candidate.canonicalize().unwrap_or(candidate));
341
356
  }
342
357
  }
343
- let uid = unsafe { libc::geteuid() };
344
- let default_root = PathBuf::from(format!("/tmp/tmux-{uid}"));
345
- let default_root = default_root
346
- .canonicalize()
347
- .unwrap_or(default_root);
348
- Some(default_root.join(socket_name))
358
+ None
359
+ }
360
+
361
+ pub(crate) fn socket_missing_hint_for_workspace(workspace: &Path) -> String {
362
+ let socket_name = socket_name_for_workspace(workspace);
363
+ let roots = tmux_socket_roots()
364
+ .into_iter()
365
+ .map(|root| root.display().to_string())
366
+ .collect::<Vec<_>>()
367
+ .join(", ");
368
+ format!(
369
+ "tmux socket {socket_name} not found under [{roots}]; run `team-agent attach-leader` or restart the team before attaching"
370
+ )
349
371
  }
350
372
 
351
373
  pub(crate) fn attach_command_for_workspace(
@@ -373,7 +395,7 @@ pub(crate) fn attach_commands_for_windows<'a>(
373
395
  .collect()
374
396
  }
375
397
 
376
- fn tmux_socket_roots() -> Vec<PathBuf> {
398
+ pub(crate) fn tmux_socket_roots() -> Vec<PathBuf> {
377
399
  let uid = unsafe { libc::geteuid() };
378
400
  let mut roots = vec![PathBuf::from(format!("/tmp/tmux-{uid}"))];
379
401
  if let Some(tmpdir) = std::env::var_os("TMPDIR") {
@@ -384,6 +406,29 @@ fn tmux_socket_roots() -> Vec<PathBuf> {
384
406
  roots
385
407
  }
386
408
 
409
+ pub(crate) fn tmux_socket_endpoints() -> Vec<String> {
410
+ let mut endpoints = Vec::new();
411
+ for root in tmux_socket_roots() {
412
+ let Ok(entries) = std::fs::read_dir(root) else {
413
+ continue;
414
+ };
415
+ for entry in entries.flatten() {
416
+ let Ok(file_type) = entry.file_type() else {
417
+ continue;
418
+ };
419
+ if !file_type.is_socket() {
420
+ continue;
421
+ }
422
+ let path = entry.path();
423
+ let path = path.canonicalize().unwrap_or(path);
424
+ endpoints.push(path.to_string_lossy().to_string());
425
+ }
426
+ }
427
+ endpoints.sort();
428
+ endpoints.dedup();
429
+ endpoints
430
+ }
431
+
387
432
  pub(crate) fn socket_name_from_tmux_env() -> Option<String> {
388
433
  let tmux = std::env::var("TMUX")
389
434
  .ok()
@@ -448,9 +493,21 @@ impl TmuxBackend {
448
493
  ];
449
494
  let output = self.run_spawn(&pane_argv)?;
450
495
  let pane = output.stdout.trim();
451
- let pane_id = if pane.is_empty() { "%0" } else { pane };
496
+ // T3-5 (harvest §1): never fabricate a `%0` pane id on an empty reply — a fake
497
+ // pane id mis-addresses every later inject/capture/kill. Surface the miss.
498
+ if pane.is_empty() {
499
+ return Err(TransportError::Subprocess {
500
+ argv: pane_argv,
501
+ code: output.code,
502
+ stderr: format!(
503
+ "tmux display-message returned no pane id for {}:{}",
504
+ session.as_str(),
505
+ window.as_str()
506
+ ),
507
+ });
508
+ }
452
509
  Ok(SpawnResult {
453
- pane_id: PaneId::new(pane_id),
510
+ pane_id: PaneId::new(pane),
454
511
  session: session.clone(),
455
512
  window: window.clone(),
456
513
  child_pid: None,
@@ -661,6 +718,10 @@ impl Transport for TmuxBackend {
661
718
  BackendKind::Tmux
662
719
  }
663
720
 
721
+ fn probes_real_tmux_socket_roots(&self) -> bool {
722
+ true
723
+ }
724
+
664
725
  fn spawn_first(
665
726
  &self,
666
727
  session: &SessionName,
@@ -847,6 +908,40 @@ impl Transport for TmuxBackend {
847
908
  }
848
909
  }
849
910
 
911
+ fn has_pane(&self, pane: &PaneId) -> Result<Option<bool>, TransportError> {
912
+ let argv = self.tmux_argv(&[
913
+ "tmux".to_string(),
914
+ "display-message".to_string(),
915
+ "-p".to_string(),
916
+ "-t".to_string(),
917
+ pane.as_str().to_string(),
918
+ "#{pane_id}".to_string(),
919
+ ]);
920
+ let output = self.runner.run(&argv)?;
921
+ if output.success {
922
+ let pane_id = output.stdout.trim();
923
+ if pane_id.is_empty() {
924
+ return Ok(Some(false));
925
+ }
926
+ if pane_id == pane.as_str()
927
+ && pane_id.starts_with('%')
928
+ && pane_id[1..].chars().all(|ch| ch.is_ascii_digit())
929
+ {
930
+ return Ok(Some(true));
931
+ }
932
+ return Ok(None);
933
+ }
934
+ let stderr = output.stderr.to_ascii_lowercase();
935
+ if stderr.contains("can't find pane")
936
+ || stderr.contains("no such pane")
937
+ || (stderr.contains("can't find") && stderr.contains("pane"))
938
+ {
939
+ Ok(Some(false))
940
+ } else {
941
+ Ok(None)
942
+ }
943
+ }
944
+
850
945
  fn list_targets(&self) -> Result<Vec<PaneInfo>, TransportError> {
851
946
  // P5 (C-P5-3): `#{pane_pid}` rides the single list-panes call (field index 11),
852
947
  // killing the per-pane display-message N+1 fallback.
@@ -18,18 +18,39 @@ pub struct SpawnRecord {
18
18
  pub argv: Vec<String>,
19
19
  }
20
20
 
21
- #[derive(Debug, Clone, Default)]
21
+ #[derive(Debug, Clone)]
22
22
  struct OfflineState {
23
23
  session_present: bool,
24
24
  session_absent_after_spawn_first: bool,
25
25
  targets: Vec<PaneInfo>,
26
26
  windows: Vec<WindowName>,
27
+ pane_presence: BTreeMap<String, bool>,
28
+ liveness: BTreeMap<String, PaneLiveness>,
29
+ default_liveness: PaneLiveness,
27
30
  calls: Vec<&'static str>,
28
31
  spawns: Vec<SpawnRecord>,
29
32
  inject_targets: Vec<Target>,
30
33
  inject_payloads: Vec<String>,
31
34
  }
32
35
 
36
+ impl Default for OfflineState {
37
+ fn default() -> Self {
38
+ Self {
39
+ session_present: false,
40
+ session_absent_after_spawn_first: false,
41
+ targets: Vec::new(),
42
+ windows: Vec::new(),
43
+ pane_presence: BTreeMap::new(),
44
+ liveness: BTreeMap::new(),
45
+ default_liveness: PaneLiveness::Unknown,
46
+ calls: Vec::new(),
47
+ spawns: Vec::new(),
48
+ inject_targets: Vec::new(),
49
+ inject_payloads: Vec::new(),
50
+ }
51
+ }
52
+ }
53
+
33
54
  #[derive(Debug, Clone, Default)]
34
55
  pub struct OfflineTransport {
35
56
  inner: Arc<Mutex<OfflineState>>,
@@ -60,6 +81,25 @@ impl OfflineTransport {
60
81
  self
61
82
  }
62
83
 
84
+ pub fn with_default_liveness(self, liveness: PaneLiveness) -> Self {
85
+ self.with_state(|state| state.default_liveness = liveness);
86
+ self
87
+ }
88
+
89
+ pub fn with_liveness(self, pane: impl Into<String>, liveness: PaneLiveness) -> Self {
90
+ self.with_state(|state| {
91
+ state.liveness.insert(pane.into(), liveness);
92
+ });
93
+ self
94
+ }
95
+
96
+ pub fn with_pane_presence(self, pane: impl Into<String>, present: bool) -> Self {
97
+ self.with_state(|state| {
98
+ state.pane_presence.insert(pane.into(), present);
99
+ });
100
+ self
101
+ }
102
+
63
103
  pub fn calls(&self) -> Vec<&'static str> {
64
104
  self.with_state(|state| state.calls.clone())
65
105
  }
@@ -198,9 +238,22 @@ impl Transport for OfflineTransport {
198
238
  Ok(None)
199
239
  }
200
240
 
201
- fn liveness(&self, _pane: &PaneId) -> Result<PaneLiveness, TransportError> {
202
- self.record("liveness");
203
- Ok(PaneLiveness::Unknown)
241
+ fn liveness(&self, pane: &PaneId) -> Result<PaneLiveness, TransportError> {
242
+ Ok(self.with_state(|state| {
243
+ state.calls.push("liveness");
244
+ state
245
+ .liveness
246
+ .get(pane.as_str())
247
+ .copied()
248
+ .unwrap_or(state.default_liveness)
249
+ }))
250
+ }
251
+
252
+ fn has_pane(&self, pane: &PaneId) -> Result<Option<bool>, TransportError> {
253
+ Ok(self.with_state(|state| {
254
+ state.calls.push("has_pane");
255
+ state.pane_presence.get(pane.as_str()).copied()
256
+ }))
204
257
  }
205
258
 
206
259
  fn list_targets(&self) -> Result<Vec<PaneInfo>, TransportError> {
@@ -399,6 +399,12 @@ pub trait Transport: Send + Sync {
399
399
  /// 后端种类(诊断/事件用)。
400
400
  fn kind(&self) -> BackendKind;
401
401
 
402
+ /// Only the concrete tmux backend should scan real tmux socket roots.
403
+ /// Test doubles stay hermetic and use their injected probe results.
404
+ fn probes_real_tmux_socket_roots(&self) -> bool {
405
+ false
406
+ }
407
+
402
408
  // —— SPAWN(ST):所有后端天然满足;cwd/env 是 spawn 参数,无独立动词(§gap-setenv)——
403
409
 
404
410
  /// tmux=`new-session -d` / wezterm=`spawn --new-window` / conpty=`openpty`+spawn。
@@ -481,6 +487,13 @@ pub trait Transport: Send + Sync {
481
487
  /// pane 存活三态(`PaneLiveness`,bug-085 穷尽 match;unknown ≠ dead ≠ live)。
482
488
  fn liveness(&self, pane: &PaneId) -> Result<PaneLiveness, TransportError>;
483
489
 
490
+ /// Cheap direct pane existence check when a backend can prove it. `Ok(None)`
491
+ /// preserves the existing Unknown boundary.
492
+ fn has_pane(&self, pane: &PaneId) -> Result<Option<bool>, TransportError> {
493
+ let _ = pane;
494
+ Ok(None)
495
+ }
496
+
484
497
  // —— ENUMERATE / IDENTITY(SL + 进程探测):身份/rebind 地基 ——
485
498
 
486
499
  /// 全局枚举所有 pane + 每 pane 的 leader_env。tmux=`list-panes -a` + 读进程 env;
package/npm/install.mjs CHANGED
@@ -80,13 +80,13 @@ function install(argv) {
80
80
  writeExecWrapper(path.join(binDir, "team-agent"), runtimeBinary, []);
81
81
  writeExecWrapper(path.join(binDir, "team_orchestrator"), runtimeBinary, ["mcp-server"]);
82
82
  writeExecWrapper(path.join(binDir, "team-agent-coordinator"), runtimeBinary, ["coordinator"]);
83
- installSkills();
83
+ installSkills(runtimeBinary);
84
84
 
85
85
  const teamAgent = path.join(binDir, "team-agent");
86
86
  console.log(`installed: ${teamAgent}`);
87
87
  console.log(`runtime: ${dest}`);
88
88
  console.log(`binary: ${platformBinary.packageName}`);
89
- console.log("skill: installed for Codex and Claude");
89
+ console.log("skill: installed for Codex, Claude and Copilot");
90
90
  console.log(`PATH: ensure ${binDir} is on PATH`);
91
91
 
92
92
  // 0.3.6 hotfix · C-5 cr verdict — post-install binary smoke 门(走 `--help`
@@ -149,14 +149,24 @@ function runDoctor(argv) {
149
149
  function uninstall(argv) {
150
150
  const opts = parseOptions(argv);
151
151
  const prefix = path.resolve(expandHome(opts.prefix || path.join(os.homedir(), ".local")));
152
+ // 卸载 skill 走二进制单源(同一 SkillTarget 表 codex/claude/copilot),在删 wrapper 前调
153
+ // (删 wrapper 后 PATH 上的 team-agent 没了,但 runtime 二进制仍在;用 runtime 二进制直调)。
154
+ const teamAgentBin = path.join(prefix, "bin", "team-agent");
155
+ if (fs.existsSync(teamAgentBin)) {
156
+ const res = spawnSync(teamAgentBin, ["install-skill", "--target", "all", "--uninstall", "--json"], {
157
+ text: true,
158
+ encoding: "utf8",
159
+ timeout: VERSION_SMOKE_TIMEOUT_MS,
160
+ });
161
+ if (res.status !== 0) {
162
+ console.error(`WARN: skill uninstall via binary failed (status=${res.status ?? "signal"}); skill dirs may remain under ~/.codex|.claude|.copilot/skills/team-agent`);
163
+ }
164
+ }
152
165
  for (const name of ["team-agent", "team_orchestrator", "team-agent-coordinator"]) {
153
166
  fs.rmSync(path.join(prefix, "bin", name), { force: true });
154
167
  }
155
- for (const skillDir of skillDestinations()) {
156
- fs.rmSync(skillDir, { recursive: true, force: true });
157
- }
158
168
  console.log(`removed wrappers from ${path.join(prefix, "bin")}`);
159
- console.log("removed skills from ~/.codex/skills/team-agent and ~/.claude/skills/team-agent");
169
+ console.log("removed skills from ~/.codex, ~/.claude and ~/.copilot skills/team-agent");
160
170
  if (opts.purgeRuntime) {
161
171
  const runtimeRoot = path.resolve(expandHome(opts.runtimeDir || path.join(os.homedir(), ".team-agent", "runtime")));
162
172
  fs.rmSync(runtimeRoot, { recursive: true, force: true });
@@ -238,46 +248,32 @@ exec ${shellQuote(binary)} ${argPrefix}"$@"
238
248
  fs.chmodSync(file, 0o755);
239
249
  }
240
250
 
241
- function installSkills() {
251
+ // RED-1 根治(单源):skill 安装唯一实现在二进制 `install-skill`(SkillTarget 表:
252
+ // codex/claude/copilot)。install.mjs 不再有自己的 JS 拷贝逻辑/目标硬编码——改调二进制,
253
+ // 失败显式报错(非零退出),绝不静默回退 JS。
254
+ function installSkills(runtimeBinary) {
242
255
  const source = path.join(packageRoot, "skills", "team-agent");
243
256
  if (!fs.existsSync(source)) {
244
257
  throw new Error(`skill source not found: ${source}`);
245
258
  }
246
- for (const dest of skillDestinations()) {
247
- fs.rmSync(dest, { recursive: true, force: true });
248
- copyTree(source, dest);
259
+ const res = spawnSync(runtimeBinary, ["install-skill", "--target", "all", "--source", source, "--json"], {
260
+ text: true,
261
+ encoding: "utf8",
262
+ timeout: VERSION_SMOKE_TIMEOUT_MS,
263
+ });
264
+ if (res.status !== 0) {
265
+ const log = (res.stderr || res.stdout || "").trim() || "no stderr/stdout";
266
+ console.error(`ERROR: skill install failed (status=${res.status ?? "signal"})`);
267
+ console.error(`ACTION: reinstall, or run \`team-agent install-skill --target all --source ${source}\` manually`);
268
+ console.error(`LOG: ${runtimeBinary} install-skill --target all => ${log}`);
269
+ process.exit(1);
249
270
  }
250
271
  }
251
272
 
252
- function skillDestinations() {
253
- return [
254
- path.join(os.homedir(), ".codex", "skills", "team-agent"),
255
- path.join(os.homedir(), ".claude", "skills", "team-agent"),
256
- ];
257
- }
258
-
259
273
  function makeDoctorWorkspace() {
260
274
  return fs.mkdtempSync(path.join(os.tmpdir(), "team-agent-doctor-"));
261
275
  }
262
276
 
263
- function copyTree(src, dest) {
264
- const stat = fs.lstatSync(src);
265
- if (stat.isDirectory()) {
266
- fs.mkdirSync(dest, { recursive: true, mode: stat.mode });
267
- for (const entry of fs.readdirSync(src)) {
268
- if (entry === ".DS_Store") {
269
- continue;
270
- }
271
- copyTree(path.join(src, entry), path.join(dest, entry));
272
- }
273
- return;
274
- }
275
- if (stat.isFile()) {
276
- fs.copyFileSync(src, dest);
277
- fs.chmodSync(dest, stat.mode);
278
- }
279
- }
280
-
281
277
  function expandHome(value) {
282
278
  if (value === "~") {
283
279
  return os.homedir();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@team-agent/installer",
3
- "version": "0.3.6",
3
+ "version": "0.3.8",
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.3.6",
24
- "@team-agent/cli-darwin-x64": "0.3.6",
25
- "@team-agent/cli-linux-x64": "0.3.6"
23
+ "@team-agent/cli-darwin-arm64": "0.3.8",
24
+ "@team-agent/cli-darwin-x64": "0.3.8",
25
+ "@team-agent/cli-linux-x64": "0.3.8"
26
26
  },
27
27
  "scripts": {
28
28
  "postinstall": "node npm/bincheck.mjs",