1use std::path::Path;
13
14use anyhow::{Context, Result, bail, ensure};
15
16use super::worker_binary::{
17 apply_staged_execution_setting, bridge_launch, container_upload_ownership_args,
18 link_shared_login, shared_profile_login, stage_profile,
19};
20use super::{Controller, execute_checked};
21use crate::targets::{self, CommandExecutor, CommandSpec, ProcessExecutor};
22use mj_core::worker_launch::{
23 ReviewMcpDelivery, ReviewMcpServer, ReviewerLaunchConfig, reviewer_staging_profile_home,
24};
25
26pub fn reviewer_stager() -> mj_client::session::ReviewerStager {
30 mj_client::session::ReviewerStager::new(ControllerReviewerStager)
31}
32
33struct ControllerReviewerStager;
34
35impl mj_client::session::ReviewerStagerBackend for ControllerReviewerStager {
36 fn stage(
37 &self,
38 _config: mj_core::config::Config,
39 session: mj_core::state::SessionRecord,
40 profile_id: String,
41 generation: u64,
42 cancelled: std::sync::Arc<std::sync::atomic::AtomicBool>,
43 ) -> Result<ReviewerLaunchConfig> {
44 let session_id = session.id.clone();
45 let controller = Controller::load()?;
46 let executor = crate::targets::CancellableProcessExecutor::new(cancelled)
47 .with_deadline(std::time::Duration::from_secs(90));
48 controller.stage_reviewer_profile_controlled(
49 &session_id,
50 &profile_id,
51 generation,
52 &[],
53 &executor,
54 )
55 }
56}
57
58impl Controller {
59 pub fn stage_reviewer_profile(
65 &self,
66 session_id: &str,
67 profile_id: &str,
68 generation: u64,
69 ) -> Result<ReviewerLaunchConfig> {
70 self.stage_reviewer_profile_controlled(
71 session_id,
72 profile_id,
73 generation,
74 &[],
75 &ProcessExecutor,
76 )
77 }
78
79 pub fn stage_reviewer_profile_controlled(
80 &self,
81 session_id: &str,
82 profile_id: &str,
83 generation: u64,
84 mcp_servers: &[ReviewMcpServer],
85 executor: &impl CommandExecutor,
86 ) -> Result<ReviewerLaunchConfig> {
87 let profile = self
88 .config
89 .profiles
90 .get(profile_id)
91 .with_context(|| format!("unknown profile {profile_id:?}"))?;
92 ensure!(
93 profile.enabled,
94 "reviewer profile {profile_id:?} is disabled"
95 );
96 let session = self
97 .state
98 .sessions
99 .get(session_id)
100 .with_context(|| format!("unknown session {session_id}"))?;
101 let target = session.target_runtime_settings(&self.config)?;
102 let execution_policy = target.execution_policy;
103 let (backend, worker_root) = self.worker_placement(session_id)?;
104
105 let staging = tempfile::tempdir().context("create reviewer staging directory")?;
106 let local = staging.path().join("profile");
107 profile.ensure_ready(profile_id)?;
108 stage_profile(profile, &local).with_context(|| format!("stage profile {profile_id:?}"))?;
109 apply_staged_execution_setting(profile.kind, execution_policy, &local)
110 .with_context(|| format!("stage profile {profile_id:?}"))?;
111 if !mcp_servers.is_empty()
115 && ReviewMcpDelivery::for_harness(profile.kind) == ReviewMcpDelivery::HarnessProfile
116 {
117 configure_staged_review_mcp(profile.kind, &local, mcp_servers)
118 .with_context(|| format!("configure reviewer MCP servers for {profile_id:?}"))?;
119 }
120 if let Some(login) = shared_profile_login(profile, &backend) {
123 link_shared_login(&local, &login)?;
124 }
125 upload_reviewer_profile(executor, &backend, &worker_root, generation, &local)?;
126
127 let (bridge_command, bridge_args) = bridge_launch(profile.kind, execution_policy);
128 let mut environment = profile.environment.resolved().clone();
129 environment.remove(profile.home_env());
133 let excluded_environment = profile.exclude_harness_environment(&mut environment);
136 if backend.container_engine() == Some("podman")
140 && profile.kind == mj_core::config::HarnessKind::Claude
141 {
142 environment.insert("IS_SANDBOX".into(), "1".into());
143 }
144 Ok(ReviewerLaunchConfig {
145 profile_id: profile_id.to_owned(),
146 harness: profile.kind,
147 bridge_command: bridge_command.into(),
148 bridge_args,
149 environment,
150 excluded_environment,
151 execution_policy,
152 model: None,
153 effort: None,
154 fast_mode: None,
155 generation,
156 mcp_servers: mcp_servers.to_vec(),
159 })
160 }
161}
162
163fn configure_staged_review_mcp(
171 harness: mj_core::config::HarnessKind,
172 profile_stage: &Path,
173 servers: &[ReviewMcpServer],
174) -> Result<()> {
175 let Some(file) = harness.mcp_config_file() else {
176 bail!("{harness:?} does not read MCP servers from its profile");
177 };
178 let kimi = harness == mj_core::config::HarnessKind::Kimi;
179 let path = profile_stage.join(file);
180 let mut document = match std::fs::read(&path) {
181 Ok(body) => serde_json::from_slice::<serde_json::Value>(&body)
182 .with_context(|| format!("parse staged reviewer configuration {}", path.display()))?,
183 Err(error) if error.kind() == std::io::ErrorKind::NotFound => {
184 serde_json::Value::Object(serde_json::Map::new())
185 }
186 Err(error) => {
187 return Err(error)
188 .with_context(|| format!("read staged reviewer configuration {}", path.display()));
189 }
190 };
191 let root = document.as_object_mut().with_context(|| {
192 format!(
193 "staged reviewer configuration {} must contain a JSON object",
194 path.display()
195 )
196 })?;
197 let configured = root
198 .entry("mcpServers")
199 .or_insert_with(|| serde_json::Value::Object(serde_json::Map::new()))
200 .as_object_mut()
201 .with_context(|| {
202 format!(
203 "mcpServers in staged reviewer configuration {} must be a JSON object",
204 path.display()
205 )
206 })?;
207 for server in servers {
208 let mut entry = serde_json::json!({
209 "command": server.command,
210 "args": server.args,
211 });
212 if kimi {
213 let object = entry
214 .as_object_mut()
215 .expect("the server entry is a JSON object");
216 object.insert("transport".into(), "stdio".into());
217 object.insert("runtime_id".into(), "local".into());
218 } else {
219 let object = entry
220 .as_object_mut()
221 .expect("the server entry is a JSON object");
222 object.insert("type".into(), "stdio".into());
223 }
224 configured.insert(server.name.clone(), entry);
225 }
226 let mut body = serde_json::to_vec_pretty(&document)?;
227 body.push(b'\n');
228 mj_core::config::atomic_write(&path, &body)
229 .with_context(|| format!("write staged reviewer configuration {}", path.display()))
230}
231
232fn reviewer_profile_home(worker_root: &str, generation: u64) -> String {
238 reviewer_staging_profile_home(Path::new(worker_root), generation)
239 .to_string_lossy()
240 .into_owned()
241}
242
243fn upload_reviewer_profile(
249 executor: &impl CommandExecutor,
250 locator: &targets::TargetLocator,
251 worker_root: &str,
252 generation: u64,
253 local: &Path,
254) -> Result<()> {
255 let home = reviewer_profile_home(worker_root, generation);
256 match locator {
257 targets::TargetLocator::LocalBare { .. } => {
258 for command in [
259 CommandSpec::new("rm", ["-rf", "--", &home])
260 .purpose("clear the local reviewer profile"),
261 CommandSpec::new("mkdir", ["-p", &home])
262 .purpose("create the local reviewer profile directory"),
263 CommandSpec::new(
264 "cp",
265 [
266 "-R".to_owned(),
267 format!("{}/.", local.display()),
268 home.clone(),
269 ],
270 )
271 .purpose("install the local reviewer profile"),
272 CommandSpec::new("chmod", ["-R", "go-rwx", &home])
273 .purpose("restrict local reviewer profile permissions"),
274 ] {
275 execute_checked(executor, command)?;
276 }
277 }
278 targets::TargetLocator::LocalPodman { container_id, .. }
279 | targets::TargetLocator::LocalDocker { container_id, .. }
280 | targets::TargetLocator::AppleContainer { container_id, .. } => {
281 let engine = match locator {
282 targets::TargetLocator::LocalPodman { .. } => "podman",
283 targets::TargetLocator::LocalDocker { .. } => "docker",
284 targets::TargetLocator::AppleContainer { .. } => "container",
285 _ => unreachable!("matched local container target"),
286 };
287 for arguments in [
288 vec![
289 "exec".to_owned(),
290 container_id.clone(),
291 "rm".to_owned(),
292 "-rf".to_owned(),
293 "--".to_owned(),
294 home.clone(),
295 ],
296 vec![
297 "exec".to_owned(),
298 container_id.clone(),
299 "mkdir".to_owned(),
300 "-p".to_owned(),
301 home.clone(),
302 ],
303 vec![
304 "cp".to_owned(),
305 format!("{}/.", local.display()),
306 format!("{container_id}:{home}"),
307 ],
308 container_upload_ownership_args(engine, container_id, worker_root, &[&home]),
309 vec![
310 "exec".to_owned(),
311 container_id.clone(),
312 "chmod".to_owned(),
313 "-R".to_owned(),
314 "go-rwx".to_owned(),
315 home.clone(),
316 ],
317 ] {
318 execute_checked(
319 executor,
320 CommandSpec::new(engine, arguments).purpose("stage the reviewer profile"),
321 )?;
322 }
323 }
324 targets::TargetLocator::AwsEc2 { ssh, .. }
325 | targets::TargetLocator::SshBare { ssh, .. } => {
326 let incoming = format!("{home}.incoming");
327 execute_checked(
328 executor,
329 crate::targets::ssh_command(ssh, ["mkdir", "-p", worker_root])
330 .purpose("create the reviewer directory"),
331 )?;
332 execute_checked(
333 executor,
334 crate::targets::ssh_command(ssh, ["rm", "-rf", "--", &incoming, &home])
335 .purpose("clear the reviewer profile"),
336 )?;
337 execute_checked(
338 executor,
339 crate::targets::scp_upload(ssh, local, &incoming, true)
340 .purpose("upload the reviewer profile"),
341 )?;
342 execute_checked(
343 executor,
344 crate::targets::ssh_command(ssh, ["mv", &incoming, &home])
345 .purpose("install the reviewer profile"),
346 )?;
347 execute_checked(
348 executor,
349 crate::targets::ssh_command(ssh, ["chmod", "-R", "go-rwx", &home])
350 .purpose("restrict reviewer profile permissions"),
351 )?;
352 }
353 targets::TargetLocator::SshPodman {
354 ssh, container_id, ..
355 }
356 | targets::TargetLocator::SshDocker {
357 ssh, container_id, ..
358 } => {
359 let engine = match locator {
360 targets::TargetLocator::SshPodman { .. } => "podman",
361 targets::TargetLocator::SshDocker { .. } => "docker",
362 _ => unreachable!("matched remote container target"),
363 };
364 let worker = Path::new(worker_root)
367 .file_name()
368 .map(|name| name.to_string_lossy().into_owned())
369 .filter(|name| !name.is_empty())
370 .ok_or_else(|| anyhow::anyhow!("worker root {worker_root:?} names no worker"))?;
371 let upload = format!(
372 "{}/{worker}-reviewer-{generation}",
373 targets::REMOTE_UPLOAD_STAGING
374 );
375 execute_checked(
376 executor,
377 crate::targets::ssh_command(ssh, ["mkdir", "-p", targets::REMOTE_UPLOAD_STAGING])
378 .purpose("create remote reviewer staging"),
379 )?;
380 execute_checked(
381 executor,
382 crate::targets::ssh_command(ssh, ["rm", "-rf", "--", &upload])
383 .purpose("clear remote reviewer staging"),
384 )?;
385 execute_checked(
386 executor,
387 crate::targets::scp_upload(ssh, local, &upload, true)
388 .purpose("upload the remote reviewer profile"),
389 )?;
390 for arguments in [
391 vec![
392 engine.to_owned(),
393 "exec".to_owned(),
394 container_id.clone(),
395 "rm".to_owned(),
396 "-rf".to_owned(),
397 "--".to_owned(),
398 home.clone(),
399 ],
400 vec![
401 engine.to_owned(),
402 "exec".to_owned(),
403 container_id.clone(),
404 "mkdir".to_owned(),
405 "-p".to_owned(),
406 home.clone(),
407 ],
408 vec![
409 engine.to_owned(),
410 "cp".to_owned(),
411 format!("{upload}/."),
412 format!("{container_id}:{home}"),
413 ],
414 std::iter::once(engine.to_owned())
415 .chain(container_upload_ownership_args(
416 engine,
417 container_id,
418 worker_root,
419 &[&home],
420 ))
421 .collect(),
422 vec![
423 engine.to_owned(),
424 "exec".to_owned(),
425 container_id.clone(),
426 "chmod".to_owned(),
427 "-R".to_owned(),
428 "go-rwx".to_owned(),
429 home.clone(),
430 ],
431 ] {
432 execute_checked(
433 executor,
434 crate::targets::ssh_command(ssh, arguments)
435 .purpose("stage the remote reviewer profile"),
436 )?;
437 }
438 execute_checked(
439 executor,
440 crate::targets::ssh_command(ssh, ["rm", "-rf", "--", &upload])
441 .purpose("remove remote reviewer staging"),
442 )?;
443 }
444 }
445 if home.trim().is_empty() {
446 bail!("the reviewer profile home resolved to an empty path");
447 }
448 Ok(())
449}
450
451#[cfg(test)]
452mod tests {
453 use std::cell::RefCell;
454 use std::collections::BTreeMap;
455
456 use super::*;
457 use crate::controller::test_support::checkpoint_test_session;
458 use mj_core::config::{Config, HarnessKind, HarnessProfile, TargetTemplate};
459 use mj_core::state::{SessionState, State};
460
461 use crate::targets::CommandOutput;
462
463 struct RecordingExecutor {
464 commands: RefCell<Vec<CommandSpec>>,
465 }
466
467 impl RecordingExecutor {
468 fn new() -> Self {
469 Self {
470 commands: RefCell::new(Vec::new()),
471 }
472 }
473
474 fn script(&self) -> Vec<String> {
476 self.commands
477 .borrow()
478 .iter()
479 .map(|command| format!("{} {}", command.program, command.args.join(" ")))
480 .collect()
481 }
482 }
483
484 impl CommandExecutor for RecordingExecutor {
485 fn execute(&self, command: &CommandSpec) -> Result<CommandOutput> {
486 self.commands.borrow_mut().push(command.clone());
487 Ok(CommandOutput {
488 status: 0,
489 stdout: Vec::new(),
490 stderr: Vec::new(),
491 })
492 }
493 }
494
495 const SESSION_ID: &str = "0123456789abcdef0123456789abcdef";
498
499 fn fixture(directory: &Path, locator: mj_core::state::TargetLocator) -> (Controller, String) {
500 let session_id = SESSION_ID;
501 let mut session = checkpoint_test_session(session_id);
502 session.target_template_id = "local".into();
503 session.state = SessionState::Running;
504 let template = match &locator {
505 mj_core::state::TargetLocator::LocalPodman { .. } => {
506 serde_json::from_str(r#"{"kind":"local-podman","image":"test"}"#).unwrap()
507 }
508 mj_core::state::TargetLocator::LocalDocker { .. } => {
509 serde_json::from_str(r#"{"kind":"local-docker","image":"test"}"#).unwrap()
510 }
511 mj_core::state::TargetLocator::SshPodman { .. } => serde_json::from_str(
512 r#"{"kind":"ssh-podman","host":"builder.test","image":"test"}"#,
513 )
514 .unwrap(),
515 _ => TargetTemplate::LocalBare,
516 };
517 session.target = Some(locator);
518 let mut config = Config::default();
519 config.targets.insert("local".into(), template);
520 for (id, kind) in [
521 ("codex", HarnessKind::Codex),
522 ("claude", HarnessKind::Claude),
523 ] {
524 let home = directory.join(id);
525 std::fs::create_dir_all(&home).unwrap();
526 config.profiles.insert(
527 id.to_owned(),
528 HarnessProfile {
529 enabled: true,
530 kind,
531 home,
532 environment: BTreeMap::from([("EXTRA".into(), "1".into())]).into(),
533 context_window_bytes: None,
534 legacy_subagents: Default::default(),
535 guardian_review_model: None,
536 },
537 );
538 }
539 (
540 Controller {
541 config,
542 state: State {
543 sessions: [(session_id.into(), session)].into_iter().collect(),
544 ..State::default()
545 },
546 },
547 session_id.to_owned(),
548 )
549 }
550
551 #[test]
552 fn staging_copies_the_chosen_profile_into_the_worker_root() {
553 let directory = tempfile::tempdir().unwrap();
554 let worker_root = directory.path().join(SESSION_ID);
555 std::fs::create_dir_all(directory.path().join("claude")).unwrap();
556 std::fs::write(directory.path().join("claude/CLAUDE.md"), b"reviewer").unwrap();
558 let (controller, session_id) = fixture(
559 directory.path(),
560 mj_core::state::TargetLocator::LocalBare {
561 worker_root: worker_root.clone(),
562 },
563 );
564 let executor = RecordingExecutor::new();
565
566 let config = controller
567 .stage_reviewer_profile_controlled(&session_id, "claude", 0, &[], &executor)
568 .unwrap();
569
570 assert_eq!(config.profile_id, "claude");
571 assert_eq!(config.harness, HarnessKind::Claude);
572 assert_eq!(config.generation, 0);
573 assert_eq!(config.model, None);
574 assert_eq!(config.effort, None);
575 assert!(
577 !config
578 .environment
579 .contains_key(HarnessKind::Claude.home_env())
580 );
581 assert_eq!(
582 config.environment.get("EXTRA").map(String::as_str),
583 Some("1")
584 );
585
586 let home = format!("{}/reviewer/profile", worker_root.display());
587 let script = executor.script();
588 let cleared = script
589 .iter()
590 .position(|line| line.starts_with("rm ") && line.contains(&home))
591 .expect("the previous reviewer profile is cleared");
592 let copied = script
593 .iter()
594 .position(|line| line.starts_with("cp ") && line.ends_with(&home))
595 .expect("the staged profile is installed");
596 assert!(
597 cleared < copied,
598 "a stale profile must go before the new one lands: {script:?}"
599 );
600 assert!(
601 script
602 .iter()
603 .any(|line| line.contains("go-rwx") && line.contains(&home)),
604 "the reviewer profile must not be world readable: {script:?}"
605 );
606 }
607
608 #[test]
610 fn remote_container_targets_stage_the_reviewer_on_the_host_not_in_the_worker_root() {
611 let directory = tempfile::tempdir().unwrap();
615 let container_id = crate::targets::resource_name(SESSION_ID).unwrap();
616 let (controller, session_id) = fixture(
617 directory.path(),
618 mj_core::state::TargetLocator::SshPodman {
619 host: "builder.test".into(),
620 container_id: container_id.clone(),
621 workspace_storage: Default::default(),
622 borrowed_from: None,
623 },
624 );
625 let executor = RecordingExecutor::new();
626
627 controller
628 .stage_reviewer_profile_controlled(&session_id, "codex", 3, &[], &executor)
629 .unwrap();
630
631 let script = executor.script();
632 let upload = script
633 .iter()
634 .find(|line| line.starts_with("scp "))
635 .expect("the profile is uploaded with scp")
636 .clone();
637 let staging = format!(
638 "{}/{session_id}-reviewer-3",
639 crate::targets::REMOTE_UPLOAD_STAGING
640 );
641 assert!(
642 upload.contains(&staging),
643 "the upload lands in host staging: {upload}"
644 );
645 assert!(
646 !upload.contains("/var/lib/hel/workers/"),
647 "the upload does not target the container's worker root: {upload}"
648 );
649 let copy = script
650 .iter()
651 .find(|line| line.contains("cp") && line.contains(&format!("{container_id}:")))
652 .unwrap_or_else(|| {
653 panic!("the engine copies the staged profile into the container: {script:?}")
654 });
655 assert!(
656 copy.contains(&format!("{staging}/.")) && copy.contains(&format!("{container_id}:")),
657 "the engine copies from host staging into the container: {copy}"
658 );
659 let last = script.last().unwrap();
660 assert!(
661 last.contains("'rm'")
662 && last.contains(&format!("'{staging}'"))
663 && !last.contains("podman"),
664 "host staging is removed afterwards: {script:?}"
665 );
666 }
667
668 #[test]
669
670 fn local_container_targets_stage_the_reviewer_through_their_engine() {
671 let directory = tempfile::tempdir().unwrap();
672 let container_id = crate::targets::resource_name(SESSION_ID).unwrap();
673 for (locator, engine) in [
674 (
675 mj_core::state::TargetLocator::LocalPodman {
676 borrowed_from: None,
677 container_id: container_id.clone(),
678 workspace_storage: Default::default(),
679 },
680 "podman",
681 ),
682 (
683 mj_core::state::TargetLocator::LocalDocker {
684 borrowed_from: None,
685 container_id: container_id.clone(),
686 },
687 "docker",
688 ),
689 ] {
690 let (controller, session_id) = fixture(directory.path(), locator);
691 let executor = RecordingExecutor::new();
692
693 controller
694 .stage_reviewer_profile_controlled(&session_id, "codex", 3, &[], &executor)
695 .unwrap();
696
697 let script = executor.script();
698 assert!(
699 script
700 .iter()
701 .all(|line| line.starts_with(&format!("{engine} "))),
702 "a container target is reached only through its engine: {script:?}"
703 );
704 let home = script
705 .iter()
706 .find_map(|line| {
707 line.split(' ')
708 .find(|word| word.contains("/reviewer/profile"))
709 })
710 .expect("the reviewer profile is placed")
711 .to_owned();
712 assert!(
713 home.contains(&format!("/{session_id}")),
714 "the reviewer lives under this session's worker root: {home}"
715 );
716 assert!(
718 !script.iter().any(|line| {
719 line.contains("run") || line.contains("git") || line.contains("create")
720 }),
721 "staging a reviewer provisions nothing: {script:?}"
722 );
723 }
724 }
725
726 #[test]
727 fn a_claude_reviewer_in_a_podman_session_is_told_it_is_sandboxed() {
728 let directory = tempfile::tempdir().unwrap();
729 let container_id = crate::targets::resource_name(SESSION_ID).unwrap();
730 let podman = mj_core::state::TargetLocator::LocalPodman {
731 borrowed_from: None,
732 container_id: container_id.clone(),
733 workspace_storage: Default::default(),
734 };
735 let docker = mj_core::state::TargetLocator::LocalDocker {
736 borrowed_from: None,
737 container_id,
738 };
739 for (locator, profile_id, sandboxed) in [
740 (podman.clone(), "claude", true),
741 (podman, "codex", false),
742 (docker, "claude", false),
743 ] {
744 let (controller, session_id) = fixture(directory.path(), locator);
745 let config = controller
746 .stage_reviewer_profile_controlled(
747 &session_id,
748 profile_id,
749 0,
750 &[],
751 &RecordingExecutor::new(),
752 )
753 .unwrap();
754 assert_eq!(
755 config.environment.get("IS_SANDBOX").map(String::as_str),
756 sandboxed.then_some("1"),
757 "{profile_id}"
758 );
759 }
760 }
761
762 #[test]
763 fn a_muse_reviewer_is_staged_with_the_permission_profile_its_policy_enforces() {
764 let directory = tempfile::tempdir().unwrap();
765 let worker_root = directory.path().join(SESSION_ID);
766 let (mut controller, session_id) = fixture(
767 directory.path(),
768 mj_core::state::TargetLocator::LocalBare {
769 worker_root: worker_root.clone(),
770 },
771 );
772 let home = directory.path().join("muse");
775 std::fs::create_dir_all(&home).unwrap();
776 std::fs::write(
777 home.join("settings.json"),
778 br#"{"schema_version":1,"permissions":{"schema_version":1,"default_profile":":auto-review"}}"#,
779 )
780 .unwrap();
781 controller.config.profiles.insert(
782 "muse".into(),
783 HarnessProfile {
784 enabled: true,
785 kind: HarnessKind::Muse,
786 home,
787 environment: BTreeMap::new().into(),
788 context_window_bytes: None,
789 legacy_subagents: Default::default(),
790 guardian_review_model: None,
791 },
792 );
793 controller
794 .stage_reviewer_profile_controlled(&session_id, "muse", 0, &[], &ProcessExecutor)
795 .unwrap();
796
797 let staged: serde_json::Value = serde_json::from_slice(
798 &std::fs::read(worker_root.join("reviewer/profile/settings.json")).unwrap(),
799 )
800 .unwrap();
801 assert_eq!(staged["permissions"]["default_profile"], ":ask-me");
803 }
804
805 #[test]
806 fn a_new_generation_travels_to_the_worker_so_it_starts_a_fresh_reviewer() {
807 let directory = tempfile::tempdir().unwrap();
808 let (controller, session_id) = fixture(
809 directory.path(),
810 mj_core::state::TargetLocator::LocalBare {
811 worker_root: directory.path().join(SESSION_ID),
812 },
813 );
814 let executor = RecordingExecutor::new();
815
816 let first = controller
817 .stage_reviewer_profile_controlled(&session_id, "codex", 0, &[], &executor)
818 .unwrap();
819 let second = controller
820 .stage_reviewer_profile_controlled(&session_id, "codex", 1, &[], &executor)
821 .unwrap();
822
823 assert!(first.reusable_for(&first));
824 assert!(
825 !first.reusable_for(&second),
826 "a new generation must not reload the old conversation"
827 );
828 }
829}