@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.
- package/builtin/noir-backend/SKILL.md +32 -4
- package/builtin/noir-backend/references/backend-patterns.md +55 -0
- package/builtin/noir-brainstorming/SKILL.md +49 -0
- package/builtin/noir-checkpoint/SKILL.md +30 -12
- package/builtin/noir-context/SKILL.md +29 -10
- package/builtin/noir-doctor/SKILL.md +23 -4
- package/builtin/noir-executing-plans/SKILL.md +59 -0
- package/builtin/noir-exploring/SKILL.md +37 -0
- package/builtin/noir-frontend/SKILL.md +32 -4
- package/builtin/noir-frontend/references/ui-patterns.md +48 -0
- package/builtin/noir-parallel/SKILL.md +25 -35
- package/builtin/noir-planning/SKILL.md +46 -0
- package/builtin/noir-prd/SKILL.md +38 -27
- package/builtin/noir-readme/SKILL.md +25 -4
- package/builtin/noir-recall/SKILL.md +31 -12
- package/builtin/noir-remember/SKILL.md +31 -17
- package/builtin/noir-rules/SKILL.md +28 -27
- package/builtin/noir-security/SKILL.md +33 -4
- package/builtin/noir-security/references/security-checklist.md +49 -0
- package/builtin/noir-shipping/SKILL.md +34 -0
- package/builtin/noir-spec/SKILL.md +48 -14
- package/builtin/noir-spec/references/spec-template.md +43 -0
- package/builtin/noir-subagent/SKILL.md +29 -30
- package/builtin/noir-subagent/references/dispatch-guide.md +50 -0
- package/builtin/noir-sync/SKILL.md +39 -12
- package/builtin/noir-systematic-debugging/SKILL.md +71 -0
- package/builtin/noir-systematic-debugging/references/tracing.md +51 -0
- package/builtin/noir-test-driven-development/SKILL.md +73 -0
- package/builtin/noir-test-driven-development/references/tdd-worked-example.md +65 -0
- package/builtin/noir-verifying/SKILL.md +40 -0
- package/builtin/noir-verifying/references/verification-checklist.md +29 -0
- package/builtin/noir-worktree/SKILL.md +23 -4
- package/builtin/noir-wrap/SKILL.md +29 -13
- package/builtin/noir-writing-skills/SKILL.md +42 -0
- package/dist/index.d.ts +159 -9
- package/dist/index.js +240 -7
- package/dist/index.js.map +1 -1
- package/evals/noir-systematic-debugging/evals.json +25 -0
- package/evals/noir-test-driven-development/evals.json +25 -0
- package/integrations/noir-clickup/SKILL.md +319 -71
- package/package.json +3 -2
- package/builtin/noir-brainstorm/SKILL.md +0 -17
- package/builtin/noir-branch/SKILL.md +0 -12
- package/builtin/noir-clarify/SKILL.md +0 -17
- package/builtin/noir-commit/SKILL.md +0 -12
- package/builtin/noir-debug/SKILL.md +0 -38
- package/builtin/noir-document/SKILL.md +0 -17
- package/builtin/noir-execute/SKILL.md +0 -17
- package/builtin/noir-explore/SKILL.md +0 -16
- package/builtin/noir-intake/SKILL.md +0 -17
- package/builtin/noir-plan/SKILL.md +0 -20
- package/builtin/noir-pr/SKILL.md +0 -12
- package/builtin/noir-review/SKILL.md +0 -28
- package/builtin/noir-skill-author/SKILL.md +0 -12
- package/builtin/noir-tdd/SKILL.md +0 -49
- package/builtin/noir-test/SKILL.md +0 -12
- 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-
|
|
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-
|
|
304
|
-
* `noir-wrap
|
|
305
|
-
* for `writeBack` so a
|
|
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
|
|
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
|
-
|
|
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
|