Packages
fnord
0.9.36
0.9.40
0.9.39
0.9.38
0.9.37
0.9.36
0.9.35
0.9.34
0.9.33
0.9.32
0.9.31
0.9.30
0.9.29
0.9.28
0.9.27
0.9.26
0.9.25
0.9.24
0.9.23
0.9.22
0.9.21
0.9.20
0.9.19
0.9.18
0.9.17
0.9.16
0.9.15
0.9.14
0.9.13
0.9.12
0.9.11
0.9.10
0.9.9
0.9.8
0.9.7
0.9.6
0.9.5
0.9.4
0.9.3
0.9.2
0.9.1
0.9.0
0.8.99
0.8.98
0.8.97
0.8.96
0.8.95
0.8.94
0.8.93
0.8.92
0.8.91
0.8.90
0.8.89
0.8.88
0.8.87
0.8.86
0.8.85
0.8.84
0.8.83
0.8.82
0.8.81
0.8.80
0.8.79
0.8.78
0.8.77
0.8.76
0.8.75
0.8.74
0.8.73
0.8.72
0.8.71
0.8.70
0.8.69
0.8.68
0.8.67
0.8.66
0.8.65
0.8.64
0.8.63
0.8.62
0.8.61
0.8.60
0.8.59
0.8.58
0.8.57
0.8.56
0.8.55
0.8.54
0.8.53
0.8.52
0.8.51
0.8.50
0.8.49
0.8.48
0.8.47
0.8.46
0.8.45
0.8.44
0.8.43
0.8.42
0.8.41
0.8.40
0.8.39
0.8.38
0.8.37
0.8.36
0.8.35
0.8.34
0.8.33
0.8.32
0.8.31
0.8.30
0.8.29
0.8.27
0.8.26
0.8.25
0.8.24
0.8.23
0.8.22
0.8.21
0.8.20
0.8.19
0.8.18
0.8.17
0.8.16
0.8.15
0.8.14
0.8.13
0.8.12
0.8.11
0.8.1
0.8.0
0.7.24
0.7.23
0.7.22
0.7.21
0.7.20
0.7.19
0.7.18
0.7.17
0.7.16
0.7.15
0.7.14
0.7.13
0.7.12
0.7.11
0.7.10
0.7.9
0.7.8
0.7.7
0.7.6
0.7.5
0.7.3
0.7.2
0.7.1
0.7.0
0.6.9
0.6.8
0.6.7
0.6.6
0.6.5
0.6.4
0.6.3
0.6.1
0.6.0
0.5.9
0.5.8
0.5.7
0.5.6
0.5.5
0.5.4
0.5.3
0.5.2
0.5.1
0.5.0
0.4.44
0.4.43
0.4.42
0.4.41
0.4.40
0.4.39
0.4.38
0.4.37
0.4.36
0.4.35
0.4.34
0.4.33
0.4.32
0.4.30
0.4.29
0.4.28
0.4.27
0.4.26
0.4.25
0.4.24
0.4.23
0.4.22
0.4.21
0.4.20
0.4.19
0.4.18
0.4.17
0.4.16
0.4.15
0.4.14
0.4.13
0.4.12
0.4.11
0.4.10
0.4.9
0.4.8
0.4.7
0.4.6
0.4.5
0.4.4
0.4.3
0.4.2
0.4.1
0.4.0
0.3.0
0.2.0
0.1.0
AI code archaeology
Current section
Files
Jump to
Current section
Files
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