mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-31 19:16:29 +00:00
refactor(sync): put every Skill Sync verb under hermes sync; drop HSP naming
Encapsulates the feature behind one command for launch, and adopts the
official product name.
One command:
- `propose` moves from `hermes skills propose` to `hermes sync propose`, so
the whole feature is one command to learn and one to document. Its handler
moves from cmd_skills to cmd_sync accordingly.
- The `hermes sync` parser now documents both halves plainly: personal sync
across your devices, and sharing with your organisation. Added an examples
epilog; rewrote the verb help in user language ("Include a skill in your
sync" rather than "Opt a skill into sync").
- Every user-facing string that pointed at `hermes skills propose` now points
at `hermes sync propose` (8 sites, including the agent-visible guidance
returned by skill_manage and the org provenance header).
This also clears the way for #39343, which adds its own top-level `sync` for
git-repo profile backup — that feature nests under `skills`, this one owns
`sync`.
Naming:
- HSP / "Hermes Sync Protocol" is gone from prose, docstrings, and comments.
The feature is "Skill Sync".
- Public identifiers renamed: HSPClient -> SyncClient, HSPError -> SyncError,
HSPConflict -> SyncConflict, hsp_address -> wire_address, HSP_VERSION ->
WIRE_VERSION.
- The WIRE names are deliberately NOT renamed: the `hsp_version` capability
field and the `x-hsp-object-type` response header are set by the deployed
gateway-gateway sync plane (verified in src/sync/syncRouter.ts), so
renaming them client-side would break sync against a live server. A comment
at the version constant records why they differ from the product name.
- The version-mismatch error is now actionable ("this server speaks sync
version X, but this Hermes speaks Y — update Hermes to sync with it")
instead of leaking the protocol acronym.
Also fixes a wiring gap found on the way: the gateway housekeeping tick
pulled personal skills but never org skills — the same defect already fixed
for the CLI. Org pull now runs there too, gated on real org membership.
Tests: the jargon guard now also fails on a bare "HSP". The two tests that
asserted the old cross-command structure are replaced by three asserting the
new one (propose IS under sync, propose is NOT under skills, sync usage
lists it). 2294 passed / 0 failed across all 51 suites that import the
changed modules, via scripts/run_tests.sh.
Verified by running the real CLI: `hermes sync --help` lists all eight verbs,
`hermes skills --help` no longer mentions propose, `hermes sync propose
--help` parses, and `hermes sync status` still reports live org state.
This commit is contained in:
parent
981feb6730
commit
4f990ec09e
10 changed files with 240 additions and 230 deletions
|
|
@ -4440,26 +4440,27 @@ def cmd_cron(args):
|
|||
|
||||
|
||||
def cmd_sync(args):
|
||||
"""HSP/1 personal skill sync management (status/pull/push/now/enable/disable)."""
|
||||
"""Skill Sync — personal sync across devices, plus sharing with your org."""
|
||||
import json as _json
|
||||
|
||||
sub = getattr(args, "sync_command", None)
|
||||
|
||||
if sub in {None, ""}:
|
||||
print(
|
||||
"usage: hermes sync <status|pull|push|now|enable|disable|device>\n"
|
||||
"usage: hermes sync "
|
||||
"<status|pull|push|now|enable|disable|device|propose>\n"
|
||||
"\n"
|
||||
" status Show sync gate, opt-in, and head state\n"
|
||||
" pull Pull the owner's HEAD, materialize opted-in skills\n"
|
||||
" push Push opted-in skills to the owner's HEAD\n"
|
||||
"Your skills, across your devices:\n"
|
||||
" status Show what is synced, and from where\n"
|
||||
" pull Pull your synced skills\n"
|
||||
" push Push your opted-in skills\n"
|
||||
" now Reconcile now: pull then push\n"
|
||||
" enable <skill> Opt a skill into sync\n"
|
||||
" disable <skill> Opt a skill out of sync\n"
|
||||
" device [--name N] Show or set this device's sync label\n"
|
||||
" enable <skill> Include a skill in your sync\n"
|
||||
" disable <skill> Exclude a skill from your sync\n"
|
||||
" device [--name N] Show or set this device's label\n"
|
||||
"\n"
|
||||
"These cover your PERSONAL skills, across your own devices.\n"
|
||||
"To share a skill with your organisation instead:\n"
|
||||
" hermes skills propose <skill>",
|
||||
"Shared with your team:\n"
|
||||
" propose <skill> Share a skill with your organisation",
|
||||
file=sys.stderr,
|
||||
)
|
||||
return 1
|
||||
|
|
@ -4485,6 +4486,28 @@ def cmd_sync(args):
|
|||
print(ssc.stable_device_id())
|
||||
return 0
|
||||
|
||||
if sub == "propose":
|
||||
from tools import skills_sync_client as ssc
|
||||
|
||||
name = args.name
|
||||
try:
|
||||
result = ssc.propose_skill(name, message=args.message)
|
||||
except ssc.SyncInertError as e:
|
||||
print(f"cannot share this skill: {e}", file=sys.stderr)
|
||||
return 1
|
||||
except ssc.SyncError as e:
|
||||
print(f"could not share '{name}': {e}", file=sys.stderr)
|
||||
return 1
|
||||
if result.get("proposal_pending"):
|
||||
print(
|
||||
f"Shared '{name}' with your organisation — an admin needs to "
|
||||
f"approve it (proposal #{result.get('proposal_id')}). It is "
|
||||
f"not live for the team until then."
|
||||
)
|
||||
else:
|
||||
print(f"Added '{name}' to your organisation's shared skills.")
|
||||
return 0
|
||||
|
||||
if sub in {"enable", "disable"}:
|
||||
from tools.skill_usage import set_sync, is_curation_eligible
|
||||
|
||||
|
|
@ -4519,7 +4542,7 @@ def cmd_sync(args):
|
|||
print(
|
||||
f" {len(modified)} with local edits not yet shared: "
|
||||
f"{', '.join(modified)}\n"
|
||||
f" Share them back with `hermes skills propose <skill>`. "
|
||||
f" Share them back with `hermes sync propose <skill>`. "
|
||||
f"Org updates will not overwrite them.",
|
||||
file=sys.stderr,
|
||||
)
|
||||
|
|
@ -4603,7 +4626,7 @@ def cmd_sync(args):
|
|||
else:
|
||||
print(f"Unknown sync subcommand: {sub}", file=sys.stderr)
|
||||
return 1
|
||||
except ssc.HSPError as e:
|
||||
except ssc.SyncError as e:
|
||||
print(f"sync failed: {e}", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
|
|
@ -13942,34 +13965,6 @@ def cmd_skills(args):
|
|||
from hermes_cli.skills_config import skills_command as skills_config_command
|
||||
|
||||
skills_config_command(args)
|
||||
elif getattr(args, "skills_action", None) == "propose":
|
||||
# M2 org-shared skills (hsp-1-contract.md §11.5): propose a local
|
||||
# skill to the org canonical set. 202 => pending review (NEVER shown
|
||||
# as live); direct merge for admins. Personal orgs have no org
|
||||
# workflow — say so plainly instead of a raw 403.
|
||||
from tools import skills_sync_client as ssc
|
||||
|
||||
name = args.name
|
||||
try:
|
||||
result = ssc.propose_skill(name, message=args.message)
|
||||
except ssc.SyncInertError as e:
|
||||
print(f"org sync unavailable: {e}", file=sys.stderr)
|
||||
return 1
|
||||
except ssc.HSPError as e:
|
||||
print(f"propose failed: {e}", file=sys.stderr)
|
||||
return 1
|
||||
if result.get("proposal_pending"):
|
||||
print(
|
||||
f"proposed '{name}' — pending admin review "
|
||||
f"(proposal #{result.get('proposal_id')}). Not live for the "
|
||||
f"org until approved."
|
||||
)
|
||||
else:
|
||||
print(
|
||||
f"merged '{name}' into the org set "
|
||||
f"(head {str(result.get('head', ''))[:19]}…)."
|
||||
)
|
||||
return 0
|
||||
else:
|
||||
from hermes_cli.skills_hub import skills_command
|
||||
|
||||
|
|
|
|||
|
|
@ -313,26 +313,4 @@ def build_skills_parser(subparsers, *, cmd_skills: Callable) -> None:
|
|||
help="Interactive skill configuration — enable/disable individual skills",
|
||||
)
|
||||
|
||||
# M2 org-shared skills (hsp-1-contract.md §11.5/§11.11): propose a local
|
||||
# skill's content to the org canonical set. MEMBER → 202 proposal
|
||||
# (pending admin review); ADMIN/OWNER → direct merge. Only meaningful for
|
||||
# multi-member orgs — personal orgs have no org workflow (the command
|
||||
# reports that instead of failing opaquely).
|
||||
skills_propose = skills_subparsers.add_parser(
|
||||
"propose",
|
||||
help="Propose a skill to your organisation's shared skill set",
|
||||
description=(
|
||||
"Snapshot the local skill and submit it to the org canonical set. "
|
||||
"An org admin's push merges directly; a member's push becomes a "
|
||||
"proposal reviewed in the org console. Personal orgs keep simple "
|
||||
"personal sync and have no proposal workflow."
|
||||
),
|
||||
)
|
||||
skills_propose.add_argument("name", help="Skill name to propose")
|
||||
skills_propose.add_argument(
|
||||
"-m",
|
||||
"--message",
|
||||
default=None,
|
||||
help="Optional proposal message (defaults to 'propose <name>')",
|
||||
)
|
||||
skills_parser.set_defaults(func=cmd_skills)
|
||||
|
|
|
|||
|
|
@ -1,21 +1,21 @@
|
|||
"""``hermes sync`` subcommand parser (personal skill sync).
|
||||
"""``hermes sync`` subcommand parser — Skill Sync.
|
||||
|
||||
Cloned from ``hermes_cli/subcommands/cron.py`` — same injected-handler shape
|
||||
(``func=cmd_sync``) so this module does not import ``main`` (cycle avoidance).
|
||||
|
||||
Commands:
|
||||
hermes sync status -- show gate/opt-in/head state
|
||||
hermes sync pull -- pull the owner's HEAD, materialize opted-in skills
|
||||
hermes sync push -- push opted-in skills to the owner's HEAD
|
||||
hermes sync now -- pull then push (full reconcile)
|
||||
hermes sync enable <skill> -- opt a skill into sync
|
||||
hermes sync disable <skill> -- opt a skill out of sync
|
||||
hermes sync device [--name] -- show or set this device's sync label
|
||||
Skill Sync covers two surfaces, both under this one command for launch:
|
||||
|
||||
This surface is PERSONAL sync only: it moves your own skills between your own
|
||||
devices via ``refs/user/<owner>/HEAD``. Sharing a skill with an organisation
|
||||
is a different operation with a different destination and an approval step —
|
||||
see ``hermes skills propose``.
|
||||
Personal — your own skills, across your own devices:
|
||||
hermes sync status show gate/opt-in/head state
|
||||
hermes sync pull pull and materialize opted-in skills
|
||||
hermes sync push push opted-in skills
|
||||
hermes sync now reconcile: pull then push
|
||||
hermes sync enable <skill> opt a skill into sync
|
||||
hermes sync disable <skill> opt a skill out of sync
|
||||
hermes sync device [--name] show or set this device's label
|
||||
|
||||
Organisation — skills shared with your team:
|
||||
hermes sync propose <skill> share a skill with your organisation
|
||||
|
||||
Sync is INERT unless the resolved Nous token carries the access-gate claim
|
||||
AND a sync base URL is configured. The commands report that state rather than
|
||||
|
|
@ -24,49 +24,48 @@ failing opaquely.
|
|||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
from typing import Callable
|
||||
|
||||
|
||||
def build_sync_parser(subparsers, *, cmd_sync: Callable) -> None:
|
||||
"""Attach the ``sync`` subcommand (and its sub-actions) to ``subparsers``."""
|
||||
import argparse
|
||||
|
||||
sync_parser = subparsers.add_parser(
|
||||
"sync",
|
||||
help="Personal skill sync across your devices",
|
||||
help="Skill Sync — sync your skills across devices and with your team",
|
||||
description=(
|
||||
"Sync agent-created and user-authored skills across your own "
|
||||
"devices."
|
||||
"Skill Sync keeps your skills with you. Personal sync moves your "
|
||||
"own skills between your devices; if you belong to an "
|
||||
"organisation, you also get its shared skills and can propose "
|
||||
"your own back to the team."
|
||||
),
|
||||
epilog=(
|
||||
"Sharing with your team:\n"
|
||||
" These commands cover your PERSONAL skills only. To share a "
|
||||
"skill with your\n"
|
||||
" organisation, use `hermes skills propose <skill>` instead — it "
|
||||
"submits the\n"
|
||||
" skill to your org's shared set (an admin approves it unless "
|
||||
"you are one).\n"
|
||||
" Approved org skills arrive automatically and are read-only "
|
||||
"locally.\n"
|
||||
"Examples:\n"
|
||||
" hermes sync status what is synced, and from where\n"
|
||||
" hermes sync enable my-skill include a skill in your sync\n"
|
||||
" hermes sync now pull, then push\n"
|
||||
" hermes sync propose my-skill share a skill with your team\n"
|
||||
),
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
)
|
||||
sync_sub = sync_parser.add_subparsers(dest="sync_command")
|
||||
|
||||
sync_sub.add_parser("status", help="Show sync gate, opt-in, and head state")
|
||||
sync_sub.add_parser("pull", help="Pull the owner's HEAD and materialize opted-in skills")
|
||||
sync_sub.add_parser("push", help="Push opted-in skills to the owner's HEAD")
|
||||
sync_sub.add_parser("status", help="Show what is synced, and from where")
|
||||
sync_sub.add_parser(
|
||||
"pull", help="Pull your synced skills (and your organisation's)"
|
||||
)
|
||||
sync_sub.add_parser("push", help="Push your opted-in skills")
|
||||
sync_sub.add_parser("now", help="Reconcile now: pull then push")
|
||||
|
||||
enable = sync_sub.add_parser("enable", help="Opt a skill into sync")
|
||||
enable = sync_sub.add_parser("enable", help="Include a skill in your sync")
|
||||
enable.add_argument("skill", help="Skill name (frontmatter name / directory name)")
|
||||
|
||||
disable = sync_sub.add_parser("disable", help="Opt a skill out of sync")
|
||||
disable = sync_sub.add_parser("disable", help="Exclude a skill from your sync")
|
||||
disable.add_argument("skill", help="Skill name (frontmatter name / directory name)")
|
||||
|
||||
device = sync_sub.add_parser(
|
||||
"device",
|
||||
help="Show or set this device's sync label (shown in the sync console)",
|
||||
help="Show or set this device's label (shown in the sync console)",
|
||||
)
|
||||
device.add_argument(
|
||||
"--name",
|
||||
|
|
@ -76,4 +75,25 @@ def build_sync_parser(subparsers, *, cmd_sync: Callable) -> None:
|
|||
"Omit to print the current label.",
|
||||
)
|
||||
|
||||
# Org-shared skills. A member's submission becomes a proposal an admin
|
||||
# reviews; an admin's merges straight into the shared set. Accounts that
|
||||
# aren't in a shared organisation are told so plainly.
|
||||
propose = sync_sub.add_parser(
|
||||
"propose",
|
||||
help="Share a skill with your organisation",
|
||||
description=(
|
||||
"Submit one of your skills to your organisation's shared set. If "
|
||||
"you are an admin it is added directly; otherwise it becomes a "
|
||||
"proposal for an admin to review. Accounts that aren't part of a "
|
||||
"shared organisation don't have this workflow."
|
||||
),
|
||||
)
|
||||
propose.add_argument("name", help="Skill name to share")
|
||||
propose.add_argument(
|
||||
"-m",
|
||||
"--message",
|
||||
default=None,
|
||||
help="Optional message describing the change",
|
||||
)
|
||||
|
||||
sync_parser.set_defaults(func=cmd_sync)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue