Skip to content

Commit fac8020

Browse files
committed
refactor(skills-generator): migrate inventory files from JSON to XML
Made-with: Cursor
1 parent 79fe95d commit fac8020

File tree

15 files changed

+276
-217
lines changed

15 files changed

+276
-217
lines changed

CHANGELOG.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1010
### Added
1111

1212
- **Skills**:
13-
- INVEST validation added to agile user-story workflow in `@014-agile-user-story` (#633)
13+
- `@001-skills-inventory` checklist skill that emits `SKILLS-JAVA.md` in the project root using the embedded system-prompts template
14+
- `@002-agents-inventory` checklist skill that emits `AGENTS-JAVA.md` with the embedded agents table and installation targets
15+
- `@003-agents-installation` interactive installer that copies the six embedded robot agents into `.cursor/agents` or `.claude/agents`
16+
- Add INVEST validation to agile user-story workflow in `@014-agile-user-story` (#633)
1417
- Maven Central search guidance skill (`@114-java-maven-search`) (#605)
1518
- OpenSpec adoption for project change management in `@042-planning-openspec` (#620, #621, #616)
1619
- GitHub issue management workflow support`@043-planning-github` (#607)

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/system-prompts/) use the PML Schema ([pml.xsd](https://jabrena.github.io/pml/schemas/0.5.0/pml.xsd)). They are transformed with [CursorRulesGenerator.java](./skills-generator/src/main/java/info/jab/pml/CursorRulesGenerator.java) and [system-prompts.xsl](./skills-generator/src/main/resources/system-prompts.xsl) when producing reference content for skills.
15-
- [Skill summaries and inventory](./skills-generator/src/main/resources/) (`skills/`, `skill-inventory.json`) drive `SKILL.md` generation.
15+
- [Skill summaries and inventory](./skills-generator/src/main/resources/) (`skills/`, `skill-inventory.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-inventory.json` aligned with `skills/` and `system-prompts/` when adding or changing skills.
25+
Keep `skill-inventory.xml` aligned with `skills/` and `system-prompts/` 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: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,5 +64,5 @@ git push --tags
6464
## Add a new Skills
6565
6666
```bash
67-
review if exist a new id in @skills-generator/src/main/resources/skill-inventory.json to review compare with the content of @skills-generator/src/main/resources/skills and if exist add a new skill summary in @skills-generator/src/main/resources/skills . to elaborate the skill review the content of the id with @skills-generator/src/main/resources/system-prompts when finish, validate generation with ./mvnw clean install -pl skills-generator and validate the skill with npx skill-check skills
67+
review if exist a new id in @skills-generator/src/main/resources/skill-inventory.xml to review compare with the content of @skills-generator/src/main/resources/skills and if exist add a new skill summary in @skills-generator/src/main/resources/skills . to elaborate the skill review the content of the id with @skills-generator/src/main/resources/system-prompts when finish, validate generation with ./mvnw clean install -pl skills-generator and validate the skill with npx skill-check skills
6868
```

documentation/openspec/specs/technologies-openapi/spec.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ The new skill SHALL complement, not duplicate, framework REST skills: framework
4040

4141
### Requirement: Generator Integration
4242

43-
New sources for **701** MUST be registered in both `skill-inventory.json` and `system-prompt-inventory.json`, and the `skills-generator` module MUST build successfully after the change.
43+
New sources for **701** MUST be registered in both `skill-inventory.xml` and `system-prompt-inventory.xml`, and the `skills-generator` module MUST build successfully after the change.
4444

4545
#### Scenario: Maven verify passes
4646

documentation/openspec/specs/technologies-wiremock/spec.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ The new skill SHALL complement, not replace, framework integration-test skills:
4040

4141
### Requirement: Generator Integration
4242

43-
New sources for **702** MUST be registered in both `skill-inventory.json` and `system-prompt-inventory.json`, and the `skills-generator` module MUST build successfully after the change.
43+
New sources for **702** MUST be registered in both `skill-inventory.xml` and `system-prompt-inventory.xml`, and the `skills-generator` module MUST build successfully after the change.
4444

4545
#### Scenario: Maven verify passes
4646

skills-generator/pom.xml

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,6 @@
1515
<packaging>jar</packaging>
1616

1717
<dependencies>
18-
<!-- JSON parsing for skill-inventory.json -->
19-
<dependency>
20-
<groupId>com.fasterxml.jackson.core</groupId>
21-
<artifactId>jackson-databind</artifactId>
22-
</dependency>
23-
2418
<!-- Logging dependencies -->
2519
<dependency>
2620
<groupId>org.slf4j</groupId>
@@ -77,11 +71,11 @@
7771
<resource>
7872
<directory>${project.basedir}/src/main/resources</directory>
7973
<includes>
80-
<include>skill-inventory.json</include>
74+
<include>skill-inventory.xml</include>
8175
<include>skills/**</include>
8276
<include>system-prompts/**</include>
8377
<include>system-prompts.xsl</include>
84-
<include>system-prompt-inventory.json</include>
78+
<include>system-prompt-inventory.xml</include>
8579
</includes>
8680
</resource>
8781
</resources>
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
package info.jab.pml;
2+
3+
import java.io.InputStream;
4+
import javax.xml.XMLConstants;
5+
import javax.xml.parsers.DocumentBuilder;
6+
import javax.xml.parsers.DocumentBuilderFactory;
7+
import javax.xml.parsers.ParserConfigurationException;
8+
import org.w3c.dom.Document;
9+
10+
/** Loads small local inventory documents with safe parser settings. */
11+
final class InventoryXmlLoader {
12+
13+
private InventoryXmlLoader() {}
14+
15+
static Document parse(InputStream in) throws Exception {
16+
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
17+
dbf.setNamespaceAware(false);
18+
dbf.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);
19+
try {
20+
dbf.setAttribute(XMLConstants.ACCESS_EXTERNAL_DTD, "");
21+
dbf.setAttribute(XMLConstants.ACCESS_EXTERNAL_SCHEMA, "");
22+
} catch (IllegalArgumentException ignored) {
23+
// not all parsers support these attributes
24+
}
25+
try {
26+
dbf.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
27+
} catch (ParserConfigurationException ignored) {
28+
// optional hardening
29+
}
30+
DocumentBuilder builder = dbf.newDocumentBuilder();
31+
return builder.parse(in);
32+
}
33+
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
* <p>
2020
* Reuses CursorRulesGenerator for full rule content. SKILL.md is sourced from
2121
* {@code skills/{numericId}-skill.md} (user-editable), where numericId is extracted from skillId (e.g. 110 from 110-java-maven-best-practices).
22-
* The list of skills to generate is defined in {@code skill-inventory.json}; each must have a
22+
* The list of skills to generate is defined in {@code skill-inventory.xml}; each must have a
2323
* matching skill summary in {@code skills/} and a matching system-prompt in {@code system-prompts/}.
2424
*/
2525
public final class SkillsGenerator {

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

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

3-
import com.fasterxml.jackson.databind.JsonNode;
4-
import com.fasterxml.jackson.databind.ObjectMapper;
53
import java.io.IOException;
64
import java.io.InputStream;
75
import java.net.JarURLConnection;
86
import java.net.URISyntaxException;
97
import java.net.URL;
10-
import java.nio.charset.StandardCharsets;
118
import java.nio.file.Files;
129
import java.nio.file.Path;
1310
import java.nio.file.Paths;
@@ -16,18 +13,22 @@
1613
import java.util.jar.JarEntry;
1714
import java.util.jar.JarFile;
1815
import java.util.stream.Stream;
16+
import org.w3c.dom.Document;
17+
import org.w3c.dom.Element;
18+
import org.w3c.dom.NodeList;
1919

2020
/**
21-
* Inventory of skills to generate, loaded from {@code skill-inventory.json}.
21+
* Inventory of skills to generate, loaded from {@code skill-inventory.xml}.
2222
* <p>
2323
* Each entry has an {@code id} (numeric or string like "010"). When {@code requiresSystemPrompt}
2424
* is true (default), the skillId is derived by matching system-prompts with prefix {@code {id}-}.
2525
* When false, the entry must specify {@code skillId} and no system-prompt is required.
26-
* Each skill must have a summary in {@code skills/{id}-skill.md}.
26+
* Each skill must have a summary in {@code skills/{id}-skill.md} or {@code skills/{id}-skill.xml}
27+
* when {@code xml="true"} on the entry.
2728
*/
2829
public final class SkillsInventory {
2930

30-
private static final String INVENTORY_RESOURCE = "skill-inventory.json";
31+
private static final String INVENTORY_RESOURCE = "skill-inventory.xml";
3132
private static final String SYSTEM_PROMPTS_PREFIX = "system-prompts/";
3233

3334
private SkillsInventory() {}
@@ -150,64 +151,79 @@ private static URL getResourceUrl(String name) {
150151
}
151152

152153
/**
153-
* Loads and parses skill-inventory.json.
154+
* Loads and parses skill-inventory.xml.
154155
*/
155156
public static List<InventoryEntry> loadInventory() {
156157
try (InputStream stream = getResource(INVENTORY_RESOURCE)) {
157158
if (stream == null) {
158159
throw new RuntimeException("Skill inventory not found: " + INVENTORY_RESOURCE);
159160
}
160-
String json = new String(stream.readAllBytes(), StandardCharsets.UTF_8);
161-
return parseInventory(json);
161+
return parseInventory(stream);
162162
} catch (Exception e) {
163163
throw new RuntimeException("Failed to load skill inventory", e);
164164
}
165165
}
166166

167-
private static List<InventoryEntry> parseInventory(String json) {
167+
private static List<InventoryEntry> parseInventory(InputStream in) {
168168
try {
169-
ObjectMapper mapper = new ObjectMapper();
170-
JsonNode root = mapper.readTree(json);
171-
if (!root.isArray()) {
172-
throw new RuntimeException("Skill inventory must be a JSON array");
169+
Document doc = InventoryXmlLoader.parse(in);
170+
Element root = doc.getDocumentElement();
171+
if (!"skill-inventory".equals(root.getNodeName())) {
172+
throw new RuntimeException("Skill inventory root must be <skill-inventory>");
173173
}
174-
174+
NodeList skillNodes = root.getElementsByTagName("skill");
175175
List<InventoryEntry> entries = new ArrayList<>();
176-
for (JsonNode node : root) {
177-
String numericId = node.get("id").isTextual()
178-
? node.get("id").asText()
179-
: String.valueOf(node.get("id").asInt());
180-
boolean requiresSystemPrompt = node.has("requiresSystemPrompt")
181-
? node.get("requiresSystemPrompt").asBoolean()
182-
: true;
183-
String skillId = node.has("skillId") ? node.get("skillId").asText() : null;
176+
for (int i = 0; i < skillNodes.getLength(); i++) {
177+
if (!(skillNodes.item(i) instanceof Element skillEl)) {
178+
continue;
179+
}
180+
if (skillEl.getParentNode() != root) {
181+
continue;
182+
}
183+
String numericId = skillEl.getAttribute("id");
184+
if (numericId == null || numericId.isBlank()) {
185+
throw new RuntimeException("skill-inventory entry missing id attribute");
186+
}
187+
boolean requiresSystemPrompt = parseBooleanAttribute(skillEl, "requiresSystemPrompt", true);
188+
String skillId = skillEl.hasAttribute("skillId")
189+
? skillEl.getAttribute("skillId").trim()
190+
: null;
191+
if (skillId != null && skillId.isEmpty()) {
192+
skillId = null;
193+
}
184194

185195
if (!requiresSystemPrompt && (skillId == null || skillId.isBlank())) {
186196
throw new RuntimeException("Entry with id " + numericId
187197
+ " has requiresSystemPrompt=false but no skillId specified.");
188198
}
189-
boolean useXml = parseXmlFlag(node);
199+
boolean useXml = parseXmlAttribute(skillEl);
190200
entries.add(new InventoryEntry(numericId, requiresSystemPrompt, skillId, useXml));
191201
}
202+
if (entries.isEmpty()) {
203+
throw new RuntimeException("Skill inventory must contain at least one <skill> entry");
204+
}
192205
return entries;
206+
} catch (RuntimeException e) {
207+
throw e;
193208
} catch (Exception e) {
194209
throw new RuntimeException("Failed to parse skill inventory", e);
195210
}
196211
}
197212

198-
private static boolean parseXmlFlag(JsonNode node) {
199-
if (!node.has("xml")) {
200-
return false;
201-
}
202-
JsonNode xmlNode = node.get("xml");
203-
if (xmlNode.isBoolean()) {
204-
return xmlNode.asBoolean();
213+
private static boolean parseBooleanAttribute(Element el, String name, boolean defaultValue) {
214+
if (!el.hasAttribute(name)) {
215+
return defaultValue;
205216
}
206-
if (xmlNode.isTextual()) {
207-
String s = xmlNode.asText().toLowerCase();
208-
return "true".equals(s) || "yes".equals(s) || "1".equals(s);
217+
String v = el.getAttribute(name).trim().toLowerCase();
218+
return "true".equals(v) || "yes".equals(v) || "1".equals(v);
219+
}
220+
221+
private static boolean parseXmlAttribute(Element skillEl) {
222+
if (!skillEl.hasAttribute("xml")) {
223+
return false;
209224
}
210-
return false;
225+
String s = skillEl.getAttribute("xml").trim().toLowerCase();
226+
return "true".equals(s) || "yes".equals(s) || "1".equals(s);
211227
}
212228

213229
private static void validateSkillSummaryExists(String numericId, boolean useXml) {
@@ -241,7 +257,7 @@ private static InputStream getResource(String name) {
241257
}
242258

243259
/**
244-
* Single entry from skill-inventory.json. When requiresSystemPrompt is true,
260+
* Single entry from skill-inventory.xml. When requiresSystemPrompt is true,
245261
* skillId is derived by matching system-prompts with prefix {@code {numericId}-}.
246262
* When false, skillId must be provided and no system-prompt is required.
247263
* When useXml is true, skill summary is loaded from skills/{numericId}-skill.xml

skills-generator/src/main/resources/skill-inventory.json

Lines changed: 0 additions & 70 deletions
This file was deleted.

0 commit comments

Comments
 (0)