@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.
@@ -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 __dirname = path.dirname(fileURLToPath(import.meta.url));
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
- const command = process.argv[2] || "install";
17
- const args = process.argv.slice(3);
18
-
19
- if (["-h", "--help", "help"].includes(command)) {
20
- printHelp();
21
- process.exit(0);
17
+ if (isCliEntrypoint()) {
18
+ main();
22
19
  }
23
20
 
24
- try {
25
- if (command === "install" || command === "update") {
26
- install(args);
27
- } else if (command === "doctor") {
28
- runDoctor(args);
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(2);
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
- } catch (error) {
37
- console.error(error instanceof Error ? error.message : String(error));
38
- process.exit(1);
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
- if (doctor.status === 0) {
120
- console.log("doctor: ok");
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.24",
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",
24
- "@team-agent/cli-darwin-x64": "0.3.24",
25
- "@team-agent/cli-linux-x64": "0.3.24"
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",