Skip to content

Commit b77ca57

Browse files
WOLIKIMCHENGroot
andauthored
Fix Alquimia argument hints after folded descriptions (#4063)
Co-authored-by: root <kinsonnee@gmail.com>
1 parent c1bceb6 commit b77ca57

2 files changed

Lines changed: 121 additions & 2 deletions

File tree

src/specify_cli/integrations/alquimia/__init__.py

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,16 @@ def _build_skill_fm(self, name: str, description: str, source: str) -> dict:
6565

6666
@staticmethod
6767
def inject_argument_hint(content: str, hint: str) -> str:
68-
"""Insert ``argument-hint`` after the first ``description:`` in YAML frontmatter.
68+
"""Insert ``argument-hint`` after the ``description:`` scalar in YAML frontmatter.
69+
70+
A long ``description`` gets folded by the YAML dumper across
71+
indented continuation lines (plain or quoted), and an embedded
72+
paragraph break can add unindented blank lines inside a quoted
73+
scalar. Inserting the new line right after the *first* line of
74+
that scalar — instead of after the whole scalar — either produces
75+
invalid YAML or gets silently absorbed into the description
76+
string (#4044), so every continuation line (indented, or blank)
77+
is skipped first.
6978
7079
Skips injection if ``argument-hint:`` already exists in the
7180
frontmatter to avoid duplicate keys.
@@ -88,15 +97,29 @@ def inject_argument_hint(content: str, hint: str) -> str:
8897
in_fm = False
8998
dash_count = 0
9099
injected = False
91-
for line in lines:
100+
i = 0
101+
n = len(lines)
102+
while i < n:
103+
line = lines[i]
92104
stripped = line.rstrip("\n\r")
93105
if stripped == "---":
94106
dash_count += 1
95107
in_fm = dash_count == 1
96108
out.append(line)
109+
i += 1
97110
continue
98111
if in_fm and not injected and stripped.startswith("description:"):
99112
out.append(line)
113+
i += 1
114+
# Skip folded/quoted continuation lines before inserting
115+
# so the new key lands after the description scalar ends.
116+
# Blank lines count too: PyYAML emits unindented blank
117+
# lines for embedded "\n\n" inside a quoted scalar.
118+
while i < n and (
119+
lines[i][:1] in (" ", "\t") or lines[i].rstrip("\r\n") == ""
120+
):
121+
out.append(lines[i])
122+
i += 1
100123
# Preserve the exact line-ending style (\r\n vs \n)
101124
if line.endswith("\r\n"):
102125
eol = "\r\n"
@@ -109,6 +132,7 @@ def inject_argument_hint(content: str, hint: str) -> str:
109132
injected = True
110133
continue
111134
out.append(line)
135+
i += 1
112136
return "".join(out)
113137

114138
@staticmethod

tests/integrations/test_integration_alquimia.py

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -471,6 +471,101 @@ def test_inject_argument_hint_skips_if_already_present(self):
471471
hint_count = sum(1 for ln in lines if ln.startswith("argument-hint:"))
472472
assert hint_count == 1
473473

474+
def test_inject_argument_hint_survives_folded_description(self):
475+
"""A long description folded across lines must not corrupt the YAML (#4044).
476+
477+
A description long enough for the YAML dumper to fold it into a
478+
multi-line plain scalar previously had ``argument-hint:`` spliced
479+
into the *middle* of that scalar, producing invalid YAML.
480+
"""
481+
from specify_cli.integrations.alquimia import AlquimiaAIIntegration
482+
483+
frontmatter = {
484+
"name": "speckit-specify",
485+
"description": (
486+
"Create or update the feature specification from a natural "
487+
"language feature description. Also accepts an issue URL "
488+
"resolved via gh CLI (demo customization)."
489+
),
490+
"compatibility": "Requires spec-kit project structure with .specify/ directory",
491+
}
492+
frontmatter_text = yaml.safe_dump(
493+
frontmatter, sort_keys=False, allow_unicode=True
494+
).strip()
495+
content = f"---\n{frontmatter_text}\n---\n\nBody text\n"
496+
assert "\n " in content, "fixture description must actually fold across lines"
497+
498+
result = AlquimiaAIIntegration.inject_argument_hint(
499+
content, "Describe the feature"
500+
)
501+
502+
parsed = yaml.safe_load(result.split("---")[1])
503+
assert parsed["argument-hint"] == "Describe the feature"
504+
assert parsed["description"] == frontmatter["description"]
505+
506+
def test_inject_argument_hint_survives_quoted_folded_description(self):
507+
"""A folded description forced into quotes must not absorb the hint (#4044)."""
508+
from specify_cli.integrations.alquimia import AlquimiaAIIntegration
509+
510+
frontmatter = {
511+
"name": "speckit-specify",
512+
"description": (
513+
"Create or update the feature specification from a natural "
514+
"language feature description. Also accepts a GitHub "
515+
"issue/PR URL or #N reference resolved via gh CLI (demo)."
516+
),
517+
"compatibility": "Requires spec-kit project structure with .specify/ directory",
518+
}
519+
frontmatter_text = yaml.safe_dump(
520+
frontmatter, sort_keys=False, allow_unicode=True
521+
).strip()
522+
content = f"---\n{frontmatter_text}\n---\n\nBody text\n"
523+
assert "\n " in content, "fixture description must actually fold across lines"
524+
525+
result = AlquimiaAIIntegration.inject_argument_hint(
526+
content, "Describe the feature"
527+
)
528+
529+
parsed = yaml.safe_load(result.split("---")[1])
530+
assert parsed["argument-hint"] == "Describe the feature"
531+
assert parsed["description"] == frontmatter["description"]
532+
533+
def test_inject_argument_hint_survives_multi_paragraph_description(self):
534+
"""A description with an embedded blank line must not absorb the hint.
535+
536+
PyYAML serializes an embedded ``\\n\\n`` inside a quoted scalar as
537+
unindented blank lines, not indented ones, so a fix that only skips
538+
indented continuation lines still fails on this case.
539+
"""
540+
from specify_cli.integrations.alquimia import AlquimiaAIIntegration
541+
542+
frontmatter = {
543+
"name": "speckit-specify",
544+
"description": (
545+
"First paragraph of a fairly long description that will "
546+
"need to wrap across multiple lines when dumped by PyYAML."
547+
"\n\n"
548+
"Second paragraph continues the description after a blank "
549+
"line separator to force embedded newlines in the scalar."
550+
),
551+
"compatibility": "Requires spec-kit project structure with .specify/ directory",
552+
}
553+
frontmatter_text = yaml.safe_dump(
554+
frontmatter, sort_keys=False, allow_unicode=True
555+
).strip()
556+
content = f"---\n{frontmatter_text}\n---\n\nBody text\n"
557+
assert "\n\n" in frontmatter_text, (
558+
"fixture must produce a blank continuation line"
559+
)
560+
561+
result = AlquimiaAIIntegration.inject_argument_hint(
562+
content, "Describe the feature"
563+
)
564+
565+
parsed = yaml.safe_load(result.split("---")[1])
566+
assert parsed["argument-hint"] == "Describe the feature"
567+
assert parsed["description"] == frontmatter["description"]
568+
474569

475570
class TestAlquimiaDisableModelInvocation:
476571
"""Verify disable-model-invocation is false for Alquimia skills."""

0 commit comments

Comments
 (0)