55from pathlib import Path
66from typing import Any
77
8- import re
9-
108import yaml
119
10+ from .skill_postprocess import (
11+ ARGUMENT_HINTS ,
12+ apply_claude_skill_postprocess ,
13+ inject_argument_hint ,
14+ inject_frontmatter_flag ,
15+ inject_hook_command_note ,
16+ set_frontmatter_key ,
17+ )
1218from ..base import SkillsIntegration
1319from ..manifest import IntegrationManifest
1420
15- # Note injected into hook sections so Claude maps dot-notation command
16- # names (from extensions.yml) to the hyphenated skill names it uses.
17- _HOOK_COMMAND_NOTE = (
18- "- When constructing slash commands from hook command names, "
19- "replace dots (`.`) with hyphens (`-`). "
20- "For example, `speckit.git.commit` → `/speckit-git-commit`.\n "
21- )
22-
23- # Mapping of command template stem → argument-hint text shown inline
24- # when a user invokes the slash command in Claude Code.
25- ARGUMENT_HINTS : dict [str , str ] = {
26- "specify" : "Describe the feature you want to specify" ,
27- "plan" : "Optional guidance for the planning phase" ,
28- "tasks" : "Optional task generation constraints" ,
29- "implement" : "Optional implementation guidance or task filter" ,
30- "analyze" : "Optional focus areas for analysis" ,
31- "clarify" : "Optional areas to clarify in the spec" ,
32- "constitution" : "Principles or values for the project constitution" ,
33- "checklist" : "Domain or focus area for the checklist" ,
34- "taskstoissues" : "Optional filter or label for GitHub issues" ,
35- }
36-
3721
3822class ClaudeIntegration (SkillsIntegration ):
3923 """Integration for Claude Code skills."""
@@ -56,51 +40,18 @@ class ClaudeIntegration(SkillsIntegration):
5640
5741 @staticmethod
5842 def inject_argument_hint (content : str , hint : str ) -> str :
59- """Insert ``argument-hint`` after the first ``description:`` in YAML frontmatter.
43+ """Delegate to shared Claude skill transform implementation."""
44+ return inject_argument_hint (content , hint )
6045
61- Skips injection if ``argument-hint:`` already exists in the
62- frontmatter to avoid duplicate keys.
63- """
64- lines = content .splitlines (keepends = True )
65-
66- # Pre-scan: bail out if argument-hint already present in frontmatter
67- dash_count = 0
68- for line in lines :
69- stripped = line .rstrip ("\n \r " )
70- if stripped == "---" :
71- dash_count += 1
72- if dash_count == 2 :
73- break
74- continue
75- if dash_count == 1 and stripped .startswith ("argument-hint:" ):
76- return content # already present
77-
78- out : list [str ] = []
79- in_fm = False
80- dash_count = 0
81- injected = False
82- for line in lines :
83- stripped = line .rstrip ("\n \r " )
84- if stripped == "---" :
85- dash_count += 1
86- in_fm = dash_count == 1
87- out .append (line )
88- continue
89- if in_fm and not injected and stripped .startswith ("description:" ):
90- out .append (line )
91- # Preserve the exact line-ending style (\r\n vs \n)
92- if line .endswith ("\r \n " ):
93- eol = "\r \n "
94- elif line .endswith ("\n " ):
95- eol = "\n "
96- else :
97- eol = ""
98- escaped = hint .replace ("\\ " , "\\ \\ " ).replace ('"' , '\\ "' )
99- out .append (f'argument-hint: "{ escaped } "{ eol } ' )
100- injected = True
101- continue
102- out .append (line )
103- return "" .join (out )
46+ @staticmethod
47+ def _inject_frontmatter_flag (content : str , key : str , value : str = "true" ) -> str :
48+ """Delegate to shared Claude skill transform implementation."""
49+ return inject_frontmatter_flag (content , key , value )
50+
51+ @staticmethod
52+ def _inject_hook_command_note (content : str ) -> str :
53+ """Delegate to shared Claude skill transform implementation."""
54+ return inject_hook_command_note (content )
10455
10556 def _render_skill (self , template_name : str , frontmatter : dict [str , Any ], body : str ) -> str :
10657 """Render a processed command template as a Claude skill."""
@@ -121,95 +72,35 @@ def _build_skill_fm(self, name: str, description: str, source: str) -> dict:
12172 self .key , name , description , source
12273 )
12374
124- @staticmethod
125- def _inject_frontmatter_flag (content : str , key : str , value : str = "true" ) -> str :
126- """Insert ``key: value`` before the closing ``---`` if not already present."""
127- lines = content .splitlines (keepends = True )
128-
129- # Pre-scan: bail out if already present in frontmatter
130- dash_count = 0
131- for line in lines :
132- stripped = line .rstrip ("\n \r " )
133- if stripped == "---" :
134- dash_count += 1
135- if dash_count == 2 :
136- break
137- continue
138- if dash_count == 1 and stripped .startswith (f"{ key } :" ):
139- return content
140-
141- # Inject before the closing --- of frontmatter
142- out : list [str ] = []
143- dash_count = 0
144- injected = False
145- for line in lines :
146- stripped = line .rstrip ("\n \r " )
147- if stripped == "---" :
148- dash_count += 1
149- if dash_count == 2 and not injected :
150- if line .endswith ("\r \n " ):
151- eol = "\r \n "
152- elif line .endswith ("\n " ):
153- eol = "\n "
154- else :
155- eol = ""
156- out .append (f"{ key } : { value } { eol } " )
157- injected = True
158- out .append (line )
159- return "" .join (out )
160-
161- @staticmethod
162- def _inject_hook_command_note (content : str ) -> str :
163- """Insert a dot-to-hyphen note before each hook output instruction.
75+ def post_process_skill_content (self , content : str ) -> str :
76+ """Inject Claude-specific frontmatter flags and hook notes (no argument-hint).
16477
165- Targets the line ``- For each executable hook, output the following``
166- and inserts the note on the line before it, matching its indentation.
167- Skips if the note is already present.
78+ Used by preset/extension skill generators; matches flags applied during
79+ ``setup()`` except for fenced question rendering and argument-hint lines.
16880 """
169- if "replace dots" in content :
170- return content
171-
172- def repl (m : re .Match [str ]) -> str :
173- indent = m .group (1 )
174- instruction = m .group (2 )
175- eol = m .group (3 )
176- return (
177- indent
178- + _HOOK_COMMAND_NOTE .rstrip ("\n " )
179- + eol
180- + indent
181- + instruction
182- + eol
183- )
184-
185- return re .sub (
186- r"(?m)^(\s*)(- For each executable hook, output the following[^\r\n]*)(\r\n|\n|$)" ,
187- repl ,
188- content ,
189- )
190-
191- def post_process_skill_content (self , content : str ) -> str :
192- """Inject Claude-specific frontmatter flags and hook notes."""
193- updated = self ._inject_frontmatter_flag (content , "user-invocable" )
194- updated = self ._inject_frontmatter_flag (updated , "disable-model-invocation" , "false" )
195- updated = self ._inject_hook_command_note (updated )
81+ updated = inject_frontmatter_flag (content , "user-invocable" )
82+ updated = set_frontmatter_key (updated , "disable-model-invocation" , "false" )
83+ updated = inject_hook_command_note (updated )
19684 return updated
19785
86+ @classmethod
87+ def render_skill_postprocess (cls , content : str , skill_path : Path ) -> str :
88+ """Run Claude-specific skill post-processing pipeline."""
89+ return apply_claude_skill_postprocess (content , skill_path )
90+
19891 def setup (
19992 self ,
20093 project_root : Path ,
20194 manifest : IntegrationManifest ,
20295 parsed_options : dict [str , Any ] | None = None ,
20396 ** opts : Any ,
20497 ) -> list [Path ]:
205- """Install Claude skills, then inject Claude-specific flags and argument-hints ."""
98+ """Install Claude skills, then run the skill post-process extension chain ."""
20699 created = super ().setup (project_root , manifest , parsed_options , ** opts )
207100
208- # Post-process generated skill files
209101 skills_dir = self .skills_dest (project_root ).resolve ()
210102
211103 for path in created :
212- # Only touch SKILL.md files under the skills directory
213104 try :
214105 path .resolve ().relative_to (skills_dir )
215106 except ValueError :
@@ -220,16 +111,7 @@ def setup(
220111 content_bytes = path .read_bytes ()
221112 content = content_bytes .decode ("utf-8" )
222113
223- updated = self .post_process_skill_content (content )
224-
225- # Inject argument-hint if available for this skill
226- skill_dir_name = path .parent .name # e.g. "speckit-plan"
227- stem = skill_dir_name
228- if stem .startswith ("speckit-" ):
229- stem = stem [len ("speckit-" ):]
230- hint = ARGUMENT_HINTS .get (stem , "" )
231- if hint :
232- updated = self .inject_argument_hint (updated , hint )
114+ updated = self .render_skill_postprocess (content , path )
233115
234116 if updated != content :
235117 path .write_bytes (updated .encode ("utf-8" ))
0 commit comments