Skip to content

Commit cfe908a

Browse files
committed
Make the manifest contract executable instead of advisory
The thin manifest was still carrying path and alias metadata that the workflow was not enforcing. This change adds a manifest-structure check so CI now proves that each manifest entry points at a real skill directory, matches the declared SKILL.md frontmatter name, and that deprecated aliases resolve to canonical replacement targets. Constraint: Keep verification lightweight and inline without reintroducing local scripts Rejected: Leave path metadata unverified | keeps the manifest thinner in theory but weaker in practice Confidence: high Scope-risk: narrow Reversibility: clean Directive: If new manifest fields are added later, either validate them in CI or remove them Tested: Local inline manifest verification script; local `npx skills add . --list --full-depth` Not-tested: GitHub Actions execution after push
1 parent 2560b23 commit cfe908a

1 file changed

Lines changed: 89 additions & 0 deletions

File tree

.github/workflows/verify-pack.yml

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,95 @@ jobs:
1717
with:
1818
node-version: "20"
1919

20+
- name: Verify manifest structure and skill paths
21+
run: |
22+
node -e '
23+
const fs = require("fs");
24+
const path = require("path");
25+
26+
const fail = (message) => {
27+
console.error(message);
28+
process.exit(1);
29+
};
30+
31+
const manifest = JSON.parse(fs.readFileSync("skills-pack.json", "utf8"));
32+
if (!manifest.name || typeof manifest.name !== "string") {
33+
fail("Manifest is missing a valid pack name.");
34+
}
35+
if (!manifest.default_install || typeof manifest.default_install !== "string") {
36+
fail("Manifest is missing a valid default_install command.");
37+
}
38+
if (!Array.isArray(manifest.skills) || manifest.skills.length === 0) {
39+
fail("Manifest must define at least one canonical skill.");
40+
}
41+
42+
const canonicalNames = new Set(manifest.skills.map((skill) => skill.name));
43+
const seenNames = new Set();
44+
45+
const readFrontmatterName = (skillFile) => {
46+
const content = fs.readFileSync(skillFile, "utf8");
47+
const frontmatter = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
48+
if (!frontmatter) {
49+
fail(`Missing frontmatter in ${skillFile}`);
50+
}
51+
const nameMatch = frontmatter[1].match(/^name:\s*(.+)$/m);
52+
if (!nameMatch) {
53+
fail(`Missing frontmatter name in ${skillFile}`);
54+
}
55+
return nameMatch[1].trim().replace(/^["'']|["'']$/g, "");
56+
};
57+
58+
for (const skill of manifest.skills) {
59+
if (!skill.name || !skill.path) {
60+
fail(`Canonical skill entry is missing required fields: ${JSON.stringify(skill)}`);
61+
}
62+
if (seenNames.has(skill.name)) {
63+
fail(`Duplicate manifest entry for skill: ${skill.name}`);
64+
}
65+
seenNames.add(skill.name);
66+
const skillPath = path.resolve(skill.path);
67+
const skillFile = path.join(skillPath, "SKILL.md");
68+
if (!fs.existsSync(skillPath) || !fs.statSync(skillPath).isDirectory()) {
69+
fail(`Canonical skill path does not exist: ${skill.path}`);
70+
}
71+
if (!fs.existsSync(skillFile)) {
72+
fail(`Canonical skill is missing SKILL.md: ${skill.path}`);
73+
}
74+
const frontmatterName = readFrontmatterName(skillFile);
75+
if (frontmatterName !== skill.name) {
76+
fail(`Manifest name/path mismatch for ${skill.name}; SKILL.md declares ${frontmatterName}`);
77+
}
78+
}
79+
80+
for (const alias of manifest.aliases ?? []) {
81+
if (!alias.name || !alias.path || !alias.replaced_by) {
82+
fail(`Alias entry is missing required fields: ${JSON.stringify(alias)}`);
83+
}
84+
if (seenNames.has(alias.name)) {
85+
fail(`Duplicate manifest entry for alias: ${alias.name}`);
86+
}
87+
if (!alias.deprecated) {
88+
fail(`Alias ${alias.name} must be marked deprecated.`);
89+
}
90+
if (!canonicalNames.has(alias.replaced_by)) {
91+
fail(`Alias ${alias.name} points to missing replacement target: ${alias.replaced_by}`);
92+
}
93+
seenNames.add(alias.name);
94+
const aliasPath = path.resolve(alias.path);
95+
const aliasFile = path.join(aliasPath, "SKILL.md");
96+
if (!fs.existsSync(aliasPath) || !fs.statSync(aliasPath).isDirectory()) {
97+
fail(`Alias path does not exist: ${alias.path}`);
98+
}
99+
if (!fs.existsSync(aliasFile)) {
100+
fail(`Alias is missing SKILL.md: ${alias.path}`);
101+
}
102+
const frontmatterName = readFrontmatterName(aliasFile);
103+
if (frontmatterName !== alias.name) {
104+
fail(`Manifest alias/path mismatch for ${alias.name}; SKILL.md declares ${frontmatterName}`);
105+
}
106+
}
107+
'
108+
20109
- name: Verify discovery output matches manifest
21110
run: |
22111
OUTPUT="$(npx skills add . --list --full-depth)"

0 commit comments

Comments
 (0)