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.
Files changed (64) hide show
  1. package/CHANGELOG.md +102 -0
  2. package/NOTICE.md +14 -0
  3. package/assets/crew-vibes.ttf +0 -0
  4. package/assets/runner-spritesheet.png +0 -0
  5. package/dist/build-meta.json +245 -48
  6. package/dist/index.mjs +1611 -243
  7. package/dist/index.mjs.map +4 -4
  8. package/docs/perf/optimization-plan-2026-07-verified.md +396 -0
  9. package/package.json +9 -2
  10. package/scripts/bench-check.mjs +96 -0
  11. package/scripts/bench-cold-start.mjs +216 -0
  12. package/scripts/build-bundle.mjs +84 -0
  13. package/scripts/build-crew-vibes-font.py +328 -0
  14. package/scripts/check-all-skills.ts +294 -0
  15. package/scripts/check-bundle-staleness.mjs +97 -0
  16. package/scripts/check-conflict-markers.mjs +91 -0
  17. package/scripts/check-lazy-imports.mjs +28 -0
  18. package/scripts/install-crew-vibes-font.mjs +91 -0
  19. package/scripts/postinstall.mjs +47 -0
  20. package/scripts/profile-startup.mjs +125 -0
  21. package/scripts/release-smoke.mjs +74 -0
  22. package/scripts/run-bench.mjs +47 -0
  23. package/scripts/run-real-chain.ts +41 -0
  24. package/scripts/test-issue-29-crash.ts +111 -0
  25. package/scripts/test-issue-29-e2e.ts +183 -0
  26. package/scripts/test-issue-29-real-runtime.ts +330 -0
  27. package/scripts/test-issue-29-real-tasks.ts +387 -0
  28. package/scripts/test-issue-29-team-tool.ts +105 -0
  29. package/scripts/test-runner.mjs +74 -0
  30. package/scripts/verify-flicker-fix.ts +109 -0
  31. package/scripts/verify-skill.ts +550 -0
  32. package/scripts/watch-bundle.mjs +259 -0
  33. package/scripts/watchdog-harness.ts +119 -0
  34. package/src/agents/agent-config.ts +2 -0
  35. package/src/agents/discover-agents.ts +10 -1
  36. package/src/config/defaults.ts +6 -0
  37. package/src/extension/crew-vibes/cat-frames.ts +18 -0
  38. package/src/extension/crew-vibes/config.ts +195 -0
  39. package/src/extension/crew-vibes/figures.ts +94 -0
  40. package/src/extension/crew-vibes/font-detect.ts +58 -0
  41. package/src/extension/crew-vibes/index.ts +385 -0
  42. package/src/extension/crew-vibes/provider-usage.ts +396 -0
  43. package/src/extension/crew-vibes/render.ts +206 -0
  44. package/src/extension/crew-vibes/speed.ts +286 -0
  45. package/src/extension/knowledge-injection.ts +12 -3
  46. package/src/extension/management.ts +23 -3
  47. package/src/extension/register.ts +7 -0
  48. package/src/extension/team-tool.ts +11 -0
  49. package/src/prompt/prompt-runtime.ts +65 -0
  50. package/src/runtime/child-pi.ts +123 -10
  51. package/src/runtime/crew-agent-records.ts +10 -3
  52. package/src/runtime/pi-args.ts +2 -0
  53. package/src/runtime/retry-executor.ts +4 -1
  54. package/src/runtime/task-runner/state-helpers.ts +9 -30
  55. package/src/runtime/task-runner.ts +1 -0
  56. package/src/runtime/team-runner.ts +5 -3
  57. package/src/state/atomic-write.ts +153 -49
  58. package/src/state/event-log.ts +16 -10
  59. package/src/state/locks.ts +7 -8
  60. package/src/state/mailbox.ts +15 -4
  61. package/src/state/state-store.ts +39 -10
  62. package/src/teams/discover-teams.ts +56 -1
  63. package/src/utils/safe-paths.ts +2 -1
  64. 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
+ });