From eeb7671e69dbfbcd17db019d41ff16cd0a7b791d Mon Sep 17 00:00:00 2001 From: Teknium <127238744+teknium1@users.noreply.github.com> Date: Thu, 3 Sep 2026 13:05:57 -0700 Subject: [PATCH] =?UTF-8?q?simplify(compat):=20hermes=5Fcli=20small=20faca?= =?UTF-8?q?des=20=E2=80=94=20drop=207=20re-exports/aliases=20(+relay=5Frun?= =?UTF-8?q?time=20alias=20module),=20repoint=2012=20callers/tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- cli.py | 53 +------------------ docs/observability/relay-shared-metrics.md | 2 +- hermes_cli/_subprocess_compat.py | 5 +- hermes_cli/backup.py | 1 - hermes_cli/claw.py | 3 +- hermes_cli/cli_agent_setup_mixin.py | 3 +- hermes_cli/cli_commands_mixin.py | 13 ++--- hermes_cli/cli_info_mixin.py | 10 ++-- hermes_cli/cli_loops_mixin.py | 7 ++- hermes_cli/cli_session_mixin.py | 6 +-- hermes_cli/cron.py | 5 -- hermes_cli/doctor_platform.py | 10 ++-- hermes_cli/kanban_db_workspace.py | 2 +- hermes_cli/plugins_cmd.py | 1 - hermes_cli/sessions_cmd.py | 9 ++-- hermes_cli/worktree_gc.py | 28 +++++----- tests/agent/test_treekill_consolidation.py | 10 +--- tests/cli/test_cli_context_warning.py | 14 ++--- tests/cli/test_cli_preloaded_skills.py | 2 +- tests/cli/test_cli_provider_resolution.py | 2 +- tests/cli/test_cli_status_command.py | 4 +- .../test_cli_terminal_response_sanitizer.py | 7 ++- tests/cli/test_worktree.py | 46 ++++++++-------- tests/cli/test_worktree_pushed_tier.py | 22 ++++---- tests/cli/test_worktree_selfheal.py | 6 +-- tests/cli/test_worktree_sync_base.py | 16 +++--- tests/hermes_cli/test_backup.py | 6 +-- tests/hermes_cli/test_gateway_restart_loop.py | 6 +-- tests/hermes_cli/test_git_probe_tree_kill.py | 10 ++-- .../test_kanban_worktree_teardown.py | 4 +- tests/hermes_cli/test_plugins_cmd_list.py | 5 +- .../test_relay_shared_metrics_runtime.py | 3 +- tests/test_model_tools.py | 4 +- tests/test_tui_gateway_server.py | 12 ++--- tests/tools/test_windows_native_support.py | 2 +- tests/tools/test_zombie_process_cleanup.py | 4 +- tests/tui_gateway/test_protocol.py | 2 +- .../test_session_git_metadata_generation.py | 8 +-- tui_gateway/agent_callbacks.py | 2 +- tui_gateway/compute_host_bridge.py | 6 +-- tui_gateway/methods_config.py | 2 +- tui_gateway/methods_config_set.py | 2 +- tui_gateway/methods_projects.py | 6 +-- tui_gateway/methods_session.py | 6 +-- tui_gateway/server.py | 8 +-- tui_gateway/session_workdir.py | 17 ++---- 46 files changed, 171 insertions(+), 231 deletions(-) diff --git a/cli.py b/cli.py index a19f88212a..8069ce7998 100644 --- a/cli.py +++ b/cli.py @@ -14,7 +14,6 @@ import shutil import sys import json import re -import concurrent.futures # noqa: F401 (tests monkeypatch cli.concurrent.futures) import atexit import errno import time @@ -86,10 +85,6 @@ def _lazy_shim(module: str, name: str, alias: str | None = None): return shim -CanonicalUsage = _lazy_shim("agent.usage_pricing", "CanonicalUsage") -estimate_usage_cost = _lazy_shim("agent.usage_pricing", "estimate_usage_cost") - - def format_duration_compact(*args, **kwargs): seconds = float(args[0] if args else kwargs.get("seconds", 0.0)) if seconds < 60: @@ -162,8 +157,6 @@ def format_token_count_compact(*args, **kwargs): return f"{value:,}" -is_table_divider = _lazy_shim("agent.markdown_tables", "is_table_divider") -looks_like_table_row = _lazy_shim("agent.markdown_tables", "looks_like_table_row") realign_markdown_tables = _lazy_shim("agent.markdown_tables", "realign_markdown_tables") from hermes_cli.banner import format_banner_version_label @@ -171,7 +164,7 @@ _COMMAND_SPINNER_FRAMES = ("⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧ # ~/.hermes/.env first, project .env as dev fallback; user env files override stale shell exports. -from hermes_constants import get_hermes_home, display_hermes_home # noqa: F401 (mixins import via cli) +from hermes_constants import get_hermes_home from hermes_cli.env_loader import load_hermes_dotenv from utils import base_url_host_matches, base_url_hostname, fast_safe_load @@ -575,9 +568,6 @@ from rich.markup import escape as _escape from rich.text import Text as _RichText # Agent/tool systems load lazily: bare startup only needs the prompt. -AIAgent = _lazy_shim("run_agent", "AIAgent") - - def get_tool_definitions(*args, **kwargs): from hermes_cli.mcp_startup import wait_for_mcp_discovery from model_tools import get_tool_definitions as _get_tool_definitions @@ -586,12 +576,6 @@ def get_tool_definitions(*args, **kwargs): return _get_tool_definitions(*args, **kwargs) -get_toolset_for_tool = _lazy_shim("model_tools", "get_toolset_for_tool") - -from hermes_cli.banner import build_welcome_banner # noqa: F401 (CLIInfoMixin imports via cli) - -get_all_toolsets = _lazy_shim("toolsets", "get_all_toolsets") -get_toolset_info = _lazy_shim("toolsets", "get_toolset_info") validate_toolset = _lazy_shim("toolsets", "validate_toolset") @@ -602,7 +586,6 @@ def _sync_process_session_id(session_id: str) -> None: set_current_session_id(session_id) -get_job = _lazy_shim("cron", "get_job") _cleanup_all_terminals = _lazy_shim("tools.terminal_tool", "cleanup_all_environments", "_cleanup_all_terminals") set_sudo_password_callback = _lazy_shim("tools.terminal_tool", "set_sudo_password_callback") set_approval_callback = _lazy_shim("tools.terminal_tool", "set_approval_callback") @@ -1036,39 +1019,14 @@ def _reset_terminal_input_modes_on_exit() -> None: tty.flush() -from hermes_cli.worktree_ops import ( # noqa: F401 (mixins/tests/worktree_gc import via cli) - _PACK_SPRAWL_THRESHOLD, - _WORKTREE_MERGE_CACHE_MAX, - _classify_prune_candidates, - _cleanup_failed_worktree_add, - _copy_worktree_includes, - _deepen_shallow_repo, - _ensure_worktrees_gitignored, - _fetch_remote_branch_heads, - _git, +from hermes_cli.worktree_ops import ( _git_quiet, _git_repo_root, - _load_worktree_merge_cache, _maintain_pack_health, - _normalize_git_bash_path, - _path_is_within_root, - _prune_candidates, - _prune_orphaned_branches, _prune_stale_worktrees, - _reap_prune_verdicts, _repo_is_shallow, - _resolve_worktree_base, - _save_worktree_merge_cache, _setup_worktree, - _worktree_add, - _worktree_branch_pr_merged, - _worktree_branch_pushed_exact, - _worktree_commits_all_merged_upstream, - _worktree_current_branch, _worktree_has_unpushed_commits, - _worktree_is_dirty, - _worktree_lock_is_live, - _worktree_merge_cache_path, ) # ============================================================================= Git Worktree Isolation @@ -2239,12 +2197,6 @@ def _strip_leaked_terminal_responses_with_meta(text: str) -> tuple[str, bool]: return text, had_mouse_reports -def _strip_leaked_terminal_responses(text: str) -> str: - """Compatibility wrapper returning only cleaned text.""" - cleaned, _ = _strip_leaked_terminal_responses_with_meta(text) - return cleaned - - def _estimate_tui_input_height( lines: list[str] | tuple[str, ...], prompt_text: str, terminal_columns: int, *, max_height: int = 8, ) -> int: @@ -2325,7 +2277,6 @@ class ChatConsole: """No-op ``console.status`` so slash helpers don't duplicate ``_busy_command()``'s indicator.""" yield self -from hermes_cli.banner import HERMES_AGENT_LOGO, HERMES_CADUCEUS # noqa: E402,F401 (re-exported) def _build_compact_banner() -> str: diff --git a/docs/observability/relay-shared-metrics.md b/docs/observability/relay-shared-metrics.md index 14f98bbb90..23d016659c 100644 --- a/docs/observability/relay-shared-metrics.md +++ b/docs/observability/relay-shared-metrics.md @@ -126,7 +126,7 @@ shared-metrics consent, local SQLite state, or ATIF trajectory grouping. Hermes core owns one Relay host and one isolated Relay session scope per Hermes session. Core lifecycle producers use -`hermes_cli.observability.relay_runtime` to obtain the shared session handle or +`agent.relay_runtime` to obtain the shared session handle or run Relay scope, LLM, tool, and mark APIs in that session context. New product marks do not require Hermes plugin registration. Shared-metrics marks must still contain only fields approved by the versioned allowlist; the hard diff --git a/hermes_cli/_subprocess_compat.py b/hermes_cli/_subprocess_compat.py index e3830efd25..a994de6239 100644 --- a/hermes_cli/_subprocess_compat.py +++ b/hermes_cli/_subprocess_compat.py @@ -452,7 +452,7 @@ def bounded_git_probe(argv: Sequence[str], *, timeout: float) -> str: ``text`` with UTF-8 ``errors="replace"`` decoding, and the hidden-window ``creationflags`` on Windows only. On POSIX the probe is additionally placed in its own process group (``process_group=0``, Python ≥3.11) so timeout cleanup can take down descendants — credential helpers, ``git-remote-https``, hook - children — with the launcher instead of orphaning them (see :func:`_kill_git_process_tree`; port of + children — with the launcher instead of orphaning them (see :func:`kill_process_tree`; port of openai/codex#36793). ``process_group`` only changes which group the child belongs to; it does not detach the terminal or alter the fast path. """ @@ -461,6 +461,3 @@ def bounded_git_probe(argv: Sequence[str], *, timeout: float) -> str: return "" return (result.stdout or "").strip() - -# Backward-compat alias — existing call sites/tests import the historical name. -_kill_git_process_tree = kill_process_tree diff --git a/hermes_cli/backup.py b/hermes_cli/backup.py index 7e564f2d81..55f7ccc366 100644 --- a/hermes_cli/backup.py +++ b/hermes_cli/backup.py @@ -23,7 +23,6 @@ from utils import ( _preserve_file_mode, _preserve_file_owner, _restore_file_mode, _restore_file_owner, atomic_replace, ) -# Private alias kept: claw.py and the backup tests import ``_format_size`` from here. from hermes_cli.sizefmt import format_bytes as _format_size logger = logging.getLogger(__name__) diff --git a/hermes_cli/claw.py b/hermes_cli/claw.py index 773299754f..fd229a75f1 100644 --- a/hermes_cli/claw.py +++ b/hermes_cli/claw.py @@ -340,7 +340,8 @@ def _apply_migration(run_migrator: Callable[[bool], dict], opts: SimpleNamespace backup_archive: Optional[Path] = None if not opts.no_backup: try: - from hermes_cli.backup import create_pre_migration_backup, _format_size + from hermes_cli.backup import create_pre_migration_backup + from hermes_cli.sizefmt import format_bytes as _format_size backup_archive = create_pre_migration_backup(hermes_home=opts.hermes_home) if backup_archive: print() diff --git a/hermes_cli/cli_agent_setup_mixin.py b/hermes_cli/cli_agent_setup_mixin.py index 1807c8378a..100c18a378 100644 --- a/hermes_cli/cli_agent_setup_mixin.py +++ b/hermes_cli/cli_agent_setup_mixin.py @@ -474,7 +474,8 @@ class CLIAgentSetupMixin: def _init_agent(self, *, model_override: str = None, runtime_override: dict = None, request_overrides: dict | None = None) -> bool: """Build the agent on first use; when resuming, restore history from SQLite. Returns True on success.""" - from cli import AIAgent, ChatConsole, _cprint, _prepare_deferred_agent_startup, logger + from cli import ChatConsole, _cprint, _prepare_deferred_agent_startup, logger + from run_agent import AIAgent if self.agent is not None: return True diff --git a/hermes_cli/cli_commands_mixin.py b/hermes_cli/cli_commands_mixin.py index e86a0aecb7..4b982113d8 100644 --- a/hermes_cli/cli_commands_mixin.py +++ b/hermes_cli/cli_commands_mixin.py @@ -398,9 +398,9 @@ def _refresh_tui_before_print(cli) -> None: def _print_lightpanda_engine_status() -> None: """``/browser status`` line(s) about ``browser.engine: lightpanda`` — silent unless set; says whether it is in use or which higher-precedence setting shadows it.""" - if not _probe("tools.browser_tool", "_using_lightpanda_engine", False): + if not _probe("tools.browser_tool_lightpanda_fallback", "_using_lightpanda_engine", False): return - used, reason = _probe("tools.browser_tool", "lightpanda_engine_status", (None, None)) + used, reason = _probe("tools.browser_tool_lightpanda_fallback", "lightpanda_engine_status", (None, None)) if reason is None: return if not used: @@ -521,7 +521,7 @@ def _browser_connect(cli, cdp_url: str) -> None: "debugging and retry /browser connect") os.environ["BROWSER_CDP_URL"] = found # Eagerly start the CDP supervisor so pending_dialogs + frame_tree show up in the next snapshot. - _probe("tools.browser_tool", "_ensure_cdp_supervisor", None, "default") + _probe("tools.browser_tool_cdp", "_ensure_cdp_supervisor", None, "default") _say_block("🌐 Browser connected to live Chromium-family browser via CDP", f" Endpoint: {found}") # Tell the model the CDP browser was made available on purpose. if hasattr(cli, '_pending_input'): @@ -543,7 +543,8 @@ def _browser_disconnect(cli) -> None: "(already using default mode)") os.environ.pop("BROWSER_CDP_URL", None) with suppress(Exception): - from tools.browser_tool import cleanup_all_browsers, _stop_cdp_supervisor + from tools.browser_tool import cleanup_all_browsers + from tools.browser_tool_cdp import _stop_cdp_supervisor _stop_cdp_supervisor("default") cleanup_all_browsers() _say_block("🌐 Browser disconnected from live Chromium-family browser", @@ -590,7 +591,7 @@ def _browser_status() -> None: print(f"🌐 Browser: {provider.provider_name()} (cloud)") _print_lightpanda_engine_status() else: - engine = _probe("tools.browser_tool", "_get_browser_engine", "auto") + engine = _probe("tools.browser_tool_cloud", "_get_browser_engine", "auto") _pr(*_LOCAL_ENGINE_LINES.get(engine, _LOCAL_ENGINE_LINES["auto"])) if engine == "lightpanda": _print_lightpanda_engine_status() @@ -1708,7 +1709,7 @@ class CLICommandsMixin: print(f" Next run: {result['next_run_at']}") def _cron_edit(self, subcommand: str, opts: dict) -> None: - from cli import get_job + from cron import get_job positionals = opts["positionals"] if not positionals: return print("(._.) Usage: /cron edit " diff --git a/hermes_cli/cli_info_mixin.py b/hermes_cli/cli_info_mixin.py index 834aa5535a..4c1ba2e3b7 100644 --- a/hermes_cli/cli_info_mixin.py +++ b/hermes_cli/cli_info_mixin.py @@ -76,7 +76,8 @@ class CLIInfoMixin: def show_banner(self): """Display the welcome banner in Claude Code style.""" - from cli import _build_compact_banner, build_welcome_banner, get_tool_definitions, logger + from cli import _build_compact_banner, get_tool_definitions, logger + from hermes_cli.banner import build_welcome_banner self.console.clear() ctx_len = None if hasattr(self, 'agent') and self.agent and hasattr(self.agent, 'context_compressor'): @@ -333,7 +334,8 @@ class CLIInfoMixin: def show_tools(self): """Display available tools with kawaii ASCII art.""" - from cli import get_tool_definitions, get_toolset_for_tool + from cli import get_tool_definitions + from model_tools import get_toolset_for_tool # Pre-assembly list: /tools is a discovery surface, so it must show the full catalog # including tools deferred behind the tool_search bridge (users verify MCP installs here). tools = get_tool_definitions(enabled_toolsets=self.enabled_toolsets, quiet_mode=True, @@ -367,7 +369,7 @@ class CLIInfoMixin: def show_toolsets(self): """Display available toolsets with kawaii ASCII art.""" - from cli import get_all_toolsets, get_toolset_info + from toolsets import get_all_toolsets, get_toolset_info all_toolsets = get_all_toolsets() print() @@ -475,7 +477,7 @@ class CLIInfoMixin: def _show_gateway_status(self): """Show status of the gateway and connected messaging platforms.""" - from cli import display_hermes_home + from hermes_constants import display_hermes_home from gateway.config import load_gateway_config, Platform print() diff --git a/hermes_cli/cli_loops_mixin.py b/hermes_cli/cli_loops_mixin.py index 259ac5b45d..14060639c0 100644 --- a/hermes_cli/cli_loops_mixin.py +++ b/hermes_cli/cli_loops_mixin.py @@ -54,9 +54,8 @@ class CLILoopsMixin: _cprint(f" {_DIM}✓ UI redrawn{_RST}") def _cmd_clear(self, cmd_original: str): - from cli import ( - ChatConsole, _build_compact_banner, _clear_output_history, _cprint, - build_welcome_banner, get_tool_definitions) + from cli import ChatConsole, _build_compact_banner, _clear_output_history, _cprint, get_tool_definitions + from hermes_cli.banner import build_welcome_banner if self._confirm_destructive_slash( "clear", "This clears the screen and starts a new session.\n" @@ -221,7 +220,7 @@ class CLILoopsMixin: self._reload_skills() def _cmd_plugins(self, cmd_original: str): - from cli import display_hermes_home + from hermes_constants import display_hermes_home try: # Discover from disk (bundled + user) like `hermes plugins list`, so # installed-but-not-enabled plugins show up; the plugin manager only knows diff --git a/hermes_cli/cli_session_mixin.py b/hermes_cli/cli_session_mixin.py index 91f8b7cda2..45c0d738b8 100644 --- a/hermes_cli/cli_session_mixin.py +++ b/hermes_cli/cli_session_mixin.py @@ -24,7 +24,7 @@ def _user_turn_indices(history: list) -> list[int]: """Indices of *real* user turns: excludes ephemeral scaffolding, display_kind timeline rows and compaction handoffs — the same predicate as resume turn counting.""" from agent.context_compressor import user_originated_turn_view - from run_agent import _is_ephemeral_scaffolding + from agent.session_persistence import _is_ephemeral_scaffolding return [ i for i, m in enumerate(history) @@ -244,7 +244,7 @@ class CLISessionMixin: def _show_session_status(self): """Show gateway-style status for the current CLI session.""" - from cli import display_hermes_home + from hermes_constants import display_hermes_home session_meta = {} if self._session_db: with contextlib.suppress(Exception): @@ -715,7 +715,7 @@ class CLISessionMixin: user_originated_turn_view) from agent.memory_manager import sanitize_context from agent.tool_dispatch_helpers import _is_multimodal_tool_result, _multimodal_text_summary - from run_agent import _is_ephemeral_scaffolding + from agent.session_persistence import _is_ephemeral_scaffolding def _persistence_content(content: Any) -> Any: """Project warm content exactly as the session DB flush does.""" diff --git a/hermes_cli/cron.py b/hermes_cli/cron.py index f8b0288de6..c56df9b69d 100644 --- a/hermes_cli/cron.py +++ b/hermes_cli/cron.py @@ -12,11 +12,6 @@ sys.path.insert(0, str(PROJECT_ROOT)) from hermes_cli.colors import Colors, color -# Re-exported: ``tools/terminal_tool.py`` imports it from here to hard-block the same -# gateway-lifecycle commands at execution time when ``_HERMES_GATEWAY=1``. -from cron.lifecycle_guard import ( # noqa: F401 - contains_gateway_lifecycle_command as _contains_gateway_lifecycle_command) - def _normalize_skills(single_skill=None, skills: Optional[Iterable[str]] = None) -> Optional[List[str]]: """Deduped, stripped skill names; None when neither argument was given.""" diff --git a/hermes_cli/doctor_platform.py b/hermes_cli/doctor_platform.py index 59892cdc6c..b175c56d76 100644 --- a/hermes_cli/doctor_platform.py +++ b/hermes_cli/doctor_platform.py @@ -27,7 +27,8 @@ def _system_package_install_cmd(pkg: str) -> str: def _sqlite_upgrade_hint(install_method: str | None = None) -> str: """Return an actionable SQLite upgrade hint for this install layout.""" - from hermes_cli.doctor import PROJECT_ROOT, detect_install_method + from hermes_cli.doctor import PROJECT_ROOT + from hermes_cli.config import detect_install_method method = install_method or detect_install_method(PROJECT_ROOT) cmd = recommended_update_command_for_method(method) action = cmd if is_nix_install_method(method) else { # nix: prose guidance, not a shell command @@ -81,7 +82,7 @@ def _read_journal_mode(db_path: Path) -> tuple[str | None, str | None]: def _format_db_size(db_path: Path) -> str: - from hermes_cli.backup import _format_size # backup.py owns size formatting + from hermes_cli.sizefmt import format_bytes as _format_size try: return _format_size(db_path.stat().st_size) except OSError: @@ -91,7 +92,7 @@ def _format_db_size(db_path: Path) -> str: def _report_database_journal_modes(hermes_home: Path | None = None, version_info: tuple[int, ...] | None = None) -> None: """List each database's journal mode; warn on WAL under a vulnerable SQLite.""" from hermes_cli.doctor import HERMES_HOME - from hermes_state import _wal_reset_repair_hint, is_sqlite_wal_reset_vulnerable + from hermes_state_wal import _wal_reset_repair_hint, is_sqlite_wal_reset_vulnerable vulnerable = is_sqlite_wal_reset_vulnerable(version_info) try: databases = _hermes_database_paths(hermes_home if hermes_home is not None else HERMES_HOME) @@ -261,7 +262,6 @@ def check_macos_tcc_grants() -> None: See #86385. """ - from hermes_cli.doctor import _desktop_app_bundle, _macos_desktop_dr app = _desktop_app_bundle() if sys.platform == "darwin" else None if app is None: return @@ -379,7 +379,7 @@ def _check_python_environment(should_fix: bool, f: Finding) -> None: # python-build-standalone can keep a vulnerable SQLite across upgrades). with warn_on_error("SQLite version probe failed: {e}", ""): import sqlite3 - from hermes_state import is_sqlite_wal_reset_vulnerable, sqlite_source_id + from hermes_state_wal import is_sqlite_wal_reset_vulnerable, sqlite_source_id src = sqlite_source_id() # Warn-only: Hermes already refuses WAL on fresh DBs and runtime repair is best-effort. check_bool(not is_sqlite_wal_reset_vulnerable(), f"SQLite {sqlite3.sqlite_version}", diff --git a/hermes_cli/kanban_db_workspace.py b/hermes_cli/kanban_db_workspace.py index 91c96f904f..426b8181c8 100644 --- a/hermes_cli/kanban_db_workspace.py +++ b/hermes_cli/kanban_db_workspace.py @@ -180,7 +180,7 @@ def _cleanup_worktree_workspace( it. The auto-generated ``wt/`` branch is deleted with it; custom branches are kept. Best-effort.""" try: - from cli import _worktree_has_unpushed_commits, _worktree_is_dirty + from hermes_cli.worktree_ops import _worktree_has_unpushed_commits, _worktree_is_dirty except Exception: return # CLI safety predicates unavailable — preserve try: diff --git a/hermes_cli/plugins_cmd.py b/hermes_cli/plugins_cmd.py index 16a78d336b..0f40a28095 100644 --- a/hermes_cli/plugins_cmd.py +++ b/hermes_cli/plugins_cmd.py @@ -3,7 +3,6 @@ from __future__ import annotations import functools -import importlib.metadata # noqa: F401 — tests patch ``plugins_cmd.importlib.metadata.entry_points`` import json import logging import os diff --git a/hermes_cli/sessions_cmd.py b/hermes_cli/sessions_cmd.py index 94b2647a72..748c0a73e5 100644 --- a/hermes_cli/sessions_cmd.py +++ b/hermes_cli/sessions_cmd.py @@ -13,9 +13,7 @@ import sys from functools import partial from pathlib import Path -from hermes_cli.sessions_cmd_browse import ( # noqa: F401 (re-exports) - _annotate_session_statuses, _relative_time, _session_browse_picker, _session_status_tag, -) +from hermes_cli.sessions_cmd_browse import _relative_time, _session_browse_picker def get_hermes_home(): @@ -88,7 +86,8 @@ def _write_output(output, text, summary) -> None: # -- handlers that must run BEFORE SessionDB() is opened ---------------------- def _cmd_repair(args): - from hermes_state import DEFAULT_DB_PATH as db_path, SessionDB, _db_opens_cleanly, repair_state_db_schema + from hermes_state import DEFAULT_DB_PATH as db_path, SessionDB + from hermes_state_repair import _db_opens_cleanly, repair_state_db_schema if not db_path.exists(): print(f"No session database at {db_path} (nothing to repair).") return @@ -250,7 +249,7 @@ def _default_exclude(args): def _cmd_list(db, args): - from hermes_state import workspace_key as _ws_key + from hermes_state_sessions import workspace_key as _ws_key sessions = db.list_sessions_rich(source=args.source, exclude_sources=_default_exclude(args), limit=args.limit) # Workspace filter: workspace key (git repo root, else cwd) — path substring or exact basename. diff --git a/hermes_cli/worktree_gc.py b/hermes_cli/worktree_gc.py index 1a40139272..5969c7e4c4 100644 --- a/hermes_cli/worktree_gc.py +++ b/hermes_cli/worktree_gc.py @@ -108,23 +108,23 @@ def _archive_untracked(tree: Path, untracked: List[str]) -> Optional[Path]: return None -def _classify_tree(_cli, repo_root: str, entry: Path, merge_cache, remote_heads) -> tuple[str, str, List[str]]: +def _classify_tree(_ops, repo_root: str, entry: Path, merge_cache, remote_heads) -> tuple[str, str, List[str]]: """Return (verdict, reason, untracked) for one tree under ``.worktrees/``.""" path = str(entry) if _KANBAN_RE.match(entry.name): return "keep", "kanban task tree (owned by kanban gc)", [] - if _cli._worktree_lock_is_live(repo_root, path, timeout=5) == "live": + if _ops._worktree_lock_is_live(repo_root, path, timeout=5) == "live": return "keep", "in use by a running hermes session", [] tracked_dirty, untracked = _dirty_split(path) if tracked_dirty: return "keep", "uncommitted tracked changes (real work)", [] archive_note = f"{len(untracked)} untracked file(s) will be archived" - if _cli._worktree_has_unpushed_commits(path, timeout=5) and not _cli._worktree_commits_all_merged_upstream( + if _ops._worktree_has_unpushed_commits(path, timeout=5) and not _ops._worktree_commits_all_merged_upstream( path, timeout=30, cache=merge_cache, max_ahead=_MAX_CHERRY_AHEAD): # Pushed-branch tier: single-branch fetch refspecs (managed-install default) leave pushed # PR branches with no refs/remotes/* entry, so `git log HEAD --not --remotes` reads them # as unpushed forever. A head EXACTLY matching the remote branch has nothing origin lacks. - if not _cli._worktree_branch_pushed_exact(path, remote_heads, timeout=10): + if not _ops._worktree_branch_pushed_exact(path, remote_heads, timeout=10): return "keep", "unpushed commits not found upstream", [] if untracked: return "reap-keep-branch", f"pushed to origin (open-PR lane); branch kept; {archive_note}", untracked @@ -136,18 +136,18 @@ def _classify_tree(_cli, repo_root: str, entry: Path, merge_cache, remote_heads) def audit_worktrees(repo_root: str, *, with_sizes: bool = True) -> List[TreeRecord]: """Classify every tree under ``.worktrees/`` without mutating anything.""" - import cli as _cli # lazy: cli.py is heavy + from hermes_cli import worktree_ops as _ops worktrees_dir = Path(repo_root) / ".worktrees" if not worktrees_dir.exists(): return [] - if _cli._repo_is_shallow(repo_root): - _cli._deepen_shallow_repo(repo_root) + if _ops._repo_is_shallow(repo_root): + _ops._deepen_shallow_repo(repo_root) - merge_cache = _cli._load_worktree_merge_cache() + merge_cache = _ops._load_worktree_merge_cache() cache_size_before = len(merge_cache) # One ls-remote for the whole sweep; None (offline) degrades pushed-tier verdicts to keep. - remote_heads = _cli._fetch_remote_branch_heads(repo_root) + remote_heads = _ops._fetch_remote_branch_heads(repo_root) now = time.time() records: List[TreeRecord] = [] @@ -162,14 +162,14 @@ def audit_worktrees(repo_root: str, *, with_sizes: bool = True) -> List[TreeReco branch = _git(["branch", "--show-current"], cwd=str(entry), timeout=5).stdout.strip() except Exception: branch = "" - verdict, reason, untracked = _classify_tree(_cli, repo_root, entry, merge_cache, remote_heads) + verdict, reason, untracked = _classify_tree(_ops, repo_root, entry, merge_cache, remote_heads) records.append(TreeRecord( name=entry.name, path=str(entry), branch=branch, age_days=age_days, size_mb=_tree_size_mb(entry) if with_sizes else None, verdict=verdict, reason=reason, untracked=untracked)) if len(merge_cache) != cache_size_before: - _cli._save_worktree_merge_cache(merge_cache) + _ops._save_worktree_merge_cache(merge_cache) return records @@ -225,9 +225,9 @@ def reclaim_worktrees( def audit_branches(repo_root: str) -> List[BranchRecord]: """Classify EVERY local branch: deletable when fully merged OR every commit is patch-equivalent upstream (``git cherry``) and not checked out. The gate is content reachability, not name.""" - import cli as _cli - if _cli._repo_is_shallow(repo_root): - _cli._deepen_shallow_repo(repo_root) + from hermes_cli import worktree_ops as _ops + if _ops._repo_is_shallow(repo_root): + _ops._deepen_shallow_repo(repo_root) def _lines(result) -> List[str]: return [b.strip() for b in result.stdout.splitlines() if b.strip()] diff --git a/tests/agent/test_treekill_consolidation.py b/tests/agent/test_treekill_consolidation.py index 902629986c..cfffa0aaa3 100644 --- a/tests/agent/test_treekill_consolidation.py +++ b/tests/agent/test_treekill_consolidation.py @@ -74,14 +74,6 @@ class TestSubprocessCompatDelegation: assert shell_hooks.kill_process_tree is _subprocess_compat.kill_process_tree - def test_backcompat_alias_preserved(self): - from hermes_cli import _subprocess_compat - - assert ( - _subprocess_compat._kill_git_process_tree - is _subprocess_compat.kill_process_tree - ) - # --------------------------------------------------------------------------- # (2) tools.browser_tool._kill_process_tree @@ -108,7 +100,7 @@ class TestBrowserToolDelegation: monkeypatch.setattr(deadline_mod, "kill_process_tree", _boom) legacy_calls = [] monkeypatch.setattr( - browser_tool, "_legacy_kill_process_tree", lambda proc: legacy_calls.append(proc) + "tools.browser_tool_lifecycle._legacy_kill_process_tree", lambda proc: legacy_calls.append(proc) ) proc = _FakeProc(pid=4444) browser_tool._kill_process_tree(proc) # must not raise diff --git a/tests/cli/test_cli_context_warning.py b/tests/cli/test_cli_context_warning.py index 2fe11647ea..82a86e1296 100644 --- a/tests/cli/test_cli_context_warning.py +++ b/tests/cli/test_cli_context_warning.py @@ -24,7 +24,7 @@ def cli_obj(_isolate): "display": {"tool_progress": "new"}, "terminal": {}, }), patch("cli.get_tool_definitions", return_value=[]), \ - patch("cli.build_welcome_banner"): + patch("hermes_cli.banner.build_welcome_banner"): from cli import HermesCLI obj = HermesCLI.__new__(HermesCLI) obj.model = "test-model" @@ -50,7 +50,7 @@ class TestLowContextWarning: """Warning shown when context is below Hermes' minimum.""" cli_obj.agent.context_compressor.context_length = 32768 with patch("cli.get_tool_definitions", return_value=[]), \ - patch("cli.build_welcome_banner"): + patch("hermes_cli.banner.build_welcome_banner"): cli_obj.show_banner() calls = [str(c) for c in cli_obj.console.print.call_args_list] @@ -64,7 +64,7 @@ class TestLowContextWarning: """Warning shown for 2048 tokens (common LM Studio default).""" cli_obj.agent.context_compressor.context_length = 2048 with patch("cli.get_tool_definitions", return_value=[]), \ - patch("cli.build_welcome_banner"): + patch("hermes_cli.banner.build_welcome_banner"): cli_obj.show_banner() calls = [str(c) for c in cli_obj.console.print.call_args_list] @@ -75,7 +75,7 @@ class TestLowContextWarning: """No warning at exactly Hermes' minimum context length.""" cli_obj.agent.context_compressor.context_length = MINIMUM_CONTEXT_LENGTH with patch("cli.get_tool_definitions", return_value=[]), \ - patch("cli.build_welcome_banner"): + patch("hermes_cli.banner.build_welcome_banner"): cli_obj.show_banner() calls = [str(c) for c in cli_obj.console.print.call_args_list] @@ -86,7 +86,7 @@ class TestLowContextWarning: """No warning above Hermes' minimum context length.""" cli_obj.agent.context_compressor.context_length = MINIMUM_CONTEXT_LENGTH + 1 with patch("cli.get_tool_definitions", return_value=[]), \ - patch("cli.build_welcome_banner"): + patch("hermes_cli.banner.build_welcome_banner"): cli_obj.show_banner() calls = [str(c) for c in cli_obj.console.print.call_args_list] @@ -98,7 +98,7 @@ class TestLowContextWarning: cli_obj.agent.context_compressor.context_length = 4096 cli_obj.base_url = "http://localhost:11434/v1" with patch("cli.get_tool_definitions", return_value=[]), \ - patch("cli.build_welcome_banner"): + patch("hermes_cli.banner.build_welcome_banner"): cli_obj.show_banner() calls = [str(c) for c in cli_obj.console.print.call_args_list] @@ -112,7 +112,7 @@ class TestLowContextWarning: cli_obj.agent.context_compressor.context_length = 4096 cli_obj.base_url = "http://localhost:8080/v1" with patch("cli.get_tool_definitions", return_value=[]), \ - patch("cli.build_welcome_banner"): + patch("hermes_cli.banner.build_welcome_banner"): cli_obj.show_banner() calls = [str(c) for c in cli_obj.console.print.call_args_list] diff --git a/tests/cli/test_cli_preloaded_skills.py b/tests/cli/test_cli_preloaded_skills.py index 83f65be1f9..732fee4e83 100644 --- a/tests/cli/test_cli_preloaded_skills.py +++ b/tests/cli/test_cli_preloaded_skills.py @@ -140,7 +140,7 @@ def test_show_banner_does_not_print_skills(): cli_obj.preloaded_skills = ["hermes-agent-dev", "github-auth"] cli_obj.console = MagicMock() - with patch("cli.build_welcome_banner") as mock_banner, patch( + with patch("hermes_cli.banner.build_welcome_banner") as mock_banner, patch( "shutil.get_terminal_size", return_value=os.terminal_size((120, 40)) ): cli_obj.show_banner() diff --git a/tests/cli/test_cli_provider_resolution.py b/tests/cli/test_cli_provider_resolution.py index ff4734f896..959aed8b08 100644 --- a/tests/cli/test_cli_provider_resolution.py +++ b/tests/cli/test_cli_provider_resolution.py @@ -249,7 +249,7 @@ def test_runtime_resolution_failure_is_not_sticky(monkeypatch): monkeypatch.setattr("hermes_cli.runtime_provider.resolve_runtime_provider", _runtime_resolve) monkeypatch.setattr("hermes_cli.runtime_provider.format_runtime_provider_error", lambda exc: str(exc)) - monkeypatch.setattr(cli, "AIAgent", _DummyAgent) + monkeypatch.setattr("run_agent.AIAgent", _DummyAgent) shell = cli.HermesCLI(model="gpt-5", compact=True, max_turns=1) diff --git a/tests/cli/test_cli_status_command.py b/tests/cli/test_cli_status_command.py index 99518e2deb..ac1e4d42d8 100644 --- a/tests/cli/test_cli_status_command.py +++ b/tests/cli/test_cli_status_command.py @@ -78,7 +78,7 @@ def test_show_session_status_prints_gateway_style_summary(): "started_at": 1775791440, } - with patch("cli.display_hermes_home", return_value="~/.hermes"): + with patch("hermes_constants.display_hermes_home", return_value="~/.hermes"): cli_obj._show_session_status() printed = "\n".join(str(call.args[0]) for call in cli_obj.console.print.call_args_list) @@ -107,7 +107,7 @@ def test_show_session_status_includes_reasoning_approvals_context(): "context_tokens": 50000, "context_length": 200000, "context_percent": 25, } - with patch("cli.display_hermes_home", return_value="~/.hermes"), \ + with patch("hermes_constants.display_hermes_home", return_value="~/.hermes"), \ patch("tools.approval._get_approval_mode", return_value="manual"), \ patch("tools.approval.is_approval_bypass_active_for_session", return_value=False): cli_obj._show_session_status() diff --git a/tests/cli/test_cli_terminal_response_sanitizer.py b/tests/cli/test_cli_terminal_response_sanitizer.py index cd095f0368..eb8c66c9cb 100644 --- a/tests/cli/test_cli_terminal_response_sanitizer.py +++ b/tests/cli/test_cli_terminal_response_sanitizer.py @@ -5,7 +5,12 @@ leak into the input buffer after terminal resize storms or multiplexer tab switches — see issue #14692. """ -from cli import _strip_leaked_terminal_responses +from cli import _strip_leaked_terminal_responses_with_meta + + +def _strip_leaked_terminal_responses(text: str) -> str: + cleaned, _ = _strip_leaked_terminal_responses_with_meta(text) + return cleaned class TestStripLeakedTerminalResponses: diff --git a/tests/cli/test_worktree.py b/tests/cli/test_worktree.py index 3b75c2e40a..d7bb2a148d 100644 --- a/tests/cli/test_worktree.py +++ b/tests/cli/test_worktree.py @@ -8,6 +8,8 @@ import os import shutil import subprocess import pytest + +from hermes_cli import worktree_ops from pathlib import Path @@ -845,7 +847,7 @@ class TestWorktreeLockReaping: import cli wt = self._mk(cli, git_repo, "hermes-dead", pid=999999) # sanity: this is the accumulation bug — remove --force alone can't do it - assert cli._worktree_lock_is_live(str(git_repo), str(wt)) == "dead" + assert worktree_ops._worktree_lock_is_live(str(git_repo), str(wt)) == "dead" cli._prune_stale_worktrees(str(git_repo)) assert not wt.exists(), "dead-locked clean worktree should be unlocked + reaped" @@ -899,19 +901,19 @@ class TestWorktreeLockPredicate: ["git", "worktree", "add", str(p), "-b", "hermes/hermes-x", "HEAD"], cwd=git_repo, capture_output=True, ) - assert cli._worktree_lock_is_live(str(git_repo), str(p)) is None + assert worktree_ops._worktree_lock_is_live(str(git_repo), str(p)) is None def test_foreign_lock_reason_returns_dead(self, git_repo): import cli p = self._mk_locked(git_repo, "hermes-foreign", "some other tool") - assert cli._worktree_lock_is_live(str(git_repo), str(p)) == "dead" + assert worktree_ops._worktree_lock_is_live(str(git_repo), str(p)) == "dead" def test_bad_repo_root_fails_safe_to_live(self, tmp_path): import cli # Not a git repo -> git query fails -> must report "live" (never delete) - assert cli._worktree_lock_is_live(str(tmp_path), str(tmp_path / "x")) == "live" + assert worktree_ops._worktree_lock_is_live(str(tmp_path), str(tmp_path / "x")) == "live" class TestWidenedPruner: @@ -1021,7 +1023,7 @@ class TestWidenedPruner: ["git", "worktree", "add", str(p), "-b", "wt/noremote", "HEAD"], cwd=repo, capture_output=True, ) - assert cli._worktree_commits_all_merged_upstream(str(p)) is False + assert worktree_ops._worktree_commits_all_merged_upstream(str(p)) is False # -- preserved-work warning ---------------------------------------------- @@ -1050,10 +1052,10 @@ class TestMergeVerdictCache: wt, sha = self._mk(git_repo, "hermes-cachehit", commit=True) self._merge_upstream(git_repo, sha) - uncached = cli._worktree_commits_all_merged_upstream(str(wt)) + uncached = worktree_ops._worktree_commits_all_merged_upstream(str(wt)) cache = {} - cold = cli._worktree_commits_all_merged_upstream(str(wt), cache=cache) - warm = cli._worktree_commits_all_merged_upstream(str(wt), cache=cache) + cold = worktree_ops._worktree_commits_all_merged_upstream(str(wt), cache=cache) + warm = worktree_ops._worktree_commits_all_merged_upstream(str(wt), cache=cache) assert cache, "verdict should have been memoized" assert uncached is cold is warm is True @@ -1070,7 +1072,7 @@ class TestMergeVerdictCache: self._merge_upstream(git_repo, sha) cache = {} - assert cli._worktree_commits_all_merged_upstream(str(wt), cache=cache) is True + assert worktree_ops._worktree_commits_all_merged_upstream(str(wt), cache=cache) is True key_after_merge = set(cache) # New local-only work lands in the worktree. @@ -1078,7 +1080,7 @@ class TestMergeVerdictCache: subprocess.run(["git", "add", "more.txt"], cwd=wt, capture_output=True) subprocess.run(["git", "commit", "-m", "new work"], cwd=wt, capture_output=True) - assert cli._worktree_commits_all_merged_upstream(str(wt), cache=cache) is False + assert worktree_ops._worktree_commits_all_merged_upstream(str(wt), cache=cache) is False assert set(cache) != key_after_merge, "moved HEAD must produce a new key" @@ -1091,8 +1093,8 @@ class TestMergeVerdictCache: monkeypatch.setattr(worktree_ops, "_worktree_merge_cache_path", lambda: path) monkeypatch.setattr(worktree_ops, "_WORKTREE_MERGE_CACHE_MAX", 10) - cli._save_worktree_merge_cache({f"sha{i}..sha{i}:20": True for i in range(50)}) - assert len(cli._load_worktree_merge_cache()) == 10 + worktree_ops._save_worktree_merge_cache({f"sha{i}..sha{i}:20": True for i in range(50)}) + assert len(worktree_ops._load_worktree_merge_cache()) == 10 class TestPruneParallelEquivalence: @@ -1151,7 +1153,7 @@ class TestPruneParallelEquivalence: # Phase B — an independent board (distinct branch names so creation # can't collide with phase A's leftover refs) run through a real pool. try: - cli._worktree_merge_cache_path().unlink() + worktree_ops._worktree_merge_cache_path().unlink() except Exception: pass board2 = self._board(git_repo, tag="b") @@ -1177,7 +1179,9 @@ class TestPruneParallelEquivalence: def __init__(self, *a, **kw): raise RuntimeError("cannot start thread") - monkeypatch.setattr(cli.concurrent.futures, "ThreadPoolExecutor", _Boom) + import concurrent.futures + + monkeypatch.setattr(concurrent.futures, "ThreadPoolExecutor", _Boom) monkeypatch.setattr(cli.os, "cpu_count", lambda: 8) cli._prune_stale_worktrees(str(git_repo)) @@ -1289,7 +1293,7 @@ class TestShallowCloneDeepening: _, clone, wt = self._stuck_worktree(tmp_path) assert cli._worktree_has_unpushed_commits(str(wt)) - assert cli._deepen_shallow_repo(str(clone)) is True + assert worktree_ops._deepen_shallow_repo(str(clone)) is True assert not cli._repo_is_shallow(str(clone)) assert not cli._worktree_has_unpushed_commits(str(wt)), ( "after deepening, the worktree's HEAD is an ancestor of " @@ -1317,7 +1321,7 @@ class TestShallowCloneDeepening: ["git", "remote", "set-url", "origin", f"file://{tmp_path}/gone"], clone, ) - assert cli._deepen_shallow_repo(str(clone), timeout=30) is False + assert worktree_ops._deepen_shallow_repo(str(clone), timeout=30) is False cli._prune_stale_worktrees(str(clone)) assert wt.exists(), ( "offline deepen failure must fall back to preserving the tree" @@ -1325,7 +1329,7 @@ class TestShallowCloneDeepening: def test_deepen_noop_on_full_clone(self, git_repo): import cli - assert cli._deepen_shallow_repo(str(git_repo)) is True + assert worktree_ops._deepen_shallow_repo(str(git_repo)) is True def test_real_unpushed_work_survives_deepening(self, tmp_path): """Deepening must not turn genuinely unpushed commits reapable.""" @@ -1384,7 +1388,7 @@ class TestPrMergedEscapeHatch: def test_merged_pr_tree_is_reaped(self, git_repo, tmp_path, monkeypatch): import cli wt = self._mk_diverged(git_repo, "hermes-rebase-merged") - assert cli._worktree_commits_all_merged_upstream(str(wt)) is False, ( + assert worktree_ops._worktree_commits_all_merged_upstream(str(wt)) is False, ( "precondition: cherry must NOT consider this merged — the PR " "check is the only thing that can reap it" ) @@ -1426,19 +1430,19 @@ class TestPrMergedEscapeHatch: wt = self._mk_diverged(git_repo, "hermes-memo") self._stub_gh(tmp_path, monkeypatch) cache: dict = {} - assert cli._worktree_branch_pr_merged(str(wt), cache=cache) is True + assert worktree_ops._worktree_branch_pr_merged(str(wt), cache=cache) is True keys = [k for k in cache if k.startswith("pr-merged:")] assert len(keys) == 1 and cache[keys[0]] is True # Break gh: a cached True verdict must not re-consult it. self._stub_gh(tmp_path, monkeypatch, stdout="", exit_code=1) - assert cli._worktree_branch_pr_merged(str(wt), cache=cache) is True + assert worktree_ops._worktree_branch_pr_merged(str(wt), cache=cache) is True def test_negative_verdict_not_cached(self, git_repo, tmp_path, monkeypatch): import cli wt = self._mk_diverged(git_repo, "hermes-nocache-neg") self._stub_gh(tmp_path, monkeypatch, stdout="[]") cache: dict = {} - assert cli._worktree_branch_pr_merged(str(wt), cache=cache) is False + assert worktree_ops._worktree_branch_pr_merged(str(wt), cache=cache) is False assert not [k for k in cache if k.startswith("pr-merged:")], ( "False must not be memoized — the PR can merge later with the " "same (branch, head) key" diff --git a/tests/cli/test_worktree_pushed_tier.py b/tests/cli/test_worktree_pushed_tier.py index b7d4cc13d4..c1f4990537 100644 --- a/tests/cli/test_worktree_pushed_tier.py +++ b/tests/cli/test_worktree_pushed_tier.py @@ -15,6 +15,8 @@ import time import pytest +from hermes_cli import worktree_ops + def _run(args, cwd): return subprocess.run( @@ -93,7 +95,7 @@ class TestFetchRemoteBranchHeads: import cli repo = repo_with_bare_origin _mk_worktree(repo, "hermes-a", "hermes/hermes-a", push=True) - heads = cli._fetch_remote_branch_heads(str(repo)) + heads = worktree_ops._fetch_remote_branch_heads(str(repo)) assert heads is not None assert "main" in heads assert "hermes/hermes-a" in heads @@ -104,7 +106,7 @@ class TestFetchRemoteBranchHeads: repo.mkdir() _run(["git", "init"], repo) _run(["git", "remote", "add", "origin", str(tmp_path / "missing.git")], repo) - assert cli._fetch_remote_branch_heads(str(repo)) is None + assert worktree_ops._fetch_remote_branch_heads(str(repo)) is None class TestBranchPushedExact: @@ -112,8 +114,8 @@ class TestBranchPushedExact: import cli repo = repo_with_bare_origin wt = _mk_worktree(repo, "hermes-x", "hermes/hermes-x", push=True) - heads = cli._fetch_remote_branch_heads(str(repo)) - assert cli._worktree_branch_pushed_exact(str(wt), heads) is True + heads = worktree_ops._fetch_remote_branch_heads(str(repo)) + assert worktree_ops._worktree_branch_pushed_exact(str(wt), heads) is True def test_local_ahead_of_push_false(self, repo_with_bare_origin): import cli @@ -122,22 +124,22 @@ class TestBranchPushedExact: repo, "hermes-y", "hermes/hermes-y", push=True, extra_commit_after_push=True, ) - heads = cli._fetch_remote_branch_heads(str(repo)) - assert cli._worktree_branch_pushed_exact(str(wt), heads) is False + heads = worktree_ops._fetch_remote_branch_heads(str(repo)) + assert worktree_ops._worktree_branch_pushed_exact(str(wt), heads) is False def test_never_pushed_false(self, repo_with_bare_origin): import cli repo = repo_with_bare_origin wt = _mk_worktree(repo, "hermes-z", "hermes/hermes-z", push=False) - heads = cli._fetch_remote_branch_heads(str(repo)) - assert cli._worktree_branch_pushed_exact(str(wt), heads) is False + heads = worktree_ops._fetch_remote_branch_heads(str(repo)) + assert worktree_ops._worktree_branch_pushed_exact(str(wt), heads) is False def test_none_heads_false(self, repo_with_bare_origin): import cli repo = repo_with_bare_origin wt = _mk_worktree(repo, "hermes-n", "hermes/hermes-n", push=True) - assert cli._worktree_branch_pushed_exact(str(wt), None) is False - assert cli._worktree_branch_pushed_exact(str(wt), {}) is False + assert worktree_ops._worktree_branch_pushed_exact(str(wt), None) is False + assert worktree_ops._worktree_branch_pushed_exact(str(wt), {}) is False class TestStartupPrunerPushedTier: diff --git a/tests/cli/test_worktree_selfheal.py b/tests/cli/test_worktree_selfheal.py index 2328ae7da8..936665da1b 100644 --- a/tests/cli/test_worktree_selfheal.py +++ b/tests/cli/test_worktree_selfheal.py @@ -53,7 +53,7 @@ class TestCleanupFailedWorktreeAdd: return wt def test_sweeps_dir_admin_entry_and_branch(self, repo): - from cli import _cleanup_failed_worktree_add + from hermes_cli.worktree_ops import _cleanup_failed_worktree_add wt = self._simulate_timed_out_add(repo) admin = repo / ".git" / "worktrees" / "hermes-dead00" @@ -68,7 +68,7 @@ class TestCleanupFailedWorktreeAdd: def test_retry_succeeds_after_cleanup(self, repo): """The whole point: the same worktree name is creatable again.""" - from cli import _cleanup_failed_worktree_add + from hermes_cli.worktree_ops import _cleanup_failed_worktree_add wt = self._simulate_timed_out_add(repo) _cleanup_failed_worktree_add(str(repo), wt, "hermes/hermes-dead00") @@ -80,7 +80,7 @@ class TestCleanupFailedWorktreeAdd: def test_noop_when_nothing_exists(self, repo): """Fail-soft on an error path where git never created anything.""" - from cli import _cleanup_failed_worktree_add + from hermes_cli.worktree_ops import _cleanup_failed_worktree_add _cleanup_failed_worktree_add( str(repo), repo / ".worktrees" / "never-existed", "hermes/never-existed" diff --git a/tests/cli/test_worktree_sync_base.py b/tests/cli/test_worktree_sync_base.py index 224eeeb150..64505b52c9 100644 --- a/tests/cli/test_worktree_sync_base.py +++ b/tests/cli/test_worktree_sync_base.py @@ -4,7 +4,7 @@ A worktree created off the standalone clone's local ``HEAD`` roots the new branch on a stale base when that clone lags the remote. ``_resolve_worktree_base`` fetches and branches from the remote tip instead so the worktree starts current. -These tests exercise the REAL ``cli._resolve_worktree_base`` / +These tests exercise the REAL ``worktree_ops._resolve_worktree_base`` / ``cli._setup_worktree`` against a real local "remote" repo (so ``git fetch`` works offline in the hermetic sandbox), proving the worktree includes commits that exist on the remote but not on the stale local HEAD. @@ -17,6 +17,8 @@ from pathlib import Path import pytest +from hermes_cli import worktree_ops + import cli @@ -76,7 +78,7 @@ def remote_and_clone(tmp_path): class TestResolveWorktreeBase: def test_resolves_to_fetched_upstream(self, remote_and_clone): clone, remote_head, stale_local_head = remote_and_clone - base_ref, label = cli._resolve_worktree_base(str(clone)) + base_ref, label = worktree_ops._resolve_worktree_base(str(clone)) # Should resolve to the upstream tracking ref and have fetched it. assert base_ref == "origin/main" assert "fetched" in label @@ -92,7 +94,7 @@ class TestResolveWorktreeBase: _run(["git", "config", "user.email", "t@t.com"], repo) _run(["git", "config", "user.name", "T"], repo) _commit(repo, "README.md", "only commit") - base_ref, label = cli._resolve_worktree_base(str(repo)) + base_ref, label = worktree_ops._resolve_worktree_base(str(repo)) assert base_ref == "HEAD" assert "HEAD" in label @@ -119,7 +121,7 @@ class TestResolveWorktreeBaseStartupCost: return real_run(args, **kw) monkeypatch.setattr(subprocess, "run", spy) - base_ref, label = cli._resolve_worktree_base(str(clone)) + base_ref, label = worktree_ops._resolve_worktree_base(str(clone)) assert base_ref == "origin/main" assert "fetched" in label and "ago" in label assert calls == [], "fresh FETCH_HEAD must skip the network fetch" @@ -134,7 +136,7 @@ class TestResolveWorktreeBaseStartupCost: fetch_head = Path(clone) / ".git" / "FETCH_HEAD" old = time.time() - 3600 os.utime(fetch_head, (old, old)) - base_ref, label = cli._resolve_worktree_base(str(clone)) + base_ref, label = worktree_ops._resolve_worktree_base(str(clone)) assert base_ref == "origin/main" assert label == "origin/main (fetched)" @@ -154,7 +156,7 @@ class TestResolveWorktreeBaseStartupCost: monkeypatch.setattr(subprocess, "run", stall_fetches) start = time.monotonic() - base_ref, label = cli._resolve_worktree_base(str(clone)) + base_ref, label = worktree_ops._resolve_worktree_base(str(clone)) elapsed = time.monotonic() - start # Cached tracking ref, single fetch attempt, no step-2 cascade. assert base_ref == "origin/main" @@ -185,7 +187,7 @@ class TestResolveWorktreeBaseStartupCost: _run( ["git", "config", "branch.main.merge", "refs/heads/main"], repo ) - base_ref, label = cli._resolve_worktree_base(str(repo)) + base_ref, label = worktree_ops._resolve_worktree_base(str(repo)) assert base_ref == "HEAD" assert "HEAD" in label diff --git a/tests/hermes_cli/test_backup.py b/tests/hermes_cli/test_backup.py index 459c5ec8b1..3850cce801 100644 --- a/tests/hermes_cli/test_backup.py +++ b/tests/hermes_cli/test_backup.py @@ -632,16 +632,16 @@ class TestRoundTrip: class TestFormatSize: def test_bytes(self): - from hermes_cli.backup import _format_size + from hermes_cli.sizefmt import format_bytes as _format_size assert _format_size(512) == "512 B" def test_kilobytes(self): - from hermes_cli.backup import _format_size + from hermes_cli.sizefmt import format_bytes as _format_size assert "KB" in _format_size(2048) def test_terabytes(self): - from hermes_cli.backup import _format_size + from hermes_cli.sizefmt import format_bytes as _format_size assert "TB" in _format_size(2 * 1024 ** 4) diff --git a/tests/hermes_cli/test_gateway_restart_loop.py b/tests/hermes_cli/test_gateway_restart_loop.py index 58eb2bcd6f..423ae78088 100644 --- a/tests/hermes_cli/test_gateway_restart_loop.py +++ b/tests/hermes_cli/test_gateway_restart_loop.py @@ -12,10 +12,8 @@ from argparse import Namespace import pytest -from hermes_cli.cron import ( - _contains_gateway_lifecycle_command, - cron_command, -) +from cron.lifecycle_guard import contains_gateway_lifecycle_command as _contains_gateway_lifecycle_command +from hermes_cli.cron import cron_command # --------------------------------------------------------------------------- diff --git a/tests/hermes_cli/test_git_probe_tree_kill.py b/tests/hermes_cli/test_git_probe_tree_kill.py index 05fec64007..1a38d7fa37 100644 --- a/tests/hermes_cli/test_git_probe_tree_kill.py +++ b/tests/hermes_cli/test_git_probe_tree_kill.py @@ -3,7 +3,7 @@ Timing out a git probe must not leave helper descendants (credential helpers, ``git-remote-https``, hook children) running after the probe returns. The probe spawns the child in its own process group (``process_group=0``) and -``_kill_git_process_tree`` signals the whole group with ``os.killpg`` — but +``kill_process_tree`` signals the whole group with ``os.killpg`` — but only when the child actually leads its own group, so a shared-group spawn can never take down unrelated processes. @@ -20,7 +20,7 @@ import time import pytest from hermes_cli import _subprocess_compat -from hermes_cli._subprocess_compat import _kill_git_process_tree, bounded_git_probe +from hermes_cli._subprocess_compat import bounded_git_probe, kill_process_tree pytestmark = pytest.mark.skipif( sys.platform == "win32", reason="POSIX process-group semantics" @@ -94,7 +94,7 @@ def test_posix_spawn_uses_own_process_group(tmp_path): def test_group_kill_skipped_when_child_shares_our_group(): - """_kill_git_process_tree must never killpg a group the child doesn't lead. + """kill_process_tree must never killpg a group the child doesn't lead. Spawn WITHOUT process_group=0 (child inherits OUR group): the ownership check (pgid == pid) must skip the group signal, or the test process itself @@ -107,7 +107,7 @@ def test_group_kill_skipped_when_child_shares_our_group(): stdin=subprocess.DEVNULL, ) assert os.getpgid(proc.pid) == os.getpgid(0) # shared group precondition - _kill_git_process_tree(proc) + kill_process_tree(proc) proc.wait(timeout=5) # We are alive to make this assertion — killpg on our own group would have # taken the test runner down. The direct child is still killed. @@ -132,4 +132,4 @@ def test_kill_helper_swallow_all_failures(): def kill(self): raise OSError("already reaped") - _kill_git_process_tree(_Dead()) # must not raise + kill_process_tree(_Dead()) # must not raise diff --git a/tests/hermes_cli/test_kanban_worktree_teardown.py b/tests/hermes_cli/test_kanban_worktree_teardown.py index 0fc9593912..260835ddec 100644 --- a/tests/hermes_cli/test_kanban_worktree_teardown.py +++ b/tests/hermes_cli/test_kanban_worktree_teardown.py @@ -144,7 +144,9 @@ def test_tree_dirtied_between_check_and_removal_preserved( (wt / "late-wip.txt").write_text("dirtied after the check\n", encoding="utf-8") # Pre-check lies (as if the file appeared just after it ran) — real git # must still refuse the removal. - monkeypatch.setattr(cli, "_worktree_is_dirty", lambda _p: False) + from hermes_cli import worktree_ops + + monkeypatch.setattr(worktree_ops, "_worktree_is_dirty", lambda _p: False) kb._cleanup_worktree_workspace("t_gggg7777", str(wt)) assert wt.is_dir() assert (wt / "late-wip.txt").exists() diff --git a/tests/hermes_cli/test_plugins_cmd_list.py b/tests/hermes_cli/test_plugins_cmd_list.py index 9c8ffe6042..c1f033f2cc 100644 --- a/tests/hermes_cli/test_plugins_cmd_list.py +++ b/tests/hermes_cli/test_plugins_cmd_list.py @@ -1,3 +1,4 @@ +import importlib.metadata import argparse import json from types import SimpleNamespace @@ -75,7 +76,7 @@ def test_discover_all_plugins_includes_entrypoint_plugins(monkeypatch, tmp_path) lambda: bundled_dir, ) monkeypatch.setattr( - plugins_cmd.importlib.metadata, + importlib.metadata, "entry_points", lambda: [entry_point], ) @@ -117,7 +118,7 @@ def test_declared_capabilities_for_entrypoint_uses_distribution_metadata( "hermes_cli.plugins.get_bundled_plugins_dir", lambda: bundled_dir ) monkeypatch.setattr( - plugins_cmd.importlib.metadata, + importlib.metadata, "entry_points", lambda: [plugin_ep, capability_ep], ) diff --git a/tests/hermes_cli/test_relay_shared_metrics_runtime.py b/tests/hermes_cli/test_relay_shared_metrics_runtime.py index 292c43abbe..1e3f591743 100644 --- a/tests/hermes_cli/test_relay_shared_metrics_runtime.py +++ b/tests/hermes_cli/test_relay_shared_metrics_runtime.py @@ -15,7 +15,8 @@ from typing import Any import pytest from hermes_cli import lifecycle, plugins -from hermes_cli.observability import relay_runtime, relay_shared_metrics +from agent import relay_runtime +from hermes_cli.observability import relay_shared_metrics from hermes_cli.plugins import PluginManager diff --git a/tests/test_model_tools.py b/tests/test_model_tools.py index 03d78e2595..fb84b69e4c 100644 --- a/tests/test_model_tools.py +++ b/tests/test_model_tools.py @@ -284,7 +284,7 @@ class TestPreToolCallBlocking: monkeypatch.setattr("hermes_cli.plugins.invoke_hook", fake_invoke_hook) monkeypatch.setattr("model_tools.registry.dispatch", lambda *a, **kw: (_ for _ in ()).throw(AssertionError("should not run"))) - monkeypatch.setattr("tools.file_tools.notify_other_tool_call", + monkeypatch.setattr("tools.file_tools_read_tracking.notify_other_tool_call", lambda task_id: notifications.append(task_id)) result = json.loads(handle_function_call("web_search", {"q": "test"}, task_id="t1")) @@ -327,7 +327,7 @@ class TestPreToolCallBlocking: return json.dumps({"ok": True}) monkeypatch.setattr( - "hermes_cli.observability.relay_runtime.apply_tool_request_intercepts", + "agent.relay_runtime.apply_tool_request_intercepts", rewrite, ) monkeypatch.setattr("hermes_cli.plugins.invoke_hook", fake_invoke_hook) diff --git a/tests/test_tui_gateway_server.py b/tests/test_tui_gateway_server.py index b5ea0fccdc..20ef2731c7 100644 --- a/tests/test_tui_gateway_server.py +++ b/tests/test_tui_gateway_server.py @@ -20726,7 +20726,7 @@ def test_fallback_session_info_reports_session_cwd_not_launch_dir(monkeypatch): wrong project for any session resumed without a built agent (#71254). """ monkeypatch.setattr(server, "_default_session_cwd", lambda: "/gateway/launch/dir") - monkeypatch.setattr(server, "_git_branch_for_cwd", lambda cwd: "bb/feature") + monkeypatch.setattr(server.git_probe, "branch", lambda cwd: "bb/feature") monkeypatch.setattr(server, "_project_info_for_cwd", lambda cwd: None) monkeypatch.setattr(server, "_resolve_model", lambda: "test-model") @@ -20743,7 +20743,7 @@ def test_fallback_session_info_always_emits_branch(monkeypatch): after switching into a non-git session. """ monkeypatch.setattr(server, "_default_session_cwd", lambda: "/gateway/launch/dir") - monkeypatch.setattr(server, "_git_branch_for_cwd", lambda cwd: "") + monkeypatch.setattr(server.git_probe, "branch", lambda cwd: "") monkeypatch.setattr(server, "_project_info_for_cwd", lambda cwd: None) monkeypatch.setattr(server, "_resolve_model", lambda: "test-model") @@ -22238,13 +22238,13 @@ def test_workspace_move_rehomes_running_session(monkeypatch, tmp_path): monkeypatch.setattr(server, "_profile_db", _fake_db) monkeypatch.setattr( - server, - "_git_branch_for_cwd", + server.git_probe, + "branch", lambda cwd: "main", ) monkeypatch.setattr( - server, - "_git_common_repo_root_for_cwd", + server.git_probe, + "common_repo_root", lambda cwd: str(new_cwd), ) diff --git a/tests/tools/test_windows_native_support.py b/tests/tools/test_windows_native_support.py index c689f165d5..7e65985243 100644 --- a/tests/tools/test_windows_native_support.py +++ b/tests/tools/test_windows_native_support.py @@ -749,7 +749,7 @@ class TestGitBashPathNormalization: def test_posix_noop(self): """Must NOT mutate paths on Linux/macOS.""" - from cli import _normalize_git_bash_path + from hermes_cli.worktree_ops import _normalize_git_bash_path if sys.platform != "win32": assert _normalize_git_bash_path("/home/teknium/foo") == "/home/teknium/foo" assert _normalize_git_bash_path("/c/Users/foo") == "/c/Users/foo" diff --git a/tests/tools/test_zombie_process_cleanup.py b/tests/tools/test_zombie_process_cleanup.py index ba3c20eebc..141f81c96e 100644 --- a/tests/tools/test_zombie_process_cleanup.py +++ b/tests/tools/test_zombie_process_cleanup.py @@ -370,7 +370,7 @@ class TestDelegationCleanup: reset_hermes_home_override, set_hermes_home_override, ) - from hermes_cli.observability import relay_runtime + from agent import relay_runtime from tools.delegate_tool import _run_single_child parent = MagicMock() @@ -415,7 +415,7 @@ class TestDelegationCleanup: def test_active_child_turn_owns_relay_scope_cleanup(self, monkeypatch): from unittest.mock import MagicMock - from hermes_cli.observability import relay_runtime + from agent import relay_runtime from tools.delegate_tool import _run_single_child parent = MagicMock() diff --git a/tests/tui_gateway/test_protocol.py b/tests/tui_gateway/test_protocol.py index 5412f333a8..b0b7f4825a 100644 --- a/tests/tui_gateway/test_protocol.py +++ b/tests/tui_gateway/test_protocol.py @@ -999,7 +999,7 @@ def test_session_resume_active_turn_payload_matches_desktop_fixture(server, monk "session_key": fixture["session_key"], } monkeypatch.setattr(server, "_get_db", lambda: _DB()) - monkeypatch.setattr(server, "_session_info", lambda _agent: fixture["info"]) + monkeypatch.setattr(server, "_session_info", lambda _agent, _session=None: fixture["info"]) # JSON round-trip the real RPC envelope: the desktop fixture must stay # faithful to what the gateway actually serializes, not a copied shape. diff --git a/tests/tui_gateway/test_session_git_metadata_generation.py b/tests/tui_gateway/test_session_git_metadata_generation.py index 89dca81201..91cd7d7106 100644 --- a/tests/tui_gateway/test_session_git_metadata_generation.py +++ b/tests/tui_gateway/test_session_git_metadata_generation.py @@ -32,11 +32,11 @@ def test_cwd_claim_precedes_probe_and_generation_reaches_publish(monkeypatch): monkeypatch.setattr(server, "_get_db", lambda: DB()) monkeypatch.setattr(server.threading, "Thread", _ImmediateThread) monkeypatch.setattr( - server, - "_git_branch_for_cwd", + server.git_probe, + "branch", lambda cwd: events.append(("probe", cwd)) or "feature", ) - monkeypatch.setattr(server, "_git_common_repo_root_for_cwd", lambda _cwd: "/repo") + monkeypatch.setattr(server.git_probe, "common_repo_root", lambda _cwd: "/repo") generation = server._persist_session_cwd_and_schedule_git_meta( {"session_key": "session"}, "/repo/worktree" @@ -61,7 +61,7 @@ def test_missing_db_claim_never_starts_git_probe(monkeypatch): probed = [] monkeypatch.setattr(server, "_get_db", lambda: None) monkeypatch.setattr( - server, "_git_branch_for_cwd", lambda cwd: probed.append(cwd) + server.git_probe, "branch", lambda cwd: probed.append(cwd) ) generation = server._persist_session_cwd_and_schedule_git_meta( diff --git a/tui_gateway/agent_callbacks.py b/tui_gateway/agent_callbacks.py index 43f230c0fd..5eb95c0982 100644 --- a/tui_gateway/agent_callbacks.py +++ b/tui_gateway/agent_callbacks.py @@ -145,7 +145,7 @@ def _apply_project_workspace(task_id: str, path: str, _name: str = "") -> None: try: agent = session.get("agent") info = _session_info(agent, session) if agent is not None else { - "cwd": resolved, "branch": _git_branch_for_cwd(resolved), + "cwd": resolved, "branch": git_probe.branch(resolved), "project": _project_info_for_cwd(resolved), "lazy": True} _emit("session.info", sid, info) except Exception: diff --git a/tui_gateway/compute_host_bridge.py b/tui_gateway/compute_host_bridge.py index 003c119b2e..1804ce9da4 100644 --- a/tui_gateway/compute_host_bridge.py +++ b/tui_gateway/compute_host_bridge.py @@ -75,11 +75,7 @@ def _metadata_mirror(session: dict | None) -> dict: def _compute_host_session_info(session: dict) -> dict: - # Tolerate a legacy one-arg _session_info (tests patch it that way). - try: - return _session_info(session.get("agent"), session) - except TypeError: - return _session_info(session.get("agent")) + return _session_info(session.get("agent"), session) def _compute_host_adopt_frame_meta(session: dict, frame: dict) -> None: diff --git a/tui_gateway/methods_config.py b/tui_gateway/methods_config.py index 9fa325ef78..b60562e1a8 100644 --- a/tui_gateway/methods_config.py +++ b/tui_gateway/methods_config.py @@ -139,7 +139,7 @@ def _cfg_get_provider(params): def _cfg_get_project(params): raw = str(params.get("cwd", "") or (_load_cfg().get("terminal") or {}).get("cwd", "") or "").strip() cwd = _completion_cwd({"cwd": raw} if raw else {}) - return {"cwd": cwd, "branch": _git_branch_for_cwd(cwd)} + return {"cwd": cwd, "branch": git_probe.branch(cwd)} def _cfg_get_personality(params): diff --git a/tui_gateway/methods_config_set.py b/tui_gateway/methods_config_set.py index a961286277..d676c8f2ce 100644 --- a/tui_gateway/methods_config_set.py +++ b/tui_gateway/methods_config_set.py @@ -401,7 +401,7 @@ def _set_cwd(rid, params, key, value, session): return _err(rid, 4002, f"working directory does not exist: {raw}") _write_config_key("terminal.cwd", cwd) os.environ["TERMINAL_CWD"] = cwd - return _kv(rid, "terminal.cwd", cwd, cwd=cwd, branch=_git_branch_for_cwd(cwd)) + return _kv(rid, "terminal.cwd", cwd, cwd=cwd, branch=git_probe.branch(cwd)) @_cfgset_guarded diff --git a/tui_gateway/methods_projects.py b/tui_gateway/methods_projects.py index f8abc8fa6f..f8ba640e22 100644 --- a/tui_gateway/methods_projects.py +++ b/tui_gateway/methods_projects.py @@ -125,7 +125,7 @@ def _(rid, params, pdb, conn) -> dict: proj = pdb.project_for_path(conn, cwd) return _ok(rid, { "project": proj.to_dict() if proj else None, "cwd": cwd, - "branch": _git_branch_for_cwd(cwd)}) + "branch": git_probe.branch(cwd)}) def _non_workspace_dirs() -> set[str]: @@ -277,7 +277,7 @@ def _discover_repos_payload( cwd_to_root: dict[str, str] = {} for row in cwd_rows: cwd = str(row.get("cwd") or "") - root = _git_common_repo_root_for_cwd(cwd) + root = git_probe.common_repo_root(cwd) if not root: continue cwd_to_root[cwd] = root @@ -386,7 +386,7 @@ def _build_project_tree( [str(f.get("path") or "") for p in projects for f in (p.get("folders") or [])] + [str(r.get("root") or "") for r in discovered]) tree = project_tree.build_tree( - projects, sessions, discovered, _resolve_cwd_git, preview_limit=preview_limit, + projects, sessions, discovered, git_probe.resolve, preview_limit=preview_limit, hydrate=hydrate, is_junk_root=_is_repo_junk, is_junk_cwd=_is_session_cwd_junk, exists=_dir_exists_cached) return tree, active_id diff --git a/tui_gateway/methods_session.py b/tui_gateway/methods_session.py index b8374d15e8..605db33aab 100644 --- a/tui_gateway/methods_session.py +++ b/tui_gateway/methods_session.py @@ -117,7 +117,7 @@ def _cwd_info(session: dict, cwd: str, branch=None) -> dict: """session.info after a cwd change: the full agent view, or the lazy shape.""" if (agent := session.get("agent")) is not None: return _session_info(agent, session) - return {"cwd": cwd, "branch": _git_branch_for_cwd(cwd) if branch is None else branch, + return {"cwd": cwd, "branch": git_probe.branch(cwd) if branch is None else branch, "project": _project_info_for_cwd(cwd), "lazy": True} @@ -355,7 +355,7 @@ def _(rid, params: dict) -> dict: # Reflect the override now so the client doesn't clobber its sticky pick. "info": {"model": override.get("model") if override else _resolve_model(), **({"provider": override["provider"]} if override.get("provider") else {}), - "tools": {}, "skills": {}, "cwd": cwd, "branch": _git_branch_for_cwd(cwd), + "tools": {}, "skills": {}, "cwd": cwd, "branch": git_probe.branch(cwd), "project": _project_info_for_cwd(cwd), "lazy": True, "desktop_contract": DESKTOP_BACKEND_CONTRACT, "profile_name": _response_profile_name(profile)}}) @@ -851,7 +851,7 @@ def _(rid, params: dict) -> dict: with _sessions_lock: live_sid, live = next( ((sid, sess) for sid, sess in list(_sessions.items()) if sess.get("session_key") == target), ("", None)) - branch, root = _git_branch_for_cwd(resolved), _git_common_repo_root_for_cwd(resolved) + branch, root = git_probe.branch(resolved), git_probe.common_repo_root(resolved) with _profile_db(params) as db: if db is None: return _db_unavailable_error(rid, code=5007) diff --git a/tui_gateway/server.py b/tui_gateway/server.py index dae582a0e0..01fee96b9c 100644 --- a/tui_gateway/server.py +++ b/tui_gateway/server.py @@ -31,7 +31,7 @@ from agent.replay_cleanup import sanitize_replay_history from agent.compaction_display import project_compaction_message_for_display # noqa: F401 from agent.skill_commands import describe_skill_invocation # noqa: F401 from agent.conversation_loop import INTERRUPT_WAITING_FOR_MODEL_PREFIX # noqa: F401 -from tui_gateway import git_probe # noqa: F401 +from tui_gateway import git_probe from tui_gateway._env import env_float, env_int from tui_gateway.turn_marker import clear_turn_marker, read_turn_marker, record_turn_start # noqa: F401 from tui_gateway.transport import (StdioTransport, Transport, bind_transport, current_transport, reset_transport) @@ -2062,7 +2062,7 @@ def _session_info(agent, session: dict | None = None) -> dict: "yolo": yolo, "approval_mode": approval_mode, "tools": dict(mirror.get("tools") or {}) if isinstance(mirror.get("tools"), dict) else {}, "skills": dict(mirror.get("skills") or {}) if isinstance(mirror.get("skills"), dict) else {}, - "cwd": cwd, "branch": _git_branch_for_cwd(cwd), "project": _project_info_for_cwd(cwd), + "cwd": cwd, "branch": git_probe.branch(cwd), "project": _project_info_for_cwd(cwd), "terminal_backend": _effective_terminal_backend(), "personality": str(personality or ""), "running": bool(sess.get("running")), "turn_started_at": _turn_started_at(session), "title": _session_live_title(sess, session_key) if session_key else "", @@ -2390,7 +2390,7 @@ def _resolve_checkpoint_hash(mgr, cwd: str, ref: str) -> str: def _lazy_resume_info(cwd: str, *, model: str = "", provider: str = "", profile: str | None = None) -> dict: """session.info for a not-yet-built session (session.create's shape); tools/skills land with the deferred build.""" return { - "cwd": cwd, "branch": _git_branch_for_cwd(cwd), "project": _project_info_for_cwd(cwd), + "cwd": cwd, "branch": git_probe.branch(cwd), "project": _project_info_for_cwd(cwd), "model": model or _resolve_model(), "tools": {}, "skills": {}, "lazy": True, "desktop_contract": DESKTOP_BACKEND_CONTRACT, "profile_name": _response_profile_name(profile), **({"provider": provider} if provider else {}), @@ -2644,7 +2644,7 @@ def _fallback_session_info(session: dict) -> dict: # above already follows. cwd = _session_cwd(session) return { - "cwd": cwd, "branch": _git_branch_for_cwd(cwd), "project": _project_info_for_cwd(cwd), "lazy": True, + "cwd": cwd, "branch": git_probe.branch(cwd), "project": _project_info_for_cwd(cwd), "lazy": True, "model": _resolve_model(), "skills": {}, "tools": {}, "desktop_contract": DESKTOP_BACKEND_CONTRACT, } diff --git a/tui_gateway/session_workdir.py b/tui_gateway/session_workdir.py index 8f8a9748ad..4b078d126d 100644 --- a/tui_gateway/session_workdir.py +++ b/tui_gateway/session_workdir.py @@ -69,13 +69,6 @@ def _terminal_task_cwd_with_source(session: dict | None) -> tuple[str, str]: return _completion_cwd(), "process" -# Git probing lives in git_probe; these keep the in-server names call sites use. -_git_branch_for_cwd = git_probe.branch -_git_repo_root_for_cwd = git_probe.repo_root -_git_common_repo_root_for_cwd = git_probe.common_repo_root -_resolve_cwd_git = git_probe.resolve - - def _session_cwd(session: dict | None) -> str: return str(session["cwd"]) if session and session.get("cwd") else _completion_cwd() @@ -116,7 +109,7 @@ def _heal_dead_cwd(cwd: str) -> str: if not os.path.isdir(probe): return raw with contextlib.suppress(Exception): - return _git_common_repo_root_for_cwd(probe) or _git_repo_root_for_cwd(probe) or probe + return git_probe.common_repo_root(probe) or git_probe.repo_root(probe) or probe return probe @@ -170,11 +163,11 @@ def _reconcile_session_cwd_from_terminal(session: dict | None) -> bool: return False # Worktree ROOTS (folding to the common root would hide the move), both in a git tree, different from each other, # sharing the SAME common .git dir. - landed, current_root = _git_repo_root_for_cwd(resolved), _git_repo_root_for_cwd(current) + landed, current_root = git_probe.repo_root(resolved), git_probe.repo_root(current) if not landed or not current_root or landed == current_root: return False - landed_common = _git_common_repo_root_for_cwd(resolved) - if not landed_common or landed_common != _git_common_repo_root_for_cwd(current): + landed_common = git_probe.common_repo_root(resolved) + if not landed_common or landed_common != git_probe.common_repo_root(current): return False # This is the session's workspace now (a desktop launch-artifact cwd earns a real row); the settle marker keeps it # overridable by the NEXT settle. @@ -490,7 +483,7 @@ def _persist_session_git_meta(session: dict, cwd: str, generation: int) -> None: def _run() -> None: try: - branch, root = _git_branch_for_cwd(cwd), _git_common_repo_root_for_cwd(cwd) + branch, root = git_probe.branch(cwd), git_probe.common_repo_root(cwd) if not (branch or root): return with _session_db(db_session) as db: