Add managed worktrees to codex exec (#42652)

## What changed

- Add the experimental `worktrees` feature and a shared `--worktree` flag for new and forked `codex exec` sessions.
- Create each enabled session in a managed Git worktree, use that checkout as the session working directory, and bind the checkout to the new thread.
- Share the configured worktree pool with Desktop while leaving automatic cleanup disabled for CLI allocations.
- Reject unsupported commands, remote execution, ignored user configuration, ephemeral sessions, and use without the feature enabled before allocating a worktree.

## Testing

- Cover flag placement and inheritance, supported and rejected command combinations, worktree allocation and thread ownership, configuration gating, and compatibility with existing worktree-backed sessions.

GitOrigin-RevId: 011ff4639b09e8992c50d7b823df23e71798670e
This commit is contained in:
Benjamin Carlsson authored and copyberry committed 2026-09-04 01:56:38 +00:00
1 parent f46671b14a
commit eb5a00b068
18 files changed
+631 -9

No files matched your search

+97
View File
@@ -1060,6 +1060,7 @@ async fn cli_main(
mut interactive,
subcommand,
} = MultitoolCli::parse();
reject_unsupported_worktree_for_subcommand(interactive.shared.worktree, &subcommand)?;
// Fold --enable/--disable into config overrides so they flow to all subcommands.
let toggle_overrides = feature_toggles.to_overrides()?;
root_config_overrides.raw_overrides.extend(toggle_overrides);
@@ -2395,6 +2396,42 @@ fn reject_remote_mode_for_subcommand(
Ok(())
}
fn reject_unsupported_worktree_for_subcommand(
root_worktree: bool,
subcommand: &Option<Subcommand>,
) -> anyhow::Result<()> {
let subcommand_worktree = match subcommand {
Some(Subcommand::Exec(command)) => command.shared.worktree,
Some(Subcommand::Resume(command)) => command.config_overrides.0.shared.worktree,
Some(Subcommand::Fork(command)) => command.config_overrides.0.shared.worktree,
Some(Subcommand::Archive(command)) | Some(Subcommand::Unarchive(command)) => {
command.config_overrides.shared.worktree
}
Some(Subcommand::Delete(command)) => command.session.config_overrides.shared.worktree,
Some(Subcommand::Queue(command)) => command.config_overrides.shared.worktree,
_ => false,
};
if !root_worktree && !subcommand_worktree {
return Ok(());
}
match subcommand {
Some(Subcommand::Exec(command)) => match &command.command {
None | Some(ExecCommand::Fork(_)) => Ok(()),
Some(ExecCommand::Resume(_)) => anyhow::bail!(
"`--worktree` cannot resume an existing session; use `codex exec fork --worktree`"
),
Some(ExecCommand::Review(_)) => {
anyhow::bail!("`--worktree` is not supported for code review")
}
},
_ => {
anyhow::bail!("`--worktree` currently supports only `codex exec` and `codex exec fork`")
}
}
}
fn reject_root_strict_config_for_subcommand(
strict_config: bool,
subcommand: &Option<Subcommand>,
@@ -3211,6 +3248,66 @@ mod tests {
);
}
#[test]
fn exec_worktree_flag_supports_root_local_and_nested_fork_positions() {
let arguments = [
vec!["codex", "--worktree", "exec", "hello"],
vec!["codex", "exec", "--worktree", "hello"],
vec![
"codex",
"exec",
"fork",
"--worktree",
"019f1234-5678-7000-8000-000000000001",
],
];
for arguments in arguments {
let cli = MultitoolCli::try_parse_from(&arguments).expect("parse worktree command");
assert!(
reject_unsupported_worktree_for_subcommand(
cli.interactive.shared.worktree,
&cli.subcommand,
)
.is_ok(),
"headless worktree command should be accepted: {arguments:?}",
);
}
}
#[test]
fn worktree_flag_rejects_unsupported_session_and_management_commands() {
let arguments = [
vec!["codex", "--worktree"],
vec!["codex", "--worktree", "login"],
vec!["codex", "exec", "resume", "--worktree", "session"],
vec!["codex", "exec", "review", "--worktree"],
vec!["codex", "resume", "--worktree", "session"],
vec!["codex", "archive", "session", "--worktree"],
vec![
"codex",
"queue",
"--thread",
"session",
"--message",
"hi",
"--worktree",
],
];
for arguments in arguments {
let cli = MultitoolCli::try_parse_from(&arguments).expect("parse shared worktree flag");
assert!(
reject_unsupported_worktree_for_subcommand(
cli.interactive.shared.worktree,
&cli.subcommand,
)
.is_err(),
"unsupported command must be rejected: {arguments:?}",
);
}
}
#[test]
fn exec_resume_last_accepts_prompt_positional() {
let cli =