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:
@@ -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, "
|
||||
|
||||
22
optional-skills/DESCRIPTION.md
Normal file
22
optional-skills/DESCRIPTION.md
Normal 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.
|
||||
2
optional-skills/autonomous-ai-agents/DESCRIPTION.md
Normal file
2
optional-skills/autonomous-ai-agents/DESCRIPTION.md
Normal file
@@ -0,0 +1,2 @@
|
||||
Optional autonomous AI agent integrations — external coding agent CLIs
|
||||
that can be delegated to for independent coding tasks.
|
||||
143
optional-skills/autonomous-ai-agents/blackbox/SKILL.md
Normal file
143
optional-skills/autonomous-ai-agents/blackbox/SKILL.md
Normal 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
|
||||
@@ -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:
|
||||
|
||||
@@ -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),
|
||||
|
||||
Reference in New Issue
Block a user