@team-agent/installer 0.5.118 → 0.5.120

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 (72) hide show
  1. package/Cargo.lock +1 -1
  2. package/Cargo.toml +1 -1
  3. package/README.md +46 -1
  4. package/README.zh.md +46 -1
  5. package/crates/team-agent/src/cli/adapters.rs +11 -0
  6. package/crates/team-agent/src/cli/arguments.rs +401 -0
  7. package/crates/team-agent/src/cli/emit.rs +241 -142
  8. package/crates/team-agent/src/cli/intent/runtime.rs +1139 -0
  9. package/crates/team-agent/src/cli/intent/runtime_tests.rs +584 -0
  10. package/crates/team-agent/src/cli/intent/tests.rs +654 -0
  11. package/crates/team-agent/src/cli/intent.rs +642 -0
  12. package/crates/team-agent/src/cli/mod.rs +49 -5
  13. package/crates/team-agent/src/cli/named_address.rs +10 -1
  14. package/crates/team-agent/src/cli/spec.rs +22 -10
  15. package/crates/team-agent/src/cli/status_port/brief.rs +117 -16
  16. package/crates/team-agent/src/cli/tests/base.rs +6 -1
  17. package/crates/team-agent/src/cli/tests/parser_safety.rs +383 -0
  18. package/crates/team-agent/src/cli/tests/run_delegation.rs +63 -19
  19. package/crates/team-agent/src/cli/tests/verb_install_skill.rs +3 -2
  20. package/crates/team-agent/src/cli/tests/verb_profile.rs +3 -1
  21. package/crates/team-agent/src/contract_runtime/backend.rs +1 -1
  22. package/crates/team-agent/src/db/message_store.rs +10 -3
  23. package/crates/team-agent/src/lifecycle/launch/quick_start.rs +166 -77
  24. package/crates/team-agent/src/lifecycle/launch.rs +28 -0
  25. package/crates/team-agent/src/lifecycle/restart.rs +17 -0
  26. package/crates/team-agent/src/lifecycle/tests/acceptance_b_batch_red.rs +1 -3
  27. package/crates/team-agent/src/lifecycle/tests/behavioral_diff_264_red.rs +5 -15
  28. package/crates/team-agent/src/lifecycle/tests/claude_compatible_config_red.rs +2 -3
  29. package/crates/team-agent/src/lifecycle/tests/claude_profile_launch_red.rs +2 -3
  30. package/crates/team-agent/src/lifecycle/tests/cli_argv_routing_red.rs +10 -2
  31. package/crates/team-agent/src/lifecycle/tests/clone_agent_preserves_source_tools.rs +8 -1
  32. package/crates/team-agent/src/lifecycle/tests/clone_fork_copilot_perms_red.rs +3 -2
  33. package/crates/team-agent/src/lifecycle/tests/communication_mode_runtime_contract_red.rs +6 -9
  34. package/crates/team-agent/src/lifecycle/tests/copilot_provider_red.rs +7 -5
  35. package/crates/team-agent/src/lifecycle/tests/cursor_mcp_overlay.rs +4 -2
  36. package/crates/team-agent/src/lifecycle/tests/cursor_require_explicit_model_red.rs +5 -2
  37. package/crates/team-agent/src/lifecycle/tests/cursor_restart_resume_red.rs +4 -3
  38. package/crates/team-agent/src/lifecycle/tests/grok_effort_argv_red.rs +2 -4
  39. package/crates/team-agent/src/lifecycle/tests/grok_mcp_overlay_red.rs +3 -4
  40. package/crates/team-agent/src/lifecycle/tests/grok_require_explicit_model_red.rs +5 -4
  41. package/crates/team-agent/src/lifecycle/tests/grok_restart_resume_red.rs +2 -4
  42. package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +78 -36
  43. package/crates/team-agent/src/lifecycle/tests/phase_b_contracts.rs +8 -4
  44. package/crates/team-agent/src/lifecycle/tests/phase_golden.rs +6 -12
  45. package/crates/team-agent/src/lifecycle/tests/pi_fork_session_inheritance_red.rs +4 -0
  46. package/crates/team-agent/src/lifecycle/tests/startup_latency_contract.rs +1 -3
  47. package/crates/team-agent/src/lifecycle/tests/status_credential_redaction_contract.rs +7 -0
  48. package/crates/team-agent/src/lifecycle/tests/team_in_team_identity_scope_red.rs +1 -3
  49. package/crates/team-agent/src/lifecycle/tests/verify_rs031_window_consistency_red.rs +2 -1
  50. package/crates/team-agent/src/lifecycle/types.rs +10 -0
  51. package/crates/team-agent/src/messaging/delivery.rs +132 -39
  52. package/crates/team-agent/src/messaging/delivery_consumption_tests.rs +223 -0
  53. package/crates/team-agent/src/messaging/enter_rescue.rs +182 -154
  54. package/crates/team-agent/src/messaging/enter_rescue_tests.rs +344 -277
  55. package/crates/team-agent/src/messaging/mod.rs +1 -1
  56. package/crates/team-agent/src/tmux_backend/enter_rescue.rs +325 -0
  57. package/crates/team-agent/src/tmux_backend/enter_rescue_tests.rs +432 -0
  58. package/crates/team-agent/src/tmux_backend/native_input.rs +19 -0
  59. package/crates/team-agent/src/tmux_backend/tests.rs +37 -0
  60. package/crates/team-agent/src/tmux_backend.rs +35 -1
  61. package/crates/team-agent/src/transport/enter_rescue.rs +166 -0
  62. package/crates/team-agent/src/transport/native_input.rs +5 -1
  63. package/crates/team-agent/src/transport/test_support.rs +25 -0
  64. package/crates/team-agent/src/transport.rs +34 -0
  65. package/package.json +4 -4
  66. package/skills/team-agent/SKILL.md +71 -54
  67. package/skills/team-agent/command-coverage.json +283 -13
  68. package/skills/team-agent/docs/reference/team-agent-operator.md +220 -0
  69. package/skills/team-agent/references/bug-as-artifact-flow.md +1 -1
  70. package/skills/team-agent/references/recovery-runbook.md +61 -212
  71. package/skills/team-agent/references/team-agent-operator.md +181 -48
  72. package/skills/team-agent/references/team-in-team.md +47 -80
@@ -64,7 +64,7 @@ use crate::transport::{PaneId, Target, Transport};
64
64
  pub mod activity;
65
65
  pub mod address;
66
66
  pub mod delivery;
67
- mod enter_rescue;
67
+ pub(crate) mod enter_rescue;
68
68
  pub mod helpers;
69
69
  pub mod leader_channel;
70
70
  pub mod leader_receiver;
@@ -0,0 +1,325 @@
1
+ //! Independent watchdog effects. No native-input state-machine evidence is adopted.
2
+
3
+ use std::time::{Duration, Instant};
4
+
5
+ use sha2::{Digest, Sha256};
6
+
7
+ use super::*;
8
+ use crate::lifecycle::pane_input_lock::{acquire_required, PaneInputLockRequest};
9
+ use crate::transport::enter_rescue::{
10
+ EnterRescueContext, EnterRescueOutcome, EnterRescueProof, EnterRescueTarget,
11
+ };
12
+ use crate::transport::native_input::InputFrame;
13
+
14
+ const STABLE: Duration = Duration::from_millis(200);
15
+ const POLL: Duration = Duration::from_millis(50);
16
+ const OBSERVE: Duration = Duration::from_secs(10);
17
+
18
+ fn digest(value: &str) -> String {
19
+ format!("{:x}", Sha256::digest(value.as_bytes()))
20
+ }
21
+ fn marker_count(value: &str, marker: &str) -> usize {
22
+ value
23
+ .lines()
24
+ .map(str::trim)
25
+ .collect::<String>()
26
+ .matches(marker)
27
+ .count()
28
+ }
29
+ fn current_marker_only(context: &EnterRescueContext, frame: &InputFrame) -> bool {
30
+ let composer: String = frame.composer.lines().map(str::trim).collect();
31
+ composer.matches("[team-agent-token:").count() == 1
32
+ && composer.matches(&context.marker()).count() == 1
33
+ }
34
+
35
+ fn no_effect(reason: &'static str, reobserve: bool) -> EnterRescueOutcome {
36
+ EnterRescueOutcome::NoEffect { reason, reobserve }
37
+ }
38
+ fn refusal(reason: &str) -> TransportError {
39
+ TransportError::NativeInputNotReady {
40
+ reason: reason.into(),
41
+ }
42
+ }
43
+
44
+ fn pending(context: &EnterRescueContext, frame: &InputFrame) -> bool {
45
+ let Some(proof) = context.proof.as_ref().filter(|_| context.valid_proof()) else {
46
+ return false;
47
+ };
48
+ if frame.busy
49
+ || frame.identity != proof.ui_identity
50
+ || digest(&frame.footer) != proof.footer_sha256
51
+ {
52
+ return false;
53
+ }
54
+ let marker = context.marker();
55
+ if marker_count(&frame.transcript, &marker) != 0 {
56
+ return false;
57
+ }
58
+ (proof.visible_marker && current_marker_only(context, frame))
59
+ || proof.owned_capsule.as_ref().is_some_and(|capsule| {
60
+ capsule.valid()
61
+ && frame.capsule.as_ref() == Some(&capsule.label)
62
+ && frame.composer.trim() == capsule.label
63
+ && !frame
64
+ .transcript
65
+ .lines()
66
+ .any(|line| line.trim() == capsule.label)
67
+ })
68
+ }
69
+
70
+ fn accepted(context: &EnterRescueContext, frame: &InputFrame) -> bool {
71
+ let Some(proof) = context.proof.as_ref().filter(|_| context.valid_proof()) else {
72
+ return false;
73
+ };
74
+ // Invisible whitespace is not an empty composer receipt.
75
+ if !frame.composer.is_empty() || frame.identity != proof.ui_identity {
76
+ return false;
77
+ }
78
+ if proof.visible_marker
79
+ && proof.transcript_marker_absent
80
+ && marker_count(&frame.transcript, &context.marker()) == 1
81
+ {
82
+ return true;
83
+ }
84
+ proof.owned_capsule.as_ref().is_some_and(|capsule| {
85
+ capsule.valid()
86
+ && frame
87
+ .transcript
88
+ .lines()
89
+ .filter(|line| line.trim() == capsule.label)
90
+ .count()
91
+ == 1
92
+ })
93
+ }
94
+
95
+ fn same_editor(left: &InputFrame, right: &InputFrame) -> bool {
96
+ left.composer == right.composer
97
+ && left.identity == right.identity
98
+ && left.footer == right.footer
99
+ && left.capsule == right.capsule
100
+ && left.busy == right.busy
101
+ }
102
+
103
+ impl TmuxBackend {
104
+ fn rescue_binding(
105
+ &self,
106
+ selected: &EnterRescueTarget,
107
+ allow_unset_nonce: bool,
108
+ ) -> Result<EnterRescueTarget, TransportError> {
109
+ let argv = self.tmux_argv(&[
110
+ "tmux".into(), "display-message".into(), "-p".into(), "-t".into(), selected.pane_id.clone(),
111
+ "#{socket_path}|#{pane_id}|#{pane_pid}|#{session_name}|#{window_name}|#{@team_agent_pane_binding_nonce}|#{pane_in_mode}|#{pane_height}".into(),
112
+ ]);
113
+ let output = self.runner.run(&argv)?;
114
+ if !output.success || output.stdout.lines().count() != 1 {
115
+ return Err(refusal("rescue_binding_query"));
116
+ }
117
+ let fields: Vec<_> = output.stdout.trim_end().split('|').collect();
118
+ if fields.len() != 8
119
+ || !std::path::Path::new(fields[0]).is_absolute()
120
+ || fields[1] != selected.pane_id
121
+ || fields[3].is_empty()
122
+ || fields[4].is_empty()
123
+ || (fields[5].is_empty() && !allow_unset_nonce)
124
+ || fields[6] != "0"
125
+ {
126
+ return Err(refusal("rescue_binding_invalid"));
127
+ }
128
+ let pid = fields[2]
129
+ .parse::<u32>()
130
+ .map_err(|_| refusal("rescue_binding_pid"))?;
131
+ let height = fields[7]
132
+ .parse::<u32>()
133
+ .map_err(|_| refusal("rescue_binding_height"))?;
134
+ if pid <= 1 || height < 10 {
135
+ return Err(refusal("rescue_binding_invalid"));
136
+ }
137
+ let actual = EnterRescueTarget {
138
+ provider: selected.provider,
139
+ endpoint: fields[0].into(),
140
+ pane_id: fields[1].into(),
141
+ pane_pid: pid,
142
+ pane_height: height,
143
+ carrier_birth: team_agent_contract::host::process::process_birth_identity(pid)
144
+ .map_err(|_| refusal("rescue_carrier_birth"))?,
145
+ binding_nonce: fields[5].into(),
146
+ session: fields[3].into(),
147
+ window: fields[4].into(),
148
+ };
149
+ if !actual.same_selection(selected) {
150
+ return Err(refusal("rescue_selection_changed"));
151
+ }
152
+ Ok(actual)
153
+ }
154
+
155
+ fn rescue_frame(
156
+ &self,
157
+ expected: &EnterRescueTarget,
158
+ ) -> Result<Option<InputFrame>, TransportError> {
159
+ if self.rescue_binding(expected, false)? != *expected {
160
+ return Err(refusal("rescue_binding_changed"));
161
+ }
162
+ let frame = match self.observe_native_input(
163
+ &Target::Pane(PaneId::new(&expected.pane_id)),
164
+ expected.provider,
165
+ ) {
166
+ Ok(frame) => frame,
167
+ Err(TransportError::NativeInputNotReady { reason })
168
+ if reason == "native_surface_unknown" =>
169
+ {
170
+ None
171
+ }
172
+ Err(error) => return Err(error),
173
+ };
174
+ if self.rescue_binding(expected, false)? != *expected {
175
+ return Err(refusal("rescue_binding_changed"));
176
+ }
177
+ Ok(frame)
178
+ }
179
+
180
+ /// Same original delivery boundary only. No late snapshot is taken by fire.
181
+ pub(super) fn snapshot_rescue_context(
182
+ &self,
183
+ target: &Target,
184
+ context: &EnterRescueContext,
185
+ rendered_payload: &str,
186
+ ) -> Result<EnterRescueContext, TransportError> {
187
+ let mut result = context.clone();
188
+ if !context.valid_message() || digest(rendered_payload) != context.payload_sha256 {
189
+ return Ok(result);
190
+ }
191
+ let Some(selected) = context.target.as_ref() else {
192
+ return Ok(result);
193
+ };
194
+ if pane_from_target(target).as_str() != selected.pane_id {
195
+ return Ok(result);
196
+ }
197
+ let Some(_lock) = acquire_required(PaneInputLockRequest {
198
+ workspace: self.event_workspace.as_deref(),
199
+ target_key: &selected.pane_id,
200
+ operation: "enter_rescue_snapshot",
201
+ }) else {
202
+ return Ok(result);
203
+ };
204
+ let initial = match self.rescue_binding(selected, true) {
205
+ Ok(value) => value,
206
+ Err(_) => return Ok(result),
207
+ };
208
+ if (!selected.endpoint.is_empty() && initial.endpoint != selected.endpoint)
209
+ || (!selected.carrier_birth.is_empty()
210
+ && initial.carrier_birth != selected.carrier_birth)
211
+ || (!selected.binding_nonce.is_empty()
212
+ && initial.binding_nonce != selected.binding_nonce)
213
+ {
214
+ return Ok(result);
215
+ }
216
+ let frame = match self.observe_native_input(target, selected.provider) {
217
+ Ok(Some(frame)) => frame,
218
+ _ => return Ok(result),
219
+ };
220
+ if !current_marker_only(context, &frame)
221
+ || marker_count(&frame.transcript, &context.marker()) != 0
222
+ {
223
+ return Ok(result);
224
+ }
225
+ if self.rescue_binding(selected, true)? != initial {
226
+ return Ok(result);
227
+ }
228
+ let mut expected = initial;
229
+ if expected.binding_nonce.is_empty() {
230
+ let Some(workspace) = self.event_workspace.as_deref() else {
231
+ return Ok(result);
232
+ };
233
+ let pane = PaneId::new(&expected.pane_id);
234
+ let proposed = next_pane_binding_nonce(workspace, &expected.endpoint, &pane);
235
+ expected.binding_nonce = self.set_pane_binding_nonce_if_unset(&pane, &proposed)?;
236
+ }
237
+ if self.rescue_binding(selected, false)? != expected {
238
+ return Ok(result);
239
+ }
240
+ result.target = Some(expected);
241
+ result.proof = Some(EnterRescueProof {
242
+ ui_identity: frame.identity,
243
+ footer_sha256: digest(&frame.footer),
244
+ visible_marker: true,
245
+ transcript_marker_absent: true,
246
+ owned_capsule: None,
247
+ });
248
+ Ok(result)
249
+ }
250
+
251
+ pub(super) fn rescue_guarded_enter(
252
+ &self,
253
+ context: &EnterRescueContext,
254
+ authorize: &dyn Fn() -> bool,
255
+ observer: Option<&dyn SubmitObserver>,
256
+ ) -> Result<EnterRescueOutcome, TransportError> {
257
+ let Some(expected) = context.target.as_ref().filter(|target| target.bound()) else {
258
+ return Ok(no_effect("binding_missing", false));
259
+ };
260
+ if !context.valid_message() || !context.valid_proof() {
261
+ return Ok(no_effect("proof_missing", false));
262
+ }
263
+ let Some(_lock) = acquire_required(PaneInputLockRequest {
264
+ workspace: self.event_workspace.as_deref(),
265
+ target_key: &expected.pane_id,
266
+ operation: "enter_rescue",
267
+ }) else {
268
+ return Ok(no_effect("input_lock", true));
269
+ };
270
+ let Some(first) = self.rescue_frame(expected)? else {
271
+ return Ok(no_effect("surface_unknown", true));
272
+ };
273
+ if accepted(context, &first) {
274
+ return Ok(if authorize() {
275
+ EnterRescueOutcome::Consumed
276
+ } else {
277
+ no_effect("canonical_changed", false)
278
+ });
279
+ }
280
+ if first.busy {
281
+ return Ok(no_effect("busy", true));
282
+ }
283
+ if !pending(context, &first) {
284
+ return Ok(no_effect("residue_mismatch", false));
285
+ }
286
+ std::thread::sleep(STABLE);
287
+ let Some(stable) = self.rescue_frame(expected)? else {
288
+ return Ok(no_effect("surface_unknown", true));
289
+ };
290
+ if !same_editor(&first, &stable) || !pending(context, &stable) {
291
+ return Ok(no_effect("composer_changed", false));
292
+ }
293
+ // Last fresh current-message/busy/binding checks are under the same lock.
294
+ let Some(last) = self.rescue_frame(expected)? else {
295
+ return Ok(no_effect("surface_unknown", true));
296
+ };
297
+ if !same_editor(&stable, &last) || !pending(context, &last) {
298
+ return Ok(no_effect("composer_changed", false));
299
+ }
300
+ if !authorize() {
301
+ return Ok(no_effect("canonical_changed", false));
302
+ }
303
+ self.run_inject_stage(
304
+ &tmux_send_submit_argv(&PaneId::new(&expected.pane_id), Key::Enter),
305
+ InjectStage::Submit,
306
+ )?;
307
+ notify_submit_observer(observer);
308
+ // Any failure after the key is uncertain, not a retryable no-effect.
309
+ let start = Instant::now();
310
+ while start.elapsed() < OBSERVE {
311
+ match self.rescue_frame(expected) {
312
+ Ok(Some(frame)) if accepted(context, &frame) => {
313
+ return Ok(EnterRescueOutcome::Entered { accepted: true })
314
+ }
315
+ Err(_) => break,
316
+ _ => std::thread::sleep(POLL),
317
+ }
318
+ }
319
+ Ok(EnterRescueOutcome::Entered { accepted: false })
320
+ }
321
+ }
322
+
323
+ #[cfg(test)]
324
+ #[path = "enter_rescue_tests.rs"]
325
+ mod tests;