Packages
fnord
0.9.40
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/ai/tools/run_skill.ex
defmodule AI.Tools.RunSkill do
@moduledoc """
Execute an enabled Skill by name.
This is an integration point between the coordinator and Skills. It:
- lists skills dynamically in its tool spec,
- resolves a selected skill by name,
- enforces RW gating (skills requiring `rw` tools need `--edit`),
- enforces a recursion depth limit for skill-to-skill calls,
- runs the skill via `AI.Agent.Skill`.
"""
@behaviour AI.Tools
@tool_name "run_skill"
@impl AI.Tools
def async?, do: false
@impl AI.Tools
def is_available?, do: true
@impl AI.Tools
def read_args(%{} = args) do
with {:ok, skill} <- get_string(args, "skill"),
{:ok, prompt} <- get_string(args, "prompt") do
{:ok, %{"skill" => skill, "prompt" => prompt}}
end
end
defp get_string(args, key) do
case Map.fetch(args, key) do
{:ok, value} when is_binary(value) and byte_size(value) > 0 -> {:ok, value}
_ -> {:error, {:missing_arg, key}}
end
end
@impl AI.Tools
def spec() do
names = all_skill_names()
details = all_skill_descriptions()
%{
type: "function",
name: @tool_name,
description: "Run a named skill.\n\nAvailable skills: #{names}",
parameters: %{
type: "object",
required: ["skill", "prompt"],
additionalProperties: false,
properties: %{
skill: %{
type: "string",
description: "Skill name. Available skills:\n#{details}"
},
prompt: %{
type: "string",
description: "Input prompt to send to the skill"
}
}
}
}
end
# Fnord TOML skills + enabled claude/cursor external skills.
defp all_skill_names() do
fnord =
case Skills.list_enabled() do
{:ok, skills} -> Enum.map(skills, & &1.name)
{:error, _} -> []
end
external = enabled_external_skills() |> Enum.map(& &1.name)
case fnord ++ external do
[] -> "(none)"
names -> Enum.join(names, ", ")
end
end
defp all_skill_descriptions() do
fnord_lines =
case Skills.list_enabled() do
{:ok, []} ->
[]
{:ok, skills} ->
Enum.map(skills, fn %{name: name, effective: %{skill: skill}} ->
"- #{name}: #{skill.description}"
end)
{:error, reason} ->
["(error loading skills: #{inspect(reason)})"]
end
external_lines =
enabled_external_skills()
|> Enum.map(fn %ExternalConfigs.Skill{
name: name,
description: d,
when_to_use: w,
flavor: flavor
} ->
type = if flavor == :claude, do: "Claude Code", else: "Cursor"
desc = combine_skill_description(d, w)
"- #{name} [#{type} skill]: #{desc}"
end)
case fnord_lines ++ external_lines do
[] -> "(none found)"
lines -> Enum.join(lines, "\n")
end
end
defp combine_skill_description(nil, nil), do: "(no description)"
defp combine_skill_description(d, nil), do: d
defp combine_skill_description(nil, w), do: w
defp combine_skill_description(d, w), do: d <> " - " <> w
# Load enabled external (claude + cursor) skills for the current project.
# Returns [] when no project is selected or no external skills are enabled.
defp enabled_external_skills() do
case Store.get_project() do
{:ok, project} ->
flags = Settings.ExternalConfigs.flags(project.name)
cursor =
if flags.cursor_skills do
ExternalConfigs.Loader.load_cursor_skills(project)
else
[]
end
claude =
if flags.claude_skills do
ExternalConfigs.Loader.load_claude_skills(project)
else
[]
end
ExternalConfigs.Loader.dedup_cross_flavor(cursor, claude) ++ claude
_ ->
[]
end
end
@impl AI.Tools
def call(%{"skill" => name, "prompt" => prompt}) do
# Soft-gating: when max depth is reached, still run the skill but strip
# its ability to call other skills (mirrors the researcher pattern).
can_recurse? =
case Services.SkillDepth.inc_depth() do
{:ok, _depth} -> true
{:error, :max_depth_reached} -> false
end
try do
resolve_and_run(name, prompt, not can_recurse?)
after
case can_recurse? do
true -> Services.SkillDepth.dec_depth()
false -> :ok
end
end
end
# Try fnord TOML skills first, then fall through to external SKILL.md skills.
defp resolve_and_run(name, prompt, strip_skills?) do
case Skills.get_enabled(name) do
{:ok, resolved} ->
with :ok <- enforce_rw_gating(resolved) do
run_skill(resolved, prompt, strip_skills?)
end
{:error, :not_found} ->
case find_external_skill(name) do
{:ok, ext_skill} -> run_external_skill(ext_skill, prompt, strip_skills?)
{:error, :not_found} -> {:error, :not_found}
end
{:error, reason} ->
{:error, reason}
end
end
defp enforce_rw_gating(%{effective: %{skill: %Skills.Skill{tools: tools}}}) do
if Enum.member?(tools, "rw") and not Settings.get_edit_mode() do
{:error, {:denied, "Cannot run rw skills; the user did not pass --edit."}}
else
:ok
end
end
defp run_skill(%{effective: %{skill: skill}}, prompt, strip_skills?) do
agent = AI.Agent.new(AI.Agent.Skill)
AI.Agent.get_response(agent, %{
skill: skill,
prompt: prompt,
strip_skills?: strip_skills?
})
end
# Find an external (claude or cursor) skill by name in the enabled sources
# for the current project.
defp find_external_skill(name) do
enabled_external_skills()
|> Enum.find(&(&1.name == name))
|> case do
nil -> {:error, :not_found}
skill -> {:ok, skill}
end
end
# Synthesize a Skills.Skill from an ExternalConfigs.Skill and run it as a
# sub-agent. External skills don't specify model/tools, so we use
# conservative defaults; their SKILL.md body becomes the system prompt.
defp run_external_skill(%ExternalConfigs.Skill{} = ext_skill, prompt, strip_skills?) do
skill = %Skills.Skill{
name: ext_skill.name,
description: ext_skill.description || "",
model: "balanced",
tools: ["basic"],
system_prompt: ext_skill.body,
response_format: nil
}
agent = AI.Agent.new(AI.Agent.Skill)
AI.Agent.get_response(agent, %{
skill: skill,
prompt: prompt,
strip_skills?: strip_skills?
})
end
@impl AI.Tools
def ui_note_on_request(%{"skill" => skill}) do
{"Run skill", skill}
end
@impl AI.Tools
def ui_note_on_result(_args, _result) do
nil
end
@impl AI.Tools
def tool_call_failure_message(_args, _error) do
:default
end
end