Commit Graph
19 Commits
Author SHA1 Message Date
Addy Osmani ff335f2184 Merge main: keep both the persona-lint and frontmatter tests 2026-10-10 00:59:58 -07:00
root 1ab25ea1c8 fix(linter): ignore comments and nested metadata keys in skill frontmatter 2026-10-07 15:51:07 +00:00
ayobamiseun 488c1ee06e feat(validator): lint personas for name, description and Composition
The smaller version asked for when #610 was closed. docs/agents.md states
three rules for agents/<name>.md — the same frontmatter format as a skill
with name equal to the file stem (the name is the subagent_type commands
spawn) and a description, and a closing Composition block — and no
validator checked them.

Add lintPersonaContent to scripts/lib/skill-lint.js, reusing the existing
frontmatter parsing and YAML-validity rules, and have validate-skills.js
walk agents/*.md after the skills, skipping an index README.md. Headings
are read from fence-stripped content so an example block after
Composition does not count as a later section. No spawn-reference
scanning, no listing-table checks, no new script, no workflow change.

All four personas pass: 25 skills and 4 personas checked, 0 errors.
2026-10-03 10:48:32 +01:00
Addy Osmani 9d0c60d406 Merge #608: reject non-spec top-level frontmatter keys and link the per-agent guide 2026-10-01 20:29:28 -07:00
Addy Osmani e6bc6b1b47 Merge #603: reject frontmatter values starting with a YAML indicator 2026-10-01 20:29:20 -07:00
Abhishek Sharma 30d4493755 fix(validator): flag the indicators both parsers actually reject
Addresses review on #603.

The comma claim was wrong. The code comment said a leading `,` parses cleanly;
PyYAML rejects it with a ParserError and psych with a SyntaxError. The same
comment was wrong about `]` and `}`. I had asserted all three without measuring,
and a test pinned the comma claim, so the mistake was defended rather than
caught.

Now flagged, each verified against PyYAML and psych: `,` `*` and a one-line
`|`/`>` header. A bare `|` with indented lines under it is a real block scalar
and stays accepted; `|foo` is not one.

Still not flagged, because each parses: `&anchor`, `#`, `:`/`-` without a
following space, and `[`/`{`, which open valid flow collections. `]` and `}` are
rejected by both parsers but left out for now - no frontmatter has ever started
with them, and widening past what was asked risks false positives I have not
thought through.

Docstring no longer says "the three shapes".
2026-09-29 11:47:47 -07:00
ayobamiseun 0898b52faa feat(validator): reject frontmatter keys outside the Agent Skills spec set
docs/advanced-per-agent-configuration.md says vendor and runtime fields
such as model, tools, max_turns and context must not appear at the top
level of a SKILL.md: hosts do not promise to ignore unknown keys and a
strict client can reject the file. Nothing enforced it; a skill with
model: at the top level passed CI.

Add SPEC_FRONTMATTER_KEYS (name, description, license, compatibility,
metadata, allowed-tools) and an error for any other column-zero key,
naming the key and pointing at the guide. Nested lines under metadata and
list items are children of a spec key and are left alone. Export
topLevelFrontmatterKeys for direct tests. validate-commands.js shares only
frontmatterYamlErrors, so command frontmatter is unaffected.

All 25 skills use only name and description, so the rule fires on nothing
today and follows the #571 pattern of errors for rules nothing violates.
The self-exemption test now expects two errors, since exempt: is also not
a spec key.
2026-09-26 18:53:01 +01:00
Addy Osmani a3e0361c1f Merge #605: strip every negated trigger clause, not just the first 2026-09-25 21:18:10 -07:00
Abhishek Sharma fc3026efd3 fix(skill-lint): strip every negated trigger clause, not just the first
The negated-trigger check removed only the first "Do not use when …" clause
before testing whether a positive trigger remained, because the replace used the
non-global DESCRIPTION_TRIGGER_NEGATE. A description carrying two or more negated
clauses and no positive trigger left a second "use when" substring behind, which
DESCRIPTION_TRIGGER then matched, so the skill passed validation with no trigger
at all.

One negated clause was correctly rejected; two were accepted. Adds a global form
of the pattern for the strip and keeps the non-global one for .test(), since a
/g regex carries lastIndex across test() calls.
2026-09-24 18:31:59 -07:00
Abhishek Sharma cdb801652d fix(validator): reject frontmatter values starting with a YAML indicator
The frontmatter YAML checks catch an unquoted `: `, a tab indent and an
unterminated quote, but not a plain scalar that *begins* with a character YAML
reserves. A real parser rejects the whole file on those, which is the failure
the check exists to prevent.

The backtick is the one that bites this repo. Descriptions routinely name other
skills, and

    description: `other-skill` designs things. Use when …

is a natural way to start one. It passes the validator today and throws
`YAMLException: bad indentation of a mapping entry` in any consumer that parses
the frontmatter.

The rejected set was measured against a real parser rather than read off the
spec, every candidate indicator tried both with and without a following space,
and only the ones invalid in *both* forms with no legitimate single-line use
are rejected:

    always invalid:            ` @ %
    invalid before a space:    - ? &          (`:` was already covered)

Deliberately still accepted, because each parses cleanly and flagging it would
be a false positive on valid frontmatter: `:` or `-` when *not* followed by a
space (`:platform`, `-hyphenated`), a leading `,`, `]`, `}` or `#`, and `[`/`{`,
which open valid flow collections. `|` and `>` are left alone because they
legitimately open a multi-line block scalar. Those exclusions are pinned by a
test of their own, so the rule cannot be widened into false positives later —
both widening mutations are caught by exactly that test.

Cross-checked the finished rule against the parser over 38 values spanning every
indicator in both forms: **0 false positives, 11 remaining misses**, all of them
the deliberate exclusions above.

Seven mutations caught: removing either rule, dropping the backtick or the at
sign from the set, widening either rule (to anywhere in the value, or to ignore
the required space), and letting quoted values fall through to the new checks.

The repo's own 25 skills still pass with 0 errors and 0 warnings.
2026-09-23 15:33:44 -07:00
Abhishek Sharma d6c11c40a2 feat(validator): enforce the skill-anatomy layout rules
docs/skill-anatomy.md states three structural rules that nothing checks:
the 500-line context budget for SKILL.md, no empty subdirectories, and
lowercase-hyphen-separated supporting filenames. skill-lint.js calls
itself the single source of truth for that document, so the rules belong
there rather than in a sixth validator script.

The line-count rule is a warning: an over-budget skill is an editorial
call, and failing CI on it would block unrelated work. Layout rules are
errors — nothing on main violates them, so they cannot break the build.

Also wires scripts/lib/skill-lint-test.js into CI. It has never run
there; every other *-test.js in scripts/ has its own step.
2026-09-23 13:29:17 -07:00
Abhishek Sharma 6fa76fa090 fix(validator): reject frontmatter that is not valid YAML
`parseFrontmatter` splits each line on its first colon. That is enough to read
`name` and `description`, and it is forgiving by design — but it means a
SKILL.md whose frontmatter is not valid YAML passes every check in the linter.

The hosts that consume these skills do not share that forgiveness. Per Cursor
staff in #494, attaching a skill to a message parses its frontmatter as YAML,
and a parse failure fails the request with a generic server error and takes the
rest of the chat's context with it. So the repo could ship frontmatter that
breaks a host while CI stayed green.

Three shapes pass the current linter and are rejected by a strict parser
(confirmed against ruby's psych, each isolated by mutating only the
frontmatter of a real SKILL.md that otherwise lints clean):

  description: Use this: when X     colon-space: YAML reads a nested mapping
  \tkey: value                       tab indentation is invalid YAML whitespace
  description: "unterminated        an unclosed quote

All three now fail with the line number and the fix. Deliberate boundaries:
a colon with no trailing space is untouched, so `https://example.com/a:b`
still passes, and a duplicate key is NOT reported because `safe_load` accepts
one — the rule tracks the parser, not taste.

This was verified once by hand against all 25 skills in the #494 thread. This
makes it a check instead of a memory.

Narrow on purpose: the repo has no package.json and therefore no YAML parser to
depend on, so this is a small set of rules aimed at the classes actually
observed to break a host, not a YAML implementation. No current frontmatter
uses block scalars or multi-line values and the check says so.

All 25 skills still pass with 0 errors, every validator exits 0, and the six
script suites are green (35 pass in skill-lint-test). Mutation-tested 4/4:
removing the call, widening the colon rule to any colon, and dropping either
the tab or the unterminated-quote rule each fail the new tests.

Refs #494
2026-09-21 11:17:42 -07:00
ayobamiseun 42d4c6085f fix(validator): share fence parsing to preserve reference-link checks
Reuse the skill linter's fence parser so inline backticks cannot hide broken references and trailing text cannot close a code fence. Cover both failures with LF and CRLF regression cases, preserving diagnostic line numbers.
2026-09-20 18:37:17 +01:00
ayobamiseun a1a80d56d1 fix(validator): strip every CommonMark fence form from linted prose (#437)
stripFencedCodeBlocks matched fences with a single whole-document regex that
only recognised a column-zero backtick fence closed by a run of exactly the
same length. Three CommonMark-legal forms slipped through and their contents
were linted as prose: tilde fences, fences indented one to three spaces, and
closing fences longer than their opener. This is live on main today —
spec-driven-development's list-nested examples use indented fences, so the
linter has been reading their contents as skill text.

Replace the regex with a line scanner that tracks the opener's marker and
length, closes only on a same-marker run at least as long, treats an
unterminated fence as running to end of file (so sections after it fail
loud), and follows the CommonMark rule that a backtick fence's info string
may not contain backticks. Stripped lines become empty lines so positions
are preserved.

Add unit coverage for each form plus CRLF input, and run
scripts/lib/skill-lint-test.js in CI — it was the only test file the
workflow did not execute.

Revives the approach from #444 by @ujjwalks, which was approved and then
closed after a rebase conflict.

Closes #437
2026-09-14 14:04:13 +01:00
CaoBochun 6a268d7710 fix(performance): document the missing guard step 2026-08-28 12:15:32 +08:00
Abhishek Sharma c59c4b481c fix(validator): allowlisted skills only, not Object.prototype keys
`dirName in SECTION_EXEMPT_SKILLS` walks the prototype chain, and
`constructor` satisfies KEBAB_CASE (the `*` quantifier permits a single
word). A skill directory named `constructor` therefore resolved to
Object.prototype.constructor and was silently exempt from every
required-section check — `exempt: true`, zero errors, no sections. The
bypass guard missed it for the same reason, since the resolved value is
truthy.

Both lookups now use Object.hasOwn, which is what the comment above the
allowlist already promises: "Exemptions live HERE, not in skill
frontmatter, so contributors cannot bypass the validator by editing their
own skill file."

Adds scripts/lib/skill-lint-test.js — skill-lint.js was the only
validator without a test file. Confirmed the prototype-chain test goes red
against main; the other six pass either way, so existing behaviour is
unchanged.

Scope kept to the exemption lookup: #428 already rewrites frontmatter
parsing and the cross-reference patterns, so this stays clear of both.
2026-08-21 09:05:25 -07:00
Kevin GlynnandCursor f25f467e7b fix(validator): reject fenced-block headings and negated triggers (#387)
Two heuristic bugs in the section and trigger checks, surfaced in
addyosmani#387 and triaged by @addyosmani and @nucliweb:

Section check: content.includes('## Overview') matches headings inside
fenced code blocks and sub-headings (### Overview) because substring
matching can't distinguish prose from examples. Replace with a
line-anchored regex match against content that has had fenced code
blocks stripped, so only real top-level headings satisfy the check.

Trigger check: 'Do not use when testing' satisfied the DESCRIPTION_TRIGGER
regex because it contains 'use … when'. Add a negation pattern and reject
descriptions where the only trigger match is negated.

Verified: all 24 skills pass (output unchanged — no current skill has
either bug pattern). Crafted edge cases confirm the fixes catch both
issues. Eval suite green, rank-1 rate unchanged at 86%.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-16 10:47:14 -04:00
Joan Leon 495b5a830f refactor(skill-lint): export only functions, keep policy state private
Per review: the policy collections (REQUIRED_SECTIONS, SECTION_EXEMPT_SKILLS,
SKILL_REF_PATTERNS, regexes) were exported by reference, so a consumer could
mutate shared state and change lint results process-wide. Export only the
linting functions; keep the policy collections private. No behavior change
(validator output byte-identical).
2026-07-10 20:05:41 +02:00
Joan Leon 94eb14c74b refactor(validate-skills): extract lint rules into scripts/lib/skill-lint.js
Split the skill validation rules out of the validate-skills.js CLI into a
shared, importable scripts/lib/skill-lint.js: a pure lintSkillContent() with
no I/O plus a thin lintSkill() file wrapper. validate-skills.js becomes a thin
CLI over the lib. No behavior change to the validator's output or exit codes;
this only makes the rules unit-testable for a follow-up test battery.
2026-07-10 00:49:00 +02:00