Skip to content

Commit 0ea3019

Browse files
committed
feat: improve stolt/skill-md integration
1 parent 1e3239e commit 0ea3019

6 files changed

Lines changed: 231 additions & 39 deletions

File tree

.github/workflows/test.yml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@ jobs:
1414
strategy:
1515
matrix:
1616
php:
17-
- "8.2"
1817
- "8.3"
1918
- "8.4"
2019
- "8.5"

CHANGELOG.md

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,12 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this p
77

88
## [Unreleased]
99

10+
## [v0.1.0] - 2026-05-16
11+
12+
### Improved
13+
14+
- Improved the integrating of the [stolt/skill-md](https://github.com/raphaelstolt/skill-md/) package.
15+
1016
## [v0.0.5] - 2026-05-13
1117

1218
### Added
@@ -35,8 +41,9 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this p
3541

3642
- Initial release.
3743

38-
[Unreleased]: https://github.com/raphaelstolt/skill-validator/compare/v0.0.5...HEAD
44+
[Unreleased]: https://github.com/raphaelstolt/skill-validator/compare/v0.1.0...HEAD
3945

46+
[v0.1.0]: https://github.com/raphaelstolt/skill-validator/compare/v0.0.5...v0.1.0
4047
[v0.0.5]: https://github.com/raphaelstolt/skill-validator/compare/v0.0.4...v0.0.5
4148
[v0.0.4]: https://github.com/raphaelstolt/skill-validator/compare/v0.0.3...v0.0.4
4249
[v0.0.3]: https://github.com/raphaelstolt/skill-validator/compare/v0.0.2...v0.0.3

README.md

Lines changed: 55 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,11 @@ composer require stolt/skill-validator
1919

2020
## Usage
2121

22-
The validator can validate either existing `SKILL.md` files or raw `SKILL.md` content.
22+
The `SkillMd` class from the [stolt/skill-md](https://github.com/raphaelstolt/skill-md/) package is the primary
23+
abstraction for a validated skill: every valid result exposes a `SkillMd` instance, and the validator also accepts
24+
`SkillMd` instances directly as input.
25+
26+
The validator can validate existing `SKILL.md` files, raw `SKILL.md` content, or `SkillMd` instances.
2327

2428
### Validating a `SKILL.md` file
2529

@@ -60,14 +64,31 @@ $validator = new Validator();
6064
$result = $validator->validateContent('raw-skill-content');
6165
```
6266

67+
### Validating a `SkillMd` instance
68+
69+
```php
70+
use Stolt\Ai\Skill\Validator;
71+
use Stolt\Ai\SkillMd;
72+
73+
$skillMd = SkillMd::create(
74+
'code-review',
75+
'Review code changes and provide actionable feedback.',
76+
"# Code review\n\nReview the changed files and report issues.",
77+
['tags' => ['php', 'review'], 'version' => '1.0.0']
78+
);
79+
80+
$validator = new Validator();
81+
$result = $validator->validateSkillMd($skillMd);
82+
```
83+
6384
> [!TIP]
64-
> The `validate` alias method accepts either a file path, directory path, or raw content and delegates to the
65-
> appropriate method automatically.
85+
> The `validate` alias method accepts a file path, directory path, raw content, or a `SkillMd` instance and delegates
86+
> to the appropriate method automatically.
6687
6788
### Accessing validation results and metadata
6889

69-
Validation returns a `Stolt\Ai\Skill\ValidationResult` object. When the `SKILL.md` content contains the required `name`
70-
and `description` fields, the parsed metadata is exposed as a `Stolt\Ai\Skill\Metadata` object.
90+
Validation returns a `Stolt\Ai\Skill\ValidationResult` object. When the `SKILL.md` content is valid, a `SkillMd`
91+
instance is available directly. The parsed metadata is also accessible as a `Stolt\Ai\Skill\Metadata` object.
7192

7293
```php
7394
use Stolt\Ai\Skill\Validator;
@@ -78,28 +99,30 @@ $result = $validator->validateContent('raw-skill-content');
7899
if ($result->isInvalid()) {
79100
foreach ($result->errors() as $error) {
80101
echo $error . PHP_EOL;
81-
}
102+
}
82103
// Raw metadata can still be inspected when parsing succeeded but validation failed.
83104
$rawMetadata = $result->rawMetadata();
84105
exit(1);
85-
}
106+
}
86107

87-
$metadata = $result->metadata();
108+
// Primary SkillMd abstraction — available on every valid result.
109+
$skillMd = $result->skillMd(); // returns ?SkillMd (null when invalid)
88110

89-
if ($metadata === null) {
90-
throw new RuntimeException('Expected validated SKILL.md metadata.');
91-
}
111+
// Or assert the SkillMd directly, which throws a LogicException when the result is invalid.
112+
$skillMd = $result->toSkillMd();
92113

93-
// Required SKILL.md metadata fields.
94-
$name = $metadata->name();
95-
$description = $metadata->description();
114+
// Use the SkillMd instance.
115+
$name = $skillMd->name();
116+
$description = $skillMd->description();
117+
$body = $skillMd->body();
118+
$tags = $skillMd->tags();
119+
$version = $skillMd->version();
96120

97-
// Optional SKILL.md metadata fields.
98-
$version = $metadata->version();
99-
$tags = $metadata->tags();
100-
$allowedTools = $metadata->get('allowed-tools', []);
101-
$model = $metadata->get('model');
102-
$effort = $metadata->get('effort');
121+
// Metadata object for field access with defaults.
122+
$metadata = $result->metadata();
123+
$allowedTools = $metadata?->get('allowed-tools', []);
124+
$model = $metadata?->get('model');
125+
$effort = $metadata?->get('effort');
103126

104127
// Markdown instructions after the YAML frontmatter.
105128
$instructions = $result->body();
@@ -110,20 +133,28 @@ $arrayResult = $result->toArray();
110133
echo sprintf('Skill "%s" is valid: %s', $name, $description) . PHP_EOL;
111134
```
112135

113-
For an actual integration, the project [list-skills-command](https://github.com/raphaelstolt/list-skills-command) can also be consolidated.
136+
### Round-tripping between content and `SkillMd`
114137

115-
> [!TIP]
116-
> As of version `0.0.5` you can use the `toSkillMd()` method to collect a populated `SkillMd` instance of the [stolt/skill-md](https://github.com/raphaelstolt/skill-md/) package.
138+
Because `validateSkillMd()` accepts a `SkillMd` instance and `toSkillMd()` returns one, validation results and
139+
`SkillMd` objects round-trip cleanly:
117140

118141
```php
119142
use Stolt\Ai\Skill\Validator;
120143

121144
$validator = new Validator();
122-
$result = $validator->validateContent('raw-skill-content');
123145

146+
// Parse and validate raw content.
147+
$result = $validator->validateContent($rawContent);
148+
149+
// Obtain the primary SkillMd abstraction.
124150
$skillMd = $result->toSkillMd();
151+
152+
// Re-validate the SkillMd — e.g. after modifying it.
153+
$revalidated = $validator->validateSkillMd($skillMd);
125154
```
126155

156+
For an actual integration, the project [list-skills-command](https://github.com/raphaelstolt/list-skills-command) can also be consolidated.
157+
127158
## Validation rules
128159

129160
The validator checks that a `SKILL.md` document:

src/ValidationResult.php

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ private function __construct(
1818
private ?Metadata $metadata,
1919
private array $rawMetadata,
2020
private string $body,
21+
private ?SkillMd $skillMd = null,
2122
) {
2223
}
2324

@@ -28,7 +29,13 @@ public static function valid(Metadata $metadata, string $body): self
2829
[],
2930
$metadata,
3031
$metadata->toArray(),
31-
$body
32+
$body,
33+
SkillMd::create(
34+
$metadata->name(),
35+
$metadata->description(),
36+
$body,
37+
$metadata->additionalFields()
38+
)
3239
);
3340
}
3441

@@ -105,14 +112,20 @@ public function body(): string
105112
return $this->body;
106113
}
107114

115+
public function skillMd(): ?SkillMd
116+
{
117+
return $this->skillMd;
118+
}
119+
108120
public function toSkillMd(): SkillMd
109121
{
110-
return SkillMd::create(
111-
$this->metadata?->get('name'),
112-
$this->metadata?->get('description'),
113-
$this->body(),
114-
$this->metadata?->toArray() ?? []
115-
);
122+
if ($this->skillMd === null) {
123+
throw new \LogicException(
124+
'Cannot convert an invalid ValidationResult to a SkillMd instance.'
125+
);
126+
}
127+
128+
return $this->skillMd;
116129
}
117130

118131
/**

src/Validator.php

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44

55
namespace Stolt\Ai\Skill;
66

7+
use Stolt\Ai\SkillMd;
8+
79
final class Validator
810
{
911
private const MAX_NAME_LENGTH = 64;
@@ -75,8 +77,12 @@ public function validateFromDirectory(string $directory): array
7577
/**
7678
* @return ValidationResult|array<string, ValidationResult>
7779
*/
78-
public function validate(string $input): ValidationResult|array
80+
public function validate(string|SkillMd $input): ValidationResult|array
7981
{
82+
if ($input instanceof SkillMd) {
83+
return $this->validateSkillMd($input);
84+
}
85+
8086
if (\is_dir($input)) {
8187
return $this->validateFromDirectory($input);
8288
}
@@ -88,6 +94,11 @@ public function validate(string $input): ValidationResult|array
8894
return $this->validateContent($input);
8995
}
9096

97+
public function validateSkillMd(SkillMd $skillMd): ValidationResult
98+
{
99+
return $this->validateContent($skillMd->toMarkdown());
100+
}
101+
91102
public function validateFile(string $skillFile): ValidationResult
92103
{
93104
if (\is_file($skillFile) === false) {

0 commit comments

Comments
 (0)