Skip to content

Commit fec8cb5

Browse files
committed
WIP
1 parent c7a3520 commit fec8cb5

File tree

15 files changed

+361
-153
lines changed

15 files changed

+361
-153
lines changed
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
821f2ce8d073b28f
1+
c177538279c21315

packages/codex/skills/.system/skill-creator/SKILL.md

Lines changed: 28 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ This skill provides guidance for creating effective skills.
1111

1212
## About Skills
1313

14-
Skills are modular, self-contained packages that extend Codex's capabilities by providing
14+
Skills are modular, self-contained folders that extend Codex's capabilities by providing
1515
specialized knowledge, workflows, and tools. Think of them as "onboarding guides" for specific
1616
domains or tasks—they transform Codex from a general-purpose agent into a specialized agent
1717
equipped with procedural knowledge that no model can fully possess.
@@ -56,6 +56,8 @@ skill-name/
5656
│ │ ├── name: (required)
5757
│ │ └── description: (required)
5858
│ └── Markdown instructions (required)
59+
├── agents/ (recommended)
60+
│ └── openai.yaml - UI metadata for skill lists and chips
5961
└── Bundled Resources (optional)
6062
├── scripts/ - Executable code (Python/Bash/etc.)
6163
├── references/ - Documentation intended to be loaded into context as needed
@@ -69,6 +71,16 @@ Every SKILL.md consists of:
6971
- **Frontmatter** (YAML): Contains `name` and `description` fields. These are the only fields that Codex reads to determine when the skill gets used, thus it is very important to be clear and comprehensive in describing what the skill is, and when it should be used.
7072
- **Body** (Markdown): Instructions and guidance for using the skill. Only loaded AFTER the skill triggers (if at all).
7173

74+
#### Agents metadata (recommended)
75+
76+
- UI-facing metadata for skill lists and chips
77+
- Read references/openai_yaml.md before generating values and follow its descriptions and constraints
78+
- Create: human-facing `display_name`, `short_description`, and `default_prompt` by reading the skill
79+
- Generate deterministically by passing the values as `--interface key=value` to `scripts/generate_openai_yaml.py` or `scripts/init_skill.py`
80+
- On updates: validate `agents/openai.yaml` still matches SKILL.md; regenerate if stale
81+
- Only include other optional interface fields (icons, brand color) if explicitly provided
82+
- See references/openai_yaml.md for field definitions and examples
83+
7284
#### Bundled Resources (optional)
7385

7486
##### Scripts (`scripts/`)
@@ -208,7 +220,7 @@ Skill creation involves these steps:
208220
2. Plan reusable skill contents (scripts, references, assets)
209221
3. Initialize the skill (run init_skill.py)
210222
4. Edit the skill (implement resources and write SKILL.md)
211-
5. Package the skill (run package_skill.py)
223+
5. Validate the skill (run quick_validate.py)
212224
6. Iterate based on real usage
213225

214226
Follow these steps in order, skipping only if there is a clear reason why they are not applicable.
@@ -266,7 +278,7 @@ To establish the skill's contents, analyze each concrete example to create a lis
266278

267279
At this point, it is time to actually create the skill.
268280

269-
Skip this step only if the skill being developed already exists, and iteration or packaging is needed. In this case, continue to the next step.
281+
Skip this step only if the skill being developed already exists. In this case, continue to the next step.
270282

271283
When creating a new skill from scratch, always run the `init_skill.py` script. The script conveniently generates a new template skill directory that automatically includes everything a skill requires, making the skill creation process much more efficient and reliable.
272284

@@ -288,11 +300,20 @@ The script:
288300

289301
- Creates the skill directory at the specified path
290302
- Generates a SKILL.md template with proper frontmatter and TODO placeholders
303+
- Creates `agents/openai.yaml` using agent-generated `display_name`, `short_description`, and `default_prompt` passed via `--interface key=value`
291304
- Optionally creates resource directories based on `--resources`
292305
- Optionally adds example files when `--examples` is set
293306

294307
After initialization, customize the SKILL.md and add resources as needed. If you used `--examples`, replace or delete placeholder files.
295308

309+
Generate `display_name`, `short_description`, and `default_prompt` by reading the skill, then pass them as `--interface key=value` to `init_skill.py` or regenerate with:
310+
311+
```bash
312+
scripts/generate_openai_yaml.py <path/to/skill-folder> --interface key=value
313+
```
314+
315+
Only include other optional interface fields when the user explicitly provides them. For full field descriptions and examples, see references/openai_yaml.md.
316+
296317
### Step 4: Edit the Skill
297318

298319
When editing the (newly-generated or existing) skill, remember that the skill is being created for another instance of Codex to use. Include information that would be beneficial and non-obvious to Codex. Consider what procedural knowledge, domain-specific details, or reusable assets would help another Codex instance execute these tasks more effectively.
@@ -334,32 +355,15 @@ Do not include any other fields in YAML frontmatter.
334355

335356
Write instructions for using the skill and its bundled resources.
336357

337-
### Step 5: Packaging a Skill
358+
### Step 5: Validate the Skill
338359

339-
Once development of the skill is complete, it must be packaged into a distributable .skill file that gets shared with the user. The packaging process automatically validates the skill first to ensure it meets all requirements:
360+
Once development of the skill is complete, validate the skill folder to catch basic issues early:
340361

341362
```bash
342-
scripts/package_skill.py <path/to/skill-folder>
363+
scripts/quick_validate.py <path/to/skill-folder>
343364
```
344365

345-
Optional output directory specification:
346-
347-
```bash
348-
scripts/package_skill.py <path/to/skill-folder> ./dist
349-
```
350-
351-
The packaging script will:
352-
353-
1. **Validate** the skill automatically, checking:
354-
355-
- YAML frontmatter format and required fields
356-
- Skill naming conventions and directory structure
357-
- Description completeness and quality
358-
- File organization and resource references
359-
360-
2. **Package** the skill if validation passes, creating a .skill file named after the skill (e.g., `my-skill.skill`) that includes all files and maintains the proper directory structure for distribution. The .skill file is a zip file with a .skill extension.
361-
362-
If validation fails, the script will report the errors and exit without creating a package. Fix any validation errors and run the packaging command again.
366+
The validation script checks YAML frontmatter format, required fields, and naming rules. If validation fails, fix the reported issues and run the command again.
363367

364368
### Step 6: Iterate
365369

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
interface:
2+
display_name: "Skill Creator"
3+
short_description: "Create or update a skill"
4+
icon_small: "./assets/skill-creator-small.svg"
5+
icon_large: "./assets/skill-creator.png"
Lines changed: 3 additions & 0 deletions
Loading
1.53 KB
Loading
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
# openai.yaml fields (full example + descriptions)
2+
3+
`agents/openai.yaml` is an extended, product-specific config intended for the machine/harness to read, not the agent. Other product-specific config can also live in the `agents/` folder.
4+
5+
## Full example
6+
7+
```yaml
8+
interface:
9+
display_name: "Optional user-facing name"
10+
short_description: "Optional user-facing description"
11+
icon_small: "./assets/small-400px.png"
12+
icon_large: "./assets/large-logo.svg"
13+
brand_color: "#3B82F6"
14+
default_prompt: "Optional surrounding prompt to use the skill with"
15+
16+
dependencies:
17+
tools:
18+
- type: "mcp"
19+
value: "github"
20+
description: "GitHub MCP server"
21+
transport: "streamable_http"
22+
url: "https://api.githubcopilot.com/mcp/"
23+
```
24+
25+
## Field descriptions and constraints
26+
27+
Top-level constraints:
28+
29+
- Quote all string values.
30+
- Keep keys unquoted.
31+
- For `interface.default_prompt`: generate a helpful, short (typically 1 sentence) example starting prompt based on the skill. It must explicitly mention the skill as `$skill-name` (e.g., "Use $skill-name-here to draft a concise weekly status update.").
32+
33+
- `interface.display_name`: Human-facing title shown in UI skill lists and chips.
34+
- `interface.short_description`: Human-facing short UI blurb (25–64 chars) for quick scanning.
35+
- `interface.icon_small`: Path to a small icon asset (relative to skill dir). Default to `./assets/` and place icons in the skill's `assets/` folder.
36+
- `interface.icon_large`: Path to a larger logo asset (relative to skill dir). Default to `./assets/` and place icons in the skill's `assets/` folder.
37+
- `interface.brand_color`: Hex color used for UI accents (e.g., badges).
38+
- `interface.default_prompt`: Default prompt snippet inserted when invoking the skill.
39+
- `dependencies.tools[].type`: Dependency category. Only `mcp` is supported for now.
40+
- `dependencies.tools[].value`: Identifier of the tool or dependency.
41+
- `dependencies.tools[].description`: Human-readable explanation of the dependency.
42+
- `dependencies.tools[].transport`: Connection type when `type` is `mcp`.
43+
- `dependencies.tools[].url`: MCP server URL when `type` is `mcp`.
Lines changed: 225 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,225 @@
1+
#!/usr/bin/env python3
2+
"""
3+
OpenAI YAML Generator - Creates agents/openai.yaml for a skill folder.
4+
5+
Usage:
6+
generate_openai_yaml.py <skill_dir> [--name <skill_name>] [--interface key=value]
7+
"""
8+
9+
import argparse
10+
import re
11+
import sys
12+
from pathlib import Path
13+
14+
import yaml
15+
16+
ACRONYMS = {
17+
"GH",
18+
"MCP",
19+
"API",
20+
"CI",
21+
"CLI",
22+
"LLM",
23+
"PDF",
24+
"PR",
25+
"UI",
26+
"URL",
27+
"SQL",
28+
}
29+
30+
BRANDS = {
31+
"openai": "OpenAI",
32+
"openapi": "OpenAPI",
33+
"github": "GitHub",
34+
"pagerduty": "PagerDuty",
35+
"datadog": "DataDog",
36+
"sqlite": "SQLite",
37+
"fastapi": "FastAPI",
38+
}
39+
40+
SMALL_WORDS = {"and", "or", "to", "up", "with"}
41+
42+
ALLOWED_INTERFACE_KEYS = {
43+
"display_name",
44+
"short_description",
45+
"icon_small",
46+
"icon_large",
47+
"brand_color",
48+
"default_prompt",
49+
}
50+
51+
52+
def yaml_quote(value):
53+
escaped = value.replace("\\", "\\\\").replace('"', '\\"').replace("\n", "\\n")
54+
return f'"{escaped}"'
55+
56+
57+
def format_display_name(skill_name):
58+
words = [word for word in skill_name.split("-") if word]
59+
formatted = []
60+
for index, word in enumerate(words):
61+
lower = word.lower()
62+
upper = word.upper()
63+
if upper in ACRONYMS:
64+
formatted.append(upper)
65+
continue
66+
if lower in BRANDS:
67+
formatted.append(BRANDS[lower])
68+
continue
69+
if index > 0 and lower in SMALL_WORDS:
70+
formatted.append(lower)
71+
continue
72+
formatted.append(word.capitalize())
73+
return " ".join(formatted)
74+
75+
76+
def generate_short_description(display_name):
77+
description = f"Help with {display_name} tasks"
78+
79+
if len(description) < 25:
80+
description = f"Help with {display_name} tasks and workflows"
81+
if len(description) < 25:
82+
description = f"Help with {display_name} tasks with guidance"
83+
84+
if len(description) > 64:
85+
description = f"Help with {display_name}"
86+
if len(description) > 64:
87+
description = f"{display_name} helper"
88+
if len(description) > 64:
89+
description = f"{display_name} tools"
90+
if len(description) > 64:
91+
suffix = " helper"
92+
max_name_length = 64 - len(suffix)
93+
trimmed = display_name[:max_name_length].rstrip()
94+
description = f"{trimmed}{suffix}"
95+
if len(description) > 64:
96+
description = description[:64].rstrip()
97+
98+
if len(description) < 25:
99+
description = f"{description} workflows"
100+
if len(description) > 64:
101+
description = description[:64].rstrip()
102+
103+
return description
104+
105+
106+
def read_frontmatter_name(skill_dir):
107+
skill_md = Path(skill_dir) / "SKILL.md"
108+
if not skill_md.exists():
109+
print(f"[ERROR] SKILL.md not found in {skill_dir}")
110+
return None
111+
content = skill_md.read_text()
112+
match = re.match(r"^---\n(.*?)\n---", content, re.DOTALL)
113+
if not match:
114+
print("[ERROR] Invalid SKILL.md frontmatter format.")
115+
return None
116+
frontmatter_text = match.group(1)
117+
try:
118+
frontmatter = yaml.safe_load(frontmatter_text)
119+
except yaml.YAMLError as exc:
120+
print(f"[ERROR] Invalid YAML frontmatter: {exc}")
121+
return None
122+
if not isinstance(frontmatter, dict):
123+
print("[ERROR] Frontmatter must be a YAML dictionary.")
124+
return None
125+
name = frontmatter.get("name", "")
126+
if not isinstance(name, str) or not name.strip():
127+
print("[ERROR] Frontmatter 'name' is missing or invalid.")
128+
return None
129+
return name.strip()
130+
131+
132+
def parse_interface_overrides(raw_overrides):
133+
overrides = {}
134+
optional_order = []
135+
for item in raw_overrides:
136+
if "=" not in item:
137+
print(f"[ERROR] Invalid interface override '{item}'. Use key=value.")
138+
return None, None
139+
key, value = item.split("=", 1)
140+
key = key.strip()
141+
value = value.strip()
142+
if not key:
143+
print(f"[ERROR] Invalid interface override '{item}'. Key is empty.")
144+
return None, None
145+
if key not in ALLOWED_INTERFACE_KEYS:
146+
allowed = ", ".join(sorted(ALLOWED_INTERFACE_KEYS))
147+
print(f"[ERROR] Unknown interface field '{key}'. Allowed: {allowed}")
148+
return None, None
149+
overrides[key] = value
150+
if key not in ("display_name", "short_description") and key not in optional_order:
151+
optional_order.append(key)
152+
return overrides, optional_order
153+
154+
155+
def write_openai_yaml(skill_dir, skill_name, raw_overrides):
156+
overrides, optional_order = parse_interface_overrides(raw_overrides)
157+
if overrides is None:
158+
return None
159+
160+
display_name = overrides.get("display_name") or format_display_name(skill_name)
161+
short_description = overrides.get("short_description") or generate_short_description(display_name)
162+
163+
if not (25 <= len(short_description) <= 64):
164+
print(
165+
"[ERROR] short_description must be 25-64 characters "
166+
f"(got {len(short_description)})."
167+
)
168+
return None
169+
170+
interface_lines = [
171+
"interface:",
172+
f" display_name: {yaml_quote(display_name)}",
173+
f" short_description: {yaml_quote(short_description)}",
174+
]
175+
176+
for key in optional_order:
177+
value = overrides.get(key)
178+
if value is not None:
179+
interface_lines.append(f" {key}: {yaml_quote(value)}")
180+
181+
agents_dir = Path(skill_dir) / "agents"
182+
agents_dir.mkdir(parents=True, exist_ok=True)
183+
output_path = agents_dir / "openai.yaml"
184+
output_path.write_text("\n".join(interface_lines) + "\n")
185+
print(f"[OK] Created agents/openai.yaml")
186+
return output_path
187+
188+
189+
def main():
190+
parser = argparse.ArgumentParser(
191+
description="Create agents/openai.yaml for a skill directory.",
192+
)
193+
parser.add_argument("skill_dir", help="Path to the skill directory")
194+
parser.add_argument(
195+
"--name",
196+
help="Skill name override (defaults to SKILL.md frontmatter)",
197+
)
198+
parser.add_argument(
199+
"--interface",
200+
action="append",
201+
default=[],
202+
help="Interface override in key=value format (repeatable)",
203+
)
204+
args = parser.parse_args()
205+
206+
skill_dir = Path(args.skill_dir).resolve()
207+
if not skill_dir.exists():
208+
print(f"[ERROR] Skill directory not found: {skill_dir}")
209+
sys.exit(1)
210+
if not skill_dir.is_dir():
211+
print(f"[ERROR] Path is not a directory: {skill_dir}")
212+
sys.exit(1)
213+
214+
skill_name = args.name or read_frontmatter_name(skill_dir)
215+
if not skill_name:
216+
sys.exit(1)
217+
218+
result = write_openai_yaml(skill_dir, skill_name, args.interface)
219+
if result:
220+
sys.exit(0)
221+
sys.exit(1)
222+
223+
224+
if __name__ == "__main__":
225+
main()

0 commit comments

Comments
 (0)