@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.
- package/Cargo.lock +1 -1
- package/Cargo.toml +1 -1
- package/crates/team-agent/src/cli/adapters.rs +52 -7
- package/crates/team-agent/src/cli/diagnose.rs +9 -0
- package/crates/team-agent/src/cli/emit.rs +175 -0
- package/crates/team-agent/src/cli/mod.rs +455 -63
- package/crates/team-agent/src/cli/status_port.rs +62 -0
- package/crates/team-agent/src/cli/tests/base.rs +9 -4
- package/crates/team-agent/src/cli/tests/missing_subcommands.rs +83 -1
- package/crates/team-agent/src/cli/tests/mod.rs +1 -0
- package/crates/team-agent/src/cli/tests/run_delegation.rs +10 -2
- package/crates/team-agent/src/cli/tests/shutdown_kill_plan.rs +86 -21
- package/crates/team-agent/src/cli/tests/verb_install_skill.rs +76 -0
- package/crates/team-agent/src/cli/types.rs +3 -2
- package/crates/team-agent/src/compiler.rs +73 -50
- package/crates/team-agent/src/coordinator/tick.rs +108 -20
- package/crates/team-agent/src/db/migration.rs +17 -1
- package/crates/team-agent/src/leader/owner_bind.rs +59 -20
- package/crates/team-agent/src/lifecycle/launch.rs +378 -56
- package/crates/team-agent/src/lifecycle/restart/common.rs +4 -9
- package/crates/team-agent/src/lifecycle/restart/rebuild.rs +91 -12
- package/crates/team-agent/src/lifecycle/restart/selection.rs +6 -4
- package/crates/team-agent/src/lifecycle/tests/core.rs +238 -3
- package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +257 -7
- package/crates/team-agent/src/lifecycle/types.rs +2 -0
- package/crates/team-agent/src/mcp_server/normalize.rs +29 -7
- package/crates/team-agent/src/mcp_server/tests/golden.rs +7 -5
- package/crates/team-agent/src/mcp_server/tests/normalize.rs +5 -2
- package/crates/team-agent/src/mcp_server/tools.rs +25 -1
- package/crates/team-agent/src/mcp_server/wire.rs +11 -1
- package/crates/team-agent/src/model/paths.rs +7 -0
- package/crates/team-agent/src/model/spec.rs +23 -1
- package/crates/team-agent/src/packaging/install.rs +42 -4
- package/crates/team-agent/src/packaging/tests.rs +91 -14
- package/crates/team-agent/src/packaging/types.rs +13 -1
- package/crates/team-agent/src/provider/adapter.rs +381 -15
- package/crates/team-agent/src/state/identity.rs +29 -0
- package/crates/team-agent/src/state/selector.rs +48 -14
- package/crates/team-agent/src/tmux_backend/tests.rs +44 -0
- package/crates/team-agent/src/tmux_backend.rs +104 -9
- package/crates/team-agent/src/transport/test_support.rs +57 -4
- package/crates/team-agent/src/transport.rs +13 -0
- package/npm/install.mjs +31 -35
- package/package.json +4 -4
- 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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
|
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,
|
|
202
|
-
self.
|
|
203
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
247
|
-
|
|
248
|
-
|
|
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.
|
|
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.
|
|
24
|
-
"@team-agent/cli-darwin-x64": "0.3.
|
|
25
|
-
"@team-agent/cli-linux-x64": "0.3.
|
|
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",
|