Generic over a tool Vocabulary (default OpenToolVocabulary — no
constraint), exactly like experimental_agent(): a vocabulary-bound experimental_skill
(e.g. vigiles/claude-code) makes purity: "pure" + a side-effecting tool a
tsc error; the bare core one accepts any tools, as before.
The name also disambiguates: vigiles/testing exports a skill() that is a
DIFFERENT function — a Check<Trace> taking an id string, asking whether a
skill fired. This one authors a skill; that one observes one.
Its helper vocabulary hangs off it — experimental_skill.input(…) and
experimental_skill.step(…) — rather than being exported beside it. Both are
used ONLY by skill specs (measured: zero uses in agent/claude specs, against
cmd/file/ref/result, which are shared and therefore stay top-level).
Hanging them here makes the experimental marking STRUCTURAL for the whole
family: you cannot reach input() without naming experimental_skill first.
The prefix convention alone could not do that — it is a habit, and it had
already leaked once when skill() shipped stable-named against its own docs.
Honest limit: const { input } = experimental_skill strips the marker again
inside one file. What the shape actually guarantees is narrower and still
worth having — an unmarked name never crosses the package boundary.
Object.assign rather than export namespace: the latter is banned by this
repo's own lint (no-namespace: error, inherited from strict-type-checked).
Define a SKILL.md specification.
// skills/my-skill/SKILL.md.spec.ts export default experimental_skill({ name: "my-skill", description: "…" });
Generic over a tool
Vocabulary(defaultOpenToolVocabulary— no constraint), exactly likeexperimental_agent(): a vocabulary-boundexperimental_skill(e.g.vigiles/claude-code) makespurity: "pure"+ a side-effecting tool atscerror; the bare core one accepts any tools, as before.The name also disambiguates:
vigiles/testingexports askill()that is a DIFFERENT function — aCheck<Trace>taking an id string, asking whether a skill fired. This one authors a skill; that one observes one.Its helper vocabulary hangs off it —
experimental_skill.input(…)andexperimental_skill.step(…)— rather than being exported beside it. Both are used ONLY by skill specs (measured: zero uses in agent/claude specs, againstcmd/file/ref/result, which are shared and therefore stay top-level). Hanging them here makes the experimental marking STRUCTURAL for the whole family: you cannot reachinput()without namingexperimental_skillfirst. The prefix convention alone could not do that — it is a habit, and it had already leaked once whenskill()shipped stable-named against its own docs.Honest limit:
const { input } = experimental_skillstrips the marker again inside one file. What the shape actually guarantees is narrower and still worth having — an unmarked name never crosses the package boundary.Object.assignrather thanexport namespace: the latter is banned by this repo's own lint (no-namespace: error, inherited from strict-type-checked).