Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/specify_cli/agents.py
Original file line number Diff line number Diff line change
Expand Up @@ -733,7 +733,7 @@ def _write_registered_output(
link_outputs: bool,
) -> None:
"""Write a rendered agent artifact, optionally as a dev-mode symlink."""
if not link_outputs:
if not link_outputs or (agent_name == "codex" and extension == "/SKILL.md"):
dest_file.write_text(content, encoding="utf-8")
return

Expand Down
5 changes: 3 additions & 2 deletions src/specify_cli/extensions.py
Original file line number Diff line number Diff line change
Expand Up @@ -1012,13 +1012,14 @@ def _register_extension_skills(
skill_file = skill_subdir / "SKILL.md"
cache_root = extension_dir / ".specify-dev" / "extension-skills"
cache_file = cache_root / skill_name / "SKILL.md"
use_dev_symlink = link_outputs and selected_ai != "codex"
CommandRegistrar._ensure_inside(cache_file, cache_root)
if skill_file.exists() or skill_file.is_symlink():
# Do not overwrite user-customized skills, but allow dev-mode
# symlinks that point back to this extension's generated cache
# to be refreshed on a subsequent dev install.
if not (
link_outputs
use_dev_symlink
and self._is_expected_dev_symlink(skill_file, cache_file)
):
continue
Expand Down Expand Up @@ -1089,7 +1090,7 @@ def _register_extension_skills(
skill_content
)

if link_outputs:
if use_dev_symlink:
try:
cache_file.parent.mkdir(parents=True, exist_ok=True)
cache_file.write_text(skill_content, encoding="utf-8")
Expand Down
36 changes: 36 additions & 0 deletions tests/test_extensions.py
Original file line number Diff line number Diff line change
Expand Up @@ -4619,6 +4619,42 @@ def test_add_dev_links_copilot_agent_when_supported(
else:
assert not agent_file.is_symlink()

def test_add_dev_writes_codex_skills_as_files(self, extension_dir, project_dir):
"""Codex dev skills should be written as files so Codex can load them."""
from typer.testing import CliRunner
from unittest.mock import patch
from specify_cli import app

init_options = project_dir / ".specify" / "init-options.json"
init_options.write_text(
json.dumps({"ai": "codex", "ai_skills": True}), encoding="utf-8"
)

runner = CliRunner()
with patch.object(Path, "cwd", return_value=project_dir):
result = runner.invoke(
app,
["extension", "add", str(extension_dir), "--dev"],
catch_exceptions=True,
)

assert result.exit_code == 0, result.output

skill_file = (
project_dir
/ ".agents"
/ "skills"
/ "speckit-test-ext-hello"
/ "SKILL.md"
)
assert skill_file.exists()
assert not skill_file.is_symlink()

content = skill_file.read_text(encoding="utf-8")
assert "name: speckit-test-ext-hello" in content
assert "metadata:" in content
assert "source: test-ext:commands/hello.md" in content

def test_add_dev_falls_back_to_copy_when_windows_symlinks_unavailable(
self, extension_dir, project_dir, monkeypatch
):
Expand Down