---
slug: "skilled-labour"
source_type: "readme"
source_url: "https://cdn.jsdelivr.net/gh/jwdevantier/skilled-labour@main/README.md"
repo: "https://github.com/jwdevantier/skilled-labour"
source_file: "README.md"
branch: "main"
---
# skilled-labour

A [pi](https://github.com/earendil-works/pi-coding-agent) extension that makes
small models actually load skills by leveraging their ability to issue tool
calls.

## How skills work
Pi implements a skills system. At startup, it builds an `<available_skills>`
block, summarizing each discovered skill's name and when to load, and telling
the model where the skill is to be found.
The model is then told (in text) to:
* consider whether any skill(s) are relevant to present tasks
* load those skill(s) by calling the `read` tool to read the `SKILL.md` file first
  * (potentially reading supporting files, depending on the skill)

## The problem
- Models excel at what they were trained on.
- Even small models are trained heavily on tool-calling
- Local models are bad at using skills; probably no trained to do so

## The solution
**Make loading a skill a _tool call_** - leverage existing strengths

---


## What skilled-labour does

1. Strips pi's skills block and instructions from the system prompt.
2. Appends a compact replacement carrying only skill names and descriptions.
3. Registers a `load_skill(NAME)` tool for the LLM to load a skill
    * the tool resolves the path to the skills `SKILL.md` and loads it
    * the response explicitly tells the LLM the skill's directory so referenced supporting files can be loaded if need be.


`load_skill` is idempotent: a re-call for an already-loaded skill no-ops (so
the model's reflexive re-calls don't bloat context), and `session_compact`
clears the "loaded" set so a post-compaction call re-injects the full body
exactly when re-injection is needed.

---

## Install

Place this directory in your pi extensions folder:

* Linux/MacOS: `~/.pi/agent/extensions`
* Windows: `%USERPROFILE%\.pi\agent\extensions\`

---

## Implementation Details

### Before - (default SKILLs implementation)

Injected into the system prompt verbatim. One `<skill>` block per skill, each
carrying a `<location>` path the model is expected to copy into a `read` call:

```
The following skills provide specialized instructions for specific tasks.
Use the read tool to load a skill's file when the task matches its description.
When a skill file references a relative path, resolve it against the skill directory (parent of SKILL.md / dirname of the path) and use that absolute path in tool commands.

<available_skills>
  <skill>
    <name>odin-programming</name>
    <description>Write, create, and edit Odin programs...</description>
    <location>/home/nixos/.pi/agent/skills/odin-programming/SKILL.md</location>
  </skill>
  <skill>
    <name>nix-flakes-editor</name>
    <description>Write, create, and edit Nix flakes...</description>
    <location>/home/nixos/.pi/agent/skills/nix-flakes-editor/SKILL.md</location>
  </skill>
</available_skills>
```

The model is then expected to *read prose instructions* and spontaneously issue
a `read` tool call against one of those `<location>` paths. Small models
don't.

### After — (skilled-labour)

The verbose block is stripped and replaced with a compact one-line-per-skill
section (no `<location>` — the tool resolves it) plus a pointer to `load_skill`:

```
<available_skills>
  <skill name="odin-programming">Write, create, and edit Odin programs...</skill>
  <skill name="nix-flakes-editor">Write, create, and edit Nix flakes...</skill>
</available_skills>
Call the load_skill tool with a skill's name to load its full instructions, BEFORE acting on a task the skill covers. It is cheap and strongly recommended. If a skill's instructions are already in the conversation above (loaded earlier this session), do not reload it.
```

And `load_skill` is registered as a real tool whose description and guidelines
carry the same nudge — reconsidered by the model every turn:

```ts
pi.registerTool({
  name: "load_skill",
  description:
    "Load a skill's full instructions by name. Call this BEFORE acting on any task that a listed skill covers — it is cheap (one call) and nearly always improves results. No-op if the skill is already loaded. Pass the exact skill name from the <available_skills> list in the system prompt.",
  promptGuidelines: [
    "Before acting on a task, check the <available_skills> list in the system prompt. If any skill matches the task, call load_skill with its name FIRST, then follow the instructions it returns. Do not write code or run commands for a skill-covered task without loading the skill first.",
  ],
  parameters: Type.Object({
    name: Type.String({ description: 'Exact skill name from the <available_skills> list, e.g. "odin-programming".' }),
  }),
  async execute(_toolCallId, params) { /* resolve name -> read file -> return body */ },
});
```

