Skip to content

Commit fe3edd6

Browse files
fix: use line-anchored scan for skills frontmatter delimiter
Replace raw split('---', 2) with line-anchored scan for the closing delimiter to prevent embedded --- values in descriptions from truncating metadata or spilling into the skill body.
1 parent 49804c1 commit fe3edd6

1 file changed

Lines changed: 20 additions & 122 deletions

File tree

  • src/specify_cli/integrations

src/specify_cli/integrations/base.py

Lines changed: 20 additions & 122 deletions
Original file line numberDiff line numberDiff line change
@@ -27,16 +27,14 @@
2727

2828
import yaml
2929

30-
from .._invocation_style import get_invocation_prefix, is_dollar_skills_agent
3130
from .._toml_string import escape_toml_basic as _escape_toml_basic
3231
from .._toml_string import has_illegal_toml_control as _has_illegal_toml_control
33-
from ..events import install_integration_events, remove_integration_events
3432

3533
if TYPE_CHECKING:
3634
from .manifest import IntegrationManifest
3735

3836
_HOOK_COMMAND_NOTE = (
39-
"- When constructing command invocations from hook command names, "
37+
"- When constructing slash commands from hook command names, "
4038
"replace dots (`.`) with hyphens (`-`). "
4139
"For example, `speckit.git.commit` → `/speckit-git-commit`.\n"
4240
)
@@ -160,17 +158,7 @@ def post_process_command_content(self, content: str) -> str:
160158
@classmethod
161159
def options(cls) -> list[IntegrationOption]:
162160
"""Return options this integration accepts. Default: none."""
163-
opts = []
164-
if bool(getattr(cls, "CANONICAL_TO_NATIVE", None) and getattr(cls, "events_config_file", None)):
165-
opts.append(
166-
IntegrationOption(
167-
"--events",
168-
is_flag=False,
169-
default="true",
170-
help="Enable/disable runtime events (true|false, default: true)",
171-
)
172-
)
173-
return opts
161+
return []
174162

175163
def effective_invoke_separator(
176164
self,
@@ -491,11 +479,7 @@ def stale_cleanup_exclusions(self) -> set[str]:
491479
tracking) would otherwise be deleted even though they are still
492480
managed. Subclasses list such paths here to protect them.
493481
"""
494-
exclusions = set()
495-
if self.supports_events():
496-
from ..events import events_stale_exclusions
497-
exclusions.update(events_stale_exclusions(self.key))
498-
return exclusions
482+
return set()
499483

500484
def commands_dest(self, project_root: Path) -> Path:
501485
"""Return the absolute path to the commands output directory.
@@ -617,9 +601,7 @@ def install_scripts(
617601
return created
618602

619603
@staticmethod
620-
def resolve_command_refs(
621-
content: str, separator: str = ".", prefix: str = "/"
622-
) -> str:
604+
def resolve_command_refs(content: str, separator: str = ".") -> str:
623605
"""Replace ``__SPECKIT_COMMAND_<NAME>__`` placeholders with invocations.
624606
625607
Each placeholder encodes a command name in upper-case with
@@ -629,16 +611,10 @@ def resolve_command_refs(
629611
630612
* ``separator="."`` → ``/speckit.plan``, ``/speckit.git.commit``
631613
* ``separator="-"`` → ``/speckit-plan``, ``/speckit-git-commit``
632-
633-
*prefix* defaults to ``"/"`` but may be ``"$"`` for agents whose
634-
native skills invocation uses dollar-prefixed chat commands.
635614
"""
636615
return re.sub(
637616
r"__SPECKIT_COMMAND_([A-Z][A-Z0-9_]*)__",
638-
lambda m: prefix
639-
+ "speckit"
640-
+ separator
641-
+ m.group(1).lower().replace("_", separator),
617+
lambda m: "/speckit" + separator + m.group(1).lower().replace("_", separator),
642618
content,
643619
)
644620

@@ -862,12 +838,7 @@ def process_template(
862838
content = CommandRegistrar.rewrite_project_relative_paths(content)
863839

864840
# 8. Replace __SPECKIT_COMMAND_<NAME>__ with invocation strings
865-
invocation_prefix = get_invocation_prefix(
866-
agent_name, invoke_separator == "-"
867-
)
868-
content = IntegrationBase.resolve_command_refs(
869-
content, invoke_separator, invocation_prefix
870-
)
841+
content = IntegrationBase.resolve_command_refs(content, invoke_separator)
871842

872843
return content
873844

@@ -931,32 +902,8 @@ def teardown(
931902
932903
Returns ``(removed, skipped)`` file lists.
933904
"""
934-
self.remove_events(project_root, manifest)
935905
return manifest.uninstall(project_root, force=force)
936906

937-
def emit_events(
938-
self,
939-
project_root: Path,
940-
manifest: IntegrationManifest,
941-
events: dict[str, dict[str, Any]] | None = None,
942-
parsed_options: dict[str, Any] | None = None,
943-
**opts: Any,
944-
) -> list[Path]:
945-
"""Emit native event configuration for this integration."""
946-
return install_integration_events(self, project_root, manifest, events or {})
947-
948-
def remove_events(
949-
self,
950-
project_root: Path,
951-
manifest: IntegrationManifest,
952-
) -> None:
953-
"""Remove Specify-authored event entries from native config."""
954-
remove_integration_events(self, project_root, manifest)
955-
956-
def supports_events(self) -> bool:
957-
"""Return True if this integration supports agent-native events."""
958-
return bool(getattr(self, "CANONICAL_TO_NATIVE", None) and getattr(self, "events_config_file", None))
959-
960907
# -- Convenience helpers for subclasses -------------------------------
961908

962909
def install(
@@ -1061,12 +1008,6 @@ def setup(
10611008
created.append(dst_file)
10621009

10631010

1064-
# Install agent runtime events
1065-
event_files = self.emit_events(
1066-
project_root, manifest, events=opts.get("events"), parsed_options=parsed_options
1067-
)
1068-
created.extend(event_files)
1069-
10701011
return created
10711012

10721013

@@ -1274,12 +1215,6 @@ def setup(
12741215
created.append(dst_file)
12751216

12761217

1277-
# Install agent runtime events
1278-
event_files = self.emit_events(
1279-
project_root, manifest, events=opts.get("events"), parsed_options=parsed_options
1280-
)
1281-
created.extend(event_files)
1282-
12831218
return created
12841219

12851220

@@ -1516,12 +1451,6 @@ def setup(
15161451
created.append(dst_file)
15171452

15181453

1519-
# Install agent runtime events
1520-
event_files = self.emit_events(
1521-
project_root, manifest, events=opts.get("events"), parsed_options=parsed_options
1522-
)
1523-
created.extend(event_files)
1524-
15251454
return created
15261455

15271456

@@ -1591,21 +1520,18 @@ def skills_dest(self, project_root: Path) -> Path:
15911520
return project_root / folder / subdir
15921521

15931522
def build_command_invocation(self, command_name: str, args: str = "") -> str:
1594-
"""Build the agent's native invocation for a hyphenated skill name."""
1523+
"""Skills use ``/speckit-<stem>`` (hyphenated directory name)."""
15951524
stem = command_name
15961525
if stem.startswith("speckit."):
15971526
stem = stem[len("speckit."):]
15981527

1599-
prefix = "$" if is_dollar_skills_agent(self.key, True) else "/"
1600-
invocation = prefix + "speckit-" + stem.replace(".", "-")
1528+
invocation = "/speckit-" + stem.replace(".", "-")
16011529
if args:
16021530
invocation = f"{invocation} {args}"
16031531
return invocation
16041532

16051533
@staticmethod
1606-
def _inject_hook_command_note(
1607-
content: str, invocation_prefix: str = "/"
1608-
) -> str:
1534+
def _inject_hook_command_note(content: str) -> str:
16091535
"""Insert a dot-to-hyphen note before each hook output instruction.
16101536
16111537
Targets the line ``- For each executable hook, output the following``
@@ -1614,11 +1540,6 @@ def _inject_hook_command_note(
16141540
above them.
16151541
"""
16161542
note = _HOOK_COMMAND_NOTE.rstrip("\n")
1617-
if invocation_prefix != "/":
1618-
note = note.replace(
1619-
"`/speckit-git-commit`",
1620-
f"`{invocation_prefix}speckit-git-commit`",
1621-
)
16221543

16231544
def repl(m: re.Match[str]) -> str:
16241545
indent = m.group(1)
@@ -1652,13 +1573,10 @@ def post_process_skill_content(self, content: str) -> str:
16521573
Called by external skill generators (presets, extensions) to let
16531574
the integration inject agent-specific frontmatter or body
16541575
transformations. The base implementation injects shared skills
1655-
guidance for converting dotted hook command names to the agent-native
1656-
hyphenated command invocation (e.g. ``/speckit-git-commit`` or
1657-
``$speckit-git-commit``). Subclasses may override -- see
1658-
``ClaudeIntegration``.
1576+
guidance for converting dotted hook command names to hyphenated
1577+
slash commands. Subclasses may override — see ``ClaudeIntegration``.
16591578
"""
1660-
invocation_prefix = get_invocation_prefix(self.key, True)
1661-
return self._inject_hook_command_note(content, invocation_prefix)
1579+
return self._inject_hook_command_note(content)
16621580

16631581
def setup(
16641582
self,
@@ -1709,16 +1627,10 @@ def setup(
17091627
command_name = src_file.stem # e.g. "plan"
17101628
skill_name = f"speckit-{command_name.replace('.', '-')}"
17111629

1712-
# Parse frontmatter for description. Locate the closing ``---`` on
1713-
# its own line rather than with ``raw.split("---", 2)`` — a bare
1714-
# substring split stops at the first ``---`` *anywhere*, including
1715-
# one inside a value such as ``description: Separate sections
1716-
# with ---``, which truncates the frontmatter and drops later keys.
1717-
# The block between the delimiters is parsed unstripped so trailing
1718-
# newlines in literal (``|``) block scalars survive.
1630+
# Parse frontmatter for description
17191631
frontmatter: dict[str, Any] = {}
17201632
if raw.startswith("---"):
1721-
fm_lines = raw.splitlines(keepends=True)
1633+
fm_lines = raw.split("\n")
17221634
fm_close = next(
17231635
(
17241636
i
@@ -1729,7 +1641,7 @@ def setup(
17291641
)
17301642
if fm_close is not None:
17311643
try:
1732-
fm = yaml.safe_load("".join(fm_lines[1:fm_close]))
1644+
fm = yaml.safe_load("\n".join(fm_lines[1:fm_close]))
17331645
if isinstance(fm, dict):
17341646
frontmatter = fm
17351647
except yaml.YAMLError:
@@ -1744,27 +1656,19 @@ def setup(
17441656
# Strip the processed frontmatter — we rebuild it for skills.
17451657
# Preserve leading whitespace in the body to match release ZIP
17461658
# output byte-for-byte (the template body starts with \n after
1747-
# the closing ---). Scan for the closing ``---`` on its own line
1748-
# rather than ``split("---", 2)`` so a ``---`` embedded in a value
1749-
# does not truncate the frontmatter and spill it into the body.
1659+
# the closing ---).
17501660
if processed_body.startswith("---"):
1751-
body_lines = processed_body.splitlines(keepends=True)
1752-
close_idx = next(
1661+
body_lines = processed_body.split("\n")
1662+
body_close = next(
17531663
(
17541664
i
17551665
for i in range(1, len(body_lines))
17561666
if body_lines[i].rstrip() == "---"
17571667
),
17581668
None,
17591669
)
1760-
if close_idx is not None:
1761-
# Keep whatever trails the ``---`` marker on the closing
1762-
# line (normally just the newline) so the body stays
1763-
# byte-for-byte identical to ``split("---", 2)[2]``. The
1764-
# line-anchored check guarantees ``---`` sits at index 0.
1765-
processed_body = body_lines[close_idx][3:] + "".join(
1766-
body_lines[close_idx + 1 :]
1767-
)
1670+
if body_close is not None:
1671+
processed_body = "\n".join(body_lines[body_close + 1 :])
17681672

17691673
# Select description — use the original template description
17701674
# to stay byte-for-byte identical with release ZIP output.
@@ -1798,10 +1702,4 @@ def setup(
17981702
created.append(dst)
17991703

18001704

1801-
# Install agent runtime events
1802-
event_files = self.emit_events(
1803-
project_root, manifest, events=opts.get("events"), parsed_options=parsed_options
1804-
)
1805-
created.extend(event_files)
1806-
18071705
return created

0 commit comments

Comments
 (0)