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:
Ben Barclay 2026-07-29 07:47:06 +10:00
parent 981feb6730
commit 4f990ec09e
10 changed files with 240 additions and 230 deletions

View file

@ -639,7 +639,7 @@ def _maybe_auto_propose_org_edit(name: str, skill_path: Path) -> Optional[str]:
return (
f"This skill is shared by your organisation. Your edit is "
f"saved locally and will not be overwritten by org updates. "
f"Run `hermes skills propose {name}` to share it back."
f"Run `hermes sync propose {name}` to share it back."
)
result = ssc.propose_skill(name)
if result.get("proposal_pending"):
@ -652,7 +652,7 @@ def _maybe_auto_propose_org_edit(name: str, skill_path: Path) -> Optional[str]:
logger.debug("auto-propose skipped for %s: %s", name, e)
return (
f"Edit saved locally. Could not submit it to your organisation "
f"right now — run `hermes skills propose {name}` to retry."
f"right now — run `hermes sync propose {name}` to retry."
)
@ -668,7 +668,7 @@ def _org_mirror_write_guard(name: str, skill_path: Path, action: str) -> Optiona
Now an edit lands in the mirror and is protected from being overwritten by
the next org pull (see the baseline sidecar in skills_sync_client). It
reaches the organisation when the user runs `hermes skills propose`, or
reaches the organisation when the user runs `hermes sync propose`, or
immediately if `sync.org_auto_propose` is on.
Deletion is still refused: the mirror is a materialized view of the org
@ -688,7 +688,7 @@ def _org_mirror_write_guard(name: str, skill_path: Path, action: str) -> Optiona
"organisation, so a local delete would just come back on "
"the next sync. Ask an org admin to remove it for "
"everyone. (Editing it IS allowed — your changes are kept "
"and can be proposed back with `hermes skills propose "
"and can be proposed back with `hermes sync propose "
f"{name}`.)"
),
}
@ -1438,7 +1438,7 @@ def apply_skill_pending(payload: Dict[str, Any]) -> str:
_skill_gate_bypass.reset(token)
# Debounce state for the HSP sync push hook. A burst of skill_manage writes
# Debounce state for the sync push hook. A burst of skill_manage writes
# (e.g. create + several write_file calls) collapses into a single push after
# a short quiet window, on a daemon timer so the agent write never blocks.
_sync_push_timer = None
@ -1447,7 +1447,7 @@ _SYNC_PUSH_DEBOUNCE_S = 5.0
def _maybe_debounced_sync_push(skill_name: str) -> None:
"""Schedule a debounced best-effort HSP push after a skill write.
"""Schedule a debounced best-effort sync push after a skill write.
Cheap fast-path: if the skill isn't opted into sync, do nothing (no auth,
no network). Otherwise (re)arm a daemon timer; the actual push runs through
@ -1586,7 +1586,7 @@ def skill_manage(
except Exception:
pass
# HSP sync push hook (debounced, best-effort). Fires only AFTER the
# Sync push hook (debounced, best-effort). Fires only AFTER the
# write gate passed (staged/unapproved writes never reach here -- the
# gate returns early above), so we never push un-reviewed content.
# Inert unless the DEV-PHASE gate is open (tool_gateway_admin on the