nix-hermes-agent/docs/SKILLS-DECLARATIVE-PRD.md
2026-03-18 02:30:20 +00:00

19 KiB

Declarative Skills for nix-hermes-agent

Status: Draft Owner: 0xrsydn / Ciphercat Branch: feat/skills

TL;DR

nix-hermes-agent already makes Hermes config, documents, auth seeding, and service wiring declarative. The next maturity step is to make skills first-class declarative state too.

This document proposes a practical, upstream-aware design that:

  1. preserves Hermes' filesystem-based skill model,
  2. respects upstream bundled/optional/hub distinctions,
  3. keeps Nix-managed state reproducible,
  4. avoids fragile runtime-only snowflake setup,
  5. leaves room for future hub/external skills without making the first version messy.

The recommended rollout is:

  • Phase 1: declarative bundled + optional + local custom skills
  • Phase 2: declarative enable/disable controls per platform
  • Phase 3: optional support for declarative external/hub skills
  • Phase 4: profile abstractions on top

Problem

Today nix-hermes-agent can declaratively manage:

  • package version
  • service wiring
  • HERMES_HOME
  • generated config
  • documents like SOUL.md, AGENTS.md, USER.md
  • auth seed file
  • env files
  • MCP servers

But skills remain effectively under-managed.

That creates several problems:

1. Reproducibility gap

Useful Hermes deployments are shaped heavily by available skills. Right now those are not modeled as deployable state in the Nix module.

2. Snowflake drift

A machine can gain skills interactively via Hermes CLI, but the resulting state is not cleanly captured in Nix. Rebuilds and reprovisioning become less trustworthy.

3. Poor fleet ergonomics

If we want Hermes to live as:

  • a gateway agent,
  • a CLI/TUI agent over SSH,
  • an ACP-connected IDE agent,
  • a research/creative/coding sandbox on a dedicated VM,

then the skill substrate needs to be reproducible and composable.

4. Missing product maturity

A declarative agent deployment layer that cannot declaratively express skills is not finished.


Goal

Make nix-hermes-agent mature enough that a user can declare Hermes skill state in Nix with the same confidence they already declare config and documents.

Success means a user can express:

  • which built-in/bundled skills exist,
  • which upstream optional skills are installed,
  • which local custom skills are present,
  • which skills are disabled globally or per platform,
  • eventually, which external/hub skills are desired,

and rebuild a host into the same functional Hermes deployment.


Non-goals

Not a full rewrite of upstream skill management

We should not fork or replace Hermes' skill architecture. We should layer on top of real upstream semantics.

Not immediate perfect support for every hub workflow

Hub-managed skills involve remote registries, quarantine, audit, taps, lock files, and provenance. We should not block the entire feature set on solving every edge case in v1.

Not soul/prompt redesign

This effort is about capability substrate and deployment maturity, not voice tuning.


Upstream architecture findings

This section captures the relevant upstream seams that matter for Nix integration.

1. Single runtime skill root

Hermes treats this as the runtime source of truth:

  • HERMES_HOME = Path(os.getenv("HERMES_HOME", Path.home() / ".hermes"))
  • SKILLS_DIR = HERMES_HOME / "skills"

Relevant upstream files:

  • tools/skills_tool.py
  • tools/skills_hub.py
  • tools/skills_sync.py

Implication:

Nix should target ${HERMES_HOME}/skills as the canonical runtime location. Any declarative solution that ignores this will fight upstream.

2. Filesystem-based discovery

Skills are discovered by scanning SKILLS_DIR recursively for SKILL.md.

Relevant upstream behavior:

  • tools/skills_tool.py:_find_all_skills()
  • category derived from relative path under SKILLS_DIR
  • category descriptions can come from DESCRIPTION.md

Implication:

Declarative skills do not need a custom registry format. They mainly need correct directory materialization.

3. Bundled skills are synced via a manifest

Upstream has a sync layer for bundled skills:

  • tools/skills_sync.py
  • manifest file: ~/.hermes/skills/.bundled_manifest

Behavior summary:

  • bundled skills are synced into ~/.hermes/skills/
  • manifest tracks origin hashes
  • user modifications/deletions are handled intentionally
  • removed bundled skills are cleaned from manifest

Implication:

Bundled skills are not just copied blindly. If we want to align with upstream behavior, we should respect that bundled skills have their own lifecycle semantics.

4. Optional skills are shipped in repo, but not bundled into runtime by default

Upstream has optional-skills/ and exposes them through hub-style install/search flows.

Relevant upstream behavior:

  • tools/skills_hub.py: OptionalSkillsSource
  • source path: repo optional-skills/
  • install identifiers look like official/category/skill or official/skill

Implication:

Optional skills are a clean fit for declarative install because they are effectively pinned to the package revision already.

5. Skill enable/disable is config-driven

Skill disabling lives in Hermes config, not the filesystem.

Relevant upstream behavior:

  • hermes_cli/skills_config.py
  • tools/skills_tool.py:_get_disabled_skill_names()
  • config keys:
skills:
  disabled: []
  platform_disabled: {}

Implication:

Installation and enablement are separate concerns. The Nix module should model both.

6. Hub/external skills carry provenance state

Hub state lives under:

  • ~/.hermes/skills/.hub/lock.json
  • ~/.hermes/skills/.hub/taps.json
  • plus quarantine/audit state

Relevant upstream behavior:

  • tools/skills_hub.py:HubLockFile
  • tools/skills_hub.py:TapsManager
  • install flow records source/trust/provenance/install_path/hash

Implication:

External skill support has real statefulness and should be treated as a separate phase.


Product principles

1. Reproducibility first

The primary value of nix-hermes-agent is that Hermes deployment stops being snowflake state. Skills must follow the same principle.

2. Align with upstream instead of papering over it

Where upstream already has a lifecycle concept:

  • bundled manifest,
  • optional skill source,
  • hub lock file,
  • disabled config,

we should design with those semantics in mind.

3. Prefer pinned/local skill materialization over runtime network installs

For Nix-managed deployments, local/pinned skills are cleaner than runtime remote fetches.

4. Separate “present on disk” from “enabled for runtime”

This mirrors upstream and keeps the module more expressive.

5. Make the first version boring and reliable

Copy/sync into runtime state first. Do not over-optimize for immutable symlink purity if it breaks upstream assumptions.


Proposed module design

Phase 1: Declarative local skill materialization

New option group: services.hermes-agent.skills

Suggested top-level shape:

services.hermes-agent.skills = {
  bundled.enable = true;

  optional = [
    "research/foo"
    "creative/bar"
  ];

  custom = {
    my-playbook = {
      category = "research";
      source = ./skills/my-playbook;
    };
  };

  disabled = [ "foo" ];

  platformDisabled = {
    telegram = [ "shell-heavy-skill" ];
    cli = [ ];
  };
};

Sub-feature A: bundled skills

Goal

Allow nix-hermes-agent to ensure upstream bundled skills are present declaratively.

Why

This makes package upgrades and fresh machines deterministic.

Design options

Option A — let upstream sync bundled skills at runtime

Pros:

  • closest to upstream lifecycle
  • manifest semantics stay upstream-owned

Cons:

  • requires that sync path is guaranteed to run in the packaged flow
  • harder to reason about from Nix module alone
Option B — module-managed bundled sync/materialization

Pros:

  • explicit and controllable from module
  • can be tested from Nix side

Cons:

  • risks partially duplicating upstream logic unless done carefully

Recommendation

Use module-managed materialization initially, but preserve upstream-compatible structure and manifest awareness. If we later find an upstream-supported sync entrypoint that is stable, we can switch implementation without changing user-facing options.

Sub-feature B: optional skills

Goal

Install upstream optional-skills/ declaratively by package revision.

Why

These are the cleanest next-step skills because they are already version-pinned by the packaged source tree.

Recommendation

Expose a list of relative skill paths from upstream optional-skills. Example:

services.hermes-agent.skills.optional = [
  "research/deep-research"
  "creative/story-ideation"
];

The module activation step should materialize these into ${HERMES_HOME}/skills/... preserving category layout.

Sub-feature C: custom local skills

Goal

Allow users to declaratively ship their own skills.

Recommendation

Support both:

  • source = ./path/to/skill-dir
  • later maybe text = '' ... SKILL.md ... '' for convenience

Suggested shape:

services.hermes-agent.skills.custom = {
  repo-watch = {
    category = "research";
    source = ./skills/repo-watch;
  };
};

This should build a derivation containing normalized skill trees, then copy/sync them into runtime state.


Phase 2: Declarative enable/disable controls

Goal

Manage the upstream config shape declaratively.

Suggested mapping:

services.hermes-agent.skills.disabled = [ "foo" ];
services.hermes-agent.skills.platformDisabled.telegram = [ "bar" ];

Module implementation should merge this into generated Hermes config:

config.skills = {
  disabled = ...;
  platform_disabled = ...;
};

Why separate from installation?

Because upstream separates them, and it enables useful patterns:

  • skill present but disabled on messaging platforms
  • skill enabled only in CLI/ACP contexts
  • same deployment with different platform affordances

This matters a lot if Hermes is used across:

  • gateway,
  • SSH CLI/TUI,
  • ACP/IDE connection.

Phase 3: Declarative external / hub skills

This should be treated as a separate feature set, not bundled into v1.

Problem

Hub skills are not just files. They have upstream lifecycle semantics:

  • provenance
  • trust level
  • lock entries
  • taps
  • audit log
  • quarantine/security scanning

Two viable models

Model A — runtime installation via Hermes CLI

Declare desired hub skills in Nix, then reconcile at activation/runtime using Hermes itself:

services.hermes-agent.skills.hub = [
  {
    identifier = "owner/repo/path/to/skill";
    source = "github";
  }
];

Pros:

  • preserves upstream lock/audit/scan behavior

Cons:

  • network-dependent
  • less reproducible
  • slower/more stateful

Model B — pin external skill sources in Nix and treat them as local skills

Use fetchFromGitHub or flake inputs, then place them under custom/local managed skills.

Pros:

  • reproducible
  • reviewable
  • pinned by hash
  • easier to reason about in infra repos

Cons:

  • bypasses hub provenance model unless explicitly emulated

Recommendation

For nix-hermes-agent, prefer Model B philosophically. Support Model A later as an escape hatch for people who want native Hermes Hub behavior.


Phase 4: Profiles

Once the substrate exists, we can define higher-level role profiles. Examples:

  • researcher
  • creative-lab
  • ops-lite
  • coding-explorer

These profiles would be syntactic sugar over:

  • installed skills
  • disabled skills
  • model/tool config
  • maybe documents

This should come after raw primitives are solid.


Proposed option schema

This is a proposed user-facing module API, not final code.

services.hermes-agent.skills = {
  enable = lib.mkEnableOption "declarative Hermes skills";

  bundled = {
    enable = lib.mkOption {
      type = lib.types.bool;
      default = true;
    };
  };

  optional = lib.mkOption {
    type = lib.types.listOf lib.types.str;
    default = [ ];
    example = [ "research/deep-research" ];
  };

  custom = lib.mkOption {
    type = lib.types.attrsOf (lib.types.submodule ({ ... }: {
      options = {
        category = lib.mkOption {
          type = lib.types.nullOr lib.types.str;
          default = null;
        };
        source = lib.mkOption {
          type = lib.types.path;
        };
      };
    }));
    default = { };
  };

  disabled = lib.mkOption {
    type = lib.types.listOf lib.types.str;
    default = [ ];
  };

  platformDisabled = lib.mkOption {
    type = lib.types.attrsOf (lib.types.listOf lib.types.str);
    default = { };
  };

  hub = lib.mkOption {
    type = lib.types.listOf (lib.types.submodule ({ ... }: {
      options = {
        identifier = lib.mkOption { type = lib.types.str; };
        source = lib.mkOption {
          type = lib.types.enum [ "github" "official" "well-known" "skills-sh" ];
          default = "github";
        };
      };
    }));
    default = [ ];
  };
};

Implementation plan

Milestone 1 — groundwork and packaging introspection

Deliverables

  • confirm packaged source paths for bundled skills and optional-skills
  • add tests or at least build-time assertions that those paths exist in the package output
  • document path assumptions

Notes

Current package already fetches upstream source with submodules and builds Hermes as a Python app. We need a stable way to refer to upstream skills trees from the installed package or derivation source.

Risks

  • packaged output may not expose source trees exactly where activation expects them
  • upstream package layout may shift across releases

Mitigation

Add explicit path probes in checks/docs and keep implementation centralized.


Milestone 2 — declarative bundled + optional + custom skill materialization

Deliverables

  • module options for bundled/optional/custom skills
  • activation step that creates ${HERMES_HOME}/skills
  • copy/sync logic preserving category layout
  • initial tests covering presence of selected skills in state dir

Implementation guidance

Use a generated derivation to normalize all Nix-managed skill content into one tree, then reconcile that tree into runtime state.

Example conceptual pipeline:

  1. build a Nix store tree containing:
    • selected bundled skills
    • selected optional skills
    • custom local skills
  2. activation script syncs that tree into ${cfg.stateDir}/.hermes/skills
  3. preserve non-Nix-managed hub state unless explicitly managing it

Important constraint

Do not clobber .hub/ blindly. Do not wipe user-managed runtime state unless explicitly requested.


Milestone 3 — declarative disabled/platformDisabled wiring

Deliverables

  • module options
  • merge into generated Hermes config
  • tests that rendered config contains expected shape

Why this milestone is low risk

This is a direct mapping to upstream config semantics and does not require custom lifecycle logic.


Milestone 4 — docs and examples

Deliverables

  • README examples for:
    • basic bundled skill deployment
    • optional skills
    • custom local skills
    • platform-specific disabling
  • migration guidance for users already managing skills interactively

Milestone 5 — optional hub skill support

Deliverables

  • design decision: native runtime install vs pinned-local strategy
  • if implemented, add explicit caveats about reproducibility and network dependency
  • preserve upstream lock/audit semantics

Recommendation

Do not block the core feature on this milestone.


State reconciliation strategy

This is the most important implementation choice.

Use a managed subset of ${HERMES_HOME}/skills while leaving upstream-owned dynamic state alone.

Desired behavior

Nix should manage:

  • bundled skills selected by module policy
  • optional skills selected by module policy
  • custom local skills selected by module policy

Nix should avoid trampling:

  • .hub/
  • maybe .bundled_manifest unless we intentionally integrate with it
  • runtime-installed non-managed skills unless user opts into strict mode

Possible implementation pattern

Maintain a managed marker file or managed manifest under Hermes home, e.g.:

  • ${HERMES_HOME}/skills/.nix-managed-manifest.json

Track which installed paths belong to Nix-managed state. On activation:

  • create/update managed paths
  • remove managed paths no longer desired
  • leave unmanaged paths untouched

This avoids destructive full-directory replacement and plays better with upstream hub installs.


Testing strategy

1. Evaluation tests

Ensure module options evaluate and merge correctly.

2. Render tests

Validate generated config contains expected skill disable keys.

3. Activation tests

On a NixOS test VM or shell-based checks:

  • deploy with custom skill
  • assert SKILL.md lands under expected path
  • deploy with optional skill
  • assert category structure preserved
  • rebuild with skill removed
  • assert only managed skill removed
  • assert .hub/ untouched

4. Regression tests

Check package path assumptions for upstream bundled and optional-skills trees.


Open questions

1. Should bundled skills be managed by module or left to upstream sync?

Current recommendation: module-managed initially, but keep implementation swappable.

2. Should Nix-managed skills be copied or symlinked into runtime?

Current recommendation: copy/sync first. This is more boring but more compatible with any upstream expectations around writable trees and file operations.

3. Should hub skills be in scope for the first PR series?

Current recommendation: no. Document the plan, but ship local/offline-managed skills first.

4. Should we support inline SKILL.md text for custom skills?

Probably yes later, but path-based custom skills are enough for v1.


PR 1 — groundwork

  • package path assertions
  • module option scaffolding
  • docs stub

PR 2 — declarative local skills

  • bundled/optional/custom skill options
  • managed reconciliation logic
  • tests

PR 3 — declarative disable controls

  • disabled
  • platformDisabled
  • config wiring
  • tests

PR 4 — docs/examples

  • README updates
  • migration examples
  • profile examples

PR 5 — optional hub support (if still desired)

  • separate decision doc
  • explicit caveats

Recommendation

The repo should focus on declarative skills maturity now, not soul tuning.

The best next step is to implement a boring, reliable, upstream-aware layer for:

  • bundled skills,
  • optional skills,
  • custom local skills,
  • disabled/platform-disabled config.

That will make nix-hermes-agent meaningfully more mature and unlock better Hermes roles across:

  • gateway usage,
  • SSH CLI/TUI usage,
  • ACP/IDE connections,
  • dedicated research/creative sandbox VMs.

Appendix: Upstream files worth watching

For future maintainers, these upstream files are the primary integration seams:

  • tools/skills_tool.py

    • SKILLS_DIR
    • _find_all_skills()
    • _get_disabled_skill_names()
    • category/path discovery
  • tools/skills_sync.py

    • bundled skill sync behavior
    • .bundled_manifest
  • tools/skills_hub.py

    • OptionalSkillsSource
    • HubLockFile
    • TapsManager
    • install/uninstall semantics
  • hermes_cli/skills_config.py

    • user-facing config model for disabled/platform-disabled skills
  • hermes_cli/config.py

    • HERMES_HOME
    • merged config semantics

These are the places likely to matter most when upstream changes skill behavior.