@team-agent/installer 0.3.2 → 0.3.4
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 +34 -1
- package/Cargo.toml +1 -1
- package/crates/team-agent/Cargo.toml +1 -1
- package/crates/team-agent/src/cli/adapters.rs +196 -19
- package/crates/team-agent/src/cli/diagnose.rs +145 -11
- package/crates/team-agent/src/cli/emit.rs +287 -53
- package/crates/team-agent/src/cli/leader.rs +37 -8
- package/crates/team-agent/src/cli/mod.rs +807 -316
- package/crates/team-agent/src/cli/status_port.rs +25 -2
- package/crates/team-agent/src/cli/tests/divergence.rs +1 -2
- package/crates/team-agent/src/cli/tests/lane_c.rs +23 -13
- package/crates/team-agent/src/cli/tests/main_preserved.rs +2 -0
- package/crates/team-agent/src/cli/tests/run_delegation.rs +57 -3
- package/crates/team-agent/src/cli/types.rs +17 -0
- package/crates/team-agent/src/compiler/tests.rs +2 -2
- package/crates/team-agent/src/compiler.rs +16 -6
- package/crates/team-agent/src/coordinator/health.rs +89 -20
- package/crates/team-agent/src/coordinator/mod.rs +4 -0
- package/crates/team-agent/src/coordinator/runtime_detectors.rs +500 -0
- package/crates/team-agent/src/coordinator/runtime_observation.rs +58 -0
- package/crates/team-agent/src/coordinator/tests/watch.rs +4 -2
- package/crates/team-agent/src/coordinator/tick.rs +222 -69
- package/crates/team-agent/src/coordinator/types.rs +15 -3
- package/crates/team-agent/src/db/schema.rs +37 -2
- package/crates/team-agent/src/diagnose/comms.rs +226 -0
- package/crates/team-agent/src/diagnose/mod.rs +45 -0
- package/crates/team-agent/src/diagnose/orphans.rs +658 -0
- package/crates/team-agent/src/fake_worker.rs +146 -3
- package/crates/team-agent/src/leader/start.rs +121 -23
- package/crates/team-agent/src/leader/types.rs +44 -1
- package/crates/team-agent/src/lib.rs +3 -0
- package/crates/team-agent/src/lifecycle/display.rs +648 -50
- package/crates/team-agent/src/lifecycle/launch.rs +1048 -264
- package/crates/team-agent/src/lifecycle/mod.rs +3 -0
- package/crates/team-agent/src/lifecycle/profile_launch.rs +810 -0
- package/crates/team-agent/src/lifecycle/profile_smoke.rs +522 -0
- package/crates/team-agent/src/lifecycle/restart/agent.rs +113 -26
- package/crates/team-agent/src/lifecycle/restart/common.rs +189 -102
- package/crates/team-agent/src/lifecycle/restart/rebuild.rs +465 -25
- package/crates/team-agent/src/lifecycle/restart/remove.rs +22 -6
- package/crates/team-agent/src/lifecycle/restart/team_state.rs +19 -0
- package/crates/team-agent/src/lifecycle/restart.rs +4 -1
- package/crates/team-agent/src/lifecycle/tests/core.rs +4 -4
- package/crates/team-agent/src/lifecycle/tests/lane_ops.rs +5 -5
- package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +39 -9
- package/crates/team-agent/src/lifecycle/types.rs +23 -0
- package/crates/team-agent/src/lifecycle/worker_command_context.rs +326 -0
- package/crates/team-agent/src/mcp_server/helpers.rs +1 -0
- package/crates/team-agent/src/mcp_server/lifecycle_tools/agent_ops.rs +341 -0
- package/crates/team-agent/src/mcp_server/lifecycle_tools/mod.rs +10 -0
- package/crates/team-agent/src/mcp_server/lifecycle_tools/state_status.rs +158 -0
- package/crates/team-agent/src/mcp_server/mod.rs +3 -74
- package/crates/team-agent/src/mcp_server/tests/scoped.rs +1 -1
- package/crates/team-agent/src/mcp_server/tests/send.rs +6 -5
- package/crates/team-agent/src/mcp_server/tools.rs +312 -111
- package/crates/team-agent/src/mcp_server/types.rs +6 -4
- package/crates/team-agent/src/mcp_server/wire.rs +19 -7
- package/crates/team-agent/src/message_store.rs +21 -4
- package/crates/team-agent/src/messaging/delivery.rs +87 -37
- package/crates/team-agent/src/messaging/mod.rs +9 -6
- package/crates/team-agent/src/messaging/results.rs +153 -16
- package/crates/team-agent/src/messaging/selftest.rs +199 -12
- package/crates/team-agent/src/messaging/send.rs +35 -3
- package/crates/team-agent/src/messaging/tests/runtime.rs +19 -4
- package/crates/team-agent/src/messaging/types.rs +11 -3
- package/crates/team-agent/src/os_probe.rs +119 -0
- package/crates/team-agent/src/packaging/migrate.rs +10 -2
- package/crates/team-agent/src/packaging/tests.rs +23 -0
- package/crates/team-agent/src/provider/adapter.rs +483 -67
- package/crates/team-agent/src/provider/approvals/runtime_prompts.rs +1 -7
- package/crates/team-agent/src/provider/classify.rs +51 -4
- package/crates/team-agent/src/provider/startup_prompt.rs +94 -0
- package/crates/team-agent/src/provider/types.rs +47 -0
- package/crates/team-agent/src/session_capture.rs +616 -0
- package/crates/team-agent/src/state/persist.rs +57 -0
- package/crates/team-agent/src/state/projection.rs +32 -23
- package/crates/team-agent/src/state/selector.rs +5 -2
- package/crates/team-agent/src/tmux_backend.rs +151 -60
- package/crates/team-agent/src/transport/test_support.rs +9 -0
- package/crates/team-agent/src/transport/tests/wire.rs +4 -0
- package/crates/team-agent/src/transport.rs +13 -2
- package/package.json +4 -4
|
@@ -37,10 +37,24 @@ use rusqlite::params;
|
|
|
37
37
|
.cloned()
|
|
38
38
|
.unwrap_or_else(|| json!({}));
|
|
39
39
|
let session_name = state.get("session_name").cloned().unwrap_or(Value::Null);
|
|
40
|
+
let tmux_present = tmux_session_present(workspace, session_name.as_str());
|
|
41
|
+
let mut readiness_state = state.clone();
|
|
42
|
+
if let Some(obj) = readiness_state.as_object_mut() {
|
|
43
|
+
obj.insert("tmux_session_present".to_string(), serde_json::json!(tmux_present));
|
|
44
|
+
}
|
|
45
|
+
let readiness = crate::cli::diagnose::wait_readiness(&readiness_state);
|
|
40
46
|
let full = json!({
|
|
47
|
+
"ok": true,
|
|
41
48
|
"team": state.pointer("/leader/id").cloned().unwrap_or_else(|| json!("leader")),
|
|
42
49
|
"session_name": state.get("session_name").cloned().unwrap_or(Value::Null),
|
|
43
|
-
"tmux_session_present":
|
|
50
|
+
"tmux_session_present": tmux_present,
|
|
51
|
+
"all_spawned": readiness.get("all_spawned").cloned().unwrap_or(Value::Bool(false)),
|
|
52
|
+
"all_attached_receiver": readiness.get("all_attached_receiver").cloned().unwrap_or(Value::Bool(true)),
|
|
53
|
+
"all_resumable_have_session": readiness.get("all_resumable_have_session").cloned().unwrap_or(Value::Bool(true)),
|
|
54
|
+
"session_capture_complete": readiness.get("session_capture_complete").cloned().unwrap_or(Value::Bool(true)),
|
|
55
|
+
"session_capture_incomplete": readiness.get("session_capture_incomplete").cloned().unwrap_or(Value::Bool(false)),
|
|
56
|
+
"incomplete_session_capture_agents": readiness.get("incomplete_session_capture_agents").cloned().unwrap_or_else(|| json!([])),
|
|
57
|
+
"pending_session_agent_ids": readiness.get("pending_session_agent_ids").cloned().unwrap_or_else(|| json!([])),
|
|
44
58
|
"leader_receiver": leader_receiver,
|
|
45
59
|
"teams": state.get("teams").cloned().unwrap_or_else(|| json!({})),
|
|
46
60
|
"agents": agents,
|
|
@@ -50,6 +64,7 @@ use rusqlite::params;
|
|
|
50
64
|
"queued_messages": queued_messages(&conn, owner_team_id, 8)?,
|
|
51
65
|
"results": result_counts(&conn, owner_team_id)?,
|
|
52
66
|
"latest_results": json!([]),
|
|
67
|
+
"readiness": readiness,
|
|
53
68
|
"coordinator": coordinator_health_value(health),
|
|
54
69
|
"last_events": Value::Array(
|
|
55
70
|
crate::event_log::EventLog::new(workspace)
|
|
@@ -401,6 +416,13 @@ use rusqlite::params;
|
|
|
401
416
|
"team": full.get("team").cloned().unwrap_or(Value::Null),
|
|
402
417
|
"session_name": full.get("session_name").cloned().unwrap_or(Value::Null),
|
|
403
418
|
"tmux_session_present": full.get("tmux_session_present").cloned().unwrap_or(Value::Bool(false)),
|
|
419
|
+
"all_spawned": full.get("all_spawned").cloned().unwrap_or(Value::Bool(false)),
|
|
420
|
+
"all_attached_receiver": full.get("all_attached_receiver").cloned().unwrap_or(Value::Bool(true)),
|
|
421
|
+
"all_resumable_have_session": full.get("all_resumable_have_session").cloned().unwrap_or(Value::Bool(true)),
|
|
422
|
+
"session_capture_complete": full.get("session_capture_complete").cloned().unwrap_or(Value::Bool(true)),
|
|
423
|
+
"session_capture_incomplete": full.get("session_capture_incomplete").cloned().unwrap_or(Value::Bool(false)),
|
|
424
|
+
"incomplete_session_capture_agents": full.get("incomplete_session_capture_agents").cloned().unwrap_or_else(|| json!([])),
|
|
425
|
+
"pending_session_agent_ids": full.get("pending_session_agent_ids").cloned().unwrap_or_else(|| json!([])),
|
|
404
426
|
"leader_receiver": compact_object(full.get("leader_receiver"), &[
|
|
405
427
|
"status", "provider", "mode", "session_name", "window_name", "pane_id", "pane_current_command",
|
|
406
428
|
]),
|
|
@@ -411,6 +433,7 @@ use rusqlite::params;
|
|
|
411
433
|
"queued_messages": take_array(full.get("queued_messages"), 8),
|
|
412
434
|
"results": full.get("results").cloned().unwrap_or_else(|| json!({})),
|
|
413
435
|
"latest_results": take_array(full.get("latest_results"), 5),
|
|
436
|
+
"readiness": full.get("readiness").cloned().unwrap_or_else(|| json!({})),
|
|
414
437
|
"coordinator": compact_object(full.get("coordinator"), &["status", "pid", "metadata_ok", "schema_ok"]),
|
|
415
438
|
"last_events": take_array_tail(full.get("last_events"), 10),
|
|
416
439
|
})
|
|
@@ -470,7 +493,7 @@ use rusqlite::params;
|
|
|
470
493
|
};
|
|
471
494
|
Value::Array(
|
|
472
495
|
tasks.iter()
|
|
473
|
-
.map(|task| compact_object(Some(task), &["id", "title", "status", "assignee", "type"]))
|
|
496
|
+
.map(|task| compact_object(Some(task), &["id", "title", "status", "assignee", "type", "accepted_result_id"]))
|
|
474
497
|
.collect(),
|
|
475
498
|
)
|
|
476
499
|
}
|
|
@@ -456,7 +456,7 @@ Hint: team-agent inbox leader";
|
|
|
456
456
|
// Rust always does CmdResult::from_json -> CmdOutput::Json (wrong shape).
|
|
457
457
|
#[test]
|
|
458
458
|
fn red_cmd_doctor_comms_human_is_boundary_text_plus_sorted_json() {
|
|
459
|
-
const COMMS_BOUNDARY_TEXT: &str = "validates live pane binding consistency. Does NOT perform live runtime message round-trip.
|
|
459
|
+
const COMMS_BOUNDARY_TEXT: &str = "validates live pane binding consistency and zero-token comms contracts. Does NOT perform live runtime message round-trip. (zero token, zero pollution)";
|
|
460
460
|
let args = DoctorArgs {
|
|
461
461
|
spec: None,
|
|
462
462
|
workspace: PathBuf::from("."),
|
|
@@ -506,4 +506,3 @@ Hint: team-agent inbox leader";
|
|
|
506
506
|
assert_eq!(run(&["codex".to_string(), "-h".to_string()], Path::new(".")), ExitCode::Ok);
|
|
507
507
|
assert_eq!(run(&["claude".to_string(), "-h".to_string()], Path::new(".")), ExitCode::Ok);
|
|
508
508
|
}
|
|
509
|
-
|
|
@@ -216,27 +216,35 @@ use super::*;
|
|
|
216
216
|
// the deterministic check sub-shapes (run_id is a random uuid; not value-locked). ────────────────
|
|
217
217
|
#[test]
|
|
218
218
|
fn comms_selftest_golden_boundary_scope_and_check_shapes() {
|
|
219
|
-
let boundary = "validates live pane binding consistency
|
|
220
|
-
|
|
221
|
-
(zero token, zero pollution)";
|
|
219
|
+
let boundary = "validates live pane binding consistency and zero-token comms contracts. \
|
|
220
|
+
Does NOT perform live runtime message round-trip. (zero token, zero pollution)";
|
|
222
221
|
let ws = tmp_workspace();
|
|
223
222
|
let v = diagnose_port::comms_selftest(&ws, None, None).expect("comms_selftest");
|
|
224
223
|
let obj = v.as_object().expect("comms dict");
|
|
225
224
|
assert_eq!(v["boundary"], json!(boundary), "golden COMMS_BOUNDARY_TEXT prefix (comms.py:11-14)");
|
|
226
225
|
assert_eq!(v["scope"], json!("binding_consistency"), "golden scope");
|
|
227
|
-
assert_eq!(
|
|
226
|
+
assert_eq!(
|
|
227
|
+
v["status"],
|
|
228
|
+
json!("fail"),
|
|
229
|
+
"empty workspace has no runtime receiver binding, so comms gate must not pass"
|
|
230
|
+
);
|
|
228
231
|
assert!(obj.contains_key("run_id"), "golden carries a run_id (uuid hex[:12])");
|
|
229
232
|
assert!(!obj.contains_key("team"), "golden has NO `team` key");
|
|
230
233
|
assert!(!obj.contains_key("gate"), "golden has NO `gate` key");
|
|
231
|
-
assert_eq!(
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
234
|
+
assert_eq!(v["checks"]["contract_suite"]["status"], json!("pass"));
|
|
235
|
+
assert_eq!(v["checks"]["contract_suite"]["failed"], json!([]));
|
|
236
|
+
let suite_names: Vec<&str> = v["checks"]["contract_suite"]["checks"]
|
|
237
|
+
.as_array()
|
|
238
|
+
.expect("contract suite checks")
|
|
239
|
+
.iter()
|
|
240
|
+
.filter_map(|check| check.get("name").and_then(Value::as_str))
|
|
241
|
+
.collect();
|
|
242
|
+
assert!(
|
|
243
|
+
suite_names.contains(&"message_store_schema")
|
|
244
|
+
&& suite_names.contains(&"message_token_shape")
|
|
245
|
+
&& suite_names.contains(&"result_notification_render")
|
|
246
|
+
&& suite_names.contains(&"leader_projection_owner_team"),
|
|
247
|
+
"contract suite must be executable, not deferred: {suite_names:?}"
|
|
240
248
|
);
|
|
241
249
|
assert_eq!(
|
|
242
250
|
v["checks"]["provider_sdk_calls"]["calls"],
|
|
@@ -271,6 +279,8 @@ use super::*;
|
|
|
271
279
|
// {ok,scanned,orphans,dry_run:true,scanned_at,action_required}. RUST mod.rs:534-535 stub
|
|
272
280
|
// {ok,confirm,cleaned}. scanned/scanned_at are machine/clock-derived; lock the deterministic ones. RED.
|
|
273
281
|
#[test]
|
|
282
|
+
#[ignore = "real-machine: cleanup_orphans scans machine-wide ta-* tmux/process residue"]
|
|
283
|
+
#[serial_test::file_serial(tmux)]
|
|
274
284
|
fn cleanup_orphans_dryrun_golden_envelope() {
|
|
275
285
|
let v = diagnose_port::cleanup_orphans(/*confirm=*/ false).expect("cleanup_orphans");
|
|
276
286
|
let obj = v.as_object().expect("cleanup dict");
|
|
@@ -4,6 +4,39 @@ struct TeamSocketGuard {
|
|
|
4
4
|
ws: std::path::PathBuf,
|
|
5
5
|
}
|
|
6
6
|
|
|
7
|
+
struct EnvGuard {
|
|
8
|
+
previous: Vec<(&'static str, Option<String>)>,
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
impl EnvGuard {
|
|
12
|
+
fn unset(keys: &[&'static str]) -> Self {
|
|
13
|
+
let previous = keys
|
|
14
|
+
.iter()
|
|
15
|
+
.map(|key| (*key, std::env::var(key).ok()))
|
|
16
|
+
.collect::<Vec<_>>();
|
|
17
|
+
for key in keys {
|
|
18
|
+
unsafe {
|
|
19
|
+
std::env::remove_var(key);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
Self { previous }
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
impl Drop for EnvGuard {
|
|
27
|
+
fn drop(&mut self) {
|
|
28
|
+
for (key, value) in self.previous.drain(..).rev() {
|
|
29
|
+
unsafe {
|
|
30
|
+
if let Some(value) = value {
|
|
31
|
+
std::env::set_var(key, value);
|
|
32
|
+
} else {
|
|
33
|
+
std::env::remove_var(key);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
7
40
|
impl Drop for TeamSocketGuard {
|
|
8
41
|
fn drop(&mut self) {
|
|
9
42
|
crate::tmux_backend::TmuxBackend::for_workspace(&self.ws).kill_server();
|
|
@@ -122,7 +155,18 @@ fn current_uid() -> Option<String> {
|
|
|
122
155
|
#[ignore = "real-machine: quick-start --yes spawns a real team (tmux) + coordinator daemon"]
|
|
123
156
|
fn run_dispatches_quick_start_compiles_spec() {
|
|
124
157
|
// The full quick-start path spawns workers + the coordinator; on a real machine we assert it
|
|
125
|
-
// dispatched to cmd_quick_start (team.spec.yaml compiled under the team dir)
|
|
158
|
+
// dispatched to cmd_quick_start (team.spec.yaml compiled under the team dir). With no positive
|
|
159
|
+
// caller pane, honest-readiness reports leader_receiver_unbound and exits nonzero.
|
|
160
|
+
let _env = EnvGuard::unset(&[
|
|
161
|
+
"TMUX",
|
|
162
|
+
"TMUX_PANE",
|
|
163
|
+
"TEAM_AGENT_ID",
|
|
164
|
+
"TEAM_AGENT_TEAM_ID",
|
|
165
|
+
"TEAM_AGENT_LEADER_PANE_ID",
|
|
166
|
+
"TEAM_AGENT_LEADER_SESSION_UUID",
|
|
167
|
+
"TEAM_AGENT_LEADER_SESSION_UUID_OVERRIDE",
|
|
168
|
+
"TEAM_AGENT_LEADER_PROVIDER",
|
|
169
|
+
]);
|
|
126
170
|
let dir = std::env::temp_dir().join(format!("ta-cli-qs-{}", std::process::id()));
|
|
127
171
|
std::fs::create_dir_all(dir.join("agents")).unwrap();
|
|
128
172
|
std::fs::write(dir.join("TEAM.md"), "---\nname: t\nobjective: o\nprovider: codex\n---\n\nteam.\n").unwrap();
|
|
@@ -134,7 +178,11 @@ fn current_uid() -> Option<String> {
|
|
|
134
178
|
let argv = vec!["quick-start".to_string(), dir.to_string_lossy().to_string(), "--yes".to_string()];
|
|
135
179
|
let exit = run(&argv, Path::new("."));
|
|
136
180
|
assert!(dir.join("team.spec.yaml").exists(), "quick-start must compile the spec under the team dir");
|
|
137
|
-
assert_eq!(
|
|
181
|
+
assert_eq!(
|
|
182
|
+
exit,
|
|
183
|
+
ExitCode::Error,
|
|
184
|
+
"quick-start must still dispatch and compile, but an unbound leader receiver is an honest-readiness failure, not ExitCode::Ok"
|
|
185
|
+
);
|
|
138
186
|
}
|
|
139
187
|
|
|
140
188
|
// =========================================================================
|
|
@@ -200,7 +248,13 @@ fn current_uid() -> Option<String> {
|
|
|
200
248
|
#[test]
|
|
201
249
|
fn cli_restart_missing_spec_surfaces_real_teamselect() {
|
|
202
250
|
let ws = deleg_uniq_dir("restart"); // no team.spec.yaml
|
|
203
|
-
let args = RestartArgs {
|
|
251
|
+
let args = RestartArgs {
|
|
252
|
+
workspace: ws,
|
|
253
|
+
team: None,
|
|
254
|
+
allow_fresh: false,
|
|
255
|
+
session_converge_deadline_ms: None,
|
|
256
|
+
json: true,
|
|
257
|
+
};
|
|
204
258
|
let text = outcome_text(cmd_restart(&args));
|
|
205
259
|
assert!(
|
|
206
260
|
text.contains("missing spec for restart"),
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
//! 每子命令的 clap-style arg 结构 / 五行 summary 计数桶。
|
|
3
3
|
|
|
4
4
|
use super::*;
|
|
5
|
+
use crate::provider::Provider;
|
|
6
|
+
use crate::transport::PaneId;
|
|
5
7
|
|
|
6
8
|
// =============================================================================
|
|
7
9
|
// ERRORS / EXIT(helpers.py `_emit_cli_error` / `_cli_error_payload`)
|
|
@@ -363,6 +365,18 @@ pub struct ClaimLeaderArgs {
|
|
|
363
365
|
pub json: bool,
|
|
364
366
|
}
|
|
365
367
|
|
|
368
|
+
/// `attach-leader` public CLI args. `cmd_attach_leader` consumes the typed pane/provider
|
|
369
|
+
/// fields and returns/writes a `leader_receiver` binding through the leader lease port.
|
|
370
|
+
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
371
|
+
pub struct AttachLeaderArgs {
|
|
372
|
+
pub workspace: PathBuf,
|
|
373
|
+
pub team: Option<String>,
|
|
374
|
+
pub pane: Option<PaneId>,
|
|
375
|
+
pub provider: Provider,
|
|
376
|
+
pub confirm: bool,
|
|
377
|
+
pub json: bool,
|
|
378
|
+
}
|
|
379
|
+
|
|
366
380
|
/// `identity`(`parser.py:256`)。
|
|
367
381
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
368
382
|
pub struct IdentityArgs {
|
|
@@ -386,6 +400,7 @@ pub struct RestartArgs {
|
|
|
386
400
|
pub workspace: PathBuf,
|
|
387
401
|
pub team: Option<String>,
|
|
388
402
|
pub allow_fresh: bool,
|
|
403
|
+
pub session_converge_deadline_ms: Option<u64>,
|
|
389
404
|
pub json: bool,
|
|
390
405
|
}
|
|
391
406
|
|
|
@@ -601,6 +616,8 @@ pub struct PeekArgs {
|
|
|
601
616
|
pub agent: String,
|
|
602
617
|
pub workspace: PathBuf,
|
|
603
618
|
pub tail: usize,
|
|
619
|
+
pub head: Option<usize>,
|
|
620
|
+
pub search: Option<String>,
|
|
604
621
|
pub allow_raw_screen: bool,
|
|
605
622
|
pub json: bool,
|
|
606
623
|
}
|
|
@@ -179,7 +179,7 @@ fn front_matter_non_object_errors() {
|
|
|
179
179
|
// separators=(",",":"))` with the workspace path templated to __WS__.
|
|
180
180
|
// (team-agent-public v0.2.11, /tmp/probe_compiler.py.)
|
|
181
181
|
|
|
182
|
-
const BASE_NOPROFILE_JSON: &str = r#"{"version":1,"team":{"name":"doc-team","mode":"supervisor_worker","objective":"Compile role docs.","workspace":"__WS__"},"leader":{"id":"leader","role":"leader","provider":"codex","model":"gpt-5.5","tools":["fs_read","fs_list","mcp_team"],"context_policy":{"keep_user_thread":true,"receive_worker_outputs":"business_messages_and_short_summaries","max_worker_result_tokens":2000}},"agents":[{"id":"implementer","role":"Implementation Engineer","provider":"codex","model":"gpt-5.5","auth_mode":"subscription","working_directory":"__WS__","system_prompt":{"inline":"Implement bounded tasks and report result_envelope_v1.","file":null},"tools":["fs_read","fs_write","execute_bash","mcp_team"],"permission_mode":"restricted","preferred_for":["implementer","Implementation Engineer"],"avoid_for":[],"output_contract":{"format":"result_envelope_v1","required_fields":["task_id","status","summary","artifacts"]}}],"routing":{"default_assignee":"implementer","rules":[{"id":"route-implementer","match":{"assignee":["implementer"]},"assign_to":"implementer","priority":10}]},"communication":{"protocol":"mcp_inbox","topology":"leader_centered","worker_to_worker":true,"ack_timeout_sec":60,"result_format":"result_envelope_v1","message_store":{"sqlite":".team/runtime/team.db","mirror_files":".team/messages"}},"runtime":{"backend":"tmux","display_backend":"
|
|
182
|
+
const BASE_NOPROFILE_JSON: &str = r#"{"version":1,"team":{"name":"doc-team","mode":"supervisor_worker","objective":"Compile role docs.","workspace":"__WS__"},"leader":{"id":"leader","role":"leader","provider":"codex","model":"gpt-5.5","tools":["fs_read","fs_list","mcp_team"],"context_policy":{"keep_user_thread":true,"receive_worker_outputs":"business_messages_and_short_summaries","max_worker_result_tokens":2000}},"agents":[{"id":"implementer","role":"Implementation Engineer","provider":"codex","model":"gpt-5.5","auth_mode":"subscription","working_directory":"__WS__","system_prompt":{"inline":"Implement bounded tasks and report result_envelope_v1.","file":null},"tools":["fs_read","fs_write","execute_bash","mcp_team"],"permission_mode":"restricted","preferred_for":["implementer","Implementation Engineer"],"avoid_for":[],"output_contract":{"format":"result_envelope_v1","required_fields":["task_id","status","summary","artifacts"]}}],"routing":{"default_assignee":"implementer","rules":[{"id":"route-implementer","match":{"assignee":["implementer"]},"assign_to":"implementer","priority":10}]},"communication":{"protocol":"mcp_inbox","topology":"leader_centered","worker_to_worker":true,"ack_timeout_sec":60,"result_format":"result_envelope_v1","message_store":{"sqlite":".team/runtime/team.db","mirror_files":".team/messages"}},"runtime":{"backend":"tmux","display_backend":"none","session_name":"team-doc-team","auto_launch":true,"require_user_approval_before_launch":true,"max_active_agents":1,"startup_order":["implementer"],"dangerous_auto_approve":false,"fast":false,"tick_interval_sec":2,"push_min_interval_sec":60,"stuck_timeout_sec":300},"context":{"state_file":"team_state.md","artifact_dir":".team/artifacts","log_dir":".team/logs","summarization":{"worker_full_logs":"retain_outside_leader_context","state_update":"after_each_result"}},"tasks":[{"id":"task_initial","title":"Initial document-driven team task","type":"implementation","assignee":"implementer","deps":[],"acceptance":["Worker reports valid result_envelope_v1"],"status":"pending","requires_tools":["mcp_team"],"files":[],"risk":"low"}]}"#;
|
|
183
183
|
|
|
184
184
|
#[test]
|
|
185
185
|
fn compile_base_noprofile_matches_python_dict_order_and_values() {
|
|
@@ -352,7 +352,7 @@ tools:
|
|
|
352
352
|
Bravo body.
|
|
353
353
|
";
|
|
354
354
|
|
|
355
|
-
const TWO_AGENTS_JSON: &str = r#"{"version":1,"team":{"name":"doc-team","mode":"supervisor_worker","objective":"Compile role docs.","workspace":"__WS__"},"leader":{"id":"leader","role":"leader","provider":"codex","model":"gpt-5.5","tools":["fs_read","fs_list","mcp_team"],"context_policy":{"keep_user_thread":true,"receive_worker_outputs":"business_messages_and_short_summaries","max_worker_result_tokens":2000}},"agents":[{"id":"alpha","role":"Alpha Worker","provider":"codex","model":"gpt-5.5","auth_mode":"subscription","working_directory":"__WS__","system_prompt":{"inline":"Alpha body.","file":null},"tools":["mcp_team"],"permission_mode":"restricted","preferred_for":["alpha","Alpha Worker"],"avoid_for":[],"output_contract":{"format":"result_envelope_v1","required_fields":["task_id","status","summary","artifacts"]}},{"id":"bravo","role":"Bravo Worker","provider":"codex","model":"gpt-5.5","auth_mode":"subscription","working_directory":"__WS__","system_prompt":{"inline":"Bravo body.","file":null},"tools":["mcp_team"],"permission_mode":"restricted","preferred_for":["bravo","Bravo Worker"],"avoid_for":[],"output_contract":{"format":"result_envelope_v1","required_fields":["task_id","status","summary","artifacts"]}}],"routing":{"default_assignee":"alpha","rules":[{"id":"route-alpha","match":{"assignee":["alpha"]},"assign_to":"alpha","priority":10},{"id":"route-bravo","match":{"assignee":["bravo"]},"assign_to":"bravo","priority":10}]},"communication":{"protocol":"mcp_inbox","topology":"leader_centered","worker_to_worker":true,"ack_timeout_sec":60,"result_format":"result_envelope_v1","message_store":{"sqlite":".team/runtime/team.db","mirror_files":".team/messages"}},"runtime":{"backend":"tmux","display_backend":"
|
|
355
|
+
const TWO_AGENTS_JSON: &str = r#"{"version":1,"team":{"name":"doc-team","mode":"supervisor_worker","objective":"Compile role docs.","workspace":"__WS__"},"leader":{"id":"leader","role":"leader","provider":"codex","model":"gpt-5.5","tools":["fs_read","fs_list","mcp_team"],"context_policy":{"keep_user_thread":true,"receive_worker_outputs":"business_messages_and_short_summaries","max_worker_result_tokens":2000}},"agents":[{"id":"alpha","role":"Alpha Worker","provider":"codex","model":"gpt-5.5","auth_mode":"subscription","working_directory":"__WS__","system_prompt":{"inline":"Alpha body.","file":null},"tools":["mcp_team"],"permission_mode":"restricted","preferred_for":["alpha","Alpha Worker"],"avoid_for":[],"output_contract":{"format":"result_envelope_v1","required_fields":["task_id","status","summary","artifacts"]}},{"id":"bravo","role":"Bravo Worker","provider":"codex","model":"gpt-5.5","auth_mode":"subscription","working_directory":"__WS__","system_prompt":{"inline":"Bravo body.","file":null},"tools":["mcp_team"],"permission_mode":"restricted","preferred_for":["bravo","Bravo Worker"],"avoid_for":[],"output_contract":{"format":"result_envelope_v1","required_fields":["task_id","status","summary","artifacts"]}}],"routing":{"default_assignee":"alpha","rules":[{"id":"route-alpha","match":{"assignee":["alpha"]},"assign_to":"alpha","priority":10},{"id":"route-bravo","match":{"assignee":["bravo"]},"assign_to":"bravo","priority":10}]},"communication":{"protocol":"mcp_inbox","topology":"leader_centered","worker_to_worker":true,"ack_timeout_sec":60,"result_format":"result_envelope_v1","message_store":{"sqlite":".team/runtime/team.db","mirror_files":".team/messages"}},"runtime":{"backend":"tmux","display_backend":"none","session_name":"team-doc-team","auto_launch":true,"require_user_approval_before_launch":true,"max_active_agents":2,"startup_order":["alpha","bravo"],"dangerous_auto_approve":false,"fast":false,"tick_interval_sec":2,"push_min_interval_sec":60,"stuck_timeout_sec":300},"context":{"state_file":"team_state.md","artifact_dir":".team/artifacts","log_dir":".team/logs","summarization":{"worker_full_logs":"retain_outside_leader_context","state_update":"after_each_result"}},"tasks":[{"id":"task_initial","title":"Initial document-driven team task","type":"implementation","assignee":"alpha","deps":[],"acceptance":["Worker reports valid result_envelope_v1"],"status":"pending","requires_tools":["mcp_team"],"files":[],"risk":"low"}]}"#;
|
|
356
356
|
|
|
357
357
|
#[test]
|
|
358
358
|
fn compile_two_agents_sorted_by_filename_with_routing_and_startup_order() {
|
|
@@ -158,7 +158,7 @@ pub fn compile_team(team_dir: &Path) -> Result<Value, ModelError> {
|
|
|
158
158
|
let tools = required_tools(&meta, &path)?;
|
|
159
159
|
let prompt_inline = non_empty_trimmed(&body).unwrap_or_else(|| role.clone());
|
|
160
160
|
agent_ids.push(id.clone());
|
|
161
|
-
|
|
161
|
+
let mut agent_items = vec![
|
|
162
162
|
("id", Value::Str(id.clone())),
|
|
163
163
|
("role", Value::Str(role.clone())),
|
|
164
164
|
("provider", Value::Str(provider)),
|
|
@@ -186,7 +186,11 @@ pub fn compile_team(team_dir: &Path) -> Result<Value, ModelError> {
|
|
|
186
186
|
),
|
|
187
187
|
]),
|
|
188
188
|
),
|
|
189
|
-
]
|
|
189
|
+
];
|
|
190
|
+
if let Some(profile) = string_field(&meta, "profile") {
|
|
191
|
+
agent_items.push(("profile", Value::Str(profile)));
|
|
192
|
+
}
|
|
193
|
+
agents.push(map(agent_items));
|
|
190
194
|
}
|
|
191
195
|
|
|
192
196
|
let default_assignee = agent_ids.first().cloned().unwrap_or_default();
|
|
@@ -267,7 +271,7 @@ pub fn compile_team(team_dir: &Path) -> Result<Value, ModelError> {
|
|
|
267
271
|
"display_backend",
|
|
268
272
|
Value::Str(
|
|
269
273
|
string_field(&team_meta, "display_backend")
|
|
270
|
-
.unwrap_or_else(|| "
|
|
274
|
+
.unwrap_or_else(|| "none".to_string()),
|
|
271
275
|
),
|
|
272
276
|
),
|
|
273
277
|
("session_name", Value::Str(session_name(&team_meta, &team_name))),
|
|
@@ -409,10 +413,16 @@ fn resolve_model(role_meta: &Value, team_meta: &Value, provider: &str) -> Value
|
|
|
409
413
|
if let Some(model) = string_field(role_meta, "model") {
|
|
410
414
|
return Value::Str(model);
|
|
411
415
|
}
|
|
412
|
-
provider_model(team_meta, provider)
|
|
416
|
+
if let Some(model) = provider_model(team_meta, provider)
|
|
413
417
|
.or_else(|| string_field(team_meta, "default_model"))
|
|
414
|
-
|
|
415
|
-
|
|
418
|
+
{
|
|
419
|
+
return Value::Str(model);
|
|
420
|
+
}
|
|
421
|
+
if role_meta.get("profile").is_some() {
|
|
422
|
+
return Value::Null;
|
|
423
|
+
}
|
|
424
|
+
builtin_provider_model(provider)
|
|
425
|
+
.map(|m| Value::Str(m.to_string()))
|
|
416
426
|
.unwrap_or(Value::Null)
|
|
417
427
|
}
|
|
418
428
|
|
|
@@ -147,15 +147,7 @@ pub fn stop_coordinator(workspace: &WorkspacePath) -> Result<StopReport, StopErr
|
|
|
147
147
|
pid: Some(pid),
|
|
148
148
|
});
|
|
149
149
|
}
|
|
150
|
-
|
|
151
|
-
return Ok(StopReport {
|
|
152
|
-
ok: false,
|
|
153
|
-
status: StopOutcome::KillFailed,
|
|
154
|
-
pid: Some(pid),
|
|
155
|
-
});
|
|
156
|
-
};
|
|
157
|
-
let rc = unsafe { libc::kill(pid_t, libc::SIGTERM) };
|
|
158
|
-
if rc != 0 {
|
|
150
|
+
if !terminate_pid(pid) {
|
|
159
151
|
return Ok(StopReport {
|
|
160
152
|
ok: false,
|
|
161
153
|
status: StopOutcome::KillFailed,
|
|
@@ -206,9 +198,11 @@ fn stop_discovered_coordinators(
|
|
|
206
198
|
}
|
|
207
199
|
|
|
208
200
|
fn discover_coordinator_pids(workspace: &WorkspacePath) -> Vec<Pid> {
|
|
209
|
-
let output = match
|
|
210
|
-
.args(["-axo", "pid=,command="])
|
|
211
|
-
|
|
201
|
+
let output = match crate::os_probe::bounded_command_output_with_probe(
|
|
202
|
+
Command::new("ps").args(["-axo", "pid=,command="]),
|
|
203
|
+
"ps_table",
|
|
204
|
+
None,
|
|
205
|
+
)
|
|
212
206
|
{
|
|
213
207
|
Ok(output) if output.status.success() => output,
|
|
214
208
|
_ => return Vec::new(),
|
|
@@ -259,13 +253,86 @@ fn terminate_pid(pid: Pid) -> bool {
|
|
|
259
253
|
if pid_is_running(pid).ok() == Some(false) {
|
|
260
254
|
return true;
|
|
261
255
|
}
|
|
262
|
-
|
|
263
|
-
|
|
256
|
+
let pids = process_tree_pids(pid);
|
|
257
|
+
for child in pids.iter().rev() {
|
|
258
|
+
let _ = send_signal(*child, libc::SIGTERM);
|
|
264
259
|
}
|
|
265
|
-
if
|
|
266
|
-
|
|
260
|
+
if !wait_until_all_not_running(&pids, Duration::from_secs(5)) {
|
|
261
|
+
for child in pids.iter().rev() {
|
|
262
|
+
let _ = send_signal(*child, libc::SIGKILL);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
wait_until_all_not_running(&pids, Duration::from_secs(5))
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/// Public wrapper for diagnostic cleanup paths that must reuse coordinator
|
|
269
|
+
/// shutdown's SIGTERM-then-SIGKILL semantics.
|
|
270
|
+
pub fn terminate_pid_tree(pid: Pid) -> bool {
|
|
271
|
+
terminate_pid(pid)
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
fn process_tree_pids(root: Pid) -> Vec<Pid> {
|
|
275
|
+
let root_pid = root.get();
|
|
276
|
+
let pairs = crate::os_probe::bounded_command_output_with_probe(
|
|
277
|
+
Command::new("ps").args(["-axo", "pid=,ppid="]),
|
|
278
|
+
"ps_parent",
|
|
279
|
+
None,
|
|
280
|
+
)
|
|
281
|
+
.ok()
|
|
282
|
+
.map(|out| String::from_utf8_lossy(&out.stdout).to_string())
|
|
283
|
+
.unwrap_or_default()
|
|
284
|
+
.lines()
|
|
285
|
+
.filter_map(|line| {
|
|
286
|
+
let mut parts = line.split_whitespace();
|
|
287
|
+
let pid = parts.next()?.parse::<u32>().ok()?;
|
|
288
|
+
let ppid = parts.next()?.parse::<u32>().ok()?;
|
|
289
|
+
Some((pid, ppid))
|
|
290
|
+
})
|
|
291
|
+
.collect::<Vec<_>>();
|
|
292
|
+
let mut out = Vec::new();
|
|
293
|
+
collect_child_pids(root_pid, &pairs, &mut out);
|
|
294
|
+
out.push(root_pid);
|
|
295
|
+
out.sort_unstable();
|
|
296
|
+
out.dedup();
|
|
297
|
+
out.into_iter().map(Pid::new).collect()
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
fn collect_child_pids(parent: u32, pairs: &[(u32, u32)], out: &mut Vec<u32>) {
|
|
301
|
+
for (pid, ppid) in pairs {
|
|
302
|
+
if *ppid == parent && !out.contains(pid) {
|
|
303
|
+
out.push(*pid);
|
|
304
|
+
collect_child_pids(*pid, pairs, out);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
fn wait_until_all_not_running(pids: &[Pid], timeout: Duration) -> bool {
|
|
310
|
+
let start = std::time::Instant::now();
|
|
311
|
+
loop {
|
|
312
|
+
for pid in pids {
|
|
313
|
+
reap_child_if_possible(*pid);
|
|
314
|
+
}
|
|
315
|
+
if pids
|
|
316
|
+
.iter()
|
|
317
|
+
.all(|pid| pid_is_running(*pid).ok() != Some(true))
|
|
318
|
+
{
|
|
319
|
+
return true;
|
|
320
|
+
}
|
|
321
|
+
if start.elapsed() >= timeout {
|
|
322
|
+
return false;
|
|
323
|
+
}
|
|
324
|
+
std::thread::sleep(Duration::from_millis(25));
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
fn reap_child_if_possible(pid: Pid) {
|
|
329
|
+
let Ok(pid_t) = libc::pid_t::try_from(pid.get()) else {
|
|
330
|
+
return;
|
|
331
|
+
};
|
|
332
|
+
let mut status = 0;
|
|
333
|
+
unsafe {
|
|
334
|
+
libc::waitpid(pid_t, &mut status, libc::WNOHANG);
|
|
267
335
|
}
|
|
268
|
-
send_signal(pid, libc::SIGKILL) && wait_until_not_running(pid, Duration::from_millis(750))
|
|
269
336
|
}
|
|
270
337
|
|
|
271
338
|
fn send_signal(pid: Pid, signal: libc::c_int) -> bool {
|
|
@@ -306,9 +373,11 @@ pub fn pid_is_running(pid: Pid) -> Result<bool, std::io::Error> {
|
|
|
306
373
|
_ => Err(err),
|
|
307
374
|
};
|
|
308
375
|
}
|
|
309
|
-
let out =
|
|
310
|
-
.args(["-p", &pid.to_string(), "-o", "stat="])
|
|
311
|
-
|
|
376
|
+
let out = crate::os_probe::bounded_command_output_with_probe(
|
|
377
|
+
Command::new("ps").args(["-p", &pid.to_string(), "-o", "stat="]),
|
|
378
|
+
"ps_table",
|
|
379
|
+
Some(pid.get()),
|
|
380
|
+
)?;
|
|
312
381
|
if !out.status.success() {
|
|
313
382
|
return Ok(false);
|
|
314
383
|
}
|
|
@@ -64,6 +64,8 @@ use serde_json::Value;
|
|
|
64
64
|
pub mod backoff;
|
|
65
65
|
pub mod health;
|
|
66
66
|
pub mod orphan;
|
|
67
|
+
pub mod runtime_detectors;
|
|
68
|
+
pub mod runtime_observation;
|
|
67
69
|
pub mod tick;
|
|
68
70
|
pub mod types;
|
|
69
71
|
|
|
@@ -75,6 +77,8 @@ pub use tick::*;
|
|
|
75
77
|
pub use backoff::*;
|
|
76
78
|
pub use orphan::*;
|
|
77
79
|
pub use health::*;
|
|
80
|
+
pub use runtime_detectors::*;
|
|
81
|
+
pub use runtime_observation::*;
|
|
78
82
|
|
|
79
83
|
#[cfg(test)]
|
|
80
84
|
mod tests;
|