diff --git a/gateway/slash_commands.py b/gateway/slash_commands.py index 92db5b42f0c..bfcef143f44 100644 --- a/gateway/slash_commands.py +++ b/gateway/slash_commands.py @@ -2214,7 +2214,9 @@ class GatewaySlashCommandsMixin: stranded). ``diff`` output is truncated for chat bubbles — the full diff lives in - the CLI (``/skills diff ``) and the pending JSON file. + the pending JSON file under ``~/.hermes/pending/skills/``. (Note this is + the write-approval ``diff ``; the CLI also has an unrelated + ``hermes skills diff `` that diffs a bundled skill vs stock.) """ from gateway.run import _hermes_home from hermes_cli.write_approval_commands import handle_pending_subcommand @@ -2252,12 +2254,14 @@ class GatewaySlashCommandsMixin: "(Search/install are CLI-only.)") # Chat bubbles can't hold a full skill diff — truncate and point at - # the real review surfaces. + # the real review surface. (Note: `hermes skills diff ` is a + # *different* command — it diffs a bundled skill against its stock + # version — so we point at the pending JSON file, not that command.) if args and args[0].lower() == "diff" and len(out) > 3000: pending_id = args[1] if len(args) > 1 else "" out = (out[:3000] - + f"\n… (truncated — full diff: `/skills diff {pending_id}` " - f"on the CLI, or ~/.hermes/pending/skills/{pending_id}.json)") + + "\n… (truncated — full diff in " + f"~/.hermes/pending/skills/{pending_id}.json)") return out async def _handle_fast_command(self, event: MessageEvent) -> str: diff --git a/hermes_cli/main.py b/hermes_cli/main.py index 376de5bd543..c69cd60b42a 100644 --- a/hermes_cli/main.py +++ b/hermes_cli/main.py @@ -6073,6 +6073,10 @@ def _update_via_zip(args): ) if result.get("user_modified"): print(f" ~ {len(result['user_modified'])} user-modified (kept)") + print( + " → see them: hermes skills list-modified " + "(diff/reset to resume updates)" + ) if result.get("cleaned"): print(f" − {len(result['cleaned'])} removed from manifest") if not result["copied"] and not result.get("updated"): diff --git a/tests/hermes_cli/test_update_modified_notice.py b/tests/hermes_cli/test_update_modified_notice.py new file mode 100644 index 00000000000..37e41c73be4 --- /dev/null +++ b/tests/hermes_cli/test_update_modified_notice.py @@ -0,0 +1,50 @@ +"""Guard: every `hermes update` path that reports user-modified skills must +also tell the user how to find them. + +`hermes update` keeps (does not overwrite) bundled skills the user edited and +prints a ``~ N user-modified (kept)`` count. There are two independent update +code paths in ``hermes_cli/main.py`` that print this notice (the git-pull path +in ``_cmd_update_impl`` and the unpack/install path). Both must point the user +at ``hermes skills list-modified`` so the count is actionable — otherwise, +depending on which path a user hits, they may never learn the discovery command +exists. + +This is an *invariant* test (the two sibling notices must agree), not a literal +snapshot: it asserts the relationship "count line ⇒ discovery hint", so it +keeps holding if the wording is reworded, as long as both sites stay in sync. +""" + +import re +from pathlib import Path + +import hermes_cli.main as main_mod + + +_COUNT_RE = re.compile(r"user-modified \(kept\)") +_HINT_RE = re.compile(r"hermes skills list-modified") + + +def _source_lines() -> list[str]: + return Path(main_mod.__file__).read_text(encoding="utf-8").splitlines() + + +def test_every_user_modified_notice_points_at_list_modified(): + lines = _source_lines() + count_sites = [i for i, ln in enumerate(lines) if _COUNT_RE.search(ln)] + + # There are at least two such notices today; the bug was that only one of + # them carried the discovery hint. Assert each is followed (within a small + # window — the count print + the hint print) by the list-modified pointer. + assert len(count_sites) >= 2, ( + "expected at least two 'user-modified (kept)' notices in main.py; " + f"found {len(count_sites)}" + ) + + for idx in count_sites: + window = "\n".join(lines[idx : idx + 5]) + assert _HINT_RE.search(window), ( + "a 'user-modified (kept)' notice near line " + f"{idx + 1} of main.py does not point users at " + "`hermes skills list-modified` within the following lines — the " + "two update paths have drifted apart again:\n" + window + )