@team-agent/installer 0.3.1 → 0.3.3

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 (79) 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 +234 -26
  5. package/crates/team-agent/src/cli/diagnose.rs +144 -10
  6. package/crates/team-agent/src/cli/emit.rs +289 -54
  7. package/crates/team-agent/src/cli/leader.rs +37 -8
  8. package/crates/team-agent/src/cli/mod.rs +1281 -196
  9. package/crates/team-agent/src/cli/status_port.rs +195 -46
  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 +59 -3
  14. package/crates/team-agent/src/cli/types.rs +18 -0
  15. package/crates/team-agent/src/compiler.rs +15 -5
  16. package/crates/team-agent/src/coordinator/health.rs +95 -17
  17. package/crates/team-agent/src/coordinator/mod.rs +4 -0
  18. package/crates/team-agent/src/coordinator/runtime_detectors.rs +500 -0
  19. package/crates/team-agent/src/coordinator/runtime_observation.rs +58 -0
  20. package/crates/team-agent/src/coordinator/tick.rs +222 -69
  21. package/crates/team-agent/src/coordinator/types.rs +15 -3
  22. package/crates/team-agent/src/db/schema.rs +37 -2
  23. package/crates/team-agent/src/diagnose/comms.rs +226 -0
  24. package/crates/team-agent/src/diagnose/mod.rs +45 -0
  25. package/crates/team-agent/src/diagnose/orphans.rs +658 -0
  26. package/crates/team-agent/src/fake_worker.rs +146 -3
  27. package/crates/team-agent/src/leader/start.rs +121 -23
  28. package/crates/team-agent/src/leader/types.rs +44 -1
  29. package/crates/team-agent/src/lib.rs +3 -0
  30. package/crates/team-agent/src/lifecycle/display.rs +645 -47
  31. package/crates/team-agent/src/lifecycle/launch.rs +1061 -146
  32. package/crates/team-agent/src/lifecycle/mod.rs +2 -0
  33. package/crates/team-agent/src/lifecycle/profile_launch.rs +810 -0
  34. package/crates/team-agent/src/lifecycle/profile_smoke.rs +522 -0
  35. package/crates/team-agent/src/lifecycle/restart/agent.rs +99 -23
  36. package/crates/team-agent/src/lifecycle/restart/common.rs +183 -24
  37. package/crates/team-agent/src/lifecycle/restart/rebuild.rs +498 -22
  38. package/crates/team-agent/src/lifecycle/restart/remove.rs +27 -7
  39. package/crates/team-agent/src/lifecycle/restart/team_state.rs +19 -0
  40. package/crates/team-agent/src/lifecycle/restart.rs +24 -1
  41. package/crates/team-agent/src/lifecycle/tests/lane_ops.rs +5 -5
  42. package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +37 -7
  43. package/crates/team-agent/src/lifecycle/types.rs +19 -0
  44. package/crates/team-agent/src/mcp_server/helpers.rs +1 -0
  45. package/crates/team-agent/src/mcp_server/lifecycle_tools/agent_ops.rs +341 -0
  46. package/crates/team-agent/src/mcp_server/lifecycle_tools/mod.rs +10 -0
  47. package/crates/team-agent/src/mcp_server/lifecycle_tools/state_status.rs +158 -0
  48. package/crates/team-agent/src/mcp_server/mod.rs +3 -74
  49. package/crates/team-agent/src/mcp_server/tests/scoped.rs +1 -1
  50. package/crates/team-agent/src/mcp_server/tests/send.rs +6 -5
  51. package/crates/team-agent/src/mcp_server/tools.rs +312 -111
  52. package/crates/team-agent/src/mcp_server/types.rs +6 -4
  53. package/crates/team-agent/src/mcp_server/wire.rs +19 -7
  54. package/crates/team-agent/src/message_store.rs +21 -4
  55. package/crates/team-agent/src/messaging/delivery.rs +470 -59
  56. package/crates/team-agent/src/messaging/mod.rs +9 -6
  57. package/crates/team-agent/src/messaging/results.rs +353 -63
  58. package/crates/team-agent/src/messaging/selftest.rs +199 -12
  59. package/crates/team-agent/src/messaging/send.rs +35 -3
  60. package/crates/team-agent/src/messaging/tests/runtime.rs +19 -4
  61. package/crates/team-agent/src/messaging/types.rs +11 -3
  62. package/crates/team-agent/src/os_probe.rs +119 -0
  63. package/crates/team-agent/src/packaging/migrate.rs +10 -2
  64. package/crates/team-agent/src/packaging/tests.rs +23 -0
  65. package/crates/team-agent/src/provider/adapter.rs +564 -63
  66. package/crates/team-agent/src/provider/approvals/runtime_prompts.rs +1 -7
  67. package/crates/team-agent/src/provider/classify.rs +51 -4
  68. package/crates/team-agent/src/provider/helpers.rs +10 -1
  69. package/crates/team-agent/src/provider/startup_prompt.rs +94 -0
  70. package/crates/team-agent/src/provider/types.rs +47 -0
  71. package/crates/team-agent/src/session_capture.rs +616 -0
  72. package/crates/team-agent/src/state/persist.rs +170 -1
  73. package/crates/team-agent/src/state/projection.rs +141 -8
  74. package/crates/team-agent/src/state/selector.rs +5 -2
  75. package/crates/team-agent/src/tmux_backend.rs +161 -64
  76. package/crates/team-agent/src/transport/test_support.rs +9 -0
  77. package/crates/team-agent/src/transport/tests/wire.rs +4 -0
  78. package/crates/team-agent/src/transport.rs +13 -2
  79. package/package.json +4 -4
@@ -5,8 +5,9 @@ use std::process::Command;
5
5
 
6
6
  use super::helpers::{find_session_id, parse_jsonl_records, patterns};
7
7
  use super::types::{
8
- AuthHintStatus, CaptureVia, CapturedSession, Confidence, McpConfig, ProviderCaps,
9
- ProviderError, RolloutPath, SessionId, StatusPatterns,
8
+ AuthHintStatus, CaptureVia, CapturedSession, CommandPlan, Confidence, McpConfig,
9
+ ProviderCaps, ProviderCommandContext, ProviderError, RolloutPath, SessionId,
10
+ StatusPatterns,
10
11
  };
11
12
  use super::{AuthMode, Provider};
12
13
 
@@ -57,6 +58,20 @@ pub trait ProviderAdapter {
57
58
  tools: &[&str],
58
59
  ) -> Result<Vec<String>, ProviderError>;
59
60
 
61
+ fn build_command_plan(
62
+ &self,
63
+ ctx: ProviderCommandContext<'_>,
64
+ ) -> Result<CommandPlan, ProviderError> {
65
+ self.build_command_with_tools(
66
+ ctx.auth_mode,
67
+ ctx.mcp_config,
68
+ ctx.system_prompt,
69
+ ctx.model,
70
+ ctx.tools,
71
+ )
72
+ .map(CommandPlan::argv_only)
73
+ }
74
+
60
75
  /// 启动后从 provider session 日志捕获 session_id + rollout_path
61
76
  /// (`claude.py:73`/`codex.py:62`)。fs watch / mtime fallback / repair。
62
77
  fn capture_session_id(
@@ -66,6 +81,25 @@ pub trait ProviderAdapter {
66
81
  timeout_s: u64,
67
82
  ) -> Result<Option<CapturedSession>, ProviderError>;
68
83
 
84
+ /// Internal capture surface for same-team multi-agent attribution: enumerate every
85
+ /// cwd-matching provider transcript candidate, then let the runtime allocate them
86
+ /// once per tick/restart pass using per-agent context.
87
+ fn capture_session_candidates(
88
+ &self,
89
+ context: &CaptureSessionContext,
90
+ timeout_s: u64,
91
+ ) -> Result<Vec<CapturedSessionCandidate>, ProviderError> {
92
+ Ok(self
93
+ .capture_session_id(&context.agent_id, &context.spawn_cwd, timeout_s)?
94
+ .into_iter()
95
+ .map(|captured| CapturedSessionCandidate {
96
+ captured,
97
+ positive_agent_id_match: false,
98
+ agent_path_match: false,
99
+ })
100
+ .collect())
101
+ }
102
+
69
103
  /// restart/reset 路径:从已存 transcript/rollout 回收 session_id
70
104
  /// (`claude.py:115`)。`None` 合法(找不到)。
71
105
  fn recover_session_id(
@@ -100,6 +134,22 @@ pub trait ProviderAdapter {
100
134
  tools: &[&str],
101
135
  ) -> Result<Vec<String>, ProviderError>;
102
136
 
137
+ fn build_resume_command_plan(
138
+ &self,
139
+ session_id: Option<&SessionId>,
140
+ ctx: ProviderCommandContext<'_>,
141
+ ) -> Result<CommandPlan, ProviderError> {
142
+ self.build_resume_command_with_context(
143
+ session_id,
144
+ ctx.auth_mode,
145
+ ctx.mcp_config,
146
+ ctx.system_prompt,
147
+ ctx.model,
148
+ ctx.tools,
149
+ )
150
+ .map(CommandPlan::argv_only)
151
+ }
152
+
103
153
  /// 构造 fork 命令(`providers.py:99`)。fork 需 caps.fork ∧ auth_mode!=compatible_api;
104
154
  /// 不支持 → `Err`。
105
155
  fn fork(
@@ -119,6 +169,22 @@ pub trait ProviderAdapter {
119
169
  tools: &[&str],
120
170
  ) -> Result<Vec<String>, ProviderError>;
121
171
 
172
+ fn fork_plan(
173
+ &self,
174
+ session_id: Option<&SessionId>,
175
+ ctx: ProviderCommandContext<'_>,
176
+ ) -> Result<CommandPlan, ProviderError> {
177
+ self.fork_with_context(
178
+ session_id,
179
+ ctx.auth_mode,
180
+ ctx.mcp_config,
181
+ ctx.system_prompt,
182
+ ctx.model,
183
+ ctx.tools,
184
+ )
185
+ .map(CommandPlan::argv_only)
186
+ }
187
+
122
188
  /// 计算本 provider 该用的 MCP server 配置(`adapter.py` mcp_config;claude
123
189
  /// compatible_api 走 `ensure_compatible_claude_mcp_config`)。
124
190
  fn mcp_config(&self, auth_mode: AuthMode) -> Result<McpConfig, ProviderError>;
@@ -134,8 +200,9 @@ pub trait ProviderAdapter {
134
200
  /// 校验 model 名对本 provider 合法(`codex debug models` 等;doctor)。
135
201
  fn validate_model(&self, model: &str) -> Result<bool, ProviderError>;
136
202
 
137
- /// Provider-specific startup prompt handling. Non-Codex adapters return an empty list; Codex
138
- /// delegates to the provider-layer recognizer.
203
+ /// Provider-specific startup prompt handling. Codex and Claude delegate to
204
+ /// provider-layer recognizers; providers without startup prompts return an
205
+ /// empty list.
139
206
  fn handle_startup_prompts(
140
207
  &self,
141
208
  transport: &dyn crate::transport::Transport,
@@ -147,12 +214,35 @@ pub trait ProviderAdapter {
147
214
  Provider::Codex => {
148
215
  super::startup_prompt::codex_handle_startup_prompts(transport, target, checks, sleep_s)
149
216
  }
217
+ Provider::Claude | Provider::ClaudeCode => {
218
+ super::startup_prompt::claude_handle_startup_prompts(
219
+ transport, target, checks, sleep_s,
220
+ )
221
+ }
150
222
  _ => Vec::new(),
151
223
  }))
152
224
  .unwrap_or_default()
153
225
  }
154
226
  }
155
227
 
228
+ #[derive(Debug, Clone, PartialEq, Eq)]
229
+ pub struct CaptureSessionContext {
230
+ pub agent_id: String,
231
+ pub spawn_cwd: PathBuf,
232
+ pub pane_id: Option<String>,
233
+ pub pane_pid: Option<u32>,
234
+ pub spawned_at: Option<String>,
235
+ pub expected_session_id: Option<SessionId>,
236
+ pub provider_projects_root: Option<PathBuf>,
237
+ }
238
+
239
+ #[derive(Debug, Clone, PartialEq, Eq)]
240
+ pub struct CapturedSessionCandidate {
241
+ pub captured: CapturedSession,
242
+ pub positive_agent_id_match: bool,
243
+ pub agent_path_match: bool,
244
+ }
245
+
156
246
  // ===========================================================================
157
247
  // FACADE 自由函数 (doc §71 providers.get_adapter — body unimplemented)
158
248
  // ===========================================================================
@@ -230,9 +320,12 @@ impl ProviderAdapter for BasicProviderAdapter {
230
320
  }
231
321
 
232
322
  fn auth_hint(&self, auth_mode: AuthMode) -> AuthHintStatus {
233
- match auth_mode {
234
- AuthMode::Subscription => AuthHintStatus::Present,
235
- AuthMode::OfficialApi | AuthMode::CompatibleApi => AuthHintStatus::MissingOrUnknown,
323
+ match self.provider {
324
+ Provider::Claude | Provider::ClaudeCode => claude_auth_hint(auth_mode),
325
+ _ => match auth_mode {
326
+ AuthMode::Subscription => AuthHintStatus::Present,
327
+ AuthMode::OfficialApi | AuthMode::CompatibleApi => AuthHintStatus::MissingOrUnknown,
328
+ },
236
329
  }
237
330
  }
238
331
 
@@ -256,7 +349,7 @@ impl ProviderAdapter for BasicProviderAdapter {
256
349
  ) -> Result<Vec<String>, ProviderError> {
257
350
  match self.provider {
258
351
  Provider::Claude | Provider::ClaudeCode => {
259
- Ok(claude_launch_command(self, auth_mode, mcp_config, system_prompt, model)?)
352
+ Ok(claude_launch_command(self, auth_mode, mcp_config, system_prompt, model, tools)?)
260
353
  }
261
354
  Provider::Codex => Ok(codex_base_command(None, auth_mode, mcp_config, system_prompt, model, tools)),
262
355
  Provider::GeminiCli => {
@@ -271,47 +364,83 @@ impl ProviderAdapter for BasicProviderAdapter {
271
364
  }
272
365
  }
273
366
 
367
+ fn build_command_plan(
368
+ &self,
369
+ ctx: ProviderCommandContext<'_>,
370
+ ) -> Result<CommandPlan, ProviderError> {
371
+ match self.provider {
372
+ Provider::Claude | Provider::ClaudeCode => {
373
+ let expected = next_session_token();
374
+ let managed = ctx.profile_launch.is_some_and(|profile| profile.managed_mcp_config);
375
+ let projects_root = ctx
376
+ .profile_launch
377
+ .and_then(|profile| profile.claude_projects_root.clone());
378
+ let model = claude_context_model(ctx);
379
+ let mut argv = claude_base_command(
380
+ self,
381
+ ctx.auth_mode,
382
+ ctx.mcp_config,
383
+ ctx.system_prompt,
384
+ model,
385
+ ctx.tools,
386
+ managed,
387
+ )?;
388
+ argv.push("--session-id".to_string());
389
+ argv.push(expected.clone());
390
+ Ok(CommandPlan {
391
+ argv,
392
+ expected_session_id: Some(SessionId::new(expected)),
393
+ provider_projects_root: projects_root,
394
+ managed_mcp_config: managed,
395
+ })
396
+ }
397
+ _ => self
398
+ .build_command_with_tools(
399
+ ctx.auth_mode,
400
+ ctx.mcp_config,
401
+ ctx.system_prompt,
402
+ ctx.model,
403
+ ctx.tools,
404
+ )
405
+ .map(CommandPlan::argv_only),
406
+ }
407
+ }
408
+
274
409
  fn capture_session_id(
275
410
  &self,
276
411
  agent_id: &str,
277
412
  spawn_cwd: &Path,
278
- _timeout_s: u64,
413
+ timeout_s: u64,
279
414
  ) -> Result<Option<CapturedSession>, ProviderError> {
280
- let candidates = candidate_session_files(spawn_cwd, agent_id)?;
281
- for path in candidates {
282
- let Ok(text) = std::fs::read_to_string(&path) else {
283
- continue;
284
- };
285
- let records = parse_jsonl_records(&text);
286
- if records.is_empty() {
287
- continue;
288
- }
289
- let session_id = records.iter().find_map(find_session_id);
290
- if matches!(self.provider, Provider::Claude | Provider::ClaudeCode)
291
- && session_id.is_some()
292
- && !records.iter().any(has_cwd_field)
293
- {
294
- continue;
415
+ let context = CaptureSessionContext {
416
+ agent_id: agent_id.to_string(),
417
+ spawn_cwd: spawn_cwd.to_path_buf(),
418
+ pane_id: None,
419
+ pane_pid: None,
420
+ spawned_at: None,
421
+ expected_session_id: None,
422
+ provider_projects_root: None,
423
+ };
424
+ Ok(self
425
+ .capture_session_candidates(&context, timeout_s)?
426
+ .into_iter()
427
+ .next()
428
+ .map(|candidate| candidate.captured))
429
+ }
430
+
431
+ fn capture_session_candidates(
432
+ &self,
433
+ context: &CaptureSessionContext,
434
+ timeout_s: u64,
435
+ ) -> Result<Vec<CapturedSessionCandidate>, ProviderError> {
436
+ let deadline = std::time::Instant::now() + std::time::Duration::from_secs(timeout_s);
437
+ loop {
438
+ let out = scan_session_candidates_once(self.provider, context)?;
439
+ if !out.is_empty() || timeout_s == 0 || std::time::Instant::now() >= deadline {
440
+ return Ok(out);
295
441
  }
296
- let captured_via = if session_id.is_some() {
297
- CaptureVia::FsWatch
298
- } else {
299
- CaptureVia::FsMtimeFallback
300
- };
301
- let attribution_confidence = if session_id.is_some() {
302
- Confidence::High
303
- } else {
304
- Confidence::Low
305
- };
306
- return Ok(Some(CapturedSession {
307
- session_id: session_id.map(SessionId::new),
308
- rollout_path: Some(RolloutPath::new(path)),
309
- captured_via,
310
- attribution_confidence,
311
- spawn_cwd: spawn_cwd.to_path_buf(),
312
- }));
442
+ std::thread::sleep(std::time::Duration::from_millis(100));
313
443
  }
314
- Ok(None)
315
444
  }
316
445
 
317
446
  fn recover_session_id(
@@ -376,7 +505,8 @@ impl ProviderAdapter for BasicProviderAdapter {
376
505
  Ok(argv)
377
506
  }
378
507
  Provider::Claude | Provider::ClaudeCode => {
379
- let mut argv = claude_base_command(self, auth_mode, mcp_config, system_prompt, model)?;
508
+ let mut argv =
509
+ claude_base_command(self, auth_mode, mcp_config, system_prompt, model, tools, false)?;
380
510
  argv.push("--resume".to_string());
381
511
  argv.push(session_id.as_str().to_string());
382
512
  Ok(argv)
@@ -388,6 +518,49 @@ impl ProviderAdapter for BasicProviderAdapter {
388
518
  }
389
519
  }
390
520
 
521
+ fn build_resume_command_plan(
522
+ &self,
523
+ session_id: Option<&SessionId>,
524
+ ctx: ProviderCommandContext<'_>,
525
+ ) -> Result<CommandPlan, ProviderError> {
526
+ match self.provider {
527
+ Provider::Claude | Provider::ClaudeCode => {
528
+ let Some(session_id) = session_id else {
529
+ return Err(ProviderError::ResumeUnavailable("resume requires session_id".to_string()));
530
+ };
531
+ let managed = ctx.profile_launch.is_some_and(|profile| profile.managed_mcp_config);
532
+ let model = claude_context_model(ctx);
533
+ let mut argv = claude_base_command(
534
+ self,
535
+ ctx.auth_mode,
536
+ ctx.mcp_config,
537
+ ctx.system_prompt,
538
+ model,
539
+ ctx.tools,
540
+ managed,
541
+ )?;
542
+ argv.push("--resume".to_string());
543
+ argv.push(session_id.as_str().to_string());
544
+ let mut plan = CommandPlan::argv_only(argv);
545
+ plan.provider_projects_root = ctx
546
+ .profile_launch
547
+ .and_then(|profile| profile.claude_projects_root.clone());
548
+ plan.managed_mcp_config = managed;
549
+ Ok(plan)
550
+ }
551
+ _ => self
552
+ .build_resume_command_with_context(
553
+ session_id,
554
+ ctx.auth_mode,
555
+ ctx.mcp_config,
556
+ ctx.system_prompt,
557
+ ctx.model,
558
+ ctx.tools,
559
+ )
560
+ .map(CommandPlan::argv_only),
561
+ }
562
+ }
563
+
391
564
  fn fork(
392
565
  &self,
393
566
  session_id: Option<&SessionId>,
@@ -429,7 +602,8 @@ impl ProviderAdapter for BasicProviderAdapter {
429
602
  Ok(argv)
430
603
  }
431
604
  Provider::Claude | Provider::ClaudeCode => {
432
- let mut argv = claude_base_command(self, auth_mode, mcp_config, system_prompt, model)?;
605
+ let mut argv =
606
+ claude_base_command(self, auth_mode, mcp_config, system_prompt, model, tools, false)?;
433
607
  argv.push("--session-id".to_string());
434
608
  argv.push(next_session_token());
435
609
  argv.push("--resume".to_string());
@@ -444,6 +618,62 @@ impl ProviderAdapter for BasicProviderAdapter {
444
618
  }
445
619
  }
446
620
 
621
+ fn fork_plan(
622
+ &self,
623
+ session_id: Option<&SessionId>,
624
+ ctx: ProviderCommandContext<'_>,
625
+ ) -> Result<CommandPlan, ProviderError> {
626
+ match self.provider {
627
+ Provider::Claude | Provider::ClaudeCode => {
628
+ if !self.caps().fork || ctx.auth_mode == AuthMode::CompatibleApi {
629
+ return Err(ProviderError::CapabilityUnsupported(format!(
630
+ "{} does not support native session fork",
631
+ provider_wire(self.provider)
632
+ )));
633
+ }
634
+ let Some(session_id) = session_id else {
635
+ return Err(ProviderError::ResumeUnavailable("fork requires session_id".to_string()));
636
+ };
637
+ let expected = next_session_token();
638
+ let managed = ctx.profile_launch.is_some_and(|profile| profile.managed_mcp_config);
639
+ let projects_root = ctx
640
+ .profile_launch
641
+ .and_then(|profile| profile.claude_projects_root.clone());
642
+ let model = claude_context_model(ctx);
643
+ let mut argv = claude_base_command(
644
+ self,
645
+ ctx.auth_mode,
646
+ ctx.mcp_config,
647
+ ctx.system_prompt,
648
+ model,
649
+ ctx.tools,
650
+ managed,
651
+ )?;
652
+ argv.push("--session-id".to_string());
653
+ argv.push(expected.clone());
654
+ argv.push("--resume".to_string());
655
+ argv.push(session_id.as_str().to_string());
656
+ argv.push("--fork-session".to_string());
657
+ Ok(CommandPlan {
658
+ argv,
659
+ expected_session_id: Some(SessionId::new(expected)),
660
+ provider_projects_root: projects_root,
661
+ managed_mcp_config: managed,
662
+ })
663
+ }
664
+ _ => self
665
+ .fork_with_context(
666
+ session_id,
667
+ ctx.auth_mode,
668
+ ctx.mcp_config,
669
+ ctx.system_prompt,
670
+ ctx.model,
671
+ ctx.tools,
672
+ )
673
+ .map(CommandPlan::argv_only),
674
+ }
675
+ }
676
+
447
677
  fn mcp_config(&self, auth_mode: AuthMode) -> Result<McpConfig, ProviderError> {
448
678
  let server = mcp_server_config(auth_mode);
449
679
  Ok(McpConfig {
@@ -511,6 +741,101 @@ fn auth_mode_wire(auth_mode: AuthMode) -> &'static str {
511
741
  }
512
742
  }
513
743
 
744
+ fn claude_auth_hint(auth_mode: AuthMode) -> AuthHintStatus {
745
+ if auth_mode != AuthMode::Subscription {
746
+ return AuthHintStatus::MissingOrUnknown;
747
+ }
748
+ if !command_on_path("claude") {
749
+ return AuthHintStatus::Missing;
750
+ }
751
+ let output = match Command::new("claude").args(["auth", "status"]).output() {
752
+ Ok(output) => output,
753
+ Err(_) => return AuthHintStatus::MissingOrUnknown,
754
+ };
755
+ let text = if output.stdout.is_empty() {
756
+ String::from_utf8_lossy(&output.stderr).to_string()
757
+ } else {
758
+ String::from_utf8_lossy(&output.stdout).to_string()
759
+ };
760
+ let status = serde_json::from_str::<serde_json::Value>(text.trim()).unwrap_or_default();
761
+ if status.get("loggedIn").and_then(serde_json::Value::as_bool) == Some(true)
762
+ || output.status.success()
763
+ {
764
+ AuthHintStatus::Present
765
+ } else {
766
+ AuthHintStatus::Missing
767
+ }
768
+ }
769
+
770
+ fn claude_context_model(ctx: ProviderCommandContext<'_>) -> Option<&str> {
771
+ ctx.profile_launch
772
+ .and_then(|profile| profile.command_overrides.model.as_deref())
773
+ .or(ctx.model)
774
+ }
775
+
776
+ fn scan_session_candidates_once(
777
+ provider: Provider,
778
+ context: &CaptureSessionContext,
779
+ ) -> Result<Vec<CapturedSessionCandidate>, ProviderError> {
780
+ let candidates = candidate_session_files(provider, context)?;
781
+ let mut out = Vec::new();
782
+ for candidate in candidates {
783
+ let path = candidate.path;
784
+ let Ok(text) = std::fs::read_to_string(&path) else {
785
+ continue;
786
+ };
787
+ let records = parse_session_records(&text);
788
+ if records.is_empty() {
789
+ continue;
790
+ }
791
+ if candidate.requires_cwd_match
792
+ && !provider_home_records_match_spawn_cwd(&records, &context.spawn_cwd)
793
+ {
794
+ continue;
795
+ }
796
+ let session_id = records.iter().find_map(find_session_id);
797
+ if matches!(provider, Provider::Claude | Provider::ClaudeCode)
798
+ && session_id.is_some()
799
+ && !records.iter().any(has_cwd_field)
800
+ {
801
+ continue;
802
+ }
803
+ let captured_via = if session_id.is_some() {
804
+ CaptureVia::FsWatch
805
+ } else {
806
+ CaptureVia::FsMtimeFallback
807
+ };
808
+ let attribution_confidence = if session_id.is_some() {
809
+ Confidence::High
810
+ } else {
811
+ Confidence::Low
812
+ };
813
+ let positive_agent_id_match = candidate_text_has_team_agent_id(&text, context);
814
+ let agent_path_match = candidate_path_matches_agent_id(&path, context);
815
+ out.push(CapturedSessionCandidate {
816
+ captured: CapturedSession {
817
+ session_id: session_id.map(SessionId::new),
818
+ rollout_path: Some(RolloutPath::new(path)),
819
+ captured_via,
820
+ attribution_confidence,
821
+ spawn_cwd: context.spawn_cwd.clone(),
822
+ },
823
+ positive_agent_id_match,
824
+ agent_path_match,
825
+ });
826
+ }
827
+ if let Some(expected) = context.expected_session_id.as_ref() {
828
+ out.sort_by_key(|candidate| {
829
+ candidate
830
+ .captured
831
+ .session_id
832
+ .as_ref()
833
+ .is_none_or(|session| session.as_str() != expected.as_str())
834
+ });
835
+ }
836
+ Ok(out)
837
+ }
838
+
514
839
  fn command_on_path(name: &str) -> bool {
515
840
  let Some(path) = std::env::var_os("PATH") else {
516
841
  return false;
@@ -518,18 +843,58 @@ fn command_on_path(name: &str) -> bool {
518
843
  std::env::split_paths(&path).any(|dir| dir.join(name).is_file())
519
844
  }
520
845
 
521
- fn candidate_session_files(spawn_cwd: &Path, agent_id: &str) -> Result<Vec<PathBuf>, ProviderError> {
846
+ struct SessionCandidate {
847
+ path: PathBuf,
848
+ requires_cwd_match: bool,
849
+ }
850
+
851
+ fn candidate_session_files(
852
+ provider: Provider,
853
+ context: &CaptureSessionContext,
854
+ ) -> Result<Vec<SessionCandidate>, ProviderError> {
522
855
  let mut out = Vec::new();
523
- collect_candidate_files(spawn_cwd, agent_id, 0, &mut out)?;
524
- out.sort_by(|a, b| a.to_string_lossy().cmp(&b.to_string_lossy()));
856
+ if let Some(root) = context.provider_projects_root.as_ref() {
857
+ collect_optional_candidate_files(root, &context.agent_id, &mut out)?;
858
+ }
859
+ collect_candidate_files(&context.spawn_cwd, &context.agent_id, 0, false, &mut out)?;
860
+ if let Some(home) = std::env::var_os("HOME").map(PathBuf::from) {
861
+ match provider {
862
+ Provider::Codex => {
863
+ collect_optional_candidate_files(&home.join(".codex").join("sessions"), &context.agent_id, &mut out)?;
864
+ }
865
+ Provider::Claude | Provider::ClaudeCode => {
866
+ collect_optional_candidate_files(&home.join(".claude").join("sessions"), &context.agent_id, &mut out)?;
867
+ collect_optional_candidate_files(&home.join(".claude").join("projects"), &context.agent_id, &mut out)?;
868
+ }
869
+ Provider::GeminiCli | Provider::Fake => {}
870
+ }
871
+ }
872
+ out.sort_by(|a, b| {
873
+ a.requires_cwd_match
874
+ .cmp(&b.requires_cwd_match)
875
+ .then_with(|| a.path.to_string_lossy().cmp(&b.path.to_string_lossy()))
876
+ });
877
+ out.dedup_by(|a, b| a.path == b.path && a.requires_cwd_match == b.requires_cwd_match);
525
878
  Ok(out)
526
879
  }
527
880
 
881
+ fn collect_optional_candidate_files(
882
+ dir: &Path,
883
+ agent_id: &str,
884
+ out: &mut Vec<SessionCandidate>,
885
+ ) -> Result<(), ProviderError> {
886
+ if dir.exists() {
887
+ let _ = collect_candidate_files(dir, agent_id, 0, true, out);
888
+ }
889
+ Ok(())
890
+ }
891
+
528
892
  fn collect_candidate_files(
529
893
  dir: &Path,
530
894
  agent_id: &str,
531
895
  depth: usize,
532
- out: &mut Vec<PathBuf>,
896
+ requires_cwd_match: bool,
897
+ out: &mut Vec<SessionCandidate>,
533
898
  ) -> Result<(), ProviderError> {
534
899
  if depth > 4 {
535
900
  return Ok(());
@@ -545,9 +910,12 @@ fn collect_candidate_files(
545
910
  };
546
911
  let path = entry.path();
547
912
  if path.is_dir() {
548
- collect_candidate_files(&path, agent_id, depth.saturating_add(1), out)?;
913
+ collect_candidate_files(&path, agent_id, depth.saturating_add(1), requires_cwd_match, out)?;
549
914
  } else if looks_like_session_file(&path, agent_id) {
550
- out.push(path);
915
+ out.push(SessionCandidate {
916
+ path,
917
+ requires_cwd_match,
918
+ });
551
919
  }
552
920
  }
553
921
  Ok(())
@@ -572,6 +940,77 @@ fn looks_like_session_file(path: &Path, agent_id: &str) -> bool {
572
940
  || (!agent_id.is_empty() && name.contains(agent_id))
573
941
  }
574
942
 
943
+ fn parse_session_records(text: &str) -> Vec<serde_json::Value> {
944
+ match serde_json::from_str::<serde_json::Value>(text) {
945
+ Ok(serde_json::Value::Array(items)) => items,
946
+ Ok(value) => vec![value],
947
+ Err(_) => parse_jsonl_records(text),
948
+ }
949
+ }
950
+
951
+ fn provider_home_records_match_spawn_cwd(records: &[serde_json::Value], spawn_cwd: &Path) -> bool {
952
+ let cwd_values: Vec<String> = records.iter().filter_map(record_cwd).collect();
953
+ !cwd_values.is_empty()
954
+ && cwd_values
955
+ .iter()
956
+ .any(|cwd| paths_equivalent(Path::new(cwd), spawn_cwd))
957
+ }
958
+
959
+ fn candidate_text_has_team_agent_id(text: &str, context: &CaptureSessionContext) -> bool {
960
+ let id = context.agent_id.as_str();
961
+ if id.is_empty() {
962
+ return false;
963
+ }
964
+ [
965
+ format!("\"TEAM_AGENT_ID\":\"{id}\""),
966
+ format!("\"TEAM_AGENT_ID\": \"{id}\""),
967
+ format!("TEAM_AGENT_ID={id}"),
968
+ format!("env.TEAM_AGENT_ID=\"{id}\""),
969
+ format!("env.TEAM_AGENT_ID=\\\"{id}\\\""),
970
+ format!("\"TEAM_AGENT_AGENT_ID\":\"{id}\""),
971
+ format!("\"TEAM_AGENT_AGENT_ID\": \"{id}\""),
972
+ format!("TEAM_AGENT_AGENT_ID={id}"),
973
+ ]
974
+ .iter()
975
+ .any(|needle| text.contains(needle))
976
+ }
977
+
978
+ fn candidate_path_matches_agent_id(path: &Path, context: &CaptureSessionContext) -> bool {
979
+ let id = context.agent_id.as_str();
980
+ if id.is_empty() {
981
+ return false;
982
+ }
983
+ let Some(name) = path.file_name().and_then(|value| value.to_str()) else {
984
+ return false;
985
+ };
986
+ let dashed = id.replace('_', "-");
987
+ name.contains(id) || name.contains(&dashed)
988
+ }
989
+
990
+ fn record_cwd(record: &serde_json::Value) -> Option<String> {
991
+ record
992
+ .get("cwd")
993
+ .and_then(serde_json::Value::as_str)
994
+ .or_else(|| {
995
+ record
996
+ .get("session_meta")
997
+ .and_then(|v| v.get("payload"))
998
+ .or_else(|| record.get("payload"))
999
+ .and_then(|v| v.get("cwd"))
1000
+ .and_then(serde_json::Value::as_str)
1001
+ })
1002
+ .map(ToString::to_string)
1003
+ }
1004
+
1005
+ fn paths_equivalent(left: &Path, right: &Path) -> bool {
1006
+ if left == right {
1007
+ return true;
1008
+ }
1009
+ let left = std::fs::canonicalize(left).unwrap_or_else(|_| left.to_path_buf());
1010
+ let right = std::fs::canonicalize(right).unwrap_or_else(|_| right.to_path_buf());
1011
+ left == right || left.parent().is_some_and(|parent| parent == right)
1012
+ }
1013
+
575
1014
  /// `true` iff any path component is `.team` (the Team Agent runtime/logs root) — used
576
1015
  /// to gate session-file detection so `<workspace>/.team/logs/events.jsonl`,
577
1016
  /// `.team/runtime/team.db`, etc. are NEVER mistaken for a provider transcript.
@@ -601,8 +1040,9 @@ fn claude_launch_command(
601
1040
  mcp_config: Option<&McpConfig>,
602
1041
  system_prompt: Option<&str>,
603
1042
  model: Option<&str>,
1043
+ tools: &[&str],
604
1044
  ) -> Result<Vec<String>, ProviderError> {
605
- let mut argv = claude_base_command(adapter, auth_mode, mcp_config, system_prompt, model)?;
1045
+ let mut argv = claude_base_command(adapter, auth_mode, mcp_config, system_prompt, model, tools, false)?;
606
1046
  argv.push("--session-id".to_string());
607
1047
  argv.push(next_session_token());
608
1048
  Ok(argv)
@@ -614,12 +1054,16 @@ fn claude_base_command(
614
1054
  mcp_config: Option<&McpConfig>,
615
1055
  system_prompt: Option<&str>,
616
1056
  model: Option<&str>,
1057
+ tools: &[&str],
1058
+ managed_mcp_config: bool,
617
1059
  ) -> Result<Vec<String>, ProviderError> {
618
- let mut argv = vec![
619
- "claude".to_string(),
620
- "--permission-mode".to_string(),
621
- "default".to_string(),
622
- ];
1060
+ let mut argv = vec!["claude".to_string()];
1061
+ if claude_dangerous_auto_approve(tools) {
1062
+ argv.push("--dangerously-skip-permissions".to_string());
1063
+ } else {
1064
+ argv.push("--permission-mode".to_string());
1065
+ argv.push("default".to_string());
1066
+ }
623
1067
  if let Some(model) = model {
624
1068
  argv.push("--model".to_string());
625
1069
  argv.push(model.to_string());
@@ -628,7 +1072,11 @@ fn claude_base_command(
628
1072
  argv.push("--append-system-prompt".to_string());
629
1073
  argv.push(prompt.to_string());
630
1074
  }
631
- if mcp_config.is_some() || auth_mode == AuthMode::CompatibleApi || system_prompt.is_some_and(prompt_needs_native_mcp) {
1075
+ if !managed_mcp_config
1076
+ && (mcp_config.is_some()
1077
+ || auth_mode == AuthMode::CompatibleApi
1078
+ || system_prompt.is_some_and(prompt_needs_native_mcp))
1079
+ {
632
1080
  let raw = if let Some(config) = mcp_config {
633
1081
  serde_json::json!({"mcpServers": config.raw.clone()})
634
1082
  } else {
@@ -637,10 +1085,10 @@ fn claude_base_command(
637
1085
  argv.push("--mcp-config".to_string());
638
1086
  argv.push(raw.to_string());
639
1087
  argv.push("--strict-mcp-config".to_string());
640
- for tool in ["Bash", "Grep"] {
641
- argv.push("--disallowedTools".to_string());
642
- argv.push(tool.to_string());
643
- }
1088
+ }
1089
+ for tool in claude_disallowed_tools(tools) {
1090
+ argv.push("--disallowedTools".to_string());
1091
+ argv.push(tool.to_string());
644
1092
  }
645
1093
  Ok(argv)
646
1094
  }
@@ -740,6 +1188,27 @@ fn codex_dangerous_auto_approve(tools: &[&str]) -> bool {
740
1188
  tools.contains(&"dangerous_auto_approve")
741
1189
  }
742
1190
 
1191
+ fn claude_dangerous_auto_approve(tools: &[&str]) -> bool {
1192
+ tools.contains(&"dangerous_auto_approve")
1193
+ }
1194
+
1195
+ fn claude_disallowed_tools(tools: &[&str]) -> Vec<&'static str> {
1196
+ let mut disallowed = Vec::new();
1197
+ if !tools.contains(&"execute_bash") {
1198
+ disallowed.push("Bash");
1199
+ }
1200
+ if !tools.contains(&"fs_read") {
1201
+ disallowed.push("Read");
1202
+ }
1203
+ if !tools.contains(&"fs_write") {
1204
+ disallowed.extend(["Edit", "Write", "MultiEdit", "NotebookEdit"]);
1205
+ }
1206
+ if !tools.contains(&"fs_list") {
1207
+ disallowed.extend(["Glob", "Grep"]);
1208
+ }
1209
+ disallowed
1210
+ }
1211
+
743
1212
  fn codex_sandbox_mode(tools: &[&str]) -> &'static str {
744
1213
  if tools.iter().any(|tool| matches!(*tool, "fs_write" | "execute_bash")) {
745
1214
  "workspace-write"
@@ -790,12 +1259,44 @@ fn current_team_agent_command() -> String {
790
1259
  }
791
1260
 
792
1261
  fn has_cwd_field(record: &serde_json::Value) -> bool {
793
- record.get("cwd").and_then(serde_json::Value::as_str).is_some()
1262
+ record_cwd(record).is_some()
794
1263
  }
795
1264
 
796
1265
  fn next_session_token() -> String {
1266
+ use sha2::Digest;
1267
+
1268
+ static COUNTER: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
1269
+
797
1270
  let nanos = std::time::SystemTime::now()
798
1271
  .duration_since(std::time::UNIX_EPOCH)
799
1272
  .map_or(0, |d| d.as_nanos());
800
- format!("session-{nanos:x}")
1273
+ let counter = COUNTER.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
1274
+ let mut hasher = sha2::Sha256::new();
1275
+ hasher.update(nanos.to_le_bytes());
1276
+ hasher.update(std::process::id().to_le_bytes());
1277
+ hasher.update(counter.to_le_bytes());
1278
+ let digest = hasher.finalize();
1279
+ let mut bytes = [0u8; 16];
1280
+ bytes.copy_from_slice(&digest[..16]);
1281
+ bytes[6] = (bytes[6] & 0x0f) | 0x40;
1282
+ bytes[8] = (bytes[8] & 0x3f) | 0x80;
1283
+ format!(
1284
+ "{:02x}{:02x}{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}",
1285
+ bytes[0],
1286
+ bytes[1],
1287
+ bytes[2],
1288
+ bytes[3],
1289
+ bytes[4],
1290
+ bytes[5],
1291
+ bytes[6],
1292
+ bytes[7],
1293
+ bytes[8],
1294
+ bytes[9],
1295
+ bytes[10],
1296
+ bytes[11],
1297
+ bytes[12],
1298
+ bytes[13],
1299
+ bytes[14],
1300
+ bytes[15],
1301
+ )
801
1302
  }