A skill is a directory, and the spec recommends splitting once SKILL.md passes 500 lines — so references/, scripts/ and assets/ companions are the normal shape. docs/skills.md did not mention them. The load-bearing decision is what the section stops doing. The first version restated the detector's rules — which cues suppress a reference, which target shapes are skipped, whether a directive verb is needed. Every restatement was a claim about another component's regex, and review found a wrong one on each pass. The section now says what to DO and sends the reader to rules/skill-resource-resolves.md for what is CHECKED, with that page stating its own lists are illustrative and the constants in src/core/skill-resources.ts are the authority. Reversed the file() advice. An earlier revision told authors to reference a companion with file() instead of a markdown link. That is wrong for any skill that ships: renderFragment emits a FileRef as a visible repo-root-relative path, nothing rebases it at packaging, and the installed skill would carry a working link beside a pointer that resolves nowhere. Guidance is the markdown link — skill-relative, travels with the skill, already linted. The missing piece (a compile-verified reference that stays skill-relative and emits nothing) is named as a gap, not worked around. Corrected two stale Status/pending claims: the integrity header no longer precedes the frontmatter (line 1 vs line 10, measured), and a compiled SKILL.md now has one datapoint as an installed skill — recorded with its limit, since the skill pre-existed the conversion and firing was never measured. The exit criterion says the milestone is unmet. Two product defects documented but not fixed here: resolution is not transitive (a link inside a companion is unchecked, and progressive disclosure produces exactly that chain), and reported line numbers are body-relative because scanSkills strips the frontmatter before counting.
6.8 KiB
skill-resource-resolves
Flag a SKILL.md body that references a bundled file which doesn't exist on
disk under the skill directory. A SKILL.md routinely points the agent at local
files shipped beside it — scripts/run.sh, references/api.md, an inline
`scripts/setup.py`. When the referenced file is missing, the agent reads the
instruction, gets nothing, and silently continues — a documented top skill
pain. This is the cross-reference moat applied to the SKILL.md body. Same detector
vigiles audit uses (skillResourceIssues in src/core/skill-resources.ts).
What it flags
A body reference that is unambiguously a local bundled resource but doesn't resolve on disk:
✗ pdf-extract: bundled resource "scripts/extract.py" (line 14) is referenced but
missing — the agent reads the instruction and gets nothing.
It matches two shapes:
- a markdown link
[setup](./scripts/run.sh)whose target is a relative path with a file extension, and - an inline-code path
`scripts/foo.sh`that is prefixed by a standard bundle dir (scripts/,references/,assets/), has an extension, and sits in a line that directs the agent to use it (see below).
High-precision (FP-safe)
By the same don't-cry-wolf discipline as the loader's danglingRefs, it flags
only what is clearly a local resource and skips everything ambiguous:
- URLs (
http://…,mailto:), absolute paths (/etc/x,C:\…), ${VAR}/$VARtokens (plugin-root / runtime paths, uncheckable),../escapes out of the skill dir (a sibling skill or the repo),- extension-less mentions (a bare word or a directory name is undecidable prose),
- and a generic inline
`config.json`/`src/foo.ts`API mention (not a bundle-dir path).
An inline-code path gets one extra gate, because it is a weaker signal than a
link. A skill that teaches how to build skills is full of bundle paths used as
examples of what a skill could contain, not files it ships — "a
`scripts/rotate_pdf.py` would be helpful to store", "Examples:
`references/finance.md`", "- `references/patterns.md` — Common
patterns". So the two shapes are gated differently, and the asymmetry is deliberate.
Two cues suppress a reference, and they apply to BOTH shapes:
- an illustrative cue —
example,e.g.,such as,would be,template,→; - a generated-file cue — the line says the file is made, not read:
gitignored,.gitignore,cached to,cache file,written to/writes to,generated at runtime,created at runtime. A file the author states is produced at runtime or kept out of the repository cannot be a missing bundled resource.
On top of that, an inline backtick path additionally needs a use directive
(read / run / see / load / …), because a bare mention is noisy. A markdown
link does not: it is explicit follow-me syntax, so Resources: [the API](references/api.md)
is checked even with no verb in the line, while "For example, see
[a report](assets/example.pdf)" is suppressed by the cue.
(This paragraph previously said a link also required a directive. That was
true until the link branch was narrowed to cue-only suppression to close an
under-detection; candidateFor in src/core/skill-resources.ts and the
"plain resource link with NO use directive" test are the authority.)
A heading counts as a directive. The verb gate reads prose, and a heading is not prose — it is the section's label — so the most common way a skill points at its own script went unchecked:
## 🏗 START WITH THE MECHANICAL LEG — `scripts/structure.mjs`
Measured on a real skill whose structure.mjs sits at the skill root, not
under scripts/: that line yielded nothing, while rewriting it to "Run the
mechanical leg" — same file, same missing target — reported it correctly. The
tool's answer depended on the author's choice of verb. An ATX heading naming a
bundle path is now treated like an explicit "run …". The illustrative-cue veto
still applies (## Examples: … stays skipped), and the wider net stops there: a
mid-paragraph mention with no directive is still not checked, because flagging
every backticked bundle path is what cries wolf on teaching skills.
A link's TEXT is not a reference. Candidates come from a markdown parse (markdown-it), not from regexes over each line, so the destination and the label can be told apart:
See [`references/api.md` § Lookups](../add-dataverse/references/api.md#lookups)
The destination is what the agent follows, and it resolves. The backtick span is
its human-readable label. A line-oriented scan could not see that the span sat
inside a link, and reported the label as a missing bundled resource — an
accusation against a correct link (measured on microsoft/power-platform-skills
and rohitg00/pro-workflow, 2026-08-17). A code span nested in a link's or an
image's text is therefore not a candidate at all. For the same reason, link
syntax that lives inside a code span is not a link: `- [ ] [Phase N](phase-N.md)`
documents a checklist format, and phase-N.md is a metavariable that will never
exist.
The detector prefers missing a real ref over a false positive — a noisy
resource check would teach users to ignore it. A skill whose body is inherently
full of illustrative bundle-path examples (a skill-authoring tutorial) can opt out
entirely with a <!-- vigiles-disable skill-resource-resolves --> marker.
Configuration
{ "rules": { "skill-resource-resolves": "warn" } }
Severity
| Value | Behavior |
|---|---|
"error" |
vigiles lint exits non-zero (2) on a missing bundled resource |
"warn" (default) |
Prints a warning, exits 0 (don't-cry-wolf rollout) |
false |
Skip the check |
Default is warn during rollout; raise to "error" to gate CI once you trust
it on your own skills.
Scope
Skills (skills/*/SKILL.md) — every harness has skills, so this rule is not
capability-gated. References resolve against the skill's own directory (resources
ship beside the SKILL.md).
Why
A SKILL.md is freeform markdown, so a renamed or un-shipped scripts/ file rots
silently — the model just doesn't get the resource it was told to use. vigiles
already verifies file/script refs inside typed specs and intra-plugin hook refs in
the loader; this extends that guarantee to the SKILL.md body.
See also
- hook-script-exists — the same idea for a hook command's script file.
- skill-frontmatter — recommends an explicit, reliable trigger surface.