Skip to content

Commit bda2259

Browse files
authored
Merge pull request #47 from LiTeXz/codex/skill-metadata-check-release-0.2.15
feat: add skill metadata checks and release 0.2.15
2 parents 19d3701 + 4da08ea commit bda2259

30 files changed

Lines changed: 358 additions & 33 deletions

File tree

.codex-plugin/plugin.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "devflow-skills",
3-
"version": "0.2.14",
3+
"version": "0.2.15",
44
"description": "DevFlow engineering workflow skills for Codex: routing, DDD, TDD, planning, execution, debugging, review, verification, and branch finishing.",
55
"composerIcon": "./assets/app-icon-small.svg",
66
"logo": "./assets/app-icon.png",
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
name: Skill Metadata Check
2+
3+
on:
4+
push:
5+
branches: [main]
6+
pull_request:
7+
branches: [main]
8+
9+
jobs:
10+
skill-metadata-check:
11+
runs-on: ubuntu-latest
12+
steps:
13+
- uses: actions/checkout@v7
14+
- uses: oven-sh/setup-bun@v2.2.0
15+
with:
16+
bun-version: latest
17+
- name: Check skill folder and front matter names
18+
run: bun run check:skill-metadata

agents/df-publisher.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# devflow-version = "0.2.14"
1+
# devflow-version = "0.2.15"
22
name = "df-publisher"
33
description = "唯一被授权执行 git 和 gh 命令的 Codex worker session。负责提交、推送、PR、合并、发布等全部 git/github 操作。遵守保护分支规则。"
44
nickname_candidates = ["df-publisher", "publisher"]

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
{
22
"name": "devflow-skills",
3-
"version": "0.2.14",
3+
"version": "0.2.15",
44
"type": "module",
55
"private": true,
66
"scripts": {
7+
"check:skill-metadata": "bun scripts/check-skill-metadata.ts",
78
"test": "bun test",
89
"test:hooks": "bun test scripts/",
910
"test:validators": "bun test skills/df-tdd-skill/scripts/ skills/df-ddd-event-storming-design/scripts/",
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
import { describe, expect, it } from "bun:test";
2+
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
3+
import { tmpdir } from "node:os";
4+
import { join } from "node:path";
5+
import { validateSkillMetadata } from "./check-skill-metadata";
6+
7+
function withSkillsRoot(run: (skillsRoot: string) => void): void {
8+
const root = mkdtempSync(join(tmpdir(), "skill-metadata-"));
9+
try {
10+
const skillsRoot = join(root, "skills");
11+
mkdirSync(skillsRoot);
12+
run(skillsRoot);
13+
} finally {
14+
rmSync(root, { recursive: true, force: true });
15+
}
16+
}
17+
18+
function writeSkill(
19+
skillsRoot: string,
20+
folderName: string,
21+
frontMatterName: string,
22+
description = "Test skill",
23+
) {
24+
const skillDir = join(skillsRoot, folderName);
25+
mkdirSync(skillDir, { recursive: true });
26+
writeFileSync(
27+
join(skillDir, "SKILL.md"),
28+
`---\nname: ${frontMatterName}\ndescription: ${description}\n---\n\n# Test\n`,
29+
"utf-8",
30+
);
31+
}
32+
33+
function writeOpenAiAgent(
34+
skillsRoot: string,
35+
folderName: string,
36+
displayName: string,
37+
shortDescription = "Test skill",
38+
) {
39+
const agentDir = join(skillsRoot, folderName, "agents");
40+
mkdirSync(agentDir, { recursive: true });
41+
writeFileSync(
42+
join(agentDir, "openai.yaml"),
43+
`interface:\n display_name: "${displayName}"\n short_description: "${shortDescription}"\n default_prompt: "Use this test skill."\n`,
44+
"utf-8",
45+
);
46+
}
47+
48+
describe("skill metadata validation", () => {
49+
it("accepts df-prefixed skill folders whose front matter name matches", () => {
50+
withSkillsRoot((skillsRoot) => {
51+
writeSkill(skillsRoot, "df-example-skill", "df-example-skill");
52+
writeOpenAiAgent(skillsRoot, "df-example-skill", "Example Skill");
53+
54+
expect(validateSkillMetadata(skillsRoot)).toEqual([]);
55+
});
56+
});
57+
58+
it("rejects SKILL.md front matter names that do not match the folder", () => {
59+
withSkillsRoot((skillsRoot) => {
60+
writeSkill(skillsRoot, "df-example-skill", "different-name");
61+
writeOpenAiAgent(skillsRoot, "df-example-skill", "Example Skill");
62+
63+
expect(validateSkillMetadata(skillsRoot)).toEqual([
64+
{
65+
path: join(skillsRoot, "df-example-skill", "SKILL.md"),
66+
message:
67+
'front matter name "different-name" must match folder "df-example-skill"',
68+
},
69+
]);
70+
});
71+
});
72+
73+
it("rejects skill folders that do not start with df-", () => {
74+
withSkillsRoot((skillsRoot) => {
75+
writeSkill(skillsRoot, "example-skill", "example-skill");
76+
writeOpenAiAgent(skillsRoot, "example-skill", "Example Skill");
77+
78+
expect(validateSkillMetadata(skillsRoot)).toEqual([
79+
{
80+
path: join(skillsRoot, "example-skill", "SKILL.md"),
81+
message: 'skill folder "example-skill" must start with "df-"',
82+
},
83+
]);
84+
});
85+
});
86+
87+
it("rejects skills without agents/openai.yaml", () => {
88+
withSkillsRoot((skillsRoot) => {
89+
writeSkill(skillsRoot, "df-example-skill", "df-example-skill");
90+
91+
expect(validateSkillMetadata(skillsRoot)).toEqual([
92+
{
93+
path: join(skillsRoot, "df-example-skill", "agents", "openai.yaml"),
94+
message: "agents/openai.yaml is required",
95+
},
96+
]);
97+
});
98+
});
99+
100+
it("rejects display names that are not derived from the folder name", () => {
101+
withSkillsRoot((skillsRoot) => {
102+
writeSkill(skillsRoot, "df-example-skill", "df-example-skill");
103+
writeOpenAiAgent(skillsRoot, "df-example-skill", "Df Example Skill");
104+
105+
expect(validateSkillMetadata(skillsRoot)).toEqual([
106+
{
107+
path: join(skillsRoot, "df-example-skill", "agents", "openai.yaml"),
108+
message:
109+
'interface.display_name "Df Example Skill" must be "Example Skill"',
110+
},
111+
]);
112+
});
113+
});
114+
115+
it("rejects short descriptions that do not match SKILL.md descriptions", () => {
116+
withSkillsRoot((skillsRoot) => {
117+
writeSkill(
118+
skillsRoot,
119+
"df-example-skill",
120+
"df-example-skill",
121+
"Full SKILL description",
122+
);
123+
writeOpenAiAgent(
124+
skillsRoot,
125+
"df-example-skill",
126+
"Example Skill",
127+
"Short UI description",
128+
);
129+
130+
expect(validateSkillMetadata(skillsRoot)).toEqual([
131+
{
132+
path: join(skillsRoot, "df-example-skill", "agents", "openai.yaml"),
133+
message:
134+
'interface.short_description must exactly match SKILL.md description "Full SKILL description"',
135+
},
136+
]);
137+
});
138+
});
139+
});

scripts/check-skill-metadata.ts

Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
#!/usr/bin/env bun
2+
3+
import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
4+
import { basename, join } from "node:path";
5+
6+
export interface SkillMetadataViolation {
7+
path: string;
8+
message: string;
9+
}
10+
11+
interface SkillFrontMatter {
12+
name?: string;
13+
description?: string;
14+
}
15+
16+
interface OpenAiAgentInterface {
17+
displayName?: string;
18+
shortDescription?: string;
19+
}
20+
21+
function unquote(value: string): string {
22+
return value.trim().replace(/^["']|["']$/g, "");
23+
}
24+
25+
function parseSkillFrontMatter(content: string): SkillFrontMatter {
26+
const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/);
27+
if (!match) return {};
28+
29+
const frontMatter: SkillFrontMatter = {};
30+
for (const line of match[1].split(/\r?\n/)) {
31+
const fieldMatch = line.match(/^(name|description):\s*(.+?)\s*$/);
32+
if (!fieldMatch) continue;
33+
frontMatter[fieldMatch[1] as keyof SkillFrontMatter] = unquote(
34+
fieldMatch[2],
35+
);
36+
}
37+
38+
return frontMatter;
39+
}
40+
41+
function parseOpenAiAgentInterface(content: string): OpenAiAgentInterface {
42+
const result: OpenAiAgentInterface = {};
43+
for (const line of content.split(/\r?\n/)) {
44+
const displayNameMatch = line.match(/^\s{2}display_name:\s*(.+?)\s*$/);
45+
if (displayNameMatch) {
46+
result.displayName = unquote(displayNameMatch[1]);
47+
continue;
48+
}
49+
const shortDescriptionMatch = line.match(
50+
/^\s{2}short_description:\s*(.+?)\s*$/,
51+
);
52+
if (shortDescriptionMatch) {
53+
result.shortDescription = unquote(shortDescriptionMatch[1]);
54+
}
55+
}
56+
return result;
57+
}
58+
59+
function expectedDisplayName(folderName: string): string {
60+
return folderName
61+
.replace(/^df-/, "")
62+
.split("-")
63+
.filter(Boolean)
64+
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
65+
.join(" ");
66+
}
67+
68+
export function validateSkillMetadata(
69+
skillsRoot = join(import.meta.dir, "..", "skills"),
70+
): SkillMetadataViolation[] {
71+
const violations: SkillMetadataViolation[] = [];
72+
if (!existsSync(skillsRoot)) {
73+
return [
74+
{
75+
path: skillsRoot,
76+
message: "skills root does not exist",
77+
},
78+
];
79+
}
80+
81+
for (const entry of readdirSync(skillsRoot, { withFileTypes: true })) {
82+
if (!entry.isDirectory()) continue;
83+
84+
const skillDir = join(skillsRoot, entry.name);
85+
const skillPath = join(skillDir, "SKILL.md");
86+
if (!existsSync(skillPath) || !statSync(skillPath).isFile()) continue;
87+
88+
if (!entry.name.startsWith("df-")) {
89+
violations.push({
90+
path: skillPath,
91+
message: `skill folder "${entry.name}" must start with "df-"`,
92+
});
93+
}
94+
95+
const frontMatter = parseSkillFrontMatter(readFileSync(skillPath, "utf-8"));
96+
if (frontMatter.name !== entry.name) {
97+
violations.push({
98+
path: skillPath,
99+
message: frontMatter.name
100+
? `front matter name "${frontMatter.name}" must match folder "${entry.name}"`
101+
: `front matter name is required and must match folder "${entry.name}"`,
102+
});
103+
}
104+
105+
const agentPath = join(skillDir, "agents", "openai.yaml");
106+
if (!existsSync(agentPath) || !statSync(agentPath).isFile()) {
107+
violations.push({
108+
path: agentPath,
109+
message: "agents/openai.yaml is required",
110+
});
111+
continue;
112+
}
113+
114+
const agentInterface = parseOpenAiAgentInterface(
115+
readFileSync(agentPath, "utf-8"),
116+
);
117+
const displayName = expectedDisplayName(entry.name);
118+
if (agentInterface.displayName !== displayName) {
119+
violations.push({
120+
path: agentPath,
121+
message: agentInterface.displayName
122+
? `interface.display_name "${agentInterface.displayName}" must be "${displayName}"`
123+
: `interface.display_name is required and must be "${displayName}"`,
124+
});
125+
}
126+
127+
if (agentInterface.shortDescription !== frontMatter.description) {
128+
violations.push({
129+
path: agentPath,
130+
message: frontMatter.description
131+
? `interface.short_description must exactly match SKILL.md description "${frontMatter.description}"`
132+
: "SKILL.md description is required before interface.short_description can be validated",
133+
});
134+
}
135+
}
136+
137+
return violations;
138+
}
139+
140+
function main(): number {
141+
const skillsRoot = Bun.argv[2] ?? join(import.meta.dir, "..", "skills");
142+
const violations = validateSkillMetadata(skillsRoot);
143+
if (!violations.length) {
144+
console.log(
145+
`Skill metadata check passed for ${basename(skillsRoot) || skillsRoot}`,
146+
);
147+
return 0;
148+
}
149+
150+
for (const violation of violations) {
151+
console.error(`::error file=${violation.path}::${violation.message}`);
152+
}
153+
return 1;
154+
}
155+
156+
if (import.meta.main) {
157+
process.exit(main());
158+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
interface:
2+
display_name: "Codex Assets"
3+
short_description: "Bootstrap DevFlow Codex hook runtime assets into an installed plugin mirror before other hooks run."
4+
default_prompt: "Use $df-codex-assets."
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
58a5fee02adc8e8a28248c6103cd7698691f3ce4b68ed05aed669dba06cfb694
1+
e80b15d7dcd6ba133770aee1b421110a9c9a32dd4788e01b2a853f53e49251a1

skills/df-ddd-event-storming-design/agents/openai.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
interface:
2-
display_name: "DDD Event Storming"
3-
short_description: "Requirements intake and pure DDD modeling from actors and events"
2+
display_name: "Ddd Event Storming Design"
3+
short_description: "使用事件风暴、CQRS 和需求追踪进行通用 DDD 领域建模。Use this skill when Codex needs to clarify raw business requirements, split stakeholder needs into requirement items, evolve a persistent domain model, identify actors and multi-role collaboration, domain events, commands, policies, aggregates, domain services, read models, produce structured Markdown or optional Mermaid/PlantUML diagrams, and review designs that may be CRUD, database, package-structure, or DDD-terminology driven instead of problem-domain driven. Especially use for CRUD-looking admin requirements such as company, department, position, employee, account, role, or permission management that need behavior-first modeling instead of flat noun aggregates."
44
default_prompt: "Use $df-ddd-event-storming-design to clarify raw business requirements, split requirement items, analyze with event storming, identify actors, commands, events, policies, aggregates, and read models, and produce a traceable behavior-first DDD domain design specification."
55

66
policy:
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
interface:
2-
display_name: "DDD to TDD Handoff"
3-
short_description: "Convert traceable DDD artifacts into language-agnostic TDD slices"
2+
display_name: "Ddd To Tdd Handoff"
3+
short_description: "Convert confirmed DDD event-storming outputs and requirement traceability into executable, language-agnostic TDD implementation slices. Use after df-ddd-event-storming-design has produced confirmed requirements, commands, events, aggregates, policies, invariants, read models, or relationships and Codex needs tests, implementation planning, or development sequencing."
44
default_prompt: "Use $df-ddd-to-tdd-handoff to turn the confirmed DDD design and requirement traceability into small, language-agnostic TDD implementation slices."

0 commit comments

Comments
 (0)