mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-06-17 09:41:58 +00:00
feat(skills): blank-slate skills — install --no-skills + opt-out/opt-in (#36228)
* feat(install): --no-skills flag for blank-slate default profile Add an install-time --no-skills flag so the default ~/.hermes profile can be created with zero bundled skills, matching what `hermes profile create --no-skills` already does for named profiles. The flag writes $HERMES_HOME/.no-bundled-skills and skips the install-time seed. sync_skills() now honors that marker with an early return (skipped_opt_out=True), so neither the installer, a later `hermes update`, nor a direct sync re-injects bundled skills into a profile that opted out. Previously the marker was only checked by seed_profile_skills() (named profiles); the default profile had no opt-out and `hermes update` would re-seed it every time. Tests: TestNoBundledSkillsOptOut covers marker-present (no-op) and marker-absent (normal seed) paths. * feat(skills): hermes skills opt-out / opt-in for existing profiles Adds an interactive counterpart to the install-time --no-skills flag so an already-installed profile (default or named) can toggle the .no-bundled-skills marker without reinstalling. - `hermes skills opt-out` writes the marker (stop future seeding). Safe by default: nothing on disk is touched. - `hermes skills opt-out --remove` ALSO deletes already-present bundled skills, but ONLY ones that are manifest-tracked AND byte-identical to their origin hash. User-edited bundled skills, hub-installed skills, and hand-written skills are never removed. Previews + confirms before deleting (--yes to skip). - `hermes skills opt-in [--sync]` removes the marker and optionally re-seeds immediately. Core logic lives in tools/skills_sync.py (set_bundled_skills_opt_out, is_bundled_skills_opt_out, remove_pristine_bundled_skills) reusing the existing manifest origin-hash machinery for the safety check. Tests: TestOptOutToggleAndRemove covers marker toggle idempotency and proves user-modified + non-bundled skills survive --remove. * docs: blank-slate skills — install --no-skills + opt-out/opt-in - features/skills.md: new 'Starting with a blank slate' section covering the install flag, profile-create flag, and runtime opt-out/opt-in, with a safe-by-default note. - reference/cli-commands.md: document the new skills opt-out / opt-in subcommands + examples. - reference/profile-commands.md: fix the marker filename (was .no-skills, actually .no-bundled-skills) and cross-link the runtime commands. Validated with a full docusaurus build (exit 0); the three edited pages compile clean with no new warnings.
This commit is contained in:
parent
70e1571d89
commit
2ed96372ad
8 changed files with 475 additions and 9 deletions
|
|
@ -13267,6 +13267,43 @@ Examples:
|
|||
help="Skip confirmation prompt when using --restore",
|
||||
)
|
||||
|
||||
skills_opt_out = skills_subparsers.add_parser(
|
||||
"opt-out",
|
||||
help="Stop bundled skills from being seeded into this profile",
|
||||
description=(
|
||||
"Write the .no-bundled-skills marker so the installer, "
|
||||
"`hermes update`, and any direct sync stop seeding bundled skills "
|
||||
"into the active profile. By default nothing already on disk is "
|
||||
"touched. Pass --remove to ALSO delete bundled skills that are "
|
||||
"unmodified (user-edited and hub/local skills are never removed)."
|
||||
),
|
||||
)
|
||||
skills_opt_out.add_argument(
|
||||
"--remove",
|
||||
action="store_true",
|
||||
help="Also delete already-present unmodified bundled skills",
|
||||
)
|
||||
skills_opt_out.add_argument(
|
||||
"--yes",
|
||||
"-y",
|
||||
action="store_true",
|
||||
help="Skip confirmation prompt when using --remove",
|
||||
)
|
||||
|
||||
skills_opt_in = skills_subparsers.add_parser(
|
||||
"opt-in",
|
||||
help="Re-enable bundled-skill seeding (undo opt-out)",
|
||||
description=(
|
||||
"Remove the .no-bundled-skills marker so bundled skills are seeded "
|
||||
"again on the next `hermes update`. Pass --sync to re-seed now."
|
||||
),
|
||||
)
|
||||
skills_opt_in.add_argument(
|
||||
"--sync",
|
||||
action="store_true",
|
||||
help="Re-seed bundled skills immediately instead of waiting for update",
|
||||
)
|
||||
|
||||
skills_repair_official = skills_subparsers.add_parser(
|
||||
"repair-official",
|
||||
help="Backfill or restore official optional skills from repo source",
|
||||
|
|
|
|||
|
|
@ -1072,6 +1072,107 @@ def do_reset(name: str, restore: bool = False,
|
|||
c.print("[dim]Use /reset to start a new session now, or --now to apply immediately (invalidates prompt cache).[/]\n")
|
||||
|
||||
|
||||
def do_opt_out(remove: bool = False,
|
||||
console: Optional[Console] = None,
|
||||
skip_confirm: bool = False,
|
||||
invalidate_cache: bool = True) -> None:
|
||||
"""Opt the active profile out of bundled-skill seeding.
|
||||
|
||||
Always writes the .no-bundled-skills marker (stop future seeding). With
|
||||
``remove``, also deletes already-present bundled skills that are pristine
|
||||
(manifest-tracked AND unmodified); user-edited and non-bundled skills are
|
||||
never touched.
|
||||
"""
|
||||
from tools.skills_sync import (
|
||||
set_bundled_skills_opt_out,
|
||||
remove_pristine_bundled_skills,
|
||||
)
|
||||
|
||||
c = console or _console
|
||||
|
||||
# Write the marker first (the always-safe part).
|
||||
res = set_bundled_skills_opt_out(True)
|
||||
if not res["ok"]:
|
||||
c.print(f"[bold red]Error:[/] {res['message']}\n")
|
||||
return
|
||||
c.print(f"[bold green]{res['message']}[/]")
|
||||
c.print(f"[dim]Marker: {res['marker']}[/]")
|
||||
|
||||
if not remove:
|
||||
c.print("[dim]Existing skills on disk were left in place. "
|
||||
"Re-run with --remove to also delete unmodified bundled skills.[/]\n")
|
||||
return
|
||||
|
||||
# Destructive step: preview, confirm, then delete.
|
||||
preview = remove_pristine_bundled_skills(dry_run=True)
|
||||
candidates = preview["removed"]
|
||||
kept = preview["skipped"]
|
||||
if not candidates:
|
||||
c.print("[dim]No pristine bundled skills to remove "
|
||||
"(nothing tracked, or all are user-modified/local).[/]\n")
|
||||
return
|
||||
|
||||
c.print(f"\n[bold]Will remove {len(candidates)} unmodified bundled skill(s):[/]")
|
||||
c.print(f"[dim]{', '.join(candidates)}[/]")
|
||||
if kept:
|
||||
c.print(f"[dim]Keeping {len(kept)} (user-modified or non-bundled).[/]")
|
||||
|
||||
if not skip_confirm:
|
||||
c.print("[dim]This deletes the on-disk copies. User-edited and "
|
||||
"hub/local skills are NOT touched.[/]")
|
||||
try:
|
||||
answer = input("Confirm [y/N]: ").strip().lower()
|
||||
except (EOFError, KeyboardInterrupt):
|
||||
answer = "n"
|
||||
if answer not in {"y", "yes"}:
|
||||
c.print("[dim]Marker kept; no skills deleted.[/]\n")
|
||||
return
|
||||
|
||||
result = remove_pristine_bundled_skills(dry_run=False)
|
||||
c.print(f"[bold green]{result['message']}[/]")
|
||||
if result["removed"]:
|
||||
c.print(f"[dim]Removed: {', '.join(result['removed'])}[/]")
|
||||
c.print()
|
||||
|
||||
if invalidate_cache:
|
||||
try:
|
||||
from agent.prompt_builder import clear_skills_system_prompt_cache
|
||||
clear_skills_system_prompt_cache(clear_snapshot=True)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def do_opt_in(sync: bool = False,
|
||||
console: Optional[Console] = None,
|
||||
invalidate_cache: bool = True) -> None:
|
||||
"""Remove the opt-out marker so bundled-skill seeding resumes.
|
||||
|
||||
With ``sync``, immediately re-seed bundled skills instead of waiting for
|
||||
the next ``hermes update``.
|
||||
"""
|
||||
from tools.skills_sync import set_bundled_skills_opt_out, sync_skills
|
||||
|
||||
c = console or _console
|
||||
|
||||
res = set_bundled_skills_opt_out(False)
|
||||
if not res["ok"]:
|
||||
c.print(f"[bold red]Error:[/] {res['message']}\n")
|
||||
return
|
||||
c.print(f"[bold green]{res['message']}[/]")
|
||||
|
||||
if sync:
|
||||
synced = sync_skills(quiet=True)
|
||||
copied = len(synced.get("copied", []))
|
||||
c.print(f"[dim]Re-seeded {copied} bundled skill(s).[/]")
|
||||
if invalidate_cache:
|
||||
try:
|
||||
from agent.prompt_builder import clear_skills_system_prompt_cache
|
||||
clear_skills_system_prompt_cache(clear_snapshot=True)
|
||||
except Exception:
|
||||
pass
|
||||
c.print()
|
||||
|
||||
|
||||
def do_repair_official(name: str, restore: bool = False,
|
||||
console: Optional[Console] = None,
|
||||
skip_confirm: bool = False,
|
||||
|
|
@ -1446,6 +1547,11 @@ def skills_command(args) -> None:
|
|||
elif action == "reset":
|
||||
do_reset(args.name, restore=getattr(args, "restore", False),
|
||||
skip_confirm=getattr(args, "yes", False))
|
||||
elif action == "opt-out":
|
||||
do_opt_out(remove=getattr(args, "remove", False),
|
||||
skip_confirm=getattr(args, "yes", False))
|
||||
elif action == "opt-in":
|
||||
do_opt_in(sync=getattr(args, "sync", False))
|
||||
elif action == "repair-official":
|
||||
do_repair_official(args.name, restore=getattr(args, "restore", False),
|
||||
skip_confirm=getattr(args, "yes", False))
|
||||
|
|
@ -1471,7 +1577,7 @@ def skills_command(args) -> None:
|
|||
return
|
||||
do_tap(tap_action, repo=repo)
|
||||
else:
|
||||
_console.print("Usage: hermes skills [browse|search|install|inspect|list|check|update|audit|uninstall|reset|publish|snapshot|tap]\n")
|
||||
_console.print("Usage: hermes skills [browse|search|install|inspect|list|check|update|audit|uninstall|reset|opt-out|opt-in|publish|snapshot|tap]\n")
|
||||
_console.print("Run 'hermes skills <command> --help' for details.\n")
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue