@meyverick/agentic 5.0.2 → 5.1.0
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/AGENTS.md +1 -1
- package/CHANGELOG.md +15 -0
- package/README.md +2 -1
- package/package.json +5 -5
- package/scripts/{check-deps.mjs → check-deps.ts} +124 -59
- package/scripts/{git-dl.mjs → git-dl.ts} +11 -9
- package/skills/create-skill/SKILL.md +16 -16
- package/skills/create-skill/assets/templates/SKILL.md.template +1 -1
- package/skills/create-skill/evals/evals.json +4 -4
- package/skills/create-skill/evals/grading-template.json +1 -1
- package/skills/create-skill/references/component-decomposition.md +1 -1
- package/skills/create-skill/scripts/{audit-antipatterns.mjs → audit-antipatterns.ts} +75 -31
- package/skills/create-skill/scripts/{compute-benchmark.mjs → compute-benchmark.ts} +71 -30
- package/skills/create-skill/scripts/run-cold-eval.ts +202 -0
- package/skills/create-skill/scripts/{scaffold-skill.mjs → scaffold-skill.ts} +45 -25
- package/skills/create-skill/scripts/validate-routing.ts +218 -0
- package/skills/create-skill/scripts/{validate-structure.mjs → validate-structure.ts} +97 -36
- package/skills/okf-docs/SKILL.md +1 -1
- package/skills/okf-docs/evals/evals.json +2 -2
- package/skills/okf-docs/scripts/{validate-frontmatter.mjs → validate-frontmatter.ts} +73 -32
- package/skills/openspec-learn/references/evaluation-methodology.md +2 -2
- package/skills/openspec-pi-apply/SKILL.md +129 -0
- package/skills/openspec-pi-apply/references/rpc-protocol.md +43 -0
- package/skills/openspec-pi-apply/scripts/pi-rpc-apply.ts +421 -0
- package/skills/create-skill/scripts/run-cold-eval.mjs +0 -118
- package/skills/create-skill/scripts/validate-routing.mjs +0 -137
|
@@ -1,46 +1,64 @@
|
|
|
1
|
-
#!/usr/bin/env
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
2
|
/**
|
|
3
|
-
* scaffold-skill.
|
|
4
|
-
* Usage:
|
|
3
|
+
* scaffold-skill.ts — Create skill directory structure with SKILL.md skeleton
|
|
4
|
+
* Usage: bun scaffold-skill.ts <skill-name> [output-dir]
|
|
5
5
|
* Output: JSON with created path
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import { mkdirSync, writeFileSync
|
|
9
|
-
import { join } from 'path';
|
|
8
|
+
import { mkdirSync, writeFileSync } from 'node:fs';
|
|
9
|
+
import { join } from 'node:path';
|
|
10
|
+
|
|
11
|
+
interface ScaffoldResult {
|
|
12
|
+
status: string;
|
|
13
|
+
path: string;
|
|
14
|
+
files: string[];
|
|
15
|
+
}
|
|
10
16
|
|
|
11
17
|
const skillName = process.argv[2];
|
|
12
18
|
const outputDir = process.argv[3] || '.';
|
|
13
19
|
|
|
14
|
-
if (!skillName) {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
20
|
+
if (!skillName || skillName === '-h' || skillName === '--help') {
|
|
21
|
+
if (skillName === '-h' || skillName === '--help') {
|
|
22
|
+
console.log('Usage: bun scaffold-skill.ts <skill-name> [output-dir]');
|
|
23
|
+
process.exit(0);
|
|
24
|
+
}
|
|
25
|
+
console.error(
|
|
26
|
+
JSON.stringify({
|
|
27
|
+
error: 'Usage: bun scaffold-skill.ts <skill-name> [output-dir]'
|
|
28
|
+
})
|
|
29
|
+
);
|
|
18
30
|
process.exit(1);
|
|
19
31
|
}
|
|
20
32
|
|
|
21
33
|
// Validate skill name format
|
|
22
34
|
if (!/^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/.test(skillName)) {
|
|
23
|
-
console.error(
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
35
|
+
console.error(
|
|
36
|
+
JSON.stringify({
|
|
37
|
+
error: 'Invalid skill name. Must be lowercase letters, numbers, hyphens only. No leading/trailing hyphens.',
|
|
38
|
+
name: skillName
|
|
39
|
+
})
|
|
40
|
+
);
|
|
27
41
|
process.exit(1);
|
|
28
42
|
}
|
|
29
43
|
|
|
30
44
|
if (skillName.length > 64) {
|
|
31
|
-
console.error(
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
45
|
+
console.error(
|
|
46
|
+
JSON.stringify({
|
|
47
|
+
error: 'Skill name too long. Maximum 64 characters.',
|
|
48
|
+
name: skillName,
|
|
49
|
+
length: skillName.length
|
|
50
|
+
})
|
|
51
|
+
);
|
|
36
52
|
process.exit(1);
|
|
37
53
|
}
|
|
38
54
|
|
|
39
55
|
if (skillName.includes('--')) {
|
|
40
|
-
console.error(
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
56
|
+
console.error(
|
|
57
|
+
JSON.stringify({
|
|
58
|
+
error: 'Skill name contains consecutive hyphens.',
|
|
59
|
+
name: skillName
|
|
60
|
+
})
|
|
61
|
+
);
|
|
44
62
|
process.exit(1);
|
|
45
63
|
}
|
|
46
64
|
|
|
@@ -73,8 +91,7 @@ TODO: List environment-specific facts, common failures, non-obvious behaviors.
|
|
|
73
91
|
|
|
74
92
|
writeFileSync(join(skillDir, 'SKILL.md'), skillMd);
|
|
75
93
|
|
|
76
|
-
|
|
77
|
-
console.log(JSON.stringify({
|
|
94
|
+
const result: ScaffoldResult = {
|
|
78
95
|
status: 'created',
|
|
79
96
|
path: skillDir,
|
|
80
97
|
files: [
|
|
@@ -83,4 +100,7 @@ console.log(JSON.stringify({
|
|
|
83
100
|
join(skillDir, 'references/'),
|
|
84
101
|
join(skillDir, 'assets/templates/')
|
|
85
102
|
]
|
|
86
|
-
}
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
// Output JSON
|
|
106
|
+
console.log(JSON.stringify(result));
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* validate-routing.ts — Semantic routing validation for skills
|
|
4
|
+
* Usage: bun validate-routing.ts <skill-dir>
|
|
5
|
+
* Output: unified JSON envelope {target, pass, checks:[{id,status,detail}], summary}
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { readFileSync, existsSync } from 'node:fs';
|
|
9
|
+
import { join } from 'node:path';
|
|
10
|
+
|
|
11
|
+
type CheckStatus = 'PASS' | 'FAIL' | 'WARN' | 'SKIP';
|
|
12
|
+
|
|
13
|
+
interface CheckEntry {
|
|
14
|
+
id: string;
|
|
15
|
+
status: CheckStatus;
|
|
16
|
+
detail: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
interface ValidationSummary {
|
|
20
|
+
total: number;
|
|
21
|
+
pass: number;
|
|
22
|
+
fail: number;
|
|
23
|
+
warn: number;
|
|
24
|
+
skip: number;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
interface ValidationReport {
|
|
28
|
+
target: string;
|
|
29
|
+
pass: boolean;
|
|
30
|
+
checks: CheckEntry[];
|
|
31
|
+
summary: ValidationSummary;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const skillDir = process.argv[2];
|
|
35
|
+
|
|
36
|
+
if (!skillDir || skillDir === '-h' || skillDir === '--help') {
|
|
37
|
+
if (skillDir === '-h' || skillDir === '--help') {
|
|
38
|
+
console.log('Usage: bun validate-routing.ts <skill-dir>');
|
|
39
|
+
process.exit(0);
|
|
40
|
+
}
|
|
41
|
+
console.error(
|
|
42
|
+
JSON.stringify({
|
|
43
|
+
error: 'Usage: bun validate-routing.ts <skill-dir>'
|
|
44
|
+
})
|
|
45
|
+
);
|
|
46
|
+
process.exit(1);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const skillFile = join(skillDir, 'SKILL.md');
|
|
50
|
+
|
|
51
|
+
if (!existsSync(skillFile)) {
|
|
52
|
+
const report: ValidationReport = {
|
|
53
|
+
target: skillDir,
|
|
54
|
+
pass: false,
|
|
55
|
+
checks: [{ id: 'routing.skill-file', status: 'FAIL', detail: 'SKILL.md not found' }],
|
|
56
|
+
summary: { total: 1, pass: 0, fail: 1, warn: 0, skip: 0 }
|
|
57
|
+
};
|
|
58
|
+
console.log(JSON.stringify(report));
|
|
59
|
+
process.exit(1);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const skillMd = readFileSync(skillFile, 'utf-8');
|
|
63
|
+
const checks: CheckEntry[] = [];
|
|
64
|
+
|
|
65
|
+
function add(id: string, passed: boolean, detail: string): void {
|
|
66
|
+
checks.push({ id, status: passed ? 'PASS' : 'FAIL', detail });
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Extract frontmatter
|
|
70
|
+
const frontmatterMatch = skillMd.match(/^---\n([\s\S]*?)\n---/);
|
|
71
|
+
const frontmatter = frontmatterMatch ? frontmatterMatch[1] : '';
|
|
72
|
+
|
|
73
|
+
// Extract body (everything after frontmatter)
|
|
74
|
+
const bodyStart = skillMd.indexOf('---', 3);
|
|
75
|
+
const body = bodyStart !== -1 ? skillMd.slice(bodyStart + 3) : skillMd;
|
|
76
|
+
|
|
77
|
+
// Extract description
|
|
78
|
+
const descMatch = frontmatter.match(/^description:\s*([\s\S]*?)(?=\n\w+:|\n---)/m);
|
|
79
|
+
const description = descMatch ? descMatch[1].replace(/^>\s*\n?/, '').trim() : '';
|
|
80
|
+
|
|
81
|
+
// CHECK 1: positive_triggers coverage (min 3 entries)
|
|
82
|
+
const ptMatch = frontmatter.match(/^positive_triggers:\s*\n((?:\s+-\s+.+\n?)*)/m);
|
|
83
|
+
let ptCount = 0;
|
|
84
|
+
if (ptMatch) {
|
|
85
|
+
const triggers = ptMatch[1].split('\n').filter((l) => l.trim().startsWith('-'));
|
|
86
|
+
ptCount = triggers.length;
|
|
87
|
+
}
|
|
88
|
+
add(
|
|
89
|
+
'routing.positive-triggers',
|
|
90
|
+
ptCount >= 3,
|
|
91
|
+
`${ptCount} entries found (minimum 3 required) — improves semantic routing accuracy`
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
// CHECK 2: anti_triggers coverage (min 2 entries)
|
|
95
|
+
const atMatch = frontmatter.match(/^anti_triggers:\s*\n((?:\s+-\s+.+\n?)*)/m);
|
|
96
|
+
let atCount = 0;
|
|
97
|
+
if (atMatch) {
|
|
98
|
+
const triggers = atMatch[1].split('\n').filter((l) => l.trim().startsWith('-'));
|
|
99
|
+
atCount = triggers.length;
|
|
100
|
+
}
|
|
101
|
+
add(
|
|
102
|
+
'routing.anti-triggers',
|
|
103
|
+
atCount >= 2,
|
|
104
|
+
`${atCount} entries found (minimum 2 required) — boosts routing precision by 31.8%`
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
// CHECK 3: Description contains "Use when" phrasing
|
|
108
|
+
const hasUseWhen = /use when/i.test(description);
|
|
109
|
+
add(
|
|
110
|
+
'routing.use-when',
|
|
111
|
+
hasUseWhen,
|
|
112
|
+
hasUseWhen
|
|
113
|
+
? 'Found "Use when" phrasing'
|
|
114
|
+
: 'Missing "Use when" phrasing in description — imperative phrasing helps agents decide activation'
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
// CHECK 4: Description contains "Do NOT use when" phrasing
|
|
118
|
+
const hasNotUse = /do not use when|don't use when/i.test(description);
|
|
119
|
+
add(
|
|
120
|
+
'routing.negative-scope',
|
|
121
|
+
hasNotUse,
|
|
122
|
+
hasNotUse
|
|
123
|
+
? 'Found "Do NOT use when" phrasing'
|
|
124
|
+
: 'Missing negative scope in description — prevents over-firing on similar-domain queries'
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
// CHECK 5: Description-body alignment (keywords in description appear in body)
|
|
128
|
+
let alignmentScore = 0;
|
|
129
|
+
let alignmentTotal = 0;
|
|
130
|
+
if (description) {
|
|
131
|
+
const stopWords = new Set([
|
|
132
|
+
'the',
|
|
133
|
+
'and',
|
|
134
|
+
'for',
|
|
135
|
+
'with',
|
|
136
|
+
'this',
|
|
137
|
+
'that',
|
|
138
|
+
'when',
|
|
139
|
+
'not',
|
|
140
|
+
'use',
|
|
141
|
+
'from',
|
|
142
|
+
'are',
|
|
143
|
+
'was',
|
|
144
|
+
'have',
|
|
145
|
+
'has',
|
|
146
|
+
'will',
|
|
147
|
+
'can',
|
|
148
|
+
'should',
|
|
149
|
+
'does',
|
|
150
|
+
'its'
|
|
151
|
+
]);
|
|
152
|
+
const words = description
|
|
153
|
+
.toLowerCase()
|
|
154
|
+
.replace(/[^a-z0-9\s]/g, ' ')
|
|
155
|
+
.split(/\s+/)
|
|
156
|
+
.filter((w) => w.length > 3 && !stopWords.has(w));
|
|
157
|
+
|
|
158
|
+
const uniqueWords = [...new Set(words)];
|
|
159
|
+
alignmentTotal = Math.min(uniqueWords.length, 10);
|
|
160
|
+
|
|
161
|
+
for (const word of uniqueWords.slice(0, 10)) {
|
|
162
|
+
if (body.toLowerCase().includes(word)) {
|
|
163
|
+
alignmentScore++;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const alignmentRatio = alignmentTotal > 0 ? alignmentScore / alignmentTotal : 0;
|
|
169
|
+
add(
|
|
170
|
+
'routing.alignment',
|
|
171
|
+
alignmentRatio >= 0.5,
|
|
172
|
+
`${alignmentScore}/${alignmentTotal} description keywords found in body (${Math.round(
|
|
173
|
+
alignmentRatio * 100
|
|
174
|
+
)}% alignment) — frontmatter-only indexing loses 29-44% recall`
|
|
175
|
+
);
|
|
176
|
+
|
|
177
|
+
// CHECK 6: Single-responsibility verification
|
|
178
|
+
let singleResponsibility = true;
|
|
179
|
+
let srDetail = 'Single atomic intent detected';
|
|
180
|
+
if (description) {
|
|
181
|
+
const compoundMarkers = /\b(and also\b|\badditionally\b|\bas well as\b)/i;
|
|
182
|
+
if (compoundMarkers.test(description)) {
|
|
183
|
+
singleResponsibility = false;
|
|
184
|
+
srDetail = 'Compound intent detected. Description contains multiple operations joined by "and also" or "additionally".';
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const actionVerbs = description.match(
|
|
188
|
+
/\b(?:create|delete|update|modify|analyze|generate|process|manage|handle|configure|deploy|monitor)\b/gi
|
|
189
|
+
);
|
|
190
|
+
if (actionVerbs && new Set(actionVerbs.map((v) => v.toLowerCase())).size > 2) {
|
|
191
|
+
singleResponsibility = false;
|
|
192
|
+
srDetail = `Multiple distinct action verbs detected (${[
|
|
193
|
+
...new Set(actionVerbs.map((v) => v.toLowerCase()))
|
|
194
|
+
].join(', ')}). Consider splitting into separate skills.`;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
add(
|
|
198
|
+
'routing.atomic-intent',
|
|
199
|
+
singleResponsibility,
|
|
200
|
+
`${srDetail} — multi-domain descriptions cause trigger dilution`
|
|
201
|
+
);
|
|
202
|
+
|
|
203
|
+
// Unified envelope
|
|
204
|
+
const passCount = checks.filter((c) => c.status === 'PASS').length;
|
|
205
|
+
const report: ValidationReport = {
|
|
206
|
+
target: skillDir,
|
|
207
|
+
pass: passCount === checks.length,
|
|
208
|
+
checks,
|
|
209
|
+
summary: {
|
|
210
|
+
total: checks.length,
|
|
211
|
+
pass: passCount,
|
|
212
|
+
fail: checks.filter((c) => c.status === 'FAIL').length,
|
|
213
|
+
warn: checks.filter((c) => c.status === 'WARN').length,
|
|
214
|
+
skip: checks.filter((c) => c.status === 'SKIP').length
|
|
215
|
+
}
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
console.log(JSON.stringify(report));
|
|
@@ -1,39 +1,71 @@
|
|
|
1
|
-
#!/usr/bin/env
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
2
|
/**
|
|
3
|
-
* validate-structure.
|
|
4
|
-
* Usage:
|
|
3
|
+
* validate-structure.ts — Check skill directory structure and SKILL.md compliance
|
|
4
|
+
* Usage: bun validate-structure.ts <skill-dir>
|
|
5
5
|
* Output: unified JSON envelope {target, pass, checks:[{id,status,detail}], summary}
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import { readFileSync, existsSync, readdirSync
|
|
9
|
-
import { join, basename } from 'path';
|
|
8
|
+
import { readFileSync, existsSync, readdirSync } from 'node:fs';
|
|
9
|
+
import { join, basename } from 'node:path';
|
|
10
|
+
|
|
11
|
+
type CheckStatus = 'PASS' | 'FAIL' | 'WARN' | 'SKIP';
|
|
12
|
+
|
|
13
|
+
interface CheckEntry {
|
|
14
|
+
id: string;
|
|
15
|
+
status: CheckStatus;
|
|
16
|
+
detail: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
interface ValidationSummary {
|
|
20
|
+
total: number;
|
|
21
|
+
pass: number;
|
|
22
|
+
fail: number;
|
|
23
|
+
warn: number;
|
|
24
|
+
skip: number;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
interface ValidationReport {
|
|
28
|
+
target: string;
|
|
29
|
+
pass: boolean;
|
|
30
|
+
name?: string;
|
|
31
|
+
description_length?: number;
|
|
32
|
+
checks: CheckEntry[];
|
|
33
|
+
summary: ValidationSummary;
|
|
34
|
+
}
|
|
10
35
|
|
|
11
36
|
const skillDir = process.argv[2];
|
|
12
37
|
|
|
13
|
-
if (!skillDir) {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
38
|
+
if (!skillDir || skillDir === '-h' || skillDir === '--help') {
|
|
39
|
+
if (skillDir === '-h' || skillDir === '--help') {
|
|
40
|
+
console.log('Usage: bun validate-structure.ts <skill-dir>');
|
|
41
|
+
process.exit(0);
|
|
42
|
+
}
|
|
43
|
+
console.error(
|
|
44
|
+
JSON.stringify({
|
|
45
|
+
error: 'Usage: bun validate-structure.ts <skill-dir>'
|
|
46
|
+
})
|
|
47
|
+
);
|
|
17
48
|
process.exit(1);
|
|
18
49
|
}
|
|
19
50
|
|
|
20
|
-
const checks = [];
|
|
51
|
+
const checks: CheckEntry[] = [];
|
|
21
52
|
|
|
22
|
-
function add(id, severity, detail) {
|
|
53
|
+
function add(id: string, severity: CheckStatus, detail: string): void {
|
|
23
54
|
checks.push({ id, status: severity, detail });
|
|
24
55
|
}
|
|
25
|
-
const fail = (id, detail) => add(id, 'FAIL', detail);
|
|
26
|
-
const warn = (id, detail) => add(id, 'WARN', detail);
|
|
27
|
-
const pass = (id, detail) => add(id, 'PASS', detail);
|
|
56
|
+
const fail = (id: string, detail: string): void => add(id, 'FAIL', detail);
|
|
57
|
+
const warn = (id: string, detail: string): void => add(id, 'WARN', detail);
|
|
58
|
+
const pass = (id: string, detail: string): void => add(id, 'PASS', detail);
|
|
28
59
|
|
|
29
60
|
// Check SKILL.md exists
|
|
30
61
|
if (!existsSync(join(skillDir, 'SKILL.md'))) {
|
|
31
|
-
|
|
62
|
+
const report: ValidationReport = {
|
|
32
63
|
target: skillDir,
|
|
33
64
|
pass: false,
|
|
34
65
|
checks: [{ id: 'structure.skill-file', status: 'FAIL', detail: 'SKILL.md not found' }],
|
|
35
66
|
summary: { total: 1, pass: 0, fail: 1, warn: 0, skip: 0 }
|
|
36
|
-
}
|
|
67
|
+
};
|
|
68
|
+
console.log(JSON.stringify(report));
|
|
37
69
|
process.exit(1);
|
|
38
70
|
}
|
|
39
71
|
|
|
@@ -57,7 +89,10 @@ const name = nameMatch ? nameMatch[1].replace(/"/g, '').trim() : '';
|
|
|
57
89
|
if (!name) {
|
|
58
90
|
fail('structure.name', "Missing required 'name' field in frontmatter");
|
|
59
91
|
} else if (!/^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/.test(name)) {
|
|
60
|
-
fail(
|
|
92
|
+
fail(
|
|
93
|
+
'structure.name',
|
|
94
|
+
`Invalid name format: '${name}'. Must be lowercase letters, numbers, hyphens only. No leading/trailing hyphens.`
|
|
95
|
+
);
|
|
61
96
|
} else if (name.length > 64) {
|
|
62
97
|
fail('structure.name', `Name too long: ${name.length} chars. Maximum 64 characters.`);
|
|
63
98
|
} else if (name.includes('--')) {
|
|
@@ -69,7 +104,10 @@ if (!name) {
|
|
|
69
104
|
// Check name matches directory
|
|
70
105
|
const dirName = basename(skillDir);
|
|
71
106
|
if (name && name !== dirName) {
|
|
72
|
-
warn(
|
|
107
|
+
warn(
|
|
108
|
+
'structure.name-dir-match',
|
|
109
|
+
`Name '${name}' does not match directory name '${dirName}'. Pi allows this, but the Agent Skills standard requires matching.`
|
|
110
|
+
);
|
|
73
111
|
}
|
|
74
112
|
|
|
75
113
|
// Check description field (handles both single-line and multi-line YAML)
|
|
@@ -113,40 +151,58 @@ if (desc && !/do not use when|don't use when/i.test(desc)) {
|
|
|
113
151
|
if (desc) {
|
|
114
152
|
const compoundMarkers = /\b(and also\b|\badditionally\b|\bas well as\b)/i;
|
|
115
153
|
if (compoundMarkers.test(desc)) {
|
|
116
|
-
warn(
|
|
154
|
+
warn(
|
|
155
|
+
'structure.compound-intent',
|
|
156
|
+
'Description may contain compound intent (multiple operations). Consider splitting into separate skills.'
|
|
157
|
+
);
|
|
117
158
|
}
|
|
118
159
|
}
|
|
119
160
|
|
|
120
161
|
// positive_triggers array (min 3 entries)
|
|
121
162
|
const ptMatch = frontmatter.match(/^positive_triggers:\s*\n((?:\s+-\s+.+\n?)*)/m);
|
|
122
163
|
if (ptMatch) {
|
|
123
|
-
const triggers = ptMatch[1].split('\n').filter(l => l.trim().startsWith('-'));
|
|
164
|
+
const triggers = ptMatch[1].split('\n').filter((l) => l.trim().startsWith('-'));
|
|
124
165
|
if (triggers.length < 3) {
|
|
125
|
-
warn(
|
|
166
|
+
warn(
|
|
167
|
+
'structure.positive-triggers',
|
|
168
|
+
`positive_triggers has ${triggers.length} entries. Minimum 3 recommended for routing accuracy.`
|
|
169
|
+
);
|
|
126
170
|
} else {
|
|
127
171
|
pass('structure.positive-triggers', `${triggers.length} entries`);
|
|
128
172
|
}
|
|
129
173
|
} else {
|
|
130
|
-
warn(
|
|
174
|
+
warn(
|
|
175
|
+
'structure.positive-triggers',
|
|
176
|
+
"Missing 'positive_triggers' array in frontmatter. Recommended minimum 3 entries for semantic routing."
|
|
177
|
+
);
|
|
131
178
|
}
|
|
132
179
|
|
|
133
180
|
// anti_triggers array (min 2 entries)
|
|
134
181
|
const atMatch = frontmatter.match(/^anti_triggers:\s*\n((?:\s+-\s+.+\n?)*)/m);
|
|
135
182
|
if (atMatch) {
|
|
136
|
-
const triggers = atMatch[1].split('\n').filter(l => l.trim().startsWith('-'));
|
|
183
|
+
const triggers = atMatch[1].split('\n').filter((l) => l.trim().startsWith('-'));
|
|
137
184
|
if (triggers.length < 2) {
|
|
138
|
-
warn(
|
|
185
|
+
warn(
|
|
186
|
+
'structure.anti-triggers',
|
|
187
|
+
`anti_triggers has ${triggers.length} entries. Minimum 2 recommended for precision (+31.8%).`
|
|
188
|
+
);
|
|
139
189
|
} else {
|
|
140
190
|
pass('structure.anti-triggers', `${triggers.length} entries`);
|
|
141
191
|
}
|
|
142
192
|
} else {
|
|
143
|
-
warn(
|
|
193
|
+
warn(
|
|
194
|
+
'structure.anti-triggers',
|
|
195
|
+
"Missing 'anti_triggers' array in frontmatter. Anti-triggers boost routing precision by 31.8%."
|
|
196
|
+
);
|
|
144
197
|
}
|
|
145
198
|
|
|
146
|
-
// Runtime contract when scripts exist
|
|
199
|
+
// Runtime contract when scripts exist (.ts, .mjs, .js, .py, .sh)
|
|
147
200
|
const scriptsDir = join(skillDir, 'scripts');
|
|
201
|
+
const SCRIPT_EXTENSIONS = ['.ts', '.mjs', '.js', '.py', '.sh'];
|
|
148
202
|
if (existsSync(scriptsDir)) {
|
|
149
|
-
const scriptFiles = readdirSync(scriptsDir).filter(
|
|
203
|
+
const scriptFiles = readdirSync(scriptsDir).filter((f) =>
|
|
204
|
+
SCRIPT_EXTENSIONS.some((ext) => f.endsWith(ext))
|
|
205
|
+
);
|
|
150
206
|
if (scriptFiles.length > 0) {
|
|
151
207
|
if (!frontmatter.includes('runtime:') && !frontmatter.includes('timeout_seconds')) {
|
|
152
208
|
warn('structure.runtime-contract', 'Scripts exist but no runtime contract declared in frontmatter (runtime:, timeout_seconds:)');
|
|
@@ -158,7 +214,9 @@ if (existsSync(scriptsDir)) {
|
|
|
158
214
|
|
|
159
215
|
// Hardcoded absolute paths in script files
|
|
160
216
|
if (existsSync(scriptsDir)) {
|
|
161
|
-
const scriptFiles = readdirSync(scriptsDir).filter(
|
|
217
|
+
const scriptFiles = readdirSync(scriptsDir).filter((f) =>
|
|
218
|
+
SCRIPT_EXTENSIONS.some((ext) => f.endsWith(ext))
|
|
219
|
+
);
|
|
162
220
|
for (const file of scriptFiles) {
|
|
163
221
|
const content = readFileSync(join(scriptsDir, file), 'utf-8');
|
|
164
222
|
const absPathPatterns = [
|
|
@@ -177,7 +235,7 @@ if (existsSync(scriptsDir)) {
|
|
|
177
235
|
fail(`structure.portability.${file}`, `Harness-specific directory reference detected in scripts/${file}. Scripts must be portable across harnesses.`);
|
|
178
236
|
}
|
|
179
237
|
}
|
|
180
|
-
if (!checks.some(c => c.id.startsWith('structure.portability.') && c.status === 'FAIL') && scriptFiles.length > 0) {
|
|
238
|
+
if (!checks.some((c) => c.id.startsWith('structure.portability.') && c.status === 'FAIL') && scriptFiles.length > 0) {
|
|
181
239
|
pass('structure.portability', 'No hardcoded/harness-specific paths in scripts');
|
|
182
240
|
}
|
|
183
241
|
}
|
|
@@ -199,15 +257,15 @@ for (const line of lines) {
|
|
|
199
257
|
const ref = refMatch[1];
|
|
200
258
|
if (ref && !ref.startsWith('http') && !ref.startsWith('#') && !ref.startsWith('mailto:')) {
|
|
201
259
|
if (!existsSync(join(skillDir, ref))) {
|
|
202
|
-
warn(
|
|
260
|
+
warn('structure.reference', `File reference not found: ${ref}`);
|
|
203
261
|
}
|
|
204
262
|
}
|
|
205
263
|
}
|
|
206
264
|
}
|
|
207
265
|
|
|
208
266
|
// Unified envelope
|
|
209
|
-
const fails = checks.filter(c => c.status === 'FAIL').length;
|
|
210
|
-
|
|
267
|
+
const fails = checks.filter((c) => c.status === 'FAIL').length;
|
|
268
|
+
const report: ValidationReport = {
|
|
211
269
|
target: skillDir,
|
|
212
270
|
pass: fails === 0,
|
|
213
271
|
name,
|
|
@@ -215,9 +273,12 @@ console.log(JSON.stringify({
|
|
|
215
273
|
checks,
|
|
216
274
|
summary: {
|
|
217
275
|
total: checks.length,
|
|
218
|
-
pass: checks.filter(c => c.status === 'PASS').length,
|
|
276
|
+
pass: checks.filter((c) => c.status === 'PASS').length,
|
|
219
277
|
fail: fails,
|
|
220
|
-
warn: checks.filter(c => c.status === 'WARN').length,
|
|
221
|
-
skip: checks.filter(c => c.status === 'SKIP').length
|
|
278
|
+
warn: checks.filter((c) => c.status === 'WARN').length,
|
|
279
|
+
skip: checks.filter((c) => c.status === 'SKIP').length
|
|
222
280
|
}
|
|
223
|
-
}
|
|
281
|
+
};
|
|
282
|
+
|
|
283
|
+
console.log(JSON.stringify(report));
|
|
284
|
+
process.exit(fails > 0 ? 1 : 0);
|
package/skills/okf-docs/SKILL.md
CHANGED
|
@@ -67,7 +67,7 @@ Set `status: draft` on creation. Flip to `stable` after human review (`verified:
|
|
|
67
67
|
### 5. Validate Before Delivery
|
|
68
68
|
|
|
69
69
|
```bash
|
|
70
|
-
bun scripts/validate-frontmatter.
|
|
70
|
+
bun scripts/validate-frontmatter.ts <document.md>
|
|
71
71
|
```
|
|
72
72
|
|
|
73
73
|
Envelope must read `pass: true`. Fix named FAIL entries; re-run until clean.
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
{
|
|
5
5
|
"id": 1,
|
|
6
6
|
"prompt": "Write an ADR documenting our decision to adopt PostgreSQL as the central datastore.",
|
|
7
|
-
"expected_output": "Skill activates; authored document carries OKF v0.2 frontmatter (type: Architecture Decision Record, generated actor-convention provenance, sources list) and passes scripts/validate-frontmatter.
|
|
7
|
+
"expected_output": "Skill activates; authored document carries OKF v0.2 frontmatter (type: Architecture Decision Record, generated actor-convention provenance, sources list) and passes scripts/validate-frontmatter.ts before delivery.",
|
|
8
8
|
"files": [],
|
|
9
9
|
"assertions": [
|
|
10
10
|
"Frontmatter contains type + generated.by/at in actor convention",
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
{
|
|
26
26
|
"id": 3,
|
|
27
27
|
"prompt": "Add proper frontmatter to this legacy module doc so it passes validation.",
|
|
28
|
-
"expected_output": "Agent applies OKF v0.2 frontmatter (type per intent, generated provenance, status lifecycle), runs validate-frontmatter.
|
|
28
|
+
"expected_output": "Agent applies OKF v0.2 frontmatter (type per intent, generated provenance, status lifecycle), runs validate-frontmatter.ts, iterates until pass:true, and leaves body content unaltered beyond attribution footnotes if claims need sourcing.",
|
|
29
29
|
"files": [],
|
|
30
30
|
"assertions": [
|
|
31
31
|
"Validator run on the updated document",
|