xdrs-core 0.49.1 → 0.50.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/lib/lint.test.js CHANGED
@@ -338,8 +338,6 @@ test('accepts all known frontmatter fields in SKILL.md files', () => {
338
338
  'license: MIT',
339
339
  'metadata:',
340
340
  ' version: "1.0"',
341
- 'compatibility: node >= 18',
342
- 'allowed-tools: read_file grep_search',
343
341
  '---',
344
342
  '',
345
343
  '## Overview',
@@ -358,6 +356,553 @@ test('accepts all known frontmatter fields in SKILL.md files', () => {
358
356
  expect(result.errors.join('\n')).not.toContain('unknown field');
359
357
  });
360
358
 
359
+ test('reports missing ### Inputs subsection in SKILL.md ## Overview', () => {
360
+ const workspaceRoot = createWorkspace('skill-missing-inputs', {
361
+ '.xdrs/index.md': rootIndex(),
362
+ '.xdrs/_local/adrs/index.md': localAdrIndex([
363
+ '- [check-links](principles/skills/check-links/SKILL.md) - Check links'
364
+ ]),
365
+ '.xdrs/_local/adrs/principles/skills/check-links/SKILL.md': fullSkillDocument({ omitInputs: true }),
366
+ });
367
+
368
+ const result = lintWorkspace(workspaceRoot);
369
+
370
+ expect(result.errors.join('\n')).toContain('Skill "## Overview" is missing required subsection "### Inputs"');
371
+ });
372
+
373
+ test('reports missing ### Outputs subsection in SKILL.md ## Overview', () => {
374
+ const workspaceRoot = createWorkspace('skill-missing-outputs', {
375
+ '.xdrs/index.md': rootIndex(),
376
+ '.xdrs/_local/adrs/index.md': localAdrIndex([
377
+ '- [check-links](principles/skills/check-links/SKILL.md) - Check links'
378
+ ]),
379
+ '.xdrs/_local/adrs/principles/skills/check-links/SKILL.md': fullSkillDocument({ omitOutputs: true }),
380
+ });
381
+
382
+ const result = lintWorkspace(workspaceRoot);
383
+
384
+ expect(result.errors.join('\n')).toContain('Skill "## Overview" is missing required subsection "### Outputs"');
385
+ });
386
+
387
+ test('reports missing ### Halt Conditions subsection in SKILL.md ## Overview', () => {
388
+ const workspaceRoot = createWorkspace('skill-missing-halt-conditions', {
389
+ '.xdrs/index.md': rootIndex(),
390
+ '.xdrs/_local/adrs/index.md': localAdrIndex([
391
+ '- [check-links](principles/skills/check-links/SKILL.md) - Check links'
392
+ ]),
393
+ '.xdrs/_local/adrs/principles/skills/check-links/SKILL.md': fullSkillDocument({ omitHaltConditions: true }),
394
+ });
395
+
396
+ const result = lintWorkspace(workspaceRoot);
397
+
398
+ expect(result.errors.join('\n')).toContain('Skill "## Overview" is missing required subsection "### Halt Conditions"');
399
+ });
400
+
401
+ test('reports SKILL.md ### Inputs missing #### Optional subsection', () => {
402
+ const workspaceRoot = createWorkspace('skill-inputs-missing-optional', {
403
+ '.xdrs/index.md': rootIndex(),
404
+ '.xdrs/_local/adrs/index.md': localAdrIndex([
405
+ '- [check-links](principles/skills/check-links/SKILL.md) - Check links'
406
+ ]),
407
+ '.xdrs/_local/adrs/principles/skills/check-links/SKILL.md': fullSkillDocument({ omitInputsOptional: true }),
408
+ });
409
+
410
+ const result = lintWorkspace(workspaceRoot);
411
+
412
+ expect(result.errors.join('\n')).toContain('Skill "### Inputs" section is missing "#### Optional"');
413
+ });
414
+
415
+ test('reports SKILL.md ### Inputs subsections out of order', () => {
416
+ const workspaceRoot = createWorkspace('skill-inputs-out-of-order', {
417
+ '.xdrs/index.md': rootIndex(),
418
+ '.xdrs/_local/adrs/index.md': localAdrIndex([
419
+ '- [check-links](principles/skills/check-links/SKILL.md) - Check links'
420
+ ]),
421
+ '.xdrs/_local/adrs/principles/skills/check-links/SKILL.md': fullSkillDocument({ swapInputsOrder: true }),
422
+ });
423
+
424
+ const result = lintWorkspace(workspaceRoot);
425
+
426
+ expect(result.errors.join('\n')).toContain('Skill "### Inputs" subsections must appear in order (#### Required, #### Optional)');
427
+ });
428
+
429
+ test('reports SKILL.md ### Outputs missing #### Changes subsection', () => {
430
+ const workspaceRoot = createWorkspace('skill-outputs-missing-changes', {
431
+ '.xdrs/index.md': rootIndex(),
432
+ '.xdrs/_local/adrs/index.md': localAdrIndex([
433
+ '- [check-links](principles/skills/check-links/SKILL.md) - Check links'
434
+ ]),
435
+ '.xdrs/_local/adrs/principles/skills/check-links/SKILL.md': fullSkillDocument({ omitOutputsChanges: true }),
436
+ });
437
+
438
+ const result = lintWorkspace(workspaceRoot);
439
+
440
+ expect(result.errors.join('\n')).toContain('Skill "### Outputs" section is missing "#### Changes"');
441
+ });
442
+
443
+ test('passes SKILL.md with valid None bullets in Inputs, Outputs, and Halt Conditions', () => {
444
+ const workspaceRoot = createWorkspace('skill-valid-none-bullets', {
445
+ '.xdrs/index.md': rootIndex(),
446
+ '.xdrs/_local/adrs/index.md': localAdrIndex([
447
+ '- [check-links](principles/skills/check-links/SKILL.md) - Check links'
448
+ ]),
449
+ '.xdrs/_local/adrs/principles/skills/check-links/SKILL.md': fullSkillDocument({
450
+ inputsRequired: '- None',
451
+ inputsOptional: '- None',
452
+ outputsContents: '- None',
453
+ outputsChanges: '- None',
454
+ haltConditions: '- None',
455
+ }),
456
+ });
457
+
458
+ const result = lintWorkspace(workspaceRoot);
459
+
460
+ expect(result.errors.join('\n')).not.toContain('must be under 10 words or a single "None"');
461
+ });
462
+
463
+ test('reports SKILL.md bullet over 10 words in #### Required', () => {
464
+ const workspaceRoot = createWorkspace('skill-required-bullet-too-long', {
465
+ '.xdrs/index.md': rootIndex(),
466
+ '.xdrs/_local/adrs/index.md': localAdrIndex([
467
+ '- [check-links](principles/skills/check-links/SKILL.md) - Check links'
468
+ ]),
469
+ '.xdrs/_local/adrs/principles/skills/check-links/SKILL.md': fullSkillDocument({
470
+ inputsRequired: '- A file path that is way too long to fit under the ten word limit',
471
+ }),
472
+ });
473
+
474
+ const result = lintWorkspace(workspaceRoot);
475
+
476
+ expect(result.errors.join('\n')).toContain('Skill "### Inputs" -> "#### Required" bullet must be under 10 words');
477
+ });
478
+
479
+ test('passes SKILL.md when both optional sections are omitted', () => {
480
+ const workspaceRoot = createWorkspace('skill-optional-sections-omitted', {
481
+ '.xdrs/index.md': rootIndex(),
482
+ '.xdrs/_local/adrs/index.md': localAdrIndex([
483
+ '- [check-links](principles/skills/check-links/SKILL.md) - Check links'
484
+ ]),
485
+ '.xdrs/_local/adrs/principles/skills/check-links/SKILL.md': fullSkillDocument(),
486
+ });
487
+
488
+ const result = lintWorkspace(workspaceRoot);
489
+
490
+ expect(result.errors.join('\n')).not.toContain('### Runtime Requirements');
491
+ expect(result.errors.join('\n')).not.toContain('### User Interaction');
492
+ });
493
+
494
+ test('reports empty ### Runtime Requirements section in SKILL.md', () => {
495
+ const workspaceRoot = createWorkspace('skill-empty-runtime-requirements', {
496
+ '.xdrs/index.md': rootIndex(),
497
+ '.xdrs/_local/adrs/index.md': localAdrIndex([
498
+ '- [check-links](principles/skills/check-links/SKILL.md) - Check links'
499
+ ]),
500
+ '.xdrs/_local/adrs/principles/skills/check-links/SKILL.md': fullSkillDocument({ runtimeRequirements: '' }),
501
+ });
502
+
503
+ const result = lintWorkspace(workspaceRoot);
504
+
505
+ expect(result.errors.join('\n')).toContain('Skill section "### Runtime Requirements" must not be empty');
506
+ });
507
+
508
+ test('reports empty ### User Interaction section in SKILL.md', () => {
509
+ const workspaceRoot = createWorkspace('skill-empty-user-interaction', {
510
+ '.xdrs/index.md': rootIndex(),
511
+ '.xdrs/_local/adrs/index.md': localAdrIndex([
512
+ '- [check-links](principles/skills/check-links/SKILL.md) - Check links'
513
+ ]),
514
+ '.xdrs/_local/adrs/principles/skills/check-links/SKILL.md': fullSkillDocument({ userInteraction: '' }),
515
+ });
516
+
517
+ const result = lintWorkspace(workspaceRoot);
518
+
519
+ expect(result.errors.join('\n')).toContain('Skill section "### User Interaction" must not be empty');
520
+ });
521
+
522
+ test('passes SKILL.md with a long free-form Runtime Requirements bullet', () => {
523
+ const workspaceRoot = createWorkspace('skill-runtime-requirements-free-form', {
524
+ '.xdrs/index.md': rootIndex(),
525
+ '.xdrs/_local/adrs/index.md': localAdrIndex([
526
+ '- [check-links](principles/skills/check-links/SKILL.md) - Check links'
527
+ ]),
528
+ '.xdrs/_local/adrs/principles/skills/check-links/SKILL.md': fullSkillDocument({
529
+ runtimeRequirements: '- Write access to a directory that is not gitignored and has enough free disk space.',
530
+ }),
531
+ });
532
+
533
+ const result = lintWorkspace(workspaceRoot);
534
+
535
+ expect(result.errors.join('\n')).not.toContain('### Runtime Requirements');
536
+ });
537
+
538
+ test('reports SKILL.md User Interaction bullet over 10 words', () => {
539
+ const workspaceRoot = createWorkspace('skill-user-interaction-bullet-too-long', {
540
+ '.xdrs/index.md': rootIndex(),
541
+ '.xdrs/_local/adrs/index.md': localAdrIndex([
542
+ '- [check-links](principles/skills/check-links/SKILL.md) - Check links'
543
+ ]),
544
+ '.xdrs/_local/adrs/principles/skills/check-links/SKILL.md': fullSkillDocument({
545
+ userInteraction: '- A clarifying question that is much too long to fit under the ten word limit',
546
+ }),
547
+ });
548
+
549
+ const result = lintWorkspace(workspaceRoot);
550
+
551
+ expect(result.errors.join('\n')).toContain('Skill "### User Interaction" bullet must be under 10 words');
552
+ });
553
+
554
+ test('reports SKILL.md top-level sections out of template order', () => {
555
+ const workspaceRoot = createWorkspace('skill-sections-out-of-order', {
556
+ '.xdrs/index.md': rootIndex(),
557
+ '.xdrs/_local/adrs/index.md': localAdrIndex([
558
+ '- [check-links](principles/skills/check-links/SKILL.md) - Check links'
559
+ ]),
560
+ '.xdrs/_local/adrs/principles/skills/check-links/SKILL.md': [
561
+ '---',
562
+ 'name: check-links',
563
+ 'description: Test skill document',
564
+ '---',
565
+ '',
566
+ '## Overview',
567
+ '',
568
+ 'Test skill overview.',
569
+ '',
570
+ '### Inputs',
571
+ '',
572
+ '#### Required',
573
+ '- None',
574
+ '',
575
+ '#### Optional',
576
+ '- None',
577
+ '',
578
+ '### Outputs',
579
+ '',
580
+ '#### Contents',
581
+ '- None',
582
+ '',
583
+ '#### Changes',
584
+ '- None',
585
+ '',
586
+ '### Halt Conditions',
587
+ '- None',
588
+ '',
589
+ '## Anti-Patterns',
590
+ '',
591
+ '- **Mistake:** First mistake. **Why it happens:** Reason. **Instead:** Fix.',
592
+ '- **Mistake:** Second mistake. **Why it happens:** Reason. **Instead:** Fix.',
593
+ '- **Mistake:** Third mistake. **Why it happens:** Reason. **Instead:** Fix.',
594
+ '',
595
+ '## Instructions',
596
+ '',
597
+ 'Test instructions.',
598
+ ''
599
+ ].join('\n'),
600
+ });
601
+
602
+ const result = lintWorkspace(workspaceRoot);
603
+
604
+ expect(result.errors.join('\n')).toContain('Skill sections must appear in template order');
605
+ });
606
+
607
+ test('reports SKILL.md ## Overview subsections out of template order', () => {
608
+ const workspaceRoot = createWorkspace('skill-overview-subsections-out-of-order', {
609
+ '.xdrs/index.md': rootIndex(),
610
+ '.xdrs/_local/adrs/index.md': localAdrIndex([
611
+ '- [check-links](principles/skills/check-links/SKILL.md) - Check links'
612
+ ]),
613
+ '.xdrs/_local/adrs/principles/skills/check-links/SKILL.md': [
614
+ '---',
615
+ 'name: check-links',
616
+ 'description: Test skill document',
617
+ '---',
618
+ '',
619
+ '## Overview',
620
+ '',
621
+ 'Test skill overview.',
622
+ '',
623
+ '### Halt Conditions',
624
+ '- None',
625
+ '',
626
+ '### Inputs',
627
+ '',
628
+ '#### Required',
629
+ '- None',
630
+ '',
631
+ '#### Optional',
632
+ '- None',
633
+ '',
634
+ '### Outputs',
635
+ '',
636
+ '#### Contents',
637
+ '- None',
638
+ '',
639
+ '#### Changes',
640
+ '- None',
641
+ '',
642
+ '## Instructions',
643
+ '',
644
+ 'Test instructions.',
645
+ '',
646
+ '## Anti-Patterns',
647
+ '',
648
+ '- **Mistake:** First mistake. **Why it happens:** Reason. **Instead:** Fix.',
649
+ '- **Mistake:** Second mistake. **Why it happens:** Reason. **Instead:** Fix.',
650
+ '- **Mistake:** Third mistake. **Why it happens:** Reason. **Instead:** Fix.',
651
+ ''
652
+ ].join('\n'),
653
+ });
654
+
655
+ const result = lintWorkspace(workspaceRoot);
656
+
657
+ expect(result.errors.join('\n')).toContain('Skill "## Overview" subsections must appear in order');
658
+ });
659
+
660
+ test('reports skill Anti-Patterns section with fewer than 3 entries', () => {
661
+ const workspaceRoot = createWorkspace('skill-anti-patterns-too-few', {
662
+ '.xdrs/index.md': rootIndex(),
663
+ '.xdrs/_local/adrs/index.md': localAdrIndex([
664
+ '- [check-links](principles/skills/check-links/SKILL.md) - Check links'
665
+ ]),
666
+ '.xdrs/_local/adrs/principles/skills/check-links/SKILL.md': [
667
+ '---',
668
+ 'name: check-links',
669
+ 'description: Test skill document',
670
+ '---',
671
+ '',
672
+ '## Overview',
673
+ '',
674
+ 'Test skill overview.',
675
+ '',
676
+ '## Instructions',
677
+ '',
678
+ 'Test instructions.',
679
+ '',
680
+ '## Anti-Patterns',
681
+ '',
682
+ '- **Mistake:** First mistake. **Why it happens:** Reason. **Instead:** Fix.',
683
+ '- **Mistake:** Second mistake. **Why it happens:** Reason. **Instead:** Fix.',
684
+ ''
685
+ ].join('\n'),
686
+ });
687
+
688
+ const result = lintWorkspace(workspaceRoot);
689
+
690
+ expect(result.errors.join('\n')).toContain('"## Anti-Patterns" section must include at least 3 entries (found 2)');
691
+ });
692
+
693
+ test('passes when skill Anti-Patterns section has at least 3 entries', () => {
694
+ const workspaceRoot = createWorkspace('skill-anti-patterns-enough', {
695
+ '.xdrs/index.md': rootIndex(),
696
+ '.xdrs/_local/adrs/index.md': localAdrIndex([
697
+ '- [check-links](principles/skills/check-links/SKILL.md) - Check links'
698
+ ]),
699
+ '.xdrs/_local/adrs/principles/skills/check-links/SKILL.md': [
700
+ '---',
701
+ 'name: check-links',
702
+ 'description: Test skill document',
703
+ '---',
704
+ '',
705
+ '## Overview',
706
+ '',
707
+ 'Test skill overview.',
708
+ '',
709
+ '## Instructions',
710
+ '',
711
+ 'Test instructions.',
712
+ '',
713
+ '## Anti-Patterns',
714
+ '',
715
+ '- **Mistake:** First mistake. **Why it happens:** Reason. **Instead:** Fix.',
716
+ '- **Mistake:** Second mistake. **Why it happens:** Reason. **Instead:** Fix.',
717
+ '- **Mistake:** Third mistake. **Why it happens:** Reason. **Instead:** Fix.',
718
+ ''
719
+ ].join('\n'),
720
+ });
721
+
722
+ const result = lintWorkspace(workspaceRoot);
723
+
724
+ expect(result.errors.join('\n')).not.toContain('"## Anti-Patterns" section must include at least');
725
+ });
726
+
727
+ test('reports skill bundling Makefile missing required targets', () => {
728
+ const workspaceRoot = createWorkspace('skill-makefile-missing-targets', {
729
+ '.xdrs/index.md': rootIndex(),
730
+ '.gitignore': 'dist\n',
731
+ '.xdrs/_local/adrs/index.md': localAdrIndex([
732
+ '- [check-links](principles/skills/check-links/SKILL.md) - Check links'
733
+ ]),
734
+ '.xdrs/_local/adrs/principles/skills/check-links/SKILL.md': skillDocument('Test instructions.'),
735
+ '.xdrs/_local/adrs/principles/skills/check-links/Makefile': [
736
+ 'build:',
737
+ '\techo build',
738
+ ''
739
+ ].join('\n'),
740
+ });
741
+
742
+ const result = lintWorkspace(workspaceRoot);
743
+
744
+ expect(result.errors.join('\n')).toContain('Skill bundling Makefile is missing required "test" target');
745
+ expect(result.errors.join('\n')).toContain('Skill bundling Makefile is missing required "clean" target');
746
+ expect(result.errors.join('\n')).not.toContain('Skill bundling Makefile is missing required "build" target');
747
+ });
748
+
749
+ test('passes when skill bundling Makefile has build, test, and clean targets', () => {
750
+ const workspaceRoot = createWorkspace('skill-makefile-complete', {
751
+ '.xdrs/index.md': rootIndex(),
752
+ '.gitignore': 'dist\n',
753
+ '.xdrs/_local/adrs/index.md': localAdrIndex([
754
+ '- [check-links](principles/skills/check-links/SKILL.md) - Check links'
755
+ ]),
756
+ '.xdrs/_local/adrs/principles/skills/check-links/SKILL.md': skillDocument('Test instructions.'),
757
+ '.xdrs/_local/adrs/principles/skills/check-links/Makefile': [
758
+ 'build: clean',
759
+ '\techo build',
760
+ 'test:',
761
+ '\techo test',
762
+ 'clean:',
763
+ '\trm -rf dist',
764
+ ''
765
+ ].join('\n'),
766
+ });
767
+
768
+ const result = lintWorkspace(workspaceRoot);
769
+
770
+ expect(result.errors.join('\n')).not.toContain('Skill bundling Makefile is missing required');
771
+ });
772
+
773
+ test('reports skill bundling Makefile build target not depending on clean', () => {
774
+ const workspaceRoot = createWorkspace('skill-makefile-build-no-clean-dep', {
775
+ '.xdrs/index.md': rootIndex(),
776
+ '.gitignore': 'dist\n',
777
+ '.xdrs/_local/adrs/index.md': localAdrIndex([
778
+ '- [check-links](principles/skills/check-links/SKILL.md) - Check links'
779
+ ]),
780
+ '.xdrs/_local/adrs/principles/skills/check-links/SKILL.md': skillDocument('Test instructions.'),
781
+ '.xdrs/_local/adrs/principles/skills/check-links/Makefile': [
782
+ 'build:',
783
+ '\trm -rf dist',
784
+ '\techo build',
785
+ 'test:',
786
+ '\techo test',
787
+ 'clean:',
788
+ '\trm -rf dist',
789
+ ''
790
+ ].join('\n'),
791
+ });
792
+
793
+ const result = lintWorkspace(workspaceRoot);
794
+
795
+ expect(result.errors.join('\n')).toContain('Skill bundling Makefile\'s "build" target must depend on "clean"');
796
+ });
797
+
798
+ test('passes when skill bundling Makefile build target depends on clean', () => {
799
+ const workspaceRoot = createWorkspace('skill-makefile-build-clean-dep', {
800
+ '.xdrs/index.md': rootIndex(),
801
+ '.gitignore': 'dist\n',
802
+ '.xdrs/_local/adrs/index.md': localAdrIndex([
803
+ '- [check-links](principles/skills/check-links/SKILL.md) - Check links'
804
+ ]),
805
+ '.xdrs/_local/adrs/principles/skills/check-links/SKILL.md': skillDocument('Test instructions.'),
806
+ '.xdrs/_local/adrs/principles/skills/check-links/Makefile': [
807
+ 'build: clean',
808
+ '\techo build',
809
+ 'test:',
810
+ '\techo test',
811
+ 'clean:',
812
+ '\trm -rf dist',
813
+ ''
814
+ ].join('\n'),
815
+ });
816
+
817
+ const result = lintWorkspace(workspaceRoot);
818
+
819
+ expect(result.errors.join('\n')).not.toContain('must depend on "clean"');
820
+ });
821
+
822
+ test('reports skill bundling Makefile dist output not covered by any .gitignore', () => {
823
+ const workspaceRoot = createWorkspace('skill-makefile-no-gitignore', {
824
+ '.xdrs/index.md': rootIndex(),
825
+ '.xdrs/_local/adrs/index.md': localAdrIndex([
826
+ '- [check-links](principles/skills/check-links/SKILL.md) - Check links'
827
+ ]),
828
+ '.xdrs/_local/adrs/principles/skills/check-links/SKILL.md': skillDocument('Test instructions.'),
829
+ '.xdrs/_local/adrs/principles/skills/check-links/Makefile': [
830
+ 'build: clean',
831
+ '\techo build',
832
+ 'test:',
833
+ '\techo test',
834
+ 'clean:',
835
+ '\trm -rf dist',
836
+ ''
837
+ ].join('\n'),
838
+ });
839
+
840
+ const result = lintWorkspace(workspaceRoot);
841
+
842
+ expect(result.errors.join('\n')).toContain('is not covered by any .gitignore');
843
+ });
844
+
845
+ test('passes when skill bundling Makefile dist output is covered by a local .gitignore', () => {
846
+ const workspaceRoot = createWorkspace('skill-makefile-local-gitignore', {
847
+ '.xdrs/index.md': rootIndex(),
848
+ '.xdrs/_local/adrs/index.md': localAdrIndex([
849
+ '- [check-links](principles/skills/check-links/SKILL.md) - Check links'
850
+ ]),
851
+ '.xdrs/_local/adrs/principles/skills/check-links/SKILL.md': skillDocument('Test instructions.'),
852
+ '.xdrs/_local/adrs/principles/skills/check-links/Makefile': [
853
+ 'build: clean',
854
+ '\techo build',
855
+ 'test:',
856
+ '\techo test',
857
+ 'clean:',
858
+ '\trm -rf dist',
859
+ ''
860
+ ].join('\n'),
861
+ '.xdrs/_local/adrs/principles/skills/check-links/.gitignore': 'dist/\n',
862
+ });
863
+
864
+ const result = lintWorkspace(workspaceRoot);
865
+
866
+ expect(result.errors.join('\n')).not.toContain('is not covered by any .gitignore');
867
+ });
868
+
869
+ test('reports scripts/ file missing a shebang or header comment', () => {
870
+ const workspaceRoot = createWorkspace('skill-script-missing-header', {
871
+ '.xdrs/index.md': rootIndex(),
872
+ '.xdrs/_local/adrs/index.md': localAdrIndex([
873
+ '- [check-links](principles/skills/check-links/SKILL.md) - Check links'
874
+ ]),
875
+ '.xdrs/_local/adrs/principles/skills/check-links/SKILL.md': skillDocument('Test instructions.'),
876
+ '.xdrs/_local/adrs/principles/skills/check-links/scripts/run.sh': [
877
+ 'echo "no header here"',
878
+ ''
879
+ ].join('\n'),
880
+ });
881
+
882
+ const result = lintWorkspace(workspaceRoot);
883
+
884
+ expect(result.errors.join('\n')).toContain('Script must declare its runtime/interpreter at the top');
885
+ });
886
+
887
+ test('passes when scripts/ file declares a shebang', () => {
888
+ const workspaceRoot = createWorkspace('skill-script-with-shebang', {
889
+ '.xdrs/index.md': rootIndex(),
890
+ '.xdrs/_local/adrs/index.md': localAdrIndex([
891
+ '- [check-links](principles/skills/check-links/SKILL.md) - Check links'
892
+ ]),
893
+ '.xdrs/_local/adrs/principles/skills/check-links/SKILL.md': skillDocument('Test instructions.'),
894
+ '.xdrs/_local/adrs/principles/skills/check-links/scripts/run.sh': [
895
+ '#!/bin/bash',
896
+ 'echo "has a shebang"',
897
+ ''
898
+ ].join('\n'),
899
+ });
900
+
901
+ const result = lintWorkspace(workspaceRoot);
902
+
903
+ expect(result.errors.join('\n')).not.toContain('Script must declare its runtime/interpreter at the top');
904
+ });
905
+
361
906
  test('accepts skill package names containing digits, including as a prefix', () => {
362
907
  const workspaceRoot = createWorkspace('skill-name-with-digits', {
363
908
  '.xdrs/index.md': rootIndex(),
@@ -3226,6 +3771,71 @@ function skillDocument(body) {
3226
3771
  ].join('\n');
3227
3772
  }
3228
3773
 
3774
+ function fullSkillDocument(overrides = {}) {
3775
+ const {
3776
+ omitInputs = false,
3777
+ omitInputsOptional = false,
3778
+ swapInputsOrder = false,
3779
+ inputsRequired = '- A file path to scan',
3780
+ inputsOptional = '- None',
3781
+ runtimeRequirements = null,
3782
+ omitOutputs = false,
3783
+ omitOutputsChanges = false,
3784
+ outputsContents = '- A list of broken links',
3785
+ outputsChanges = '- None',
3786
+ omitHaltConditions = false,
3787
+ haltConditions = '- None',
3788
+ userInteraction = null,
3789
+ } = overrides;
3790
+
3791
+ const lines = ['---', 'name: check-links', 'description: Test skill document', '---', '', '## Overview', '', 'Test skill overview.', ''];
3792
+
3793
+ if (!omitInputs) {
3794
+ lines.push('### Inputs', '');
3795
+ if (omitInputsOptional) {
3796
+ lines.push('#### Required', inputsRequired, '');
3797
+ } else if (swapInputsOrder) {
3798
+ lines.push('#### Optional', inputsOptional, '', '#### Required', inputsRequired, '');
3799
+ } else {
3800
+ lines.push('#### Required', inputsRequired, '', '#### Optional', inputsOptional, '');
3801
+ }
3802
+ }
3803
+
3804
+ if (!omitOutputs) {
3805
+ lines.push('### Outputs', '');
3806
+ if (omitOutputsChanges) {
3807
+ lines.push('#### Contents', outputsContents, '');
3808
+ } else {
3809
+ lines.push('#### Contents', outputsContents, '', '#### Changes', outputsChanges, '');
3810
+ }
3811
+ }
3812
+
3813
+ if (!omitHaltConditions) {
3814
+ lines.push('### Halt Conditions', '', haltConditions, '');
3815
+ }
3816
+
3817
+ if (userInteraction !== null) {
3818
+ lines.push('### User Interaction', '', userInteraction, '');
3819
+ }
3820
+
3821
+ if (runtimeRequirements !== null) {
3822
+ lines.push('### Runtime Requirements', '', runtimeRequirements, '');
3823
+ }
3824
+
3825
+ lines.push('## Instructions', '', 'Test instructions.', '');
3826
+
3827
+ lines.push(
3828
+ '## Anti-Patterns',
3829
+ '',
3830
+ '- **Mistake:** First mistake. **Why it happens:** Reason. **Instead:** Fix.',
3831
+ '- **Mistake:** Second mistake. **Why it happens:** Reason. **Instead:** Fix.',
3832
+ '- **Mistake:** Third mistake. **Why it happens:** Reason. **Instead:** Fix.',
3833
+ ''
3834
+ );
3835
+
3836
+ return lines.join('\n');
3837
+ }
3838
+
3229
3839
  test('accepts a custom root folder name as the XDRS root when it contains index.md', () => {
3230
3840
  const doc = [
3231
3841
  '---',
@@ -3716,8 +4326,8 @@ test('reports research exceeding 5000 word limit', () => {
3716
4326
  expect(result.errors.join('\n')).toContain('Research exceeds maximum word count of 5000');
3717
4327
  });
3718
4328
 
3719
- test('reports SKILL.md exceeding 6500 word limit', () => {
3720
- const longBody = ('word '.repeat(6501)).trimEnd();
4329
+ test('reports SKILL.md exceeding 7000 word limit', () => {
4330
+ const longBody = ('word '.repeat(7001)).trimEnd();
3721
4331
  const workspaceRoot = createWorkspace('skill-too-many-words', {
3722
4332
  '.xdrs/index.md': rootIndex(),
3723
4333
  '.xdrs/_local/adrs/index.md': localAdrIndex([
@@ -3728,7 +4338,7 @@ test('reports SKILL.md exceeding 6500 word limit', () => {
3728
4338
 
3729
4339
  const result = lintWorkspace(workspaceRoot);
3730
4340
 
3731
- expect(result.errors.join('\n')).toContain('Skill exceeds maximum word count of 6500');
4341
+ expect(result.errors.join('\n')).toContain('Skill exceeds maximum word count of 7000');
3732
4342
  });
3733
4343
 
3734
4344
  // ─── Files outside .assets in skill package root ────────────────────────────
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "xdrs-core",
3
- "version": "0.49.1",
3
+ "version": "0.50.0",
4
4
  "description": "XDRS (eXtensible Decision Record System): a framework to structure, compile and distribute Architectural (ADR), Business (BDR), and Engineering (EDR) decision records contents so that AI agents and humans can reliably find and use them with hierarchical scopes and controlled rollout in the format of distributable versioned packages.",
5
5
  "repository": {
6
6
  "type": "git",