feat: optional skills — official skills shipped but not activated by default

Add 'optional-skills/' directory for official skills that ship with the repo
but are not copied to ~/.hermes/skills/ during setup. They are:
- NOT shown to the model in the system prompt
- NOT copied during hermes setup/update
- Discoverable via 'hermes skills search' labeled as 'official'
- Installable via 'hermes skills install' with builtin trust (no third-party warning)
- Auto-categorized on install based on directory structure

Implementation:
- OptionalSkillSource adapter in tools/skills_hub.py (search/fetch/inspect)
- Added to create_source_router() as first source (highest priority)
- Trust level 'builtin' for official skills in skills_guard.py
- Friendly install message for official skills (no third-party warning)
- 'official' label in cyan in search results and skill list

First optional skill: Blackbox CLI (autonomous-ai-agents/blackbox)
- Multi-model coding agent with built-in judge/Chairman pattern
- Delegates to Claude, Codex, Gemini, and Blackbox models
- Open-source CLI (GPL-3.0, TypeScript, forked from Gemini CLI)
- Requires paid Blackbox AI API key

Refs: #475
This commit is contained in:
teknium1
2026-03-06 01:24:11 -08:00
parent 8c80b96318
commit f2e24faaca
6 changed files with 358 additions and 14 deletions

View File

@@ -99,12 +99,13 @@ def do_search(query: str, source: str = "all", limit: int = 10,
table.add_column("Identifier", style="dim")
for r in results:
trust_style = {"trusted": "green", "community": "yellow"}.get(r.trust_level, "dim")
trust_style = {"builtin": "bright_cyan", "trusted": "green", "community": "yellow"}.get(r.trust_level, "dim")
trust_label = "official" if r.source == "official" else r.trust_level
table.add_row(
r.name,
r.description[:60] + ("..." if len(r.description) > 60 else ""),
r.source,
f"[{trust_style}]{r.trust_level}[/]",
f"[{trust_style}]{trust_label}[/]",
r.identifier,
)
@@ -147,6 +148,12 @@ def do_install(identifier: str, category: str = "", force: bool = False,
c.print(f"[bold red]Error:[/] Could not fetch '{identifier}' from any source.\n")
return
# Auto-detect category for official skills (e.g. "official/autonomous-ai-agents/blackbox")
if bundle.source == "official" and not category:
id_parts = bundle.identifier.split("/") # ["official", "category", "skill"]
if len(id_parts) >= 3:
category = id_parts[1]
# Check if already installed
lock = HubLockFile()
existing = lock.get_installed(bundle.name)
@@ -177,9 +184,19 @@ def do_install(identifier: str, category: str = "", force: bool = False,
f"{len(result.findings)}_findings")
return
# Confirm with user — always show risk warning regardless of source
# Confirm with user — show appropriate warning based on source
if not force:
c.print()
if bundle.source == "official":
c.print(Panel(
"[bold bright_cyan]This is an official optional skill maintained by Nous Research.[/]\n\n"
"It ships with hermes-agent but is not activated by default.\n"
"Installing will copy it to your skills directory where the agent can use it.\n\n"
f"Files will be at: [cyan]~/.hermes/skills/{category + '/' if category else ''}{bundle.name}/[/]",
title="Official Skill",
border_style="bright_cyan",
))
else:
c.print(Panel(
"[bold yellow]You are installing a third-party skill at your own risk.[/]\n\n"
"External skills can contain instructions that influence agent behavior,\n"
@@ -297,8 +314,9 @@ def do_list(source_filter: str = "all", console: Optional[Console] = None) -> No
if source_filter == "builtin" and hub_entry:
continue
trust_style = {"builtin": "blue", "trusted": "green", "community": "yellow"}.get(trust, "dim")
table.add_row(name, category, source_display, f"[{trust_style}]{trust}[/]")
trust_style = {"builtin": "bright_cyan", "trusted": "green", "community": "yellow"}.get(trust, "dim")
trust_label = "official" if source_display == "official" else trust
table.add_row(name, category, source_display, f"[{trust_style}]{trust_label}[/]")
c.print(table)
c.print(f"[dim]{len(hub_installed)} hub-installed, "

View File

@@ -0,0 +1,22 @@
# Optional Skills
Official skills maintained by Nous Research that are **not activated by default**.
These skills ship with the hermes-agent repository but are not copied to
`~/.hermes/skills/` during setup. They are discoverable via the Skills Hub:
```bash
hermes skills search <query> # finds optional skills labeled "official"
hermes skills install <identifier> # copies to ~/.hermes/skills/ and activates
```
## Why optional?
Some skills are useful but not broadly needed by every user:
- **Niche integrations** — specific paid services, specialized tools
- **Experimental features** — promising but not yet proven
- **Heavyweight dependencies** — require significant setup (API keys, installs)
By keeping them optional, we keep the default skill set lean while still
providing curated, tested, official skills for users who want them.

View File

@@ -0,0 +1,2 @@
Optional autonomous AI agent integrations — external coding agent CLIs
that can be delegated to for independent coding tasks.

View File

@@ -0,0 +1,143 @@
---
name: blackbox
description: Delegate coding tasks to Blackbox AI CLI agent. Multi-model agent with built-in judge that runs tasks through multiple LLMs and picks the best result. Requires the blackbox CLI and a Blackbox AI API key.
version: 1.0.0
author: Hermes Agent (Nous Research)
license: MIT
metadata:
hermes:
tags: [Coding-Agent, Blackbox, Multi-Agent, Judge, Multi-Model]
related_skills: [claude-code, codex, hermes-agent]
---
# Blackbox CLI
Delegate coding tasks to [Blackbox AI](https://www.blackbox.ai/) via the Hermes terminal. Blackbox is a multi-model coding agent CLI that dispatches tasks to multiple LLMs (Claude, Codex, Gemini, Blackbox Pro) and uses a judge to select the best implementation.
The CLI is [open-source](https://github.com/blackboxaicode/cli) (GPL-3.0, TypeScript, forked from Gemini CLI) and supports interactive sessions, non-interactive one-shots, checkpointing, MCP, and vision model switching.
## Prerequisites
- Node.js 20+ installed
- Blackbox CLI installed: `npm install -g @blackboxai/cli`
- Or install from source:
```
git clone https://github.com/blackboxaicode/cli.git
cd cli && npm install && npm install -g .
```
- API key from [app.blackbox.ai/dashboard](https://app.blackbox.ai/dashboard)
- Configured: run `blackbox configure` and enter your API key
- Use `pty=true` in terminal calls — Blackbox CLI is an interactive terminal app
## One-Shot Tasks
```
terminal(command="blackbox --prompt 'Add JWT authentication with refresh tokens to the Express API'", workdir="/path/to/project", pty=true)
```
For quick scratch work:
```
terminal(command="cd $(mktemp -d) && git init && blackbox --prompt 'Build a REST API for todos with SQLite'", pty=true)
```
## Background Mode (Long Tasks)
For tasks that take minutes, use background mode so you can monitor progress:
```
# Start in background with PTY
terminal(command="blackbox --prompt 'Refactor the auth module to use OAuth 2.0'", workdir="~/project", background=true, pty=true)
# Returns session_id
# Monitor progress
process(action="poll", session_id="<id>")
process(action="log", session_id="<id>")
# Send input if Blackbox asks a question
process(action="submit", session_id="<id>", data="yes")
# Kill if needed
process(action="kill", session_id="<id>")
```
## Checkpoints & Resume
Blackbox CLI has built-in checkpoint support for pausing and resuming tasks:
```
# After a task completes, Blackbox shows a checkpoint tag
# Resume with a follow-up task:
terminal(command="blackbox --resume-checkpoint 'task-abc123-2026-03-06' --prompt 'Now add rate limiting to the endpoints'", workdir="~/project", pty=true)
```
## Session Commands
During an interactive session, use these commands:
| Command | Effect |
|---------|--------|
| `/compress` | Shrink conversation history to save tokens |
| `/clear` | Wipe history and start fresh |
| `/stats` | View current token usage |
| `Ctrl+C` | Cancel current operation |
## PR Reviews
Clone to a temp directory to avoid modifying the working tree:
```
terminal(command="REVIEW=$(mktemp -d) && git clone https://github.com/user/repo.git $REVIEW && cd $REVIEW && gh pr checkout 42 && blackbox --prompt 'Review this PR against main. Check for bugs, security issues, and code quality.'", pty=true)
```
## Parallel Work
Spawn multiple Blackbox instances for independent tasks:
```
terminal(command="blackbox --prompt 'Fix the login bug'", workdir="/tmp/issue-1", background=true, pty=true)
terminal(command="blackbox --prompt 'Add unit tests for auth'", workdir="/tmp/issue-2", background=true, pty=true)
# Monitor all
process(action="list")
```
## Multi-Model Mode
Blackbox's unique feature is running the same task through multiple models and judging the results. Configure which models to use via `blackbox configure` — select multiple providers to enable the Chairman/judge workflow where the CLI evaluates outputs from different models and picks the best one.
## Key Flags
| Flag | Effect |
|------|--------|
| `--prompt "task"` | Non-interactive one-shot execution |
| `--resume-checkpoint "tag"` | Resume from a saved checkpoint |
| `--yolo` | Auto-approve all actions and model switches |
| `blackbox session` | Start interactive chat session |
| `blackbox configure` | Change settings, providers, models |
| `blackbox info` | Display system information |
## Vision Support
Blackbox automatically detects images in input and can switch to multimodal analysis. VLM modes:
- `"once"` — Switch model for current query only
- `"session"` — Switch for entire session
- `"persist"` — Stay on current model (no switch)
## Token Limits
Control token usage via `.blackboxcli/settings.json`:
```json
{
"sessionTokenLimit": 32000
}
```
## Rules
1. **Always use `pty=true`** — Blackbox CLI is an interactive terminal app and will hang without a PTY
2. **Use `workdir`** — keep the agent focused on the right directory
3. **Background for long tasks** — use `background=true` and monitor with `process` tool
4. **Don't interfere** — monitor with `poll`/`log`, don't kill sessions because they're slow
5. **Report results** — after completion, check what changed and summarize for the user
6. **Credits cost money** — Blackbox uses a credit-based system; multi-model mode consumes credits faster
7. **Check prerequisites** — verify `blackbox` CLI is installed before attempting delegation

View File

@@ -1046,6 +1046,9 @@ def _get_configured_model() -> str:
def _resolve_trust_level(source: str) -> str:
"""Map a source identifier to a trust level."""
# Official optional skills shipped with the repo
if source.startswith("official/") or source == "official":
return "builtin"
# Check if source matches any trusted repo
for trusted in TRUSTED_REPOS:
if source.startswith(trusted) or source == trusted:

View File

@@ -5,6 +5,7 @@ Skills Hub — Source adapters and hub state management for the Hermes Skills Hu
This is a library module (not an agent tool). It provides:
- GitHubAuth: Shared GitHub API authentication (PAT, gh CLI, GitHub App)
- SkillSource ABC: Interface for all skill registry adapters
- OptionalSkillSource: Official optional skills shipped with the repo (not activated by default)
- GitHubSource: Fetch skills from any GitHub repo via the Contents API
- HubLockFile: Track provenance of installed hub skills
- Hub state directory management (quarantine, audit log, taps, index cache)
@@ -941,6 +942,160 @@ class LobeHubSource(SkillSource):
return "\n".join(fm_lines) + "\n\n" + "\n".join(body_lines) + "\n"
# ---------------------------------------------------------------------------
# Official optional skills source adapter
# ---------------------------------------------------------------------------
class OptionalSkillSource(SkillSource):
"""
Fetch skills from the optional-skills/ directory shipped with the repo.
These skills are official (maintained by Nous Research) but not activated
by default — they don't appear in the system prompt and aren't copied to
~/.hermes/skills/ during setup. They are discoverable via the Skills Hub
(search / install / inspect) and labelled "official" with "builtin" trust.
"""
def __init__(self):
self._optional_dir = Path(__file__).parent.parent / "optional-skills"
def source_id(self) -> str:
return "official"
def trust_level_for(self, identifier: str) -> str:
return "builtin"
# -- search -----------------------------------------------------------
def search(self, query: str, limit: int = 10) -> List[SkillMeta]:
results: List[SkillMeta] = []
query_lower = query.lower()
for meta in self._scan_all():
searchable = f"{meta.name} {meta.description} {' '.join(meta.tags)}".lower()
if query_lower in searchable:
results.append(meta)
if len(results) >= limit:
break
return results
# -- fetch ------------------------------------------------------------
def fetch(self, identifier: str) -> Optional[SkillBundle]:
# identifier format: "official/category/skill" or "official/skill"
rel = identifier.split("/", 1)[-1] if identifier.startswith("official/") else identifier
skill_dir = self._optional_dir / rel
if not skill_dir.is_dir():
# Try searching by skill name only (last segment)
skill_name = rel.rsplit("/", 1)[-1]
skill_dir = self._find_skill_dir(skill_name)
if not skill_dir:
return None
files: Dict[str, str] = {}
for f in skill_dir.rglob("*"):
if f.is_file() and not f.name.startswith("."):
rel_path = str(f.relative_to(skill_dir))
try:
files[rel_path] = f.read_text(encoding="utf-8")
except (OSError, UnicodeDecodeError):
continue
if not files:
return None
# Determine category from directory structure
name = skill_dir.name
return SkillBundle(
name=name,
files=files,
source="official",
identifier=f"official/{skill_dir.relative_to(self._optional_dir)}",
trust_level="builtin",
)
# -- inspect ----------------------------------------------------------
def inspect(self, identifier: str) -> Optional[SkillMeta]:
rel = identifier.split("/", 1)[-1] if identifier.startswith("official/") else identifier
skill_name = rel.rsplit("/", 1)[-1]
for meta in self._scan_all():
if meta.name == skill_name:
return meta
return None
# -- internal helpers -------------------------------------------------
def _find_skill_dir(self, name: str) -> Optional[Path]:
"""Find a skill directory by name anywhere in optional-skills/."""
if not self._optional_dir.is_dir():
return None
for skill_md in self._optional_dir.rglob("SKILL.md"):
if skill_md.parent.name == name:
return skill_md.parent
return None
def _scan_all(self) -> List[SkillMeta]:
"""Enumerate all optional skills with metadata."""
if not self._optional_dir.is_dir():
return []
results: List[SkillMeta] = []
for skill_md in sorted(self._optional_dir.rglob("SKILL.md")):
parent = skill_md.parent
rel_parts = parent.relative_to(self._optional_dir).parts
if any(part.startswith(".") for part in rel_parts):
continue
try:
content = skill_md.read_text(encoding="utf-8")
except (OSError, UnicodeDecodeError):
continue
fm = self._parse_frontmatter(content)
name = fm.get("name", parent.name)
desc = fm.get("description", "")
tags = []
meta_block = fm.get("metadata", {})
if isinstance(meta_block, dict):
hermes_meta = meta_block.get("hermes", {})
if isinstance(hermes_meta, dict):
tags = hermes_meta.get("tags", [])
rel_path = str(parent.relative_to(self._optional_dir))
results.append(SkillMeta(
name=name,
description=desc[:200],
source="official",
identifier=f"official/{rel_path}",
trust_level="builtin",
path=rel_path,
tags=tags if isinstance(tags, list) else [],
))
return results
@staticmethod
def _parse_frontmatter(content: str) -> dict:
"""Parse YAML frontmatter from SKILL.md content."""
if not content.startswith("---"):
return {}
match = re.search(r'\n---\s*\n', content[3:])
if not match:
return {}
yaml_text = content[3:match.start() + 3]
try:
parsed = yaml.safe_load(yaml_text)
return parsed if isinstance(parsed, dict) else {}
except yaml.YAMLError:
return {}
# ---------------------------------------------------------------------------
# Shared cache helpers (used by multiple adapters)
# ---------------------------------------------------------------------------
@@ -1219,6 +1374,7 @@ def create_source_router(auth: Optional[GitHubAuth] = None) -> List[SkillSource]
extra_taps = taps_mgr.list_taps()
sources: List[SkillSource] = [
OptionalSkillSource(), # Official optional skills (highest priority)
GitHubSource(auth=auth, extra_taps=extra_taps),
ClawHubSource(),
ClaudeMarketplaceSource(auth=auth),