opencode-swarm-plugin 0.14.0 → 0.15.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.
@@ -357,6 +357,200 @@ describe("ES module compatibility", () => {
357
357
  });
358
358
  });
359
359
 
360
+ // ============================================================================
361
+ // Tests: CSO Validation
362
+ // ============================================================================
363
+
364
+ import { validateCSOCompliance } from "./skills";
365
+
366
+ describe("validateCSOCompliance", () => {
367
+ describe("description validation", () => {
368
+ it("passes for CSO-compliant description with 'Use when'", () => {
369
+ const warnings = validateCSOCompliance(
370
+ "testing-async",
371
+ "Use when tests have race conditions - replaces arbitrary timeouts with condition polling",
372
+ );
373
+
374
+ expect(warnings.critical).toHaveLength(0);
375
+ expect(warnings.suggestions).toHaveLength(0);
376
+ });
377
+
378
+ it("warns when missing 'Use when...' pattern", () => {
379
+ const warnings = validateCSOCompliance(
380
+ "testing-async",
381
+ "For async testing patterns",
382
+ );
383
+
384
+ expect(warnings.critical).toContain(
385
+ "Description should include 'Use when...' to focus on triggering conditions",
386
+ );
387
+ });
388
+
389
+ it("warns for first-person voice", () => {
390
+ const warnings = validateCSOCompliance(
391
+ "testing-async",
392
+ "I can help you with async tests when I detect race conditions",
393
+ );
394
+
395
+ expect(warnings.critical.some((w) => w.includes("first-person"))).toBe(
396
+ true,
397
+ );
398
+ });
399
+
400
+ it("warns for second-person voice", () => {
401
+ const warnings = validateCSOCompliance(
402
+ "testing-async",
403
+ "Use when you need to test async code and your tests have race conditions",
404
+ );
405
+
406
+ expect(warnings.critical.some((w) => w.includes("second-person"))).toBe(
407
+ true,
408
+ );
409
+ });
410
+
411
+ it("rejects description > 1024 chars", () => {
412
+ const longDesc = "a".repeat(1025);
413
+ const warnings = validateCSOCompliance("test", longDesc);
414
+
415
+ expect(
416
+ warnings.critical.some(
417
+ (w) => w.includes("1025") && w.includes("max 1024"),
418
+ ),
419
+ ).toBe(true);
420
+ });
421
+
422
+ it("suggests improvement for description > 500 chars", () => {
423
+ const mediumDesc = "Use when testing. " + "a".repeat(490);
424
+ const warnings = validateCSOCompliance("test", mediumDesc);
425
+
426
+ expect(warnings.critical).toHaveLength(0); // Not critical
427
+ expect(warnings.suggestions.some((w) => w.includes("aim for <500"))).toBe(
428
+ true,
429
+ );
430
+ });
431
+
432
+ it("accepts description < 500 chars with no length warnings", () => {
433
+ const shortDesc =
434
+ "Use when tests have race conditions - replaces timeouts";
435
+ const warnings = validateCSOCompliance("testing-async", shortDesc);
436
+
437
+ const hasLengthWarning =
438
+ warnings.critical.some((w) => w.includes("chars")) ||
439
+ warnings.suggestions.some((w) => w.includes("chars"));
440
+
441
+ expect(hasLengthWarning).toBe(false);
442
+ });
443
+ });
444
+
445
+ describe("name validation", () => {
446
+ it("accepts gerund-based names", () => {
447
+ const warnings = validateCSOCompliance(
448
+ "testing-async",
449
+ "Use when testing async code",
450
+ );
451
+
452
+ const hasNameWarning = warnings.suggestions.some((w) =>
453
+ w.includes("verb-first"),
454
+ );
455
+ expect(hasNameWarning).toBe(false);
456
+ });
457
+
458
+ it("accepts verb-first names", () => {
459
+ const warnings = validateCSOCompliance(
460
+ "validate-schemas",
461
+ "Use when validating schemas",
462
+ );
463
+
464
+ const hasNameWarning = warnings.suggestions.some((w) =>
465
+ w.includes("verb-first"),
466
+ );
467
+ expect(hasNameWarning).toBe(false);
468
+ });
469
+
470
+ it("accepts action verbs", () => {
471
+ const actionVerbs = [
472
+ "test-runner",
473
+ "debug-tools",
474
+ "scan-code",
475
+ "check-types",
476
+ "build-artifacts",
477
+ ];
478
+
479
+ for (const name of actionVerbs) {
480
+ const warnings = validateCSOCompliance(name, "Use when testing");
481
+ const hasNameWarning = warnings.suggestions.some((w) =>
482
+ w.includes("verb-first"),
483
+ );
484
+ expect(hasNameWarning).toBe(false);
485
+ }
486
+ });
487
+
488
+ it("suggests verb-first for noun-first names", () => {
489
+ const warnings = validateCSOCompliance(
490
+ "async-test",
491
+ "Use when testing async",
492
+ );
493
+
494
+ expect(
495
+ warnings.suggestions.some((w) =>
496
+ w.includes("doesn't follow verb-first"),
497
+ ),
498
+ ).toBe(true);
499
+ });
500
+
501
+ it("warns for name > 64 chars", () => {
502
+ const longName = "a".repeat(65);
503
+ const warnings = validateCSOCompliance(longName, "Use when testing");
504
+
505
+ expect(warnings.critical.some((w) => w.includes("64 character"))).toBe(
506
+ true,
507
+ );
508
+ });
509
+
510
+ it("warns for invalid name format", () => {
511
+ const warnings = validateCSOCompliance(
512
+ "Invalid_Name",
513
+ "Use when testing",
514
+ );
515
+
516
+ expect(
517
+ warnings.critical.some((w) =>
518
+ w.includes("lowercase letters, numbers, and hyphens"),
519
+ ),
520
+ ).toBe(true);
521
+ });
522
+ });
523
+
524
+ describe("comprehensive examples", () => {
525
+ it("perfect CSO compliance", () => {
526
+ const warnings = validateCSOCompliance(
527
+ "testing-race-conditions",
528
+ "Use when tests have race conditions - replaces arbitrary timeouts with condition polling and retry logic",
529
+ );
530
+
531
+ expect(warnings.critical).toHaveLength(0);
532
+ expect(warnings.suggestions).toHaveLength(0);
533
+ });
534
+
535
+ it("multiple critical issues", () => {
536
+ const warnings = validateCSOCompliance(
537
+ "BadName_123",
538
+ "I can help you test async code when you need to avoid race conditions. " +
539
+ "a".repeat(1000),
540
+ );
541
+
542
+ expect(warnings.critical.length).toBeGreaterThan(2);
543
+ expect(warnings.critical.some((w) => w.includes("first-person"))).toBe(
544
+ true,
545
+ );
546
+ expect(warnings.critical.some((w) => w.includes("second-person"))).toBe(
547
+ true,
548
+ );
549
+ expect(warnings.critical.some((w) => w.includes("lowercase"))).toBe(true);
550
+ });
551
+ });
552
+ });
553
+
360
554
  // ============================================================================
361
555
  // Tests: Edge Cases
362
556
  // ============================================================================
package/src/skills.ts CHANGED
@@ -628,6 +628,167 @@ Use this to access supplementary skill resources.`,
628
628
  */
629
629
  const DEFAULT_SKILLS_DIR = ".opencode/skills";
630
630
 
631
+ // =============================================================================
632
+ // CSO (Claude Search Optimization) Validation
633
+ // =============================================================================
634
+
635
+ /**
636
+ * CSO validation warnings for skill metadata
637
+ */
638
+ export interface CSOValidationWarnings {
639
+ /** Critical warnings (strong indicators of poor discoverability) */
640
+ critical: string[];
641
+ /** Suggestions for improvement */
642
+ suggestions: string[];
643
+ }
644
+
645
+ /**
646
+ * Validate skill metadata against Claude Search Optimization best practices
647
+ *
648
+ * Checks:
649
+ * - 'Use when...' format in description
650
+ * - Description length (warn > 500, max 1024)
651
+ * - Third-person voice (no 'I', 'you')
652
+ * - Name conventions (verb-first, gerunds, hyphens)
653
+ *
654
+ * @returns Warnings object with critical issues and suggestions
655
+ */
656
+ export function validateCSOCompliance(
657
+ name: string,
658
+ description: string,
659
+ ): CSOValidationWarnings {
660
+ const warnings: CSOValidationWarnings = {
661
+ critical: [],
662
+ suggestions: [],
663
+ };
664
+
665
+ // Description: Check for 'Use when...' pattern
666
+ const hasUseWhen = /\buse when\b/i.test(description);
667
+ if (!hasUseWhen) {
668
+ warnings.critical.push(
669
+ "Description should include 'Use when...' to focus on triggering conditions",
670
+ );
671
+ }
672
+
673
+ // Description: Length checks
674
+ if (description.length > 1024) {
675
+ warnings.critical.push(
676
+ `Description is ${description.length} chars (max 1024) - will be rejected`,
677
+ );
678
+ } else if (description.length > 500) {
679
+ warnings.suggestions.push(
680
+ `Description is ${description.length} chars (aim for <500 for optimal discoverability)`,
681
+ );
682
+ }
683
+
684
+ // Description: Third-person check (no 'I', 'you')
685
+ const firstPersonPattern = /\b(I|I'm|I'll|my|mine|myself)\b/i;
686
+ const secondPersonPattern = /\b(you|you're|you'll|your|yours|yourself)\b/i;
687
+
688
+ if (firstPersonPattern.test(description)) {
689
+ warnings.critical.push(
690
+ "Description uses first-person ('I', 'my') - skills are injected into system prompt, use third-person only",
691
+ );
692
+ }
693
+
694
+ if (secondPersonPattern.test(description)) {
695
+ warnings.critical.push(
696
+ "Description uses second-person ('you', 'your') - use third-person voice (e.g., 'Handles X' not 'You can handle X')",
697
+ );
698
+ }
699
+
700
+ // Name: Check for verb-first/gerund patterns
701
+ const nameWords = name.split("-");
702
+ const firstWord = nameWords[0];
703
+
704
+ // Common gerund endings: -ing
705
+ // Common verb forms: -ing, -ize, -ify, -ate
706
+ const isGerund = /ing$/.test(firstWord);
707
+ const isVerbForm = /(ing|ize|ify|ate)$/.test(firstWord);
708
+
709
+ if (!isGerund && !isVerbForm) {
710
+ // Check if it's a common action verb
711
+ const actionVerbs = [
712
+ "test",
713
+ "debug",
714
+ "fix",
715
+ "scan",
716
+ "check",
717
+ "validate",
718
+ "create",
719
+ "build",
720
+ "deploy",
721
+ "run",
722
+ "load",
723
+ "fetch",
724
+ "parse",
725
+ ];
726
+ const startsWithAction = actionVerbs.includes(firstWord);
727
+
728
+ if (!startsWithAction) {
729
+ warnings.suggestions.push(
730
+ `Name '${name}' doesn't follow verb-first pattern. Consider gerunds (e.g., 'testing-skills' not 'test-skill') or action verbs for better clarity`,
731
+ );
732
+ }
733
+ }
734
+
735
+ // Name: Check length
736
+ if (name.length > 64) {
737
+ warnings.critical.push(
738
+ `Name exceeds 64 character limit (${name.length} chars)`,
739
+ );
740
+ }
741
+
742
+ // Name: Validate format (already enforced by schema, but good to document)
743
+ if (!/^[a-z0-9-]+$/.test(name)) {
744
+ warnings.critical.push(
745
+ "Name must be lowercase letters, numbers, and hyphens only",
746
+ );
747
+ }
748
+
749
+ return warnings;
750
+ }
751
+
752
+ /**
753
+ * Format CSO warnings into a readable message for tool output
754
+ */
755
+ function formatCSOWarnings(warnings: CSOValidationWarnings): string | null {
756
+ if (warnings.critical.length === 0 && warnings.suggestions.length === 0) {
757
+ return null;
758
+ }
759
+
760
+ const parts: string[] = [];
761
+
762
+ if (warnings.critical.length > 0) {
763
+ parts.push("**CSO Critical Issues:**");
764
+ for (const warning of warnings.critical) {
765
+ parts.push(` ⚠️ ${warning}`);
766
+ }
767
+ }
768
+
769
+ if (warnings.suggestions.length > 0) {
770
+ parts.push("\n**CSO Suggestions:**");
771
+ for (const suggestion of warnings.suggestions) {
772
+ parts.push(` 💡 ${suggestion}`);
773
+ }
774
+ }
775
+
776
+ parts.push("\n**CSO Guide:**");
777
+ parts.push(
778
+ " • Start description with 'Use when...' (focus on triggering conditions)",
779
+ );
780
+ parts.push(" • Keep description <500 chars (max 1024)");
781
+ parts.push(" • Use third-person voice only (injected into system prompt)");
782
+ parts.push(
783
+ " • Name: verb-first or gerunds (e.g., 'testing-async' not 'async-test')",
784
+ );
785
+ parts.push(
786
+ "\n Example: 'Use when tests have race conditions - replaces arbitrary timeouts with condition polling'",
787
+ );
788
+
789
+ return parts.join("\n");
790
+ }
791
+
631
792
  /**
632
793
  * Quote a YAML scalar if it contains special characters
633
794
  * Uses double quotes and escapes internal quotes/newlines
@@ -749,6 +910,9 @@ Good skills have:
749
910
  return `Skill '${args.name}' already exists at ${existing.path}. Use skills_update to modify it.`;
750
911
  }
751
912
 
913
+ // Validate CSO compliance (advisory warnings only)
914
+ const csoWarnings = validateCSOCompliance(args.name, args.description);
915
+
752
916
  // Determine target directory
753
917
  let skillDir: string;
754
918
  if (args.directory === "global") {
@@ -778,21 +942,26 @@ Good skills have:
778
942
  // Invalidate cache so new skill is discoverable
779
943
  invalidateSkillsCache();
780
944
 
781
- return JSON.stringify(
782
- {
783
- success: true,
784
- skill: args.name,
785
- path: skillPath,
786
- message: `Created skill '${args.name}'. It's now discoverable via skills_list.`,
787
- next_steps: [
788
- "Test with skills_use to verify instructions are clear",
789
- "Add examples.md or reference.md for supplementary content",
790
- "Add scripts/ directory for executable helpers",
791
- ],
792
- },
793
- null,
794
- 2,
795
- );
945
+ // Build response with CSO warnings if present
946
+ const response: Record<string, unknown> = {
947
+ success: true,
948
+ skill: args.name,
949
+ path: skillPath,
950
+ message: `Created skill '${args.name}'. It's now discoverable via skills_list.`,
951
+ next_steps: [
952
+ "Test with skills_use to verify instructions are clear",
953
+ "Add examples.md or reference.md for supplementary content",
954
+ "Add scripts/ directory for executable helpers",
955
+ ],
956
+ };
957
+
958
+ // Add CSO warnings if any
959
+ const warningsMessage = formatCSOWarnings(csoWarnings);
960
+ if (warningsMessage) {
961
+ response.cso_warnings = warningsMessage;
962
+ }
963
+
964
+ return JSON.stringify(response, null, 2);
796
965
  } catch (error) {
797
966
  return `Failed to create skill: ${error instanceof Error ? error.message : String(error)}`;
798
967
  }
@@ -1231,10 +1231,10 @@ describe("Graceful Degradation", () => {
1231
1231
  mockContext,
1232
1232
  );
1233
1233
 
1234
- // Check that agent-mail discipline is in the prompt
1234
+ // Check that swarm-mail discipline is in the prompt
1235
1235
  expect(result).toContain("MANDATORY");
1236
- expect(result).toContain("Agent Mail");
1237
- expect(result).toContain("agentmail_send");
1236
+ expect(result).toContain("Swarm Mail");
1237
+ expect(result).toContain("swarmmail_send");
1238
1238
  expect(result).toContain("Report progress");
1239
1239
  });
1240
1240
  });
@@ -1243,7 +1243,7 @@ describe("Graceful Degradation", () => {
1243
1243
  // Coordinator-Centric Swarm Tools (V2)
1244
1244
  // ============================================================================
1245
1245
 
1246
- describe("Swarm Prompt V2 (with Agent Mail/Beads)", () => {
1246
+ describe("Swarm Prompt V2 (with Swarm Mail/Beads)", () => {
1247
1247
  describe("formatSubtaskPromptV2", () => {
1248
1248
  it("generates correct prompt with all fields", () => {
1249
1249
  const result = formatSubtaskPromptV2({