@team-agent/installer 0.5.2 → 0.5.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 (51) hide show
  1. package/Cargo.lock +3 -1
  2. package/Cargo.toml +1 -1
  3. package/crates/team-agent/Cargo.toml +20 -0
  4. package/crates/team-agent/src/cli/diagnose.rs +191 -31
  5. package/crates/team-agent/src/cli/emit.rs +5 -0
  6. package/crates/team-agent/src/cli/mod.rs +213 -89
  7. package/crates/team-agent/src/cli/tests/named_address.rs +4 -0
  8. package/crates/team-agent/src/codex_app_server.rs +62 -1
  9. package/crates/team-agent/src/conpty/backend.rs +120 -8
  10. package/crates/team-agent/src/coordinator/backoff.rs +88 -2
  11. package/crates/team-agent/src/coordinator/conpty_shim.rs +730 -0
  12. package/crates/team-agent/src/coordinator/health.rs +97 -11
  13. package/crates/team-agent/src/coordinator/mod.rs +8 -0
  14. package/crates/team-agent/src/coordinator/tests/daemon.rs +2 -1
  15. package/crates/team-agent/src/coordinator/tests/tick_core.rs +6 -0
  16. package/crates/team-agent/src/coordinator/tick.rs +13 -0
  17. package/crates/team-agent/src/diagnose/orphans.rs +18 -7
  18. package/crates/team-agent/src/leader/provider_attribution.rs +19 -34
  19. package/crates/team-agent/src/leader/tests/lease_api.rs +7 -0
  20. package/crates/team-agent/src/lib.rs +14 -1
  21. package/crates/team-agent/src/lifecycle/launch.rs +89 -92
  22. package/crates/team-agent/src/lifecycle/lock.rs +40 -33
  23. package/crates/team-agent/src/lifecycle/restart/agent.rs +25 -19
  24. package/crates/team-agent/src/lifecycle/restart/common.rs +53 -2
  25. package/crates/team-agent/src/lifecycle/restart/rebuild.rs +108 -6
  26. package/crates/team-agent/src/lifecycle/restart/selection.rs +47 -18
  27. package/crates/team-agent/src/lifecycle/restart.rs +16 -6
  28. package/crates/team-agent/src/lifecycle/tests/agent_ops.rs +7 -0
  29. package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +287 -53
  30. package/crates/team-agent/src/lifecycle/tests/restart.rs +144 -5
  31. package/crates/team-agent/src/lifecycle/types.rs +1 -0
  32. package/crates/team-agent/src/mcp_server/wire.rs +6 -7
  33. package/crates/team-agent/src/messaging/tests/runtime.rs +5 -0
  34. package/crates/team-agent/src/packaging/tests.rs +41 -6
  35. package/crates/team-agent/src/packaging/types.rs +31 -3
  36. package/crates/team-agent/src/platform/argv.rs +324 -0
  37. package/crates/team-agent/src/platform/errors.rs +95 -0
  38. package/crates/team-agent/src/platform/file_lock.rs +418 -0
  39. package/crates/team-agent/src/platform/mod.rs +66 -0
  40. package/crates/team-agent/src/platform/process.rs +555 -0
  41. package/crates/team-agent/src/provider/adapter.rs +103 -41
  42. package/crates/team-agent/src/provider/session/capture.rs +328 -66
  43. package/crates/team-agent/src/provider/session/resume.rs +30 -28
  44. package/crates/team-agent/src/provider/session_scan/common.rs +117 -1
  45. package/crates/team-agent/src/provider/session_scan/copilot.rs +1 -0
  46. package/crates/team-agent/src/provider/session_scan.rs +1 -0
  47. package/crates/team-agent/src/state/persist.rs +222 -25
  48. package/crates/team-agent/src/state/projection.rs +59 -4
  49. package/crates/team-agent/src/tmux_backend.rs +63 -13
  50. package/crates/team-agent/src/transport_factory.rs +124 -5
  51. package/package.json +4 -4
package/Cargo.lock CHANGED
@@ -100,6 +100,7 @@ version = "0.1.0"
100
100
  dependencies = [
101
101
  "serde",
102
102
  "serde_json",
103
+ "windows",
103
104
  ]
104
105
 
105
106
  [[package]]
@@ -574,7 +575,7 @@ dependencies = [
574
575
 
575
576
  [[package]]
576
577
  name = "team-agent"
577
- version = "0.5.2"
578
+ version = "0.5.4"
578
579
  dependencies = [
579
580
  "anyhow",
580
581
  "chrono",
@@ -587,6 +588,7 @@ dependencies = [
587
588
  "serial_test",
588
589
  "sha2",
589
590
  "thiserror",
591
+ "windows",
590
592
  ]
591
593
 
592
594
  [[package]]
package/Cargo.toml CHANGED
@@ -9,7 +9,7 @@ members = ["crates/team-agent", "crates/win-conpty-phase0", "crates/conpty-trans
9
9
 
10
10
  [workspace.package]
11
11
  edition = "2021"
12
- version = "0.5.2"
12
+ version = "0.5.4"
13
13
  license = "AGPL-3.0"
14
14
  rust-version = "1.95"
15
15
 
@@ -27,6 +27,26 @@ chrono.workspace = true
27
27
  libc.workspace = true
28
28
  rusqlite.workspace = true # step 3 db(bundled SQLite,静态链接)
29
29
 
30
+ # 0.5.x Windows portability Batch 2/3: uses the same `windows = "0.61"`
31
+ # crate as `win-conpty-phase0` so the workspace shares a single
32
+ # Windows-API crate + already-locked Cargo.lock version.
33
+ #
34
+ # - Batch 2 (`platform::file_lock`): `Win32_Storage_FileSystem`
35
+ # (`LockFileEx`/`UnlockFileEx`) + `Win32_System_IO::OVERLAPPED`.
36
+ # - Batch 3 (`platform::process`): `Win32_System_Threading`
37
+ # (`OpenProcess`, `TerminateProcess`, `GetExitCodeProcess`,
38
+ # `GetCurrentProcessId`) + `Win32_System_Diagnostics_ToolHelp`
39
+ # (Toolhelp snapshot for parent-pid discovery).
40
+ [target.'cfg(windows)'.dependencies.windows]
41
+ version = "0.61"
42
+ features = [
43
+ "Win32_Foundation",
44
+ "Win32_Storage_FileSystem",
45
+ "Win32_System_IO",
46
+ "Win32_System_Threading",
47
+ "Win32_System_Diagnostics_ToolHelp",
48
+ ]
49
+
30
50
  [dev-dependencies]
31
51
  serial_test = { version = "3", features = ["file_locks"] }
32
52
 
@@ -1,14 +1,20 @@
1
1
  //! diagnose/preflight/wait-ready CLI helpers.
2
2
  use super::*;
3
- use crate::provider::wire::{command_name, provider_wire};
3
+ use crate::provider::wire::{command_name, parse_provider, provider_wire};
4
4
  use crate::transport::Transport;
5
5
 
6
6
  pub(crate) fn diagnose_runtime(state: &Value, backend: &dyn Transport) -> (Value, Value) {
7
7
  let mut issues = Vec::new();
8
8
  let mut repairs = Vec::new();
9
9
 
10
- if let Some(session_name) = state.get("session_name").and_then(Value::as_str).filter(|s| !s.is_empty()) {
11
- match backend.has_session(&crate::transport::SessionName::new(session_name.to_string())) {
10
+ if let Some(session_name) = state
11
+ .get("session_name")
12
+ .and_then(Value::as_str)
13
+ .filter(|s| !s.is_empty())
14
+ {
15
+ match backend.has_session(&crate::transport::SessionName::new(
16
+ session_name.to_string(),
17
+ )) {
12
18
  Ok(true) => {}
13
19
  Ok(false) => {
14
20
  issues.push(json!("tmux_session_missing"));
@@ -69,13 +75,55 @@ pub(crate) fn diagnose_runtime(state: &Value, backend: &dyn Transport) -> (Value
69
75
  }
70
76
  }
71
77
 
72
- if let Some(session_name) = state.get("session_name").and_then(Value::as_str).filter(|s| !s.is_empty()) {
73
- if let Ok(windows) = backend.list_windows(&crate::transport::SessionName::new(session_name.to_string())) {
78
+ if let Some(session_name) = state
79
+ .get("session_name")
80
+ .and_then(Value::as_str)
81
+ .filter(|s| !s.is_empty())
82
+ {
83
+ if let Ok(windows) = backend.list_windows(&crate::transport::SessionName::new(
84
+ session_name.to_string(),
85
+ )) {
74
86
  if let Some(agents) = state.get("agents").and_then(Value::as_object) {
75
87
  for (agent_id, agent_state) in agents {
88
+ let provider = agent_state
89
+ .get("provider")
90
+ .and_then(Value::as_str)
91
+ .and_then(parse_provider)
92
+ .unwrap_or(crate::provider::Provider::Codex);
93
+ let rollout_path = agent_state
94
+ .get("rollout_path")
95
+ .and_then(Value::as_str)
96
+ .filter(|path| !path.is_empty())
97
+ .map(crate::provider::RolloutPath::new);
98
+ let identity_probe =
99
+ crate::lifecycle::restart::session_identity_probe_for_agent(
100
+ &crate::model::ids::AgentId::new(agent_id.clone()),
101
+ provider,
102
+ rollout_path.as_ref(),
103
+ );
104
+ if identity_probe.identity_ok == Some(false) {
105
+ let issue = format!("session_identity_mismatch:{agent_id}");
106
+ issues.push(json!(issue));
107
+ repairs.push(json!({
108
+ "issue": format!("session_identity_mismatch:{agent_id}"),
109
+ "action": format!(
110
+ "run `team-agent restart --allow-fresh` to discard the poisoned session tuple for `{agent_id}`"
111
+ ),
112
+ "expected_agent_id": agent_id,
113
+ "embedded_agent_id": identity_probe.embedded_agent_id,
114
+ "rollout_path": identity_probe
115
+ .rollout_path
116
+ .map(|path| path.to_string_lossy().to_string()),
117
+ }));
118
+ }
76
119
  let window = ["window", "window_name"]
77
120
  .iter()
78
- .find_map(|key| agent_state.get(*key).and_then(Value::as_str).filter(|s| !s.is_empty()))
121
+ .find_map(|key| {
122
+ agent_state
123
+ .get(*key)
124
+ .and_then(Value::as_str)
125
+ .filter(|s| !s.is_empty())
126
+ })
79
127
  .unwrap_or(agent_id);
80
128
  if !windows.iter().any(|w| w.as_str() == window) {
81
129
  issues.push(json!(format!("worker_window_missing:{agent_id}")));
@@ -131,6 +179,80 @@ fn leader_receiver_attached(state: &Value) -> bool {
131
179
  mode_direct && status_attached && pane_present
132
180
  }
133
181
 
182
+ #[cfg(test)]
183
+ mod tests {
184
+ use super::*;
185
+
186
+ fn write_codex_identity_rollout(
187
+ workspace: &std::path::Path,
188
+ session_id: &str,
189
+ embedded_agent_id: &str,
190
+ ) -> std::path::PathBuf {
191
+ let path = workspace.join("rollout-poison.jsonl");
192
+ let text = format!(
193
+ "{{\"session_meta\":{{\"payload\":{{\"id\":\"{session_id}\",\"cwd\":\"{}\"}}}}}}\n\
194
+ {{\"type\":\"turn_context\",\"payload\":{{}}}}\n\
195
+ {{\"type\":\"response_item\",\"payload\":{{\"content\":[{{\"type\":\"input_text\",\"text\":\"You are Team Agent worker `{embedded_agent_id}` with role `fixture`.\"}}]}}}}\n",
196
+ workspace.to_string_lossy()
197
+ );
198
+ std::fs::write(&path, text).expect("write rollout");
199
+ path
200
+ }
201
+
202
+ #[test]
203
+ fn diagnose_surfaces_codex_session_identity_mismatch() {
204
+ let workspace =
205
+ std::env::temp_dir().join(format!("ta-diagnose-crossbind-{}", std::process::id()));
206
+ let _ = std::fs::remove_dir_all(&workspace);
207
+ std::fs::create_dir_all(&workspace).unwrap();
208
+ let rollout = write_codex_identity_rollout(
209
+ &workspace,
210
+ "019f3327-c35a-7023-b3cd-1bea93a7a157",
211
+ "ios-dev",
212
+ );
213
+ let state = json!({
214
+ "session_name": "team-fixture",
215
+ "leader_receiver": {
216
+ "mode": "direct_tmux",
217
+ "status": "attached",
218
+ "pane_id": "%leader",
219
+ "provider": "codex"
220
+ },
221
+ "agents": {
222
+ "frontend": {
223
+ "provider": "codex",
224
+ "status": "running",
225
+ "session_id": "019f3327-c35a-7023-b3cd-1bea93a7a157",
226
+ "rollout_path": rollout.to_string_lossy(),
227
+ "captured_at": "2026-07-05T17:04:04Z",
228
+ "captured_via": "fs_watch",
229
+ "spawn_cwd": workspace.to_string_lossy(),
230
+ "window": "frontend"
231
+ }
232
+ }
233
+ });
234
+ let backend = crate::transport::test_support::OfflineTransport::new()
235
+ .with_session_present(true)
236
+ .with_windows(vec![crate::transport::WindowName::new("frontend")]);
237
+ let (issues, repairs) = diagnose_runtime(&state, &backend);
238
+ assert!(
239
+ issues
240
+ .as_array()
241
+ .is_some_and(|items| items.iter().any(|item| item.as_str()
242
+ == Some("session_identity_mismatch:frontend"))),
243
+ "diagnose must surface session_identity_mismatch for poisoned Codex tuples; issues={issues}"
244
+ );
245
+ assert!(
246
+ repairs.as_array().is_some_and(|items| items.iter().any(|item| {
247
+ item.get("issue").and_then(Value::as_str)
248
+ == Some("session_identity_mismatch:frontend")
249
+ })),
250
+ "diagnose should include an explicit repair hint for the mismatched tuple; repairs={repairs}"
251
+ );
252
+ let _ = std::fs::remove_dir_all(&workspace);
253
+ }
254
+ }
255
+
134
256
  pub(crate) fn build_preflight_report(team: &std::path::Path) -> Result<Value, CliError> {
135
257
  let mut checks = Vec::new();
136
258
  let mut next_actions = Vec::new();
@@ -150,7 +272,9 @@ pub(crate) fn build_preflight_report(team: &std::path::Path) -> Result<Value, Cl
150
272
  "ok": false,
151
273
  "error": error.to_string(),
152
274
  }));
153
- next_actions.push(json!("fix TEAM.md and role front matter, then run preflight again"));
275
+ next_actions.push(json!(
276
+ "fix TEAM.md and role front matter, then run preflight again"
277
+ ));
154
278
  None
155
279
  }
156
280
  };
@@ -183,7 +307,11 @@ pub(crate) fn build_preflight_report(team: &std::path::Path) -> Result<Value, Cl
183
307
 
184
308
  let workspace = crate::model::paths::team_workspace(team)
185
309
  .map_err(|error| CliError::Runtime(error.to_string()))?;
186
- let profile_dir_exists = workspace.join(".team").join("current").join("profiles").exists()
310
+ let profile_dir_exists = workspace
311
+ .join(".team")
312
+ .join("current")
313
+ .join("profiles")
314
+ .exists()
187
315
  || team.join("profiles").exists();
188
316
  let profile_dir_check = json!({
189
317
  "name": "profile_dir",
@@ -229,7 +357,10 @@ pub(crate) fn build_preflight_report(team: &std::path::Path) -> Result<Value, Cl
229
357
  let summary = if ok {
230
358
  "preflight passed".to_string()
231
359
  } else {
232
- format!("preflight found blockers: {}", blocker_names(&blockers).join(", "))
360
+ format!(
361
+ "preflight found blockers: {}",
362
+ blocker_names(&blockers).join(", ")
363
+ )
233
364
  };
234
365
  let checks_value = Value::Array(checks);
235
366
  let details_log = write_details_log(
@@ -255,7 +386,9 @@ pub(crate) fn build_preflight_report(team: &std::path::Path) -> Result<Value, Cl
255
386
  Ok(report)
256
387
  }
257
388
 
258
- pub(crate) fn build_profile_smoke_check_for_team(team: &std::path::Path) -> Result<Value, CliError> {
389
+ pub(crate) fn build_profile_smoke_check_for_team(
390
+ team: &std::path::Path,
391
+ ) -> Result<Value, CliError> {
259
392
  let workspace = crate::model::paths::team_workspace(team)
260
393
  .map_err(|error| CliError::Runtime(error.to_string()))?;
261
394
  let spec = match crate::compiler::compile_team(team) {
@@ -376,10 +509,14 @@ pub(crate) fn build_wait_ready_report(
376
509
  }));
377
510
  }
378
511
  };
379
- let timeout = if timeout.is_finite() && timeout > 0.0 { timeout } else { 0.0 };
512
+ let timeout = if timeout.is_finite() && timeout > 0.0 {
513
+ timeout
514
+ } else {
515
+ 0.0
516
+ };
380
517
  let deadline = std::time::Instant::now() + std::time::Duration::from_secs_f64(timeout);
381
518
  let mut readiness;
382
- let mut state_read_error: Option<String> = None;
519
+ let mut state_read_error: Option<String>;
383
520
  loop {
384
521
  let mut state = match crate::state::projection::select_runtime_state(
385
522
  &selected.run_workspace,
@@ -419,7 +556,9 @@ pub(crate) fn build_wait_ready_report(
419
556
  "error",
420
557
  "state_read_error",
421
558
  "runtime state could not be read",
422
- vec![json!("inspect .team/runtime/state.json (corrupt or unreadable) and retry")],
559
+ vec![json!(
560
+ "inspect .team/runtime/state.json (corrupt or unreadable) and retry"
561
+ )],
423
562
  )
424
563
  } else if awaiting_trust {
425
564
  (
@@ -427,11 +566,17 @@ pub(crate) fn build_wait_ready_report(
427
566
  "pending",
428
567
  "awaiting_trust_prompt",
429
568
  "workers are awaiting trust prompt",
430
- vec![json!("answer the provider trust prompt, then run wait-ready again")],
569
+ vec![json!(
570
+ "answer the provider trust prompt, then run wait-ready again"
571
+ )],
431
572
  )
432
573
  } else if ready {
433
574
  (true, "ready", "ready", "workers ready", Vec::new())
434
- } else if readiness.get("session_capture_complete").and_then(Value::as_bool) == Some(false) {
575
+ } else if readiness
576
+ .get("session_capture_complete")
577
+ .and_then(Value::as_bool)
578
+ == Some(false)
579
+ {
435
580
  (
436
581
  false,
437
582
  "pending",
@@ -445,7 +590,9 @@ pub(crate) fn build_wait_ready_report(
445
590
  "timeout",
446
591
  "workers_not_ready",
447
592
  "workers not ready before timeout",
448
- vec![json!("inspect team-agent diagnose output and worker terminals")],
593
+ vec![json!(
594
+ "inspect team-agent diagnose output and worker terminals"
595
+ )],
449
596
  )
450
597
  };
451
598
  let details_log = write_details_log(
@@ -479,7 +626,11 @@ fn inject_tmux_session_present(workspace: &std::path::Path, state: &mut Value) {
479
626
  // runtime actually uses (state.tmux_endpoint / tmux_socket), not the
480
627
  // workspace-hash socket. Otherwise readiness reports `process_started=false`
481
628
  // even though the session is alive on the persisted socket.
482
- let Some(session_name) = state.get("session_name").and_then(Value::as_str).filter(|s| !s.is_empty()) else {
629
+ let Some(session_name) = state
630
+ .get("session_name")
631
+ .and_then(Value::as_str)
632
+ .filter(|s| !s.is_empty())
633
+ else {
483
634
  return;
484
635
  };
485
636
  let session_name_owned = session_name.to_string();
@@ -519,14 +670,11 @@ pub(crate) fn wait_readiness(state: &Value) -> Value {
519
670
  .get("leader_receiver")
520
671
  .and_then(Value::as_object)
521
672
  .is_some_and(|receiver| {
522
- receiver
523
- .get("status")
524
- .and_then(Value::as_str)
525
- == Some("attached")
673
+ receiver.get("status").and_then(Value::as_str) == Some("attached")
526
674
  || receiver
527
- .get("pane_id")
528
- .and_then(Value::as_str)
529
- .is_some_and(|pane| !pane.is_empty() && pane != "__team_agent_unbound__")
675
+ .get("pane_id")
676
+ .and_then(Value::as_str)
677
+ .is_some_and(|pane| !pane.is_empty() && pane != "__team_agent_unbound__")
530
678
  });
531
679
 
532
680
  if let Some(agents) = agents {
@@ -544,8 +692,7 @@ pub(crate) fn wait_readiness(state: &Value) -> Value {
544
692
  Some("running" | "busy" | "ready")
545
693
  )
546
694
  });
547
- mcp_ready = !agents.is_empty()
548
- && agents.values().all(agent_mcp_ready);
695
+ mcp_ready = !agents.is_empty() && agents.values().all(agent_mcp_ready);
549
696
  task_prompt_delivered = !agents.is_empty()
550
697
  && (message_counts_positive(state.get("messages"))
551
698
  || agents.values().all(|agent| {
@@ -560,7 +707,8 @@ pub(crate) fn wait_readiness(state: &Value) -> Value {
560
707
  .and_then(Value::as_str)
561
708
  == Some("awaiting_trust_prompt")
562
709
  });
563
- incomplete_sessions = crate::session_capture::incomplete_interacted_resumable_agent_ids(state);
710
+ incomplete_sessions =
711
+ crate::session_capture::incomplete_interacted_resumable_agent_ids(state);
564
712
  }
565
713
  let all_resumable_have_session = incomplete_sessions.is_empty();
566
714
  let session_capture_incomplete = !all_resumable_have_session;
@@ -591,7 +739,9 @@ fn inject_message_counts(workspace: &std::path::Path, state: &mut Value) -> Resu
591
739
  let mut stmt = conn
592
740
  .prepare("select status, count(*) from messages group by status order by status")
593
741
  .map_err(|e| CliError::Runtime(e.to_string()))?;
594
- let mut rows = stmt.query([]).map_err(|e| CliError::Runtime(e.to_string()))?;
742
+ let mut rows = stmt
743
+ .query([])
744
+ .map_err(|e| CliError::Runtime(e.to_string()))?;
595
745
  let mut messages = Map::new();
596
746
  while let Some(row) = rows.next().map_err(|e| CliError::Runtime(e.to_string()))? {
597
747
  let status: String = row.get(0).map_err(|e| CliError::Runtime(e.to_string()))?;
@@ -619,7 +769,10 @@ fn message_counts_positive(value: Option<&Value>) -> bool {
619
769
  fn legacy_process_started(agents: &serde_json::Map<String, Value>) -> bool {
620
770
  !agents.is_empty()
621
771
  && agents.values().all(|agent| {
622
- agent.get("pane_id").and_then(Value::as_str).is_some_and(|s| !s.is_empty())
772
+ agent
773
+ .get("pane_id")
774
+ .and_then(Value::as_str)
775
+ .is_some_and(|s| !s.is_empty())
623
776
  || agent.get("pid").and_then(Value::as_i64).is_some()
624
777
  || agent.get("process_started").and_then(Value::as_bool) == Some(true)
625
778
  || fake_agent_started(agent)
@@ -717,7 +870,10 @@ fn preflight_blockers(checks: &[Value]) -> Vec<Value> {
717
870
  .iter()
718
871
  .filter(|check| check.get("ok").and_then(Value::as_bool) == Some(false))
719
872
  .map(|check| {
720
- let name = check.get("name").and_then(Value::as_str).unwrap_or("unknown");
873
+ let name = check
874
+ .get("name")
875
+ .and_then(Value::as_str)
876
+ .unwrap_or("unknown");
721
877
  let reason = check
722
878
  .get("error")
723
879
  .or_else(|| check.get("reason"))
@@ -747,7 +903,11 @@ fn yaml_path_str<'a>(value: &'a crate::model::yaml::Value, keys: &[&str]) -> Opt
747
903
  current.as_str()
748
904
  }
749
905
 
750
- fn write_details_log(workspace: &std::path::Path, prefix: &str, value: &Value) -> Result<std::path::PathBuf, CliError> {
906
+ fn write_details_log(
907
+ workspace: &std::path::Path,
908
+ prefix: &str,
909
+ value: &Value,
910
+ ) -> Result<std::path::PathBuf, CliError> {
751
911
  let logs = workspace.join(".team").join("logs");
752
912
  std::fs::create_dir_all(&logs)?;
753
913
  let path = logs.join(format!("{prefix}-{}.json", timestamp_slug()));
@@ -1517,10 +1517,15 @@ fn peek_args(args: &[String], cwd: &Path) -> Result<PeekArgs, CliError> {
1517
1517
  fn run_coordinator(args: &[String], cwd: &Path) -> Result<ExitCode, CliError> {
1518
1518
  let parsed = parse_args(args);
1519
1519
  let workspace = crate::coordinator::WorkspacePath::new(workspace(&parsed, cwd));
1520
+ // 0.5.x Windows portability Batch 9 F8: pass `--team` through
1521
+ // to the daemon so it doesn't have to derive from state at
1522
+ // boot time. `parse_args` already recognizes `--team`; we just
1523
+ // thread the value into `DaemonArgs::team_key`.
1520
1524
  crate::coordinator::run_daemon(crate::coordinator::DaemonArgs {
1521
1525
  workspace,
1522
1526
  once: parsed.once,
1523
1527
  tick_interval_sec: parsed.tick_interval,
1528
+ team_key: parsed.team.clone(),
1524
1529
  })
1525
1530
  .map(|()| ExitCode::Ok)
1526
1531
  .map_err(|e| CliError::Runtime(e.to_string()))