Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 51 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -728,27 +728,62 @@ The coordinator model has access to these tools:

**Task tracking:** `todo_write`, `todo_read`

## Skills
## Agent Skills

Skills are Python modules loaded from `~/.team-harness/skills/` and `<effective cwd>/skills/`. Each skill exports `name`, `description`, `parameters_schema`, and an async `execute(**args, ctx)` function.
team-harness supports the [Agent Skills](https://agentskills.io) standard — a cross-tool format for giving AI agents specialized knowledge and workflows.

Example (`skills/summarise.py`):
A skill is a directory containing a `SKILL.md` file with YAML frontmatter (name + description) and markdown instructions. The coordinator sees skill metadata at startup and can read the full instructions via its `read_file` tool when a task calls for it.

```python
name = "summarise_file"
description = "Summarise a file using the coordinator model."
parameters_schema = {
"type": "object",
"properties": {"path": {"type": "string"}},
"required": ["path"],
}

async def execute(path: str, ctx):
content = await ctx.read_file(path)
# ctx.client gives access to the coordinator model
return f"Summary of {path}: {len(content)} chars"
### Skill directories

| Location | Scope |
|----------|-------|
| `<cwd>/.agents/skills/` | Project-local (also searched in parent directories up to root) |
| `~/.agents/skills/` | User-global |

Project skills override user-global skills of the same name. The `.agents/skills/` path matches the Codex CLI convention, so skills written for Codex work in team-harness without changes.

### Creating a skill

```bash
mkdir -p .agents/skills/my-skill
cat > .agents/skills/my-skill/SKILL.md << 'EOF'
---
name: my-skill
description: Summarize files and produce a brief report. Use when the user asks for a summary or overview.
---

# My Skill

## Steps

1. Read the target files using `read_file`
2. Summarize the key points
3. Write a brief report

## Notes

- Keep summaries under 500 words
- Focus on actionable insights
EOF
```

### Skill naming rules

- 1-64 characters, lowercase letters, digits, and hyphens only
- Must not start or end with a hyphen, no consecutive hyphens
- Directory name is the canonical skill name

### Optional subdirectories

| Directory | Purpose |
|-----------|---------|
| `scripts/` | Executable code the agent can run |
| `references/` | Additional documentation loaded on demand |
| `assets/` | Templates, data files, schemas |

The agent reads these files on demand via its file tools — they are not loaded at startup.

## Run logs

Each run creates a directory under `~/.team-harness/runs/<run-id>/` containing:
Expand Down
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ dependencies = [
"openai>=1.78",
"pydantic>=2.11",
"prompt_toolkit>=3.0.43",
"pyyaml>=6.0",
"rich>=14.0",
"click>=8.1",
]
Expand Down
8 changes: 2 additions & 6 deletions src/team_harness/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,7 @@
from team_harness.harness import _show_no_config_hint
from team_harness.harness import _warn_provider_startup
from team_harness.harness import TeamHarness
from team_harness.skills.loader import load_skills
from team_harness.skills.loader import SkillContext
from team_harness.skills.loader import load_skill_metadata
from team_harness.tools import agent_tools
from team_harness.tools import fs_tools
from team_harness.tools import todo_tools
Expand Down Expand Up @@ -267,7 +266,7 @@ async def _repl(**kwargs: Any) -> None:
ctx = ContextTracker(model_id=config.model, model_limit=model_limit)
ui = make_console(ctx=ctx, manager=manager, run_dir=run_dir, mode="auto")
_warn_provider_startup(config, ui=ui)
skills = load_skills(cwd=config.cwd)
skills = load_skill_metadata(cwd=config.cwd)
allowed_types = get_allowed_types(config)
validate_templates(config=config, allowed_types=allowed_types)
agent_tools.setup(
Expand All @@ -279,11 +278,8 @@ async def _repl(**kwargs: Any) -> None:
)
todo_tools.setup(run_dir=run_dir)
fs_tools.setup_fs()
skill_ctx = SkillContext(client=client, config=config)
registry = _build_registry(
allowed_types=allowed_types,
skills=skills,
skill_ctx=skill_ctx,
manager=manager,
run_log=run_log,
config=config,
Expand Down
2 changes: 1 addition & 1 deletion src/team_harness/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
LOCAL_CONFIG_DIR_NAME = ".team-harness"
CONFIG_PATH = Path.home() / ".team-harness" / "config.toml"
RUNS_DIR = Path.home() / ".team-harness" / "runs"
SKILLS_USER_DIR = Path.home() / ".team-harness" / "skills"
SKILLS_USER_DIR = Path.home() / ".agents" / "skills"
PROMPT_FILE_MAX_BYTES = 100 * 1024


Expand Down
31 changes: 26 additions & 5 deletions src/team_harness/coordinator/system_prompt.py
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,29 @@
---""".strip()


def _render_skills_section(skills: list) -> str:
"""Render the skills section for the system prompt."""
if not skills:
return ""
lines = [
"## Skills",
"",
"Skills are local instruction sets that provide specialized knowledge.",
"Available skills:",
"",
]
for skill in skills:
lines.append(f"- **{skill.name}**: {skill.description} (file: {skill.path})")
lines.extend(
[
"",
"To use a skill: read its SKILL.md file to get full instructions,",
"then follow those instructions. Load referenced files only as needed.",
]
)
return "\n".join(lines)


def build_system_prompt(
config: object, allowed_types: list[str], skills: list, session_output_dir: str
) -> str:
Expand All @@ -248,11 +271,9 @@ def build_system_prompt(
if extension:
parts.append(extension)

if skills:
parts.append(
"Additional tools (skills) available:\n"
+ "\n".join(f"- {skill.name}: {skill.description}" for skill in skills)
)
skills_section = _render_skills_section(skills)
if skills_section:
parts.append(skills_section)

worker_suffix = getattr(config, "worker_suffix", "")
if worker_suffix:
Expand Down
33 changes: 2 additions & 31 deletions src/team_harness/harness.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,7 @@
from team_harness.coordinator.loop import run
from team_harness.coordinator.protocols import CoordinatorLike
from team_harness.coordinator.system_prompt import build_system_prompt
from team_harness.skills.loader import load_skills
from team_harness.skills.loader import Skill
from team_harness.skills.loader import SkillContext
from team_harness.skills.loader import load_skill_metadata
from team_harness.tools import shell_tools
from team_harness.tools.agent_tools import build_agent_tool_bindings
from team_harness.tools.fs_tools import build_fs_tool_bindings
Expand Down Expand Up @@ -128,14 +126,11 @@ async def run(self, task: str) -> TeamHarnessResult:
)
_show_no_config_hint(config, ui=ui)
_warn_provider_startup(config, ui=ui)
skills = load_skills(cwd=config.cwd)
skills = load_skill_metadata(cwd=config.cwd)
allowed_types = get_allowed_types(config)
validate_templates(config=config, allowed_types=allowed_types)
skill_ctx = SkillContext(client=client, config=config)
registry = _build_registry(
allowed_types=allowed_types,
skills=skills,
skill_ctx=skill_ctx,
manager=manager,
run_log=run_log,
config=config,
Expand Down Expand Up @@ -415,20 +410,9 @@ def _make_run_id() -> str:
)


def _make_skill_wrapper(skill: Skill, ctx: SkillContext) -> Any:
"""Create an async wrapper that invokes a skill with its context."""

async def _wrapper(**args: object) -> str:
return await skill.execute(ctx=ctx, **args)

return _wrapper


def _build_registry(
*,
allowed_types: list[str],
skills: list[Skill],
skill_ctx: SkillContext,
manager: AgentManager,
run_log: RunLogWriter,
config: Config,
Expand Down Expand Up @@ -464,17 +448,4 @@ def _build_registry(
for schema, fn in todo_bindings:
registry.register(schema=schema, fn=fn)

# Skills
for skill in skills:
registry.register(
schema={
"type": "function",
"function": {
"name": skill.name,
"description": skill.description,
"parameters": skill.parameters_schema,
},
},
fn=_make_skill_wrapper(skill=skill, ctx=skill_ctx),
)
return registry
3 changes: 3 additions & 0 deletions src/team_harness/skills/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1,4 @@
from team_harness.skills.loader import load_skill_metadata
from team_harness.skills.loader import SkillMetadata

__all__ = ["SkillMetadata", "load_skill_metadata"]
Loading
Loading