fix(presets): preserve argument-hint in preset SKILL.md generation#2978
fix(presets): preserve argument-hint in preset SKILL.md generation#2978jawwad-ali wants to merge 2 commits into
Conversation
There was a problem hiding this comment.
Pull request overview
This PR ensures argument-hint declared in preset-provided (and preset-override/restore) command frontmatter is preserved in generated SKILL.md frontmatter for Claude, matching the earlier extension-path fix and preventing YAML breakage with folded descriptions.
Changes:
- Added
CommandRegistrar.apply_argument_hint(...)to copyargument-hintinto the skill frontmatter dict pre-serialization, gated on integrations that support it (Claude). - Wired the helper into all preset skill generation/restore paths and refactored the extensions path to reuse the shared helper.
- Added preset-focused regression tests verifying (1)
argument-hintis preserved for Claude and (2) not emitted for non-Claude skill agents.
Show a summary per file
| File | Description |
|---|---|
src/specify_cli/agents.py |
Introduces the shared apply_argument_hint helper on CommandRegistrar. |
src/specify_cli/presets.py |
Applies the helper across preset skill generation and restore paths to preserve argument-hint. |
src/specify_cli/extensions.py |
Refactors the existing extension-path injection to call the shared helper. |
tests/test_presets.py |
Adds regression tests covering argument-hint preservation for preset-generated skills. |
Copilot's findings
Tip
Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
- Files reviewed: 4/4 changed files
- Comments generated: 1
| if not isinstance(source_frontmatter, dict): | ||
| return | ||
| argument_hint = source_frontmatter.get("argument-hint") | ||
| if ( | ||
| argument_hint | ||
| and integration is not None | ||
| and hasattr(integration, "inject_argument_hint") | ||
| ): | ||
| skill_frontmatter["argument-hint"] = str(argument_hint) |
| def apply_argument_hint( | ||
| source_frontmatter: dict, | ||
| skill_frontmatter: dict, | ||
| integration: object, | ||
| ) -> None: |
|
Please address Copilot feedback |
Preset-provided and extension-override commands that declare `argument-hint:` in their frontmatter had it dropped from the generated Claude SKILL.md, and it was re-dropped when a preset was removed and its overridden skill restored. This is the preset-side analog of the extension fix in github#2903 / github#2916. Factor the argument-hint carry-over into a shared CommandRegistrar.apply_argument_hint() helper and apply it at the four preset skill-generation sites (register, reconcile override-restore, and the core/extension unregister-restore paths). The extension path from The helper writes argument-hint into the frontmatter dict before serialization (so a folded multi-line description cannot be split into invalid YAML) and only for integrations that support it (those exposing inject_argument_hint -- currently Claude), leaving build_skill_frontmatter's shared shape unchanged for every other agent. Core templates carry no argument-hint, so the core-restore path is a no-op. No behavior change for non-Claude agents or the core path. Add regression tests covering a folding description (Claude) and the non-Claude gate (codex). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…en apply_argument_hint annotations Add a symmetric isinstance(skill_frontmatter, dict) guard so the helper stays a safe no-op if a caller passes a non-dict, and annotate the parameters as Dict[str, Any] with an optional integration to match real call-site usage. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
51c2631 to
5670d63
Compare
|
Thanks for the review! Addressed in 5670d63:
I also rebased onto Disclosure: this PR and this comment were prepared with Claude Code (Claude Opus 4.8) under my direction; I reviewed the diff and ran the tests before pushing. |
Description
Follow-up to #2903 / #2916. That fix preserved
argument-hintfor extension commands when generating ClaudeSKILL.md; the same field is still dropped by the preset skill generators.When a preset-provided command (or a preset override of an extension command) declares
argument-hint:in its frontmatter, the generated.claude/skills/<name>/SKILL.mdloses it, because the preset generators build the skill frontmatter via the sharedCommandRegistrar.build_skill_frontmatter()(which only emitsname/description/compatibility/metadata) and never forwardargument-hint. It is also re-dropped on preset removal, when an overridden skill is restored from its source command — which silently undoes the #2916 extension fix in a real workflow (override an extension skill via a preset, then remove the preset).Change
Rather than copy the #2916 inline block to each site, this factors the carry-over into one helper and reuses it:
agents.py— newCommandRegistrar.apply_argument_hint(source_frontmatter, skill_frontmatter, integration). It copiesargument-hintfrom the parsed source command frontmatter into the skill frontmatter dict, beforeyaml.safe_dump(so a folded multi-linedescriptioncan't be split into invalid YAML — the failure mode fix(extensions): preserve argument-hint in extension Claude SKILL.md (#2903) #2916 documented), and only when the integration exposesinject_argument_hint.presets.py— applied at all fourbuild_skill_frontmattersites:_register_skills, the reconcile override-restore path, and the core/extension_unregister_skillsrestore paths.extensions.py— the fix(extensions): preserve argument-hint in extension Claude SKILL.md (#2903) #2916 inline block now calls the shared helper (no behavior change).Why it's safe (no behavior change for non-Claude agents or the core path)
inject_argument_hintis defined only onClaudeIntegration(not inherited), so thehasattrgate adds the key for Claude only —build_skill_frontmatter's shared shape is byte-identical for every other agent (codex, kimi, …).argument-hint(0 of 9templates/commands/*.md), so the core-template restore site is a deliberate no-op.get_integration(...)lookup is hoisted a few lines abovebuild_skill_frontmatterso the hint can be applied pre-serialization — is a pure reorder:get_integrationis a side-effect-free registry lookup, the same value still feedspost_process_skill_content, and there is no import cycle (it was already a method-local import).Scope
Kept focused per CONTRIBUTING. The init-time
agents.py::render_skill_commandgenerator also omitsargument-hint, but it takes anagent_name(no integration object) and so needs separate plumbing — deferred to a follow-up rather than widening this PR.Heads-up on overlapping PRs
Open PRs #2917 ("preserve non-ASCII characters in skill frontmatter") and #2103 touch the same
presets.pyskill-generation lines. The concerns are orthogonal —apply_argument_hintmutates the frontmatter dict before whatever dump function runs, so it composes cleanly with #2917'sdump_frontmatter()swap. Happy to rebase whichever lands first.Testing
uv run specify --help(exit 0)uv sync && uv run pytestDetails (Windows 11, Python 3.12.12):
uvx ruff check src/— All checks passeduv run pytest tests/test_presets.py tests/test_extension_skills.py tests/integrations/test_integration_claude.py -q— 397 passed, 2 skipped (the fix(extensions): preserve argument-hint in extension Claude SKILL.md (#2903) #2916 extension tests pass unchanged, confirming the refactor is behavior-preserving)uv run pytest— 3610 passed, 148 skipped, 14 failed; the 14 failures are pre-existing and environment-only (os.symlink→WinError 1314; creating symlinks needs elevation/Developer Mode on Windows), identical to cleanmainon this host and unrelated to this change.New tests in
tests/test_presets.py:test_argument_hint_preserved_for_preset_command— installs a preset command withargument-hintand a long, folding description; asserts the generatedSKILL.mdfrontmatter parses and retains bothargument-hintand the full description. (Fails onmainwithKeyError: 'argument-hint'.)test_argument_hint_not_added_for_non_claude_preset_command— same under a non-Claude skills agent (codex); assertsargument-hintis not present, proving the Claude-only gate.Manual sanity: this changes only the emitted
SKILL.mdfrontmatter — no slash command's behavior changes; generated files parse as valid YAML and surface the hint in Claude Code.AI Disclosure
Developed with Claude Code (Claude Opus 4.8) under my direction — identifying the parallel preset drop sites, factoring the shared helper, writing the regression tests, and running the verification above. I personally confirmed the no-op behavior for non-Claude agents and the core path, verified the override-restore reorder is behavior-preserving, and reviewed the full diff before submitting. I'll disclose if any review responses are AI-assisted as well.