@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.
Files changed (26) hide show
  1. package/AGENTS.md +1 -1
  2. package/CHANGELOG.md +15 -0
  3. package/README.md +2 -1
  4. package/package.json +5 -5
  5. package/scripts/{check-deps.mjs → check-deps.ts} +124 -59
  6. package/scripts/{git-dl.mjs → git-dl.ts} +11 -9
  7. package/skills/create-skill/SKILL.md +16 -16
  8. package/skills/create-skill/assets/templates/SKILL.md.template +1 -1
  9. package/skills/create-skill/evals/evals.json +4 -4
  10. package/skills/create-skill/evals/grading-template.json +1 -1
  11. package/skills/create-skill/references/component-decomposition.md +1 -1
  12. package/skills/create-skill/scripts/{audit-antipatterns.mjs → audit-antipatterns.ts} +75 -31
  13. package/skills/create-skill/scripts/{compute-benchmark.mjs → compute-benchmark.ts} +71 -30
  14. package/skills/create-skill/scripts/run-cold-eval.ts +202 -0
  15. package/skills/create-skill/scripts/{scaffold-skill.mjs → scaffold-skill.ts} +45 -25
  16. package/skills/create-skill/scripts/validate-routing.ts +218 -0
  17. package/skills/create-skill/scripts/{validate-structure.mjs → validate-structure.ts} +97 -36
  18. package/skills/okf-docs/SKILL.md +1 -1
  19. package/skills/okf-docs/evals/evals.json +2 -2
  20. package/skills/okf-docs/scripts/{validate-frontmatter.mjs → validate-frontmatter.ts} +73 -32
  21. package/skills/openspec-learn/references/evaluation-methodology.md +2 -2
  22. package/skills/openspec-pi-apply/SKILL.md +129 -0
  23. package/skills/openspec-pi-apply/references/rpc-protocol.md +43 -0
  24. package/skills/openspec-pi-apply/scripts/pi-rpc-apply.ts +421 -0
  25. package/skills/create-skill/scripts/run-cold-eval.mjs +0 -118
  26. package/skills/create-skill/scripts/validate-routing.mjs +0 -137
@@ -1,46 +1,64 @@
1
- #!/usr/bin/env node
1
+ #!/usr/bin/env bun
2
2
  /**
3
- * scaffold-skill.mjs — Create skill directory structure with SKILL.md skeleton
4
- * Usage: node scaffold-skill.mjs <skill-name> [output-dir]
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, existsSync } from 'fs';
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
- console.error(JSON.stringify({
16
- error: 'Usage: node scaffold-skill.mjs <skill-name> [output-dir]'
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(JSON.stringify({
24
- error: 'Invalid skill name. Must be lowercase letters, numbers, hyphens only. No leading/trailing hyphens.',
25
- name: skillName
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(JSON.stringify({
32
- error: 'Skill name too long. Maximum 64 characters.',
33
- name: skillName,
34
- length: skillName.length
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(JSON.stringify({
41
- error: 'Skill name contains consecutive hyphens.',
42
- name: skillName
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
- // Output JSON
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 node
1
+ #!/usr/bin/env bun
2
2
  /**
3
- * validate-structure.mjs — Check skill directory structure and SKILL.md compliance
4
- * Usage: node validate-structure.mjs <skill-dir>
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, statSync } from 'fs';
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
- console.error(JSON.stringify({
15
- error: 'Usage: node validate-structure.mjs <skill-dir>'
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
- console.log(JSON.stringify({
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('structure.name', `Invalid name format: '${name}'. Must be lowercase letters, numbers, hyphens only. No leading/trailing hyphens.`);
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('structure.name-dir-match', `Name '${name}' does not match directory name '${dirName}'. Pi allows this, but the Agent Skills standard requires matching.`);
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('structure.compound-intent', "Description may contain compound intent (multiple operations). Consider splitting into separate skills.");
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('structure.positive-triggers', `positive_triggers has ${triggers.length} entries. Minimum 3 recommended for routing accuracy.`);
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('structure.positive-triggers', "Missing 'positive_triggers' array in frontmatter. Recommended minimum 3 entries for semantic routing.");
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('structure.anti-triggers', `anti_triggers has ${triggers.length} entries. Minimum 2 recommended for precision (+31.8%).`);
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('structure.anti-triggers', "Missing 'anti_triggers' array in frontmatter. Anti-triggers boost routing precision by 31.8%.");
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(f => f.endsWith('.mjs') || f.endsWith('.js') || f.endsWith('.py') || f.endsWith('.sh'));
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(f => f.endsWith('.mjs') || f.endsWith('.js') || f.endsWith('.py') || f.endsWith('.sh'));
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(`structure.reference`, `File reference not found: ${ref}`);
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
- console.log(JSON.stringify({
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);
@@ -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.mjs <document.md>
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.mjs before delivery.",
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.mjs, iterates until pass:true, and leaves body content unaltered beyond attribution footnotes if claims need sourcing.",
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",