pi-crew 0.9.26 → 0.9.28
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/CHANGELOG.md +102 -0
- package/NOTICE.md +14 -0
- package/assets/crew-vibes.ttf +0 -0
- package/assets/runner-spritesheet.png +0 -0
- package/dist/build-meta.json +245 -48
- package/dist/index.mjs +1611 -243
- package/dist/index.mjs.map +4 -4
- package/docs/perf/optimization-plan-2026-07-verified.md +396 -0
- package/package.json +9 -2
- package/scripts/bench-check.mjs +96 -0
- package/scripts/bench-cold-start.mjs +216 -0
- package/scripts/build-bundle.mjs +84 -0
- package/scripts/build-crew-vibes-font.py +328 -0
- package/scripts/check-all-skills.ts +294 -0
- package/scripts/check-bundle-staleness.mjs +97 -0
- package/scripts/check-conflict-markers.mjs +91 -0
- package/scripts/check-lazy-imports.mjs +28 -0
- package/scripts/install-crew-vibes-font.mjs +91 -0
- package/scripts/postinstall.mjs +47 -0
- package/scripts/profile-startup.mjs +125 -0
- package/scripts/release-smoke.mjs +74 -0
- package/scripts/run-bench.mjs +47 -0
- package/scripts/run-real-chain.ts +41 -0
- package/scripts/test-issue-29-crash.ts +111 -0
- package/scripts/test-issue-29-e2e.ts +183 -0
- package/scripts/test-issue-29-real-runtime.ts +330 -0
- package/scripts/test-issue-29-real-tasks.ts +387 -0
- package/scripts/test-issue-29-team-tool.ts +105 -0
- package/scripts/test-runner.mjs +74 -0
- package/scripts/verify-flicker-fix.ts +109 -0
- package/scripts/verify-skill.ts +550 -0
- package/scripts/watch-bundle.mjs +259 -0
- package/scripts/watchdog-harness.ts +119 -0
- package/src/agents/agent-config.ts +2 -0
- package/src/agents/discover-agents.ts +10 -1
- package/src/config/defaults.ts +6 -0
- package/src/extension/crew-vibes/cat-frames.ts +18 -0
- package/src/extension/crew-vibes/config.ts +195 -0
- package/src/extension/crew-vibes/figures.ts +94 -0
- package/src/extension/crew-vibes/font-detect.ts +58 -0
- package/src/extension/crew-vibes/index.ts +385 -0
- package/src/extension/crew-vibes/provider-usage.ts +396 -0
- package/src/extension/crew-vibes/render.ts +206 -0
- package/src/extension/crew-vibes/speed.ts +286 -0
- package/src/extension/knowledge-injection.ts +12 -3
- package/src/extension/management.ts +23 -3
- package/src/extension/register.ts +7 -0
- package/src/extension/team-tool.ts +11 -0
- package/src/prompt/prompt-runtime.ts +65 -0
- package/src/runtime/child-pi.ts +123 -10
- package/src/runtime/crew-agent-records.ts +10 -3
- package/src/runtime/pi-args.ts +2 -0
- package/src/runtime/retry-executor.ts +4 -1
- package/src/runtime/task-runner/state-helpers.ts +9 -30
- package/src/runtime/task-runner.ts +1 -0
- package/src/runtime/team-runner.ts +5 -3
- package/src/state/atomic-write.ts +153 -49
- package/src/state/event-log.ts +16 -10
- package/src/state/locks.ts +7 -8
- package/src/state/mailbox.ts +15 -4
- package/src/state/state-store.ts +39 -10
- package/src/teams/discover-teams.ts +56 -1
- package/src/utils/safe-paths.ts +2 -1
- package/src/workflows/discover-workflows.ts +72 -1
|
@@ -0,0 +1,550 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Skill Verification Script
|
|
4
|
+
*
|
|
5
|
+
* Verifies that a skill has proper gate enforcement (RED/GREEN gates or pi-crew format).
|
|
6
|
+
* Supports both:
|
|
7
|
+
* - Classic RED/GREEN gate format: ## RED Gate, ## GREEN Gate
|
|
8
|
+
* - pi-crew format: ## Refuse Gate, ## Enforcement, checkbox lists
|
|
9
|
+
*
|
|
10
|
+
* Usage:
|
|
11
|
+
* node scripts/verify-skill.ts skills/systematic-debugging/SKILL.md # single skill
|
|
12
|
+
* node scripts/verify-skill.ts skills/ # batch mode
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import * as fs from "fs";
|
|
16
|
+
import * as path from "path";
|
|
17
|
+
|
|
18
|
+
interface Gate {
|
|
19
|
+
type: "red" | "green";
|
|
20
|
+
condition: string;
|
|
21
|
+
check: string;
|
|
22
|
+
failMessage: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
interface VerificationResult {
|
|
26
|
+
skillPath: string;
|
|
27
|
+
skillName: string;
|
|
28
|
+
hasTriggerSection: boolean;
|
|
29
|
+
hasGates: boolean;
|
|
30
|
+
gates: Gate[];
|
|
31
|
+
hasAntiPatterns: boolean;
|
|
32
|
+
hasEnforceableGates: boolean;
|
|
33
|
+
isDescriptiveOnly: boolean;
|
|
34
|
+
errors: string[];
|
|
35
|
+
warnings: string[];
|
|
36
|
+
passed: boolean;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// ============================================================
|
|
40
|
+
// PATTERN DEFINITIONS
|
|
41
|
+
// ============================================================
|
|
42
|
+
|
|
43
|
+
// Trigger/activation patterns
|
|
44
|
+
const TRIGGER_PATTERNS = [
|
|
45
|
+
/^#+\s*(When (to|should) Activate|Trigger|Conditions?|Use When|Apply When|Activation Criteria)/im,
|
|
46
|
+
/(?:^|\n)##\s*(When (to|should) Activate|Trigger|Conditions?|Use When|Apply When|Activation Criteria)/im,
|
|
47
|
+
/(?:^|\n)##\s*Activation/im,
|
|
48
|
+
/^#+\s*Triggers?\s*\n/im,
|
|
49
|
+
/^Use this skill (when|whenever|if)/im,
|
|
50
|
+
/^Triggers?:/im,
|
|
51
|
+
/description:.*Triggers?:/i,
|
|
52
|
+
];
|
|
53
|
+
|
|
54
|
+
// Anti-pattern patterns
|
|
55
|
+
const ANTI_PATTERN_PATTERNS = [
|
|
56
|
+
/(?:^|\n)##\s*Anti-?patterns?\s*\n/im,
|
|
57
|
+
/(?:^|\n)##\s*What (NOT|not) (to|to do)|Don't|DO NOT/im,
|
|
58
|
+
/(?:^|\n)##\s*Pitfalls?\s*\n/im,
|
|
59
|
+
/(?:^|\n)##\s*Common Mistakes?\s*\n/im,
|
|
60
|
+
/(?:^|\n)##\s*Avoid\s*\n/im,
|
|
61
|
+
];
|
|
62
|
+
|
|
63
|
+
// Classic RED/GREEN gate patterns
|
|
64
|
+
const CLASSIC_GATE_PATTERNS = [
|
|
65
|
+
/(?:^|\n)##\s*(RED|GREEN)[\s_-]*(GATE|Gates?)\s*\n/im,
|
|
66
|
+
/(?:^|\n)###\s*(RED|GREEN)[\s_-]*(GATE|Gates?)\s*\n/im,
|
|
67
|
+
/(?:^|\n)(RED|GREEN)[\s_-]*(GATE|Gates?):/im,
|
|
68
|
+
];
|
|
69
|
+
|
|
70
|
+
// pi-crew specific gate patterns
|
|
71
|
+
const PI_CREW_REFUSE_GATE_PATTERNS = [
|
|
72
|
+
/(?:^|\n)##\s*Refuse\s*Gate[^\n]*\n/i,
|
|
73
|
+
/(?:^|\n)###\s*Refuse\s*Gate[^\n]*\n/i,
|
|
74
|
+
/(?:^|\n)##\s*STOP[\s_-]*gate[^\n]*\n/i,
|
|
75
|
+
/(?:^|\n)###\s*STOP[\s_-]*gate[^\n]*\n/i,
|
|
76
|
+
];
|
|
77
|
+
|
|
78
|
+
const PI_CREW_ENFORCEMENT_PATTERNS = [
|
|
79
|
+
/(?:^|\n)##\s*Enforcement[^\n]*\n/i,
|
|
80
|
+
/(?:^|\n)###\s*Enforcement[^\n]*\n/i,
|
|
81
|
+
/(?:^|\n)##\s*Gate[^\n]*\n/i,
|
|
82
|
+
/(?:^|\n)###\s*Gate[^\n]*\n/i,
|
|
83
|
+
];
|
|
84
|
+
|
|
85
|
+
const PI_CREW_PROCEED_PATTERNS = [
|
|
86
|
+
/(?:^|\n)##\s*Proceed\s*Gate[^\n]*\n/i,
|
|
87
|
+
/(?:^|\n)###\s*Proceed\s*Gate[^\n]*\n/i,
|
|
88
|
+
/(?:^|\n)##\s*(GREEN|Go)[\s_-]*(GATE|Gates?)[^\n]*\n/i,
|
|
89
|
+
];
|
|
90
|
+
|
|
91
|
+
// Checkbox list pattern (can indicate enforceable gates)
|
|
92
|
+
const CHECKBOX_PATTERN = /(?:^|\n)(\s*)-\s*\[([ xX])\]/g;
|
|
93
|
+
|
|
94
|
+
// Pass/fail patterns
|
|
95
|
+
const PASS_FAIL_PATTERNS = [
|
|
96
|
+
/(?:^|\n)###\s*(PASS|FAIL|RED|GREEN)/im,
|
|
97
|
+
/(?:^|\n)\|\s*(PASS|FAIL|RED|GREEN)\s*\|/im,
|
|
98
|
+
/(?:^|\n)_\(PASS\)|_\(FAIL\)/im,
|
|
99
|
+
/(?:^|\n)\*\*PASS\*\*|\*\*FAIL\*\*/im,
|
|
100
|
+
/(?:^|\n)(?:✓|✗|✅|❌)\s*(PASS|FAIL|pass|fail)/im,
|
|
101
|
+
];
|
|
102
|
+
|
|
103
|
+
// ============================================================
|
|
104
|
+
// PATTERN MATCHING FUNCTIONS
|
|
105
|
+
// ============================================================
|
|
106
|
+
|
|
107
|
+
function hasTriggerSection(content: string): boolean {
|
|
108
|
+
return TRIGGER_PATTERNS.some((pattern) => pattern.test(content));
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function hasAntiPatternSection(content: string): boolean {
|
|
112
|
+
return ANTI_PATTERN_PATTERNS.some((pattern) => pattern.test(content));
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Extract gates from classic RED/GREEN format
|
|
117
|
+
*/
|
|
118
|
+
function extractClassicGates(content: string): Gate[] {
|
|
119
|
+
const gates: Gate[] = [];
|
|
120
|
+
|
|
121
|
+
for (const pattern of CLASSIC_GATE_PATTERNS) {
|
|
122
|
+
const re = new RegExp(pattern.source, "gi");
|
|
123
|
+
let m: RegExpExecArray | null;
|
|
124
|
+
while ((m = re.exec(content)) !== null) {
|
|
125
|
+
const typeMatch = m[0].match(/(RED|GREEN)/i);
|
|
126
|
+
if (typeMatch) {
|
|
127
|
+
const type = typeMatch[1].toLowerCase() as "red" | "green";
|
|
128
|
+
gates.push({
|
|
129
|
+
type,
|
|
130
|
+
condition: "classic gate",
|
|
131
|
+
check: "see section",
|
|
132
|
+
failMessage: "",
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return gates;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Extract gates from pi-crew format (Refuse Gate, Enforcement, etc.)
|
|
143
|
+
*/
|
|
144
|
+
function extractPiCrewGates(content: string): Gate[] {
|
|
145
|
+
const gates: Gate[] = [];
|
|
146
|
+
|
|
147
|
+
// Check for Refuse Gate (RED gate equivalent)
|
|
148
|
+
for (const pattern of PI_CREW_REFUSE_GATE_PATTERNS) {
|
|
149
|
+
if (pattern.test(content)) {
|
|
150
|
+
gates.push({
|
|
151
|
+
type: "red",
|
|
152
|
+
condition: "Refuse Gate (pi-crew format)",
|
|
153
|
+
check: "checklist items",
|
|
154
|
+
failMessage: "Stop and state what's missing",
|
|
155
|
+
});
|
|
156
|
+
break;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Check for Enforcement sections (could be RED or GREEN)
|
|
161
|
+
for (const pattern of PI_CREW_ENFORCEMENT_PATTERNS) {
|
|
162
|
+
const match = content.match(pattern);
|
|
163
|
+
if (match) {
|
|
164
|
+
const sectionTitle = match[0].trim();
|
|
165
|
+
// Determine if this looks like a RED or GREEN gate based on context
|
|
166
|
+
const isRedGate = /refuse|stop|block|prevent/i.test(sectionTitle);
|
|
167
|
+
gates.push({
|
|
168
|
+
type: isRedGate ? "red" : "green",
|
|
169
|
+
condition: sectionTitle,
|
|
170
|
+
check: "checklist or criteria",
|
|
171
|
+
failMessage: isRedGate ? "Do not proceed" : "Verify before proceeding",
|
|
172
|
+
});
|
|
173
|
+
break;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Check for Proceed/Go gates (GREEN gate equivalent)
|
|
178
|
+
for (const pattern of PI_CREW_PROCEED_PATTERNS) {
|
|
179
|
+
if (pattern.test(content)) {
|
|
180
|
+
gates.push({
|
|
181
|
+
type: "green",
|
|
182
|
+
condition: "Proceed Gate (pi-crew format)",
|
|
183
|
+
check: "pre-conditions met",
|
|
184
|
+
failMessage: "Wait until conditions are met",
|
|
185
|
+
});
|
|
186
|
+
break;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return gates;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Extract gates from checkbox lists (pi-crew enforcement pattern)
|
|
195
|
+
* Looks for checkbox lists that represent gate conditions
|
|
196
|
+
*/
|
|
197
|
+
function extractCheckboxGates(content: string): Gate[] {
|
|
198
|
+
const gates: Gate[] = [];
|
|
199
|
+
|
|
200
|
+
// Find all checkbox items
|
|
201
|
+
const checkboxMatches = content.match(/(?:^|\n)(\s*)-\s*\[([ xX])\]/g);
|
|
202
|
+
|
|
203
|
+
if (checkboxMatches && checkboxMatches.length >= 2) {
|
|
204
|
+
// Check if checkboxes are in a gate-like section
|
|
205
|
+
const gateSections = [...PI_CREW_REFUSE_GATE_PATTERNS, ...PI_CREW_ENFORCEMENT_PATTERNS, ...PI_CREW_PROCEED_PATTERNS];
|
|
206
|
+
|
|
207
|
+
for (const sectionPattern of gateSections) {
|
|
208
|
+
if (sectionPattern.test(content)) {
|
|
209
|
+
// Found checkbox items in a gate section
|
|
210
|
+
gates.push({
|
|
211
|
+
type: /proceed|green|go/i.test(sectionPattern.source) ? "green" : "red",
|
|
212
|
+
condition: `${checkboxMatches.length} checklist items`,
|
|
213
|
+
check: "checkbox items are checked",
|
|
214
|
+
failMessage: "All items must be satisfied",
|
|
215
|
+
});
|
|
216
|
+
return gates;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Checkboxes found but not in a named gate section - still count if substantial
|
|
221
|
+
if (checkboxMatches.length >= 3) {
|
|
222
|
+
gates.push({
|
|
223
|
+
type: "red",
|
|
224
|
+
condition: `${checkboxMatches.length} unchecked items`,
|
|
225
|
+
check: "see checklist",
|
|
226
|
+
failMessage: "Verify all conditions",
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
return gates;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Extract gates from pass/fail patterns
|
|
236
|
+
*/
|
|
237
|
+
function extractPassFailGates(content: string): Gate[] {
|
|
238
|
+
const gates: Gate[] = [];
|
|
239
|
+
|
|
240
|
+
for (const pattern of PASS_FAIL_PATTERNS) {
|
|
241
|
+
const re = new RegExp(pattern.source, "gi");
|
|
242
|
+
let m: RegExpExecArray | null;
|
|
243
|
+
while ((m = re.exec(content)) !== null) {
|
|
244
|
+
const match = m[0];
|
|
245
|
+
gates.push({
|
|
246
|
+
type: /pass|green/i.test(match) ? "green" : "red",
|
|
247
|
+
condition: "explicit criteria",
|
|
248
|
+
check: "see text",
|
|
249
|
+
failMessage: "",
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
return gates;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Extract all gates from content
|
|
259
|
+
*/
|
|
260
|
+
function extractGates(content: string): Gate[] {
|
|
261
|
+
const gates: Gate[] = [];
|
|
262
|
+
|
|
263
|
+
// Try all extraction methods
|
|
264
|
+
gates.push(...extractClassicGates(content));
|
|
265
|
+
gates.push(...extractPiCrewGates(content));
|
|
266
|
+
gates.push(...extractCheckboxGates(content));
|
|
267
|
+
gates.push(...extractPassFailGates(content));
|
|
268
|
+
|
|
269
|
+
// Deduplicate by condition
|
|
270
|
+
const seen = new Set<string>();
|
|
271
|
+
return gates.filter((gate) => {
|
|
272
|
+
const key = `${gate.type}:${gate.condition}`;
|
|
273
|
+
if (seen.has(key)) return false;
|
|
274
|
+
seen.add(key);
|
|
275
|
+
return true;
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Determine if skill is purely descriptive without enforcement
|
|
281
|
+
*/
|
|
282
|
+
function isDescriptiveOnly(content: string): boolean {
|
|
283
|
+
const descriptiveIndicators = [
|
|
284
|
+
/best\s+practices?\s*(only|only\s+descriptive)?/i,
|
|
285
|
+
/recommendations?\s+only/i,
|
|
286
|
+
/guidelines?\s+only/i,
|
|
287
|
+
/no\s+(enforcement|validation|checks?)/i,
|
|
288
|
+
/purely\s+descriptive/i,
|
|
289
|
+
/descriptive\s+only/i,
|
|
290
|
+
/\[\s*TODO.*enforce/i,
|
|
291
|
+
];
|
|
292
|
+
|
|
293
|
+
const hasDescriptiveOnly = descriptiveIndicators.some((pattern) => pattern.test(content));
|
|
294
|
+
|
|
295
|
+
// Check for "should" vs "must" ratio
|
|
296
|
+
const shouldCount = (content.match(/\bshould\b/gi) || []).length;
|
|
297
|
+
const mustCount = (content.match(/\bmust\b/gi) || []).length;
|
|
298
|
+
const shallCount = (content.match(/\bshall\b/gi) || []).length;
|
|
299
|
+
|
|
300
|
+
return hasDescriptiveOnly || (shouldCount > 10 && mustCount === 0 && shallCount === 0);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// ============================================================
|
|
304
|
+
// VERIFICATION FUNCTION
|
|
305
|
+
// ============================================================
|
|
306
|
+
|
|
307
|
+
function verifySkill(skillPath: string): VerificationResult {
|
|
308
|
+
const result: VerificationResult = {
|
|
309
|
+
skillPath,
|
|
310
|
+
skillName: path.basename(path.dirname(skillPath)),
|
|
311
|
+
hasTriggerSection: false,
|
|
312
|
+
hasGates: false,
|
|
313
|
+
gates: [],
|
|
314
|
+
hasAntiPatterns: false,
|
|
315
|
+
hasEnforceableGates: false,
|
|
316
|
+
isDescriptiveOnly: false,
|
|
317
|
+
errors: [],
|
|
318
|
+
warnings: [],
|
|
319
|
+
passed: false,
|
|
320
|
+
};
|
|
321
|
+
|
|
322
|
+
try {
|
|
323
|
+
const content = fs.readFileSync(skillPath, "utf-8");
|
|
324
|
+
|
|
325
|
+
// Check YAML frontmatter for required fields
|
|
326
|
+
const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/);
|
|
327
|
+
if (frontmatterMatch) {
|
|
328
|
+
const frontmatter = frontmatterMatch[1];
|
|
329
|
+
if (!/^origin:/m.test(frontmatter)) {
|
|
330
|
+
result.errors.push("Missing 'origin' field in YAML frontmatter");
|
|
331
|
+
}
|
|
332
|
+
if (!/^name:/m.test(frontmatter)) {
|
|
333
|
+
result.errors.push("Missing 'name' field in YAML frontmatter");
|
|
334
|
+
}
|
|
335
|
+
if (!/^description:/m.test(frontmatter)) {
|
|
336
|
+
result.errors.push("Missing 'description' field in YAML frontmatter");
|
|
337
|
+
}
|
|
338
|
+
} else {
|
|
339
|
+
result.errors.push("No YAML frontmatter found");
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// Check for trigger section
|
|
343
|
+
result.hasTriggerSection = hasTriggerSection(content);
|
|
344
|
+
if (!result.hasTriggerSection) {
|
|
345
|
+
result.warnings.push("No trigger section found (When to Activate, Trigger, etc.)");
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// Check for anti-patterns
|
|
349
|
+
result.hasAntiPatterns = hasAntiPatternSection(content);
|
|
350
|
+
if (!result.hasAntiPatterns) {
|
|
351
|
+
result.warnings.push("No anti-patterns section found");
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// Extract gates
|
|
355
|
+
result.gates = extractGates(content);
|
|
356
|
+
result.hasGates = result.gates.length > 0;
|
|
357
|
+
|
|
358
|
+
if (!result.hasGates) {
|
|
359
|
+
result.errors.push("No gate found (RED/GREEN/Refuse/Enforcement)");
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// Check if purely descriptive
|
|
363
|
+
result.isDescriptiveOnly = isDescriptiveOnly(content);
|
|
364
|
+
if (result.isDescriptiveOnly) {
|
|
365
|
+
result.warnings.push("Skill appears to be purely descriptive without enforcement");
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// Determine if has enforceable gates
|
|
369
|
+
result.hasEnforceableGates = result.hasGates && !result.isDescriptiveOnly;
|
|
370
|
+
|
|
371
|
+
// Determine pass/fail
|
|
372
|
+
result.passed = result.hasTriggerSection && result.hasEnforceableGates;
|
|
373
|
+
} catch (err) {
|
|
374
|
+
result.errors.push(`Failed to read skill: ${err}`);
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
return result;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// ============================================================
|
|
381
|
+
// OUTPUT FORMATTING
|
|
382
|
+
// ============================================================
|
|
383
|
+
|
|
384
|
+
function formatResult(result: VerificationResult): string {
|
|
385
|
+
const lines: string[] = [];
|
|
386
|
+
|
|
387
|
+
lines.push(`=== Skill: ${result.skillName} ===`);
|
|
388
|
+
|
|
389
|
+
if (result.hasTriggerSection) {
|
|
390
|
+
lines.push("✅ Has trigger section");
|
|
391
|
+
} else {
|
|
392
|
+
lines.push("⚠️ No trigger section found");
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
if (result.hasGates) {
|
|
396
|
+
for (const gate of result.gates.slice(0, 3)) {
|
|
397
|
+
const label = gate.type.toUpperCase();
|
|
398
|
+
const check = gate.check !== "see section" && gate.check !== "see text" ? ` (check: ${gate.check})` : "";
|
|
399
|
+
lines.push(`✅ Has ${label} gate: "${gate.condition}"${check}`);
|
|
400
|
+
}
|
|
401
|
+
if (result.gates.length > 3) {
|
|
402
|
+
lines.push(` ... and ${result.gates.length - 3} more gates`);
|
|
403
|
+
}
|
|
404
|
+
} else {
|
|
405
|
+
lines.push("⚠️ No gate found - only descriptive text");
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
if (result.hasAntiPatterns) {
|
|
409
|
+
lines.push("✅ Has anti-patterns");
|
|
410
|
+
} else {
|
|
411
|
+
lines.push("⚠️ No anti-patterns section");
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
if (result.warnings.length > 0) {
|
|
415
|
+
for (const warning of result.warnings) {
|
|
416
|
+
lines.push(`⚠️ ${warning}`);
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
if (result.errors.length > 0) {
|
|
421
|
+
for (const error of result.errors) {
|
|
422
|
+
lines.push(`❌ ${error}`);
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
if (result.passed) {
|
|
427
|
+
lines.push("✅ PASS - Skill has enforceable gates");
|
|
428
|
+
} else {
|
|
429
|
+
lines.push("❌ FAIL - Skill lacks enforceable gates");
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
return lines.join("\n");
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
// ============================================================
|
|
436
|
+
// FILE DISCOVERY
|
|
437
|
+
// ============================================================
|
|
438
|
+
|
|
439
|
+
function getAllSkillFiles(dirPath: string): string[] {
|
|
440
|
+
const skills: string[] = [];
|
|
441
|
+
|
|
442
|
+
if (!fs.existsSync(dirPath)) {
|
|
443
|
+
return skills;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
const entries = fs.readdirSync(dirPath, { withFileTypes: true });
|
|
447
|
+
|
|
448
|
+
for (const entry of entries) {
|
|
449
|
+
const fullPath = path.join(dirPath, entry.name);
|
|
450
|
+
if (entry.isDirectory()) {
|
|
451
|
+
const skillFile = path.join(fullPath, "SKILL.md");
|
|
452
|
+
if (fs.existsSync(skillFile)) {
|
|
453
|
+
skills.push(skillFile);
|
|
454
|
+
} else {
|
|
455
|
+
// Skip subdirectories - skills should be flat in the skills/ folder
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
return skills;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
// ============================================================
|
|
464
|
+
// MAIN
|
|
465
|
+
// ============================================================
|
|
466
|
+
|
|
467
|
+
async function main() {
|
|
468
|
+
const args = process.argv.slice(2);
|
|
469
|
+
|
|
470
|
+
if (args.length === 0) {
|
|
471
|
+
console.error("Usage: node scripts/verify-skill.ts <skill-path> [skill-path2 ...]");
|
|
472
|
+
console.error(" node scripts/verify-skill.ts skills/ # batch mode");
|
|
473
|
+
process.exit(1);
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
const results: VerificationResult[] = [];
|
|
477
|
+
|
|
478
|
+
// Handle batch mode
|
|
479
|
+
if (args.length === 1 && fs.statSync(args[0]).isDirectory()) {
|
|
480
|
+
const skillFiles = getAllSkillFiles(args[0]);
|
|
481
|
+
console.log(`Checking ${skillFiles.length} skills in batch mode...\n`);
|
|
482
|
+
|
|
483
|
+
for (const skillFile of skillFiles) {
|
|
484
|
+
const result = verifySkill(skillFile);
|
|
485
|
+
results.push(result);
|
|
486
|
+
console.log(formatResult(result));
|
|
487
|
+
console.log("");
|
|
488
|
+
}
|
|
489
|
+
} else {
|
|
490
|
+
// Single or multiple skill files
|
|
491
|
+
for (const arg of args) {
|
|
492
|
+
if (!fs.existsSync(arg)) {
|
|
493
|
+
console.error(`Error: File not found: ${arg}`);
|
|
494
|
+
continue;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
if (fs.statSync(arg).isDirectory()) {
|
|
498
|
+
const skillFiles = getAllSkillFiles(arg);
|
|
499
|
+
for (const skillFile of skillFiles) {
|
|
500
|
+
const result = verifySkill(skillFile);
|
|
501
|
+
results.push(result);
|
|
502
|
+
console.log(formatResult(result));
|
|
503
|
+
console.log("");
|
|
504
|
+
}
|
|
505
|
+
} else if (arg.endsWith("SKILL.md") || arg.endsWith(".md")) {
|
|
506
|
+
const result = verifySkill(arg);
|
|
507
|
+
results.push(result);
|
|
508
|
+
console.log(formatResult(result));
|
|
509
|
+
console.log("");
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
// Summary
|
|
515
|
+
const passed = results.filter((r) => r.passed).length;
|
|
516
|
+
const failed = results.filter((r) => !r.passed && r.errors.length > 0).length;
|
|
517
|
+
const warningsOnly = results.filter((r) => r.passed || (r.warnings.length > 0 && r.errors.length === 0)).length;
|
|
518
|
+
|
|
519
|
+
console.log("=== Summary ===");
|
|
520
|
+
console.log(`Total: ${results.length}`);
|
|
521
|
+
console.log(`Passed: ${passed}`);
|
|
522
|
+
console.log(`Failed: ${failed}`);
|
|
523
|
+
console.log(`Warnings only: ${warningsOnly}`);
|
|
524
|
+
|
|
525
|
+
// List failing skills
|
|
526
|
+
if (failed > 0) {
|
|
527
|
+
console.log("\nFailing skills:");
|
|
528
|
+
for (const r of results.filter((r) => !r.passed && r.errors.length > 0)) {
|
|
529
|
+
console.log(` - ${r.skillName}`);
|
|
530
|
+
for (const err of r.errors) {
|
|
531
|
+
console.log(` ${err}`);
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
// Determine exit code
|
|
537
|
+
let exitCode = 0;
|
|
538
|
+
if (failed > 0) {
|
|
539
|
+
exitCode = 1;
|
|
540
|
+
} else if (warningsOnly > 0 && passed > 0) {
|
|
541
|
+
exitCode = 2;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
process.exit(exitCode);
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
main().catch((err) => {
|
|
548
|
+
console.error("Fatal error:", err);
|
|
549
|
+
process.exit(1);
|
|
550
|
+
});
|