@team-agent/installer 0.3.24 → 0.3.25
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/coordinator/tick.rs +55 -7
- package/crates/team-agent/src/lifecycle/launch.rs +217 -19
- package/crates/team-agent/src/lifecycle/restart/agent.rs +61 -0
- package/crates/team-agent/src/lifecycle/restart/common.rs +66 -1
- package/crates/team-agent/src/lifecycle/restart.rs +19 -2
- package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +994 -0
- package/crates/team-agent/src/messaging/delivery.rs +12 -0
- package/crates/team-agent/src/messaging/helpers.rs +64 -24
- package/crates/team-agent/src/messaging/tests/spine.rs +157 -4
- package/crates/team-agent/src/tmux_backend/tests.rs +251 -0
- package/crates/team-agent/src/tmux_backend.rs +128 -0
- package/crates/team-agent/src/transport.rs +26 -1
- package/npm/install.mjs +90 -25
- package/package.json +4 -4
|
@@ -927,6 +927,51 @@ fn capture_has_pasted_content_prompt(text: &str) -> bool {
|
|
|
927
927
|
const PASTED_CONTENT_APPEAR_POLLS: u32 = 5;
|
|
928
928
|
const PASTED_CONTENT_SUBMIT_ATTEMPTS: u32 = 3;
|
|
929
929
|
|
|
930
|
+
/// E46 (0.3.24 bug#5): bounded resend cap for post-Enter consumption probe.
|
|
931
|
+
/// Mirrors PASTED_CONTENT_SUBMIT_ATTEMPTS shape: try Enter then bounded
|
|
932
|
+
/// re-checks of the pane's input region. Each iteration first re-checks that
|
|
933
|
+
/// the input still has content before resending Enter — guards against double
|
|
934
|
+
/// submission when the first Enter was consumed but our readback was slow.
|
|
935
|
+
const POST_SUBMIT_CONSUMPTION_ATTEMPTS: u32 = 3;
|
|
936
|
+
const POST_SUBMIT_CONSUMPTION_POLL_MS: u64 = 60;
|
|
937
|
+
|
|
938
|
+
/// E46 (0.3.24 bug#5, C5 provider-agnostic detector): the pane's input region
|
|
939
|
+
/// is "consumed" when the token text that was just visible BEFORE the Enter
|
|
940
|
+
/// is no longer present in the captured tail. Structural signal — no
|
|
941
|
+
/// provider-specific UI string. Works across claude / codex / copilot because
|
|
942
|
+
/// every provider's composer clears the input area after a successful submit
|
|
943
|
+
/// (the content scrolls into history, leaving the prompt empty).
|
|
944
|
+
///
|
|
945
|
+
/// Returns:
|
|
946
|
+
/// * `Some(true)` — token was visible BEFORE submit and is GONE from
|
|
947
|
+
/// the visible input area now → consumption confirmed.
|
|
948
|
+
/// * `Some(false)` — token still visible (or other reason to think not yet
|
|
949
|
+
/// consumed).
|
|
950
|
+
/// * `None` — payload has no token marker (peer message without token,
|
|
951
|
+
/// empty payload) so we can't structurally check; caller treats this as
|
|
952
|
+
/// non-blocking (the pre-existing `EnterSentWithoutPlaceholderCheck`
|
|
953
|
+
/// path).
|
|
954
|
+
fn post_submit_input_consumed(
|
|
955
|
+
backend: &TmuxBackend,
|
|
956
|
+
target: &Target,
|
|
957
|
+
payload: &InjectPayload,
|
|
958
|
+
) -> Result<Option<bool>, TransportError> {
|
|
959
|
+
let Some(marker) = payload_token_marker(payload) else {
|
|
960
|
+
return Ok(None);
|
|
961
|
+
};
|
|
962
|
+
let captured = backend.capture(target, CaptureRange::Tail(30))?;
|
|
963
|
+
// The token may legitimately appear in scrollback (a successful submit
|
|
964
|
+
// pushes it into history). We only treat the BOTTOM-of-pane region (last
|
|
965
|
+
// few lines, where the input area lives) as the consumption signal. Tail
|
|
966
|
+
// 30 lines is small enough that the input area still dominates if the
|
|
967
|
+
// submit didn't go through, while a successful submit has pushed the
|
|
968
|
+
// token marker out of the bottom 5 lines by the time the response
|
|
969
|
+
// composer redraws.
|
|
970
|
+
let tail_lines: Vec<&str> = captured.text.lines().rev().take(5).collect();
|
|
971
|
+
let token_in_tail = tail_lines.iter().any(|line| line.contains(marker));
|
|
972
|
+
Ok(Some(!token_in_tail))
|
|
973
|
+
}
|
|
974
|
+
|
|
930
975
|
fn shell_command(
|
|
931
976
|
argv: &[String],
|
|
932
977
|
cwd: &Path,
|
|
@@ -1117,11 +1162,94 @@ impl Transport for TmuxBackend {
|
|
|
1117
1162
|
// downgrade to CaptureMissingToken.
|
|
1118
1163
|
token_visible_for_report =
|
|
1119
1164
|
pre_submit_token_visible(self, target, payload).unwrap_or(None);
|
|
1165
|
+
// E46 (0.3.24 bug#5, demo-director卡 bracketed paste root cause):
|
|
1166
|
+
// bracketed-paste-mode on a fresh provider TUI swallows the framework's
|
|
1167
|
+
// Enter as paste content. Send `Escape` first to exit the paste bracket
|
|
1168
|
+
// so the subsequent Enter is interpreted as submit. Safe across
|
|
1169
|
+
// claude/codex/copilot: Escape on an empty composer is a no-op
|
|
1170
|
+
// (cancel any pending mode), it only matters when bracketed-paste is
|
|
1171
|
+
// actually open. Architect-approved + macmini real-machine verified.
|
|
1172
|
+
// Only Enter benefits — explicit menu navigation (Key::Down etc.)
|
|
1173
|
+
// should not pre-cancel mode.
|
|
1174
|
+
if bracketed
|
|
1175
|
+
&& matches!(payload, InjectPayload::Text(_))
|
|
1176
|
+
&& matches!(submit, Key::Enter)
|
|
1177
|
+
{
|
|
1178
|
+
let escape_argv = tmux_send_keys_argv(&pane, &[Key::Escape]);
|
|
1179
|
+
// We don't fail the whole inject on Escape error — Escape failure
|
|
1180
|
+
// would surface downstream as the same SubmitConsumptionUnverified
|
|
1181
|
+
// when post-Enter probe still sees the token in the input region.
|
|
1182
|
+
let _ = self.run_inject_stage(&escape_argv, InjectStage::Submit);
|
|
1183
|
+
}
|
|
1120
1184
|
self.run_inject_stage(&submit_argv, InjectStage::Submit)?;
|
|
1121
1185
|
if matches!(token_visible_for_report, Some(false)) {
|
|
1122
1186
|
token_visible_for_report =
|
|
1123
1187
|
post_submit_token_visible(self, target, payload).unwrap_or(Some(false));
|
|
1124
1188
|
}
|
|
1189
|
+
// E46 C2 + C3: post-Enter consumption gate with bounded resend cap.
|
|
1190
|
+
// Only fires for:
|
|
1191
|
+
// - submit key == Enter (other keys like Down/Up are explicit
|
|
1192
|
+
// menu navigation; codex uses `Down Enter` to dismiss a
|
|
1193
|
+
// prompt — those carry their own verification semantics
|
|
1194
|
+
// via KeySentAfterVisibleToken),
|
|
1195
|
+
// - token-bearing Text payloads (the structural input-empty
|
|
1196
|
+
// signal needs a token marker to anchor on).
|
|
1197
|
+
// Each iteration RE-CHECKS that the input still contains the
|
|
1198
|
+
// token BEFORE resending Enter — guards against the C3
|
|
1199
|
+
// double-submit hazard when the first Enter was consumed but
|
|
1200
|
+
// our readback was slow to catch the empty-input state.
|
|
1201
|
+
let mut consumption_attempts: u32 = 1;
|
|
1202
|
+
let mut consumed = if matches!(submit, Key::Enter) {
|
|
1203
|
+
post_submit_input_consumed(self, target, payload).unwrap_or(None)
|
|
1204
|
+
} else {
|
|
1205
|
+
None
|
|
1206
|
+
};
|
|
1207
|
+
if matches!(consumed, Some(false)) {
|
|
1208
|
+
for _ in 1..POST_SUBMIT_CONSUMPTION_ATTEMPTS {
|
|
1209
|
+
std::thread::sleep(Duration::from_millis(
|
|
1210
|
+
POST_SUBMIT_CONSUMPTION_POLL_MS,
|
|
1211
|
+
));
|
|
1212
|
+
// C3 critical: re-check BEFORE resending. If the input
|
|
1213
|
+
// is now empty (= first Enter actually consumed, just
|
|
1214
|
+
// observable now), DO NOT resend — bumping a spurious
|
|
1215
|
+
// empty Enter would open an empty turn.
|
|
1216
|
+
consumed = post_submit_input_consumed(self, target, payload)
|
|
1217
|
+
.unwrap_or(None);
|
|
1218
|
+
if !matches!(consumed, Some(false)) {
|
|
1219
|
+
break;
|
|
1220
|
+
}
|
|
1221
|
+
consumption_attempts += 1;
|
|
1222
|
+
let _ = self.run_inject_stage(&submit_argv, InjectStage::Submit);
|
|
1223
|
+
}
|
|
1224
|
+
}
|
|
1225
|
+
let submit_verification = match consumed {
|
|
1226
|
+
// Consumption confirmed → MUST-10 path: keep the canonical
|
|
1227
|
+
// EnterSentWithoutPlaceholderCheck so the existing
|
|
1228
|
+
// provider_submit_verification_red contract holds.
|
|
1229
|
+
Some(true) => SubmitVerification::EnterSentWithoutPlaceholderCheck,
|
|
1230
|
+
// Consumption explicitly NOT observed (token still in tail
|
|
1231
|
+
// after resend cap) → E46's new variant. Delivery treats
|
|
1232
|
+
// as not delivered.
|
|
1233
|
+
Some(false) => SubmitVerification::SubmitConsumptionUnverified,
|
|
1234
|
+
// No structural anchor (non-token payload) → preserve the
|
|
1235
|
+
// historic non-checked variant; the pre-existing
|
|
1236
|
+
// pre/post-submit token readback + U1 #7 cover the other
|
|
1237
|
+
// false-positive (silent paste drop).
|
|
1238
|
+
None => submit_verification_for_key(submit),
|
|
1239
|
+
};
|
|
1240
|
+
return Ok(InjectReport {
|
|
1241
|
+
stage_reached: InjectStage::Submit,
|
|
1242
|
+
inject_verification: inject_verification_after_readback(
|
|
1243
|
+
payload,
|
|
1244
|
+
token_visible_for_report,
|
|
1245
|
+
),
|
|
1246
|
+
submit_verification,
|
|
1247
|
+
turn_verification: match payload {
|
|
1248
|
+
InjectPayload::Empty => TurnVerification::NotRequired,
|
|
1249
|
+
InjectPayload::Text(_) => TurnVerification::NotYetObserved,
|
|
1250
|
+
},
|
|
1251
|
+
attempts: consumption_attempts,
|
|
1252
|
+
});
|
|
1125
1253
|
}
|
|
1126
1254
|
}
|
|
1127
1255
|
Ok(InjectReport {
|
|
@@ -157,6 +157,13 @@ pub enum Key {
|
|
|
157
157
|
Char(char),
|
|
158
158
|
/// `\x03`。
|
|
159
159
|
CtrlC,
|
|
160
|
+
/// E46 (0.3.24 bug#5): pre-submit Escape to exit bracketed-paste mode on
|
|
161
|
+
/// fresh provider TUIs (claude). When a bracketed paste lands on a TUI
|
|
162
|
+
/// whose composer is still initialising, the framework's plain `Enter`
|
|
163
|
+
/// gets interpreted as paste content, not submit. Sending `Escape` first
|
|
164
|
+
/// closes the paste bracket so the subsequent `Enter` submits.
|
|
165
|
+
/// Real-machine truth source: macmini demo-director repro.
|
|
166
|
+
Escape,
|
|
160
167
|
/// tmux `-X cancel` / `q` / `d`;非 tmux 后端无 copy-mode 概念 → no-op。
|
|
161
168
|
CancelMode,
|
|
162
169
|
}
|
|
@@ -287,7 +294,10 @@ pub enum InjectVerification {
|
|
|
287
294
|
/// `{key}_sent_after_visible_token` 是模板 → 用携带 Key 的 variant 表达。
|
|
288
295
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
289
296
|
pub enum SubmitVerification {
|
|
290
|
-
/// `enter_sent_without_placeholder_check`。
|
|
297
|
+
/// `enter_sent_without_placeholder_check`。MUST-10:此 variant 代表 paste+Enter
|
|
298
|
+
/// 完成且无需 placeholder probe(纯 peer 消息路径)。**保留** ⇒ delivered 语义;
|
|
299
|
+
/// E46 后只有 post-Enter consumption 确认通过(input 清空 / Working 信号)才返回
|
|
300
|
+
/// 此 variant — fresh TUI 未消费走 [`Self::SubmitConsumptionUnverified`]。
|
|
291
301
|
EnterSentWithoutPlaceholderCheck,
|
|
292
302
|
/// `pasted_content_prompt_absent_after_submit`。
|
|
293
303
|
PastedContentPromptAbsentAfterSubmit,
|
|
@@ -297,6 +307,14 @@ pub enum SubmitVerification {
|
|
|
297
307
|
KeySentAfterVisibleToken { key: Key },
|
|
298
308
|
/// `send_keys_failed`。
|
|
299
309
|
SendKeysFailed,
|
|
310
|
+
/// E46 (0.3.24 bug#5, demo-director 卡 bracketed paste): Enter 已发但
|
|
311
|
+
/// post-Enter 接收侧消费信号(input 行清空 / provider 进 Working)在 bounded
|
|
312
|
+
/// resend 上限内未观察到。**delivery 不当作 delivered**;走 submitted_unverified /
|
|
313
|
+
/// failed 路径。区别于
|
|
314
|
+
/// [`Self::PastedContentPromptStillPresentAfterSubmit`](claude paste-prompt
|
|
315
|
+
/// 折叠场景)— 本 variant 是结构化 input-empty 检测,适配 demo-director
|
|
316
|
+
/// 直接渲染文本路径。
|
|
317
|
+
SubmitConsumptionUnverified,
|
|
300
318
|
}
|
|
301
319
|
|
|
302
320
|
/// turn-boundary 观测(tmux_io.py:224-260,09-transport.md 表 §40)。
|
|
@@ -621,6 +639,10 @@ pub fn tmux_key_name(key: Key) -> &'static str {
|
|
|
621
639
|
Key::Char('8') => "8",
|
|
622
640
|
Key::Char('9') => "9",
|
|
623
641
|
Key::CtrlC => "C-c",
|
|
642
|
+
// E46 (0.3.24 bug#5): tmux supports `Escape` as a key name; sending
|
|
643
|
+
// it as a send-keys arg emits `\x1b` to the pane (closes bracketed
|
|
644
|
+
// paste mode on a stuck TUI composer).
|
|
645
|
+
Key::Escape => "Escape",
|
|
624
646
|
Key::CancelMode | Key::Char(_) => "",
|
|
625
647
|
}
|
|
626
648
|
}
|
|
@@ -850,6 +872,9 @@ pub fn submit_verification_wire(v: SubmitVerification) -> String {
|
|
|
850
872
|
format!("{}_sent_after_visible_token", tmux_key_name(key))
|
|
851
873
|
}
|
|
852
874
|
SubmitVerification::SendKeysFailed => "send_keys_failed".to_string(),
|
|
875
|
+
SubmitVerification::SubmitConsumptionUnverified => {
|
|
876
|
+
"submit_consumption_unverified".to_string()
|
|
877
|
+
}
|
|
853
878
|
}
|
|
854
879
|
}
|
|
855
880
|
|
package/npm/install.mjs
CHANGED
|
@@ -6,36 +6,104 @@ import os from "node:os";
|
|
|
6
6
|
import path from "node:path";
|
|
7
7
|
import { fileURLToPath } from "node:url";
|
|
8
8
|
|
|
9
|
-
const
|
|
9
|
+
const modulePath = fileURLToPath(import.meta.url);
|
|
10
|
+
const __dirname = path.dirname(modulePath);
|
|
10
11
|
const packageRoot = path.resolve(__dirname, "..");
|
|
11
12
|
const require = createRequire(import.meta.url);
|
|
12
13
|
const packageJson = JSON.parse(fs.readFileSync(path.join(packageRoot, "package.json"), "utf8"));
|
|
13
14
|
const DOCTOR_TIMEOUT_MS = 5000;
|
|
14
15
|
const VERSION_SMOKE_TIMEOUT_MS = 5000;
|
|
15
16
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
if (["-h", "--help", "help"].includes(command)) {
|
|
20
|
-
printHelp();
|
|
21
|
-
process.exit(0);
|
|
17
|
+
if (isCliEntrypoint()) {
|
|
18
|
+
main();
|
|
22
19
|
}
|
|
23
20
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
} else if (command === "uninstall") {
|
|
30
|
-
uninstall(args);
|
|
31
|
-
} else {
|
|
32
|
-
console.error(`unknown command: ${command}`);
|
|
21
|
+
function main() {
|
|
22
|
+
const command = process.argv[2] || "install";
|
|
23
|
+
const args = process.argv.slice(3);
|
|
24
|
+
|
|
25
|
+
if (["-h", "--help", "help"].includes(command)) {
|
|
33
26
|
printHelp();
|
|
34
|
-
process.exit(
|
|
27
|
+
process.exit(0);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
try {
|
|
31
|
+
if (command === "install" || command === "update") {
|
|
32
|
+
install(args);
|
|
33
|
+
} else if (command === "doctor") {
|
|
34
|
+
runDoctor(args);
|
|
35
|
+
} else if (command === "uninstall") {
|
|
36
|
+
uninstall(args);
|
|
37
|
+
} else {
|
|
38
|
+
console.error(`unknown command: ${command}`);
|
|
39
|
+
printHelp();
|
|
40
|
+
process.exit(2);
|
|
41
|
+
}
|
|
42
|
+
} catch (error) {
|
|
43
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
44
|
+
process.exit(1);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function isCliEntrypoint() {
|
|
49
|
+
if (!process.argv[1]) {
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
try {
|
|
53
|
+
return fs.realpathSync(process.argv[1]) === fs.realpathSync(modulePath);
|
|
54
|
+
} catch {
|
|
55
|
+
return path.resolve(process.argv[1]) === modulePath;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function doctorSelfCheckVerdict(doctorBody, spawnMeta = {}) {
|
|
60
|
+
if (spawnMeta.error || spawnMeta.signal) {
|
|
61
|
+
return { kind: "advisory", blockers: [] };
|
|
35
62
|
}
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
63
|
+
|
|
64
|
+
let body;
|
|
65
|
+
try {
|
|
66
|
+
body = JSON.parse(doctorBody || "");
|
|
67
|
+
} catch {
|
|
68
|
+
return { kind: "advisory", blockers: [] };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const blockers = [];
|
|
72
|
+
// Doctor JSON source: crates/team-agent/src/cli/mod.rs:2739-2764.
|
|
73
|
+
if (body?.tmux?.installed === false) {
|
|
74
|
+
blockers.push("tmux not installed");
|
|
75
|
+
}
|
|
76
|
+
if (!body?.mcp?.server_command) {
|
|
77
|
+
blockers.push("MCP server command missing");
|
|
78
|
+
}
|
|
79
|
+
const profileSmokeStatus = body?.profile_smoke?.status;
|
|
80
|
+
const profileSmokeNonBlocking =
|
|
81
|
+
profileSmokeStatus === "legacy_team_invalid" || profileSmokeStatus === "not_required";
|
|
82
|
+
if (
|
|
83
|
+
body?.error === "profile_smoke_failed" ||
|
|
84
|
+
(body?.profile_smoke?.ok === false && !profileSmokeNonBlocking)
|
|
85
|
+
) {
|
|
86
|
+
blockers.push("profile smoke failed");
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (blockers.length > 0) {
|
|
90
|
+
return { kind: "blockers", blockers };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const noContext =
|
|
94
|
+
body?.ok === false && body?.error === "workspace has no Team Agent spec or runtime context";
|
|
95
|
+
if (body?.ok === true || noContext) {
|
|
96
|
+
return { kind: "ok", blockers: [] };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return { kind: "advisory", blockers: [] };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function doctorSelfCheckLine(verdict) {
|
|
103
|
+
if (verdict.kind === "blockers") {
|
|
104
|
+
return `doctor: found blockers (${verdict.blockers.join("; ")}); run team-agent doctor in your project for details`;
|
|
105
|
+
}
|
|
106
|
+
return "doctor: ok (run team-agent doctor inside a team workspace for a full report)";
|
|
39
107
|
}
|
|
40
108
|
|
|
41
109
|
function printHelp() {
|
|
@@ -116,11 +184,8 @@ function install(argv) {
|
|
|
116
184
|
encoding: "utf8",
|
|
117
185
|
timeout: DOCTOR_TIMEOUT_MS,
|
|
118
186
|
});
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
} else {
|
|
122
|
-
console.log("doctor: has blockers; run `team-agent doctor` after updating PATH");
|
|
123
|
-
}
|
|
187
|
+
const verdict = doctorSelfCheckVerdict(doctor.stdout, doctor);
|
|
188
|
+
console.log(doctorSelfCheckLine(verdict));
|
|
124
189
|
} finally {
|
|
125
190
|
fs.rmSync(doctorWorkspace, { recursive: true, force: true });
|
|
126
191
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@team-agent/installer",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.25",
|
|
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.25",
|
|
24
|
+
"@team-agent/cli-darwin-x64": "0.3.25",
|
|
25
|
+
"@team-agent/cli-linux-x64": "0.3.25"
|
|
26
26
|
},
|
|
27
27
|
"scripts": {
|
|
28
28
|
"postinstall": "node npm/bincheck.mjs",
|