mirror of
https://github.com/magnus919/agent-skills.git
synced 2026-09-11 19:47:12 +03:00
chore: align governance with shipped artifact types (#62)
Closes #61\n\nImplemented and independently reviewed with AI assistance from Jasper on behalf of Magnus Hedemark.
This commit is contained in:
@@ -19,4 +19,6 @@
|
||||
|
||||
## Review notes
|
||||
|
||||
- [ ] Final-head review evidence is tied to commit SHA `<sha>`, and all actionable findings are resolved.
|
||||
|
||||
<!-- Call out compatibility concerns, intentional simplifications, or follow-up work. -->
|
||||
|
||||
@@ -20,3 +20,11 @@ jobs:
|
||||
ruby-version: '3.3'
|
||||
- name: Validate skill format and links
|
||||
run: ruby scripts/validate-skills.rb
|
||||
- name: Check tracked repository artifacts
|
||||
run: python3 scripts/check-artifacts.py
|
||||
- name: Build tracked Python packages
|
||||
run: |
|
||||
wheel_dir=$(mktemp -d)
|
||||
while IFS= read -r pyproject; do
|
||||
python3 -m pip wheel --no-deps "./$(dirname "$pyproject")" --wheel-dir "$wheel_dir"
|
||||
done < <(git ls-files -- ':(glob)**/pyproject.toml')
|
||||
|
||||
@@ -32,6 +32,10 @@ ruby scripts/validate-skills.rb
|
||||
|
||||
The same validation runs in GitHub Actions for pushes and pull requests. If a skill includes executable scripts or a package, run its documented checks as well and include the commands and results in your pull request.
|
||||
|
||||
## Deprecating a skill
|
||||
|
||||
When replacing a skill, preserve its old directory as a routing stub. Prefix its description with `Deprecated: use <replacement>`, explain the migration in the stub, and remove it only after compatibility is no longer required.
|
||||
|
||||
## Pull requests
|
||||
|
||||
- Create a branch from `main`: `feat/short-description`, `fix/short-description`, or `docs/short-description`.
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
A collection of AI agent skills — reusable workflows, protocols, and knowledge packs for agentic systems. Skills follow the [Agent Skills open format](https://agentskills.io), making them compatible with any agent framework that supports the standard.
|
||||
|
||||
Bundles organize related skills under a single umbrella with shared reference material and auto-loading by trigger context; they appear in the same alphabetical catalog as standalone skills.
|
||||
Bundles are this repository's convention for organizing related skills under a single umbrella with shared reference material; they appear in the same alphabetical catalog as standalone skills. Compatible harnesses are guaranteed to see the umbrella skill. Nested subskill auto-loading depends on the harness or on the umbrella skill's instructions.
|
||||
|
||||
## Skills
|
||||
|
||||
|
||||
+1
-1
@@ -17,4 +17,4 @@ Do not include credentials, private URLs, or personal data in the report unless
|
||||
|
||||
You should receive an acknowledgement within seven days. We will investigate, keep the reporter informed when practical, and coordinate disclosure after a fix or mitigation is available.
|
||||
|
||||
Supported versions are the latest commit on `main` and the most recent tagged release. Older releases may receive security fixes when the impact warrants it.
|
||||
Supported version: the current `main` branch only.
|
||||
|
||||
@@ -38,6 +38,8 @@ Or install from the skill directory:
|
||||
pip install -e /path/to/agent-council/
|
||||
```
|
||||
|
||||
Pip and wheel installs include generated and user-supplied personas, but not the `hermes-profiles` library. To use real bundled profiles, start from a recursive source checkout.
|
||||
|
||||
## Usage
|
||||
|
||||
```bash
|
||||
|
||||
@@ -82,7 +82,7 @@ Options:
|
||||
|
||||
## Profile Selection
|
||||
|
||||
By default, the council auto-selects relevant profiles from a library of **39 real professional profiles** (shipped as a git submodule from the [hermes-profiles](https://github.com/magnus919/hermes-profiles) repository). Each profile has a SOUL.md — an identity document with real methodology, values, and operating principles — rather than fabricated personas.
|
||||
In a recursive source checkout, the council auto-selects relevant real professional profiles from the included [hermes-profiles](https://github.com/magnus919/hermes-profiles) library. Each profile has a SOUL.md — an identity document with real methodology, values, and operating principles — rather than a fabricated persona. Pip and wheel installs do not bundle that library; use generated or user-supplied personas instead.
|
||||
|
||||
### Auto-selection
|
||||
|
||||
@@ -102,10 +102,10 @@ Three ways to populate the council, with different tradeoffs:
|
||||
|
||||
| Method | Best for | Diversity | Setup |
|
||||
|--------|----------|-----------|-------|
|
||||
| `--profiles` (auto-select) | Single-domain questions with clear keywords | High — profiles have real SOUL.md methodology | Zero — just ask the question |
|
||||
| `--profiles name1,name2` | Targeted debates where you know the stakeholders | Highest — you pick specific methodological voices | Know the profile names |
|
||||
| `--profiles` (auto-select) | Single-domain questions with clear keywords | High — profiles have real SOUL.md methodology | Recursive source checkout required |
|
||||
| `--profiles name1,name2` | Targeted debates where you know the stakeholders | Highest — you pick specific methodological voices | Recursive source checkout and profile names |
|
||||
| `--persona-file file.json` | Full control over agent identities, custom domains | Variable — depends on how you design them | Create a JSON file |
|
||||
| Auto (no flag) | Default — uses profiles if available, falls back to generated | Good — auto-selects from 39 profiles | Zero |
|
||||
| Auto (no flag) | Default — uses profiles if available, falls back to generated | Good — varies with available profiles | No setup for generated personas; recursive source checkout for real profiles |
|
||||
|
||||
For most cases, let it auto-select or use `--profiles` with 3-5 names. Only use `--persona-file` when you need specific invented expertise that doesn't map to any existing profile.
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
@@ -11,24 +10,10 @@ import yaml
|
||||
from agent_council.state import ProfileInfo
|
||||
|
||||
|
||||
# Path to the profiles submodule within the skill directory
|
||||
# Path to the locally available profiles library within the skill directory.
|
||||
PROFILES_DIR = Path(__file__).resolve().parent.parent.parent / "profiles" / "profiles"
|
||||
|
||||
|
||||
def _update_submodule() -> None:
|
||||
"""Pull the latest profiles from the hermes-profiles submodule."""
|
||||
skill_root = PROFILES_DIR.parent # agent-council/profiles/
|
||||
try:
|
||||
subprocess.run(
|
||||
["git", "submodule", "update", "--remote", "--init"],
|
||||
cwd=skill_root.parent, # agent-council/
|
||||
capture_output=True,
|
||||
timeout=30,
|
||||
)
|
||||
except Exception:
|
||||
pass # Non-fatal — use whatever version we have
|
||||
|
||||
|
||||
def _list_available() -> list[str]:
|
||||
"""List all available profile names."""
|
||||
if not PROFILES_DIR.exists():
|
||||
@@ -63,8 +48,7 @@ def _load_profile(name: str) -> ProfileInfo | None:
|
||||
|
||||
|
||||
def load_all() -> list[ProfileInfo]:
|
||||
"""Load all available profiles. Updates submodule first."""
|
||||
_update_submodule()
|
||||
"""Load all locally available profiles."""
|
||||
profiles = []
|
||||
for name in _list_available():
|
||||
p = _load_profile(name)
|
||||
@@ -75,7 +59,6 @@ def load_all() -> list[ProfileInfo]:
|
||||
|
||||
def select_by_names(names: list[str]) -> list[ProfileInfo]:
|
||||
"""Load specific profiles by name."""
|
||||
_update_submodule()
|
||||
profiles = []
|
||||
for name in names:
|
||||
name = name.strip().lower()
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
import importlib.util
|
||||
import sys
|
||||
import tempfile
|
||||
import types
|
||||
import unittest
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
|
||||
|
||||
yaml = types.ModuleType("yaml")
|
||||
yaml.safe_load = lambda _: {}
|
||||
sys.modules["yaml"] = yaml
|
||||
|
||||
state = types.ModuleType("agent_council.state")
|
||||
|
||||
|
||||
@dataclass
|
||||
class ProfileInfo:
|
||||
name: str
|
||||
description: str
|
||||
soul_content: str
|
||||
|
||||
|
||||
state.ProfileInfo = ProfileInfo
|
||||
sys.modules["agent_council.state"] = state
|
||||
|
||||
select_spec = importlib.util.spec_from_file_location(
|
||||
"select", Path(__file__).parents[1] / "agent_council" / "phases" / "select.py"
|
||||
)
|
||||
select = importlib.util.module_from_spec(select_spec)
|
||||
select_spec.loader.exec_module(select)
|
||||
|
||||
|
||||
class ProfileSelectionTests(unittest.TestCase):
|
||||
def test_missing_profiles_are_empty_without_running_git(self):
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
with patch.object(select, "PROFILES_DIR", Path(directory) / "profiles"):
|
||||
self.assertEqual(select.load_all(), [])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -66,7 +66,6 @@ The `SKILL.md` file must contain YAML frontmatter followed by Markdown body cont
|
||||
| `compatibility` | No | Max 500 chars. Indicates environment requirements. |
|
||||
| `metadata` | No | Arbitrary key-value mapping. |
|
||||
| `allowed-tools` | No | Space-separated string of pre-approved tools. (Experimental) |
|
||||
| `confirmation` | No | Boolean (default: false). Signals destructive operations requiring explicit user confirmation. |
|
||||
|
||||
#### `name` field rules
|
||||
- 1–64 characters
|
||||
|
||||
@@ -33,7 +33,7 @@ The `SKILL.md` file must contain YAML frontmatter followed by Markdown content.
|
||||
| `compatibility` | No | Max 500 characters. Indicates environment requirements (intended product, system packages, network access, etc.). |
|
||||
| `metadata` | No | Arbitrary key-value mapping for additional metadata. |
|
||||
| `allowed-tools` | No | Space-separated string of pre-approved tools the skill may use. (Experimental) |
|
||||
| `confirmation` | No | Boolean (default: false). Signals destructive or state-changing operations requiring explicit user confirmation. |
|
||||
|
||||
|
||||
<Card>
|
||||
**Minimal example:**
|
||||
@@ -197,10 +197,6 @@ The optional `allowed-tools` field:
|
||||
```
|
||||
</Card>
|
||||
|
||||
#### `confirmation` field
|
||||
|
||||
The optional `confirmation` field must be a boolean. When `true`, the agent must obtain explicit user confirmation before carrying out destructive or state-changing operations directed by the skill. This is a safety signal, not a permissions system. Most skills should omit it.
|
||||
|
||||
### Body content
|
||||
|
||||
The Markdown body after the frontmatter contains the skill instructions. There are no format restrictions. Write whatever helps agents perform the task effectively.
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Validate runnable repository artifacts selected from the Git index."""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import subprocess
|
||||
import sys
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
|
||||
|
||||
def tracked_files() -> list[Path]:
|
||||
result = subprocess.run(
|
||||
["git", "ls-files", "-z"],
|
||||
cwd=ROOT,
|
||||
check=True,
|
||||
capture_output=True,
|
||||
)
|
||||
return [ROOT / name for name in result.stdout.decode().split("\0") if name]
|
||||
|
||||
|
||||
def is_shell_script(path: Path) -> bool:
|
||||
if path.suffix in {".sh", ".bash"}:
|
||||
return True
|
||||
try:
|
||||
first_line = path.open("rb").readline().decode("utf-8", "replace")
|
||||
except OSError:
|
||||
return False
|
||||
return first_line.startswith("#!") and any(
|
||||
shell in first_line for shell in ("/sh", "/bash", "env sh", "env bash")
|
||||
)
|
||||
|
||||
|
||||
def test_directories(files: list[Path]) -> list[Path]:
|
||||
directories = set()
|
||||
for path in files:
|
||||
for parent in path.parents:
|
||||
if parent.name == "tests":
|
||||
directories.add(parent)
|
||||
break
|
||||
if parent == ROOT:
|
||||
break
|
||||
return sorted(directories)
|
||||
|
||||
|
||||
def run_checks(files: list[Path]) -> list[str]:
|
||||
errors = []
|
||||
for path in files:
|
||||
relative = path.relative_to(ROOT)
|
||||
if path.suffix == ".py":
|
||||
try:
|
||||
compile(path.read_bytes(), str(relative), "exec")
|
||||
except (OSError, SyntaxError) as error:
|
||||
errors.append(f"python {relative}: {error}")
|
||||
elif path.suffix == ".json":
|
||||
try:
|
||||
json.loads(path.read_text(encoding="utf-8"))
|
||||
except (OSError, json.JSONDecodeError) as error:
|
||||
errors.append(f"json {relative}: {error}")
|
||||
if is_shell_script(path):
|
||||
result = subprocess.run(
|
||||
["bash", "-n", str(path)], cwd=ROOT, text=True, capture_output=True
|
||||
)
|
||||
if result.returncode:
|
||||
errors.append(f"bash -n {relative}: {result.stderr.strip()}")
|
||||
|
||||
for directory in test_directories(files):
|
||||
relative = directory.relative_to(ROOT)
|
||||
result = unittest.TextTestRunner(verbosity=0).run(
|
||||
unittest.defaultTestLoader.discover(
|
||||
str(directory), top_level_dir=str(directory)
|
||||
)
|
||||
)
|
||||
if not result.wasSuccessful():
|
||||
errors.append(
|
||||
f"unittest discover {relative}: "
|
||||
f"{len(result.failures)} failures, {len(result.errors)} errors"
|
||||
)
|
||||
return errors
|
||||
|
||||
|
||||
def self_check() -> None:
|
||||
files = [ROOT / "example/tests/test_example.py", ROOT / "elsewhere/file.py"]
|
||||
assert test_directories(files) == [ROOT / "example/tests"]
|
||||
assert is_shell_script(ROOT / "scripts/check-artifacts.py") is False
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--self-check", action="store_true")
|
||||
args = parser.parse_args()
|
||||
if args.self_check:
|
||||
self_check()
|
||||
return 0
|
||||
|
||||
errors = run_checks(tracked_files())
|
||||
for error in errors:
|
||||
print(error, file=sys.stderr)
|
||||
return 1 if errors else 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Reference in New Issue
Block a user