hermes-jev-curator
Typed, per-pair skill-relationship judgments for Hermes Agent's background skill curator, served by TypeSafe's Jev decision model.
Experimental release (0.1.0). The plugin loads and runs against stock Hermes, passes
hermes plugins doctor/validate, and has been exercised against the live TypeSafe endpoint with whole, chunked, and locally unavailable pairs. The bundled synthetic corpus is still a contract self-check, not a model-quality benchmark. Default mode is read-onlyobserve;run --applystill refuses unlessmode: applyis set explicitly.
What it does
Hermes' background curator reviews agent-created skills on an idle timer: it marks stale skills,
archives idle ones, and — with the LLM pass enabled — rewrites overlapping skills into umbrellas
through the ledgered skill_manage tool. That pass decides in prose.
This plugin adds a typed layer in front of it:
- Deterministic candidates. A read-only inventory of curator-managed skills plus lexical top-k neighbor pairs. No model call; unchanged inputs produce identical pairs, ids, and order.
- Typed judgments. Each candidate pair uses one bounded request when both packages fit, or
deterministic per-direction chunk requests otherwise. Every request answers a versioned
question contract: an 8-way
relationchoice (duplicate,a_subset_of_b,b_subset_of_a,same_class,complementary,conflict,unrelated,insufficient_evidence) plus the applicablecoverage, preservation, andconflictprobability questions. - Direct-edge graph. Judgments become typed edges between skill names, bound to both
content digests. Merge plans are star-shaped — every absorbed member needs its own direct
containment/duplicate judgment against the canonical; A
B plus BC never merges C into A. - Core keeps the pen. The plugin writes no skill text. Its
applypath only archives sources that a validated plan says the canonical already preserves, through core's ledgeredskill_manage, after a pre-apply snapshot. Umbrella prose stays core's job.
Hermes Curator integration
This plugin complements stock Hermes Curator; it does not replace core's idle scheduler or
mutation machinery. Core still owns the managed-skill population, consolidation agent, snapshots,
skill_manage, archive ledger, and rollback. The plugin adds an event-driven refresh: every
successful create/install/edit/archive/restore/stale lifecycle mutation resets a profile-local
60-second debounce. After one quiet minute it runs run(apply=False) automatically when
mode is not off and allow_content_egress: true. Repeated mutations collapse into one run;
profiles remain isolated; a busy profile claim retries after another minute; plugin unload cancels
pending timers. Mere skill loads do not schedule work.
Compatibility note: the one-minute debounce is a pragmatic workaround over Hermes' current
per-event on_skill_lifecycle seam, not a replacement for core. The cleaner long-term API would be
one host hook after Hermes has finished all lifecycle actions in a burst. We intend to propose that
small upstream feature; until then, the debounce provides autonomous refresh without a
core patch.
The timer preserves the originating profile's $HERMES_HOME and credential scope. Automatic runs
never apply or archive skills, even in apply mode: observe refreshes evidence/reports, while
guard and apply also replace guard_plans.json with fresh hash-bound plans.
Core's LLM consolidation pass is off by default even though Curator itself is enabled by default.
This is the core curator.consolidate setting, separate from the plugin settings:
curator:
enabled: true
consolidate: true # opt into automatic LLM umbrella passes; false by default
For a one-off pass, hermes curator run --consolidate enables consolidation for that invocation.
When core starts that fork, it uses platform="curator" and the skills toolset. The enabled
plugin then automatically:
- adds the read-only
jev_skill_relationstool to that toolset; - injects a Curator-only prompt requiring typed evidence before merges or absorptions;
- records skill lifecycle events and schedules the debounced dry refresh; and
- in
guard/applymode, checks backgroundskill_managemutations with the localpre_tool_callhook.
The plugin's commands are the immediate evidence/control path. After the quiet-minute refresh,
core consolidation can use the fresh local plans directly; run hermes jev-curator run manually
only when you do not want to wait for the debounce:
hermes jev-curator run
hermes curator run --dry-run --consolidate
hermes curator run --consolidate
The guard performs no network call. It permits only an exact authorized archive into an existing
canonical; content patches, overwrites, removals, stale hashes, and unplanned archives are blocked.
hermes jev-curator run --apply is different: it bypasses the LLM consolidation fork and directly
executes the plugin's terminal-only, snapshot-first archive path for already-preserved sources.
Scope boundary: this integration governs the LLM consolidation fork's skill_manage calls.
It does not change Hermes Curator's deterministic age-based stale/archive transitions.
Status (0.1.0)
| component | state |
|---|---|
| inventory, candidate generation, question contract, transport | implemented, unit-tested |
plugin manifest + register(ctx) (tool / prompt / lifecycle debounce + guard hooks / commands) |
implemented; doctor + validate pass; real-load verified |
graph, plans, state (audit / cache / lock / reports), run --apply |
implemented and unit-tested; no production-library apply has been run |
| live Jev endpoint | exercised end-to-end with whole and multi-request chunked evidence; no model-quality claim |
frozen offline benchmark (benchmarks/) |
21 synthetic relation/adversarial cases; self-check only, not a live-model quality claim |
Stock-core integration
The plugin imports stock Hermes modules read-only. Core is not modified, patched, or monkeypatched; nothing in this tree requires a core change.
| import | used for |
|---|---|
tools.skill_usage |
curated_report() / usage_report() rows, provenance(), is_curation_eligible() |
agent.skill_utils |
iter_skill_index_files(), parse_frontmatter() |
hermes_constants |
active-profile home plus context-local binding for delayed runs |
agent.secret_scope |
preserve the originating profile's credential scope across the timer thread |
cron.jobs |
referenced_skill_names() — cron-referenced skills are flagged protected |
hermes_cli.runtime_provider |
credential resolution through Hermes' provider pool |
hermes_cli.config |
get_env_value_prefer_dotenv() credential fallback |
agent.redact |
mandatory redaction before egress; best-effort redaction in audit/report text |
agent.curator_backup |
pre-apply skills snapshot (snapshot_skills) |
tools.skill_provenance |
marks apply-mode writes as background-review origin |
tools.write_approval |
refuses apply when staged replay would lose background-review provenance |
Verified against Hermes Agent v0.21.3 using source checkout 522e121e.
Modes
Set under plugins.entries.jev-curator.settings.mode; read on every call, so an edit applies
without a restart. Any value that is not one of the four below falls back to observe — never
to a write mode.
| mode | behavior today |
|---|---|
off |
no Jev requests and no automatic lifecycle run (inventory, candidates, and the deterministic baseline remain available manually) |
observe (default) |
judgments + plans + reports; consented lifecycle mutations trigger the 60-second dry refresh; no skill mutation of any kind |
guard |
the dry refresh installs current hash-bound relation plans and blocks background destructive skill_manage calls that are not an exact authorized absorption; foreground calls remain untouched |
apply |
automatic refreshes remain dry; only explicit terminal hermes jev-curator run --apply archives sources of complete direct-edge, hash-bound plans via ledgered skill_manage, after a snapshot and digest re-check; requires core's _archived confirmation and verifies each source disappeared while the canonical stayed unchanged; refuses while skills.write_approval is enabled |
Install
Install from GitHub:
hermes plugins install anpicasso/hermes-jev-curator/plugin
hermes plugins enable jev-curator
hermes gateway restart
For local development, copy plugin/ to $HERMES_HOME/plugins/jev-curator/ and add
jev-curator to plugins.enabled. Plugins are profile-scoped: repeat the install for every
$HERMES_HOME that should use it.
Configuration
Settings live in the host config, never in a file inside the plugin directory (that directory is a replaceable installed snapshot):
plugins:
enabled: [jev-curator]
entries:
jev-curator:
settings:
mode: observe # "off" | observe | guard | apply (quote off for YAML 1.1 parsers)
provider: typesafe # typesafe | openrouter | custom
allow_content_egress: false # set true only after accepting the disclosure below
# base_url: https://jev.example/v1/systemone # required for provider: custom
# key_env: MY_JEV_KEY # custom endpoints only
| key | default | range | meaning |
|---|---|---|---|
mode |
observe |
off/observe/guard/apply | see Modes |
provider |
typesafe |
typesafe/openrouter/custom | decision-endpoint preset |
base_url |
— | https, port 443 | complete Jev decision endpoint; custom only |
jev_model |
provider default (jev-latest; OpenRouter: ~typesafe/jev-latest) |
— | backend model id; renamed because Hermes reserves the plugin setting root model |
key_env |
— | environment-variable name | custom endpoints only; anonymous when unset |
allow_content_egress |
false |
boolean | explicit consent required before skill text can be sent to any endpoint |
timeout_seconds |
25 |
1–120 | one overall deadline per request, retries included |
max_requests |
50 |
1–500 | per-operation request budget; chunk requests count individually |
max_pairs |
100 |
1–2000 | per-scan candidate-pair budget |
top_k |
5 |
1–20 | lexical neighbors kept per skill |
Long pairs
Jev 1.13 allows 64,000 tokens per request, with a stricter 32,000-token limit for state plus the longest question. This plugin deliberately stays below both: pairs whose redacted package text fits the independent 160,000-byte serialized-state ceiling and 24,000-token estimate use one whole-pair request. The byte check counts JSON escaping and catches low-token whitespace; the token check catches dense Markdown/code. The 160,000 figure is bytes, not Jev tokens.
Larger pairs are planned per containment direction. The candidate source is split at package-file markers, then Markdown headings, then fixed-overlap hard boundaries, while the proposed container stays whole in every request. Each request records its side, chunk index/count, file scope, and containment question. A direction is unavailable when its container cannot fit whole.
Aggregation is fail-closed, not a vote: every planned chunk must answer; preservation and coverage
use min, conflict uses max, and any conflict label wins. Certified directions determine
duplicate/subset labels; disagreement becomes insufficient_evidence; an unmeasured direction is
always 0.0. If neither direction is plannable, the pair becomes local
insufficient_evidence without a network call. The planner first compares a conservative
byte/token lower bound with the remaining per-operation request budget and refuses immediately
when it cannot fit. Otherwise it materializes chunks and checks the exact count before sending;
scans persist any refusal in scan["skipped"], while explicit pair review returns a bounded error.
No partial calls are made.
Endpoint presets:
| provider | endpoint | credential |
|---|---|---|
typesafe (default) |
https://api.typesafe.ai/v1/systemone |
TYPESAFE_API_KEY in the active profile's .env / environment; a matching installed provider credential is also accepted |
openrouter |
https://openrouter.ai/api/alpha/decisions |
hermes auth add openrouter, or OPENROUTER_API_KEY |
custom |
your base_url |
only the configured key_env; anonymous when unset — it never inherits TypeSafe or OpenRouter credentials |
Credentials resolve through Hermes' own chain (matching-host provider credential → active-profile
.env → environment). A credential resolved for another host is never forwarded to the Jev endpoint.
The optional jev-approvals companion
registers the typesafe-jev Hermes credential provider; without it, TYPESAFE_API_KEY in the
active profile's .env or environment is sufficient.
State and audit live under the active profile's $HERMES_HOME/jev-curator/ (bound the audit log
with JEV_CURATOR_AUDIT_MAX_BYTES; 0 disables it). Files are mode 0600;
audit rows and report text are redacted and bounded; the plugin refuses to write core-owned
files (skills/.usage.json, skills/.curator_state).
Commands
hermes jev-curator status # mode, inventory size, last run
hermes jev-curator scan # inventory + candidate pairs (+ Jev judgments)
hermes jev-curator review NAME # one skill's candidate pairs
hermes jev-curator graph # edge summary with refusal counts
hermes jev-curator plan # merge plans and blockers
hermes jev-curator run # full pipeline, dry; writes a report
hermes jev-curator run --apply # executes validated plans; needs mode=apply, terminal only
hermes jev-curator doctor # route / inventory / mutation-default checks
Every subcommand takes --json for the bounded machine-readable payload. The /jev-curator
slash command exposes the same subcommands but always runs dry: --apply is refused from chat.
Execution it rides (core's own):
hermes curator status # scheduler state, last run, skill stats
hermes curator run --consolidate # the LLM umbrella pass this plugin informs
hermes curator ledger # per-mutation audit entries
hermes curator rollback [entry-id] # restore one mutation (or the whole tree)
Verify the install:
hermes plugins doctor plugin --ci # runtime discovery, manifest parse, import, register
hermes plugins validate plugin # manifest-vs-registration diff, security scan
python3 -m pytest plugin/tests -q # unit suite (determinism, graph gates, state, commands,
# transport contract, synthetic merge corpus)
Network egress and data handling
The only network egress is the decision request itself. Manual and lifecycle-debounced requests are
POSTed to the configured endpoint only when mode is not off and
allow_content_egress: true. The default is no egress and no automatic run.
- What leaves the machine after consent: one bounded state containing redacted skill names and either both complete packages or one complete package plus one labeled chunk, plus the question contract and configured model id. No package digest is sent; package text is never truncated.
- Where it goes: the configured endpoint —
https://api.typesafe.ai/v1/systemoneby default,https://openrouter.ai/api/alpha/decisionsfor OpenRouter, or yourcustomURL. A third party therefore sees your skill content unless you pointcustomat your own host. - Redaction uses Hermes' own redactor and the request fails closed if that redactor is unavailable. Redaction is still a hygiene measure, not a confidentiality control.
- Transport boundary: HTTPS on port 443 only; URL credentials, query strings, and fragments are rejected; cross-origin redirects are refused; requests and responses are capped at 2 MB; retries (429/529/5xx) are bounded and share one overall deadline.
What it never does
- The tool never mutates.
jev_skill_relationsreturns judgments only. - No direct skill writes. No code path writes skill files; mutations go through core's
skill_manageso the ledger records them andhermes curator rollbackcan restore them. - No deletion. The apply path archives sources (
deletewithabsorbed_into), which core implements as a recoverable move toskills/.archive/(hermes curator restore <name>); a pre-apply snapshot is taken first, and a missing snapshot refuses the run. A partial failure returns the snapshot plus exacthermes curator restore <name>recovery commands. - No chat-driven mutation.
/jev-curator run --applyis refused; apply needs a terminal,mode: apply, and validated plans whose digests still match. - No timer-driven mutation. Lifecycle debounce always calls
run(apply=False); it may refresh evidence and local guard authorization, never archive or rewrite a skill. - No stale application. If any affected skill package changed after judgment, apply refuses and asks for a rescan. After every archive it re-reads only that source and canonical, requires the source to be gone, and requires the canonical digest to remain unchanged.
Restart
Python plugins load at discovery; there is no hot reload. After installing, enabling, or
changing plugin code, restart the gateway (hermes gateway restart; named profiles:
hermes --profile <name> gateway restart). Settings are re-read on every call, so config edits
do not need a restart.
Uninstall
hermes plugins disable jev-curator # stop loading it, keep the files
hermes plugins remove jev-curator # remove the installed snapshot
Then delete $HERMES_HOME/jev-curator/ (audit, cache, state, reports) if you want it gone.
Nothing else to undo: the plugin does not modify skills outside run --apply, config, or
credentials, and no core file was changed.
Detailed documentation
Requirements
- Hermes Agent v0.21.2+ (verified against v0.21.3 source checkout
522e121e) - Python 3.10+; standard library only — no third-party Python dependencies
- A Jev credential only if you use TypeSafe or OpenRouter
Licence
MIT.