@sunnygg/distributor
Distributor
Synchronize one canonical Agent Skills tree across your agent harnesses.
npx @sunnygg/distributor init
Getting started
npx @sunnygg/distributor init
npx @sunnygg/distributor import
npx @sunnygg/distributor sync
init creates the Distributor configuration and prompts you to choose project
or global syncing, a skill source, and target agent harnesses. It also scans
supported harness directories and offers to import any existing skills it
finds.
import scans every native and compatible skill directory declared by the
available harness adapters. You can select which discovered skills to copy into
the configured source. Existing source skills are preserved. If Distributor
has not been initialized, import runs interactive initialization first.
sync links the skills from that source into the configured agent harnesses.
Add your first skill
Initialize a project with the defaults, add a skill, inspect the plan, and sync it:
distributor init --yes
mkdir -p .agents/skills/code-review
cat > .agents/skills/code-review/SKILL.md <<'EOF'
---
name: code-review
description: Review code changes.
---
EOF
distributor sync --dry-run
distributor sync
init --yes creates the default config, .agents/skills, and the local-state
ignore file without overwriting existing content. Plain distributor init
prompts when run interactively. In a non-interactive terminal, use --yes.
Each non-hidden directory immediately under the source root that contains a
regular file named exactly SKILL.md is a skill and must have valid skill
frontmatter. Other non-hidden files and directories are helper content and are
preserved at the same relative paths as independent file links. The optional
agents/openai.yaml (or agents/openai.yml) is emitted only to Codex targets
and omitted from Claude, Cline, and every other non-Codex target.
Command details
distributor # show help
distributor --help # show help
distributor help # show help
distributor help sync # show sync help
distributor --version # print the installed version
distributor -V # print the installed version
distributor version # print the installed version
distributor init # interactive initialization
distributor init --yes # initialize with defaults
distributor init -y # same as --yes
distributor import # import skills from harness directories
distributor status # show skill and reference status
distributor sync # sync every enabled harness
distributor sync --harness codex # sync one enabled harness
distributor sync --dry-run # plan without writing
distributor remove # remove every managed link
distributor status reports the number of source skills, the number of active
skill-to-placement references, and whether those references are up to date. It
uses the same read-only planning checks as sync and does not write links,
directories, or managed state.
--harness <id> may appear only once and must name an available harness that
is enabled in the project config. A dry run follows the same config, skill,
adapter, state, target, conflict, and diff-inspection path as a real sync, then
stops before the first write-capable operation. It does not create directories,
links, .distributor files, or filesystem metadata.
Configuration
Distributor searches upward from the current directory for the nearest
distributor.config.json, distributor.config.js, or
distributor.config.ts. In a Git worktree, discovery stops at the worktree
root. The config's directory is the project root.
The exact config generated by distributor init --yes is:
{
"scope": "project",
"source": ".agents/skills",
"harnesses": [
"codex",
"claude-code",
"opencode",
"cursor",
"gemini-cli",
"antigravity",
"github-copilot",
"openhands",
"pi",
"cline",
"goose",
"crush",
"qwen-code",
"kilo-code",
"roo-code",
"trae-agent"
]
}
scope defaults to project when omitted. project links skills into harness
directories in the project. global still reads the source from the project,
but links those skills into user-level harness directories. Harnesses that
support ~/.agents/skills share that target; other harnesses use their native
user skill directory.
source defaults to .agents/skills when omitted. A string harness, or an
object without targets, requests automatic placement for the configured
scope. The expanded form below shows explicit project and user placements plus
a custom project target:
{
"scope": "project",
"source": ".agents/skills",
"harnesses": [
{
"name": "codex",
"targets": [{ "placement": "project" }, { "placement": "user" }]
},
{
"name": "claude-code",
"targets": [
{
"placement": "project",
"path": ".custom/claude-skills"
},
{ "placement": "user" }
]
},
{
"name": "opencode",
"targets": [{ "placement": "agents-project" }, { "placement": "user" }]
}
]
}
Configuration rules:
harnessesis a non-empty array. Harness IDs must be unique.- A target may contain
placement,path, or both. Whenplacementis omitted, the adapter's default project placement supplies its behavior. - Relative paths resolve from the project root.
~,$HOME, and$PROJECT_ROOTare supported; other variable syntax is rejected. - Only project and explicitly selected user placements are allowed. Admin, system, plugin, package, and configured scopes are rejected.
pathexplicitly overrides the selected placement's target root and is how to choose a custom directory.- Unknown fields, placements, adapters, empty target arrays, and duplicate effective targets are errors.
JavaScript and TypeScript configs use the same shape and must default-export their value:
import type { DistributorConfig } from "@sunnygg/distributor";
const config = {
source: ".agents/skills",
harnesses: [
"codex",
"claude-code",
"opencode",
"cursor",
"gemini-cli",
"antigravity",
"github-copilot",
"openhands",
"pi",
"cline",
"goose",
"crush",
"qwen-code",
"kilo-code",
"roo-code",
"trae-agent",
],
} satisfies DistributorConfig;
export default config;
JavaScript and TypeScript config files are trusted executable code and are loaded by Distributor. Use them only when you trust the project. Skill Markdown, YAML, scripts, references, and assets are read as data and are never executed by Distributor.
Writing custom adapters
Distributor loads custom adapters from .distributor/adapters in the exact
directory where the command is run. It reads immediate files ending in
.json, .js, or .ts; other files and nested directories are ignored.
The adapter's name field is its harness ID, regardless of the filename.
JSON files contain the adapter object directly. JavaScript and TypeScript files
must default-export it. Import the public HarnessConfig type to check a
TypeScript adapter:
import type { HarnessConfig } from "@sunnygg/distributor";
const adapter = {
name: "team-agent",
displayName: "Team Agent",
adapterStatus: "available",
supportsNativeSkills: true,
placements: [
{
id: "project",
item: "skills",
support: "native",
scope: "project",
defaultPath: ".team-agent/skills",
createIfMissing: true,
},
],
} satisfies HarnessConfig;
export default adapter;
sources and verifiedAt are optional. When present, sources must be a
non-empty array of URLs and verifiedAt must be an ISO date such as
2026-07-17.
An available adapter may omit defaultProjectPlacementId when it declares
exactly one placement; Distributor uses that placement as the default.
Adapters with multiple placements must identify their default project
placement explicitly.
Adapter IDs must be unique across built-in and custom adapters. Distributor
stops with an error instead of overriding either adapter when it finds a
duplicate. Available custom adapters can be named in project configuration and
are included in both interactive initialization and distributor init --yes.
JavaScript and TypeScript adapters are trusted executable code and run when
Distributor loads them. Commit only adapters you trust. New initialization
metadata keeps .distributor/adapters visible to Git while ignoring local
state; existing .distributor/.gitignore files may need these rules:
!adapters/
!adapters/**
Available adapters and placements
| Harness ID | Automatic project behavior | Explicit selectable placements |
|---|---|---|
codex |
Uses .agents/skills; the default source is already satisfied |
project (.agents/skills), user (~/.agents/skills) |
claude-code |
Uses .claude/skills; the default source is linked there |
project (.claude/skills), user (~/.claude/skills) |
opencode |
Uses .opencode/skills, but also recognizes .agents/skills and .claude/skills |
project, agents-project, claude-project, user, agents-user, claude-user |
cursor |
Uses .cursor/skills and recognizes .agents/skills |
project, agents-project, user, agents-user |
gemini-cli |
Uses .gemini/skills and recognizes .agents/skills |
project, agents-project, user, agents-user |
antigravity |
Uses .agents/skills; the default source is already satisfied |
project, legacy-project, user |
github-copilot |
Uses .github/skills and recognizes shared Agent/Claude paths |
project, agents-project, claude-project, user, agents-user |
openhands |
Uses .agents/skills; the default source is already satisfied |
project, legacy-project, user, openhands-user |
pi |
Uses .pi/skills and recognizes .agents/skills |
project, agents-project, user, agents-user |
cline |
Uses .cline/skills and recognizes Cline Rules and Claude paths |
project, clinerules-project, claude-project, user |
goose |
Uses .agents/skills; requires the Summon extension |
project, goose-project, claude-project, user, claude-user |
crush |
Uses .crush/skills and recognizes shared Agent/Claude/Cursor paths |
project, agents-project, claude-project, cursor-project, user, agents-user, claude-user |
qwen-code |
Uses .qwen/skills |
project, user |
kilo-code |
Uses .kilo/skills and recognizes the shared Agent path |
project, agents-project, user |
roo-code |
Uses .roo/skills and recognizes .agents/skills |
project, agents-project, user, agents-user |
trae-agent |
Uses .trae/skills |
project, user |
Automatic resolution checks all native and compatible project placements before choosing a fallback. An explicit target array uses exactly the placements listed in that array.
Project placements stay inside the project unless an explicit path says
otherwise. Each adapter declares its user paths; inspect the table's placement
IDs or the harness specification before selecting one explicitly.
If a project-local source is linked to a target outside the project, Distributor warns and uses an absolute source link. Moving or deleting the project will break that external target.
Ownership, conflicts, and stale targets
Distributor records ownership in <project-root>/.distributor/state.json.
This is local implementation state and should not be committed. A state entry
grants ownership only while the target remains a symlink with the exact raw
link value Distributor recorded.
- An existing exact expected symlink can be adopted without replacing it.
- An unmanaged file, directory, different symlink, or user-modified managed symlink is a conflict and is never overwritten.
- Planning inspects every requested harness before applying. One conflict makes the entire plan non-applicable, with no target or state writes.
- Independent apply-time failures may leave other successful operations in
place and recorded; the run exits
1and remains safe to retry. - A managed target becomes stale when its source disappears, its harness is removed, or its placement changes. Stale targets are removed during sync after Distributor verifies that the recorded link is unchanged.
distributor removeremoves every recorded target that is still the exact symbolic link Distributor recorded. Missing targets are cleared from state; changed links and non-links are reported and preserved. Empty target directories are left in place.
Exit codes
| Code | Meaning |
|---|---|
0 |
Success, including no-op, warning-only, and stale-only runs |
1 |
Operational, source, state, conflict, symlink, or filesystem failure |
2 |
Invalid command usage or invalid/missing project configuration |
Initial-release scope
All sixteen harnesses in the configuration specification are available. This release intentionally has no force mode, copy or junction fallback, automatic cleanup during sync, transforms, generated artifacts, or content/frontmatter rewriting. Distributor creates direct file symlinks and never directory symlinks.