refactor(cli): derive relaunch flag table from argparse introspection

Pull the top-level + chat parser construction out of main() into
hermes_cli/_parser.py so relaunch.py can introspect parser._actions to
discover which flags exist and whether they take values, instead of
maintaining a parallel hand-rolled (flag, takes_value) tuple list.

- _parser.py: build_top_level_parser() returns (parser, subparsers,
  chat_parser); side-effect-free import.
- main.py: ~290 lines of inline parser construction collapsed to a
  helper call. Other subparsers stay inline (dispatch is bound to
  module-level cmd_* functions).
- _parser._inherited_flag(parser, ...): wraps parser.add_argument and
  sets action.inherit_on_relaunch = True. Used in place of
  parser.add_argument for the 25 flags (top-level + chat) that need to
  carry over.
- _parser.PRE_ARGPARSE_INHERITED_FLAGS: holds --profile/-p, which
  isn't on argparse (consumed earlier by main._apply_profile_override).
- relaunch.py: drops _CRITICAL_DESTS and _PRE_ARGPARSE_FLAGS; the table
  builder now filters by getattr(action, 'inherit_on_relaunch', False).
- test_ignore_user_config_flags.py: brittle inspect.getsource grep
  replaced with proper parser introspection.
- test_relaunch.py: introspection sanity tests added.

Salvaged from PR #17549; added top-level -t/--toolsets flag to
_parser.py so #17623 (fix(tui): honor launch toolsets) behavior is
preserved on current main.

Co-authored-by: ethernet <arilotter@gmail.com>
This commit is contained in:
ethernet
2026-04-29 20:33:29 -07:00
committed by Teknium
parent 95f2802f84
commit 3c673468b4
5 changed files with 479 additions and 363 deletions
+49 -38
View File
@@ -13,50 +13,60 @@ import shutil
import sys
from typing import Optional, Sequence
# (option_string, takes_value) — flags whose presence (and value, where
# applicable) on the original argv must survive a self-relaunch.
_CRITICAL_FLAGS: list[tuple[str, bool]] = [
("--tui", False),
("--dev", False),
("--profile", True),
("-p", True),
("--model", True),
("-m", True),
("--provider", True),
("--yolo", False),
("--ignore-user-config", False),
("--ignore-rules", False),
("--pass-session-id", False),
("--accept-hooks", False),
("--worktree", False),
("-w", False),
("--skills", True),
("-s", True),
("--quiet", False),
("-Q", False),
("--verbose", False),
("-v", False),
("--source", True),
]
from hermes_cli._parser import (
PRE_ARGPARSE_INHERITED_FLAGS,
build_top_level_parser,
)
def _extract_critical_flags(argv: Sequence[str]) -> list[str]:
"""Pull out flags that affect session behaviour / UI mode."""
def _build_inherited_flag_table() -> list[tuple[str, bool]]:
"""Build the ``(option_string, takes_value)`` table of flags that must
survive a self-relaunch, by introspecting the real parser used by
``hermes`` itself.
A flag participates if its argparse Action carries
``inherit_on_relaunch = True`` — set by ``_parser._inherited_flag``.
"""
parser, _subparsers, chat_parser = build_top_level_parser()
table: list[tuple[str, bool]] = []
seen: set[tuple[str, bool]] = set()
for p in (parser, chat_parser):
for action in p._actions:
if not action.option_strings:
continue # positional / no flag form
if not getattr(action, "inherit_on_relaunch", False):
continue
takes_value = action.nargs != 0 # store_true/false set nargs=0
for opt in action.option_strings:
key = (opt, takes_value)
if key not in seen:
seen.add(key)
table.append(key)
table.extend(PRE_ARGPARSE_INHERITED_FLAGS)
return table
_INHERITED_FLAGS_TABLE = _build_inherited_flag_table()
def _extract_inherited_flags(argv: Sequence[str]) -> list[str]:
"""Pull out flags that should carry over into a self-relaunched hermes."""
flags: list[str] = []
i = 0
while i < len(argv):
arg = argv[i]
if "=" in arg:
key = arg.split("=", 1)[0]
for flag, _ in _CRITICAL_FLAGS:
for flag, _ in _INHERITED_FLAGS_TABLE:
if key == flag:
flags.append(arg)
break
i += 1
continue
for flag, takes_value in _CRITICAL_FLAGS:
for flag, takes_value in _INHERITED_FLAGS_TABLE:
if arg == flag:
flags.append(arg)
if takes_value and i + 1 < len(argv) and not argv[i + 1].startswith("-"):
@@ -98,14 +108,15 @@ def resolve_hermes_bin() -> Optional[str]:
def build_relaunch_argv(
extra_args: Sequence[str],
*,
preserve_critical: bool = True,
preserve_inherited: bool = True,
original_argv: Optional[Sequence[str]] = None,
) -> list[str]:
"""Construct an argv list for replacing the current process with hermes.
Args:
extra_args: Arguments to append (e.g. ``["--resume", id]``).
preserve_critical: Whether to carry over UI / behaviour flags.
preserve_inherited: Whether to carry over UI / behaviour flags
tagged with ``inherit_on_relaunch`` in the parser.
original_argv: The original argv to scan for flags (defaults to
``sys.argv[1:]``).
"""
@@ -118,8 +129,8 @@ def build_relaunch_argv(
src = list(original_argv) if original_argv is not None else list(sys.argv[1:])
if preserve_critical:
argv.extend(_extract_critical_flags(src))
if preserve_inherited:
argv.extend(_extract_inherited_flags(src))
argv.extend(extra_args)
return argv
@@ -128,20 +139,20 @@ def build_relaunch_argv(
def relaunch(
extra_args: Sequence[str],
*,
preserve_critical: bool = True,
preserve_inherited: bool = True,
original_argv: Optional[Sequence[str]] = None,
) -> None:
"""Replace the current process with a fresh hermes invocation."""
new_argv = build_relaunch_argv(
extra_args, preserve_critical=preserve_critical, original_argv=original_argv
extra_args, preserve_inherited=preserve_inherited, original_argv=original_argv
)
os.execvp(new_argv[0], new_argv)
def relaunch_chat(
*,
preserve_critical: bool = True,
preserve_inherited: bool = True,
original_argv: Optional[Sequence[str]] = None,
) -> None:
"""Convenience wrapper: relaunch into ``hermes chat``."""
relaunch(["chat"], preserve_critical=preserve_critical, original_argv=original_argv)
relaunch(["chat"], preserve_inherited=preserve_inherited, original_argv=original_argv)