Skip to content
Merged
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
35 changes: 35 additions & 0 deletions src/autoskillit/workspace/session_skills.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down Expand Up @@ -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:
Expand Down
49 changes: 49 additions & 0 deletions tests/workspace/test_session_skills_codex.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
Loading