Files
hermes-agent/hermes_cli/subcommands/pause.py
Teknium 5db1b72b1f feat(cli): global emergency stop — hermes pause / hermes resume
Resumable ESTOP sentinel at $HERMES_HOME/ESTOP that halts NEW work only:

- agent/estop.py: sentinel engage/disengage/is_engaged (single stat, no
  caching), optional reason + timestamp stored as JSON, paused_reply()
  notice, check_paused() log-once-per-engagement helper. Corrupt/empty
  sentinel still pauses (fail safe); a `touch ~/.hermes/ESTOP` works.
- cron/scheduler.py: tick() skips dispatch while engaged (logged once per
  engagement, not per tick). Due jobs simply wait for the next tick after
  resume — in-flight runs are never touched.
- gateway/kanban_watchers.py: dispatcher skips auto-decompose and worker
  spawning while engaged; zombie reaping still runs and running workers
  finish naturally.
- gateway/run.py: new gateway turns (post-auth, non-internal) get a brief
  "Hermes is paused" reply instead of an agent run. Internal events
  (in-flight background completions) bypass the gate.
- hermes_cli/subcommands/pause.py: `hermes pause [--reason]` and
  `hermes resume`, wired into main() and _BUILTIN_SUBCOMMANDS.
- hermes_cli/status.py: `hermes status` shows a PAUSED banner (one stat).
- tests/test_estop.py: 20 tests — sentinel lifecycle, reason surfacing,
  log-once, cron skip + resume, kanban gate, gateway paused reply +
  internal bypass, CLI idempotence, builtin-set parity, status line.

Never kills in-flight work; resumable with no restart. Footprint ladder:
CLI command only, no new model tool, no new env vars.

Ported from: gastownhall/gastown estop.go (MIT); related prior art:
#26778 (/panic — kill/exit semantics, deliberately different: ours is a
resumable pause), #44617 (interrupt in-flight cron — out of scope here).
2026-08-07 08:58:14 -07:00

71 lines
2.5 KiB
Python

"""``hermes pause`` / ``hermes resume`` — the global emergency stop.
``hermes pause`` writes the ESTOP sentinel at ``$HERMES_HOME/ESTOP``, which
halts cron dispatch, kanban dispatch, and new gateway turns on their next
check. In-flight work is never killed. ``hermes resume`` removes the
sentinel and normal operation resumes on the next tick — no restart needed.
Ported from: gastownhall/gastown estop.go (MIT); related prior art:
#26778 (/panic — kill/exit semantics, different), #44617.
"""
from __future__ import annotations
import argparse
def cmd_pause(args: argparse.Namespace) -> int:
"""Engage the global emergency stop."""
from agent.estop import engage, get_state, is_engaged
reason = getattr(args, "reason", None)
already = is_engaged()
path = engage(reason=reason)
state = get_state() or {}
verb = "Still paused" if already else "Hermes paused"
detail = f" — reason: {state['reason']}" if state.get("reason") else ""
print(f"⏸️ {verb}{detail}")
print(f" sentinel: {path}")
print(
" Cron dispatch, kanban dispatch, and new gateway turns are on hold.\n"
" In-flight work keeps running. Run `hermes resume` to lift the pause."
)
return 0
def cmd_resume(args: argparse.Namespace) -> int:
"""Disengage the global emergency stop."""
from agent.estop import disengage, sentinel_path
if disengage():
print("▶️ Hermes resumed — dispatch picks up on the next tick.")
else:
print(f"Hermes is not paused (no sentinel at {sentinel_path()}).")
return 0
def build_pause_parser(subparsers) -> None:
"""Attach the ``pause`` and ``resume`` subcommands to ``subparsers``."""
pause_parser = subparsers.add_parser(
"pause",
help="Emergency stop: pause cron/kanban dispatch and new gateway turns",
description=(
"Engage the global emergency stop. Halts NEW work only — cron "
"dispatch, kanban dispatch, and new gateway turns — until "
"`hermes resume`. In-flight work is never killed."
),
)
pause_parser.add_argument(
"--reason",
default=None,
help="Optional reason stored in the sentinel and shown to users",
)
pause_parser.set_defaults(func=cmd_pause)
resume_parser = subparsers.add_parser(
"resume",
help="Lift the emergency stop set by `hermes pause`",
description="Remove the ESTOP sentinel; dispatch resumes on the next tick.",
)
resume_parser.set_defaults(func=cmd_resume)