@team-agent/installer 0.3.0 → 0.3.2
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 +38 -7
- package/crates/team-agent/src/cli/emit.rs +182 -54
- package/crates/team-agent/src/cli/mod.rs +703 -35
- package/crates/team-agent/src/cli/status_port.rs +170 -44
- package/crates/team-agent/src/cli/tests/run_delegation.rs +2 -0
- package/crates/team-agent/src/cli/types.rs +1 -0
- package/crates/team-agent/src/coordinator/health.rs +130 -0
- package/crates/team-agent/src/leader/lease.rs +23 -2
- package/crates/team-agent/src/leader/rediscover/tests.rs +1 -0
- package/crates/team-agent/src/leader/rediscover.rs +2 -0
- package/crates/team-agent/src/leader/tests/byte_findings.rs +9 -6
- package/crates/team-agent/src/leader/tests/idle.rs +1 -0
- package/crates/team-agent/src/leader/tests/lease_claim.rs +157 -0
- package/crates/team-agent/src/leader/types.rs +2 -0
- package/crates/team-agent/src/lifecycle/launch.rs +554 -65
- package/crates/team-agent/src/lifecycle/restart/common.rs +65 -0
- package/crates/team-agent/src/lifecycle/restart/rebuild.rs +57 -15
- package/crates/team-agent/src/lifecycle/restart/remove.rs +5 -1
- package/crates/team-agent/src/lifecycle/restart.rs +20 -0
- package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +52 -0
- package/crates/team-agent/src/lifecycle/types.rs +25 -0
- package/crates/team-agent/src/mcp_server/tests/wire.rs +28 -0
- package/crates/team-agent/src/mcp_server/wire.rs +81 -1
- package/crates/team-agent/src/messaging/delivery.rs +574 -12
- package/crates/team-agent/src/messaging/leader_receiver.rs +26 -37
- package/crates/team-agent/src/messaging/mod.rs +1 -1
- package/crates/team-agent/src/messaging/results.rs +218 -49
- package/crates/team-agent/src/messaging/send.rs +15 -19
- package/crates/team-agent/src/provider/adapter.rs +95 -10
- package/crates/team-agent/src/provider/helpers.rs +10 -1
- package/crates/team-agent/src/state/identity.rs +3 -0
- package/crates/team-agent/src/state/persist.rs +113 -1
- package/crates/team-agent/src/state/projection.rs +127 -3
- package/crates/team-agent/src/tmux_backend/tests.rs +179 -0
- package/crates/team-agent/src/tmux_backend.rs +124 -12
- package/npm/install.mjs +29 -7
- package/package.json +4 -4
|
@@ -161,7 +161,12 @@ pub struct TmuxBackend {
|
|
|
161
161
|
runner: Box<dyn CommandRunner>,
|
|
162
162
|
/// `Some(name)` for a per-team socket -> every `tmux` argv gets `-L <name>` injected after the
|
|
163
163
|
/// leading "tmux" token; `None` (default) -> bare `tmux` on the shared default socket.
|
|
164
|
-
socket: Option<
|
|
164
|
+
socket: Option<TmuxSocketEndpoint>,
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
enum TmuxSocketEndpoint {
|
|
168
|
+
Name(String),
|
|
169
|
+
Path(String),
|
|
165
170
|
}
|
|
166
171
|
|
|
167
172
|
impl TmuxBackend {
|
|
@@ -177,7 +182,25 @@ impl TmuxBackend {
|
|
|
177
182
|
pub fn for_workspace(workspace: &Path) -> Self {
|
|
178
183
|
Self {
|
|
179
184
|
runner: Box::new(RealCommandRunner),
|
|
180
|
-
socket: Some(socket_name_for_workspace(workspace)),
|
|
185
|
+
socket: Some(TmuxSocketEndpoint::Name(socket_name_for_workspace(workspace))),
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
pub(crate) fn for_socket_name(socket: &str) -> Self {
|
|
190
|
+
if socket.is_empty() || socket == "default" {
|
|
191
|
+
Self::new()
|
|
192
|
+
} else {
|
|
193
|
+
Self { runner: Box::new(RealCommandRunner), socket: Some(TmuxSocketEndpoint::Name(socket.to_string())) }
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
pub(crate) fn for_tmux_endpoint(endpoint: &str) -> Self {
|
|
198
|
+
if endpoint.is_empty() || endpoint == "default" {
|
|
199
|
+
Self::new()
|
|
200
|
+
} else if Path::new(endpoint).is_absolute() {
|
|
201
|
+
Self { runner: Box::new(RealCommandRunner), socket: Some(TmuxSocketEndpoint::Path(endpoint.to_string())) }
|
|
202
|
+
} else {
|
|
203
|
+
Self::new()
|
|
181
204
|
}
|
|
182
205
|
}
|
|
183
206
|
|
|
@@ -189,7 +212,17 @@ impl TmuxBackend {
|
|
|
189
212
|
/// Backend with an injected runner bound to a per-workspace socket (tests: assert the `-L` is in
|
|
190
213
|
/// the recorded argv for a workspace-bound backend).
|
|
191
214
|
pub fn with_runner_for_workspace(runner: Box<dyn CommandRunner>, workspace: &Path) -> Self {
|
|
192
|
-
Self { runner, socket: Some(socket_name_for_workspace(workspace)) }
|
|
215
|
+
Self { runner, socket: Some(TmuxSocketEndpoint::Name(socket_name_for_workspace(workspace))) }
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
pub(crate) fn with_runner_for_tmux_endpoint(runner: Box<dyn CommandRunner>, endpoint: &str) -> Self {
|
|
219
|
+
if Path::new(endpoint).is_absolute() {
|
|
220
|
+
Self { runner, socket: Some(TmuxSocketEndpoint::Path(endpoint.to_string())) }
|
|
221
|
+
} else if endpoint.is_empty() || endpoint == "default" {
|
|
222
|
+
Self { runner, socket: None }
|
|
223
|
+
} else {
|
|
224
|
+
Self { runner, socket: None }
|
|
225
|
+
}
|
|
193
226
|
}
|
|
194
227
|
|
|
195
228
|
/// THE RUN CHOKEPOINT: every executed `tmux` argv is funneled through here. When a per-team
|
|
@@ -197,11 +230,19 @@ impl TmuxBackend {
|
|
|
197
230
|
/// through unchanged. Non-`tmux` argv (e.g. the spawned provider command) is never rewritten.
|
|
198
231
|
fn tmux_argv(&self, argv: &[String]) -> Vec<String> {
|
|
199
232
|
match &self.socket {
|
|
200
|
-
Some(
|
|
233
|
+
Some(endpoint) if argv.first().map(String::as_str) == Some("tmux") => {
|
|
201
234
|
let mut out = Vec::with_capacity(argv.len() + 2);
|
|
202
235
|
out.push("tmux".to_string());
|
|
203
|
-
|
|
204
|
-
|
|
236
|
+
match endpoint {
|
|
237
|
+
TmuxSocketEndpoint::Name(socket) => {
|
|
238
|
+
out.push("-L".to_string());
|
|
239
|
+
out.push(socket.clone());
|
|
240
|
+
}
|
|
241
|
+
TmuxSocketEndpoint::Path(socket) => {
|
|
242
|
+
out.push("-S".to_string());
|
|
243
|
+
out.push(socket.clone());
|
|
244
|
+
}
|
|
245
|
+
}
|
|
205
246
|
out.extend(argv.iter().skip(1).cloned());
|
|
206
247
|
out
|
|
207
248
|
}
|
|
@@ -237,6 +278,17 @@ pub(crate) fn socket_name_for_workspace(workspace: &Path) -> String {
|
|
|
237
278
|
format!("ta-{:012x}", hasher.finish() & 0xffff_ffff_ffff)
|
|
238
279
|
}
|
|
239
280
|
|
|
281
|
+
pub(crate) fn socket_name_from_tmux_env() -> Option<String> {
|
|
282
|
+
let tmux = std::env::var("TMUX")
|
|
283
|
+
.ok()
|
|
284
|
+
.filter(|value| !value.is_empty())?;
|
|
285
|
+
let socket_path = tmux.split(',').next().unwrap_or("").trim();
|
|
286
|
+
if socket_path.is_empty() || !Path::new(socket_path).is_absolute() {
|
|
287
|
+
return None;
|
|
288
|
+
}
|
|
289
|
+
Some(socket_path.to_string())
|
|
290
|
+
}
|
|
291
|
+
|
|
240
292
|
/// Deterministic FNV-1a (64-bit) — std `DefaultHasher` is NOT stable across releases, so a fixed
|
|
241
293
|
/// FNV keeps the socket identical for the CLI, the daemon, and every later op on the same workspace.
|
|
242
294
|
struct Fnv1a(u64);
|
|
@@ -442,6 +494,14 @@ fn submit_verification_for_key(key: Key) -> SubmitVerification {
|
|
|
442
494
|
}
|
|
443
495
|
}
|
|
444
496
|
|
|
497
|
+
fn capture_has_pasted_content_prompt(text: &str) -> bool {
|
|
498
|
+
let lower = text.to_ascii_lowercase();
|
|
499
|
+
lower.contains("pasted content") || lower.contains("pasted text")
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
const PASTED_CONTENT_APPEAR_POLLS: u32 = 5;
|
|
503
|
+
const PASTED_CONTENT_SUBMIT_ATTEMPTS: u32 = 3;
|
|
504
|
+
|
|
445
505
|
fn shell_command(argv: &[String], cwd: &Path, env: &BTreeMap<String, String>) -> String {
|
|
446
506
|
let mut parts = Vec::new();
|
|
447
507
|
parts.push("cd".to_string());
|
|
@@ -527,7 +587,40 @@ impl Transport for TmuxBackend {
|
|
|
527
587
|
self.run_inject_stage(&argv, stage)?;
|
|
528
588
|
}
|
|
529
589
|
}
|
|
590
|
+
let mut saw_pasted_prompt = false;
|
|
591
|
+
for _ in 0..PASTED_CONTENT_APPEAR_POLLS {
|
|
592
|
+
let captured = self.capture(target, CaptureRange::Tail(80))?;
|
|
593
|
+
if capture_has_pasted_content_prompt(&captured.text) {
|
|
594
|
+
saw_pasted_prompt = true;
|
|
595
|
+
break;
|
|
596
|
+
}
|
|
597
|
+
std::thread::sleep(Duration::from_millis(25));
|
|
598
|
+
}
|
|
530
599
|
let submit_argv = tmux_send_keys_argv(&pane, &[submit]);
|
|
600
|
+
if saw_pasted_prompt {
|
|
601
|
+
let mut attempts = 0;
|
|
602
|
+
let mut cleared = false;
|
|
603
|
+
for _ in 0..PASTED_CONTENT_SUBMIT_ATTEMPTS {
|
|
604
|
+
attempts += 1;
|
|
605
|
+
self.run_inject_stage(&submit_argv, InjectStage::Submit)?;
|
|
606
|
+
let captured = self.capture(target, CaptureRange::Tail(80))?;
|
|
607
|
+
if !capture_has_pasted_content_prompt(&captured.text) {
|
|
608
|
+
cleared = true;
|
|
609
|
+
break;
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
return Ok(InjectReport {
|
|
613
|
+
stage_reached: InjectStage::Submit,
|
|
614
|
+
inject_verification: InjectVerification::CaptureContainsNewPastedContentPrompt,
|
|
615
|
+
submit_verification: if cleared {
|
|
616
|
+
SubmitVerification::PastedContentPromptAbsentAfterSubmit
|
|
617
|
+
} else {
|
|
618
|
+
submit_verification_for_key(submit)
|
|
619
|
+
},
|
|
620
|
+
turn_verification: TurnVerification::NotYetObserved,
|
|
621
|
+
attempts,
|
|
622
|
+
});
|
|
623
|
+
}
|
|
531
624
|
self.run_inject_stage(&submit_argv, InjectStage::Submit)?;
|
|
532
625
|
}
|
|
533
626
|
}
|
|
@@ -622,7 +715,10 @@ impl Transport for TmuxBackend {
|
|
|
622
715
|
}
|
|
623
716
|
let mut panes = Vec::new();
|
|
624
717
|
for line in output.stdout.lines().filter(|line| !line.is_empty()) {
|
|
625
|
-
if let Some(pane) = parse_pane_info_line(line) {
|
|
718
|
+
if let Some(mut pane) = parse_pane_info_line(line) {
|
|
719
|
+
if pane.pane_pid.is_none() {
|
|
720
|
+
pane.pane_pid = query_pane_pid(self, &pane.pane_id)?;
|
|
721
|
+
}
|
|
626
722
|
panes.push(pane);
|
|
627
723
|
}
|
|
628
724
|
}
|
|
@@ -685,22 +781,22 @@ impl Transport for TmuxBackend {
|
|
|
685
781
|
}
|
|
686
782
|
|
|
687
783
|
fn kill_session(&self, session: &SessionName) -> Result<(), TransportError> {
|
|
688
|
-
let argv =
|
|
784
|
+
let argv = self.tmux_argv(&[
|
|
689
785
|
"tmux".to_string(),
|
|
690
786
|
"kill-session".to_string(),
|
|
691
787
|
"-t".to_string(),
|
|
692
788
|
session.as_str().to_string(),
|
|
693
|
-
];
|
|
789
|
+
]);
|
|
694
790
|
self.run_ok(&argv)
|
|
695
791
|
}
|
|
696
792
|
|
|
697
793
|
fn kill_window(&self, target: &Target) -> Result<(), TransportError> {
|
|
698
|
-
let argv =
|
|
794
|
+
let argv = self.tmux_argv(&[
|
|
699
795
|
"tmux".to_string(),
|
|
700
796
|
"kill-window".to_string(),
|
|
701
797
|
"-t".to_string(),
|
|
702
798
|
target_name(target),
|
|
703
|
-
];
|
|
799
|
+
]);
|
|
704
800
|
self.run_ok(&argv)
|
|
705
801
|
}
|
|
706
802
|
|
|
@@ -719,6 +815,22 @@ impl Transport for TmuxBackend {
|
|
|
719
815
|
}
|
|
720
816
|
}
|
|
721
817
|
|
|
818
|
+
fn query_pane_pid(backend: &TmuxBackend, pane: &PaneId) -> Result<Option<u32>, TransportError> {
|
|
819
|
+
let argv = backend.tmux_argv(&[
|
|
820
|
+
"tmux".to_string(),
|
|
821
|
+
"display-message".to_string(),
|
|
822
|
+
"-p".to_string(),
|
|
823
|
+
"-t".to_string(),
|
|
824
|
+
pane.as_str().to_string(),
|
|
825
|
+
"#{pane_pid}".to_string(),
|
|
826
|
+
]);
|
|
827
|
+
let output = backend.runner.run(&argv)?;
|
|
828
|
+
if !output.success {
|
|
829
|
+
return Ok(None);
|
|
830
|
+
}
|
|
831
|
+
Ok(parse_optional_u32(output.stdout.trim()))
|
|
832
|
+
}
|
|
833
|
+
|
|
722
834
|
fn parse_pane_info_line(line: &str) -> Option<PaneInfo> {
|
|
723
835
|
let fields = line.split('\t').collect::<Vec<_>>();
|
|
724
836
|
if fields.len() < 11 {
|
|
@@ -734,7 +846,7 @@ fn parse_pane_info_line(line: &str) -> Option<PaneInfo> {
|
|
|
734
846
|
current_command: non_empty(fields[6]).map(str::to_string),
|
|
735
847
|
active: fields[7] == "1",
|
|
736
848
|
current_path: non_empty(fields[8]).map(PathBuf::from),
|
|
737
|
-
pane_pid:
|
|
849
|
+
pane_pid: fields.get(11).and_then(|raw| parse_optional_u32(raw)),
|
|
738
850
|
leader_env: BTreeMap::new(),
|
|
739
851
|
})
|
|
740
852
|
}
|
package/npm/install.mjs
CHANGED
|
@@ -10,6 +10,7 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
|
10
10
|
const packageRoot = path.resolve(__dirname, "..");
|
|
11
11
|
const require = createRequire(import.meta.url);
|
|
12
12
|
const packageJson = JSON.parse(fs.readFileSync(path.join(packageRoot, "package.json"), "utf8"));
|
|
13
|
+
const DOCTOR_TIMEOUT_MS = 5000;
|
|
13
14
|
|
|
14
15
|
const command = process.argv[2] || "install";
|
|
15
16
|
const args = process.argv.slice(3);
|
|
@@ -87,11 +88,20 @@ function install(argv) {
|
|
|
87
88
|
console.log("skill: installed for Codex and Claude");
|
|
88
89
|
console.log(`PATH: ensure ${binDir} is on PATH`);
|
|
89
90
|
|
|
90
|
-
const
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
91
|
+
const doctorWorkspace = makeDoctorWorkspace();
|
|
92
|
+
try {
|
|
93
|
+
const doctor = spawnSync(teamAgent, ["doctor", "--json", "--workspace", doctorWorkspace], {
|
|
94
|
+
text: true,
|
|
95
|
+
encoding: "utf8",
|
|
96
|
+
timeout: DOCTOR_TIMEOUT_MS,
|
|
97
|
+
});
|
|
98
|
+
if (doctor.status === 0) {
|
|
99
|
+
console.log("doctor: ok");
|
|
100
|
+
} else {
|
|
101
|
+
console.log("doctor: has blockers; run `team-agent doctor` after updating PATH");
|
|
102
|
+
}
|
|
103
|
+
} finally {
|
|
104
|
+
fs.rmSync(doctorWorkspace, { recursive: true, force: true });
|
|
95
105
|
}
|
|
96
106
|
}
|
|
97
107
|
|
|
@@ -103,8 +113,16 @@ function runDoctor(argv) {
|
|
|
103
113
|
console.error(`team-agent wrapper not found: ${teamAgent}`);
|
|
104
114
|
process.exit(1);
|
|
105
115
|
}
|
|
106
|
-
const
|
|
107
|
-
|
|
116
|
+
const doctorWorkspace = makeDoctorWorkspace();
|
|
117
|
+
try {
|
|
118
|
+
const proc = spawnSync(teamAgent, ["doctor", "--workspace", doctorWorkspace], {
|
|
119
|
+
stdio: "inherit",
|
|
120
|
+
timeout: DOCTOR_TIMEOUT_MS,
|
|
121
|
+
});
|
|
122
|
+
process.exit(proc.status ?? 1);
|
|
123
|
+
} finally {
|
|
124
|
+
fs.rmSync(doctorWorkspace, { recursive: true, force: true });
|
|
125
|
+
}
|
|
108
126
|
}
|
|
109
127
|
|
|
110
128
|
function uninstall(argv) {
|
|
@@ -217,6 +235,10 @@ function skillDestinations() {
|
|
|
217
235
|
];
|
|
218
236
|
}
|
|
219
237
|
|
|
238
|
+
function makeDoctorWorkspace() {
|
|
239
|
+
return fs.mkdtempSync(path.join(os.tmpdir(), "team-agent-doctor-"));
|
|
240
|
+
}
|
|
241
|
+
|
|
220
242
|
function copyTree(src, dest) {
|
|
221
243
|
const stat = fs.lstatSync(src);
|
|
222
244
|
if (stat.isDirectory()) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@team-agent/installer",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.2",
|
|
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.2",
|
|
24
|
+
"@team-agent/cli-darwin-x64": "0.3.2",
|
|
25
|
+
"@team-agent/cli-linux-x64": "0.3.2"
|
|
26
26
|
},
|
|
27
27
|
"scripts": {
|
|
28
28
|
"postinstall": "node npm/bincheck.mjs",
|