@team-agent/installer 0.5.98 → 0.5.100

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 (71) hide show
  1. package/Cargo.lock +1 -1
  2. package/Cargo.toml +1 -1
  3. package/crates/team-agent/src/cli/adapters.rs +1 -163
  4. package/crates/team-agent/src/cli/diagnose.rs +3 -19
  5. package/crates/team-agent/src/cli/emit.rs +20 -204
  6. package/crates/team-agent/src/cli/mod.rs +138 -205
  7. package/crates/team-agent/src/cli/send/coordinator.rs +2 -2
  8. package/crates/team-agent/src/cli/spec.rs +41 -67
  9. package/crates/team-agent/src/cli/tests/main_preserved.rs +2 -186
  10. package/crates/team-agent/src/cli/tests/missing_subcommands.rs +9 -10
  11. package/crates/team-agent/src/cli/tests/run_delegation.rs +0 -3
  12. package/crates/team-agent/src/cli/tests/verb_validate.rs +0 -1
  13. package/crates/team-agent/src/cli/triage.rs +1 -1
  14. package/crates/team-agent/src/cli/types.rs +0 -55
  15. package/crates/team-agent/src/compiler/tests.rs +5 -8
  16. package/crates/team-agent/src/compiler.rs +6 -14
  17. package/crates/team-agent/src/leader/lease.rs +1 -1
  18. package/crates/team-agent/src/lifecycle/launch/add_agent.rs +1 -1
  19. package/crates/team-agent/src/lifecycle/launch/add_agent_state.rs +1 -1
  20. package/crates/team-agent/src/lifecycle/launch/agent_state.rs +5 -25
  21. package/crates/team-agent/src/lifecycle/launch/identity.rs +2 -18
  22. package/crates/team-agent/src/lifecycle/launch/layout.rs +7 -29
  23. package/crates/team-agent/src/lifecycle/launch/leader_context.rs +1 -1
  24. package/crates/team-agent/src/lifecycle/launch/mcp_config.rs +1 -1
  25. package/crates/team-agent/src/lifecycle/launch/ownership.rs +1 -1
  26. package/crates/team-agent/src/lifecycle/launch/pi_mcp.rs +7 -7
  27. package/crates/team-agent/src/lifecycle/launch/quick_start.rs +12 -49
  28. package/crates/team-agent/src/lifecycle/launch/quick_start_transport.rs +3 -9
  29. package/crates/team-agent/src/lifecycle/launch/readiness.rs +1 -1
  30. package/crates/team-agent/src/lifecycle/launch/spawn.rs +2 -61
  31. package/crates/team-agent/src/lifecycle/launch/spec_state.rs +2 -15
  32. package/crates/team-agent/src/lifecycle/launch/state_projection.rs +3 -7
  33. package/crates/team-agent/src/lifecycle/launch/worker_env.rs +1 -1
  34. package/crates/team-agent/src/lifecycle/launch.rs +3 -4
  35. package/crates/team-agent/src/lifecycle/mod.rs +0 -8
  36. package/crates/team-agent/src/lifecycle/restart/agent.rs +62 -8
  37. package/crates/team-agent/src/lifecycle/restart/common.rs +0 -32
  38. package/crates/team-agent/src/lifecycle/restart/rebuild.rs +6 -15
  39. package/crates/team-agent/src/lifecycle/tests/b0_reader_hideone_audit_contract.rs +2 -2
  40. package/crates/team-agent/src/lifecycle/tests/claude_profile_launch_red.rs +1 -1
  41. package/crates/team-agent/src/lifecycle/tests/clone_agent_preserves_source_tools.rs +1 -2
  42. package/crates/team-agent/src/lifecycle/tests/clone_fork_copilot_perms_red.rs +0 -1
  43. package/crates/team-agent/src/lifecycle/tests/core.rs +0 -213
  44. package/crates/team-agent/src/lifecycle/tests/issue238_codex_effort_contract.rs +96 -0
  45. package/crates/team-agent/src/lifecycle/tests/lane_ops.rs +1 -0
  46. package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +31 -77
  47. package/crates/team-agent/src/lifecycle/tests/main_preserved.rs +2 -2
  48. package/crates/team-agent/src/lifecycle/tests/phase_b_contracts.rs +4 -8
  49. package/crates/team-agent/src/lifecycle/tests/phase_golden.rs +16 -9
  50. package/crates/team-agent/src/lifecycle/tests/pi_executable_mcp_red.rs +1 -1
  51. package/crates/team-agent/src/lifecycle/tests/realmachine_clusters_1_6_red.rs +0 -60
  52. package/crates/team-agent/src/lifecycle/tests/startup_latency_contract.rs +3 -4
  53. package/crates/team-agent/src/lifecycle/tests/team_in_team_identity_scope_red.rs +2 -2
  54. package/crates/team-agent/src/lifecycle/tests.rs +1 -2
  55. package/crates/team-agent/src/lifecycle/types.rs +3 -133
  56. package/crates/team-agent/src/messaging/send.rs +2 -2
  57. package/crates/team-agent/src/model/enums.rs +21 -71
  58. package/crates/team-agent/src/model/spec.rs +5 -20
  59. package/crates/team-agent/src/provider/adapters/codex.rs +2 -2
  60. package/crates/team-agent/src/provider/types.rs +7 -6
  61. package/crates/team-agent/src/state/repository.rs +0 -5
  62. package/examples/team.spec.yaml +0 -1
  63. package/package.json +4 -4
  64. package/schemas/team.schema.json +2 -2
  65. package/skills/team-agent/SKILL.md +1 -1
  66. package/skills/team-agent/references/recovery-runbook.md +2 -2
  67. package/skills/team-agent/references/team-agent-operator.md +3 -10
  68. package/skills/team-agent/references/team-in-team.md +0 -6
  69. package/crates/team-agent/src/lifecycle/display.rs +0 -550
  70. package/crates/team-agent/src/lifecycle/tests/core_034_real_red.rs +0 -884
  71. package/crates/team-agent/src/lifecycle/tests/display_adaptive_red.rs +0 -481
@@ -14,7 +14,6 @@
14
14
  //! what: 按 team 参数或唯一性选出重启目标
15
15
  //! depends:
16
16
  //! - crate::lifecycle::lock
17
- //! - crate::lifecycle::display
18
17
  //! - crate::lifecycle::restart::selection
19
18
  //! - crate::lifecycle::restart::preflight
20
19
  //! - crate::lifecycle::restart::remove
@@ -337,7 +336,7 @@ fn restart_with_selected_team_and_transport(
337
336
  .first()
338
337
  .cloned()
339
338
  .unwrap_or_else(|| "dirty_topology".to_string()),
340
- error: "restart refused: tmux endpoint/socket topology is inconsistent; run diagnose from the intended leader socket before restarting".to_string(),
339
+ error: "restart refused: tmux endpoint/socket topology is inconsistent; run doctor from the intended leader socket before restarting".to_string(),
341
340
  issue_ids: topology_issue_ids,
342
341
  });
343
342
  }
@@ -1367,14 +1366,6 @@ fn restart_with_selected_team_and_transport(
1367
1366
  Some(selected.team_key.as_str()),
1368
1367
  &state,
1369
1368
  );
1370
- if let Ok(probe) = crate::lifecycle::display::probe_display_capabilities(&selected.run_workspace)
1371
- {
1372
- let _ = crate::lifecycle::display::rebuild_adaptive_display_after_rebind(
1373
- &selected.run_workspace,
1374
- &session_name,
1375
- &probe,
1376
- );
1377
- }
1378
1369
  // 0.3.28 Step 1: topology invariant guard (warn-only). Same pattern as
1379
1370
  // `lifecycle::launch::launch_with_transport_in_workspace` — logs to stderr,
1380
1371
  // never panics. Hard error path is deferred to Step 10.
@@ -1813,7 +1804,7 @@ fn restart_readiness_timeout_message(
1813
1804
  - tmux session created: {session}\n\
1814
1805
  - worker pane addressable: {pane}\n\
1815
1806
  - coordinator alive: {coordinator}\n\
1816
- Action: check coordinator log {log}, then inspect this workspace with `team-agent diagnose --workspace '{workspace}'`\n\
1807
+ Action: check coordinator log {log}, then inspect this workspace with `team-agent doctor --workspace '{workspace}'`\n\
1817
1808
  Log: coordinator_log={log} state={state} pid_file={pid}",
1818
1809
  missing = restart_readiness_missing_summary(readiness),
1819
1810
  session = yes_no(readiness.session_created),
@@ -2712,7 +2703,7 @@ fn restart_failure_phase(
2712
2703
  // 0.5.39 Slice 1 (tmux-server-death-locate §11.1 B): promote
2713
2704
  // spawn/readiness errors whose stderr contains "server exited
2714
2705
  // unexpectedly" to `tmux_server_crashed`, so downstream event
2715
- // classification and diagnose next_actions ("team-agent diagnose")
2706
+ // classification and doctor next_actions ("team-agent doctor")
2716
2707
  // don't misattribute a whole-server death to a per-agent provider
2717
2708
  // failure. Restart itself does not recover from server crashes in
2718
2709
  // 0.5.39 (§11.1 C is Slice 3, later car); it just classifies clearly.
@@ -2785,7 +2776,7 @@ fn write_restart_agent_failed_event(
2785
2776
  "phase": phase,
2786
2777
  "error": error,
2787
2778
  "action": format!(
2788
- "inspect worker {} output, then restart that worker with `team-agent restart-agent {}` or rerun `team-agent restart --allow-fresh`",
2779
+ "inspect worker {} output, then reset that worker with `team-agent reset-agent {} --discard-session` or rerun `team-agent restart --allow-fresh`",
2789
2780
  decision.agent_id,
2790
2781
  decision.agent_id
2791
2782
  ),
@@ -3271,7 +3262,7 @@ fn restart_failure_next_actions(failed_agents: &[RestartFailedAgent]) -> Vec<Str
3271
3262
  .iter()
3272
3263
  .map(|failure| {
3273
3264
  format!(
3274
- "inspect worker {} output, then restart that worker with `team-agent restart-agent {}` or rerun `team-agent restart --allow-fresh`",
3265
+ "inspect worker {} output, then reset that worker with `team-agent reset-agent {} --discard-session` or rerun `team-agent restart --allow-fresh`",
3275
3266
  failure.agent_id, failure.agent_id
3276
3267
  )
3277
3268
  })
@@ -4920,7 +4911,7 @@ tasks:
4920
4911
  },
4921
4912
  std::time::Duration::from_secs(30),
4922
4913
  );
4923
- assert!(message.contains("team-agent diagnose --workspace"));
4914
+ assert!(message.contains("team-agent doctor --workspace"));
4924
4915
  assert!(message.contains(workspace.to_string_lossy().as_ref()));
4925
4916
  assert!(!message.contains("--allow-fresh"));
4926
4917
  assert!(!message.contains("restart <agent>"));
@@ -122,7 +122,7 @@ fn stale_snapshot_cannot_flip_status_or_diagnose_ok_readiness() {
122
122
  "--json",
123
123
  ]);
124
124
  let diagnose_before = case.run_json(&[
125
- "diagnose",
125
+ "doctor",
126
126
  "--workspace",
127
127
  case.workspace_str(),
128
128
  "--team",
@@ -142,7 +142,7 @@ fn stale_snapshot_cannot_flip_status_or_diagnose_ok_readiness() {
142
142
  "--json",
143
143
  ]);
144
144
  let diagnose_after = case.run_json(&[
145
- "diagnose",
145
+ "doctor",
146
146
  "--workspace",
147
147
  case.workspace_str(),
148
148
  "--team",
@@ -182,7 +182,7 @@ fn diagnose_missing_profile_is_rc1_ok_false_and_zero_token() {
182
182
  .unwrap();
183
183
 
184
184
  let output = Command::new(crate::lifecycle::tests::test_binary_path())
185
- .args(["diagnose", team.to_str().unwrap(), "--workspace", ws.to_str().unwrap(), "--json"])
185
+ .args(["doctor", team.to_str().unwrap(), "--workspace", ws.to_str().unwrap(), "--json"])
186
186
  .current_dir(&team)
187
187
  .output()
188
188
  .expect("diagnose subprocess should run");
@@ -184,7 +184,7 @@ fn write_team_docs(workspace: &Path) {
184
184
  std::fs::write(
185
185
  workspace.join("TEAM.md"),
186
186
  format!(
187
- "---\nname: {TEAM_NAME}\nobjective: clone-agent tools preservation\nprovider: fake\ndisplay_backend: none\n---\n"
187
+ "---\nname: {TEAM_NAME}\nobjective: clone-agent tools preservation\nprovider: fake\n---\n"
188
188
  ),
189
189
  )
190
190
  .expect("TEAM.md");
@@ -324,7 +324,6 @@ fn clone_ok(case: &Case, source: &str, dest: &str, phase: &str) {
324
324
  case.ws(),
325
325
  "--team",
326
326
  TEAM_NAME,
327
- "--no-display",
328
327
  "--json",
329
328
  ]);
330
329
  let stderr = String::from_utf8_lossy(&out.stderr);
@@ -293,7 +293,6 @@ impl Case {
293
293
  self.ws(),
294
294
  "--team",
295
295
  TEAM_NAME,
296
- "--no-display",
297
296
  "--json",
298
297
  ]);
299
298
  let stderr = String::from_utf8_lossy(&out.stderr).to_string();
@@ -211,159 +211,6 @@ fn plan_condition_rejects_out_of_grammar() {
211
211
  }
212
212
  }
213
213
 
214
- // ───────────────────────────────────────────────────────────────────────
215
- // resolve_display_backend — display/backend.py
216
- // 默认 adaptive;显式 none 是逃生口。
217
- // ───────────────────────────────────────────────────────────────────────
218
-
219
- #[test]
220
- fn resolve_backend_defaults_to_adaptive_when_none_requested() {
221
- let r = resolve_display_backend(None, None);
222
- assert_eq!(r.backend, DisplayBackend::Adaptive);
223
- assert!(!r.non_default, "默认 adaptive 不应标记 non_default");
224
- }
225
-
226
- #[test]
227
- fn resolve_backend_explicit_none_is_escape_hatch() {
228
- let r = resolve_display_backend(Some(DisplayBackend::None), None);
229
- assert_eq!(r.backend, DisplayBackend::None);
230
- assert!(r.non_default, "显式 none 是偏离默认 adaptive 的逃生口");
231
- }
232
-
233
- #[test]
234
- fn resolve_backend_requested_overrides_and_marks_non_default() {
235
- // 显式 requested 非 adaptive → 采用且 non_default=true(非静默发事件)。
236
- let r = resolve_display_backend(Some(DisplayBackend::GhosttyWindow), None);
237
- assert_eq!(r.backend, DisplayBackend::GhosttyWindow);
238
- assert!(r.non_default);
239
- }
240
-
241
- #[test]
242
- fn resolve_backend_recorded_used_when_no_request() {
243
- // 无 requested 但 state 有 recorded → 复用 recorded(restart 一致性)。
244
- let r = resolve_display_backend(None, Some(DisplayBackend::GhosttyWorkspace));
245
- assert_eq!(r.backend, DisplayBackend::GhosttyWorkspace);
246
- assert!(r.non_default);
247
- }
248
-
249
- // ───────────────────────────────────────────────────────────────────────
250
- // probe_display_capabilities — display/adaptive.py:31 (C13)
251
- // 分支只看 probe 结果,NOT cfg!(target_os)。golden 实跑:
252
- // linux no-tmux : in_tmux=false, adaptive_status=leader_not_in_tmux, reason=leader_not_in_tmux
253
- // linux in-tmux : in_tmux=true, adaptive_status=available(opened), reason=None, caps both true
254
- // windows/wsl : in_tmux=false, adaptive_status=not_implemented_this_platform, caps both false
255
- // blocked 是 typed outcome(DisplayStatus::Blocked + reason),NOT error。
256
- // 注:RUST 入口签名为 probe_display_capabilities(workspace) —— 它内部读环境/平台;
257
- // 本测试在干净 CI workspace 上跑,只断言"不是 Err(平台降级是 typed outcome)"
258
- // 以及 reason 必属封闭集(若有)。
259
- // ───────────────────────────────────────────────────────────────────────
260
-
261
- #[test]
262
- fn probe_never_errors_platform_degradation_is_typed_outcome() {
263
- // C13:能力性降级绝不走 LifecycleError —— 它是 DisplayStatus::Blocked + reason。
264
- let ws = temp_ws();
265
- let probe =
266
- probe_display_capabilities(&ws).expect("probe 平台降级必须是 typed outcome,不是 Err");
267
- // 若 blocked,reason 必属 AdaptiveBlockReason 封闭集且与 status 一致。
268
- match probe.adaptive_status {
269
- DisplayStatus::Blocked => {
270
- assert!(probe.reason.is_some(), "blocked 必带 reason(C16 封闭集)")
271
- }
272
- DisplayStatus::Opened => assert!(probe.reason.is_none(), "opened 不应带 block reason"),
273
- DisplayStatus::Stopped => {}
274
- }
275
- }
276
-
277
- #[test]
278
- fn probe_caps_consistent_with_in_tmux() {
279
- // caps.adaptive_display == (in_tmux && 平台支持);不在 tmux → caps 全 false。
280
- let ws = temp_ws();
281
- let probe = probe_display_capabilities(&ws).expect("probe 不应 Err");
282
- if !probe.in_tmux {
283
- assert!(!probe.caps.adaptive_display);
284
- assert!(!probe.caps.tmux_append_windows);
285
- }
286
- }
287
-
288
- // ───────────────────────────────────────────────────────────────────────
289
- // AdaptiveBlockReason 封闭集 — ADAPTIVE_BLOCK_REASONS (adaptive.py:21)
290
- // golden 实跑(6 个,无更多无更少):
291
- // aggregator_rebuild_failed, leader_not_in_tmux, not_implemented_this_platform,
292
- // split_failed, window_create_failed, worker_session_missing
293
- // serde rename = snake_case,JSON 名与 Python 字符串一致。
294
- // ───────────────────────────────────────────────────────────────────────
295
-
296
- /// 封闭集 ALL — 一处穷举所有 AdaptiveBlockReason 变体(新增第 7 个变体会编译错此 match,
297
- /// 把 cardinality 锁成编译期事实)。golden ADAPTIVE_BLOCK_REASONS 恰 6 个(adaptive.py:21)。
298
- const ALL_BLOCK_REASONS: [AdaptiveBlockReason; 6] = [
299
- AdaptiveBlockReason::LeaderNotInTmux,
300
- AdaptiveBlockReason::SplitFailed,
301
- AdaptiveBlockReason::WindowCreateFailed,
302
- AdaptiveBlockReason::WorkerSessionMissing,
303
- AdaptiveBlockReason::NotImplementedThisPlatform,
304
- AdaptiveBlockReason::AggregatorRebuildFailed,
305
- ];
306
-
307
- #[test]
308
- fn adaptive_block_reason_serde_names_match_python_and_set_is_exactly_six() {
309
- // (a) 每个变体 wire-name 与 Python 字符串一致。
310
- let expected = [
311
- "\"leader_not_in_tmux\"",
312
- "\"split_failed\"",
313
- "\"window_create_failed\"",
314
- "\"worker_session_missing\"",
315
- "\"not_implemented_this_platform\"",
316
- "\"aggregator_rebuild_failed\"",
317
- ];
318
- for (variant, want) in ALL_BLOCK_REASONS.iter().zip(expected.iter()) {
319
- assert_eq!(&serde_json::to_string(variant).unwrap(), want);
320
- }
321
- // (b) 封闭集 CARDINALITY == 6(无多无少;rogue 第 7 变体使 ALL_BLOCK_REASONS match 编译失败)。
322
- let mut names: Vec<String> = ALL_BLOCK_REASONS
323
- .iter()
324
- .map(|r| serde_json::to_string(r).unwrap())
325
- .collect();
326
- names.sort();
327
- names.dedup();
328
- assert_eq!(
329
- names.len(),
330
- 6,
331
- "ADAPTIVE_BLOCK_REASONS 恰 6 个,无重复无遗漏"
332
- );
333
- }
334
-
335
- #[test]
336
- fn adaptive_blocked_out_of_set_reason_bottoms_to_aggregator_rebuild_failed() {
337
- // adaptive.py 兜底:越界 / aggregator 重建失败 → AggregatorRebuildFailed(documented overflow)。
338
- // RED:经真实路径 —— probe 在 blocked 平台 open_worker_displays,worker display 必属封闭集;
339
- // 这里用 probe.reason 越界场景驱动 open_worker_displays 而非静态枚举,捕获 reason 发射回归。
340
- let ws = temp_ws();
341
- let probe = DisplayProbe {
342
- in_tmux: true,
343
- platform: "linux".to_string(),
344
- leader_session: Some(sess("leader")),
345
- leader_pane: None,
346
- caps: CapsFlags {
347
- tmux_append_windows: true,
348
- adaptive_display: true,
349
- },
350
- // 模拟 aggregator 重建失败封闭:open 必把 worker display 兜底成 AggregatorRebuildFailed。
351
- adaptive_status: DisplayStatus::Blocked,
352
- reason: Some(AdaptiveBlockReason::AggregatorRebuildFailed),
353
- };
354
- let rep = open_worker_displays(&ws, &sess("team-a"), DisplayBackend::Adaptive, &probe)
355
- .expect("C14:显示失败不阻塞 readiness");
356
- for (id, d) in rep.displays.iter() {
357
- match d {
358
- WorkerDisplay::Blocked { reason } => assert!(
359
- ALL_BLOCK_REASONS.contains(reason),
360
- "worker {id} 的 block reason 必属封闭集:{reason:?}"
361
- ),
362
- other => panic!("blocked probe 下 worker {id} 应 Blocked:{other:?}"),
363
- }
364
- }
365
- }
366
-
367
214
  #[test]
368
215
  fn start_mode_serde_names_match_python_start_mode_strings() {
369
216
  // 低价值 wire-format 守卫:start_mode ∈ {"resumed","fresh","fresh_after_missing_rollout","noop"}。
@@ -1300,66 +1147,6 @@ impl Drop for EnvVarGuard {
1300
1147
  }
1301
1148
  }
1302
1149
 
1303
- // ───────────────────────────────────────────────────────────────────────
1304
- // open_worker_displays — worker_window.py (C14)
1305
- // 显示失败不阻塞 readiness:probe 为 blocked 平台时,returns typed Blocked displays,
1306
- // 绝不 Err。这里用 backend=None(无 worker views)验证至少不 panic 成 Err。
1307
- // ───────────────────────────────────────────────────────────────────────
1308
-
1309
- #[test]
1310
- fn open_worker_displays_blocked_probe_yields_typed_blocked_not_error() {
1311
- let ws = temp_ws();
1312
- let probe = DisplayProbe {
1313
- in_tmux: false,
1314
- platform: "windows".to_string(),
1315
- leader_session: None,
1316
- leader_pane: None,
1317
- caps: CapsFlags {
1318
- tmux_append_windows: false,
1319
- adaptive_display: false,
1320
- },
1321
- adaptive_status: DisplayStatus::Blocked,
1322
- reason: Some(AdaptiveBlockReason::NotImplementedThisPlatform),
1323
- };
1324
- let rep = open_worker_displays(&ws, &sess("team-a"), DisplayBackend::Adaptive, &probe)
1325
- .expect("C14:显示失败不阻塞 readiness —— 不得 Err");
1326
- // 每个 worker 的 display 在 blocked 平台上应是 Blocked 变体(若有 worker)。
1327
- for (id, d) in rep.displays.iter() {
1328
- assert!(
1329
- matches!(
1330
- d,
1331
- WorkerDisplay::Blocked {
1332
- reason: AdaptiveBlockReason::NotImplementedThisPlatform
1333
- }
1334
- ),
1335
- "worker {id} 在 windows 平台应 Blocked(not_implemented):{d:?}"
1336
- );
1337
- }
1338
- }
1339
-
1340
- // ───────────────────────────────────────────────────────────────────────
1341
- // close_team_display_backends — display/close.py (C9 close-by-recorded-backend)
1342
- // 空 workspace 无 recorded backend 无 session → 空 closed / 空 orphans(不 Err)。
1343
- // adaptive 只删带 team-tag 的窗口(C2 leader pane 安全)。
1344
- // ───────────────────────────────────────────────────────────────────────
1345
-
1346
- #[test]
1347
- fn close_team_display_empty_workspace_closes_nothing_not_error() {
1348
- let ws = temp_ws();
1349
- let rep = close_team_display_backends(&ws, &sess("team-a"))
1350
- .expect("C9:无 recorded backend 应 Ok(空报告),不是 Err");
1351
- assert!(
1352
- rep.closed.is_empty(),
1353
- "无 recorded backend 不应关任何东西:{:?}",
1354
- rep.closed
1355
- );
1356
- assert!(
1357
- rep.orphans_cleaned.is_empty(),
1358
- "空 workspace 无 orphan:{:?}",
1359
- rep.orphans_cleaned
1360
- );
1361
- }
1362
-
1363
1150
  // ───────────────────────────────────────────────────────────────────────
1364
1151
  // fork_agent — operations.py:284 (native session fork eligibility)
1365
1152
  // 资格门(adapter.supports_session_fork = auth_mode != "compatible_api"):
@@ -0,0 +1,96 @@
1
+ //! Issue #238 lifecycle-side contracts for persisted/recovered effort and Pi
2
+ //! leader argument admission. This module deliberately calls the same
3
+ //! lifecycle helpers used by spawn/restart instead of duplicating resolution.
4
+
5
+ #![allow(clippy::expect_used, clippy::panic, clippy::unwrap_used)]
6
+
7
+ use crate::lifecycle::launch::{
8
+ provider_effort_for_spawn, provider_effort_for_spawn_json, provider_effort_from_raw,
9
+ };
10
+ use crate::lifecycle::launch::pi_mcp::parse_pi_leader_args;
11
+ use crate::model::enums::{Provider, ProviderEffort};
12
+ use crate::model::yaml::Value as Yaml;
13
+ use serde_json::json;
14
+
15
+ fn args(items: &[&str]) -> Vec<String> {
16
+ items.iter().map(|item| (*item).to_string()).collect()
17
+ }
18
+
19
+ #[test]
20
+ fn r6_spawn_and_persisted_recovery_preserve_codex_effort_literals() {
21
+ for raw in ["max", "ultra"] {
22
+ let yaml_agent = Yaml::Map(vec![(
23
+ "effort".to_string(),
24
+ Yaml::Str(raw.to_string()),
25
+ )]);
26
+ let json_agent = json!({"effort": raw});
27
+
28
+ let direct = provider_effort_from_raw(Some(raw), Provider::Codex)
29
+ .expect("direct lifecycle resolution")
30
+ .expect("Codex effort");
31
+ let spawned = provider_effort_for_spawn(&yaml_agent, Provider::Codex)
32
+ .expect("YAML spawn resolution")
33
+ .expect("spawned Codex effort");
34
+ let recovered = provider_effort_for_spawn_json(&json_agent, Provider::Codex)
35
+ .expect("JSON recovery resolution")
36
+ .expect("recovered Codex effort");
37
+ assert_eq!(direct.as_str(), raw);
38
+ assert_eq!(spawned.as_str(), raw);
39
+ assert_eq!(recovered.as_str(), raw);
40
+ }
41
+ }
42
+
43
+ #[test]
44
+ fn r6_unsupported_ultra_is_not_silently_dropped_on_legacy_provider() {
45
+ let error = provider_effort_from_raw(Some("ultra"), Provider::Fake)
46
+ .expect_err("ultra must not be silently ignored")
47
+ .to_string();
48
+ assert!(error.contains("effort 'ultra' is only supported by codex"), "{error}");
49
+ }
50
+
51
+ #[test]
52
+ fn r7_pi_leader_parser_resolves_thinking_by_provider_semantics() {
53
+ let parsed = parse_pi_leader_args(&args(&[
54
+ "--",
55
+ "--model",
56
+ "openai-codex/gpt-5.6-luna",
57
+ "--thinking",
58
+ "max",
59
+ ]))
60
+ .expect("Pi max arguments");
61
+ assert_eq!(parsed.model.as_deref(), Some("openai-codex/gpt-5.6-luna"));
62
+ assert_eq!(parsed.effort, Some(ProviderEffort::Max));
63
+
64
+ let error = parse_pi_leader_args(&args(&[
65
+ "--model",
66
+ "openai-codex/gpt-5.6-luna",
67
+ "--thinking",
68
+ "ultra",
69
+ ]))
70
+ .expect_err("Pi ultra must be rejected by shared resolver")
71
+ .to_string();
72
+ assert!(error.contains("effort 'ultra' is only supported by codex"), "{error}");
73
+ }
74
+
75
+ #[test]
76
+ fn r7_pi_leader_parser_keeps_boundary_errors_fail_closed() {
77
+ for invalid in [
78
+ args(&["--thinking"]),
79
+ args(&["--unknown", "value"]),
80
+ args(&["--model", "codex"]),
81
+ args(&["--model", "openai-codex/gpt-5.6-luna", "--model", "other/x"]),
82
+ ] {
83
+ assert!(parse_pi_leader_args(&invalid).is_err(), "must reject {invalid:?}");
84
+ }
85
+
86
+ let no_effort = parse_pi_leader_args(&args(&["--model", "openai-codex/gpt-5.6-luna"]))
87
+ .expect("Pi model-only arguments");
88
+ assert_eq!(no_effort.effort, None);
89
+ }
90
+
91
+ #[test]
92
+ fn r7_pi_provider_authority_is_not_model_namespace_authority() {
93
+ let error = provider_effort_from_raw(Some("ultra"), Provider::Pi)
94
+ .expect_err("Pi ultra");
95
+ assert!(error.to_string().contains("effort 'ultra' is only supported by codex"));
96
+ }
@@ -1284,6 +1284,7 @@ fn lanea_stop_window_present_kills_and_stopped_true() {
1284
1284
  // touches the display (mark_agent_stopped leaves it as-is) and hardcodes display_closed:false. RED: the
1285
1285
  // persisted display.status/pane_title are the in-process observable.
1286
1286
  #[test]
1287
+ #[ignore = "legacy external display contract removed; runtime is silent tmux only"]
1287
1288
  fn lanea_stop_ghostty_workspace_relabels_slot_to_stopped() {
1288
1289
  let ws = lanea_ws_agents(json!({
1289
1290
  "alpha": { "status": "running", "provider": "codex", "window": "alpha",