Packages
fnord
0.9.24
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/agent.ex
defmodule ExternalConfigs.Agent do
@moduledoc """
A parsed Claude Code subagent definition from a single `.md` file under
`~/.claude/agents/` or `<project>/.claude/agents/`.
Agents are sibling to skills: a named, described capability with an
instruction body. They differ in format (a single `.md` file instead
of a directory with `SKILL.md`) and in intent: an agent is a role
(system prompt + allowed tools + model hint) that Claude Code would
delegate to, whereas a skill is a procedure to follow. Fnord doesn't
have a subagent-delegation mechanism, so from the coordinator's
perspective an agent is reference material: the body is guidance to
internalize when the description matches the task.
## Data flow
1. `ExternalConfigs.Loader.load_claude_agents/1` discovers `.md` files
under the global and project agents dirs, calls `from_file/2` per
entry, and caches the resulting list.
2. `ExternalConfigs.Catalog` filters out agents whose tools list
implies edit capability when fnord isn't in edit mode (the
partition_agents helper inside that module), then feeds the rest
to the skills catalog prompt.
3. The coordinator's bootstrap (the external_configs_msg step)
appends the catalog as a system message; the Frippery
log_external_skills boot line prints the enabled agents' names.
"""
defstruct [
:name,
:description,
:tools,
:model,
:body,
:path,
:source
]
@type source :: :global | :project
@type t :: %__MODULE__{
name: String.t(),
description: String.t() | nil,
tools: [String.t()],
model: String.t() | nil,
body: String.t(),
path: String.t(),
source: source()
}
@doc """
Load an agent from a single `.md` file. The agent name is derived from
the filename (without the `.md` extension) when no `name` key is set
in frontmatter.
"""
@spec from_file(String.t(), source()) :: {:ok, t} | {:error, term()}
def from_file(path, source) when is_binary(path) do
with {:ok, content} <- File.read(path),
{:ok, %{frontmatter: fm, body: body}} <-
ExternalConfigs.Frontmatter.parse(content) do
name = fetch_string(fm, "name") || derive_name(path)
description = fetch_string(fm, "description")
tools = fetch_tools(fm, "tools")
model = fetch_string(fm, "model")
{:ok,
%__MODULE__{
name: name,
description: description,
tools: tools,
model: model,
body: String.trim(body),
path: path,
source: source
}}
end
end
defp derive_name(path) do
path
|> Path.basename()
|> String.replace_suffix(".md", "")
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
# `tools` is conventionally a comma-separated string; the Agent Skills
# standard also allows a YAML list. Accept either.
defp fetch_tools(fm, key) do
case Map.get(fm, key) do
nil ->
[]
list when is_list(list) ->
list
|> Enum.filter(&is_binary/1)
|> Enum.map(&String.trim/1)
|> Enum.reject(&(&1 == ""))
str when is_binary(str) ->
str
|> String.split(",")
|> Enum.map(&String.trim/1)
|> Enum.reject(&(&1 == ""))
_ ->
[]
end
end
end