fix(kanban): spawn workers headless — TUI can never eat a worker run

An inherited HERMES_TUI=1 or a `display.interface: tui` config default sent
kanban workers into the Ink TUI, whose no-TTY bail-out exits 0 without doing
the task — every attempt ended in "protocol violation". Two layers:

- _default_spawn pins `--cli` (highest-precedence interface flag) and strips
  HERMES_TUI from the child env (covers older builds on PATH).
- _resolve_use_tui gates ambient TUI prefs (env/config) behind a real TTY;
  an explicit --tui still wins so the informative bail-out stays reachable.
This commit is contained in:
Brooklyn Nicholson 2026-07-09 09:02:04 -05:00
parent 5829fe1378
commit e87c495dc2
4 changed files with 108 additions and 5 deletions

View file

@ -2193,16 +2193,34 @@ def _resolve_use_tui(args) -> bool:
Precedence (highest first):
1. ``--cli`` flag always classic REPL
2. ``--tui`` flag / ``HERMES_TUI=1`` always TUI
3. ``display.interface`` config value ("cli" | "tui")
4. default classic REPL
2. ``--tui`` flag always TUI (explicit ask)
3. no TTY always classic (ambient prefs don't apply)
4. ``HERMES_TUI=1`` env TUI
5. ``display.interface`` config value ("cli" | "tui")
6. default classic REPL
Explicit flags always win over config so muscle memory and scripts keep
working regardless of the configured default.
The TTY gate (3) is load-bearing: ambient TUI preferences (env var or
config default) must never hijack a NON-interactive invocation. Kanban
workers, cron jobs, and pipelines run ``hermes chat -q`` with stdout
on a pipe; booting the Ink TUI there hits its no-TTY bail-out, which
prints a resume hint and exits 0 a kanban worker then dies with
"exited cleanly without calling kanban_complete — protocol violation"
on every attempt (found dogfooding the desktop kanban board). A user
who *explicitly* passes ``--tui`` still gets the informative bail-out.
"""
if getattr(args, "cli", False):
return False
if getattr(args, "tui", False) or os.environ.get("HERMES_TUI") == "1":
if getattr(args, "tui", False):
return True
try:
if not (sys.stdin.isatty() and sys.stdout.isatty()):
return False
except Exception:
return False
if os.environ.get("HERMES_TUI") == "1":
return True
try:
from hermes_cli.config import load_config