Setting the file. One moment.
Skills · Migrate To Codex · openai/skills · Skills Docs
ContentsBack to the top of the page 230
def skill_report_detail
— line 230
This file
Number 19.13
Position 13 of 18
Type Python
Size 12 KB
Lines 393 scripts/migrate/ skills.py
Python · 393 lines · 12 KB
Sequence
15 from pathlib import Path
16
17 from migrate.common import (
18 CODEX_SKILLS_ROOT ,
19 ArtifactKind,
20 ConversionResult,
21 GeneratedText,
22 MigrationReportItem,
23 ParsedDocument,
24 PlannedArtifact,
25 SimpleYamlFrontmatter,
26 append_report_item,
27 format_bullets,
28 format_frontmatter,
29 format_manual_migration_block,
30 is_path_within_root,
31 manual_report_item,
32 parse_frontmatter,
33 unsupported_frontmatter_fields,
34 )
35 from utils.util import slugify_name
36
37
38 COMMAND_FILE_SOURCES = (
39 (Path( ".claude" ) / "commands" , "source-command" , "source command" ),
40 )
41
42 SKILL_SOURCE_ROOTS = (
43 Path( ".claude" ) / "skills" ,
44 )
45 SKILL_SUPPORT_DIRS = ( "scripts" , "references" , "assets" )
46
47
48 def iter_skill_files (source_root: Path) -> tuple[Path, ... ]:
49 if not source_root.exists():
50 return ()
51 single_file_skills = tuple (
52 source_file
53 for source_file in sorted (source_root.glob( "*.md" ))
54 if source_file.stem != "README"
55 )
56 directory_skills = tuple ( sorted (source_root.glob( "*/SKILL.md" )))
57 return single_file_skills + directory_skills
58
59
60 def skill_target_name (source_file: Path) -> str :
61 if source_file.name == "SKILL.md" :
62 return source_file.parent.name
63 return source_file.stem
64
65
66 def command_caveats (
67 template: str ,
68 unsupported_fields: Sequence[ str ],
69 ) -> tuple[ str , ... ]:
70 caveats: list[ str ] = []
71 if re.search( r " \$ ( ARGUMENTS | \d + )\b " , template):
72 caveats.append(
73 "Provider argument placeholders like `$ARGUMENTS` or `$1` were preserved as text; rewrite them into natural-language instructions for Codex."
74 )
75 if " {{ " in template and " }} " in template:
76 caveats.append(
77 "Provider template variables like ` {{ name }} ` were preserved as text; rewrite them into natural-language instructions for Codex."
78 )
79 if re.search( r "! \s * `" , template):
80 caveats.append(
81 "Provider shell-output interpolation like ``!`command` `` was preserved as text; replace it with explicit Codex instructions to run the command when needed."
82 )
83 if re.search( r " (^ | \s) @ [\w ./~:- ] + " , template):
84 caveats.append(
85 "Provider automatic file-reference expansion was preserved as text; verify Codex should read those files explicitly."
86 )
87 if unsupported_fields:
88 caveats.append(
89 "Review unsupported command metadata manually: "
90 + ", " .join( f "` { field_name } `" for field_name in unsupported_fields)
91 + "."
92 )
93 return tuple (caveats)
94
95
96 def convert_skills (source_root: Path) -> ConversionResult:
97 result = convert_skill_files(source_root / ".claude" / "skills" )
98 result.add(convert_command_skills(source_root))
99 return result
100
101
102 def convert_skill_files (source_root: Path) -> ConversionResult:
103 result = ConversionResult()
104 for source_file in iter_skill_files(source_root):
105 artifacts, report_item = convert_skill_file(source_file)
106 result.artifacts.extend(artifacts)
107 result.summary.skills += 1
108 result.report_items.append(report_item)
109 return result
110
111
112 def convert_command_skills (source_root: Path) -> ConversionResult:
113 result = ConversionResult()
114 for command_source_root, name_prefix, provider in COMMAND_FILE_SOURCES :
115 result.add(
116 convert_markdown_command_files(
117 source_root / command_source_root,
118 name_prefix,
119 provider,
120 )
121 )
122 return result
123
124
125 def codex_skill_frontmatter (name: str , description: str ) -> SimpleYamlFrontmatter:
126 return SimpleYamlFrontmatter(
127 {
128 "name" : name,
129 "description" : description,
130 }
131 )
132
133
134 def convert_skill_file (source_file: Path) -> tuple[list[PlannedArtifact], MigrationReportItem]:
135 document = ParsedDocument.from_file(source_file)
136 name = document.frontmatter.required_string( "name" )
137 description = document.frontmatter.required_string( "description" )
138 allowed_tools = document.frontmatter.string_tuple( "allowed-tools" )
139 unsupported_fields = unsupported_frontmatter_fields(
140 document.frontmatter.to_dict(),
141 ( "name" , "description" , "allowed-tools" ),
142 )
143 artifacts = [
144 PlannedArtifact(
145 relative_path = CODEX_SKILLS_ROOT / skill_target_name(source_file) / "SKILL.md" ,
146 payload = GeneratedText(
147 render_skill(
148 document.body,
149 name = name,
150 description = description,
151 allowed_tools = allowed_tools,
152 unsupported_fields = unsupported_fields,
153 )
154 ),
155 kind = ArtifactKind. SKILL ,
156 )
157 ]
158 artifacts.extend(skill_support_artifacts(source_file))
159 return artifacts, skill_report_item(source_file, allowed_tools, unsupported_fields)
160
161
162 def skill_support_artifacts (source_file: Path) -> list[PlannedArtifact]:
163 if source_file.name != "SKILL.md" :
164 return []
165
166 artifacts: list[PlannedArtifact] = []
167 skill_root = source_file.parent
168 target_root = CODEX_SKILLS_ROOT / skill_root.name
169 source_files: list[Path] = []
170 for dirname in SKILL_SUPPORT_DIRS :
171 source_dir = skill_root / dirname
172 if not source_dir.exists():
173 continue
174 source_files.extend(
175 source_file
176 for source_file in source_dir.rglob( "*" )
177 if source_file.is_file() and is_path_within_root(source_file, skill_root)
178 )
179 for support_file in sorted (
180 source_files,
181 key =lambda path: path.relative_to(skill_root).as_posix(),
182 ):
183 artifacts.append(
184 PlannedArtifact.from_source_file(
185 support_file,
186 target_root / support_file.relative_to(skill_root),
187 )
188 )
189 return artifacts
190
191
192 def render_skill (
193 body: str ,
194 * ,
195 name: str ,
196 description: str ,
197 allowed_tools: tuple[ str , ... ],
198 unsupported_fields: tuple[ str , ... ],
199 ) -> str :
200 return format_frontmatter(
201 codex_skill_frontmatter(name, description),
202 render_skill_body(body, allowed_tools, unsupported_fields),
203 )
204
205
206 def render_skill_body (
207 body: str ,
208 allowed_tools: tuple[ str , ... ],
209 unsupported_fields: tuple[ str , ... ],
210 ) -> str :
211 manual_notes: list[ str ] = []
212 if allowed_tools:
213 manual_notes.append(
214 "Claude `allowed-tools` was preserved as prompt guidance, not a Codex permission boundary. \n\n "
215 "You're allowed to use these tools: \n\n "
216 f " { format_bullets(allowed_tools) } "
217 )
218 if unsupported_fields:
219 manual_notes.append(
220 "Review unsupported Claude skill fields manually: "
221 f " { ', ' .join( f '` { field_name } `' for field_name in unsupported_fields) } ."
222 )
223
224 if not manual_notes:
225 return body
226
227 return f " { body.rstrip() }\n\n{ format_manual_migration_block(manual_notes) }\n "
228
229
230 def skill_report_detail (
231 allowed_tools: tuple[ str , ... ],
232 unsupported_fields: tuple[ str , ... ],
233 ) -> str :
234 caveats: list[ str ] = []
235 if allowed_tools:
236 caveats.append( "allowed-tools" )
237 caveats.extend(unsupported_fields)
238 if not caveats:
239 return "Converted Claude skill."
240 return (
241 "Manual review required for Claude skill fields: "
242 + ", " .join( f "` { field_name } `" for field_name in caveats)
243 + "."
244 )
245
246
247 def skill_report_item (
248 source_file: Path,
249 allowed_tools: tuple[ str , ... ],
250 unsupported_fields: tuple[ str , ... ],
251 ) -> MigrationReportItem:
252 report_items: list[MigrationReportItem] = []
253 detail = skill_report_detail(allowed_tools, unsupported_fields)
254 append_report_item(
255 report_items,
256 allowed_tools or unsupported_fields,
257 CODEX_SKILLS_ROOT / skill_target_name(source_file) / "SKILL.md" ,
258 detail,
259 detail,
260 )
261 return report_items[ 0 ]
262
263
264 def convert_markdown_command_files (
265 source_root: Path,
266 name_prefix: str ,
267 provider: str ,
268 ) -> ConversionResult:
269 result = ConversionResult()
270 if not source_root.exists():
271 return result
272 for source_file in sorted (source_root.rglob( "*.md" )):
273 artifact, report_item = convert_command_file(
274 source_root,
275 source_file,
276 name_prefix,
277 provider,
278 )
279 result.artifacts.append(artifact)
280 result.summary.skills += 1
281 result.report_items.append(report_item)
282 return result
283
284
285 def convert_command_file (
286 source_root: Path,
287 source_file: Path,
288 name_prefix: str ,
289 provider: str ,
290 ) -> tuple[PlannedArtifact, MigrationReportItem]:
291 document = ParsedDocument.from_file(source_file)
292 source_name = "-" .join(source_file.relative_to(source_root).with_suffix( "" ).parts)
293 name = slugify_name( f " { name_prefix } - { source_name } " )
294 description = document.frontmatter.optional_string( "description" )
295 if not description:
296 description = f "Run the migrated { provider } ` { source_name } `."
297 unsupported_fields = unsupported_frontmatter_fields(
298 document.frontmatter.to_dict(),
299 ( "description" ,),
300 )
301 caveats = command_caveats(document.body, unsupported_fields)
302 artifact = PlannedArtifact(
303 relative_path = CODEX_SKILLS_ROOT / name / "SKILL.md" ,
304 payload = GeneratedText(
305 render_command_skill(
306 document.body,
307 name = name,
308 description = description,
309 provider = provider,
310 source_name = source_name,
311 caveats = caveats,
312 )
313 ),
314 kind = ArtifactKind. SKILL ,
315 )
316 return artifact, command_report_item(name, provider, source_name)
317
318
319 def render_command_skill (
320 body: str ,
321 * ,
322 name: str ,
323 description: str ,
324 provider: str ,
325 source_name: str ,
326 caveats: tuple[ str , ... ],
327 ) -> str :
328 manual_notes = [
329 f "Migrated from { provider } ` { source_name } ` into a Codex skill. "
330 f "Invoke it as `$ { name } ` and manually rewrite any slash-command behavior that depended on provider-specific runtime expansion."
331 ]
332 manual_notes.extend(caveats)
333 template_body = body.strip() or "No command template body was found."
334 return format_frontmatter(
335 codex_skill_frontmatter(name, description),
336 f "# { name }\n\n "
337 "Use this skill when the user asks to run the migrated "
338 f " { provider } ` { source_name } `. \n\n "
339 "## Command Template \n\n "
340 f " { template_body }\n\n "
341 f " { format_manual_migration_block(manual_notes) }\n " ,
342 )
343
344
345 def validate_skill_files (target_root: Path) -> list[MigrationReportItem]:
346 skills_root = target_root / CODEX_SKILLS_ROOT
347 if not skills_root.exists():
348 return []
349
350 report_items: list[MigrationReportItem] = []
351 for skill_file in sorted (skills_root.glob( "*/SKILL.md" )):
352 relative_path = skill_file.relative_to(target_root)
353 document = parse_frontmatter(skill_file.read_text(), skill_file)
354 missing = [
355 key
356 for key in ( "name" , "description" )
357 if not document.frontmatter.optional_string(key)
358 ]
359 if missing:
360 report_items.append(
361 MigrationReportItem(
362 "error" ,
363 relative_path,
364 "skill frontmatter missing " + ", " .join(missing) + "." ,
365 )
366 )
367 continue
368 report_items.append(
369 MigrationReportItem(
370 "ok" ,
371 relative_path,
372 "skill frontmatter has name and description." ,
373 )
374 )
375 return report_items
376
377
378 def command_report_detail (provider: str , source_name: str ) -> str :
379 return (
380 f "Converted { provider } ` { source_name } ` to a single-file Codex skill; "
381 "review invocation and template placeholder semantics."
382 )
383
384
385 def command_report_item (
386 name: str ,
387 provider: str ,
388 source_name: str ,
389 ) -> MigrationReportItem:
390 return manual_report_item(
391 CODEX_SKILLS_ROOT / name / "SKILL.md" ,
392 command_report_detail(provider, source_name),
393 )