Packages

fnord

0.9.36

AI code archaeology

Current section

Files

Jump to
fnord lib external_configs skill.ex
Raw

lib/external_configs/skill.ex

defmodule ExternalConfigs.Skill do
@moduledoc """
A parsed Agent Skill from a `SKILL.md` file.
The SKILL.md format is shared between Claude Code and Cursor: a directory
containing a required `SKILL.md` with YAML frontmatter (name + description
at minimum) and a markdown body. The `flavor` field distinguishes which
ecosystem the skill came from (`:claude` or `:cursor`) so we can present
them under separate headings to the coordinator.
## Self-delegation opt-out
External skills frequently exist as shims that delegate Claude Code or
Cursor invocations back to fnord (`fnord ask -W . -q ...`). When fnord
itself loads such a skill, the directionality is reversed and the agent
shells out to a fresh fnord process - infinite recursion.
Two fields mark a skill as "do not expose to fnord agents":
- `:fnord_skip` - boolean; true iff the skill should be filtered before
being offered to fnord's coordinator.
- `:fnord_skip_reason` - `:frontmatter` (explicit opt-out via the
`fnord_skip: true` frontmatter key) or `:body_invokes_fnord` (fallback
body scan for `fnord ask` / `fnord-dev ask`), or nil when not skipped.
An explicit `fnord_skip: false` in the frontmatter overrides the body scan.
This lets a skill author opt back in if their SKILL.md legitimately
references `fnord ask` in passing.
"""
defstruct [
:name,
:description,
:when_to_use,
:body,
:path,
:flavor,
:source,
fnord_skip: false,
fnord_skip_reason: nil
]
@type flavor :: :claude | :cursor
@type source :: :global | :project
@type skip_reason :: :frontmatter | :body_invokes_fnord | nil
@type t :: %__MODULE__{
name: String.t(),
description: String.t() | nil,
when_to_use: String.t() | nil,
body: String.t(),
path: String.t(),
flavor: flavor(),
source: source(),
fnord_skip: boolean(),
fnord_skip_reason: skip_reason()
}
# Matches a shell invocation of `fnord ask` or `fnord-dev ask` regardless of
# surrounding punctuation (backticks, code fences). Word boundaries keep
# `xfnord ask` and `fnordask` from triggering. Case-sensitive: shell
# commands are case-sensitive, and English prose like "Fnord ask" is
# almost always the project name in a sentence, not a command.
@fnord_invocation_re ~r/\bfnord(?:-dev)?\s+ask\b/
@doc """
Load a skill from a directory containing `SKILL.md`.
"""
@spec from_dir(String.t(), flavor(), source()) :: {:ok, t} | {:error, term()}
def from_dir(dir, flavor, source) when is_binary(dir) do
skill_path = Path.join(dir, "SKILL.md")
with {:ok, content} <- File.read(skill_path),
{:ok, %{frontmatter: fm, body: body}} <-
ExternalConfigs.Frontmatter.parse(content) do
name =
fetch_string(fm, "name") ||
dir |> Path.basename() |> String.downcase()
description = fetch_string(fm, "description")
when_to_use = fetch_string(fm, "when_to_use")
trimmed_body = String.trim(body)
{fnord_skip, fnord_skip_reason} = detect_fnord_skip(fm, trimmed_body)
{:ok,
%__MODULE__{
name: name,
description: description,
when_to_use: when_to_use,
body: trimmed_body,
path: skill_path,
flavor: flavor,
source: source,
fnord_skip: fnord_skip,
fnord_skip_reason: fnord_skip_reason
}}
end
end
# Resolves the skip decision. Order:
# 1. Explicit `fnord_skip: true` -> skip (reason :frontmatter).
# 2. Explicit `fnord_skip: false` -> load, even if the body matches.
# 3. Body invokes `fnord ask` -> skip (reason :body_invokes_fnord).
# 4. Otherwise -> load.
defp detect_fnord_skip(fm, body) do
case fetch_bool(fm, "fnord_skip") do
true -> {true, :frontmatter}
false -> {false, nil}
nil -> body_skip(body)
end
end
defp body_skip(body) do
if Regex.match?(@fnord_invocation_re, body) do
{true, :body_invokes_fnord}
else
{false, nil}
end
end
defp fetch_string(fm, key) do
case Map.get(fm, key) do
v when is_binary(v) ->
case String.trim(v) do
"" -> nil
trimmed -> trimmed
end
_ ->
nil
end
end
defp fetch_bool(fm, key) do
case Map.get(fm, key) do
true -> true
false -> false
"true" -> true
"false" -> false
_ -> nil
end
end
end