From 52a5dda8691de6271966a7eb6f26ff41131c6b16 Mon Sep 17 00:00:00 2001 From: webtecnica <75556242+webtecnica@users.noreply.github.com> Date: Sun, 19 Jul 2026 09:06:51 -0300 Subject: [PATCH] fix(cli): forward --worktree to run_oneshot so -z -w works (#67458) The --worktree/-w flag was silently ignored in one-shot mode (-z): run_oneshot() didn't accept a worktree parameter, and neither call site in main.py forwarded the parsed flag. Commits landed on the current branch instead of an isolated worktree. - Add to run_oneshot() in hermes_cli/oneshot.py - Wire worktree setup (git_repo_root, prune_stale_worktrees, _setup_worktree) and cleanup in oneshot.py, mirroring the interactive-mode path in cli.py - Set HERMES_CWD and TERMINAL_CWD so the agent runs inside the worktree directory - Clean up the worktree in the finally block (preserving it when it has unpushed commits, same policy as interactive mode) - Pass worktree=getattr(args, 'worktree', False) from both run_oneshot() call sites in main.py --- hermes_cli/main.py | 2 ++ hermes_cli/oneshot.py | 40 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 42 insertions(+) diff --git a/hermes_cli/main.py b/hermes_cli/main.py index 88f5fa375bf1b..d5feb189bf9f7 100644 --- a/hermes_cli/main.py +++ b/hermes_cli/main.py @@ -12982,6 +12982,7 @@ def _try_termux_fast_cli_launch() -> bool: provider=getattr(args, "provider", None), toolsets=getattr(args, "toolsets", None), usage_file=getattr(args, "usage_file", None), + worktree=getattr(args, "worktree", False), ) ) @@ -15139,6 +15140,7 @@ def _export_one(session_id: str): provider=getattr(args, "provider", None), toolsets=getattr(args, "toolsets", None), usage_file=getattr(args, "usage_file", None), + worktree=getattr(args, "worktree", False), ) ) diff --git a/hermes_cli/oneshot.py b/hermes_cli/oneshot.py index 0ed1dcd303a14..6719de07aba21 100644 --- a/hermes_cli/oneshot.py +++ b/hermes_cli/oneshot.py @@ -173,6 +173,7 @@ def run_oneshot( provider: Optional[str] = None, toolsets: object = None, usage_file: Optional[str] = None, + worktree: bool = False, ) -> int: """Execute a single prompt and print only the final content block. @@ -187,6 +188,9 @@ def run_oneshot( cost, token counts, model, api_calls) is written there after the run — even when the run fails — so pipelines can account for spend per invocation. + worktree: When True, create an isolated git worktree so the agent's + commits don't land on the current branch (same as ``-w`` in + interactive mode). Returns the exit code. Caller should sys.exit() with the return. """ @@ -230,6 +234,34 @@ def run_oneshot( # to its inline/synchronous path. See declare_stateless_channel(). declare_stateless_channel() + # Set up isolated git worktree when -w/--worktree is requested. + _wt_info = None + if worktree: + try: + from cli import ( + _cleanup_worktree, + _git_repo_root, + _prune_stale_worktrees, + _setup_worktree, + ) + + repo = _git_repo_root() + if repo: + _prune_stale_worktrees(repo) + _wt_info = _setup_worktree() + if not _wt_info: + sys.stderr.write( + "hermes -z: --worktree failed (are you inside a git repo?).\n" + ) + return 1 + os.environ["HERMES_CWD"] = _wt_info["path"] + os.environ["TERMINAL_CWD"] = _wt_info["path"] + except Exception as exc: + sys.stderr.write( + f"hermes -z: --worktree setup failed: {exc}\n" + ) + return 1 + # Redirect stderr AND stdout to devnull for the entire call tree. # We'll print the final response to the real stdout at the end. real_stdout = sys.stdout @@ -263,6 +295,14 @@ def run_oneshot( devnull.close() except Exception: pass + # Clean up worktree on exit (unless it has unpushed commits — same + # policy as interactive mode). + if _wt_info is not None: + try: + from cli import _cleanup_worktree + _cleanup_worktree(_wt_info) + except Exception: + pass if failure is not None: # Re-raise control-flow exceptions so the parent handles them as usual