Skip to content

Add plugin-bundled skills discovery and install CLI#7289

Open
AdonaiVera wants to merge 4 commits intodevelopfrom
feat/add-cli-plugin-skills
Open

Add plugin-bundled skills discovery and install CLI#7289
AdonaiVera wants to merge 4 commits intodevelopfrom
feat/add-cli-plugin-skills

Conversation

@AdonaiVera
Copy link
Copy Markdown
Member

@AdonaiVera AdonaiVera commented Apr 2, 2026

📋 What changes are proposed in this pull request?

This PR adds support for skills inside plugins in FiftyOne, aligned with the data agent work.

The idea is pretty simple. Skills are just Markdown files for AI agents, and now you can define them directly in fiftyone.yml, the same way we already do with operators and panels. So fiftyone.yml becomes the source of truth, and you just list your skills under skills: using relative paths. Plugins can even be just skills, no operators needed.

name: "@voxel51/fiftyone-internal-skills"
description: Internal FiftyOne skills for data-centric ML workflows
version: 1.0.0
url: https://github.com/voxel51/fiftyone-internal-skills

skills:
  - skills/fiftyone-annotation-qa/SKILL.md

🧪 How is this patch tested? If it is not, please explain why.

  • pre-commit run --files passes
  • Unit tests pass: pytest tests/unittests/plugins/
  • Manual tests:
    • fiftyone plugins skills list shows skills
(env) ado@Adonais-MacBook-Pro fiftyone % fiftyone plugins skills list
name                        category    plugin                             path
--------------------------  ----------  ---------------------------------  ---------------------------------------------------------------------------------------------------
fiftyone-annotation-qa      QA          @voxel51/fiftyone-internal-skills  /Users/ado/fiftyone/__plugins__/fiftyone-internal-skills/skills/fiftyone-annotation-qa/SKILL.md
fiftyone-auto-labeling      Annotate    @voxel51/fiftyone-internal-skills  /Users/ado/fiftyone/__plugins__/fiftyone-internal-skills/skills/fiftyone-auto-labeling/SKILL.md
fiftyone-data-distribution  Curate      @voxel51/fiftyone-internal-skills  /Users/ado/fiftyone/__plugins__/fiftyone-internal-skills/skills/fiftyone-data-distribution/SKILL.md
fiftyone-data-enrichment    Curate      @voxel51/fiftyone-internal-skills  /Users/ado/fiftyone/__plugins__/fiftyone-internal-skills/skills/fiftyone-data-enrichment/SKILL.md
fiftyone-data-export        Export      @voxel51/fiftyone-internal-skills  /Users/ado/fiftyone/__plugins__/fiftyone-internal-skills/skills/fiftyone-data-export/SKILL.md
fiftyone-data-import        Import      @voxel51/fiftyone-internal-skills  /Users/ado/fiftyone/__plugins__/fiftyone-internal-skills/skills/fiftyone-data-import/SKILL.md
fiftyone-data-quality       Curate      @voxel51/fiftyone-internal-skills  /Users/ado/fiftyone/__plugins__/fiftyone-internal-skills/skills/fiftyone-data-quality/SKILL.md
fiftyone-dataset-split      Annotate    @voxel51/fiftyone-internal-skills  /Users/ado/fiftyone/__plugins__/fiftyone-internal-skills/skills/fiftyone-dataset-split/SKILL.md
fiftyone-me-panel-config    Evaluate    @voxel51/fiftyone-internal-skills  /Users/ado/fiftyone/__plugins__/fiftyone-internal-skills/skills/fiftyone-me-panel-config/SKILL.md
fiftyone-model-evaluation   Evaluate    @voxel51/fiftyone-internal-skills  /Users/ado/fiftyone/__plugins__/fiftyone-internal-skills/skills/fiftyone-model-evaluation/SKILL.md
fiftyone-nl-dataset-query   Curate      @voxel51/fiftyone-internal-skills  /Users/ado/fiftyone/__plugins__/fiftyone-internal-skills/skills/fiftyone-nl-dataset-query/SKILL.md

Some example for testing

fiftyone plugins skills list            
fiftyone plugins skills list --plugin "@voxel51/fiftyone-internal-skills"
fiftyone plugins skills list --category qa                                                                      
fiftyone plugins skills list --names-only

📝 Release Notes

Is this a user-facing change that should be mentioned in the release notes?

  • Yes. Give a description of this change to be included in the release
    notes for FiftyOne users.

Adds support for plugin-based AI agent skills in FiftyOne, including CLI commands to list skills.


What areas of FiftyOne does this PR affect?

  • App: FiftyOne application changes
  • Build: Build and test infrastructure changes
  • Core: Core fiftyone Python library changes
  • Documentation: FiftyOne documentation changes
  • Other

Summary by CodeRabbit

  • New Features

    • Added a "plugins skills" command group with a "list" subcommand to discover plugin-provided skills (filter by plugin, category, or names-only).
    • Plugin metadata and public plugin API now expose discovered skills for serialization and querying.
  • Tests

    • Added tests covering skills discovery, filtering, frontmatter parsing, missing skill files, and behavior with disabled plugins.

@AdonaiVera AdonaiVera requested review from a team as code owners April 2, 2026 03:35
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 2, 2026

Walkthrough

Adds a new "skills" subsystem for plugins: a SkillDefinition model and a public list_skills(enabled=True, plugin=None, category=None) function that discovers SKILL.md files declared in plugin metadata, parses optional YAML frontmatter for name/category/description, and yields SkillDefinition instances. Exposes list_skills from fiftyone.plugins and adds a plugins skills CLI group with a list subcommand supporting --plugin, --category, and --names-only. PluginDefinition gains a skills property and to_dict() now includes skills. Tests for discovery and frontmatter parsing were added.

Sequence Diagram(s)

sequenceDiagram
    actor User
    participant CLI as "fiftyone plugins skills list"
    participant ListSkills as list_skills()
    participant Plugins as Plugin Registry
    participant FS as File System

    User->>CLI: run list (--plugin/--category/--names-only)
    CLI->>ListSkills: list_skills(plugin, category)
    ListSkills->>Plugins: list_plugins(enabled=True, builtin="all")
    Plugins-->>ListSkills: [PluginDefinition...]
    loop per plugin
        ListSkills->>FS: resolve declared SKILL.md paths
        FS-->>ListSkills: file content or missing
        ListSkills->>ListSkills: parse YAML frontmatter -> metadata
        ListSkills->>ListSkills: construct SkillDefinition, apply filters
    end
    ListSkills-->>CLI: [SkillDefinition...]
    CLI-->>User: print names or table
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 25.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Add plugin-bundled skills discovery and install CLI' clearly and concisely describes the main change: adding plugin skills discovery and a CLI interface.
Description check ✅ Passed The description comprehensively covers what changes are proposed, includes testing details (pre-commit, unit tests, manual tests with examples), and properly completes the release notes section with user-facing change confirmation.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/add-cli-plugin-skills

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@fiftyone/core/cli.py`:
- Around line 4595-4600: The CLI argument definition for "--plugin-names" (in
fiftyone/core/cli.py) uses nargs="*" which allows an empty list (e.g.,
`--plugin-names` -> []), causing silent no-op installs; change to nargs="+" to
require at least one value, or detect an empty list after parsing and normalize
it to None before downstream filtering (update the parser entry for
"--plugin-names" or the code that reads plugin_names to treat [] as None).

In `@fiftyone/plugins/skills.py`:
- Around line 51-56: The name() method currently falls back to the file basename
(which yields "SKILL" for skills/<slug>/SKILL.md) causing ambiguous names;
update the fallback in name(self) to use the parent directory name instead
(e.g., os.path.basename(os.path.dirname(self._path))) so unnamed skills use
their skill directory slug; modify the return in name() to first try
self._metadata["name"] and otherwise return the parent directory basename.
- Around line 207-215: The code currently trusts plugin_definition.skills
entries verbatim, allowing paths outside the plugin directory; update
_iter_plugin_skills to canonicalize and validate each skill path before reading:
compute skill_path = os.path.abspath(os.path.join(plugin_definition.directory,
rel_path)) and ensure it lies inside the plugin directory (e.g., compare
os.path.commonpath([plugin_definition.directory, skill_path]) ==
os.path.abspath(plugin_definition.directory)); if the check fails or the path is
absolute/attempts traversal, log/debug a rejection and continue without calling
_parse_skill_frontmatter or yielding a SkillDefinition to prevent reading
arbitrary local files.
- Around line 178-199: skill.name is used directly to build paths
(skill_central, link_path) allowing absolute names or '..' segments to escape
managed directories; validate and sanitize it before joining: ensure skill.name
is not absolute (use os.path.isabs), does not contain path separators or parent
segments (no '..'), and is equal to a safe basename (e.g., compare to
os.path.basename(skill.name)); if validation fails, skip or raise a clear error
and log it. Apply this check before computing skill_central, dest, and link_path
in the loop that references skill.name so only safe directory names are
created/copied and symlinked.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 97f9143b-5cff-4b4c-9410-5f2fe8a9db69

📥 Commits

Reviewing files that changed from the base of the PR and between 01d6325 and 624ad67.

📒 Files selected for processing (5)
  • fiftyone/core/cli.py
  • fiftyone/plugins/__init__.py
  • fiftyone/plugins/definitions.py
  • fiftyone/plugins/skills.py
  • tests/unittests/plugins/skills_tests.py

Copy link
Copy Markdown
Member

@kaixi-wang kaixi-wang left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure if this intended for oss local only, but this may not work in teams as is due to the writing of files and reliance on local files

@AdonaiVera
Copy link
Copy Markdown
Member Author

Hi @kaixi-wang

Just to clarify, the skill installation part is CLI-only for open source. In enterprise, we’re not installing skills directly to a specific agent since we already have our own infrastructure handling that.
What should work in enterprise is the skills storage. That’s already covered by the plugin framework, so it follows the same storage approach as any other FO plugin.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@fiftyone/plugins/skills.py`:
- Around line 186-191: The current copy/symlink logic silently overwrites
existing skills when two plugins share the same skill.name; change it to detect
conflicts and avoid overwriting: before os.makedirs/ shutil.copy2 (references:
variables skill_central, dest, and skill.path) check whether skill_central or
dest already exists and if the existing content comes from a different plugin
(use the plugin identifier/source available on the skill object), then do not
overwrite—log an error/warning including both plugin identities and skip or
create a namespaced directory instead (e.g., append plugin id or timestamp) so
installations do not clobber each other; apply the same conflict check and
behavior to the symlink creation code that uses skill.name as the key (the block
around the symlink handling referenced at Lines ~199-209).
- Around line 200-204: The cleanup before creating symlinks in the function
handling link_path (in fiftyone/plugins/skills.py) only removes symlinks and
directories, causing os.symlink() to raise FileExistsError when a regular file
exists; update that block to also detect and remove regular files (use
os.path.isfile or os.path.exists with not islink/isdir) and call
os.remove(link_path) before creating the symlink so all pre-existing file types
are handled safely.
- Around line 131-133: Replace the bare except with "except Exception:" and log
the full traceback using logger.exception so failures are diagnosable;
specifically, in the block that currently reads "except:" referencing the plugin
descriptor variable pd (pd.name), change it to "except Exception:" and call
logger.exception with a message that includes pd.name so the stack trace is
recorded while the function can continue skipping that plugin.
- Around line 250-252: The YAML frontmatter parser _parse_skill_frontmatter
should guard against non-dict results from yaml.safe_load to avoid runtime
crashes when SkillDefinition later calls .get(); change the logic in
_parse_skill_frontmatter to assign the result of yaml.safe_load(parts[1]) to a
variable, check isinstance(result, dict) and return result if true, otherwise
return an empty dict (still catching yaml.YAMLError as before), so malformed
frontmatter like a number or list won't propagate a non-dict into
SkillDefinition.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 75f48638-3f69-473d-93ff-ef4d616e5c1f

📥 Commits

Reviewing files that changed from the base of the PR and between 624ad67 and 255f875.

📒 Files selected for processing (2)
  • fiftyone/core/cli.py
  • fiftyone/plugins/skills.py

@AdonaiVera
Copy link
Copy Markdown
Member Author

Skills should not be moved out of their original location. Similar to how we handle fiftyone.yml files.

@ritch I'm not moving the skills. It is just copying the skill to a specific folder so agents like Claude or Cursor can use it. The skill still stays defined in the fiftyone.yml file. Then, when the user runs it locally with DevAgent, it gets added to that user’s environment and becomes available there.

Copy link
Copy Markdown
Member

@Burhan-Q Burhan-Q left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I stopped making pathlib.Path suggestions after checking that the ~ handling works on Windows. Still, I would love to see its adoption, but not at all blocking. Couple other minor nits, but overall looks like a solid feature add

for s in skills:
print(s.name)

return
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the Voxel51 consensus on "naked" return statements? Personally, I'm not a fan, but I'm not saying this has to change, just looking to ensure that I understand the established convention for the repo.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just following the FO style, not my preference.


return

headers = ["name", "category", "plugin", "path"]
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wonder if a "last updated" or "last downloaded" column might make sense? Not clear how feasible that would be, but maybe something to consider

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh I think this could be super cool, but it should be part of the plugin framework itself. It’s out of scope for this PR.

"""The name of the skill."""
return self._metadata.get(
"name",
os.path.basename(os.path.dirname(self._path)),
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
os.path.basename(os.path.dirname(self._path)),
self._path.parent.name,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you opt to take my suggestion for using pathlib.Path

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the suggestion! I kept SkillDefinition consistent with PluginDefinition, which uses os.path strings. Using pathlib only here would make them inconsistent.

"""

def __init__(self, path, metadata, plugin_name):
self._path = path
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
self._path = path
self._path = Path(path).absolute()

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as above.

@AdonaiVera
Copy link
Copy Markdown
Member Author

I removed the CLI agent installation part. For reference that was just a helper to copy our skills from the plugin to agents like Claude, Codex, Cursor, etc. I moved that out to keep this PR clean and focused only on the skill listing functionality @ritch

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@fiftyone/core/cli.py`:
- Around line 4491-4508: The CLI currently only accepts single values for
--plugin and --category; update the parser.add_argument calls for the
"-p/--plugin" and "-c/--category" options to accept multiple values (e.g., add
nargs="+" or action="append") so they produce lists, and make sure the code that
calls list_skills(plugin=..., category=...) passes those lists (if using
action="append" flatten the resulting list) so list_skills() receives the
expected list types while leaving the "-n/--names-only" flag unchanged.

In `@fiftyone/plugins/skills.py`:
- Around line 63-64: The file reads use the system default encoding; update the
file-open calls to specify encoding='utf-8' to ensure cross-platform
consistency: change the open in the method that reads the skill markdown (the
with open(self._path, "r") as f block) to include encoding='utf-8', and make the
same change in the _parse_skill_frontmatter function where it opens/reads the
skill file so both places explicitly use UTF-8.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: ba311043-0113-4653-88a9-dd2daab7e879

📥 Commits

Reviewing files that changed from the base of the PR and between 255f875 and 66aa3f9.

📒 Files selected for processing (4)
  • fiftyone/core/cli.py
  • fiftyone/plugins/__init__.py
  • fiftyone/plugins/skills.py
  • tests/unittests/plugins/skills_tests.py

@AdonaiVera AdonaiVera requested a review from ritch April 2, 2026 17:10
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@fiftyone/plugins/skills.py`:
- Around line 148-157: The current frontmatter detection uses
content.startswith("---") and content.split("---", 2), which can match `---`
inside YAML values; update parsing to only treat `---` as delimiters when they
appear on their own line. Modify the logic around the variable content (where
parts = content.split...) to locate the first and second delimiter by searching
for lines that match exactly `---` (e.g., using a line-scan or a multiline regex
like `(?m)^---\s*$`), extract the text between those two delimiter lines, then
call yaml.safe_load on that extracted block and return it if it is a dict.
Ensure yaml.safe_load(parts[1]) is replaced with loading the extracted
frontmatter block and retain the existing fallback to return {} for malformed or
missing frontmatter.
- Around line 107-115: The plugin-skill discovery currently catches exceptions
around the whole per-plugin loop, so a single bad skill aborts loading all
remaining skills from that plugin; change the error handling to catch exceptions
per-skill inside _iter_plugin_skills (or inside the inner loop over
plugin_definition.skills) so that on exception you log which rel_path and
plugin_definition.name failed and continue to the next skill (i.e., move the
try/except into the inner loop and keep yielding valid SkillDefinition instances
from _iter_plugin_skills).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 52c9a1df-e84d-44eb-a97a-5050c3d5b862

📥 Commits

Reviewing files that changed from the base of the PR and between 66aa3f9 and 826ff77.

📒 Files selected for processing (2)
  • fiftyone/core/cli.py
  • fiftyone/plugins/skills.py

Comment on lines +107 to +115
for pd in plugin_defs:
try:
for skill in _iter_plugin_skills(pd):
if category is not None and skill.category not in category:
continue
skills.append(skill)
except:
logger.info(f"Failed to load skills from plugin '{pd.name}'")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Handle failures per skill, not per plugin.

The try here aborts discovery of every remaining skill in a plugin after the first bad entry. A single unreadable file or malformed rel_path should be skipped without hiding valid sibling skills.

Suggested fix
-    for pd in plugin_defs:
-        try:
-            for skill in _iter_plugin_skills(pd):
-                if category is not None and skill.category not in category:
-                    continue
-                skills.append(skill)
-        except:
-            logger.info(f"Failed to load skills from plugin '{pd.name}'")
+    for pd in plugin_defs:
+        for skill in _iter_plugin_skills(pd):
+            if category is not None and skill.category not in category:
+                continue
+            skills.append(skill)
def _iter_plugin_skills(plugin_definition):
    plugin_dir = os.path.realpath(plugin_definition.directory)
    for rel_path in plugin_definition.skills:
        try:
            skill_path = os.path.realpath(os.path.join(plugin_dir, rel_path))
            # existing validation...
            metadata = _parse_skill_frontmatter(skill_path)
            yield SkillDefinition(skill_path, metadata, plugin_definition.name)
        except:
            logger.info(
                f"Failed to load skill '{rel_path}' from plugin "
                f"'{plugin_definition.name}'"
            )
            continue
Based on learnings, the bare `except:` + `logger.info(...)` pattern is intentional in plugin loading/discovery here; the issue is that the current catch scope drops the rest of the plugin's skills after the first failure.
🧰 Tools
🪛 Ruff (0.15.7)

[error] 113-113: Do not use bare except

(E722)


[warning] 114-114: Logging statement uses f-string

(G004)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@fiftyone/plugins/skills.py` around lines 107 - 115, The plugin-skill
discovery currently catches exceptions around the whole per-plugin loop, so a
single bad skill aborts loading all remaining skills from that plugin; change
the error handling to catch exceptions per-skill inside _iter_plugin_skills (or
inside the inner loop over plugin_definition.skills) so that on exception you
log which rel_path and plugin_definition.name failed and continue to the next
skill (i.e., move the try/except into the inner loop and keep yielding valid
SkillDefinition instances from _iter_plugin_skills).

Comment on lines +148 to +157
if not content.startswith("---"):
return {}

parts = content.split("---", 2)
if len(parts) < 3:
return {}

try:
result = yaml.safe_load(parts[1])
return result if isinstance(result, dict) else {}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Parse frontmatter delimiters as standalone lines.

startswith("---") plus split("---", 2) stops at the next --- anywhere in the file. Valid YAML like description: "foo --- bar" or block scalars containing --- will be truncated, which silently drops metadata and can break --category filtering.

Suggested fix
-    if not content.startswith("---"):
+    lines = content.splitlines(keepends=True)
+    if not lines or lines[0].strip() != "---":
         return {}
 
-    parts = content.split("---", 2)
-    if len(parts) < 3:
+    frontmatter = None
+    for idx, line in enumerate(lines[1:], 1):
+        if line.strip() == "---":
+            frontmatter = "".join(lines[1:idx])
+            break
+
+    if frontmatter is None:
         return {}
 
     try:
-        result = yaml.safe_load(parts[1])
+        result = yaml.safe_load(frontmatter)
         return result if isinstance(result, dict) else {}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@fiftyone/plugins/skills.py` around lines 148 - 157, The current frontmatter
detection uses content.startswith("---") and content.split("---", 2), which can
match `---` inside YAML values; update parsing to only treat `---` as delimiters
when they appear on their own line. Modify the logic around the variable content
(where parts = content.split...) to locate the first and second delimiter by
searching for lines that match exactly `---` (e.g., using a line-scan or a
multiline regex like `(?m)^---\s*$`), extract the text between those two
delimiter lines, then call yaml.safe_load on that extracted block and return it
if it is a dict. Ensure yaml.safe_load(parts[1]) is replaced with loading the
extracted frontmatter block and retain the existing fallback to return {} for
malformed or missing frontmatter.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants