toolnexus.skill/list-skills
Clojure (JVM) + cljgo · package net.clojars.muthuishere/toolnexus · SPEC §3 · clojure/src/toolnexus/skill.cljc
(list-skills input);; input: every shape load-skills takes — a root, a seq of roots, skill;; definitions, or the options map. :filter is accepted and IGNORED.;;;; => {:skills [{:name … :description … :location … :content …;; :dir … :base … :origin "fs" | "logical"}];; :skipped [{:location "…/SKILL.md" :reason "malformed-frontmatter"}]}list-skills runs the same discovery and parsing as
toolnexus.skill/load-skills and hands the result back as a plain
report: the skills that parsed, and the candidates that were rejected with a typed reason. No
skill tool is built, no prompt is rendered, nothing is left open.
The :skipped vector is the reason this function exists. load-skills quietly drops a malformed
SKILL.md — it is not going to refuse to build a toolkit over one bad file — so nothing tells you
that a capability you wrote is missing from the model’s catalog. list-skills names the path and
the reason, which is what turns that into a build failure.
It is deliberately unfiltered: :filter is stripped before discovery runs. The inventory is
what you author an allowlist from, so filtering it would be circular. It also omits :by-name,
the key skill-tool and execute-skill read — so a report cannot be handed to them by accident.
When to use it
Section titled “When to use it”- A CI gate — assert
:skippedis empty, so a typo in frontmatter fails the build instead of vanishing a skill. - Authoring an allowlist — enumerate everything that exists, then feed names into
load-skills’s:filter. This is what the unfiltered inventory is for. - Checking the frontmatter subset — this port’s parser is stricter than the other ports’ (see below), and this is how you find out which of your skills fall outside it.
- Tooling — a
skills listcommand, an admin page, a doctor command. The report is data with no side effects, so it is safe to call from anywhere.
Why this and not the alternative
Section titled “Why this and not the alternative”Both take the same input, so one configuration value can be validated and then loaded without being written twice. Neither caches; both re-read the tree on every call, so call this once at startup rather than per request.
Examples
Section titled “Examples”Inventory the shared fixture
Section titled “Inventory the shared fixture”(require '[clojure.string :as str] '[toolnexus.skill :as skill])
(def report (skill/list-skills "examples/skills"))
(:skipped report) ;=> [](map :name (:skills report)) ;=> ("hello-world")
(let [hello (first (:skills report))] (:origin hello) ;=> "fs" (:location hello) ;=> "examples/skills/hello-world/SKILL.md" ;; the instruction body is already parsed and available (str/includes? (:content hello) "# Hello World Skill")) ;=> trueThe four skip reasons, and a gate over them
Section titled “The four skip reasons, and a gate over them”(require '[toolnexus.skill :as skill])
(def report (skill/list-skills ["project/skills" "team/skills"]))
(map (juxt :reason :location) (:skipped report));; => (["malformed-frontmatter" "project/skills/broken/SKILL.md"];; ["missing-name" "project/skills/nameless/SKILL.md"];; ["duplicate-name" "team/skills/deploy/SKILL.md"])
(defn check-skills! "Fail the build rather than ship an agent that silently lost a capability." [roots] (let [{:keys [skills skipped]} (skill/list-skills roots)] (when (seq skipped) (throw (ex-info "skills failed to load" {:skipped skipped}))) (count skills)))| Reason | Cause |
|---|---|
"missing-name" |
Frontmatter parsed, but name is absent or blank. A file with no frontmatter at all lands here too, as does a data definition without a :name. |
"malformed-frontmatter" |
The --- fences are present and what sits between them is outside toolnexus.frontmatter’s accepted subset. |
"duplicate-name" |
An earlier candidate already claimed that name. First wins. |
"unreadable" |
The SKILL.md bytes could not be read — permissions, a dangling symlink. |
Validate a whole configuration, then load it
Section titled “Validate a whole configuration, then load it”(require '[toolnexus.skill :as skill])
(def config {:dirs ["examples/skills"] :skills [{:name "triage" :description "Triage an incoming bug report" :content "1. Reproduce it."} {:name "release" :description "Cut a release" :content "1. Bump the version."} ;; no :name — a skip, not a crash {:description "Anonymous" :content "…"}]})
(def report (skill/list-skills config))
(sort (map :name (:skills report))) ;=> ("hello-world" "release" "triage")(map :reason (:skipped report)) ;=> ("missing-name")
;; Data-sourced skills carry a logical base and never touch disk.(map (juxt :name :origin) (:skills report));; => (["hello-world" "fs"] ["triage" "logical"] ["release" "logical"])
;; Now author the allowlist from the inventory and load only what this agent gets.(def loaded (skill/load-skills (assoc config :filter {"hello-world" true "triage" true})))(sort (map :name (:skills loaded))) ;=> ("hello-world" "triage")
;; list-skills stays UNFILTERED even when the filter is present — that is the point.(count (:skills (skill/list-skills (assoc config :filter {"hello-world" true})))) ;=> 3| Return key | What it is |
|---|---|
:skills |
Parsed skills in discovery order, deduped by name, never filtered. Each carries :name, :description, :location, :content, :dir, :base and :origin. |
:skipped |
One {:location :reason} map per rejected candidate. |
See also
Section titled “See also”toolnexus.skill/load-skills— Glob a skills directory and expose one skill tool with progressive disclosure.- Data skills, filters and sampling — the option map both functions accept.