simplify(compat): hermes_cli small facades — drop 7 re-exports/aliases (+relay_runtime alias module), repoint 12 callers/tests
This commit is contained in:
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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__)
|
||||
|
||||
+2
-1
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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 <job_id> "
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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."""
|
||||
|
||||
@@ -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."""
|
||||
|
||||
@@ -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}",
|
||||
|
||||
@@ -180,7 +180,7 @@ def _cleanup_worktree_workspace(
|
||||
it. The auto-generated ``wt/<task-id>`` 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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
+14
-14
@@ -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()]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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:
|
||||
|
||||
+25
-21
@@ -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"
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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],
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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),
|
||||
)
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user