From de9b5a245856e22a7eea605e6ea2e18ed6983b08 Mon Sep 17 00:00:00 2001 From: Trecek Date: Sat, 25 Jul 2026 15:13:10 -0700 Subject: [PATCH] fix: restore Codex cook skill discovery --- src/autoskillit/workspace/session_skills.py | 35 ++++++++++++++ tests/workspace/test_session_skills_codex.py | 49 ++++++++++++++++++++ 2 files changed, 84 insertions(+) diff --git a/src/autoskillit/workspace/session_skills.py b/src/autoskillit/workspace/session_skills.py index 31be0c0dd..e492b1314 100644 --- a/src/autoskillit/workspace/session_skills.py +++ b/src/autoskillit/workspace/session_skills.py @@ -285,6 +285,34 @@ def materialize_codex_profile_skills( return len(_materialize_codex_profile_skill_infos(session_dir, backend)) +def _link_generated_home_skill_view( + generated_home: Path, + projected_skills: Path, + *, + skills_subdir: Path, +) -> int: + """Expose projected skills at a persistent backend's home discovery root.""" + discovery_root = generated_home / skills_subdir + discovery_root.mkdir(parents=True, exist_ok=True) + count = 0 + for source in sorted(projected_skills.iterdir(), key=lambda entry: entry.name): + skill_md = source / "SKILL.md" + if source.is_symlink() or not source.is_dir() or not skill_md.is_file(): + raise SkillContractError(f"invalid projected session skill directory: {source}") + target = discovery_root / source.name + if os.path.lexists(target): + logger.debug( + "generated_home_skill_collision_preserved", + skill=source.name, + target=str(target), + ) + continue + relative_source = Path(os.path.relpath(source, start=discovery_root)) + target.symlink_to(relative_source, target_is_directory=True) + count += 1 + return count + + def resolve_ephemeral_root() -> Path: """Return a writable ephemeral root directory for session skill dirs. @@ -772,6 +800,13 @@ def _materialize_session( else tuple(record for record in records if record.source is not SkillSource.BUNDLED) ) materialize_agent_skill_tree(skills_base, session_records, ungated_context) + if backend is not None and backend.capabilities.session_dir_persistent: + linked = _link_generated_home_skill_view( + generated_home, + skills_base, + skills_subdir=skills_subdir, + ) + logger.debug("generated_home_skill_view_linked", count=linked) if backend is not None and backend.capabilities.session_dir_persistent: self._create_inert_rollout_paths(generated_home, backend) if backend is not None: diff --git a/tests/workspace/test_session_skills_codex.py b/tests/workspace/test_session_skills_codex.py index fa1c298fa..50c5168e7 100644 --- a/tests/workspace/test_session_skills_codex.py +++ b/tests/workspace/test_session_skills_codex.py @@ -108,6 +108,55 @@ def test_codex_init_session_creates_skills_subdir(make_session_skill_manager, co assert not (session_path / ClaudeDirectoryConventions.ADD_DIR_SKILLS_SUBDIR).exists() +def test_codex_generated_home_links_projected_catalog_into_discovery_root( + make_session_skill_manager, + codex_env, +) -> None: + mgr = make_session_skill_manager() + add_dir = _materialize( + mgr, + "sid", + backend=codex_env.backend, + names=frozenset({"investigate"}), + ) + + add_dir_path = Path(str(add_dir)) + projected = add_dir_path / "skills" / "investigate" + discoverable = add_dir_path.parent / "skills" / "investigate" + + assert discoverable.is_symlink() + assert not discoverable.readlink().is_absolute() + assert discoverable.resolve() == projected.resolve() + + +def test_codex_generated_home_preserves_existing_profile_skill_on_collision( + make_session_skill_manager, + codex_env, +) -> None: + profile_content = "---\nname: investigate\ndescription: profile copy\n---\n" + + def setup_session_dir(session_dir: Path) -> None: + profile_skill = session_dir / "skills" / "investigate" + profile_skill.mkdir(parents=True) + (profile_skill / "SKILL.md").write_text(profile_content) + + codex_env.backend.setup_session_dir.side_effect = setup_session_dir + mgr = make_session_skill_manager() + add_dir = _materialize( + mgr, + "sid", + backend=codex_env.backend, + names=frozenset({"investigate"}), + ) + + add_dir_path = Path(str(add_dir)) + discoverable = add_dir_path.parent / "skills" / "investigate" + + assert not discoverable.is_symlink() + assert (discoverable / "SKILL.md").read_text() == profile_content + assert (add_dir_path / "skills" / "investigate" / "SKILL.md").is_file() + + def test_codex_init_session_delegates_to_setup_session_dir( make_session_skill_manager, codex_env ) -> None: