@team-agent/installer 0.5.119 → 0.5.121
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/README.md +46 -1
- package/README.zh.md +46 -1
- package/crates/team-agent/src/cli/adapters.rs +11 -0
- package/crates/team-agent/src/cli/arguments.rs +401 -0
- package/crates/team-agent/src/cli/emit.rs +241 -142
- package/crates/team-agent/src/cli/intent/runtime.rs +1175 -0
- package/crates/team-agent/src/cli/intent/runtime_tests.rs +698 -0
- package/crates/team-agent/src/cli/intent/tests.rs +654 -0
- package/crates/team-agent/src/cli/intent.rs +642 -0
- package/crates/team-agent/src/cli/mod.rs +53 -6
- package/crates/team-agent/src/cli/named_address.rs +10 -1
- package/crates/team-agent/src/cli/spec.rs +22 -10
- package/crates/team-agent/src/cli/status_port/brief.rs +117 -16
- package/crates/team-agent/src/cli/tests/base.rs +6 -1
- package/crates/team-agent/src/cli/tests/parser_safety.rs +383 -0
- package/crates/team-agent/src/cli/tests/run_delegation.rs +63 -19
- package/crates/team-agent/src/cli/tests/shutdown_kill_plan.rs +56 -0
- package/crates/team-agent/src/cli/tests/verb_install_skill.rs +3 -2
- package/crates/team-agent/src/cli/tests/verb_profile.rs +3 -1
- package/crates/team-agent/src/contract_runtime/backend.rs +1 -1
- package/crates/team-agent/src/db/message_store.rs +65 -0
- package/crates/team-agent/src/lifecycle/launch/agent_state.rs +8 -0
- package/crates/team-agent/src/lifecycle/launch/cursor_mcp.rs +111 -4
- package/crates/team-agent/src/lifecycle/launch/cursor_mcp_iso.rs +244 -82
- package/crates/team-agent/src/lifecycle/launch/quick_start.rs +166 -77
- package/crates/team-agent/src/lifecycle/launch.rs +29 -0
- package/crates/team-agent/src/lifecycle/restart/common.rs +7 -1
- package/crates/team-agent/src/lifecycle/restart.rs +17 -0
- package/crates/team-agent/src/lifecycle/tests/acceptance_b_batch_red.rs +1 -3
- package/crates/team-agent/src/lifecycle/tests/behavioral_diff_264_red.rs +5 -15
- package/crates/team-agent/src/lifecycle/tests/claude_compatible_config_red.rs +2 -3
- package/crates/team-agent/src/lifecycle/tests/claude_profile_launch_red.rs +2 -3
- package/crates/team-agent/src/lifecycle/tests/cli_argv_routing_red.rs +10 -2
- package/crates/team-agent/src/lifecycle/tests/clone_agent_preserves_source_tools.rs +8 -1
- package/crates/team-agent/src/lifecycle/tests/clone_fork_copilot_perms_red.rs +3 -2
- package/crates/team-agent/src/lifecycle/tests/communication_mode_runtime_contract_red.rs +6 -9
- package/crates/team-agent/src/lifecycle/tests/copilot_provider_red.rs +7 -5
- package/crates/team-agent/src/lifecycle/tests/cursor_mcp_overlay.rs +64 -19
- package/crates/team-agent/src/lifecycle/tests/cursor_require_explicit_model_red.rs +5 -2
- package/crates/team-agent/src/lifecycle/tests/cursor_restart_resume_red.rs +34 -10
- package/crates/team-agent/src/lifecycle/tests/grok_effort_argv_red.rs +2 -4
- package/crates/team-agent/src/lifecycle/tests/grok_mcp_overlay_red.rs +3 -4
- package/crates/team-agent/src/lifecycle/tests/grok_require_explicit_model_red.rs +5 -4
- package/crates/team-agent/src/lifecycle/tests/grok_restart_resume_red.rs +2 -4
- package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +78 -36
- package/crates/team-agent/src/lifecycle/tests/phase_b_contracts.rs +8 -4
- package/crates/team-agent/src/lifecycle/tests/phase_golden.rs +6 -12
- package/crates/team-agent/src/lifecycle/tests/pi_fork_session_inheritance_red.rs +4 -0
- package/crates/team-agent/src/lifecycle/tests/startup_latency_contract.rs +1 -3
- package/crates/team-agent/src/lifecycle/tests/status_credential_redaction_contract.rs +7 -0
- package/crates/team-agent/src/lifecycle/tests/subprep_codex_trust_roundtrip_red.rs +47 -4
- package/crates/team-agent/src/lifecycle/tests/team_in_team_identity_scope_red.rs +1 -3
- package/crates/team-agent/src/lifecycle/tests/verify_rs031_window_consistency_red.rs +2 -1
- package/crates/team-agent/src/lifecycle/types.rs +10 -0
- package/crates/team-agent/src/mcp_server/mod.rs +2 -0
- package/crates/team-agent/src/mcp_server/readiness.rs +432 -0
- package/crates/team-agent/src/mcp_server/readiness_tests.rs +242 -0
- package/crates/team-agent/src/mcp_server/wire.rs +96 -11
- package/crates/team-agent/src/messaging/delivery.rs +62 -0
- package/crates/team-agent/src/messaging/tests/dup_inject.rs +41 -1
- package/crates/team-agent/src/provider/adapter.rs +7 -0
- package/crates/team-agent/src/tmux_backend/tests.rs +37 -0
- package/crates/team-agent/src/tmux_backend.rs +8 -1
- package/package.json +4 -4
- package/skills/team-agent/SKILL.md +71 -54
- package/skills/team-agent/command-coverage.json +283 -13
- package/skills/team-agent/docs/reference/team-agent-operator.md +220 -0
- package/skills/team-agent/references/bug-as-artifact-flow.md +1 -1
- package/skills/team-agent/references/recovery-runbook.md +61 -212
- package/skills/team-agent/references/team-agent-operator.md +181 -48
- package/skills/team-agent/references/team-in-team.md +47 -80
|
@@ -190,17 +190,20 @@ fn run_stdio_loop_inner<R: BufRead, W: Write>(
|
|
|
190
190
|
report: &mut ServerRunReport,
|
|
191
191
|
marker: &McpServerLifecycleMarker,
|
|
192
192
|
) -> Result<ServerRunReport, McpError> {
|
|
193
|
+
let mut initialized = false;
|
|
193
194
|
for line in reader.lines() {
|
|
194
195
|
let line = line?;
|
|
195
196
|
report.requests_read = report.requests_read.saturating_add(1);
|
|
196
|
-
let (frame,
|
|
197
|
+
let (frame, response_kind) = handle_stdin_line(tools, &line, report)?;
|
|
197
198
|
if let Some(value) = frame {
|
|
198
199
|
let value = crate::redaction::redact_external_value(&value);
|
|
199
200
|
serde_json::to_writer(&mut *writer, &value)?;
|
|
200
201
|
writer.write_all(b"\n")?;
|
|
201
202
|
writer.flush()?;
|
|
202
|
-
|
|
203
|
-
|
|
203
|
+
match response_kind {
|
|
204
|
+
ResponseKind::Initialize => initialized = true,
|
|
205
|
+
ResponseKind::ToolsList => marker.write_tools_list_response_written(initialized),
|
|
206
|
+
ResponseKind::Other => {}
|
|
204
207
|
}
|
|
205
208
|
report.responses_written = report.responses_written.saturating_add(1);
|
|
206
209
|
}
|
|
@@ -209,11 +212,17 @@ fn run_stdio_loop_inner<R: BufRead, W: Write>(
|
|
|
209
212
|
Ok(report.clone())
|
|
210
213
|
}
|
|
211
214
|
|
|
215
|
+
enum ResponseKind {
|
|
216
|
+
Initialize,
|
|
217
|
+
ToolsList,
|
|
218
|
+
Other,
|
|
219
|
+
}
|
|
220
|
+
|
|
212
221
|
fn handle_stdin_line(
|
|
213
222
|
tools: &TeamOrchestratorTools,
|
|
214
223
|
line: &str,
|
|
215
224
|
report: &mut ServerRunReport,
|
|
216
|
-
) -> Result<(Option<Value>,
|
|
225
|
+
) -> Result<(Option<Value>, ResponseKind), McpError> {
|
|
217
226
|
let request: Value = match serde_json::from_str(line) {
|
|
218
227
|
Ok(value) => value,
|
|
219
228
|
Err(err) => {
|
|
@@ -224,23 +233,28 @@ fn handle_stdin_line(
|
|
|
224
233
|
-32700,
|
|
225
234
|
format!("parse error: {err}"),
|
|
226
235
|
)),
|
|
227
|
-
|
|
236
|
+
ResponseKind::Other,
|
|
228
237
|
));
|
|
229
238
|
}
|
|
230
239
|
};
|
|
231
240
|
if request.get("jsonrpc").and_then(Value::as_str) == Some("2.0") {
|
|
232
|
-
let
|
|
241
|
+
let method = request.get("method").and_then(Value::as_str);
|
|
242
|
+
let valid_id = request.get("id").is_some_and(|id| id.is_string() || id.is_number());
|
|
233
243
|
match handle_mcp(tools, &request)? {
|
|
234
244
|
Some(response) => {
|
|
235
|
-
let
|
|
245
|
+
let kind = match (method, valid_id && response.error.is_none()) {
|
|
246
|
+
(Some("initialize"), true) => ResponseKind::Initialize,
|
|
247
|
+
(Some("tools/list"), true) => ResponseKind::ToolsList,
|
|
248
|
+
_ => ResponseKind::Other,
|
|
249
|
+
};
|
|
236
250
|
if response.error.is_some() {
|
|
237
251
|
report.error_frames = report.error_frames.saturating_add(1);
|
|
238
252
|
}
|
|
239
|
-
Ok((Some(serde_json::to_value(response)?),
|
|
253
|
+
Ok((Some(serde_json::to_value(response)?), kind))
|
|
240
254
|
}
|
|
241
255
|
None => {
|
|
242
256
|
report.notifications_skipped = report.notifications_skipped.saturating_add(1);
|
|
243
|
-
Ok((None,
|
|
257
|
+
Ok((None, ResponseKind::Other))
|
|
244
258
|
}
|
|
245
259
|
}
|
|
246
260
|
} else {
|
|
@@ -251,7 +265,7 @@ fn handle_stdin_line(
|
|
|
251
265
|
err.to_envelope()
|
|
252
266
|
}
|
|
253
267
|
};
|
|
254
|
-
Ok((Some(value),
|
|
268
|
+
Ok((Some(value), ResponseKind::Other))
|
|
255
269
|
}
|
|
256
270
|
}
|
|
257
271
|
|
|
@@ -281,6 +295,7 @@ struct McpServerLifecycleMarker {
|
|
|
281
295
|
workspace: std::path::PathBuf,
|
|
282
296
|
agent_id: Option<String>,
|
|
283
297
|
owner_team_id: Option<String>,
|
|
298
|
+
started_at: chrono::DateTime<chrono::Utc>,
|
|
284
299
|
pid: u32,
|
|
285
300
|
ppid: u32,
|
|
286
301
|
}
|
|
@@ -291,6 +306,7 @@ impl McpServerLifecycleMarker {
|
|
|
291
306
|
workspace: workspace.to_path_buf(),
|
|
292
307
|
agent_id: non_empty_env("TEAM_AGENT_ID"),
|
|
293
308
|
owner_team_id: non_empty_env("TEAM_AGENT_OWNER_TEAM_ID"),
|
|
309
|
+
started_at: chrono::Utc::now(),
|
|
294
310
|
pid: std::process::id(),
|
|
295
311
|
ppid: parent_pid(),
|
|
296
312
|
}
|
|
@@ -300,7 +316,7 @@ impl McpServerLifecycleMarker {
|
|
|
300
316
|
self.write("mcp.server_started", None, None);
|
|
301
317
|
}
|
|
302
318
|
|
|
303
|
-
fn write_tools_list_response_written(&self) {
|
|
319
|
+
fn write_tools_list_response_written(&self, initialized: bool) {
|
|
304
320
|
let _ = EventLog::new(&self.workspace).write(
|
|
305
321
|
"mcp.tools_list_response_written",
|
|
306
322
|
serde_json::json!({
|
|
@@ -313,8 +329,17 @@ impl McpServerLifecycleMarker {
|
|
|
313
329
|
"ppid": self.ppid,
|
|
314
330
|
"spawn_epoch": null,
|
|
315
331
|
"spawn_epoch_source": "unavailable",
|
|
332
|
+
"initialize_response_flushed": initialized,
|
|
316
333
|
}),
|
|
317
334
|
);
|
|
335
|
+
#[cfg(unix)]
|
|
336
|
+
if initialized {
|
|
337
|
+
if let (Some(owner), Some(agent)) = (&self.owner_team_id, &self.agent_id) {
|
|
338
|
+
if let Err(reason) = super::readiness::publish(&self.workspace, owner, agent, self.started_at) {
|
|
339
|
+
self.write("mcp.handshake_receipt_failed", Some(reason), None);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
}
|
|
318
343
|
}
|
|
319
344
|
|
|
320
345
|
fn write_exit(&self, reason: &str, error: Option<&str>) {
|
|
@@ -725,6 +750,66 @@ mod e23_lifecycle_marker_tests {
|
|
|
725
750
|
let _ = std::fs::remove_dir_all(&ws);
|
|
726
751
|
}
|
|
727
752
|
|
|
753
|
+
#[cfg(unix)]
|
|
754
|
+
fn run_bound_handshake<W: Write>(ws: &Path, input: &str, writer: &mut W) -> Result<ServerRunReport, McpError> {
|
|
755
|
+
let marker = McpServerLifecycleMarker {
|
|
756
|
+
workspace: ws.to_path_buf(), agent_id: Some("worker".into()),
|
|
757
|
+
owner_team_id: Some("owner".into()), started_at: chrono::Utc::now(),
|
|
758
|
+
pid: std::process::id(), ppid: parent_pid(),
|
|
759
|
+
};
|
|
760
|
+
run_stdio_loop_inner(&TeamOrchestratorTools::new(ws), std::io::Cursor::new(input.as_bytes()),
|
|
761
|
+
writer, &mut ServerRunReport::default(), &marker)
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
#[cfg(unix)]
|
|
765
|
+
fn handshake_receipts(ws: &Path) -> usize {
|
|
766
|
+
std::fs::read_dir(ws.join(".team/runtime/mcp-handshakes"))
|
|
767
|
+
.map(|files| files.filter_map(Result::ok)
|
|
768
|
+
.filter(|file| file.path().extension().is_some_and(|ext| ext == "json")).count())
|
|
769
|
+
.unwrap_or(0)
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
#[test]
|
|
773
|
+
#[cfg(unix)]
|
|
774
|
+
fn stdio_handshake_receipt_requires_initialize_and_tools_list_flush() {
|
|
775
|
+
let initialize = r#"{"jsonrpc":"2.0","id":1,"method":"initialize"}"#;
|
|
776
|
+
let tools = r#"{"jsonrpc":"2.0","id":2,"method":"tools/list"}"#;
|
|
777
|
+
let invalid_initialize = r#"{"jsonrpc":"2.0","method":"initialize"}"#;
|
|
778
|
+
let invalid_tools = r#"{"jsonrpc":"2.0","method":"tools/list"}"#;
|
|
779
|
+
for (tag, requests, expected) in [
|
|
780
|
+
("init-only", vec![initialize], 0),
|
|
781
|
+
("list-only", vec![tools], 0),
|
|
782
|
+
("invalid-init", vec![invalid_initialize, tools], 0),
|
|
783
|
+
("invalid-list", vec![initialize, invalid_tools], 0),
|
|
784
|
+
("ready", vec![initialize, tools], 1),
|
|
785
|
+
] {
|
|
786
|
+
let ws = marker_ws(tag);
|
|
787
|
+
let mut output = Vec::new();
|
|
788
|
+
run_bound_handshake(&ws, &(requests.join("\n") + "\n"), &mut output).unwrap();
|
|
789
|
+
assert_eq!(handshake_receipts(&ws), expected, "{tag}");
|
|
790
|
+
let _ = std::fs::remove_dir_all(ws);
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
#[test]
|
|
795
|
+
#[cfg(unix)]
|
|
796
|
+
fn stdio_failed_tools_list_flush_cannot_publish_a_handshake() {
|
|
797
|
+
struct FailSecondFlush(usize);
|
|
798
|
+
impl Write for FailSecondFlush {
|
|
799
|
+
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> { Ok(buf.len()) }
|
|
800
|
+
fn flush(&mut self) -> std::io::Result<()> {
|
|
801
|
+
self.0 += 1;
|
|
802
|
+
if self.0 == 2 { Err(std::io::Error::other("controlled tools/list flush failure")) }
|
|
803
|
+
else { Ok(()) }
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
let ws = marker_ws("handshake-flush-failure");
|
|
807
|
+
let input = "{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"initialize\"}\n{\"jsonrpc\":\"2.0\",\"id\":2,\"method\":\"tools/list\"}\n";
|
|
808
|
+
assert!(run_bound_handshake(&ws, input, &mut FailSecondFlush(0)).is_err());
|
|
809
|
+
assert_eq!(handshake_receipts(&ws), 0);
|
|
810
|
+
let _ = std::fs::remove_dir_all(ws);
|
|
811
|
+
}
|
|
812
|
+
|
|
728
813
|
#[test]
|
|
729
814
|
fn stdio_loop_writes_started_and_stdin_eof_exit_markers() {
|
|
730
815
|
let ws = marker_ws("eof");
|
|
@@ -302,6 +302,49 @@ fn prepare_message_state(
|
|
|
302
302
|
Ok(Ok(prepared))
|
|
303
303
|
}
|
|
304
304
|
|
|
305
|
+
#[cfg(unix)]
|
|
306
|
+
fn mcp_handshake_pending_outcome(
|
|
307
|
+
store: &MessageStore,
|
|
308
|
+
event_log: &EventLog,
|
|
309
|
+
message_id: &str,
|
|
310
|
+
attempt: u32,
|
|
311
|
+
reason: &str,
|
|
312
|
+
) -> Result<DeliveryOutcome, MessagingError> {
|
|
313
|
+
// No paste/key has occurred. Only this exact claim may be released; a
|
|
314
|
+
// concurrent cancellation or newer delivery must never be overwritten.
|
|
315
|
+
if !store.release_delivery_before_input(message_id, attempt, reason)? {
|
|
316
|
+
return Ok(DeliveryOutcome {
|
|
317
|
+
ok: false,
|
|
318
|
+
status: DeliveryStatus::Refused,
|
|
319
|
+
message_status: MessageStatusShadow("target_resolved".to_string()),
|
|
320
|
+
message_id: Some(message_id.to_string()),
|
|
321
|
+
verification: None,
|
|
322
|
+
stage: None,
|
|
323
|
+
reason: Some(DeliveryRefusal::MessageAlreadyClaimed),
|
|
324
|
+
channel: None,
|
|
325
|
+
ack_forced_off: false,
|
|
326
|
+
turn_verification: None,
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
event_log.write("delivery.deferred_mcp_handshake", serde_json::json!({
|
|
330
|
+
"message_id": message_id,
|
|
331
|
+
"reason": reason,
|
|
332
|
+
"effect": "none",
|
|
333
|
+
}))?;
|
|
334
|
+
Ok(DeliveryOutcome {
|
|
335
|
+
ok: false,
|
|
336
|
+
status: DeliveryStatus::Degraded,
|
|
337
|
+
message_status: MessageStatusShadow("target_resolved".to_string()),
|
|
338
|
+
message_id: Some(message_id.to_string()),
|
|
339
|
+
verification: Some(reason.to_string()),
|
|
340
|
+
stage: None,
|
|
341
|
+
reason: None,
|
|
342
|
+
channel: None,
|
|
343
|
+
ack_forced_off: false,
|
|
344
|
+
turn_verification: None,
|
|
345
|
+
})
|
|
346
|
+
}
|
|
347
|
+
|
|
305
348
|
fn deliver_prepared_message(
|
|
306
349
|
workspace: &Path,
|
|
307
350
|
store: &MessageStore,
|
|
@@ -642,6 +685,25 @@ fn deliver_prepared_message(
|
|
|
642
685
|
turn_verification: None,
|
|
643
686
|
});
|
|
644
687
|
}
|
|
688
|
+
// Startup control must remain reachable before protocol admission: some
|
|
689
|
+
// clients cannot initialize MCP until their trust prompt is answered.
|
|
690
|
+
// No business input is allowed before the current tools/list flush. A
|
|
691
|
+
// readiness-only refusal releases this exact claim without spending an
|
|
692
|
+
// input attempt; it never holds a pane-input lock or sleeps for startup.
|
|
693
|
+
#[cfg(unix)]
|
|
694
|
+
{
|
|
695
|
+
let admission = crate::mcp_server::readiness::preflight(
|
|
696
|
+
workspace, canonical_owner_team_id.as_deref(), &message.recipient, state,
|
|
697
|
+
).and_then(|admission| {
|
|
698
|
+
if let Some(admission) = admission.as_ref() {
|
|
699
|
+
crate::mcp_server::readiness::revalidate(admission, &target, &live_targets)?;
|
|
700
|
+
}
|
|
701
|
+
Ok(())
|
|
702
|
+
});
|
|
703
|
+
if let Err(reason) = admission {
|
|
704
|
+
return mcp_handshake_pending_outcome(store, event_log, message_id, attempt, reason);
|
|
705
|
+
}
|
|
706
|
+
}
|
|
645
707
|
let rendered = render_message(
|
|
646
708
|
&message.sender,
|
|
647
709
|
message.task_id.as_deref(),
|
|
@@ -28,6 +28,44 @@ use crate::transport::{
|
|
|
28
28
|
TurnVerification, WindowName,
|
|
29
29
|
};
|
|
30
30
|
|
|
31
|
+
#[test]
|
|
32
|
+
#[cfg(unix)]
|
|
33
|
+
fn eager_mcp_waits_without_spending_input_attempts_then_delivers_the_same_message() {
|
|
34
|
+
let mut case = WorkerCase::new("mcp-handshake", "cursor_agent");
|
|
35
|
+
let carrier = crate::mcp_server::readiness::test_carrier_pid().unwrap();
|
|
36
|
+
let config = case.workspace.join(".team/runtime/mcp/w1.json");
|
|
37
|
+
std::fs::create_dir_all(config.parent().unwrap()).unwrap();
|
|
38
|
+
std::fs::write(&config, serde_json::to_vec(&serde_json::json!({
|
|
39
|
+
"mcpServers": {"team_orchestrator": {
|
|
40
|
+
"command": std::env::current_exe().unwrap().canonicalize().unwrap(),
|
|
41
|
+
"env": {"TEAM_AGENT_WORKSPACE": case.workspace, "TEAM_AGENT_ID": "w1", "TEAM_AGENT_OWNER_TEAM_ID": "dup-team"}
|
|
42
|
+
}}
|
|
43
|
+
})).unwrap()).unwrap();
|
|
44
|
+
case.state["team_key"] = serde_json::json!("dup-team");
|
|
45
|
+
case.state["agents"]["w1"]["mcp_config"] = serde_json::json!(config);
|
|
46
|
+
case.state["agents"]["w1"]["pane_pid"] = serde_json::json!(carrier);
|
|
47
|
+
case.state["agents"]["w1"]["spawned_at"] = serde_json::json!((chrono::Utc::now() - chrono::Duration::seconds(10)).to_rfc3339());
|
|
48
|
+
case.state["teams"] = serde_json::json!({"dup-team": case.state.clone()});
|
|
49
|
+
crate::state::persist::save_runtime_state(&case.workspace, &case.state).unwrap();
|
|
50
|
+
let transport = RecordingTransport { carrier: Some(carrier), ..RecordingTransport::new("") };
|
|
51
|
+
let message_id = case.store.create_message(None, "leader", "w1", "handshake canary", None, false, Some("dup-team")).unwrap();
|
|
52
|
+
for _ in 0..3 {
|
|
53
|
+
let outcome = deliver_pending_message(&case.workspace, &case.store, &transport,
|
|
54
|
+
&message_id, &case.event_log, &case.state).unwrap();
|
|
55
|
+
assert!(!outcome.ok);
|
|
56
|
+
assert!(outcome.verification.as_deref().unwrap().starts_with("mcp_handshake_"));
|
|
57
|
+
assert_eq!(transport.inject_count(), 0);
|
|
58
|
+
}
|
|
59
|
+
let connection = rusqlite::Connection::open(case.store.db_path()).unwrap();
|
|
60
|
+
let attempts: i64 = connection.query_row("select delivery_attempts from messages where message_id=?1", [&message_id], |row| row.get(0)).unwrap();
|
|
61
|
+
assert_eq!(attempts, 0, "waiting is not a physical delivery attempt");
|
|
62
|
+
crate::mcp_server::readiness::publish(&case.workspace, "dup-team", "w1", chrono::Utc::now()).unwrap();
|
|
63
|
+
let outcome = deliver_pending_message(&case.workspace, &case.store, &transport,
|
|
64
|
+
&message_id, &case.event_log, &case.state).unwrap();
|
|
65
|
+
assert_eq!(outcome.message_id.as_deref(), Some(message_id.as_str()));
|
|
66
|
+
assert_eq!(transport.inject_count(), 1, "same queued row proceeds after the real receipt");
|
|
67
|
+
}
|
|
68
|
+
|
|
31
69
|
#[test]
|
|
32
70
|
fn error_retry_claim_is_single_winner_across_two_delivers() {
|
|
33
71
|
let case = WorkerCase::new("claim-gate", "fake");
|
|
@@ -273,6 +311,7 @@ fn poison_state_file(workspace: &Path) {
|
|
|
273
311
|
struct RecordingTransport {
|
|
274
312
|
injects: Arc<AtomicUsize>,
|
|
275
313
|
capture: Arc<Mutex<String>>,
|
|
314
|
+
carrier: Option<u32>,
|
|
276
315
|
}
|
|
277
316
|
|
|
278
317
|
impl RecordingTransport {
|
|
@@ -280,6 +319,7 @@ impl RecordingTransport {
|
|
|
280
319
|
Self {
|
|
281
320
|
injects: Arc::new(AtomicUsize::new(0)),
|
|
282
321
|
capture: Arc::new(Mutex::new(capture.to_string())),
|
|
322
|
+
carrier: None,
|
|
283
323
|
}
|
|
284
324
|
}
|
|
285
325
|
|
|
@@ -370,7 +410,7 @@ impl Transport for RecordingTransport {
|
|
|
370
410
|
current_command: Some("sleep".to_string()),
|
|
371
411
|
current_path: None,
|
|
372
412
|
active: true,
|
|
373
|
-
pane_pid:
|
|
413
|
+
pane_pid: self.carrier,
|
|
374
414
|
leader_env: BTreeMap::new(),
|
|
375
415
|
}])
|
|
376
416
|
}
|
|
@@ -214,6 +214,13 @@ pub trait ProviderAdapter {
|
|
|
214
214
|
/// compatible_api 走 `ensure_compatible_claude_mcp_config`)。
|
|
215
215
|
fn mcp_config(&self, auth_mode: AuthMode) -> Result<McpConfig, ProviderError>;
|
|
216
216
|
|
|
217
|
+
/// Eager stdio clients must publish their initial MCP handshake before input.
|
|
218
|
+
/// Pi owns admission in its runtime registry hooks; Fake has no MCP client.
|
|
219
|
+
/// The process-stamp receipt port is currently POSIX-only, not a Windows claim.
|
|
220
|
+
fn requires_worker_mcp_handshake(&self) -> bool {
|
|
221
|
+
cfg!(unix) && !matches!(self.provider(), Provider::Fake | Provider::Pi)
|
|
222
|
+
}
|
|
223
|
+
|
|
217
224
|
/// 安装 MCP server。gemini 写全局 `~/.gemini/settings.json` 并备份/还原
|
|
218
225
|
/// (`gemini.py:40-78`)——有副作用 I/O,失败可还原 → Result。
|
|
219
226
|
fn install_mcp(&self, config: &McpConfig) -> Result<(), ProviderError>;
|
|
@@ -2217,6 +2217,43 @@ fn kill_session_and_kill_window_argv() {
|
|
|
2217
2217
|
);
|
|
2218
2218
|
}
|
|
2219
2219
|
|
|
2220
|
+
#[test]
|
|
2221
|
+
fn issue336_session_listing_accepts_tab_and_literal_escape_without_rewriting_names() {
|
|
2222
|
+
for separator in ["\t", "\\t"] {
|
|
2223
|
+
let (backend, calls) = backend_with(
|
|
2224
|
+
MockResp::Out(ok("")),
|
|
2225
|
+
vec![
|
|
2226
|
+
MockResp::Out(ok(&format!("%7{separator}worker\\tname\n"))),
|
|
2227
|
+
MockResp::Out(ok("%8\n")),
|
|
2228
|
+
],
|
|
2229
|
+
);
|
|
2230
|
+
backend.preserve_session_before_stop(
|
|
2231
|
+
&SessionName::new("sess"),
|
|
2232
|
+
&[Target::SessionWindow {
|
|
2233
|
+
session: SessionName::new("sess"),
|
|
2234
|
+
window: WindowName::new("worker\\tname"),
|
|
2235
|
+
}],
|
|
2236
|
+
).unwrap();
|
|
2237
|
+
assert_eq!(calls.lock().unwrap().len(), 4, "separator={separator:?}");
|
|
2238
|
+
}
|
|
2239
|
+
}
|
|
2240
|
+
|
|
2241
|
+
#[test]
|
|
2242
|
+
fn issue336_escaped_listing_keeps_survivors_and_malformed_rows_fail_closed() {
|
|
2243
|
+
let (backend, calls) = backend_with(MockResp::Out(ok("%7\\tworker\n%8\\tleader\n")), vec![]);
|
|
2244
|
+
backend.preserve_session_before_stop(
|
|
2245
|
+
&SessionName::new("sess"), &[Target::Pane(PaneId::new("%7"))],
|
|
2246
|
+
).unwrap();
|
|
2247
|
+
assert_eq!(calls.lock().unwrap().len(), 1);
|
|
2248
|
+
for row in ["%7 worker", "%7", ""] {
|
|
2249
|
+
let (backend, calls) = backend_with(MockResp::Out(ok(row)), vec![]);
|
|
2250
|
+
assert!(backend.preserve_session_before_stop(
|
|
2251
|
+
&SessionName::new("sess"), &[Target::Pane(PaneId::new("%7"))],
|
|
2252
|
+
).is_err());
|
|
2253
|
+
assert_eq!(calls.lock().unwrap().len(), 1);
|
|
2254
|
+
}
|
|
2255
|
+
}
|
|
2256
|
+
|
|
2220
2257
|
#[test]
|
|
2221
2258
|
fn stop_session_preservation_creates_processless_anchor_by_exact_pane_id() {
|
|
2222
2259
|
let (be, rec) = backend_with(
|
|
@@ -4201,13 +4201,20 @@ impl Transport for TmuxBackend {
|
|
|
4201
4201
|
return Err(subprocess_error(list_argv, output));
|
|
4202
4202
|
}
|
|
4203
4203
|
for line in output.stdout.lines() {
|
|
4204
|
-
|
|
4204
|
+
// Some tmux/runner combinations return the literal escape. Split
|
|
4205
|
+
// only the first column boundary; window names are not unescaped.
|
|
4206
|
+
let separator = line.find('\t').map(|at| (at, 1)).into_iter()
|
|
4207
|
+
.chain(line.find("\\t").map(|at| (at, 2)))
|
|
4208
|
+
.min_by_key(|(at, _)| *at);
|
|
4209
|
+
let Some((at, width)) = separator else {
|
|
4205
4210
|
return Err(TransportError::Subprocess {
|
|
4206
4211
|
argv: list_argv,
|
|
4207
4212
|
code: output.code,
|
|
4208
4213
|
stderr: "tmux returned an invalid session pane listing".to_string(),
|
|
4209
4214
|
});
|
|
4210
4215
|
};
|
|
4216
|
+
let (pane, rest) = line.split_at(at);
|
|
4217
|
+
let window = &rest[width..];
|
|
4211
4218
|
if !targets.iter().any(|target| match target {
|
|
4212
4219
|
Target::Pane(id) => id.as_str() == pane,
|
|
4213
4220
|
Target::SessionWindow {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@team-agent/installer",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.121",
|
|
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.5.
|
|
24
|
-
"@team-agent/cli-darwin-x64": "0.5.
|
|
25
|
-
"@team-agent/cli-linux-x64": "0.5.
|
|
23
|
+
"@team-agent/cli-darwin-arm64": "0.5.121",
|
|
24
|
+
"@team-agent/cli-darwin-x64": "0.5.121",
|
|
25
|
+
"@team-agent/cli-linux-x64": "0.5.121"
|
|
26
26
|
},
|
|
27
27
|
"scripts": {
|
|
28
28
|
"postinstall": "node npm/bincheck.mjs",
|
|
@@ -5,27 +5,30 @@ requires_team_agent: ">=0.5.0"
|
|
|
5
5
|
---
|
|
6
6
|
# Team Agent
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
Use the authorized **Leader's** native tool context for lifecycle operations. Workers report through their team-scoped MCP tools; loading this skill does not promote a worker to Leader. Operate in the workspace root, not inside another team's role directory.
|
|
9
9
|
|
|
10
|
-
##
|
|
10
|
+
## Check the actual CLI
|
|
11
|
+
|
|
12
|
+
Check `team-agent --version`, `team-agent start --help` and `team-agent team --help` before using this guide. It requires the intuitive `start`, `stop`, `team` and `agent` surface. A version string or this skill's compatibility floor is not proof those commands exist. If missing, ask for an authorized installation of a supporting build; do not silently switch binaries, rewrite bindings or fall back to an old creation recipe.
|
|
13
|
+
|
|
14
|
+
## User decisions come first
|
|
11
15
|
|
|
12
16
|
**The user decides each worker's provider and permission bypass. Do not silently choose Pi, true, or false.** Reuse an explicit user policy when it covers the worker; otherwise explain the choices and ask before starting.
|
|
13
17
|
|
|
14
|
-
Use existing resources rather than establishing
|
|
18
|
+
Use existing resources rather than establishing accounts. `team-agent doctor --workspace . --json` reports discovered tools; installed is not authenticated, and an unknown probe is not proof of unavailability. Use `team-agent models --provider NAME` where supported for exact model IDs. Do not inspect credentials, automatically log in, or silently substitute providers.
|
|
15
19
|
|
|
16
|
-
| Choice |
|
|
20
|
+
| Choice | Explain |
|
|
17
21
|
|---|---|
|
|
18
|
-
| Pi |
|
|
19
|
-
| Codex | The user's existing
|
|
20
|
-
|
|
|
21
|
-
| Bypass `
|
|
22
|
-
| Bypass `false` | Keep the selected tool's native permissions; human confirmation may interrupt unattended work |
|
|
22
|
+
| Pi | Several model catalogs; an explicit model needs a qualified ID such as `openai-codex/gpt-6-luna` |
|
|
23
|
+
| Codex / Claude / other native CLIs | The user's existing workflow, native permissions, model catalog and supported session capabilities |
|
|
24
|
+
| Bypass `true` | Fewer interruptions, broader automatic authority |
|
|
25
|
+
| Bypass `false` | Native permissions remain; human confirmation may interrupt unattended work |
|
|
23
26
|
|
|
24
|
-
False is **not** a read-only or sandbox guarantee.
|
|
27
|
+
False is **not** a read-only or sandbox guarantee. Different workers may use different providers; an existing worker cannot change provider. Do not invent a model or effort when the user left it unset.
|
|
25
28
|
|
|
26
|
-
##
|
|
29
|
+
## Prepare once, start every day
|
|
27
30
|
|
|
28
|
-
Keep the goal in `TEAM.md` and each worker's instructions in `agents/*.md`. The filename stem is the worker's in-team short name
|
|
31
|
+
Keep the goal in `TEAM.md` and each worker's instructions in `agents/*.md`. The filename stem is the worker's in-team short name:
|
|
29
32
|
|
|
30
33
|
```text
|
|
31
34
|
.team/current/
|
|
@@ -33,7 +36,7 @@ Keep the goal in `TEAM.md` and each worker's instructions in `agents/*.md`. The
|
|
|
33
36
|
agents/reviewer.md
|
|
34
37
|
```
|
|
35
38
|
|
|
36
|
-
|
|
39
|
+
Record provider and boolean bypass in every new role. This example assumes the user explicitly chose Pi and bypass false; it is not a default:
|
|
37
40
|
|
|
38
41
|
```markdown
|
|
39
42
|
---
|
|
@@ -43,43 +46,58 @@ dangerously_skip_permissions: false
|
|
|
43
46
|
Review the change and report concrete regression risks.
|
|
44
47
|
```
|
|
45
48
|
|
|
49
|
+
From the authorized Leader context, select the intended workspace/team:
|
|
50
|
+
|
|
46
51
|
```sh
|
|
47
|
-
team-agent
|
|
48
|
-
team-agent
|
|
49
|
-
team-agent
|
|
52
|
+
team-agent start --from .team/current --workspace . --team project-team
|
|
53
|
+
team-agent start --workspace . --team project-team
|
|
54
|
+
team-agent send reviewer "Review this change" --workspace . --team project-team
|
|
55
|
+
team-agent inbox leader --workspace . --team project-team -n 3
|
|
50
56
|
```
|
|
51
57
|
|
|
52
|
-
|
|
58
|
+
The first `start` creates a complete configured team through the original creation path. Later `start` keeps physically verified, drift-free live seats unchanged and starts stopped non-paused seats, including explicitly stopped seats. It is not a reset. Missing configuration, ambiguous scope or unknown physical identity is a refusal, not permission to overwrite state. `--dry-run` plans without lifecycle effects; final locked admission is still deferred.
|
|
59
|
+
|
|
60
|
+
Accepted/queued send is **not** the worker's answer. Wait for a natural reply/result. `ok: true` with pending readiness is not business readiness. A live no-op does not synchronize edited role bodies or apply changed launch settings.
|
|
53
61
|
|
|
54
|
-
|
|
62
|
+
## Stop, wake and restart
|
|
55
63
|
|
|
56
64
|
```sh
|
|
57
|
-
|
|
58
|
-
team-agent
|
|
59
|
-
|
|
60
|
-
team-agent
|
|
65
|
+
team-agent stop reviewer --workspace . --team project-team
|
|
66
|
+
team-agent start reviewer --workspace . --team project-team
|
|
67
|
+
team-agent restart reviewer --workspace . --team project-team
|
|
68
|
+
team-agent stop --workspace . --team project-team --json
|
|
69
|
+
team-agent start --workspace . --team project-team
|
|
70
|
+
team-agent team restart --workspace . --team project-team --json
|
|
61
71
|
```
|
|
62
72
|
|
|
63
|
-
|
|
73
|
+
| Intent | Rule |
|
|
74
|
+
|---|---|
|
|
75
|
+
| `start AGENT` | Wake a stopped registered seat; verified live/no-drift is a no-op. A RoleOnly seat can be added from its complete role file |
|
|
76
|
+
| `start` | Explicitly activate the selected non-paused roster; do not use it unless that activation is authorized |
|
|
77
|
+
| `stop AGENT` / `stop` | Retain saved session associations; whole-team stop also checks scoped residual resources |
|
|
78
|
+
| `restart AGENT` / `team restart` | Default `--session auto`: resume when proven, otherwise only the original legal fresh fallback. **Auto does not guarantee preserved memory** |
|
|
79
|
+
| `--session preserve` | Require available continuation evidence; refuse before stopping when continuation is known unavailable |
|
|
80
|
+
|
|
81
|
+
Team restart preserves explicitly stopped and paused seats; use `start` when intentionally waking the roster. Paused seats are not implicitly unpaused. A same-named directory makes `restart ID` ambiguous: use `agent restart ID`. `reviewer.v2` remains an agent ID; choose a team source with `--from DIR`, `team start DIR` or an explicit path such as `./roles`.
|
|
82
|
+
|
|
83
|
+
For strict recovery, prefer `team-agent team restart --session preserve --workspace . --team project-team`. Do not convert a refusal or partial/unknown replacement into reset, force or blind retry. Inspect `status`/`doctor` in the same scope first. Native unsupported capabilities remain refusals, never a legacy fallback.
|
|
64
84
|
|
|
65
|
-
##
|
|
85
|
+
## Add or change one worker
|
|
66
86
|
|
|
67
|
-
|
|
87
|
+
With the user's provider/bypass choices, `start` unifies adding an absent seat and waking an existing one:
|
|
68
88
|
|
|
69
89
|
```sh
|
|
70
|
-
team-agent
|
|
71
|
-
team-agent start
|
|
72
|
-
team-agent stop
|
|
73
|
-
team-agent start
|
|
90
|
+
team-agent start auditor --role-file /absolute/path/to/auditor.md
|
|
91
|
+
team-agent start analyst --provider pi --bypass false --prompt "Analyze the proposed change"
|
|
92
|
+
team-agent stop reviewer
|
|
93
|
+
team-agent start reviewer --effort high --bypass false
|
|
74
94
|
```
|
|
75
95
|
|
|
76
|
-
|
|
96
|
+
The Pi/false example above still requires the user's explicit choices. File/CLI conflicts are rejected before writing or startup. Omitted settings/body are preserved; `--prompt` replaces the permanent role body, not a one-time task. Stop a running worker before changing settings or instructions. Provider changes are refused. Startup failure restores role/spec bytes and reports failure, not an applied update.
|
|
77
97
|
|
|
78
|
-
|
|
98
|
+
Use `agent add/remove/reset/clone/fork` only for those explicit operations. Reset needs intentional approval and `--discard-session`; fork requires `--as NEW_ID`, and `--in-place` is not supported. See [the operator reference](references/team-agent-operator.md) for capability, profile, force and import boundaries. Never put secrets in role text or copy authentication from another directory.
|
|
79
99
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
## Global native CLI argv (optional)
|
|
100
|
+
## Optional native CLI argv routing
|
|
83
101
|
|
|
84
102
|
Default OFF; no workspace or provider login is needed to manage routes:
|
|
85
103
|
|
|
@@ -93,29 +111,28 @@ team-agent route clear pi
|
|
|
93
111
|
team-agent route disable
|
|
94
112
|
```
|
|
95
113
|
|
|
96
|
-
`set` replaces one mapping, `add` appends without deduplication, `clear` removes it;
|
|
97
|
-
none changes the switch. `enable`/`disable` preserve mappings in `~/.team-agent/argv-routing.json`.
|
|
98
|
-
`TEAM_AGENT_CLI_ARGV_ROUTING` overrides the switch: `1/true/on`, `0/false/off` (case-insensitive,
|
|
99
|
-
trimmed); invalid values disable routing. Check `persisted_enabled`, `effective_enabled` and
|
|
100
|
-
`override_status` rather than assuming `enable` defeats an environment OFF.
|
|
114
|
+
`set` replaces one mapping, `add` appends without deduplication, `clear` removes it; none changes the switch. `enable`/`disable` preserve mappings in `~/.team-agent/argv-routing.json`. `TEAM_AGENT_CLI_ARGV_ROUTING` overrides the switch: `1/true/on`, `0/false/off` (case-insensitive, trimmed); invalid values disable routing. Check `persisted_enabled`, `effective_enabled` and `override_status`.
|
|
101
115
|
|
|
102
|
-
Keys: `claude`, `codex`, `copilot`, `gemini_cli`, `grok`, `cursor_agent`, `pi`.
|
|
103
|
-
Claude/Claude Code share `claude`; aliases `claude_code/claude-code`, `agent/cursor` are accepted.
|
|
104
|
-
The first `--` ends control parsing: subsequent tokens, including `--help`, `--json`, empty strings
|
|
105
|
-
and `{workspace}`, are literal data. Tokens go after the executable and before its original tail;
|
|
106
|
-
there is no shell expansion. Do not route secrets or override framework-managed session/MCP/permission flags.
|
|
116
|
+
Keys: `claude`, `codex`, `copilot`, `gemini_cli`, `grok`, `cursor_agent`, `pi`. Claude/Claude Code share `claude`; aliases `claude_code/claude-code`, `agent/cursor` are accepted. Everything after the first `--`, including `--help`, `--json`, empty strings and `{workspace}`, is literal argv. No shell expansion. Do not route secrets or override framework-managed session/MCP/permission flags.
|
|
107
117
|
|
|
108
|
-
|
|
109
|
-
add/clone, Pi new-seat fork and remove rollback), not running/attached processes, dry-run or helper probes.
|
|
110
|
-
Enabled bad configuration prevents native spawn; an emergency environment OFF avoids reading it,
|
|
111
|
-
while `route disable` will not overwrite a broken file. Pi `--mode rpc` does not provide an RPC host
|
|
112
|
-
or change the existing Team Agent transport.
|
|
118
|
+
Mappings affect the next actual native spawn, not running/attached processes, dry-run or helper probes. Invalid enabled configuration refuses spawn; an emergency environment OFF avoids reading it, while `route disable` will not overwrite a broken file. Pi `--mode rpc` does not provide an RPC host or replace Team Agent's transport.
|
|
113
119
|
|
|
114
|
-
##
|
|
120
|
+
## Observe, communicate and clean up
|
|
115
121
|
|
|
116
|
-
Use `team-agent status --json`
|
|
122
|
+
Use `team-agent status --json` (or `team status`) for the concise node projection and `doctor` for diagnosis; `--detail` does not restore old internal status dumps. Use `<workspace>::<team>/<agent>` for an explicitly qualified logical recipient, or the in-team short name. Workers have exactly `send_message`, `report_result` and optional `get_team_status`; lifecycle stays on the authorized CLI. Read [nested teams](references/team-in-team.md) only when explicitly requested and [recovery](references/recovery-runbook.md) for incidents.
|
|
117
123
|
|
|
118
|
-
|
|
124
|
+
Finish with `team-agent stop --workspace . --team project-team --json` when authorized. Check the actual scoped residual report; exit0 alone is not proof of zero resources. Do not kill shared servers or touch sibling workspaces.
|
|
119
125
|
|
|
120
|
-
|
|
121
|
-
|
|
126
|
+
## Compatibility bridges, not the daily workflow
|
|
127
|
+
|
|
128
|
+
Old commands remain available for existing scripts; their output/defaults are not silently rewritten:
|
|
129
|
+
|
|
130
|
+
| Compatibility entry | Preferred daily surface |
|
|
131
|
+
|---|---|
|
|
132
|
+
| `quick-start` | `start --from TEAMDIR` for creation and later `start` for activation |
|
|
133
|
+
| `shutdown` | `stop` / `team stop` |
|
|
134
|
+
| `add-agent` / `start-agent` / `stop-agent` | `agent add` for strict add, or `start AGENT` / `stop AGENT` |
|
|
135
|
+
| `remove-agent` / `reset-agent` / `clone-agent` / `fork-agent` | The corresponding `agent` operation |
|
|
136
|
+
| Bare or explicit-path `restart` | Frozen preserve bridge; deliberate `team restart` defaults to auto |
|
|
137
|
+
|
|
138
|
+
These are not all byte-equivalent aliases: legacy single-seat start refuses a running seat, while new start can prove a no-op. Do not mechanically rename automation, assume unsupported provider capabilities, or infer deployment from package metadata.
|