Skip to content

Commit d052cd0

Browse files
committed
fix: address eighteenth round of Copilot PR review feedback
- Add public register_commands_for_non_skill_agents() on CommandRegistrar: reconciliation no longer accesses private _ensure_configs() or iterates AGENT_CONFIGS directly - Cache parsed preset manifests in PresetResolver: _get_manifest() caches by pack_dir to avoid re-parsing YAML on each collect_all_layers() call - Fix frontmatter detection for empty dicts: detect based on --- fence presence rather than parsed dict truthiness, so empty frontmatter blocks are properly stripped during command composition
1 parent 74313f3 commit d052cd0

2 files changed

Lines changed: 80 additions & 36 deletions

File tree

src/specify_cli/agents.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -620,6 +620,46 @@ def register_commands_for_all_agents(
620620

621621
return results
622622

623+
def register_commands_for_non_skill_agents(
624+
self,
625+
commands: List[Dict[str, Any]],
626+
source_id: str,
627+
source_dir: Path,
628+
project_root: Path,
629+
) -> Dict[str, List[str]]:
630+
"""Register commands for all non-skill agents in the project.
631+
632+
Like register_commands_for_all_agents but skips skill-based agents
633+
(those with extension '/SKILL.md'). Used by reconciliation to avoid
634+
overwriting properly formatted SKILL.md files.
635+
636+
Args:
637+
commands: List of command info dicts
638+
source_id: Identifier of the source
639+
source_dir: Directory containing command source files
640+
project_root: Path to project root
641+
642+
Returns:
643+
Dictionary mapping agent names to list of registered commands
644+
"""
645+
results = {}
646+
self._ensure_configs()
647+
for agent_name, agent_config in self.AGENT_CONFIGS.items():
648+
if agent_config.get("extension") == "/SKILL.md":
649+
continue
650+
agent_dir = project_root / agent_config["dir"]
651+
if agent_dir.exists():
652+
try:
653+
registered = self.register_commands(
654+
agent_name, commands, source_id,
655+
source_dir, project_root,
656+
)
657+
if registered:
658+
results[agent_name] = registered
659+
except ValueError:
660+
continue
661+
return results
662+
623663
def unregister_commands(
624664
self, registered_commands: Dict[str, List[str]], project_root: Path
625665
) -> None:

src/specify_cli/presets.py

Lines changed: 40 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -777,19 +777,9 @@ def _register_for_non_skill_agents(
777777
Used during reconciliation to avoid overwriting properly formatted
778778
SKILL.md files that were written by _register_skills().
779779
"""
780-
registrar._ensure_configs()
781-
for agent_name, agent_config in registrar.AGENT_CONFIGS.items():
782-
if agent_config.get("extension") == "/SKILL.md":
783-
continue
784-
agent_dir = self.project_root / agent_config["dir"]
785-
if agent_dir.exists():
786-
try:
787-
registrar.register_commands(
788-
agent_name, commands, source_id,
789-
source_dir, self.project_root,
790-
)
791-
except ValueError:
792-
continue
780+
registrar.register_commands_for_non_skill_agents(
781+
commands, source_id, source_dir, self.project_root
782+
)
793783

794784
def _reconcile_skills(self, command_names: List[str]) -> None:
795785
"""Re-register skills for commands whose winning layer changed.
@@ -2018,6 +2008,21 @@ def __init__(self, project_root: Path):
20182008
self.presets_dir = project_root / ".specify" / "presets"
20192009
self.overrides_dir = self.templates_dir / "overrides"
20202010
self.extensions_dir = project_root / ".specify" / "extensions"
2011+
self._manifest_cache: Dict[str, Optional["PresetManifest"]] = {}
2012+
2013+
def _get_manifest(self, pack_dir: Path) -> Optional["PresetManifest"]:
2014+
"""Get a cached preset manifest, parsing it on first access."""
2015+
key = str(pack_dir)
2016+
if key not in self._manifest_cache:
2017+
manifest_path = pack_dir / "preset.yml"
2018+
if manifest_path.exists():
2019+
try:
2020+
self._manifest_cache[key] = PresetManifest(manifest_path)
2021+
except PresetValidationError:
2022+
self._manifest_cache[key] = None
2023+
else:
2024+
self._manifest_cache[key] = None
2025+
return self._manifest_cache[key]
20212026

20222027
def _get_all_extensions_by_priority(self) -> list[tuple[int, str, dict | None]]:
20232028
"""Build unified list of registered and unregistered extensions sorted by priority.
@@ -2285,20 +2290,14 @@ def _find_in_subdirs(base_dir: Path) -> Optional[Path]:
22852290
# Read strategy and manifest file path from preset manifest
22862291
strategy = "replace"
22872292
manifest_file_path = None
2288-
manifest_path = pack_dir / "preset.yml"
2289-
if manifest_path.exists():
2290-
try:
2291-
manifest = PresetManifest(manifest_path)
2292-
for tmpl in manifest.templates:
2293-
if (tmpl.get("name") == template_name
2294-
and tmpl.get("type") == template_type):
2295-
strategy = tmpl.get("strategy", "replace")
2296-
manifest_file_path = tmpl.get("file")
2297-
break
2298-
except PresetValidationError:
2299-
# Invalid manifest — fall back to default "replace"
2300-
# strategy so layer resolution still works.
2301-
pass
2293+
manifest = self._get_manifest(pack_dir)
2294+
if manifest:
2295+
for tmpl in manifest.templates:
2296+
if (tmpl.get("name") == template_name
2297+
and tmpl.get("type") == template_type):
2298+
strategy = tmpl.get("strategy", "replace")
2299+
manifest_file_path = tmpl.get("file")
2300+
break
23022301
# Use manifest file path if specified, otherwise convention-based lookup
23032302
candidate = None
23042303
if manifest_file_path:
@@ -2444,15 +2443,20 @@ def resolve_content(
24442443
top_frontmatter_text = None
24452444

24462445
def _split_frontmatter(text: str) -> tuple:
2447-
"""Return (frontmatter_block_with_fences, body) or (None, text)."""
2448-
from .agents import CommandRegistrar
2449-
fm_dict, body = CommandRegistrar.parse_frontmatter(text)
2450-
if fm_dict:
2451-
# Re-extract the raw frontmatter block (text before body)
2452-
end = text.find("---", 3)
2453-
fm_block = text[:end + 3]
2454-
return fm_block, body
2455-
return None, text
2446+
"""Return (frontmatter_block_with_fences, body) or (None, text).
2447+
2448+
Detects frontmatter by the presence of ``---`` fences rather than
2449+
whether the parsed YAML has keys, so empty frontmatter blocks
2450+
(``---\\n---``) are also stripped.
2451+
"""
2452+
if not text.startswith("---"):
2453+
return None, text
2454+
end = text.find("---", 3)
2455+
if end == -1:
2456+
return None, text
2457+
fm_block = text[:end + 3]
2458+
body = text[end + 3:].strip()
2459+
return fm_block, body
24562460

24572461
if is_command:
24582462
fm, body = _split_frontmatter(content)

0 commit comments

Comments
 (0)