NAME

Langertha::Raider::Context::Skills - Internal catalogue of the skills raider lists and loads on demand

VERSION

version 0.504

SYNOPSIS

# Internal to Langertha-Raider -- no API promise.
my $skills = Langertha::Raider::Context::Skills->new(
  roots => [
    { dir => "$root/.raider/skills", source => '.raider/skills', layout => 'skills' },
    { dir => "$root/my-skills",      source => '--skills my-skills', layout => 'flat' },
  ],
  plugins => [ { name => 'superpowers', dirs => [ @skill_dirs ] } ],
);
my $prompt = $skills->prompt_text;            # undef when nothing is listed
my $text   = $skills->tool_text('pdf', 'report.pdf');

DESCRIPTION

Internal module. Its interface may change without notice.

The skill catalogue of ADR 0019 §4, which holds the custom commands too (ADR 0030 §1: commands are skills). It takes roots in precedence order and the skill directories and command paths of plugins, and knows every entry's name, description and body. Which roots exist in which layout mode is the caller's decision; this module only reads what it is given. A root or skill directory that does not exist is skipped silently.

An entry -- a skill or a command -- is a plain hash ref:

{
  name            => 'pdf',               # the identifier the model calls, /pdf for the user
  kind            => 'skill',             # or 'command' (a commands root or plugin commands)
  description     => 'Work with PDF files ...',
  dir             => '/abs/skills/pdf',   # base directory; a command's: the directory of its file
  file            => '/abs/skills/pdf/SKILL.md',
  source          => '.claude/skills',    # root label, or 'plugin superpowers'
  frontmatter     => { name => 'pdf', description => '...', ... },   # every key kept
  model_invocable => 1,                   # 0 iff disable-model-invocation is true
  user_invocable  => 1,                   # 0 iff user-invocable is false
  plugin_root     => '/abs/plugin',       # plugin entries only: ${CLAUDE_PLUGIN_ROOT}
}
  • Name. In the skills layout the skill directory's name, as Claude Code uses it as the identifier. A frontmatter name that differs stays in frontmatter but does not rename the skill. In the flat layout the file name without .md. In the commands layout the file's path below the root without .md, directories joined with : (frontend/component.md is frontend:component). Plugin skills are <plugin>:<dir>, plugin commands <plugin>:<name> (plugin:sub:name below a subdirectory).

  • Booleans. disable-model-invocation and user-invocable take true / yes / on / 1 and false / no / off / 0, in any case. Unset, disable-model-invocation is false and user-invocable true.

  • Description. The frontmatter description (whitespace collapsed), else the first non-empty line of the body that is not a Markdown heading, trimmed and cut to 250 characters.

  • Frontmatter. A leading --- ... --- block, read by Langertha::Raider::Context::Frontmatter: YAML, else the lenient line reader for what YAML rejects but Claude Code reads (true / false are booleans for disable-model-invocation and user-invocable). Only when that finds no key either, or the YAML is not a mapping, the frontmatter is empty and "problems" gets an entry; the skill is still loaded.

  • Precedence. Roots in the given order, then plugins (each plugin's skills, then its commands). The first entry of a name wins; later ones are reported by "shadowed". One exception: a skill beats a command of the same name wherever either came from -- the command is shadowed (Claude Code). Plugin names carry a :, so they never meet a root skill (a root command in a subdirectory can).

roots

Roots, highest precedence first. Required. Each a hash ref:

{ dir => '/abs/dir', source => 'label', layout => 'skills' | 'flat' | 'commands' }

skills: a directory of skill directories, each with a SKILL.md. flat: a directory of *.md files, each one skill (the old --skills DIR behaviour); without top-level *.md files it holds no skills. file: one skill file (the hash has file; dir is its parent): named by its file name without .md; a file that is not there is a problem. commands: a directory searched recursively for *.md (hidden entries skipped), each one command (kind command), named as above.

A root may name its own base (see "base").

plugins

Plugin skill directories and command paths, after all roots. Defaults to none. Each a hash ref

{
  name     => 'plugin',
  dirs     => [ '/abs/skill/dir', ... ],              # Plugins->skill_dirs
  commands => [ '/abs/commands', '/abs/x.md', ... ],  # Plugins->command_dirs, optional
  root     => '/abs/plugin',                          # optional, becomes plugin_root
}

-- the name, "skill_dirs" in Langertha::Raider::Context::Plugins, "command_dirs" in Langertha::Raider::Context::Plugins and root of a plugin. A command path is a directory, read like a commands root, or one *.md file, named by its file name.

base

The directory the paths: globs of a skill are relative to, unless its root names its own base (the directory with the .claude/ or .raider/ the skill lives in). Optional; without it a skill with paths: can never be activated.

activated

HashRef of the names of skills with paths: that a touched file has activated ("activate_for"). Shared with the caller, who keeps it across catalogues -- a /reload builds a new one.

skills

for my $s ($skills->skills) { say $s->{name}.' ('.$s->{source}.')' }

Every entry (skill or command) that won its name, in precedence order: roots in the given order, sorted by name inside a root, then the plugins in the given order. Model- or user-invocable or not. A skill that took its name from a command stands where the skill was read.

shadowed

for my $s ($skills->shadowed) { say $s->{name}.' in '.$s->{source}.' shadowed by '.$s->{by} }

Entries that lost their name to an earlier one, or commands that lost it to a skill, each { name, source, file, by } -- by is the winner's source.

problems

warn $_->{file}.': '.$_->{reason}."\n" for $skills->problems;

Skill files that could not be read (not loaded) or whose frontmatter is broken (loaded with an empty frontmatter), each { file, reason }.

find

my $skill = $skills->find('/pdf');   # or 'pdf', 'superpowers:brainstorming'

The skill of exactly that name (a leading / is ignored), or undef. Finds skills that are not model-invocable too.

find_user_invocable

my $entry = $skills->find_user_invocable('/frontend:component');

The entry a user runs as /name: "find", but only an entry with user_invocable; undef otherwise. Commands and skills alike, also those that are not model-invocable.

user_invocable

for my $e ($skills->user_invocable) { say '/'.$e->{name} }

The "skills" with user_invocable, in catalogue order.

listed

my @offered = $skills->listed;

The "skills" the model may see and load ("is_listed").

is_listed

if ($skills->is_listed($skill)) { ... }

True for a skill with model_invocable that has no paths: in its frontmatter, or whose paths: a touched file matched ("activate_for").

invocable

The "skills" with model_invocable, listed or not yet: the skills the model can have now or later.

paths_of

my @globs = $skills->paths_of($skill);

The globs of the paths: frontmatter of a skill (Claude Code's conditional skills): a YAML list, or a string with the globs separated by commas (not inside {a,b}). None for a skill without. A glob is matched against the path relative to the skill's base: * within a path segment, ** over any number of segments, ? one character, {a,b} alternatives; a glob without / matches the file name in any directory.

add_roots

my @new = $skills->add_roots({ dir => ..., source => ..., layout => 'skills', base => ... });

Reads more roots into the catalogue, after everything it holds: a name that is known keeps its entry (the new one is "shadowed") -- unless the known one is a command and the new one a skill. Returns the entries that are new. Nested .claude/skills found while working in a subtree come in this way.

activate_for

my @new = $skills->activate_for('/abs/path/to/file');

Marks every skill whose paths: glob matches the path (relative to its base) as "activated", and returns those that were not activated before.

body

my $md = $skills->body('pdf');

The skill file's text without the frontmatter and leading blank lines, or undef for an unknown name.

tool_text

my $text = $skills->tool_text('pdf', 'report.pdf');

What the skill tool returns (the shape of Claude Code's Skill tool), or undef for an unknown name:

Base directory for this skill: /abs/skills/pdf

<body>

A command (kind command) has no Base directory line: its text is the body alone (ADR 0030 §2).

Every $ARGUMENTS in the body is replaced by $args; without $args (undefined or empty) by nothing, as Claude Code does. When the body has no $ARGUMENTS and $args is given, ARGUMENTS: $args is appended after a blank line.

This is the plain text without a renderer. The application's skill tool renders through Langertha::Raider::SlashCommand instead ("skill_tool_text" in Langertha::Raider::Application, ADR 0030 §2): every placeholder and !`cmd`, the same shape.

prompt_text

my $block = $skills->prompt_text;

The system-prompt block that lists the "listed" skills, or undef when none is listed:

Skills: specialised instructions loaded on demand. When a task matches a
skill's description, call the `skill` tool with its name BEFORE acting,
then follow what it returns. Available skills:
- pdf: Work with PDF files ...
- superpowers:brainstorming: ...

(the first paragraph is one line). A skill without description is listed by its name alone.

announce_text

my $text = $skills->announce_text(@new);

The note for skills that became available during the session (a nested .claude/skills or a paths: match), worded as Claude Code does:

The following skills are available for use with the Skill tool:

- name: description

undef for none.

SEE ALSO

SUPPORT

Issues

Please report bugs and feature requests at https://github.com/Getty/langertha-raider/issues.

IRC

Join #langertha on irc.perl.org or message Getty directly.

CONTRIBUTING

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

AUTHOR

Torsten Raudssus <torsten@raudssus.de> https://raudssus.de/

COPYRIGHT AND LICENSE

This software is copyright (c) 2026 by Torsten Raudssus.

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

https://raudssus.de/