Skip to content

Commit 139a128

Browse files
authored
feat(skills): Improve structure of any SKILL.md with a defined Workflow (#684)
* feat(skills): Improve structure of any SKILL.md with a defined Workflow * feat(skills): Simplify Skill generator
1 parent cf8966d commit 139a128

128 files changed

Lines changed: 1930 additions & 678 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

CONTRIBUTING.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ Read [AGENTS.md](./AGENTS.md) for the full contributor guide (tech stack, bounda
1212
The unified `skills-generator` module holds all XML sources and Java code used to build **agent skills** under `skills/`.
1313

1414
- [System prompt XML files](./skills-generator/src/main/resources/skill-references/) use the PML Schema ([pml.xsd](https://jabrena.github.io/pml/schemas/0.5.0/pml.xsd)). They are transformed with [SkillReferenceGenerator.java](./skills-generator/src/main/java/info/jab/pml/SkillReferenceGenerator.java) and [skill-reference-to-markdown.xsl](./skills-generator/src/main/resources/skill-reference-to-markdown.xsl) when producing reference content for skills.
15-
- [Skill summaries and inventory](./skills-generator/src/main/resources/) (`skill-indexes/`, `skill-indexes.xml`) drive `SKILL.md` generation.
15+
- [Skill summaries and inventory](./skills-generator/src/main/resources/) (`skill-indexes/`, `skills.xml`) drive `SKILL.md` generation.
1616

1717
If you have the idea to contribute, review the whole process in detail:
1818

@@ -22,7 +22,7 @@ If you have the idea to contribute, review the whole process in detail:
2222
npx skill-check skills # Validate generated skills
2323
```
2424

25-
Keep `skill-indexes.xml` aligned with `skill-indexes/` and `skill-references/` when adding or changing skills.
25+
Keep `skills.xml` aligned with `skill-indexes/` and `skill-references/` when adding or changing skills.
2626

2727
When you feel confident with the process, fork the repository and try to create new XML documents. Models will help you because an XML file is more rigid than natural language and it has `a common vocabulary` to create prompts.
2828

documentation/MAINTENANCE.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ Can you update the current changelog for 0.15.0 comparing git commits in relatio
5353
# Review Skill registries
5454
https://github.com/jabrena/cursor-rules-java
5555
https://tessl.io/registry/skills/submit
56+
npx tessl skill review ./skills/xxx
5657
cd target && npx skills add jabrena/cursor-rules-java --all --agent cursor && cd ..
5758
```
5859
@@ -86,5 +87,5 @@ git push --tags
8687
## Add a new Skills
8788
8889
```bash
89-
review if exist a new id in @skills-generator/src/main/resources/skill-indexes.xml to review compare with the content of @skills-generator/src/main/resources/skill-indexes and if exist add a new skill summary in @skills-generator/src/main/resources/skill-indexes . to elaborate the skill review the content of the id with @skills-generator/src/main/resources/skill-references when finish, validate generation with ./mvnw clean install -pl skills-generator and validate the skill with npx skill-check skills
90+
review if exist a new id in @skills-generator/src/main/resources/skills.xml to review compare with the content of @skills-generator/src/main/resources/skill-indexes and if exist add a new skill summary in @skills-generator/src/main/resources/skill-indexes. to elaborate the skill, review the `reference-list/reference` relation declared for that id in @skills-generator/src/main/resources/skills.xml. when finish, validate generation with ./mvnw clean install -pl skills-generator and validate the skill with npx skill-check skills
9091
```

skills-generator/pom.xml

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -71,11 +71,10 @@
7171
<resource>
7272
<directory>${project.basedir}/src/main/resources</directory>
7373
<includes>
74-
<include>skill-indexes.xml</include>
74+
<include>skills.xml</include>
7575
<include>skill-indexes/**</include>
7676
<include>skill-references/**</include>
7777
<include>skill-reference-to-markdown.xsl</include>
78-
<include>skill-references.xml</include>
7978
</includes>
8079
</resource>
8180
</resources>

skills-generator/src/main/java/info/jab/pml/SkillIndexes.java

Lines changed: 54 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,26 @@
11
package info.jab.pml;
22

3-
import java.io.IOException;
43
import java.io.InputStream;
5-
import java.net.JarURLConnection;
6-
import java.net.URISyntaxException;
7-
import java.net.URL;
8-
import java.nio.file.Files;
9-
import java.nio.file.Path;
10-
import java.nio.file.Paths;
114
import java.util.ArrayList;
125
import java.util.List;
13-
import java.util.jar.JarEntry;
14-
import java.util.jar.JarFile;
156
import java.util.stream.Stream;
167
import org.w3c.dom.Document;
178
import org.w3c.dom.Element;
189
import org.w3c.dom.NodeList;
1910

2011
/**
21-
* Inventory of skills to generate, loaded from {@code skill-indexes.xml}.
12+
* Inventory of skills to generate, loaded from {@code skills.xml}.
2213
* <p>
2314
* Each entry has an {@code id} (numeric or string like "010"). When {@code requiresSystemPrompt}
24-
* is true (default), the skillId is derived by matching skill-references with prefix {@code {id}-}.
25-
* When false, the entry must specify {@code skillId} and no skill-reference is required.
15+
* is true (default), the skillId is derived from the first item in {@code references/reference-list/reference}
16+
* unless {@code skillId} is explicitly set. When false, the entry must specify {@code skillId}
17+
* and no reference is required.
2618
* Each skill must have a summary in {@code skill-indexes/{id}-skill.md} or {@code skill-indexes/{id}-skill.xml}
2719
* when {@code xml="true"} on the entry.
2820
*/
2921
public final class SkillIndexes {
3022

31-
private static final String INVENTORY_RESOURCE = "skill-indexes.xml";
32-
private static final String SYSTEM_PROMPTS_PREFIX = "skill-references/";
33-
23+
private static final String INVENTORY_RESOURCE = "skills.xml";
3424
private SkillIndexes() {}
3525

3626
/**
@@ -49,7 +39,7 @@ public static Stream<String> skillIds() {
4939
String numericId = entry.numericId();
5040
validateSkillSummaryExists(numericId, entry.useXml());
5141
String skillId = entry.requiresSystemPrompt()
52-
? resolveSkillIdFromPrefix(numericId)
42+
? resolveSkillId(entry)
5343
: entry.skillId();
5444
skillIds.add(skillId);
5545
}
@@ -67,91 +57,38 @@ public static Stream<SkillDescriptor> skillDescriptors() {
6757
String numericId = entry.numericId();
6858
validateSkillSummaryExists(numericId, entry.useXml());
6959
String skillId = entry.requiresSystemPrompt()
70-
? resolveSkillIdFromPrefix(numericId)
60+
? resolveSkillId(entry)
7161
: entry.skillId();
72-
descriptors.add(new SkillDescriptor(skillId, entry.requiresSystemPrompt(), entry.useXml()));
62+
descriptors.add(new SkillDescriptor(skillId, entry.requiresSystemPrompt(), entry.useXml(), entry.references()));
7363
}
7464
return descriptors.stream();
7565
}
7666

7767
/**
7868
* Skill ID, whether it requires a system prompt for reference generation, and whether to use XML source.
7969
*/
80-
public record SkillDescriptor(String skillId, boolean requiresSystemPrompt, boolean useXml) {}
70+
public record SkillDescriptor(String skillId, boolean requiresSystemPrompt, boolean useXml, List<String> references) {}
8171

8272
/**
83-
* Resolves skillId by finding the skill-reference XML that starts with {@code {numericId}-}.
73+
* Resolves skillId from inventory entry.
8474
*
85-
* @param numericId numeric id from inventory (e.g. "111" or "014")
75+
* @param entry inventory entry
8676
* @return full skillId (e.g. 110-java-maven-best-practices)
87-
* @throws RuntimeException if none or multiple skill-references match
77+
* @throws RuntimeException if required references are missing
8878
*/
89-
public static String resolveSkillIdFromPrefix(String numericId) {
90-
String prefix = numericId + "-";
91-
List<String> matches = listSystemPromptBaseNames().stream()
92-
.filter(name -> name.startsWith(prefix) && name.endsWith(".xml"))
93-
.map(name -> name.substring(0, name.length() - 4))
94-
.toList();
95-
96-
if (matches.isEmpty()) {
97-
throw new RuntimeException("No skill-reference found for id " + numericId
98-
+ ". Add a skill-references/" + prefix + "*.xml file under skills-generator/src/main/resources/skill-references.");
99-
}
100-
if (matches.size() > 1) {
101-
throw new RuntimeException("Multiple skill-references match id " + numericId + ": " + matches);
102-
}
103-
return matches.getFirst();
104-
}
105-
106-
private static List<String> listSystemPromptBaseNames() {
107-
try {
108-
// Anchor on skill-reference-to-markdown.xsl to locate the JAR or exploded classes directory
109-
URL anchor = getResourceUrl("skill-reference-to-markdown.xsl");
110-
if (anchor == null) {
111-
throw new RuntimeException("skill-reference-to-markdown.xsl not found on classpath");
112-
}
113-
if ("jar".equals(anchor.getProtocol())) {
114-
JarURLConnection conn = (JarURLConnection) anchor.openConnection();
115-
try (JarFile jar = conn.getJarFile()) {
116-
return jar.stream()
117-
.map(JarEntry::getName)
118-
.filter(name -> name.startsWith(SYSTEM_PROMPTS_PREFIX) && name.endsWith(".xml"))
119-
.filter(name -> !name.contains("/assets/"))
120-
.map(name -> name.substring(SYSTEM_PROMPTS_PREFIX.length()))
121-
.toList();
122-
}
123-
}
124-
if ("file".equals(anchor.getProtocol())) {
125-
Path base = Paths.get(anchor.toURI()).getParent();
126-
Path systemPromptsDir = base.resolve("skill-references");
127-
if (!Files.isDirectory(systemPromptsDir)) {
128-
return List.of();
129-
}
130-
try (Stream<Path> files = Files.list(systemPromptsDir)) {
131-
return files
132-
.filter(p -> p.toString().endsWith(".xml"))
133-
.filter(p -> !p.toString().contains("assets"))
134-
.map(p -> p.getFileName().toString())
135-
.toList();
136-
}
137-
}
138-
return List.of();
139-
} catch (IOException | URISyntaxException e) {
140-
throw new RuntimeException("Failed to list skill-references", e);
79+
public static String resolveSkillId(InventoryEntry entry) {
80+
if (entry.skillId() != null && !entry.skillId().isBlank()) {
81+
return entry.skillId();
14182
}
142-
}
143-
144-
private static URL getResourceUrl(String name) {
145-
ClassLoader cl = SkillIndexes.class.getClassLoader();
146-
URL url = cl.getResource(name);
147-
if (url == null) {
148-
url = Thread.currentThread().getContextClassLoader().getResource(name);
83+
if (entry.references().isEmpty()) {
84+
throw new RuntimeException("No skill-reference found for id " + entry.numericId()
85+
+ ". Add at least one reference-list/reference entry in skills.xml.");
14986
}
150-
return url;
87+
return entry.references().getFirst();
15188
}
15289

15390
/**
154-
* Loads and parses skill-indexes.xml.
91+
* Loads and parses skills.xml.
15592
*/
15693
public static List<InventoryEntry> loadInventory() {
15794
try (InputStream stream = getResource(INVENTORY_RESOURCE)) {
@@ -197,7 +134,12 @@ private static List<InventoryEntry> parseInventory(InputStream in) {
197134
+ " has requiresSystemPrompt=false but no skillId specified.");
198135
}
199136
boolean useXml = parseXmlAttribute(skillEl);
200-
entries.add(new InventoryEntry(numericId, requiresSystemPrompt, skillId, useXml));
137+
List<String> references = parseReferences(skillEl);
138+
if (requiresSystemPrompt && references.isEmpty() && (skillId == null || skillId.isBlank())) {
139+
throw new RuntimeException("Entry with id " + numericId
140+
+ " requires a system prompt but has no reference-list/reference and no skillId.");
141+
}
142+
entries.add(new InventoryEntry(numericId, requiresSystemPrompt, skillId, useXml, references));
201143
}
202144
if (entries.isEmpty()) {
203145
throw new RuntimeException("Skill inventory must contain at least one <skill> entry");
@@ -226,6 +168,25 @@ private static boolean parseXmlAttribute(Element skillEl) {
226168
return "true".equals(s) || "yes".equals(s) || "1".equals(s);
227169
}
228170

171+
private static List<String> parseReferences(Element skillEl) {
172+
// Canonical format: <skill><reference-list><reference>...</reference></reference-list></skill>
173+
NodeList referenceLists = skillEl.getElementsByTagName("reference-list");
174+
NodeList refs = referenceLists.getLength() > 0
175+
? ((Element) referenceLists.item(0)).getElementsByTagName("reference")
176+
: skillEl.getElementsByTagName("reference"); // backward compatibility
177+
List<String> values = new ArrayList<>();
178+
for (int i = 0; i < refs.getLength(); i++) {
179+
if (!(refs.item(i) instanceof Element refEl)) {
180+
continue;
181+
}
182+
String value = refEl.getTextContent();
183+
if (value != null && !value.trim().isEmpty()) {
184+
values.add(value.trim());
185+
}
186+
}
187+
return values;
188+
}
189+
229190
private static void validateSkillSummaryExists(String numericId, boolean useXml) {
230191
String resourceName = useXml
231192
? "skill-indexes/" + numericId + "-skill.xml"
@@ -257,11 +218,17 @@ private static InputStream getResource(String name) {
257218
}
258219

259220
/**
260-
* Single entry from skill-indexes.xml. When requiresSystemPrompt is true,
261-
* skillId is derived by matching skill-references with prefix {@code {numericId}-}.
221+
* Single entry from skills.xml. When requiresSystemPrompt is true,
222+
* skillId is derived from the first reference entry when not explicitly provided.
262223
* When false, skillId must be provided and no skill-reference is required.
263224
* When useXml is true, skill summary is loaded from skill-indexes/{numericId}-skill.xml
264225
* and transformed via schema validation and XSLT; otherwise from skill-indexes/{numericId}-skill.md.
265226
*/
266-
public record InventoryEntry(String numericId, boolean requiresSystemPrompt, String skillId, boolean useXml) {}
227+
public record InventoryEntry(
228+
String numericId,
229+
boolean requiresSystemPrompt,
230+
String skillId,
231+
boolean useXml,
232+
List<String> references
233+
) {}
267234
}
Lines changed: 5 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -1,72 +1,24 @@
11
package info.jab.pml;
22

3-
import java.io.InputStream;
4-
import java.util.ArrayList;
5-
import java.util.List;
63
import java.util.stream.Stream;
7-
import org.w3c.dom.Document;
8-
import org.w3c.dom.Element;
9-
import org.w3c.dom.NodeList;
104

115
/**
12-
* Inventory of system prompts to generate, loaded from {@code skill-references.xml}.
6+
* Inventory of system prompts to generate, loaded from {@code skills.xml}.
137
* <p>
14-
* Each entry has a {@code name} attribute corresponding to the base name of the XML source file
15-
* (e.g. {@code 110-java-maven-best-practices} maps to {@code skill-references/110-java-maven-best-practices.xml}).
8+
* References are declared per skill under {@code reference-list/reference}.
169
*/
1710
public final class SkillReferences {
1811

19-
private static final String INVENTORY_RESOURCE = "skill-references.xml";
20-
2112
private SkillReferences() {
2213
}
2314

2415
public static Stream<String> baseNames() {
25-
return loadInventory().stream();
16+
return SkillIndexes.loadInventory().stream()
17+
.flatMap(entry -> entry.references().stream())
18+
.distinct();
2619
}
2720

2821
public static Stream<String> xmlFilenames() {
2922
return baseNames().map(name -> "skill-references/" + name + ".xml");
3023
}
31-
32-
private static List<String> loadInventory() {
33-
ClassLoader cl = SkillReferences.class.getClassLoader();
34-
try (InputStream stream = cl.getResourceAsStream(INVENTORY_RESOURCE)) {
35-
if (stream == null) {
36-
throw new RuntimeException("System prompt inventory not found: " + INVENTORY_RESOURCE);
37-
}
38-
return parseInventory(stream);
39-
} catch (RuntimeException e) {
40-
throw e;
41-
} catch (Exception e) {
42-
throw new RuntimeException("Failed to load system prompt inventory", e);
43-
}
44-
}
45-
46-
private static List<String> parseInventory(InputStream in) throws Exception {
47-
Document doc = InventoryXmlLoader.parse(in);
48-
Element root = doc.getDocumentElement();
49-
if (!"system-prompt-inventory".equals(root.getNodeName())) {
50-
throw new RuntimeException("System prompt inventory root must be <system-prompt-inventory>");
51-
}
52-
NodeList nodes = root.getElementsByTagName("prompt");
53-
List<String> names = new ArrayList<>();
54-
for (int i = 0; i < nodes.getLength(); i++) {
55-
if (!(nodes.item(i) instanceof Element el)) {
56-
continue;
57-
}
58-
if (el.getParentNode() != root) {
59-
continue;
60-
}
61-
String name = el.getAttribute("name");
62-
if (name == null || name.isBlank()) {
63-
throw new RuntimeException("system-prompt-inventory entry missing name attribute");
64-
}
65-
names.add(name.trim());
66-
}
67-
if (names.isEmpty()) {
68-
throw new RuntimeException("System prompt inventory must contain at least one <prompt> entry");
69-
}
70-
return names;
71-
}
7224
}

0 commit comments

Comments
 (0)