diff --git a/docs/edge/ar/concepts/skills.mdx b/docs/edge/ar/concepts/skills.mdx index 89f29a90a..faef071d3 100644 --- a/docs/edge/ar/concepts/skills.mdx +++ b/docs/edge/ar/concepts/skills.mdx @@ -24,15 +24,23 @@ mode: "wide" ## البداية السريعة -### 1. إنشاء مجلد المهارة +### 1. إنشاء مهارة باستخدام سطر الأوامر (CLI) + +واجهة سطر الأوامر هي الطريقة المدعومة لإنشاء مهارة — فهي تُنشئ لك هيكل المجلد وملف `SKILL.md` صالحًا: + +```shell Terminal +crewai skill create code-review +``` + +داخل مشروع طاقم (حيث يوجد `pyproject.toml`) يُنشئ هذا الأمر `./skills/code-review/`؛ وخارج المشروع يُنشئ `./code-review/` في المجلد الحالي (يمكنك فرض هذا السلوك باستخدام `--no-project`): ``` skills/ └── code-review/ - ├── SKILL.md # مطلوب — التعليمات - ├── references/ # اختياري — مستندات مرجعية - │ └── style-guide.md - └── scripts/ # اختياري — سكربتات قابلة للتنفيذ + ├── SKILL.md # Required — instructions (pre-filled template) + ├── references/ # Optional — reference docs + ├── scripts/ # Optional — executable scripts + └── assets/ # Optional — static files ``` ### 2. كتابة SKILL.md الخاص بك @@ -164,6 +172,65 @@ agent = Agent( --- +## إنشاء المهارات ونشرها وتثبيتها + +للمهارات دورة حياة كاملة تُدار عبر واجهة سطر الأوامر: **أنشئها باستخدام `crewai skill create`، وانشرها باستخدام `crewai skill publish`** — إنشاء المجلدات يدويًا يصلح للتجارب المحلية، لكن واجهة سطر الأوامر هي سير العمل المقصود، وهي تحافظ على صحة هيكل المهارة وبياناتها الوصفية. + +### الإنشاء + +```shell Terminal +crewai skill create my-skill +``` + +يُنشئ هذا الأمر المجلد (داخل `./skills/` في مشروع الطاقم) مع قالب `SKILL.md`، بالإضافة إلى مجلدات فارغة `scripts/` و `references/` و `assets/`. عدّل `SKILL.md` لتعريف التعليمات. + +### النشر + +نفّذ الأمر من داخل مجلد المهارة (حيث يوجد `SKILL.md`): + +```shell Terminal +cd skills/my-skill +crewai skill publish +``` + +يقرأ النشر الحقول `name` و `description` و `metadata.version` من البيانات الوصفية في مقدمة `SKILL.md` ويدفع المهارة إلى سجل CrewAI. **المهارات المنشورة تكون دائمًا مقيّدة بنطاق مؤسستك** — مثل الأدوات، لا يستطيع رؤيتها وتثبيتها إلا أعضاء المؤسسة الناشرة؛ ولا توجد رؤية عامة. أعلام مفيدة: + +| العلم | التأثير | +| :--- | :--- | +| `--org ` | النشر تحت مؤسسة محددة (يتجاوز الإعدادات). | +| `--force` | تخطي التحقق من حالة git (تغييرات غير مُثبتة، إلخ). | + +### التثبيت + +ثبّت مهارة منشورة عبر مرجعها `@org/name`: + +```shell Terminal +crewai skill install @acme/code-review +``` + +داخل مشروع الطاقم تُثبَّت المهارة في `./skills/{name}/`؛ وخارج المشروع تذهب إلى ذاكرة التخزين المؤقتة المشتركة في `~/.crewai/skills/{org}/{name}/`. + +يمكن للوكلاء أيضًا الإشارة إلى مهارات السجل مباشرة — يتم حلّها من ذاكرة التخزين المؤقتة المحلية (أو من مجلد `skills/` في المشروع) وقت التشغيل: + +```python +agent = Agent( + role="Senior Code Reviewer", + goal="Review pull requests for quality and security issues", + backstory="Staff engineer with expertise in secure coding practices.", + skills=["@acme/code-review"], # registry ref, resolved locally +) +``` + +### عرض القائمة + +```shell Terminal +crewai skill list +``` + +يعرض المهارات المثبّتة من مجلد المشروع `./skills/` ومن ذاكرة التخزين المؤقتة العامة معًا، مع إصداراتها ومساراتها. + +--- + ## المهارات على مستوى الطاقم يمكن تعيين المهارات على الطاقم لتُطبّق على **جميع الوكلاء**: diff --git a/docs/edge/en/concepts/skills.mdx b/docs/edge/en/concepts/skills.mdx index d88602b84..23da69088 100644 --- a/docs/edge/en/concepts/skills.mdx +++ b/docs/edge/en/concepts/skills.mdx @@ -24,15 +24,23 @@ You often need **both**: skills for expertise, tools for action. They are config ## Quick Start -### 1. Create a Skill Directory +### 1. Create a Skill with the CLI + +The CLI is the supported way to create a skill — it scaffolds the directory layout and a valid `SKILL.md` for you: + +```shell Terminal +crewai skill create code-review +``` + +Inside a crew project (where `pyproject.toml` lives) this creates `./skills/code-review/`; outside a project it creates `./code-review/` in the current directory (you can force that behavior with `--no-project`): ``` skills/ └── code-review/ - ├── SKILL.md # Required — instructions + ├── SKILL.md # Required — instructions (pre-filled template) ├── references/ # Optional — reference docs - │ └── style-guide.md - └── scripts/ # Optional — executable scripts + ├── scripts/ # Optional — executable scripts + └── assets/ # Optional — static files ``` ### 2. Write Your SKILL.md @@ -164,6 +172,65 @@ agent = Agent( --- +## Creating, Publishing, and Installing Skills + +Skills have a full lifecycle managed by the CLI: **create them with `crewai skill create`, publish them with `crewai skill publish`** — hand-rolling directories works for local experiments, but the CLI is the intended workflow and keeps your skill layout and frontmatter valid. + +### Create + +```shell Terminal +crewai skill create my-skill +``` + +Scaffolds the directory (into `./skills/` inside a crew project) with a template `SKILL.md`, plus empty `scripts/`, `references/`, and `assets/` directories. Edit `SKILL.md` to define the instructions. + +### Publish + +Run from inside the skill directory (where `SKILL.md` is): + +```shell Terminal +cd skills/my-skill +crewai skill publish +``` + +Publishing reads `name`, `description`, and `metadata.version` from the `SKILL.md` frontmatter and pushes the skill to the CrewAI registry. **Published skills are always scoped to your organization** — like tools, only members of the publishing org can see and install them; there is no public visibility. Useful flags: + +| Flag | Effect | +| :--- | :--- | +| `--org ` | Publish under a specific organization (overrides settings). | +| `--force` | Skip git-state validation (uncommitted changes, etc.). | + +### Install + +Install a published skill by its `@org/name` reference: + +```shell Terminal +crewai skill install @acme/code-review +``` + +Inside a crew project the skill lands in `./skills/{name}/`; outside a project it goes to the shared cache at `~/.crewai/skills/{org}/{name}/`. + +Agents can also reference registry skills directly — they resolve from the local cache (or project `skills/` directory) at runtime: + +```python +agent = Agent( + role="Senior Code Reviewer", + goal="Review pull requests for quality and security issues", + backstory="Staff engineer with expertise in secure coding practices.", + skills=["@acme/code-review"], # registry ref, resolved locally +) +``` + +### List + +```shell Terminal +crewai skill list +``` + +Shows installed skills from both the project `./skills/` directory and the global cache, with their versions and paths. + +--- + ## Crew-Level Skills Skills can be set on a crew to apply to **all agents**: diff --git a/docs/edge/ko/concepts/skills.mdx b/docs/edge/ko/concepts/skills.mdx index ea1009dc0..1bf4e881f 100644 --- a/docs/edge/ko/concepts/skills.mdx +++ b/docs/edge/ko/concepts/skills.mdx @@ -24,15 +24,23 @@ mode: "wide" ## 빠른 시작 -### 1. 스킬 디렉터리 생성 +### 1. CLI로 스킬 생성 + +CLI는 스킬을 생성하는 공식 지원 방식입니다 — 디렉터리 레이아웃과 유효한 `SKILL.md`를 자동으로 스캐폴딩해 줍니다: + +```shell Terminal +crewai skill create code-review +``` + +크루 프로젝트 내부(`pyproject.toml`이 있는 곳)에서는 `./skills/code-review/`가 생성되고, 프로젝트 외부에서는 현재 디렉터리에 `./code-review/`가 생성됩니다 (`--no-project`로 이 동작을 강제할 수 있습니다): ``` skills/ └── code-review/ - ├── SKILL.md # 필수 — 지침 + ├── SKILL.md # 필수 — 지침 (미리 채워진 템플릿) ├── references/ # 선택 — 참조 문서 - │ └── style-guide.md - └── scripts/ # 선택 — 실행 가능한 스크립트 + ├── scripts/ # 선택 — 실행 가능한 스크립트 + └── assets/ # 선택 — 정적 파일 ``` ### 2. SKILL.md 작성 @@ -164,6 +172,65 @@ agent = Agent( --- +## 스킬 생성, 게시 및 설치 + +스킬은 CLI로 관리되는 전체 라이프사이클을 갖습니다: **`crewai skill create`로 생성하고, `crewai skill publish`로 게시하세요** — 디렉터리를 직접 만드는 방식도 로컬 실험에는 사용할 수 있지만, CLI가 의도된 워크플로우이며 스킬 레이아웃과 프론트매터를 유효하게 유지해 줍니다. + +### 생성 + +```shell Terminal +crewai skill create my-skill +``` + +디렉터리를 스캐폴딩하고 (크루 프로젝트 내부에서는 `./skills/`에 생성) 템플릿 `SKILL.md`와 함께 빈 `scripts/`, `references/`, `assets/` 디렉터리를 만듭니다. `SKILL.md`를 편집하여 지침을 정의하세요. + +### 게시 + +스킬 디렉터리 내부(`SKILL.md`가 있는 곳)에서 실행하세요: + +```shell Terminal +cd skills/my-skill +crewai skill publish +``` + +게시 시 `SKILL.md` 프론트매터에서 `name`, `description`, `metadata.version`을 읽어 스킬을 CrewAI 레지스트리로 푸시합니다. **게시된 스킬은 항상 조직 범위로 제한됩니다** — 도구와 마찬가지로 게시한 조직의 멤버만 스킬을 보고 설치할 수 있으며, 공개 가시성은 없습니다. 유용한 플래그: + +| 플래그 | 효과 | +| :--- | :--- | +| `--org ` | 특정 조직으로 게시합니다 (설정을 재정의). | +| `--force` | git 상태 검증을 건너뜁니다 (커밋되지 않은 변경 사항 등). | + +### 설치 + +게시된 스킬을 `@org/name` 참조로 설치합니다: + +```shell Terminal +crewai skill install @acme/code-review +``` + +크루 프로젝트 내부에서는 스킬이 `./skills/{name}/`에 설치되고, 프로젝트 외부에서는 공유 캐시인 `~/.crewai/skills/{org}/{name}/`에 저장됩니다. + +에이전트는 레지스트리 스킬을 직접 참조할 수도 있습니다 — 런타임에 로컬 캐시(또는 프로젝트 `skills/` 디렉터리)에서 해석됩니다: + +```python +agent = Agent( + role="Senior Code Reviewer", + goal="Review pull requests for quality and security issues", + backstory="Staff engineer with expertise in secure coding practices.", + skills=["@acme/code-review"], # registry ref, resolved locally +) +``` + +### 목록 조회 + +```shell Terminal +crewai skill list +``` + +프로젝트 `./skills/` 디렉터리와 전역 캐시 양쪽에 설치된 스킬을 버전 및 경로와 함께 보여줍니다. + +--- + ## 크루 레벨 스킬 스킬을 크루에 설정하여 **모든 에이전트**에 적용할 수 있습니다: diff --git a/docs/edge/pt-BR/concepts/skills.mdx b/docs/edge/pt-BR/concepts/skills.mdx index 0f530390f..09418ae65 100644 --- a/docs/edge/pt-BR/concepts/skills.mdx +++ b/docs/edge/pt-BR/concepts/skills.mdx @@ -24,15 +24,23 @@ Frequentemente você precisa de **ambos**: skills para expertise, ferramentas pa ## Início Rápido -### 1. Crie um Diretório de Skill +### 1. Crie uma Skill com a CLI + +A CLI é a forma suportada de criar uma skill — ela gera a estrutura de diretórios e um `SKILL.md` válido para você: + +```shell Terminal +crewai skill create code-review +``` + +Dentro de um projeto de crew (onde o `pyproject.toml` está) isso cria `./skills/code-review/`; fora de um projeto, cria `./code-review/` no diretório atual (você pode forçar esse comportamento com `--no-project`): ``` skills/ └── code-review/ - ├── SKILL.md # Obrigatório — instruções + ├── SKILL.md # Obrigatório — instruções (template pré-preenchido) ├── references/ # Opcional — documentos de referência - │ └── style-guide.md - └── scripts/ # Opcional — scripts executáveis + ├── scripts/ # Opcional — scripts executáveis + └── assets/ # Opcional — arquivos estáticos ``` ### 2. Escreva seu SKILL.md @@ -164,6 +172,65 @@ agent = Agent( --- +## Criando, Publicando e Instalando Skills + +Skills têm um ciclo de vida completo gerenciado pela CLI: **crie-as com `crewai skill create`, publique-as com `crewai skill publish`** — criar diretórios à mão funciona para experimentos locais, mas a CLI é o fluxo de trabalho pretendido e mantém a estrutura e o frontmatter da sua skill válidos. + +### Criar + +```shell Terminal +crewai skill create my-skill +``` + +Gera o diretório (em `./skills/` dentro de um projeto de crew) com um `SKILL.md` de template, além dos diretórios vazios `scripts/`, `references/` e `assets/`. Edite o `SKILL.md` para definir as instruções. + +### Publicar + +Execute de dentro do diretório da skill (onde o `SKILL.md` está): + +```shell Terminal +cd skills/my-skill +crewai skill publish +``` + +A publicação lê `name`, `description` e `metadata.version` do frontmatter do `SKILL.md` e envia a skill para o registro da CrewAI. **Skills publicadas são sempre escopadas à sua organização** — assim como ferramentas, apenas membros da organização que publicou podem vê-las e instalá-las; não há visibilidade pública. Flags úteis: + +| Flag | Efeito | +| :--- | :--- | +| `--org ` | Publica sob uma organização específica (sobrepõe as configurações). | +| `--force` | Pula a validação de estado do git (alterações não commitadas, etc.). | + +### Instalar + +Instale uma skill publicada pela sua referência `@org/name`: + +```shell Terminal +crewai skill install @acme/code-review +``` + +Dentro de um projeto de crew, a skill é colocada em `./skills/{name}/`; fora de um projeto, vai para o cache compartilhado em `~/.crewai/skills/{org}/{name}/`. + +Agentes também podem referenciar skills do registro diretamente — elas são resolvidas a partir do cache local (ou do diretório `skills/` do projeto) em tempo de execução: + +```python +agent = Agent( + role="Senior Code Reviewer", + goal="Review pull requests for quality and security issues", + backstory="Staff engineer with expertise in secure coding practices.", + skills=["@acme/code-review"], # registry ref, resolved locally +) +``` + +### Listar + +```shell Terminal +crewai skill list +``` + +Mostra as skills instaladas tanto do diretório `./skills/` do projeto quanto do cache global, com suas versões e caminhos. + +--- + ## Skills no Nível do Crew Skills podem ser definidas no crew para aplicar a **todos os agentes**: diff --git a/lib/cli/src/crewai_cli/cli.py b/lib/cli/src/crewai_cli/cli.py index 28cec9d97..e4b0342c0 100644 --- a/lib/cli/src/crewai_cli/cli.py +++ b/lib/cli/src/crewai_cli/cli.py @@ -686,20 +686,9 @@ def tool_publish(is_public: bool, force: bool) -> None: tool_cmd.publish(is_public, force) -@crewai.group() -def experimental() -> None: - """Experimental, unstable commands. Subject to change without notice.""" - import os - - if os.environ.get("CREWAI_EXPERIMENTAL") != "1": - raise click.UsageError( - "Experimental commands are gated. Set CREWAI_EXPERIMENTAL=1 to enable." - ) - - -@experimental.group(name="skill") +@crewai.group(name="skill") def skill() -> None: - """Skill Repository related commands (experimental).""" + """Create, publish, and install agent skills.""" @skill.command(name="create") @@ -713,7 +702,7 @@ def skill() -> None: help="Create skill in current dir instead of ./skills/", ) def skill_create(name: str, in_project: bool) -> None: - from crewai_cli.experimental.skills.main import SkillCommand + from crewai_cli.skills.main import SkillCommand skill_cmd = SkillCommand() skill_cmd.create(name, in_project=in_project) @@ -722,7 +711,7 @@ def skill_create(name: str, in_project: bool) -> None: @skill.command(name="install") @click.argument("ref") def skill_install(ref: str) -> None: - from crewai_cli.experimental.skills.main import SkillCommand + from crewai_cli.skills.main import SkillCommand skill_cmd = SkillCommand() skill_cmd.install(ref) @@ -736,20 +725,19 @@ def skill_install(ref: str) -> None: show_default=True, help="Skip git-state validation.", ) -@click.option("--public", "is_public", flag_value=True, default=False) -@click.option("--private", "is_public", flag_value=False) @click.option("--org", default=None, help="Organisation slug (overrides settings).") -def skill_publish(is_public: bool, org: str | None, force: bool) -> None: - from crewai_cli.experimental.skills.main import SkillCommand +def skill_publish(org: str | None, force: bool) -> None: + """Publish the skill in the current directory, scoped to your organization.""" + from crewai_cli.skills.main import SkillCommand skill_cmd = SkillCommand() - skill_cmd.publish(is_public, org=org, force=force) + skill_cmd.publish(org=org, force=force) @skill.command(name="list") def skill_list() -> None: """List locally installed skills.""" - from crewai_cli.experimental.skills.main import SkillCommand + from crewai_cli.skills.main import SkillCommand skill_cmd = SkillCommand() skill_cmd.list_cached() diff --git a/lib/cli/src/crewai_cli/experimental/skills/__init__.py b/lib/cli/src/crewai_cli/experimental/skills/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/lib/cli/src/crewai_cli/experimental/__init__.py b/lib/cli/src/crewai_cli/skills/__init__.py similarity index 100% rename from lib/cli/src/crewai_cli/experimental/__init__.py rename to lib/cli/src/crewai_cli/skills/__init__.py diff --git a/lib/cli/src/crewai_cli/experimental/skills/main.py b/lib/cli/src/crewai_cli/skills/main.py similarity index 86% rename from lib/cli/src/crewai_cli/experimental/skills/main.py rename to lib/cli/src/crewai_cli/skills/main.py index 612b85d82..0500caacd 100644 --- a/lib/cli/src/crewai_cli/experimental/skills/main.py +++ b/lib/cli/src/crewai_cli/skills/main.py @@ -13,6 +13,7 @@ import zipfile from rich.console import Console from rich.table import Table +from crewai_cli import git from crewai_cli.command import BaseCommand, PlusAPIMixin from crewai_cli.config import Settings from crewai_cli.constants import DEFAULT_CREWAI_ENTERPRISE_URL @@ -63,7 +64,7 @@ class SkillCommand(BaseCommand, PlusAPIMixin): (skill_dir / "assets").mkdir() skill_md = skill_dir / "SKILL.md" - skill_md.write_text(_SKILL_MD_TEMPLATE.format(name=name)) + skill_md.write_text(_SKILL_MD_TEMPLATE.format(name=name), encoding="utf-8") console.print( f"[green]Created skill [bold]{name}[/bold] at [bold]{skill_dir}[/bold].[/green]" @@ -148,7 +149,7 @@ class SkillCommand(BaseCommand, PlusAPIMixin): ) else: try: - from crewai.experimental.skills.cache import SkillCacheManager + from crewai.skills.cache import SkillCacheManager cache = SkillCacheManager() cache.store(org, name, version, archive_bytes) @@ -170,13 +171,19 @@ class SkillCommand(BaseCommand, PlusAPIMixin): "version": version, "installed_at": datetime.now(tz=timezone.utc).isoformat(), } - (cache_dir / ".crewai_meta.json").write_text(json.dumps(meta, indent=2)) + (cache_dir / ".crewai_meta.json").write_text( + json.dumps(meta, indent=2), encoding="utf-8" + ) console.print( f"[green]Installed [bold]{ref}[/bold]{' (' + version + ')' if version else ''} to global cache.[/green]" ) - def publish(self, is_public: bool, org: str | None, force: bool = False) -> None: - """Publish the skill in the current directory to the registry.""" + def publish(self, org: str | None = None, force: bool = False) -> None: + """Publish the skill in the current directory to the registry. + + Skills are always scoped to the publishing organization; there is no + public visibility option. + """ skill_md = Path("SKILL.md") if not skill_md.exists(): console.print( @@ -185,8 +192,44 @@ class SkillCommand(BaseCommand, PlusAPIMixin): ) raise SystemExit(1) + if not force: + try: + repository = git.Repository(fetch=False) + except ValueError as exc: + if "not a Git repository" not in str(exc): + console.print( + f"[red]Unable to validate git state: {exc}\n" + "Fix the issue or pass --force to skip this check.[/red]" + ) + raise SystemExit(1) from exc + # Standalone skill directories may live outside any git repo; + # there is no git state to validate in that case. + repository = None + if repository is not None: + try: + # Refresh remote-tracking refs so is_synced() compares + # against the actual remote, not stale local state. + repository.fetch() + except ValueError as exc: + console.print( + f"[red]Unable to validate git state: {exc}\n" + "Fix the issue or pass --force to skip this check.[/red]" + ) + raise SystemExit(1) from exc + if repository is not None and not repository.is_synced(): + console.print( + "[bold red]Failed to publish skill.[/bold red]\n" + "Local changes need to be resolved before publishing. Please do the following:\n" + "* [bold]Commit[/bold] your changes.\n" + "* [bold]Push[/bold] to sync with the remote.\n" + "* [bold]Pull[/bold] the latest changes from the remote.\n" + "\nOnce your repository is up-to-date, retry publishing the skill " + "(or pass --force to skip this check)." + ) + raise SystemExit(1) + try: - frontmatter = self._parse_frontmatter(skill_md.read_text()) + frontmatter = self._parse_frontmatter(skill_md.read_text(encoding="utf-8")) except ValueError as exc: console.print(f"[red]Failed to parse SKILL.md frontmatter: {exc}[/red]") raise SystemExit(1) from exc @@ -233,7 +276,7 @@ class SkillCommand(BaseCommand, PlusAPIMixin): org=effective_org, name=name, version=version, - is_public=is_public, + is_public=False, description=description, encoded_file=encoded_file, ) @@ -277,7 +320,7 @@ class SkillCommand(BaseCommand, PlusAPIMixin): meta_file = skill_dir / ".crewai_meta.json" if meta_file.exists(): try: - meta = json.loads(meta_file.read_text()) + meta = json.loads(meta_file.read_text(encoding="utf-8")) table.add_row( "cache", f"@{meta['org']}/{meta['name']}", @@ -368,7 +411,7 @@ class SkillCommand(BaseCommand, PlusAPIMixin): def _read_version(self, skill_md: Path) -> str | None: """Read the version from a SKILL.md file's metadata, or None.""" try: - fm = self._parse_frontmatter(skill_md.read_text()) + fm = self._parse_frontmatter(skill_md.read_text(encoding="utf-8")) raw_metadata = fm.get("metadata") if isinstance(raw_metadata, dict): return raw_metadata.get("version") diff --git a/lib/cli/tests/experimental/__init__.py b/lib/cli/tests/experimental/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/lib/cli/tests/experimental/skills/__init__.py b/lib/cli/tests/experimental/skills/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/lib/cli/tests/skills/test_cli_commands.py b/lib/cli/tests/skills/test_cli_commands.py new file mode 100644 index 000000000..d3e5127c0 --- /dev/null +++ b/lib/cli/tests/skills/test_cli_commands.py @@ -0,0 +1,63 @@ +"""Tests for the top-level `crewai skill` command group. + +The Skills Repository graduated from the gated `crewai experimental skill` +group: the commands must be reachable at `crewai skill ...` without +CREWAI_EXPERIMENTAL set. +""" + +from unittest import mock + +from click.testing import CliRunner +from crewai_cli.cli import crewai +import pytest + + +@pytest.fixture +def runner(): + return CliRunner() + + +@pytest.fixture +def no_experimental_env(monkeypatch): + monkeypatch.delenv("CREWAI_EXPERIMENTAL", raising=False) + + +class TestSkillGroupIsTopLevel: + def test_skill_group_registered_at_top_level(self, runner, no_experimental_env): + result = runner.invoke(crewai, ["skill", "--help"]) + assert result.exit_code == 0, result.output + for subcommand in ("create", "install", "publish", "list"): + assert subcommand in result.output + + def test_skill_group_not_gated_by_experimental_env( + self, runner, no_experimental_env + ): + with mock.patch("crewai_cli.skills.main.SkillCommand") as mock_cmd: + result = runner.invoke(crewai, ["skill", "create", "my-skill"]) + assert result.exit_code == 0, result.output + mock_cmd.return_value.create.assert_called_once_with( + "my-skill", in_project=True + ) + + def test_skill_create_no_project_flag(self, runner, no_experimental_env): + with mock.patch("crewai_cli.skills.main.SkillCommand") as mock_cmd: + result = runner.invoke(crewai, ["skill", "create", "my-skill", "--no-project"]) + assert result.exit_code == 0, result.output + mock_cmd.return_value.create.assert_called_once_with( + "my-skill", in_project=False + ) + + +class TestSkillPublishIsOrgScopedOnly: + def test_publish_rejects_public_flag(self, runner, no_experimental_env): + result = runner.invoke(crewai, ["skill", "publish", "--public"]) + assert result.exit_code != 0 + assert "No such option" in result.output + + def test_publish_passes_org_and_force_only(self, runner, no_experimental_env): + with mock.patch("crewai_cli.skills.main.SkillCommand") as mock_cmd: + result = runner.invoke( + crewai, ["skill", "publish", "--org", "acme", "--force"] + ) + assert result.exit_code == 0, result.output + mock_cmd.return_value.publish.assert_called_once_with(org="acme", force=True) diff --git a/lib/cli/tests/experimental/skills/test_main.py b/lib/cli/tests/skills/test_main.py similarity index 56% rename from lib/cli/tests/experimental/skills/test_main.py rename to lib/cli/tests/skills/test_main.py index f302754d9..db8fb5d88 100644 --- a/lib/cli/tests/experimental/skills/test_main.py +++ b/lib/cli/tests/skills/test_main.py @@ -36,7 +36,7 @@ def skill_command(): TokenManager().save_tokens( "test-token", (datetime.now() + timedelta(seconds=36000)).timestamp() ) - from crewai_cli.experimental.skills.main import SkillCommand + from crewai_cli.skills.main import SkillCommand cmd = SkillCommand() yield cmd @@ -113,7 +113,7 @@ class TestSkillPublish: def test_publish_no_skill_md(self, skill_command): with in_temp_dir(): with pytest.raises(SystemExit): - skill_command.publish(is_public=True, org="acme") + skill_command.publish(org="acme") def test_publish_missing_version(self, skill_command): with in_temp_dir(): @@ -121,7 +121,7 @@ class TestSkillPublish: "---\nname: my-skill\ndescription: Test.\n---\nInstructions." ) with pytest.raises(SystemExit): - skill_command.publish(is_public=True, org="acme") + skill_command.publish(org="acme") def test_publish_missing_name(self, skill_command): with in_temp_dir(): @@ -129,7 +129,7 @@ class TestSkillPublish: "---\ndescription: Test.\nversion: 1.0.0\n---\nInstructions." ) with pytest.raises(SystemExit): - skill_command.publish(is_public=True, org="acme") + skill_command.publish(org="acme") def test_publish_no_org(self, skill_command): with in_temp_dir(): @@ -142,11 +142,11 @@ class TestSkillPublish: mock_resp.status_code = 200 mock_resp.json.return_value = {} mock_client.publish_skill.return_value = mock_resp - with patch("crewai_cli.experimental.skills.main.Settings") as mock_settings_cls: + with patch("crewai_cli.skills.main.Settings") as mock_settings_cls: mock_settings_cls.return_value.org_name = None mock_settings_cls.return_value.enterprise_base_url = None with pytest.raises(SystemExit): - skill_command.publish(is_public=True, org=None) + skill_command.publish(org=None) def test_publish_calls_api(self, skill_command): with in_temp_dir(): @@ -158,16 +158,102 @@ class TestSkillPublish: mock_resp.status_code = 200 mock_resp.json.return_value = {} skill_command.plus_api_client.publish_skill = MagicMock(return_value=mock_resp) - with patch("crewai_cli.experimental.skills.main.Settings") as mock_settings_cls: + with patch("crewai_cli.skills.main.Settings") as mock_settings_cls: mock_settings_cls.return_value.org_name = "acme" mock_settings_cls.return_value.enterprise_base_url = None - skill_command.publish(is_public=False, org="acme") + skill_command.publish(org="acme") skill_command.plus_api_client.publish_skill.assert_called_once() call_kwargs = skill_command.plus_api_client.publish_skill.call_args assert call_kwargs.kwargs["name"] == "my-skill" assert call_kwargs.kwargs["version"] == "1.0.0" + # Skills are always org-scoped; there is no public visibility. + assert call_kwargs.kwargs["is_public"] is False + + def test_publish_blocked_when_git_not_synced(self, skill_command): + with in_temp_dir(): + Path("SKILL.md").write_text( + "---\nname: my-skill\ndescription: A test skill.\nmetadata:\n version: 1.0.0\n---\nInstructions." + ) + skill_command.plus_api_client.publish_skill = MagicMock() + with patch("crewai_cli.skills.main.git.Repository") as mock_repo_cls: + mock_repo_cls.return_value.is_synced.return_value = False + with pytest.raises(SystemExit): + skill_command.publish(org="acme") + skill_command.plus_api_client.publish_skill.assert_not_called() + + def test_publish_force_skips_git_check(self, skill_command): + with in_temp_dir(): + Path("SKILL.md").write_text( + "---\nname: my-skill\ndescription: A test skill.\nmetadata:\n version: 1.0.0\n---\nInstructions." + ) + mock_resp = MagicMock() + mock_resp.is_success = True + mock_resp.status_code = 200 + mock_resp.json.return_value = {} + skill_command.plus_api_client.publish_skill = MagicMock(return_value=mock_resp) + with ( + patch("crewai_cli.skills.main.git.Repository") as mock_repo_cls, + patch("crewai_cli.skills.main.Settings") as mock_settings_cls, + ): + mock_settings_cls.return_value.org_name = "acme" + mock_settings_cls.return_value.enterprise_base_url = None + skill_command.publish(org="acme", force=True) + mock_repo_cls.assert_not_called() + skill_command.plus_api_client.publish_skill.assert_called_once() + + def test_publish_blocked_when_fetch_fails(self, skill_command): + """A failing remote fetch must fail closed, not silently skip syncing.""" + with in_temp_dir(): + Path("SKILL.md").write_text( + "---\nname: my-skill\ndescription: A test skill.\nmetadata:\n version: 1.0.0\n---\nInstructions." + ) + skill_command.plus_api_client.publish_skill = MagicMock() + with patch("crewai_cli.skills.main.git.Repository") as mock_repo_cls: + mock_repo_cls.return_value.fetch.side_effect = ValueError( + "Git fetch failed with exit code 128" + ) + with pytest.raises(SystemExit): + skill_command.publish(org="acme") + skill_command.plus_api_client.publish_skill.assert_not_called() + + def test_publish_blocked_when_git_state_cannot_be_validated(self, skill_command): + """Git errors other than 'not a repo' must fail closed, not skip the check.""" + with in_temp_dir(): + Path("SKILL.md").write_text( + "---\nname: my-skill\ndescription: A test skill.\nmetadata:\n version: 1.0.0\n---\nInstructions." + ) + skill_command.plus_api_client.publish_skill = MagicMock() + with patch( + "crewai_cli.skills.main.git.Repository", + side_effect=ValueError("Git fetch failed with exit code 128"), + ): + with pytest.raises(SystemExit): + skill_command.publish(org="acme") + skill_command.plus_api_client.publish_skill.assert_not_called() + + def test_publish_proceeds_outside_git_repo(self, skill_command): + with in_temp_dir(): + Path("SKILL.md").write_text( + "---\nname: my-skill\ndescription: A test skill.\nmetadata:\n version: 1.0.0\n---\nInstructions." + ) + mock_resp = MagicMock() + mock_resp.is_success = True + mock_resp.status_code = 200 + mock_resp.json.return_value = {} + skill_command.plus_api_client.publish_skill = MagicMock(return_value=mock_resp) + with ( + patch( + "crewai_cli.skills.main.git.Repository", + side_effect=ValueError("not a Git repository"), + ), + patch("crewai_cli.skills.main.Settings") as mock_settings_cls, + ): + mock_settings_cls.return_value.org_name = "acme" + mock_settings_cls.return_value.enterprise_base_url = None + skill_command.publish(org="acme") + skill_command.plus_api_client.publish_skill.assert_called_once() class TestSkillListCached: diff --git a/lib/cli/tests/skills/test_safe_extract.py b/lib/cli/tests/skills/test_safe_extract.py index f1083f5fa..2e6e2f249 100644 --- a/lib/cli/tests/skills/test_safe_extract.py +++ b/lib/cli/tests/skills/test_safe_extract.py @@ -14,7 +14,7 @@ from pathlib import Path import pytest -from crewai_cli.experimental.skills.main import _safe_extractall +from crewai_cli.skills.main import _safe_extractall def _tar_from_members(build) -> tarfile.TarFile: diff --git a/lib/crewai/src/crewai/experimental/skills/__init__.py b/lib/crewai/src/crewai/experimental/skills/__init__.py index aef3d4ed5..760813851 100644 --- a/lib/crewai/src/crewai/experimental/skills/__init__.py +++ b/lib/crewai/src/crewai/experimental/skills/__init__.py @@ -1,12 +1,17 @@ -"""Experimental Skills Repository — registry refs, global cache, downloads. +"""Deprecated location for the Skills Repository. -This package contains the registry-backed pieces of the skills feature -(`@org/name` refs, `~/.crewai/skills/` cache, download events). The stable -filesystem-based skill loader still lives in `crewai.skills`. +The registry-backed skills feature graduated out of experimental; it now +lives in `crewai.skills` alongside the filesystem-based loader. This module +re-exports the public names and aliases the old submodules so imports +written against the experimental namespace keep working, and will be +removed in a future release. """ -from crewai.experimental.skills.cache import SkillCacheManager -from crewai.experimental.skills.registry import ( +import sys + +from crewai.skills import cache, events, registry +from crewai.skills.cache import SkillCacheManager +from crewai.skills.registry import ( SkillNotCachedError, is_registry_ref, parse_registry_ref, @@ -14,10 +19,19 @@ from crewai.experimental.skills.registry import ( ) +# Keep `crewai.experimental.skills.` imports (and patch targets) +# resolving to the real modules in crewai.skills. +sys.modules[__name__ + ".cache"] = cache +sys.modules[__name__ + ".events"] = events +sys.modules[__name__ + ".registry"] = registry + __all__ = [ "SkillCacheManager", "SkillNotCachedError", + "cache", + "events", "is_registry_ref", "parse_registry_ref", + "registry", "resolve_registry_ref", ] diff --git a/lib/crewai/src/crewai/experimental/skills/_flag.py b/lib/crewai/src/crewai/experimental/skills/_flag.py deleted file mode 100644 index beca1d270..000000000 --- a/lib/crewai/src/crewai/experimental/skills/_flag.py +++ /dev/null @@ -1,24 +0,0 @@ -"""Experimental feature gate for the Skills Repository.""" - -from __future__ import annotations - -import os - - -ENV_VAR = "CREWAI_EXPERIMENTAL" - - -class ExperimentalFeatureDisabledError(RuntimeError): - """Raised when an experimental feature is used without the flag set.""" - - -def is_enabled() -> bool: - return os.environ.get(ENV_VAR) == "1" - - -def require_experimental_skills() -> None: - if not is_enabled(): - raise ExperimentalFeatureDisabledError( - "The Skills Repository (registry refs, cache, downloads) is " - f"experimental. Set {ENV_VAR}=1 to enable it." - ) diff --git a/lib/crewai/src/crewai/skills/__init__.py b/lib/crewai/src/crewai/skills/__init__.py index 48a4a407e..daaac82ed 100644 --- a/lib/crewai/src/crewai/skills/__init__.py +++ b/lib/crewai/src/crewai/skills/__init__.py @@ -1,8 +1,11 @@ """Agent Skills standard implementation for crewAI. -Provides filesystem-based skill packaging with progressive disclosure. +Provides filesystem-based skill packaging with progressive disclosure, plus +the registry-backed Skills Repository (`@org/name` refs, global cache, +downloads). """ +from crewai.skills.cache import SkillCacheManager from crewai.skills.loader import ( activate_skill, discover_skills, @@ -11,14 +14,25 @@ from crewai.skills.loader import ( ) from crewai.skills.models import Skill, SkillFrontmatter from crewai.skills.parser import SkillParseError +from crewai.skills.registry import ( + SkillNotCachedError, + is_registry_ref, + parse_registry_ref, + resolve_registry_ref, +) __all__ = [ "Skill", + "SkillCacheManager", "SkillFrontmatter", + "SkillNotCachedError", "SkillParseError", "activate_skill", "discover_skills", + "is_registry_ref", "load_skill", "load_skills", + "parse_registry_ref", + "resolve_registry_ref", ] diff --git a/lib/crewai/src/crewai/experimental/skills/cache.py b/lib/crewai/src/crewai/skills/cache.py similarity index 95% rename from lib/crewai/src/crewai/experimental/skills/cache.py rename to lib/crewai/src/crewai/skills/cache.py index 065c2c521..64d1b7bff 100644 --- a/lib/crewai/src/crewai/experimental/skills/cache.py +++ b/lib/crewai/src/crewai/skills/cache.py @@ -88,7 +88,9 @@ class SkillCacheManager: "version": version, "installed_at": datetime.now(tz=timezone.utc).isoformat(), } - (skill_dir / _META_FILENAME).write_text(json.dumps(meta, indent=2)) + (skill_dir / _META_FILENAME).write_text( + json.dumps(meta, indent=2), encoding="utf-8" + ) return skill_dir def list_cached(self) -> list[SkillMetadata]: @@ -103,7 +105,9 @@ class SkillCacheManager: meta_file = skill_dir / _META_FILENAME if meta_file.exists(): try: - results.append(json.loads(meta_file.read_text())) + results.append( + json.loads(meta_file.read_text(encoding="utf-8")) + ) except (json.JSONDecodeError, KeyError): _logger.debug( "Skipping malformed cache entry: %s", diff --git a/lib/crewai/src/crewai/experimental/skills/events.py b/lib/crewai/src/crewai/skills/events.py similarity index 100% rename from lib/crewai/src/crewai/experimental/skills/events.py rename to lib/crewai/src/crewai/skills/events.py diff --git a/lib/crewai/src/crewai/skills/loader.py b/lib/crewai/src/crewai/skills/loader.py index ff88ae9f6..5e9ed7e81 100644 --- a/lib/crewai/src/crewai/skills/loader.py +++ b/lib/crewai/src/crewai/skills/loader.py @@ -164,7 +164,7 @@ def load_skill( for s in discover_skills(skill, source=source) ] if isinstance(skill, str) and skill.startswith("@"): - from crewai.experimental.skills.registry import resolve_registry_ref + from crewai.skills.registry import resolve_registry_ref return [resolve_registry_ref(skill, source=source)] if isinstance(skill, str) and skill.lstrip().startswith("---\n"): @@ -202,7 +202,7 @@ def load_skills( for skill in load_skill(skill_input, source=source): dedup_key = skill.name if isinstance(skill_input, str) and skill_input.startswith("@"): - from crewai.experimental.skills.registry import parse_registry_ref + from crewai.skills.registry import parse_registry_ref org, _ = parse_registry_ref(skill_input) dedup_key = f"{org}/{skill.name}" diff --git a/lib/crewai/src/crewai/experimental/skills/registry.py b/lib/crewai/src/crewai/skills/registry.py similarity index 96% rename from lib/crewai/src/crewai/experimental/skills/registry.py rename to lib/crewai/src/crewai/skills/registry.py index fa502c4f0..d82613e89 100644 --- a/lib/crewai/src/crewai/experimental/skills/registry.py +++ b/lib/crewai/src/crewai/skills/registry.py @@ -11,7 +11,7 @@ from pathlib import Path import sys from typing import Any -from crewai.experimental.skills.cache import SkillCacheManager +from crewai.skills.cache import SkillCacheManager _logger = logging.getLogger(__name__) @@ -100,11 +100,9 @@ def resolve_registry_ref( Raises: SkillNotCachedError: When not cached and running in non-interactive mode. """ - from crewai.experimental.skills._flag import require_experimental_skills from crewai.skills.loader import activate_skill from crewai.skills.parser import load_skill_metadata - require_experimental_skills() org, name = parse_registry_ref(ref) local_path = Path.cwd() / "skills" / name @@ -154,7 +152,7 @@ def download_skill( try: from crewai.events.event_bus import crewai_event_bus - from crewai.experimental.skills.events import ( + from crewai.skills.events import ( SkillDownloadCompletedEvent, SkillDownloadStartedEvent, ) diff --git a/lib/crewai/tests/experimental/skills/__init__.py b/lib/crewai/tests/experimental/skills/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/lib/crewai/tests/experimental/skills/conftest.py b/lib/crewai/tests/experimental/skills/conftest.py deleted file mode 100644 index 40baddaad..000000000 --- a/lib/crewai/tests/experimental/skills/conftest.py +++ /dev/null @@ -1,6 +0,0 @@ -import pytest - - -@pytest.fixture(autouse=True) -def _enable_experimental_skills(monkeypatch: pytest.MonkeyPatch) -> None: - monkeypatch.setenv("CREWAI_EXPERIMENTAL", "1") \ No newline at end of file diff --git a/lib/crewai/tests/experimental/skills/test_flag.py b/lib/crewai/tests/experimental/skills/test_flag.py deleted file mode 100644 index 9295de678..000000000 --- a/lib/crewai/tests/experimental/skills/test_flag.py +++ /dev/null @@ -1,30 +0,0 @@ -"""Tests for the CREWAI_EXPERIMENTAL gate on Skills Repository.""" - -from __future__ import annotations - -import pytest - -from crewai.experimental.skills._flag import ( - ExperimentalFeatureDisabledError, - require_experimental_skills, -) -from crewai.experimental.skills.registry import resolve_registry_ref - - -def test_require_raises_without_flag(monkeypatch: pytest.MonkeyPatch) -> None: - monkeypatch.delenv("CREWAI_EXPERIMENTAL", raising=False) - with pytest.raises(ExperimentalFeatureDisabledError): - require_experimental_skills() - - -def test_resolve_registry_ref_raises_without_flag( - monkeypatch: pytest.MonkeyPatch, -) -> None: - monkeypatch.delenv("CREWAI_EXPERIMENTAL", raising=False) - with pytest.raises(ExperimentalFeatureDisabledError): - resolve_registry_ref("@acme/my-skill") - - -def test_require_passes_with_flag(monkeypatch: pytest.MonkeyPatch) -> None: - monkeypatch.setenv("CREWAI_EXPERIMENTAL", "1") - require_experimental_skills() \ No newline at end of file diff --git a/lib/crewai/tests/experimental/skills/test_cache.py b/lib/crewai/tests/skills/test_cache.py similarity index 98% rename from lib/crewai/tests/experimental/skills/test_cache.py rename to lib/crewai/tests/skills/test_cache.py index b6601dccf..25ffce39b 100644 --- a/lib/crewai/tests/experimental/skills/test_cache.py +++ b/lib/crewai/tests/skills/test_cache.py @@ -10,7 +10,7 @@ from pathlib import Path import pytest -from crewai.experimental.skills.cache import SkillCacheManager, _safe_extractall +from crewai.skills.cache import SkillCacheManager, _safe_extractall def _make_tar_gz(files: dict[str, str]) -> bytes: diff --git a/lib/crewai/tests/skills/test_experimental_shim.py b/lib/crewai/tests/skills/test_experimental_shim.py new file mode 100644 index 000000000..db92dfa2a --- /dev/null +++ b/lib/crewai/tests/skills/test_experimental_shim.py @@ -0,0 +1,43 @@ +"""The old experimental namespace must keep re-exporting the public names.""" + + +def test_experimental_namespace_reexports_public_names(): + from crewai.experimental import skills as experimental_skills + from crewai.skills.cache import SkillCacheManager + from crewai.skills.registry import ( + SkillNotCachedError, + is_registry_ref, + parse_registry_ref, + resolve_registry_ref, + ) + + assert experimental_skills.SkillCacheManager is SkillCacheManager + assert experimental_skills.SkillNotCachedError is SkillNotCachedError + assert experimental_skills.is_registry_ref is is_registry_ref + assert experimental_skills.parse_registry_ref is parse_registry_ref + assert experimental_skills.resolve_registry_ref is resolve_registry_ref + + +def test_experimental_submodule_imports_alias_real_modules(): + """Old submodule import style must resolve to the crewai.skills modules.""" + import importlib + + from crewai.experimental.skills import cache as shim_cache + from crewai.experimental.skills import events as shim_events + from crewai.experimental.skills import registry as shim_registry + from crewai.experimental.skills.cache import SkillCacheManager + from crewai.experimental.skills.registry import resolve_registry_ref + from crewai.skills import cache as real_cache + from crewai.skills import events as real_events + from crewai.skills import registry as real_registry + + assert shim_cache is real_cache + assert shim_events is real_events + assert shim_registry is real_registry + assert SkillCacheManager is real_cache.SkillCacheManager + assert resolve_registry_ref is real_registry.resolve_registry_ref + # Dotted-path import resolves through sys.modules to the same module. + assert ( + importlib.import_module("crewai.experimental.skills.registry") + is real_registry + ) diff --git a/lib/crewai/tests/skills/test_loader.py b/lib/crewai/tests/skills/test_loader.py index efa30c3f1..6abca19a4 100644 --- a/lib/crewai/tests/skills/test_loader.py +++ b/lib/crewai/tests/skills/test_loader.py @@ -197,7 +197,7 @@ class TestLoadSkill: }[ref] monkeypatch.setattr( - "crewai.experimental.skills.registry.resolve_registry_ref", + "crewai.skills.registry.resolve_registry_ref", resolve_registry_ref, ) diff --git a/lib/crewai/tests/experimental/skills/test_models_version.py b/lib/crewai/tests/skills/test_models_version.py similarity index 100% rename from lib/crewai/tests/experimental/skills/test_models_version.py rename to lib/crewai/tests/skills/test_models_version.py diff --git a/lib/crewai/tests/experimental/skills/test_registry.py b/lib/crewai/tests/skills/test_registry.py similarity index 81% rename from lib/crewai/tests/experimental/skills/test_registry.py rename to lib/crewai/tests/skills/test_registry.py index 84067dc73..5f588ccf7 100644 --- a/lib/crewai/tests/experimental/skills/test_registry.py +++ b/lib/crewai/tests/skills/test_registry.py @@ -5,7 +5,7 @@ from __future__ import annotations from pathlib import Path from unittest.mock import MagicMock, patch -from crewai.experimental.skills.registry import ( +from crewai.skills.registry import ( SkillNotCachedError, is_registry_ref, parse_registry_ref, @@ -75,11 +75,11 @@ class TestResolveRegistryRef: mock_cache.get_cached_path.return_value = None with ( - patch("crewai.experimental.skills.registry._is_noninteractive", return_value=False), + patch("crewai.skills.registry._is_noninteractive", return_value=False), patch.object(Path, "cwd", return_value=tmp_path), - patch("crewai.experimental.skills.registry.SkillCacheManager", return_value=mock_cache), + patch("crewai.skills.registry.SkillCacheManager", return_value=mock_cache), ): - from crewai.experimental.skills.registry import resolve_registry_ref + from crewai.skills.registry import resolve_registry_ref skill = resolve_registry_ref("@acme/my-skill") assert skill.name == "my-skill" @@ -90,11 +90,11 @@ class TestResolveRegistryRef: mock_cache.get_cached_path.return_value = None with ( - patch("crewai.experimental.skills.registry._is_noninteractive", return_value=True), + patch("crewai.skills.registry._is_noninteractive", return_value=True), patch.object(Path, "cwd", return_value=tmp_path), - patch("crewai.experimental.skills.registry.SkillCacheManager", return_value=mock_cache), + patch("crewai.skills.registry.SkillCacheManager", return_value=mock_cache), ): - from crewai.experimental.skills.registry import resolve_registry_ref + from crewai.skills.registry import resolve_registry_ref with pytest.raises(SkillNotCachedError) as exc_info: resolve_registry_ref("@acme/ghost-skill") assert "@acme/ghost-skill" in str(exc_info.value) @@ -112,11 +112,11 @@ class TestResolveRegistryRef: # tmp_path has no ./skills/ directory with ( - patch("crewai.experimental.skills.registry._is_noninteractive", return_value=False), + patch("crewai.skills.registry._is_noninteractive", return_value=False), patch.object(Path, "cwd", return_value=tmp_path), - patch("crewai.experimental.skills.registry.SkillCacheManager", return_value=mock_cache), + patch("crewai.skills.registry.SkillCacheManager", return_value=mock_cache), ): - from crewai.experimental.skills.registry import resolve_registry_ref + from crewai.skills.registry import resolve_registry_ref skill = resolve_registry_ref("@acme/cached-skill") assert skill.name == "cached-skill"