(apply-skills-filter by-name filter-map)SPEC §3 S2 — the per-agent allowlist, semantics IDENTICAL to the MCP
per-server tools filter: nil or empty ⇒ every skill; at least one true ⇒ an
allowlist of exactly the true-mapped names; only false values ⇒ a drop-list
over the all-on baseline. Unknown names are ignored.
Returns [filtered-by-name unmatched-names]. The unmatched list is RETURNED
rather than only logged so the requirement is testable without capturing
stdout — the warn line is emitted from load-skills.
SPEC §3 S2 — the per-agent allowlist, semantics IDENTICAL to the MCP per-server tools filter: nil or empty ⇒ every skill; at least one `true` ⇒ an allowlist of exactly the true-mapped names; only `false` values ⇒ a drop-list over the all-on baseline. Unknown names are ignored. Returns `[filtered-by-name unmatched-names]`. The unmatched list is RETURNED rather than only logged so the requirement is testable without capturing stdout — the warn line is emitted from `load-skills`.
(candidates root)Every SKILL.md under root, in DISCOVERY ORDER, as a parsed skill or a typed
skip.
DISCOVERY ORDER is pinned, not inherited: discovery-compare over the path
RELATIVE to this root's logical base — depth, then code point (A15/A1c). A
symlink sorts at the path it was DISCOVERED at, not at its target, because
that is the only one of the two a caller can see.
It is sorted HERE rather than relied upon from koine.fs/find-files even
though find-files sorts: first-name-wins is order-dependent, a rule that
decides which duplicate survives may not rest on another library's promise,
and find-files' own order is plain lexicographic — which is precisely the
order A15 corrects.
Note find-files takes a SUFFIX, not a glob, so the suffix is "/SKILL.md" — a bare "SKILL.md" would also match a file named MYSKILL.md.
Every SKILL.md under `root`, in DISCOVERY ORDER, as a parsed skill or a typed skip. DISCOVERY ORDER is pinned, not inherited: `discovery-compare` over the path RELATIVE to this root's logical base — depth, then code point (A15/A1c). A symlink sorts at the path it was DISCOVERED at, not at its target, because that is the only one of the two a caller can see. It is sorted HERE rather than relied upon from `koine.fs/find-files` even though find-files sorts: first-name-wins is order-dependent, a rule that decides which duplicate survives may not rest on another library's promise, and find-files' own order is plain lexicographic — which is precisely the order A15 corrects. Note find-files takes a SUFFIX, not a glob, so the suffix is "/SKILL.md" — a bare "SKILL.md" would also match a file named MYSKILL.md.
(discovery-compare a b)The §3 discovery order over two paths RELATIVE to a root's logical base: DEPTH ascending, then Unicode CODE POINT (addendum A15, correcting A1a).
WHY DEPTH FIRST, and why A1a alone was wrong. §3 and the OpenSpec scenario both say a SHALLOWER path beats a nested copy of the same name. A pure code-point sort does not deliver that — it delivers it only for names that happen to sort before the nested directory's first segment:
docx -> docx/ (d < s, top-level wins)
xlsx -> synced/…/xlsx (x > s, the NESTED copy wins)
So the winner depended on the skill's first letter relative to a sibling directory's name, which is indefensible and is not what anyone intended. The external consumer found it on a real corpus. Sorting by depth first makes the rule that was already written down actually true, for every name.
A1a/A1c are unchanged and remain the TIE-BREAK WITHIN a depth: code POINT,
via tool/compare-strings, never a locale collator, never case folding,
never compare (which is UTF-16 code-UNIT order on the JVM host and
disagrees for anything above U+FFFF).
The §3 discovery order over two paths RELATIVE to a root's logical base:
DEPTH ascending, then Unicode CODE POINT (addendum A15, correcting A1a).
WHY DEPTH FIRST, and why A1a alone was wrong. §3 and the OpenSpec scenario
both say a SHALLOWER path beats a nested copy of the same name. A pure
code-point sort does not deliver that — it delivers it only for names that
happen to sort before the nested directory's first segment:
docx -> docx/ (d < s, top-level wins)
xlsx -> synced/…/xlsx (x > s, the NESTED copy wins)
So the winner depended on the skill's first letter relative to a sibling
directory's name, which is indefensible and is not what anyone intended. The
external consumer found it on a real corpus. Sorting by depth first makes the
rule that was already written down actually true, for every name.
A1a/A1c are unchanged and remain the TIE-BREAK WITHIN a depth: code POINT,
via `tool/compare-strings`, never a locale collator, never case folding,
never `compare` (which is UTF-16 code-UNIT order on the JVM host and
disagrees for anything above U+FFFF).(execute-skill loaded skill-name)(execute-skill loaded skill-name limit)The skill tool's execute, as a plain fn over a loaded map.
An unknown name is NOT a throw — SPEC §3 step 1 makes it
ToolResult{isError:true}, so the model sees it and can retry.
DIVERGENCE, following the ports: SPEC §3 prose says "Available: ", while
golang/skill.go, js, python, java, csharp and elixir all emit
"Available skills: " (and the literal none when there are no skills).
We match the ports. SPEC §3 should be corrected.
The `skill` tool's execute, as a plain fn over a loaded map.
An unknown name is NOT a throw — SPEC §3 step 1 makes it
`ToolResult{isError:true}`, so the model sees it and can retry.
DIVERGENCE, following the ports: SPEC §3 prose says `"Available: "`, while
golang/skill.go, js, python, java, csharp and elixir all emit
`"Available skills: "` (and the literal `none` when there are no skills).
We match the ports. SPEC §3 should be corrected.(file-name path)The last segment of path. NOT named base-name/basename — nothing in
clojure.core collides, but the name is kept explicit for readability.
The last segment of `path`. NOT named `base-name`/`basename` — nothing in clojure.core collides, but the name is kept explicit for readability.
(list-skills input)SPEC §3 S3 — list/validate inventory: {:skills … :skipped …}, no toolkit
wired, nothing left open.
DELIBERATELY UNFILTERED, following js/src/skill.ts listSkills, which ignores
opts.filter entirely: the inventory is what you AUTHOR an allowlist from, so
filtering it would be circular.
SPEC §3 S3 — list/validate inventory: `{:skills … :skipped …}`, no toolkit
wired, nothing left open.
DELIBERATELY UNFILTERED, following js/src/skill.ts `listSkills`, which ignores
`opts.filter` entirely: the inventory is what you AUTHOR an allowlist from, so
filtering it would be circular.(load-opts input)load-skills accepts every shape js/src/skill.ts' loadSkills accepts, plus
the two data shapes: a root string, a seq of roots, a seq of skill defs, one
skill def, or the full options map.
`load-skills` accepts every shape js/src/skill.ts' `loadSkills` accepts, plus the two data shapes: a root string, a seq of roots, a seq of skill defs, one skill def, or the full options map.
(load-skills input)Discover skills from every source §3 offers and shape them into one loaded map.
input is a root string, a seq of roots, a seq of skill DEFS, or an options
map {:dirs … :skills … :filter … :sample-limit …} — the same union
js/src/skill.ts' loadSkills takes. Directory roots are collected first, in
the order given, then the data defs, so the existing first-name-wins rule
resolves collisions ACROSS sources without needing a second rule.
Returns {:skills [info…] :by-name {} :skipped [] :filter-unmatched [] :sample-limit n}. A caller passing only roots gets byte-identical behaviour
to before this option existed.
Discover skills from every source §3 offers and shape them into one loaded
map.
`input` is a root string, a seq of roots, a seq of skill DEFS, or an options
map `{:dirs … :skills … :filter … :sample-limit …}` — the same union
js/src/skill.ts' `loadSkills` takes. Directory roots are collected first, in
the order given, then the data defs, so the existing first-name-wins rule
resolves collisions ACROSS sources without needing a second rule.
Returns `{:skills [info…] :by-name {} :skipped [] :filter-unmatched []
:sample-limit n}`. A caller passing only roots gets byte-identical behaviour
to before this option existed.(merge-candidates cands)Dedupe by name, FIRST WINS; later duplicates become typed skips.
Returns {:skills [info …] :by-name {name info} :skipped [skip …]}.
Dedupe by name, FIRST WINS; later duplicates become typed skips.
Returns `{:skills [info …] :by-name {name info} :skipped [skip …]}`.(parent-dir path)The directory part of path, or "." when there is no separator.
The directory part of `path`, or "." when there is no separator.
(sample-sibling-files dir limit)Up to limit files under the skill's directory, EXCLUDING every SKILL.md.
The exclusion is the detail that moves bytes and that spike s15 got wrong:
golang/skill.go:217 and js/src/skill.ts:198 both guard the sampler with
entry.Name() != "SKILL.md". Leaving SKILL.md in adds one <file> line and
the shared hello-world output stops matching the other ports.
limit: 0 ⇒ default 10 · n>0 ⇒ cap at n · -1 ⇒ nil, i.e. sampling disabled
(SPEC §3 S5).
ORDER — deliberate divergence. SPEC §3 does not pin the sample's order, and no
shipped port sorts: js/go/python all walk a DFS stack over raw readdir order
and slice the first limit entries. Go's os.ReadDir sorts, Python's
os.scandir does not, so with >1 sibling the shipped ports are not guaranteed
to agree with each other. WE SORT, because a port whose whole claim is
byte-identical output on two hosts cannot depend on readdir order. Pending a
SPEC fix that pins the sampler's order for everyone; recorded here rather than
smuggled in.
Up to `limit` files under the skill's directory, EXCLUDING every SKILL.md. The exclusion is the detail that moves bytes and that spike s15 got wrong: `golang/skill.go:217` and `js/src/skill.ts:198` both guard the sampler with `entry.Name() != "SKILL.md"`. Leaving SKILL.md in adds one `<file>` line and the shared hello-world output stops matching the other ports. `limit`: 0 ⇒ default 10 · n>0 ⇒ cap at n · -1 ⇒ nil, i.e. sampling disabled (SPEC §3 S5). ORDER — deliberate divergence. SPEC §3 does not pin the sample's order, and no shipped port sorts: js/go/python all walk a DFS stack over raw readdir order and slice the first `limit` entries. Go's os.ReadDir sorts, Python's os.scandir does not, so with >1 sibling the shipped ports are not guaranteed to agree with each other. WE SORT, because a port whose whole claim is byte-identical output on two hosts cannot depend on readdir order. Pending a SPEC fix that pins the sampler's order for everyone; recorded here rather than smuggled in.
(skill-files info limit)The <skill_files> list for one skill, or nil when there is no block.
Two sources, one rule, following js/src/skill.ts' execute:
The `<skill_files>` list for one skill, or nil when there is no block.
Two sources, one rule, following js/src/skill.ts' execute:
* an on-disk skill samples its siblings and ALWAYS emits the block (even
empty) unless sampling is off — that is the shipped byte-exact behaviour;
* a data/provider skill lists its SUPPLIED logical resources, in the order
supplied, and omits the block entirely when it has none (an
instruction-only skill has nothing to disclose).(skill-output skill files)SPEC §0.6. Built by explicit concatenation, never a template: every newline
here is part of the contract, and there is no trailing newline (the ports join
lines). files = nil disables sampling (S5 -1): the <skill_files> block
AND its "Note: file list is sampled." line are both omitted.
SPEC §0.6. Built by explicit concatenation, never a template: every newline here is part of the contract, and there is no trailing newline (the ports join lines). `files` = nil disables sampling (S5 `-1`): the `<skill_files>` block AND its "Note: file list is sampled." line are both omitted.
(skill-tool loaded)(skill-tool loaded {:keys [sample-limit]})The single skill tool (SPEC §3), shipped by default alongside the skills.
The single `skill` tool (SPEC §3), shipped by default alongside the skills.
SPEC §3 skill.txt — verbatim from opencode.
SPEC §3 skill.txt — verbatim from opencode.
(skills-prompt loaded)preamble + "\n\n" + "## Available Skills" + one line per DESCRIBED skill, sorted by name. No described skill at all ⇒ the no-skills message, with NO preamble (SPEC §3).
preamble + "\n\n" + "## Available Skills" + one line per DESCRIBED skill, sorted by name. No described skill at all ⇒ the no-skills message, with NO preamble (SPEC §3).
SPEC §3 — the fixed instruction preamble, copied verbatim from
golang/skill.go's SkillsPromptPreamble. Do not reword.
SPEC §3 — the fixed instruction preamble, copied verbatim from `golang/skill.go`'s `SkillsPromptPreamble`. Do not reword.
cljdoc builds & hosts documentation for Clojure/Script libraries
| Ctrl+k | Jump to recent docs |
| ← | Move to previous article |
| → | Move to next article |
| Ctrl+/ | Jump to the search field |