@team-agent/installer 0.3.22 → 0.3.24

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.
@@ -298,6 +298,11 @@ pub fn project_top_level_view(state: &Value, team_key: &str) -> Value {
298
298
  team_entry_from_state(state, team_key).and_then(Value::as_object).cloned().unwrap_or_default();
299
299
  let mut p = entry_obj.clone(); // projection = deepcopy(entry)
300
300
  // setdefault session_name/team_dir ← entry.get(...)(缺则插,值可为 null)。
301
+ // **0.3.24 excision** (U1-A real-machine RED v2): the ff38ab9 root-state
302
+ // `or_else` fallback was reverted along with the wave-2 U1-A drift fallback,
303
+ // since this projection change had no other consumer. v0.3.25 will re-introduce
304
+ // this together with the writer-shape + rediscover-writer fix — see
305
+ // `.team/artifacts/u1-a-realmachine-v2-fix-or-excise.md`.
301
306
  if !p.contains_key("session_name") {
302
307
  p.insert("session_name".to_string(), entry_obj.get("session_name").cloned().unwrap_or(Value::Null));
303
308
  }
@@ -526,6 +526,52 @@ CaptureMissingToken, not the static CaptureContainsToken false-positive"
526
526
  );
527
527
  }
528
528
 
529
+ #[test]
530
+ fn inject_waits_for_token_visibility_before_enter() {
531
+ let text = "hello [team-agent-token:e31]".to_string();
532
+ let marker_visible = format!("{text}\n");
533
+ let (be, rec) = backend_with(
534
+ MockResp::Out(ok("")),
535
+ vec![
536
+ MockResp::Out(ok("")), // set-buffer
537
+ MockResp::Out(ok("")), // paste-buffer
538
+ MockResp::Out(ok("")), // delete-buffer
539
+ MockResp::Out(ok("")), // pasted-content prompt poll 1
540
+ MockResp::Out(ok("")), // pasted-content prompt poll 2
541
+ MockResp::Out(ok("")), // pasted-content prompt poll 3
542
+ MockResp::Out(ok("")), // pasted-content prompt poll 4
543
+ MockResp::Out(ok("")), // pasted-content prompt poll 5
544
+ MockResp::Out(ok("")), // token gate: not visible yet
545
+ MockResp::Out(ok("")), // token gate: still not visible
546
+ MockResp::Out(ok(&marker_visible)), // token gate: visible, Enter may fire
547
+ MockResp::Out(ok("")), // send-keys Enter
548
+ ],
549
+ );
550
+
551
+ let report = be
552
+ .inject(&Target::Pane(PaneId::new("%7")), &InjectPayload::Text(text), Key::Enter, true)
553
+ .expect("inject");
554
+ let calls = rec.lock().unwrap().clone();
555
+ let submit_index = calls
556
+ .iter()
557
+ .position(|argv| argv.get(1).map(String::as_str) == Some("send-keys"))
558
+ .expect("inject must eventually send Enter");
559
+ let captures_before_submit = calls[..submit_index]
560
+ .iter()
561
+ .filter(|argv| argv.get(1).map(String::as_str) == Some("capture-pane"))
562
+ .count();
563
+
564
+ assert!(
565
+ captures_before_submit >= super::PASTED_CONTENT_APPEAR_POLLS as usize + 3,
566
+ "Enter must wait until the pasted token is visible; calls={calls:?}"
567
+ );
568
+ assert_eq!(report.inject_verification, InjectVerification::CaptureContainsToken);
569
+ assert_eq!(
570
+ report.submit_verification,
571
+ SubmitVerification::EnterSentWithoutPlaceholderCheck
572
+ );
573
+ }
574
+
529
575
  #[test]
530
576
  fn send_keys_cancel_mode_queries_mode_and_dispatches_cancel_argv() {
531
577
  let (be, rec) = backend_with(
@@ -845,16 +845,28 @@ fn token_visible_in_capture(
845
845
  }
846
846
  }
847
847
 
848
- /// U1 #7: capture the pane just before submit and report whether the just-pasted
849
- /// token marker is actually visible. `Ok(None)` for non-token payloads (nothing to
850
- /// check). `Ok(Some(false))` means the paste silently dropped a false-positive that
851
- /// the static `inject_verification_for_payload` would have reported as success.
848
+ /// U1 #7 / E31: wait briefly for the just-pasted token marker before submitting.
849
+ /// `Ok(None)` for non-token payloads (nothing to check). `Ok(Some(false))` means the
850
+ /// paste did not become visible before the Python-parity fallback delay.
852
851
  fn pre_submit_token_visible(
853
852
  backend: &TmuxBackend,
854
853
  target: &Target,
855
854
  payload: &InjectPayload,
856
855
  ) -> Result<Option<bool>, TransportError> {
857
- token_visible_in_capture(backend, target, payload)
856
+ if payload_token_marker(payload).is_none() {
857
+ return Ok(None);
858
+ }
859
+ for attempt in 0..PASTED_CONTENT_APPEAR_POLLS {
860
+ if let Some(true) = token_visible_in_capture(backend, target, payload)? {
861
+ return Ok(Some(true));
862
+ }
863
+ if attempt + 1 < PASTED_CONTENT_APPEAR_POLLS {
864
+ std::thread::sleep(Duration::from_millis(25));
865
+ }
866
+ }
867
+ // Python waits 250ms between paste-buffer and Enter to let bracketed paste settle.
868
+ std::thread::sleep(Duration::from_millis(250));
869
+ Ok(Some(false))
858
870
  }
859
871
 
860
872
  const TOKEN_POST_SUBMIT_READBACK_POLLS: u32 = 5;
@@ -37,6 +37,18 @@ struct OfflineState {
37
37
  inject_targets: Vec<Target>,
38
38
  inject_payloads: Vec<String>,
39
39
  tmux_endpoint: Option<String>,
40
+ /// U1-B contract: when set, `list_targets()` returns `TransportError::MuxUnavailable`
41
+ /// — used to model tmux server jitter (subprocess-fork-failed level). The whole
42
+ /// resolve must DEFER, not silently coerce to an empty vec.
43
+ list_targets_error: Option<String>,
44
+ /// U1-C / general: pre-staged `capture()` payload, keyed by target stringification.
45
+ /// `Target::Pane(p)` keys as `p.as_str()`; `Target::SessionWindow{session,window}`
46
+ /// keys as `format!("{session}:{window}")`. A miss returns empty text (current
47
+ /// default behaviour).
48
+ capture_text: BTreeMap<String, String>,
49
+ /// U1-C Tail-peek contract: every `capture()` call records its `CaptureRange`,
50
+ /// in order, so a test can prove the delivery peek site narrowed Full → Tail(80).
51
+ capture_ranges: Vec<CaptureRange>,
40
52
  }
41
53
 
42
54
  impl Default for OfflineState {
@@ -57,6 +69,9 @@ impl Default for OfflineState {
57
69
  inject_targets: Vec::new(),
58
70
  inject_payloads: Vec::new(),
59
71
  tmux_endpoint: None,
72
+ list_targets_error: None,
73
+ capture_text: BTreeMap::new(),
74
+ capture_ranges: Vec::new(),
60
75
  }
61
76
  }
62
77
  }
@@ -127,6 +142,40 @@ impl OfflineTransport {
127
142
  self
128
143
  }
129
144
 
145
+ /// U1-B: stage a `list_targets()` error to model tmux server jitter. While set,
146
+ /// every call returns `Err(TransportError::MuxUnavailable{detail})`. Pass an
147
+ /// empty string to clear via re-builder if needed.
148
+ pub fn with_list_targets_error(self, detail: impl Into<String>) -> Self {
149
+ self.with_state(|state| state.list_targets_error = Some(detail.into()));
150
+ self
151
+ }
152
+
153
+ /// Pre-stage `capture()` output for a `Target::Pane(pane_id)` key.
154
+ pub fn with_capture_for_pane(
155
+ self,
156
+ pane_id: impl Into<String>,
157
+ text: impl Into<String>,
158
+ ) -> Self {
159
+ self.with_state(|state| {
160
+ state.capture_text.insert(pane_id.into(), text.into());
161
+ });
162
+ self
163
+ }
164
+
165
+ /// Pre-stage `capture()` output for a `Target::SessionWindow{session,window}` key.
166
+ pub fn with_capture_for_session_window(
167
+ self,
168
+ session: impl Into<String>,
169
+ window: impl Into<String>,
170
+ text: impl Into<String>,
171
+ ) -> Self {
172
+ let key = format!("{}:{}", session.into(), window.into());
173
+ self.with_state(|state| {
174
+ state.capture_text.insert(key, text.into());
175
+ });
176
+ self
177
+ }
178
+
130
179
  pub fn calls(&self) -> Vec<&'static str> {
131
180
  self.with_state(|state| state.calls.clone())
132
181
  }
@@ -163,6 +212,12 @@ impl OfflineTransport {
163
212
  self.with_state(|state| state.inject_payloads.clone())
164
213
  }
165
214
 
215
+ /// All `capture()` ranges observed, in call order. Used by U1-C Tail-peek
216
+ /// contracts to prove the delivery peek site requested Tail rather than Full.
217
+ pub fn capture_ranges(&self) -> Vec<CaptureRange> {
218
+ self.with_state(|state| state.capture_ranges.clone())
219
+ }
220
+
166
221
  fn record(&self, call: &'static str) {
167
222
  self.with_state(|state| state.calls.push(call));
168
223
  }
@@ -294,11 +349,21 @@ impl Transport for OfflineTransport {
294
349
 
295
350
  fn capture(
296
351
  &self,
297
- _target: &Target,
352
+ target: &Target,
298
353
  range: CaptureRange,
299
354
  ) -> Result<CapturedText, TransportError> {
300
- self.record("capture");
301
- Ok(CapturedText { text: String::new(), range })
355
+ let key = match target {
356
+ Target::Pane(p) => p.as_str().to_string(),
357
+ Target::SessionWindow { session, window } => {
358
+ format!("{}:{}", session.as_str(), window.as_str())
359
+ }
360
+ };
361
+ let text = self.with_state(|state| {
362
+ state.calls.push("capture");
363
+ state.capture_ranges.push(range);
364
+ state.capture_text.get(&key).cloned().unwrap_or_default()
365
+ });
366
+ Ok(CapturedText { text, range })
302
367
  }
303
368
 
304
369
  fn query(
@@ -329,10 +394,18 @@ impl Transport for OfflineTransport {
329
394
  }
330
395
 
331
396
  fn list_targets(&self) -> Result<Vec<PaneInfo>, TransportError> {
332
- Ok(self.with_state(|state| {
397
+ // U1-B: when `with_list_targets_error` is set, return a real Err so the
398
+ // caller sees server jitter rather than a coerced empty vec.
399
+ if let Some(detail) = self.with_state(|state| {
333
400
  state.calls.push("list_targets");
334
- state.targets.clone()
335
- }))
401
+ state.list_targets_error.clone()
402
+ }) {
403
+ return Err(TransportError::MuxUnavailable {
404
+ backend: BackendKind::Tmux,
405
+ detail,
406
+ });
407
+ }
408
+ Ok(self.with_state(|state| state.targets.clone()))
336
409
  }
337
410
 
338
411
  fn has_session(&self, _session: &SessionName) -> Result<bool, TransportError> {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@team-agent/installer",
3
- "version": "0.3.22",
3
+ "version": "0.3.24",
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.22",
24
- "@team-agent/cli-darwin-x64": "0.3.22",
25
- "@team-agent/cli-linux-x64": "0.3.22"
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"
26
26
  },
27
27
  "scripts": {
28
28
  "postinstall": "node npm/bincheck.mjs",
@@ -158,6 +158,7 @@ For diagnosis, run `team-agent profile show deepseek --workspace . --json`; neve
158
158
  - Quick-start generated files stay inside the selected team directory, for example `.team/current/` or `.team/alpha/`; do not create or expect root `team.spec.yaml` or `team_state.md`.
159
159
  - Use `team-agent quick-start ./roles --team-id alpha` to create a second generated team under `.team/alpha/`, or pass an existing team directory directly such as `team-agent quick-start .team/alpha`.
160
160
  - `quick-start` is for fresh startup from role docs. If stored worker context already exists for that runtime session, follow the returned `team-agent restart . --team <session>` action to resume it. Use `--fresh` only when the user explicitly accepts new blank worker contexts.
161
+ - If the user explicitly asks a worker to create or operate a nested child team, first read `references/team-in-team.md`. Child teams must use an independent child workspace, never the parent `.team/current`.
161
162
  - `team-agent send --watch-result coder "Do the bounded task"` sends a direct worker message, returns after delivery, and lets the coordinator collect/report completion asynchronously.
162
163
  - After `send --watch-result` succeeds, do not run `sleep`, `status`, `inbox`, or `collect` polling loops unless the user explicitly asks for diagnosis; the coordinator will notify the leader when the result arrives.
163
164
  - `team-agent send --task task_initial "Start"` routes by task.
@@ -214,7 +215,7 @@ Removing a worker at runtime is the symmetric `team-agent remove-agent <agent> -
214
215
 
215
216
  ## Worker Protocol
216
217
 
217
- Workers do not run nested Team Agent teams. Workers only provide the target and content for progress, and a short completion summary at the end:
218
+ Workers normally do not run nested Team Agent teams. When the user or leader explicitly asks for a child team, follow `references/team-in-team.md`; otherwise workers only provide the target and content for progress, and a short completion summary at the end:
218
219
 
219
220
  ```text
220
221
  team_orchestrator.send_message(to="leader", content="short progress or blocker")
@@ -0,0 +1,127 @@
1
+ # Team-In-Team Reference
2
+
3
+ This reference is for explicitly requested nested Team Agent teams. Use it only when the user or leader asks a worker to create or operate a child team. For normal work, a worker should stay inside its parent team and report through `team_orchestrator`.
4
+
5
+ ## Safety Rules
6
+
7
+ - Use the Team Agent framework skill and `team-agent` CLI. Do not use a provider's built-in agent-team feature when the user asked for Team Agent.
8
+ - Never create a child team in the parent team's `.team/current` directory.
9
+ - Never edit, delete, or reuse files under the parent team's `.team/current`.
10
+ - Never shut down a team just because `.team/current` is already occupied. In a nested setup, that is probably the parent team; shutting it down can kill the main node.
11
+ - A child team needs its own workspace directory. The role-doc directory is not the child workspace.
12
+ - MCP tools are team-scoped. A worker must not try to widen `team_orchestrator.send_message` to another team.
13
+
14
+ ## Child Workspace
15
+
16
+ Pick an independent directory before writing `TEAM.md` or agent role docs. A safe default is a child workspace under the parent workspace, but outside the parent `.team/current`:
17
+
18
+ ```bash
19
+ PARENT_WORKSPACE="$PWD"
20
+ case "$PARENT_WORKSPACE" in
21
+ */.team/current|*/.team/current/*)
22
+ echo "Refusing to create a child team from parent .team/current; cd to the parent workspace root first." >&2
23
+ exit 1
24
+ ;;
25
+ esac
26
+ CHILD_TEAM="child-review-team"
27
+ CHILD_WORKSPACE="$PARENT_WORKSPACE/.team/children/review-$(date +%Y%m%d%H%M%S)"
28
+ mkdir -p "$CHILD_WORKSPACE"
29
+ cd "$CHILD_WORKSPACE"
30
+ mkdir -p .team/current/agents
31
+ ```
32
+
33
+ Then create the child team's files inside the child workspace:
34
+
35
+ ```bash
36
+ cat > .team/current/TEAM.md <<'EOF'
37
+ ---
38
+ name: child-review-team
39
+ objective: Run a bounded child-team task and report to the parent worker.
40
+ dangerous_auto_approve: false
41
+ fast: false
42
+ ---
43
+
44
+ Child team config only.
45
+ EOF
46
+
47
+ cat > .team/current/agents/reviewer.md <<'EOF'
48
+ ---
49
+ name: reviewer
50
+ role: Independent Reviewer
51
+ provider: codex
52
+ tools:
53
+ - fs_read
54
+ - fs_list
55
+ - mcp_team
56
+ ---
57
+
58
+ Review only the files and instructions provided by the child-team leader.
59
+ Report findings with team_orchestrator.report_result exactly once.
60
+ EOF
61
+
62
+ team-agent quick-start .team/current
63
+ ```
64
+
65
+ If `quick-start` reports that `current` or a tmux session already exists, do not run `shutdown`. Move to a new child workspace or change the child team name.
66
+
67
+ ## Parent Protection
68
+
69
+ The parent worker is also the child team's leader. Keep the two scopes separate:
70
+
71
+ - Parent team: the worker talks upward with its parent `team_orchestrator` MCP tools.
72
+ - Child team: the same worker talks downward with `team-agent send --workspace "$CHILD_WORKSPACE" --team "$CHILD_TEAM" ...`.
73
+
74
+ Do not run child lifecycle commands against the parent workspace. Always pass the child workspace and team when operating the child team from the parent worker.
75
+
76
+ ## Parent-Child Messaging
77
+
78
+ Use explicit two-hop routing.
79
+
80
+ Parent leader to child worker:
81
+
82
+ 1. Parent leader sends the child-task instruction to the parent worker that owns the child team.
83
+ 2. Parent worker sends into the child team:
84
+
85
+ ```bash
86
+ team-agent send reviewer "Review this patch and report findings" \
87
+ --workspace "$CHILD_WORKSPACE" \
88
+ --team "$CHILD_TEAM" \
89
+ --watch-result
90
+ ```
91
+
92
+ Child worker to parent leader:
93
+
94
+ 1. Child worker reports to its child leader:
95
+
96
+ ```text
97
+ team_orchestrator.send_message(to="leader", content="short progress")
98
+ team_orchestrator.report_result(summary="short completion", status="success")
99
+ ```
100
+
101
+ 2. The parent worker, acting as child leader, relays the relevant summary upward to the parent leader with its parent-team MCP tools.
102
+
103
+ Do not ask a child worker to send directly to a parent-team agent id. Current MCP scope is team-local and should refuse out-of-scope peers. Use CLI `--workspace` and `--team` only from the appropriate leader/operator side when crossing team boundaries.
104
+
105
+ ## Clean-Context Roles
106
+
107
+ Asking for a clean-context role is a normal feature request when the user wants a blind reviewer, a fresh second opinion, or a role that forgets earlier conversation. It is not the same as failure recovery.
108
+
109
+ For an existing worker that should restart with a blank provider context, the leader can use:
110
+
111
+ ```bash
112
+ team-agent reset-agent "$AGENT_ID" --workspace "$WORKSPACE" --team "$TEAM" --discard-session --json
113
+ ```
114
+
115
+ or the equivalent restart form:
116
+
117
+ ```bash
118
+ team-agent restart-agent "$AGENT_ID" --workspace "$WORKSPACE" --team "$TEAM" --discard-session --json
119
+ ```
120
+
121
+ Use this only when the user intentionally wants a clean context. For incident recovery, follow `recovery-runbook.md`, where context-preserving repair comes first.
122
+
123
+ ## Framework Gaps To Track
124
+
125
+ - Nested mode needs a way to pin the framework Team Agent skill by absolute path, so providers do not choose their built-in agent-team feature.
126
+ - A child-team worker should be prevented from shutting down the parent team by mistake.
127
+ - Child team creation should avoid defaulting to the fragile parent `current` directory.