66Convert Unified Rules to IDE Formats
77
88Transforms the unified markdown sources into IDE-specific bundles (Cursor,
9- Windsurf, Copilot, Claude Code ). This script is the main entry point for producing
10- distributable rule packs from the sources/ directory.
9+ Windsurf, Copilot, Agent Skills, Antigravity ). This script is the main entry point
10+ for producing distributable rule packs from the sources/ directory.
1111"""
1212
1313import re
1616from collections import defaultdict
1717
1818from converter import RuleConverter
19- from formats import CursorFormat , WindsurfFormat , CopilotFormat , ClaudeCodeFormat , AntigravityFormat
19+ from formats import (
20+ CursorFormat ,
21+ WindsurfFormat ,
22+ CopilotFormat ,
23+ AgentSkillsFormat ,
24+ AntigravityFormat ,
25+ )
2026from utils import get_version_from_pyproject
2127from validate_versions import set_plugin_version , set_marketplace_version
2228
2632
2733def sync_plugin_metadata (version : str ) -> None :
2834 """
29- Sync version from pyproject.toml to Claude Code plugin metadata files.
35+ Sync version from pyproject.toml to Agent Skills metadata files.
3036
3137 Args:
3238 version: Version string from pyproject.toml
@@ -39,17 +45,17 @@ def sync_plugin_metadata(version: str) -> None:
3945def matches_tag_filter (rule_tags : list [str ], filter_tags : list [str ]) -> bool :
4046 """
4147 Check if rule has all required tags (AND logic).
42-
48+
4349 Args:
4450 rule_tags: List of tags from the rule (already normalized to lowercase)
4551 filter_tags: List of tags to filter by (already normalized to lowercase)
46-
52+
4753 Returns:
4854 True if rule has all filter tags (or no filter), False otherwise
4955 """
5056 if not filter_tags :
5157 return True # No filter means all pass
52-
58+
5359 return all (tag in rule_tags for tag in filter_tags )
5460
5561
@@ -98,14 +104,20 @@ def update_skill_md(language_to_rules: dict[str, list[str]], skill_path: str) ->
98104 print (f"Updated SKILL.md with language mappings" )
99105
100106
101- def convert_rules (input_path : str , output_dir : str = "dist" , include_claudecode : bool = True , version : str = None , filter_tags : list [str ] = None ) -> dict [str , list [str ]]:
107+ def convert_rules (
108+ input_path : str ,
109+ output_dir : str = "dist" ,
110+ include_agentskills : bool = True ,
111+ version : str = None ,
112+ filter_tags : list [str ] = None ,
113+ ) -> dict [str , list [str ]]:
102114 """
103115 Convert rule file(s) to all supported IDE formats using RuleConverter.
104116
105117 Args:
106118 input_path: Path to a single .md file or folder containing .md files
107119 output_dir: Output directory (default: 'dist/')
108- include_claudecode : Whether to generate Claude Code plugin (default: True, only for core rules)
120+ include_agentskills : Whether to generate Agent Skills format (default: True, only for core rules)
109121 version: Version string to use (default: read from pyproject.toml)
110122 filter_tags: Optional list of tags to filter by (AND logic, case-insensitive)
111123
@@ -117,7 +129,7 @@ def convert_rules(input_path: str, output_dir: str = "dist", include_claudecode:
117129 }
118130
119131 Example:
120- results = convert_rules("sources/core", "dist", include_claudecode =True)
132+ results = convert_rules("sources/core", "dist", include_agentskills =True)
121133 print(f"Converted {len(results['success'])} rules")
122134 """
123135 if version is None :
@@ -130,10 +142,10 @@ def convert_rules(input_path: str, output_dir: str = "dist", include_claudecode:
130142 CopilotFormat (version ),
131143 AntigravityFormat (version ),
132144 ]
133-
134- # Only include Claude Code for core rules (committed plugin )
135- if include_claudecode :
136- all_formats .append (ClaudeCodeFormat (version ))
145+
146+ # Only include Agent Skills format for core rules (committed as skills )
147+ if include_agentskills :
148+ all_formats .append (AgentSkillsFormat (version ))
137149
138150 converter = RuleConverter (formats = all_formats )
139151 path = Path (input_path )
@@ -151,7 +163,7 @@ def convert_rules(input_path: str, output_dir: str = "dist", include_claudecode:
151163 md_files = sorted (list (path .rglob ("*.md" )))
152164 if not md_files :
153165 raise ValueError (f"No .md files found in { input_path } " )
154-
166+
155167 print (f"Converting { len (md_files )} files from: { path } " )
156168
157169 # Setup output directory
@@ -165,7 +177,7 @@ def convert_rules(input_path: str, output_dir: str = "dist", include_claudecode:
165177 try :
166178 # Convert the file (raises exceptions on error)
167179 result = converter .convert (md_file )
168-
180+
169181 # Apply tag filter if specified
170182 if filter_tags and not matches_tag_filter (result .tags , filter_tags ):
171183 results ["skipped" ].append (result .filename )
@@ -175,17 +187,15 @@ def convert_rules(input_path: str, output_dir: str = "dist", include_claudecode:
175187 output_files = []
176188 for format_name , output in result .outputs .items ():
177189 # Construct output path
178- # Claude Code goes to project root ./skills/
190+ # Agent Skills goes to project root ./skills/
179191 # Other formats go to dist/ (or specified output_dir)
180- if format_name == "claudecode " :
192+ if format_name == "agentskills " :
181193 base_dir = PROJECT_ROOT
182194 else :
183195 base_dir = output_base
184-
196+
185197 output_file = (
186- base_dir
187- / output .subpath
188- / f"{ result .basename } { output .extension } "
198+ base_dir / output .subpath / f"{ result .basename } { output .extension } "
189199 )
190200
191201 # Create directory if it doesn't exist and write file
@@ -225,30 +235,32 @@ def convert_rules(input_path: str, output_dir: str = "dist", include_claudecode:
225235 f"\n Results: { len (results ['success' ])} success, { len (results ['errors' ])} errors"
226236 )
227237
228- # Generate SKILL.md with language mappings (only if Claude Code is included)
229- if include_claudecode and language_to_rules :
230- template_path = PROJECT_ROOT / "sources" / "core" / "codeguard-SKILLS.md.template"
231-
238+ # Generate SKILL.md with language mappings (only if Agent Skills is included)
239+ if include_agentskills and language_to_rules :
240+ template_path = (
241+ PROJECT_ROOT / "sources" / "core" / "codeguard-SKILLS.md.template"
242+ )
243+
232244 if not template_path .exists ():
233245 raise FileNotFoundError (
234246 f"SKILL.md template not found at { template_path } . "
235- "This file is required for Claude Code plugin generation."
247+ "This file is required for Agent Skills generation."
236248 )
237-
249+
238250 output_skill_dir = PROJECT_ROOT / "skills" / "software-security"
239251 output_skill_dir .mkdir (parents = True , exist_ok = True )
240252 output_skill_path = output_skill_dir / "SKILL.md"
241-
253+
242254 # Read template and inject current version from pyproject.toml
243255 template_content = template_path .read_text (encoding = "utf-8" )
244256 # Replace the hardcoded version with actual version
245257 template_content = re .sub (
246258 r'codeguard-version:\s*"[^"]*"' ,
247259 f'codeguard-version: "{ version } "' ,
248- template_content
260+ template_content ,
249261 )
250262 output_skill_path .write_text (template_content , encoding = "utf-8" )
251-
263+
252264 update_skill_md (language_to_rules , str (output_skill_path ))
253265
254266 return results
@@ -262,15 +274,15 @@ def _resolve_source_paths(args) -> list[Path]:
262274 # If --source flags provided, resolve under sources/
263275 if args .source :
264276 return [Path ("sources" ) / src for src in args .source ]
265-
277+
266278 # Default: core rules only
267279 return [Path ("sources/core" )]
268280
269281
270282if __name__ == "__main__" :
271283 import sys
272284 from argparse import ArgumentParser
273-
285+
274286 parser = ArgumentParser (
275287 description = "Convert unified rule markdown into IDE-specific bundles."
276288 )
@@ -291,7 +303,7 @@ def _resolve_source_paths(args) -> list[Path]:
291303 dest = "tags" ,
292304 help = "Filter rules by tags (comma-separated, case-insensitive, AND logic). Example: --tag api,web-security" ,
293305 )
294-
306+
295307 cli_args = parser .parse_args ()
296308 source_paths = _resolve_source_paths (cli_args )
297309
@@ -307,27 +319,31 @@ def _resolve_source_paths(args) -> list[Path]:
307319 for source_path in source_paths :
308320 for md_file in source_path .rglob ("*.md" ):
309321 filename_to_sources [md_file .name ].append (source_path .name )
310-
311- duplicates = {name : srcs for name , srcs in filename_to_sources .items () if len (srcs ) > 1 }
322+
323+ duplicates = {
324+ name : srcs for name , srcs in filename_to_sources .items () if len (srcs ) > 1
325+ }
312326 if duplicates :
313327 print (f"❌ Found { len (duplicates )} duplicate filename(s) across sources:" )
314328 for filename , sources in duplicates .items ():
315329 print (f" - { filename } in: { ', ' .join (sources )} " )
316330 print ("\n Please rename files to have unique names across all sources." )
317331 sys .exit (1 )
318-
332+
319333 # Get version once and sync to metadata files
320334 version = get_version_from_pyproject ()
321335 sync_plugin_metadata (version )
322336
323- # Check if core is in the sources for Claude Code plugin generation
337+ # Check if core is in the sources for Agent Skills generation
324338 has_core = Path ("sources/core" ) in source_paths
325339 if has_core :
326340 # Validate template exists early
327- template_path = PROJECT_ROOT / "sources" / "core" / "codeguard-SKILLS.md.template"
341+ template_path = (
342+ PROJECT_ROOT / "sources" / "core" / "codeguard-SKILLS.md.template"
343+ )
328344 if not template_path .exists ():
329345 print (f"❌ SKILL.md template not found at { template_path } " )
330- print ("This file is required for Claude Code plugin generation." )
346+ print ("This file is required for Agent Skills generation." )
331347 sys .exit (1 )
332348
333349 # Clean output directories once before processing
@@ -341,46 +357,50 @@ def _resolve_source_paths(args) -> list[Path]:
341357 if skills_rules_dir .exists ():
342358 shutil .rmtree (skills_rules_dir )
343359 print (f"✅ Cleaned skills/ directory" )
344-
360+
345361 # Print processing summary
346362 if len (source_paths ) > 1 :
347- sources_list = ', ' .join (p .name for p in source_paths )
363+ sources_list = ", " .join (p .name for p in source_paths )
348364 print (f"\n Converting { len (source_paths )} sources: { sources_list } " )
349365 if has_core :
350- print ("(Claude Code plugin will include only core rules)" )
366+ print ("(Agent Skills will include only core rules)" )
351367 print ()
352-
368+
353369 # Convert all sources
354370 aggregated = {"success" : [], "errors" : [], "skipped" : []}
355371 # Parse comma-separated tags and normalize to lowercase
356372 filter_tags = None
357373 if cli_args .tags :
358- filter_tags = [tag .strip ().lower () for tag in cli_args .tags .split ("," ) if tag .strip ()]
359-
374+ filter_tags = [
375+ tag .strip ().lower () for tag in cli_args .tags .split ("," ) if tag .strip ()
376+ ]
377+
360378 # Print tag filter info if active
361379 if filter_tags :
362- print (f"Tag filter active: { ', ' .join (filter_tags )} (AND logic - rules must have all tags)\n " )
363-
380+ print (
381+ f"Tag filter active: { ', ' .join (filter_tags )} (AND logic - rules must have all tags)\n "
382+ )
383+
364384 for source_path in source_paths :
365385 is_core = source_path == Path ("sources/core" )
366-
386+
367387 print (f"Processing: { source_path } " )
368388 results = convert_rules (
369- str (source_path ),
370- cli_args .output_dir ,
371- include_claudecode = is_core ,
389+ str (source_path ),
390+ cli_args .output_dir ,
391+ include_agentskills = is_core ,
372392 version = version ,
373- filter_tags = filter_tags
393+ filter_tags = filter_tags ,
374394 )
375-
395+
376396 aggregated ["success" ].extend (results ["success" ])
377397 aggregated ["errors" ].extend (results ["errors" ])
378398 if "skipped" in results :
379399 aggregated ["skipped" ].extend (results ["skipped" ])
380400 print ("" )
381-
401+
382402 if aggregated ["errors" ]:
383403 print ("❌ Some conversions failed" )
384404 sys .exit (1 )
385-
405+
386406 print ("✅ All conversions successful" )
0 commit comments