@team-agent/installer 0.3.4 → 0.3.6

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 (56) hide show
  1. package/Cargo.lock +1 -1
  2. package/Cargo.toml +1 -1
  3. package/crates/team-agent/src/cli/adapters.rs +8 -0
  4. package/crates/team-agent/src/cli/diagnose.rs +51 -10
  5. package/crates/team-agent/src/cli/emit.rs +2 -1
  6. package/crates/team-agent/src/cli/mod.rs +217 -80
  7. package/crates/team-agent/src/cli/send.rs +1 -0
  8. package/crates/team-agent/src/cli/status_port.rs +135 -7
  9. package/crates/team-agent/src/cli/tests/missing_subcommands.rs +8 -1
  10. package/crates/team-agent/src/cli/tests/mod.rs +1 -0
  11. package/crates/team-agent/src/cli/tests/shutdown_kill_plan.rs +39 -0
  12. package/crates/team-agent/src/cli/types.rs +5 -1
  13. package/crates/team-agent/src/coordinator/backoff.rs +57 -9
  14. package/crates/team-agent/src/coordinator/health.rs +65 -2
  15. package/crates/team-agent/src/coordinator/runtime_detectors.rs +28 -16
  16. package/crates/team-agent/src/coordinator/tests/a0_lostupdate.rs +87 -0
  17. package/crates/team-agent/src/coordinator/tests/mod.rs +1 -0
  18. package/crates/team-agent/src/coordinator/tick.rs +195 -43
  19. package/crates/team-agent/src/leader/helpers.rs +2 -0
  20. package/crates/team-agent/src/leader/rediscover.rs +1 -0
  21. package/crates/team-agent/src/leader/start.rs +9 -1
  22. package/crates/team-agent/src/leader/takeover.rs +18 -1
  23. package/crates/team-agent/src/lifecycle/launch.rs +434 -29
  24. package/crates/team-agent/src/lifecycle/profile_launch.rs +110 -4
  25. package/crates/team-agent/src/lifecycle/profile_smoke.rs +4 -1
  26. package/crates/team-agent/src/lifecycle/restart/common.rs +19 -2
  27. package/crates/team-agent/src/lifecycle/tests/agent_ops.rs +2 -2
  28. package/crates/team-agent/src/lifecycle/tests/core.rs +1 -1
  29. package/crates/team-agent/src/lifecycle/tests/lane_ops.rs +4 -4
  30. package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +3 -1
  31. package/crates/team-agent/src/lifecycle/worker_command_context.rs +44 -9
  32. package/crates/team-agent/src/mcp_server/lifecycle_tools/agent_ops.rs +2 -1
  33. package/crates/team-agent/src/mcp_server/tests/scoped.rs +14 -1
  34. package/crates/team-agent/src/mcp_server/tests/send.rs +15 -1
  35. package/crates/team-agent/src/mcp_server/tools.rs +65 -9
  36. package/crates/team-agent/src/mcp_server/wire.rs +2 -1
  37. package/crates/team-agent/src/message_store.rs +80 -0
  38. package/crates/team-agent/src/messaging/results.rs +76 -5
  39. package/crates/team-agent/src/messaging/send.rs +3 -1
  40. package/crates/team-agent/src/messaging/types.rs +15 -1
  41. package/crates/team-agent/src/messaging/watchers.rs +68 -30
  42. package/crates/team-agent/src/model/enums.rs +7 -1
  43. package/crates/team-agent/src/model/permissions.rs +7 -0
  44. package/crates/team-agent/src/model/spec.rs +3 -1
  45. package/crates/team-agent/src/provider/adapter.rs +472 -7
  46. package/crates/team-agent/src/provider/classify.rs +6 -2
  47. package/crates/team-agent/src/provider/faults.rs +3 -2
  48. package/crates/team-agent/src/provider/startup_prompt.rs +25 -7
  49. package/crates/team-agent/src/provider/types.rs +11 -0
  50. package/crates/team-agent/src/session_capture.rs +1 -0
  51. package/crates/team-agent/src/state/persist.rs +95 -19
  52. package/crates/team-agent/src/tmux_backend/tests.rs +8 -7
  53. package/crates/team-agent/src/tmux_backend.rs +80 -6
  54. package/crates/team-agent/src/transport.rs +32 -0
  55. package/npm/install.mjs +21 -0
  56. package/package.json +4 -4
@@ -6,8 +6,8 @@ use std::process::Command;
6
6
  use super::helpers::{find_session_id, parse_jsonl_records, patterns};
7
7
  use super::types::{
8
8
  AuthHintStatus, CaptureVia, CapturedSession, CommandPlan, Confidence, McpConfig,
9
- ProviderCaps, ProviderCommandContext, ProviderError, RolloutPath, SessionId,
10
- StatusPatterns,
9
+ ProviderCaps, ProviderCommandContext, ProviderCommandOverrides, ProviderError, RolloutPath,
10
+ SessionId, StatusPatterns,
11
11
  };
12
12
  use super::{AuthMode, Provider};
13
13
 
@@ -210,6 +210,20 @@ pub trait ProviderAdapter {
210
210
  checks: usize,
211
211
  sleep_s: f64,
212
212
  ) -> Vec<crate::provider::HandledPrompt> {
213
+ self.handle_startup_prompts_outcome(transport, target, checks, sleep_s)
214
+ .handled
215
+ }
216
+
217
+ /// swallow batch 2 ② (A1): the structured variant — `capture_error` surfaces a pane
218
+ /// that could not even be captured, so callers can log the failure instead of
219
+ /// silently treating it as "no prompts" (CLAUDE.md §5).
220
+ fn handle_startup_prompts_outcome(
221
+ &self,
222
+ transport: &dyn crate::transport::Transport,
223
+ target: &crate::transport::Target,
224
+ checks: usize,
225
+ sleep_s: f64,
226
+ ) -> super::startup_prompt::StartupPromptOutcome {
213
227
  std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| match self.provider() {
214
228
  Provider::Codex => {
215
229
  super::startup_prompt::codex_handle_startup_prompts(transport, target, checks, sleep_s)
@@ -219,10 +233,32 @@ pub trait ProviderAdapter {
219
233
  transport, target, checks, sleep_s,
220
234
  )
221
235
  }
222
- _ => Vec::new(),
236
+ _ => super::startup_prompt::StartupPromptOutcome::default(),
223
237
  }))
224
238
  .unwrap_or_default()
225
239
  }
240
+
241
+ /// Python launch/core.py:235-237 + tmux_prompt.py:124-129 — `runtime.fast` toggles
242
+ /// codex fast mode by sending `/fast` + Enter to the worker pane after spawn.
243
+ /// Providers without a fast-mode toggle are a no-op so upper layers stay
244
+ /// provider-agnostic (F032). Returns whether a toggle was sent.
245
+ fn enable_fast_mode(
246
+ &self,
247
+ transport: &dyn crate::transport::Transport,
248
+ target: &crate::transport::Target,
249
+ ) -> bool {
250
+ match self.provider() {
251
+ Provider::Codex => {
252
+ let keys: Vec<crate::transport::Key> = "/fast"
253
+ .chars()
254
+ .map(crate::transport::Key::Char)
255
+ .chain([crate::transport::Key::Enter])
256
+ .collect();
257
+ transport.send_keys(target, &keys).is_ok()
258
+ }
259
+ _ => false,
260
+ }
261
+ }
226
262
  }
227
263
 
228
264
  #[derive(Debug, Clone, PartialEq, Eq)]
@@ -278,6 +314,17 @@ impl ProviderAdapter for BasicProviderAdapter {
278
314
  native_mcp_config: false,
279
315
  writes_global_settings: false,
280
316
  },
317
+ // Copilot(C-4-1 cr verdict):resume 走 --resume <sid>;**无 fork** 旗标,
318
+ // session-store 不支持 branched continuation → caps.fork=false 显式拒。
319
+ // native_mcp_config=true(`--additional-mcp-config` 接 inline JSON 或 @file);
320
+ // writes_global_settings=false(session 走 --session-id 预定 UUID,不污染
321
+ // ~/.copilot/mcp-config.json,help 原文 "augments config for this session")。
322
+ Provider::Copilot => ProviderCaps {
323
+ resume: true,
324
+ fork: false,
325
+ native_mcp_config: true,
326
+ writes_global_settings: false,
327
+ },
281
328
  Provider::GeminiCli => ProviderCaps {
282
329
  resume: false,
283
330
  fork: false,
@@ -322,6 +369,13 @@ impl ProviderAdapter for BasicProviderAdapter {
322
369
  fn auth_hint(&self, auth_mode: AuthMode) -> AuthHintStatus {
323
370
  match self.provider {
324
371
  Provider::Claude | Provider::ClaudeCode => claude_auth_hint(auth_mode),
372
+ // C-A-5 cr verdict v2(诚实 MUST-NOT-13) — copilot 无 auth status 子命令
373
+ // (main-help Commands 节仅 completion/help/init/login/mcp/plugin/update/
374
+ // version)。framework 只能弱检测(命令在 PATH + ~/.copilot/config.json
375
+ // 存在),不能假报强 Present;Subscription 档返 PresentWeak,doctor 文案
376
+ // 标"weak / no auth-status command available";Compatible/Official 走 BYOK
377
+ // 路径,有 COPILOT_PROVIDER_BASE_URL 时已脱离 GitHub 登录通道。
378
+ Provider::Copilot => copilot_auth_hint(auth_mode),
325
379
  _ => match auth_mode {
326
380
  AuthMode::Subscription => AuthHintStatus::Present,
327
381
  AuthMode::OfficialApi | AuthMode::CompatibleApi => AuthHintStatus::MissingOrUnknown,
@@ -351,7 +405,16 @@ impl ProviderAdapter for BasicProviderAdapter {
351
405
  Provider::Claude | Provider::ClaudeCode => {
352
406
  Ok(claude_launch_command(self, auth_mode, mcp_config, system_prompt, model, tools)?)
353
407
  }
354
- Provider::Codex => Ok(codex_base_command(None, auth_mode, mcp_config, system_prompt, model, tools)),
408
+ Provider::Codex => Ok(codex_base_command(None, auth_mode, mcp_config, system_prompt, model, tools, None)),
409
+ // §C1 worker argv 形态 + C-1/C-5/C-6 cr verdict:
410
+ // copilot --no-color --no-auto-update [<dangerous|granular>] [--model]
411
+ // --additional-mcp-config <inline json> --session-id <expected_uuid> -C <cwd>
412
+ // system_prompt 经 spawn env(COPILOT_CUSTOM_INSTRUCTIONS_DIRS)+ per-worker
413
+ // AGENTS.md(launch 路径写文件,见 lifecycle/launch.rs)注入,**不入 argv**
414
+ // (B2 灵魂件降级,C-1-2 禁 silent 写全局)。
415
+ Provider::Copilot => Ok(copilot_base_command(
416
+ auth_mode, mcp_config, system_prompt, model, tools,
417
+ )),
355
418
  Provider::GeminiCli => {
356
419
  let mut argv = vec!["gemini".to_string()];
357
420
  if let Some(model) = model {
@@ -394,6 +457,40 @@ impl ProviderAdapter for BasicProviderAdapter {
394
457
  managed_mcp_config: managed,
395
458
  })
396
459
  }
460
+ // codex.py:105-118 — the profile command overrides (codex_profile / codex_config)
461
+ // ride on `agent["_provider_profile"]`, which only the plan path carries.
462
+ Provider::Codex => Ok(CommandPlan::argv_only(codex_base_command(
463
+ None,
464
+ ctx.auth_mode,
465
+ ctx.mcp_config,
466
+ ctx.system_prompt,
467
+ ctx.model,
468
+ ctx.tools,
469
+ ctx.profile_launch.map(|profile| &profile.command_overrides),
470
+ ))),
471
+ // §C1 + §C4 cr verdict — copilot plan 端预定 UUID + workspace `-C` 双保险:
472
+ // * `--session-id <uuid>`(claude 同法,捕获免目录扫描,sqlite 仅校验)
473
+ // * `-C <workspace>`(双保险,即便 spawn cwd 漂移也能锚定)
474
+ // mcp_config inline 形态由 build_command 写入,launch 路径会用
475
+ // point_native_mcp_config_at_file 重写为 @<file> 形(§C1 note)。
476
+ Provider::Copilot => {
477
+ let expected = next_session_token();
478
+ let mut argv = copilot_base_command(
479
+ ctx.auth_mode,
480
+ ctx.mcp_config,
481
+ ctx.system_prompt,
482
+ ctx.model,
483
+ ctx.tools,
484
+ );
485
+ argv.push("--session-id".to_string());
486
+ argv.push(expected.clone());
487
+ Ok(CommandPlan {
488
+ argv,
489
+ expected_session_id: Some(SessionId::new(expected)),
490
+ provider_projects_root: None,
491
+ managed_mcp_config: false,
492
+ })
493
+ }
397
494
  _ => self
398
495
  .build_command_with_tools(
399
496
  ctx.auth_mode,
@@ -500,6 +597,7 @@ impl ProviderAdapter for BasicProviderAdapter {
500
597
  system_prompt,
501
598
  model,
502
599
  tools,
600
+ None,
503
601
  );
504
602
  argv.push(session_id.as_str().to_string());
505
603
  Ok(argv)
@@ -511,6 +609,16 @@ impl ProviderAdapter for BasicProviderAdapter {
511
609
  argv.push(session_id.as_str().to_string());
512
610
  Ok(argv)
513
611
  }
612
+ // §C1 cr verdict:resume 同 base + `--resume <sid>`(去 --session-id,
613
+ // copilot --resume 接受 id|name)。
614
+ Provider::Copilot => {
615
+ let mut argv = copilot_base_command_resume(
616
+ auth_mode, mcp_config, system_prompt, model, tools,
617
+ );
618
+ argv.push("--resume".to_string());
619
+ argv.push(session_id.as_str().to_string());
620
+ Ok(argv)
621
+ }
514
622
  Provider::GeminiCli | Provider::Fake => Err(ProviderError::ResumeUnavailable(format!(
515
623
  "{} resume requires session_id",
516
624
  provider_wire(self.provider)
@@ -548,6 +656,30 @@ impl ProviderAdapter for BasicProviderAdapter {
548
656
  plan.managed_mcp_config = managed;
549
657
  Ok(plan)
550
658
  }
659
+ Provider::Codex => {
660
+ if !self.session_is_resumable(session_id, ctx.auth_mode)? {
661
+ return Err(ProviderError::ResumeUnavailable(format!(
662
+ "{} resume requires session_id",
663
+ provider_wire(self.provider)
664
+ )));
665
+ }
666
+ let Some(session_id) = session_id else {
667
+ return Err(ProviderError::ResumeUnavailable(
668
+ "resume requires session_id".to_string(),
669
+ ));
670
+ };
671
+ let mut argv = codex_base_command(
672
+ Some("resume"),
673
+ ctx.auth_mode,
674
+ ctx.mcp_config,
675
+ ctx.system_prompt,
676
+ ctx.model,
677
+ ctx.tools,
678
+ ctx.profile_launch.map(|profile| &profile.command_overrides),
679
+ );
680
+ argv.push(session_id.as_str().to_string());
681
+ Ok(CommandPlan::argv_only(argv))
682
+ }
551
683
  _ => self
552
684
  .build_resume_command_with_context(
553
685
  session_id,
@@ -597,6 +729,7 @@ impl ProviderAdapter for BasicProviderAdapter {
597
729
  system_prompt,
598
730
  model,
599
731
  tools,
732
+ None,
600
733
  );
601
734
  argv.push(session_id.as_str().to_string());
602
735
  Ok(argv)
@@ -611,6 +744,13 @@ impl ProviderAdapter for BasicProviderAdapter {
611
744
  argv.push("--fork-session".to_string());
612
745
  Ok(argv)
613
746
  }
747
+ // C-4-2 cr verdict: copilot 无 fork 旗标 + session-store 不支持 branched
748
+ // continuation → 显式 CapabilityUnsupported,**绝不** silent fallback 到
749
+ // restart-from-scratch(MUST-NOT-13 诚实)。本分支理论上不可达(caps.fork=false
750
+ // 已在 fork_with_context 入口拦截,line 582),保留作 totality 守护。
751
+ Provider::Copilot => Err(ProviderError::CapabilityUnsupported(
752
+ "copilot CLI 无 fork 旗标,session-store 不支持 branched continuation".to_string(),
753
+ )),
614
754
  Provider::GeminiCli | Provider::Fake => Err(ProviderError::CapabilityUnsupported(format!(
615
755
  "{} does not support native session fork",
616
756
  provider_wire(self.provider)
@@ -661,6 +801,30 @@ impl ProviderAdapter for BasicProviderAdapter {
661
801
  managed_mcp_config: managed,
662
802
  })
663
803
  }
804
+ Provider::Codex => {
805
+ if !self.caps().fork || ctx.auth_mode == AuthMode::CompatibleApi {
806
+ return Err(ProviderError::CapabilityUnsupported(format!(
807
+ "{} does not support native session fork",
808
+ provider_wire(self.provider)
809
+ )));
810
+ }
811
+ let Some(session_id) = session_id else {
812
+ return Err(ProviderError::ResumeUnavailable(
813
+ "fork requires session_id".to_string(),
814
+ ));
815
+ };
816
+ let mut argv = codex_base_command(
817
+ Some("fork"),
818
+ ctx.auth_mode,
819
+ ctx.mcp_config,
820
+ ctx.system_prompt,
821
+ ctx.model,
822
+ ctx.tools,
823
+ ctx.profile_launch.map(|profile| &profile.command_overrides),
824
+ );
825
+ argv.push(session_id.as_str().to_string());
826
+ Ok(CommandPlan::argv_only(argv))
827
+ }
664
828
  _ => self
665
829
  .fork_with_context(
666
830
  session_id,
@@ -705,6 +869,9 @@ impl ProviderAdapter for BasicProviderAdapter {
705
869
  match self.provider {
706
870
  Provider::Claude | Provider::ClaudeCode => patterns(r"[>❯]\s", r"[✶✢✽✻✳·].*…", r"Error|Traceback"),
707
871
  Provider::Codex => patterns(r"(›|❯|codex>)", r"•.*esc to interrupt", r"Error|Traceback|panic"),
872
+ // C-3-3 cr verdict: copilot 真值待用户真会话样本(§E3 line 105),一期占位
873
+ // 仅 error 行至少能识别;idle/processing 留 Unknown(N11 守,classify→None)。
874
+ Provider::Copilot => patterns(r">", r"working|processing", r"Error|panic"),
708
875
  Provider::GeminiCli | Provider::Fake => patterns(r">", r"working|processing", r"Error|Traceback"),
709
876
  }
710
877
  }
@@ -718,6 +885,7 @@ fn command_name(provider: Provider) -> &'static str {
718
885
  match provider {
719
886
  Provider::Claude | Provider::ClaudeCode => "claude",
720
887
  Provider::Codex => "codex",
888
+ Provider::Copilot => "copilot",
721
889
  Provider::GeminiCli => "gemini",
722
890
  Provider::Fake => "team-agent",
723
891
  }
@@ -728,6 +896,7 @@ fn provider_wire(provider: Provider) -> &'static str {
728
896
  Provider::Claude => "claude",
729
897
  Provider::ClaudeCode => "claude_code",
730
898
  Provider::Codex => "codex",
899
+ Provider::Copilot => "copilot",
731
900
  Provider::GeminiCli => "gemini_cli",
732
901
  Provider::Fake => "fake",
733
902
  }
@@ -741,6 +910,27 @@ fn auth_mode_wire(auth_mode: AuthMode) -> &'static str {
741
910
  }
742
911
  }
743
912
 
913
+ /// C-A-5 cr verdict v2 — copilot 弱检测(无 auth status 子命令)。
914
+ /// 当 copilot 命令在 PATH 且 `~/.copilot/config.json` 存在 → PresentWeak;否则 Missing
915
+ /// (PATH 缺)或 MissingOrUnknown(无 config 文件)。Compatible/Official 走 BYOK,
916
+ /// 由 profile_launch 端校验(COPILOT_PROVIDER_BASE_URL 等),hint 层报 Unknown。
917
+ fn copilot_auth_hint(auth_mode: AuthMode) -> AuthHintStatus {
918
+ if !matches!(auth_mode, AuthMode::Subscription) {
919
+ return AuthHintStatus::Unknown;
920
+ }
921
+ if !command_on_path("copilot") {
922
+ return AuthHintStatus::Missing;
923
+ }
924
+ let Some(home) = std::env::var_os("HOME").map(PathBuf::from) else {
925
+ return AuthHintStatus::MissingOrUnknown;
926
+ };
927
+ if home.join(".copilot").join("config.json").exists() {
928
+ AuthHintStatus::PresentWeak
929
+ } else {
930
+ AuthHintStatus::MissingOrUnknown
931
+ }
932
+ }
933
+
744
934
  fn claude_auth_hint(auth_mode: AuthMode) -> AuthHintStatus {
745
935
  if auth_mode != AuthMode::Subscription {
746
936
  return AuthHintStatus::MissingOrUnknown;
@@ -777,11 +967,22 @@ fn scan_session_candidates_once(
777
967
  provider: Provider,
778
968
  context: &CaptureSessionContext,
779
969
  ) -> Result<Vec<CapturedSessionCandidate>, ProviderError> {
970
+ // §C4 + cr verdict: copilot session 真相源是 ~/.copilot/session-store.db(sqlite),
971
+ // 不是 jsonl 流。点查 sessions(cwd==spawn_cwd)取最新行,**禁** 走目录扫描
972
+ // (PERF P2 不放大;sqlite 点查天然有界)。decoy 文件不进 parse_session_records,
973
+ // 不会"被毒文件炸"。
974
+ if matches!(provider, Provider::Copilot) {
975
+ return Ok(scan_copilot_session_store(context));
976
+ }
780
977
  let candidates = candidate_session_files(provider, context)?;
781
978
  let mut out = Vec::new();
782
979
  for candidate in candidates {
783
980
  let path = candidate.path;
784
- let Ok(text) = std::fs::read_to_string(&path) else {
981
+ // P2 (C-P2-1/4) / Python claude.py:432 — bounded HEAD read (session_meta / cwd /
982
+ // sessionId live in the file head; Python stops at 200 lines). A poisoned
983
+ // (invalid UTF-8) tail must not silently drop the candidate the way a
984
+ // whole-file read_to_string did.
985
+ let Ok(text) = read_head_text(&path, CAPTURE_HEAD_BYTES) else {
785
986
  continue;
786
987
  };
787
988
  let records = parse_session_records(&text);
@@ -836,6 +1037,54 @@ fn scan_session_candidates_once(
836
1037
  Ok(out)
837
1038
  }
838
1039
 
1040
+ /// §C4 cr verdict — copilot session 真相源 sqlite 点查。
1041
+ ///
1042
+ /// 路径:`<HOME>/.copilot/session-store.db`,sessions 表(id/cwd/created_at/updated_at)
1043
+ /// where `cwd == context.spawn_cwd` 取 updated_at 最新行。**绝不**全文件扫描、**绝不**
1044
+ /// 走 `parse_session_records`(jsonl)路径 → decoy 毒文件不会触碰任何解析器。
1045
+ ///
1046
+ /// 失败(HOME 缺、db 缺、表缺、sqlite 错)统一返回空 candidate 列表,与既有
1047
+ /// `collect_optional_candidate_files` 同精神(absent → empty)。
1048
+ fn scan_copilot_session_store(context: &CaptureSessionContext) -> Vec<CapturedSessionCandidate> {
1049
+ let Some(home) = std::env::var_os("HOME").map(PathBuf::from) else {
1050
+ return Vec::new();
1051
+ };
1052
+ let db_path = home.join(".copilot").join("session-store.db");
1053
+ if !db_path.exists() {
1054
+ return Vec::new();
1055
+ }
1056
+ let Ok(conn) = rusqlite::Connection::open_with_flags(
1057
+ &db_path,
1058
+ rusqlite::OpenFlags::SQLITE_OPEN_READ_ONLY | rusqlite::OpenFlags::SQLITE_OPEN_NO_MUTEX,
1059
+ ) else {
1060
+ return Vec::new();
1061
+ };
1062
+ let cwd = context.spawn_cwd.to_string_lossy().to_string();
1063
+ let mut stmt = match conn.prepare(
1064
+ "select id from sessions where cwd = ?1 order by updated_at desc, id desc limit 1",
1065
+ ) {
1066
+ Ok(stmt) => stmt,
1067
+ Err(_) => return Vec::new(),
1068
+ };
1069
+ let row: Option<String> = stmt
1070
+ .query_row([cwd.as_str()], |row| row.get::<_, String>(0))
1071
+ .ok();
1072
+ let Some(session_id) = row else {
1073
+ return Vec::new();
1074
+ };
1075
+ vec![CapturedSessionCandidate {
1076
+ captured: CapturedSession {
1077
+ session_id: Some(SessionId::new(session_id)),
1078
+ rollout_path: Some(RolloutPath::new(db_path.clone())),
1079
+ captured_via: CaptureVia::FsWatch,
1080
+ attribution_confidence: Confidence::High,
1081
+ spawn_cwd: context.spawn_cwd.clone(),
1082
+ },
1083
+ positive_agent_id_match: false,
1084
+ agent_path_match: false,
1085
+ }]
1086
+ }
1087
+
839
1088
  fn command_on_path(name: &str) -> bool {
840
1089
  let Some(path) = std::env::var_os("PATH") else {
841
1090
  return false;
@@ -866,7 +1115,12 @@ fn candidate_session_files(
866
1115
  collect_optional_candidate_files(&home.join(".claude").join("sessions"), &context.agent_id, &mut out)?;
867
1116
  collect_optional_candidate_files(&home.join(".claude").join("projects"), &context.agent_id, &mut out)?;
868
1117
  }
869
- Provider::GeminiCli | Provider::Fake => {}
1118
+ // §C4 cr verdict + 设计 §C: copilot session 真相源是 ~/.copilot/session-store.db
1119
+ // (sqlite 点查 sessions.cwd==spawn_cwd 最新行)和 ~/.copilot/session-state/<uuid>/
1120
+ // workspace.yaml — **不走全文件扫描**(PERF P2 禁不放大)。主路径是
1121
+ // build_command_plan 预定 UUID(--session-id <expected>)→ pending_session_id
1122
+ // 直接命中,这里只补 sqlite 查询的二期入口。一期返空,resume 走 caps 校验。
1123
+ Provider::Copilot | Provider::GeminiCli | Provider::Fake => {}
870
1124
  }
871
1125
  }
872
1126
  out.sort_by(|a, b| {
@@ -875,9 +1129,58 @@ fn candidate_session_files(
875
1129
  .then_with(|| a.path.to_string_lossy().cmp(&b.path.to_string_lossy()))
876
1130
  });
877
1131
  out.dedup_by(|a, b| a.path == b.path && a.requires_cwd_match == b.requires_cwd_match);
1132
+ cap_candidates_by_mtime(&mut out, CAPTURE_CANDIDATE_CAP);
878
1133
  Ok(out)
879
1134
  }
880
1135
 
1136
+ /// P2 (C-P2-2/3) / Python claude.py:300 — candidates are capped to the newest `cap`
1137
+ /// by mtime (descending priority: old candidates must not crowd out new ones; the cap
1138
+ /// may be raised above Python's 300 but never lowered). The existing selection
1139
+ /// ordering of the survivors is preserved.
1140
+ const CAPTURE_CANDIDATE_CAP: usize = 300;
1141
+
1142
+ /// P2 (C-P2-1): head window ≥ Python's 200-line read (meta fields live in the head).
1143
+ const CAPTURE_HEAD_BYTES: u64 = 65_536;
1144
+
1145
+ fn cap_candidates_by_mtime(out: &mut Vec<SessionCandidate>, cap: usize) {
1146
+ if out.len() <= cap {
1147
+ return;
1148
+ }
1149
+ let mut ranked: Vec<(std::time::SystemTime, usize)> = out
1150
+ .iter()
1151
+ .enumerate()
1152
+ .map(|(index, candidate)| {
1153
+ let mtime = std::fs::metadata(&candidate.path)
1154
+ .and_then(|meta| meta.modified())
1155
+ .unwrap_or(std::time::SystemTime::UNIX_EPOCH);
1156
+ (mtime, index)
1157
+ })
1158
+ .collect();
1159
+ ranked.sort_by(|a, b| b.0.cmp(&a.0));
1160
+ let keep: std::collections::BTreeSet<usize> =
1161
+ ranked.into_iter().take(cap).map(|(_, index)| index).collect();
1162
+ let mut index = 0;
1163
+ out.retain(|_| {
1164
+ let kept = keep.contains(&index);
1165
+ index += 1;
1166
+ kept
1167
+ });
1168
+ }
1169
+
1170
+ /// P2: bounded head read, truncated to the last complete line (a cut record must not
1171
+ /// reach the JSONL parser); lossy UTF-8 so a mid-codepoint boundary stays safe.
1172
+ fn read_head_text(path: &Path, max_bytes: u64) -> std::io::Result<String> {
1173
+ use std::io::Read;
1174
+ let file = std::fs::File::open(path)?;
1175
+ let mut bytes = Vec::new();
1176
+ file.take(max_bytes).read_to_end(&mut bytes)?;
1177
+ let complete = match bytes.iter().rposition(|byte| *byte == b'\n') {
1178
+ Some(last_newline) => &bytes[..=last_newline],
1179
+ None => &bytes[..],
1180
+ };
1181
+ Ok(String::from_utf8_lossy(complete).into_owned())
1182
+ }
1183
+
881
1184
  fn collect_optional_candidate_files(
882
1185
  dir: &Path,
883
1186
  agent_id: &str,
@@ -1104,6 +1407,7 @@ fn codex_base_command(
1104
1407
  system_prompt: Option<&str>,
1105
1408
  model: Option<&str>,
1106
1409
  tools: &[&str],
1410
+ overrides: Option<&ProviderCommandOverrides>,
1107
1411
  ) -> Vec<String> {
1108
1412
  let mut argv = vec![
1109
1413
  "codex".to_string(),
@@ -1118,6 +1422,11 @@ fn codex_base_command(
1118
1422
  "--disable".to_string(),
1119
1423
  "apps".to_string(),
1120
1424
  ]);
1425
+ // codex.py:105-107 — profile CODEX_PROFILE before the sandbox/approval flags.
1426
+ if let Some(profile) = overrides.and_then(|o| o.codex_profile.as_deref()) {
1427
+ argv.push("--profile".to_string());
1428
+ argv.push(profile.to_string());
1429
+ }
1121
1430
  if codex_dangerous_auto_approve(tools) {
1122
1431
  argv.push("--dangerously-bypass-approvals-and-sandbox".to_string());
1123
1432
  } else {
@@ -1130,9 +1439,21 @@ fn codex_base_command(
1130
1439
  argv.push("--model".to_string());
1131
1440
  argv.push(model.to_string());
1132
1441
  }
1442
+ // codex.py:117-118 — profile codex_config `-c` items before developer_instructions.
1443
+ if let Some(overrides) = overrides {
1444
+ for config in &overrides.codex_config {
1445
+ argv.push("-c".to_string());
1446
+ argv.push(config.clone());
1447
+ }
1448
+ }
1133
1449
  if let Some(prompt) = system_prompt {
1450
+ // codex.py:120 — escape order matters: backslash first, then quote, then newline.
1451
+ let escaped = prompt
1452
+ .replace('\\', "\\\\")
1453
+ .replace('"', "\\\"")
1454
+ .replace('\n', "\\n");
1134
1455
  argv.push("-c".to_string());
1135
- argv.push(format!("developer_instructions=\"{}\"", prompt.replace('"', "\\\"")));
1456
+ argv.push(format!("developer_instructions=\"{escaped}\""));
1136
1457
  }
1137
1458
  // Contract C / MUST-8: Codex CLI (2026-06) does NOT take Claude's `--mcp-config <json>` flag;
1138
1459
  // instead it uses `-c mcp_servers.<name>.<field>=...` overrides, the same pattern used by
@@ -1174,6 +1495,10 @@ fn append_codex_mcp_overrides(argv: &mut Vec<String>, raw: &serde_json::Value) {
1174
1495
  argv.push("-c".to_string());
1175
1496
  argv.push(format!("mcp_servers.{name}.{key}={}", json_inline(value)));
1176
1497
  }
1498
+ // codex.py:129 — every MCP server gets a 600s tool timeout so long-running
1499
+ // team_orchestrator calls (report_result etc.) survive the codex default.
1500
+ argv.push("-c".to_string());
1501
+ argv.push(format!("mcp_servers.{name}.tool_timeout_sec=600.0"));
1177
1502
  }
1178
1503
  }
1179
1504
 
@@ -1188,6 +1513,146 @@ fn codex_dangerous_auto_approve(tools: &[&str]) -> bool {
1188
1513
  tools.contains(&"dangerous_auto_approve")
1189
1514
  }
1190
1515
 
1516
+ // ---------------------------------------------------------------------------
1517
+ // COPILOT base command(v2 实证 + cr verdict v2 30 约束)
1518
+ // ---------------------------------------------------------------------------
1519
+ //
1520
+ // 设计 v2 §B argv 形态(每条带 help 出处,逐字落地):
1521
+ // copilot --no-color --no-auto-update --no-remote # C-1-2 噪音 + 禁远控
1522
+ // --disable-builtin-mcps # C-3-1 P0 禁内建 github-mcp-server
1523
+ // --additional-mcp-config @<file> # C-3-4 用 @file 形,避 wrapper 语义
1524
+ // --allow-tool 'team_orchestrator' # C-3-5 mcp_team 免审批 (server 级)
1525
+ // --session-id <uuid> -n <agent_id> # C-7-1 plan/launch 加
1526
+ // -C <workspace> # 双保险,launch 加
1527
+ // [--allow-all | <granular deny>] # C-5-1/C-5-2
1528
+ // [--model <m>]
1529
+ // [--log-dir <dir> --log-level info] # C-6-2 launch 加
1530
+ // env: COPILOT_CUSTOM_INSTRUCTIONS_DIRS=<ws>/.../<agent_id>/ # C-2-1 launch 注入
1531
+ // COPILOT_DISABLE_TERMINAL_TITLE=1 # C-4 P0 N39 红线,launch 注入
1532
+ // banner 不入 argv(v1 错的 --banner=never 删除;banner 走 config 文件,非 CLI flag)。
1533
+ // `-i`/`-p`/`--share*`/`--no-ask-user` **绝不**入 argv(RC-1/RC-14/RC-16)。
1534
+ //
1535
+ // system_prompt(B2 灵魂件)**不进 argv**:走 spawn env COPILOT_CUSTOM_INSTRUCTIONS_DIRS
1536
+ // + per-worker AGENTS.md(B2 单源,不另拼);本函数 system_prompt 参数静默忽略。
1537
+ fn copilot_base_command(
1538
+ auth_mode: AuthMode,
1539
+ mcp_config: Option<&McpConfig>,
1540
+ system_prompt: Option<&str>,
1541
+ model: Option<&str>,
1542
+ tools: &[&str],
1543
+ ) -> Vec<String> {
1544
+ let _ = (auth_mode, system_prompt);
1545
+ let mut argv = vec![
1546
+ "copilot".to_string(),
1547
+ // C-1-2 v2:噪音控制三件 + 禁远控(防 GitHub web 远控 worker)
1548
+ "--no-color".to_string(),
1549
+ "--no-auto-update".to_string(),
1550
+ "--no-remote".to_string(),
1551
+ // C-3-1 v2 (P0):禁内建 github-mcp-server(main-help:70-71);残留风险
1552
+ // 通过 spawn 前 `copilot mcp list` 扫描 + 按名 `--disable-mcp-server <n>` 补
1553
+ // (那一段在 launch 路径加,因为需要 spawn-time 探测)。
1554
+ "--disable-builtin-mcps".to_string(),
1555
+ ];
1556
+ if copilot_dangerous_auto_approve(tools) {
1557
+ // C-5-1 v2 实证:--allow-all == --yolo == 三件套(tools+paths+urls 等价),
1558
+ // help-permissions Enabling All Permissions 节原文。**禁** --allow-all-tools
1559
+ // (仅 tools 一档,语义不全,RC-13)。
1560
+ argv.push("--allow-all".to_string());
1561
+ } else {
1562
+ // C-5-2 v2:角色缺某 canonical 能力 → 精细 deny(deny 恒优先,即便
1563
+ // --allow-all-tools,help-permissions 原文);**禁** --allow-all/--yolo(RC-14)。
1564
+ for flag in copilot_permission_flags(tools) {
1565
+ argv.push(flag);
1566
+ }
1567
+ }
1568
+ // C-3-5 v2:mcp_team ∈ canonical(team_orchestrator 是我们的 server)→ 免审批
1569
+ // (模式 `<mcp-server-name>(tool-name?)` 省略 tool = 该 server 全工具集)。
1570
+ argv.push("--allow-tool".to_string());
1571
+ argv.push("team_orchestrator".to_string());
1572
+ if let Some(model) = model {
1573
+ argv.push("--model".to_string());
1574
+ argv.push(model.to_string());
1575
+ }
1576
+ if let Some(config) = mcp_config {
1577
+ // §C1 v2 + C-3-4 cr verdict v2(te 真机实证 cmd-mcp-add schema):
1578
+ // copilot 的 mcp 配置 schema 字段名是 `transport`(取值 stdio|http|sse),
1579
+ // 不是 codex/claude 的 `type`。McpConfig.raw 是 canonical(type),写
1580
+ // --additional-mcp-config 时必须翻译 type→transport(仅 copilot 走此分支)。
1581
+ argv.push("--additional-mcp-config".to_string());
1582
+ argv.push(copilot_translate_mcp_config(&config.raw).to_string());
1583
+ }
1584
+ argv
1585
+ }
1586
+
1587
+ /// C-3-4 cr verdict v2 — 把 McpConfig.raw 的 canonical schema(`type`)翻译成
1588
+ /// copilot mcp add/--additional-mcp-config 期望的 `transport` 字段(stdio|http|sse)。
1589
+ /// 仅 Copilot 适配走此翻译,claude/codex 路径不动。
1590
+ fn copilot_translate_mcp_config(raw: &serde_json::Value) -> serde_json::Value {
1591
+ let Some(servers) = raw.as_object() else {
1592
+ return raw.clone();
1593
+ };
1594
+ let mut translated = serde_json::Map::new();
1595
+ for (name, server) in servers {
1596
+ let Some(obj) = server.as_object() else {
1597
+ translated.insert(name.clone(), server.clone());
1598
+ continue;
1599
+ };
1600
+ let mut out = serde_json::Map::new();
1601
+ for (key, value) in obj {
1602
+ if key == "type" {
1603
+ out.insert("transport".to_string(), value.clone());
1604
+ } else {
1605
+ out.insert(key.clone(), value.clone());
1606
+ }
1607
+ }
1608
+ translated.insert(name.clone(), serde_json::Value::Object(out));
1609
+ }
1610
+ serde_json::Value::Object(translated)
1611
+ }
1612
+
1613
+ /// resume 路径同 base + `--resume <sid>`(去 --session-id);单列出
1614
+ /// 避免 plan 端误 push --session-id 与 --resume 同帧。
1615
+ fn copilot_base_command_resume(
1616
+ auth_mode: AuthMode,
1617
+ mcp_config: Option<&McpConfig>,
1618
+ system_prompt: Option<&str>,
1619
+ model: Option<&str>,
1620
+ tools: &[&str],
1621
+ ) -> Vec<String> {
1622
+ copilot_base_command(auth_mode, mcp_config, system_prompt, model, tools)
1623
+ }
1624
+
1625
+ fn copilot_dangerous_auto_approve(tools: &[&str]) -> bool {
1626
+ tools.contains(&"dangerous_auto_approve")
1627
+ }
1628
+
1629
+ /// C-5-2 v2 verdict — copilot 细粒度 deny 映射(canonical tool → copilot flag,
1630
+ /// 全部走 `--deny-tool <kind>`,help-permissions Tool Permissions 节四 kind:
1631
+ /// shell/write/mcp/url):
1632
+ /// execute_bash ∉ allowed → `--deny-tool 'shell'`
1633
+ /// fs_write ∉ allowed → `--deny-tool 'write'`
1634
+ /// network ∉ allowed → `--deny-tool 'url'`(help-permissions: "url(domain-or-url?)
1635
+ /// … If omitted, matches all URLs")
1636
+ /// fs_read/fs_list 在 copilot 上无对应 deny kind(C-5-3 prompt_only 诚实)。
1637
+ fn copilot_permission_flags(tools: &[&str]) -> Vec<String> {
1638
+ let mut flags = Vec::new();
1639
+ if !tools.contains(&"execute_bash") {
1640
+ flags.push("--deny-tool".to_string());
1641
+ flags.push("shell".to_string());
1642
+ }
1643
+ if !tools.contains(&"fs_write") {
1644
+ flags.push("--deny-tool".to_string());
1645
+ flags.push("write".to_string());
1646
+ }
1647
+ if !tools.contains(&"network") {
1648
+ // v2 修正:`--deny-tool 'url'`(省略 domain 匹配全 URL),不是 `--deny-url '*'`
1649
+ // (RC-19 反向 case 守 — 全 URL 拒绝走 deny-tool kind,不走 deny-url path)。
1650
+ flags.push("--deny-tool".to_string());
1651
+ flags.push("url".to_string());
1652
+ }
1653
+ flags
1654
+ }
1655
+
1191
1656
  fn claude_dangerous_auto_approve(tools: &[&str]) -> bool {
1192
1657
  tools.contains(&"dangerous_auto_approve")
1193
1658
  }