@noir-ai/skills 1.9.3 → 1.9.4-beta.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/builtin/noir-backend/SKILL.md +32 -4
  2. package/builtin/noir-backend/references/backend-patterns.md +55 -0
  3. package/builtin/noir-brainstorming/SKILL.md +49 -0
  4. package/builtin/noir-checkpoint/SKILL.md +30 -12
  5. package/builtin/noir-context/SKILL.md +29 -10
  6. package/builtin/noir-doctor/SKILL.md +23 -4
  7. package/builtin/noir-executing-plans/SKILL.md +59 -0
  8. package/builtin/noir-exploring/SKILL.md +37 -0
  9. package/builtin/noir-frontend/SKILL.md +32 -4
  10. package/builtin/noir-frontend/references/ui-patterns.md +48 -0
  11. package/builtin/noir-parallel/SKILL.md +25 -35
  12. package/builtin/noir-planning/SKILL.md +46 -0
  13. package/builtin/noir-prd/SKILL.md +38 -27
  14. package/builtin/noir-readme/SKILL.md +25 -4
  15. package/builtin/noir-recall/SKILL.md +31 -12
  16. package/builtin/noir-remember/SKILL.md +31 -17
  17. package/builtin/noir-rules/SKILL.md +28 -27
  18. package/builtin/noir-security/SKILL.md +33 -4
  19. package/builtin/noir-security/references/security-checklist.md +49 -0
  20. package/builtin/noir-shipping/SKILL.md +34 -0
  21. package/builtin/noir-spec/SKILL.md +48 -14
  22. package/builtin/noir-spec/references/spec-template.md +43 -0
  23. package/builtin/noir-subagent/SKILL.md +29 -30
  24. package/builtin/noir-subagent/references/dispatch-guide.md +50 -0
  25. package/builtin/noir-sync/SKILL.md +39 -12
  26. package/builtin/noir-systematic-debugging/SKILL.md +71 -0
  27. package/builtin/noir-systematic-debugging/references/tracing.md +51 -0
  28. package/builtin/noir-test-driven-development/SKILL.md +73 -0
  29. package/builtin/noir-test-driven-development/references/tdd-worked-example.md +65 -0
  30. package/builtin/noir-verifying/SKILL.md +40 -0
  31. package/builtin/noir-verifying/references/verification-checklist.md +29 -0
  32. package/builtin/noir-worktree/SKILL.md +23 -4
  33. package/builtin/noir-wrap/SKILL.md +29 -13
  34. package/builtin/noir-writing-skills/SKILL.md +42 -0
  35. package/dist/index.d.ts +159 -9
  36. package/dist/index.js +240 -7
  37. package/dist/index.js.map +1 -1
  38. package/evals/noir-systematic-debugging/evals.json +25 -0
  39. package/evals/noir-test-driven-development/evals.json +25 -0
  40. package/integrations/noir-clickup/SKILL.md +319 -71
  41. package/package.json +3 -2
  42. package/builtin/noir-brainstorm/SKILL.md +0 -17
  43. package/builtin/noir-branch/SKILL.md +0 -12
  44. package/builtin/noir-clarify/SKILL.md +0 -17
  45. package/builtin/noir-commit/SKILL.md +0 -12
  46. package/builtin/noir-debug/SKILL.md +0 -38
  47. package/builtin/noir-document/SKILL.md +0 -17
  48. package/builtin/noir-execute/SKILL.md +0 -17
  49. package/builtin/noir-explore/SKILL.md +0 -16
  50. package/builtin/noir-intake/SKILL.md +0 -17
  51. package/builtin/noir-plan/SKILL.md +0 -20
  52. package/builtin/noir-pr/SKILL.md +0 -12
  53. package/builtin/noir-review/SKILL.md +0 -28
  54. package/builtin/noir-skill-author/SKILL.md +0 -12
  55. package/builtin/noir-tdd/SKILL.md +0 -49
  56. package/builtin/noir-test/SKILL.md +0 -12
  57. package/builtin/noir-verify/SKILL.md +0 -17
@@ -0,0 +1,42 @@
1
+ ---
2
+ name: noir-writing-skills
3
+ description: Use when authoring or revising a Noir skill — ensure it is valid, WHEN+WHAT-described, structurally complete, and genuinely useful when invoked. Use when the user says "write a skill" or "update a skill". Do NOT use for general documentation.
4
+ metadata:
5
+ category: meta
6
+ version: 1.0.0
7
+ license: MIT
8
+ compatibility: claude · agents-md · gemini · cursor · opencode
9
+ ---
10
+
11
+ # noir-writing-skills
12
+
13
+ The pack's authoring manual. Every skill must be: valid (passes `validateSkill`), structural (required sections), and genuinely useful (has concrete guidance the model doesn't already know). The meta-gate: `noir skills lint` reports pass/fail per skill.
14
+
15
+ ## When to use
16
+
17
+ ## Procedure
18
+ 1. **Follow the guidance** in the notes and verification.
19
+
20
+ - Creating a new `noir-*` skill.
21
+ - Revising an existing skill's body, description, or metadata.
22
+ - The user says "write a skill", "update the skill", "add a skill for X."
23
+ - Verifying the pack — `noir skills lint`.
24
+
25
+ ## Skill writing rules
26
+
27
+ 1. **Frontmatter.** `name` = dir name (`noir-<kebab>`); `description` = WHAT+WHEN, ≤1024 chars, trigger-first, keyword-rich, with boundary ("Do NOT use for..."). Add `metadata.{category,version}`, `license`, `compatibility`. For args-taking skills, add `argument-hint`.
28
+ 2. **Body.** Overview → When to use → Procedure (numbered) → Verification (`- [ ]` checklist) → Notes → "When done → next skill" footer.
29
+ 3. **Quality gate.** `noir skills lint` checks: metadata presence, required sections, line budget (<500), one-level references, WHAT clause, thin-body warnings, no-example warnings. A skill that fails `validateSkill` cannot be emitted.
30
+ 4. **Test it.** Every skill's directives must survive a real prompt. Write `evals/<skill>/evals.json` with offline assertions.
31
+ 5. **No assumptions.** A skill that says "the user knows the project layout" is a broken skill. State the path, name the tool, give the command.
32
+
33
+ ## Verification
34
+
35
+ - [ ] `validateSkill` passes (frontmatter + sections + budget).
36
+ - [ ] `noir skills lint` reports zero errors.
37
+ - [ ] The "When to use" and boundary are mutually-exclusive with sibling skills.
38
+ - [ ] The skill has a "When done → next skill" footer.
39
+
40
+ ## When done → next skill
41
+
42
+ → `noir skills lint` to verify the pack. Or continue authoring.
package/dist/index.d.ts CHANGED
@@ -4,6 +4,14 @@ interface SkillFrontmatter {
4
4
  name: string;
5
5
  description: string;
6
6
  references?: string[];
7
+ /** C3 enhancement — canonical agentskills.io optional fields. `metadata` is a
8
+ * string→string map convention; we type the two keys the registry reads. */
9
+ metadata?: {
10
+ category?: string;
11
+ version?: string;
12
+ };
13
+ license?: string;
14
+ compatibility?: string;
7
15
  [k: string]: unknown;
8
16
  }
9
17
  interface BuiltinReference {
@@ -57,6 +65,9 @@ interface IntegrationSkill extends BuiltinSkill {
57
65
  interface ValidationResult {
58
66
  ok: boolean;
59
67
  errors: string[];
68
+ /** C3 soft-quality warnings (lint-level): thin body, no example, first-person
69
+ * narration, voodoo constants, time-sensitive pins. Present only when >0. */
70
+ warnings?: string[];
60
71
  }
61
72
  interface EmittedFile {
62
73
  path: string[];
@@ -125,7 +136,7 @@ type SkillConflictResolution = 'replace' | 'preserve' | 'rename' | 'duplicate' |
125
136
  * regenerate conflicts.
126
137
  */
127
138
  interface SkillConflictContext {
128
- /** Path relative to the skills target dir (e.g. `noir-brainstorm/SKILL.md`). */
139
+ /** Path relative to the skills target dir (e.g. `noir-brainstorming/SKILL.md`). */
129
140
  relPath: string;
130
141
  /** The skill file's current on-disk bytes. */
131
142
  existing: string;
@@ -214,13 +225,63 @@ declare function discoverAll(opts?: {
214
225
  integrations: IntegrationSkill[];
215
226
  };
216
227
 
228
+ /** The max body length the canon recommends (Anthropic: "under 500 lines").
229
+ * SKILL.md is a navigator, not a repository — split to references/ past this. */
230
+ declare const MAX_BODY_LINES = 500;
231
+ /** The floor for a full (non-stub) playbook body. Stubs were ~13 lines; a real
232
+ * playbook needs at least this much substance to be loadable as guidance. */
233
+ declare const MIN_FULL_BODY_LINES = 20;
234
+ /**
235
+ * Which required sections are missing from a skill body. Returns a list of
236
+ * human-readable section names, e.g. `['## When to use', '## Verification']`.
237
+ * Empty array = the body carries the canonical playbook shape.
238
+ */
239
+ declare function missingSections(body: string): string[];
240
+ /** True when the body is within the canon line budget. */
241
+ declare function withinLineBudget(body: string, max?: number): boolean;
242
+ /** Reference files that point at another reference file (chained — forbidden).
243
+ * Returns the offending reference names. */
244
+ declare function chainedReferences(skill: BuiltinSkill): string[];
245
+ /**
246
+ * True when the description carries BOTH a WHEN trigger lead AND a WHAT clause.
247
+ * The C3 rule: `description` MUST lead with a WHEN cue (existing compiler rule)
248
+ * AND contain a compact WHAT clause naming what the skill does.
249
+ *
250
+ * Two canonical shapes both pass:
251
+ * "Use when turning an idea into a spec — draft the spec." (WHEN → WHAT)
252
+ * "Use when a task writes back to ClickUp: update its status." (WHEN → WHAT)
253
+ * The description MUST lead with a cue (the WHEN-only and WHAT-only schools are
254
+ * both rejected). The WHAT clause is the part after the trigger phrase — split
255
+ * on an em/en dash or a period that ends the trigger phrase. A real WHAT clause
256
+ * has ≥3 words naming the action, so a bare "Use when…" fails.
257
+ */
258
+ declare function isWhatWhenDescription(description: string): boolean;
259
+ /** True when the description leads with a WHEN cue (the existing rule, kept
260
+ * here so validate + lint + hygiene share one implementation). */
261
+ declare function looksLikeWhenDescription(description: string): boolean;
262
+ /**
263
+ * Soft-quality warnings for `lintSkill`. Each returns a short rule id + message.
264
+ * These are advisory — a skill can pass `validateSkill` and still carry lint
265
+ * warnings that the author should resolve. Rules are drawn from the researched
266
+ * anti-pattern list (no examples, thin body, first/second person narration,
267
+ * voodoo constants, time-sensitive version pins).
268
+ */
269
+ declare function lintWarnings(skill: BuiltinSkill): string[];
270
+
217
271
  declare function parseFrontmatter(md: string): SkillFrontmatter;
218
272
  declare function bodyOf(md: string): string;
219
- /** A WHEN description leads with its trigger. Requiring a leading cue — rather
220
- * than a loose "contains when/before/after anywhere" — avoids false positives
221
- * ("A tool that decides when to run tests") and accepts valid leads ("Upon…"). */
222
- declare function looksLikeWhenDescription(desc: string): boolean;
223
273
  declare function validateSkill(skill: BuiltinSkill): ValidationResult;
274
+ /**
275
+ * `lintSkill` — the C3 soft quality gate. Errors = `validateSkill` errors (a
276
+ * skill that fails validation is broken); warnings = `quality.ts` style rules
277
+ * (thin body, no examples, first-person narration, …). A skill can validate
278
+ * clean yet still carry lint warnings the author should resolve.
279
+ */
280
+ declare function lintSkill(skill: BuiltinSkill): {
281
+ name: string;
282
+ errors: string[];
283
+ warnings: string[];
284
+ };
224
285
  /**
225
286
  * Compile a builtin skill into the host-shaped `CompiledSkill`. The validator
226
287
  * runs for every target (a malformed skill is malformed in any format).
@@ -286,6 +347,57 @@ declare function emitSkillsToDir(targetDir: string, opts?: {
286
347
  interactive?: boolean;
287
348
  }): Promise<EmitSummary>;
288
349
 
350
+ /** A single offline assertion on the produced output. */
351
+ type EvalAssertion = {
352
+ type: 'contains';
353
+ value: string;
354
+ } | {
355
+ type: 'not-contains';
356
+ value: string;
357
+ } | {
358
+ type: 'regex';
359
+ value: string;
360
+ } | {
361
+ type: 'length-gte';
362
+ value: number;
363
+ };
364
+ /** One eval case: a prompt + the expected directive the skill must produce. */
365
+ interface SkillEval {
366
+ id: string;
367
+ prompt: string;
368
+ expected_output: string;
369
+ assertions?: EvalAssertion[];
370
+ }
371
+ /** A suite = one skill's evals (the `evals.json` shape). */
372
+ interface EvalSuite {
373
+ skill_name: string;
374
+ evals: SkillEval[];
375
+ }
376
+ /** Parse + validate a raw `evals.json` payload. Throws on malformed shape. */
377
+ declare function parseEvalSuite(raw: unknown): EvalSuite;
378
+ /** Run a list of assertions against `output`. Returns pass + per-assertion failures. */
379
+ declare function runAssertions(output: string, assertions: EvalAssertion[]): {
380
+ pass: boolean;
381
+ failures: string[];
382
+ };
383
+ declare const EVALS_DIR: string;
384
+ /**
385
+ * Load every eval suite under `dir` (default the shipped `evals/` dir — each
386
+ * subdir carries an `evals.json`). Skips non-`.json` files and missing dirs
387
+ * (no evals = no-op). Fail-fast on a malformed suite (a broken evals.json is a
388
+ * bug, not a skip).
389
+ */
390
+ declare function loadEvalSuites(dir?: string): EvalSuite[];
391
+ /** Evaluate one suite: for each eval, check its assertions against
392
+ * `expected_output` (the directive the skill should produce). Returns pass/fail
393
+ * per eval with the assertion failures. This is the offline core the vitest
394
+ * runner drives. */
395
+ declare function evaluateSuite(suite: EvalSuite): Array<{
396
+ id: string;
397
+ pass: boolean;
398
+ failures: string[];
399
+ }>;
400
+
289
401
  /** Auth shape. Locked: `env-var` only until keychain lands (Q4b — refuse OAuth,
290
402
  * never silently lower the security bar). `fallback:'manual-paste'` keeps the
291
403
  * no-token path honest (the playbook tells the user to paste a value); `'none'`
@@ -300,9 +412,10 @@ declare const IntegrationAuthSchema: z.ZodObject<{
300
412
  }>>;
301
413
  }, z.core.$strip>;
302
414
  /** SDD two-way binding. `intakeFrom` declares the external artifact kind the
303
- * `noir-intake` skill pulls from; `writeBack` enumerates the fields
304
- * `noir-wrap`/`noir-document` push back at session end. Strings (not enums)
305
- * for `writeBack` so a per-integration vocabulary stays expressible without
415
+ * `noir-brainstorming` skill pulls from (absorbed noir-intake in C3);
416
+ * `writeBack` enumerates the fields `noir-wrap` pushes back at session end
417
+ * (absorbed noir-document in C3). Strings (not enums) for `writeBack` so a
418
+ * per-integration vocabulary stays expressible without
306
419
  * churning the schema. */
307
420
  declare const IntegrationSddSchema: z.ZodDefault<z.ZodObject<{
308
421
  intakeFrom: z.ZodOptional<z.ZodEnum<{
@@ -382,6 +495,43 @@ declare function validateIntegration(obj: unknown): {
382
495
  * existing `noir mcp serve --stdio` entry so no NEW server entry is emitted. */
383
496
  declare function runtimeEmitsHostMcp(runtime: IntegrationDeclaration['runtime']): boolean;
384
497
 
498
+ /** The pack-wide `noir-` namespace a registry entry always belongs to. */
499
+ declare const NOIR_NAMESPACE = "noir-";
500
+ /** One row in the derived registry. */
501
+ interface SkillRegistryEntry {
502
+ /** Canonical `noir-<kebab>` id (== dir name). */
503
+ name: string;
504
+ /** `builtin` for the shipped pack; `integration` for an `integrations/<name>/`. */
505
+ kind: 'builtin' | 'integration';
506
+ /** Category from `metadata.category`; falls back to the `noir-`-stripped name
507
+ * (a newly-authored skill still gets a sensible cell). */
508
+ category: string;
509
+ /** Per-skill version from `metadata.version`; defaults to `0.0.0` when absent
510
+ * (the pack-level version in package.json is the authoritative release). */
511
+ version: string;
512
+ /** `full` when the body has no `> **Stub:**` marker; `stub` otherwise. */
513
+ status: 'full' | 'stub';
514
+ /** Lifecycle stage of the skill. `active` = currently shipped and usable;
515
+ * `deprecated` = superseded (a renamed/merged predecessor). C3 spec §8.
516
+ * The curated pack has no deprecated members today. */
517
+ lifecycle: 'active' | 'deprecated';
518
+ /** The WHAT+WHEN description (the trigger the host sees). */
519
+ description: string;
520
+ /** Number of `references/*.md` files. */
521
+ referenceCount: number;
522
+ /** Total SKILL.md line count (frontmatter + body). */
523
+ lines: number;
524
+ }
525
+ /**
526
+ * Build the full registry from `discoverAll()` — builtins + integrations in
527
+ * one list, sorted by name. Pure (no I/O beyond discovery); never throws for
528
+ * a malformed skill (a discovery failure surfaces upstream, the same as the
529
+ * compiler's fail-fast).
530
+ */
531
+ declare function buildRegistry(): SkillRegistryEntry[];
532
+ /** Convenience: registry filtered to a single category (CLI grouping). */
533
+ declare function registryByCategory(category: string): SkillRegistryEntry[];
534
+
385
535
  declare const FORBIDDEN_RESIDUE: readonly string[];
386
536
 
387
- export { BUILTIN_DIR, type BuiltinReference, type BuiltinSkill, type CompileTarget, type CompiledIntegration, type CompiledSkill, type EmitSummary, type EmittedFile, FORBIDDEN_RESIDUE, INTEGRATIONS_DIR, IntegrationAuthSchema, type IntegrationDeclaration$1 as IntegrationDeclaration, IntegrationDeclarationSchema, IntegrationMcpSchema, IntegrationSddSchema, type IntegrationSkill, type SkillFrontmatter, type ValidationResult, bodyOf, compileIntegration, compileSkill, discoverAll, discoverBuiltin, discoverIntegrations, emitSkillsToDir, looksLikeWhenDescription, parseFrontmatter, parseIntegration, runtimeEmitsHostMcp, validateIntegration, validateSkill };
537
+ export { BUILTIN_DIR, type BuiltinReference, type BuiltinSkill, type CompileTarget, type CompiledIntegration, type CompiledSkill, EVALS_DIR, type EmitSummary, type EmittedFile, type EvalAssertion, type EvalSuite, FORBIDDEN_RESIDUE, INTEGRATIONS_DIR, IntegrationAuthSchema, type IntegrationDeclaration$1 as IntegrationDeclaration, IntegrationDeclarationSchema, IntegrationMcpSchema, IntegrationSddSchema, type IntegrationSkill, MAX_BODY_LINES, MIN_FULL_BODY_LINES, NOIR_NAMESPACE, type SkillEval, type SkillFrontmatter, type SkillRegistryEntry, type ValidationResult, bodyOf, buildRegistry, chainedReferences, compileIntegration, compileSkill, discoverAll, discoverBuiltin, discoverIntegrations, emitSkillsToDir, evaluateSuite, isWhatWhenDescription, lintSkill, lintWarnings, loadEvalSuites, looksLikeWhenDescription, missingSections, parseEvalSuite, parseFrontmatter, parseIntegration, registryByCategory, runAssertions, runtimeEmitsHostMcp, validateIntegration, validateSkill, withinLineBudget };
package/dist/index.js CHANGED
@@ -106,10 +106,67 @@ function discoverAll(opts = {}) {
106
106
  };
107
107
  }
108
108
 
109
+ // src/quality.ts
110
+ var MAX_BODY_LINES = 500;
111
+ var MIN_FULL_BODY_LINES = 20;
112
+ var WHEN_SECTION = /^## When to use$/im;
113
+ var PROCEDURE_SECTION = /^## (Procedure|Steps)$/im;
114
+ var CLOSING_SECTION = /^## (Verification|Notes|Fallbacks|Troubleshooting|Why order matters)$/im;
115
+ var WHEN_CUE = /^(use|using|used|whenever|when|before|after|while|starting|encountering|completing|creating|about to|upon|during|to|for|on)\b/i;
116
+ var CHAINED_REF_RE = /\]\((\/?\.?\.?\/)?(references\/|\.\.\/references\/)/i;
117
+ function missingSections(body) {
118
+ const missing = [];
119
+ if (!WHEN_SECTION.test(body)) missing.push("## When to use");
120
+ if (!PROCEDURE_SECTION.test(body)) missing.push("## Procedure (or ## Steps)");
121
+ if (!CLOSING_SECTION.test(body)) {
122
+ missing.push("one of ## Verification / ## Notes / ## Fallbacks / ## Troubleshooting");
123
+ }
124
+ return missing;
125
+ }
126
+ function withinLineBudget(body, max = MAX_BODY_LINES) {
127
+ return body.split("\n").length <= max;
128
+ }
129
+ function chainedReferences(skill) {
130
+ return skill.references.filter((r) => CHAINED_REF_RE.test(r.content)).map((r) => r.name);
131
+ }
132
+ function isWhatWhenDescription(description) {
133
+ const trimmed = description.trim();
134
+ if (!trimmed) return false;
135
+ if (!WHEN_CUE.test(trimmed)) return false;
136
+ const whatPart = trimmed.split(/[—–]|(?<=\.) /).slice(1).join(" ").trim();
137
+ return whatPart.split(/\s+/).filter(Boolean).length >= 3;
138
+ }
139
+ function looksLikeWhenDescription(description) {
140
+ return WHEN_CUE.test(description.trim());
141
+ }
142
+ function lintWarnings(skill) {
143
+ const warnings = [];
144
+ const body = skill.skillMd;
145
+ const bodyLines = body.split("\n").length;
146
+ if (bodyLines < MIN_FULL_BODY_LINES + 6) {
147
+ warnings.push("thin-body: full playbook body is under 20 lines");
148
+ }
149
+ const hasExample = /```/.test(body) || /\bexample:?\b/i.test(body) || /\be\.g\.\b/i.test(body);
150
+ if (!hasExample) {
151
+ warnings.push("no-example: no concrete code fence or worked example in the body");
152
+ }
153
+ if (/\b(I|we|you)\s+(will|should|can|need|must|do|write|create|implement|run|build)\b/i.test(body)) {
154
+ warnings.push("first-person: narration addresses the reader instead of imperative steps");
155
+ }
156
+ if (/\b(?:wait|sleep|retry|backoff|limit|cap)\s+[a-z]*\s*(\d{1,4})\b/i.test(body) && !/because|to (avoid|prevent|give|let)/i.test(body)) {
157
+ warnings.push("voodoo-constant: numeric threshold without a stated reason");
158
+ }
159
+ const hasVersionPin = /as of \d{4}|\bversion \d+\.\d+\.\d+\b|"v\d+\.\d+"/i.test(body);
160
+ const hasLegacySection = /^## Legacy|^## Old patterns/i.test(body);
161
+ if (hasVersionPin && !hasLegacySection) {
162
+ warnings.push("time-sensitive: version/date pin outside a Legacy section");
163
+ }
164
+ return warnings;
165
+ }
166
+
109
167
  // src/compiler.ts
110
168
  var FRONTMATTER_RE = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/;
111
169
  var NAME_RE = /^noir-[a-z0-9]+(?:-[a-z0-9]+)*$/;
112
- var WHEN_START = /^(use|using|used|whenever|when|before|after|while|starting|encountering|completing|creating|about to|upon|during|to|for|on)\b/i;
113
170
  var MAX_DESC = 1024;
114
171
  function parseFrontmatter(md) {
115
172
  const m = md.match(FRONTMATTER_RE);
@@ -125,12 +182,10 @@ function parseFrontmatter(md) {
125
182
  function bodyOf(md) {
126
183
  return md.replace(/^---\r?\n[\s\S]*?\r?\n---\r?\n?/, "");
127
184
  }
128
- function looksLikeWhenDescription(desc) {
129
- return WHEN_START.test(desc.trim());
130
- }
131
185
  function validateSkill(skill) {
132
186
  const errors = [];
133
- const { name, description } = skill.frontmatter;
187
+ const warnings = [];
188
+ const { name, description, metadata } = skill.frontmatter;
134
189
  if (!name) errors.push("missing `name`");
135
190
  else if (!NAME_RE.test(name)) errors.push(`name "${name}" must match noir-<kebab>`);
136
191
  if (basename(skill.dir) !== name) {
@@ -140,12 +195,30 @@ function validateSkill(skill) {
140
195
  else if (description.length > MAX_DESC) errors.push(`description exceeds ${MAX_DESC} chars`);
141
196
  else if (!looksLikeWhenDescription(description)) {
142
197
  errors.push('description must state WHEN to trigger (e.g. "Use when\u2026"), not WHAT it does');
198
+ } else if (!isWhatWhenDescription(description)) {
199
+ errors.push("description must be WHAT+WHEN \u2014 a WHAT clause after the trigger phrase");
143
200
  }
201
+ if (!metadata?.category?.trim()) errors.push("missing `metadata.category`");
202
+ if (!metadata?.version?.trim()) errors.push("missing `metadata.version`");
203
+ const body = bodyOf(skill.skillMd);
204
+ const missing = missingSections(body);
205
+ for (const sec of missing) errors.push(`missing required section: ${sec}`);
206
+ if (!withinLineBudget(body)) {
207
+ errors.push(`body exceeds ${MAX_BODY_LINES}-line budget (${body.split("\n").length} lines)`);
208
+ }
209
+ const chained = chainedReferences(skill);
210
+ for (const r of chained)
211
+ errors.push(`reference "${r}" chains to another reference (must be one level deep)`);
144
212
  for (const r of skill.references) {
145
213
  if (!/^[a-z0-9-]+\.md$/i.test(r.name)) errors.push(`reference "${r.name}" must be <kebab>.md`);
146
214
  if (!r.content.trim()) errors.push(`reference "${r.name}" is empty`);
147
215
  }
148
- return { ok: errors.length === 0, errors };
216
+ warnings.push(...lintWarnings(skill));
217
+ return { ok: errors.length === 0, errors, warnings: warnings.length > 0 ? warnings : void 0 };
218
+ }
219
+ function lintSkill(skill) {
220
+ const res = validateSkill(skill);
221
+ return { name: skill.name, errors: res.errors, warnings: res.warnings ?? [] };
149
222
  }
150
223
  function compileSkill(skill, target = "claude") {
151
224
  const res = validateSkill(skill);
@@ -425,6 +498,150 @@ async function uniqueAsideAbs(abs, suffix) {
425
498
  return candidate;
426
499
  }
427
500
 
501
+ // src/evals.ts
502
+ import { readdirSync as readdirSync2, readFileSync as readFileSync2 } from "fs";
503
+ import { dirname as dirname3, join as join3 } from "path";
504
+ import { fileURLToPath as fileURLToPath2 } from "url";
505
+ function parseAssertion(a) {
506
+ if (typeof a !== "object" || a === null) throw new Error("assertion must be an object");
507
+ const { type, value } = a;
508
+ switch (type) {
509
+ case "contains":
510
+ case "not-contains":
511
+ case "regex":
512
+ if (typeof value !== "string" || value.length === 0) {
513
+ throw new Error(`assertion '${String(type)}' requires a non-empty string value`);
514
+ }
515
+ return { type, value };
516
+ case "length-gte":
517
+ if (typeof value !== "number" || !Number.isFinite(value)) {
518
+ throw new Error("assertion 'length-gte' requires a finite number value");
519
+ }
520
+ return { type, value };
521
+ default:
522
+ throw new Error(`unknown assertion type '${String(type)}'`);
523
+ }
524
+ }
525
+ function parseEvalSuite(raw) {
526
+ if (typeof raw !== "object" || raw === null) throw new Error("evals.json must be an object");
527
+ const { skill_name, evals } = raw;
528
+ if (typeof skill_name !== "string" || skill_name.length === 0) {
529
+ throw new Error("evals.json requires a non-empty string `skill_name`");
530
+ }
531
+ if (!Array.isArray(evals) || evals.length === 0) {
532
+ throw new Error(`evals.json for ${skill_name} requires a non-empty 'evals' array`);
533
+ }
534
+ const parsed = evals.map((e, i) => {
535
+ if (typeof e !== "object" || e === null) throw new Error(`eval[${i}] must be an object`);
536
+ const { id, prompt, expected_output, assertions } = e;
537
+ if (typeof id !== "string" || id.length === 0)
538
+ throw new Error(`eval[${i}] requires a string id`);
539
+ if (typeof prompt !== "string" || prompt.length === 0) {
540
+ throw new Error(`eval ${id} requires a string prompt`);
541
+ }
542
+ if (typeof expected_output !== "string") {
543
+ throw new Error(`eval ${id} requires a string expected_output`);
544
+ }
545
+ return {
546
+ id,
547
+ prompt,
548
+ expected_output,
549
+ assertions: Array.isArray(assertions) ? assertions.map(parseAssertion) : void 0
550
+ };
551
+ });
552
+ return { skill_name, evals: parsed };
553
+ }
554
+ function runAssertions(output, assertions) {
555
+ const failures = [];
556
+ for (const a of assertions) {
557
+ switch (a.type) {
558
+ case "contains":
559
+ if (!output.includes(a.value)) failures.push(`expected to contain "${a.value}"`);
560
+ break;
561
+ case "not-contains":
562
+ if (output.includes(a.value)) failures.push(`expected NOT to contain "${a.value}"`);
563
+ break;
564
+ case "regex":
565
+ if (!new RegExp(a.value, "i").test(output)) failures.push(`expected to match /${a.value}/`);
566
+ break;
567
+ case "length-gte":
568
+ if (output.length < a.value)
569
+ failures.push(`expected length >= ${a.value} (got ${output.length})`);
570
+ break;
571
+ }
572
+ }
573
+ return { pass: failures.length === 0, failures };
574
+ }
575
+ var HERE2 = dirname3(fileURLToPath2(import.meta.url));
576
+ var PKG_ROOT2 = dirname3(HERE2);
577
+ var EVALS_DIR = join3(PKG_ROOT2, "evals");
578
+ function loadEvalSuites(dir = EVALS_DIR) {
579
+ let entries;
580
+ try {
581
+ entries = readdirSync2(dir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name).sort();
582
+ } catch {
583
+ return [];
584
+ }
585
+ const suites = [];
586
+ for (const name of entries) {
587
+ const file = join3(dir, name, "evals.json");
588
+ try {
589
+ const raw = JSON.parse(readFileSync2(file, "utf8"));
590
+ suites.push(parseEvalSuite(raw));
591
+ } catch (err) {
592
+ try {
593
+ readFileSync2(file, "utf8");
594
+ throw err;
595
+ } catch (re) {
596
+ if (re === err) throw err;
597
+ }
598
+ }
599
+ }
600
+ return suites;
601
+ }
602
+ function evaluateSuite(suite) {
603
+ return suite.evals.map((e) => ({
604
+ id: e.id,
605
+ pass: e.assertions ? runAssertions(e.expected_output, e.assertions).pass : true,
606
+ failures: e.assertions ? runAssertions(e.expected_output, e.assertions).failures : []
607
+ }));
608
+ }
609
+
610
+ // src/registry.ts
611
+ var NOIR_NAMESPACE = "noir-";
612
+ function fallbackCategory(name) {
613
+ return name.replace(/^noir-/, "") || "general";
614
+ }
615
+ function toEntry(s, kind) {
616
+ const category = s.frontmatter.metadata?.category?.trim() || fallbackCategory(s.name);
617
+ const version = s.frontmatter.metadata?.version?.trim() || "0.0.0";
618
+ const status = s.skillMd.includes("> **Stub:**") ? "stub" : "full";
619
+ const lines = s.skillMd.split("\n").length;
620
+ return {
621
+ name: s.name,
622
+ kind,
623
+ category,
624
+ version,
625
+ status,
626
+ // Every shipped skill is active; `deprecated` is reserved for a future
627
+ // superseded member (C3 spec §8).
628
+ lifecycle: "active",
629
+ description: typeof s.frontmatter.description === "string" ? s.frontmatter.description : "",
630
+ referenceCount: s.references.length,
631
+ lines
632
+ };
633
+ }
634
+ function buildRegistry() {
635
+ const { builtins, integrations } = discoverAll();
636
+ return [
637
+ ...builtins.map((b) => toEntry(b, "builtin")),
638
+ ...integrations.map((i) => toEntry(i, "integration"))
639
+ ].sort((a, b) => a.name.localeCompare(b.name));
640
+ }
641
+ function registryByCategory(category) {
642
+ return buildRegistry().filter((e) => e.category === category);
643
+ }
644
+
428
645
  // src/residue.ts
429
646
  var FORBIDDEN_RESIDUE = [
430
647
  "workflow/<task",
@@ -448,24 +665,40 @@ var FORBIDDEN_RESIDUE = [
448
665
  ];
449
666
  export {
450
667
  BUILTIN_DIR,
668
+ EVALS_DIR,
451
669
  FORBIDDEN_RESIDUE,
452
670
  INTEGRATIONS_DIR,
453
671
  IntegrationAuthSchema,
454
672
  IntegrationDeclarationSchema,
455
673
  IntegrationMcpSchema,
456
674
  IntegrationSddSchema,
675
+ MAX_BODY_LINES,
676
+ MIN_FULL_BODY_LINES,
677
+ NOIR_NAMESPACE,
457
678
  bodyOf,
679
+ buildRegistry,
680
+ chainedReferences,
458
681
  compileIntegration,
459
682
  compileSkill,
460
683
  discoverAll,
461
684
  discoverBuiltin,
462
685
  discoverIntegrations,
463
686
  emitSkillsToDir,
687
+ evaluateSuite,
688
+ isWhatWhenDescription,
689
+ lintSkill,
690
+ lintWarnings,
691
+ loadEvalSuites,
464
692
  looksLikeWhenDescription,
693
+ missingSections,
694
+ parseEvalSuite,
465
695
  parseFrontmatter,
466
696
  parseIntegration,
697
+ registryByCategory,
698
+ runAssertions,
467
699
  runtimeEmitsHostMcp,
468
700
  validateIntegration,
469
- validateSkill
701
+ validateSkill,
702
+ withinLineBudget
470
703
  };
471
704
  //# sourceMappingURL=index.js.map