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
|
## 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. -->
|
<!-- Call out compatibility concerns, intentional simplifications, or follow-up work. -->
|
||||||
|
|||||||
@@ -20,3 +20,11 @@ jobs:
|
|||||||
ruby-version: '3.3'
|
ruby-version: '3.3'
|
||||||
- name: Validate skill format and links
|
- name: Validate skill format and links
|
||||||
run: ruby scripts/validate-skills.rb
|
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.
|
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
|
## Pull requests
|
||||||
|
|
||||||
- Create a branch from `main`: `feat/short-description`, `fix/short-description`, or `docs/short-description`.
|
- 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.
|
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
|
## 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.
|
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 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
|
## Usage
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
@@ -82,7 +82,7 @@ Options:
|
|||||||
|
|
||||||
## Profile Selection
|
## 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
|
### Auto-selection
|
||||||
|
|
||||||
@@ -102,10 +102,10 @@ Three ways to populate the council, with different tradeoffs:
|
|||||||
|
|
||||||
| Method | Best for | Diversity | Setup |
|
| 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` (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 | Know the profile names |
|
| `--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 |
|
| `--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.
|
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 json
|
||||||
import os
|
import os
|
||||||
import subprocess
|
|
||||||
import sys
|
import sys
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
@@ -11,24 +10,10 @@ import yaml
|
|||||||
from agent_council.state import ProfileInfo
|
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"
|
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]:
|
def _list_available() -> list[str]:
|
||||||
"""List all available profile names."""
|
"""List all available profile names."""
|
||||||
if not PROFILES_DIR.exists():
|
if not PROFILES_DIR.exists():
|
||||||
@@ -63,8 +48,7 @@ def _load_profile(name: str) -> ProfileInfo | None:
|
|||||||
|
|
||||||
|
|
||||||
def load_all() -> list[ProfileInfo]:
|
def load_all() -> list[ProfileInfo]:
|
||||||
"""Load all available profiles. Updates submodule first."""
|
"""Load all locally available profiles."""
|
||||||
_update_submodule()
|
|
||||||
profiles = []
|
profiles = []
|
||||||
for name in _list_available():
|
for name in _list_available():
|
||||||
p = _load_profile(name)
|
p = _load_profile(name)
|
||||||
@@ -75,7 +59,6 @@ def load_all() -> list[ProfileInfo]:
|
|||||||
|
|
||||||
def select_by_names(names: list[str]) -> list[ProfileInfo]:
|
def select_by_names(names: list[str]) -> list[ProfileInfo]:
|
||||||
"""Load specific profiles by name."""
|
"""Load specific profiles by name."""
|
||||||
_update_submodule()
|
|
||||||
profiles = []
|
profiles = []
|
||||||
for name in names:
|
for name in names:
|
||||||
name = name.strip().lower()
|
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. |
|
| `compatibility` | No | Max 500 chars. Indicates environment requirements. |
|
||||||
| `metadata` | No | Arbitrary key-value mapping. |
|
| `metadata` | No | Arbitrary key-value mapping. |
|
||||||
| `allowed-tools` | No | Space-separated string of pre-approved tools. (Experimental) |
|
| `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
|
#### `name` field rules
|
||||||
- 1–64 characters
|
- 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.). |
|
| `compatibility` | No | Max 500 characters. Indicates environment requirements (intended product, system packages, network access, etc.). |
|
||||||
| `metadata` | No | Arbitrary key-value mapping for additional metadata. |
|
| `metadata` | No | Arbitrary key-value mapping for additional metadata. |
|
||||||
| `allowed-tools` | No | Space-separated string of pre-approved tools the skill may use. (Experimental) |
|
| `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>
|
<Card>
|
||||||
**Minimal example:**
|
**Minimal example:**
|
||||||
@@ -197,10 +197,6 @@ The optional `allowed-tools` field:
|
|||||||
```
|
```
|
||||||
</Card>
|
</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
|
### 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.
|
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