@team-agent/installer 0.5.101 → 0.5.102

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 (42) hide show
  1. package/Cargo.lock +1 -1
  2. package/Cargo.toml +1 -1
  3. package/crates/team-agent/src/cli/tests/main_preserved.rs +0 -74
  4. package/crates/team-agent/src/compiler/tests.rs +20 -23
  5. package/crates/team-agent/src/compiler.rs +49 -65
  6. package/crates/team-agent/src/coordinator/tests/s3_bounded.rs +16 -48
  7. package/crates/team-agent/src/leader/start.rs +1 -14
  8. package/crates/team-agent/src/lifecycle/launch/spawn.rs +1 -1
  9. package/crates/team-agent/src/lifecycle/restart/common.rs +1 -1
  10. package/crates/team-agent/src/lifecycle/tests/host_cotenant_death_p0_contract.rs +0 -28
  11. package/crates/team-agent/src/lifecycle/tests/pi_compiler_red.rs +11 -9
  12. package/crates/team-agent/src/lifecycle/tests/pi_dual_entry_red.rs +8 -401
  13. package/crates/team-agent/src/lifecycle/tests/pi_executable_mcp_red.rs +2 -2
  14. package/crates/team-agent/src/lifecycle/worker_command_context.rs +22 -8
  15. package/crates/team-agent/src/mcp_server/helpers.rs +0 -16
  16. package/crates/team-agent/src/mcp_server/lifecycle_tools/mod.rs +2 -7
  17. package/crates/team-agent/src/mcp_server/lifecycle_tools/state_status.rs +1 -127
  18. package/crates/team-agent/src/mcp_server/mod.rs +7 -12
  19. package/crates/team-agent/src/mcp_server/tests/scoped.rs +1 -132
  20. package/crates/team-agent/src/mcp_server/tests/send.rs +1 -93
  21. package/crates/team-agent/src/mcp_server/tests/tools.rs +0 -61
  22. package/crates/team-agent/src/mcp_server/tests/wire.rs +6 -51
  23. package/crates/team-agent/src/mcp_server/tests.rs +4 -200
  24. package/crates/team-agent/src/mcp_server/tools.rs +5 -396
  25. package/crates/team-agent/src/mcp_server/types.rs +3 -36
  26. package/crates/team-agent/src/mcp_server/wire.rs +2 -223
  27. package/crates/team-agent/src/messaging/leader_receiver.rs +1 -1
  28. package/crates/team-agent/src/messaging/mod.rs +2 -2
  29. package/crates/team-agent/src/messaging/scheduler.rs +2 -205
  30. package/crates/team-agent/src/messaging/tests/basic.rs +0 -31
  31. package/crates/team-agent/src/messaging/tests/main_preserved.rs +0 -73
  32. package/crates/team-agent/src/messaging/tests/runtime.rs +0 -26
  33. package/crates/team-agent/src/messaging/types.rs +0 -20
  34. package/crates/team-agent/src/model/spec.rs +14 -18
  35. package/crates/team-agent/src/state/repository/tests.rs +1 -16
  36. package/crates/team-agent/src/state/repository.rs +1 -33
  37. package/package.json +4 -4
  38. package/skills/team-agent/SKILL.md +5 -5
  39. package/skills/team-agent/command-coverage.json +7 -0
  40. package/skills/team-agent/references/team-agent-operator.md +24 -116
  41. package/skills/team-agent/references/team-in-team.md +4 -19
  42. package/crates/team-agent/src/mcp_server/lifecycle_tools/agent_ops.rs +0 -484
@@ -5,135 +5,9 @@ use serde_json::Value;
5
5
 
6
6
  use crate::model::ids::TeamKey;
7
7
 
8
- use super::super::helpers::{ensure_object, object_fields, tool_runtime_error};
8
+ use super::super::helpers::{object_fields, tool_runtime_error};
9
9
  use super::super::{ToolOk, ToolResult};
10
10
 
11
- pub(crate) fn update_state(
12
- workspace: &Path,
13
- owner_team: Option<&TeamKey>,
14
- note: &str,
15
- ) -> ToolResult {
16
- let selected = match crate::state::selector::resolve_active_team(
17
- workspace,
18
- owner_team.map(TeamKey::as_str),
19
- crate::state::selector::SelectorMode::RequireSpec,
20
- ) {
21
- Ok(selected) => selected,
22
- Err(err) if is_missing_active_spec(&err) => {
23
- return update_state_without_spec(workspace, owner_team, note);
24
- }
25
- Err(err) => return Err(tool_runtime_error(err)),
26
- };
27
- let state = crate::state::repository::StateRepository::new(&selected.run_workspace).commit(
28
- crate::state::repository::StateWriteIntent::McpUpdateStateNote {
29
- team_key: Some(&selected.team_key),
30
- },
31
- |latest| {
32
- ensure_object(latest);
33
- append_note(latest, note);
34
- },
35
- )
36
- .map_err(tool_runtime_error)?;
37
- let spec_path = selected
38
- .spec_path
39
- .ok_or_else(|| tool_runtime_error("active team spec not found for update_state"))?;
40
- let spec_workspace = spec_path.parent().ok_or_else(|| {
41
- tool_runtime_error(format!(
42
- "active team spec has no parent: {}",
43
- spec_path.display()
44
- ))
45
- })?;
46
- let spec_text = std::fs::read_to_string(&spec_path).map_err(tool_runtime_error)?;
47
- let spec = crate::model::yaml::loads(&spec_text).map_err(tool_runtime_error)?;
48
- let path = crate::lifecycle::restart::write_team_state(spec_workspace, &spec, &state)
49
- .map_err(tool_runtime_error)?;
50
- Ok(update_state_ok(path))
51
- }
52
-
53
- fn update_state_without_spec(
54
- workspace: &Path,
55
- owner_team: Option<&TeamKey>,
56
- note: &str,
57
- ) -> ToolResult {
58
- let selected = crate::state::selector::resolve_active_team(
59
- workspace,
60
- owner_team.map(TeamKey::as_str),
61
- crate::state::selector::SelectorMode::RuntimeOnly,
62
- )
63
- .map_err(tool_runtime_error)?;
64
- let state = crate::state::repository::StateRepository::new(&selected.run_workspace).commit(
65
- crate::state::repository::StateWriteIntent::McpUpdateStateNote {
66
- team_key: Some(&selected.team_key),
67
- },
68
- |latest| {
69
- ensure_object(latest);
70
- seed_legacy_team_key(latest, &selected.run_workspace, &selected.team_key);
71
- append_note(latest, note);
72
- },
73
- )
74
- .map_err(tool_runtime_error)?;
75
- let path = crate::lifecycle::restart::write_team_state(
76
- &selected.run_workspace,
77
- &crate::model::yaml::Value::Null,
78
- &state,
79
- )
80
- .map_err(tool_runtime_error)?;
81
- Ok(update_state_ok(path))
82
- }
83
-
84
- fn append_note(state: &mut Value, note: &str) {
85
- if let Some(obj) = state.as_object_mut() {
86
- let notes = obj
87
- .entry("notes".to_string())
88
- .or_insert_with(|| Value::Array(Vec::new()));
89
- if !notes.is_array() {
90
- *notes = Value::Array(Vec::new());
91
- }
92
- if let Some(items) = notes.as_array_mut() {
93
- items.push(Value::String(note.to_string()));
94
- }
95
- }
96
- }
97
-
98
- fn seed_legacy_team_key(state: &mut Value, run_workspace: &Path, team_key: &str) {
99
- if state.get("team_dir").and_then(Value::as_str).is_some()
100
- || state.get("spec_path").and_then(Value::as_str).is_some()
101
- || state.get("session_name").and_then(Value::as_str).is_some()
102
- {
103
- return;
104
- }
105
- if let Some(obj) = state.as_object_mut() {
106
- obj.insert(
107
- "team_dir".to_string(),
108
- Value::String(
109
- run_workspace
110
- .join(".team")
111
- .join(team_key)
112
- .to_string_lossy()
113
- .to_string(),
114
- ),
115
- );
116
- }
117
- }
118
-
119
- fn update_state_ok(path: std::path::PathBuf) -> ToolOk {
120
- let mut fields = serde_json::Map::new();
121
- fields.insert("ok".to_string(), Value::Bool(true));
122
- fields.insert(
123
- "state_file".to_string(),
124
- Value::String(path.to_string_lossy().to_string()),
125
- );
126
- ToolOk { fields }
127
- }
128
-
129
- fn is_missing_active_spec(err: &crate::state::StateError) -> bool {
130
- matches!(
131
- err,
132
- crate::state::StateError::TeamSelect(message)
133
- if message.starts_with("active team spec not found:")
134
- )
135
- }
136
-
137
11
  pub(crate) fn get_team_status(workspace: &Path, owner_team: Option<&TeamKey>) -> ToolResult {
138
12
  let selected = crate::state::selector::resolve_active_team(
139
13
  workspace,
@@ -4,8 +4,8 @@
4
4
  //! Card: `docs/phase0/subsystems/14-mcp_cli.md` (MCP half).
5
5
  //! Truth source (READ-ONLY) `team-agent-public` @ v0.2.11 / `439bef8`:
6
6
  //! - `mcp_server/server.py` — stdio loop + JSON-RPC route (`dispatch`/`handle_mcp`/`main`)
7
- //! - `mcp_server/tools.py` — `TeamOrchestratorTools`: the 12 typed tool handlers
8
- //! - `mcp_server/contracts.py` — `TOOLS`: name/description/inputSchema (wire single-truth)
7
+ //! - `mcp_server/tools.py` — `TeamOrchestratorTools`: the three retained handlers
8
+ //! - `mcp_server/contracts.py` — retained tool names/descriptions/inputSchema
9
9
  //! - `mcp_server/normalize.py` — result envelope / compact-result regularization
10
10
  //! - `mcp_server/__init__.py` — package re-export surface locked by boundary tests
11
11
  //!
@@ -17,13 +17,9 @@
17
17
  //! `unimplemented!("step14 port: …")`.
18
18
  //!
19
19
  //! REUSE (do NOT redefine):
20
- //! - [`MessageStore`] (step 7) — `request_human` creates the leader message row.
21
- //! - [`EventLog`] (step 4) — `mcp.scope_resolved` / `mcp.send_message_refused` /
22
- //! `mcp.identity_inference_failed` / `mcp.task_inference_failed` audit events.
23
- //! - [`load_runtime_state`] / [`save_runtime_state`] (step 5 persist) — `assign_task`
24
- //! / `update_state` read-modify-write; `get_visible_peers` reads team scope.
25
- //! - [`messaging`] (step 11) — `send_message` / auto-finalizing `report_result` /
26
- //! `stuck_list` / `stuck_cancel` delegated by the tool handlers.
20
+ //! - [`EventLog`] (step 4) — retained message/result audit events.
21
+ //! - [`load_runtime_state`] (step 5 persist) — message scope and peer checks.
22
+ //! - [`messaging`] (step 11) — `send_message` and durable `report_result` delegation.
27
23
  //! - [`crate::model::enums`] (step 2) — [`ResultStatus`] / [`ChangeKind`] /
28
24
  //! [`TestStatus`] / [`RiskSeverity`] are the normalized result-envelope value
29
25
  //! enums; this layer ONLY does string-alias regularization onto them.
@@ -43,9 +39,8 @@
43
39
  //! a frame here would corrupt the stdout JSON-RPC stream.
44
40
  //! - **stdout 是传输通道** (server.py:135): every error is surfaced ON stdout as a
45
41
  //! JSON-RPC frame; logs/warnings MUST go to stderr/file, never stdout.
46
- //! - **worker-recipient 异步 accepted** (tools.py:176-183): a worker recipient with
47
- //! a message_id → [`SendOutcome::WorkerAccepted`] carrying the byte-stable
48
- //! `poll_via = "team-agent inbox <id>"`; leader/`*` → [`SendOutcome::Direct`].
42
+ //! - **worker-recipient 异步 accepted**: a worker recipient with a message_id →
43
+ //! [`SendOutcome::WorkerAccepted`]; leader/`*` → [`SendOutcome::Direct`].
49
44
  //! - **兜底字符串字节级保留** (bug-085): `_infer_task_id` failure → `"manual"` (not
50
45
  //! None); `_infer_agent_id` failure → `None` → caller routes to `"unknown"`.
51
46
  //!
@@ -1,72 +1,4 @@
1
- /// ---
2
- /// purpose: scoped MCP no-compaction update_state filesystem contract
3
- /// contract:
4
- /// provides:
5
- /// - name: update_state_path_contracts
6
- /// what: proves the raw no-compaction response preserves its state-file path in an owned fixture
7
- /// depends:
8
- /// - name: mcp_state_fixture
9
- /// what: fixture-owned root and provenance from tests.rs
10
- /// boundary:
11
- /// - retain the raw {ok,state_file} result shape
12
- /// - the owned selector checks workspace, state-file, and runtime paths remain fixture-owned
13
- /// maturity: wired
14
- /// ---
15
-
16
- fn verify_cleanup_surviving_receipt(
17
- receipt_path: &std::path::Path,
18
- raw_workspace: &std::path::Path,
19
- canonical_workspace: &std::path::Path,
20
- state_file: &std::path::Path,
21
- runtime_state: &std::path::Path,
22
- ) {
23
- let bytes = std::fs::read(receipt_path)
24
- .expect("provenance receipt must survive fixture Drop");
25
- let receipt: Value = serde_json::from_slice(&bytes).expect("provenance receipt must be JSON");
26
- assert_eq!(receipt["schema"], json!("mcp-state-provenance-v1"));
27
- let payload = &receipt["payload"];
28
- let payload_bytes = serde_json::to_vec(payload).unwrap();
29
- assert_eq!(
30
- receipt["payload_sha256"],
31
- json!(format!("{:x}", sha2::Sha256::digest(&payload_bytes)))
32
- );
33
- assert_eq!(
34
- payload["raw_workspace"],
35
- json!(raw_workspace.display().to_string())
36
- );
37
- assert_eq!(
38
- payload["canonical_workspace"],
39
- json!(canonical_workspace.display().to_string())
40
- );
41
- assert_eq!(
42
- payload["resolved_state_file"],
43
- json!(state_file.display().to_string())
44
- );
45
- assert_eq!(
46
- payload["runtime_state_path"],
47
- json!(runtime_state.display().to_string())
48
- );
49
- let facts = payload["facts"].as_array().unwrap();
50
- for name in [
51
- "root",
52
- "raw_workspace",
53
- "canonical_workspace",
54
- "team_dir",
55
- "runtime_dir",
56
- "state_parent",
57
- "runtime_state",
58
- "state_file",
59
- ] {
60
- let fact = facts
61
- .iter()
62
- .find(|fact| fact["name"] == json!(name))
63
- .unwrap_or_else(|| panic!("provenance fact missing: {name}"));
64
- assert!(fact["path"].as_str().is_some());
65
- assert!(fact["uid"].as_u64().is_some());
66
- assert!(fact["mode"].as_u64().is_some());
67
- assert!(fact["device"].as_u64().is_some());
68
- }
69
- }
1
+ /// Scoped retained-tool contracts exercise owner binding and team isolation.
70
2
 
71
3
  #[test]
72
4
  fn dispatch_send_message_worker_accepted_returned_verbatim() {
@@ -100,69 +32,6 @@ fn verify_cleanup_surviving_receipt(
100
32
  assert_eq!(v.get("poll_via"), Some(&json!(format!("team-agent inbox {mid}"))));
101
33
  }
102
34
 
103
- // ── #32/#47 request_human key order ok,message_id,status (no compaction) ────
104
- // GOLDEN (probe_events_red.py REQUEST_HUMAN-KEYS): ['ok','message_id','status'].
105
- // Rust compact_tool_result reorders to ok,status,message_id.
106
- #[test]
107
- fn request_human_key_order_is_ok_message_id_status() {
108
- let tools = TeamOrchestratorTools::with_identity(
109
- &unique_ws("reqhuman-order"),
110
- Some(AgentId::new("worker-3")),
111
- None,
112
- );
113
- let ok = tools.request_human("need approval", Some("task-1"), None).expect("request_human ok");
114
- let v = serde_json::to_value(&ok).unwrap();
115
- assert_eq!(keys(&v), vec!["ok", "message_id", "status"]);
116
- assert_eq!(v.get("status"), Some(&json!("needs_human")));
117
- }
118
-
119
- // ── #32 update_state returns RAW {ok, state_file} (NO compaction) ───────────
120
- // GOLDEN (tools.py:316-325 + probe_passthrough): update_state is NOT compacted;
121
- // state_file survives. Rust runs compact_tool_result whose ok-whitelist DROPS
122
- // state_file (not a golden whitelist key), so the key vanishes.
123
- #[test]
124
- fn update_state_state_file_survives_no_compaction() {
125
- let fixture = McpStateFixture::new("writable");
126
- let expected = fixture.state_file("team_state.md");
127
- let tools = TeamOrchestratorTools::with_identity(
128
- &fixture.workspace,
129
- Some(AgentId::new("leader")),
130
- None,
131
- );
132
- let ok = tools.update_state("note").expect("update_state ok");
133
- let v = serde_json::to_value(&ok).unwrap();
134
- assert!(v.get("state_file").and_then(Value::as_str).is_some(),
135
- "state_file must survive (update_state is not _compact_tool_result'd)");
136
- assert_eq!(keys(&v), vec!["ok", "state_file"]);
137
- let returned_state_file = PathBuf::from(v["state_file"].as_str().unwrap());
138
- assert_eq!(returned_state_file, expected);
139
- let runtime_state = crate::state::persist::runtime_state_path(&fixture.workspace);
140
- assert!(fixture.under_root(&returned_state_file));
141
- assert!(fixture.under_root(&runtime_state));
142
- let canonical_workspace =
143
- crate::model::paths::canonical_run_workspace(&fixture.workspace).unwrap();
144
- let receipt = fixture.record_provenance(&fixture.workspace, &returned_state_file);
145
- let root = fixture.root.clone();
146
- drop(fixture);
147
- assert!(!root.exists(), "fixture root must be removed before receipt verification");
148
- verify_cleanup_surviving_receipt(
149
- &receipt,
150
- &root.join("workspace"),
151
- &canonical_workspace,
152
- &returned_state_file,
153
- &runtime_state,
154
- );
155
- let receipt_bytes = std::fs::read(&receipt).unwrap();
156
- println!(
157
- "F10_PROVENANCE_RECEIPT path={} sha256={:x} payload={}",
158
- receipt.display(),
159
- sha2::Sha256::digest(&receipt_bytes),
160
- String::from_utf8_lossy(&receipt_bytes)
161
- );
162
- std::fs::remove_file(&receipt).unwrap();
163
- assert!(!receipt.exists(), "cleanup must remove only the exact receipt path");
164
- }
165
-
166
35
  // ── #36 report_result identity: neither envelope nor args can override capture ─
167
36
  #[test]
168
37
  fn report_result_rejects_envelope_and_argument_identity_mismatch() {
@@ -43,31 +43,6 @@ fn is_worker_recipient_classification() {
43
43
  ])));
44
44
  }
45
45
 
46
- // ════════════════════════════════════════════════════════════════════════
47
- // merge_tasks_by_id — prefer wins, prefer-first insertion order (tools.py:30)
48
- // Golden: prefer t1(done),t2 + fallback t1(pending),t3,{no id},"notdict"
49
- // → [t1(done), t2, t3] (t1 from prefer wins; non-dict / no-id dropped)
50
- // ════════════════════════════════════════════════════════════════════════
51
- #[test]
52
- fn merge_tasks_by_id_prefer_wins_no_done_regression() {
53
- let prefer = vec![
54
- json!({"id": "t1", "status": "done"}),
55
- json!({"id": "t2", "status": "pending"}),
56
- ];
57
- let fallback = vec![
58
- json!({"id": "t1", "status": "pending"}), // must NOT regress t1
59
- json!({"id": "t3", "status": "ready"}),
60
- json!({"no": "id"}), // dropped (no id)
61
- json!("notdict"), // dropped (not object)
62
- ];
63
- let merged = merge_tasks_by_id(&prefer, &fallback);
64
- assert_eq!(merged.len(), 3);
65
- assert_eq!(merged[0]["id"], json!("t1"));
66
- assert_eq!(merged[0]["status"], json!("done")); // prefer wins → no regression
67
- assert_eq!(merged[1]["id"], json!("t2"));
68
- assert_eq!(merged[2]["id"], json!("t3"));
69
- }
70
-
71
46
  // ════════════════════════════════════════════════════════════════════════
72
47
  // SendOutcome::to_value — worker-accepted async envelope (tools.py:177-182)
73
48
  // byte-stable: {status:"accepted",delivery_pending:true,
@@ -171,7 +146,7 @@ fn send_message_worker_recipient_returns_accepted_with_poll_hint() {
171
146
  }
172
147
 
173
148
  #[test]
174
- fn ordinary_send_assign_shape_has_no_recovery_marker() {
149
+ fn ordinary_send_shape_has_no_recovery_marker() {
175
150
  let ws = seed_current_worker_state("ordinary-no-recovery");
176
151
  let tools = TeamOrchestratorTools::with_identity(
177
152
  &ws,
@@ -198,46 +173,6 @@ fn ordinary_send_assign_shape_has_no_recovery_marker() {
198
173
  "ordinary send must not carry acceptance marker: {sent}"
199
174
  );
200
175
 
201
- let assigned = tools
202
- .assign_task(
203
- &json!({"id": "ordinary-task", "assignee": "worker-1", "title": "ordinary"}),
204
- None,
205
- )
206
- .expect("ordinary assign ok");
207
- let assigned_value = serde_json::to_value(&assigned).expect("serialize ordinary assign");
208
- assert!(
209
- !assigned_value.as_object().unwrap().contains_key("recovery"),
210
- "ordinary assign must not carry recovery marker: {assigned_value}"
211
- );
212
- assert!(
213
- assigned_value.get("acceptance_marker").is_none(),
214
- "ordinary assign must not carry acceptance marker: {assigned_value}"
215
- );
216
-
217
- let recovery_false_assigned = tools
218
- .assign_task(
219
- &json!({
220
- "id": "ordinary-recovery-false-task",
221
- "assignee": "worker-1",
222
- "title": "ordinary recovery false",
223
- "recovery": false,
224
- }),
225
- None,
226
- )
227
- .expect("recovery=false assign ok");
228
- let recovery_false_value =
229
- serde_json::to_value(&recovery_false_assigned).expect("serialize recovery=false assign");
230
- assert!(
231
- !recovery_false_value
232
- .as_object()
233
- .unwrap()
234
- .contains_key("recovery"),
235
- "recovery=false assign must not carry recovery marker: {recovery_false_value}"
236
- );
237
- assert!(
238
- recovery_false_value.get("acceptance_marker").is_none(),
239
- "recovery=false assign must not carry acceptance marker: {recovery_false_value}"
240
- );
241
176
  }
242
177
 
243
178
  #[test]
@@ -256,33 +191,6 @@ fn send_message_without_framework_identity_fails_closed() {
256
191
  assert_eq!(error.reason, ToolErrorReason::McpScopeRefused);
257
192
  }
258
193
 
259
- #[test]
260
- fn recovery_assign_shape_has_structured_marker() {
261
- let ws = seed_current_worker_state("recovery-marker");
262
- let tools = TeamOrchestratorTools::with_identity(
263
- &ws,
264
- Some(AgentId::new("leader")),
265
- Some(TeamKey::new("current")),
266
- );
267
-
268
- let assigned = tools
269
- .assign_task(
270
- &json!({
271
- "id": "recovery-task",
272
- "assignee": "worker-1",
273
- "title": "recover",
274
- "recovery": true,
275
- }),
276
- None,
277
- )
278
- .expect("recovery assign ok");
279
- assert_eq!(assigned.fields.get("recovery"), Some(&json!(true)));
280
- assert_eq!(
281
- assigned.fields.get("acceptance_marker"),
282
- Some(&json!("recovery"))
283
- );
284
- }
285
-
286
194
  #[test]
287
195
  fn send_message_worker_recipient_surfaces_dead_coordinator_warning() {
288
196
  let ws = unique_ws("send-worker-dead-coord");
@@ -290,67 +290,6 @@
290
290
  );
291
291
  }
292
292
 
293
- // ════════════════════════════════════════════════════════════════════════
294
- // CONTROL-PLANE: request_human creates a requires_ack leader message → needs_human
295
- // (tools.py:342-346). sender = explicit > env > "unknown" (never leader).
296
- //
297
- // Post-#230 N31/N32 funnel (cr-approved I-3): request_human routes through the
298
- // shared leader-delivery primitive (`send_to_leader_receiver`) instead of doing a
299
- // raw `store.create_message` bypass. Return shape from the caller's perspective is
300
- // unchanged: `status="needs_human"` + a populated `message_id`. With no leader
301
- // pane bound in this fixture, the primitive's I-4 rebind_required path STILL
302
- // persists the message row and returns its `message_id` — audit + rebind replay
303
- // both depend on it.
304
- // ════════════════════════════════════════════════════════════════════════
305
- #[test]
306
- fn request_human_returns_needs_human_with_message_id() {
307
- let tools = TeamOrchestratorTools::with_identity(
308
- &unique_ws("request-human"),
309
- Some(AgentId::new("worker-3")),
310
- Some(TeamKey::new("teamA")),
311
- );
312
- let ok = tools.request_human("need approval", Some("task-1"), None)
313
- .expect("request_human ok");
314
- let v = serde_json::to_value(&ok).unwrap();
315
- assert_eq!(v.get("status"), Some(&json!("needs_human")));
316
- assert!(v.get("message_id").and_then(Value::as_str).is_some(),
317
- "request_human must return the created leader message_id (persisted for rebind audit even on I-4 rebind_required)");
318
- }
319
-
320
- // ════════════════════════════════════════════════════════════════════════
321
- // CONTROL-PLANE: update_state appends a note + returns state_file (tools.py:316-325)
322
- // ════════════════════════════════════════════════════════════════════════
323
- #[test]
324
- fn update_state_returns_ok_and_state_file_path() {
325
- let ws = seed_state_ws(
326
- "update-state",
327
- &json!({
328
- "active_team_key": "teamA",
329
- "team_key": "teamA",
330
- "session_name": "teamA",
331
- "status": "alive",
332
- "teams": {
333
- "teamA": {
334
- "team_key": "teamA",
335
- "session_name": "teamA",
336
- "status": "alive",
337
- "agents": {}
338
- }
339
- }
340
- }),
341
- );
342
- let tools = TeamOrchestratorTools::with_identity(
343
- &ws,
344
- Some(AgentId::new("leader")),
345
- Some(TeamKey::new("teamA")),
346
- );
347
- let ok = tools.update_state("checkpoint note").expect("update_state ok");
348
- let v = serde_json::to_value(&ok).unwrap();
349
- assert_eq!(v.get("ok"), Some(&json!(true)));
350
- assert!(v.get("state_file").and_then(Value::as_str).is_some(),
351
- "update_state returns the rewritten team_state.md path");
352
- }
353
-
354
293
  // ════════════════════════════════════════════════════════════════════════
355
294
  // RpcId / RpcResponse byte-stability — null id echoed, error frame shape.
356
295
  // ════════════════════════════════════════════════════════════════════════
@@ -1,19 +1,9 @@
1
1
  #[test]
2
2
  fn mcp_tool_wire_names_and_parse_roundtrip() {
3
3
  let names = [
4
- (McpTool::AssignTask, "assign_task"),
5
4
  (McpTool::SendMessage, "send_message"),
6
5
  (McpTool::ReportResult, "report_result"),
7
- (McpTool::UpdateState, "update_state"),
8
6
  (McpTool::GetTeamStatus, "get_team_status"),
9
- (McpTool::StopAgent, "stop_agent"),
10
- (McpTool::ResetAgent, "reset_agent"),
11
- (McpTool::AddAgent, "add_agent"),
12
- (McpTool::CloneAgent, "clone_agent"),
13
- (McpTool::ForkAgent, "fork_agent"),
14
- (McpTool::RequestHuman, "request_human"),
15
- (McpTool::StuckList, "stuck_list"),
16
- (McpTool::StuckCancel, "stuck_cancel"),
17
7
  ];
18
8
  for (tool, name) in names {
19
9
  assert_eq!(tool.wire_name(), name);
@@ -45,27 +35,13 @@ fn rpc_method_classify() {
45
35
  // tools_contract — TOOLS wire list, exact names+order
46
36
  // ════════════════════════════════════════════════════════════════════════
47
37
  #[test]
48
- fn tools_contract_has_thirteen_tools_in_order() {
38
+ fn tools_contract_has_three_tools_in_order() {
49
39
  let tools = tools_contract();
50
- assert_eq!(tools.len(), 13);
40
+ assert_eq!(tools.len(), 3);
51
41
  let got: Vec<&str> = tools.iter().map(|t| t["name"].as_str().unwrap()).collect();
52
42
  assert_eq!(
53
43
  got,
54
- vec![
55
- "assign_task",
56
- "send_message",
57
- "report_result",
58
- "update_state",
59
- "get_team_status",
60
- "stop_agent",
61
- "reset_agent",
62
- "add_agent",
63
- "clone_agent",
64
- "fork_agent",
65
- "request_human",
66
- "stuck_list",
67
- "stuck_cancel",
68
- ]
44
+ vec!["send_message", "report_result", "get_team_status"]
69
45
  );
70
46
  // each carries description + inputSchema
71
47
  for t in &tools {
@@ -89,32 +65,11 @@ fn tools_contract_has_thirteen_tools_in_order() {
89
65
  "{internal} is framework-owned, not caller-supplied"
90
66
  );
91
67
  }
92
- let clone = tools
68
+ let status = tools
93
69
  .iter()
94
- .find(|tool| tool["name"] == json!("clone_agent"))
70
+ .find(|tool| tool["name"] == json!("get_team_status"))
95
71
  .unwrap();
96
- assert_eq!(
97
- clone["description"],
98
- json!("Clone a worker role into a fresh provider session.")
99
- );
100
- assert_eq!(clone["inputSchema"]["additionalProperties"], json!(false));
101
- assert_eq!(
102
- clone["inputSchema"]["required"],
103
- json!(["source_agent_id", "as_agent_id"])
104
- );
105
- assert_eq!(
106
- clone["inputSchema"]["properties"]
107
- .as_object()
108
- .unwrap()
109
- .keys()
110
- .cloned()
111
- .collect::<BTreeSet<_>>(),
112
- BTreeSet::from([
113
- "as_agent_id".to_string(),
114
- "label".to_string(),
115
- "source_agent_id".to_string(),
116
- ])
117
- );
72
+ assert_eq!(status["description"], json!("Return machine-readable team status."));
118
73
  }
119
74
 
120
75
  #[test]