Skip to content

Commit 39e3690

Browse files
committed
fix(codex): skip legacy prompts and fallback when bundled skills missing
1 parent 77efcb2 commit 39e3690

File tree

2 files changed

+61
-21
lines changed

2 files changed

+61
-21
lines changed

src/specify_cli/__init__.py

Lines changed: 52 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -945,7 +945,19 @@ def download_template_from_github(ai_assistant: str, download_dir: Path, *, scri
945945
}
946946
return zip_path, metadata
947947

948-
def download_and_extract_template(project_path: Path, ai_assistant: str, script_type: str, is_current_dir: bool = False, *, verbose: bool = True, tracker: StepTracker | None = None, client: httpx.Client = None, debug: bool = False, github_token: str = None) -> Path:
948+
def download_and_extract_template(
949+
project_path: Path,
950+
ai_assistant: str,
951+
script_type: str,
952+
is_current_dir: bool = False,
953+
*,
954+
skip_legacy_codex_prompts: bool = False,
955+
verbose: bool = True,
956+
tracker: StepTracker | None = None,
957+
client: httpx.Client = None,
958+
debug: bool = False,
959+
github_token: str = None,
960+
) -> Path:
949961
"""Download the latest release and extract it to create a new project.
950962
Returns project_path. Uses tracker if provided (with keys: fetch, download, extract, cleanup)
951963
"""
@@ -1016,6 +1028,10 @@ def download_and_extract_template(project_path: Path, ai_assistant: str, script_
10161028
console.print("[cyan]Found nested directory structure[/cyan]")
10171029

10181030
for item in source_dir.iterdir():
1031+
# Codex skills mode should not materialize legacy prompt files
1032+
# from older template archives.
1033+
if skip_legacy_codex_prompts and ai_assistant == "codex" and item.name == ".codex":
1034+
continue
10191035
dest_path = project_path / item.name
10201036
if item.is_dir():
10211037
if dest_path.exists():
@@ -1066,6 +1082,11 @@ def download_and_extract_template(project_path: Path, ai_assistant: str, script_
10661082
elif verbose:
10671083
console.print("[cyan]Flattened nested directory structure[/cyan]")
10681084

1085+
if skip_legacy_codex_prompts and ai_assistant == "codex":
1086+
legacy_codex_dir = project_path / ".codex"
1087+
if legacy_codex_dir.is_dir():
1088+
shutil.rmtree(legacy_codex_dir, ignore_errors=True)
1089+
10691090
except Exception as e:
10701091
if tracker:
10711092
tracker.error("extract", str(e))
@@ -1712,8 +1733,18 @@ def init(
17121733
local_ssl_context = ssl_context if verify else False
17131734
local_client = httpx.Client(verify=local_ssl_context)
17141735

1715-
download_and_extract_template(project_path, selected_ai, selected_script, here, verbose=False, tracker=tracker, client=local_client, debug=debug, github_token=github_token)
1716-
1736+
download_and_extract_template(
1737+
project_path,
1738+
selected_ai,
1739+
selected_script,
1740+
here,
1741+
skip_legacy_codex_prompts=(selected_ai == "codex" and ai_skills),
1742+
verbose=False,
1743+
tracker=tracker,
1744+
client=local_client,
1745+
debug=debug,
1746+
github_token=github_token,
1747+
)
17171748
# For generic agent, rename placeholder directory to user-specified path
17181749
if selected_ai == "generic" and ai_commands_dir:
17191750
placeholder_dir = project_path / ".speckit" / "commands"
@@ -1733,16 +1764,25 @@ def init(
17331764
if ai_skills:
17341765
if selected_ai in NATIVE_SKILLS_AGENTS:
17351766
skills_dir = _get_skills_dir(project_path, selected_ai)
1736-
if not _has_bundled_skills(project_path, selected_ai):
1737-
raise RuntimeError(
1738-
f"Expected bundled agent skills in {skills_dir.relative_to(project_path)}, "
1739-
"but none were found. Re-run with an up-to-date template."
1740-
)
1741-
if tracker:
1742-
tracker.start("ai-skills")
1743-
tracker.complete("ai-skills", f"bundled skills → {skills_dir.relative_to(project_path)}")
1767+
bundled_found = _has_bundled_skills(project_path, selected_ai)
1768+
if bundled_found:
1769+
if tracker:
1770+
tracker.start("ai-skills")
1771+
tracker.complete("ai-skills", f"bundled skills → {skills_dir.relative_to(project_path)}")
1772+
else:
1773+
console.print(f"[green]✓[/green] Using bundled agent skills in {skills_dir.relative_to(project_path)}/")
17441774
else:
1745-
console.print(f"[green]✓[/green] Using bundled agent skills in {skills_dir.relative_to(project_path)}/")
1775+
# Compatibility fallback: convert command templates to skills
1776+
# when an older template archive does not include native skills.
1777+
# This keeps `specify init --here --ai codex --ai-skills` usable
1778+
# in repos that already contain unrelated skills under .agents/skills.
1779+
fallback_ok = install_ai_skills(project_path, selected_ai, tracker=tracker)
1780+
if not fallback_ok:
1781+
raise RuntimeError(
1782+
f"Expected bundled agent skills in {skills_dir.relative_to(project_path)}, "
1783+
"but none were found and fallback conversion failed. "
1784+
"Re-run with an up-to-date template."
1785+
)
17461786
else:
17471787
skills_ok = install_ai_skills(project_path, selected_ai, tracker=tracker)
17481788

tests/test_ai_skills.py

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -720,8 +720,8 @@ def fake_download(project_path, *args, **kwargs):
720720
mock_skills.assert_not_called()
721721
assert (target / ".agents" / "skills" / "speckit-specify" / "SKILL.md").exists()
722722

723-
def test_codex_native_skills_missing_fails_clearly(self, tmp_path):
724-
"""Codex native skills init should fail if bundled skills are missing."""
723+
def test_codex_native_skills_missing_falls_back_then_fails_cleanly(self, tmp_path):
724+
"""Codex should attempt fallback conversion when bundled skills are missing."""
725725
from typer.testing import CliRunner
726726

727727
runner = CliRunner()
@@ -730,7 +730,7 @@ def test_codex_native_skills_missing_fails_clearly(self, tmp_path):
730730
with patch("specify_cli.download_and_extract_template", lambda *args, **kwargs: None), \
731731
patch("specify_cli.ensure_executable_scripts"), \
732732
patch("specify_cli.ensure_constitution_from_template"), \
733-
patch("specify_cli.install_ai_skills") as mock_skills, \
733+
patch("specify_cli.install_ai_skills", return_value=False) as mock_skills, \
734734
patch("specify_cli.is_git_repo", return_value=False), \
735735
patch("specify_cli.shutil.which", return_value="/usr/bin/codex"):
736736
result = runner.invoke(
@@ -739,11 +739,12 @@ def test_codex_native_skills_missing_fails_clearly(self, tmp_path):
739739
)
740740

741741
assert result.exit_code == 1
742-
mock_skills.assert_not_called()
742+
mock_skills.assert_called_once()
743743
assert "Expected bundled agent skills" in result.output
744+
assert "fallback conversion failed" in result.output
744745

745746
def test_codex_native_skills_ignores_non_speckit_skill_dirs(self, tmp_path):
746-
"""Non-spec-kit SKILL.md files should not satisfy Codex bundled-skills validation."""
747+
"""Non-spec-kit SKILL.md files should trigger fallback conversion, not hard-fail."""
747748
from typer.testing import CliRunner
748749

749750
runner = CliRunner()
@@ -757,17 +758,16 @@ def fake_download(project_path, *args, **kwargs):
757758
with patch("specify_cli.download_and_extract_template", side_effect=fake_download), \
758759
patch("specify_cli.ensure_executable_scripts"), \
759760
patch("specify_cli.ensure_constitution_from_template"), \
760-
patch("specify_cli.install_ai_skills") as mock_skills, \
761+
patch("specify_cli.install_ai_skills", return_value=True) as mock_skills, \
761762
patch("specify_cli.is_git_repo", return_value=False), \
762763
patch("specify_cli.shutil.which", return_value="/usr/bin/codex"):
763764
result = runner.invoke(
764765
app,
765766
["init", str(target), "--ai", "codex", "--ai-skills", "--script", "sh", "--no-git"],
766767
)
767768

768-
assert result.exit_code == 1
769-
mock_skills.assert_not_called()
770-
assert "Expected bundled agent skills" in result.output
769+
assert result.exit_code == 0
770+
mock_skills.assert_called_once()
771771

772772
def test_commands_preserved_when_skills_fail(self, tmp_path):
773773
"""If skills fail, commands should NOT be removed (safety net)."""

0 commit comments

Comments
 (0)