NAME

Skilletor - Install skills, agents and rules from remote sources – the Perl port of skilletor

VERSION

version 0.001

SYNOPSIS

use Skilletor;

my $sk = Skilletor->new( home => $ENV{HOME}, project_dir => $project );

# The skill as an include, straight from the source tree or the shared cache:
my $file  = $sk->resolve_skill('perl-moo');   # …/cache/79eb247685294166/skills/perl-moo/SKILL.md
my $agent = $sk->resolve_agent('reviewer');   # the project's wins over the personal one
my @rules = $sk->resolve_rules('style');      # both scopes' rules, the user's first
my $item  = $sk->resolve_item( skill => 'perl-moo' );
# { type, name, scope, source, version, source_dir, path, dir, file, files,
#   description, vars, declaredBy, via?, templated, templates }

for my $rule ( @{ $sk->items( type => 'rule' ) } ) { ... }

# Startup without the network: what the cache holds.
my $offline = Skilletor->new( home => $ENV{HOME}, project_dir => $project, fetch => 'never' );

# Install for Claude Code, as `skilletor sync` does:
use Skilletor::Report qw( report_text exit_code );
my $report = $sk->sync;                      # the reference's SyncReport
print report_text($report) || 'skilletor: up to date', "\n";
my $due = $sk->check->{changed};             # is a sync due?
my $status = $sk->status;

# The reference's resolveItems, field for field:
my $resolution = $sk->resolve( scope => 'all' );

DESCRIPTION

skilletor installs skills, agents and rules from remote sources (git repositories, https tarballs, local directories) into Claude Code, Codex and Hermes, at user and project scope. Its reference implementation is TypeScript; this distribution is its Perl port, living in the same repository, for Perl programs – an agent such as Langertha::Raider – that must resolve skills exactly as skilletor does, straight from the same config and the same cache, without Node, and sync them into Claude Code's directories as the CLI does.

The port shares everything on disk with the reference: the config files (~/.claude/skilletor.json, <project>/.claude/skilletor.json, skilletor.local.json), the source cache under ~/.claude/skilletor/cache (the same directory names, so one fetch serves both), trust.json, the sync lock, the lock files and the state records check reads, and the bytes either writes. Error messages and warnings are the reference's, word for word; the shared conformance cases (conformance/ in the repository) hold both to that.

The lookups ("resolve_skill" and its siblings, "items") answer "where is skill X" the way a sync would install it, but without installing anything: an item is served from the source tree it lies in – a local directory or the cache dir a git or url source was fetched into. "sync", "check" and "status" are the CLI's commands for the Claude Code target (Skilletor::Engine).

Scopes and precedence

Every scope resolves on its own, as a sync installs it: the user scope from the user config alone, the project scope from the merged project, local and user config (spec §3). Within a scope a type and name is one item at most – the overlap rules (explicit entries over wildcards and bundles, two sources offering one name skip it) see to that. Across the scopes a sync installs an item declared in both twice, once per scope; which one Claude Code then uses depends on the type, and a lookup without a scope follows Claude Code's documented rules (names ignore case, as everywhere in skilletor):

  • Skills: the personal one wins. "Enterprise over personal, and personal over project": with deploy in both ~/.claude/skills/ and the project's .claude/skills/, /deploy runs the personal one (code.claude.com/docs/en/skills). "resolve_skill" answers the user's.

  • Agents: the project's wins. Claude Code takes managed, then --agents, then the project's .claude/agents/, then ~/.claude/agents/, then plugins (code.claude.com/docs/en/sub-agents). "resolve_agent" answers the project's.

  • Rules: neither wins. Claude Code loads the user's rules before the project's, and neither set overrides the other (code.claude.com/docs/en/memory): both are in effect. "resolve_rules" answers both, the user's first; "resolve_rule" answers one only when one scope declares it – with both it throws Skilletor::Error::Ambiguous rather than hide one.

Pass scope => 'user' or scope => 'project' to look in one scope only; then there is never more than one.

Fetching

"fetch" is the fetch policy: always (default) resolves every source as a sync does – a git fetch, a url download, a failure falling back to the cache with a warning; missing fetches only a source with no usable cache yet; never touches no network and serves what the cache holds, a source that was never fetched being a warning. Every lookup method answers from one resolution of every scope, made at the first call; "refresh" drops it, and so does "sync". The policy holds for "sync" too: with never it syncs from the cache alone.

Templates

The Perl port renders no Nunjucks. An item with a .njk file resolves like any other – its record says templated => 1 and lists the templates – but "resolve_skill" and its siblings refuse it with a Skilletor::Error::Unsupported: an unrendered template is never served as if it were the item. A "sync" never installs one either: it warns and leaves such an item as the reference installed it, and its scope stays open for the reference's next sync ("Templates" in Skilletor::Engine).

Claude Code only

"sync", "check" and "status" serve Claude Code alone. A scope that targets Codex or Hermes – by "targets", or by Codex detected on the machine without "targets" – is refused with an error in the report, and a sync is refused where the reference left Codex or Hermes state behind; nothing is written then ("Claude Code only" in Skilletor::Engine). Unlike the lookups, they need an agent harness, as the reference does.

Modules

CONVENTIONS

Data structures carry the reference's field names (derivedName, declaredBy, trustRequests) so they compare with the TypeScript side directly; methods are snake_case. Paths are character strings, as the reference's are; Perl hands their UTF-8 bytes to the file system. A home directory is always passed in – nothing reads $ENV{HOME} on its own.

home

Required: the home whose .claude/skilletor.json and cache are used.

project_dir

The project; none (or the home dir itself) means the user scope alone.

fetch

always (default), missing or never; see "Fetching".

require_harness

True: the lookups fail as the reference does when no agent harness (Claude Code, Codex) is detected and the user config names no targets. Default false: resolving needs no harness. "sync", "check" and "status" always need one.

state_root

cache_root

codex_home

timeout_ms

lock_timeout_ms

Passed to Skilletor::Resolver; its defaults without them.

is_git_work_tree

git_tracked

git_remote

plugin_root

Passed to Skilletor::Engine, where they are described; the questions to git and the plugin root they answer default to git and CLAUDE_PLUGIN_ROOT.

resolver

The Skilletor::Engine (a Skilletor::Resolver) doing the work.

engine

The same as "resolver".

resolve

my $report = $sk->resolve( scope => 'user' | 'project' | 'all' );

A fresh resolve, the reference's ResolveReport (see Skilletor::Resolver): { scopes => [ { scope, items, sources, warnings, trustRequests } ] }, or { scopes => [], error => $message } on a config error.

resolve_item

my $rec = $sk->resolve_item( skill => 'perl-moo', scope => 'all' );

The record of the item a lookup finds by the type's precedence ("Scopes and precedence"), or undef for a name no scope resolves to (also when the item was declared but its source or the item itself could not be read – the warning in "resolve" says why):

{
  type => 'skill', name => 'perl-moo', scope => 'project',
  source => 'shared', version => 'git:30993b3',
  source_dir => '/home/me/.claude/skilletor/cache/b966aec947fa313b',
  path  => 'skills/perl-moo',                  # relative to source_dir, as in the report
  dir   => '…/skills/perl-moo',                # a skill's directory; an agent's or rule's parent
  file  => '…/skills/perl-moo/SKILL.md',       # the main file
  files => [ '…/skills/perl-moo/SKILL.md', … ],
  description => 'Write Moo classes',          # when the frontmatter has one
  vars => { … }, declaredBy => [ … ], via => [ … ],
  templated => 0, templates => [],             # the .njk files, relative
}

A rule both scopes declare throws Skilletor::Error::Ambiguous unless a scope is given. A config error throws Skilletor::Error::Config; an unreadable lock file and a sync lock held too long throw too.

resolve_items

my $recs = $sk->resolve_items( rule => 'style' );

Every record of the type and name, one per scope that declares it, in the type's order: skills user then project, agents project then user, rules user then project. The first is the one Claude Code uses for a skill or an agent; for rules all are in effect.

resolve_skill

my $file = $sk->resolve_skill( 'perl-moo', scope => 'all' );

The absolute path of the skill's SKILL.md in its source tree, or undef; with both scopes declaring the skill, the user's. Throws Skilletor::Error::Unsupported for a templated skill.

resolve_agent

The agent's <name>.md, as "resolve_skill"; with both scopes declaring the agent, the project's.

resolve_rule

The rule's <name>.md, as "resolve_skill" – when one scope declares it. With both, Claude Code loads both: this throws Skilletor::Error::Ambiguous; ask "resolve_rules", or pass a scope.

resolve_rules

my @files = $sk->resolve_rules( 'style', scope => 'all' );

The <name>.md of every scope's rule of the name, the user's first; none when no scope declares it. Throws Skilletor::Error::Unsupported when one of them is templated.

items

my $records = $sk->items( type => 'skill', scope => 'all' );

Every item record ("resolve_item"), skills first, then agents, then rules, each type in its order: one skill per name (the user's winning), one agent per name (the project's winning), and every rule of every scope. type and scope are optional filters.

sync

my $report = $sk->sync( scope => 'all', force => 0 );

Install what the config declares for Claude Code, as skilletor sync does, and drop the resolution the lookups answer from. The reference's SyncReport ("sync" in Skilletor::Engine); a config error, no agent harness, a scope targeting another harness are its error, with nothing touched. Throws when a write fails midway (the lock then owns what landed, marked partial). Skilletor::Report words it.

check

my $check = $sk->check( scope => 'all' );
print "a sync is due\n" if $check->{changed};

The reference's CheckReport ("check" in Skilletor::Engine): whether a sync is due, without one.

status

my $status = $sk->status( scope => 'all' );

The reference's StatusReport ("status" in Skilletor::Engine), offline.

refresh

Drop the resolution the lookup methods answer from; the next call resolves again (and fetches, by "fetch").

SEE ALSO

https://github.com/Getty/skilletor – the reference implementation and its design spec, docs/superpowers/specs/2026-09-22-skilletor-design.md.

SUPPORT

CONTRIBUTING

Contributions are welcome! Please fork the repository and submit a pull request.

AUTHOR

Torsten Raudssus <getty@cpan.org>

COPYRIGHT AND LICENSE

This software is copyright (c) 2026 by Torsten Raudssus <torsten@raudssus.de> https://raudssus.de/.

This is free software; you can redistribute it and/or modify it under the same terms as the Perl 5 programming language system itself.