javi-forge 1.6.0 → 1.6.1
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/dist/commands/analyze.d.ts +1 -1
- package/dist/commands/analyze.js +15 -15
- package/dist/commands/atlassian-mcp.d.ts +42 -0
- package/dist/commands/atlassian-mcp.js +98 -0
- package/dist/commands/ci.d.ts +3 -3
- package/dist/commands/ci.js +185 -147
- package/dist/commands/crash-recovery.d.ts +34 -0
- package/dist/commands/crash-recovery.js +123 -0
- package/dist/commands/doctor.d.ts +2 -2
- package/dist/commands/doctor.js +113 -61
- package/dist/commands/harness-audit.d.ts +35 -0
- package/dist/commands/harness-audit.js +277 -0
- package/dist/commands/init.d.ts +1 -1
- package/dist/commands/init.js +384 -141
- package/dist/commands/llmstxt.d.ts +1 -1
- package/dist/commands/llmstxt.js +36 -34
- package/dist/commands/parallel-batch.d.ts +42 -0
- package/dist/commands/parallel-batch.js +90 -0
- package/dist/commands/plugin.d.ts +10 -1
- package/dist/commands/plugin.js +92 -47
- package/dist/commands/secret-scanner.d.ts +30 -0
- package/dist/commands/secret-scanner.js +272 -0
- package/dist/commands/security-analysis.d.ts +74 -0
- package/dist/commands/security-analysis.js +487 -0
- package/dist/commands/security.d.ts +11 -5
- package/dist/commands/security.js +216 -76
- package/dist/commands/skill-scanner.d.ts +63 -0
- package/dist/commands/skill-scanner.js +383 -0
- package/dist/commands/skills.d.ts +62 -5
- package/dist/commands/skills.js +439 -54
- package/dist/commands/supply-chain.d.ts +23 -0
- package/dist/commands/supply-chain.js +126 -0
- package/dist/commands/tdd-pipeline.d.ts +17 -0
- package/dist/commands/tdd-pipeline.js +144 -0
- package/dist/commands/tdd.d.ts +1 -1
- package/dist/commands/tdd.js +21 -18
- package/dist/commands/team-presets.d.ts +53 -0
- package/dist/commands/team-presets.js +201 -0
- package/dist/commands/workflow.d.ts +23 -0
- package/dist/commands/workflow.js +114 -0
- package/dist/constants.d.ts +15 -1
- package/dist/constants.js +161 -122
- package/dist/index.js +308 -98
- package/dist/lib/agent-skills.d.ts +36 -1
- package/dist/lib/agent-skills.js +168 -19
- package/dist/lib/auto-skill-install.d.ts +37 -0
- package/dist/lib/auto-skill-install.js +92 -0
- package/dist/lib/auto-wire.d.ts +20 -0
- package/dist/lib/auto-wire.js +240 -0
- package/dist/lib/claudemd.d.ts +13 -1
- package/dist/lib/claudemd.js +174 -24
- package/dist/lib/codex-export.d.ts +1 -1
- package/dist/lib/codex-export.js +29 -31
- package/dist/lib/common.d.ts +1 -1
- package/dist/lib/common.js +52 -44
- package/dist/lib/context.d.ts +17 -2
- package/dist/lib/context.js +142 -13
- package/dist/lib/docker.d.ts +1 -1
- package/dist/lib/docker.js +141 -112
- package/dist/lib/frontmatter.d.ts +1 -1
- package/dist/lib/frontmatter.js +29 -15
- package/dist/lib/plugin.d.ts +9 -3
- package/dist/lib/plugin.js +128 -69
- package/dist/lib/skill-publish.d.ts +40 -0
- package/dist/lib/skill-publish.js +146 -0
- package/dist/lib/stack-detector.d.ts +38 -0
- package/dist/lib/stack-detector.js +207 -0
- package/dist/lib/template.d.ts +16 -1
- package/dist/lib/template.js +46 -17
- package/dist/lib/workflow/discovery.d.ts +19 -0
- package/dist/lib/workflow/discovery.js +68 -0
- package/dist/lib/workflow/index.d.ts +5 -0
- package/dist/lib/workflow/index.js +5 -0
- package/dist/lib/workflow/parser.d.ts +16 -0
- package/dist/lib/workflow/parser.js +198 -0
- package/dist/lib/workflow/renderer.d.ts +9 -0
- package/dist/lib/workflow/renderer.js +152 -0
- package/dist/lib/workflow/validator.d.ts +10 -0
- package/dist/lib/workflow/validator.js +189 -0
- package/dist/tasks/index.d.ts +4 -0
- package/dist/tasks/index.js +4 -0
- package/dist/tasks/scaffold-tasks.d.ts +3 -0
- package/dist/tasks/scaffold-tasks.js +14 -0
- package/dist/tasks/task-id.d.ts +30 -0
- package/dist/tasks/task-id.js +55 -0
- package/dist/tasks/task-tracker.d.ts +15 -0
- package/dist/tasks/task-tracker.js +81 -0
- package/dist/types/index.d.ts +134 -6
- package/dist/types/index.js +11 -1
- package/dist/ui/AnalyzeUI.d.ts +1 -1
- package/dist/ui/AnalyzeUI.js +38 -39
- package/dist/ui/App.d.ts +5 -3
- package/dist/ui/App.js +86 -46
- package/dist/ui/AutoSkills.d.ts +9 -0
- package/dist/ui/AutoSkills.js +124 -0
- package/dist/ui/CI.d.ts +2 -2
- package/dist/ui/CI.js +24 -26
- package/dist/ui/CIContext.d.ts +1 -1
- package/dist/ui/CIContext.js +3 -2
- package/dist/ui/CISelector.d.ts +2 -2
- package/dist/ui/CISelector.js +23 -15
- package/dist/ui/Doctor.d.ts +1 -1
- package/dist/ui/Doctor.js +35 -29
- package/dist/ui/Header.d.ts +1 -1
- package/dist/ui/Header.js +14 -14
- package/dist/ui/HookProfileSelector.d.ts +9 -0
- package/dist/ui/HookProfileSelector.js +54 -0
- package/dist/ui/LlmsTxt.d.ts +1 -1
- package/dist/ui/LlmsTxt.js +31 -22
- package/dist/ui/MemorySelector.d.ts +2 -2
- package/dist/ui/MemorySelector.js +28 -16
- package/dist/ui/NameInput.d.ts +1 -1
- package/dist/ui/NameInput.js +21 -21
- package/dist/ui/OptionSelector.d.ts +6 -2
- package/dist/ui/OptionSelector.js +83 -32
- package/dist/ui/Plugin.d.ts +4 -3
- package/dist/ui/Plugin.js +78 -35
- package/dist/ui/Progress.d.ts +3 -3
- package/dist/ui/Progress.js +23 -22
- package/dist/ui/Skills.d.ts +2 -2
- package/dist/ui/Skills.js +61 -32
- package/dist/ui/StackSelector.d.ts +2 -2
- package/dist/ui/StackSelector.js +26 -16
- package/dist/ui/Summary.d.ts +3 -3
- package/dist/ui/Summary.js +60 -50
- package/dist/ui/Welcome.d.ts +1 -1
- package/dist/ui/Welcome.js +15 -16
- package/dist/ui/theme.d.ts +1 -1
- package/dist/ui/theme.js +6 -6
- package/package.json +9 -6
- package/templates/common/atlassian/mcp-atlassian-snippet.json +16 -0
- package/templates/common/repoforge/mcp-repoforge-snippet.json +11 -0
- package/templates/common/repoforge/repoforge.yaml +34 -0
- package/templates/github/deploy-docker-zero-downtime.yml +140 -0
- package/templates/github/repoforge-graph.yml +45 -0
- package/templates/gitlab/deploy-docker-zero-downtime.yml +57 -0
- package/templates/local-ai/.env.example +17 -0
- package/templates/local-ai/docker-compose.yml +95 -0
- package/templates/security-hooks/claude-settings-security.json +30 -0
- package/templates/security-hooks/commit-msg-signing +29 -0
- package/templates/security-hooks/pre-commit-permissions +74 -0
- package/templates/security-hooks/pre-commit-secrets +74 -0
- package/templates/security-hooks/pre-push-branch-protection +62 -0
- package/templates/security-hooks/pre-push-deps +83 -0
- package/templates/security-hooks/pre-push-signing +67 -0
- package/templates/woodpecker/deploy-docker-zero-downtime.yml +50 -0
- package/templates/workflows/ci-pipeline.dot +15 -0
- package/templates/workflows/feature-flow.dot +21 -0
- package/templates/workflows/release.dot +16 -0
- package/dist/__integration__/helpers.d.ts +0 -20
- package/dist/__integration__/helpers.d.ts.map +0 -1
- package/dist/__integration__/helpers.js +0 -31
- package/dist/__integration__/helpers.js.map +0 -1
- package/dist/commands/analyze.d.ts.map +0 -1
- package/dist/commands/analyze.js.map +0 -1
- package/dist/commands/ci.d.ts.map +0 -1
- package/dist/commands/ci.js.map +0 -1
- package/dist/commands/doctor.d.ts.map +0 -1
- package/dist/commands/doctor.js.map +0 -1
- package/dist/commands/init.d.ts.map +0 -1
- package/dist/commands/init.js.map +0 -1
- package/dist/commands/llmstxt.d.ts.map +0 -1
- package/dist/commands/llmstxt.js.map +0 -1
- package/dist/commands/plugin.d.ts.map +0 -1
- package/dist/commands/plugin.js.map +0 -1
- package/dist/commands/security.d.ts.map +0 -1
- package/dist/commands/security.js.map +0 -1
- package/dist/commands/skills.d.ts.map +0 -1
- package/dist/commands/skills.js.map +0 -1
- package/dist/commands/tdd.d.ts.map +0 -1
- package/dist/commands/tdd.js.map +0 -1
- package/dist/constants.d.ts.map +0 -1
- package/dist/constants.js.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/lib/agent-skills.d.ts.map +0 -1
- package/dist/lib/agent-skills.js.map +0 -1
- package/dist/lib/claudemd.d.ts.map +0 -1
- package/dist/lib/claudemd.js.map +0 -1
- package/dist/lib/codex-export.d.ts.map +0 -1
- package/dist/lib/codex-export.js.map +0 -1
- package/dist/lib/common.d.ts.map +0 -1
- package/dist/lib/common.js.map +0 -1
- package/dist/lib/context.d.ts.map +0 -1
- package/dist/lib/context.js.map +0 -1
- package/dist/lib/docker.d.ts.map +0 -1
- package/dist/lib/docker.js.map +0 -1
- package/dist/lib/frontmatter.d.ts.map +0 -1
- package/dist/lib/frontmatter.js.map +0 -1
- package/dist/lib/plugin.d.ts.map +0 -1
- package/dist/lib/plugin.js.map +0 -1
- package/dist/lib/template.d.ts.map +0 -1
- package/dist/lib/template.js.map +0 -1
- package/dist/types/index.d.ts.map +0 -1
- package/dist/types/index.js.map +0 -1
- package/dist/ui/AnalyzeUI.d.ts.map +0 -1
- package/dist/ui/AnalyzeUI.js.map +0 -1
- package/dist/ui/App.d.ts.map +0 -1
- package/dist/ui/App.js.map +0 -1
- package/dist/ui/CI.d.ts.map +0 -1
- package/dist/ui/CI.js.map +0 -1
- package/dist/ui/CIContext.d.ts.map +0 -1
- package/dist/ui/CIContext.js.map +0 -1
- package/dist/ui/CISelector.d.ts.map +0 -1
- package/dist/ui/CISelector.js.map +0 -1
- package/dist/ui/Doctor.d.ts.map +0 -1
- package/dist/ui/Doctor.js.map +0 -1
- package/dist/ui/Header.d.ts.map +0 -1
- package/dist/ui/Header.js.map +0 -1
- package/dist/ui/LlmsTxt.d.ts.map +0 -1
- package/dist/ui/LlmsTxt.js.map +0 -1
- package/dist/ui/MemorySelector.d.ts.map +0 -1
- package/dist/ui/MemorySelector.js.map +0 -1
- package/dist/ui/NameInput.d.ts.map +0 -1
- package/dist/ui/NameInput.js.map +0 -1
- package/dist/ui/OptionSelector.d.ts.map +0 -1
- package/dist/ui/OptionSelector.js.map +0 -1
- package/dist/ui/Plugin.d.ts.map +0 -1
- package/dist/ui/Plugin.js.map +0 -1
- package/dist/ui/Progress.d.ts.map +0 -1
- package/dist/ui/Progress.js.map +0 -1
- package/dist/ui/Skills.d.ts.map +0 -1
- package/dist/ui/Skills.js.map +0 -1
- package/dist/ui/StackSelector.d.ts.map +0 -1
- package/dist/ui/StackSelector.js.map +0 -1
- package/dist/ui/Summary.d.ts.map +0 -1
- package/dist/ui/Summary.js.map +0 -1
- package/dist/ui/Welcome.d.ts.map +0 -1
- package/dist/ui/Welcome.js.map +0 -1
- package/dist/ui/theme.d.ts.map +0 -1
- package/dist/ui/theme.js.map +0 -1
package/dist/commands/skills.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import fs from
|
|
2
|
-
import path from
|
|
3
|
-
import { parseFrontmatter } from
|
|
1
|
+
import fs from "fs-extra";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { parseFrontmatter } from "../lib/frontmatter.js";
|
|
4
4
|
// ── Constants ────────────────────────────────────────────────────────────────
|
|
5
|
-
const DEFAULT_SKILLS_DIR = path.join(process.env[
|
|
5
|
+
const DEFAULT_SKILLS_DIR = path.join(process.env["HOME"] ?? "~", ".claude", "skills");
|
|
6
6
|
const DEFAULT_BUDGET = 8000;
|
|
7
7
|
/** Approximate tokens per character (GPT/Claude rough average) */
|
|
8
8
|
const CHARS_PER_TOKEN = 4;
|
|
@@ -22,6 +22,172 @@ const CONTRADICTION_PAIRS = [
|
|
|
22
22
|
[/\bavoid\b/i, /\bprefer\b/i],
|
|
23
23
|
[/\bdo not\b/i, /\bmust\b/i],
|
|
24
24
|
];
|
|
25
|
+
/**
|
|
26
|
+
* Positive and negative signal patterns.
|
|
27
|
+
* Order matters — first match wins, so more specific patterns go first.
|
|
28
|
+
*/
|
|
29
|
+
const POSITIVE_SIGNALS = [
|
|
30
|
+
/\balways use\b/i,
|
|
31
|
+
/\bmust use\b/i,
|
|
32
|
+
/\bprefer\b/i,
|
|
33
|
+
/\balways\b/i,
|
|
34
|
+
/\bmust\b/i,
|
|
35
|
+
/\brequire\b/i,
|
|
36
|
+
/\buse\b/i,
|
|
37
|
+
/\benable\b/i,
|
|
38
|
+
/\bshould\b/i,
|
|
39
|
+
];
|
|
40
|
+
const NEGATIVE_SIGNALS = [
|
|
41
|
+
/\bnever use\b/i,
|
|
42
|
+
/\bdo not use\b/i,
|
|
43
|
+
/\bdon't use\b/i,
|
|
44
|
+
/\bnever\b/i,
|
|
45
|
+
/\bavoid\b/i,
|
|
46
|
+
/\bdo not\b/i,
|
|
47
|
+
/\bdon't\b/i,
|
|
48
|
+
/\bdisable\b/i,
|
|
49
|
+
/\bno\b/i,
|
|
50
|
+
/\bforbid\b/i,
|
|
51
|
+
];
|
|
52
|
+
/**
|
|
53
|
+
* Extract a directive (sentiment + subject) from a rule string.
|
|
54
|
+
* Returns null if the rule has no clear directive.
|
|
55
|
+
*/
|
|
56
|
+
export function extractDirective(rule) {
|
|
57
|
+
const norm = rule.toLowerCase().trim();
|
|
58
|
+
// Try negative first (more specific: "never use X" before "use X")
|
|
59
|
+
for (const pattern of NEGATIVE_SIGNALS) {
|
|
60
|
+
const match = norm.match(pattern);
|
|
61
|
+
if (match) {
|
|
62
|
+
const subject = norm
|
|
63
|
+
.slice(match.index + match[0].length)
|
|
64
|
+
.trim()
|
|
65
|
+
.replace(/^(the|a|an)\s+/i, "")
|
|
66
|
+
.replace(/[.;,!]+$/, "")
|
|
67
|
+
.trim();
|
|
68
|
+
if (subject.length >= 3) {
|
|
69
|
+
return { sentiment: "negative", subject };
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
for (const pattern of POSITIVE_SIGNALS) {
|
|
74
|
+
const match = norm.match(pattern);
|
|
75
|
+
if (match) {
|
|
76
|
+
const subject = norm
|
|
77
|
+
.slice(match.index + match[0].length)
|
|
78
|
+
.trim()
|
|
79
|
+
.replace(/^(the|a|an)\s+/i, "")
|
|
80
|
+
.replace(/[.;,!]+$/, "")
|
|
81
|
+
.trim();
|
|
82
|
+
if (subject.length >= 3) {
|
|
83
|
+
return { sentiment: "positive", subject };
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Check if two subjects are similar enough to be "about the same thing".
|
|
91
|
+
* Uses simple word-overlap (Jaccard-like) — no external NLP needed.
|
|
92
|
+
*/
|
|
93
|
+
export function subjectsSimilar(a, b, threshold = 0.5) {
|
|
94
|
+
const wordsA = new Set(a.split(/\s+/).filter((w) => w.length > 2));
|
|
95
|
+
const wordsB = new Set(b.split(/\s+/).filter((w) => w.length > 2));
|
|
96
|
+
if (wordsA.size === 0 || wordsB.size === 0)
|
|
97
|
+
return false;
|
|
98
|
+
let intersection = 0;
|
|
99
|
+
for (const w of wordsA) {
|
|
100
|
+
if (wordsB.has(w))
|
|
101
|
+
intersection++;
|
|
102
|
+
}
|
|
103
|
+
const union = new Set([...wordsA, ...wordsB]).size;
|
|
104
|
+
return union > 0 && intersection / union >= threshold;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Detect a directive clash between two rules:
|
|
108
|
+
* opposite sentiments about the same subject.
|
|
109
|
+
*/
|
|
110
|
+
export function detectDirectiveClash(ruleA, ruleB) {
|
|
111
|
+
const dA = extractDirective(ruleA);
|
|
112
|
+
const dB = extractDirective(ruleB);
|
|
113
|
+
if (!dA || !dB)
|
|
114
|
+
return null;
|
|
115
|
+
if (dA.sentiment === dB.sentiment)
|
|
116
|
+
return null;
|
|
117
|
+
if (subjectsSimilar(dA.subject, dB.subject)) {
|
|
118
|
+
const posRule = dA.sentiment === "positive" ? ruleA : ruleB;
|
|
119
|
+
const negRule = dA.sentiment === "negative" ? ruleA : ruleB;
|
|
120
|
+
return `Directive clash on "${dA.subject}": positive="${posRule.slice(0, 50)}" vs negative="${negRule.slice(0, 50)}"`;
|
|
121
|
+
}
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
// ── Budget Optimization ─────────────────────────────────────────────────────
|
|
125
|
+
/**
|
|
126
|
+
* Generate minimal disable sets to bring total tokens under budget.
|
|
127
|
+
* Uses a greedy approach: disable largest skills first until under budget.
|
|
128
|
+
* Returns up to 3 alternative optimization suggestions.
|
|
129
|
+
*/
|
|
130
|
+
export function generateBudgetOptimizations(entries, totalTokens, budget) {
|
|
131
|
+
if (totalTokens <= budget)
|
|
132
|
+
return [];
|
|
133
|
+
const excess = totalTokens - budget;
|
|
134
|
+
const suggestions = [];
|
|
135
|
+
// Strategy 1: Greedy — disable largest skills first
|
|
136
|
+
const greedy = greedyDisableSet(entries, excess);
|
|
137
|
+
suggestions.push(makeSuggestion(greedy, entries, totalTokens, budget));
|
|
138
|
+
// Strategy 2: Minimal count — find smallest number of skills to disable
|
|
139
|
+
// (try single-skill solutions first, then pairs)
|
|
140
|
+
const singles = entries.filter((e) => e.tokens >= excess);
|
|
141
|
+
if (singles.length > 0) {
|
|
142
|
+
// Pick the smallest single that still meets budget
|
|
143
|
+
const sorted = [...singles].sort((a, b) => a.tokens - b.tokens);
|
|
144
|
+
const minimal = sorted[0];
|
|
145
|
+
if (minimal.skillName !== greedy[0]?.skillName) {
|
|
146
|
+
suggestions.push(makeSuggestion([minimal], entries, totalTokens, budget));
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
// Strategy 3: If we have many small skills, show a "trim many" approach
|
|
150
|
+
// Disable the bottom 50% by token count (many small skills)
|
|
151
|
+
if (entries.length >= 4) {
|
|
152
|
+
const sortedAsc = [...entries].sort((a, b) => a.tokens - b.tokens);
|
|
153
|
+
const halfCount = Math.ceil(sortedAsc.length / 2);
|
|
154
|
+
const bottomHalf = sortedAsc.slice(0, halfCount);
|
|
155
|
+
const saved = bottomHalf.reduce((s, e) => s + e.tokens, 0);
|
|
156
|
+
if (saved >= excess) {
|
|
157
|
+
const trimSet = greedyDisableSet([...bottomHalf].sort((a, b) => b.tokens - a.tokens), excess);
|
|
158
|
+
const names = new Set(trimSet.map((e) => e.skillName));
|
|
159
|
+
const alreadySuggested = suggestions.some((s) => s.disableSkills.length === names.size &&
|
|
160
|
+
s.disableSkills.every((n) => names.has(n)));
|
|
161
|
+
if (!alreadySuggested) {
|
|
162
|
+
suggestions.push(makeSuggestion(trimSet, entries, totalTokens, budget));
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return suggestions;
|
|
167
|
+
}
|
|
168
|
+
/** Greedy: pick largest entries until saved >= excess */
|
|
169
|
+
function greedyDisableSet(entries, excess) {
|
|
170
|
+
const sorted = [...entries].sort((a, b) => b.tokens - a.tokens);
|
|
171
|
+
const result = [];
|
|
172
|
+
let saved = 0;
|
|
173
|
+
for (const entry of sorted) {
|
|
174
|
+
if (saved >= excess)
|
|
175
|
+
break;
|
|
176
|
+
result.push(entry);
|
|
177
|
+
saved += entry.tokens;
|
|
178
|
+
}
|
|
179
|
+
return result;
|
|
180
|
+
}
|
|
181
|
+
function makeSuggestion(disableSet, _allEntries, totalTokens, budget) {
|
|
182
|
+
const tokensSaved = disableSet.reduce((s, e) => s + e.tokens, 0);
|
|
183
|
+
const remaining = totalTokens - tokensSaved;
|
|
184
|
+
return {
|
|
185
|
+
disableSkills: disableSet.map((e) => e.skillName),
|
|
186
|
+
tokensSaved,
|
|
187
|
+
remainingTokens: remaining,
|
|
188
|
+
meetsbudget: remaining <= budget,
|
|
189
|
+
};
|
|
190
|
+
}
|
|
25
191
|
// ── Helpers ──────────────────────────────────────────────────────────────────
|
|
26
192
|
/** Estimate token count from a string */
|
|
27
193
|
export function estimateTokens(text) {
|
|
@@ -29,15 +195,15 @@ export function estimateTokens(text) {
|
|
|
29
195
|
}
|
|
30
196
|
/** Read a SKILL.md and extract its name + critical rules section */
|
|
31
197
|
export async function parseSkillFile(skillPath) {
|
|
32
|
-
if (!await fs.pathExists(skillPath))
|
|
198
|
+
if (!(await fs.pathExists(skillPath)))
|
|
33
199
|
return null;
|
|
34
|
-
const raw = await fs.readFile(skillPath,
|
|
200
|
+
const raw = await fs.readFile(skillPath, "utf-8");
|
|
35
201
|
const fm = parseFrontmatter(raw);
|
|
36
|
-
const name = fm?.data?.[
|
|
202
|
+
const name = fm?.data?.["name"] ?? path.basename(path.dirname(skillPath));
|
|
37
203
|
// Extract critical rules — look for "Critical Rules" or numbered list after it
|
|
38
204
|
const rules = extractCriticalRules(fm?.content ?? raw);
|
|
39
205
|
// Extract trigger keywords from description
|
|
40
|
-
const description = fm?.data?.[
|
|
206
|
+
const description = fm?.data?.["description"] ?? "";
|
|
41
207
|
const triggers = extractTriggers(description);
|
|
42
208
|
return { name, rules, rawContent: raw, triggers };
|
|
43
209
|
}
|
|
@@ -60,7 +226,7 @@ export function extractCriticalRules(content) {
|
|
|
60
226
|
}
|
|
61
227
|
/** Extract a markdown section body by heading pattern */
|
|
62
228
|
function extractSection(content, headingPattern) {
|
|
63
|
-
const lines = content.split(
|
|
229
|
+
const lines = content.split("\n");
|
|
64
230
|
let capturing = false;
|
|
65
231
|
const blockLines = [];
|
|
66
232
|
for (const line of lines) {
|
|
@@ -74,13 +240,13 @@ function extractSection(content, headingPattern) {
|
|
|
74
240
|
capturing = true;
|
|
75
241
|
}
|
|
76
242
|
}
|
|
77
|
-
return blockLines.length > 0 ? blockLines.join(
|
|
243
|
+
return blockLines.length > 0 ? blockLines.join("\n") : null;
|
|
78
244
|
}
|
|
79
245
|
/** Extract numbered or bulleted list items from a markdown block */
|
|
80
246
|
function extractListItems(block, out) {
|
|
81
|
-
const lines = block.split(
|
|
247
|
+
const lines = block.split("\n");
|
|
82
248
|
for (const line of lines) {
|
|
83
|
-
const match = line.match(/^\s*(?:\d+[
|
|
249
|
+
const match = line.match(/^\s*(?:\d+[.)]\s+|-\s+|\*\s+)(.+)/);
|
|
84
250
|
if (match) {
|
|
85
251
|
const cleaned = match[1].trim();
|
|
86
252
|
if (cleaned.length > 5)
|
|
@@ -97,21 +263,24 @@ export function extractTriggers(description) {
|
|
|
97
263
|
// Split on commas, "or", "and", common delimiters
|
|
98
264
|
const keywords = triggerText
|
|
99
265
|
.split(/[,;]|\bor\b/i)
|
|
100
|
-
.map(k => k
|
|
101
|
-
.
|
|
266
|
+
.map((k) => k
|
|
267
|
+
.trim()
|
|
268
|
+
.toLowerCase()
|
|
269
|
+
.replace(/^when\s+/i, ""))
|
|
270
|
+
.filter((k) => k.length > 2);
|
|
102
271
|
return keywords;
|
|
103
272
|
}
|
|
104
273
|
// ── Core: Scan installed skills ─────────────────────────────────────────────
|
|
105
274
|
/** Discover all SKILL.md files in a skills directory */
|
|
106
275
|
export async function discoverSkills(skillsDir) {
|
|
107
|
-
if (!await fs.pathExists(skillsDir))
|
|
276
|
+
if (!(await fs.pathExists(skillsDir)))
|
|
108
277
|
return [];
|
|
109
278
|
const entries = await fs.readdir(skillsDir);
|
|
110
279
|
const skillFiles = [];
|
|
111
280
|
for (const entry of entries) {
|
|
112
|
-
if (entry.startsWith(
|
|
281
|
+
if (entry.startsWith(".") || entry.startsWith("_"))
|
|
113
282
|
continue;
|
|
114
|
-
const skillPath = path.join(skillsDir, entry,
|
|
283
|
+
const skillPath = path.join(skillsDir, entry, "SKILL.md");
|
|
115
284
|
if (await fs.pathExists(skillPath)) {
|
|
116
285
|
skillFiles.push(skillPath);
|
|
117
286
|
}
|
|
@@ -119,16 +288,25 @@ export async function discoverSkills(skillsDir) {
|
|
|
119
288
|
return skillFiles.sort();
|
|
120
289
|
}
|
|
121
290
|
// ── Conflict Detection ──────────────────────────────────────────────────────
|
|
122
|
-
/** Check if two rules contradict each other */
|
|
291
|
+
/** Check if two rules contradict each other (regex pairs + directive clash) */
|
|
123
292
|
export function detectRuleConflict(ruleA, ruleB) {
|
|
124
293
|
const normA = ruleA.toLowerCase().trim();
|
|
125
294
|
const normB = ruleB.toLowerCase().trim();
|
|
295
|
+
// Strategy 1: Hardcoded regex pairs (fast, high confidence)
|
|
126
296
|
for (const [patternA, patternB] of CONTRADICTION_PAIRS) {
|
|
127
297
|
if ((patternA.test(normA) && patternB.test(normB)) ||
|
|
128
298
|
(patternB.test(normA) && patternA.test(normB))) {
|
|
129
|
-
return
|
|
299
|
+
return {
|
|
300
|
+
reason: `"${ruleA.slice(0, 60)}" vs "${ruleB.slice(0, 60)}"`,
|
|
301
|
+
kind: "regex-pair",
|
|
302
|
+
};
|
|
130
303
|
}
|
|
131
304
|
}
|
|
305
|
+
// Strategy 2: Semantic directive clash (broader, medium confidence)
|
|
306
|
+
const clashReason = detectDirectiveClash(ruleA, ruleB);
|
|
307
|
+
if (clashReason) {
|
|
308
|
+
return { reason: clashReason, kind: "directive-clash" };
|
|
309
|
+
}
|
|
132
310
|
return null;
|
|
133
311
|
}
|
|
134
312
|
/** Scan all skills for conflicting critical rules */
|
|
@@ -157,9 +335,14 @@ export async function findConflicts(skillsDir) {
|
|
|
157
335
|
// Skip rules from the same skill
|
|
158
336
|
if (a.skillName === b.skillName)
|
|
159
337
|
continue;
|
|
160
|
-
const
|
|
161
|
-
if (
|
|
162
|
-
conflicts.push({
|
|
338
|
+
const result = detectRuleConflict(a.rule, b.rule);
|
|
339
|
+
if (result) {
|
|
340
|
+
conflicts.push({
|
|
341
|
+
ruleA: a,
|
|
342
|
+
ruleB: b,
|
|
343
|
+
reason: result.reason,
|
|
344
|
+
kind: result.kind,
|
|
345
|
+
});
|
|
163
346
|
}
|
|
164
347
|
}
|
|
165
348
|
}
|
|
@@ -197,7 +380,15 @@ export async function calculateBudget(skillsDir, budget = DEFAULT_BUDGET) {
|
|
|
197
380
|
saved += entry.tokens;
|
|
198
381
|
}
|
|
199
382
|
}
|
|
200
|
-
|
|
383
|
+
const optimizations = generateBudgetOptimizations(entries, totalTokens, budget);
|
|
384
|
+
return {
|
|
385
|
+
entries,
|
|
386
|
+
totalTokens,
|
|
387
|
+
budget,
|
|
388
|
+
overBudget,
|
|
389
|
+
suggestions,
|
|
390
|
+
optimizations,
|
|
391
|
+
};
|
|
201
392
|
}
|
|
202
393
|
// ── Duplicate Detection ─────────────────────────────────────────────────────
|
|
203
394
|
/** Find skills that overlap in scope/triggers */
|
|
@@ -215,7 +406,7 @@ export async function findDuplicates(skillsDir) {
|
|
|
215
406
|
for (let j = i + 1; j < skillData.length; j++) {
|
|
216
407
|
const a = skillData[i];
|
|
217
408
|
const b = skillData[j];
|
|
218
|
-
const sharedTriggers = a.triggers.filter(t => b.triggers.some(bt => bt.includes(t) || t.includes(bt)));
|
|
409
|
+
const sharedTriggers = a.triggers.filter((t) => b.triggers.some((bt) => bt.includes(t) || t.includes(bt)));
|
|
219
410
|
if (sharedTriggers.length === 0)
|
|
220
411
|
continue;
|
|
221
412
|
const maxTriggers = Math.max(a.triggers.length, b.triggers.length);
|
|
@@ -244,7 +435,7 @@ export async function findDuplicates(skillsDir) {
|
|
|
244
435
|
export async function runSkillsDoctor(options) {
|
|
245
436
|
const skillsDir = options.skillsDir ?? DEFAULT_SKILLS_DIR;
|
|
246
437
|
const budget = options.budget ?? DEFAULT_BUDGET;
|
|
247
|
-
if (options.mode ===
|
|
438
|
+
if (options.mode === "budget") {
|
|
248
439
|
const budgetResult = await calculateBudget(skillsDir, budget);
|
|
249
440
|
return { conflicts: [], budget: budgetResult, duplicates: [] };
|
|
250
441
|
}
|
|
@@ -260,16 +451,89 @@ export async function runSkillsDoctor(options) {
|
|
|
260
451
|
const DEFAULT_THRESHOLD = 50;
|
|
261
452
|
/** Vague terms that reduce clarity score */
|
|
262
453
|
const VAGUE_TERMS = [
|
|
263
|
-
/\bstuff\b/i,
|
|
264
|
-
/\
|
|
454
|
+
/\bstuff\b/i,
|
|
455
|
+
/\bthings?\b/i,
|
|
456
|
+
/\betc\.?\b/i,
|
|
457
|
+
/\bmisc\b/i,
|
|
458
|
+
/\bvarious\b/i,
|
|
459
|
+
/\bsome\b/i,
|
|
460
|
+
/\bmaybe\b/i,
|
|
461
|
+
/\bprobably\b/i,
|
|
265
462
|
];
|
|
266
463
|
/** Action verbs that indicate actionable rules */
|
|
267
464
|
const ACTION_VERBS = [
|
|
268
|
-
/\buse\b/i,
|
|
269
|
-
/\
|
|
270
|
-
/\
|
|
271
|
-
/\
|
|
465
|
+
/\buse\b/i,
|
|
466
|
+
/\bavoid\b/i,
|
|
467
|
+
/\bprefer\b/i,
|
|
468
|
+
/\bnever\b/i,
|
|
469
|
+
/\balways\b/i,
|
|
470
|
+
/\bmust\b/i,
|
|
471
|
+
/\bshould\b/i,
|
|
472
|
+
/\bshall\b/i,
|
|
473
|
+
/\bensure\b/i,
|
|
474
|
+
/\bwrite\b/i,
|
|
475
|
+
/\bcreate\b/i,
|
|
476
|
+
/\bfollow\b/i,
|
|
477
|
+
/\bdo not\b/i,
|
|
478
|
+
/\bapply\b/i,
|
|
479
|
+
/\bimplement\b/i,
|
|
480
|
+
/\brun\b/i,
|
|
481
|
+
];
|
|
482
|
+
/** Dangerous patterns in skill content that indicate safety risks */
|
|
483
|
+
const DANGEROUS_PATTERNS = [
|
|
484
|
+
{ pattern: /\beval\s*\(/i, label: "eval() usage", weight: 20 },
|
|
485
|
+
{ pattern: /\bexec\s*\(/i, label: "exec() usage", weight: 15 },
|
|
486
|
+
{
|
|
487
|
+
pattern: /\bchild_process\b/i,
|
|
488
|
+
label: "child_process reference",
|
|
489
|
+
weight: 10,
|
|
490
|
+
},
|
|
491
|
+
{ pattern: /\brm\s+-rf\b/i, label: "rm -rf command", weight: 20 },
|
|
492
|
+
{
|
|
493
|
+
pattern: /\bcurl\b.*\|\s*(?:sh|bash)\b/i,
|
|
494
|
+
label: "curl piped to shell",
|
|
495
|
+
weight: 25,
|
|
496
|
+
},
|
|
497
|
+
{ pattern: /\bsudo\b/i, label: "sudo usage", weight: 15 },
|
|
498
|
+
{ pattern: /\bchmod\s+777\b/i, label: "chmod 777", weight: 15 },
|
|
499
|
+
{
|
|
500
|
+
pattern: /\b(?:password|secret|token|api_key)\s*[:=]\s*['"][^'"]+['"]/i,
|
|
501
|
+
label: "hardcoded secret",
|
|
502
|
+
weight: 25,
|
|
503
|
+
},
|
|
504
|
+
{
|
|
505
|
+
pattern: /\b__proto__\b|\bconstructor\s*\[/i,
|
|
506
|
+
label: "prototype pollution",
|
|
507
|
+
weight: 20,
|
|
508
|
+
},
|
|
509
|
+
{
|
|
510
|
+
pattern: /\binnerHTML\s*=/i,
|
|
511
|
+
label: "innerHTML assignment (XSS risk)",
|
|
512
|
+
weight: 10,
|
|
513
|
+
},
|
|
514
|
+
{
|
|
515
|
+
pattern: /\bdangerouslySetInnerHTML\b/i,
|
|
516
|
+
label: "dangerouslySetInnerHTML",
|
|
517
|
+
weight: 10,
|
|
518
|
+
},
|
|
519
|
+
{
|
|
520
|
+
pattern: /\bno[- ]?verify\b.*\bgit\b|\bgit\b.*\bno[- ]?verify\b/i,
|
|
521
|
+
label: "git --no-verify bypass",
|
|
522
|
+
weight: 10,
|
|
523
|
+
},
|
|
524
|
+
{
|
|
525
|
+
pattern: /\bforce[- ]?push\b|\bpush\s+--force\b/i,
|
|
526
|
+
label: "force push instruction",
|
|
527
|
+
weight: 10,
|
|
528
|
+
},
|
|
529
|
+
{
|
|
530
|
+
pattern: /\bdisable.*(?:eslint|typescript|security)\b/i,
|
|
531
|
+
label: "linter/security disable",
|
|
532
|
+
weight: 10,
|
|
533
|
+
},
|
|
272
534
|
];
|
|
535
|
+
/** Default registry quality threshold */
|
|
536
|
+
const DEFAULT_REGISTRY_THRESHOLD = 60;
|
|
273
537
|
/**
|
|
274
538
|
* Score completeness (0-100): frontmatter fields, critical rules, structure.
|
|
275
539
|
*/
|
|
@@ -313,7 +577,7 @@ export function scoreClarity(parsed) {
|
|
|
313
577
|
score += 20;
|
|
314
578
|
// Rules contain action verbs (up to 40 pts)
|
|
315
579
|
if (parsed.rules.length > 0) {
|
|
316
|
-
const actionableCount = parsed.rules.filter(rule => ACTION_VERBS.some(verb => verb.test(rule))).length;
|
|
580
|
+
const actionableCount = parsed.rules.filter((rule) => ACTION_VERBS.some((verb) => verb.test(rule))).length;
|
|
317
581
|
const ratio = actionableCount / parsed.rules.length;
|
|
318
582
|
score += Math.round(ratio * 40);
|
|
319
583
|
}
|
|
@@ -355,7 +619,10 @@ export function scoreTestability(parsed) {
|
|
|
355
619
|
else if (gwtCount >= 1)
|
|
356
620
|
score += 25;
|
|
357
621
|
// Rules are specific enough (contain file paths, code refs, or patterns)
|
|
358
|
-
const specificRules = parsed.rules.filter(rule => /[`'"]/.test(rule) ||
|
|
622
|
+
const specificRules = parsed.rules.filter((rule) => /[`'"]/.test(rule) ||
|
|
623
|
+
/\.\w+/.test(rule) ||
|
|
624
|
+
/\bfile\b/i.test(rule) ||
|
|
625
|
+
/\bpath\b/i.test(rule)).length;
|
|
359
626
|
if (parsed.rules.length > 0) {
|
|
360
627
|
const specificity = specificRules / parsed.rules.length;
|
|
361
628
|
score += Math.round(specificity * 30);
|
|
@@ -407,7 +674,81 @@ export function scoreTokenEfficiency(parsed) {
|
|
|
407
674
|
return Math.max(0, Math.min(score, 100));
|
|
408
675
|
}
|
|
409
676
|
/**
|
|
410
|
-
* Score
|
|
677
|
+
* Score safety (0-100): absence of dangerous patterns, injection risks, credential leaks.
|
|
678
|
+
* Starts at 100 and deducts for each dangerous pattern found.
|
|
679
|
+
*/
|
|
680
|
+
export function scoreSafety(parsed) {
|
|
681
|
+
let score = 100;
|
|
682
|
+
for (const { pattern, weight } of DANGEROUS_PATTERNS) {
|
|
683
|
+
if (pattern.test(parsed.rawContent)) {
|
|
684
|
+
score -= weight;
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
// Bonus: skill explicitly mentions security best practices (+10, capped at 100)
|
|
688
|
+
if (/\bsanitiz/i.test(parsed.rawContent) ||
|
|
689
|
+
/\bescap/i.test(parsed.rawContent)) {
|
|
690
|
+
score += 10;
|
|
691
|
+
}
|
|
692
|
+
if (/\bvalidat/i.test(parsed.rawContent)) {
|
|
693
|
+
score += 5;
|
|
694
|
+
}
|
|
695
|
+
return Math.max(0, Math.min(score, 100));
|
|
696
|
+
}
|
|
697
|
+
/**
|
|
698
|
+
* Score agent readiness (0-100): how well-prepared a skill is for AI agent consumption.
|
|
699
|
+
* Checks for triggers, tool restrictions, examples, structured output, and error handling.
|
|
700
|
+
*/
|
|
701
|
+
export function scoreAgentReadiness(parsed) {
|
|
702
|
+
let score = 0;
|
|
703
|
+
// Has triggers for auto-activation (25 pts)
|
|
704
|
+
if (parsed.triggers.length >= 3)
|
|
705
|
+
score += 25;
|
|
706
|
+
else if (parsed.triggers.length >= 1)
|
|
707
|
+
score += 15;
|
|
708
|
+
// Has tool restrictions or permissions (e.g., "only use", "do not use", "allowed tools") (20 pts)
|
|
709
|
+
if (/\b(?:only use|allowed tools?|restricted to|do not use|forbidden|prohibited)\b/i.test(parsed.rawContent)) {
|
|
710
|
+
score += 20;
|
|
711
|
+
}
|
|
712
|
+
// Has examples with expected input/output or code blocks (20 pts)
|
|
713
|
+
const codeBlocks = (parsed.rawContent.match(/```/g) ?? []).length / 2;
|
|
714
|
+
if (codeBlocks >= 3)
|
|
715
|
+
score += 20;
|
|
716
|
+
else if (codeBlocks >= 1)
|
|
717
|
+
score += 10;
|
|
718
|
+
// Has structured output format (JSON, YAML, or explicit format section) (15 pts)
|
|
719
|
+
if (/\boutput format\b/i.test(parsed.rawContent) ||
|
|
720
|
+
/\breturn.*(?:json|yaml|structured)\b/i.test(parsed.rawContent)) {
|
|
721
|
+
score += 15;
|
|
722
|
+
}
|
|
723
|
+
else if (/```(?:json|yaml)/i.test(parsed.rawContent)) {
|
|
724
|
+
score += 10;
|
|
725
|
+
}
|
|
726
|
+
// Has error handling guidance ("if error", "when fails", "fallback") (10 pts)
|
|
727
|
+
if (/\b(?:if.*(?:error|fail)|fallback|edge case|error handling)\b/i.test(parsed.rawContent)) {
|
|
728
|
+
score += 10;
|
|
729
|
+
}
|
|
730
|
+
// Has a clear "when NOT to use" or scope boundary (10 pts)
|
|
731
|
+
if (/\b(?:do not trigger|not applicable|out of scope|when not to)\b/i.test(parsed.rawContent)) {
|
|
732
|
+
score += 10;
|
|
733
|
+
}
|
|
734
|
+
return Math.min(score, 100);
|
|
735
|
+
}
|
|
736
|
+
/**
|
|
737
|
+
* Convert numeric score to letter grade.
|
|
738
|
+
*/
|
|
739
|
+
export function computeGrade(overall) {
|
|
740
|
+
if (overall >= 90)
|
|
741
|
+
return "A";
|
|
742
|
+
if (overall >= 80)
|
|
743
|
+
return "B";
|
|
744
|
+
if (overall >= 70)
|
|
745
|
+
return "C";
|
|
746
|
+
if (overall >= 60)
|
|
747
|
+
return "D";
|
|
748
|
+
return "F";
|
|
749
|
+
}
|
|
750
|
+
/**
|
|
751
|
+
* Score a skill on all 6 dimensions and compute overall with letter grade.
|
|
411
752
|
*/
|
|
412
753
|
export async function scoreSkill(skillPath, threshold = DEFAULT_THRESHOLD) {
|
|
413
754
|
const parsed = await parseSkillFile(skillPath);
|
|
@@ -417,22 +758,62 @@ export async function scoreSkill(skillPath, threshold = DEFAULT_THRESHOLD) {
|
|
|
417
758
|
const clarity = scoreClarity(parsed);
|
|
418
759
|
const testability = scoreTestability(parsed);
|
|
419
760
|
const tokenEfficiency = scoreTokenEfficiency(parsed);
|
|
420
|
-
|
|
421
|
-
const
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
761
|
+
const safety = scoreSafety(parsed);
|
|
762
|
+
const agentReadiness = scoreAgentReadiness(parsed);
|
|
763
|
+
// Weighted average: completeness 20%, clarity 20%, testability 15%,
|
|
764
|
+
// token-efficiency 15%, safety 15%, agent-readiness 15%
|
|
765
|
+
const overall = Math.round(completeness * 0.2 +
|
|
766
|
+
clarity * 0.2 +
|
|
767
|
+
testability * 0.15 +
|
|
768
|
+
tokenEfficiency * 0.15 +
|
|
769
|
+
safety * 0.15 +
|
|
770
|
+
agentReadiness * 0.15);
|
|
771
|
+
const grade = computeGrade(overall);
|
|
425
772
|
return {
|
|
426
773
|
skillName: parsed.name,
|
|
427
774
|
completeness,
|
|
428
775
|
clarity,
|
|
429
776
|
testability,
|
|
430
777
|
tokenEfficiency,
|
|
778
|
+
safety,
|
|
779
|
+
agentReadiness,
|
|
431
780
|
overall,
|
|
781
|
+
grade,
|
|
432
782
|
threshold,
|
|
433
783
|
passing: overall >= threshold,
|
|
434
784
|
};
|
|
435
785
|
}
|
|
786
|
+
/**
|
|
787
|
+
* Gate check for registry inclusion. Rejects skills below the configured threshold.
|
|
788
|
+
*/
|
|
789
|
+
export async function registryGate(skillPath, threshold = DEFAULT_REGISTRY_THRESHOLD) {
|
|
790
|
+
const score = await scoreSkill(skillPath, threshold);
|
|
791
|
+
if (!score)
|
|
792
|
+
return null;
|
|
793
|
+
const accepted = score.passing;
|
|
794
|
+
let reason;
|
|
795
|
+
if (!accepted) {
|
|
796
|
+
const failures = [];
|
|
797
|
+
if (score.safety < 60)
|
|
798
|
+
failures.push(`safety=${score.safety}`);
|
|
799
|
+
if (score.completeness < 40)
|
|
800
|
+
failures.push(`completeness=${score.completeness}`);
|
|
801
|
+
if (score.clarity < 40)
|
|
802
|
+
failures.push(`clarity=${score.clarity}`);
|
|
803
|
+
if (score.agentReadiness < 30)
|
|
804
|
+
failures.push(`agent-readiness=${score.agentReadiness}`);
|
|
805
|
+
reason =
|
|
806
|
+
failures.length > 0
|
|
807
|
+
? `Rejected (${score.grade}, ${score.overall}/100): weak dimensions — ${failures.join(", ")}`
|
|
808
|
+
: `Rejected (${score.grade}, ${score.overall}/100): below threshold ${threshold}`;
|
|
809
|
+
}
|
|
810
|
+
return {
|
|
811
|
+
skillName: score.skillName,
|
|
812
|
+
score,
|
|
813
|
+
accepted,
|
|
814
|
+
reason,
|
|
815
|
+
};
|
|
816
|
+
}
|
|
436
817
|
// ── Benchmarking ───────────────────────────────────────────────────────────
|
|
437
818
|
/**
|
|
438
819
|
* Run structural quality benchmark checks against a skill.
|
|
@@ -444,13 +825,16 @@ export async function benchmarkSkill(skillPath) {
|
|
|
444
825
|
const checks = [];
|
|
445
826
|
// Check 1: Has YAML frontmatter with name
|
|
446
827
|
checks.push({
|
|
447
|
-
name:
|
|
448
|
-
passed: parsed.name.length > 0 &&
|
|
449
|
-
|
|
828
|
+
name: "has-frontmatter-name",
|
|
829
|
+
passed: parsed.name.length > 0 &&
|
|
830
|
+
parsed.name !== path.basename(path.dirname(skillPath)),
|
|
831
|
+
detail: parsed.name.length > 0
|
|
832
|
+
? `name: ${parsed.name}`
|
|
833
|
+
: "No explicit name in frontmatter",
|
|
450
834
|
});
|
|
451
835
|
// Check 2: Has trigger keywords
|
|
452
836
|
checks.push({
|
|
453
|
-
name:
|
|
837
|
+
name: "has-triggers",
|
|
454
838
|
passed: parsed.triggers.length > 0,
|
|
455
839
|
detail: parsed.triggers.length > 0
|
|
456
840
|
? `${parsed.triggers.length} trigger(s) found`
|
|
@@ -458,48 +842,49 @@ export async function benchmarkSkill(skillPath) {
|
|
|
458
842
|
});
|
|
459
843
|
// Check 3: Has critical rules (>= 3)
|
|
460
844
|
checks.push({
|
|
461
|
-
name:
|
|
845
|
+
name: "has-critical-rules",
|
|
462
846
|
passed: parsed.rules.length >= 3,
|
|
463
847
|
detail: `${parsed.rules.length} rule(s) found`,
|
|
464
848
|
});
|
|
465
849
|
// Check 4: Rules are actionable (contain verbs)
|
|
466
|
-
const actionableRules = parsed.rules.filter(rule => ACTION_VERBS.some(verb => verb.test(rule)));
|
|
850
|
+
const actionableRules = parsed.rules.filter((rule) => ACTION_VERBS.some((verb) => verb.test(rule)));
|
|
467
851
|
checks.push({
|
|
468
|
-
name:
|
|
469
|
-
passed: parsed.rules.length > 0 &&
|
|
852
|
+
name: "rules-actionable",
|
|
853
|
+
passed: parsed.rules.length > 0 &&
|
|
854
|
+
actionableRules.length / parsed.rules.length >= 0.5,
|
|
470
855
|
detail: `${actionableRules.length}/${parsed.rules.length} rules have action verbs`,
|
|
471
856
|
});
|
|
472
857
|
// Check 5: Has code examples
|
|
473
858
|
const codeBlocks = (parsed.rawContent.match(/```/g) ?? []).length / 2;
|
|
474
859
|
checks.push({
|
|
475
|
-
name:
|
|
860
|
+
name: "has-code-examples",
|
|
476
861
|
passed: codeBlocks >= 1,
|
|
477
862
|
detail: `${Math.floor(codeBlocks)} code block(s)`,
|
|
478
863
|
});
|
|
479
864
|
// Check 6: Has structured sections (headings)
|
|
480
865
|
const headings = (parsed.rawContent.match(/^#+\s/gm) ?? []).length;
|
|
481
866
|
checks.push({
|
|
482
|
-
name:
|
|
867
|
+
name: "has-sections",
|
|
483
868
|
passed: headings >= 3,
|
|
484
869
|
detail: `${headings} section heading(s)`,
|
|
485
870
|
});
|
|
486
871
|
// Check 7: Token budget reasonable (< 3000 tokens)
|
|
487
872
|
const tokens = estimateTokens(parsed.rawContent);
|
|
488
873
|
checks.push({
|
|
489
|
-
name:
|
|
874
|
+
name: "token-budget-ok",
|
|
490
875
|
passed: tokens <= 3000,
|
|
491
876
|
detail: `~${tokens} tokens`,
|
|
492
877
|
});
|
|
493
878
|
// Check 8: No vague terms in rules
|
|
494
|
-
const vagueRules = parsed.rules.filter(rule => VAGUE_TERMS.some(vague => vague.test(rule)));
|
|
879
|
+
const vagueRules = parsed.rules.filter((rule) => VAGUE_TERMS.some((vague) => vague.test(rule)));
|
|
495
880
|
checks.push({
|
|
496
|
-
name:
|
|
881
|
+
name: "no-vague-rules",
|
|
497
882
|
passed: vagueRules.length === 0,
|
|
498
883
|
detail: vagueRules.length > 0
|
|
499
884
|
? `${vagueRules.length} rule(s) contain vague terms`
|
|
500
|
-
:
|
|
885
|
+
: "All rules are specific",
|
|
501
886
|
});
|
|
502
|
-
const passedCount = checks.filter(c => c.passed).length;
|
|
887
|
+
const passedCount = checks.filter((c) => c.passed).length;
|
|
503
888
|
const passRate = Math.round((passedCount / checks.length) * 100);
|
|
504
889
|
return {
|
|
505
890
|
skillName: parsed.name,
|