@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.
Files changed (82) hide show
  1. package/Cargo.lock +34 -1
  2. package/Cargo.toml +1 -1
  3. package/crates/team-agent/Cargo.toml +1 -1
  4. package/crates/team-agent/src/cli/adapters.rs +196 -19
  5. package/crates/team-agent/src/cli/diagnose.rs +145 -11
  6. package/crates/team-agent/src/cli/emit.rs +287 -53
  7. package/crates/team-agent/src/cli/leader.rs +37 -8
  8. package/crates/team-agent/src/cli/mod.rs +807 -316
  9. package/crates/team-agent/src/cli/status_port.rs +25 -2
  10. package/crates/team-agent/src/cli/tests/divergence.rs +1 -2
  11. package/crates/team-agent/src/cli/tests/lane_c.rs +23 -13
  12. package/crates/team-agent/src/cli/tests/main_preserved.rs +2 -0
  13. package/crates/team-agent/src/cli/tests/run_delegation.rs +57 -3
  14. package/crates/team-agent/src/cli/types.rs +17 -0
  15. package/crates/team-agent/src/compiler/tests.rs +2 -2
  16. package/crates/team-agent/src/compiler.rs +16 -6
  17. package/crates/team-agent/src/coordinator/health.rs +89 -20
  18. package/crates/team-agent/src/coordinator/mod.rs +4 -0
  19. package/crates/team-agent/src/coordinator/runtime_detectors.rs +500 -0
  20. package/crates/team-agent/src/coordinator/runtime_observation.rs +58 -0
  21. package/crates/team-agent/src/coordinator/tests/watch.rs +4 -2
  22. package/crates/team-agent/src/coordinator/tick.rs +222 -69
  23. package/crates/team-agent/src/coordinator/types.rs +15 -3
  24. package/crates/team-agent/src/db/schema.rs +37 -2
  25. package/crates/team-agent/src/diagnose/comms.rs +226 -0
  26. package/crates/team-agent/src/diagnose/mod.rs +45 -0
  27. package/crates/team-agent/src/diagnose/orphans.rs +658 -0
  28. package/crates/team-agent/src/fake_worker.rs +146 -3
  29. package/crates/team-agent/src/leader/start.rs +121 -23
  30. package/crates/team-agent/src/leader/types.rs +44 -1
  31. package/crates/team-agent/src/lib.rs +3 -0
  32. package/crates/team-agent/src/lifecycle/display.rs +648 -50
  33. package/crates/team-agent/src/lifecycle/launch.rs +1048 -264
  34. package/crates/team-agent/src/lifecycle/mod.rs +3 -0
  35. package/crates/team-agent/src/lifecycle/profile_launch.rs +810 -0
  36. package/crates/team-agent/src/lifecycle/profile_smoke.rs +522 -0
  37. package/crates/team-agent/src/lifecycle/restart/agent.rs +113 -26
  38. package/crates/team-agent/src/lifecycle/restart/common.rs +189 -102
  39. package/crates/team-agent/src/lifecycle/restart/rebuild.rs +465 -25
  40. package/crates/team-agent/src/lifecycle/restart/remove.rs +22 -6
  41. package/crates/team-agent/src/lifecycle/restart/team_state.rs +19 -0
  42. package/crates/team-agent/src/lifecycle/restart.rs +4 -1
  43. package/crates/team-agent/src/lifecycle/tests/core.rs +4 -4
  44. package/crates/team-agent/src/lifecycle/tests/lane_ops.rs +5 -5
  45. package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +39 -9
  46. package/crates/team-agent/src/lifecycle/types.rs +23 -0
  47. package/crates/team-agent/src/lifecycle/worker_command_context.rs +326 -0
  48. package/crates/team-agent/src/mcp_server/helpers.rs +1 -0
  49. package/crates/team-agent/src/mcp_server/lifecycle_tools/agent_ops.rs +341 -0
  50. package/crates/team-agent/src/mcp_server/lifecycle_tools/mod.rs +10 -0
  51. package/crates/team-agent/src/mcp_server/lifecycle_tools/state_status.rs +158 -0
  52. package/crates/team-agent/src/mcp_server/mod.rs +3 -74
  53. package/crates/team-agent/src/mcp_server/tests/scoped.rs +1 -1
  54. package/crates/team-agent/src/mcp_server/tests/send.rs +6 -5
  55. package/crates/team-agent/src/mcp_server/tools.rs +312 -111
  56. package/crates/team-agent/src/mcp_server/types.rs +6 -4
  57. package/crates/team-agent/src/mcp_server/wire.rs +19 -7
  58. package/crates/team-agent/src/message_store.rs +21 -4
  59. package/crates/team-agent/src/messaging/delivery.rs +87 -37
  60. package/crates/team-agent/src/messaging/mod.rs +9 -6
  61. package/crates/team-agent/src/messaging/results.rs +153 -16
  62. package/crates/team-agent/src/messaging/selftest.rs +199 -12
  63. package/crates/team-agent/src/messaging/send.rs +35 -3
  64. package/crates/team-agent/src/messaging/tests/runtime.rs +19 -4
  65. package/crates/team-agent/src/messaging/types.rs +11 -3
  66. package/crates/team-agent/src/os_probe.rs +119 -0
  67. package/crates/team-agent/src/packaging/migrate.rs +10 -2
  68. package/crates/team-agent/src/packaging/tests.rs +23 -0
  69. package/crates/team-agent/src/provider/adapter.rs +483 -67
  70. package/crates/team-agent/src/provider/approvals/runtime_prompts.rs +1 -7
  71. package/crates/team-agent/src/provider/classify.rs +51 -4
  72. package/crates/team-agent/src/provider/startup_prompt.rs +94 -0
  73. package/crates/team-agent/src/provider/types.rs +47 -0
  74. package/crates/team-agent/src/session_capture.rs +616 -0
  75. package/crates/team-agent/src/state/persist.rs +57 -0
  76. package/crates/team-agent/src/state/projection.rs +32 -23
  77. package/crates/team-agent/src/state/selector.rs +5 -2
  78. package/crates/team-agent/src/tmux_backend.rs +151 -60
  79. package/crates/team-agent/src/transport/test_support.rs +9 -0
  80. package/crates/team-agent/src/transport/tests/wire.rs +4 -0
  81. package/crates/team-agent/src/transport.rs +13 -2
  82. 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": tmux_session_present(workspace, session_name.as_str()),
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. comms contract suite deferred to 0.2.9 (test files not shipped). (zero token, zero pollution)";
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. Does NOT perform live runtime message \
220
- round-trip. comms contract suite deferred to 0.2.9 (test files not shipped). \
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!(v["status"], json!("pass"), "empty-state selftest passes (all checks pass/deferred)");
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
- v["checks"]["contract_suite"],
233
- json!({
234
- "status": "deferred",
235
- "deferred_to": "0.2.9",
236
- "reason": "contract test files not shipped with package",
237
- "message": "comms contract verification deferred to 0.2.9; contract test files not shipped with package",
238
- }),
239
- "golden contract_suite check (comms.py:132-139)"
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");
@@ -263,6 +263,8 @@ fn seed_team_spec(ws: &std::path::Path) {
263
263
  agent: "w1".to_string(),
264
264
  workspace: ws.clone(),
265
265
  tail: 20,
266
+ head: None,
267
+ search: None,
266
268
  allow_raw_screen: true,
267
269
  json: true,
268
270
  };
@@ -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) + ExitCode::Ok.
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!(exit, ExitCode::Ok);
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 { workspace: ws, team: None, allow_fresh: false, json: true };
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":"adaptive","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"}]}"#;
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":"adaptive","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"}]}"#;
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
- agents.push(map(vec![
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(|| "adaptive".to_string()),
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
- .map(Value::Str)
415
- .or_else(|| builtin_provider_model(provider).map(|m| Value::Str(m.to_string())))
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
- let Ok(pid_t) = libc::pid_t::try_from(pid.get()) else {
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 Command::new("ps")
210
- .args(["-axo", "pid=,command="])
211
- .output()
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
- if !send_signal(pid, libc::SIGTERM) {
263
- return false;
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 wait_until_not_running(pid, Duration::from_millis(750)) {
266
- return true;
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 = Command::new("ps")
310
- .args(["-p", &pid.to_string(), "-o", "stat="])
311
- .output()?;
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;